From 81748e5709b7402cec0a9f420bc24b337fbdd3e8 Mon Sep 17 00:00:00 2001 From: Neil Date: Mon, 28 Sep 2026 12:26:29 -0700 Subject: [PATCH 1/3] test(claude): expect typed cancellation in queued-send settlements --- src/main/claude/claude-structured-control-actions.test.ts | 3 ++- src/main/claude/claude-structured-queued-stop.test.ts | 3 ++- src/main/claude/claude-structured-real-cli.test.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/claude/claude-structured-control-actions.test.ts b/src/main/claude/claude-structured-control-actions.test.ts index 5c5db2f01121..3eaf496f75ad 100644 --- a/src/main/claude/claude-structured-control-actions.test.ts +++ b/src/main/claude/claude-structured-control-actions.test.ts @@ -91,7 +91,8 @@ describe('cancelClaudeTurn', () => { { clientMessageId: 'client-1', state: 'rejected', - reason: 'provider_cancelled_before_start' + reason: 'provider_cancelled_before_start', + rejection: { kind: 'cancelled' } } ] ]) diff --git a/src/main/claude/claude-structured-queued-stop.test.ts b/src/main/claude/claude-structured-queued-stop.test.ts index 0c4b6ed6e728..2dbc99bb82be 100644 --- a/src/main/claude/claude-structured-queued-stop.test.ts +++ b/src/main/claude/claude-structured-queued-stop.test.ts @@ -136,7 +136,8 @@ const WITHDRAWN = [ sessionId: 'session-1', clientMessageId: 'client-b', state: 'rejected', - reason: DISPATCH_REJECTED_CANCELLED + reason: DISPATCH_REJECTED_CANCELLED, + rejection: { kind: 'cancelled' } } ] diff --git a/src/main/claude/claude-structured-real-cli.test.ts b/src/main/claude/claude-structured-real-cli.test.ts index 7fcd9a51de20..cfd4c8c33eab 100644 --- a/src/main/claude/claude-structured-real-cli.test.ts +++ b/src/main/claude/claude-structured-real-cli.test.ts @@ -375,7 +375,8 @@ describe.skipIf(!realClaudeAvailable)('Claude structured real CLI handshake', () sessionId: 'real-cli-handshake', clientMessageId: 'real-cli-queued-stop-b', state: 'rejected', - reason: 'provider_cancelled_before_start' + reason: 'provider_cancelled_before_start', + rejection: { kind: 'cancelled' } }) } finally { await adapter.closeAll() From b65a18df48507be6ffef956aadc3c2adc91b1e4f Mon Sep 17 00:00:00 2001 From: Neil Date: Mon, 28 Sep 2026 12:32:02 -0700 Subject: [PATCH 2/3] fix(ci): respect disabled terminal links and await browser recovery --- .../terminal-link-action-request.ts | 2 +- .../terminal-link-action-routing.test.ts | 20 ++++++++++ ...nt-hosted-browser-restart-survival.spec.ts | 37 ++++++++++--------- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/terminal-link-action-request.ts b/src/renderer/src/components/terminal-pane/terminal-link-action-request.ts index 1ddedfe9e5a1..21f917302ca4 100644 --- a/src/renderer/src/components/terminal-pane/terminal-link-action-request.ts +++ b/src/renderer/src/components/terminal-pane/terminal-link-action-request.ts @@ -50,7 +50,7 @@ export function requestTerminalLinkAction( !event || !context || !( - isTerminalLinkActionActivation(event) || + (isTerminalLinkActionActivation(event) && context.plainClickBehavior !== 'none') || (isTerminalMiddleClickActivation(event) && context.middleClickBehavior !== 'none') ) || !context.pointerGesture.canRequestAction(event) diff --git a/src/renderer/src/components/terminal-pane/terminal-link-action-routing.test.ts b/src/renderer/src/components/terminal-pane/terminal-link-action-routing.test.ts index ca1168eb7b68..be546428c907 100644 --- a/src/renderer/src/components/terminal-pane/terminal-link-action-routing.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-link-action-routing.test.ts @@ -122,6 +122,26 @@ describe('terminal link action routing', () => { expect(run).toHaveBeenCalledOnce() }) + it('leaves disabled plain clicks with the terminal while middle click remains enabled', () => { + const context = actionContext() + context.plainClickBehavior = 'none' + context.middleClickBehavior = 'open' + const event = plainEvent() + const run = vi.fn() + + expect( + requestTerminalLinkAction(event, context, { + destination: 'https://example.com', + kind: 'url', + primary: { label: 'Open', run } + }) + ).toBe(false) + expect(context.claimPtyMouse).not.toHaveBeenCalled() + expect(event.preventDefault).not.toHaveBeenCalled() + expect(context.request).not.toHaveBeenCalled() + expect(run).not.toHaveBeenCalled() + }) + it('keeps middle click available when plain clicks stay with the terminal', () => { const run = vi.fn() const context = actionContext() diff --git a/tests/e2e/paired-client-hosted-browser-restart-survival.spec.ts b/tests/e2e/paired-client-hosted-browser-restart-survival.spec.ts index 03eb0e0b18d6..0f8e24ecd6e9 100644 --- a/tests/e2e/paired-client-hosted-browser-restart-survival.spec.ts +++ b/tests/e2e/paired-client-hosted-browser-restart-survival.spec.ts @@ -112,24 +112,22 @@ test('keeps a client-hosted browser tab across a paired runtime restart', async }) .toContain(opened.remotePageId) - await expect - .poll(() => findMirroredBrowserPage(client!.page, restartedWorktreeId, fixture.origin), { - timeout: 180_000, - message: 'the client lost its client-hosted row across the runtime restart' - }) - .not.toBeNull() - - // Counted across the whole fixture origin: a recovery that also replays the create URL leaves - // two rows, and matching only the moved one would call that a pass. - const rows = await readClientBrowserRows(client.page, restartedWorktreeId) - const survivorRows = rows.filter((row) => row.url.startsWith(fixture.origin)) - expect(survivorRows, 'the tab must survive the restart exactly once').toHaveLength(1) + let survivor: Awaited> = null + // Reconnection can replace the retained row between reads; wait for the whole contract. + await expect(async () => { + const rows = await readClientBrowserRows(client!.page, restartedWorktreeId) + const survivorRows = rows.filter((row) => row.url.startsWith(fixture.origin)) + expect(survivorRows, 'the tab must survive the restart exactly once').toHaveLength(1) - const survivor = await findMirroredBrowserPage(client.page, restartedWorktreeId, fixture.origin) - expect(survivor?.remotePageId, 'recovery must keep the page identity it was created with').toBe( - opened.remotePageId - ) - expect(survivor?.placementKind, 'the surviving tab must still be client-hosted').toBe('client') + survivor = await findMirroredBrowserPage(client!.page, restartedWorktreeId, fixture.origin) + expect( + survivor?.remotePageId, + 'recovery must keep the page identity it was created with' + ).toBe(opened.remotePageId) + expect(survivor?.placementKind, 'the surviving tab must still be client-hosted').toBe( + 'client' + ) + }).toPass({ timeout: 180_000 }) await focusClientBrowserRow(client.page, restartedWorktreeId, survivor!.localPageId) expect( @@ -140,6 +138,11 @@ test('keeps a client-hosted browser tab across a paired runtime restart', async ), 'the tab must come back where the user left it, not on its create URL' ).toBe('moved-on') + const renderedRows = await readClientBrowserRows(client.page, restartedWorktreeId) + expect( + renderedRows.filter((row) => row.url.startsWith(fixture.origin)), + 'rendering the recovered guest must leave exactly one surviving tab' + ).toHaveLength(1) } finally { if (client) { await cleanupE2EDaemons(client.userDataDir).catch(() => undefined) From 167d3e5e7e1d1e8e91553a4d386d9e96662151f2 Mon Sep 17 00:00:00 2001 From: Neil Date: Mon, 28 Sep 2026 12:37:58 -0700 Subject: [PATCH 3/3] test(e2e): give legacy close client a profile authority --- .../daemon-generation-legacy-close-client.ts | 7 ++++++- .../fixtures/daemon-generation-profile-store.ts | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/fixtures/daemon-generation-profile-store.ts diff --git a/tests/e2e/fixtures/daemon-generation-legacy-close-client.ts b/tests/e2e/fixtures/daemon-generation-legacy-close-client.ts index 730ebbeffc9a..473071c030b5 100644 --- a/tests/e2e/fixtures/daemon-generation-legacy-close-client.ts +++ b/tests/e2e/fixtures/daemon-generation-legacy-close-client.ts @@ -1,11 +1,13 @@ import { readFileSync } from 'node:fs' import process from 'node:process' +import type { Store } from '../../../src/main/persistence' import { OrcaRuntimeService } from '../../../src/main/runtime/orca-runtime' import { RpcDispatcher } from '../../../src/main/runtime/rpc/dispatcher' import { SESSION_TAB_METHODS } from '../../../src/main/runtime/rpc/methods/session-tabs' import { SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY } from '../../../src/shared/protocol-version' import type { RuntimeMobileSessionTabsSnapshot } from '../../../src/shared/runtime-types' import { createDesktopDiscoveredDaemonRouter } from './daemon-generation-desktop-discovery' +import { createDaemonGenerationProfileStore } from './daemon-generation-profile-store' type FixtureSession = { protocolVersion: number @@ -126,6 +128,7 @@ async function waitForFinish(): Promise { async function main(): Promise { const config = readConfig() const { router } = await createDesktopDiscoveredDaemonRouter(config) + let store: Store | null = null try { const outputBySessionId = new Map() router.onData((event) => { @@ -149,7 +152,8 @@ async function main(): Promise { } } - const runtime = new OrcaRuntimeService() + store = createDaemonGenerationProfileStore(config.cwd) + const runtime = new OrcaRuntimeService(store) const calls: Record[] = [] const sessionByTabId = new Map(config.sessions.map((session) => [session.tabId, session])) runtime.setPtyController({ @@ -296,6 +300,7 @@ async function main(): Promise { } finally { await router.disconnectOnly().catch(() => {}) router.dispose() + await store?.freezeWritesAsync() } } diff --git a/tests/e2e/fixtures/daemon-generation-profile-store.ts b/tests/e2e/fixtures/daemon-generation-profile-store.ts new file mode 100644 index 000000000000..a709d84bcc8f --- /dev/null +++ b/tests/e2e/fixtures/daemon-generation-profile-store.ts @@ -0,0 +1,14 @@ +import path from 'node:path' +import { installOrcadHostAdapters } from '../../../src/main/orcad/orcad-entry' +import type { Store } from '../../../src/main/persistence' +import { createProfileStateStore } from '../../../src/main/persistence/profile-state/profile-state-store-factory' + +export function createDaemonGenerationProfileStore(directory: string): Store { + installOrcadHostAdapters() + // The bundled fixture has no profile writer worker; use the synchronous SQLite authority. + return createProfileStateStore({ + dataFile: path.join(directory, 'orca-data.json'), + databaseFile: path.join(directory, 'profile-state.sqlite'), + profileId: 'legacy-close-fixture' + }).store +}