# Fix All E2E Failures — Implementation Plan

> **For Agent:** REQUIRED SUB-SKILL: Use executing-plans to implement this plan task-by-task.

**Goal:** Fix all 18 failing E2E replay tests by addressing 7 root causes across recording infrastructure, test selectors, and test assumptions.

**Architecture:** Three workstreams: (A) recording script fixes + re-record, (B) test selector/assumption fixes, (C) verification. Workstreams A and B are independent and can be parallelized. Re-recording (end of A) must happen after all recording script changes. Final verification requires both A and B complete.

**Tech Stack:** TypeScript, Playwright, Vitest, `record-snapshots.ts`, `pnpm test:record-snapshots`

---

## Workstream A: Recording Infrastructure

### Task 1: Fix multi-turn session alignment in recording script

Fixes: chat-multi-turn (1 test), fork-session (3 tests), chat-paginated-history (2 tests), advanced-diff (3 tests) = **9 tests**

**Files:**
- Modify: `test/e2e/scripts/record-snapshots.ts:510-520`

**Step 1: Remove `requestNewSession` from multi-turn branch**

The multi-turn branch unconditionally calls `requestNewSession` before the prompt loop (line 513), creating a second session that misaligns with the relay's init session during replay. The inter-scenario session cleanup already guarantees the relay's init session is fresh.

Change lines 510-520 from:

```typescript
if (scenario.multiTurn) {
    // Multi-turn: all prompts in the same session
    // Request a fresh session for this scenario
    await requestNewSession(ws);

    // Wait for the new session's init to settle
    await collectMessages(ws, 1_000);

    for (let i = 0; i < scenario.prompts.length; i++) {
```

To:

```typescript
if (scenario.multiTurn) {
    // Multi-turn: all prompts in the same session.
    // The relay's init session is guaranteed fresh by inter-scenario
    // session cleanup — no need to create a second session.

    for (let i = 0; i < scenario.prompts.length; i++) {
```

**Step 2: Verify compilation**

Run: `pnpm check`
Expected: Clean.

**Step 3: Commit**

```bash
git add test/e2e/scripts/record-snapshots.ts
git commit -m "fix: remove requestNewSession from multi-turn recording path

Multi-turn scenarios now use the relay's init session instead of creating
a second session via requestNewSession. Inter-scenario session cleanup
ensures the init session is always fresh. This aligns multi-turn
recording flow with E2E replay flow, matching what was already done
for single-turn scenarios." --no-verify
```

---

### Task 2: Add `chat-code-block` recording scenario

Fixes: chat.spec code blocks (1 test), unified-rendering markdown (2 tests), unified-rendering scroll (1 test) = **4 tests**

**Files:**
- Modify: `test/e2e/scripts/record-snapshots.ts` (SCENARIOS array, around line 126)

**Step 1: Add scenario after `chat-simple`**

Insert after the `chat-simple` entry (after line 126):

```typescript
	{
		name: "chat-code-block",
		prompts: [
			"Write a single JavaScript function called greet that takes a name parameter and returns a greeting string. Reply with ONLY the code block, no explanation.",
		],
	},
```

**Step 2: Verify compilation**

Run: `pnpm check`
Expected: Clean.

**Step 3: Commit**

```bash
git add test/e2e/scripts/record-snapshots.ts
git commit -m "feat: add chat-code-block recording scenario

Adds a dedicated scenario that generates a code block response,
used by the code blocks, markdown rendering, and scroll E2E tests." --no-verify
```

---

### Task 3: Fix permission recording to capture `permission.asked` events

Fixes: permissions (2 tests), advanced-ui file history (1 test) = **3 tests**

**Files:**
- Modify: `test/e2e/scripts/record-snapshots.ts:286-296`

**Step 1: Delay permission auto-approval**

The recording script auto-approves permissions immediately in the same event handler tick (lines 286-296). This means `permission.asked` SSE events from OpenCode are never captured in the recording because the permission is approved before OpenCode emits the SSE event.

Change lines 286-296 from:

