Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions e2e-tests/attach_image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { test, Timeout } from "./helpers/test_helper";
import { expect } from "@playwright/test";
import * as fs from "fs";

// It's hard to read the snapshots, but they should be identical across
// all test cases in this file, so we use the same snapshot name to ensure
// the outputs are identical.
const SNAPSHOT_NAME = "attach-image";
// The in-chat picker and drag-and-drop requests should stay identical. The
// home request has no prior assistant turn, so it does not include Git
// provenance and uses a separate baseline.
const CHAT_SNAPSHOT_NAME = "attach-image";
const HOME_SNAPSHOT_NAME = "attach-image-home";

// attach image is implemented in two separate components
// - HomeChatInput
Expand Down Expand Up @@ -39,7 +40,7 @@ test("attach image - home chat", async ({ po }) => {
await fileChooser.setFiles("e2e-tests/fixtures/images/logo.png");

await po.sendPrompt("[dump]");
await po.snapshotServerDump("last-message", { name: SNAPSHOT_NAME });
await po.snapshotServerDump("last-message", { name: HOME_SNAPSHOT_NAME });
await po.snapshotMessages({ replaceDumpPath: true });
});

Expand Down Expand Up @@ -71,7 +72,7 @@ test("attach image - chat", async ({ po }) => {
await fileChooser.setFiles("e2e-tests/fixtures/images/logo.png");

await po.sendPrompt("[dump]");
await po.snapshotServerDump("last-message", { name: SNAPSHOT_NAME });
await po.snapshotServerDump("last-message", { name: CHAT_SNAPSHOT_NAME });
await po.snapshotMessages({ replaceDumpPath: true });
});

Expand Down Expand Up @@ -175,6 +176,6 @@ test("attach image via drag - chat", async ({ po }) => {
// submit and verify
await po.sendPrompt("[dump]");
// Note: this should match EXACTLY the server dump from the previous test.
await po.snapshotServerDump("last-message", { name: SNAPSHOT_NAME });
await po.snapshotServerDump("last-message", { name: CHAT_SNAPSHOT_NAME });
await po.snapshotMessages({ replaceDumpPath: true });
});
5 changes: 3 additions & 2 deletions e2e-tests/concurrent_chat.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test } from "./helpers/test_helper";
import { test, Timeout } from "./helpers/test_helper";
import { expect } from "@playwright/test";

