diff --git a/apps/web/tests/github-ready-review.e2e.ts b/apps/web/tests/github-ready-review.e2e.ts index 1530c35c80..fe9b498822 100644 --- a/apps/web/tests/github-ready-review.e2e.ts +++ b/apps/web/tests/github-ready-review.e2e.ts @@ -139,9 +139,22 @@ describe.skipIf(MODE === 'record')('web e2e: GitHub ready-for-review', () => { head: { ref: 'fix-session-replay', sha: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' }, }, } - expect((await send(webhookOrigin, 'ready', payload)).status).toBe(202) - await vi.waitFor(() => { expect(scaffold.ctx.agents.list()).toHaveLength(before + 1) }) - await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) }) + const completed = Promise.withResolvers() + let reviewSession: string | undefined + const off = scaffold.ctx.on('session/event', (session, event) => { + if (event.type === 'user/message' && event.data.source.kind === 'webhook' + && event.data.source.provider === 'github' && event.data.source.source === 'primary-github' + && event.data.source.deliveryId === 'ready' && event.data.source.ruleId === 'review-pr-when-ready') reviewSession = session.id + if (event.type === 'turn/end' && session.id === reviewSession) completed.resolve(undefined) + }) + try { + expect((await send(webhookOrigin, 'ready', payload)).status).toBe(202) + await completed.promise + } finally { + off() + } + expect(scaffold.ctx.agents.list()).toHaveLength(before + 1) + expect(adapter.requests).toHaveLength(1) const agent = scaffold.ctx.agents.list().find(candidate => candidate.session.header.cwd === scaffold.workspaceCwd) expect(agent).toBeDefined() diff --git a/apps/web/tests/message-actions.e2e.ts b/apps/web/tests/message-actions.e2e.ts index 68feb8c0d5..5036683851 100644 --- a/apps/web/tests/message-actions.e2e.ts +++ b/apps/web/tests/message-actions.e2e.ts @@ -233,6 +233,10 @@ describe('web e2e: message IconActions and clocks on settled history', () => { () => page.locator('[role="treeitem"][aria-selected="true"]').count(), { timeout: 10_000 }, ).toBe(1) + await expect.poll( + () => page.locator('[role="treeitem"][aria-selected="true"]').textContent(), + { timeout: 10_000 }, + ).toContain('Use the read tool twice (1)') // The row action owns a distinct ui-workspace injection from the message // action above, so exercise both through the loaded app before capture. const sourceRow = page.locator('[role="treeitem"][aria-selected="true"]') diff --git a/apps/web/tests/queue-actions.e2e.ts b/apps/web/tests/queue-actions.e2e.ts index bec9243e05..c4d5977248 100644 --- a/apps/web/tests/queue-actions.e2e.ts +++ b/apps/web/tests/queue-actions.e2e.ts @@ -65,6 +65,16 @@ describe('web e2e: queue row actions', () => { if (failures.length > 1) throw new AggregateError(failures, 'queue-actions teardown failed') }) + /** Wait for the exact queue mutation response before observing its unlocked actions. */ + async function settleQueueAction(action: () => Promise, remainingText: string): Promise { + const response = page.waitForResponse('**/api/session/updateQueue') + await action() + expect((await response).ok()).toBe(true) + const row = page.locator('[data-queue-dock] li', { hasText: remainingText }) + await expect.poll(() => row.getByRole('button', { name: 'Edit queued message' }).isEnabled()).toBe(true) + await expect.poll(() => row.getByRole('button', { name: 'Remove queued message' }).isEnabled()).toBe(true) + } + it.skipIf(MODE === 'record')('edits and removes exact occurrences and preserves Queue across stop', async () => { overrideDir = await mkdtemp(join(tmpdir(), 'dsh-web-queue-actions-')) const readyFile = join(overrideDir, '.hang-ready') @@ -96,6 +106,7 @@ describe('web e2e: queue row actions', () => { await input.press('Enter') await expect.poll(() => existsSync(readyFile), { timeout: 15_000 }).toBe(true) + const admitted = page.waitForResponse('**/api/session/prompt') const received = Promise.withResolvers() const release = Promise.withResolvers() await page.route('**/api/session/prompt', async (route) => { @@ -125,6 +136,7 @@ describe('web e2e: queue row actions', () => { } finally { release.resolve(undefined) } + expect((await admitted).ok()).toBe(true) await expect.poll(() => page.getByRole('button', { name: 'Remove queued message' }).isEnabled()).toBe(true) expect(await page.locator('[data-queue-dock] [data-submission-echo]').count()).toBe(0) expect(await page.locator('[data-queue-dock]').getByRole('status').count()).toBe(0) @@ -174,11 +186,11 @@ describe('web e2e: queue row actions', () => { await editor.fill(EDITED) const editingSnapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd) await compareOrRefreshGolden(EDITING_EXPECTED, editingSnapshot, MODE) - await page.getByRole('button', { name: 'Save queued message' }).click() + await settleQueueAction(() => page.getByRole('button', { name: 'Save queued message' }).click(), EDITED) await page.getByText(EDITED, { exact: true }).waitFor() const removeRow = page.locator('[data-queue-dock] li', { hasText: REMOVE }) - await removeRow.getByRole('button', { name: 'Remove queued message' }).click() + await settleQueueAction(() => removeRow.getByRole('button', { name: 'Remove queued message' }).click(), EDITED) await expect.poll(() => page.getByText(REMOVE, { exact: true }).count()).toBe(0) const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd) diff --git a/apps/web/tests/workspace-management.e2e.ts b/apps/web/tests/workspace-management.e2e.ts index f41af11079..c3df97ed86 100644 --- a/apps/web/tests/workspace-management.e2e.ts +++ b/apps/web/tests/workspace-management.e2e.ts @@ -57,7 +57,10 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff await dialog.getByRole('button', { name: 'Edit path' }).click() const pathInput = dialog.locator('input[aria-label="Edit path"]') await pathInput.fill(path) - await pathInput.press('Enter') + // Enter's keydown can retire the editor before keyup; target the focused keyboard, not that retiring node. + await page.keyboard.press('Enter') + await pathInput.waitFor({ state: 'detached', timeout: 10_000 }) + await dialog.getByRole('button', { name: 'Edit path', exact: true }).waitFor() return dialog }