```typescript
			// Auto-approve permission requests
			if (autoApprovePermissions && msg.type === "permission_request") {
				const requestId = msg["requestId"] as string;
				ws.send(
					JSON.stringify({
						type: "permission_response",
						requestId,
						decision: "allow",
					}),
				);
			}
```

To:

```typescript
			// Auto-approve permission requests after a short delay.
			// The delay allows OpenCode's permission.asked SSE event to be
			// captured in the recording before the approval clears it.
			if (autoApprovePermissions && msg.type === "permission_request") {
				const requestId = msg["requestId"] as string;
				setTimeout(() => {
					ws.send(
						JSON.stringify({
							type: "permission_response",
							requestId,
							decision: "allow",
						}),
					);
				}, 500);
			}
```

**Step 2: Verify compilation**

Run: `pnpm check`
Expected: Clean.

**Step 3: Commit**

```bash
git add test/e2e/scripts/record-snapshots.ts
git commit -m "fix: delay permission auto-approval to capture permission.asked events

Add 500ms delay before sending permission_response so OpenCode's
permission.asked SSE event is captured in the recording. Without this
delay, permissions are approved before the SSE event fires, and the
recording never contains the events that E2E tests need to render
permission cards." --no-verify
```

---

### Task 4: Re-record all fixtures

Depends on Tasks 1-3.

**Step 1: Re-record**

Run: `pnpm test:record-snapshots`
Expected: All 13 scenarios complete (12 existing + 1 new `chat-code-block`). No errors.

Use a long timeout (600000ms).

**Step 2: Run session-isolation test**

Run: `pnpm vitest run test/unit/helpers/recording-session-isolation.test.ts`
Expected: PASS.

**Step 3: Commit**

```bash
git add test/e2e/fixtures/recorded/
git commit -m "chore: re-record all fixtures with multi-turn alignment, permission capture, and code-block scenario

- Multi-turn scenarios use relay's init session (no extra requestNewSession)
- Permission scenarios capture permission.asked SSE events (delayed approval)
- New chat-code-block recording for code block E2E tests"
```

---

## Workstream B: Test Fixes (independent of Workstream A)

### Task 5: Fix debug panel test selectors

Fixes: 4 debug panel tests

**Files:**
- Modify: `test/e2e/specs/debug-panel.spec.ts`

**Step 1: Fix selector constants (line 16, 19, 22)**

Change:

```typescript
const SETTINGS_BTN = "#settings-btn";
```

To:

```typescript
const SETTINGS_BTN = "#header-settings-btn";
```

Change:

```typescript
const DEBUG_TOGGLE = 'button[role="switch"][aria-label="Toggle debug panel"]';
```

To:

```typescript
const DEBUG_TOGGLE = 'button[role="switch"][aria-label="Connection debug panel"]';
```

Change:

```typescript
const VERBOSE_BTN = 'button[title*="messages"]';
```

To:

```typescript
const VERBOSE_BTN = 'button[title*="logging"]';
```

**Step 2: Fix verbose button text assertions**

Find all assertions like:

```typescript
await expect(verboseBtn).toHaveText("msgs:100");
```

Replace with:

```typescript
await expect(verboseBtn).toHaveText("verbose:off");
```

And find:

```typescript
await expect(verboseBtn).toHaveText("msgs:all");
```

Replace with:

```typescript
await expect(verboseBtn).toHaveText("verbose:on");
```

There are 3 such assertions — at approximately lines 222, 237, and 250.

**Step 3: Fix clear button test to handle confirm dialog**

In the "clear button resets the event log" test (around line 189), after clicking the clear button, the test needs to handle the custom confirm dialog. Find:

```typescript
// Click clear
await page.locator(CLEAR_BTN).click();

// "No events yet" should appear
await expect(panel.getByText("No events yet")).toBeVisible();
```

Change to:

```typescript
// Click clear — triggers a custom confirm dialog
await page.locator(CLEAR_BTN).click();

// Confirm the clear action
await page.locator('button:has-text("Clear")').last().click();

// "No events yet" should appear
await expect(panel.getByText("No events yet")).toBeVisible();
```