test("concurrent chat", async ({ po }) => {
Expand All @@ -24,5 +24,6 @@ test("concurrent chat", async ({ po }) => {
.first();
await expect(chat1Tab).toBeVisible();
await chat1Tab.click();
await po.snapshotMessages({ timeout: 12_000 });
await po.chatActions.waitForChatCompletion({ timeout: Timeout.EXTRA_LONG });
await po.snapshotMessages();
});
17 changes: 11 additions & 6 deletions e2e-tests/helpers/utils/normalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,17 @@ export function normalizeMcpCallIds(dump: any): void {
*/
export function normalizeGitContextHashes(dump: any): void {
const scrub = (value: string): string =>
value.replace(/<dyad-git-context\b[^>]*>/g, (tag) =>
tag.replace(
/\b(commit|source_commit)="[0-9a-f]{40,64}"/gi,
'$1="[[GIT_COMMIT]]"',
),
);
value
.replace(
/(<system-reminder>Previous assistant message created (?:commit: |no commit\. Repository commit before that message: ))[0-9a-f]{40,64}(\.<\/system-reminder>)/gi,
"$1[[GIT_COMMIT]]$2",
)
.replace(/<dyad-git-context\b[^>]*>/g, (tag) =>
tag.replace(
/\b(commit|source_commit)="[0-9a-f]{40,64}"/gi,
'$1="[[GIT_COMMIT]]"',
),
);

const visit = (value: unknown, set: (next: unknown) => void): void => {
if (typeof value === "string") {
Expand Down
5 changes: 5 additions & 0 deletions e2e-tests/plan_mode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ testSkipIfWindows(
timeout: Timeout.MEDIUM,
});

await expect(po.page.getByTestId("messages-list")).toContainText(
"[[dyad-dump-path=",
{ timeout: Timeout.EXTRA_LONG },
);

// Verify the request sent to the server contains the correctly formatted comments
await po.snapshotServerDump("last-message");
},
Expand Down
6 changes: 4 additions & 2 deletions e2e-tests/snapshots/astro.spec.ts_astro-1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ message:
A file (2)
</dyad-write>
More
EOM<dyad-git-context commit="[[GIT_COMMIT]]"></dyad-git-context>
EOM

===
role: user
message: [dump] hi
message: [dump] hi

<system-reminder>Previous assistant message created commit: [[GIT_COMMIT]].</system-reminder>
2 changes: 1 addition & 1 deletion e2e-tests/snapshots/attach_image.spec.ts_attach-image
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
===
role: user
message: [{"type":"text","text":"[dump]"},{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAEKADAAQAAAABAAAAEAAAAADHbxzxAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgoZXuEHAAACbElEQVQ4EY1TTWgTQRR+M7NpQoKmPVTPQi/qoaDWQAsxIigqRS/JzYNHxVz0oAcrG6hQoV68ePNQECUpeBFBIqQiVBrbaqvgoXjRSzVJf8zfzu7MG2eWbGywFN8e3ux73/fNm/dmAP7TFCgSQHeurSC4t1eEAFET4+WjzPIq5AX5ZURMjO5GNEk7VbJKKWU9Or8WBg2cvPRxlLXiX7arVtFWNjVkzSX/VJBPK0YKRMIciI6477kjhsDrAxfJoebVUwd0bl1vBD0ChpzR5JkrKzGvjum2B8MtrphLRXGTY5RKCaioi7wr/lcgID+//Omsu4EzERg4GIIoWAygxrezwOvNhmiAoCrkCtZtqF9BPp33d86nl46jw173aWKtXWtzWi21kELDgzOo+mJCCRBIFIowBr3rOQK4bDJC90PVrf5Ayi/efDP62QBvn14Y5m0sKogOCoWy/nvL55syqOl4ppCRT6+9GxAKjgmtLYg3ldVkM4Hs0Fr4QSmxwi28T1gUHE+J9rpGdozqRvoWcmKWUMpyEXWH2IYJiq0KtQYr/qgRWc3T4lJ/IbNVx/xmmCrMXJ+ML3+wZP+Jmre5MN8/NVYoFKTB2XbJ+vYyPi9l/4hLq+XZpZMJ0BxzP3z1XGpO99qUDg897VFGEkd+3lm9lVy8e2NsceL7q/iqwvCIohIYU9MGm+pwuuOwbUVtm+D0uXIO5b57noxCWzJoSQJNIaAhm8BxMze7PGbboLFA/El0BYxqcJTJC+Vkg5PrLU4PO1KBg/jVo87jZ++Tb4PSDX5XMxdq14QOpvfI9XDMxTKPKQim9DqtY8H/Tv8HGFE+AZtzYdAAAAAASUVORK5CYII="}}]
message: [{"type":"text","text":"[dump]"},{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAEKADAAQAAAABAAAAEAAAAADHbxzxAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgoZXuEHAAACbElEQVQ4EY1TTWgTQRR+M7NpQoKmPVTPQi/qoaDWQAsxIigqRS/JzYNHxVz0oAcrG6hQoV68ePNQECUpeBFBIqQiVBrbaqvgoXjRSzVJf8zfzu7MG2eWbGywFN8e3ux73/fNm/dmAP7TFCgSQHeurSC4t1eEAFET4+WjzPIq5AX5ZURMjO5GNEk7VbJKKWU9Or8WBg2cvPRxlLXiX7arVtFWNjVkzSX/VJBPK0YKRMIciI6477kjhsDrAxfJoebVUwd0bl1vBD0ChpzR5JkrKzGvjum2B8MtrphLRXGTY5RKCaioi7wr/lcgID+//Omsu4EzERg4GIIoWAygxrezwOvNhmiAoCrkCtZtqF9BPp33d86nl46jw173aWKtXWtzWi21kELDgzOo+mJCCRBIFIowBr3rOQK4bDJC90PVrf5Ayi/efDP62QBvn14Y5m0sKogOCoWy/nvL55syqOl4ppCRT6+9GxAKjgmtLYg3ldVkM4Hs0Fr4QSmxwi28T1gUHE+J9rpGdozqRvoWcmKWUMpyEXWH2IYJiq0KtQYr/qgRWc3T4lJ/IbNVx/xmmCrMXJ+ML3+wZP+Jmre5MN8/NVYoFKTB2XbJ+vYyPi9l/4hLq+XZpZMJ0BxzP3z1XGpO99qUDg897VFGEkd+3lm9lVy8e2NsceL7q/iqwvCIohIYU9MGm+pwuuOwbUVtm+D0uXIO5b57noxCWzJoSQJNIaAhm8BxMze7PGbboLFA/El0BYxqcJTJC+Vkg5PrLU4PO1KBg/jVo87jZ++Tb4PSDX5XMxdq14QOpvfI9XDMxTKPKQim9DqtY8H/Tv8HGFE+AZtzYdAAAAAASUVORK5CYII="}},{"type":"text","text":"<system-reminder>Previous assistant message created no commit. Repository commit before that message: [[GIT_COMMIT]].</system-reminder>"}]
3 changes: 3 additions & 0 deletions e2e-tests/snapshots/attach_image.spec.ts_attach-image-home
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
===
role: user
message: [{"type":"text","text":"[dump]"},{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAEKADAAQAAAABAAAAEAAAAADHbxzxAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgoZXuEHAAACbElEQVQ4EY1TTWgTQRR+M7NpQoKmPVTPQi/qoaDWQAsxIigqRS/JzYNHxVz0oAcrG6hQoV68ePNQECUpeBFBIqQiVBrbaqvgoXjRSzVJf8zfzu7MG2eWbGywFN8e3ux73/fNm/dmAP7TFCgSQHeurSC4t1eEAFET4+WjzPIq5AX5ZURMjO5GNEk7VbJKKWU9Or8WBg2cvPRxlLXiX7arVtFWNjVkzSX/VJBPK0YKRMIciI6477kjhsDrAxfJoebVUwd0bl1vBD0ChpzR5JkrKzGvjum2B8MtrphLRXGTY5RKCaioi7wr/lcgID+//Omsu4EzERg4GIIoWAygxrezwOvNhmiAoCrkCtZtqF9BPp33d86nl46jw173aWKtXWtzWi21kELDgzOo+mJCCRBIFIowBr3rOQK4bDJC90PVrf5Ayi/efDP62QBvn14Y5m0sKogOCoWy/nvL55syqOl4ppCRT6+9GxAKjgmtLYg3ldVkM4Hs0Fr4QSmxwi28T1gUHE+J9rpGdozqRvoWcmKWUMpyEXWH2IYJiq0KtQYr/qgRWc3T4lJ/IbNVx/xmmCrMXJ+ML3+wZP+Jmre5MN8/NVYoFKTB2XbJ+vYyPi9l/4hLq+XZpZMJ0BxzP3z1XGpO99qUDg897VFGEkd+3lm9lVy8e2NsceL7q/iqwvCIohIYU9MGm+pwuuOwbUVtm+D0uXIO5b57noxCWzJoSQJNIaAhm8BxMze7PGbboLFA/El0BYxqcJTJC+Vkg5PrLU4PO1KBg/jVo87jZ++Tb4PSDX5XMxdq14QOpvfI9XDMxTKPKQim9DqtY8H/Tv8HGFE+AZtzYdAAAAAASUVORK5CYII="}}]

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@
{
"type": "text",
"text": "\n <dyad-write path=\"file1.txt\">\n A file (2)\n </dyad-write>\n More\n EOM"
},
{
"type": "text",
"text": "<dyad-git-context commit=\"[[GIT_COMMIT]]\"></dyad-git-context>"
}
]
},
Expand All @@ -43,7 +39,7 @@
"content": [
{
"type": "text",
"text": "[dump]"
"text": "[dump]\n\n<system-reminder>Previous assistant message created commit: [[GIT_COMMIT]].</system-reminder>"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ message:
A file (2)
</dyad-write>
More
EOM<dyad-git-context commit="[[GIT_COMMIT]]"></dyad-git-context>
EOM

===
role: user
message: [dump] @file:src/App.tsx
message: [dump] @file:src/App.tsx

<system-reminder>Previous assistant message created commit: [[GIT_COMMIT]].</system-reminder>
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
===
role: user
message: [{"type":"text","text":"I have the following comments on the plan:\n\n**Comment 1:**\n> Step two\n\nAdd more detail for step two.\n\nPlease update the plan based on these comments."}]
message: [{"type":"tool_result","tool_use_id":"[[TOOL_CALL_0]]","content":"{\"type\":\"text\",\"value\":\"Implementation plan \\\"Test Plan\\\" has been presented to the user. They can review it in the preview panel and either accept it or request changes.\"}"},{"type":"text","text":"I have the following comments on the plan:\n\n**Comment 1:**\n> Step two\n\nAdd more detail for step two.\n\nPlease update the plan based on these comments.\n\n<system-reminder>Previous assistant message created no commit. Repository commit before that message: [[GIT_COMMIT]].</system-reminder>"}]
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
===
role: user
message: [dump] tc=basic
message: [dump] tc=basic

<system-reminder>Previous assistant message created no commit. Repository commit before that message: [[GIT_COMMIT]].</system-reminder>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ message: tc=basic

===
role: assistant
message: This is a simple basic response<dyad-git-context source_commit="[[GIT_COMMIT]]" no_commit="true"></dyad-git-context>
message: This is a simple basic response

===
role: user
Expand All @@ -25,4 +25,6 @@ Snippet:
<p className="text-xl text-gray-600">
Start building your amazing project here!
</p>
```
```

<system-reminder>Previous assistant message created no commit. Repository commit before that message: [[GIT_COMMIT]].</system-reminder>
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
===
role: user
message: [dump] tc=basic
message: [dump] tc=basic

<system-reminder>Previous assistant message created no commit. Repository commit before that message: [[GIT_COMMIT]].</system-reminder>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ message: tc=basic

===
role: assistant
message: This is a simple basic response<dyad-git-context source_commit="[[GIT_COMMIT]]" no_commit="true"></dyad-git-context>
message: This is a simple basic response

===
role: user
Expand All @@ -25,4 +25,6 @@ Snippet:
</main>
<MadeWithDyad />
</div>
```
```

<system-reminder>Previous assistant message created no commit. Repository commit before that message: [[GIT_COMMIT]].</system-reminder>
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ Snippet:
href="https://www.dyad.sh/"
target="_blank"
rel="noopener noreferrer"
```
```

<system-reminder>Previous assistant message created no commit. Repository commit before that message: [[GIT_COMMIT]].</system-reminder>
4 changes: 3 additions & 1 deletion rules/e2e-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ If a helper moves from Playwright's `toMatchAriaSnapshot()` to a custom `toMatch

When a test uses both raw Playwright `toMatchAriaSnapshot()` and `po.snapshotMessages()` with the same test title, give `snapshotMessages` an explicit `name`. Otherwise the custom message snapshot can collide with Playwright's numbered component snapshot files and overwrite unrelated baselines.

When multiple tests intentionally share an explicit snapshot name, re-check that their serialized inputs remain identical after history-format changes. Conditional context such as Git provenance can make a first-turn request differ from a later in-chat request; split the baselines instead of letting `--update-snapshots=all` make the last test silently overwrite the first.

Snapshot sanitizers should normalize the captured snapshot text, not mutate React-owned DOM with `innerHTML` before snapshotting. DOM mutation during E2E can trigger React `NotFoundError: Failed to execute 'removeChild' on 'Node'` on the next render.

Custom snapshot helpers that read/write baseline files directly must fail the test after writing a missing baseline (Playwright's default `updateSnapshots: "missing"` writes the file AND fails). Returning silently after the write lets a renamed or typo'd snapshot name pass green on CI without ever comparing.
Expand Down Expand Up @@ -308,7 +310,7 @@ If a targeted E2E fails before launch with `ENOENT: no such file or directory, s
- **Filesystem-heavy IPC assertions**: Operations that delete or copy whole app directories (e.g. bulk app delete) can exceed the 5s default expect timeout on CI runners. Give the post-operation assertion an explicit `{ timeout: 30_000 }`.
- **Expensive platform/framework matrices**: Split scenarios that each install dependencies or run production builds into separate tests. Each scenario then gets its own timeout, retry, and failure identity on slower Windows runners instead of sharing one monolithic budget.
- **Deleting freshly generated apps**: Prompt completion can precede background preview dependency installation. Before bulk deletion, wait for the latest app's `preview-iframe-element`; otherwise deletion can spend its whole timeout interrupting still-installing runtimes one by one.
- **Fake Anthropic engine routes**: When app code uses Anthropic direct passthrough, the fake LLM server must handle `/v1/messages` (and provider-prefixed variants like `/engine/v1/messages`), not just `/chat/completions`. Anthropic tool results come back as user messages with `tool_result` content blocks, so fixture turn counting must skip those as user prompts.
- **Fake Anthropic engine routes**: When app code uses Anthropic direct passthrough, the fake LLM server must handle `/v1/messages` (and provider-prefixed variants like `/engine/v1/messages`), not just `/chat/completions`. Anthropic tool results come back as user messages with `tool_result` content blocks, so fixture turn counting must skip pure tool-result messages. A user message can contain both `tool_result` and real text (for example queued plan comments); route the text as a real prompt instead of discarding the whole mixed message.
- **Fake LLM fixture routing**: If a `tc=...` prompt unexpectedly returns the canned fallback and proposal buttons never appear, inspect `testing/fake-llm-server` routing. OpenAI-compatible requests can send user content as text parts or end with a non-user message, so fixture and `[sleep=...]` checks should use the extracted last user text, not raw `messages[messages.length - 1].content`.
- **Fake LLM streamed tool calls**: watch for the client giving up on `res`, never on `req`. Since Node 16 an `IncomingMessage` emits `close` as soon as the request itself is complete — for a POST whose body express already parsed, that is before the first SSE chunk goes out — so a `req.on("close")` guard trips on every call and the fixture bails mid-stream without ever calling `res.end()`. The symptom is a turn that streams forever with an empty assistant message: the tool never runs, and the E2E times out waiting on a locator that only exists after it does. `curl -N` the fixture directly and count the `data:` chunks to tell a hung fixture apart from an app bug.
- **Local-agent fixture `delayMs`**: Reserve `delayMs` for cancellation/connection tests that intentionally keep a response open. An ordinary fixture can treat the completed request as an abort and send no response, leaving the UI on the streaming animation; when waiting for background work such as a debounced indexer, wait after the prerequisite turn settles in the E2E instead.
Expand Down
Loading
Loading