Skip to content

Commit 846336e

Browse files
test: harden agent chat setup, interruptions and restart evals (#13762)
## Thinking Path > - Paperclip lets people manage agents through ongoing conversations. > - Chat users can change instructions while a provider is already working. > - Existing chat evals wait for each turn to settle before the next message. > - They cannot prove delivery during active work or the saved effect of a correction. > - Existing fixtures also enable Agent Chat through the API rather than the settings UI. > - This PR adds bounded browser workflows and checks their persisted outcomes. ## Linked Issues or Issue Description Refs #13741, #13752, #13750. **What happened?** The chat suites cover planning, delegation, status, and recovery. They lack active-turn follow-ups and the experimental settings lifecycle. A sequential conversation can pass even if messages sent during work are lost. **Expected behavior** A follow-up submitted during a provider turn survives and affects the final reply. A changed launch day appears in the saved plan. Disabling Agent Chat rejects new messages while preserving history; re-enabling resumes the same conversation. **Steps to reproduce** Run the explicit `agent-chat-stories` suite. It selects three local cases for each native Claude and Codex profile. An ordinary provider command waits for a fixture brief file so the browser can send the follow-up at an observed active-run boundary. ## What Changed - Add six opt-in Product E2E cells for settings, active follow-ups, and plan corrections. - Drive experimental settings through the UI and verify disabled sends are rejected by the public API. - Use a bounded file wait in the actual isolated agent workspace, with provider-written readiness and an undisclosed brief reference. - Grade persisted user messages, final replies, native run outcomes, and exact saved plan fields. - Accept active-turn steering or one queued successor; reject lost input, duplicate input, and stale outputs. - Allow one steered run or two sequential runs throughout the shared harness, while preserving exact counts for other cases. - Require a single marker-bearing response attributed to the final provider run. - Unload the development browser client before restarting the server, avoiding reconnect/navigation races without weakening the post-restart memory check. - Add browser regressions for restart isolation and asynchronously saved settings switches. - Document prepared-agent setup, native onboarding limits, and the separate API-tool rollout gate. ## Verification - Eval TypeScript check passed. - Eval support suite: 436 tests passed in 39 files. - New oracle calibration: six tests passed, including plausible invalid outcomes. - Browser support regressions: seven tests passed; the restart regression was observed failing before the fix. - Catalog discovery selects exactly six local native cases and leaves default paid selection unchanged. - [Consolidated existing native chat report](https://d1p6rlowie26tp.cloudfront.net/runner-e2e/campaigns/gha-35643286055-1/): master `b82661b56`, 33/34 passed, all cleanup passed. The failure was a browser navigation timeout across restart; the page request returned 200 and the chat rendered. - [Nine targeted restart/replay cells](https://d1p6rlowie26tp.cloudfront.net/runner-e2e/campaigns/gha-35645850088-1/) passed on `1fe2fe275`, including the original failure, across native Claude/Codex and local/Daytona; all cleanup passed. - [Initial six-story campaign](https://github.com/paperclipai/paperclip/actions/runs/35644832817) retained all six failures: asynchronous switch assertions, unavailable fixture paths, and rich-text escaping in raw command comparisons. The corrected fixtures preserve the same behavioral assertions. - [Six-story campaign v2](https://d1p6rlowie26tp.cloudfront.net/runner-e2e/campaigns/gha-35646270035-1/) on `8232773a0`: 4/6 passed (both settings cases and both Claude interruptions). Codex could not see the host-temp fixture outside its workspace; this failed before follow-up delivery was exercised. - [Four affected interruption cases](https://d1p6rlowie26tp.cloudfront.net/runner-e2e/campaigns/gha-35647760635-1/) all passed, including cleanup, on definition v3 / `ad6ac0545`. Files live inside the actual agent workspace and the observed run workspace is verified. Both providers saved Friday in the real plan with the undisclosed brief reference; follow-ups persisted while the original run was active. Together with both unchanged settings cases from v2, all six new scenario variants have passing live evidence. - Final head `ad6ac05456646c09d3452e320279457625353948`: 54 successful checks, two intentional skips, zero pending/failing checks; mergeable and clean. Fresh Greptile 5/5, zero unresolved findings. - Full typecheck, tests, build, and browser CI passed remotely. One earlier head encountered a signoff-policy browser timing failure; the final head passed that shard. - Local pnpm wrapper could not fetch its version/signature metadata in the restricted environment; local eval checks used the installed Node executables. Repo-wide validation was completed by GitHub Actions. ## Risks These are eval-only changes. The file wait is a timing fixture in the isolated agent workspace, not a production runner hook. Native Codex host-filesystem isolation stays unchanged. It has a two-minute limit and is released in `finally`. The prepared-agent settings case is not full native onboarding: the wizard currently offers legacy adapters. The disabled-entry assertion uses full document navigation, which clears the prior React Query cache; preserved history is checked through the public API and re-enabled chat. No production prompt, rollout default, adapter behavior, or credential policy changes. Active-task reassignment and worker-crash recovery remain outside these new cases. ## Model Used OpenAI Codex, GPT-6, with repository tools and code execution. The exact deployment model ID and context window are not exposed in this task. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
1 parent 8f8a0ab commit 846336e

18 files changed

Lines changed: 398 additions & 18 deletions

doc/evals.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ Daytona paths. Its [fixture contract](../tests/runner-e2e/README.md) distinguish
6666
startup cancellation from active response cancellation and HTTP send replay
6767
from ambiguous provider action recovery. Select it explicitly; `--all` excludes it.
6868

69+
The explicit-only `agent-chat-stories` suite covers the experimental settings
70+
lifecycle for a configured native agent and follow-ups during active work. Its
71+
fixture-driven file wait and persisted-plan oracle are documented in the
72+
[Product E2E guide](../tests/runner-e2e/README.md). It does not qualify the native
73+
onboarding wizard or change the native API-tool rollout defaults.
74+
6975
## Validation ladder
7076

7177
Start with credential-free checks and a catalog listing. For Product E2E:

tests/runner-e2e/FIXTURES.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,15 @@ The lost-acknowledgement probe may interrupt only the fixture browser's own
195195
comment request after the real server has committed it. Retain its request ID
196196
and replay that same request through the public API after restarting the server.
197197
Never fabricate tool results or repair task state after a failed assertion.
198+
199+
`chat-stories.ts` seeds an ordinary file wait in the isolated agent's actual
200+
home workspace; native Codex intentionally cannot see arbitrary host temp files.
201+
The observed run workspace must match the fixture location. This is a deterministic interruption
202+
boundary. The real provider command writes the readiness file and waits at most
203+
two minutes. The harness must persist the next browser message while the same
204+
run is active before supplying the brief. Always release the wait in `finally`.
205+
Save boundary observations independently of the final outcome. The final answer
206+
must recover a brief reference absent from both prompts; the revision oracle
207+
also reads the actual conversation plan. Fixture setup never enables native API
208+
tools for this suite. Do not describe its prepared-agent settings case as a
209+
production onboarding qualification.

tests/runner-e2e/README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ pnpm test:e2e:runner -- --suite daytona-warm-continuity
8383
pnpm test:e2e:runner -- --all
8484
```
8585

86-
The catalog contains eight suites, including the explicit-only everyday suite. `core-compatibility` (**Core Runner
86+
The catalog contains nine suites, including the explicit-only suites. `core-compatibility` (**Core Runner
8787
Compatibility**) is seven major runner profiles × local/Daytona × three
8888
workflows: 42 cells. Its cases are:
8989

@@ -176,6 +176,9 @@ fails the case instead of silently testing another phase.
176176
Restart continuity requires the agent to recall a phrase after the server
177177
restarts. The final prompt does not reveal that phrase. A generic successful
178178
reply after restart cannot pass this check.
179+
The browser leaves the old development client before the server stops, then
180+
opens the canonical chat route and waits for the composer. This avoids racing
181+
Vite's automatic reconnect navigation against the test's explicit navigation.
179182

180183
The blocker query requests a JSON status snapshot. It must name the current
181184
recorded blocker and report zero active runs independently of the task's blocked
@@ -188,6 +191,29 @@ requires the original comment, task, plan, and single consuming run. This proves
188191
HTTP request idempotency across restart, not replay safety for an ambiguous
189192
provider tool response. Existing native tool-receipt tests cover that boundary.
190193

194+
`agent-chat-stories` adds six explicit-only local cells across native Codex and
195+
Claude. `enable-disable-resume` uses the Experimental settings UI to enable
196+
Agent Chat, starts a conversation, disables new messages, verifies the public
197+
write endpoint rejects a send without creating work, and re-enables the same
198+
conversation with its remembered context. The company, credential, and native
199+
agent are fixture-provisioned. This qualifies the experimental-settings path,
200+
not native first-run onboarding: the current production wizard offers legacy
201+
adapters, and native API tools remain an independent opt-in.
202+
203+
`followup-while-running` and `revise-while-running` send a second browser message
204+
while the provider runs a bounded command waiting for a fixture brief file.
205+
The command publishes its own readiness file; the harness verifies the original
206+
run is still active after the follow-up is saved, then supplies the brief.
207+
The final reply must contain the previously undisclosed brief reference and the
208+
new request's marker. The revision case also checks the saved plan uses Friday
209+
instead of the original Monday. The oracle permits either steering the active
210+
run or one queued successor, but rejects missing/duplicate comments, failed or
211+
unfinished runs, stale plan contents, and unintended tasks/projects. This does
212+
not qualify active-task reassignment or worker-crash recovery.
213+
The maximum run count remains the cost estimate; the shared harness honors the
214+
one-run minimum only for these two interruption cases. Exactly one reply may
215+
consume the follow-up marker, and it must be attributed to the final provider run.
216+
191217
```sh
192218
pnpm test:e2e:runner -- --list --suite agent-chat-hardening
193219
pnpm test:e2e:runner -- --id agent-chat-hardening.runner-codex.local.stop-startup-new-resume
@@ -224,7 +250,8 @@ Missing provider credentials fail paid preflight and are not passing coverage.
224250

225251
The default `--all` selection is 171 cells (148 local and 23 Daytona) and 371
226252
expected paid agent turns. The explicit-only everyday suite adds 38 catalog cells
227-
and chat hardening adds 18. Both are excluded from `--all`. The full catalog has 227 cells.
253+
and chat hardening adds 18; chat stories adds six. All three are excluded from
254+
`--all`. The full catalog has 233 cells.
228255
Follow-up steps remain ordered within their cell; all other
229256
cells are independent. Narrow selectors are strongly recommended while
230257
developing fixtures.

tests/runner-e2e/catalog.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ describe("runner E2E catalog", () => {
7171
expect(localIntegrityTasks).toHaveLength(2);
7272
expect(openRouterBreadthTasks).toHaveLength(3);
7373
expect(runnerSuites.map((suite) => suite.expectedMatrixSize)).toEqual([
74-
23, 38, 52, 28, 18, 42, 14, 10, 2,
74+
23, 38, 52, 28, 18, 6, 42, 14, 10, 2,
7575
]);
76-
expect(validateRunnerCatalog()).toHaveLength(227);
77-
expect(new Set(runnerMatrix.map((entry) => entry.id)).size).toBe(227);
76+
expect(validateRunnerCatalog()).toHaveLength(233);
77+
expect(new Set(runnerMatrix.map((entry) => entry.id)).size).toBe(233);
7878
expect(
7979
runnerMatrix.filter((entry) => entry.suite.id === "core-compatibility"),
8080
).toHaveLength(42);

tests/runner-e2e/catalog.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { continuationTasks } from "./continuation-cases.js";
22
import { everydayTasks, productionStoryProfile } from "./everyday-cases.js";
33

44
import { firstTaskTasks } from "./first-task-cases.js";
5-
import { chatTasks, chatHardeningTasks } from "./chat-cases.js";
5+
import { chatTasks, chatHardeningTasks, chatStoryTasks } from "./chat-cases.js";
66
import { createHash } from "node:crypto";
77
import { createAgentSchema } from "../../packages/shared/src/validators/agent.js";
88
import { createEnvironmentSchema } from "../../packages/shared/src/validators/environment.js";
@@ -959,6 +959,15 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [
959959
["stop-startup-new-resume", "hire-delegate-reuse", "blocked-status-review"].map(task => `agent-chat-hardening.${profile}.daytona.${task}`)),
960960
definitionMetadata: { version: 5, permissions: "production-defaults", instructions: "production", grading: "durable-state-and-source-evidence", scheduling: "explicit-only", restartMemory: "required-after-restart", statusEvidence: "structured-current-blocker-and-active-run-count", readOnlyState: "public-mutation-contract-and-relations", hiringReference: "neutral-document-reference-line" },
961961
},
962+
{
963+
id: "agent-chat-stories", label: "Agent Chat Setup and Interruptions", manualOnly: true,
964+
description: "Experimental settings lifecycle and user follow-ups during active native work.",
965+
groups: ["chat", "native"],
966+
profiles: runnerProfiles.filter(profile => ["runner-codex", "runner-acpx-claude"].includes(profile.id))
967+
.map(profile => productionStoryProfile(defaultPermissionProfile(profile))),
968+
environments: [localEnvironment], tasks: chatStoryTasks, expectedMatrixSize: 6,
969+
definitionMetadata: { version: 3, setup: "configured-native-agent", permissions: "production-defaults", interruptionBoundary: "provider-file-wait-in-agent-workspace", grading: "persisted-comments-and-plan-run-attributed", scheduling: "explicit-only" },
970+
},
962971
...(process.env.PAPERCLIP_RUNNER_E2E_CONNECTION_REVIEWS === "1" ? [connectionReviewSuite] : []),
963972
{
964973
id: "core-compatibility",
@@ -1033,6 +1042,7 @@ export function suiteDefinitionHash(suite: RunnerSuiteFixture) {
10331042
id: task.id,
10341043
flow: task.flow,
10351044
expectedRunCount: task.expectedRunCount,
1045+
...(task.minimumExpectedRunCount === undefined ? {} : { minimumExpectedRunCount: task.minimumExpectedRunCount }),
10361046
restartServerBeforeQuestionAnswer:
10371047
task.restartServerBeforeQuestionAnswer ?? false,
10381048
})),

tests/runner-e2e/chat-cases.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ function buildChatTasks(definitions: readonly (readonly [string, string, number]
4747
}
4848
export const chatTasks = buildChatTasks(CHAT_CASES);
4949
export const chatHardeningTasks = buildChatTasks(HARDENING_CASES);
50+
export const chatStoryTasks = buildChatTasks([
51+
["enable-disable-resume", "Enable Agent Chat, pause access, and resume preserved history", 2],
52+
["followup-while-running", "Deliver a follow-up while a provider turn is running", 2],
53+
["revise-while-running", "Change instructions during active work and save the updated plan", 2],
54+
]).map(task => ({ ...task, ...(task.id === "enable-disable-resume" ? {} : { minimumExpectedRunCount: 1 }) }));
5055

5156
export function chatNeedsApiTools(suiteId: string, caseId: string): boolean {
5257
return suiteId === "agent-chat-hardening" && ["hire-delegate-reuse", "blocked-status-review"].includes(caseId);

tests/runner-e2e/chat-flow.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type { MatrixExecution } from "./types.js";
99
import { isBlockedUnstartedWake } from "./non-execution-wake.js";
1010
import { chatMarker } from "./chat-cases.js";
1111
import { assertChatRememberedAfterRestart, assertChatStartupStopped, isChatStopReady, runChatHardeningFlow } from "./chat-hardening.js";
12+
import { enableChatThroughSettings, runChatInterruption, runChatSettingsLifecycle } from "./chat-stories.js";
13+
import { matchesRunCount, minimumRunCount } from "./run-count.js";
1214

1315
// Public API observations only: this driver never fabricates provider results or writes DB state.
1416
export interface ChatIssue {
@@ -306,6 +308,7 @@ export async function runChatFlow(input: ChatFlowInput) {
306308
const draftMarker = chatMarker("DRAFT", nonce);
307309
const caseId = execution.task.id;
308310
const stopCase = ["stop-new-resume", "stop-startup-new-resume"].includes(caseId);
311+
const interruptionCase = ["followup-while-running", "revise-while-running"].includes(caseId);
309312
let issue: ChatIssue;
310313
let runs: ChatRun[] = [];
311314
const settings = await api.get<Record<string, unknown>>(
@@ -368,7 +371,8 @@ export async function runChatFlow(input: ChatFlowInput) {
368371
};
369372
const noTasks = async () => expect(await tasks()).toHaveLength(0);
370373
try {
371-
await api.patch("/api/instance/settings/experimental", {
374+
if (caseId === "enable-disable-resume") await enableChatThroughSettings(input);
375+
else await api.patch("/api/instance/settings/experimental", {
372376
enableAgentChat: true,
373377
enableClassicTaskInterface: false,
374378
});
@@ -380,7 +384,12 @@ export async function runChatFlow(input: ChatFlowInput) {
380384
expect(await api.get(chatPath)).toBeNull();
381385
expect(await allRuns()).toHaveLength(0);
382386

383-
if (
387+
if (caseId === "enable-disable-resume") {
388+
await runChatSettingsLifecycle({ input, marker, issue: () => issue!, idle, allRuns, comments });
389+
} else if (interruptionCase) {
390+
await runChatInterruption({ input, marker, issue: () => issue!, idle, allRuns, comments,
391+
refreshIssue: async () => { issue = await api.get<ChatIssue>(chatPath); input.observe(issue, await allRuns()); } });
392+
} else if (
384393
["continuity-restart", "new-session", "stop-new-resume", "stop-startup-new-resume"].includes(caseId)
385394
) {
386395
const secret = chatMarker("OLDCONTEXT", nonce);
@@ -911,10 +920,8 @@ export async function runChatFlow(input: ChatFlowInput) {
911920
projects,
912921
});
913922
}
914-
await idle(execution.task.expectedRunCount);
915-
expect(runs.filter((run) => !isResetRun(run))).toHaveLength(
916-
execution.task.expectedRunCount,
917-
);
923+
await idle(minimumRunCount(execution.task));
924+
expect(matchesRunCount(execution.task, runs.filter((run) => !isResetRun(run)).length)).toBe(true);
918925
for (const run of runs.filter((run) => !isResetRun(run))) {
919926
expect(run.runtimeMode).toBe(execution.profile.expectedRuntimeMode);
920927
expect(run.status).toBe(
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createServer } from "node:http";
2+
import { expect, test } from "@playwright/test";
3+
import { restartChatServer } from "./chat-restart.js";
4+
5+
test("unloads the old browser client before server restart and opens the new document", async ({ page }) => {
6+
let generation = 1;
7+
const server = createServer((_req, res) => {
8+
res.writeHead(200, { "content-type": "text/html" });
9+
res.end(`<input data-testid="composer" value="server generation ${generation}"><script>setInterval(() => fetch('/probe').catch(() => {}), 50)</script>`);
10+
});
11+
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve));
12+
const address = server.address();
13+
if (!address || typeof address === "string") throw new Error("Missing listener");
14+
const url = `http://127.0.0.1:${address.port}`;
15+
try {
16+
await page.goto(url);
17+
await expect(page.getByTestId("composer")).toHaveValue("server generation 1");
18+
await restartChatServer(page, async () => {
19+
expect(page.url(), "The old client must be gone before it can auto-reload").toBe("about:blank");
20+
server.closeAllConnections();
21+
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
22+
generation += 1;
23+
await new Promise<void>(resolve => server.listen(address.port, "127.0.0.1", resolve));
24+
});
25+
await page.goto(url, { waitUntil: "commit" });
26+
await expect(page.getByTestId("composer")).toHaveValue("server generation 2");
27+
} finally {
28+
if (server.listening) {
29+
server.closeAllConnections();
30+
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
31+
}
32+
}
33+
});

tests/runner-e2e/chat-restart.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { Page } from "@playwright/test";
2+
3+
/** Restart the test server without letting the old Vite client race navigation. */
4+
export async function restartChatServer(page: Page, restart: () => Promise<void>) {
5+
await page.goto("about:blank", { waitUntil: "commit", timeout: 30_000 });
6+
await restart();
7+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { describe, expect, it } from "vitest";
2+
import { assertInterruptedChat, prepareChatBrief } from "./chat-stories.js";
3+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4+
import { tmpdir } from "node:os";
5+
import path from "node:path";
6+
import { execFile } from "node:child_process";
7+
import { promisify } from "node:util";
8+
import { runnerMatrix } from "./catalog.js";
9+
import { buildRunnerE2EProcessEnvironment } from "./harness-env.js";
10+
11+
const run = { id: "run", companyId: "company", agentId: "agent", status: "succeeded", runtimeMode: "native", contextSnapshot: { issueId: "chat" } };
12+
const valid = {
13+
first: "Read the brief", followup: "Change the launch day", reference: "BRIEF123", marker: "UPDATED123", issueId: "chat",
14+
boundaryRun: { ...run, status: "running" }, activeAtFollowup: { ...run, status: "running" },
15+
comments: [{ id: "first", body: "Read the brief" }, { id: "followup", body: "Change the launch day" },
16+
{ id: "answer", authorAgentId: "agent", createdByRunId: "run", body: "BRIEF123 UPDATED123" }],
17+
runs: [run], revisedPlan: JSON.stringify({ launchDay: "Friday", reference: "BRIEF123", revision: "UPDATED123" }),
18+
};
19+
20+
describe("active chat follow-up oracle", () => {
21+
it("creates the missing fixture directory and waits for the host-supplied brief", async () => {
22+
const root = await mkdtemp(path.join(tmpdir(), "chat-brief-test-"));
23+
try {
24+
const { gate, ready, scriptPath } = await prepareChatBrief(path.join(root, "new workspace"), "fixture");
25+
const command = promisify(execFile)(process.execPath, [scriptPath], { timeout: 5_000 });
26+
try {
27+
await expect.poll(() => readFile(ready, "utf8").catch(() => "")).toBe("waiting");
28+
} finally {
29+
await writeFile(gate, "BRIEF fixture result");
30+
expect((await command).stdout.trim()).toBe("BRIEF fixture result");
31+
}
32+
} finally {
33+
await rm(root, { recursive: true, force: true });
34+
}
35+
});
36+
it("accepts either steering or one queued successor, with the saved correction", () => {
37+
expect(() => assertInterruptedChat(valid)).not.toThrow();
38+
expect(() => assertInterruptedChat({ ...valid, runs: [run, { ...run, id: "successor" }], comments: [...valid.comments.slice(0, 2), { ...valid.comments[2]!, createdByRunId: "successor" }] })).not.toThrow();
39+
});
40+
it("rejects follow-ups sent after the active boundary", () => {
41+
expect(() => assertInterruptedChat({ ...valid, boundaryRun: run })).toThrow();
42+
expect(() => assertInterruptedChat({ ...valid, activeAtFollowup: run })).toThrow();
43+
expect(() => assertInterruptedChat({ ...valid, activeAtFollowup: { ...valid.activeAtFollowup, id: "other" } })).toThrow();
44+
});
45+
it("rejects lost or duplicated input, missing file evidence, and stale plans", () => {
46+
expect(() => assertInterruptedChat({ ...valid, comments: [...valid.comments, { ...valid.comments[2]!, id: "duplicate-reply" }] })).toThrow();
47+
expect(() => assertInterruptedChat({ ...valid, comments: [...valid.comments.slice(0, 2), { ...valid.comments[2]!, createdByRunId: "unrelated" }] })).toThrow();
48+
expect(() => assertInterruptedChat({ ...valid, comments: valid.comments.filter(c => c.id !== "followup") })).toThrow();
49+
expect(() => assertInterruptedChat({ ...valid, comments: [...valid.comments, { id: "duplicate", body: valid.followup }] })).toThrow();
50+
expect(() => assertInterruptedChat({ ...valid, comments: [...valid.comments.slice(0, 2), { id: "answer", authorAgentId: "agent", body: "UPDATED123" }] })).toThrow();
51+
expect(() => assertInterruptedChat({ ...valid, revisedPlan: valid.revisedPlan.replace("Friday", "Monday") })).toThrow();
52+
expect(() => assertInterruptedChat({ ...valid, revisedPlan: "The updated plan is saved." })).toThrow();
53+
});
54+
it("rejects failed, unfinished, duplicated, or unrelated execution", () => {
55+
for (const status of ["running", "queued", "failed", "cancelled"])
56+
expect(() => assertInterruptedChat({ ...valid, runs: [{ ...run, status }] })).toThrow();
57+
expect(() => assertInterruptedChat({ ...valid, runs: [] })).toThrow();
58+
expect(() => assertInterruptedChat({ ...valid, runs: [run, run, run] })).toThrow();
59+
expect(() => assertInterruptedChat({ ...valid, runs: [run, run] })).toThrow();
60+
expect(() => assertInterruptedChat({ ...valid, runs: [{ ...run, id: "other" }] })).toThrow();
61+
expect(() => assertInterruptedChat({ ...valid, runs: [{ ...run, contextSnapshot: { issueId: "other" } }] })).toThrow();
62+
});
63+
it("keeps setup and interruption stories native, local, opt-in, and outside API-tool overrides", () => {
64+
const cells = runnerMatrix.filter(cell => cell.suite.id === "agent-chat-stories");
65+
expect(cells).toHaveLength(6);
66+
for (const cell of cells) {
67+
expect(cell.suite.manualOnly).toBe(true);
68+
expect(cell.profile.generation).toBe("native");
69+
expect(cell.environment.id).toBe("local");
70+
expect(buildRunnerE2EProcessEnvironment({}, [cell]).PAPERCLIP_RUNNER_API_TOOLS_ENABLED).toBeUndefined();
71+
}
72+
});
73+
});

0 commit comments

Comments
 (0)