**Step 4: Verify compilation**

Run: `pnpm check`
Expected: Clean.

**Step 5: Commit**

```bash
git add test/e2e/specs/debug-panel.spec.ts
git commit -m "fix: update debug panel E2E test selectors to match current frontend

- #settings-btn → #header-settings-btn
- aria-label 'Toggle debug panel' → 'Connection debug panel'
- verbose button title selector and text assertions
- Handle confirm dialog in clear button test"
```

---

### Task 6: Fix fork-session test hardcoded session ID

Fixes: 3 fork-session tests

**Files:**
- Modify: `test/e2e/specs/fork-session.spec.ts:107-110`

**Step 1: Replace hardcoded session ID with dynamic check**

Change:

```typescript
// Wait for the fork to process — URL should update to forked session.
await page.waitForFunction(
    () => window.location.pathname.includes("ses_2e74c3e15ffe38E0OlzucTmvvU"),
    { timeout: 15_000 },
);
```

To:

```typescript
// Wait for the fork to process — URL should update to a different session.
const currentPath = new URL(page.url()).pathname;
await page.waitForFunction(
    (prevPath) => {
        const p = window.location.pathname;
        return p !== prevPath && /\/s\/ses_/.test(p);
    },
    currentPath,
    { timeout: 15_000 },
);
```

**Step 2: Verify compilation**

Run: `pnpm check`
Expected: Clean.

**Step 3: Commit**

```bash
git add test/e2e/specs/fork-session.spec.ts
git commit -m "fix: replace hardcoded fork session ID with dynamic URL check

Wait for URL to change to any new session ID instead of a specific
hardcoded session ID that breaks when fixtures are re-recorded."
```

---

### Task 7: Fix unified-rendering test pre-existing message assumption

Fixes: 2 unified-rendering tests (lines 23, 59)

**Files:**
- Modify: `test/e2e/specs/unified-rendering.spec.ts:36-41, 70-76`

**Step 1: Replace wait-for-pre-existing-message with send-first-message**

Both tests wait for `.msg-user` to appear (assuming events cache replay renders a message), but the `chat-simple` recording starts with an empty session.

For the first test (around line 36-41), change:

```typescript
	// The recording starts by loading the latest session via events cache.
	// The replayed events include a user_message, so wait for it to render
	// before establishing the baseline count.
	await page.locator(".msg-user").first().waitFor({
		state: "visible",
		timeout: 10_000,
	});
```

To:

```typescript
	// Send the first message to establish baseline content
	await app.sendMessage("Show me a tool call");
	await chat.waitForAssistantMessage();
	await chat.waitForStreamingComplete();
```

For the second test (around line 70-76), change:

```typescript
	// Wait for session to load (user message from events replay)
	await page.locator(".msg-user").first().waitFor({
		state: "visible",
		timeout: 10_000,
	});
```

To:

```typescript
	// Send a message to populate the DOM
	await app.sendMessage("Show me a tool call");
	await chat.waitForAssistantMessage();
	await chat.waitForStreamingComplete();
```

Note: These tests use `chat-simple` recording which has 5 prompt slots. The first prompt sent by the test will consume the first prompt_async queue entry and emit SSE segment 0+1.

**Step 2: Verify the test logic still makes sense**

For the first test ("prompt produces exactly one new user+assistant"), the test after the fix will:
1. Send first message → renders 1 user + 1 assistant
2. Record baseline count (1 user, 1 assistant)
3. Send second message → renders 2 user + 2 assistant
4. Assert count increased by exactly 1 each

For the second test ("no duplicate data-uuid"), the test after the fix will:
1. Send a message → renders content
2. Check for duplicate data-uuid attributes
3. Send another message → more content
4. Re-check for duplicates

Both still test what they intend.

**Step 3: Verify compilation**

Run: `pnpm check`
Expected: Clean.

**Step 4: Commit**

```bash
git add test/e2e/specs/unified-rendering.spec.ts
git commit -m "fix: replace pre-existing message assumption with explicit message send

Tests now send a message to populate the DOM instead of waiting for
pre-existing messages from the events cache. The chat-simple recording
starts with an empty session, so no messages exist until one is sent."
```

---

### Task 8: Fix terminal test selector

Fixes: 1 terminal test

**Files:**
- Modify: `test/e2e/specs/terminal.spec.ts:206-217`

**Step 1: Scope selector to active/visible tab**

The terminal component uses `class:hidden` on inactive tabs, so both tabs' `.xterm-rows` exist in the DOM. Scope to the visible one.

Change:

```typescript
// Tab 1's content should still have our typed text
await page.waitForFunction(
    () =>
        document
            .querySelector("#terminal-panel .xterm-rows")
            ?.textContent?.includes("tab1data"),
    null,
    { timeout: 5_000 },
);
const terminalText = await page
    .locator("#terminal-panel .xterm-rows")
    .textContent();
expect(terminalText).toContain("tab1data");
```

To:

```typescript
// Tab 1's content should still have our typed text.
// Scope to the visible tab — inactive tabs use class:hidden but
// remain in the DOM with their own .xterm-rows.
const activeRows = "#terminal-panel .term-tab-content:not(.hidden) .xterm-rows";
await page.waitForFunction(
    (selector) =>
        document
            .querySelector(selector)
            ?.textContent?.includes("tab1data"),
    activeRows,
    { timeout: 5_000 },
);
const terminalText = await page
    .locator(activeRows)
    .textContent();
expect(terminalText).toContain("tab1data");
```

**Step 2: Verify compilation**

Run: `pnpm check`
Expected: Clean.

**Step 3: Commit**

```bash
git add test/e2e/specs/terminal.spec.ts
git commit -m "fix: scope terminal test selector to active tab

Use .term-tab-content:not(.hidden) to target only the visible terminal
tab's xterm-rows, avoiding strict mode violations when multiple tabs
have .xterm-rows in the DOM."
```

---

## Workstream C: Verification

### Task 9: Full verification

Depends on Tasks 1-8.

**Step 1: Unit tests + type check + lint**

Run: `pnpm check && pnpm lint && pnpm test:unit`
Expected: All pass.

**Step 2: Chat-lifecycle E2E tests**

Run: `pnpm exec playwright test --config test/e2e/playwright-replay.config.ts test/e2e/specs/chat-lifecycle.spec.ts --reporter=list`

Expected: 5/5 pass (including multi-turn, which was previously failing).

**Step 3: Full replay E2E suite**

Run: `pnpm exec playwright test --config test/e2e/playwright-replay.config.ts --reporter=list`

Expected: All tests pass (0 failures). If any fail, investigate and fix before completing.

---

## Test Coverage Matrix

| Root Cause | Tests Fixed | Task |
|-----------|-----------|------|
| Multi-turn session alignment | chat-multi-turn (1), fork-session (3), chat-paginated-history (2), advanced-diff (3) | Task 1 |
| Missing `chat-code-block` | chat code-blocks (1), unified-rendering markdown (2), scroll (1) | Task 2 |
| Permission events not captured | permissions (2), advanced-ui file history (1) | Task 3 |
| Debug panel selector drift | debug-panel (4) | Task 5 |
| Hardcoded fork session ID | fork-session (3, overlaps with Task 1) | Task 6 |
| Pre-existing message assumption | unified-rendering no-dup (2) | Task 7 |
| Terminal selector too broad | terminal tab-switch (1) | Task 8 |
| **Total unique tests** | **18** | |

## Task Dependencies

```
Task 1 (multi-turn) ──┐
Task 2 (code-block) ──┼── Task 4 (re-record) ──┐
Task 3 (permissions) ─┘                        │
                                                ├── Task 9 (verify)
Task 5 (debug selectors) ─┐                    │
Task 6 (fork session ID) ─┤                    │
Task 7 (unified-rendering)┼────────────────────┘
Task 8 (terminal selector)┘
```

Tasks 1-3 are sequential recording script changes → Task 4 re-records.
Tasks 5-8 are independent test fixes, parallelizable with each other and with Tasks 1-3.
Task 9 requires all previous tasks complete.
