test: remove 468 low-value tests (#39614)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-07 16:13:17 -08:00
committed by GitHub
co-authored by OrcaWin
parent ef55da8770
commit 810080fe86
98 changed files with 166 additions and 11347 deletions
+5 -7
View File
@@ -268,7 +268,6 @@
1 test-no-real-time-wait:src/main/runtime/terminal/wait/launched-agent-crash-guard.live-shell.test.ts
1 test-no-real-time-wait:src/main/runtime/terminal/wait/launched-agent-shell-handoff-readiness.test.ts
2 test-no-real-time-wait:src/main/runtime/terminal/wait/tui-idle-name-only-real-pty.integration.test.ts
1 test-no-real-time-wait:src/main/runtime/workspaces/removal/structured-session-worktree-teardown.test.ts
4 test-no-real-time-wait:src/main/runtime/workspaces/removal/worktree-teardown-unstopped-pty.test.ts
3 test-no-real-time-wait:src/main/skills/skill-bundle-process-termination.integration.test.ts
1 test-no-real-time-wait:src/main/skills/skill-cloud-direct-upload.test.ts
@@ -654,16 +653,15 @@
1 test-no-wall-clock:src/main/runtime/structured-child-records-switch.test.ts
1 test-no-wall-clock:src/main/runtime/structured-codex-child-work-runtime.test.ts
1 test-no-wall-clock:src/main/runtime/terminal-pane-recovery-liveness-gate.test.ts
46 test-no-wall-clock:src/main/runtime/terminal-query-responder.test.ts
24 test-no-wall-clock:src/main/runtime/terminal-query-responder.test.ts
1 test-no-wall-clock:src/main/runtime/terminal-subscriber-driven-daemon-attach.test.ts
2 test-no-wall-clock:src/main/runtime/terminal/wait/tui-idle-claude-task-wakeup.test.ts
16 test-no-wall-clock:src/main/runtime/terminal/wait/tui-idle-hook-lane-runtime.test.ts
4 test-no-wall-clock:src/main/runtime/terminal/wait/tui-idle-name-only-real-pty.integration.test.ts
4 test-no-wall-clock:src/main/runtime/worker-terminal-archive-release.test.ts
9 test-no-wall-clock:src/main/runtime/worker-terminal-archive-screen.test.ts
7 test-no-wall-clock:src/main/runtime/worker-terminal-wrap-provenance.test.ts
3 test-no-wall-clock:src/main/runtime/worker-terminal-archive-release.test.ts
4 test-no-wall-clock:src/main/runtime/worker-terminal-archive-screen.test.ts
5 test-no-wall-clock:src/main/runtime/worker-terminal-wrap-provenance.test.ts
2 test-no-wall-clock:src/main/runtime/workspace-layout-live-titles.test.ts
3 test-no-wall-clock:src/main/runtime/workspaces/process-listing/runtime-worktree-structured-agent-rows-liveness.test.ts
1 test-no-wall-clock:src/main/runtime/workspaces/process-listing/runtime-worktree-structured-agent-rows-liveness.test.ts
6 test-no-wall-clock:src/main/skills/skill-bundle-process-termination.integration.test.ts
7 test-no-wall-clock:src/main/skills/skill-install-lock.test.ts
1 test-no-wall-clock:src/main/skills/skill-install-recovery.test.ts
File diff suppressed because one or more lines are too long
@@ -3408,7 +3408,6 @@
"src/main/runtime/headless-move-leaf-to-new-tab.test.ts": 3596,
"src/main/runtime/terminal/output/headless-terminal-dispose-write-ordering.test.ts": 4309,
"src/main/runtime/terminal/output/headless-terminal-memory-bound.test.ts": 5615,
"src/main/runtime/terminal/layout/headless-terminal-split-layout.test.ts": 55,
"src/main/runtime/held-pane-reopen-layout-owner.test.ts": 5093,
"src/main/runtime/hermes-slash-readiness-transcript.test.ts": 4931,
"src/main/runtime/hook-status-resume-sleep-ordering.test.ts": 9614,
@@ -3462,8 +3461,6 @@
"src/main/runtime/mobile-session-editor-projection.test.ts": 57,
"src/main/runtime/session-lifecycle/publication/mobile-session-tabs-agent-status-heartbeat.test.ts": 65,
"src/main/runtime/terminal/surface/mobile-session-terminal-persistence-retirement.test.ts": 278,
"src/main/runtime/terminal/surface/mobile-session-terminal-retirement-proof.test.ts": 48,
"src/main/runtime/terminal/surface/mobile-session-terminal-retirement.test.ts": 236,
"src/main/runtime/mobile-session-window-tab-order.test.ts": 4194,
"src/main/runtime/mobile-store-orphaned-temp-cleanup.test.ts": 320,
"src/main/runtime/mobile-subscribe-idle-pane-hydrate.test.ts": 4782,
@@ -3482,7 +3479,6 @@
"src/main/runtime/orca-runtime-codex-idle-title-chat-address.test.ts": 5781,
"src/main/runtime/orca-runtime-cold-restore-terminal-membership-layout-owner.test.ts": 4130,
"src/main/runtime/orca-runtime-cold-restore-terminal-membership.test.ts": 3716,
"src/main/runtime/workspaces/creation/orca-runtime-create-base-prefetch.test.ts": 4048,
"src/main/runtime/orca-runtime-exact-worker-provider-session.test.ts": 3617,
"src/main/runtime/orca-runtime-exited-split-pane-close.test.ts": 4235,
"src/main/runtime/orca-runtime-failed-start-pane-layout-owner.test.ts": 4204,
@@ -3516,12 +3512,10 @@
"src/main/runtime/terminal/identity/orca-runtime-terminal-handle-incarnation.test.ts": 4449,
"src/main/runtime/terminal/title/orca-runtime-terminal-rename-retention.test.ts": 5757,
"src/main/runtime/terminal/surface/orca-runtime-terminal-retirement.test.ts": 3714,
"src/main/runtime/terminal/title/orca-runtime-terminal-saved-name-respawn.test.ts": 4349,
"src/main/runtime/terminal/layout/orca-runtime-terminal-split-authority.test.ts": 4420,
"src/main/runtime/terminal/surface/orca-runtime-terminal-surface-close.test.ts": 5648,
"src/main/runtime/orca-runtime-windowless-terminal-activity.test.ts": 3277,
"src/main/runtime/orca-runtime-workspace-ports-host.test.ts": 3642,
"src/main/runtime/workspaces/catalog/orca-runtime-worktree-change-invalidation.test.ts": 4110,
"src/main/runtime/workspaces/creation/orca-runtime-worktree-provisioning-availability.test.ts": 4411,
"src/main/runtime/orca-runtime.test.ts": 43162,
"src/main/runtime/orchestration-cli-restart-handoff.test.ts": 20192,
@@ -3968,11 +3962,9 @@
"src/main/runtime/browser/client-pages/runtime-browser-page-registry.test.ts": 52,
"src/main/runtime/client/settings/runtime-client-agent-launch-rollback.test.ts": 1577,
"src/main/runtime/client/settings/runtime-client-agent-launch-settings-durability.test.ts": 3333,
"src/main/runtime/terminal/launch/runtime-created-worktree-terminal-target.test.ts": 5528,
"src/main/runtime/runtime-desktop-pane-title-forwarding.test.ts": 6114,
"src/main/runtime/workspaces/removal/runtime-failed-local-worktree-removal.test.ts": 1333,
"src/main/runtime/runtime-file-target-execution-host.test.ts": 4187,
"src/main/runtime/workspaces/removal/runtime-folder-workspace-delete-lifecycle.test.ts": 4190,
"src/main/runtime/workspaces/startup/runtime-folder-workspace-wsl-failed-start-pane.test.ts": 4472,
"src/main/runtime/workspaces/creation/runtime-folder-workspace-wsl-launch-scope.test.ts": 3705,
"src/main/runtime/integrations/git/runtime-git-generation-admission.test.ts": 595,
@@ -3987,7 +3979,6 @@
"src/main/runtime/integrations/linear/runtime-linear-comment-edit.test.ts": 458,
"src/main/runtime/integrations/linear/runtime-linear-write-lifetime.test.ts": 512,
"src/main/runtime/workspaces/creation/runtime-local-worktree-create-pr-conflict.test.ts": 462,
"src/main/runtime/workspaces/creation/runtime-local-worktree-create-pr-conflicts.test.ts": 490,
"src/main/runtime/workspaces/creation/runtime-local-worktree-create.test.ts": 693,
"src/main/runtime/workspaces/startup/runtime-local-worktree-terminal-startup-layout-owner.test.ts": 76,
"src/main/runtime/workspaces/startup/runtime-local-worktree-terminal-startup.test.ts": 79,
@@ -4011,7 +4002,6 @@
"src/main/runtime/repositories/runtime-repository-clone-host-dedupe.test.ts": 874,
"src/main/runtime/repositories/runtime-repository-issue-command.test.ts": 557,
"src/main/runtime/repositories/runtime-repository-registration-controller.test.ts": 742,
"src/main/runtime/workspaces/catalog/runtime-resolved-worktree-cache.test.ts": 46,
"src/main/runtime/rpc/server/runtime-rpc-browser-host-admission.test.ts": 4402,
"src/main/runtime/rpc/server/runtime-rpc-delegated-device-sync.test.ts": 4986,
"src/main/runtime/rpc/server/runtime-rpc-device-key-dispatch.test.ts": 3597,
@@ -4037,14 +4027,10 @@
"src/main/runtime/terminal/identity/runtime-terminal-handle-contracts.test.ts": 3983,
"src/main/runtime/terminal/wait/runtime-terminal-idle-polls.test.ts": 240,
"src/main/runtime/terminal/query/runtime-terminal-inventory-contracts.test.ts": 4427,
"src/main/runtime/terminal/adoption/runtime-terminal-orphan-adoption-duplicate-pty.test.ts": 422,
"src/main/runtime/terminal/adoption/runtime-terminal-orphan-adoption-rollback.test.ts": 1832,
"src/main/runtime/terminal/adoption/runtime-terminal-orphan-session-adoption.test.ts": 309,
"src/main/runtime/terminal/adoption/runtime-terminal-orphan-topology-validation.test.ts": 59,
"src/main/runtime/terminal/surface/runtime-terminal-retirement-contracts.test.ts": 4505,
"src/main/runtime/terminal/title/runtime-terminal-set-pane-title.test.ts": 5216,
"src/main/runtime/terminal/input/runtime-terminal-writer.test.ts": 150,
"src/main/runtime/workspaces/catalog/runtime-visible-catalog.test.ts": 56,
"src/main/runtime/runtime-workspace-launch-contracts.test.ts": 4059,
"src/main/runtime/workspaces/session-hosts/runtime-workspace-session-controller-twin-repo.test.ts": 298,
"src/main/runtime/workspaces/process-listing/runtime-worktree-agent-rows-verdict.test.ts": 1458,
@@ -4052,9 +4038,6 @@
"src/main/runtime/workspaces/creation/runtime-worktree-create-git.test.ts": 600,
"src/main/runtime/workspaces/creation/runtime-worktree-create-repo-host.test.ts": 4484,
"src/main/runtime/workspaces/lineage/runtime-worktree-lineage-controller.test.ts": 1027,
"src/main/runtime/workspaces/process-listing/runtime-worktree-ps-summaries.test.ts": 191,
"src/main/runtime/workspaces/process-listing/runtime-worktree-ps-unverifiable-terminals.test.ts": 45,
"src/main/runtime/workspaces/catalog/runtime-worktree-selection.test.ts": 52,
"src/main/runtime/workspaces/startup/runtime-worktree-startup-readiness.test.ts": 289,
"src/main/runtime/workspaces/process-listing/runtime-worktree-structured-agent-rows-liveness.test.ts": 1412,
"src/main/runtime/workspaces/startup/runtime-worktree-terminal-provisioning.test.ts": 84,
@@ -4133,14 +4116,12 @@
"src/main/runtime/terminal-agent-interrupt.test.ts": 54,
"src/main/runtime/terminal-agent-phone-push.test.ts": 172,
"src/main/runtime/terminal-agent-prompt-answer.test.ts": 100,
"src/main/runtime/terminal-chat-owner-persistence.test.ts": 4139,
"src/main/runtime/terminal-close-observed-exit.test.ts": 5914,
"src/main/runtime/terminal-close-slow-agent-exit.test.ts": 15751,
"src/main/runtime/terminal-create-closed-tab-durability.test.ts": 5767,
"src/main/runtime/terminal-focus-navigation-coalescer.test.ts": 55,
"src/main/runtime/terminal-foreground-capture.test.ts": 296,
"src/main/runtime/terminal-identity-probe.test.ts": 46,
"src/main/runtime/terminal/launch/terminal-image-viewer-env.test.ts": 163,
"src/main/runtime/terminal-intentional-stop-exit.test.ts": 9894,
"src/main/runtime/terminal-intentional-stops.test.ts": 48,
"src/main/runtime/terminal-interactive-wait-visibility.test.ts": 4552,
@@ -4148,12 +4129,10 @@
"src/main/runtime/terminal-kitty-file-wsl-root.test.ts": 118,
"src/main/runtime/terminal-list-payload-size.test.ts": 4185,
"src/main/runtime/terminal-list-pending-pty-registration.test.ts": 4780,
"src/main/runtime/terminal/adoption/terminal-orphan-topology.test.ts": 46,
"src/main/runtime/terminal-pane-recovery-liveness-gate.test.ts": 4411,
"src/main/runtime/terminal-query-responder.test.ts": 27968,
"src/main/runtime/terminal-restore-record-seed.test.ts": 4073,
"src/main/runtime/terminal-retirement-async-durability.test.ts": 5903,
"src/main/runtime/terminal-retirement-proof-emitted-frame.test.ts": 3961,
"src/main/runtime/terminal-retirement-proof-publication.test.ts": 4976,
"src/main/runtime/terminal-run-facts-input.test.ts": 4370,
"src/main/runtime/terminal-run-facts.test.ts": 68,
@@ -4162,9 +4141,7 @@
"src/main/runtime/terminal-start-failure-readiness.test.ts": 3416,
"src/main/runtime/terminal-subscribe-exit-waiter-leak.test.ts": 4118,
"src/main/runtime/terminal-subscriber-driven-daemon-attach.test.ts": 5399,
"src/main/runtime/terminal/surface/terminal-surface-close.test.ts": 120,
"src/main/runtime/terminal/output/tail/terminal-tail-crlf.test.ts": 4235,
"src/main/runtime/terminal/output/tail/terminal-tail-redraw-scaling.test.ts": 352,
"src/main/runtime/terminal/output/tail/terminal-tail-redraw-spans.test.ts": 166,
"src/main/runtime/terminal-tail-redraw-wall-time.test.ts": 43418,
"src/main/runtime/terminal/output/tail/terminal-tail-sentinel-index.test.ts": 3649,
@@ -4183,7 +4160,6 @@
"src/main/runtime/terminal/launch/mobile-terminal-chat-fork.test.ts": 3900,
"src/main/runtime/terminal/launch/terminal-split-layout-owner.test.ts": 4503,
"src/main/runtime/terminal/launch/terminal-split-retry.test.ts": 4038,
"src/main/runtime/terminal/launch/window-tab-start-switch-off.test.ts": 4475,
"src/main/runtime/terminal/layout/agent-generated-tab-titles.test.ts": 5572,
"src/main/runtime/terminal/lifecycle/main-pane-liveness.test.ts": 4871,
"src/main/runtime/terminal/lifecycle/ssh-exit-code-pane-outcome.test.ts": 4334,
@@ -4193,17 +4169,14 @@
"src/main/runtime/terminal/stop/terminal-update-admission.test.ts": 48,
"src/main/runtime/terminal/surface/closed-pane-exit-cleanup-main.test.ts": 4281,
"src/main/runtime/terminal/surface/main-terminal-acquisition.test.ts": 4330,
"src/main/runtime/terminal/surface/pty-pane-copies-untouched-main.test.ts": 4585,
"src/main/runtime/terminal/surface/terminal-surface-replies-main.test.ts": 4324,
"src/main/runtime/terminal/surface/terminal-surface-replies-window.test.ts": 4440,
"src/main/runtime/trust-dialog-status-observer.test.ts": 4458,
"src/main/runtime/terminal/wait/tui-idle-claude-task-wakeup.test.ts": 185,
"src/main/runtime/terminal/wait/tui-idle-codex-plan-menu.test.ts": 4441,
"src/main/runtime/terminal/wait/tui-idle-command-paint.test.ts": 4148,
"src/main/runtime/terminal/wait/tui-idle-hook-lane-runtime.test.ts": 7702,
"src/main/runtime/terminal/wait/tui-idle-name-only-real-pty.integration.test.ts": 39042,
"src/main/runtime/unreadable-secret-store-preservation.win32.test.ts": 51,
"src/main/runtime/wait-blocked-check-state.test.ts": 107,
"src/main/runtime/windowless-main-layout-commands-layout-owner.test.ts": 4237,
"src/main/runtime/windows-codex-held-prompt-resubmit.test.ts": 4446,
"src/main/runtime/windows-default-route-interfaces.test.ts": 45,
@@ -4216,15 +4189,12 @@
"src/main/runtime/workspace-layout-operation-frames.test.ts": 2136,
"src/main/runtime/workspace-layout-projection.test.ts": 99,
"src/main/runtime/workspace-layout-stream.test.ts": 427,
"src/main/runtime/workspace-notification-mute.test.ts": 99,
"src/main/runtime/workspace-session-membership-scaling.test.ts": 72,
"src/main/runtime/terminal/identity/workspace-session-worktree-id.test.ts": 211,
"src/main/runtime/workspaces/persisted-surfaces/live-terminal-placement-before-initial-tab.test.ts": 6111,
"src/main/runtime/workspaces/persisted-surfaces/sweep-terminal-placement.test.ts": 12352,
"src/main/runtime/workspaces/sleep/remote-workspace-terminal-sleep.test.ts": 46,
"src/main/runtime/workspaces/workspace-host-notifications.test.ts": 4332,
"src/main/runtime/workspaces/lifecycle/workspace-lifecycle-queue.test.ts": 116,
"src/main/runtime/workspaces/catalog/workspace-query-prototype.test.ts": 4444,
"src/main/runtime/workspaces/sleep/workspace-sleep-execution.test.ts": 110,
"src/main/runtime/workspaces/startup/worktree-agent-first-surface.test.ts": 5816,
"src/main/runtime/workspaces/catalog/worktree-list-host-scope.test.ts": 1051,
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { HostStatusReply } from '../transport/host-status-reply-schema'
import { bridgeId, clientFrame, flushBridge } from './bridge-host-test-fakes'
import { bridgeId, clientFrame } from './bridge-host-test-fakes'
import { harness, subscribeFrame } from './bridge-host-test-harness'
const PROVEN: HostStatusReply = { protocolVersion: 10, minCompatibleMobileVersion: 1 }
@@ -13,66 +13,6 @@ describe('the connection verdict hold', () => {
return { bridge, verdict }
}
it('reports handshaking and keeps requests and streams off the desktop until the verdict', async () => {
const { bridge } = heldBridge()
const init = bridge.last()
expect(init).toMatchObject({ type: 'init', connection: { state: 'handshaking' } })
bridge.host.receive(
clientFrame({ type: 'request', id: bridgeId(1), method: 'worktree.ps', params: {} })
)
bridge.host.receive(subscribeFrame(bridgeId(2)))
// The PDF viewer downloads the file from the desktop, so it waits too.
bridge.host.receive(
clientFrame({ type: 'request', id: bridgeId(3), method: 'native.pdf.view', params: {} })
)
// status.get is how the page learns the verdict, so it alone goes through.
bridge.host.receive(clientFrame({ type: 'request', id: bridgeId(4), method: 'status.get' }))
bridge.client.pushState('connected')
await flushBridge()
expect(bridge.client.requests.map((request) => request.method)).toEqual(['status.get'])
expect(bridge.client.streams).toEqual([])
expect(bridge.nativeCalls).toEqual([])
expect(bridge.last()).toMatchObject({ type: 'state', connection: { state: 'handshaking' } })
})
it("serves the phone's own store before the verdict, so drafts typed offline are saved", async () => {
const { bridge } = heldBridge()
bridge.host.receive(
clientFrame({
type: 'request',
id: bridgeId(1),
method: 'native.store.rows',
params: { action: 'read', table: 'draft', keys: ['draft:chat:wt:'] }
})
)
await flushBridge()
expect(bridge.nativeCalls.map((call) => call.verb)).toEqual(['native.store.rows'])
})
it('re-sends init as connected once the verdict opens, then sends what waited', () => {
const { bridge, verdict } = heldBridge()
bridge.host.receive(
clientFrame({ type: 'request', id: bridgeId(1), method: 'worktree.ps', params: {} })
)
bridge.host.receive(subscribeFrame(bridgeId(2)))
bridge.host.receive(
clientFrame({ type: 'request', id: bridgeId(3), method: 'stats.summary', params: {} })
)
bridge.host.receive(clientFrame({ type: 'cancel', id: bridgeId(3), target: 'request' }))
verdict.status = PROVEN
bridge.host.publishHostStatus()
expect(
bridge.frames().find((frame) => frame.type === 'init' && frame.hostStatus)
).toMatchObject({ connection: { state: 'connected' }, hostStatus: PROVEN })
expect(bridge.client.requests.map((request) => request.method)).toEqual(['worktree.ps'])
expect(bridge.client.streams.map((stream) => stream.method)).toEqual(['terminal.subscribe'])
})
it('keeps page order for a frame that lands after the status opens but before the flush', () => {
const { bridge, verdict } = heldBridge()
bridge.host.receive(
@@ -1,75 +0,0 @@
import { onTestFinished, describe, expect, it, vi } from 'vitest'
import { scopeRpcClientToExecutionHost } from '../../transport/execution-host-scoped-rpc-client'
import type { RpcClient } from '../../transport/rpc-client'
import type { RpcResponse } from '../../transport/types'
import { createBridgePortPair } from './bridge-port-pair-test-harness'
import { createPageClient, INIT } from './bridge-page-client-test-harness'
const SERVER = 'runtime:env-1' as const
function recordingShellClient() {
const sendRequest = vi.fn((..._args: unknown[]) => new Promise<RpcResponse>(() => {}))
const subscribe = vi.fn(
(..._args: unknown[]) =>
() =>
undefined
)
const client: RpcClient = {
sendRequest,
subscribe,
updateTerminalSubscriptionViewport: () => {},
getState: () => 'connected',
getReconnectAttempt: () => 0,
getLastConnectedAt: () => 0,
onStateChange: () => () => undefined,
notifyForeground: () => {},
close: () => {}
}
return { client, sendRequest, subscribe }
}
describe('a server workspace call across the shell bridge', () => {
it('reaches the shell client with its executionHost on the envelope', async () => {
const shell = recordingShellClient()
const pair = createBridgePortPair({ rpc: shell.client })
onTestFinished(() => {
pair.client.close()
pair.host.dispose()
})
await pair.flush()
const server = scopeRpcClientToExecutionHost(pair.client, SERVER)
void server.sendRequest('worktree.ps', { limit: 1 }).catch(() => undefined)
server.subscribe('terminal.subscribe', { terminal: 't1' }, () => {})
await pair.flush()
expect(shell.sendRequest.mock.calls).toEqual([
['worktree.ps', { limit: 1 }, { envelope: { executionHost: SERVER } }]
])
expect(shell.subscribe.mock.calls).toEqual([
[
'terminal.subscribe',
{ terminal: 't1' },
expect.any(Function),
{ envelope: { executionHost: SERVER }, holdReplay: expect.any(Function) }
]
])
pair.client.close()
pair.host.dispose()
})
it('refuses it under a shell without the envelope, instead of running it on the desktop', async () => {
const page = createPageClient()
onTestFinished(() => page.client.close())
page.deliver({ ...INIT, grants: { ...INIT.grants, native: [] } })
expect(page.client.carriesRpcEnvelope?.()).toBe(false)
const server = scopeRpcClientToExecutionHost(page.client, SERVER)
await expect(server.sendRequest('worktree.ps', {})).rejects.toThrow(/update the Orca app/)
expect(() => server.subscribe('terminal.subscribe', { terminal: 't1' }, () => {})).toThrow(
/update the Orca app/
)
expect(page.frames().filter((frame) => frame.type !== 'ready')).toEqual([])
page.client.close()
})
})
@@ -1,78 +0,0 @@
import { afterEach, describe, expect, it } from 'vitest'
import { createFakeBridgePortPair, disposeBridgePortPair } from './bridge-port-pair-test-harness'
import { storeParamsSchema } from './bridge-store-verb'
import { createPageDeviceStore } from './page-device-store'
let pair: ReturnType<typeof createFakeBridgePortPair> | null = null
afterEach(async () => {
if (pair) {
await disposeBridgePortPair(pair)
}
pair = null
})
// A shell whose store answers asynchronously, like the real one, so calls overlap in flight.
async function pageOverShellStore() {
const rows = new Map<string, string>()
pair = createFakeBridgePortPair({
serveNativeVerb: async (_verb, params) => {
await new Promise<void>((resolve) => queueMicrotask(resolve))
const call = storeParamsSchema.parse(params)
if (call.action === 'read') {
return { values: call.keys.map((key) => rows.get(key) ?? null), deferred: [] }
}
for (const op of call.ops) {
if (op.value === null) {
rows.delete(op.key)
} else {
rows.set(op.key, op.value)
}
}
return { written: true }
}
})
await pair.flush()
return { store: createPageDeviceStore(pair.client), rows }
}
describe('the page device store', () => {
it('writes a call of more batches than the bridge has request slots', async () => {
const { store, rows } = await pageOverShellStore()
// 64 ops a batch: 65 batches.
const ops = Array.from({ length: 65 * 64 }, (_op, at) => ({ key: `k-${at}`, value: 'eA==' }))
expect(await store.write('cache', ops)).toEqual({ written: true })
expect(rows.size).toBe(4160)
})
it('refuses a value over the store cap before sending any batch', async () => {
const { store, rows } = await pageOverShellStore()
expect(
await store.write('cache', [
{ key: 'small', value: 'eA==' },
{ key: 'big', value: 'x'.repeat(540_000) }
])
).toEqual({ written: false, refusal: 'too-large' })
expect([...rows.keys()]).toEqual([])
})
it('reads a key the store cannot hold as a miss, and refuses to write one', async () => {
const { store } = await pageOverShellStore()
const longKey = 'k'.repeat(513)
await store.write('draft', [{ key: 'kept', value: 'eA==' }])
expect(await store.read('draft', ['kept', longKey, ''])).toEqual({
status: 'ok',
values: ['eA==', null, null]
})
expect(
await store.write('draft', [
{ key: 'kept', value: null },
{ key: longKey, value: 'eA==' }
])
).toEqual({ written: false, refusal: 'bad-key' })
expect(await store.read('draft', ['kept'])).toEqual({ status: 'ok', values: ['eA=='] })
})
})
@@ -89,50 +89,6 @@ describe('RpcClientStreamRegistry', () => {
])
})
it.each([
['a streamed end', (id: string) => streamingResponse(id, { type: 'end' })],
[
'a final end reply',
(id: string): RpcResponse => ({
id,
ok: true,
result: { type: 'end' },
_meta: { runtimeId: 'runtime-1' }
})
],
[
'an error reply',
(id: string): RpcResponse => ({
id,
ok: false,
error: { code: 'runtime_error', message: 'failed' },
_meta: { runtimeId: 'runtime-1' }
})
]
])('sends no terminal unsubscribe when the listener disposes on %s', (_label, ending) => {
const { registry, sent } = createRegistry()
const events: unknown[] = []
let dispose = (): void => {}
dispose = registry.subscribe(
'terminal.subscribe',
{ terminal: 'term-1', client: { id: 'phone', type: 'mobile' } },
(event) => {
events.push(event)
// The session hook disposes synchronously on end/error; a slot-named unsubscribe here
// would retire a newer same-slot stream on the host.
dispose()
}
)
const request = sent[0]!
registry.handleResponse(ending(request.id))
dispose()
expect(events).toHaveLength(1)
expect(sent).toEqual([request])
expect(registry.size()).toBe(0)
})
it('re-sends a stream its owner holds only when the owner resends it, and a cancel before then sends nothing', () => {
const { registry, sent } = createRegistry()
const held: (() => void)[] = []
@@ -153,47 +109,6 @@ describe('RpcClientStreamRegistry', () => {
])
})
it('does not replay a terminal stream the host already ended', () => {
const { registry, sent, setState } = createRegistry()
registry.subscribe(
'terminal.subscribe',
{ terminal: 'term-1', client: { id: 'phone' } },
() => {}
)
const request = sent[0]!
registry.handleResponse(streamingResponse(request.id, { type: 'subscribed', streamId: 7 }))
registry.handleResponse(streamingResponse(request.id, { type: 'end' }))
setState('connecting')
registry.markForReplay()
setState('connected')
registry.replayAfterAuthentication()
expect(sent).toEqual([request])
})
it('unsubscribes a session tabs stream by its own request id', () => {
const { registry, sent } = createRegistry()
const disposeOlder = registry.subscribe(
'session.tabs.subscribe',
{ worktree: 'wt-1' },
() => {}
)
registry.subscribe('session.tabs.subscribe', { worktree: 'wt-1' }, () => {})
const [older, newer] = sent
registry.handleResponse(streamingResponse(older!.id, { type: 'snapshot', tabs: [] }))
disposeOlder()
// Without the request id the host sweeps every stream for the worktree, including the newer one.
expect(sent[2]).toMatchObject({
method: 'session.tabs.unsubscribe',
params: { worktree: 'wt-1', subscriptionId: older!.id }
})
expect(newer!.id).not.toBe(older!.id)
expect(sent).toHaveLength(3)
})
it('holds a session tabs unsubscribe until the host registers the stream', () => {
const { registry, sent } = createRegistry()
const events: unknown[] = []
@@ -219,119 +134,6 @@ describe('RpcClientStreamRegistry', () => {
expect(registry.size()).toBe(0)
})
describe.each([
['browser.screencast', 'browser.screencast.unsubscribe', { page: 'page-1' }],
['runtime.clientEvents.subscribe', 'runtime.clientEvents.unsubscribe', null],
['notifications.subscribe', 'notifications.unsubscribe', null],
['accounts.subscribe', 'accounts.unsubscribe', null],
['session.tabs.subscribe', 'session.tabs.unsubscribe', { worktree: 'wt-1' }]
])('canceled %s delivery', (method, cleanup, params) => {
it('ignores late scrollback and terminal registration while waiting for host cleanup', () => {
const { registry, sent } = createRegistry()
const events: unknown[] = []
registry.subscribe(method, params, (event) => events.push(event))()
const request = sent[0]!
expect(
registry.handleResponse({
id: request.id,
ok: true,
result: { type: 'scrollback', serialized: 'late' }
})
).toBe(true)
registry.handleResponse(streamingResponse(request.id, { type: 'subscribed', streamId: 41 }))
registry.handleBinary(terminalOutput(41, 'late'))
expect(events).toEqual([])
expect(registry.size()).toBe(1)
expect(sent).toHaveLength(1)
registry.handleResponse(
streamingResponse(
request.id,
method === 'session.tabs.subscribe'
? { type: 'snapshot', tabs: [] }
: { type: 'ready', subscriptionId: 'late-host-id' }
)
)
expect(sent[1]).toMatchObject({
method: cleanup,
params:
method === 'session.tabs.subscribe'
? { worktree: 'wt-1', subscriptionId: request.id }
: { subscriptionId: 'late-host-id' }
})
expect(registry.size()).toBe(0)
expect(events).toEqual([])
})
it('does not replay a canceled opener queued before connection', () => {
const { registry, sent, setState } = createRegistry('connecting')
const events: unknown[] = []
const dispose = registry.subscribe(method, params, (event) => events.push(event))
dispose()
dispose()
registry.markForReplay()
setState('connected')
registry.replayAfterAuthentication()
expect(sent).toEqual([])
expect(events).toEqual([])
expect(registry.size()).toBe(0)
})
})
it('holds a session tabs unsubscribe again after a reconnect replays the stream', () => {
const { registry, sent } = createRegistry()
const dispose = registry.subscribe('session.tabs.subscribe', { worktree: 'wt-1' }, () => {})
const subscribe = sent[0]!
registry.handleResponse(streamingResponse(subscribe.id, { type: 'snapshot', tabs: [] }))
registry.markForReplay()
registry.replayAfterAuthentication()
dispose()
expect(sent.map((request) => request.method)).toEqual([
'session.tabs.subscribe',
'session.tabs.subscribe'
])
registry.handleResponse(streamingResponse(subscribe.id, { type: 'snapshot', tabs: [] }))
expect(sent[2]).toMatchObject({
method: 'session.tabs.unsubscribe',
params: { worktree: 'wt-1', subscriptionId: subscribe.id }
})
})
it('drops a held session tabs unsubscribe when the subscribe fails or reconnects first', () => {
const { registry, sent } = createRegistry()
const events: unknown[] = []
const disposeFailed = registry.subscribe(
'session.tabs.subscribe',
{ worktree: 'wt-1' },
(event) => events.push(event)
)
const disposeReplayed = registry.subscribe(
'session.tabs.subscribe',
{ worktree: 'wt-2' },
() => {}
)
const failed = sent[0]!
disposeFailed()
disposeReplayed()
registry.handleResponse({
id: failed.id,
ok: false,
error: { code: 'worktree_not_found', message: 'Worktree not found' }
})
registry.markForReplay()
registry.replayAfterAuthentication()
expect(sent).toHaveLength(2)
expect(events).toEqual([])
expect(registry.size()).toBe(0)
})
it('ends one transcript stream on dispose and leaves a sibling on the same socket (U-03)', () => {
const { registry, sent } = createRegistry()
const disposeFirst = registry.subscribe('agentSession.subscribe', { sessionId: 's1' }, () => {})
@@ -352,27 +154,6 @@ describe('RpcClientStreamRegistry', () => {
expect(registry.size()).toBe(1)
})
it('names the terminal request it sent when unsubscribing, and keeps the slot for older hosts', () => {
const { registry, sent } = createRegistry()
const dispose = registry.subscribe(
'terminal.subscribe',
{ terminal: 'term-1', client: { id: 'phone-1', type: 'mobile' } },
() => {}
)
const subscribe = sent[0]!
dispose()
expect(sent[1]).toMatchObject({
method: 'terminal.unsubscribe',
params: {
subscriptionId: 'term-1:phone-1',
client: { id: 'phone-1' },
requestId: subscribe.id
}
})
})
it('releases a replayed browser stream replaced by a new one before its ready', () => {
const { registry, sent } = createRegistry()
registry.subscribe('browser.screencast', { page: 'page-1' }, () => {})
@@ -399,65 +180,4 @@ describe('RpcClientStreamRegistry', () => {
).toEqual([{ subscriptionId: 'page-1-new-connection' }])
expect(registry.size()).toBe(1)
})
describe.each([
['runtime.clientEvents.subscribe', 'runtime.clientEvents.unsubscribe', null],
['notifications.subscribe', 'notifications.unsubscribe', null],
['accounts.subscribe', 'accounts.unsubscribe', null],
['browser.screencast', 'browser.screencast.unsubscribe', { page: 'page-1' }]
])('%s ready id across a replay', (method, unsubscribeMethod, params) => {
function unsubscribes(sent: SentRequest[]): unknown[] {
return sent.filter((request) => request.method === unsubscribeMethod).map((r) => r.params)
}
function subscribeReady() {
const harness = createRegistry()
const dispose = harness.registry.subscribe(method, params, () => {})
const requestId = harness.sent[0]!.id
harness.registry.handleResponse(
streamingResponse(requestId, { type: 'ready', subscriptionId: 'old-connection-id' })
)
return { ...harness, dispose, requestId }
}
it('releases the replayed registration when disposed before its new ready', () => {
const { registry, sent, dispose, requestId } = subscribeReady()
registry.markForReplay()
registry.replayAfterAuthentication()
dispose()
registry.handleResponse(
streamingResponse(requestId, { type: 'ready', subscriptionId: 'new-connection-id' })
)
expect(unsubscribes(sent)).toEqual([{ subscriptionId: 'new-connection-id' }])
})
it('forgets the previous connection id when marked for replay', () => {
const { registry, sent, dispose } = subscribeReady()
registry.markForReplay()
dispose()
// A disposal while disconnected has nothing to name on the next connection.
expect(unsubscribes(sent)).toEqual([])
expect(registry.size()).toBe(0)
})
it('sends one unsubscribe however often the stream is disposed', () => {
const { registry, sent, dispose, requestId } = subscribeReady()
registry.markForReplay()
registry.replayAfterAuthentication()
dispose()
dispose()
registry.handleResponse(
streamingResponse(requestId, { type: 'ready', subscriptionId: 'new-connection-id' })
)
dispose()
expect(unsubscribes(sent)).toEqual([{ subscriptionId: 'new-connection-id' }])
expect(registry.size()).toBe(0)
})
})
})
@@ -1,14 +1,13 @@
import { describe, expect, it, vi } from 'vitest'
import { RelayDialStageTracker, type RelayDialStage } from './relay-dial-stage'
import './relay-dial-stage'
import type { ConnectionState, RpcResponse } from './types'
import type { RpcClient } from './rpc-client'
import { isRpcDeliveryUnknown, markRpcDeliveryUnknown } from './rpc-delivery-ambiguity'
import './rpc-delivery-ambiguity'
import {
createStableLogicalRpcClient,
LogicalClientCutoverError,
type MobileConnectionPath
LogicalClientCutoverError
} from './stable-logical-rpc-client'
import { LogicalClientSuspendedError } from './logical-client-suspended-error'
import './logical-client-suspended-error'
class FakeSession implements RpcClient {
readonly sendRequest =
@@ -107,127 +106,6 @@ describe('stable logical RPC client', () => {
pending.resolve(success('late'))
})
it('attaches a subscription its owner holds at cutover only when the owner resumes it', async () => {
const oldSession = new FakeSession('connected')
const nextSession = new FakeSession('connected')
const client = createStableLogicalRpcClient(oldSession, 'lan')
const held: (() => void)[] = []
client.subscribe('terminal.subscribe', { terminal: 'term-1' }, vi.fn(), {
holdReplay: (resend) => held.push(resend) > 0
})
oldSession.subscribe.mockClear()
await client.migrateTo(nextSession, 'relay')
expect(nextSession.subscribe).not.toHaveBeenCalled()
held[0]!()
expect(nextSession.subscribe).toHaveBeenCalledOnce()
expect(oldSession.subscribe).not.toHaveBeenCalled()
})
it('keeps replies that commit before cutover and carries viewport state into replay', async () => {
const oldSession = new FakeSession('connected')
const nextSession = new FakeSession('connected')
oldSession.sendRequest.mockResolvedValue(success('old'))
const client = createStableLogicalRpcClient(oldSession, 'lan')
client.subscribe(
'terminal.subscribe',
{ terminal: 'term-1', viewport: { cols: 80, rows: 24 } },
vi.fn()
)
client.updateTerminalSubscriptionViewport('term-1', { cols: 120, rows: 40 })
await expect(client.sendRequest('status.get')).resolves.toEqual(success('old'))
await client.migrateTo(nextSession, 'relay')
expect(nextSession.subscribe).toHaveBeenCalledWith(
'terminal.subscribe',
{ terminal: 'term-1', viewport: { cols: 120, rows: 40 } },
expect.any(Function),
undefined
)
})
it('suspends one physical session and replays subscriptions on foreground replacement', async () => {
const oldSession = new FakeSession('connected')
const nextSession = new FakeSession('connected')
nextSession.sendRequest.mockResolvedValue(success('next'))
const client = createStableLogicalRpcClient(oldSession, 'relay')
client.subscribe('session.tabs.subscribe', { worktree: 'id:wt-1' }, vi.fn())
client.suspendActiveSession()
expect(oldSession.close).toHaveBeenCalledOnce()
expect(client.getState()).toBe('disconnected')
await expect(client.sendRequest('status.get')).rejects.toThrow('Client suspended')
await client.migrateTo(nextSession, 'relay')
expect(nextSession.subscribe).toHaveBeenCalledWith(
'session.tabs.subscribe',
{ worktree: 'id:wt-1' },
expect.any(Function),
undefined
)
await expect(client.sendRequest('status.get')).resolves.toEqual(success('next'))
})
it('reports requests the suspended session never sent as suspended, and written ones as ambiguous', async () => {
const session = new FakeSession('connected')
const unsent = deferred<RpcResponse>()
const written = deferred<RpcResponse>()
session.sendRequest.mockReturnValueOnce(unsent.promise).mockReturnValueOnce(written.promise)
const client = createStableLogicalRpcClient(session, 'relay')
const waiting = client.sendRequest('speech.dictation.chunk').catch((error: unknown) => error)
const inFlight = client.sendRequest('speech.dictation.chunk').catch((error: unknown) => error)
client.suspendActiveSession()
unsent.reject(new Error('relay session closed'))
written.reject(markRpcDeliveryUnknown(new Error('Client closed')))
expect(await waiting).toBeInstanceOf(LogicalClientSuspendedError)
expect(isRpcDeliveryUnknown(await inFlight)).toBe(true)
})
it('preserves delivery ambiguity without replaying a mutation after relay replacement', async () => {
const session = new FakeSession('connected')
const replacement = new FakeSession('connected')
const inFlight = deferred<RpcResponse>()
session.sendRequest.mockReturnValue(inFlight.promise)
// Mirror the real physical contract: close() rejects post-write pendings
// with a delivery-unknown-marked error.
const closeError = markRpcDeliveryUnknown(new Error('Client closed'))
session.close.mockImplementation(() => inFlight.reject(closeError))
const client = createStableLogicalRpcClient(session, 'relay')
const request = client.sendRequest('terminal.send', { terminal: 'term', text: 'hi' })
client.suspendActiveSession()
await expect(request).rejects.toBe(closeError)
await expect(request.catch((error: unknown) => isRpcDeliveryUnknown(error))).resolves.toBe(true)
await client.migrateTo(replacement, 'relay')
expect(replacement.sendRequest).not.toHaveBeenCalled()
replacement.sendRequest.mockResolvedValue(success('next'))
await expect(client.sendRequest('status.get')).resolves.toEqual(success('next'))
})
it('lets the physical close settle in-flight requests on close, keeping pre-write failures definite', async () => {
const session = new FakeSession('connected')
const inFlight = deferred<RpcResponse>()
session.sendRequest.mockReturnValue(inFlight.promise)
// A request still waiting for connect wrote no frame and must remain unmarked on logical close.
const preWriteError = new Error('Connection closed')
session.close.mockImplementation(() => inFlight.reject(preWriteError))
const client = createStableLogicalRpcClient(session, 'lan')
const request = client.sendRequest('terminal.send', { terminal: 'term', text: 'hi' })
client.close()
await expect(request).rejects.toBe(preWriteError)
await expect(request.catch((error: unknown) => isRpcDeliveryUnknown(error))).resolves.toBe(
false
)
})
it('preserves a committed response when logical close wins the callback race', async () => {
const session = new FakeSession('connected')
const inFlight = deferred<RpcResponse>()
@@ -240,204 +118,6 @@ describe('stable logical RPC client', () => {
await expect(request).resolves.toEqual(success('accepted'))
})
it('publishes the replacement dial phases while the client is suspended', async () => {
const oldSession = new FakeSession('connected')
const replacement = new FakeSession('connecting')
const client = createStableLogicalRpcClient(oldSession, 'relay')
const states: ConnectionState[] = []
client.onStateChange((next) => states.push(next))
client.suspendActiveSession()
expect(client.getPendingPath()).toBeNull()
const migrating = client.migrateTo(replacement, 'relay')
// The replacement never announces its initial connecting state; the client must still show it.
expect(client.getState()).toBe('connecting')
expect(client.getPendingPath()).toBe('relay')
replacement.setState('handshaking')
expect(client.getState()).toBe('handshaking')
replacement.setState('connected')
await migrating
expect(states).toEqual(['disconnected', 'connecting', 'handshaking', 'connected'])
expect(client.getPendingPath()).toBeNull()
expect(client.getActivePath()).toBe('relay')
})
// Forward replacement reconnecting phases so the suspended client shows progress during the dial.
it('forwards a reconnecting phase published by the dialing session itself', async () => {
const oldSession = new FakeSession('connected')
const replacement = new FakeSession('connecting')
const client = createStableLogicalRpcClient(oldSession, 'relay')
client.suspendActiveSession()
const states: ConnectionState[] = []
client.onStateChange((next) => states.push(next))
const migrating = client.migrateTo(replacement, 'relay')
replacement.setState('connecting')
replacement.setState('reconnecting')
expect(client.getState()).toBe('reconnecting')
replacement.setState('handshaking')
replacement.setState('connected')
await migrating
expect(states).toEqual(['connecting', 'reconnecting', 'handshaking', 'connected'])
})
// A direct session already reconnecting needs no phase forwarding, but must still name the pending relay path.
it('names a relay dial started from reconnecting without disturbing the state', async () => {
const direct = new FakeSession('connected')
const replacement = new FakeSession('connecting')
const client = createStableLogicalRpcClient(direct, 'lan')
direct.setState('reconnecting')
const states: ConnectionState[] = []
const paths: (MobileConnectionPath | null)[] = []
client.onStateChange((next) => states.push(next))
client.onConnectionPathChange(() => paths.push(client.getPendingPath()))
const migrating = client.migrateTo(replacement, 'relay')
expect(client.getPendingPath()).toBe('relay')
expect(paths).toEqual(['relay'])
replacement.setState('connecting')
replacement.setState('handshaking')
expect(client.getState()).toBe('reconnecting')
expect(states).toEqual([])
replacement.setState('connected')
await migrating
expect(states).toEqual(['connected'])
expect(client.getPendingPath()).toBeNull()
expect(client.getActivePath()).toBe('relay')
})
it('publishes recovery-path changes and keeps Relay pending between failed dials', async () => {
const direct = new FakeSession('reconnecting')
const replacement = new FakeSession('connecting')
const client = createStableLogicalRpcClient(direct, 'tailscale')
const paths: (MobileConnectionPath | null)[] = []
client.onConnectionPathChange(() => paths.push(client.getPendingPath()))
client.setRecoveryPath('relay')
const migrating = client.migrateTo(replacement, 'relay')
replacement.setState('disconnected')
await expect(migrating).rejects.toThrow(/disconnected/)
expect(client.getPendingPath()).toBe('relay')
expect(paths).toEqual(['relay'])
client.setRecoveryPath(null)
expect(client.getPendingPath()).toBeNull()
expect(paths).toEqual(['relay', null])
})
it('does not revive a stale recovery path after a connection later drops', () => {
const direct = new FakeSession('reconnecting')
const client = createStableLogicalRpcClient(direct, 'tailscale')
client.setRecoveryPath('relay')
direct.setState('connected')
direct.setState('reconnecting')
expect(client.getPendingPath()).toBeNull()
})
it('drops the pending path when the previous session recovers mid-dial', async () => {
const direct = new FakeSession('connected')
const replacement = new FakeSession('connecting')
const client = createStableLogicalRpcClient(direct, 'lan')
direct.setState('reconnecting')
const migrating = client.migrateTo(replacement, 'relay')
expect(client.getPendingPath()).toBe('relay')
direct.setState('connected')
expect(client.getPendingPath()).toBeNull()
replacement.setState('connected')
await migrating
})
it('never downgrades a live session while a make-before-break replacement dials', async () => {
const oldSession = new FakeSession('connected')
const replacement = new FakeSession('connecting')
const client = createStableLogicalRpcClient(oldSession, 'relay')
const states: ConnectionState[] = []
client.onStateChange((next) => states.push(next))
const migrating = client.migrateTo(replacement, 'relay')
replacement.setState('connecting')
replacement.setState('handshaking')
expect(client.getState()).toBe('connected')
expect(client.getPendingPath()).toBeNull()
replacement.setState('connected')
await migrating
expect(states).toEqual(['connected'])
})
it('restores disconnected when a dial it was narrating fails', async () => {
const session = new FakeSession('connected')
const replacement = new FakeSession('connecting')
const client = createStableLogicalRpcClient(session, 'relay')
client.suspendActiveSession()
const states: ConnectionState[] = []
client.onStateChange((next) => states.push(next))
const migrating = client.migrateTo(replacement, 'relay')
replacement.setState('handshaking')
replacement.setState('disconnected')
await expect(migrating).rejects.toThrow(/disconnected/)
expect(states).toEqual(['connecting', 'handshaking', 'disconnected'])
expect(client.getState()).toBe('disconnected')
expect(client.getPendingPath()).toBeNull()
})
it('outlives the flat bound when the relay cell holds the dial', async () => {
vi.useFakeTimers()
try {
const oldSession = new FakeSession('connected')
const replacement = Object.assign(new FakeSession('connecting'), {
dialStage: new RelayDialStageTracker(),
getDialStage(): RelayDialStage {
return this.dialStage.getDialStage()
},
onDialStageChange(listener: (stage: RelayDialStage) => void) {
return this.dialStage.onDialStageChange(listener)
}
})
const client = createStableLogicalRpcClient(oldSession, 'lan')
const migrating = client.migrateTo(replacement, 'relay', 12_000)
await vi.advanceTimersByTimeAsync(1_000)
replacement.dialStage.advance('awaiting-hello')
await vi.advanceTimersByTimeAsync(20_000)
expect(replacement.close).not.toHaveBeenCalled()
replacement.setState('connected')
await migrating
expect(client.getActivePath()).toBe('relay')
} finally {
vi.useRealTimers()
}
})
it('closes a replacement that fails authentication and preserves the active session', async () => {
const oldSession = new FakeSession('connected')
const replacement = new FakeSession('connecting')
const client = createStableLogicalRpcClient(oldSession, 'lan')
const migrating = client.migrateTo(replacement, 'relay')
replacement.setState('auth-failed')
await expect(migrating).rejects.toThrow(/auth-failed/)
expect(replacement.close).toHaveBeenCalledOnce()
expect(oldSession.close).not.toHaveBeenCalled()
expect(client.getActivePath()).toBe('lan')
expect(client.getGeneration()).toBe(1)
})
})
describe('stable logical RPC client subscription fencing', () => {
@@ -1,80 +0,0 @@
/** Natural PTY exit/disconnect must evict leader teams too; explicit close was once the only cleanup. */
import { describe, it, expect } from 'vitest'
import { OrcaRuntimeService } from '../orca-runtime'
import type { ClaudeAgentTeamsService } from '../claude-agent-teams-service'
export function registerClaudeAgentTeamsPtyExitLeakTests(): void {
type RuntimeInternals = {
claudeAgentTeams: ClaudeAgentTeamsService
handleByPtyId: Map<string, string>
dropDisconnectedPtyRecord: (ptyId: string) => void
}
function internals(runtime: OrcaRuntimeService): RuntimeInternals {
return runtime as unknown as RuntimeInternals
}
function registerTeam(runtime: OrcaRuntimeService, ptyId: string, leaderHandle: string): void {
const { claudeAgentTeams, handleByPtyId } = internals(runtime)
claudeAgentTeams.createLaunchEnv({
leaderHandle,
baseEnv: {},
shimDir: '/tmp/orca-shim',
shimBin: 'orca'
})
handleByPtyId.set(ptyId, leaderHandle)
}
describe('ClaudeAgentTeams eviction on natural PTY exit (leak regression)', () => {
it('evicts the team when its leader PTY exits naturally (onPtyExit)', () => {
const runtime = new OrcaRuntimeService()
registerTeam(runtime, 'pty-leader', 'handle-leader')
expect(internals(runtime).claudeAgentTeams.getActiveTeamCount()).toBe(1)
runtime.onPtyExit('pty-leader', 0)
expect(internals(runtime).claudeAgentTeams.getActiveTeamCount()).toBe(0)
})
it('evicts the team when the disconnected PTY record is pruned', () => {
const runtime = new OrcaRuntimeService()
registerTeam(runtime, 'pty-leader', 'handle-leader')
expect(internals(runtime).claudeAgentTeams.getActiveTeamCount()).toBe(1)
internals(runtime).dropDisconnectedPtyRecord('pty-leader')
expect(internals(runtime).claudeAgentTeams.getActiveTeamCount()).toBe(0)
})
it('does not accumulate teams across many natural leader exits', () => {
const runtime = new OrcaRuntimeService()
for (let i = 0; i < 100; i++) {
registerTeam(runtime, `pty-${i}`, `handle-${i}`)
runtime.onPtyExit(`pty-${i}`, 0)
}
expect(internals(runtime).claudeAgentTeams.getActiveTeamCount()).toBe(0)
})
it('leaves a concurrently-launched team intact when only one leader exits', () => {
const runtime = new OrcaRuntimeService()
registerTeam(runtime, 'pty-a', 'handle-a')
registerTeam(runtime, 'pty-b', 'handle-b')
expect(internals(runtime).claudeAgentTeams.getActiveTeamCount()).toBe(2)
runtime.onPtyExit('pty-a', 0)
expect(internals(runtime).claudeAgentTeams.getActiveTeamCount()).toBe(1)
})
it('is a no-op for a PTY that never launched a team', () => {
const runtime = new OrcaRuntimeService()
const { handleByPtyId } = internals(runtime)
handleByPtyId.set('pty-plain', 'handle-plain')
expect(internals(runtime).claudeAgentTeams.getActiveTeamCount()).toBe(0)
runtime.onPtyExit('pty-plain', 0)
expect(internals(runtime).claudeAgentTeams.getActiveTeamCount()).toBe(0)
})
})
}
@@ -1,74 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-session-contracts'
import { OrcaRuntimeService } from '../orca-runtime'
export function registerSessionTabsEmptyWorktreePublicationTests(): void {
const WORKTREE = 'repo::/never-opened'
type WorktreeAnswerInternals = {
getMobileSessionTabsForWorktree: (
worktreeId: string,
clientNavigationId?: string
) => RuntimeMobileSessionTabsResult
}
function createHeadedRuntime(): OrcaRuntimeService {
const runtime = new OrcaRuntimeService()
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these tests only reach listProcesses.
runtime.setPtyController({ listProcesses: vi.fn(async () => []) } as never)
runtime.attachWindow(1)
return runtime
}
function answerFor(runtime: OrcaRuntimeService, clientNavigationId?: string) {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: reads the runtime's own per-worktree answer.
return (runtime as unknown as WorktreeAnswerInternals).getMobileSessionTabsForWorktree(
WORKTREE,
clientNavigationId
)
}
function recordAnswers(runtime: OrcaRuntimeService): RuntimeMobileSessionTabsResult[] {
const answers: RuntimeMobileSessionTabsResult[] = []
runtime.onMobileSessionTabsChanged((snapshot) => {
if (snapshot.worktree === WORKTREE) {
answers.push(snapshot)
}
}, 'device-1')
return answers
}
function publishGraphWithoutWorktree(runtime: OrcaRuntimeService): void {
runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [] })
}
describe('answering for a worktree the host has no tabs for', () => {
it('answers "ask me later" only until the renderer graph publishes', () => {
const runtime = createHeadedRuntime()
expect(answerFor(runtime)).toMatchObject({ publicationEpoch: 'none', tabs: [] })
expect(answerFor(runtime, 'device-1')).toMatchObject({
publicationEpoch: 'none:client-navigation',
tabs: []
})
publishGraphWithoutWorktree(runtime)
const published = answerFor(runtime, 'device-1')
expect(published.publicationEpoch).not.toMatch(/^none/)
expect(published.tabs).toEqual([])
})
it('tells a client that asked during startup once the graph publishes', () => {
const runtime = createHeadedRuntime()
const answers = recordAnswers(runtime)
answerFor(runtime, 'device-1')
answerFor(runtime, 'device-1')
publishGraphWithoutWorktree(runtime)
expect(answers).toHaveLength(1)
expect(answers[0].publicationEpoch).not.toMatch(/^none/)
expect(answers[0].tabs).toEqual([])
})
})
}
@@ -6,11 +6,8 @@ import {
type AgentTransition
} from '../../shared/agent-status/lifecycle/agent-transitions'
import type { NotificationSettings } from '../../shared/notification-settings-types'
import type {
MobileNotificationDispatchEvent,
MobileNotificationEvent
} from './push/notifications/runtime-mobile-notification-controller'
import { createNotificationDeliveryService } from '../notifications/notification-delivery-service'
import type { MobileNotificationDispatchEvent } from './push/notifications/runtime-mobile-notification-controller'
import '../notifications/notification-delivery-service'
import { TerminalAgentPhonePush } from './terminal-agent-phone-push'
const WT = 'repo-1::/tmp/wt'
@@ -149,26 +146,6 @@ describe('TerminalAgentPhonePush', () => {
])
})
it.each([null, 'host-a'])(
'pushes windowless news with its execution owner: %s',
(connectionId) => {
const { sent, finish } = harness()
finish({ connectionId })
expect(sent).toHaveLength(1)
expect(sent[0]?.workspaceExecutionHostId ?? 'local').toBe(
connectionId ? `ssh:${connectionId}` : 'local'
)
}
)
it('waits out the quiet window for a Pi agent even with no live work seen', () => {
const { sent, advance, finish } = harness()
finish({ agentType: 'pi' })
expect(sent).toEqual([])
advance(AGENT_DONE_QUIET_MS)
expect(sent).toHaveLength(1)
})
it('announces no milestone a goal agent resumes from within the quiet window', () => {
const { push, sent, advance, working, finish } = harness()
working()
@@ -180,26 +157,6 @@ describe('TerminalAgentPhonePush', () => {
expect(sent).toEqual([])
})
it('pushes an agent that needs input at once, and a failed turn as finished', () => {
const { push, sent, state, advance } = harness({
hookRow: row({
state: 'waiting',
lastAssistantMessage: undefined,
toolName: 'Bash',
toolInput: 'rm -rf build'
})
})
push.observe(moved('needs-input', 'needs-you'))
expect(sent).toHaveLength(1)
advance(5_000)
state.hookRow = row({ mainAgent: { state: 'done', outcome: 'failure', stateStartedAt: 5_000 } })
push.observe(moved('errored'))
expect(sent.map((event) => [event.agentState, event.title, event.body])).toEqual([
['waiting', 'orca / feature - Claude needs input', 'Using Bash: rm -rf build'],
['done', 'orca / feature - Claude failed', 'Fixed it.']
])
})
it('stays silent while a window owns the host, including one that arrives mid quiet window', () => {
const { sent, state, advance, working, finish } = harness()
state.hasWindow = true
@@ -214,66 +171,6 @@ describe('TerminalAgentPhonePush', () => {
expect(sent).toEqual([])
})
it('collapses a burst in one workspace as the window’s fan-out does', () => {
const { push, sent, finish } = harness()
finish()
push.observe(moved('prompt-submitted', 'working'))
finish({ stateStartedAt: 6_000 })
expect(sent).toHaveLength(1)
})
it('pushes nothing for a prompt, a resume, a rest, a session boundary, or a structured chat’s row', () => {
const { push, sent, state, advance } = harness()
push.observe(moved('prompt-submitted', 'working'))
push.observe(moved(null, 'working'))
push.observe(moved(null, 'rest'))
// The detector names no news for a boundary's done.
push.observe(moved(null, 'done'))
push.observe({
kind: 'seeded',
paneKey: PANE,
workspace: { worktreeId: WT, sshHostId: null },
phase: 'done',
at: 1
})
state.hookRow = row({ structuredHost: 'owned' })
push.observe(moved('turn-finished'))
advance(AGENT_DONE_QUIET_MS)
expect(sent).toEqual([])
})
it('pushes nothing for a muted workspace', () => {
const { sent, finish } = harness({ muted: true })
finish()
expect(sent).toEqual([])
})
it('leaves a dispatched worker’s finish to its coordinator, and still pushes its prompt', () => {
const { push, sent, state, advance, finish } = harness({ dispatched: true })
finish()
advance(AGENT_DONE_QUIET_MS)
expect(sent).toEqual([])
state.hookRow = row({ state: 'waiting', stateStartedAt: 6_000 })
push.observe(moved('needs-input', 'needs-you'))
expect(sent.map((event) => event.agentState)).toEqual(['waiting'])
})
it('pushes a dispatched worker’s finish whose title still waits for approval, as the window does', () => {
const { sent, advance, finish } = harness({
dispatched: true,
title: 'Codex - waiting for approval'
})
finish()
advance(AGENT_DONE_QUIET_MS)
expect(sent).toHaveLength(1)
})
it('marks the push not desktop-allowed when agent notifications are off', () => {
const { sent, finish } = harness({ settings: { agentTaskComplete: false } })
finish()
expect(sent[0]?.desktopAllowed).toBe(false)
})
it('announces a lead agent’s finish while background work runs, and not its later all-clear', () => {
const { sent, advance, working, finish } = harness()
working()
@@ -289,28 +186,6 @@ describe('TerminalAgentPhonePush', () => {
expect(sent).toHaveLength(1)
})
it('waits out the quiet window for a pane restored mid-work, as the window seeds it', () => {
const { push, sent, advance, finish } = harness({ hookRow: row({ state: 'working' }) })
const workspace = { worktreeId: WT, sshHostId: null }
push.observe({ kind: 'seeded', paneKey: PANE, workspace, phase: 'working', at: 1 })
finish()
expect(sent).toEqual([])
advance(AGENT_DONE_QUIET_MS)
expect(sent).toHaveLength(1)
})
it('counts a pane restored mid background work as work seen, as the window seeds it', () => {
const { push, sent, advance, finish } = harness({
hookRow: row({ state: 'working', turnCompletedAt: 7_000 })
})
const workspace = { worktreeId: WT, sshHostId: null }
push.observe({ kind: 'seeded', paneKey: PANE, workspace, phase: 'working', at: 1 })
finish()
expect(sent).toEqual([])
advance(AGENT_DONE_QUIET_MS)
expect(sent).toHaveLength(1)
})
it('announces no finish restored with the pane', () => {
const { push, sent, advance, working, finish } = harness({
hookRow: row({ state: 'working', turnCompletedAt: 7_000 })
@@ -322,48 +197,4 @@ describe('TerminalAgentPhonePush', () => {
advance(AGENT_DONE_QUIET_MS)
expect(sent).toEqual([])
})
it('sends phones, old ones included, the alert the window’s own request would have', () => {
const { sent, finish } = harness()
finish()
const fromWindow: MobileNotificationEvent[] = []
const service = createNotificationDeliveryService({
readNotificationSettings: () => ({
enabled: false,
agentTaskComplete: true,
agentNeedsInput: true,
terminalBell: true,
suppressWhenFocused: true,
customSoundId: 'system',
customSoundPath: null,
customSoundVolume: 1,
mutedNotificationSourceIds: [],
notifyHiddenWorkspaces: true
}),
findActiveWindow: () => null,
isWindowVisible: () => false,
setTrayAttention: () => {},
isNotificationSupported: () => false,
dispatchMobileNotification: (event) => fromWindow.push(event),
readAuthorizationStatus: async () => null,
recordDeliveryOutcome: () => {},
deliverNative: () => ({ delivered: false }),
platform: 'linux',
now: () => 10_000
})
void service.dispatch({
source: 'agent-task-complete',
notificationId: sent[0]?.notificationId,
worktreeId: WT,
paneKey: PANE,
repoLabel: 'orca',
worktreeLabel: 'feature',
agentType: 'claude',
agentState: 'done',
agentPrompt: 'fix the bug',
agentLastAssistantMessage: 'Fixed it.'
})
// The window's request was made with desktop banners off; only that flag differs.
expect(fromWindow).toEqual([{ ...sent[0], desktopAllowed: false }])
})
})
@@ -68,18 +68,6 @@ function harness() {
}
describe('TerminalAgentPromptAnswers', () => {
it('refuses a second answer to the same prompt before the agent reports moving on', async () => {
const h = harness()
const { promptId } = h.answers.pending(h.runtime, target)!
await h.answer(promptId)
expect(h.answers.pending(h.runtime, target)).toBeNull()
await expect(h.answer(promptId)).resolves.toEqual({
outcome: 'refused',
reason: 'prompt_not_pending'
})
expect(h.keys).toEqual(['1'])
})
it('allows only one concurrent answer to claim the prompt', async () => {
const h = harness()
const { promptId } = h.answers.pending(h.runtime, target)!
@@ -118,16 +106,6 @@ describe('TerminalAgentPromptAnswers', () => {
expect(h.answers.pending(h.runtime, target)).toBeNull()
})
it('refuses a prompt the agent has moved on from, or a later one', async () => {
const h = harness()
const { promptId } = h.answers.pending(h.runtime, target)!
h.state.row = approvalRow(20)
await expect(h.answer(promptId)).resolves.toMatchObject({ reason: 'prompt_not_pending' })
h.state.row = { ...approvalRow(20), state: 'working' }
await expect(h.answer(promptId)).resolves.toMatchObject({ reason: 'prompt_not_pending' })
expect(h.keys).toEqual([])
})
it('refuses when the prompt or the process changes between the read and the write', async () => {
const h = harness()
const first = h.answers.pending(h.runtime, target)!.promptId
@@ -144,25 +122,6 @@ describe('TerminalAgentPromptAnswers', () => {
expect(h.keys).toEqual([])
})
it('refuses an option the prompt does not offer, and a prompt only Orca can answer', async () => {
const h = harness()
const { promptId } = h.answers.pending(h.runtime, target)!
await expect(h.answer(promptId, 'allow_always')).resolves.toMatchObject({
reason: 'option_not_offered'
})
const hookless = { ...target, answerable: false }
const unsupported = h.answers.pending(h.runtime, hookless)!
expect(unsupported.kind).toBe('unsupported')
await expect(
h.answers.answer(h.runtime, hookless, {
promptId: unsupported.promptId,
optionId: 'allow_once',
signal: new AbortController().signal
})
).resolves.toMatchObject({ reason: 'prompt_unsupported' })
expect(h.keys).toEqual([])
})
it('keeps a write the transport refused answerable, and an unprovable one answered', async () => {
const h = harness()
const { promptId } = h.answers.pending(h.runtime, target)!
@@ -191,20 +150,4 @@ describe('TerminalAgentPromptAnswers', () => {
})
expect(h.answers.pending(h.runtime, target)?.promptId).toBe(promptId)
})
it('refuses before any key when the caller’s own check fails', async () => {
const h = harness()
const { promptId } = h.answers.pending(h.runtime, target)!
await expect(
h.answers.answer(h.runtime, target, {
promptId,
optionId: 'allow_once',
signal: new AbortController().signal,
beforeWrite: async () => {
throw new Error('agent_not_in_foreground')
}
})
).resolves.toEqual({ outcome: 'refused', reason: 'agent_not_reachable' })
expect(h.keys).toEqual([])
})
})
@@ -1,52 +0,0 @@
import { describe, expect, it } from 'vitest'
import './orca-runtime-tests/runtime-test-module-registration.spec'
import { OrcaRuntimeService } from './orca-runtime'
import {
HEADLESS_LEAF_ID,
TEST_WORKTREE_ID,
makeRuntimeStoreWithWorkspaceSession,
makeWorkspaceSessionWithHeadlessTerminal
} from './orca-runtime-tests/orca-runtime-test-fixtures.spec'
import { UpdatePaneLayout } from '../../shared/rpc-contract/session-tabs-schemas-params'
describe('remote terminal chat ownership', () => {
it('preserves, changes, and explicitly clears the owner across the RPC and restart boundary', async () => {
const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal()
)
const runtime = new OrcaRuntimeService(runtimeStore)
const update = (owner: string | null | undefined) => {
const params = UpdatePaneLayout.parse({
worktree: `id:${TEST_WORKTREE_ID}`,
tabId: 'host-tab',
root: getSession().terminalLayoutsByTabId['host-tab']!.root,
expandedLeafId: null,
...(owner !== undefined ? { chatLeafId: owner } : {})
})
return runtime.updateMobileSessionPaneLayout(params.worktree, {
...params,
expandedLeafId: params.expandedLeafId ?? null
})
}
await update(HEADLESS_LEAF_ID)
expect(getSession().terminalLayoutsByTabId['host-tab']?.chatLeafId).toBe(HEADLESS_LEAF_ID)
// An older client changing geometry must not clear the owner.
await update(undefined)
expect(getSession().terminalLayoutsByTabId['host-tab']?.chatLeafId).toBe(HEADLESS_LEAF_ID)
runtime['mobileSessionTabsByWorktree'].delete(TEST_WORKTREE_ID)
runtime['hydrateHeadlessMobileSessionTabsFromWorkspaceSession'](TEST_WORKTREE_ID)
const rehydrated = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
const surface = rehydrated.tabs.find(
(tab) => tab.type === 'terminal' && tab.parentTabId === 'host-tab'
)
expect(surface?.type === 'terminal' && surface.parentLayout?.chatLeafId).toBe(HEADLESS_LEAF_ID)
await update(null)
expect(getSession().terminalLayoutsByTabId['host-tab']?.chatLeafId).toBeUndefined()
const cleared = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
expect(
cleared.tabs
.filter((tab) => tab.type === 'terminal')
.every((tab) => !tab.parentLayout?.chatLeafId)
).toBe(true)
})
})
@@ -7,7 +7,7 @@ import { makePaneKey } from '../../shared/workspace-layout/stable-pane-id'
import { Store } from '../persistence/loading-store/store'
import { ProfileStateSqliteAuthority } from '../persistence/profile-state/profile-state-sqlite-authority'
import { wirePtyIpcSession } from '../ipc/pty/delivery/wire-session'
import { SYNTHETIC_KILL_EXIT_DUPLICATE_WINDOW_MS } from '../ipc/pty/delivery/visibility-state'
import '../ipc/pty/delivery/visibility-state'
import { bindProviderListeners } from '../ipc/pty/provider/bind-listeners'
import {
stopRendererOwnedPty,
@@ -149,24 +149,6 @@ describe('intentional stops keep the pane through the exit', () => {
])
})
it('keeps the tab and its wake binding when the renderer hibernates the pane', async () => {
const harness = createHarness()
await stopRendererOwnedPty(harness.deps, { id: PTY_ID, keepHistory: true })
expect(harness.tabIds()).toEqual([TAB_ID])
expect(harness.boundPtyId()).toBe(PTY_ID)
expect(harness.rendererExits()).toEqual([
{
id: PTY_ID,
code: 0,
incarnationId: INCARNATION_ID,
hostId: 'local',
preserveRendererBinding: true
}
])
})
it('keeps a typed pane that a restart replaces, and binds the replacement', async () => {
const harness = createHarness()
harness.runtime.terminalRunFacts.recordSpawnCommit({
@@ -204,48 +186,6 @@ describe('intentional stops keep the pane through the exit', () => {
])
})
it('labels the exit for both a sleep and a restart that stop the same process', async () => {
const harness = createHarness()
const settleSleep = harness.runtime.intentionalPtyStops.mark(
PTY_ID,
'reversible',
INCARNATION_ID
)
await stopReplacedPanePty(harness.deps, PTY_ID)
settleSleep(true)
expect(harness.boundPtyId()).toBe(PTY_ID)
expect(harness.rendererExits()).toEqual([
{
id: PTY_ID,
code: 0,
incarnationId: INCARNATION_ID,
hostId: 'local',
preserveRendererBinding: true,
replacedByRestart: true
}
])
})
it('keeps the pane when main restarts the daemon under it', () => {
const harness = createHarness()
harness.emitDaemonRestartExit(true)
expect(harness.tabIds()).toEqual([TAB_ID])
expect(harness.boundPtyId()).toBe(PTY_ID)
expect(harness.rendererExits()).toEqual([
{
id: PTY_ID,
code: -1,
incarnationId: INCARNATION_ID,
hostId: 'local',
replacedByRestart: true
}
])
})
it('retires the pane on a daemon-wide exit that no restart claims', () => {
const harness = createHarness()
@@ -308,20 +248,6 @@ describe('intentional stops keep the pane through the exit', () => {
})
})
it('forgets the stop once the duplicate-exit window after it closes', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
const harness = createHarness({ lateProviderExit: true })
await stopRendererOwnedPty(harness.deps, { id: PTY_ID, keepHistory: true })
vi.advanceTimersByTime(SYNTHETIC_KILL_EXIT_DUPLICATE_WINDOW_MS - 1)
expect(harness.runtime.intentionalPtyStops.claimExit(PTY_ID, INCARNATION_ID)).toEqual([
'reversible'
])
vi.advanceTimersByTime(1)
expect(harness.runtime.intentionalPtyStops.claimExit(PTY_ID, INCARNATION_ID)).toEqual([])
})
it('keeps the tab and its wake binding when the runtime puts the worktree to sleep', async () => {
const harness = createHarness({ folder: true })
const inventories = [[{ id: PTY_ID, worktreeId: WORKTREE_ID, cwd: WORKTREE_PATH, title: 'a' }]]
@@ -1,15 +1,10 @@
import type { TerminalWaiter } from './terminal/wait/terminal-waiter'
import type {
RuntimeTerminalWait,
RuntimeTerminalAgentStatus
} from '../../shared/runtime-terminal-contracts'
import { RuntimeMachineName } from './hosts/platform-probe/runtime-machine-name'
import { OrcaRuntimeService } from './orca-runtime'
import { makeTuiIdlePty } from './terminal/wait/tui-idle-wait-test-harness'
import './hosts/platform-probe/runtime-machine-name'
import './orca-runtime'
import './terminal/wait/tui-idle-wait-test-harness'
import { readFileSync } from 'node:fs'
import { makeAgentStatusStoreWiring } from './agent-status-store-wiring.test-fixture'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { TERMINAL_INTERACTIVE_WAIT_PROBE_TIMEOUT_MS } from './terminal/wait/terminal-interactive-probe-budget'
import {
createTranscriptPane,
@@ -36,8 +31,6 @@ describe('Interactive wait visibility and terminal admission', () => {
return readFileSync(join(__dirname, '__fixtures__', `${name}.txt`), 'utf8')
}
const CURSOR_APPROVAL = fixture('cursor-agent-approval-prompt')
const CURSOR_LONG_TOOL_CALL = fixture('cursor-agent-long-tool-call')
const CURSOR_IDLE = fixture('cursor-agent-idle-after-approval')
// Claude Code 2.1.234's own trust screen, which the runtime already matched by shape.
const CLAUDE_TRUST = [
@@ -67,23 +60,6 @@ describe('Interactive wait visibility and terminal admission', () => {
describe('terminal interactive-wait visibility (STA-4513, STA-3714)', () => {
describe('cursor-agent approval menu, the case no hook reports', () => {
it('names the pending approval on the pane a coordinator inspects', async () => {
const { runtime, handle } = await createPane({
paneTitle: CURSOR_TITLE,
foregroundProcess: 'cursor-agent',
data: CURSOR_APPROVAL
})
await expect(runtime.showTerminal(handle)).resolves.toMatchObject({
agentWait: { source: 'prompt-text', reason: 'agent-approval-prompt' }
})
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toMatchObject({
source: 'prompt-text',
reason: 'agent-approval-prompt',
since: expect.any(Number)
})
})
it('refuses a guarded send and a dispatch preamble while the approval is up', async () => {
// Refuse both injection paths so coordinator preambles cannot land in approval dialogs (STA-2631).
const { runtime, handle } = await createPane({
@@ -104,18 +80,6 @@ describe('Interactive wait visibility and terminal admission', () => {
).rejects.toThrow('agent_prompt_blocked')
})
it('lets a dispatch preamble through once the same lane is working', async () => {
const { runtime, handle } = await createPane({
paneTitle: CURSOR_TITLE,
foregroundProcess: 'cursor-agent',
data: CURSOR_LONG_TOOL_CALL
})
await expect(
assertTerminalAgentSendable({ runtime, handle, assertWritable: () => {} })
).resolves.toBeUndefined()
})
it('refuses to call the lane idle while the approval is unanswered', async () => {
const { runtime, handle } = await createPane({
paneTitle: CURSOR_TITLE,
@@ -128,39 +92,6 @@ describe('Interactive wait visibility and terminal admission', () => {
).resolves.toMatchObject({ satisfied: false, blockedReason: 'agent-approval-prompt' })
})
it('reports no wait for the same agent inside a long tool call', async () => {
// The control: identical vendor, identical spinner title, a real `sleep 60` running.
const { runtime, handle } = await createPane({
paneTitle: CURSOR_TITLE,
foregroundProcess: 'cursor-agent',
data: CURSOR_LONG_TOOL_CALL
})
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toBeNull()
await expect(runtime.showTerminal(handle)).resolves.toMatchObject({ agentWait: null })
})
it('reports no wait once the agent is idle again', async () => {
const { runtime, handle } = await createPane({
paneTitle: CURSOR_TITLE,
foregroundProcess: 'cursor-agent',
data: CURSOR_IDLE
})
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toBeNull()
})
it('treats an answered menu still in scrollback as scrollback', async () => {
// A live dialog must still own the screen bottom; stale menu text can remain in scrollback.
const { runtime, handle } = await createPane({
paneTitle: CURSOR_TITLE,
foregroundProcess: 'cursor-agent',
data: `${CURSOR_APPROVAL}\n${CURSOR_IDLE}`
})
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toBeNull()
})
it('treats an answered menu followed by any later output as scrollback', async () => {
// Any output after a menu can disprove it; matching only Cursor's follow-up text would block healthy prompt injection.
const { runtime, handle } = await createPane({
@@ -178,80 +109,9 @@ describe('Interactive wait visibility and terminal admission', () => {
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toBeNull()
})
it('is not anchored by the agent narrating a choice after the menu', async () => {
// Independent marker matches can let later prose revive an answered menu at screen bottom.
const { runtime, handle } = await createPane({
paneTitle: CURSOR_TITLE,
foregroundProcess: 'cursor-agent',
data: `${CURSOR_APPROVAL}\nApproved. Next time I will suggest Run Everything instead.\n`
})
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toBeNull()
})
it('is not anchored by the agent narrating a choice before the menu', async () => {
const { runtime, handle } = await createPane({
paneTitle: CURSOR_TITLE,
foregroundProcess: 'cursor-agent',
data: `You can pick Run Everything or Run (once) if you prefer.\nStill thinking.\n`
})
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toBeNull()
})
it('requires the dialog to be the last thing on the screen', async () => {
// One line of slack revives answered menus; live captures end exactly at the dialog.
const { runtime, handle } = await createPane({
paneTitle: CURSOR_TITLE,
foregroundProcess: 'cursor-agent',
data: `${CURSOR_APPROVAL}\n Auto · 6.1%\n`
})
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toBeNull()
})
it('reports the same wait for a pane whose PTY is not local', async () => {
// The verdict is derived from retained tail and title state, so an SSH or WSL pane
// reaches it the same way a local one does.
const { runtime, handle } = await createPane({
paneTitle: CURSOR_TITLE,
foregroundProcess: 'cursor-agent',
data: CURSOR_APPROVAL,
connectionId: 'ssh:build-host'
})
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toMatchObject({
source: 'prompt-text',
reason: 'agent-approval-prompt'
})
})
it('ignores a partial menu that names no decision keys', async () => {
const { runtime, handle } = await createPane({
paneTitle: CURSOR_TITLE,
foregroundProcess: 'cursor-agent',
data: 'The agent asked: Run this command? I said yes and it worked.\n'
})
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toBeNull()
})
})
describe('prompts the runtime already matched but never surfaced', () => {
it('surfaces a startup trust screen on the pane, not only on terminal wait', async () => {
// A pane on its trust screen still wears Orca's tab title; the agent has set none.
const { runtime, handle } = await createPane({
paneTitle: 'sta4513-claude',
foregroundProcess: 'claude',
data: CLAUDE_TRUST
})
await expect(runtime.showTerminal(handle)).resolves.toMatchObject({
agentWait: { source: 'prompt-text', reason: 'agent-trust-workspace' }
})
})
it('still lets a live working title clear a stale startup prompt', async () => {
// Startup prompt text lacks dismissal markers; a live non-permission title proves staleness for both status and send guards.
const { runtime, handle } = await createPane({
@@ -281,16 +141,6 @@ describe('Interactive wait visibility and terminal admission', () => {
})
})
it('reports no wait for a hook-reported working turn', async () => {
const { runtime, handle } = await createPane({
paneTitle: '✻ Claude Code',
foregroundProcess: 'claude',
data: agentStatusOsc('working')
})
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toBeNull()
})
it('drops a retained permission row once the agent stops owning the pane', async () => {
// Shell titles may match no rule; a fresh hook row could otherwise report a dead agent waiting for 30 minutes.
const { runtime, handle } = await createPane({
@@ -301,16 +151,6 @@ describe('Interactive wait visibility and terminal admission', () => {
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toBeNull()
})
it('drops a retained permission row once a shell owns the pane', async () => {
const { runtime, handle } = await createPane({
paneTitle: 'zsh',
foregroundProcess: 'zsh',
data: agentStatusOsc('blocked')
})
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toBeNull()
})
})
it('still reports a live dialog restored from terminal history', async () => {
@@ -384,32 +224,6 @@ describe('Interactive wait visibility and terminal admission', () => {
}
}, 20_000)
it('accepts a menu whose keys are rendered as glyphs', async () => {
const { runtime, handle } = await createPane({
paneTitle: CURSOR_TITLE,
foregroundProcess: 'cursor-agent',
data: [
'Run this command?\n',
' → Run (once) (\u21b5)\n',
' Run Everything (\u21e7\u21b9)\n'
].join('')
})
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toMatchObject({
reason: 'agent-approval-prompt'
})
})
it('still rejects prose that merely ends in parentheses', async () => {
const { runtime, handle } = await createPane({
paneTitle: CURSOR_TITLE,
foregroundProcess: 'cursor-agent',
data: `${CURSOR_APPROVAL}\nNext time I will suggest Run Everything (as before)\n`
})
await expect(runtime.getTerminalInteractiveWait(handle)).resolves.toBeNull()
})
it('leaves the wait unevaluated when the foreground probe wedges', async () => {
// Bound remote PTY probes; a wedged host must leave wait unknown without stalling showTerminal.
const { runtime, handle } = await createPane({
@@ -434,18 +248,6 @@ describe('Interactive wait visibility and terminal admission', () => {
vi.useRealTimers()
}
}, 15_000)
it('answers undefined rather than "not waiting" for a pane it cannot read', async () => {
const { runtime } = await createPane({
paneTitle: CURSOR_TITLE,
foregroundProcess: 'cursor-agent',
data: CURSOR_APPROVAL
})
await expect(
runtime.getTerminalInteractiveWait('term_does_not_exist')
).resolves.toBeUndefined()
})
})
})
@@ -472,224 +274,3 @@ describe('Screen rule readiness scope', () => {
}, 20_000)
})
})
/** Stands in for a mailbox with work pending, which these timer mechanics take as given. */
function withPendingDelivery(runtime: OrcaRuntimeService): OrcaRuntimeService {
runtime['getOrchestrationMailboxTerminal'] = (ptyId) => ({
ptyId,
handle: 'term_pending',
paneKeys: [],
lastAgentStatus: 'idle',
lastAgentStatusObservedLive: true
})
vi.spyOn(runtime['orchestrationMailboxNotifications'], 'hasDeliverableMail').mockReturnValue(true)
return runtime
}
describe('terminal wait ownership', () => {
beforeEach(() => {
vi.spyOn(RuntimeMachineName.prototype, 'start').mockImplementation(() => {})
vi.useFakeTimers()
vi.setSystemTime(10_000)
})
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
})
function pendingStatus() {
const { promise, resolve } = Promise.withResolvers<RuntimeTerminalAgentStatus>()
return { status: promise, finish: resolve }
}
function interactiveWait(getStatus: (handle: string) => Promise<RuntimeTerminalAgentStatus>) {
const runtime = new OrcaRuntimeService()
runtime['getTerminalAgentStatusPtyId'] = () => 'pty-1'
runtime['getTerminalWaitPermissionInputs'] = () => ({
terminal: {
waitText: '',
waitBlockedAt: null,
title: null,
titleStatus: null,
titleStatusIsLive: false
},
lifecycle: null
})
runtime['getFreshExplicitAgentStatusForHandle'] = () => ({
status: 'permission',
updatedAt: 42,
stateStartedAt: 42
})
runtime['resolveAuthoritativeTerminalWaitPermission'] = () => null
runtime.getTerminalAgentStatus = getStatus
return runtime
}
it('shares permission probes per PTY and releases the completed probe', async () => {
const pending = pendingStatus()
const getStatus = vi.fn(() => pending.status)
const wait = interactiveWait(getStatus)
const first = wait.getTerminalInteractiveWait('handle-1')
const second = wait.getTerminalInteractiveWait('handle-2')
expect(getStatus).toHaveBeenCalledOnce()
pending.finish({ handle: 'handle-1', isRunningAgent: true, status: 'permission' })
expect(await first).toEqual({ source: 'hook', since: 42 })
expect(await second).toEqual({ source: 'hook', since: 42 })
await wait.getTerminalInteractiveWait('handle-3')
expect(getStatus).toHaveBeenCalledTimes(2)
})
it('keeps a timed-out permission probe in flight until its host responds', async () => {
const pending = pendingStatus()
const getStatus = vi.fn(() => pending.status)
const wait = interactiveWait(getStatus)
const first = wait.getTerminalInteractiveWait('handle-1')
await vi.advanceTimersByTimeAsync(2000)
expect(await first).toBeUndefined()
const second = wait.getTerminalInteractiveWait('handle-2')
expect(getStatus).toHaveBeenCalledOnce()
pending.finish({ handle: 'handle-1', isRunningAgent: true, status: 'permission' })
expect(await second).toEqual({ source: 'hook', since: 42 })
})
it('isolates permission probes between runtimes with the same PTY id', async () => {
const pending = pendingStatus()
const getStatus = vi.fn(() => pending.status)
const first = interactiveWait(getStatus).getTerminalInteractiveWait('handle-1')
const second = interactiveWait(getStatus).getTerminalInteractiveWait('handle-1')
expect(getStatus).toHaveBeenCalledTimes(2)
pending.finish({ handle: 'handle-1', isRunningAgent: false, status: null })
expect(await first).toBeNull()
expect(await second).toBeNull()
})
it('rechecks current terminal evidence before delivery and coalesces repeated refusals', async () => {
const pty = makeTuiIdlePty({
lastOutputAt: Date.now(),
lastAgentStatus: 'idle',
lastOscTitle: 'Codex'
})
const deliver = vi.fn()
const runtime = withPendingDelivery(new OrcaRuntimeService())
const ownedPty = runtime['terminalIdentity'].createPty(pty)
runtime['deliverPendingMessagesForPty'] = deliver
expect(runtime['checkDeliverySettledAndArmRecheck'](pty.ptyId)).toBe(false)
expect(runtime['checkDeliverySettledAndArmRecheck'](pty.ptyId)).toBe(false)
expect(vi.getTimerCount()).toBe(1)
ownedPty.lastOscTitle = null
await vi.advanceTimersByTimeAsync(3050)
expect(deliver).toHaveBeenCalledExactlyOnceWith(pty.ptyId)
expect(vi.getTimerCount()).toBe(0)
})
it('cancels only the delivery recheck belonging to its runtime', async () => {
const pty = makeTuiIdlePty({
lastOutputAt: Date.now(),
lastAgentStatus: 'idle',
lastOscTitle: 'Codex'
})
const first = withPendingDelivery(new OrcaRuntimeService())
const second = withPendingDelivery(new OrcaRuntimeService())
first['terminalIdentity'].createPty(pty)
second['terminalIdentity'].createPty(pty)
first['checkDeliverySettledAndArmRecheck'](pty.ptyId)
second['checkDeliverySettledAndArmRecheck'](pty.ptyId)
expect(vi.getTimerCount()).toBe(2)
first['clearDeliveryRecheck'](pty.ptyId)
expect(vi.getTimerCount()).toBe(1)
second['clearDeliveryRecheck'](pty.ptyId)
expect(vi.getTimerCount()).toBe(0)
})
it('clears a pending blocked scan without cancelling another runtime scan', async () => {
const pty = makeTuiIdlePty({ waitBlockedAt: null })
const getPty = vi.fn(() => pty)
const first = new OrcaRuntimeService()
const second = new OrcaRuntimeService()
vi.spyOn(first['ptysById'], 'get').mockImplementation(getPty)
vi.spyOn(second['ptysById'], 'get').mockImplementation(getPty)
first['scheduleWaitBlockedCheck']('pty-1', 'output', 10_000)
second['scheduleWaitBlockedCheck']('pty-1', 'output', 10_000)
first['scheduleWaitBlockedCheck']('pty-1', 'more', 10_001)
second['scheduleWaitBlockedCheck']('pty-1', 'more', 10_001)
expect(vi.getTimerCount()).toBe(2)
first['clearWaitBlockedCheckState']('pty-1')
expect(vi.getTimerCount()).toBe(1)
await vi.advanceTimersByTimeAsync(50)
expect(getPty).toHaveBeenCalledTimes(3)
expect(vi.getTimerCount()).toBe(0)
})
it('routes settlement through the current runtime verdict and resolver', async () => {
const runtime = new OrcaRuntimeService()
const pending = Promise.withResolvers<RuntimeTerminalWait>()
const waiter: TerminalWaiter = {
handle: 'handle-1',
ptyId: 'pty-1',
condition: 'tui-idle',
resolve: pending.resolve,
reject: pending.reject,
timeout: null,
cancelIdlePoll: null,
abortCleanup: null
}
runtime['terminalWaiters'].add(waiter)
const verdict = vi.fn<OrcaRuntimeService['evaluateTuiIdleForPty']>(() => ({
kind: 'ready-strong'
}))
runtime['evaluateTuiIdleForPty'] = verdict
const original = runtime['resolveWaiter']
const resolve = vi.fn<OrcaRuntimeService['resolveWaiter']>((...args) =>
original.call(runtime, ...args)
)
runtime['resolveWaiter'] = resolve
const pty = makeTuiIdlePty({ lastAgentStatus: 'working' })
runtime['resolvePtyTuiIdleWaiters'](pty, 'pty-1')
expect(verdict).toHaveBeenCalledExactlyOnceWith(pty)
expect(resolve).toHaveBeenCalledOnce()
await expect(pending.promise).resolves.toMatchObject({ satisfied: true, condition: 'tui-idle' })
})
it('keeps an absent launcher target as a rejected promise', async () => {
const runtime = new OrcaRuntimeService()
const result = runtime.resolveOrchestrationAgentLauncherForTarget('custom', {})
expect(result).toBeInstanceOf(Promise)
await expect(result).rejects.toThrow()
})
it('reads the launcher resolver before forwarding an absent target', async () => {
const runtime = new OrcaRuntimeService()
const events: string[] = []
const rejection = new Error('original_resolver_rejection')
const resolve = vi.fn<OrcaRuntimeService['resolveTerminalWorkspaceLaunchScope']>(
async function (this: OrcaRuntimeService) {
events.push('original-call')
expect(this).toBe(runtime)
throw rejection
}
)
const replacement = vi.fn()
Object.defineProperty(runtime, 'resolveTerminalWorkspaceLaunchScope', {
configurable: true,
get() {
events.push('callee')
return resolve
}
})
const target = {
get worktree() {
events.push('argument')
Object.defineProperty(runtime, 'resolveTerminalWorkspaceLaunchScope', {
value: replacement
})
return undefined
}
}
const result = runtime.resolveOrchestrationAgentLauncherForTarget('custom', target)
expect(result).toBeInstanceOf(Promise)
await expect(result).rejects.toBe(rejection)
expect(events).toEqual(['callee', 'argument', 'original-call'])
expect(resolve).toHaveBeenCalledExactlyOnceWith(undefined)
expect(replacement).not.toHaveBeenCalled()
})
})
@@ -59,27 +59,6 @@ describe('terminal.recoverPane liveness gate', () => {
expect(createTerminal).toHaveBeenCalledOnce()
})
it('refuses to recreate a shell for an expired lease the host proved is still live', async () => {
// Successful relay reattach followed by binding refusal records live then expired; another spawn would duplicate a proven-live agent.
const tabId = 'tab-proved-live'
const ptyId = 'ssh:ssh-target@@pty-11'
const runtime = createRuntimeWithSshLease(ptyId, tabId)
const paneKey = makePaneKey(tabId, HEADLESS_LEAF_ID)
registerBoundPty(runtime, ptyId, TEST_WORKTREE_ID, 'ssh-target', {
tabId,
leafId: HEADLESS_LEAF_ID
})
const handle = runtime.resolveTerminalPane(paneKey, TEST_WORKTREE_ID).handle
runtime.onPtyExit(ptyId, -1)
runtime.markPtyLivenessLive(ptyId)
const createTerminal = vi.spyOn(runtime, 'createTerminal')
await expect(runtime.recoverTerminalPane(paneKey, TEST_WORKTREE_ID, handle)).rejects.toThrow(
'terminal_not_recoverable'
)
expect(createTerminal).not.toHaveBeenCalled()
})
it('refuses to recreate a shell for an expired lease whose PTY liveness is unverifiable', async () => {
// An unconfirmed SSH -1 exit plus expired lease proves no process exit and cannot authorize replacement.
const tabId = 'tab-unverifiable-gate'
@@ -184,38 +163,6 @@ function replacementTerminal(tabId: string): RuntimeTerminalCreate {
describe('terminal pane recovery uses the current Root state', () => {
afterEach(() => vi.restoreAllMocks())
it('reads the replacement pending map before connected-pane and lease checks', async () => {
const tabId = 'tab-current-recovery-map'
const { runtime, paneKey, handle, recoveryKey } = createRecoveryPane(tabId)
const original = runtime.pendingRecoveries()
original.set(
recoveryKey,
Promise.resolve({
handle: 'term-old-map',
tabId,
leafId: HEADLESS_LEAF_ID,
ptyId: null
})
)
const pending = Promise.resolve({
handle: 'term-current-map',
tabId,
leafId: HEADLESS_LEAF_ID,
ptyId: null
})
const current = new Map([[recoveryKey, pending]])
runtime.usePendingRecoveries(current)
const create = vi.spyOn(runtime, 'createTerminal')
await expect(
runtime.recoverTerminalPane(paneKey, TEST_WORKTREE_ID, handle)
).resolves.toMatchObject({ handle: 'term-current-map' })
expect(runtime.pendingRecoveries()).toBe(current)
expect(current.get(recoveryKey)).toBe(pending)
expect(original.has(recoveryKey)).toBe(true)
expect(create).not.toHaveBeenCalled()
})
it('shares a rejected recovery and removes it before a later retry', async () => {
const tabId = 'tab-rejected-recovery'
const { runtime, paneKey, ptyId, handle, recoveryKey } = createRecoveryPane(tabId)
@@ -243,37 +190,4 @@ describe('terminal pane recovery uses the current Root state', () => {
).resolves.toMatchObject({ handle: 'term-replacement' })
expect(create).toHaveBeenCalledTimes(2)
})
it('keeps a newer pending entry when an earlier recovery settles', async () => {
const tabId = 'tab-newer-recovery'
const { runtime, paneKey, ptyId, handle, recoveryKey } = createRecoveryPane(tabId)
runtime.onPtyExit(ptyId, -1, undefined, { hostExitConfirmed: true })
const creation = deferred<RuntimeTerminalCreate>()
const create = vi.spyOn(runtime, 'createTerminal').mockImplementation(function (
this: PaneRecoveryRuntime,
selector,
options
) {
expect(this).toBe(runtime)
expect(arguments.length).toBe(2)
expect(selector).toBe(`id:${TEST_WORKTREE_ID}`)
expect(options).toEqual({ tabId, leafId: HEADLESS_LEAF_ID, focus: false, reopensPane: true })
return creation.promise
})
const first = runtime.recoverTerminalPane(paneKey, TEST_WORKTREE_ID, handle)
const newer = Promise.resolve({
handle: 'term-newer',
tabId,
leafId: HEADLESS_LEAF_ID,
ptyId: null
})
const current = new Map([[recoveryKey, newer]])
runtime.usePendingRecoveries(current)
creation.resolve(replacementTerminal(tabId))
await expect(first).resolves.toMatchObject({ handle: 'term-replacement' })
expect(create).toHaveBeenCalledOnce()
expect(runtime.pendingRecoveries()).toBe(current)
expect(current.get(recoveryKey)).toBe(newer)
})
})
@@ -5,7 +5,6 @@ import { HeadlessEmulator } from '../daemon/emulation/headless-emulator'
import {
_resetHiddenRendererPtyDeliveryGateForTest,
markHiddenRendererPty,
setRendererPtyDeliveryInterest,
unmarkHiddenRendererPty
} from '../ipc/pty/delivery/pty-hidden-delivery-gate'
import {
@@ -116,116 +115,7 @@ afterEach(() => {
settingsState.terminalModelQueryAuthority = true
})
describe('reply parity for hidden-dropped chunks', () => {
// Pin design replies against bundled xterm/headless, which shares renderer core.
it.each([
['DA1 CSI c', '\x1b[c', ['\x1b[?1;2c']],
['DA1 CSI 0 c variant', '\x1b[0c', ['\x1b[?1;2c']],
['DA2', '\x1b[>c', ['\x1b[>0;276;0c']],
['DSR 5n operating status', '\x1b[5n', ['\x1b[0n']],
['CPR 6n at origin', '\x1b[6n', ['\x1b[1;1R']],
['CPR 6n reports the model cursor position', 'hello\r\nworld\x1b[6n', ['\x1b[2;6R']],
['DECXCPR ?6n', '\x1b[?6n', ['\x1b[?1;1R']],
['DECRPM ?1 DECCKM default', '\x1b[?1$p', ['\x1b[?1;2$y']],
['DECRPM ?6 DECOM default', '\x1b[?6$p', ['\x1b[?6;2$y']],
['DECRPM ?7 DECAWM default', '\x1b[?7$p', ['\x1b[?7;1$y']],
['DECRPM ?25 DECTCEM default', '\x1b[?25$p', ['\x1b[?25;1$y']],
['DECRPM ?1004 focus events default', '\x1b[?1004$p', ['\x1b[?1004;2$y']],
['DECRPM ?1006 SGR mouse default', '\x1b[?1006$p', ['\x1b[?1006;2$y']],
['DECRPM ?1016 SGR pixels default', '\x1b[?1016$p', ['\x1b[?1016;2$y']],
['DECRPM ?1049 alt screen default', '\x1b[?1049$p', ['\x1b[?1049;2$y']],
['DECRPM ?2004 bracketed paste default', '\x1b[?2004$p', ['\x1b[?2004;2$y']],
['DECRPM ?2026 synchronized output default', '\x1b[?2026$p', ['\x1b[?2026;2$y']],
['DECRPM reports a set mode as enabled', '\x1b[?2004h\x1b[?2004$p', ['\x1b[?2004;1$y']],
['DECRPM unknown mode reports 0', '\x1b[?12345$p', ['\x1b[?12345;0$y']],
['DECRQM ANSI insert mode', '\x1b[4$p', ['\x1b[4;2$y']],
['DECRQSS DECSTBM default margins', '\x1bP$qr\x1b\\', ['\x1bP1$r1;24r\x1b\\']],
['DECRQSS DECSTBM after margin set', '\x1b[5;20r\x1bP$qr\x1b\\', ['\x1bP1$r5;20r\x1b\\']],
['DECRQSS DECSCUSR default cursor', '\x1bP$q q\x1b\\', ['\x1bP1$r2 q\x1b\\']],
['DECRQSS DECSCA', '\x1bP$q"q\x1b\\', ['\x1bP1$r0"q\x1b\\']],
['DECRQSS SGR', '\x1bP$qm\x1b\\', ['\x1bP1$r0m\x1b\\']],
['XTVERSION', '\x1b[>0q', ['\x1bP>|xterm.js(6.0.0)\x1b\\']],
['kitty CSI ? u default flags', '\x1b[?u', ['\x1b[?0u']],
['kitty CSI ? u reports pushed flags', '\x1b[=5;1u\x1b[?u', ['\x1b[?5u']]
])('%s', async (_label, chunk, expectedReplies) => {
const { runtime, replies, inputKinds } = createResponderRuntime()
markHiddenRendererPty('pty-q')
runtime.onPtyData('pty-q', chunk, Date.now())
await settle(runtime, 'pty-q')
expect(replies.map((reply) => reply.data)).toEqual(expectedReplies)
expect(replies.every((reply) => reply.ptyId === 'pty-q')).toBe(true)
// Why: a reply written as driving input would read the untouched run as typed.
expect(inputKinds).toEqual(expectedReplies.map(() => 'query-reply'))
})
it.each([
['XTWINOPS', '\x1b[14t'],
['XTGETTCAP', '\x1bP+q544e\x1b\\'],
['DSR ?15n printer status', '\x1b[?15n'],
['DSR ?25n UDK status', '\x1b[?25n'],
['DSR ?26n keyboard status', '\x1b[?26n'],
['DSR ?53n locator status', '\x1b[?53n'],
// No renderer attributes means silence; fabricated colors would revive the default-black OSC-11 bug.
['OSC 10 foreground query', '\x1b]10;?\x07'],
['OSC 11 background query', '\x1b]11;?\x07'],
['OSC 12 cursor-color query', '\x1b]12;?\x1b\\'],
['OSC 4 palette query', '\x1b]4;1;?\x07'],
['DSR ?996n color-scheme query', '\x1b[?996n']
])('stays silent for %s', async (_label, chunk) => {
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-q')
runtime.onPtyData('pty-q', chunk, Date.now())
await settle(runtime, 'pty-q')
expect(replies).toEqual([])
})
})
describe('reply ownership matrix', () => {
it('never answers delivered (unmarked) chunks — the visible xterm owns them', async () => {
const { runtime, replies } = createResponderRuntime()
setTerminalViewAttributes(viewAttributes())
runtime.onPtyData('pty-v', QUERY_BATCH, Date.now())
await settle(runtime, 'pty-v')
expect(replies).toEqual([])
})
it('never answers while renderer delivery interest holds the chunk delivered', async () => {
const { runtime, replies } = createResponderRuntime()
setTerminalViewAttributes(viewAttributes())
markHiddenRendererPty('pty-i')
setRendererPtyDeliveryInterest('pty-i', true)
runtime.onPtyData('pty-i', QUERY_BATCH, Date.now())
await settle(runtime, 'pty-i')
expect(replies).toEqual([])
})
it.each([
['terminalModelQueryAuthority', () => (settingsState.terminalModelQueryAuthority = false)],
['terminalHiddenDeliveryGate', () => (settingsState.terminalHiddenDeliveryGate = false)],
[
'terminalMainSideEffectAuthority',
() => (settingsState.terminalMainSideEffectAuthority = false)
]
])('never answers with kill switch %s off', async (_label, flip) => {
flip()
const { runtime, replies } = createResponderRuntime()
setTerminalViewAttributes(viewAttributes())
markHiddenRendererPty('pty-k')
runtime.onPtyData('pty-k', QUERY_BATCH, Date.now())
await settle(runtime, 'pty-k')
expect(replies).toEqual([])
})
it('yields until every remote view subscriber releases, including repeated release', async () => {
const { runtime, replies } = createResponderRuntime()
setTerminalViewAttributes(viewAttributes())
@@ -286,38 +176,6 @@ describe('reply ownership matrix', () => {
expect(replies).toHaveLength(2)
})
// #32944: a paused view's discarded queries fall back to main only when that view alone owned them.
it('reports sole remote query ownership and answers yielded stateless queries', async () => {
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-yield')
const releaseA = runtime.registerRemoteTerminalViewSubscriber('pty-yield', () => false)
expect(runtime.isSoleRemoteTerminalQueryOwner('pty-yield')).toBe(true)
const releaseB = runtime.registerRemoteTerminalViewSubscriber('pty-yield', () => false)
expect(runtime.isSoleRemoteTerminalQueryOwner('pty-yield')).toBe(false)
releaseB()
releaseA()
runtime.onPtyData('pty-yield', 'prompt', 0)
runtime.answerYieldedTerminalQueries('pty-yield', '\x1b[c')
await settle(runtime, 'pty-yield')
expect(replies.map((reply) => reply.data)).toEqual(['\x1b[?1;2c'])
})
it('keeps raw preview presence separate from terminal query authority', async () => {
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-preview')
const release = runtime.registerRawTerminalViewSubscriber('pty-preview')
expect(runtime.hasRawTerminalViewSubscriber('pty-preview')).toBe(true)
expect(runtime.hasRemoteTerminalViewSubscriber('pty-preview')).toBe(false)
runtime.onPtyData('pty-preview', '\x1b[c', Date.now())
await settle(runtime, 'pty-preview')
expect(replies.map((reply) => reply.data)).toEqual(['\x1b[?1;2c'])
release()
expect(runtime.hasRawTerminalViewSubscriber('pty-preview')).toBe(false)
})
it('yields to a phone-fitted mobile view but answers for a passive desktop-size one', async () => {
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-fit')
@@ -355,20 +213,6 @@ describe('main-side replay guard', () => {
'\x1b[?997;1n'
])
})
it('never answers queries replayed by renderer-buffer hydration', async () => {
setTerminalViewAttributes(viewAttributes())
const { runtime, replies } = createResponderRuntime({
rendererBuffer: { data: `restored screen${QUERY_BATCH}`, cols: 80, rows: 24 }
})
markHiddenRendererPty('pty-hyd')
// Hydrated snapshots must not answer embedded terminal queries.
runtime.onPtyData('pty-hyd', 'live output', Date.now())
await settle(runtime, 'pty-hyd')
expect(replies).toEqual([])
})
})
describe('kitty flag re-seed parity (terminal-query-authority.md §kitty)', () => {
@@ -387,17 +231,6 @@ describe('kitty flag re-seed parity (terminal-query-authority.md §kitty)', () =
await settle(runtime, 'pty-kitty')
expect(replies.map((reply) => reply.data)).toEqual(['\x1b[?5u'])
})
it('answers ?0u when the snapshot carried no flags (fresh-shell paths)', async () => {
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-kitty0')
runtime.seedHeadlessTerminal('pty-kitty0', 'restored prompt')
runtime.onPtyData('pty-kitty0', '\x1b[?u', Date.now())
await settle(runtime, 'pty-kitty0')
expect(replies.map((reply) => reply.data)).toEqual(['\x1b[?0u'])
})
})
describe('ingestion-time ownership capture', () => {
@@ -414,33 +247,11 @@ describe('ingestion-time ownership capture', () => {
expect(replies.map((reply) => reply.data)).toEqual(['\x1b[?1;2c'])
})
it('stays silent when the hidden mark lands after ingestion', async () => {
const { runtime, replies } = createResponderRuntime()
runtime.onPtyData('pty-race2', DA1, Date.now())
markHiddenRendererPty('pty-race2')
await settle(runtime, 'pty-race2')
expect(replies).toEqual([])
})
})
describe('stale writeChain links after dispose', () => {
const DA1 = '\x1b[c'
it('never forwards a queued reply once the PTY state is disposed', async () => {
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-stale')
// Queue a forward-flagged chain link, then dispose before it runs.
runtime.onPtyData('pty-stale', DA1, Date.now())
runtime.onPtyExit('pty-stale', 0)
await new Promise((resolve) => setTimeout(resolve, 0))
expect(replies).toEqual([])
})
it('never injects a stale reply into a successor PTY reusing the session id', async () => {
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-reuse')
@@ -471,60 +282,9 @@ describe('ConPTY DA1 override', () => {
expect(replies.map((reply) => reply.data)).toEqual(['\x1b[?61;4c'])
})
it('answers once at creation and after a repeated native-Windows mark', async () => {
const { runtime, replies } = createResponderRuntime()
markNativeWindowsConptyPty('pty-win-idem')
markHiddenRendererPty('pty-win-idem')
runtime.onPtyData('pty-win-idem', '\x1b[c', Date.now())
await settle(runtime, 'pty-win-idem')
expect(replies.map((reply) => reply.data)).toEqual(['\x1b[?61;4c'])
markNativeWindowsConptyPty('pty-win-idem')
runtime.onPtyData('pty-win-idem', '\x1b[c', Date.now())
await settle(runtime, 'pty-win-idem')
expect(replies.map((reply) => reply.data)).toEqual(['\x1b[?61;4c', '\x1b[?61;4c'])
})
it('lets non-primary device-attribute queries fall through to the core', async () => {
const { runtime, replies } = createResponderRuntime()
markNativeWindowsConptyPty('pty-win2')
markHiddenRendererPty('pty-win2')
runtime.onPtyData('pty-win2', '\x1b[>c', Date.now())
await settle(runtime, 'pty-win2')
expect(replies.map((reply) => reply.data)).toEqual(['\x1b[>0;276;0c'])
})
it('keeps the override silent for delivered chunks', async () => {
const { runtime, replies } = createResponderRuntime()
markNativeWindowsConptyPty('pty-win3')
runtime.onPtyData('pty-win3', '\x1b[c', Date.now())
await settle(runtime, 'pty-win3')
expect(replies).toEqual([])
})
})
describe('HeadlessEmulator forwarding window', () => {
it('forwards replies only for writes flagged forwardQueryReplies', async () => {
const onQueryReply = vi.fn()
const emulator = new HeadlessEmulator({ cols: 80, rows: 24, onQueryReply })
try {
await emulator.write('\x1b[c')
expect(onQueryReply).not.toHaveBeenCalled()
await emulator.write('\x1b[c', { forwardQueryReplies: true })
expect(onQueryReply).toHaveBeenCalledTimes(1)
expect(onQueryReply).toHaveBeenCalledWith('\x1b[?1;2c')
} finally {
emulator.dispose()
}
})
it('scopes the async-fallback forwarding window to the flagged chunk parse', async () => {
const onQueryReply = vi.fn()
const emulator = new HeadlessEmulator({ cols: 80, rows: 24, onQueryReply })
@@ -594,48 +354,6 @@ describe('view-attribute bridge replies (after renderer push)', () => {
expect(replies.map((reply) => reply.data)).toEqual(expectedReplies)
})
it('answers ?996n from palette luminance, not the pushed app mode (dark palette, light app mode)', async () => {
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-lum-dark')
// Separate-light-theme off can leave dark terminal colors in light app mode; match xterm luminance-based ?996n.
setTerminalViewAttributes(viewAttributes({ colorSchemeMode: 'light' }))
runtime.onPtyData('pty-lum-dark', '\x1b[?996n', Date.now())
await settle(runtime, 'pty-lum-dark')
expect(replies.map((reply) => reply.data)).toEqual(['\x1b[?997;1n'])
})
it('answers ?996n light for a light palette regardless of the pushed app mode', async () => {
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-lum-light')
setTerminalViewAttributes(
viewAttributes({
foreground: [0x33, 0x33, 0x33],
background: [0xfa, 0xfa, 0xfa],
colorSchemeMode: 'dark'
})
)
runtime.onPtyData('pty-lum-light', '\x1b[?996n', Date.now())
await settle(runtime, 'pty-lum-light')
expect(replies.map((reply) => reply.data)).toEqual(['\x1b[?997;2n'])
})
it('answers ?996n from OSC-SET-mutated colors like a visible xterm', async () => {
// _reportColorScheme reads the CURRENT theme-service colors, which include
// OSC 10/11 SET mutations — the per-PTY overlays layer the same way.
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-lum-set')
setTerminalViewAttributes(viewAttributes())
runtime.onPtyData('pty-lum-set', '\x1b]11;#ffffff\x07\x1b]10;#101010\x07\x1b[?996n', Date.now())
await settle(runtime, 'pty-lum-set')
expect(replies.map((reply) => reply.data)).toEqual(['\x1b[?997;2n'])
})
it('stays silent before the first push, then answers the same query after it', async () => {
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-first')
@@ -650,21 +368,6 @@ describe('view-attribute bridge replies (after renderer push)', () => {
await settle(runtime, 'pty-first')
expect(replies.map((reply) => reply.data)).toEqual(['\x1b]11;rgb:1e1e/1e1e/2e2e\x1b\\'])
})
it('retrofits cursor options onto already-live emulators when the push lands late', async () => {
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-late')
// Emulator exists before any push: core default DECSCUSR is steady block.
runtime.onPtyData('pty-late', '\x1bP$q q\x1b\\', Date.now())
await settle(runtime, 'pty-late')
expect(replies.map((reply) => reply.data)).toEqual(['\x1bP1$r2 q\x1b\\'])
setTerminalViewAttributes(viewAttributes({ cursorStyle: 'underline', cursorBlink: false }))
runtime.onPtyData('pty-late', '\x1bP$q q\x1b\\', Date.now())
await settle(runtime, 'pty-late')
expect(replies.map((reply) => reply.data)).toEqual(['\x1bP1$r2 q\x1b\\', '\x1bP1$r4 q\x1b\\'])
})
})
describe('per-PTY OSC color SET layering', () => {
@@ -685,60 +388,6 @@ describe('per-PTY OSC color SET layering', () => {
])
})
it('restores a single indexed color via OSC 104;<idx>', async () => {
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-104')
setTerminalViewAttributes(viewAttributes())
runtime.onPtyData('pty-104', '\x1b]4;1;#00ff00\x07\x1b]104;1\x07\x1b]4;1;?\x07', Date.now())
await settle(runtime, 'pty-104')
expect(replies.map((reply) => reply.data)).toEqual(['\x1b]4;1;rgb:cccc/0000/0000\x1b\\'])
})
it('restores the whole indexed table via bare OSC 104', async () => {
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-104all')
setTerminalViewAttributes(viewAttributes())
runtime.onPtyData(
'pty-104all',
'\x1b]4;1;#00ff00;196;#0000ff\x07\x1b]104\x07\x1b]4;1;?;196;?\x07',
Date.now()
)
await settle(runtime, 'pty-104all')
expect(replies.map((reply) => reply.data)).toEqual([
'\x1b]4;1;rgb:cccc/0000/0000\x1b\\',
'\x1b]4;196;rgb:c4c4/8888/4c4c\x1b\\'
])
})
it('layers OSC 10/11/12 SETs and restores them via 110/111/112', async () => {
const { runtime, replies } = createResponderRuntime()
markHiddenRendererPty('pty-special')
setTerminalViewAttributes(viewAttributes())
runtime.onPtyData(
'pty-special',
'\x1b]10;#010203\x07\x1b]11;rgb:ff/ff/ff\x07\x1b]12;#0a0b0c\x07' +
'\x1b]10;?\x07\x1b]11;?\x07\x1b]12;?\x07' +
'\x1b]110\x07\x1b]111\x07\x1b]112\x07' +
'\x1b]10;?\x07\x1b]11;?\x07\x1b]12;?\x07',
Date.now()
)
await settle(runtime, 'pty-special')
expect(replies.map((reply) => reply.data)).toEqual([
'\x1b]10;rgb:0101/0202/0303\x1b\\',
'\x1b]11;rgb:ffff/ffff/ffff\x1b\\',
'\x1b]12;rgb:0a0a/0b0b/0c0c\x1b\\',
'\x1b]10;rgb:d0d0/d0d0/d0d0\x1b\\',
'\x1b]11;rgb:1e1e/1e1e/2e2e\x1b\\',
'\x1b]12;rgb:ffff/9999/0000\x1b\\'
])
})
it('tracks SET mutations parsed from a seed without replying, like renderer replay', async () => {
// Restored OSC SETs update theme state like visible xterm, while replay remains silent.
const { runtime, replies } = createResponderRuntime()
@@ -1,17 +1,10 @@
import { describe, expect, it, vi, afterEach } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { OrcaRuntimeService } from './orca-runtime'
import { getDefaultWorkspaceSession } from '../../shared/constants'
import type { WorkspaceSessionState } from '../../shared/workspace-session/workspace-session-state-types'
import { makePaneKey } from '../../shared/workspace-layout/stable-pane-id'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch'
import { bindPaneFirst } from './orca-runtime-tests/bind-first-pane.test-fixture'
import type {
RuntimePtyTabCloseAuthority,
HeadlessSeedMetadata,
RuntimeTerminalBufferSnapshot
} from './terminal/records/runtime-terminal-state-records'
import type { RuntimeMobileSessionCreateTerminalResult } from '../../shared/runtime-session-contracts'
import type { PtyProviderBufferSnapshot } from '../providers/pty-provider-contract'
import '../../shared/workspace-layout/stable-pane-id'
import '../../shared/layout-owner-switch'
import './orca-runtime-tests/bind-first-pane.test-fixture'
// Reattach restore arrives as RPC data, not onPtyData; seed preview/tail/title without inventing recency or side-effect facts.
@@ -152,40 +145,6 @@ describe('seedTerminalRestoreTail', () => {
expect(terminals[0]!.lastOutputAt).toBe(42)
})
it('keeps a live-tracked title over the payload title', async () => {
const runtime = makeRuntimeWithLeaf()
runtime.onPtyData(PTY_ID, '\x1b]0;live-title\x07', 42)
runtime.seedTerminalRestoreTail(PTY_ID, { lastTitle: 'stale-persisted-title' })
const internals = runtime as unknown as RuntimeRecordInternals
expect(internals.ptysById.get(PTY_ID)!.lastOscTitle).toBe('live-title')
})
it('seeds title-only payloads (no restore text)', async () => {
const runtime = makeRuntimeWithLeaf()
runtime.seedTerminalRestoreTail(PTY_ID, { lastTitle: 'title-only-restore' })
const { terminals } = await runtime.listTerminals(`id:${WORKTREE_ID}`)
expect(terminals[0]!.title).toBe('title-only-restore')
expect(terminals[0]!.preview).toBe('')
expect(terminals[0]!.lastOutputAt).toBeNull()
})
it('republishes session tabs when a seeded title lands', () => {
const runtime = makeRuntimeWithLeaf()
const touch = vi.spyOn(
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: OrcaRuntimeService defines this protected method; the spy only observes it.
runtime as unknown as { touchMobileSessionSnapshotsForPty: (ptyId: string) => void },
'touchMobileSessionSnapshotsForPty'
)
runtime.seedTerminalRestoreTail(PTY_ID, { lastTitle: 'restored-agent-title' })
expect(touch).toHaveBeenCalledWith(PTY_ID)
})
it('caps the parsed suffix of an oversized payload at a line boundary', async () => {
const runtime = makeRuntimeWithLeaf()
@@ -221,420 +180,4 @@ describe('seedTerminalRestoreTail', () => {
runtime.onPtyData(PTY_ID, 'Do you trust this workspace?\r\nPress t to trust\r\n', 2_000_000)
expect(pty.waitBlockedAt).toBe(2_000_000)
})
it('seeds the pty record for a daemon-scoped id with no synced leaf', async () => {
const runtime = makeRuntimeWithLeaf()
const orphanPtyId = `${WORKTREE_ID}@@session-orphan`
runtime.seedTerminalRestoreTail(orphanPtyId, { text: 'orphan restored\r\n' })
const internals = runtime as unknown as RuntimeRecordInternals
const pty = internals.ptysById.get(orphanPtyId)!
expect(pty.preview).toBe('orphan restored')
expect(pty.lastOutputAt).toBeNull()
})
})
describe('terminal query dispatch characterization', () => {
class QueryDispatchRuntime extends OrcaRuntimeService {
prepareMobileCreate() {
this.registerPty('pty-query', 'workspace')
const pty = this.ptysById.get('pty-query')
if (!pty) {
throw new Error('missing_test_pty')
}
this.terminalIdentity.setRecordedSurface(
pty,
'tab',
makePaneKey('tab', '00000000-0000-4000-8000-000000000001'),
undefined
)
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
// Main's layout is the only answer to which pane holds the terminal; it needs a session.
Reflect.set(this, 'store', {})
bindPaneFirst(this, {
worktreeId: 'workspace',
tabId: 'tab',
leafId: '00000000-0000-4000-8000-000000000001',
ptyId: 'pty-query'
})
}
this.pendingMobileTerminalCreatesByKey.set('workspace::tab', {
activate: false,
paired: false,
selectIfNoActiveTab: false
})
return pty
}
ensureSurface() {
return this.ensurePtyBackedMobileSurfaceForRendererTab('workspace', 'tab')
}
resolveAuthority(authority: RuntimePtyTabCloseAuthority) {
return this.resolvePtyTabCloseSurfaceAuthority(authority)
}
}
afterEach(() => vi.restoreAllMocks())
describe('terminal query dispatch', () => {
it('preserves the two arguments in both protected mobile surface lookups', () => {
const runtime = new QueryDispatchRuntime(null)
const pty = runtime.prepareMobileCreate()
const find = vi.fn(function (this: typeof runtime) {
expect(this).toBe(runtime)
expect(arguments.length).toBe(2)
return null
})
Object.defineProperty(runtime, 'findMobileTerminalSurface', { value: find })
Object.defineProperty(runtime, 'findLiveRegisteredPtyForRendererTab', { value: () => pty })
Object.defineProperty(runtime, 'publishPtyBackedMobileSessionTerminal', { value: () => {} })
try {
expect(runtime.ensureSurface()).toBeNull()
expect(find).toHaveBeenCalledTimes(2)
expect(pty.runtimeSessionOwned).toBe(true)
} finally {
runtime.onPtyExit('pty-query', 0)
}
})
it('captures the protected live PTY lookup before reading authority.handle', () => {
const runtime = new QueryDispatchRuntime(null)
const original = vi.fn(function (this: typeof runtime) {
expect(this).toBe(runtime)
expect(arguments.length).toBe(1)
return null
})
const replacement = vi.fn(() => null)
Object.defineProperty(runtime, 'getLivePtyForHandle', {
configurable: true,
get: () => original
})
const authority: RuntimePtyTabCloseAuthority = {
get handle() {
Object.defineProperty(runtime, 'getLivePtyForHandle', { value: replacement })
return 'handle'
},
ptyId: 'pty-query',
incarnationId: null,
worktreeId: 'workspace'
}
expect(runtime.resolveAuthority(authority)).toBeNull()
expect(original).toHaveBeenCalledExactlyOnceWith('handle')
expect(replacement).not.toHaveBeenCalled()
})
it.each([
['seedHeadlessTerminal', 4],
['resizeHeadlessTerminal', 3],
['trackHeadlessTerminalData', 3]
] as const)('captures recovery %s before its argument getters', (method, argc) => {
const runtime = new QueryDispatchRuntime(null)
const original = vi.fn(function (this: typeof runtime) {
expect(this).toBe(runtime)
expect(arguments.length).toBe(argc)
})
const replacement = vi.fn()
Object.defineProperty(runtime, 'disposeHeadlessTerminal', { value: () => {} })
Object.defineProperty(runtime, 'seedHeadlessTerminal', {
configurable: true,
value: () => {}
})
Object.defineProperty(runtime, 'resizeHeadlessTerminal', {
configurable: true,
value: () => {}
})
Object.defineProperty(runtime, 'trackHeadlessTerminalData', {
configurable: true,
value: () => {}
})
Object.defineProperty(runtime, method, { get: () => original })
const replace = () => Object.defineProperty(runtime, method, { value: replacement })
let reads = 0
Object.defineProperty(runtime, 'getTerminalSize', {
value: () => ({
get cols() {
if (method === 'resizeHeadlessTerminal') {
replace()
}
return 100
},
rows: 40
})
})
runtime.replaceHeadlessTerminalFromRendererSnapshotForRecovery(
'pty-query',
{
get data() {
if (++reads === 2 && method === 'seedHeadlessTerminal') {
replace()
}
return 'snapshot'
},
cols: 80,
rows: 24
},
[
{
get data() {
if (method === 'trackHeadlessTerminalData') {
replace()
}
return 'tail'
},
seq: 7
}
]
)
expect(original).toHaveBeenCalledOnce()
expect(replacement).not.toHaveBeenCalled()
})
it.each(['resizeHeadlessTerminal', 'trackHeadlessTerminalData'] as const)(
'preserves invoke-time Root lookup for recovery %s',
(method) => {
const runtime = new QueryDispatchRuntime(null)
const original = vi.fn()
const replacement = vi.fn(function (this: typeof runtime) {
expect(this).toBe(runtime)
expect(arguments.length).toBe(3)
})
Object.defineProperty(runtime, method, { configurable: true, value: original })
const argument = {
get value() {
Object.defineProperty(runtime, method, { value: replacement })
return 'pty-query'
}
}
Object.defineProperty(runtime, 'disposeHeadlessTerminal', { value: () => {} })
Object.defineProperty(runtime, 'seedHeadlessTerminal', { value: () => {} })
Object.defineProperty(runtime, 'getTerminalSize', {
value: () => (method === 'resizeHeadlessTerminal' ? { cols: 80, rows: 24 } : null)
})
runtime.replaceHeadlessTerminalFromRendererSnapshotForRecovery(
argument.value,
{ data: 'snapshot', cols: 80, rows: 24 },
method === 'trackHeadlessTerminalData' ? [{ data: 'tail', seq: 7 }] : []
)
expect(original).not.toHaveBeenCalled()
expect(replacement).toHaveBeenCalledOnce()
expect(replacement).toHaveBeenCalledWith(
'pty-query',
method === 'resizeHeadlessTerminal' ? 80 : 'tail',
method === 'resizeHeadlessTerminal' ? 24 : 7
)
}
)
})
})
describe('terminal query reentry characterization', () => {
class QueryRuntime extends OrcaRuntimeService {
readonly calls: string[] = []
surfaceReads = 0
override getRendererTerminalSerializerGeneration(ptyId: string): number {
this.calls.push(`generation:${ptyId}`)
return 27
}
override getTerminalSize(ptyId: string): { cols: number; rows: number } {
this.calls.push(`grid:${ptyId}`)
return { cols: 100, rows: 40 }
}
protected override disposeHeadlessTerminal(ptyId: string): void {
this.calls.push(`dispose:${ptyId}`)
}
override seedHeadlessTerminal(
ptyId: string,
data: string,
size?: { cols: number; rows: number },
metadata: HeadlessSeedMetadata = {}
): void {
this.calls.push(`seed:${ptyId}:${data}:${size?.cols}:${metadata.cwd}`)
}
protected override resizeHeadlessTerminal(ptyId: string, cols: number, rows: number): void {
this.calls.push(`resize:${ptyId}:${cols}:${rows}`)
}
protected override trackHeadlessTerminalData(ptyId: string, data: string, seq?: number): void {
this.calls.push(`data:${ptyId}:${data}:${seq}`)
}
protected override findMobileTerminalSurface(
_worktreeId: string,
_parentTabId: string,
_options: { requireReady?: boolean } = {}
): RuntimeMobileSessionCreateTerminalResult | null {
this.surfaceReads += 1
return null
}
ptyHandle(ptyId: string): string {
this.registerPty(ptyId, 'repo::/tmp/query-workspace')
const pty = this.ptysById.get(ptyId)
if (!pty) {
throw new Error('missing_test_pty')
}
return this.issuePtyHandle(pty)
}
recoveryState(ptyId: string): { preferred: boolean; hydration: string | undefined } {
return {
preferred: this.providerSnapshotPreferredPtys.has(ptyId),
hydration: this.headlessHydrationState.get(ptyId)
}
}
waitForSurface(signal: AbortSignal): Promise<RuntimeMobileSessionCreateTerminalResult> {
return this.waitForMobileTerminalSurface('repo::/tmp/query-workspace', 'tab-query', {
signal
})
}
get pendingGraphChecks(): number {
return this.graphSyncCallbacks.length
}
}
describe('terminal query runtime re-entry', () => {
it('uses the public renderer generation override for a handle', () => {
const runtime = new QueryRuntime(null)
const handle = runtime.ptyHandle('pty-query')
expect(runtime.getRendererTerminalSerializerGenerationForHandle(handle)).toBe(27)
expect(runtime.calls).toEqual(['generation:pty-query'])
runtime.onPtyExit('pty-query', 0)
})
it('recovers through the current runtime overrides in the original output order', () => {
const runtime = new QueryRuntime(null)
runtime.replaceHeadlessTerminalFromRendererSnapshotForRecovery(
'pty-recovery',
{ data: 'snapshot', cols: 80, rows: 24, cwd: '/tmp/query' },
[{ data: 'tail', seq: 7 }]
)
expect(runtime.calls).toEqual([
'dispose:pty-recovery',
'seed:pty-recovery:snapshot:80:/tmp/query',
'grid:pty-recovery',
'resize:pty-recovery:100:40',
'data:pty-recovery:tail:7'
])
expect(runtime.recoveryState('pty-recovery')).toEqual({ preferred: true, hydration: 'done' })
runtime.onPtyExit('pty-recovery', 0)
})
it('leaves output ownership untouched when the recovery snapshot is empty', () => {
const runtime = new QueryRuntime(null)
runtime.replaceHeadlessTerminalFromRendererSnapshotForRecovery('pty-empty', {
data: '',
cols: 80,
rows: 24
})
expect(runtime.calls).toEqual([])
expect(runtime.recoveryState('pty-empty')).toEqual({ preferred: false, hydration: undefined })
})
it('uses the protected surface override and removes its graph subscription on abort', async () => {
const runtime = new QueryRuntime(null)
const client = new AbortController()
const waiting = runtime.waitForSurface(client.signal)
expect(runtime.surfaceReads).toBe(2)
expect(runtime.pendingGraphChecks).toBe(1)
client.abort()
await expect(waiting).rejects.toThrow('client_disconnected')
expect(runtime.pendingGraphChecks).toBe(0)
})
})
})
describe('terminal view promise characterization', () => {
const snapshot: RuntimeTerminalBufferSnapshot = { data: 'screen', cols: 80, rows: 24 }
it('returns the current available-state promise with the runtime receiver and original arity', () => {
const runtime = new OrcaRuntimeService()
const expected = Promise.resolve(snapshot)
const receivers: OrcaRuntimeService[] = []
const arities: number[] = []
runtime['serializeTerminalBufferFromAvailableState'] = function (
this: OrcaRuntimeService,
...args
) {
receivers.push(this)
arities.push(args.length)
expect(args).toEqual(['view-pty', {}])
return expected
}
expect(runtime.serializeTerminalBuffer('view-pty')).toBe(expected)
expect(receivers).toEqual([runtime])
expect(arities).toEqual([2])
})
it('reads the current fallback after provider completion and keeps provider-first options', async () => {
const runtime = new OrcaRuntimeService()
const provider = Promise.withResolvers<PtyProviderBufferSnapshot | null>()
const receivers: OrcaRuntimeService[] = []
const opts = { scrollbackRows: 12 }
runtime['serializeProviderTerminalBuffer'] = function (this: OrcaRuntimeService, ...args) {
receivers.push(this)
expect(args).toEqual(['view-pty', opts, { timeoutMs: 8000, retireOnTimeout: true }])
return provider.promise
}
const firstFallback = vi.fn(async () => null)
runtime['serializeTerminalBufferFromAvailableState'] = firstFallback
const pending = runtime.serializeAuthoritativeTerminalBuffer('view-pty', opts)
runtime['serializeTerminalBufferFromAvailableState'] = function (
this: OrcaRuntimeService,
...args
) {
receivers.push(this)
expect(args).toEqual(['view-pty', opts])
return Promise.resolve(snapshot)
}
provider.resolve(null)
await expect(pending).resolves.toBe(snapshot)
expect(firstFallback).not.toHaveBeenCalled()
expect(receivers).toEqual([runtime, runtime])
})
it('skips driver reads for empty input and rechecks the current driver before a write', async () => {
const runtime = new OrcaRuntimeService()
const initialDriver = vi.fn(() => ({ kind: 'desktop' as const }))
runtime.getDriver = initialDriver
await expect(runtime.writeTerminalPreviewInput('view-pty', '')).resolves.toBe(false)
expect(initialDriver).not.toHaveBeenCalled()
const receivers: OrcaRuntimeService[] = []
const writeChunks = runtime['writeTerminalInputChunks']
const currentDriver = vi.fn(function (this: OrcaRuntimeService, _ptyId: string) {
receivers.push(this)
return { kind: 'mobile' as const, clientId: 'phone' }
})
runtime['writeTerminalInputChunks'] = vi.fn(function (
this: OrcaRuntimeService,
...args: Parameters<typeof writeChunks>
) {
receivers.push(this)
runtime.getDriver = currentDriver
return writeChunks.apply(this, args)
})
await expect(runtime.writeTerminalPreviewInput('view-pty', 'paste')).resolves.toBe(false)
expect(runtime['writeTerminalInputChunks']).toHaveBeenCalledExactlyOnceWith(
'view-pty',
'paste',
{
inputKind: 'driving',
inputSource: 'desktop'
}
)
expect(initialDriver).not.toHaveBeenCalled()
expect(currentDriver).toHaveBeenCalledExactlyOnceWith('view-pty')
expect(receivers).toEqual([runtime, runtime])
})
})
@@ -1,92 +0,0 @@
import { expect, it, vi } from 'vitest'
import type {
RuntimeMobileSessionTabsResult,
RuntimeMobileSessionTabsSnapshot
} from '../../shared/runtime-types'
await import('./orca-runtime-tests/runtime-test-module-registration.spec')
const { OrcaRuntimeService } = await import('./orca-runtime')
await import('./orca-runtime-tests/orca-runtime-test-lifecycle.spec')
const { store, TEST_WORKTREE_ID } =
await import('./orca-runtime-tests/orca-runtime-test-fixtures.spec')
const { committingSpawn } = await import('./orca-runtime-tests/bind-first-pane.test-fixture')
const retired = {
parentTabId: 'tab',
leafId: 'leaf',
ptyId: 'pty',
terminal: 'term_old',
incarnationId: 'inc'
}
type RuntimeInternals = {
mobileSessionTabsByWorktree: Map<string, RuntimeMobileSessionTabsSnapshot>
storeMobileSessionSnapshot: (
worktreeId: string,
snapshot: RuntimeMobileSessionTabsSnapshot
) => RuntimeMobileSessionTabsSnapshot
}
function seedRuntimeWithStoredProof(): {
runtime: InstanceType<typeof OrcaRuntimeService>
internals: RuntimeInternals
} {
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
// As the PTY host's spawn commit: the new pane is bound before the terminal registers.
spawn: vi.fn(committingSpawn(runtime, async () => ({ id: 'pty-runtime-fallback' }))),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.syncWindowGraph(0, {
tabs: [],
leaves: [],
mobileSessionTabs: [
{
worktree: TEST_WORKTREE_ID,
publicationEpoch: 'headless:active-generation',
snapshotVersion: 7,
activeGroupId: null,
activeTabId: null,
activeTabType: null,
tabs: []
}
]
})
const internals = runtime as unknown as RuntimeInternals
const stored = internals.mobileSessionTabsByWorktree.get(TEST_WORKTREE_ID)!
internals.storeMobileSessionSnapshot(TEST_WORKTREE_ID, {
...stored,
snapshotVersion: stored.snapshotVersion + 1,
retiredTerminalSurfaces: [retired]
})
return { runtime, internals }
}
// Subscribers dedupe epoch/version; emitting a pre-store frame at the stored version would hide proofs until another bump.
it('emits the stored retirement proofs on the frame a runtime-owned create publishes', async () => {
const { runtime, internals } = seedRuntimeWithStoredProof()
const events: RuntimeMobileSessionTabsResult[] = []
const unsubscribe = runtime.onMobileSessionTabsChanged(
(frame) => events.push(frame),
'paired-client'
)
try {
await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, {
activate: false,
select: false,
navigation: 'caller',
clientNavigationId: 'paired-client'
})
const storedAfter = internals.mobileSessionTabsByWorktree.get(TEST_WORKTREE_ID)!
const emitted = events.at(-1)!
expect(storedAfter.retiredTerminalSurfaces).toEqual([retired])
expect(emitted.snapshotVersion).toBe(storedAfter.snapshotVersion)
expect(emitted.retiredTerminalSurfaces).toEqual([retired])
} finally {
unsubscribe()
}
})
@@ -133,33 +133,6 @@ describe('run facts: the controller write funnel', () => {
}
})
it('records stream input from a client', async () => {
const run = await createFreshRun()
await sendTerminalStreamInput(run.runtime, {
terminal: run.handle,
text: 'l',
client: undefined,
isMobile: false
})
expect(run.firstUserInputAt()).not.toBeNull()
})
it('records a dispatched agent prompt before its first write', async () => {
vi.useFakeTimers()
const run = await createFreshRun()
const submission = run.runtime.sendTerminalAgentPrompt(run.handle, 'review this', {
inputKind: 'driving'
})
await vi.runAllTimersAsync()
await submission.catch(() => undefined)
expect(run.firstUserInputAt()).not.toBeNull()
expect(run.writes[0]?.inputRecorded).toBe(true)
})
it('reads a run launched with a prompt as untyped until someone drives it', async () => {
vi.useFakeTimers()
const run = await createFreshRun()
@@ -191,14 +164,6 @@ describe('run facts: the controller write funnel', () => {
expect(run.runtime.terminalRunFacts.readLastDrivingInputAt(PTY_ID)).toBeNull()
})
it('records nothing for a client query reply', async () => {
const run = await createFreshRun()
await run.runtime.sendTerminal(run.handle, { text: 'y' }, { inputKind: 'query-reply' })
expect(run.firstUserInputAt()).toBeNull()
})
it.each([
['a terminal reply', '\x1b[3;4R'],
['focus reports', '\x1b[I\x1b[O'],
@@ -217,14 +182,6 @@ describe('run facts: the controller write funnel', () => {
expect(run.firstUserInputAt()).toBeNull()
})
it('records a reply mixed with a keystroke', async () => {
const run = await createFreshRun()
await run.runtime.sendTerminal(run.handle, { text: '\x1b[3;4Rx' }, { inputKind: 'driving' })
expect(run.firstUserInputAt()).not.toBeNull()
})
it('records dashboard preview typing before the write that could end the process', async () => {
const run = await createFreshRun()
@@ -232,16 +189,6 @@ describe('run facts: the controller write funnel', () => {
expect(run.writes.map((write) => write.inputRecorded)).toEqual([true])
})
it('records nothing for dashboard preview bytes that are only a reply or focus reports', async () => {
const run = await createFreshRun()
await run.runtime.writeTerminalPreviewInput(PTY_ID, '\x1b[3;4R')
await run.runtime.writeTerminalPreviewInput(PTY_ID, '\x1b[O\x1b[I')
expect(run.writes).toHaveLength(2)
expect(run.firstUserInputAt()).toBeNull()
})
})
describe('run facts: a created worktree’s startup writes', () => {
@@ -100,17 +100,6 @@ describe('sendTerminal absence gate for leaf-branch writes', () => {
expect(write).not.toHaveBeenCalled()
})
it('gates agent prompt sends behind the same proven-absence check', async () => {
const probe = vi.fn(async () => false)
const { runtime, handle, write } = await makeRuntimeWithLeafHandle({ probePtyLiveness: probe })
await expect(
runtime.sendTerminalAgentPrompt(handle, 'do the thing', { inputKind: 'driving' })
).rejects.toThrow('terminal_not_writable')
expect(write).not.toHaveBeenCalled()
})
it('proceeds when the probe answers unknown (null) — unknown is not absence', async () => {
const { runtime, handle, write } = await makeRuntimeWithLeafHandle({
probePtyLiveness: async () => null
@@ -126,79 +115,6 @@ describe('sendTerminal absence gate for leaf-branch writes', () => {
expect(write).toHaveBeenCalledWith(STALE_PTY_ID, 'ping', 'driving')
})
it('treats a throwing probe as unknown and proceeds', async () => {
const { runtime, handle, write } = await makeRuntimeWithLeafHandle({
probePtyLiveness: async () => {
throw new Error('probe transport down')
}
})
await expect(
runtime.sendTerminal(handle, { text: 'ping' }, { inputKind: 'driving' })
).resolves.toMatchObject({
accepted: true
})
expect(write).toHaveBeenCalledWith(STALE_PTY_ID, 'ping', 'driving')
})
it('proceeds when the probe answers live (restored session before its pane remounts)', async () => {
const { runtime, handle, write } = await makeRuntimeWithLeafHandle({
probePtyLiveness: async () => true
})
await expect(
runtime.sendTerminal(handle, { text: 'ping' }, { inputKind: 'driving' })
).resolves.toMatchObject({
accepted: true
})
expect(write).toHaveBeenCalledWith(STALE_PTY_ID, 'ping', 'driving')
})
it('proceeds unchanged when the controller exposes no probe', async () => {
const { runtime, handle, write } = await makeRuntimeWithLeafHandle({})
await expect(
runtime.sendTerminal(handle, { text: 'ping' }, { inputKind: 'driving' })
).resolves.toMatchObject({
accepted: true
})
expect(write).toHaveBeenCalledWith(STALE_PTY_ID, 'ping', 'driving')
})
it('never probes when the provider synchronously knows the id (live pty)', async () => {
const probe = vi.fn(async () => false)
const { runtime, handle, write } = await makeRuntimeWithLeafHandle({
probePtyLiveness: probe,
hasPty: (ptyId) => ptyId === STALE_PTY_ID
})
await expect(
runtime.sendTerminal(handle, { text: 'ping' }, { inputKind: 'driving' })
).resolves.toMatchObject({
accepted: true
})
expect(probe).not.toHaveBeenCalled()
expect(write).toHaveBeenCalledWith(STALE_PTY_ID, 'ping', 'driving')
})
it('reuses a proven-absent verdict across repeated sends instead of re-probing', async () => {
const probe = vi.fn(async () => false)
const { runtime, handle } = await makeRuntimeWithLeafHandle({ probePtyLiveness: probe })
await expect(
runtime.sendTerminal(handle, { text: 'a' }, { inputKind: 'driving' })
).rejects.toThrow('terminal_not_writable')
await expect(
runtime.sendTerminal(handle, { text: 'b' }, { inputKind: 'driving' })
).rejects.toThrow('terminal_not_writable')
expect(probe).toHaveBeenCalledTimes(1)
})
it('drops the cached absent verdict once the provider re-learns the id', async () => {
const probe = vi.fn(async () => false)
const livePtyIds = new Set<string>()
@@ -615,37 +531,6 @@ describe('push-on-idle orchestration delivery absence gate', () => {
expect(payloads.some((data) => data.includes('late completion'))).toBe(false)
})
it('keeps messages queued instead of marking a proven-absent pty delivered', async () => {
const { runtime, handle, write, stub } = await makeIdleLeafWithoutPtyRecord({
probePtyLiveness: async () => false
})
stub.insert('lost forever?')
runtime.deliverPendingMessagesForHandle(handle)
await new Promise((resolve) => setTimeout(resolve, 0))
expect(write).not.toHaveBeenCalled()
expect(stub.stageMailboxPointerEnter).not.toHaveBeenCalled()
expect(stub.rows[0].delivered_at).toBeNull()
})
it('still delivers on unknown liveness after the probe resolves', async () => {
const { runtime, handle, write, stub } = await makeIdleLeafWithoutPtyRecord({
probePtyLiveness: async () => null
})
stub.insert('hello')
runtime.deliverPendingMessagesForHandle(handle)
await new Promise((resolve) => setTimeout(resolve, 0))
// Why twice: the probe continuation yields a turn before delivering.
await new Promise((resolve) => setTimeout(resolve, 0))
expect(write).toHaveBeenCalledWith(
STALE_PTY_ID,
expect.stringContaining('You have 1 orchestration message')
)
})
// Single-flight must span pointer through Enter; park new triggers until the sequence watermark advances.
it('delivers once across concurrent probe triggers and an in-window re-trigger, then flushes parked rows', async () => {
vi.useFakeTimers()
@@ -703,45 +588,6 @@ describe('push-on-idle orchestration delivery absence gate', () => {
}
})
it('sync-path double-trigger inside the Enter window delivers the first batch once and parks the rest', async () => {
vi.useFakeTimers()
try {
const probe = vi.fn(async () => null)
const { runtime, handle, write, stub } = await makeIdleLeafWithoutPtyRecord({
probePtyLiveness: probe,
// Provider knows the id: delivery takes the pure synchronous path.
hasPty: (ptyId) => ptyId === STALE_PTY_ID
})
stub.insert('first')
runtime.deliverPendingMessagesForHandle(handle)
stub.insert('second')
runtime.deliverPendingMessagesForHandle(handle)
const pointerWrites = () =>
write.mock.calls.filter(
([, data]) => typeof data === 'string' && data.includes('orchestration check')
)
expect(pointerWrites()).toHaveLength(1)
expect(pointerWrites()[0]?.[1]).toContain('You have 1 orchestration message')
expect(probe).not.toHaveBeenCalled()
// Settle flushes the parked trigger; both still-pending rows are counted,
// while the newer sequence authorizes exactly one fresh pointer.
await vi.advanceTimersByTimeAsync(500)
expect(pointerWrites()).toHaveLength(2)
expect(pointerWrites()[1]?.[1]).toContain('You have 1 orchestration message')
await vi.advanceTimersByTimeAsync(500)
expect(stub.stageMailboxPointerEnter).toHaveBeenCalledTimes(2)
expect(stub.rows.map((row) => row.delivered_at)).toEqual(
stub.rows.map(() => expect.any(String))
)
} finally {
vi.useRealTimers()
}
})
// Cold restore reuses the id; an Enter authorized by the dead incarnation must not reach its replacement.
it('retires an armed Enter when the pty exits and respawns under the same id inside the window', async () => {
vi.useFakeTimers()
@@ -786,33 +632,6 @@ describe('push-on-idle orchestration delivery absence gate', () => {
}
})
it('cleans all delivery state on exit without id reuse — no leak, no stray settle effects', async () => {
vi.useFakeTimers()
try {
const { runtime, handle, write, stub } = await makeIdleLeafWithoutPtyRecord({
probePtyLiveness: async () => null,
hasPty: (ptyId) => ptyId === STALE_PTY_ID
})
stub.insert('first')
runtime.deliverPendingMessagesForHandle(handle)
stub.insert('second')
runtime.deliverPendingMessagesForHandle(handle)
await Promise.resolve()
runtime.onPtyExit(STALE_PTY_ID, 0)
await vi.advanceTimersByTimeAsync(500)
expect(write.mock.calls.filter(([, data]) => data === '\r')).toHaveLength(0)
expect(stub.stageMailboxPointerEnter).toHaveBeenCalledOnce()
expect(stub.releaseMailboxPointerEnter).toHaveBeenCalledOnce()
// No stray settle flushed the parked trigger into the dead pty.
expect(write).toHaveBeenCalledTimes(1)
expect(stub.rows.every((row) => row.delivered_at === null)).toBe(true)
} finally {
vi.useRealTimers()
}
})
// Graph resync replaces leaf objects; read current writable state rather than a stale closure before Enter.
it('does not fire a stale Enter through an orphaned leaf snapshot after resync and exit', async () => {
vi.useFakeTimers()
@@ -364,62 +364,6 @@ describe('subscriber-driven daemon attach (never-activated tab)', () => {
expect(model.attachCalls).toEqual([PTY_ID])
})
it('never attaches SSH-scoped sessions or sessions absent from the daemon inventory', async () => {
const { runtime, model } = setupNeverAttachedDaemonSession({ snapshotCapable: false })
const sshPtyId = 'ssh:conn-9@@relay-pty-4'
const sshRecord = internals(runtime).recordPtyWorktree(sshPtyId, WORKTREE_ID, {
connected: true
})
const sshHandle = internals(runtime).issuePtyHandle(sshRecord)
const absentPtyId = `${WORKTREE_ID}@@99999999`
const absentRecord = internals(runtime).recordPtyWorktree(absentPtyId, WORKTREE_ID, {
connected: true
})
const absentHandle = internals(runtime).issuePtyHandle(absentRecord)
const harness = startMultiplex(runtime)
await vi.waitFor(() => expect(harness.handlers.has(0)).toBe(true))
sendSubscribe(harness, 1, sshHandle, 'client-ssh')
await waitForSubscribed(harness, 1)
expect(model.attachCalls).toEqual([])
// Absent session: subscribing must not create it or surface a new error.
sendSubscribe(harness, 2, absentHandle, 'client-absent')
await waitForSubscribed(harness, 2)
await Promise.resolve()
expect(model.sessions.has(absentPtyId)).toBe(false)
expect(model.emitData(absentPtyId, 'ghost')).toBe(false)
expect(harness.messages.some((message) => message.result?.type === 'error')).toBe(false)
// Unrelated live sessions are untouched by another pty's subscribers.
expect(model.sessions.get(PTY_ID)?.attached).toBe(false)
expect(model.attachCalls).not.toContain(PTY_ID)
expect(model.resizeCalls).toEqual([])
})
it('retries a refused attach for a later subscriber once the daemon learns the session', async () => {
const { runtime, model, handle } = setupNeverAttachedDaemonSession({ snapshotCapable: false })
// Degraded-daemon shape: main knows the record, the daemon does not own the
// id yet, and the controller answers false rather than a no-op success.
model.sessions.delete(PTY_ID)
const harness = startMultiplex(runtime)
await vi.waitFor(() => expect(harness.handlers.has(0)).toBe(true))
sendSubscribe(harness, 1, handle, 'client-a')
await waitForSubscribed(harness, 1)
await vi.waitFor(() => expect(model.attachCalls).toEqual([PTY_ID]))
// A refused attach must not pin sticky success while the stream is blank.
expect(model.emitData(PTY_ID, 'ghost')).toBe(false)
model.sessions.set(PTY_ID, { cols: 100, rows: 30, attached: false, screen: '' })
sendSubscribe(harness, 2, handle, 'client-b')
await waitForSubscribed(harness, 2)
await vi.waitFor(() => expect(model.attachCalls).toEqual([PTY_ID, PTY_ID]))
expect(model.emitData(PTY_ID, 'late daemon hello\r\n')).toBe(true)
await vi.waitFor(() => expect(outputText(harness)).toContain('late daemon hello'))
})
it('retries an existing subscriber when provider inventory becomes ready', async () => {
const { runtime, model, handle } = setupNeverAttachedDaemonSession({
snapshotCapable: false
@@ -446,17 +390,6 @@ describe('subscriber-driven daemon attach (never-activated tab)', () => {
await vi.waitFor(() => expect(outputText(harness)).toContain('after inventory'))
})
it('attaches a surviving session at inventory adoption with no view subscribed', async () => {
const { runtime, model, handle } = setupNeverAttachedDaemonSession({ snapshotCapable: false })
await internals(runtime).refreshPtyWorktreeRecordsWithControllerInventory([], null)
await vi.waitFor(() => expect(model.attachCalls).toEqual([PTY_ID]))
expect(model.emitData(PTY_ID, 'worker still printing\r\n')).toBe(true)
const read = await runtime.readTerminal(handle)
expect(read.tail.join('\n')).toContain('worker still printing')
})
it('streams a survivor a terminal.create retry adopts after a restart', async () => {
const runtime = new OrcaRuntimeService()
const model = createDaemonProviderModel({ snapshotCapable: false })
@@ -519,25 +452,6 @@ describe('subscriber-driven daemon attach (never-activated tab)', () => {
expect(read.tail).toEqual([])
})
it('terminal read falls back to the provider tail for a never-attached live pty', async () => {
const { runtime, handle } = setupNeverAttachedDaemonSession({
snapshotCapable: true,
screen: 'retained daemon line\r\n'
})
const read = await runtime.readTerminal(handle)
expect(read.tail.join('\n')).toContain('retained daemon line')
})
it('terminal read stays empty (not an error) when provider state is unprovable', async () => {
const { runtime, handle } = setupNeverAttachedDaemonSession({ snapshotCapable: false })
const read = await runtime.readTerminal(handle)
expect(read.tail).toEqual([])
})
it('terminal read keeps preferring the ingested model over provider state once attached', async () => {
const { runtime, model, handle } = setupNeverAttachedDaemonSession({
snapshotCapable: true,
@@ -577,19 +491,6 @@ describe('re-attach after a daemon reconnect detached a spawned session', () =>
internals(runtime).notePtyEndpointLoss?.(PTY_ID)
}
it('takes back the stream of a session no view shows', async () => {
const { runtime, model, handle } = setupSpawnedAttachedSession()
loseEndpoint(runtime, model)
await vi.waitFor(() => expect(model.attachCalls).toEqual([PTY_ID]))
expect(model.emitData(PTY_ID, 'background agent output\r\n')).toBe(true)
await expect(
runtime.sendTerminal(handle, { text: 'typed' }, { inputKind: 'driving' })
).resolves.toMatchObject({ accepted: true })
expect(model.writes).toEqual(['typed'])
})
it('holds input sent while the re-attach is in flight instead of refusing it', async () => {
const { runtime, model, handle } = setupSpawnedAttachedSession()
const releaseAttach = model.deferNextAttach(true)
@@ -646,41 +547,6 @@ describe('re-attach after a daemon reconnect detached a spawned session', () =>
expect(model.writes).toEqual([])
})
it('leaves a failed re-attach to the next view or inventory pass, not to each keystroke', async () => {
const { runtime, model, handle } = setupSpawnedAttachedSession()
const releaseAttach = model.deferNextAttach(false)
loseEndpoint(runtime, model)
releaseAttach()
await vi.waitFor(() => expect(model.attachCalls).toEqual([PTY_ID]))
await runtime.sendTerminal(handle, { text: 'a' }, { inputKind: 'driving' }).catch(() => {})
await runtime.sendTerminal(handle, { text: 'b' }, { inputKind: 'driving' }).catch(() => {})
expect(model.attachCalls).toEqual([PTY_ID])
const harness = startMultiplex(runtime)
await vi.waitFor(() => expect(harness.handlers.has(0)).toBe(true))
sendSubscribe(harness, 1, handle)
await waitForSubscribed(harness, 1)
await vi.waitFor(() => expect(model.attachCalls).toEqual([PTY_ID, PTY_ID]))
expect(model.emitData(PTY_ID, 'live again\r\n')).toBe(true)
})
it('redoes an earlier subscriber-driven attach that the loss undid', async () => {
// Never-attached session: the first remote subscribe attaches it (the restored-tab path).
const { runtime, model, handle } = setupNeverAttachedDaemonSession({ snapshotCapable: false })
const harness = startMultiplex(runtime)
await vi.waitFor(() => expect(harness.handlers.has(0)).toBe(true))
sendSubscribe(harness, 1, handle)
await waitForSubscribed(harness, 1)
await vi.waitFor(() => expect(model.attachCalls).toEqual([PTY_ID]))
loseEndpoint(runtime, model)
await vi.waitFor(() => expect(model.attachCalls).toEqual([PTY_ID, PTY_ID]))
expect(model.emitData(PTY_ID, 'second life\r\n')).toBe(true)
await vi.waitFor(() => expect(outputText(harness)).toContain('second life'))
})
it('attaches again when another loss lands while the re-attach is in flight', async () => {
const { runtime, model } = setupSpawnedAttachedSession()
const releaseFirst = model.deferNextAttach(true)
@@ -47,23 +47,6 @@ afterEach(() => {
})
describe("an update's last terminal census", () => {
it('refuses every new terminal until the stop, with a retryable message', async () => {
collect.mockResolvedValue(idle)
const runtime = new OrcaRuntimeService(store)
await expect(census(runtime, updateRecount)).resolves.toMatchObject({
liveSessions: 0,
admissionClosedUntil: expect.any(Number)
})
await expect(runtime.acquireWorktreeTerminalSpawn(target)).rejects.toThrow(
TERMINAL_UPDATE_ADMISSION_CLOSED_MESSAGE
)
// A spawn outside any worktree dies with the stop all the same.
await expect(runtime.acquireWorktreeTerminalSpawn(undefined)).rejects.toThrow(
TERMINAL_UPDATE_ADMISSION_CLOSED_MESSAGE
)
})
it('counts only after a spawn admitted before it has finished starting', async () => {
collect.mockResolvedValue({ ...idle, liveSessions: 1 })
const runtime = new OrcaRuntimeService(store)
@@ -96,19 +79,4 @@ describe("an update's last terminal census", () => {
runtime.acquireWorktreeTerminalSpawn(target, { reattachSessionId: 'pty-live' })
).rejects.toThrow(TERMINAL_UPDATE_ADMISSION_CLOSED_MESSAGE)
})
it('reopens admission when a deferred update asks with 0, and a plain census never closes it', async () => {
collect.mockResolvedValue(idle)
const runtime = new OrcaRuntimeService(store)
await expect(census(runtime, { releaseFinishedAutomationTerminals: true })).resolves.toEqual(
idle
)
;(await runtime.acquireWorktreeTerminalSpawn(target))()
await census(runtime, updateRecount)
await expect(census(runtime, { closeAdmissionForMs: 0 })).resolves.toMatchObject({
admissionClosedUntil: 0
})
;(await runtime.acquireWorktreeTerminalSpawn(target))()
})
})
@@ -1,105 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import type { RuntimeMobileSessionTabsResult } from '../../../../shared/runtime-session-contracts'
import type { RuntimeTerminalOrphanAdoptionRequest } from '../../../../shared/runtime-terminal-contracts'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
import {
GIT_KEY,
addWorkspace,
emptySession,
leaf
} from '../../../../shared/workspace-layout/workspace-layout-session.test-fixture'
import { adoptRuntimeTerminalOrphansFromInventory } from './runtime-terminal-orphan-adoption'
import type { RuntimePtyWorktreeRecord } from '../records/runtime-terminal-state-records'
import { paneOfPty } from '../surface/pty-pane-query'
const TAB = 'tab-1'
const KEPT = leaf(1)
const DUPLICATE = leaf(2)
function duplicateSession(): WorkspaceSessionState {
const session = addWorkspace(emptySession(), GIT_KEY, [
{
id: 'group-1',
tabs: [
{
id: TAB,
leaves: [
[KEPT, 'pty-1'],
[DUPLICATE, 'pty-1']
]
}
]
}
])
return { ...session, terminalPtyIncarnationsByPaneKey: { [`${TAB}:${KEPT}`]: 'inc-1' } }
}
function adoptKeptPane(session: WorkspaceSessionState) {
const request: RuntimeTerminalOrphanAdoptionRequest = {
worktree: GIT_KEY,
expectedTopologyRevision: 0,
claims: [
{ terminal: 'term-1', ptyId: 'pty-1', incarnationId: 'inc-1', tabId: TAB, leafId: KEPT }
]
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: adoption reads only these PTY fields before the persisted-binding check.
const pty = {
ptyId: 'pty-1',
incarnationId: 'inc-1',
worktreeId: GIT_KEY,
connectionId: null,
connected: true,
tabId: null,
paneKey: null
} as RuntimePtyWorktreeRecord
const replayPersistedSurface = vi.fn()
const adopted = adoptRuntimeTerminalOrphansFromInventory({
hostId: LOCAL_EXECUTION_HOST_ID,
request,
workspace: {
id: GIT_KEY,
path: '/Users/dev/orca',
connectionId: null,
repo: null,
folderWorkspace: null
},
inventory: {
livePtyIds: new Set(['pty-1']),
allLivePtyIds: new Set(['pty-1']),
terminalIdentityByPtyId: new Map([
['pty-1', { handle: 'term-1', incarnationId: 'inc-1', wslDistro: null }]
]),
queriedHostIds: new Set([LOCAL_EXECUTION_HOST_ID])
},
repoId: 'repo-1',
worktreeWslDistro: null,
ports: {
readSession: () => ({ session, sessionWorktreeId: GIT_KEY, currentRevision: 0 }),
getPty: () => pty,
paneOf: (record) => paneOfPty(record, () => null),
getLeaves: () => [],
getLeaf: () => undefined,
getDisplayTitle: () => null,
replayPersistedSurface,
recordAdoptedSurface: vi.fn(),
getMobileSnapshots: () => [],
getSession: () => session,
setSession: vi.fn(),
flushSession: async () => {},
hydrateSession: vi.fn(),
notifySessionChanged: vi.fn(),
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the replay path returns this value untouched.
getSnapshot: () => ({}) as RuntimeMobileSessionTabsResult
}
})
return { adopted, replayPersistedSurface }
}
describe('orphan adoption of a terminal main repaired at load', () => {
it('refuses the stored duplicate as a competing owner (the reader that skips the Loader)', async () => {
await expect(adoptKeptPane(duplicateSession()).adopted).rejects.toThrow(
'terminal_orphan_competing_owner'
)
})
})
@@ -1,73 +0,0 @@
import { expect, it } from 'vitest'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
import type { RuntimeTerminalOrphanAdoptionRequest } from '../../../../shared/runtime-terminal-contracts'
import { collectLayoutLeafIdsInOrder } from '../../../../shared/workspace-layout/terminal-pane-tree'
import {
GIT_KEY,
addWorkspace,
emptySession,
leaf
} from '../../../../shared/workspace-layout/workspace-layout-session.test-fixture'
import { buildRuntimeTerminalOrphanSession } from './runtime-terminal-orphan-session-adoption'
import type { RuntimePtyWorktreeRecord } from '../records/runtime-terminal-state-records'
const TAB = 'tab-1'
const HEALTHY = leaf(1)
const ORPHAN = leaf(2)
function adopt(existingLeaves: [string, string?][]) {
const session = addWorkspace(emptySession(), GIT_KEY, [
{ id: 'group-1', tabs: [{ id: TAB, leaves: existingLeaves }] }
])
const claim = {
terminal: 'terminal-orphan',
ptyId: 'pty-orphan',
incarnationId: 'incarnation-orphan',
tabId: TAB,
leafId: ORPHAN
}
const topology: NonNullable<RuntimeTerminalOrphanAdoptionRequest['topology']> = {
tabs: [
{
tabId: TAB,
root: { type: 'leaf', leafId: ORPHAN },
activeLeafId: ORPHAN,
expandedLeafId: null
}
],
groups: [{ id: 'group-1', activeTabId: TAB, tabOrder: [TAB] }]
}
const request: RuntimeTerminalOrphanAdoptionRequest = {
worktree: GIT_KEY,
expectedTopologyRevision: 0,
claims: [claim],
topology
}
return buildRuntimeTerminalOrphanSession({
hostId: LOCAL_EXECUTION_HOST_ID,
session,
sessionWorktreeId: GIT_KEY,
worktreeId: GIT_KEY,
request,
validated: [
{
claim,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the tab already exists, so the builder never reads the PTY record.
pty: {} as RuntimePtyWorktreeRecord,
paneKey: makePaneKey(TAB, ORPHAN)
}
],
topologyTabsById: new Map(topology.tabs.map((tab) => [tab.tabId, tab])),
topologyGroups: topology.groups,
getDisplayTitle: () => null
})
}
it('keeps the healthy sibling pane and its PTY when one split pane is recovered', () => {
const layout = adopt([[HEALTHY, 'pty-healthy']]).terminalLayoutsByTabId[TAB]
expect(collectLayoutLeafIdsInOrder(layout?.root)).toEqual([HEALTHY, ORPHAN])
expect(layout?.ptyIdsByLeafId).toEqual({ [HEALTHY]: 'pty-healthy', [ORPHAN]: 'pty-orphan' })
expect(layout?.activeLeafId).toBe(ORPHAN)
})
@@ -1,136 +0,0 @@
import { expect, it } from 'vitest'
import type { RuntimeTerminalOrphanAdoptionRequest } from '../../../../shared/runtime-terminal-contracts'
import { validateRuntimeTerminalOrphanTopology } from './runtime-terminal-orphan-topology-validation'
function fixture(count: number): RuntimeTerminalOrphanAdoptionRequest {
const ids = Array.from({ length: count }, (_, index) => `tab-${index}`)
return {
worktree: 'folder-workspace',
expectedTopologyRevision: 1,
claims: ids.map((tabId) => ({
tabId,
leafId: tabId,
terminal: tabId,
ptyId: tabId,
incarnationId: tabId
})) as RuntimeTerminalOrphanAdoptionRequest['claims'],
topology: {
tabs: ids.map((tabId) => ({
tabId,
root: { type: 'leaf', leafId: tabId },
activeLeafId: tabId,
expandedLeafId: null
})),
groups: [{ id: 'g', activeTabId: ids[0], tabOrder: ids, recentTabIds: ids.toReversed() }]
}
}
}
it('validates large restored MRU lists with linear tab-order reads', () => {
const request = fixture(1000)
let reads = 0
const group = request.topology!.groups[0]
group.tabOrder = new Proxy(group.tabOrder, {
get(target, key, receiver) {
if (typeof key === 'string' && /^\d+$/.test(key)) {
reads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, key, receiver)
}
})
expect(
validateRuntimeTerminalOrphanTopology(
request,
request.claims.map((claim) => ({ claim }))
).topologyTabsById.size
).toBe(1000)
expect(reads).toBeLessThanOrEqual(3000)
})
it.each(['duplicate', 'foreign-recent', 'foreign-active'])(
'rejects %s group membership',
(kind) => {
const request = fixture(2)
const group = request.topology!.groups[0]
if (kind === 'duplicate') {
group.tabOrder.push(group.tabOrder[0])
}
if (kind === 'foreign-recent') {
group.recentTabIds = ['foreign']
}
if (kind === 'foreign-active') {
group.activeTabId = 'foreign'
}
expect(() =>
validateRuntimeTerminalOrphanTopology(
request,
request.claims.map((claim) => ({ claim }))
)
).toThrow('terminal_orphan_topology_invalid')
}
)
function validate(request: RuntimeTerminalOrphanAdoptionRequest) {
return validateRuntimeTerminalOrphanTopology(
request,
request.claims.map((claim) => ({ claim }))
)
}
type Groups = NonNullable<RuntimeTerminalOrphanAdoptionRequest['topology']>['groups']
function withGroups(count: number, groups: Groups): RuntimeTerminalOrphanAdoptionRequest {
const request = fixture(count)
request.topology!.groups = groups
return request
}
// Tab uniqueness is global across groups; per-group membership alone permits cross-wired duplicate adoption.
it('rejects a tab claimed by two different groups', () => {
expect(() =>
validate(
withGroups(2, [
{ id: 'g1', activeTabId: 'tab-0', tabOrder: ['tab-0', 'tab-1'], recentTabIds: [] },
{ id: 'g2', activeTabId: 'tab-1', tabOrder: ['tab-1'], recentTabIds: [] }
])
)
).toThrow('terminal_orphan_topology_invalid')
})
it('rejects a duplicated group id', () => {
expect(() =>
validate(
withGroups(2, [
{ id: 'g', activeTabId: 'tab-0', tabOrder: ['tab-0'], recentTabIds: [] },
{ id: 'g', activeTabId: 'tab-1', tabOrder: ['tab-1'], recentTabIds: [] }
])
)
).toThrow('terminal_orphan_topology_invalid')
})
// Empty tab order cannot hold its active tab; fail closed instead of selecting an arbitrary pane.
it('rejects an empty tab order', () => {
expect(() =>
validate(withGroups(2, [{ id: 'g', activeTabId: 'tab-0', tabOrder: [], recentTabIds: [] }]))
).toThrow('terminal_orphan_topology_invalid')
})
it('rejects a claimed tab that no group lists', () => {
expect(() =>
validate(
withGroups(2, [{ id: 'g', activeTabId: 'tab-0', tabOrder: ['tab-0'], recentTabIds: [] }])
)
).toThrow('terminal_orphan_topology_invalid')
})
it.each([
['omitted', undefined],
['empty', [] as string[]]
])('accepts %s recentTabIds', (_label, recentTabIds) => {
expect(
validate(
withGroups(2, [{ id: 'g', activeTabId: 'tab-0', tabOrder: ['tab-0', 'tab-1'], recentTabIds }])
).topologyTabsById.size
).toBe(2)
})
@@ -1,107 +0,0 @@
import { describe, expect, it } from 'vitest'
import { mergeTerminalOrphanGroupLayout } from './terminal-orphan-topology'
describe('terminal orphan topology', () => {
it('keeps current host layout while preserving an unrelated proposed subtree', () => {
expect(
mergeTerminalOrphanGroupLayout({
existingLayout: { type: 'leaf', groupId: 'group-live' },
existingGroupIds: ['group-live'],
proposedLayout: {
type: 'split',
direction: 'vertical',
ratio: 0.6,
first: { type: 'leaf', groupId: 'group-old-left' },
second: { type: 'leaf', groupId: 'group-old-right' }
},
proposedGroupIds: ['group-old-left', 'group-old-right'],
mergedGroupIds: ['group-live', 'group-old-left', 'group-old-right']
})
).toEqual({
type: 'split',
direction: 'vertical',
ratio: 0.6,
first: { type: 'leaf', groupId: 'group-live' },
second: {
type: 'split',
direction: 'vertical',
ratio: 0.6,
first: { type: 'leaf', groupId: 'group-old-left' },
second: { type: 'leaf', groupId: 'group-old-right' }
}
})
})
it('grafts proposed groups beside their one current anchor without duplicating it', () => {
expect(
mergeTerminalOrphanGroupLayout({
existingLayout: {
type: 'split',
direction: 'vertical',
first: { type: 'leaf', groupId: 'group-live' },
second: { type: 'leaf', groupId: 'group-other' }
},
existingGroupIds: ['group-live', 'group-other'],
proposedLayout: {
type: 'split',
direction: 'horizontal',
ratio: 0.7,
first: { type: 'leaf', groupId: 'group-live' },
second: { type: 'leaf', groupId: 'group-recovered' }
},
proposedGroupIds: ['group-live', 'group-recovered'],
mergedGroupIds: ['group-live', 'group-other', 'group-recovered']
})
).toEqual({
type: 'split',
direction: 'vertical',
first: {
type: 'split',
direction: 'horizontal',
ratio: 0.7,
first: { type: 'leaf', groupId: 'group-live' },
second: { type: 'leaf', groupId: 'group-recovered' }
},
second: { type: 'leaf', groupId: 'group-other' }
})
})
it('appends new groups when multiple shared anchors make placement ambiguous', () => {
expect(
mergeTerminalOrphanGroupLayout({
existingLayout: {
type: 'split',
direction: 'horizontal',
first: { type: 'leaf', groupId: 'group-a' },
second: { type: 'leaf', groupId: 'group-b' }
},
existingGroupIds: ['group-a', 'group-b'],
proposedLayout: {
type: 'split',
direction: 'vertical',
ratio: 0.7,
first: {
type: 'split',
direction: 'horizontal',
first: { type: 'leaf', groupId: 'group-a' },
second: { type: 'leaf', groupId: 'group-b' }
},
second: { type: 'leaf', groupId: 'group-recovered' }
},
proposedGroupIds: ['group-a', 'group-b', 'group-recovered'],
mergedGroupIds: ['group-a', 'group-b', 'group-recovered']
})
).toEqual({
type: 'split',
direction: 'vertical',
ratio: 0.7,
first: {
type: 'split',
direction: 'horizontal',
first: { type: 'leaf', groupId: 'group-a' },
second: { type: 'leaf', groupId: 'group-b' }
},
second: { type: 'leaf', groupId: 'group-recovered' }
})
})
})
@@ -55,16 +55,6 @@ async function hostTabRows(runtime: ReturnType<typeof restartedHost>) {
}
describe('a terminal record arriving after main composed its panes (switch on)', () => {
it('lists the restored pane once, with the terminal its saved layout binds', async () => {
const runtime = restartedHost('inc-now', () => [
{ id: 'persisted-pty', incarnationId: 'inc-now', connected: true }
])
expect(await hostTabRows(runtime)).toEqual([
{ ptyId: 'persisted-pty', incarnationId: 'inc-now' }
])
})
it('re-composes when the record learns the incarnation the pane is stamped with', async () => {
let incarnationId = 'inc-before'
const runtime = restartedHost('inc-now', () => [
@@ -78,39 +68,4 @@ describe('a terminal record arriving after main composed its panes (switch on)',
{ ptyId: 'persisted-pty', incarnationId: 'inc-now' }
])
})
it('publishes nothing when an unchanged inventory refresh records the same terminal', async () => {
const runtime = restartedHost('inc-now', () => [
{ id: 'persisted-pty', incarnationId: 'inc-now', connected: true }
])
await hostTabRows(runtime)
await new Promise((resolve) => setImmediate(resolve))
const published = vi.spyOn(runtime, 'syncWindowGraph')
await hostTabRows(runtime)
await new Promise((resolve) => setImmediate(resolve))
expect(published).not.toHaveBeenCalled()
})
it('re-composes when the record moves to another workspace', async () => {
const runtime = restartedHost('inc-now', () => [
{ id: 'persisted-pty', incarnationId: 'inc-now', connected: true }
])
// A window publishes, so main republishes its panes when a record changes.
runtime.attachWindow(1)
runtime.syncWindowGraph(1, { tabs: [], leaves: [], mountedLeaves: [] })
expect(await hostTabRows(runtime)).toEqual([
{ ptyId: 'persisted-pty', incarnationId: 'inc-now' }
])
await new Promise((resolve) => setImmediate(resolve))
const published = vi.spyOn(runtime, 'syncWindowGraph')
// Simulate a worktree-identity migration.
runtime['recordPtyWorktree']('persisted-pty', 'repo-1::/tmp/worktree-b')
await new Promise((resolve) => setImmediate(resolve))
expect(runtime['paneOfPty'](runtime['ptysById'].get('persisted-pty')).tabId).toBeNull()
expect(published).toHaveBeenCalled()
})
})
@@ -1,33 +0,0 @@
/** Absent tab rows mean never initialized; explicit empty rows are closed-last-tab tombstones. Re-keying must preserve that distinction. */
import { describe, expect, it } from 'vitest'
import { getDefaultWorkspaceSession } from '../../../../shared/constants'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
import { canonicalizeTerminalSessionWorktreeId } from './workspace-session-worktree-id'
const SOURCE = 'repo-1::/old/path'
const TARGET = 'repo-1::/new/path'
function session(overrides: Partial<WorkspaceSessionState> = {}): WorkspaceSessionState {
return { ...getDefaultWorkspaceSession(), ...overrides }
}
describe('canonicalizeTerminalSessionWorktreeId', () => {
it('does not fabricate a tombstone for a workspace that had no tabs row', () => {
const next = session({ tabsByWorktree: {} })
canonicalizeTerminalSessionWorktreeId(next, SOURCE, TARGET)
expect(Object.hasOwn(next.tabsByWorktree, TARGET)).toBe(false)
})
it('carries a real closed-last-terminal tombstone across the re-key', () => {
// Explicit empty rows preserve the user's closed-last-terminal intent.
const next = session({ tabsByWorktree: { [SOURCE]: [] } })
canonicalizeTerminalSessionWorktreeId(next, SOURCE, TARGET)
expect(Object.hasOwn(next.tabsByWorktree, TARGET)).toBe(true)
expect(next.tabsByWorktree[TARGET]).toEqual([])
expect(Object.hasOwn(next.tabsByWorktree, SOURCE)).toBe(false)
})
})
@@ -1,9 +1,9 @@
import { describe, expect, it, vi } from 'vitest'
import { expect, it, vi } from 'vitest'
import type { RuntimeNotifier } from '../../runtime-notifier-contract'
import { buildMobileTerminalChatFork } from './mobile-terminal-chat-fork'
import './mobile-terminal-chat-fork'
import { prepareStructuredChatCliFork } from '../../../ai-vault/structured-chat-cli-fork'
import { OrchestrationError } from '../../orchestration/orchestration-error'
import { PROJECT_RUNTIME_REPAIR_REQUIRED_CODE } from '../../../../shared/project-runtime-repair-message'
import '../../orchestration/orchestration-error'
import '../../../../shared/project-runtime-repair-message'
import '../../orca-runtime-tests/orca-runtime-test-lifecycle.spec'
import { committingSpawn } from '../../orca-runtime-tests/bind-first-pane.test-fixture'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
@@ -14,65 +14,6 @@ vi.mock('../../../ai-vault/structured-chat-cli-fork', () => ({
}))
const cwd = process.platform === 'win32' ? 'C:\\repo' : '/tmp/repo'
const filePath = process.platform === 'win32' ? 'C:\\sessions\\chat.jsonl' : '/tmp/chat.jsonl'
const runtime = {
platform: process.platform,
pty: { shellOverride: undefined, terminalWindowsWslDistro: null }
}
describe('native chat fork launch recipe', () => {
it('keeps Manual empty args, custom command and environment in an independent fork', () => {
const startup = buildMobileTerminalChatFork({
prepared: {
useRealCodexHome: false,
cliForkSource: {
agent: 'claude',
sourceAgent: 'claude',
sessionId: 'original-chat',
filePath,
cwd,
codexHome: null,
executionHostPlatform: process.platform
}
},
settings: {
agentCmdOverrides: { claude: 'my-claude' },
agentDefaultArgs: { claude: '' },
agentDefaultEnv: { claude: { MY_SETTING: 'present' } }
},
runtime
})
expect(startup.command).toContain('my-claude')
expect(startup.command).toContain('original-chat')
expect(startup.command).toContain('fork-session')
expect(startup.env).toMatchObject({ MY_SETTING: 'present' })
expect(startup.cwd).toBe(cwd)
expect(startup.launchRuntime).toBe(runtime)
expect(startup).not.toHaveProperty('resumeProviderSession')
})
it('opens the host copy without reusing the chat identity', () => {
const startup = buildMobileTerminalChatFork({
prepared: {
useRealCodexHome: false,
forkedSession: { agent: 'opencode', sessionId: 'copy', filePath },
cliForkSource: {
agent: 'opencode',
sourceAgent: 'opencode',
sessionId: 'copy',
filePath,
cwd,
codexHome: null,
executionHostPlatform: process.platform
}
},
settings: { agentDefaultArgs: { opencode: '' } },
runtime
})
expect(startup.command).toContain('copy')
expect(startup.command).not.toContain('original-chat')
expect(startup.resumeProviderSession?.id).toBe('copy')
})
})
it.each([true, false])(
'forks a source-selected chat with saved placement available: %s',
@@ -223,53 +164,3 @@ it.each([true, false])(
)
}
)
it('keeps preparation and runtime failures from starting a terminal', async () => {
await import('../../orca-runtime-tests/runtime-test-module-registration.spec')
const { OrcaRuntimeService } = await import('../../orca-runtime')
const { store, TEST_WORKTREE_ID } =
await import('../../orca-runtime-tests/orca-runtime-test-fixtures.spec')
const runtime = new OrcaRuntimeService(store)
const spawn = vi.fn()
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
const ensure = vi.spyOn(runtime, 'ensureStructuredAgentSessionHost')
for (const [failure, expected] of [
[
new Error(
'Could not confirm this conversation was saved. Try again after the agent responds.'
),
{ message: 'chat_fork_source_unavailable' }
],
[
new Error('This chat has no saved conversation to copy.'),
{ message: 'chat_fork_source_unavailable' }
],
[
new OrchestrationError(PROJECT_RUNTIME_REPAIR_REQUIRED_CODE, 'Repair project settings.'),
{ code: PROJECT_RUNTIME_REPAIR_REQUIRED_CODE }
]
] as const) {
ensure.mockResolvedValueOnce(undefined)
vi.mocked(prepareStructuredChatCliFork).mockRejectedValueOnce(failure)
await expect(
runtime['createMobileChatForkTerminal']({
workspace: {
id: TEST_WORKTREE_ID,
path: cwd,
connectionId: null,
repo: null,
folderWorkspace: null
},
worktreeId: TEST_WORKTREE_ID,
opts: { sourceChatFork: { sessionId: 'unsaved-chat' }, clientMutationId: 'next-create' },
graphEpoch: 0
})
).rejects.toMatchObject(expected)
}
expect(spawn).not.toHaveBeenCalled()
})
@@ -1,183 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import type { Repo } from '../../../../shared/repo-types'
import type { WorktreeMeta } from '../../../../shared/worktree/meta-types'
import type { Worktree } from '../../../../shared/worktree/types'
import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types'
import type { RuntimeStore } from '../../runtime-store-contract'
import type { ResolvedWorktree } from '../../workspaces/catalog/runtime-worktree-path-identity'
import { mergeWorktree } from '../../../ipc/worktree-metadata-merge'
import { resolveCreatedWorktreeTerminalTarget } from './runtime-created-worktree-terminal-target'
import { committingSpawn } from '../../orca-runtime-tests/bind-first-pane.test-fixture'
vi.mock('electron', () => ({
BrowserWindow: { fromId: vi.fn(() => null) },
webContents: { fromId: vi.fn(() => null) },
ipcMain: { on: vi.fn(), removeListener: vi.fn() },
app: { getPath: vi.fn(() => '/tmp'), isPackaged: false }
}))
import { OrcaRuntimeService } from '../../orca-runtime'
class StartupRuntime extends OrcaRuntimeService {
readonly lookup = vi.fn<(selector: string) => Promise<ResolvedWorktree>>()
readonly repoLookup = vi.fn<(selector: string) => Promise<Repo>>()
protected override resolveRepoSelector(selector: string): Promise<Repo> {
return this.repoLookup(selector)
}
protected override resolveGitWorktreeSelector(selector: string): Promise<ResolvedWorktree> {
return this.lookup(selector)
}
}
function owner(hostId: ExecutionHostId = 'local'): Repo {
return {
id: 'repo-1',
path: '/repos/app',
displayName: 'app',
badgeColor: 'blue',
addedAt: 1,
executionHostId: hostId
}
}
function makeFixture(repos: Repo[] = [owner()], hostId: ExecutionHostId = 'local') {
const meta: WorktreeMeta = {
instanceId: 'new-instance',
hostId,
displayName: '',
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0
}
const worktree = mergeWorktree(
'repo-1',
{
path: '/workspaces/new',
head: 'abc123',
branch: 'refs/heads/new',
isBare: false,
isMainWorktree: false
},
meta
)
const metadata: Record<string, WorktreeMeta> = { [worktree.id]: meta }
const lineageById: Record<string, WorktreeLineage> = {}
const store: RuntimeStore = {
getRepos: () => repos,
getRepo: (id) => repos.find((repo) => repo.id === id),
addRepo: vi.fn<RuntimeStore['addRepo']>(),
updateRepo: vi.fn<RuntimeStore['updateRepo']>(),
getAllWorktreeMeta: () => metadata,
getWorktreeMeta: (id) => metadata[id],
getAllWorktreeLineage: () => lineageById,
setWorktreeMeta: (target, patch) =>
(metadata[target.id] = { ...meta, ...metadata[target.id], ...patch }),
removeWorktreeMeta: (target) => {
delete metadata[target.id]
},
getGitHubCache: vi.fn<RuntimeStore['getGitHubCache']>(),
getSettings: () => ({
workspaceDir: '/workspaces',
nestWorkspaces: false,
refreshLocalBaseRefOnWorktreeCreate: false,
branchPrefix: 'none',
branchPrefixCustom: ''
})
}
const runtime = new StartupRuntime(store)
const fallback: ResolvedWorktree = {
...worktree,
git: worktree,
parentWorktreeId: null,
childWorktreeIds: [],
lineage: null
}
runtime.lookup.mockResolvedValue(fallback)
let ptySequence = 0
const spawn = vi.fn(async (_args: { worktreeId?: string; tabId?: string; leafId?: string }) => ({
id: `new-pty-${++ptySequence}`
}))
runtime.setPtyController({
spawn: committingSpawn(runtime, spawn),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
return { runtime, store, worktree, meta, metadata, lineageById, spawn }
}
describe('created worktree terminal evidence', () => {
it('does not reuse creation evidence for a different selector or a replaced instance', async () => {
const { store, worktree, metadata } = makeFixture()
expect(resolveCreatedWorktreeTerminalTarget(store, 'branch:new', worktree)).toBeNull()
expect(
resolveCreatedWorktreeTerminalTarget(store, 'id:repo-1::/elsewhere', worktree)
).toBeNull()
metadata[worktree.id] = { ...metadata[worktree.id]!, instanceId: 'replacement' }
expect(resolveCreatedWorktreeTerminalTarget(store, `id:${worktree.id}`, worktree)).toBeNull()
})
it('rejects another host metadata and ambiguous legacy ownership', async () => {
const { store, worktree, metadata } = makeFixture([owner(), owner('ssh:builder')])
metadata[worktree.id] = { ...metadata[worktree.id]!, hostId: 'ssh:builder' }
expect(resolveCreatedWorktreeTerminalTarget(store, `id:${worktree.id}`, worktree)).toBeNull()
metadata[worktree.id] = { ...metadata[worktree.id]!, hostId: undefined }
const unstamped: Worktree = { ...worktree, hostId: undefined }
expect(resolveCreatedWorktreeTerminalTarget(store, `id:${worktree.id}`, unstamped)).toBeNull()
expect(resolveCreatedWorktreeTerminalTarget(store, `id:${worktree.id}`, worktree)).toBeNull()
})
it('keeps folder workspaces on their existing launch lookup', () => {
const { store, worktree } = makeFixture([{ ...owner(), kind: 'folder' }])
expect(resolveCreatedWorktreeTerminalTarget(store, `id:${worktree.id}`, worktree)).toBeNull()
})
it('preserves stored lineage while rejecting stale parents and cycles', () => {
const { store, worktree, meta, metadata, lineageById } = makeFixture()
const parentId = 'repo-1::/workspaces/parent'
const childId = 'repo-1::/workspaces/child'
metadata[parentId] = { ...meta, instanceId: 'parent-instance' }
metadata[childId] = { ...meta, instanceId: 'child-instance' }
const lineage: WorktreeLineage = {
worktreeId: worktree.id,
worktreeInstanceId: 'new-instance',
parentWorktreeId: parentId,
parentWorktreeInstanceId: 'parent-instance',
origin: 'cli',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
createdAt: 1
}
lineageById[worktree.id] = lineage
lineageById[childId] = {
...lineage,
worktreeId: childId,
worktreeInstanceId: 'child-instance',
parentWorktreeId: worktree.id,
parentWorktreeInstanceId: 'new-instance'
}
const resolve = () => resolveCreatedWorktreeTerminalTarget(store, `id:${worktree.id}`, worktree)
expect(resolve()).toEqual(
expect.objectContaining({ parentWorktreeId: parentId, childWorktreeIds: [childId], lineage })
)
metadata[parentId] = { ...meta, instanceId: 'replacement' }
expect(resolve()).toEqual(expect.objectContaining({ parentWorktreeId: null, lineage: null }))
metadata[parentId] = { ...meta, instanceId: 'parent-instance' }
lineageById[parentId] = {
...lineage,
worktreeId: parentId,
worktreeInstanceId: 'parent-instance',
parentWorktreeId: worktree.id,
parentWorktreeInstanceId: 'new-instance'
}
expect(resolve()).toEqual(expect.objectContaining({ parentWorktreeId: null, lineage: null }))
})
})
@@ -1,67 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES } from '../../../../shared/electron-remote-runtime-client-capabilities'
import { NATIVE_REMOTE_RUNTIME_CLIENT_CAPABILITIES } from '../../../../shared/protocol-version'
import { TERMINAL_IMAGE_VIEWER_CAPABILITY } from '../../../../shared/terminal/images/terminal-image-viewer-capability'
import {
applyClaudeTerminalImageViewerEnv,
terminalImageViewerSupported
} from './terminal-image-viewer-env'
const paired = { caller: { kind: 'paired-device', deviceId: 'phone' } as const }
afterEach(() => vi.unstubAllEnvs())
describe('Claude images follow the launching viewer', () => {
it.each([undefined, [], NATIVE_REMOTE_RUNTIME_CLIENT_CAPABILITIES])(
'keeps older and native clients on old output %#',
(clientCapabilities) => {
expect(
terminalImageViewerSupported({ ...paired, clientKind: 'runtime', clientCapabilities })
).toBe(false)
}
)
it('enables a capable paired Electron viewer', () => {
expect(
terminalImageViewerSupported({
...paired,
clientKind: 'runtime',
clientCapabilities: ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES
})
).toBe(true)
})
it('does not treat a mobile viewer as an image renderer', () => {
expect(
terminalImageViewerSupported({
...paired,
clientKind: 'mobile',
clientCapabilities: [TERMINAL_IMAGE_VIEWER_CAPABILITY]
})
).toBe(false)
})
it.each([
{},
{ ...paired },
{ ...paired, clientCapabilities: [TERMINAL_IMAGE_VIEWER_CAPABILITY] },
{ clientKind: 'runtime' as const, clientCapabilities: [TERMINAL_IMAGE_VIEWER_CAPABILITY] },
{ caller: { kind: 'local-cli' as const }, connectionId: 'remote' },
{ caller: { kind: 'local-cli' as const }, pairedDeviceId: 'remote' },
{ caller: { kind: 'local-cli' as const }, clientId: 'remote' }
])('fails closed for an unclassified or remote context %#', (context) => {
expect(terminalImageViewerSupported(context)).toBe(false)
})
it.each([
{ caller: { kind: 'desktop' as const }, clientKind: 'runtime' as const },
{ caller: { kind: 'local-cli' as const } }
])('keeps same-build local launches on their existing policy %#', (context) => {
expect(terminalImageViewerSupported(context)).toBeUndefined()
})
it('disables automatic image forcing only for unsupported Claude', () => {
vi.stubEnv('CLAUDE_CODE_FORCE_TERMINAL_IMAGES', undefined)
const launch = { launchAgent: 'claude' as const, env: { KEEP: 'yes' } }
expect(applyClaudeTerminalImageViewerEnv(launch, false).env).toEqual({
KEEP: 'yes',
CLAUDE_CODE_FORCE_TERMINAL_IMAGES: '0'
})
expect(launch.env).toEqual({ KEEP: 'yes' })
const codex = { launchAgent: 'codex' as const }
expect(applyClaudeTerminalImageViewerEnv(codex, false)).toBe(codex)
})
})
@@ -70,74 +70,6 @@ export function registerTerminalPtyExitWaiterTests(): void {
})
describe('fresh composer lifetime', () => {
it.each(['exit', 'replacement', 'prune', 'graph-retirement', 'abort', 'timeout'] as const)(
'retires its scanner, listeners and deadline on %s',
async (end) => {
const { runtime, handle } = await createTranscriptPane({
paneTitle: 'shell',
foregroundProcess: 'bash',
data: ''
})
const subscribe = vi.spyOn(runtime, 'subscribeToTerminalData')
const stop = new AbortController()
const removeAbort = vi.spyOn(stop.signal, 'removeEventListener')
vi.useFakeTimers()
try {
const deadlines = vi.spyOn(globalThis, 'setTimeout')
const clearDeadline = vi.spyOn(globalThis, 'clearTimeout')
const pending = runtime.waitForFreshWorkerComposer(handle, 'claude', 60_000, {
signal: stop.signal
})
const outcome = pending.catch((error: unknown) => error)
const settled = vi.fn()
outcome.then(settled)
expect(runtime['terminalWaiters'].forPty(TRANSCRIPT_PANE_PTY_ID)).toHaveLength(1)
expect(subscribe).toHaveBeenCalledOnce()
const deadline =
deadlines.mock.results[deadlines.mock.calls.findIndex(([, ms]) => ms === 60_000)].value
if (end === 'exit') {
runtime.onPtyExit(TRANSCRIPT_PANE_PTY_ID, 137, 'inc-1', { providerExitObserved: true })
} else if (end === 'replacement') {
runtime.onPtySpawned(TRANSCRIPT_PANE_PTY_ID, 'inc-2')
runtime['recordPtyWorktree'](TRANSCRIPT_PANE_PTY_ID, 'wt-1', {
connected: true,
incarnationId: 'inc-2'
})
runtime.onPtyExit(TRANSCRIPT_PANE_PTY_ID, 137, 'inc-1', { providerExitObserved: true })
} else if (end === 'prune') {
runtime['dropDisconnectedPtyRecord'](TRANSCRIPT_PANE_PTY_ID)
} else if (end === 'graph-retirement') {
runtime['restoreHeadlessGraphAuthority']()
} else if (end === 'abort') {
stop.abort()
} else {
await vi.advanceTimersByTimeAsync(60_000)
}
await vi.advanceTimersByTimeAsync(0)
expect(settled).toHaveBeenCalledOnce()
expect(await outcome).toMatchObject({
message:
end === 'exit'
? 'terminal_exited'
: end === 'abort'
? 'request_aborted'
: end === 'timeout'
? 'timeout'
: 'terminal_handle_stale'
})
expect(runtime['terminalWaiters'].forPty(TRANSCRIPT_PANE_PTY_ID)).toHaveLength(0)
expect(
runtime['terminalStreamConsumers']['dataListeners'].get(TRANSCRIPT_PANE_PTY_ID)
).toBeUndefined()
expect(removeAbort).toHaveBeenCalledWith('abort', expect.any(Function))
expect(clearDeadline).toHaveBeenCalledWith(deadline)
} finally {
vi.useRealTimers()
vi.restoreAllMocks()
}
}
)
it.each(['accept-exit', 'exit', 'replacement'] as const)(
'ignores a late acceptance/foreground success after %s',
async (end) => {
@@ -125,22 +125,6 @@ describe('a split of a pane the window draws, in a layout-owner build', () => {
expect(split.tabId).toBe(TAB_ID)
})
it('lists the new pane to phones and session.tabs with no window report', async () => {
const { runtime, handle } = setup()
// A phone already lists the tab, so the new pane must be published, not just hydrated.
await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
const split = await runtime.splitTerminal(handle, { direction: 'vertical' })
const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
expect(
listed.tabs.some(
(tab) =>
tab.type === 'terminal' && tab.parentTabId === TAB_ID && tab.leafId === split.leafId
)
).toBe(true)
})
it('a split no one asked to see leaves the phones’ active tab alone; one asked for takes it', async () => {
const { runtime, handle } = setup()
const listed = () => runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
@@ -153,30 +137,6 @@ describe('a split of a pane the window draws, in a layout-owner build', () => {
expect((await listed()).activeTabId).toBe(`${TAB_ID}::${shown.leafId}`)
})
it('the window’s own split keeps its proposed pane id, folder and size', async () => {
const { runtime, leafIds, spawn } = setup()
const split = await runtime.splitTerminalPane(
{ worktreeId: TEST_WORKTREE_ID, tabId: TAB_ID, leafId: HEADLESS_LEAF_ID },
{
direction: 'horizontal',
newLeafId: NEW_LEAF_ID,
cwd: TEST_WORKTREE_PATH,
initialSize: { cols: 90, rows: 20 },
keepPaneOnStartFailure: true
}
)
expect(split.leafId).toBe(NEW_LEAF_ID)
expect(leafIds()).toEqual([HEADLESS_LEAF_ID, NEW_LEAF_ID])
expect(spawn.mock.calls[0]![0]).toMatchObject({
leafId: NEW_LEAF_ID,
cwd: TEST_WORKTREE_PATH,
cols: 90,
rows: 20
})
})
it('a window split whose terminal fails to start keeps its pane, showing why', async () => {
const { runtime, leafIds, spawn } = setup()
spawn.mockRejectedValueOnce(new Error('shell not found'))
@@ -206,126 +166,4 @@ describe('a split of a pane the window draws, in a layout-owner build', () => {
expect(leafIds()).toEqual([HEADLESS_LEAF_ID])
expect(notifier.closeTerminalPane).not.toHaveBeenCalled()
})
it('starts nothing when main cannot write the pane', async () => {
const { runtime, runtimeStore, handle, splitTerminal, leafIds, spawn } = setup()
Reflect.set(
runtimeStore,
'admitTerminalPane',
vi.fn(async () => 'pane_not_found')
)
await expect(runtime.splitTerminal(handle, { direction: 'vertical' })).rejects.toThrow(
'terminal_split_source_not_found'
)
expect(spawn).not.toHaveBeenCalled()
expect(splitTerminal).not.toHaveBeenCalled()
expect(leafIds()).toEqual([HEADLESS_LEAF_ID])
})
it('starts nothing for a pane-named split of a tab main does not hold', async () => {
const { runtime, spawn } = setup()
await expect(
runtime.splitTerminalPane(
{ worktreeId: TEST_WORKTREE_ID, tabId: 'tab-elsewhere', leafId: HEADLESS_LEAF_ID },
{ direction: 'vertical' }
)
).rejects.toThrow('terminal_split_source_not_found')
expect(spawn).not.toHaveBeenCalled()
})
// The real spawn commit refuses a pane that left the layout while it started (spawn-commit.ts).
function closeWhileStarting(setupResult: ReturnType<typeof setup>, close: () => Promise<void>) {
const { spawn, getSession } = setupResult
spawn.mockImplementationOnce(async (args: { tabId?: string; leafId?: string }) => {
await close()
const root = getSession().terminalLayoutsByTabId[TAB_ID]?.root
if (!root || !collectLayoutLeafIdsInOrder(root).includes(args.leafId!)) {
throw new Error('terminal_split_source_not_found')
}
return { id: 'pty-late' }
})
}
it.each([
['its pane', { kind: 'pane', tabId: TAB_ID, leafId: NEW_LEAF_ID }],
['its tab', { kind: 'tab', tabId: TAB_ID }]
] as const)(
'answers a split once, and leaves nothing, when the user closes %s while it starts',
async (_, target) => {
const context = setup()
const { runtime, getSession } = context
closeWhileStarting(context, () =>
runtime.closeTerminalSurfaceFromRenderer({
worktreeId: TEST_WORKTREE_ID,
target,
reason: 'user'
})
)
await expect(
runtime.splitTerminalPane(
{ worktreeId: TEST_WORKTREE_ID, tabId: TAB_ID, leafId: HEADLESS_LEAF_ID },
{ direction: 'vertical', newLeafId: NEW_LEAF_ID, keepPaneOnStartFailure: true }
)
).rejects.toThrow('terminal_split_source_not_found')
const root = getSession().terminalLayoutsByTabId[TAB_ID]?.root
expect(root ? collectLayoutLeafIdsInOrder(root) : []).not.toContain(NEW_LEAF_ID)
expect(runtime.startStatusOf(makePaneKey(TAB_ID, NEW_LEAF_ID))).toBeNull()
}
)
it('refuses a named pane id another pane already holds, starting nothing', async () => {
const { runtime, spawn } = setup()
await expect(
runtime.splitTerminalPane(
{ worktreeId: TEST_WORKTREE_ID, tabId: TAB_ID, leafId: HEADLESS_LEAF_ID },
{ direction: 'vertical', newLeafId: HEADLESS_LEAF_ID }
)
).rejects.toThrow('pane_exists')
expect(spawn).not.toHaveBeenCalled()
expect(runtime.startStatusOf(makePaneKey(TAB_ID, HEADLESS_LEAF_ID))).toBeNull()
})
it('a window split main cannot start at all still shows its pane, failed, with why', async () => {
const { runtime, leafIds } = setup()
runtime.setPtyController({
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
await expect(
runtime.splitTerminalPane(
{ worktreeId: TEST_WORKTREE_ID, tabId: TAB_ID, leafId: HEADLESS_LEAF_ID },
{ direction: 'vertical', newLeafId: NEW_LEAF_ID, keepPaneOnStartFailure: true }
)
).rejects.toThrow('runtime_unavailable')
expect(leafIds()).toEqual([HEADLESS_LEAF_ID, NEW_LEAF_ID])
expect(runtime.startStatusOf(makePaneKey(TAB_ID, NEW_LEAF_ID))).toEqual({
status: 'failed',
reason: 'runtime_unavailable'
})
})
it('refuses a taken pane id before anything else, even when main cannot start', async () => {
const { runtime } = setup()
runtime.setPtyController({
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
await expect(
runtime.splitTerminalPane(
{ worktreeId: TEST_WORKTREE_ID, tabId: TAB_ID, leafId: HEADLESS_LEAF_ID },
{ direction: 'vertical', newLeafId: HEADLESS_LEAF_ID, keepPaneOnStartFailure: true }
)
).rejects.toThrow('pane_exists')
expect(runtime.startStatusOf(makePaneKey(TAB_ID, HEADLESS_LEAF_ID))).toBeNull()
})
})
@@ -1,19 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import {
setupLayoutOwnerRuntime,
WORKTREE_ID
} from '../../orca-runtime-tests/orca-runtime-layout-owner-start-fixture'
// Why: pinned off, so a run with ORCA_LAYOUT_OWNER=main still checks the switch-off refusal.
vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: false }))
describe("the window's new-tab start with the layout-owner build off", () => {
it('is refused, starting nothing, so the window starts its tab as today', async () => {
const { runtime, spawn } = setupLayoutOwnerRuntime()
await expect(
runtime.startWindowTabTerminal(WORKTREE_ID, { tabId: 'tab', leafId: 'leaf' }, {})
).rejects.toThrow('host_not_owned')
expect(spawn).not.toHaveBeenCalled()
})
})
@@ -79,15 +79,6 @@ Your task ID is: ${taskId}
${task}`
describe('main titles agent tabs from its own hook status', () => {
it('titles a headless host’s tab, which no window ever opened', async () => {
const { runtime, titleOf } = await hostWith([GIT])
runtime.recordHookStatusTitle(status(GIT))
await runtime.hookStatusTitlesSettled()
expect(titleOf(GIT)).toBe('Refactor the auth middleware to use JWT')
})
it('gives a batch’s titles of several workspaces in one rename and one frame', async () => {
const { runtime, apply, titleOf } = await hostWith([GIT, FOLDER])
const frames: WorkspaceLayoutStreamEvent[] = []
@@ -106,36 +97,6 @@ describe('main titles agent tabs from its own hook status', () => {
])
})
it('gives nothing with the setting off', async () => {
const { runtime, apply, titleOf } = await hostWith([GIT], {
titles: false
})
runtime.recordHookStatusTitle(status(GIT))
await runtime.hookStatusTitlesSettled()
expect(apply).not.toHaveBeenCalled()
expect(titleOf(GIT)).toBeUndefined()
})
it('leaves a renamed tab and a quick command’s tab untitled', async () => {
const { runtime, apply, titleOf } = await hostWith([GIT, FOLDER], {
edit: (session) => {
const [git] = session.session.tabsByWorktree[GIT]!
const [folder] = session.session.tabsByWorktree[FOLDER]!
session.session.tabsByWorktree[GIT] = [{ ...git!, customTitle: 'Status tests' }]
session.session.tabsByWorktree[FOLDER] = [{ ...folder!, quickCommandLabel: 'Run tests' }]
}
})
runtime.recordHookStatusTitle(status(GIT))
runtime.recordHookStatusTitle(status(FOLDER))
await runtime.hookStatusTitlesSettled()
expect(apply).not.toHaveBeenCalled()
expect([titleOf(GIT), titleOf(FOLDER)]).toEqual([undefined, undefined])
})
it('has the titles in the first snapshot a window that starts after them reads', async () => {
const { runtime } = await hostWith([GIT])
@@ -181,25 +142,6 @@ describe('main keeps the window’s generated-title rules', () => {
}
})
it('keeps the first prompt’s title', async () => {
const { titleOf } = await titled([
[status(GIT)],
[status(GIT, { prompt: 'Replace this with a later task name' })]
])
expect(titleOf(GIT)).toBe('Refactor the auth middleware to use JWT')
})
it('titles a dispatch from its task, not its preamble', async () => {
const { titleOf } = await titled([
[
status(GIT, {
prompt: dispatch('task-1', 'Implement the detailed worker instructions')
})
]
])
expect(titleOf(GIT)).toBe('Implement the detailed worker')
})
it('replaces a dispatch’s preamble title once main has its labels', async () => {
const prompt = dispatch('task-1', 'Implement the detailed worker instructions')
const host = await titled(
@@ -213,39 +155,6 @@ describe('main keeps the window’s generated-title rules', () => {
expect(host.titleOf(GIT)).toBe('Better worker label')
})
it('keeps a dispatch label when a later non-dispatch prompt comes', async () => {
const { titleOf } = await titled(
[
[
status(GIT, {
prompt: dispatch('task-1', 'Implement the worker instructions')
})
],
[
status(GIT, {
prompt: 'Refactor the auth middleware to use JWT tokens'
})
]
],
labelsOf('task-1', 'Better worker label')
)
expect(titleOf(GIT)).toBe('Better worker label')
})
it('titles a later non-dispatch prompt while sticky labels remain but no title exists', async () => {
const { titleOf } = await titled(
[
[
status(GIT, {
orchestration: labelsOf('task-1', 'Stale worker label')[paneOf(GIT)]
})
]
],
labelsOf('task-1', 'Stale worker label')
)
expect(titleOf(GIT)).toBe('Refactor the auth middleware to use JWT')
})
it('does not re-pin task A’s label onto a later dispatch of task B', async () => {
const { titleOf } = await titled(
[
@@ -264,17 +173,4 @@ describe('main keeps the window’s generated-title rules', () => {
)
expect(titleOf(GIT)).toBe('Implement task B worker instructions')
})
it('titles a paste-sized prompt from its start', async () => {
const prompt = `Fix the flaky status tests ${'large pasted text '.repeat(5000)}`
const { titleOf } = await titled([[status(GIT, { prompt })]])
expect(titleOf(GIT)).toBe('Fix the flaky status tests large pasted')
})
it('keeps a title given in the same batch, as the window’s batch does', async () => {
const { titleOf } = await titled([
[status(GIT), status(GIT, { prompt: 'Replace this with a later task name' })]
])
expect(titleOf(GIT)).toBe('Refactor the auth middleware to use JWT')
})
})
@@ -1,76 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
buildHeadlessTerminalSplitLayout,
countTerminalLayoutLeaves
} from './headless-terminal-split-layout'
describe('buildHeadlessTerminalSplitLayout (headless split persistence)', () => {
it('replaces the provisional admission leaf with the requested split direction', () => {
const next = buildHeadlessTerminalSplitLayout(
{
root: {
type: 'split',
direction: 'vertical',
first: { type: 'leaf', leafId: 'leaf-a' },
second: { type: 'leaf', leafId: 'leaf-c' }
},
activeLeafId: 'leaf-c',
expandedLeafId: null,
ptyIdsByLeafId: { 'leaf-a': 'pty-a', 'leaf-c': 'provisional-pty' }
},
{ leafId: 'leaf-c', ptyId: 'pty-c', splitFromLeafId: 'leaf-a', direction: 'horizontal' }
)
expect(next.root).toEqual({
type: 'split',
direction: 'horizontal',
first: { type: 'leaf', leafId: 'leaf-a' },
second: { type: 'leaf', leafId: 'leaf-c' }
})
expect(next.ptyIdsByLeafId).toEqual({ 'leaf-a': 'pty-a', 'leaf-c': 'pty-c' })
})
it('preserves a sibling committed by a concurrent split', () => {
const next = buildHeadlessTerminalSplitLayout(
{
root: {
type: 'split',
direction: 'vertical',
first: {
type: 'split',
direction: 'horizontal',
first: { type: 'leaf', leafId: 'leaf-a' },
second: { type: 'leaf', leafId: 'leaf-b' }
},
second: { type: 'leaf', leafId: 'leaf-c' }
},
activeLeafId: 'leaf-c',
expandedLeafId: null,
ptyIdsByLeafId: {
'leaf-a': 'pty-a',
'leaf-b': 'pty-b',
'leaf-c': 'provisional-pty'
}
},
{ leafId: 'leaf-c', ptyId: 'pty-c', splitFromLeafId: 'leaf-a', direction: 'vertical' }
)
expect(next.root).toEqual({
type: 'split',
direction: 'horizontal',
first: {
type: 'split',
direction: 'vertical',
first: { type: 'leaf', leafId: 'leaf-a' },
second: { type: 'leaf', leafId: 'leaf-c' }
},
second: { type: 'leaf', leafId: 'leaf-b' }
})
expect(countTerminalLayoutLeaves(next.root)).toBe(3)
expect(next.ptyIdsByLeafId).toEqual({
'leaf-a': 'pty-a',
'leaf-b': 'pty-b',
'leaf-c': 'pty-c'
})
})
})
@@ -18,10 +18,6 @@ const {
HEADLESS_SECOND_LEAF_ID,
TEST_WORKTREE_ID,
TEST_WORKTREE_PATH,
makeFolderProjectGroup,
makeFolderWorkspace,
TEST_FOLDER_WORKSPACE_KEY,
TEST_FOLDER_WORKSPACE_PATH,
makeHeadlessTerminalLayout,
makeRuntimeStoreWithWorkspaceSession,
makeWorkspaceSessionWithHeadlessTerminal
@@ -135,16 +131,6 @@ afterEach(() => {
})
describe('a pane main owns does not vouch for its terminal (switch on)', () => {
it('reads a headless pane disconnected once the host stops listing its shell', async () => {
const { runtime, host } = savedPaneHost('saved-pty')
expect(await rowStatus(runtime, TEST_WORKTREE_ID)).toBe('ready')
host.live.clear()
expect(await rowStatus(runtime, TEST_WORKTREE_ID)).toBe('pending-handle')
expect(record(runtime, 'saved-pty')?.connected).toBe(false)
})
it('reads a windowed pane disconnected, and a window republish does not reconnect it', async () => {
const { runtime, host, publishWindowGraph } = savedPaneHost('saved-pty', { windowed: true })
expect(await rowStatus(runtime, TEST_WORKTREE_ID)).toBe('ready')
@@ -158,24 +144,6 @@ describe('a pane main owns does not vouch for its terminal (switch on)', () => {
expect(await rowStatus(runtime, TEST_WORKTREE_ID)).toBe('pending-handle')
})
it('never shows a restored pane not ready while its shell is listed', async () => {
const { runtime } = savedPaneHost('saved-pty', { windowed: true })
const statuses: string[] = []
const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => {
for (const tab of snapshot.tabs) {
if (tab.type === 'terminal') {
statuses.push(tab.status)
}
}
})
expect(await rowStatus(runtime, TEST_WORKTREE_ID)).toBe('ready')
await new Promise((resolve) => setImmediate(resolve))
unsubscribe()
expect(statuses.filter((status) => status !== 'ready')).toEqual([])
})
it('leaves an SSH pane to the relay: a local sweep is not absence, the relay omitting it is', async () => {
const { runtime, host } = savedPaneHost(SSH_PTY)
runtime.registerPty(SSH_PTY, TEST_WORKTREE_ID, 'ssh-target', {
@@ -195,63 +163,6 @@ describe('a pane main owns does not vouch for its terminal (switch on)', () => {
expect(record(runtime, SSH_PTY)?.connected).toBe(false)
expect(runtime.getPtyLivenessVerdict(SSH_PTY)?.status).not.toBe('exited')
})
it('applies the same rule to a folder workspace', async () => {
const host: Host = { live: new Set(['folder-pty']) }
const session = makeWorkspaceSessionWithHeadlessTerminal({
activeWorktreeId: TEST_FOLDER_WORKSPACE_KEY,
tabsByWorktree: {
[TEST_FOLDER_WORKSPACE_KEY]: [
{
id: 'folder-tab',
ptyId: 'folder-pty',
worktreeId: TEST_FOLDER_WORKSPACE_KEY,
title: 'Folder',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
]
},
terminalLayoutsByTabId: {
'folder-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: 'folder-pty' })
}
})
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(session)
const runtime = new OrcaRuntimeService({
...runtimeStore,
getFolderWorkspaces: () => [makeFolderWorkspace()],
getProjectGroups: () => [makeFolderProjectGroup()]
})
runtime.setPtyController(
hostController(host, TEST_FOLDER_WORKSPACE_PATH, TEST_FOLDER_WORKSPACE_KEY)
)
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
expect(await rowStatus(runtime, TEST_FOLDER_WORKSPACE_KEY)).toBe('ready')
host.live.clear()
expect(await rowStatus(runtime, TEST_FOLDER_WORKSPACE_KEY)).toBe('pending-handle')
})
})
describe('a launch into a pane through a listing gap (switch on)', () => {
it('treats a disconnected terminal main still binds as running until its exit is certified', async () => {
const { runtime, host } = savedPaneHost('saved-pty', { windowed: true })
const paneKey = `host-tab:${HEADLESS_LEAF_ID}`
await rowStatus(runtime, TEST_WORKTREE_ID)
expect(runtime.hasLiveTerminalForPaneKey(paneKey)).toBe(true)
// The host omits the shell once: an agent launch must attach or refuse, never start a second.
host.live.clear()
await rowStatus(runtime, TEST_WORKTREE_ID)
expect(record(runtime, 'saved-pty')?.connected).toBe(false)
expect(runtime.hasLiveTerminalForPaneKey(paneKey)).toBe(true)
await runtime.onPtyExit('saved-pty', 0)
expect(runtime.hasLiveTerminalForPaneKey(paneKey)).toBe(false)
})
})
describe('stopping a workspace through a listing gap (switch on)', () => {
@@ -426,16 +337,3 @@ describe('a terminal registered while a listing is in flight (switch on)', () =>
expect(record(runtime, 'split-pty')?.connected).toBe(true)
})
})
describe('switch off: a window leaf still vouches for its terminal', () => {
it('keeps the window record but withholds a ready mobile handle when the host loses its PTY', async () => {
layoutOwner.isMain = false
const { runtime, host } = savedPaneHost('saved-pty', { windowed: true })
expect(await rowStatus(runtime, TEST_WORKTREE_ID)).toBe('ready')
host.live.clear()
expect(await rowStatus(runtime, TEST_WORKTREE_ID)).toBe('pending-handle')
expect(record(runtime, 'saved-pty')?.connected).toBe(true)
})
})
@@ -1,34 +0,0 @@
import { describe, expect, it } from 'vitest'
import { appendNormalizedToTailBuffer } from './terminal-tail-buffer'
// #11315: an SSH TUI (Pi ask_user) repainting full-width rows pinned Electron main at 100% CPU
// because every character written behind the row end rebuilt the whole row string.
const ESC = '\x1b'
// One frame: climb to the top of a three-row panel and overwrite every row in place.
function overwriteFrame(width: number): string {
return [
`${ESC}[3A\r${'a'.repeat(width)}\n`,
`\r${ESC}[5G${'b'.repeat(width)}\n`,
`\r${'c'.repeat(width)}\n`
].join('')
}
describe('terminal tail redraw cost', () => {
it('keeps the overwritten rows exact', () => {
const width = 40
const seed = appendNormalizedToTailBuffer([], '', `${'x'.repeat(width)}\n`.repeat(3), null)
const next = appendNormalizedToTailBuffer(
seed.lines,
seed.partialLine,
overwriteFrame(width).repeat(2),
seed.redrawCursor
)
expect(next.lines.slice(-3)).toEqual([
'a'.repeat(width),
` ${'b'.repeat(width)}`,
'c'.repeat(width)
])
})
})
@@ -1,7 +1,5 @@
import { registerTerminalListExecutionHostScopeTests } from './terminal-list-execution-host-scope.spec'
import { registerPtyInventoryLivenessVerdictTests } from '../../orca-runtime-tests/pty-inventory-liveness-verdict.spec'
import { registerSessionTabsEmptyWorktreePublicationTests } from '../../orca-runtime-tests/session-tabs-empty-worktree-publication.spec'
registerTerminalListExecutionHostScopeTests()
registerPtyInventoryLivenessVerdictTests()
registerSessionTabsEmptyWorktreePublicationTests()
@@ -1,4 +1,4 @@
import { identityFixture as fixture, LEAF, PANE } from './terminal-identity.test-fixture'
import { identityFixture as fixture, PANE } from './terminal-identity.test-fixture'
import { OrchestrationDb } from '../../orchestration/db/orchestration-db'
import '../../orchestration/db/contract-constants'
import '../../orchestration/db/messages/mailbox-routing-page'
@@ -9,11 +9,10 @@ import { readCrewRuntimeInputs } from '../../orchestration/crew/crew-runtime-wat
import { expect, it, vi } from 'vitest'
import '../../orca-runtime-tests/runtime-test-module-registration.spec'
import { TEST_WORKTREE_ID } from '../../orca-runtime-tests/orca-runtime-test-fixtures.spec'
import { createBoundRun } from '../../rpc/methods/orchestration/runs/runs'
import { resolveOrchestrationCaller } from '../../rpc/methods/orchestration/runs/run-scope'
import { formatOrcaSessionAddress } from '../../../../shared/orca-session-address'
import { testOrcaSessionId } from '../../../../shared/orca-session-address-test-fixture'
import type { RuntimeSyncWindowGraph } from '../../../../shared/runtime-types'
import '../../rpc/methods/orchestration/runs/runs'
import '../../rpc/methods/orchestration/runs/run-scope'
import '../../../../shared/orca-session-address'
import '../../../../shared/orca-session-address-test-fixture'
function countIterations<K, V>(
map: ReadonlyMap<K, V>,
@@ -51,59 +50,6 @@ function countIterations<K, V>(
}
}
it('charges actual native map visits at N/10N for missing, moved and cold identities and leaves fast hits lazy', () => {
const measure = (population: number, indexed: boolean, fast: boolean) => {
const f = fixture()
for (let i = 0; i < population; i++) {
const pane = `tab-${i}:aaaaaaaa-aaaa-4aaa-8aaa-${String(i).padStart(12, '0')}`
f.add(`pty-${i}`, `term_${i}`, pane)
f.stamp(`term_${i}`, `pty-${i}`)
}
const counts = { entries: 0, get: 0, set: 0, delete: 0 }
countIterations(f.owner.handles, counts)
countIterations(f.owner.handleByPtyId, counts)
countIterations(f.owner.leaves, counts)
countIterations(f.owner.ptysById, counts)
const context = indexed ? f.runtime.createTerminalIdentityReadContext() : undefined
for (let i = 0; i < population; i++) {
f.owner.getLivePtyForHandle(fast ? `term_${i}` : `term_missing_${i}`, context)
if (!fast) {
expect(f.owner.getPtyRecordForPaneKey(`missing-${i}:${LEAF}`, context)).toBeNull()
}
}
return counts
}
const baseline = [measure(3, false, false), measure(30, false, false)]
const candidate = [measure(3, true, false), measure(30, true, false)]
expect(baseline[1].entries).toBe(baseline[0].entries * 100)
expect(candidate[1].entries).toBe(candidate[0].entries * 10)
expect(measure(30, true, true).entries).toBe(0)
const cold = (population: number, indexed: boolean) => {
const f = fixture()
for (let i = 0; i < population; i++) {
f.add(`cold-${i}`, `term_cold_${i}`, `cold-pane-${i}`)
f.stamp(`term_cold_${i}`, `cold-${i}`)
f.owner.source.unbindPty(`cold-${i}`)
}
const counts = { entries: 0, get: 0, set: 0, delete: 0 }
countIterations(f.owner.handles, counts)
countIterations(f.owner.ptysById, counts)
countIterations(f.owner.handleByPtyId, counts)
const context = indexed ? f.runtime.createTerminalIdentityReadContext() : undefined
for (let i = 0; i < population; i++) {
expect(f.runtime.getTerminalHandleForPaneKey(`cold-pane-${i}`, context)).toBe(
`term_cold_${i}`
)
expect(f.owner.getLivePtyForHandle(`term_cold_${i}`, context)?.pty.ptyId).toBe(`cold-${i}`)
}
return counts
}
const coldBaseline = [cold(3, false), cold(30, false)]
const coldCandidate = [cold(3, true), cold(30, true)]
expect(coldBaseline[1].entries).toBeGreaterThan(coldBaseline[0].entries * 50)
expect(coldCandidate[1].entries).toBe(coldCandidate[0].entries * 10)
})
it('invalidates actual SSH-held facts on register and finally-release with equal and conflicting claims', async () => {
const f = fixture()
const wake = vi.fn()
@@ -287,112 +233,3 @@ it('reuses raw inputs for new historical parties through the production Crew bou
vi.useRealTimers()
}
})
it('charges retained incarnation materialization through actual pending-pane Run producers and the complete Crew read', async () => {
vi.useFakeTimers({ now: new Date('2026-10-28T20:00:00Z') })
const measure = async (peers: number, inventory: number) => {
const f = fixture()
const db = new OrchestrationDb(':memory:')
f.runtime.setOrchestrationDb(db)
const graph: RuntimeSyncWindowGraph = {
tabs: [
{
tabId: 'tab-cost',
worktreeId: TEST_WORKTREE_ID,
title: 'Cost',
activeLeafId: null,
layout: null
}
],
leaves: []
}
for (let i = 0; i < peers + inventory; i++) {
const leafId = `aaaaaaaa-aaaa-4aaa-8aaa-${String(i).padStart(12, '0')}`
const ptyId = i < peers ? null : `inventory-${i}`
graph.leaves.push({
tabId: 'tab-cost',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: i + 1,
ptyId
})
if (ptyId) {
f.runtime.registerPty(ptyId, TEST_WORKTREE_ID, null, {
incarnationId: `process-${i}`,
tabId: 'tab-cost',
leafId
})
}
}
f.runtime.attachWindow(1)
f.runtime.syncWindowGraph(1, graph)
for (const leaf of graph.leaves.slice(peers)) {
expect(f.runtime.getTerminalHandleForPaneKey(`tab-cost:${leaf.leafId}`)).toMatch(/^term_/)
}
const self = formatOrcaSessionAddress(testOrcaSessionId('eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee'))
const pendingHandles: string[] = []
for (const leaf of graph.leaves.slice(0, peers)) {
graph.tabs[0].activeLeafId = leaf.leafId
f.runtime.syncWindowGraph(1, graph)
const handle = await f.runtime.resolveActiveTerminal()
const caller = resolveOrchestrationCaller(f.runtime, {
callerTerminalHandle: handle,
callerSession: undefined,
requireStablePane: true
})
expect(caller.paneKey).toBe(`tab-cost:${leaf.leafId}`)
expect(f.owner.handles.get(handle)?.ptyId).toBeNull()
const run = createBoundRun(f.runtime, caller, handle, 'Pending coordinator')
db.insertMessage({
from: `run:${run.id}`,
to: self,
subject: 'Retained Run',
body: `peer-${pendingHandles.length}`
})
pendingHandles.push(handle)
}
expect(f.owner.handleByPtyIncarnation.size).toBe(inventory)
const counts = { invocations: 0, yielded: 0 }
const incarnations = f.owner.handleByPtyIncarnation
const iterate = incarnations[Symbol.iterator].bind(incarnations)
Object.defineProperty(incarnations, Symbol.iterator, {
configurable: true,
value: function* () {
counts.invocations++
for (const entry of iterate()) {
counts.yielded++
yield entry
}
}
})
const process = vi.spyOn(f.runtime, 'getTerminalProcessIncarnation')
const model = new CrewReadModel(
(previous) => readCrewRuntimeInputs(f.owner.crewReaders, db, true, 'cost-host', previous),
f.owner.crewChanges.subscribe
)
const stop = model.subscribe(() => {})
try {
const output = model.read(self)
expect(output.status).toBe('ok')
expect(output.sections.talkingWith).toHaveLength(peers)
for (const handle of pendingHandles) {
expect(process.mock.calls.some(([current]) => current === handle)).toBe(true)
}
expect(counts.yielded).toBeLessThanOrEqual(4 * inventory + 4 * peers)
} finally {
stop()
process.mockRestore()
db.close()
}
}
try {
for (const [peers, inventory] of [
[3, 3],
[30, 30]
]) {
await measure(peers, inventory)
}
} finally {
vi.useRealTimers()
}
})
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
import { OrcaRuntimeService } from '../../orca-runtime'
import { getDefaultWorkspaceSession } from '../../../../shared/constants'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
import '../../../../shared/workspace-scope'
import type { FolderWorkspace } from '../../../../shared/folder-workspaces/folder-workspace-types'
import { type ExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import type { PtyProcessInfo } from '../../../providers/pty-process-info'
@@ -49,35 +49,6 @@ export function registerTerminalListExecutionHostScopeTests(): void {
}
}
function makeSshFolderWorkspace() {
return {
id: 'folder-ssh',
projectGroupId: 'group-1',
name: 'SSH folder',
folderPath: '/remote/folder',
connectionId: 'box-2',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
createdAt: 0,
updatedAt: 0
}
}
function makeRuntimeFolderWorkspace() {
return {
...makeSshFolderWorkspace(),
id: 'folder-runtime',
name: 'Runtime folder',
connectionId: 'stale-box',
executionHostId: 'runtime:env-9' as const
}
}
type GraphLeaf = { worktreeId: string; leafId: string; ptyId: string }
function makeRuntime(leaves: GraphLeaf[], store = makeStore()): OrcaRuntimeService {
@@ -111,67 +82,6 @@ export function registerTerminalListExecutionHostScopeTests(): void {
}
describe('listTerminals scope declaration', () => {
it('covers a queried SSH host used only by a folder workspace', async () => {
const baseStore = makeStore()
const folderWorkspace = makeSshFolderWorkspace()
const runtime = makeRuntime([], {
...baseStore,
getRepos: vi.fn(() => [REPOS[0]!]),
getRepo: vi.fn((id: string) => (id === REPOS[0]!.id ? REPOS[0] : undefined)),
getWorkspaceSessionHostIds: vi.fn(() => ['local']),
getFolderWorkspaces: vi.fn(() => [folderWorkspace])
})
runtime.setPtyController({
spawn: vi.fn(async () => ({ id: 'never' })),
write: () => true,
kill: () => true,
listProcesses: vi.fn(async () => []),
listProcessesWithHostScope: vi.fn(async () => ({
processes: [],
hostIds: ['local', 'ssh:box-2']
}))
} as never)
const result = await runtime.listTerminals(`id:${folderWorkspaceKey(folderWorkspace.id)}`)
expect(result.hostScope?.hostIds).toEqual(['ssh:box-2'])
expect(result.hostScope?.omittedHostIds).toEqual(['local'])
})
it('does not claim local coverage for a paired-runtime folder workspace', async () => {
const baseStore = makeStore()
const folderWorkspace = makeRuntimeFolderWorkspace()
const runtime = makeRuntime([], {
...baseStore,
getRepos: vi.fn(() => [REPOS[0]!]),
getRepo: vi.fn((id: string) => (id === REPOS[0]!.id ? REPOS[0] : undefined)),
getWorkspaceSessionHostIds: vi.fn(() => ['local']),
getFolderWorkspaces: vi.fn(() => [folderWorkspace])
})
const result = await runtime.listTerminals(`id:${folderWorkspaceKey(folderWorkspace.id)}`)
expect(result.hostScope?.hostIds).toEqual([])
expect(result.hostScope?.omittedHostIds).toEqual(['local', 'runtime:env-9'])
})
it('marks every known host omitted when process inventory is unverifiable', async () => {
const runtime = makeRuntime([])
runtime.setPtyController({
spawn: vi.fn(async () => ({ id: 'never' })),
write: () => true,
kill: () => true,
listProcesses: vi.fn(async () => {
throw new Error('relay unavailable')
})
} as never)
const result = await runtime.listTerminals()
expect(result.hostScope?.hostIds).toEqual([])
expect(result.hostScope?.omittedHostIds).toEqual(['local', 'ssh:box-1'])
})
// A targeted listing a newer census overtook joins that census instead of answering for no
// host; re-listing would overtake the newer one in turn (two workspaces sleeping at once).
it('answers from a newer concurrent inventory refresh instead of an empty scope', async () => {
@@ -1,5 +1,3 @@
import { registerTerminalPtyExitWaiterTests } from '../launch/terminal-pty-exit-waiter.spec'
import { registerClaudeAgentTeamsPtyExitLeakTests } from '../../orca-runtime-tests/claude-agent-teams-pty-exit-leak.spec'
registerTerminalPtyExitWaiterTests()
registerClaudeAgentTeamsPtyExitLeakTests()
@@ -1,45 +0,0 @@
import { describe, expect, it } from 'vitest'
import { preserveTerminalRetirementProofs } from './mobile-session-terminal-retirement-proof'
import type { RuntimeMobileSessionTabsSnapshot } from '../../../../shared/runtime-session-contracts'
const retired = {
parentTabId: 'tab',
leafId: 'leaf',
ptyId: 'pty',
terminal: 'term',
incarnationId: 'inc'
}
function snapshot(
overrides: Partial<RuntimeMobileSessionTabsSnapshot> = {}
): RuntimeMobileSessionTabsSnapshot {
return {
worktree: 'worktree',
worktreeInstanceId: 'instance',
publicationEpoch: 'epoch',
snapshotVersion: 1,
activeGroupId: null,
activeTabId: null,
activeTabType: null,
tabs: [],
...overrides
}
}
describe('mobile session terminal retirement proofs', () => {
// Identity-less host writes must inherit the occupant, preventing renderer(A) -> host -> renderer(B) proof laundering.
it('does not launder proofs across occupants through an identity-less host write', () => {
const occupantA = snapshot({
worktreeInstanceId: 'instance-a',
retiredTerminalSurfaces: [retired]
})
const hostWrite = preserveTerminalRetirementProofs(
snapshot({ worktreeInstanceId: undefined, snapshotVersion: 2 }),
occupantA
)
expect(hostWrite.worktreeInstanceId).toBe('instance-a')
expect(hostWrite.retiredTerminalSurfaces).toEqual([retired])
const occupantB = snapshot({ worktreeInstanceId: 'instance-b', snapshotVersion: 3 })
expect(preserveTerminalRetirementProofs(occupantB, hostWrite)).toBe(occupantB)
})
})
@@ -1,196 +0,0 @@
import { describe, expect, it } from 'vitest'
import { getDefaultWorkspaceSession } from '../../../../shared/constants'
import type { RuntimeMobileSessionTabsSnapshot } from '../../../../shared/runtime-session-contracts'
import type { RuntimeMobileSessionTerminalTab } from '../../../../shared/runtime-mobile-session-tab-contracts'
import { retireTerminalSurfacesFromSnapshot } from './mobile-session-terminal-retirement'
import { retireTerminalSurfaceFromPersistence } from './mobile-session-terminal-persistence-retirement'
const WORKTREE_ID = 'repo::/worktree'
function splitSnapshot(): RuntimeMobileSessionTabsSnapshot {
const parentLayout = {
root: {
type: 'split' as const,
direction: 'vertical' as const,
first: { type: 'leaf' as const, leafId: 'left' },
second: { type: 'leaf' as const, leafId: 'right' }
},
activeLeafId: 'left',
expandedLeafId: 'left',
ptyIdsByLeafId: { left: 'pty-left', right: 'pty-right' },
buffersByLeafId: { left: 'left buffer', right: 'right buffer' },
titlesByLeafId: { left: 'Left', right: 'Right' }
}
return {
worktree: WORKTREE_ID,
publicationEpoch: 'host',
snapshotVersion: 4,
activeGroupId: 'terminals',
activeTabId: 'terminal::left',
activeTabType: 'terminal',
tabGroups: [
{
id: 'terminals',
activeTabId: 'terminal',
tabOrder: ['terminal', 'notes'],
recentTabIds: ['notes', 'terminal']
},
{ id: 'secondary', activeTabId: 'other', tabOrder: ['other'] }
],
tabGroupLayout: {
type: 'split',
direction: 'horizontal',
first: { type: 'leaf', groupId: 'terminals' },
second: { type: 'leaf', groupId: 'secondary' }
},
tabs: [
{
type: 'terminal',
id: 'terminal::left',
parentTabId: 'terminal',
leafId: 'left',
ptyId: 'pty-left',
title: 'Left',
parentLayout,
isActive: true
},
{
type: 'terminal',
id: 'terminal::right',
parentTabId: 'terminal',
leafId: 'right',
ptyId: 'pty-right',
title: 'Right',
parentLayout,
isActive: false
},
{
type: 'markdown',
id: 'notes',
title: 'Notes',
filePath: '/worktree/notes.md',
relativePath: 'notes.md',
language: 'markdown',
mode: 'edit',
isDirty: false,
sourceFileId: 'notes.md',
sourceFilePath: '/worktree/notes.md',
sourceRelativePath: 'notes.md',
documentVersion: '1',
isActive: false
},
{
type: 'file',
id: 'other',
title: 'Other',
filePath: '/worktree/other.ts',
relativePath: 'other.ts',
language: 'typescript',
isDirty: false,
isActive: false
}
]
}
}
describe('mobile session terminal retirement', () => {
it('retires one split leaf and rewrites every surviving sibling layout', () => {
const result = retireTerminalSurfacesFromSnapshot({
snapshot: splitSnapshot(),
ptyId: 'pty-left'
})
expect(result?.snapshot).toMatchObject({
snapshotVersion: 5,
activeGroupId: 'terminals',
activeTabId: 'terminal::right',
activeTabType: 'terminal'
})
expect(result?.snapshot.tabs).toHaveLength(3)
expect(result?.snapshot.tabs[0]).toMatchObject({
id: 'terminal::right',
isActive: true,
parentLayout: {
root: { type: 'leaf', leafId: 'right' },
activeLeafId: 'right',
expandedLeafId: null,
ptyIdsByLeafId: { right: 'pty-right' },
buffersByLeafId: { right: 'right buffer' },
titlesByLeafId: { right: 'Right' }
}
})
})
it('does not retire an exact surface rebound to a replacement PTY', () => {
const snapshot = splitSnapshot()
const rebound = {
...snapshot,
tabs: snapshot.tabs.map((tab) =>
tab.type === 'terminal' && tab.leafId === 'left'
? {
...tab,
ptyId: 'pty-replacement',
parentLayout: tab.parentLayout
? {
...tab.parentLayout,
ptyIdsByLeafId: {
...tab.parentLayout.ptyIdsByLeafId,
left: 'pty-replacement'
}
}
: undefined
}
: tab
)
}
expect(
retireTerminalSurfacesFromSnapshot({
snapshot: rebound,
ptyId: 'pty-left',
exactSurfaces: [{ parentTabId: 'terminal', leafId: 'left' }]
})
).toBeNull()
})
it('de-persists an exact leaf without removing its live sibling', () => {
const session = {
...getDefaultWorkspaceSession(),
activeTabId: 'terminal',
activeTabIdByWorktree: { [WORKTREE_ID]: 'terminal' },
tabsByWorktree: {
[WORKTREE_ID]: [
{
id: 'terminal',
ptyId: 'pty-left',
worktreeId: WORKTREE_ID,
title: 'Terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
]
},
terminalLayoutsByTabId: {
terminal: (splitSnapshot().tabs[0] as RuntimeMobileSessionTerminalTab).parentLayout!
}
}
const result = retireTerminalSurfaceFromPersistence(session, {
worktreeId: WORKTREE_ID,
parentTabId: 'terminal',
leafId: 'left',
ptyId: 'pty-left'
})
expect(result.tabsByWorktree[WORKTREE_ID]).toEqual([
expect.objectContaining({ id: 'terminal', ptyId: 'pty-right' })
])
expect(result.terminalLayoutsByTabId.terminal).toMatchObject({
root: { type: 'leaf', leafId: 'right' },
activeLeafId: 'right',
ptyIdsByLeafId: { right: 'pty-right' }
})
})
})
@@ -1,75 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import {
HEADLESS_LEAF_ID,
TEST_WORKTREE_ID
} from '../../orca-runtime-tests/orca-runtime-test-fixtures.spec'
import type { RuntimePtyWorktreeRecord } from '../records/runtime-terminal-state-records'
import { SURFACE_TAB_ID, surfaceRuntime } from './terminal-surface-replies.test-fixture'
vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
const COPIES = ['tabId', 'paneKey', 'surfaceRecordedAtGraphSequence'] as const
/** Every touch of a record's pane copies, which a layout-owner build must never make (L80). */
function trapPaneCopies(ptysById: Map<string, RuntimePtyWorktreeRecord>): string[] {
const touched: string[] = []
const trap = (record: RuntimePtyWorktreeRecord): void => {
for (const field of COPIES) {
expect(Object.hasOwn(record, field)).toBe(false)
Object.defineProperty(record, field, {
configurable: true,
get() {
touched.push(`read ${record.ptyId}.${field}`)
throw new Error(`pane copy read: ${field}`)
},
set() {
touched.push(`write ${record.ptyId}.${field}`)
throw new Error(`pane copy write: ${field}`)
}
})
}
}
for (const record of ptysById.values()) {
trap(record)
}
// A record made later (a spawn) is trapped as it is stored.
const set = ptysById.set.bind(ptysById)
ptysById.set = (ptyId, record) => {
const stored = set(ptyId, record)
trap(record)
return stored
}
return touched
}
describe('PTY record pane copies (switch on)', () => {
it('are not needed with no window: main answers before any graph is installed', async () => {
const { runtime } = surfaceRuntime()
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: ptysById is the runtime's PTY registry; the test traps the records it keeps.
const { ptysById } = runtime as unknown as { ptysById: Map<string, RuntimePtyWorktreeRecord> }
const touched = trapPaneCopies(ptysById)
const published = vi.spyOn(runtime, 'syncWindowGraph')
expect(runtime['paneOfPty'](ptysById.get('pty-a')!)).toMatchObject({
tabId: SURFACE_TAB_ID,
paneKey: `${SURFACE_TAB_ID}:${HEADLESS_LEAF_ID}`
})
const listed = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`)
expect(
listed.terminals.map(({ tabId, ptyId, orphaned }) => ({ tabId, ptyId, orphaned }))
).toEqual([
{ tabId: SURFACE_TAB_ID, ptyId: 'pty-a', orphaned: false },
{ tabId: SURFACE_TAB_ID, ptyId: 'pty-b', orphaned: false }
])
for (const terminal of listed.terminals) {
await expect(runtime.showTerminal(terminal.handle)).resolves.toMatchObject({
tabId: SURFACE_TAB_ID
})
}
runtime.registerPty('pty-c', TEST_WORKTREE_ID)
// Answering is not publishing: no graph is installed until a window or the host sends one.
expect(published).not.toHaveBeenCalled()
expect(touched).toEqual([])
})
})
@@ -1,64 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { TerminalLayoutSnapshot } from '../../../../shared/workspace-layout/terminal-tab-types'
import {
resolveTerminalCloseTarget,
type TerminalCloseLayoutCopies
} from './terminal-surface-close'
const PANE = { kind: 'pane', tabId: 'tab-1', leafId: 'leaf-a' } as const
function layout(...leafIds: string[]): TerminalLayoutSnapshot {
const [first, second] = leafIds
return {
root:
first === undefined
? null
: second === undefined
? { type: 'leaf', leafId: first }
: {
type: 'split',
direction: 'horizontal',
first: { type: 'leaf', leafId: first },
second: { type: 'leaf', leafId: second }
},
activeLeafId: first ?? null,
expandedLeafId: null
}
}
function copies(overrides: Partial<TerminalCloseLayoutCopies>): TerminalCloseLayoutCopies {
return {
rendererListsTab: false,
snapshotRows: [],
graphLeafIds: [],
sessionLayout: undefined,
...overrides
}
}
describe('resolveTerminalCloseTarget', () => {
it('never widens a pane close when the owner copy records no panes', () => {
// A renderer tab registered before its panes, with nothing published for it yet.
expect(resolveTerminalCloseTarget(PANE, copies({ rendererListsTab: true }))).toBe('absent')
// A main-owned tab whose layout was saved before its pane mounted, and no other copy.
expect(resolveTerminalCloseTarget(PANE, copies({ sessionLayout: layout() }))).toBe('absent')
})
it('reads past an empty copy to one that records the split', () => {
expect(
resolveTerminalCloseTarget(
PANE,
copies({
sessionLayout: layout(),
snapshotRows: [{ leafId: 'leaf-a', parentLayout: layout('leaf-a', 'leaf-b') }]
})
)
).toBe('pane')
})
it('treats a pane the owner copy lacks as absent', () => {
expect(
resolveTerminalCloseTarget(PANE, copies({ rendererListsTab: true, graphLeafIds: ['leaf-b'] }))
).toBe('absent')
})
})
@@ -1,4 +1,4 @@
import path from 'node:path'
import 'node:path'
import { describe, expect, it, vi } from 'vitest'
import {
@@ -10,17 +10,11 @@ import {
import {
SURFACE_PTYS,
SURFACE_TAB_ID,
captureSurfaceReplies,
mountedSurfaceGraph,
recordedSurfaceReplies,
surfaceRuntime
} from './terminal-surface-replies.test-fixture'
vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
const golden = (name: string): string =>
recordedSurfaceReplies(path.join(__dirname, '__fixtures__', name))
const MOUNTED = Object.keys(SURFACE_PTYS).map((leafId, index) => ({
tabId: SURFACE_TAB_ID,
leafId,
@@ -29,19 +23,6 @@ const MOUNTED = Object.keys(SURFACE_PTYS).map((leafId, index) => ({
}))
describe('terminal replies from main (switch on)', () => {
it('match the window-owned build when the window sends only what it mounts', async () => {
const { runtime } = surfaceRuntime()
runtime.attachWindow(1)
runtime.syncWindowGraph(1, { tabs: [], leaves: [], mountedLeaves: MOUNTED })
// Older readers skip the added verdict; every field they read is unchanged.
expect(await captureSurfaceReplies(runtime, { ignore: ['verdict'] })).toBe(
golden('terminal-surface-replies-desktop.json')
)
const listed = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`)
expect(listed.terminals.map((terminal) => terminal.verdict)).toEqual(['live', 'live'])
})
it('keep answering after the window closes', async () => {
const { runtime } = surfaceRuntime()
runtime.attachWindow(1)
@@ -70,49 +51,6 @@ describe('terminal replies from main (switch on)', () => {
expect(shown).toMatchObject({ tabId: SURFACE_TAB_ID, ptyId: 'pty-a', paneRuntimeId: -1 })
})
it('list a pane split into the layout before any window shows it', async () => {
const { runtime, store, setSession } = surfaceRuntime()
runtime.attachWindow(1)
runtime.syncWindowGraph(1, { tabs: [], leaves: [], mountedLeaves: MOUNTED })
const session = store.getSession()
const layout = session.terminalLayoutsByTabId[SURFACE_TAB_ID]!
setSession({
...session,
terminalLayoutsByTabId: {
[SURFACE_TAB_ID]: {
...layout,
root: {
type: 'split',
direction: 'vertical',
first: layout.root!,
second: { type: 'leaf', leafId: THIRD_LEAF_ID }
},
ptyIdsByLeafId: { ...layout.ptyIdsByLeafId, [THIRD_LEAF_ID]: 'pty-c' }
}
}
})
runtime.registerPty('pty-c', TEST_WORKTREE_ID)
await Promise.resolve()
const listed = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`)
expect(listed.terminals.map((terminal) => terminal.ptyId)).toEqual(['pty-a', 'pty-b', 'pty-c'])
})
it('close a tab by a pane handle in main, with no session row from the window', async () => {
const { runtime, store, killed } = surfaceRuntime()
runtime.attachWindow(1)
runtime.syncWindowGraph(1, { tabs: [], leaves: [], mountedLeaves: MOUNTED })
const listed = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`)
const second = listed.terminals.find((terminal) => terminal.ptyId === 'pty-b')!
await expect(runtime.closeTerminalTab(second.handle)).resolves.toMatchObject({
tabId: SURFACE_TAB_ID,
closeMode: 'tab'
})
expect(store.getSession().tabsByWorktree[TEST_WORKTREE_ID] ?? []).toEqual([])
expect(killed.toSorted()).toEqual(['pty-a', 'pty-b'])
})
it("close a tab by an unbound pane's handle, stopping the tab's other terminals", async () => {
const { runtime, store, setSession, killed } = surfaceRuntime()
runtime.attachWindow(1)
@@ -156,32 +94,4 @@ describe('terminal replies from main (switch on)', () => {
expect(layout?.root).toEqual({ type: 'leaf', leafId: HEADLESS_LEAF_ID })
expect(killed).toEqual(['pty-b'])
})
// Main-owned liveness comes from host inventory, so graph sync must not reconnect disconnected records.
it("a graph sync leaves a disconnected terminal of main's pane disconnected, with no reinstall", async () => {
const { runtime } = surfaceRuntime()
runtime.attachWindow(1)
runtime.syncWindowGraph(1, mountedSurfaceGraph())
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: ptysById is the runtime's PTY registry; the test edits the record it keeps.
const { ptysById } = runtime as unknown as { ptysById: Map<string, { connected: boolean }> }
ptysById.get('pty-a')!.connected = false
const installs: { depth: number; connected: boolean | undefined }[] = []
let depth = 0
const sync = runtime.syncWindowGraph.bind(runtime)
vi.spyOn(runtime, 'syncWindowGraph').mockImplementation((windowId, graph) => {
installs.push({ depth, connected: ptysById.get('pty-a')?.connected })
depth += 1
try {
return sync(windowId, graph)
} finally {
depth -= 1
}
})
runtime.syncWindowGraph(1, mountedSurfaceGraph())
await Promise.resolve()
expect(installs).toEqual([{ depth: 0, connected: false }])
expect(ptysById.get('pty-a')?.connected).toBe(false)
})
})
@@ -1,122 +0,0 @@
import '../../orca-runtime-tests/orca-runtime-test-lifecycle.spec'
import { describe, expect, it, vi } from 'vitest'
import '../../orca-runtime-tests/runtime-test-module-registration.spec'
import { OrcaRuntimeService } from '../../orca-runtime'
import {
HEADLESS_LEAF_ID,
TEST_WORKTREE_ID,
makeRuntimeStoreWithWorkspaceSession,
makeWorkspaceSessionWithHeadlessTerminal
} from '../../orca-runtime-tests/orca-runtime-test-fixtures.spec'
import type { RuntimeNotifier } from '../../runtime-notifier-contract'
// After a restart, a reconnecting client restarts each saved tab's terminal. The mobile row for
// that tab must carry the tab's saved name, not only the new terminal's own title.
function notifier(): RuntimeNotifier {
return {
worktreesChanged() {},
reposChanged() {},
activateWorktree() {},
createTerminal() {},
splitTerminal() {},
renameTerminal() {},
focusTerminal() {},
closeTerminal() {},
terminalFitOverrideChanged() {},
terminalDriverChanged() {}
}
}
async function phoneSurface(runtime: OrcaRuntimeService) {
const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
const surface = listed.tabs.find(
(tab) => tab.type === 'terminal' && tab.parentTabId === 'host-tab'
)
return surface?.type === 'terminal' ? surface : undefined
}
function runtimeWithSession(savedName?: string | null): OrcaRuntimeService {
const session = makeWorkspaceSessionWithHeadlessTerminal()
if (savedName !== undefined) {
session.tabsByWorktree[TEST_WORKTREE_ID]![0]!.customTitle = savedName
}
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(session)
return new OrcaRuntimeService(runtimeStore)
}
function controller(prefix: string) {
let spawned = 0
return {
spawn: vi.fn(async () => ({ id: `${prefix}-${++spawned}` })),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
}
}
describe('a saved tab name after its terminal is restarted', () => {
it.each([
['with a notifier', true],
['headless', false]
])('reaches the phone %s', async (_label, withNotifier) => {
const runtime = runtimeWithSession('Saved name')
if (withNotifier) {
runtime.setNotifier(notifier())
}
runtime.setPtyController(controller('respawned-pty'))
await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, {
tabId: 'host-tab',
leafId: HEADLESS_LEAF_ID,
activate: true
})
expect(await phoneSurface(runtime)).toMatchObject({
title: 'Saved name',
customTitle: 'Saved name'
})
})
// The path a reconnecting client takes after a restart: the host restarts the saved tab's
// terminal for the client by the tab's identity.
it('reaches the phone when a client restarts the saved tab by its identity', async () => {
const runtime = runtimeWithSession('Saved name')
runtime.setNotifier(notifier())
runtime.setPtyController(controller('client-restarted-pty'))
await runtime['createRuntimeOwnedMobileSessionTerminal'](TEST_WORKTREE_ID, true, undefined, {
identity: { tabId: 'host-tab', leafId: HEADLESS_LEAF_ID }
})
expect(await phoneSurface(runtime)).toMatchObject({
title: 'Saved name',
customTitle: 'Saved name'
})
})
it('keeps a name cleared on the phone row cleared when the terminal restarts', async () => {
const runtime = runtimeWithSession('Saved name')
runtime.setNotifier(notifier())
runtime.setPtyController(controller('client-restarted-pty'))
const identity = { tabId: 'host-tab', leafId: HEADLESS_LEAF_ID }
await runtime['createRuntimeOwnedMobileSessionTerminal'](TEST_WORKTREE_ID, true, undefined, {
identity
})
// A desktop clear reaches the row before its debounced session write drops the old name.
const snapshot = runtime['mobileSessionTabsByWorktree'].get(TEST_WORKTREE_ID)!
runtime['mobileSessionTabsByWorktree'].set(TEST_WORKTREE_ID, {
...snapshot,
tabs: snapshot.tabs.map((tab) =>
tab.type === 'terminal' && tab.parentTabId === 'host-tab'
? { ...tab, customTitle: null }
: tab
)
})
await runtime['createRuntimeOwnedMobileSessionTerminal'](TEST_WORKTREE_ID, true, undefined, {
identity
})
expect((await phoneSurface(runtime))?.customTitle).toBeNull()
})
})
@@ -217,16 +217,6 @@ describe('active workspace context for a focused desktop-catalog workspace (real
}
})
it('answers the real branch and display name for the linked worktree', async () => {
const id = `${REPO_ID}::${linkedPath}`
await expect(runtimeFor(focused(id, 'local')).resolveActiveWorktreeContext()).resolves.toEqual({
worktreeId: id,
path: linkedPath,
branch: 'refs/heads/feature/parity',
displayName: 'Parity feature'
})
})
it('matches the pre-#38653 answer for a focused SSH worktree whose host is not connected', async () => {
const id = 'ssh-repo::/home/me/app-feature'
for (const displayName of ['', 'Named on the box']) {
@@ -238,17 +228,6 @@ describe('active workspace context for a focused desktop-catalog workspace (real
}
})
it('still resolves a workspace whose stored focus host is stale (local) as it did before', async () => {
// The focus was stamped local; the repo row now routes to an SSH host (e.g. after a move).
const id = 'moved-repo::/home/me/app'
const rows = sshRows('moved-repo', id, 'Moved main')
const before = await contextBefore(id, rows)
expect(before).toMatchObject({ worktreeId: id, displayName: 'Moved main' })
await expect(
runtimeFor(focused(id, 'local'), rows).resolveActiveWorktreeContext()
).resolves.toEqual(before)
})
it("never answers the local twin's context for a paired-server focus with the same id", async () => {
const id = `${REPO_ID}::${linkedPath}`
const session = focused(id, 'runtime:env-1')
@@ -25,22 +25,6 @@ function guardHarness(foregrounds: LaunchedAgentForeground[]) {
}
describe('the check before each write of a launch prompt', () => {
it.each([
['a shell turning bracketed paste on', '$ \x1b[?2004h'],
['a shell integration prompt mark', '\x1b]133;D;1\x07\x1b]133;A\x07$ '],
['a mark split across two chunks', ['\x1b[?20', '04h']]
] as const)('reads again after %s, and refuses a shell', async (_label, output) => {
const { runtime, guard, emit } = guardHarness(['agent', 'shell'])
await guard.beforeWrite('pty-1')
for (const chunk of typeof output === 'string' ? [output] : output) {
emit(chunk)
}
await expect(guard.beforeWrite('pty-1')).rejects.toThrow('agent_not_in_foreground')
expect(runtime.readLaunchedAgentForeground).toHaveBeenCalledTimes(2)
})
it('catches a shell that returns while the first read is still running', async () => {
const { runtime, guard, emit } = guardHarness([])
vi.mocked(runtime.readLaunchedAgentForeground)
@@ -54,20 +38,6 @@ describe('the check before each write of a launch prompt', () => {
await expect(guard.beforeWrite('pty-1')).rejects.toThrow('agent_not_in_foreground')
})
it('stops watching when it refuses and when it is disposed', async () => {
const refused = guardHarness(['shell'])
await expect(refused.guard.beforeWrite('pty-1')).rejects.toThrow('agent_not_in_foreground')
expect(refused.unsubscribe).toHaveBeenCalledTimes(1)
const cleared = guardHarness(['agent'])
await cleared.guard.beforeWrite('pty-1')
const scheduled = Promise.resolve().then(() => cleared.guard.beforeWrite('pty-1'))
cleared.guard.dispose()
await expect(scheduled).rejects.toThrow('request_aborted')
expect(cleared.runtime.subscribeToTerminalData).toHaveBeenCalledTimes(1)
expect(cleared.unsubscribe).toHaveBeenCalledTimes(1)
})
})
it('invalidates reused and pending foreground proof when the terminal session is replaced', async () => {
@@ -1,116 +0,0 @@
import { describe, expect, it } from 'vitest'
import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../shared/agent-status/contracts/agent-status-freshness'
import type { AgentStatusIpcPayload } from '../../../../shared/agent-status/contracts/agent-status-ipc-payload'
import { wslHookRelayConnectionId } from '../../../../shared/wsl-hook-relay-contract'
import { evaluateHookTurn, readTuiIdleHookTurn } from './tui-idle-hook-lane'
const PANE = 'tab:11111111-1111-4111-8111-111111111111'
function pendingRow(overrides: Partial<AgentStatusIpcPayload> = {}): AgentStatusIpcPayload {
const now = Date.now()
return {
paneKey: PANE,
state: 'working',
mainAgent: { state: 'done', stateStartedAt: now },
prompt: '',
agentType: 'claude',
connectionId: 'host-a',
launchToken: 'launch-a',
claudeTaskWakeupPending: 'notification',
providerSession: { key: 'session_id', id: 'session-a' },
observation: {
origin: 'hook',
authorityId: 'host-a',
incarnation: 1,
revision: 1,
observedAt: now
},
receivedAt: now,
stateStartedAt: now,
...overrides
}
}
function verdict(row: AgentStatusIpcPayload, blocked = false, titleObservedAtEpochMs?: number) {
return evaluateHookTurn('claude', () =>
readTuiIdleHookTurn({
agent: 'claude',
handles: [],
paneKeys: [PANE],
hookRows: [row],
connectionId: 'host-a',
launchToken: 'launch-a',
titleObservedAtEpochMs,
hasExplicitIdleTitle: titleObservedAtEpochMs !== undefined,
resolveBlockedText: () => (blocked ? 'agent-interactive-prompt' : null)
})
)
}
describe('Claude task wake-up readiness authority', () => {
it('vetoes rest from its own fresh hook without promoting ordinary Claude done to ready', () => {
expect(verdict(pendingRow())).toEqual({ kind: 'working' })
expect(verdict(pendingRow({ state: 'done', claudeTaskWakeupPending: undefined }))).toBeNull()
expect(verdict(pendingRow({ claudeTaskWakeupPending: undefined }))).toBeNull()
})
it('reports an opaque child permission wait during the finishing turn', () => {
expect(verdict(pendingRow({ state: 'waiting' }), true)).toEqual({
kind: 'blocked',
reason: 'agent-interactive-prompt'
})
})
it.each([
{ connectionId: 'host-b' },
{ launchToken: 'old-launch' },
{ agentType: 'codex' },
{ providerSession: undefined },
{ providerSessionOnly: true },
{ restoredUnconfirmed: true },
{ observation: undefined },
{ paneKey: 'another-pane' }
])('does not take authority from an unmatched or unverifiable row: %j', (overrides) => {
expect(verdict(pendingRow(overrides))).toBeNull()
})
it('allows a finishing turn’s fresh native rest when its Stop was lost, while owing still holds', () => {
const finishing = pendingRow({ claudeTaskWakeupPending: 'finishing-turn', turnStartedAt: 10 })
expect(verdict(finishing, false, 9)).toEqual({ kind: 'working' })
expect(verdict(finishing, false, 10)).toEqual({ kind: 'working' })
expect(verdict(finishing, false, 11)).toBeNull()
expect(verdict(pendingRow(), false, 11)).toEqual({ kind: 'working' })
const replay = pendingRow({ claudeTaskWakeupPending: 'finishing-turn' })
expect(verdict(replay)).toEqual({ kind: 'working' })
expect(verdict(replay, false, replay.receivedAt)).toEqual({ kind: 'working' })
expect(verdict(replay, false, replay.receivedAt + 1)).toBeNull()
})
it('joins native, SSH and WSL execution hosts without accepting another distro', () => {
for (const [connectionId, wslDistro, rowConnectionId, held] of [
[null, null, null, true],
['ssh-a', 'Ubuntu', 'ssh-a', true],
[null, 'Ubuntu', wslHookRelayConnectionId('Ubuntu'), true],
[null, 'Ubuntu', wslHookRelayConnectionId('Debian'), false],
['ssh-a', 'Ubuntu', wslHookRelayConnectionId('Ubuntu'), false]
] as const) {
const turn = readTuiIdleHookTurn({
agent: 'claude',
handles: [],
paneKeys: [PANE],
hookRows: [pendingRow({ connectionId: rowConnectionId })],
connectionId,
wslDistro,
launchToken: 'launch-a',
resolveBlockedText: () => null
})
expect(evaluateHookTurn('claude', () => turn)).toEqual(held ? { kind: 'working' } : null)
}
})
it('uses evidence age rather than a reconnect receipt clock', () => {
expect(
verdict(pendingRow({ evidenceObservedAt: Date.now() - AGENT_STATUS_STALE_AFTER_MS - 1 }))
).toBeNull()
})
})
@@ -21,15 +21,6 @@ const PANE_KEY = makePaneKey('tab-1', '11111111-1111-4111-8111-111111111111')
const FIXTURE = readRuntimeFixture('codex-0-160-0-plan-implement-menu')
const MENU_START = FIXTURE.indexOf('\x1b[30;1H\x1b[2m Worked for')
const MENU_END = FIXTURE.indexOf('\x1b[?2026l', FIXTURE.indexOf('esc\x1b[22m', MENU_START))
// A plan turn whose output quotes the menu as its last lines, with no menu on screen.
const QUOTED_MENU = [
'• Proposed Plan',
'• The plan menu reads:',
' Implement this plan?',
' 3. No, stay in Plan mode',
' enter select · esc back',
''
].join('\r\n')
const QUIESCENCE_MS = 3000
function doneRow(receivedAt: number): AgentStatusIpcPayload {
@@ -116,15 +107,4 @@ describe('Codex plan menu painted after its Stop hook', () => {
expect((await waiting).blockedReason).toBe('agent-interactive-prompt')
})
it('settles on a done that follows output quoting the menu', async () => {
let rows: AgentStatusIpcPayload[] = []
const { runtime, handle } = await codexPane(() => rows)
paint(runtime, QUOTED_MENU)
await vi.advanceTimersByTimeAsync(100)
rows = [doneRow(Date.now())]
// Held for the turn's last frame, then settled before the next poll sweep.
expect(await waitOutcome(runtime, handle, 1000)).toBe('ready')
})
})
@@ -202,30 +202,4 @@ describeOnWindows('a secure store that exists but cannot be read', () => {
expectUntouched(filePath, locked)
})
/** Refresh failures must not delete unreadable account sessions; only found sessions permit that path. */
it('does not delete the account session it could not read', async () => {
vi.doMock('electron', () => ({
safeStorage: {
isEncryptionAvailable: () => true,
encryptString: (value: string) => Buffer.from(value),
decryptString: (buffer: Buffer) => buffer.toString()
}
}))
const { readOrcaCloudSession, getOrcaCloudSessionPath } =
await import('./../orca-profiles/profile-cloud-session-store')
const dir = join(root, 'profiles')
mkdirSync(dir, { recursive: true })
const filePath = getOrcaCloudSessionPath('profile-1', dir)
mkdirSync(join(filePath, '..'), { recursive: true })
const original = JSON.stringify({ version: 1, format: 'dev-plaintext-v1', savedAt: 1 })
writeFileSync(filePath, original)
const locked = makeUnreadable(filePath)
// The status the delete path keys off: `unreadable`, never `decrypt-failed`.
expect(readOrcaCloudSession('profile-1', dir).status).toBe('unreadable')
expectUntouched(filePath, locked)
vi.doUnmock('electron')
})
})
@@ -1,71 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
appendWaitBlockedCarry,
createWaitBlockedAppendedCarry,
readWaitBlockedCarry
} from './wait-blocked-check-state'
import { MAX_TAIL_CHARS } from './terminal/output/tail/terminal-tail-limits'
/** The accumulation this replaced, verbatim, as the equivalence oracle. */
function referenceAppend(previous: string, chunk: string): string {
return previous.length + chunk.length > MAX_TAIL_CHARS
? `${previous}${chunk}`.slice(-MAX_TAIL_CHARS)
: `${previous}${chunk}`
}
function mulberry32(seed: number): () => number {
let a = seed >>> 0
return () => {
a = (a + 0x6d2b79f5) | 0
let t = Math.imul(a ^ (a >>> 15), 1 | a)
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}
describe('wait-blocked appended carry', () => {
it('is byte-identical to the concat+slice carry across a 1MB flood in one 50ms window', () => {
// A 50ms TUI flood must retain exactly the reference rolling window's bytes.
const carry = createWaitBlockedAppendedCarry()
let reference = ''
const rng = mulberry32(20260902)
let produced = 0
let chunkIndex = 0
while (produced < 1024 * 1024) {
const size = 1 + Math.floor(rng() * 8192)
const chunk = `${chunkIndex}:${'█'.repeat(Math.max(0, size - 3))}\n`
chunkIndex += 1
produced += chunk.length
appendWaitBlockedCarry(carry, chunk)
reference = referenceAppend(reference, chunk)
expect(carry.chars).toBe(reference.length)
}
expect(readWaitBlockedCarry(carry)).toBe(reference)
expect(reference.length).toBe(MAX_TAIL_CHARS)
})
it('matches the reference for boundary shapes: empty, exact-cap, and over-cap single chunks', () => {
const cases: string[][] = [
[],
[''],
['abc', '', 'def'],
['x'.repeat(MAX_TAIL_CHARS)],
['x'.repeat(MAX_TAIL_CHARS), 'y'],
['a', 'b'.repeat(MAX_TAIL_CHARS + 5)],
['a'.repeat(MAX_TAIL_CHARS - 1), 'bc'],
['a'.repeat(10), 'b'.repeat(MAX_TAIL_CHARS - 10)],
['a'.repeat(10), 'b'.repeat(MAX_TAIL_CHARS - 10), 'c']
]
for (const chunks of cases) {
const carry = createWaitBlockedAppendedCarry()
let reference = ''
for (const chunk of chunks) {
appendWaitBlockedCarry(carry, chunk)
reference = referenceAppend(reference, chunk)
}
expect(readWaitBlockedCarry(carry)).toBe(reference)
expect(carry.chars).toBe(reference.length)
}
})
})
@@ -125,86 +125,6 @@ describe('released worker archives preserve the host screen', () => {
}
})
it.each([
{ cols: 100, prefix: 89, provider: false },
{ cols: 100, prefix: 97, provider: false },
{ cols: 10, prefix: 7, provider: false },
{ cols: 3, prefix: 0, provider: false },
{ cols: 100, prefix: 89, provider: true },
{ cols: 10, prefix: 7, provider: true }
])(
'masks wrapped tokens in stored and post-close output (%j)',
async ({ cols, prefix, provider }) => {
h.setup()
const { dispatchId } = await h.startSettledWorker('failed')
const secret = `dcap_${'s'.repeat(24)}`
const text = `${'x'.repeat(prefix)} ${secret}`
const fixture = await createTerminalArchiveFixture({
hydrate: false,
cols,
provider,
screenLines: [text, 'Safe diagnostic']
})
fixture.runtime.onPtyData(
'pty-1',
provider ? 'retained history\r\n' : `${text}\r\nSafe diagnostic\r\n`,
Date.now()
)
const screen = await fixture.runtime.readTerminal(fixture.handle, { screen: true })
expect(screen.tail.join('')).toContain(secret)
expect(screen.tail).not.toContain(secret)
vi.mocked(h.runtime.readTerminal).mockImplementation((_handle, opts) =>
fixture.runtime.readTerminal(fixture.handle, opts)
)
vi.mocked(h.runtime.closeTerminal).mockImplementation(async (handle) => {
fixture.runtime.onPtyExit('pty-1', 0)
return { handle, tabId: 'tab-worker', ptyKilled: true }
})
await expect(
h.call('orchestration.workerRelease', { dispatch: dispatchId })
).resolves.toMatchObject({ state: 'released' })
const stored = h.db.getWorkerTerminalArchive(dispatchId)
if (!stored) {
throw new Error('No stored archive')
}
expect(stored.content).not.toContain(secret)
const content: unknown = JSON.parse(stored.content)
if (
typeof content !== 'object' ||
content === null ||
!('lines' in content) ||
!Array.isArray(content.lines)
) {
throw new Error('No stored terminal lines')
}
expect(content.lines.join('')).not.toContain(secret)
expect(content.lines.join('')).toContain('Safe diagnostic')
vi.mocked(h.runtime.readTerminal).mockRejectedValue(new Error('Live output gone'))
const read = await h.call('orchestration.workerRead', { dispatch: dispatchId, limit: 2000 })
expect(read).toMatchObject({
archived: true,
terminal: { nextCursor: null },
warnings: expect.arrayContaining([
'Dispatch capability tokens were redacted from terminal output.'
])
})
if (
typeof read !== 'object' ||
read === null ||
!('terminal' in read) ||
typeof read.terminal !== 'object' ||
read.terminal === null ||
!('tail' in read.terminal) ||
!Array.isArray(read.terminal.tail)
) {
throw new Error('No archived terminal output')
}
expect(read.terminal.tail.join('')).not.toContain(secret)
expect(read.terminal.tail.join('')).toContain('Safe diagnostic')
}
)
it.each(['soft wrap', 'multiple lines'] as const)(
'masks a real composer draft after release (%s)',
async (layout) => {
@@ -1,8 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
ARCHIVE_SCREEN_LINES,
createTerminalArchiveFixture
} from './worker-terminal-archive.test-support'
import { createTerminalArchiveFixture } from './worker-terminal-archive.test-support'
import { syncSinglePty } from './orca-runtime-tests/orca-runtime-test-fixtures.spec'
afterEach(() => vi.restoreAllMocks())
@@ -26,79 +23,6 @@ describe('worker archive through real terminal read routing', () => {
)
})
it('keeps an ordinary live stream without duplicating an identical screen', async () => {
const fixture = await createTerminalArchiveFixture({ hydrate: false })
fixture.runtime.onPtyData('pty-1', `${ARCHIVE_SCREEN_LINES.join('\r\n')}\r\n`, Date.now())
const archive = await fixture.capture()
if (archive.kind !== 'terminal_tail') {
throw new Error('Expected terminal archive')
}
expect(archive.content.lines).toEqual(ARCHIVE_SCREEN_LINES)
expect(archive.content.warnings).toEqual([])
})
it.each(['\r\n'.repeat(7), `host % codex\r\n${'\r\n'.repeat(6)}`])(
'preserves a populated normal screen alongside a short stream (%j)',
async (stream) => {
const fixture = await createTerminalArchiveFixture()
fixture.runtime.onPtyData('pty-1', stream, Date.now())
const preview = await fixture.runtime.readTerminal(fixture.handle)
expect(preview.source).toBe('stream')
expect(preview.tail).not.toContain(ARCHIVE_SCREEN_LINES[1])
const archive = await fixture.capture()
expect(archive.kind).toBe('terminal_tail')
expect(archive.status).toBe('captured')
if (archive.kind !== 'terminal_tail') {
throw new Error('Expected terminal archive')
}
expect(archive.content.lines).toEqual(expect.arrayContaining(ARCHIVE_SCREEN_LINES))
expect(archive.content.warnings.join(' ')).toContain('screen')
if (stream.includes('host % codex')) {
expect(archive.content.lines).toContain('host % codex')
}
}
)
it('preserves retained history beyond the preview and the current screen', async () => {
const fixture = await createTerminalArchiveFixture({ provider: true })
const history = Array.from({ length: 180 }, (_, index) => `history ${index}`)
fixture.runtime.onPtyData('pty-1', `${history.join('\r\n')}\r\n`, Date.now())
expect((await fixture.runtime.readTerminal(fixture.handle)).tail).not.toContain('history 0')
const archive = await fixture.capture()
if (archive.kind !== 'terminal_tail') {
throw new Error('Expected terminal archive')
}
expect(archive.content.lines).toEqual(
expect.arrayContaining([...history, ...ARCHIVE_SCREEN_LINES])
)
expect(archive.content.warnings.join(' ')).toContain('screen')
})
it('preserves the alternate screen using the same capture path', async () => {
const fixture = await createTerminalArchiveFixture({ alternate: true })
fixture.runtime.onPtyData('pty-1', '\r\n'.repeat(7), Date.now())
const archive = await fixture.capture()
if (archive.kind !== 'terminal_tail') {
throw new Error('Expected terminal archive')
}
expect(archive.content.lines).toEqual(expect.arrayContaining(ARCHIVE_SCREEN_LINES))
})
it('allows a genuinely empty terminal with honest unavailable-screen provenance', async () => {
const fixture = await createTerminalArchiveFixture({ screenLines: [], hydrate: false })
const archive = await fixture.capture()
expect(archive.status).toBe('empty')
if (archive.kind !== 'terminal_tail') {
throw new Error('Expected terminal archive')
}
expect(archive.content.lines.every((line) => line.trim() === '')).toBe(true)
expect(archive.content.warnings.join(' ')).toContain('screen was unavailable')
})
it.each(['unavailable', 'stale'] as const)(
'preserves useful stream output when the provider screen is %s',
async (authority) => {
@@ -130,21 +54,6 @@ describe('worker archive through real terminal read routing', () => {
}
)
it('preserves and redacts a composer draft from the authoritative screen', async () => {
const fixture = await createTerminalArchiveFixture({ hydrate: false })
const secret = `dcap_${'d'.repeat(24)}`
fixture.runtime.onPtyData('pty-1', `output\r\n────────\r\n❯ ${secret}`, Date.now())
const archive = await fixture.capture()
if (archive.kind !== 'terminal_tail') {
throw new Error('Expected terminal archive')
}
expect(archive.content.draft).toBe('[dispatch capability redacted]')
expect(JSON.stringify(archive.content)).not.toContain(secret)
expect(archive.content.lines).toContain('output')
})
it('refuses capture when the terminal handle changes during a host screen read', async () => {
const fixture = await createTerminalArchiveFixture({ provider: true })
fixture.runtime.onPtyData('pty-1', 'retained diagnostic\r\n', Date.now())
@@ -108,23 +108,6 @@ describe('host wrap provenance in worker output', () => {
}
)
it('preserves a whitespace token boundary removed from raw display rows', async () => {
const fixture = await createTerminalArchiveFixture({
provider: true,
cols: 25,
screenLines: [`dcap_sssss${' '.repeat(15)}PermissionDenied`, 'Safe diagnostic']
})
fixture.runtime.onPtyData('pty-1', 'useful history\r\n', Date.now())
const raw = await fixture.runtime.readTerminal(fixture.handle, { screen: true })
expect(raw.tail).toEqual(['dcap_sssss', 'PermissionDenied', 'Safe diagnostic'])
expect(raw.tailRows?.[1].wrapped).toBe(true)
expect(raw.tailRows?.[0].trailingSpaces).toBe(15)
const archive = await fixture.capture()
expect(archive.content).toMatchObject({
lines: ['useful history', 'dcap_sssss', 'PermissionDenied', 'Safe diagnostic']
})
})
it.each(['screen' as const, undefined])(
'retains complete-line semantics and coverage limits without wrap metadata (source=%s)',
async (source) => {
@@ -160,50 +143,6 @@ describe('host wrap provenance in worker output', () => {
}
)
it.each([false, true])(
'keeps hard screen diagnostics in live and archived reads (legacy=%s)',
async (legacy) => {
h.setup()
const { dispatchId } = await h.startSettledWorker('failed')
const fixture = await createTerminalArchiveFixture({
provider: true,
screenLines: ['dcap_sssss', 'PermissionDenied', 'Safe diagnostic']
})
fixture.runtime.onPtyData('pty-1', 'useful history\r\n', Date.now())
vi.mocked(h.runtime.readTerminal).mockImplementation(async (_handle, opts) => {
const read = await fixture.runtime.readTerminal(fixture.handle, opts)
if (!legacy) {
return read
}
// Older responses carry neither source nor physical wrap provenance.
const { source: _source, tailRows: _tailRows, ...old } = read
return old
})
const live = await h.call('orchestration.workerRead', {
dispatch: dispatchId,
source: 'terminal',
limit: 2000
})
expect(readTail(live)).toContain('useful history')
vi.mocked(h.runtime.closeTerminal).mockImplementation(async (handle) => {
fixture.runtime.onPtyExit('pty-1', 0)
return { handle, tabId: 'tab-worker', ptyKilled: true }
})
await h.call('orchestration.workerRelease', { dispatch: dispatchId })
const stored = h.db.getWorkerTerminalArchive(dispatchId)
expect(stored?.content).toContain('PermissionDenied')
vi.mocked(h.runtime.readTerminal).mockRejectedValue(new Error('Live output gone'))
const read = await h.call('orchestration.workerRead', { dispatch: dispatchId, limit: 2000 })
expect(read).toMatchObject({ archived: true, terminal: { nextCursor: null } })
expect(readTail(read)).toEqual([
'useful history',
'dcap_sssss',
'PermissionDenied',
'Safe diagnostic'
])
}
)
it('keeps hard composer breaks in stored and full archived draft output', async () => {
h.setup()
const { dispatchId } = await h.startSettledWorker('failed')
@@ -12,20 +12,13 @@ import type { Store } from '../persistence/loading-store/store'
import { WindowSession } from '../persistence/terminal-topology/terminal-topology-window-session-fixture'
import {
emptyTerminalSessionProfile,
FIXTURE_FOLDER_WORKTREE_ID as FOLDER,
FIXTURE_GIT_WORKTREE_ID as GIT,
openTopologyStore
} from '../persistence/terminal-topology/terminal-topology-profile-fixture'
import { applyWindowLayoutCommand } from './terminal/layout/runtime-layout-apply'
import {
layoutFrameModelErrors,
logLayoutFrameModelErrors
} from './workspace-layout-frame-model-check'
import './workspace-layout-frame-model-check'
import { installLayoutFrameModelCheck } from './workspace-layout-frame-model-check.test-fixture'
import {
observeWorkspaceLayoutFrames,
type LayoutFrameObservation
} from './workspace-layout-frame-observer'
import './workspace-layout-frame-observer'
import {
WorkspaceLayoutStream,
type LayoutOperationCommit
@@ -113,35 +106,6 @@ afterEach(() => {
})
describe('one records frame per main operation', () => {
const SPREAD = { [GIT]: 50, [FOLDER]: 49, [SSH_KEY]: 1 }
it('publishes 100 generated titles in three workspaces and two partitions as one frame', async () => {
const store = await storeWith(SPREAD)
const frames: WorkspaceLayoutStreamEvent[] = []
const { seq } = streamOver(store).subscribe((frame) => frames.push(frame))
await expect(renameTabs(store, titles(SPREAD))).resolves.toEqual({ ok: true, result: {} })
expect(frames).toHaveLength(1)
const frame = frames[0]
if (frame?.type !== 'records') {
throw new Error('expected one records frame')
}
expect(frame.seq).toBe(seq + 1)
expect(
frame.workspaces.map(({ key, change, ...records }) => [key, change, Object.keys(records)])
).toEqual([
[GIT, 'records', ['tabs']],
[FOLDER, 'records', ['tabs']],
[SSH_KEY, 'records', ['tabs']]
])
expect(
frame.workspaces.flatMap((change) => ('tabs' in change ? change.tabs!.set! : []))
).toHaveLength(100)
expect(check.failures).toEqual([])
expect(check.checked()).toBe(1)
})
it('publishes a pane bind as one binding record, and a same-id respawn as one more', async () => {
const store = await storeWith({ [GIT]: 2 })
const frames: WorkspaceLayoutStreamEvent[] = []
@@ -171,16 +135,6 @@ describe('one records frame per main operation', () => {
])
expect(check.failures).toEqual([])
})
it('sends nothing for an operation that changes no record', async () => {
const store = await storeWith({ [GIT]: 1 })
const frames: WorkspaceLayoutStreamEvent[] = []
streamOver(store).subscribe((frame) => frames.push(frame))
await renameTabs(store, [{ workspace: GIT, tabId: 'missing', title: 'Gone' }])
expect(frames).toEqual([])
})
})
describe('a throwing operation listener', () => {
@@ -201,50 +155,6 @@ describe('a throwing operation listener', () => {
})
})
describe("main's model check on every frame", () => {
it('passes the frame main sent and catches a broken copy of it', async () => {
check.uninstall()
const store = await storeWith({ [GIT]: 3 })
const observed: LayoutFrameObservation[] = []
observeWorkspaceLayoutFrames((observation) => observed.push(observation))
streamOver(store).subscribe(() => {})
await renameTabs(store, titles({ [GIT]: 3 }))
expect(observed).toHaveLength(1)
const observation = observed[0]!
expect(layoutFrameModelErrors(observation)).toEqual([])
const broken = structuredClone(observation.frame)
const change = broken.workspaces[0]!
if (change.change !== 'records') {
throw new Error('expected a records change')
}
change.tabs!.set!.pop()
expect(layoutFrameModelErrors({ ...observation, frame: broken })).toEqual([
`frame ${broken.seq}: ${GIT} differs from a fresh projection`
])
observeWorkspaceLayoutFrames(null)
})
})
describe("the dev build's model check", () => {
it('skips a frame that only changed live titles, and checks a structural one', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const fresh = vi.fn(() => new Map())
const frame = {
type: 'records' as const,
epoch: 'epoch-1',
seq: 1,
workspaces: [{ key: GIT, change: 'removed' as const }]
}
logLayoutFrameModelErrors({ frame, before: new Map(), fresh, titlesOnly: true })
expect(fresh).not.toHaveBeenCalled()
logLayoutFrameModelErrors({ frame, before: new Map(), fresh, titlesOnly: false })
expect(fresh).toHaveBeenCalledTimes(1)
warn.mockRestore()
})
})
describe('a headless main', () => {
it('hears each operation with no layout subscriber, from one projection it shares', async () => {
const store = await storeWith({ [GIT]: 2, [SSH_KEY]: 1 })
@@ -271,27 +181,4 @@ describe('a headless main', () => {
'Next'
)
})
it('sends a window-style and a phone-style subscriber the same frames', async () => {
const store = await storeWith({ [GIT]: 2, [FOLDER]: 1 })
const stream = streamOver(store)
const window: WorkspaceLayoutStreamEvent[] = []
const phone: WorkspaceLayoutStreamEvent[] = []
const windowSub = stream.subscribe((frame) => window.push(frame))
const phoneSub = stream.subscribe((frame) => phone.push(frame))
await renameTabs(store, titles({ [GIT]: 2, [FOLDER]: 1 }))
await store.persistPtyBinding({
worktreeId: GIT,
tabId: tabIdOf(GIT, 0),
leafId: leafOf(0),
ptyId: 'pty-0'
})
expect(window).toHaveLength(2)
expect(phone).toEqual(window)
expect(phoneSub.snapshot).toEqual(windowSub.snapshot)
expect(check.failures).toEqual([])
expect(check.checked()).toBe(2)
})
})
@@ -3,27 +3,21 @@ import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../shared/exec
import type { WorkspaceSessionState } from '../../shared/workspace-session/workspace-session-state-types'
import {
localDesktopSession,
relaySshSession,
serverRuntimeSession
relaySshSession
} from '../../shared/workspace-layout/workspace-layout-profile.test-fixture'
import {
emptySession,
FOLDER_KEY,
GIT_KEY,
SERVER_KEY,
SSH_KEY
} from '../../shared/workspace-layout/workspace-layout-session.test-fixture'
import { loadWorkspaceLayout } from '../../shared/workspace-layout/workspace-layout-load'
import { nameBasedLoadContext } from '../../shared/workspace-layout/workspace-layout-minted-ids'
import {
publishWorkspaceLayout,
type PublishedWorkspaceLayout
} from '../../shared/workspace-layout/workspace-layout-published'
import '../../shared/workspace-layout/workspace-layout-load'
import '../../shared/workspace-layout/workspace-layout-minted-ids'
import '../../shared/workspace-layout/workspace-layout-published'
import { WorkspaceLayoutStream } from './terminal/surface/workspace-layout-stream'
import { collectLayoutEvents } from './orca-runtime-tests/workspace-layout-stream-events.test-fixture'
const SSH_HOST: ExecutionHostId = 'ssh:target-1'
const SERVER_HOST: ExecutionHostId = 'runtime:env-1'
function fakeStore(partitions: Map<ExecutionHostId, WorkspaceSessionState>) {
const listeners = new Set<(hostId: ExecutionHostId | null) => void>()
@@ -54,23 +48,6 @@ function fakeStore(partitions: Map<ExecutionHostId, WorkspaceSessionState>) {
}
}
/** Full uncached projection is the reference for partition-scoped updates. */
function fullProjection(
partitions: Map<ExecutionHostId, WorkspaceSessionState>,
homeHostId: (key: string) => ExecutionHostId | null
): [string, PublishedWorkspaceLayout][] {
const projected = new Map<string, PublishedWorkspaceLayout>()
for (const [hostId, session] of partitions) {
const { layout } = loadWorkspaceLayout(hostId, session, nameBasedLoadContext())
for (const [key, workspace] of Object.entries(layout.workspaces)) {
if (homeHostId(key) === hostId) {
projected.set(key, publishWorkspaceLayout(workspace, hostId))
}
}
}
return [...projected]
}
function setup() {
const partitions = new Map<ExecutionHostId, WorkspaceSessionState>([
[LOCAL_EXECUTION_HOST_ID, localDesktopSession()],
@@ -102,18 +79,6 @@ function subscribeEvents(stream: WorkspaceLayoutStream) {
}
describe('WorkspaceLayoutStream', () => {
it('does nothing until someone subscribes, and stops when the last one leaves', () => {
const { stream, store, listeners } = setup()
expect(store.onWorkspaceSessionWritten).not.toHaveBeenCalled()
const first = stream.subscribe(() => {})
const second = stream.subscribe(() => {})
expect(listeners.size).toBe(1)
first.unsubscribe()
expect(listeners.size).toBe(1)
second.unsubscribe()
expect(listeners.size).toBe(0)
})
it('snapshots each workspace from its owning partition only', () => {
const { stream, homes } = setup()
const { snapshot } = stream.subscribe(() => {})
@@ -133,45 +98,6 @@ describe('WorkspaceLayoutStream', () => {
expect(strayless.map((entry) => entry.key)).toEqual([GIT_KEY, FOLDER_KEY])
})
it('publishes a workspace when a write changes it by value, and its removal', async () => {
const { stream, write } = setup()
const { events } = subscribeEvents(stream)
const session = localDesktopSession()
write(LOCAL_EXECUTION_HOST_ID, session)
await flush()
expect(events).toEqual([])
session.tabGroups![GIT_KEY]![0]!.tabOrder.reverse()
write(LOCAL_EXECUTION_HOST_ID, session)
write(LOCAL_EXECUTION_HOST_ID, session)
await flush()
expect(events.map((event) => [event.type, event.key])).toEqual([['workspace', GIT_KEY]])
const withoutFolder = structuredClone(session)
delete withoutFolder.tabsByWorktree[FOLDER_KEY]
delete withoutFolder.unifiedTabs![FOLDER_KEY]
delete withoutFolder.tabGroups![FOLDER_KEY]
delete withoutFolder.tabGroupLayouts![FOLDER_KEY]
write(LOCAL_EXECUTION_HOST_ID, withoutFolder)
await flush()
expect(events.at(-1)).toEqual({ type: 'removed', key: FOLDER_KEY, seq: events[0]!.seq! + 1 })
})
it("publishes nothing for a pane's grid, which is main's own record", async () => {
const { stream, write } = setup()
const { events } = subscribeEvents(stream)
const session = localDesktopSession()
const [tabId, layout] = Object.entries(session.terminalLayoutsByTabId).find(
([, candidate]) => candidate.root?.type === 'leaf'
)!
const leafId = layout.root!.type === 'leaf' ? layout.root!.leafId : ''
layout.lastGridsByLeafId = { [leafId]: { cols: 91, rows: 33 } }
write(LOCAL_EXECUTION_HOST_ID, session)
await flush()
expect(session.terminalLayoutsByTabId[tabId]?.lastGridsByLeafId).toBeDefined()
expect(events).toEqual([])
})
it('numbers each change frame, so a reply can name the frame holding its write', async () => {
const { stream, write } = setup()
const { events, subscription: first } = subscribeEvents(stream)
@@ -196,23 +122,6 @@ describe('WorkspaceLayoutStream', () => {
second.unsubscribe()
})
it('logs a failing listener once and never throws into the write', async () => {
const { stream, write } = setup()
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
stream.subscribe(() => {
throw new Error('listener failed')
})
const session = localDesktopSession()
session.tabGroups![GIT_KEY]![0]!.tabOrder.reverse()
expect(() => write(LOCAL_EXECUTION_HOST_ID, session)).not.toThrow()
await flush()
session.tabGroups![GIT_KEY]![0]!.tabOrder.reverse()
write(LOCAL_EXECUTION_HOST_ID, structuredClone(session))
await flush()
expect(warn).toHaveBeenCalledTimes(1)
warn.mockRestore()
})
it('keeps delivering to every listener when one throws', async () => {
const { stream, write } = setup()
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
@@ -280,58 +189,6 @@ describe('WorkspaceLayoutStream', () => {
}
})
it('re-projects only the partition a write names', async () => {
const { stream, store, write, writeUnnamed } = setup()
stream.subscribe(() => {})
store.getWorkspaceSession.mockClear()
write(SSH_HOST, relaySshSession('target-1'))
await flush()
expect(store.getWorkspaceSession.mock.calls).toEqual([[SSH_HOST]])
store.getWorkspaceSession.mockClear()
writeUnnamed(SSH_HOST, relaySshSession('target-1'))
await flush()
expect(store.getWorkspaceSession.mock.calls).toEqual([[LOCAL_EXECUTION_HOST_ID], [SSH_HOST]])
})
it('publishes what a full re-projection with the load report would, write after write', async () => {
const { stream, write, writeUnnamed, removePartition, homes, partitions } = setup()
const homeHostId = (key: string) => homes[key] ?? null
const { collector } = subscribeEvents(stream)
const expectFullProjection = async (): Promise<void> => {
await flush()
const expected = fullProjection(partitions, homeHostId)
expect(Object.fromEntries(collector.layouts)).toEqual(Object.fromEntries(expected))
const late = stream.subscribe(() => {})
expect(late.snapshot.map((entry) => [entry.key, entry.layout])).toEqual(expected)
late.unsubscribe()
}
await expectFullProjection()
const local = localDesktopSession()
local.tabGroups![GIT_KEY]![0]!.tabOrder.reverse()
write(LOCAL_EXECUTION_HOST_ID, local)
await expectFullProjection()
// A named write and an unnamed one in the same task.
const ssh = relaySshSession('target-1')
delete ssh.tabsByWorktree[SSH_KEY]
delete ssh.unifiedTabs![SSH_KEY]
write(SSH_HOST, ssh)
writeUnnamed(LOCAL_EXECUTION_HOST_ID, localDesktopSession())
await expectFullProjection()
// A new partition, a home that moves between partitions, then the partition removed.
homes[SERVER_KEY] = SERVER_HOST
write(SERVER_HOST, serverRuntimeSession('env-1'))
await expectFullProjection()
homes[GIT_KEY] = SERVER_HOST
write(SSH_HOST, relaySshSession('target-1'))
await expectFullProjection()
homes[GIT_KEY] = LOCAL_EXECUTION_HOST_ID
removePartition(SERVER_HOST)
await expectFullProjection()
})
it('re-projects every partition after a failed projection', async () => {
const { stream, store, write } = setup()
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
@@ -351,89 +208,4 @@ describe('WorkspaceLayoutStream', () => {
expect(warn).toHaveBeenCalledTimes(1)
warn.mockRestore()
})
describe('live pane titles', () => {
const panesOf = (layout: PublishedWorkspaceLayout) =>
layout.tabs.flatMap((tab) => tab.terminal?.panes ?? [])
const ptyIdsOf = (layout: PublishedWorkspaceLayout) =>
panesOf(layout).flatMap((pane) => (pane.ptyId ? [pane.ptyId] : []))
it('lays each pane terminal title over its pane, and only there', () => {
const { stream, liveTitles } = setup()
const bare = stream.subscribe(() => {}).snapshot
const [first, second] = ptyIdsOf(bare.find((entry) => entry.key === GIT_KEY)!.layout)
liveTitles.set(first, 'claude: working')
liveTitles.set(second, 'vim')
const late = setup()
late.liveTitles.set(first, 'claude: working')
late.liveTitles.set(second, 'vim')
const titled = late.stream.subscribe(() => {}).snapshot
const git = titled.find((entry) => entry.key === GIT_KEY)!.layout
const byPty = new Map(panesOf(git).map((pane) => [pane.ptyId, pane.liveTitle]))
expect(byPty.get(first)).toBe('claude: working')
expect(byPty.get(second)).toBe('vim')
const stripped = titled.map((entry) => ({
key: entry.key,
layout: JSON.parse(
JSON.stringify(entry.layout, (k, v) => (k === 'liveTitle' ? undefined : v))
)
}))
expect(stripped).toEqual(bare)
})
it('publishes a title change without reading the Store, one frame per burst', async () => {
const { stream, store, liveTitles, write } = setup()
const {
events,
subscription: { snapshot }
} = subscribeEvents(stream)
const [ptyId] = ptyIdsOf(snapshot.find((entry) => entry.key === GIT_KEY)!.layout)
store.getWorkspaceSession.mockClear()
for (let frame = 0; frame < 50; frame++) {
liveTitles.set(ptyId, `build ${frame}`)
stream.markLiveTitlesDirty()
}
await flush()
expect(store.getWorkspaceSession).not.toHaveBeenCalled()
expect(events).toHaveLength(1)
const event = events[0]
expect(event.type === 'workspace' && event.key).toBe(GIT_KEY)
expect(
event.type === 'workspace' && panesOf(event.layout).find((p) => p.ptyId === ptyId)
).toMatchObject({ liveTitle: 'build 49' })
// An unchanged title sends nothing; a structural write keeps the title on.
stream.markLiveTitlesDirty()
await flush()
expect(events).toHaveLength(1)
const session = localDesktopSession()
session.tabGroups![GIT_KEY]![0]!.tabOrder.reverse()
write(LOCAL_EXECUTION_HOST_ID, session)
await flush()
const rewritten = events.at(-1)!
expect(
rewritten.type === 'workspace' && panesOf(rewritten.layout).find((p) => p.ptyId === ptyId)
).toMatchObject({ liveTitle: 'build 49' })
liveTitles.delete(ptyId)
stream.markLiveTitlesDirty()
await flush()
const cleared = events.at(-1)!
expect(cleared.type === 'workspace' && panesOf(cleared.layout).some((p) => p.liveTitle)).toBe(
false
)
})
it('reads no title while nobody subscribes', async () => {
const { stream, liveTitle } = setup()
stream.markLiveTitlesDirty()
await flush()
expect(liveTitle).not.toHaveBeenCalled()
stream.subscribe(() => {}).unsubscribe()
liveTitle.mockClear()
stream.markLiveTitlesDirty()
await flush()
expect(liveTitle).not.toHaveBeenCalled()
})
})
})
@@ -1,242 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { NotificationSettings } from '../../shared/notification-settings-types'
import type { PersistedState } from '../../shared/persisted-state-types'
import type { Worktree } from '../../shared/worktree/types'
import type { WorktreeMeta } from '../../shared/worktree/meta-types'
import type { WorktreeLineage } from '../../shared/worktree/lineage-types'
import type { FolderWorkspace } from '../../shared/folder-workspaces/folder-workspace-types'
import { getDefaultNotificationSettings } from '../../shared/notification-settings-defaults'
import { normalizeNotificationSettings } from '../persistence/applying-settings/onboarding-normalization'
import {
isWorkspaceHiddenBySidebarFilters,
isWorkspaceNotificationMuted,
type WorkspaceNotificationMuteDeps
} from './push/notifications/workspace-notification-mute'
const WORKSPACE_ID = 'repo-1::/work/feature'
const SCOPE = WORKSPACE_ID
function listed(overrides: Partial<Worktree> = {}): Worktree {
return {
id: WORKSPACE_ID,
repoId: 'repo-1',
path: '/work/feature',
head: 'abc1234',
branch: 'refs/heads/feature',
isBare: false,
isMainWorktree: false,
displayName: 'feature',
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
...overrides
}
}
function deps(options: {
ui?: Partial<PersistedState['ui']> | null
resolved?: Worktree[] | null
meta?: Partial<WorktreeMeta>
metaById?: Record<string, Partial<WorktreeMeta>>
folders?: FolderWorkspace[]
lineage?: Record<string, Pick<WorktreeLineage, 'worktreeId' | 'parentWorktreeId'>>
notifications?: NotificationSettings
}): WorkspaceNotificationMuteDeps {
return {
store: {
getSettings: () => ({ notifications: options.notifications }),
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the check reads only the hide* switches of this partial UI.
getUI: () => (options.ui === null ? undefined : options.ui) as PersistedState['ui'],
getRepo: () => undefined,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the check reads only provenance fields of this partial meta.
getWorktreeMeta: (id) => (options.metaById?.[id] ?? options.meta) as WorktreeMeta | undefined,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the check reads only worktreeId and parentWorktreeId.
getAllWorktreeLineage: () => (options.lineage ?? {}) as Record<string, WorktreeLineage>,
getFolderWorkspaces: () => options.folders ?? []
},
resolvedWorktrees: options.resolved === undefined ? [listed()] : options.resolved
}
}
const CLI_META: Partial<WorktreeMeta> = { cliProvenance: { kind: 'created-by-cli', createdAt: 0 } }
describe('isWorkspaceHiddenBySidebarFilters', () => {
it('hides a CLI-created workspace from its provenance even with no Git listing', () => {
expect(
isWorkspaceHiddenBySidebarFilters(
deps({ ui: { hideCliCreatedWorkspaces: true }, resolved: null, meta: CLI_META }),
SCOPE
)
).toBe(true)
})
it('hides an automation-created, default-branch or detached-HEAD workspace by its switch', () => {
const automation: Partial<WorktreeMeta> = {
automationProvenance: {
kind: 'created-by-automation',
automationId: 'a',
automationNameSnapshot: 'a',
automationRunId: 'r',
automationRunTitleSnapshot: 'r',
createdAt: 0,
executionTargetType: 'local',
executionTargetId: 'local',
projectId: 'p'
}
}
expect(
isWorkspaceHiddenBySidebarFilters(
deps({ ui: { hideAutomationGeneratedWorkspaces: true }, meta: automation, resolved: null }),
SCOPE
)
).toBe(true)
expect(
isWorkspaceHiddenBySidebarFilters(
deps({
ui: { hideDefaultBranchWorkspace: true },
resolved: [listed({ isMainWorktree: true, branch: 'refs/heads/main' })]
}),
SCOPE
)
).toBe(true)
expect(
isWorkspaceHiddenBySidebarFilters(
deps({ ui: { hideDetachedHeadWorkspaces: true }, resolved: [listed({ branch: '' })] }),
SCOPE
)
).toBe(true)
})
it('is not hidden when no switch matches or the switch is off', () => {
const on = { hideCliCreatedWorkspaces: true }
expect(isWorkspaceHiddenBySidebarFilters(deps({ ui: on }), SCOPE)).toBe(false)
expect(isWorkspaceHiddenBySidebarFilters(deps({ ui: {}, meta: CLI_META }), SCOPE)).toBe(false)
})
it('fails open without persisted UI, a workspace record, or for a folder or shown lineage parent', () => {
const on = { hideCliCreatedWorkspaces: true }
expect(isWorkspaceHiddenBySidebarFilters(deps({ ui: null, meta: CLI_META }), SCOPE)).toBe(false)
expect(isWorkspaceHiddenBySidebarFilters(deps({ ui: on, resolved: null }), SCOPE)).toBe(false)
expect(
isWorkspaceHiddenBySidebarFilters(deps({ ui: on, meta: CLI_META }), 'folder:missing')
).toBe(false)
expect(
isWorkspaceHiddenBySidebarFilters(
deps({
ui: on,
resolved: null,
meta: CLI_META,
metaById: { child: {} },
lineage: { child: { worktreeId: 'child', parentWorktreeId: WORKSPACE_ID } }
}),
SCOPE
)
).toBe(false)
})
it('hides a lineage parent whose children are all hidden too', () => {
expect(
isWorkspaceHiddenBySidebarFilters(
deps({
ui: { hideCliCreatedWorkspaces: true },
resolved: null,
meta: CLI_META,
lineage: { child: { worktreeId: 'child', parentWorktreeId: WORKSPACE_ID } }
}),
SCOPE
)
).toBe(true)
})
it('hides another client’s worktree or folder workspace under Hide other-client workspaces', () => {
const fromPhone = { kind: 'paired-device', deviceId: 'phone' } as const
const hide = { hideWorkspacesFromOtherDevices: true }
expect(
isWorkspaceHiddenBySidebarFilters(
deps({ ui: hide, resolved: [listed({ creatorProvenance: fromPhone })] }),
SCOPE
)
).toBe(true)
expect(
isWorkspaceHiddenBySidebarFilters(
deps({ ui: hide, meta: { creatorProvenance: { kind: 'host' } } }),
SCOPE
)
).toBe(false)
const folder: FolderWorkspace = {
id: 'folder-1',
projectGroupId: 'g1',
name: 'notes',
folderPath: '/notes',
creatorProvenance: fromPhone,
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
createdAt: 0,
updatedAt: 0
}
const folderScope = 'folder:folder-1'
expect(
isWorkspaceHiddenBySidebarFilters(deps({ ui: hide, folders: [folder] }), folderScope)
).toBe(true)
expect(
isWorkspaceHiddenBySidebarFilters(
deps({ ui: { hideCliCreatedWorkspaces: true }, folders: [folder] }),
folderScope
)
).toBe(false)
})
})
describe('isWorkspaceNotificationMuted', () => {
const hidden = { ui: { hideCliCreatedWorkspaces: true }, resolved: null, meta: CLI_META }
const notify = (notifyHiddenWorkspaces: boolean) => ({
...getDefaultNotificationSettings(),
notifyHiddenWorkspaces
})
it('mutes a hidden workspace only while "Notify About Hidden Workspaces" is off', () => {
expect(
isWorkspaceNotificationMuted(deps({ ...hidden, notifications: notify(false) }), SCOPE)
).toBe(true)
expect(
isWorkspaceNotificationMuted(deps({ ...hidden, notifications: notify(true) }), SCOPE)
).toBe(false)
expect(isWorkspaceNotificationMuted(deps({ notifications: notify(false) }), SCOPE)).toBe(false)
})
it('reads the saved value as loaded from disk, and the default (off) for a file without it', () => {
const saved = normalizeNotificationSettings({ notifyHiddenWorkspaces: true })
expect(isWorkspaceNotificationMuted(deps({ ...hidden, notifications: saved }), SCOPE)).toBe(
false
)
const older = normalizeNotificationSettings({ enabled: true })
expect(older.notifyHiddenWorkspaces).toBe(false)
expect(isWorkspaceNotificationMuted(deps({ ...hidden, notifications: older }), SCOPE)).toBe(
true
)
})
it('reads the persisted default (off), and fails open with no notification settings', () => {
expect(getDefaultNotificationSettings().notifyHiddenWorkspaces).toBe(false)
expect(
isWorkspaceNotificationMuted(
deps({ ...hidden, notifications: getDefaultNotificationSettings() }),
SCOPE
)
).toBe(true)
expect(isWorkspaceNotificationMuted(deps({ ...hidden, notifications: undefined }), SCOPE)).toBe(
false
)
})
})
@@ -479,59 +479,4 @@ describe('external worktree discovery for paired clients', () => {
runtime.notifyReposChangedForRemoteClients()
expect(resultType(await reader.next('collision-events'))).toBe('reposChanged')
})
it('keeps the shared runtime publication valid without a headed renderer', async () => {
vi.mocked(listWorktrees).mockResolvedValue([initialWorktree])
vi.mocked(listWorktreesStrict).mockResolvedValue([initialWorktree])
const runtime = new OrcaRuntimeService(makeStore() as never)
const userDataPath = mkdtempSync(join(tmpdir(), 'o-ewd-h-'))
tempDirs.push(userDataPath)
const server = new OrcaRuntimeRpcServer({
runtime,
userDataPath,
enableWebSocket: true,
wsPort: 0
})
servers.push(server)
await server.start()
const offer = server.createPairingOffer({
address: '127.0.0.1',
name: 'headless-client',
scope: 'runtime'
})
if (!offer.available) {
throw new Error('pairing_unavailable')
}
const client = await authenticate(offer.pairingUrl)
const reader = createReader(client)
sessions.push(client)
readers.push(reader)
send(client, { id: 'headless-events', method: 'runtime.clientEvents.subscribe' })
await reader.next('headless-events', (response) => resultType(response) === 'ready')
send(client, {
id: 'headless-initial-catalog',
method: 'worktree.list',
params: { repo: REPO_ID, limit: 100 }
})
expect(catalogPaths(await reader.next('headless-initial-catalog'))).toEqual([
initialWorktreePath
])
vi.mocked(listWorktrees).mockResolvedValue([initialWorktree, externalWorktree])
vi.mocked(listWorktreesStrict).mockResolvedValue([initialWorktree, externalWorktree])
runtime.notifyWorktreeCatalogChangedForRemoteClients(REPO_ID)
await expect(
reader.next('headless-events', (response) => resultType(response) === 'worktreesChanged')
).resolves.toMatchObject({ result: { repoId: REPO_ID } })
send(client, {
id: 'headless-refreshed-catalog',
method: 'worktree.list',
params: { repo: REPO_ID, limit: 100 }
})
expect(catalogPaths(await reader.next('headless-refreshed-catalog'))).toEqual([
initialWorktreePath,
externalWorktreePath
])
})
})
@@ -1,133 +0,0 @@
import type * as WorktreeListingModule from '../../../git/worktrees/catalog/worktree-listing'
// Headless hosts never load ipc/worktrees; runtime changes must invalidate both scan generation and cache.
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Repo } from '../../../../shared/repo-types'
import type { DetectedWorktreeListResult } from '../../../../shared/worktree/types'
const electronMocks = vi.hoisted(() => {
const ipcMain = {
on: vi.fn(() => ipcMain),
removeListener: vi.fn(() => ipcMain),
emit: vi.fn(() => true)
}
return {
BrowserWindow: { fromId: vi.fn((): unknown => null) },
webContents: { fromId: vi.fn((): unknown => null) },
ipcMain,
app: { getPath: vi.fn(() => '/tmp'), isPackaged: false }
}
})
vi.mock('../../../git/worktrees/catalog/worktree-listing', async (importOriginal) => {
return {
...(await importOriginal<typeof WorktreeListingModule>()),
listWorktreesStrict: listWorktreesStrictMock
}
})
vi.mock('electron', () => electronMocks)
const listWorktreesStrictMock = vi.hoisted(() => vi.fn())
vi.mock('./repo-worktree-admin-fingerprint', () => ({
readRepoWorktreeAdminFingerprint: vi.fn(async () => null)
}))
import { OrcaRuntimeService } from '../../orca-runtime'
import { runWorktreeChangeInvalidators } from '../../../ipc/worktrees/listing/worktree-change-invalidators'
import { getLocalWorktreeScanGeneration } from '../../../workspaces/catalog/local-worktree-scan-generation'
import { partialRuntimeStore } from '../../orca-runtime-tests/runtime-store-partial-fixture'
const REPO_ID = 'repo-local'
const REPO_PATH = '/Users/me/dev/app'
const CREATED_PATH = '/Users/me/dev/app-created'
const repo: Repo = {
id: REPO_ID,
path: REPO_PATH,
displayName: 'app',
badgeColor: 'blue',
addedAt: 1
}
function row(path: string) {
return { path, head: 'abc', branch: 'main', isBare: false, isMainWorktree: path === REPO_PATH }
}
function makeStore() {
return {
getRepo: (id: string) => (id === REPO_ID ? repo : undefined),
getRepos: () => [repo],
getAllWorktreeMeta: () => ({}),
getWorktreeMeta: () => undefined,
setWorktreeMeta: vi.fn(),
removeWorktreeMeta: () => {},
getAllWorktreeLineage: () => ({}),
getAllWorkspaceLineage: () => ({}),
removeWorktreeLineage: vi.fn(),
removeWorkspaceLineage: vi.fn(),
getSettings: () => ({
workspaceDir: '/tmp/workspaces',
nestWorkspaces: false,
refreshLocalBaseRefOnWorktreeCreate: false,
branchPrefix: 'none',
branchPrefixCustom: ''
}),
getProjects: () => []
}
}
function makeRuntimeService(): OrcaRuntimeService {
return new OrcaRuntimeService(partialRuntimeStore(makeStore()))
}
function makeRuntime(): () => Promise<DetectedWorktreeListResult> {
const runtime = makeRuntimeService()
return () => runtime.listDetectedManagedWorktrees(`id:${REPO_ID}`)
}
describe('runtime worktree change invalidation', () => {
beforeEach(() => {
listWorktreesStrictMock.mockReset()
listWorktreesStrictMock.mockResolvedValue([row(REPO_PATH)])
})
it('bumps that generation from the change event the runtime itself sends, with no window notifier attached', () => {
const runtime = makeRuntimeService()
const before = getLocalWorktreeScanGeneration(REPO_ID)
runtime.notifyBranchRenamed(REPO_ID)
expect(getLocalWorktreeScanGeneration(REPO_ID)).not.toBe(before)
})
it('clears the runtime scan cache so the next listing scans again', async () => {
const list = makeRuntime()
await list()
await list()
expect(listWorktreesStrictMock).toHaveBeenCalledTimes(1)
runWorktreeChangeInvalidators(REPO_ID)
await list()
expect(listWorktreesStrictMock).toHaveBeenCalledTimes(2)
})
it('re-lists a scan a worktree change overtook and publishes the rows that include the change', async () => {
const list = makeRuntime()
listWorktreesStrictMock
.mockImplementationOnce(async () => {
runWorktreeChangeInvalidators(REPO_ID)
return [row(REPO_PATH)]
})
.mockResolvedValueOnce([row(REPO_PATH), row(CREATED_PATH)])
const result = await list()
expect(listWorktreesStrictMock).toHaveBeenCalledTimes(2)
expect(result).toMatchObject({ authoritative: true, source: 'git' })
expect(result.worktrees.map((worktree) => worktree.path).sort()).toEqual([
REPO_PATH,
CREATED_PATH
])
})
})
@@ -1,17 +1,16 @@
import { joinResolvedWorktreeScans } from './resolved-worktree-scan-join'
import { getRepoHostIdentityForParts } from '../../../../shared/repo-host-identity'
import { WorkspaceGitIdentity } from '../../sidebar-inputs/workspace-git-identity'
import './resolved-worktree-scan-join'
import '../../../../shared/repo-host-identity'
import '../../sidebar-inputs/workspace-git-identity'
import { describe, expect, it, vi } from 'vitest'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import type { Repo } from '../../../../shared/repo-types'
import type { WorktreeMeta } from '../../../../shared/worktree/meta-types'
import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types'
import type { GitWorktreeInfo, Worktree } from '../../../../shared/worktree/types'
import type { Store } from '../../../persistence/loading-store/store'
import { mergeWorktreeMetaForWrite } from '../../../persistence/loading-store/worktree-meta-write-normalization'
import { buildDetectedGitWorktrees } from '../../../ipc/worktrees/listing/ssh-worktree-fallback'
import {
listStoredWorktreeRowsForRepo,
resolveRepoWorktreeRows,
resolveScopedWorktreeIdRow,
type RepoWorktreeRowDeps
@@ -72,80 +71,6 @@ function createDeps(repos: Repo[]): RepoWorktreeRowDeps & {
}
describe('host-qualified scoped worktree resolution', () => {
it('projects cross-repository lineage by scanning only related repos on the selected host', async () => {
const childRepo = repo('child-repo', '/child-repo', { executionHostId: 'local' })
const parentRepo = repo('parent-repo', '/parent-repo', { executionHostId: 'local' })
const remoteParent = repo('parent-repo', '/remote-parent-repo', {
connectionId: 'builder',
executionHostId: 'ssh:builder'
})
const unrelated = repo('unrelated', '/unrelated', { executionHostId: 'local' })
const deps = createDeps([childRepo, parentRepo, remoteParent, unrelated])
const childId = 'child-repo::/child'
const parentId = 'parent-repo::/parent'
deps.metaById[childId] = mergeWorktreeMetaForWrite(undefined, {
instanceId: 'child-instance',
hostId: 'local',
projectId: 'child-project'
})
deps.metaById[parentId] = mergeWorktreeMetaForWrite(undefined, {
instanceId: 'parent-instance',
hostId: 'local',
projectId: 'parent-project'
})
const lineage: WorktreeLineage = {
worktreeId: childId,
worktreeInstanceId: 'child-instance',
parentWorktreeId: parentId,
parentWorktreeInstanceId: 'parent-instance',
origin: 'cli',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
createdAt: 1
}
Object.assign(deps.store, { getAllWorktreeLineage: () => ({ [childId]: lineage }) })
deps.scanRepo.mockImplementation(async (owner) => ({
ok: true,
worktrees: [gitWorktree(owner.id === 'child-repo' ? '/child' : '/parent')]
}))
await expect(resolveScopedWorktreeIdRow(deps, childId, 'local')).resolves.toMatchObject({
parentWorktreeId: parentId,
lineage
})
expect(deps.scanRepo.mock.calls.map(([owner]) => owner)).toEqual([childRepo, parentRepo])
deps.scanRepo.mockClear()
await expect(resolveScopedWorktreeIdRow(deps, parentId, 'local')).resolves.toMatchObject({
childWorktreeIds: [childId]
})
expect(deps.scanRepo.mock.calls.map(([owner]) => owner)).toEqual([parentRepo, childRepo])
})
it('scans only the selected local or SSH owner when repo ids and paths collide', async () => {
const owners = [
repo('shared', '/local/repo', { executionHostId: 'local' }),
repo('shared', '/remote/repo', {
connectionId: 'builder',
executionHostId: 'ssh:builder'
}),
repo('unrelated', '/unrelated/repo', { connectionId: 'slow-box' })
]
const deps = createDeps(owners)
const worktreeId = 'shared::/same/worktree'
await expect(
resolveScopedWorktreeIdRow(deps, worktreeId, 'ssh:builder')
).resolves.toMatchObject({ id: worktreeId, hostId: 'ssh:builder' })
expect(deps.scanRepo.mock.calls.map(([owner]) => owner)).toEqual([owners[1]])
deps.scanRepo.mockClear()
await expect(resolveScopedWorktreeIdRow(deps, worktreeId, 'local')).resolves.toMatchObject({
id: worktreeId,
hostId: 'local'
})
expect(deps.scanRepo.mock.calls.map(([owner]) => owner)).toEqual([owners[0]])
})
it('does not project another host metadata onto a colliding scoped row', async () => {
const deps = createDeps([
repo('shared', '/local/repo', { executionHostId: 'local' }),
@@ -170,94 +95,6 @@ describe('host-qualified scoped worktree resolution', () => {
displayName: 'feature'
})
})
it('uses the host-qualified metadata owner when a legacy row has another host', async () => {
const deps = createDeps([
repo('shared', '/remote/repo', {
connectionId: 'builder',
executionHostId: 'ssh:builder'
})
])
const worktreeId = 'shared::/same/worktree'
const remoteMeta = {
displayName: 'remote workspace',
hostId: 'ssh:builder',
instanceId: 'remote-instance'
} as unknown as WorktreeMeta
deps.metaById[worktreeId] = {
displayName: 'stale local workspace',
hostId: 'local',
instanceId: 'local-instance'
} as unknown as WorktreeMeta
;(
deps.store as Store & {
getWorktreeMetaForHost: () => WorktreeMeta
}
).getWorktreeMetaForHost = () => remoteMeta
const rows = await resolveRepoWorktreeRows(
deps,
deps.store.getRepos()[0]!,
deps.metaById,
new Map()
)
expect(rows[0]).toMatchObject({
displayName: 'remote workspace',
hostId: 'ssh:builder',
instanceId: 'remote-instance'
})
})
it('restores canonical-only SSH rows without leaking colliding local metadata', () => {
const local = repo('shared', '/local/repo', { executionHostId: 'local' })
const remote = repo('shared', '/remote/repo', {
connectionId: 'builder',
executionHostId: 'ssh:builder'
})
const deps = createDeps([local, remote])
deps.metaById['shared::/local/worktree'] = {
displayName: 'local workspace',
hostId: 'local'
} as WorktreeMeta
;(
deps.store as Store & {
getAllWorktreeMetaForHost: () => Record<string, WorktreeMeta>
}
).getAllWorktreeMetaForHost = () => ({
'shared::/remote/worktree': {
displayName: 'remote workspace',
hostId: 'ssh:builder'
} as unknown as WorktreeMeta
})
expect(listStoredWorktreeRowsForRepo(deps.store, remote, 2)).toEqual([
expect.objectContaining({ path: '/remote/worktree' })
])
})
it.each([
['runtime:windows', String.raw`C:\Users\dev\orca worktree`],
['local', '/mnt/c/Users/dev/orca worktree']
] satisfies [ExecutionHostId, string][])(
'keeps %s path resolution scoped',
async (hostId, path) => {
const target = repo('shared', path, { executionHostId: hostId })
const deps = createDeps([
target,
repo('shared', '/other-host/repo', { executionHostId: 'ssh:unrelated' }),
repo('unrelated', '/unrelated/repo')
])
deps.scanRepo.mockImplementation(async () => ({ ok: true, worktrees: [gitWorktree(path)] }))
const worktreeId = `shared::${path}`
await expect(resolveScopedWorktreeIdRow(deps, worktreeId, hostId)).resolves.toMatchObject({
id: worktreeId,
hostId,
path
})
expect(deps.scanRepo.mock.calls.map(([owner]) => owner)).toEqual([target])
}
)
it('resolves only the selected folder-workspace owner without scanning Git providers', async () => {
const local = repo('folders', '/local/folders', {
@@ -296,65 +133,10 @@ describe('host-qualified scoped worktree resolution', () => {
expect(deps.listFolderWorkspaces).toHaveBeenCalledExactlyOnceWith(remote, 2)
expect(deps.scanRepo).not.toHaveBeenCalled()
})
it('does not scan when the requested host does not own the repo id', async () => {
const deps = createDeps([
repo('shared', '/local/repo', { executionHostId: 'local' }),
repo('unrelated', '/unrelated/repo', { executionHostId: 'runtime:other' })
])
await expect(
resolveScopedWorktreeIdRow(deps, 'shared::/same/worktree', 'ssh:builder')
).resolves.toBeNull()
expect(deps.scanRepo).not.toHaveBeenCalled()
})
it('reuses fleet owner counts without reloading repos per row', async () => {
const owners = Array.from({ length: 100 }, (_, index) =>
repo(`repo-${index}`, `/repos/${index}`, { executionHostId: 'local' })
)
const deps = createDeps(owners)
const getRepos = vi.spyOn(deps.store, 'getRepos')
await Promise.all(
owners.map((owner) => resolveRepoWorktreeRows(deps, owner, deps.metaById, new Map(), 1))
)
expect(getRepos).not.toHaveBeenCalled()
})
})
/** Normalize id-selector paths like path selectors so UI removal resolves the same host-qualified row as CLI (#16243). */
describe('scoped worktree id resolution across path spellings (#16243)', () => {
it.each([
['a trailing slash', '/same/worktree', 'shared::/same/worktree/'],
['a doubled separator', '/same/worktree', 'shared::/same//worktree'],
['an NFD name', '/same/café', `shared::${'/same/café'.normalize('NFD')}`]
])('resolves the scanned row when the id carries %s', async (_label, scannedPath, worktreeId) => {
const owner = repo('shared', '/local/repo', { executionHostId: 'local' })
const deps = createDeps([owner])
deps.scanRepo.mockImplementation(async () => ({
ok: true,
worktrees: [gitWorktree(scannedPath)]
}))
await expect(resolveScopedWorktreeIdRow(deps, worktreeId, 'local')).resolves.toMatchObject({
id: `shared::${scannedPath}`,
path: scannedPath
})
})
it('still refuses the same path under a different repo id', async () => {
const deps = createDeps([
repo('shared', '/local/repo', { executionHostId: 'local' }),
repo('unrelated', '/unrelated/repo', { executionHostId: 'local' })
])
await expect(
resolveScopedWorktreeIdRow(deps, 'unrelated::/same/worktree/', 'local')
).resolves.toBeNull()
})
it('refuses rather than guessing when two rows spell one path', async () => {
const owner = repo('shared', '/local/repo', { executionHostId: 'local' })
const deps = createDeps([owner])
@@ -368,19 +150,6 @@ describe('scoped worktree id resolution across path spellings (#16243)', () => {
).resolves.toBeNull()
})
it('prefers the exactly matching row over an equivalent spelling', async () => {
const owner = repo('shared', '/local/repo', { executionHostId: 'local' })
const deps = createDeps([owner])
deps.scanRepo.mockImplementation(async () => ({
ok: true,
worktrees: [gitWorktree('/same//worktree'), gitWorktree('/same/worktree')]
}))
await expect(
resolveScopedWorktreeIdRow(deps, 'shared::/same//worktree', 'local')
).resolves.toMatchObject({ id: 'shared::/same//worktree' })
})
// #15598/#15616: the backslash spelling is what a pre-restart Windows registration recorded.
it('resolves a Windows backslash id against the forward-slash spelling git reports', async () => {
const path = 'D:/Agentic/game2/battle-core'
@@ -392,16 +161,6 @@ describe('scoped worktree id resolution across path spellings (#16243)', () => {
resolveScopedWorktreeIdRow(deps, 'shared::D:\\Agentic\\game2\\battle-core', 'runtime:windows')
).resolves.toMatchObject({ id: `shared::${path}`, path })
})
it.each([
['no repo boundary', 'not-an-id'],
['an empty path', 'shared::']
])('keeps exact matching for a malformed id with %s', async (_label, worktreeId) => {
const deps = createDeps([repo('shared', '/local/repo', { executionHostId: 'local' })])
await expect(resolveScopedWorktreeIdRow(deps, worktreeId, 'local')).resolves.toBeNull()
expect(deps.scanRepo).not.toHaveBeenCalled()
})
})
describe('folder-to-Git checkout identity', () => {
@@ -442,31 +201,3 @@ describe('folder-to-Git checkout identity', () => {
}
)
})
it('stamps the scan/sidebar join once and keeps the title across detach', () => {
const owner = repo('row-owner', '/repo', { executionHostId: 'local' })
const deps = createDeps([owner])
const writes = vi.spyOn(deps.store, 'setWorktreeMeta').mockImplementation(({ id }, updates) => {
const next = mergeWorktreeMetaForWrite(deps.metaById[id], updates)
deps.metaById[id] = next
return next
})
const key = getRepoHostIdentityForParts(owner.id, 'local')
const scans = new Map([
[key, { ok: true as const, worktrees: [gitWorktree('/repo/work')], gitRun: {} }]
])
const first = joinResolvedWorktreeScans(deps.store, () => deps, scans)
expect(writes).toHaveBeenCalledOnce()
const identity = new WorkspaceGitIdentity()
const rows = (joined: typeof first) => joined.worktrees.map((row) => ({ ...row, gitRun: {} }))
identity.observeScan(rows(first), first.scannedRepoKeys)
scans.set(key, {
ok: true,
worktrees: [{ ...gitWorktree('/repo/work'), branch: '' }],
gitRun: {}
})
const detached = joinResolvedWorktreeScans(deps.store, () => deps, scans)
identity.observeScan(rows(detached), detached.scannedRepoKeys)
expect(identity.title(detached.worktrees[0]!)).toBe(first.worktrees[0]!.displayName)
expect(writes).toHaveBeenCalledOnce()
})
@@ -36,14 +36,12 @@ vi.mock(
import { RuntimeManagedWorktreeQueries } from './runtime-managed-worktree-queries'
import type { RuntimeStore } from '../../runtime-store-contract'
import { partialRuntimeStore } from '../../orca-runtime-tests/runtime-store-partial-fixture'
import { getRepoHostIdentityForParts } from '../../../../shared/repo-host-identity'
import '../../orca-runtime-tests/runtime-store-partial-fixture'
import '../../../../shared/repo-host-identity'
import {
bumpLocalWorktreeScanGeneration,
getLocalWorktreeScanGeneration,
resetLocalWorktreeScanGenerationsForTests
} from '../../../workspaces/catalog/local-worktree-scan-generation'
import type { HostWorktreeTarget } from '../../../../shared/worktree/host-qualified-identity'
const settings: ReturnType<RuntimeStore['getSettings']> = {
workspaceDir: '/worktrees',
@@ -96,101 +94,7 @@ function queries(
})
}
describe('RuntimeManagedWorktreeQueries.buildVisibilityMatchers', () => {
it.each([false, true])(
'keeps host-qualified matcher inputs and defaults (reversed=%s)',
(reversed) => {
const local = folderRepo({
customWorktreeVisibilitySources: [{ id: 'local-source', rootPath: '/local-source' }]
})
const remote = folderRepo({
connectionId: 'builder',
path: '/remote/app',
worktreeBasePath: '.claude/worktrees',
customWorktreeVisibilitySources: [{ id: 'remote-source', rootPath: '/remote-source' }]
})
const repos = reversed ? [remote, local] : [local, remote]
const worktrees = ['/checkout/first', '/checkout/second'].map((path, index) => ({
...mergeWorktree(
local.id,
{ path, head: 'abc', branch: 'topic', isBare: false, isMainWorktree: false },
undefined
),
hostId: index === 0 ? ('local' as const) : ('ssh:builder' as const)
}))
const defaults = { customSources: [{ id: 'default-source', rootPath: '/default-source' }] }
const catalog = queries(
partialRuntimeStore({
getRepos: () => [...repos, folderRepo({ id: 'without-checkout' })],
getSettings: () => ({ ...settings, worktreeVisibilityDefaults: defaults })
})
)
const matchers = catalog.buildVisibilityMatchers(worktrees)
expect([...matchers.keys()]).toEqual([
...repos.map((repo) =>
getRepoHostIdentityForParts(repo.id, repo.connectionId ? 'ssh:builder' : 'local')
),
getRepoHostIdentityForParts('without-checkout', 'local')
])
for (const [index, hostId] of (['local', 'ssh:builder'] as const).entries()) {
const key = getRepoHostIdentityForParts(local.id, hostId)
const matcher = matchers.get(key)!
expect(matcher(`${worktrees[index]!.path}/.claude/worktrees/topic`)).toEqual({
kind: 'built-in',
id: 'claude'
})
expect(matcher(`${worktrees[1 - index]!.path}/.claude/worktrees/topic`)).toBeNull()
expect(matcher('/local-source/topic')).toEqual(
index === 0 ? { kind: 'custom', id: 'local-source' } : null
)
expect(matcher('/remote-source/topic')).toEqual(
index === 1 ? { kind: 'custom', id: 'remote-source' } : null
)
expect(
matcher(`${index === 0 ? local.path : remote.path}/.claude/worktrees/topic`)
).toEqual(index === 0 ? { kind: 'built-in', id: 'claude' } : null)
expect(matcher('/default-source/topic')).toEqual({ kind: 'custom', id: 'default-source' })
expect(
catalog.buildVisibilityMatchers(worktrees, false).get(key)!('/default-source/topic')
).toBeNull()
expect(
catalog.buildVisibilityMatchers(worktrees, true, settings).get(key)!(
'/default-source/topic'
)
).toBeNull()
}
}
)
})
describe('RuntimeManagedWorktreeQueries.listDetected', () => {
it('stamps a scanned listing with the generation its scan began at', async () => {
resetLocalWorktreeScanGenerationsForTests()
const repo = folderRepo({ kind: 'git', path: '/source/git-app' })
const store = {
getRepos: () => [repo],
getRepo: () => repo,
getAllWorktreeMeta: () => ({}),
getWorktreeMeta: () => undefined,
setWorktreeMeta: vi.fn(),
getAllWorktreeLineage: () => ({}),
getSettings: () => settings
} as unknown as RuntimeStore
const atScanStart = getLocalWorktreeScanGeneration(repo.id)
const scanRepo = vi.fn(async () => {
// A mutation lands after the scan settles; the listing still names the catalog it scanned.
return { ok: true as const, worktrees: [] }
})
const listing = await queries(store, { scanRepo }).listDetected(repo)
bumpLocalWorktreeScanGeneration(repo.id)
expect(listing.authoritative).toBe(true)
expect(listing.catalogVersion?.sequence).toBe(atScanStart)
expect(listing.catalogVersion?.epoch).toEqual(expect.any(String))
expect(getLocalWorktreeScanGeneration(repo.id)).toBeGreaterThan(atScanStart)
})
it("does not project another host's folder metadata", async () => {
const local = folderRepo()
const remote = folderRepo({ connectionId: 'build-box', displayName: 'Remote app' })
@@ -215,149 +119,9 @@ describe('RuntimeManagedWorktreeQueries.listDetected', () => {
displayName: 'Local app'
})
})
it('omits host-owned source defaults for clients that do not support them', async () => {
const repo = folderRepo({ path: '/source/app' })
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial store mock; the code under test reads only the members it defines.
const store = {
getRepos: () => [repo],
getRepo: () => repo,
getAllWorktreeMeta: () => ({}),
getWorktreeMeta: () => undefined,
setWorktreeMeta: vi.fn((_target: HostWorktreeTarget, updates) => metadata(updates)),
getAllWorktreeLineage: () => ({}),
getSettings: () => ({
...settings,
worktreeVisibilityDefaults: {
external: 'show' as const,
customSources: [{ id: 'host-source', rootPath: '/source' }],
sourcePreferences: { custom: { 'host-source': 'show' as const } }
}
})
} as unknown as RuntimeStore
const current = await queries(store).listDetected(repo, true)
const legacy = await queries(store).listDetected(repo, false)
expect(current.worktrees[0]).toMatchObject({
visibilitySource: { kind: 'custom', id: 'host-source' }
})
expect(legacy.worktrees[0]).not.toHaveProperty('visibilitySource')
})
})
describe('RuntimeManagedWorktreeQueries.list host scope', () => {
it('uses the resolved legacy host matcher and omits ambiguous legacy rows', async () => {
const repo = folderRepo({
kind: 'git',
connectionId: 'builder',
customWorktreeVisibilitySources: [{ id: 'remote-source', rootPath: '/source' }],
worktreeVisibilitySourcePreferences: { custom: { 'remote-source': 'hide' } }
})
const git: GitWorktreeInfo = {
path: '/source/topic',
head: '',
branch: 'topic',
isBare: false,
isMainWorktree: false
}
const row: ResolvedWorktree = {
...mergeWorktree(repo.id, git, undefined),
git,
parentWorktreeId: null,
childWorktreeIds: [],
lineage: null
}
let repos = [repo]
let rows = [row]
const listing = queries(
partialRuntimeStore({ getRepos: () => repos, getSettings: () => settings }),
{
listResolved: async () => rows
}
)
expect((await listing.list(undefined, 10)).worktrees).toEqual([])
repos = [repo, folderRepo({ kind: 'git' })]
expect((await listing.list(undefined, 10)).worktrees).toEqual([])
rows = [
{ ...row, hostId: 'ssh:builder' },
{ ...row, hostId: 'local' }
]
expect((await listing.list('remote', 10)).worktrees).toEqual([])
})
it.each([5, 50])('bounds listing hydration and ownership work at scale %s', async (size) => {
let idReads = 0
let hydrated = 0
const local = folderRepo({ kind: 'git', externalWorktreeVisibility: 'hide' })
const ssh = folderRepo({
kind: 'git',
connectionId: 'builder',
externalWorktreeVisibility: 'show'
})
const repos = [
local,
ssh,
...Array.from({ length: size }, (_, index) => folderRepo({ id: `unrelated-${index}` }))
]
const getRepos = vi.fn(() =>
repos.map((repo) => {
hydrated++
return {
...repo,
get id() {
idReads++
return repo.id
}
}
})
)
const rows: ResolvedWorktree[] = [local, ssh].flatMap((repo) =>
Array.from({ length: size }, (_, index) => {
const git: GitWorktreeInfo = {
path: index === 0 ? repo.path : `/outside/work-${index}`,
head: 'abc',
branch: `branch-${index}`,
isBare: false,
isMainWorktree: index === 0
}
return {
...mergeWorktree(repo.id, git, undefined),
hostId: repo.connectionId ? ('ssh:builder' as const) : ('local' as const),
parentWorktreeId: null,
childWorktreeIds: [],
lineage: null,
git
}
})
)
const store: RuntimeStore = {
getRepo: vi.fn<RuntimeStore['getRepo']>(),
addRepo: vi.fn<RuntimeStore['addRepo']>(),
updateRepo: vi.fn<RuntimeStore['updateRepo']>(),
setWorktreeMeta: vi.fn<RuntimeStore['setWorktreeMeta']>(),
removeWorktreeMeta: vi.fn<RuntimeStore['removeWorktreeMeta']>(),
getGitHubCache: vi.fn<RuntimeStore['getGitHubCache']>(),
getRepos,
getAllWorktreeMeta: () => ({}),
getWorktreeMeta: () => undefined,
getSettings: () => settings
}
const listing = queries(store, {
listResolved: async () => rows,
listKnownHostIds: () => ['local', 'ssh:builder']
})
const page = await listing.list(undefined, 2)
expect(page.totalCount).toBe(size + 1)
expect(page.truncated).toBe(true)
expect(page.worktrees.map((row) => row.hostId)).toEqual(['local', 'ssh:builder'])
expect(getRepos.mock.calls.length).toBeLessThanOrEqual(3)
expect(hydrated).toBeLessThanOrEqual(3 * repos.length)
expect(idReads).toBeLessThanOrEqual(20 * (repos.length + rows.length))
})
it.each([false, true])(
'preserves host twins, expandable coverage and per-host visibility (reversed=%s)',
async (reversed) => {
@@ -465,82 +229,6 @@ describe('RuntimeManagedWorktreeQueries.list host scope', () => {
hiddenHostIds.clear()
}
})
it('does not report the scoped host as omitted once it contributes rows', async () => {
const store = sshStore()
const result = await queries(store, {
listResolved: async () =>
[
{
id: 'repo-ssh::/home/dev/app',
repoId: 'repo-ssh',
path: '/home/dev/app',
hostId: 'ssh:conn-1'
}
] as never
}).list('repo-ssh', 50)
expect(result.hostScope?.hostIds).toEqual(['ssh:conn-1'])
expect(result.hostScope?.omittedHostIds).toEqual([])
})
// The caller scoped the listing, so the hosts they excluded must not come back as gaps.
it('never names a host the caller scoped out', async () => {
const scoped = await queries(sshStore(), {
listKnownHostIds: () => ['local', 'ssh:other', 'runtime:elsewhere'] as never
}).list('repo-ssh', 50)
expect(scoped.hostScope?.omittedHostIds).toEqual(['ssh:conn-1'])
})
// Check both host spellings so scoped coverage names the actual execution host.
it('names the local host for a scoped local repo', async () => {
const repo = folderRepo({ id: 'repo-local', kind: 'git', path: '/workspace/local' })
const store = {
getRepos: () => [repo],
getRepo: () => repo,
getAllWorktreeMeta: () => ({}),
getWorktreeMeta: () => undefined,
setWorktreeMeta: vi.fn(),
getAllWorktreeLineage: () => ({}),
getSettings: () => settings
} as unknown as RuntimeStore
const result = await queries(store).list('repo-local', 50)
expect(result.hostScope?.omittedHostIds).toEqual(['local'])
})
it('prefers executionHostId over connectionId for the scoped host', async () => {
const repo = folderRepo({
id: 'repo-runtime',
kind: 'git',
connectionId: 'conn-legacy',
executionHostId: 'runtime:env-1',
path: '/workspace/runtime'
})
const store = {
getRepos: () => [repo],
getRepo: () => repo,
getAllWorktreeMeta: () => ({}),
getWorktreeMeta: () => undefined,
setWorktreeMeta: vi.fn(),
getAllWorktreeLineage: () => ({}),
getSettings: () => settings
} as unknown as RuntimeStore
const result = await queries(store).list('repo-runtime', 50)
expect(result.hostScope?.omittedHostIds).toEqual(['runtime:env-1'])
})
it('still reports every configured host when the listing is unscoped', async () => {
const unscoped = await queries(sshStore(), {
listKnownHostIds: () => ['local', 'ssh:conn-1'] as never
}).list(undefined, 50)
expect(unscoped.hostScope?.omittedHostIds).toEqual(['local', 'ssh:conn-1'])
})
})
describe('RuntimeManagedWorktreeQueries.listDetected overtaken scan', () => {
@@ -601,35 +289,6 @@ describe('RuntimeManagedWorktreeQueries.listDetected overtaken scan', () => {
expect(pruneLineageMock.mock.calls[0]?.[2]).toEqual(rows([repo.path, created]))
})
it('prunes metadata against the expectation and generation of the scan it publishes', async () => {
const expectations: unknown[] = []
const generations: number[] = []
const store = gitStore({
pruneSessionlessMissingLocalWorktreeMetadataForRepo: vi.fn(() => []),
getProfileStorageDirectory: () => '/profile',
captureNativeLocalWorktreeMetadataScanExpectation: vi.fn(() => {
const expectation = { repo, metadata: [] }
expectations.push(expectation)
generations.push(getLocalWorktreeScanGeneration(repo.id))
return expectation
})
})
const scanRepo = vi
.fn()
.mockImplementationOnce(overtakenScan([repo.path]))
.mockResolvedValueOnce({ ok: true, worktrees: rows([repo.path, created]) })
await queries(store, { scanRepo }).listDetected(repo)
expect(expectations).toHaveLength(2)
expect(pruneMetadataMock).toHaveBeenCalledTimes(1)
expect(pruneMetadataMock.mock.calls[0]?.[0]).toMatchObject({
scan: expectations[1],
scanGeneration: generations[1],
gitWorktrees: rows([repo.path, created])
})
})
it('publishes the last scan non-authoritatively once the re-scan bound is spent', async () => {
const scanRepo = vi.fn(overtakenScan([repo.path]))
@@ -644,16 +303,4 @@ describe('RuntimeManagedWorktreeQueries.listDetected overtaken scan', () => {
expect(pruneLineageMock).not.toHaveBeenCalled()
expect(pruneMetadataMock).not.toHaveBeenCalled()
})
it('does not re-run a failed scan the change overtook', async () => {
const scanRepo = vi.fn(async () => {
bumpLocalWorktreeScanGeneration(repo.id)
return { ok: false as const, worktrees: [] }
})
const result = await queries(gitStore(), { scanRepo }).listDetected(repo)
expect(scanRepo).toHaveBeenCalledTimes(1)
expect(result).toMatchObject({ authoritative: false, source: 'metadata-fallback' })
})
})
@@ -1,69 +0,0 @@
import { describe, expect, it } from 'vitest'
import { RuntimeResolvedWorktreeCache } from './runtime-resolved-worktree-cache'
import type { ResolvedWorktreeSnapshot } from './runtime-resolved-worktree-cache'
import {
getLocalWorktreeScanGeneration,
getWorktreeScanMutationRevision
} from '../../../workspaces/catalog/local-worktree-scan-generation'
function snapshotOf(ids: string[]): ResolvedWorktreeSnapshot {
return {
worktrees: ids.map((id) => ({ id }) as ResolvedWorktreeSnapshot['worktrees'][number]),
platformByRepoId: new Map()
}
}
describe('RuntimeResolvedWorktreeCache', () => {
it('recomputes when the repo inventory moved, even well inside the TTL', async () => {
// Repo registration invalidates earlier snapshots: absence there cannot prove workspace not found.
const cache = new RuntimeResolvedWorktreeCache()
const results = [snapshotOf(['repo-1::/a']), snapshotOf(['repo-1::/a', 'repo-2::/b'])]
let computes = 0
const compute = async (): Promise<ResolvedWorktreeSnapshot> => results[computes++]
await cache.getSnapshot(compute, 60_000, 7)
const afterRegistration = await cache.getSnapshot(compute, 60_000, 8)
expect(computes).toBe(2)
expect(afterRegistration.worktrees.map((worktree) => worktree.id)).toEqual([
'repo-1::/a',
'repo-2::/b'
])
})
it('does not join an in-flight compute that started under a stale inventory', async () => {
const cache = new RuntimeResolvedWorktreeCache()
const computed: number[] = []
const compute = async (): Promise<ResolvedWorktreeSnapshot> => {
computed.push(computed.length)
return snapshotOf([])
}
const first = cache.getSnapshot(compute, 60_000, 7)
const second = cache.getSnapshot(compute, 60_000, 8)
await Promise.all([first, second])
expect(computed).toHaveLength(2)
})
it('keeps a primed snapshot servable when nothing mutated', async () => {
// Headless reattach resolves from a primed cache without Git; non-mutation reads must preserve its freshness.
const cache = new RuntimeResolvedWorktreeCache()
let computes = 0
const prime = async (): Promise<ResolvedWorktreeSnapshot> => {
computes += 1
return snapshotOf(['repo-restore::/tmp/restore-records'])
}
await cache.getSnapshot(prime, 60_000, getWorktreeScanMutationRevision())
// A read that mints a scan generation for a repo nothing has scanned yet is not a mutation.
getLocalWorktreeScanGeneration(`repo-never-scanned-${Math.random()}`)
expect(cache.isFresh(getWorktreeScanMutationRevision())).toBe(true)
const served = await cache.getSnapshot(prime, 60_000, getWorktreeScanMutationRevision())
expect(computes).toBe(1)
expect(served.worktrees.map((worktree) => worktree.id)).toEqual([
'repo-restore::/tmp/restore-records'
])
})
})
@@ -1,78 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { Repo } from '../../../../shared/repo-types'
const { visibleRepos } = vi.hoisted(() => ({
visibleRepos:
vi.fn<(store: { getRepos: () => Repo[]; getSshTargets: () => unknown[] }) => Repo[]>()
}))
vi.mock('../../../ssh/orcad-retained-source', () => ({
visibleRepos,
visibleProjectGroups: vi.fn(() => []),
visibleFolderWorkspaces: vi.fn(() => [])
}))
const { runtimeHiddenHostIds, runtimeHiddenRepos } = await import('./runtime-visible-catalog')
const sshTarget = { id: 'target-1', label: 'dev', host: 'dev', port: 22, username: 'dev' }
const repo = (id: string, connectionId?: string): Repo => ({
id,
...(connectionId ? { connectionId } : {}),
path: `/src/${id}`,
displayName: id,
badgeColor: '#000',
addedAt: 0
})
const storeWith = (repos: Repo[], withTargets: boolean) => ({
getRepos: () => repos,
...(withTargets ? { getSshTargets: () => [sshTarget] } : {})
})
describe('runtime visible catalog', () => {
it('hides a moved local row by host while the WSL host row with the same id stays listed', () => {
const local = repo('moved')
const onHost: Repo = { ...repo('moved'), executionHostId: 'runtime:wsl-ubuntu' }
visibleRepos.mockImplementation((store) => store.getRepos().filter((r) => r !== local))
const hidden = runtimeHiddenRepos(storeWith([local, onHost], true))
expect(hidden.size).toBe(1)
expect(hidden.hasRepo(local)).toBe(true)
expect(hidden.hasRepo(onHost)).toBe(false)
expect(hidden.hasWorktree({ repoId: 'moved', hostId: 'local' })).toBe(true)
expect(hidden.hasWorktree({ repoId: 'moved', hostId: 'runtime:wsl-ubuntu' })).toBe(false)
})
it('keeps a host that still has a visible row', () => {
visibleRepos.mockImplementation((store) => store.getRepos().filter((r) => r.id !== 'a'))
const store = storeWith([repo('a', 'target-2'), repo('b', 'target-2')], true)
expect(runtimeHiddenHostIds(store).size).toBe(0)
})
describe('a repo id shared by a visible host and a retained SSH source', () => {
const local = repo('same-project')
const retained = repo('same-project', 'target-1')
const store = storeWith([local, retained], true)
// Same id on both rows: only the SSH row is the retired source.
const hideRetained = (s: { getRepos: () => Repo[] }): Repo[] =>
s.getRepos().filter((r) => r.connectionId !== 'target-1')
it('still names the retired SSH host as hidden', () => {
visibleRepos.mockImplementation(hideRetained)
expect([...runtimeHiddenHostIds(store)]).toEqual(['ssh:target-1'])
})
it('hides only the worktrees on the retained host', () => {
visibleRepos.mockImplementation(hideRetained)
const hidden = runtimeHiddenRepos(store)
expect(hidden.hasRepo(retained)).toBe(true)
expect(hidden.hasRepo(local)).toBe(false)
expect(hidden.hasWorktree({ repoId: 'same-project', hostId: 'ssh:target-1' })).toBe(true)
expect(hidden.hasWorktree({ repoId: 'same-project', hostId: 'local' })).toBe(false)
// A legacy worktree with no host cannot be placed on either row, and one row is visible.
expect(hidden.hasWorktree({ repoId: 'same-project' })).toBe(false)
})
it('hides a hostless worktree whose every row is hidden', () => {
visibleRepos.mockImplementation(() => [])
expect(runtimeHiddenRepos(store).hasWorktree({ repoId: 'same-project' })).toBe(true)
})
})
})
@@ -1,59 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
getRuntimeWorktreeRemovalOptionsKey,
runtimeRepoMatchesExecutionHost
} from './runtime-worktree-selection'
describe('getRuntimeWorktreeRemovalOptionsKey', () => {
it('separates a waived archive-hook retry from the attempt about to refuse on it (#19334)', () => {
const strict = getRuntimeWorktreeRemovalOptionsKey({ runHooks: true })
expect(
getRuntimeWorktreeRemovalOptionsKey({ runHooks: true, allowFailedArchiveHook: true })
).not.toBe(strict)
})
it('keeps every waiver on its own axis, so none of them coalesce', () => {
const keys = [
{},
{ force: true },
{ deleteBranch: false },
{ deleteBranch: true },
{ runHooks: true },
{ allowUnverifiedPtyStop: true },
{ allowFailedArchiveHook: true }
].map(getRuntimeWorktreeRemovalOptionsKey)
expect(new Set(keys).size).toBe(keys.length)
})
})
describe('runtimeRepoMatchesExecutionHost', () => {
it('matches an unstamped SSH repo against its own host (#11163)', () => {
// Legacy connectionId must match ssh:<target> requests or repo-add/clone dedupe creates duplicate rows.
expect(runtimeRepoMatchesExecutionHost({ connectionId: 'target-1' }, 'ssh:target-1')).toBe(true)
})
it('rejects an unstamped SSH repo against a different SSH host', () => {
expect(runtimeRepoMatchesExecutionHost({ connectionId: 'target-1' }, 'ssh:target-2')).toBe(
false
)
})
it('rejects an unstamped SSH repo against local and runtime hosts', () => {
expect(runtimeRepoMatchesExecutionHost({ connectionId: 'target-1' }, 'local')).toBe(false)
expect(runtimeRepoMatchesExecutionHost({ connectionId: 'target-1' }, 'runtime:env-1')).toBe(
false
)
})
it('keeps a stamped repo bound to the host it names', () => {
expect(runtimeRepoMatchesExecutionHost({ executionHostId: 'runtime:env-1' }, 'local')).toBe(
false
)
expect(
runtimeRepoMatchesExecutionHost(
{ executionHostId: 'local', connectionId: 'target-1' },
'local'
)
).toBe(true)
})
})
@@ -1,548 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import '../../orca-runtime-tests/runtime-test-module-registration.spec'
import { OrcaRuntimeService } from '../../orca-runtime'
import { store, TEST_WORKTREE_ID } from '../../orca-runtime-tests/orca-runtime-test-fixtures.spec'
import type { Repo } from '../../../../shared/repo-types'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import type { ResolvedWorktree } from './runtime-worktree-path-identity'
import type { ResolvedWorktreeSnapshot } from './runtime-resolved-worktree-cache'
import type { DetectedWorktreeListResult } from '../../../../shared/worktree/types'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
FLOATING_TERMINAL_WORKTREE_ID,
getDefaultWorkspaceSession
} from '../../../../shared/constants'
import type { BrowserTabInfo } from '../../../../shared/runtime-browser-contracts'
import type { RuntimeMobileSessionTabsSnapshot } from '../../../../shared/runtime-session-contracts'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
import { firstWorkRenameDeps } from '../../../agent-hooks/first-work-rename-runtime'
const repo: Repo = {
id: 'workspace-query',
path: '/tmp/workspace-query',
displayName: 'Workspace query',
addedAt: 1,
badgeColor: 'blue'
}
const resolved: ResolvedWorktree = {
...store.getWorktreeMeta(TEST_WORKTREE_ID),
id: TEST_WORKTREE_ID,
repoId: 'repo-1',
path: '/tmp/worktree-a',
branch: 'main',
displayName: 'Workspace',
linkedIssue: null,
comment: '',
parentWorktreeId: null,
childWorktreeIds: [],
lineage: null,
git: {
path: '/tmp/worktree-a',
head: '',
branch: 'main',
isBare: false,
isMainWorktree: false
}
}
const snapshot: ResolvedWorktreeSnapshot = {
worktrees: [resolved],
platformByRepoId: new Map()
}
describe('workspace query prototype dispatch', () => {
it('re-enters the runtime repo selector', async () => {
const seen: unknown[][] = []
class Probe extends OrcaRuntimeService {
protected override selectReposBySelector(selector: string): Repo[] {
seen.push([this, selector])
return [repo]
}
run() {
return this.resolveRepoSelector('id:workspace-query')
}
}
const runtime = new Probe(store)
await expect(runtime.run()).resolves.toBe(repo)
expect(seen).toEqual([[runtime, 'id:workspace-query']])
})
it('re-enters the runtime snapshot from async listing', async () => {
const seen: OrcaRuntimeService[] = []
class Probe extends OrcaRuntimeService {
protected override async listResolvedWorktreeSnapshot(): Promise<ResolvedWorktreeSnapshot> {
seen.push(this)
return snapshot
}
run() {
return this.listResolvedWorktrees()
}
}
const runtime = new Probe(store)
await expect(runtime.run()).resolves.toEqual([resolved])
expect(seen).toEqual([runtime])
})
it('calls the runtime scoped resolver with the original single argument', async () => {
const seen: { receiver: OrcaRuntimeService; args: unknown[] }[] = []
class Probe extends OrcaRuntimeService {
protected override hasFreshResolvedWorktreeCache(): boolean {
return false
}
protected override async resolveExplicitWorktreeIdScoped(
_worktreeId: string,
_hostId?: ExecutionHostId
): Promise<ResolvedWorktree> {
seen.push({ receiver: this, args: Array.from(arguments) })
return resolved
}
run() {
return this.resolveGitWorktreeSelector(`id:${TEST_WORKTREE_ID}`)
}
}
const runtime = new Probe(store)
await expect(runtime.run()).resolves.toBe(resolved)
expect(seen).toEqual([{ receiver: runtime, args: [TEST_WORKTREE_ID] }])
})
it('keeps disconnected SSH stored rows with a failed scan verdict', async () => {
class Probe extends OrcaRuntimeService {
run(remote: Repo) {
return this.listRepoWorktreesForResolutionUncached(remote, undefined)
}
}
const remote: Repo = { ...repo, executionHostId: 'ssh:workspace-query' }
const id = `${repo.id}::/remote/stored-worktree`
const meta = {
...store.getWorktreeMeta(TEST_WORKTREE_ID),
hostId: remote.executionHostId,
customName: 'Stored',
comment: '',
issueNumber: null,
issueUrl: null,
issueTitle: null,
mergeStatus: null
}
const runtime = new Probe({
...store,
getRepos: () => [remote],
getAllWorktreeMeta: () => ({ [id]: meta }),
getWorktreeMeta: () => meta
})
const result = await runtime.run(remote)
expect(result.ok).toBe(false)
expect(result.worktrees.map((row) => row.path)).toEqual(['/remote/stored-worktree'])
})
})
describe('workspace-detected-listing-capture characterization', () => {
describe('detected workspace listing during repo resolution', () => {
it('captures the listing method before awaiting resolution and keeps its runtime receiver', async () => {
const pending = Promise.withResolvers<Repo>()
const events: string[] = []
const repo: Repo = {
id: 'repo',
path: '/repo',
displayName: 'repo',
badgeColor: 'blue',
addedAt: 0
}
const result: DetectedWorktreeListResult = {
repoId: repo.id,
authoritative: true,
source: 'git',
worktrees: []
}
const runtime = new (class extends OrcaRuntimeService {
override resolveRepoSelectorForConnection(selector: string, connectionId?: string | null) {
events.push('resolve')
expect([selector, connectionId]).toEqual(['repo', 'ssh:box'])
return pending.promise
}
})()
const original = vi.fn(function (
this: OrcaRuntimeService,
resolved: Repo,
sourceDefaultsSupported?: boolean
) {
events.push('invoke original')
expect(this).toBe(runtime)
expect([resolved, sourceDefaultsSupported]).toEqual([repo, false])
return Promise.resolve(result)
})
const replacement = vi.fn(() => Promise.resolve({ ...result, repoId: 'replacement' }))
let listing: (
repo: Repo,
sourceDefaultsSupported?: boolean
) => Promise<DetectedWorktreeListResult> = original
Object.defineProperty(runtime, 'listDetectedWorktreesForResolvedRepo', {
get: () => {
events.push('capture listing')
return listing
}
})
const listingPromise = runtime.listDetectedManagedWorktrees('repo', 'ssh:box', false)
listing = replacement
pending.resolve(repo)
await expect(listingPromise).resolves.toBe(result)
expect(events).toEqual(['capture listing', 'resolve', 'invoke original'])
expect(original).toHaveBeenCalledOnce()
expect(replacement).not.toHaveBeenCalled()
})
})
describe('workspace methods captured before request property reads', () => {
it('captures port probes before reading repoId', async () => {
const runtime = new OrcaRuntimeService()
const events: string[] = []
const original = vi.fn(async () => [])
const replacement = vi.fn(async () => [])
let probes = original
Object.defineProperty(runtime, 'getWorkspacePortProbes', {
get: () => {
events.push('capture probes')
return probes
}
})
await runtime.killWorkspacePort({
pid: 4242,
port: 3020,
get repoId() {
events.push('read repoId')
probes = replacement
return 'repo'
}
})
expect(events).toEqual(['capture probes', 'read repoId'])
expect(original).toHaveBeenCalledWith('repo')
expect(original.mock.contexts[0]).toBe(runtime)
expect(replacement).not.toHaveBeenCalled()
})
it('captures file routing before reading the requested workspace', async () => {
const runtime = new OrcaRuntimeService()
const events: string[] = []
const original = vi.fn(async () => ({ executionHostId: 'ssh:box' }))
const replacement = vi.fn(async () => ({ executionHostId: 'ssh:box' }))
let routing = original
Object.defineProperty(runtime, 'resolveRuntimeFileTarget', {
get: () => {
events.push('capture routing')
return routing
}
})
await runtime.killWorkspacePortOnHost({
executionHostId: 'ssh:box',
pid: 4242,
port: 3020,
get worktree() {
events.push('read worktree')
routing = replacement
return 'folder:remote'
}
})
expect(events).toEqual(['capture routing', 'read worktree'])
expect(original).toHaveBeenCalledWith('folder:remote')
expect(original.mock.contexts[0]).toBe(runtime)
expect(replacement).not.toHaveBeenCalled()
})
it('captures scan invalidation before reading the resolved repo id', async () => {
const runtime = new OrcaRuntimeService()
const events: string[] = []
const original = vi.fn()
const replacement = vi.fn()
let invalidate = original
const repo: Repo = {
get id() {
events.push('read repo id')
invalidate = replacement
return 'repo'
},
path: '/repo',
displayName: 'repo',
badgeColor: 'blue',
addedAt: 0
}
Object.defineProperty(runtime, 'invalidateWorktreeScanCacheForRepo', {
get: () => {
events.push('capture invalidation')
return invalidate
}
})
Object.defineProperty(runtime, 'resolveRepoSelectorForConnection', {
value: async () => repo
})
const detected: DetectedWorktreeListResult = {
repoId: 'repo',
authoritative: false,
source: 'metadata-fallback',
worktrees: []
}
Object.defineProperty(runtime, 'listDetectedWorktreesForResolvedRepo', {
value: async () => detected
})
await expect(runtime.teardownMissingManagedWorktreeTerminals('repo', [])).resolves.toEqual({
stoppedWorktreeIds: []
})
expect(events).toEqual(['capture invalidation', 'read repo id'])
expect(original).toHaveBeenCalledWith('repo')
expect(original.mock.contexts[0]).toBe(runtime)
expect(replacement).not.toHaveBeenCalled()
})
})
})
describe('workspace-file-target-capture characterization', () => {
describe('floating workspace file target resolution', () => {
it('captures the converter before awaiting the path and keeps its runtime receiver', async () => {
const runtime = new OrcaRuntimeService()
const pending = Promise.withResolvers<string>()
const events: string[] = []
const convert = runtime['floatingWorkspaceToResolvedWorktree']
const original = vi.fn(function (this: OrcaRuntimeService, path: string) {
events.push('invoke original')
expect(this).toBe(runtime)
return convert.call(this, path)
})
const replacement = vi.fn((path: string) => convert.call(runtime, path))
let converter: typeof convert = original
Object.defineProperty(runtime, 'floatingWorkspaceToResolvedWorktree', {
get: () => {
events.push('capture converter')
return converter
}
})
Object.defineProperty(runtime, 'resolveFloatingWorkspacePath', {
value: () => {
events.push('resolve path')
return pending.promise
}
})
const result = runtime['resolveRuntimeFileTarget'](FLOATING_TERMINAL_WORKTREE_ID)
converter = replacement
const path = join(tmpdir(), 'floating')
pending.resolve(path)
await expect(result).resolves.toMatchObject({
worktree: { path },
executionHostId: 'local'
})
expect(events).toEqual(['capture converter', 'resolve path', 'invoke original'])
expect(original).toHaveBeenCalledWith(path)
expect(replacement).not.toHaveBeenCalled()
})
})
})
describe('workspace-session-read-order characterization', () => {
const WORKTREE_ID = 'repo::/workspace'
describe('workspace session callable lookup order', () => {
it('keeps the native refusal for a launcher without a workspace selector', async () => {
const runtime = new OrcaRuntimeService(null)
await expect(
runtime.resolveOrchestrationAgentLauncherForTarget('missing-launcher', {})
).rejects.toBeInstanceOf(TypeError)
})
it('captures the browser reader before evaluating the snapshot workspace getter', () => {
const runtime = new OrcaRuntimeService(null)
const order: string[] = []
const replacement = vi.fn(() => new Map<string, BrowserTabInfo>())
const original = function (
this: InstanceType<typeof OrcaRuntimeService>,
worktreeId: string
) {
expect(this).toBe(runtime)
expect(worktreeId).toBe(WORKTREE_ID)
order.push('original call')
return new Map<string, BrowserTabInfo>()
}
Object.defineProperty(runtime, 'getListedBrowserTabsByPageId', {
configurable: true,
get: () => {
order.push('reader lookup')
return original
}
})
const snapshot: RuntimeMobileSessionTabsSnapshot = {
worktree: WORKTREE_ID,
publicationEpoch: 'seed',
snapshotVersion: 1,
tabs: [],
activeGroupId: null,
activeTabId: null,
activeTabType: null
}
Object.defineProperty(snapshot, 'worktree', {
get: () => {
order.push('argument lookup')
Object.defineProperty(runtime, 'getListedBrowserTabsByPageId', { value: replacement })
return WORKTREE_ID
}
})
expect(runtime['collectPublicMobileSessionTabIds'](snapshot)).toEqual(new Set())
expect(order).toEqual(['reader lookup', 'argument lookup', 'original call'])
expect(replacement).not.toHaveBeenCalled()
})
it('captures the title writer before constructing a session with getter-backed fields', () => {
const runtime = new OrcaRuntimeService(null)
const order: string[] = []
const replacement = vi.fn()
const original = function (
this: InstanceType<typeof OrcaRuntimeService>,
worktreeId: string,
session: WorkspaceSessionState
) {
expect(this).toBe(runtime)
expect(worktreeId).toBe(WORKTREE_ID)
expect(session.tabsByWorktree[WORKTREE_ID]?.[0]?.customTitle).toBe('renamed')
order.push('original write')
}
const session = getDefaultWorkspaceSession()
const tabs: WorkspaceSessionState['tabsByWorktree'] = {
[WORKTREE_ID]: [
{
id: 'tab',
ptyId: null,
worktreeId: WORKTREE_ID,
title: 'Terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
]
}
let tabReads = 0
Object.defineProperty(session, 'tabsByWorktree', {
enumerable: true,
get: () => {
tabReads += 1
if (tabReads === 2) {
order.push('session spread')
Object.defineProperty(runtime, 'setWorkspaceSessionForWorktree', { value: replacement })
}
return tabs
}
})
Object.defineProperty(runtime, 'store', { value: { setWorkspaceSession: () => {} } })
Object.defineProperty(runtime, 'getWorkspaceSessionForWorktree', { value: () => session })
Object.defineProperty(runtime, 'setWorkspaceSessionForWorktree', {
configurable: true,
get: () => {
order.push('writer lookup')
return original
}
})
runtime['persistHeadlessTerminalTitle'](WORKTREE_ID, 'tab', 'renamed')
expect(order).toEqual(['writer lookup', 'session spread', 'original write'])
expect(replacement).not.toHaveBeenCalled()
})
})
})
describe('first-work rename lazy Store capabilities', () => {
type RenameStore = Parameters<typeof firstWorkRenameDeps>[0]
type RenameDeps = ReturnType<typeof firstWorkRenameDeps>
function renameStore(): RenameStore {
return {
getSettings: () => ({
workspaceDir: '/workspace',
nestWorkspaces: false,
refreshLocalBaseRefOnWorktreeCreate: false,
branchPrefix: 'none',
branchPrefixCustom: ''
}),
getRepo: () => undefined,
getRepos: () => [],
getWorktreeMeta: () => undefined,
setWorktreeMeta: vi.fn()
}
}
const missingCalls: {
name: string
invoke: (deps: RenameDeps) => unknown
provideFolder?: true
}[] = [
{ name: 'display name', invoke: (deps) => deps.getCurrentDisplayName('folder:sample') },
{ name: 'folder path', invoke: (deps) => deps.getFolderWorkspacePath?.('folder:sample') },
{
name: 'pending rename',
invoke: (deps) => deps.isPendingFirstAgentMessageRename?.('folder:sample')
},
{ name: 'display write', invoke: (deps) => deps.setDisplayName('folder:sample', 'New') },
{ name: 'error read', invoke: (deps) => deps.setRenameError('folder:sample', 'Failure') },
{
name: 'error write',
provideFolder: true,
invoke: (deps) => deps.setRenameError('folder:sample', 'Failure')
},
{ name: 'tab owner', invoke: (deps) => deps.resolveWorktreeIdForTab('tab') }
]
it.each(missingCalls)(
'preserves the direct missing $name capability failure',
({ invoke, provideFolder }) => {
const store = renameStore()
if (provideFolder) {
store.getFolderWorkspace = () => undefined
}
const runtime = new OrcaRuntimeService(null)
let notified = false
runtime.notifyFolderWorkspaceChanged = () => {
notified = true
}
const deps = firstWorkRenameDeps(store, runtime)
expect(() => invoke(deps)).toThrow(TypeError)
expect(notified).toBe(false)
expect(deps.renameWorktreeFolder).toBeUndefined()
}
)
it('looks up the current writer on each call and retains Store and Root receivers', () => {
const store = renameStore()
const runtime = new OrcaRuntimeService(null)
const order: string[] = []
runtime.notifyFolderWorkspaceChanged = function () {
expect(this).toBe(runtime)
order.push('notify')
}
const deps = firstWorkRenameDeps(store, runtime)
const write: NonNullable<RenameStore['updateFolderWorkspace']> = function (
this: RenameStore,
id,
updates
) {
expect(this).toBe(store)
expect(arguments).toHaveLength(2)
expect(id).toBe('sample')
expect(updates).toEqual({
name: 'New',
pendingFirstAgentMessageRename: false,
firstAgentMessageRenameError: null
})
order.push('write')
return null
}
Object.defineProperty(store, 'updateFolderWorkspace', {
configurable: true,
get() {
order.push('lookup')
return write
}
})
deps.setDisplayName('folder:sample', 'New')
expect(order).toEqual(['lookup', 'write', 'notify'])
delete store.updateFolderWorkspace
expect(() => deps.setDisplayName('folder:sample', 'Again')).toThrow(TypeError)
expect(order).toEqual(['lookup', 'write', 'notify'])
})
})
@@ -93,24 +93,4 @@ describe('worktree.list host coverage under the row cap', () => {
expect(remote).toHaveLength(24)
expect(result.hostScope).toEqual({ hostIds: ['local', 'ssh:box-1'], omittedHostIds: [] })
})
it('names a configured host that contributed no rows at all', async () => {
// Failed repo scans and empty hosts both contribute no rows; scope must expose the uncertainty.
const result = await queries(fleet(3, 0), ['local', 'ssh:box-1', 'runtime:paired']).list(
undefined,
200
)
expect(result.hostScope).toEqual({
hostIds: ['local'],
omittedHostIds: ['runtime:paired', 'ssh:box-1']
})
})
it('does not report configured hosts as omitted from a --repo listing', async () => {
// Caller-scoped listings must omit unrelated hosts.
const result = await queries(fleet(0, 5)).list('id:repo-ssh', 200)
expect(result.hostScope).toEqual({ hostIds: ['ssh:box-1'], omittedHostIds: [] })
})
})
@@ -31,12 +31,9 @@ vi.mock('./repo-worktree-admin-fingerprint', () => ({
}))
import { OrcaRuntimeService } from '../../orca-runtime'
import {
WORKTREE_SCAN_ADMIN_FINGERPRINT_TIMEOUT_MS,
WORKTREE_SCAN_ADMIN_RECONCILE_INTERVAL_MS
} from './worktree-scan-admin-budget'
import { RESOLVED_WORKTREE_REPO_TIMEOUT_MS } from './repo-worktree-row-resolution'
import { canonicalWorktreeIdentity } from '../../../../shared/worktree/identity'
import { WORKTREE_SCAN_ADMIN_FINGERPRINT_TIMEOUT_MS } from './worktree-scan-admin-budget'
import './repo-worktree-row-resolution'
import '../../../../shared/worktree/identity'
import type { HostWorktreeTarget } from '../../../../shared/worktree/host-qualified-identity'
const REPO_ID = 'repo-local'
@@ -45,8 +42,6 @@ const WORKTREE_PATH = '/Users/me/dev/app-feature'
const WORKTREE_ID = `${REPO_ID}::${WORKTREE_PATH}`
const MAIN_WORKTREE_ID = `${REPO_ID}::${REPO_PATH}`
const SCAN_TTL_MS = 30_000
// Slow healthy stat work must permit reuse rather than trigger repeating Git scans.
const SLOW_BUT_HEALTHY_PROBE_MS = 3_000
function makeMeta(overrides: Record<string, unknown> = {}) {
return {
@@ -182,87 +177,6 @@ describe('worktree scan admin-fingerprint gate', () => {
readRepoWorktreeAdminFingerprintMock.mockResolvedValue('fp-1')
})
it('skips the Git scan past the TTL while the admin fingerprint is unchanged', async () => {
vi.useFakeTimers()
try {
const { list } = makeRuntime()
await list()
expect(scanCount()).toBe(1)
vi.advanceTimersByTime(SCAN_TTL_MS + 1_000)
await list()
expect(scanCount()).toBe(1)
vi.advanceTimersByTime(1_000)
await list()
expect(scanCount()).toBe(1)
expect(readRepoWorktreeAdminFingerprintMock).toHaveBeenCalledTimes(2)
} finally {
vi.useRealTimers()
}
})
it('rescans at the TTL once the admin fingerprint changes', async () => {
vi.useFakeTimers()
try {
const { list } = makeRuntime()
await list()
expect(scanCount()).toBe(1)
readRepoWorktreeAdminFingerprintMock.mockResolvedValue('fp-2')
vi.advanceTimersByTime(SCAN_TTL_MS + 1_000)
await list()
expect(scanCount()).toBe(2)
} finally {
vi.useRealTimers()
}
})
it('reconciles with a real scan once the bounded interval elapses', async () => {
vi.useFakeTimers()
try {
const { list } = makeRuntime()
await list()
expect(scanCount()).toBe(1)
// Keep polling past the TTL with an unchanged fingerprint; only the reconcile deadline rescans.
for (
let elapsed = 31_000;
elapsed < WORKTREE_SCAN_ADMIN_RECONCILE_INTERVAL_MS;
elapsed += 31_000
) {
vi.advanceTimersByTime(31_000)
await list()
}
expect(scanCount()).toBe(1)
vi.advanceTimersByTime(WORKTREE_SCAN_ADMIN_RECONCILE_INTERVAL_MS)
await list()
expect(scanCount()).toBe(2)
} finally {
vi.useRealTimers()
}
})
it('still rescans immediately when an event invalidates the repo', async () => {
vi.useFakeTimers()
try {
const { runtime, list } = makeRuntime()
await list()
expect(scanCount()).toBe(1)
runtime.notifyBranchRenamed(REPO_ID)
await list()
expect(scanCount()).toBe(2)
} finally {
vi.useRealTimers()
}
})
it('resolves a just-created id after invalidation even within both cache TTLs', async () => {
const { runtime, list } = makeRuntime()
listWorktreesStrictMock.mockResolvedValueOnce([
@@ -279,96 +193,6 @@ describe('worktree scan admin-fingerprint gate', () => {
expect(scanCount()).toBe(2)
})
it('scans when the probe cannot describe the repo', async () => {
vi.useFakeTimers()
try {
readRepoWorktreeAdminFingerprintMock.mockResolvedValue(null)
const { list } = makeRuntime()
await list()
vi.advanceTimersByTime(SCAN_TTL_MS + 1_000)
await list()
expect(scanCount()).toBe(2)
} finally {
vi.useRealTimers()
}
})
it('never probes a repo whose scan TTL already reaches the reconcile interval', async () => {
vi.useFakeTimers()
try {
const { list } = makeRuntime({ repoPath: '/tmp/.codex-tmp/capsule-a' })
await list()
vi.advanceTimersByTime(WORKTREE_SCAN_ADMIN_RECONCILE_INTERVAL_MS + 1_000)
await list()
expect(scanCount()).toBe(2)
expect(readRepoWorktreeAdminFingerprintMock).not.toHaveBeenCalled()
} finally {
vi.useRealTimers()
}
})
it('never consults the probe for SSH repos', async () => {
vi.useFakeTimers()
try {
const { list } = makeRuntime({ connectionId: 'ssh-remote-1' })
await list()
vi.advanceTimersByTime(SCAN_TTL_MS + 1_000)
await list()
expect(listWorktreesStrictMock).not.toHaveBeenCalled()
expect(readRepoWorktreeAdminFingerprintMock).not.toHaveBeenCalled()
} finally {
vi.useRealTimers()
}
})
it('does not extend a failed scan on an unchanged fingerprint', async () => {
vi.useFakeTimers()
try {
listWorktreesStrictMock.mockRejectedValue(new Error('git unavailable'))
const { list } = makeRuntime()
await list()
expect(scanCount()).toBe(1)
vi.advanceTimersByTime(SCAN_TTL_MS + 1_000)
await list()
expect(scanCount()).toBe(2)
} finally {
vi.useRealTimers()
}
})
it('bounds Git fan-out under a sustained 1 Hz polling workload', async () => {
// Ten idle repos polled faster than snapshot TTL formerly spawned git worktree list every 30s.
vi.useFakeTimers()
try {
const REPOS = 10
const POLL_SECONDS = 30 * 60
const { list } = makeRuntime({ repoCount: REPOS })
for (let second = 0; second < POLL_SECONDS; second += 1) {
await list()
vi.advanceTimersByTime(1_000)
}
// Reconciliation cuts this half-hour workload from 600 scans to 60.
const ttlOnlyScans = (REPOS * POLL_SECONDS * 1_000) / SCAN_TTL_MS
const reconcileScans =
(REPOS * POLL_SECONDS * 1_000) / WORKTREE_SCAN_ADMIN_RECONCILE_INTERVAL_MS
expect(ttlOnlyScans).toBe(600)
expect(reconcileScans).toBe(60)
expect(scanCount()).toBe(reconcileScans)
} finally {
vi.useRealTimers()
}
})
it('keeps refreshing when a probe on a wedged filesystem never settles', async () => {
vi.useFakeTimers()
try {
@@ -400,137 +224,6 @@ describe('worktree scan admin-fingerprint gate', () => {
}
})
it('issues no further probes while one is still outstanding', async () => {
vi.useFakeTimers()
try {
readRepoWorktreeAdminFingerprintMock.mockReturnValue(new Promise<string | null>(() => {}))
const { list } = makeRuntime()
await list()
expect(readRepoWorktreeAdminFingerprintMock).toHaveBeenCalledTimes(1)
const startedAt = Date.now()
for (let refresh = 0; refresh < 20; refresh += 1) {
vi.advanceTimersByTime(SCAN_TTL_MS + 1_000)
await list()
}
// Each abandoned probe can pin a libuv filesystem worker indefinitely.
expect(readRepoWorktreeAdminFingerprintMock).toHaveBeenCalledTimes(1)
// Only the reconcile ceiling rescans while the probe stays pending, never every refresh.
const elapsed = Date.now() - startedAt
expect(scanCount()).toBeGreaterThan(1)
expect(scanCount()).toBeLessThanOrEqual(
1 + Math.ceil(elapsed / WORKTREE_SCAN_ADMIN_RECONCILE_INTERVAL_MS)
)
} finally {
vi.useRealTimers()
}
})
it('resumes gating once the filesystem recovers', async () => {
vi.useFakeTimers()
try {
const releaseStalledProbe = stallProbeOnce()
const { list } = makeRuntime()
await list()
vi.advanceTimersByTime(SCAN_TTL_MS + 1_000)
await list()
expect(scanCount()).toBe(1)
// The mount unwedges; its stale answer must not gate anything, but the repo must probe again.
releaseStalledProbe('fp-stale')
await drainMicrotasks()
vi.advanceTimersByTime(SCAN_TTL_MS + 1_000)
await list()
expect(scanCount()).toBe(2)
vi.advanceTimersByTime(SCAN_TTL_MS + 1_000)
await list()
expect(scanCount()).toBe(2)
} finally {
vi.useRealTimers()
}
})
it('reuses the scanned rows when the awaited probe outlives its deadline', async () => {
vi.useFakeTimers()
try {
const { list } = makeRuntime()
await list()
expect(scanCount()).toBe(1)
stallProbeOnce()
vi.advanceTimersByTime(SCAN_TTL_MS + 1_000)
const second = list()
const secondSettled = trackSettled(second)
await drainMicrotasks()
expect(secondSettled()).toBe(false)
expect(scanCount()).toBe(1)
await vi.advanceTimersByTimeAsync(WORKTREE_SCAN_ADMIN_FINGERPRINT_TIMEOUT_MS)
await drainMicrotasks()
// #19312: a timeout is "unknown", never a mismatch, so it must not start a full scan.
expect(scanCount()).toBe(1)
expect(heads(await second)).toEqual(['abc', 'def'])
} finally {
vi.useRealTimers()
}
})
it('still reconciles on schedule while every probe times out', async () => {
vi.useFakeTimers()
try {
readRepoWorktreeAdminFingerprintMock.mockImplementation(() => new Promise(() => {}))
const { list } = makeRuntime()
const scannedAt = Date.now()
await list()
expect(scanCount()).toBe(1)
while (Date.now() - scannedAt < WORKTREE_SCAN_ADMIN_RECONCILE_INTERVAL_MS - SCAN_TTL_MS) {
vi.advanceTimersByTime(SCAN_TTL_MS + 1_000)
await list()
}
expect(scanCount()).toBe(1)
vi.advanceTimersByTime(2 * SCAN_TTL_MS)
await list()
expect(scanCount()).toBe(2)
} finally {
vi.useRealTimers()
}
})
it('bounds the awaited probe by the caller per-repo budget', () => {
// The wait runs inside that budget, so outlasting it can only strand the caller on persisted rows.
expect(WORKTREE_SCAN_ADMIN_FINGERPRINT_TIMEOUT_MS).toBeLessThan(
RESOLVED_WORKTREE_REPO_TIMEOUT_MS
)
})
it('still reuses the scan when a slow probe answers inside its deadline', async () => {
vi.useFakeTimers()
try {
const { list } = makeRuntime()
await list()
expect(scanCount()).toBe(1)
const releaseSlowProbe = stallProbeOnce()
vi.advanceTimersByTime(SCAN_TTL_MS + 1_000)
const second = list()
// Use an absolute healthy latency so shortening the deadline cannot make a self-referential test pass.
await vi.advanceTimersByTimeAsync(SLOW_BUT_HEALTHY_PROBE_MS)
releaseSlowProbe('fp-1')
await second
expect(scanCount()).toBe(1)
} finally {
vi.useRealTimers()
}
})
it('still returns scanned rows within the per-repo budget when the probe stalls', async () => {
vi.useFakeTimers()
try {
@@ -552,36 +245,6 @@ describe('worktree scan admin-fingerprint gate', () => {
vi.useRealTimers()
}
})
it('does not publish an already-expired snapshot after a slow compute', async () => {
vi.useFakeTimers()
try {
const { list, store } = makeRuntime()
await list()
stallProbeOnce()
vi.advanceTimersByTime(SCAN_TTL_MS + 1_000)
const second = list()
await vi.advanceTimersByTimeAsync(WORKTREE_SCAN_ADMIN_FINGERPRINT_TIMEOUT_MS)
await second
// Stamp completion time: a probe slower than snapshot TTL otherwise creates an already-expired entry.
const getRepos = vi.spyOn(store, 'getRepos')
await list()
expect(getRepos).not.toHaveBeenCalled()
} finally {
vi.useRealTimers()
}
})
it('shares one probe and one scan across concurrent callers', async () => {
const { list } = makeRuntime()
await Promise.all([list(), list(), list()])
expect(scanCount()).toBe(1)
expect(readRepoWorktreeAdminFingerprintMock).toHaveBeenCalledTimes(1)
})
})
describe('scoped explicit worktree-id resolution', () => {
@@ -605,43 +268,6 @@ describe('scoped explicit worktree-id resolution', () => {
return listWorktreesStrictMock.mock.calls.map((call) => call[0] as string)
}
it('scans only the owning repo for an id: selector on a cold cache', async () => {
const runtime = new OrcaRuntimeService(makeStore({ repoCount: 10 }) as never)
const resolve = (selector: string): Promise<{ id: string }> =>
runtime['resolveGitWorktreeSelector'](selector)
const resolved = await resolve(`id:${MAIN_WORKTREE_ID}`)
expect(resolved.id).toBe(MAIN_WORKTREE_ID)
expect(scannedRepoPaths()).toEqual([REPO_PATH])
})
it('resolves an exact canonical identity without relying on the mutable locator', async () => {
const runtime = new OrcaRuntimeService(makeStore({ repoCount: 10 }) as never)
const resolve = (selector: string): Promise<{ id: string }> =>
runtime['resolveGitWorktreeSelector'](selector)
const identityKey = canonicalWorktreeIdentity({
worktreeId: MAIN_WORKTREE_ID,
executionHostId: 'local',
instanceId: '22222222-2222-4222-8222-222222222222'
})
const resolved = await resolve(`identity:${identityKey}`)
expect(resolved.id).toBe(MAIN_WORKTREE_ID)
})
it('still finds worktrees in other repos through the fleet path', async () => {
const runtime = new OrcaRuntimeService(makeStore({ repoCount: 10 }) as never)
const resolve = (selector: string): Promise<{ id: string }> =>
runtime['resolveGitWorktreeSelector'](selector)
const otherId = `${REPO_ID}-3::${REPO_PATH}-3`
const resolved = await resolve(`id:${otherId}`)
expect(resolved.id).toBe(otherId)
expect(scannedRepoPaths()).toEqual([`${REPO_PATH}-3`])
})
it('keeps cross-repo selectors on the fleet path so ambiguity still throws', async () => {
const runtime = new OrcaRuntimeService(makeStore({ repoCount: 10 }) as never)
const resolve = (selector: string): Promise<unknown> =>
@@ -650,13 +276,4 @@ describe('scoped explicit worktree-id resolution', () => {
await expect(resolve('branch:main')).rejects.toThrow('selector_ambiguous')
expect(new Set(scannedRepoPaths()).size).toBe(10)
})
it('falls back to the fleet path when the id names no registered repo', async () => {
const runtime = new OrcaRuntimeService(makeStore({ repoCount: 10 }) as never)
const resolve = (selector: string): Promise<unknown> =>
runtime['resolveGitWorktreeSelector'](selector)
await expect(resolve('id:missing-repo::/nowhere')).rejects.toThrow('selector_not_found')
expect(new Set(scannedRepoPaths()).size).toBe(10)
})
})
@@ -45,14 +45,6 @@ const MAIN_ROW = {
isBare: false,
isMainWorktree: true
}
// Linux paths are case-sensitive: a different spelling is a different checkout.
const OTHER_SPELLING_ROW = {
path: '/home/me/FileLoc',
head: 'def',
branch: 'refs/heads/feat',
isBare: false,
isMainWorktree: false
}
function makeRuntime(options: { connectionId?: string } = {}): OrcaRuntimeService {
const metaById: Record<string, WorktreeMeta> = {}
@@ -103,33 +95,6 @@ describe('worktree scan with a repeated path', () => {
localScanMock.mockReset()
})
describe('rows that agree, as reported', () => {
beforeEach(() => {
gitLists([MAIN_ROW, { ...MAIN_ROW, isMainWorktree: false }, OTHER_SPELLING_ROW])
})
it('resolves the id the CLI sends for `active` and `current` after a listing', async () => {
const runtime = makeRuntime()
await runtime.listManagedWorktrees()
await expect(runtime.showManagedWorktree(`id:${MAIN_WORKTREE_ID}`)).resolves.toMatchObject({
id: MAIN_WORKTREE_ID,
isMainWorktree: true
})
})
it('lists each path once for the CLI and for paired clients', async () => {
const runtime = makeRuntime()
const expectedIds = [MAIN_WORKTREE_ID, `${REPO_ID}::/home/me/FileLoc`]
const listed = await runtime.listManagedWorktrees()
expect(listed.worktrees.map((worktree) => worktree.id)).toEqual(expectedIds)
const detected = await runtime.listDetectedManagedWorktrees(`id:${REPO_ID}`)
expect(detected.worktrees.map((worktree) => worktree.id)).toEqual(expectedIds)
})
})
it('keeps the main checkout over a stale registration on another branch', async () => {
gitLists([
MAIN_ROW,
@@ -156,18 +156,4 @@ describe('runtime creation registry', () => {
).rejects.toThrow('could not create')
expect(creationsOf(runtime)).toEqual({})
})
it('lists a folder workspace create too', async () => {
const runtime = makeRuntime({ id: 'folder', path: '/notes', kind: 'folder' })
let during: Record<string, unknown> = {}
mocks.createFolder.mockReset().mockImplementation(async () => {
during = creationsOf(runtime)
return { worktree: { id: 'folder::/notes' } }
})
await runtime.createManagedWorktree({ repoSelector: 'folder', name: 'notes' })
expect(Object.values(during)).toEqual([{ repoId: 'folder' }])
expect(creationsOf(runtime)).toEqual({})
})
})
@@ -47,11 +47,7 @@ vi.mock('../../../ipc/worktrees/removal/registered-worktree-roots-scoped-invalid
import { OrcaRuntimeService } from '../../orca-runtime'
import type { WorktreeCreateTimingRecorder } from '../../../workspaces/creation/worktree-create-timing'
import type { PreparationRearmHolder } from '../../../workspaces/creation/preparation/worktree-create-preparation'
import {
_resetWorktreeCreateConcurrencyForTests,
beginPreparationWork,
type PreparationWork
} from '../../../workspaces/creation/worktree-create-concurrency'
import { _resetWorktreeCreateConcurrencyForTests } from '../../../workspaces/creation/worktree-create-concurrency'
import { partialRuntimeStore } from '../../orca-runtime-tests/runtime-store-partial-fixture'
const gitRepo = {
@@ -163,24 +159,6 @@ describe('runtime create events', () => {
expect(events('workspace_created')).toHaveLength(0)
})
it('counts preparation work running during the create but not the re-arm the create starts', async () => {
let competing: PreparationWork | undefined
mocks.createLocal.mockImplementation(async ({ rearm }: LocalCreateArgs) => {
competing = beginPreparationWork()
rearm.fire = () => {
beginPreparationWork()
beginPreparationWork()
}
return localCreateResult()
})
await makeRuntime().createManagedWorktree({ repoSelector: 'repo-1', name: 'app' })
competing?.end()
await vi.waitFor(() => expect(events('workspace_created')).toHaveLength(1))
expect(events('workspace_created')[0]).toMatchObject({ concurrent_preparations: 1 })
})
it('still returns the create when sending its event throws', async () => {
vi.spyOn(console, 'warn').mockImplementation(() => {})
mocks.track.mockImplementation(() => {
@@ -205,36 +183,4 @@ describe('runtime create events', () => {
).rejects.toThrow('could not create work tree')
expect(mocks.track).toHaveBeenCalledOnce()
})
it('refuses an SSH create and still names its host on the failed event', async () => {
const runtime = makeRuntime({
id: 'repo-remote',
path: '/srv/app',
kind: 'git',
executionHostId: 'ssh:remote-1'
})
await expect(
runtime.createManagedWorktree({ repoSelector: 'repo-remote', name: 'app' })
).rejects.toThrow('Remote connection dropped')
await vi.waitFor(() => expect(events('workspace_create_failed')).toHaveLength(1))
expect(events('workspace_create_failed')[0]).toMatchObject({
create_entry_point: 'runtime',
execution_host: 'ssh'
})
expect(mocks.probe).not.toHaveBeenCalled()
})
it('sends nothing when the request is rejected before any create starts', async () => {
await expect(
makeRuntime().createManagedWorktree({
repoSelector: 'repo-1',
name: 'app',
startupAgent: 'codex'
})
).rejects.toThrow('Selected agent is disabled')
expect(mocks.createLocal).not.toHaveBeenCalled()
expect(mocks.track).not.toHaveBeenCalled()
})
})
@@ -1,98 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as WorktreeCreatePreparation from '../../../workspaces/creation/preparation/worktree-create-preparation'
import type { Project } from '../../../../shared/project-types'
import type { Repo } from '../../../../shared/repo-types'
import {
_resetWslCachesForTests,
_setWslCachesForTests
} from '../../../wsl/discovery/distro-catalog'
const mocks = vi.hoisted(() => ({
prefetchWorktreeCreateBase: vi.fn(),
prepareWorktreeCreateForRepo: vi.fn()
}))
vi.mock('../../../workspaces/creation/worktree-create-base-prefetch', () => ({
prefetchWorktreeCreateBase: mocks.prefetchWorktreeCreateBase
}))
vi.mock(
'../../../workspaces/creation/preparation/worktree-create-preparation',
async (importOriginal) => ({
...(await importOriginal<typeof WorktreeCreatePreparation>()),
prepareWorktreeCreateForRepo: mocks.prepareWorktreeCreateForRepo
})
)
import { OrcaRuntimeService } from '../../orca-runtime'
const repo: Repo = {
id: 'repo-1',
displayName: 'Repo',
path: String.raw`C:\workspace\repo`,
badgeColor: '#000000',
addedAt: 0
}
const project: Project = {
id: 'project-1',
displayName: 'Project',
badgeColor: '#000000',
sourceRepoIds: ['repo-1'],
createdAt: 0,
updatedAt: 0,
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }
}
function makeStore(overrides: Partial<Project> = {}): unknown {
return {
getRepos: () => [repo],
getRepo: (id: string) => (id === repo.id ? repo : undefined),
getProjects: () => [{ ...project, ...overrides }],
getSettings: () => ({ localWindowsRuntimeDefault: { kind: 'windows-host' } })
}
}
function setPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', { configurable: true, value: platform })
}
const hostPlatform = process.platform
beforeEach(() => {
mocks.prefetchWorktreeCreateBase.mockReset().mockResolvedValue(undefined)
mocks.prepareWorktreeCreateForRepo.mockReset().mockResolvedValue(undefined)
})
afterEach(() => {
setPlatform(hostPlatform)
_resetWslCachesForTests()
})
// RPC prefetch must match IPC project routing; construct through the barrel for later selector subclasses.
describe('prefetchManagedWorktreeCreateBase (orca-runtime-get-worktree-terminal-provisioning-host)', () => {
it('warms up in the distro a WSL-routed project runs in', async () => {
_setWslCachesForTests({ available: true, distros: ['Ubuntu'] })
setPlatform('win32')
const runtime = new OrcaRuntimeService(makeStore() as never)
await runtime.prefetchManagedWorktreeCreateBase({ repoSelector: 'repo-1' })
expect(mocks.prefetchWorktreeCreateBase).toHaveBeenCalledWith(
expect.objectContaining({ gitOptions: { wslDistro: 'Ubuntu' } })
)
})
it('does not surface a repair-required project runtime as a prefetch failure', async () => {
_setWslCachesForTests({ available: true, distros: ['Debian'] })
setPlatform('win32')
const runtime = new OrcaRuntimeService(makeStore() as never)
await expect(
runtime.prefetchManagedWorktreeCreateBase({ repoSelector: 'repo-1' })
).resolves.toBeUndefined()
expect(mocks.prefetchWorktreeCreateBase).toHaveBeenCalledWith(
expect.objectContaining({ gitOptions: {} })
)
})
})
@@ -13,9 +13,7 @@ import { listWorktrees } from '../../../git/worktrees/catalog/worktree-scan-cach
import { shouldRunSetupForCreate } from '../../../workspace-hooks/effective-hook-config'
import { store } from '../../orca-runtime-tests/orca-runtime-test-fixtures.spec'
import { createMobileCreateTestNotifier } from '../../orca-runtime-tests/orca-runtime-test-scenario-builders.spec'
import { tmpdir } from 'node:os'
import type { Worktree } from '../../../../shared/worktree/types'
import type { RuntimePtyController } from '../../runtime-pty-controller-contract'
import 'node:os'
describe('host worktree creation while its renderer is unavailable', () => {
it.each([
@@ -96,104 +94,3 @@ describe('host worktree creation while its renderer is unavailable', () => {
}
)
})
describe('startup hosts retain current runtime capabilities', () => {
it('refuses base prefetch without a runtime store', async () => {
const runtime = new OrcaRuntimeService(null)
await expect(
runtime.prefetchManagedWorktreeCreateBase({ repoSelector: 'id:missing' })
).rejects.toThrow('runtime_unavailable')
})
it('reads the current create override with the captured worktree and three arguments', async () => {
const runtime = new OrcaRuntimeService(store)
onTestFinished(() => {
vi.restoreAllMocks()
})
const created: Worktree = {
id: 'created-worktree',
repoId: 'repo-1',
displayName: 'Created',
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
path: tmpdir(),
head: 'abc',
branch: 'created',
isBare: false,
isMainWorktree: false
}
const host = runtime['getWorktreeTerminalProvisioningHost'](created)
const options = { command: 'echo startup' }
const create = vi.spyOn(runtime, 'createTerminal').mockImplementation(async function (
this: OrcaRuntimeService,
selector,
input,
evidence
) {
expect(this).toBe(runtime)
expect(arguments.length).toBe(3)
expect([selector, input, evidence]).toEqual(['id:created-worktree', options, created])
expect(evidence).toBe(created)
return { handle: 'created-handle', worktreeId: created.id, title: null }
})
await host.createTerminal('id:created-worktree', options)
expect(create).toHaveBeenCalledOnce()
})
it('reads replacement providers and preserves readiness failures after removal', async () => {
const runtime = new OrcaRuntimeService(store)
const host = runtime['getWorktreeStartupReadinessHost']()
const hasChildProcesses = host.hasChildProcesses
if (!hasChildProcesses) {
throw new Error('missing readiness child-process callback')
}
const provisioning = runtime['getWorktreeTerminalProvisioningHost']()
expect(provisioning.canSpawn()).toBe(false)
const controller: RuntimePtyController = {
write: () => true,
kill: () => true,
getForegroundProcess: vi.fn(async function (this: RuntimePtyController, ptyId: string) {
expect(this).toBe(controller)
expect(arguments.length).toBe(1)
expect(ptyId).toBe('startup-pty')
return 'late provider'
})
}
runtime.setPtyController(controller)
await expect(host.getForegroundProcess('startup-pty')).resolves.toBe('late provider')
await expect(hasChildProcesses('startup-pty')).resolves.toBe(false)
const replacement: RuntimePtyController = {
write: () => true,
kill: () => true,
spawn: async () => ({ id: 'spawned' }),
getForegroundProcess: async () => 'replacement provider',
hasChildProcesses: async () => true
}
runtime.setPtyController(replacement)
expect(provisioning.canSpawn()).toBe(true)
await expect(host.getForegroundProcess('startup-pty')).resolves.toBe('replacement provider')
await expect(hasChildProcesses('startup-pty')).resolves.toBe(true)
runtime.setPtyController(null)
expect(provisioning.canSpawn()).toBe(false)
expect(() => host.getForegroundProcess('startup-pty')).toThrow(TypeError)
expect(() => hasChildProcesses('startup-pty')).toThrow(TypeError)
})
it('writes the canonical setup-token map when invoked', () => {
const runtime = new OrcaRuntimeService(store)
const host = runtime['getWorktreeTerminalProvisioningHost']()
const original = runtime['setupCompletionTokenByPtyId']
const replacement = new Map<string, string>()
Object.defineProperty(runtime, 'setupCompletionTokenByPtyId', { value: replacement })
host.recordSetupCompletionToken?.('startup-pty', 'late-token')
expect(replacement.get('startup-pty')).toBe('late-token')
expect(original.has('startup-pty')).toBe(false)
})
})
@@ -1,10 +1,8 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { resolve } from 'node:path'
import 'node:path'
const {
runtimeCreateMocks: mocks,
worktreePath,
pickedBranch,
createWorktree,
resetRuntimeCreateMocks
} = await vi.hoisted(async () => await import('./runtime-local-worktree-create-test-harness'))
@@ -94,142 +92,6 @@ describe('runtime create PR conflict probe', () => {
mocks.branchName.mockImplementation(async (_path, override, name: string) => override ?? name)
})
it.each([1, 10])(
'keeps the selected name and source through %i occupied paths, without PR eligibility',
async (occupied) => {
const options = { wslDistro: 'Ubuntu' }
mocks.routing.mockReturnValue(options)
mocks.remoteBase.mockResolvedValue(remoteTrackingBase)
mocks.githubPr.mockResolvedValue({ number: 42 })
let pathChecks = 0
mocks.pathExists.mockImplementation(async () => pathChecks++ < occupied)
await createWorktree({ ...pickedBranch, sparseCheckout: { directories: ['src'] } })
expect(mocks.remoteBase.mock.calls).toEqual([['/repo', 'origin/main', options]])
expect(mocks.branchName).toHaveBeenCalledOnce()
expect(mocks.branchConflict.mock.calls).toEqual([
['/repo', 'main', remoteTrackingBase.ref, options]
])
expect(mocks.pathExists).toHaveBeenCalledTimes(occupied + 1)
expect(mocks.githubPr).not.toHaveBeenCalled()
expect(mocks.consume).not.toHaveBeenCalled()
expect(mocks.addSparse).toHaveBeenCalledExactlyOnceWith(
'/repo',
resolve('/worktrees', `app-${occupied + 1}`),
'main',
['src'],
remoteTrackingBase.base,
false,
expect.objectContaining({ remoteTrackingBase, trackRemoteBranch: true, ...options })
)
}
)
it.each([true, false])(
'keeps local eligibility with picker intent (reusable: %s)',
async (reusable) => {
mocks.branchConflict.mockImplementation(async (_path, branch: string) =>
branch === 'main' ? 'local' : null
)
mocks.canCheckout.mockImplementation(
async (_path, branch: string) => reusable && branch === 'main'
)
mocks.pathExists.mockImplementation(async (path) => path === worktreePath)
mocks.consume.mockResolvedValue({ status: 'miss', reason: 'not_prepared' })
if (!reusable) {
await expect(createWorktree(pickedBranch)).rejects.toThrow('was not found locally')
expect(mocks.add).not.toHaveBeenCalled()
expect(mocks.consume).not.toHaveBeenCalled()
return
}
await createWorktree(pickedBranch)
expect(mocks.add).toHaveBeenCalledWith(
'/repo',
resolve('/worktrees', 'app-2'),
'main',
'origin/main',
false,
false,
expect.objectContaining({ checkoutExistingBranch: true })
)
expect(mocks.githubPr).not.toHaveBeenCalled()
}
)
it('keeps unflagged legacy, mobile New branch and Advanced collisions on baseline allocation', async () => {
mocks.consume.mockResolvedValue({ status: 'miss', reason: 'not_prepared' })
mocks.branchName.mockImplementation(async (_path, override, name: string) => override ?? name)
mocks.branchConflict.mockImplementation(async (_path, branch: string) =>
branch === 'feature/test' ? 'remote' : null
)
await createWorktree({ branchNameOverride: 'feature/test', baseBranch: 'origin/main' })
expect(mocks.add.mock.calls[0][2]).toBe('feature/test-2')
})
it('runs the first name PR lookup while the base fetch is still in flight', async () => {
mocks.remoteBase.mockResolvedValue(remoteTrackingBase)
let finishFetch!: () => void
mocks.refresh.mockReturnValue(
new Promise((resolve) => {
finishFetch = () => resolve({ ok: true })
})
)
let fetchPendingAtLookup = false
mocks.githubPr.mockImplementation(async () => {
fetchPendingAtLookup = mocks.refresh.mock.calls.length === 1
finishFetch?.()
return null
})
await createWorktree()
expect(fetchPendingAtLookup).toBe(true)
expect(mocks.githubPr).toHaveBeenCalledWith('/repo', 'app', {})
expect(mocks.consume).toHaveBeenCalledWith(
expect.objectContaining({
branch: 'app',
options: expect.objectContaining({ remoteTrackingBase })
})
)
})
it('suffixes past a PR on the first name', async () => {
mocks.githubPr.mockResolvedValueOnce({ number: 7, state: 'closed' }).mockResolvedValue(null)
await createWorktree()
expect(mocks.githubPr.mock.calls.map(([, branch]) => branch)).toEqual(['app', 'app-2'])
expect(mocks.consume).toHaveBeenCalledWith(expect.objectContaining({ branch: 'app-2' }))
})
it.each([undefined, 'Ubuntu'])(
'probes the next name for a PR once the first name collides, routed for %s',
async (wslDistro) => {
const routing = wslDistro ? { wslDistro } : {}
mocks.routing.mockReturnValue(routing)
mocks.branchConflict.mockResolvedValueOnce('local').mockResolvedValue(null)
await createWorktree()
expect(mocks.githubPr).toHaveBeenCalledOnce()
expect(mocks.githubPr).toHaveBeenCalledWith('/repo', 'app-2', routing)
}
)
it('skips a later name that already has a PR', async () => {
mocks.branchConflict.mockResolvedValueOnce('local').mockResolvedValue(null)
mocks.githubPr.mockResolvedValueOnce({ number: 7, state: 'closed' }).mockResolvedValue(null)
await createWorktree()
expect(mocks.githubPr.mock.calls.map(([, branch]) => branch)).toEqual(['app-2', 'app-3'])
expect(mocks.consume).toHaveBeenCalledWith(expect.objectContaining({ branch: 'app-3' }))
})
it('reports the naming failure when the started base fetch also fails', async () => {
mocks.remoteBase.mockResolvedValue(remoteTrackingBase)
mocks.refresh.mockRejectedValue(new Error('offline'))
@@ -247,28 +109,6 @@ describe('runtime create PR conflict probe', () => {
}
})
it('runs the first name PR lookup while the origin fetch is still in flight', async () => {
mocks.hasBase.mockResolvedValue(false)
let finishFetch!: () => void
mocks.fetch.mockReturnValue(
new Promise<void>((resolve) => {
finishFetch = resolve
})
)
let fetchPendingAtLookup = false
mocks.githubPr.mockImplementation(async () => {
fetchPendingAtLookup = mocks.fetch.mock.calls.length === 1
finishFetch?.()
return null
})
await createWorktree()
expect(fetchPendingAtLookup).toBe(true)
expect(mocks.fetch).toHaveBeenCalledWith('/repo', 'origin', {})
expect(mocks.consume).toHaveBeenCalledWith(expect.objectContaining({ branch: 'app' }))
})
it('reports the naming failure when the started origin fetch also fails', async () => {
mocks.hasBase.mockResolvedValue(false)
mocks.fetch.mockRejectedValue(new Error('offline'))
@@ -1,427 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { resolve } from 'node:path'
import type { Store } from '../../../persistence/loading-store/store'
import type { WorktreeMeta } from '../../../../shared/worktree/meta-types'
import type { HostWorktreeTarget } from '../../../../shared/worktree/host-qualified-identity'
import type { Repo } from '../../../../shared/repo-types'
import type { RuntimeManagedWorktreeCreateArgs } from './runtime-managed-worktree-create-types'
import { resolveGitAdmissionTier } from '../../../git/command-runner/git-operation-executor'
const mocks = vi.hoisted(() => ({
git: vi.fn(),
rearm: vi.fn(),
routing: vi.fn<() => { wslDistro?: string }>(),
defaultBase: vi.fn(),
hasBase: vi.fn(),
branchName: vi.fn(),
canCheckout: vi.fn(),
pathExists: vi.fn(),
branchConflict: vi.fn(),
githubPr: vi.fn(),
consume: vi.fn(),
add: vi.fn(),
addSparse: vi.fn(),
pushTarget: vi.fn(),
listing: vi.fn(),
remoteBase: vi.fn(),
hasRemoteRef: vi.fn(),
refresh: vi.fn(),
fetch: vi.fn(),
resolveShared: vi.fn<() => Promise<string[]>>(),
resolveInclude: vi.fn<() => Promise<string[]>>(),
copyPaths: vi.fn<() => Promise<string[]>>(),
effectiveHooks: vi.fn(),
created: {
path: '',
head: 'abc123',
branch: 'app',
isBare: false,
isMainWorktree: false
}
}))
vi.mock('../../../text-generation/commit-message-text-generation', () => ({
resolveTextGenerationParams: () => ({
ok: true,
params: { agentId: 'claude', model: 'fixture' }
}),
generateBranchNameFromContext: async () => ({ success: true, slug: 'fix-auth' })
}))
vi.mock('../../../text-generation/commit-message-agent-environment', () => ({
prepareLocalCommitMessageAgentEnv: async () => ({ ok: true })
}))
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
gitExecFileAsync: mocks.git
}))
vi.mock('../../../workspaces/execution-host/project-runtime-git-options', () => ({
getLocalProjectGitExecOptions: () => ({ cwd: '/repo', ...mocks.routing() }),
getLocalProjectWorktreeGitOptions: mocks.routing,
getWorktreeMirrorDistro: () => undefined
}))
vi.mock('../../../git/repo-default-base-ref', () => ({
getBaseRefDefault: mocks.defaultBase,
resolveDefaultBaseRefWithLocalGit: mocks.defaultBase
}))
vi.mock('../../../git/repo-branch-conflict', () => ({
getBranchConflictKind: mocks.branchConflict
}))
vi.mock('../../../git/git-username', () => ({ resolveLocalGitUsername: async () => '' }))
vi.mock('../../../workspace-hooks/hooks', () => ({ getEffectiveHooks: mocks.effectiveHooks }))
vi.mock('../../../git/worktrees/creation/worktree-base-ref-probe', () => ({
hasLocalWorktreeBaseRef: mocks.hasBase
}))
vi.mock('./runtime-worktree-create-git', () => ({
resolveCreateBranchName: mocks.branchName,
canCheckoutExistingLocalBranch: mocks.canCheckout,
getLocalGitHubPrForBranch: mocks.githubPr,
getSelectedHostedReviewForBranch: vi.fn()
}))
vi.mock('../catalog/runtime-worktree-filesystem', () => ({ runtimePathExists: mocks.pathExists }))
vi.mock('../../../workspaces/creation/preparation/worktree-create-preparation', () => ({
consumePreparedWorktreeCreate: mocks.consume
}))
vi.mock('../../../git/worktrees/creation/worktree-add', () => ({
addWorktree: mocks.add
}))
vi.mock('../../../git/worktrees/creation/worktree-sparse-add', () => ({
addSparseWorktree: mocks.addSparse
}))
vi.mock('../../../ipc/worktree-remote', () => ({
configureCreatedWorktreePushTarget: mocks.pushTarget
}))
vi.mock('../../../ipc/created-worktree-reconciliation', () => ({
resolveCreatedWorktree: mocks.listing
}))
vi.mock('../../../workspaces/removal/retirement/worktree-name-retirement', () => ({
failedWorktreeCreationNeedsRetirement: vi.fn(),
retireGeneratedWorktreeName: vi.fn()
}))
vi.mock('../../../git/worktrees/content-paths/worktree-shared-directories', () => ({
resolveWorktreeSharedDirectories: mocks.resolveShared
}))
vi.mock('../../../git/worktrees/content-paths/worktree-include-file', () => ({
resolveWorktreeIncludePaths: mocks.resolveInclude
}))
vi.mock('../../../ipc/worktree-symlinks', () => ({
createWorktreeCopiedPaths: mocks.copyPaths,
createWorktreeLinkedPaths: vi.fn(),
createWorktreeSharedPaths: vi.fn()
}))
import { createRuntimeLocalManagedWorktree } from './runtime-local-worktree-create'
import type { PreparationRearmHolder } from '../../../workspaces/creation/preparation/worktree-create-preparation'
import {
createWorktreeCreateTimingRecorder,
type WorktreeCreateTimingRecorder
} from '../../../workspaces/creation/worktree-create-timing'
import type { SparsePreset } from '../../../../shared/worktree/create-types'
const worktreePath = resolve('/worktrees', 'app')
const pickedBranch = {
baseBranch: 'origin/main',
branchNameOverride: 'main',
branchIntent: 'open-existing-v2'
} as const
const writtenMeta: Partial<WorktreeMeta>[] = []
const WEB_PRESET: SparsePreset = {
id: 'preset-web',
repoId: 'repo-1',
name: 'web',
directories: ['apps/web', 'packages/ui'],
createdAt: 0,
updatedAt: 0
}
const readSparsePresets = vi.fn<(repoId: string) => SparsePreset[]>()
function createWorktree(
request: Partial<RuntimeManagedWorktreeCreateArgs> = {},
rearm: PreparationRearmHolder = { fire: () => {} },
timing: WorktreeCreateTimingRecorder = createWorktreeCreateTimingRecorder(),
repoOverrides: Partial<Repo> = {},
autoRenameBranchFromWork = false
) {
const store = {
getSettings: () => ({
workspaceDir: '/worktrees',
nestWorkspaces: false,
refreshLocalBaseRefOnWorktreeCreate: false,
branchPrefix: '',
autoRenameBranchFromWork
}),
setWorktreeMeta: (_target: HostWorktreeTarget, updates: Partial<WorktreeMeta>) => {
writtenMeta.push(updates)
return updates
},
getSparsePresets: (repoId: string): SparsePreset[] => readSparsePresets(repoId)
}
return createRuntimeLocalManagedWorktree({
request: { repoSelector: 'repo-1', name: 'app', baseBranch: 'main', ...request },
repo: {
id: 'repo-1',
path: '/repo',
displayName: 'Repo',
badgeColor: '#000000',
addedAt: 0,
...repoOverrides
},
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: All store methods reached by this isolated create path are supplied above.
store: store as Store,
createdWithAgent: undefined,
resolveRemoteTrackingBase: mocks.remoteBase,
hasRemoteTrackingRef: mocks.hasRemoteRef,
// vi.fn marks returned promises handled; a fresh promise exposes unawaited fetch rejection.
refreshRemoteTrackingBase: async (...refreshArgs) => await mocks.refresh(...refreshArgs),
fetchRemote: async (...fetchArgs) => await mocks.fetch(...fetchArgs),
onWorktreeMetadataPersisted: () => undefined,
rearm,
timing
})
}
beforeEach(() => {
vi.resetAllMocks()
writtenMeta.length = 0
mocks.created.branch = 'app'
mocks.git.mockResolvedValue({ stdout: '', stderr: '' })
mocks.created.path = worktreePath
mocks.routing.mockReturnValue({})
mocks.defaultBase.mockImplementation(async () => {
expect(resolveGitAdmissionTier()).toBe('interactive')
return 'main'
})
mocks.hasBase.mockResolvedValue(true)
mocks.branchName.mockResolvedValue('app')
mocks.canCheckout.mockResolvedValue(false)
mocks.pathExists.mockResolvedValue(false)
mocks.branchConflict.mockResolvedValue(null)
mocks.githubPr.mockResolvedValue(null)
mocks.consume.mockResolvedValue({ status: 'hit', result: {}, rearm: mocks.rearm })
mocks.add.mockResolvedValue({})
mocks.addSparse.mockResolvedValue({})
mocks.listing.mockImplementation(async () => {
expect(resolveGitAdmissionTier()).toBe('interactive')
return { created: mocks.created }
})
mocks.remoteBase.mockImplementation(async () => {
expect(resolveGitAdmissionTier()).toBe('interactive')
return null
})
mocks.hasRemoteRef.mockResolvedValue(true)
mocks.refresh.mockResolvedValue({ ok: true })
mocks.fetch.mockResolvedValue(undefined)
mocks.resolveShared.mockResolvedValue([])
mocks.resolveInclude.mockResolvedValue(['.env'])
mocks.copyPaths.mockResolvedValue([])
mocks.effectiveHooks.mockReturnValue(null)
readSparsePresets.mockReturnValue([WEB_PRESET])
})
describe('runtime create PR conflict probe', () => {
const remoteTrackingBase = {
remote: 'origin',
branch: 'main',
base: 'origin/main',
ref: 'refs/remotes/origin/main'
}
beforeEach(() => {
mocks.branchName.mockImplementation(async (_path, override, name: string) => override ?? name)
})
it.each([1, 10])(
'keeps the selected name and source through %i occupied paths, without PR eligibility',
async (occupied) => {
const options = { wslDistro: 'Ubuntu' }
mocks.routing.mockReturnValue(options)
mocks.remoteBase.mockResolvedValue(remoteTrackingBase)
mocks.githubPr.mockResolvedValue({ number: 42 })
let pathChecks = 0
mocks.pathExists.mockImplementation(async () => pathChecks++ < occupied)
await createWorktree({ ...pickedBranch, sparseCheckout: { directories: ['src'] } })
expect(mocks.remoteBase.mock.calls).toEqual([['/repo', 'origin/main', options]])
expect(mocks.branchName).toHaveBeenCalledOnce()
expect(mocks.branchConflict.mock.calls).toEqual([
['/repo', 'main', remoteTrackingBase.ref, options]
])
expect(mocks.pathExists).toHaveBeenCalledTimes(occupied + 1)
expect(mocks.githubPr).not.toHaveBeenCalled()
expect(mocks.consume).not.toHaveBeenCalled()
expect(mocks.addSparse).toHaveBeenCalledExactlyOnceWith(
'/repo',
resolve('/worktrees', `app-${occupied + 1}`),
'main',
['src'],
remoteTrackingBase.base,
false,
expect.objectContaining({ remoteTrackingBase, trackRemoteBranch: true, ...options })
)
}
)
it.each([true, false])(
'keeps local eligibility with picker intent (reusable: %s)',
async (reusable) => {
mocks.branchConflict.mockImplementation(async (_path, branch: string) =>
branch === 'main' ? 'local' : null
)
mocks.canCheckout.mockImplementation(
async (_path, branch: string) => reusable && branch === 'main'
)
mocks.pathExists.mockImplementation(async (path) => path === worktreePath)
mocks.consume.mockResolvedValue({ status: 'miss', reason: 'not_prepared' })
if (!reusable) {
await expect(createWorktree(pickedBranch)).rejects.toThrow('was not found locally')
expect(mocks.add).not.toHaveBeenCalled()
expect(mocks.consume).not.toHaveBeenCalled()
return
}
await createWorktree(pickedBranch)
expect(mocks.add).toHaveBeenCalledWith(
'/repo',
resolve('/worktrees', 'app-2'),
'main',
'origin/main',
false,
false,
expect.objectContaining({ checkoutExistingBranch: true })
)
expect(mocks.githubPr).not.toHaveBeenCalled()
}
)
it('keeps unflagged legacy, mobile New branch and Advanced collisions on baseline allocation', async () => {
mocks.consume.mockResolvedValue({ status: 'miss', reason: 'not_prepared' })
mocks.branchName.mockImplementation(async (_path, override, name: string) => override ?? name)
mocks.branchConflict.mockImplementation(async (_path, branch: string) =>
branch === 'feature/test' ? 'remote' : null
)
await createWorktree({ branchNameOverride: 'feature/test', baseBranch: 'origin/main' })
expect(mocks.add.mock.calls[0][2]).toBe('feature/test-2')
})
it('runs the first name PR lookup while the base fetch is still in flight', async () => {
mocks.remoteBase.mockResolvedValue(remoteTrackingBase)
let finishFetch!: () => void
mocks.refresh.mockReturnValue(
new Promise((resolve) => {
finishFetch = () => resolve({ ok: true })
})
)
let fetchPendingAtLookup = false
mocks.githubPr.mockImplementation(async () => {
fetchPendingAtLookup = mocks.refresh.mock.calls.length === 1
finishFetch?.()
return null
})
await createWorktree()
expect(fetchPendingAtLookup).toBe(true)
expect(mocks.githubPr).toHaveBeenCalledWith('/repo', 'app', {})
expect(mocks.consume).toHaveBeenCalledWith(
expect.objectContaining({
branch: 'app',
options: expect.objectContaining({ remoteTrackingBase })
})
)
})
it('suffixes past a PR on the first name', async () => {
mocks.githubPr.mockResolvedValueOnce({ number: 7, state: 'closed' }).mockResolvedValue(null)
await createWorktree()
expect(mocks.githubPr.mock.calls.map(([, branch]) => branch)).toEqual(['app', 'app-2'])
expect(mocks.consume).toHaveBeenCalledWith(expect.objectContaining({ branch: 'app-2' }))
})
it.each([undefined, 'Ubuntu'])(
'probes the next name for a PR once the first name collides, routed for %s',
async (wslDistro) => {
const routing = wslDistro ? { wslDistro } : {}
mocks.routing.mockReturnValue(routing)
mocks.branchConflict.mockResolvedValueOnce('local').mockResolvedValue(null)
await createWorktree()
expect(mocks.githubPr).toHaveBeenCalledOnce()
expect(mocks.githubPr).toHaveBeenCalledWith('/repo', 'app-2', routing)
}
)
it('skips a later name that already has a PR', async () => {
mocks.branchConflict.mockResolvedValueOnce('local').mockResolvedValue(null)
mocks.githubPr.mockResolvedValueOnce({ number: 7, state: 'closed' }).mockResolvedValue(null)
await createWorktree()
expect(mocks.githubPr.mock.calls.map(([, branch]) => branch)).toEqual(['app-2', 'app-3'])
expect(mocks.consume).toHaveBeenCalledWith(expect.objectContaining({ branch: 'app-3' }))
})
it('reports the naming failure when the started base fetch also fails', async () => {
mocks.remoteBase.mockResolvedValue(remoteTrackingBase)
mocks.refresh.mockRejectedValue(new Error('offline'))
mocks.branchConflict.mockResolvedValue('local')
const unhandled = vi.fn()
process.on('unhandledRejection', unhandled)
try {
await expect(createWorktree()).rejects.toThrow('Branch "app-')
await new Promise((resolve) => setImmediate(resolve))
expect(mocks.refresh).toHaveBeenCalledOnce()
expect(unhandled).not.toHaveBeenCalled()
expect(mocks.consume).not.toHaveBeenCalled()
} finally {
process.off('unhandledRejection', unhandled)
}
})
it('runs the first name PR lookup while the origin fetch is still in flight', async () => {
mocks.hasBase.mockResolvedValue(false)
let finishFetch!: () => void
mocks.fetch.mockReturnValue(
new Promise<void>((resolve) => {
finishFetch = resolve
})
)
let fetchPendingAtLookup = false
mocks.githubPr.mockImplementation(async () => {
fetchPendingAtLookup = mocks.fetch.mock.calls.length === 1
finishFetch?.()
return null
})
await createWorktree()
expect(fetchPendingAtLookup).toBe(true)
expect(mocks.fetch).toHaveBeenCalledWith('/repo', 'origin', {})
expect(mocks.consume).toHaveBeenCalledWith(expect.objectContaining({ branch: 'app' }))
})
it('reports the naming failure when the started origin fetch also fails', async () => {
mocks.hasBase.mockResolvedValue(false)
mocks.fetch.mockRejectedValue(new Error('offline'))
mocks.branchConflict.mockResolvedValue('local')
const unhandled = vi.fn()
process.on('unhandledRejection', unhandled)
try {
await expect(createWorktree()).rejects.toThrow('Branch "app-')
await new Promise((resolve) => setImmediate(resolve))
expect(mocks.fetch).toHaveBeenCalledOnce()
expect(unhandled).not.toHaveBeenCalled()
expect(mocks.consume).not.toHaveBeenCalled()
} finally {
process.off('unhandledRejection', unhandled)
}
})
})
@@ -167,51 +167,6 @@ describe('runtime concurrent creates on one name', () => {
})
describe('runtime prepared-worktree replenishment', () => {
it('keeps the failed claim release available when the fallback also fails', async () => {
mocks.consume.mockResolvedValue({
status: 'miss',
reason: 'prepare_failed',
rearm: mocks.rearm
})
mocks.add.mockRejectedValue(new Error('normal add failed'))
const rearm: PreparationRearmHolder = { fire: () => {} }
await expect(createWorktree({}, rearm)).rejects.toThrow('normal add failed')
expect(mocks.rearm).not.toHaveBeenCalled()
rearm.fire()
expect(mocks.rearm).toHaveBeenCalledOnce()
})
it('leaves the re-arm holder armed but unfired once probes and include copies finish', async () => {
const rearm: PreparationRearmHolder = { fire: () => {} }
let finishProbe!: (paths: string[]) => void
mocks.resolveShared.mockImplementation(
() =>
new Promise<string[]>((resolve) => {
finishProbe = resolve
})
)
let finishCopy!: (paths: string[]) => void
mocks.copyPaths.mockImplementation(
() =>
new Promise<string[]>((resolve) => {
finishCopy = resolve
})
)
const creation = createWorktree({}, rearm)
await vi.waitFor(() => expect(mocks.resolveShared).toHaveBeenCalledOnce())
expect(mocks.rearm).not.toHaveBeenCalled()
finishProbe([])
await vi.waitFor(() => expect(mocks.copyPaths).toHaveBeenCalledOnce())
expect(mocks.rearm).not.toHaveBeenCalled()
finishCopy([])
await creation
// The caller launches terminals before arming, so create must not fire it itself.
expect(mocks.rearm).not.toHaveBeenCalled()
rearm.fire()
expect(mocks.rearm).toHaveBeenCalledOnce()
})
it('arms the holder even when materialization fails', async () => {
mocks.copyPaths.mockRejectedValue(new Error('copy failed'))
const rearm: PreparationRearmHolder = { fire: () => {} }
@@ -273,27 +228,6 @@ describe('runtime create base without a tracking ref', () => {
mocks.hasBase.mockImplementation(async (_repo: string, ref: string) => ref === 'main')
})
it('names the workspace only after the fetch has settled', async () => {
let refreshSettled = false
mocks.refresh.mockImplementation(async () => {
await Promise.resolve()
mocks.hasRemoteRef.mockResolvedValue(true)
refreshSettled = true
return { ok: true }
})
const settledAtNaming: boolean[] = []
mocks.canCheckout.mockImplementation(async () => {
settledAtNaming.push(refreshSettled)
return false
})
await createWorktree({ baseBranch: 'origin/main', branchNameOverride: 'app' })
expect(settledAtNaming).toEqual([true])
expect(mocks.canCheckout).toHaveBeenCalledWith('/repo', 'app', 'origin/main', {})
expect(mocks.branchConflict).toHaveBeenCalledWith('/repo', 'app', 'origin/main', {}, undefined)
})
it('creates from the local branch and reports it when the fetch fails', async () => {
mocks.refresh.mockResolvedValue({ ok: false, errorKind: 'git_error' })
@@ -309,22 +243,6 @@ describe('runtime create base without a tracking ref', () => {
expect(result.addResult).not.toHaveProperty('baseFallback')
expect(result.worktree.baseRef).toBe('main')
})
it('decides the base before branch reuse and conflict checks', async () => {
mocks.refresh.mockResolvedValue({ ok: false, errorKind: 'git_error' })
await createWorktree({
baseBranch: 'origin/main',
branchNameOverride: 'app',
allowLocalBaseFallback: true
})
expect(mocks.refresh.mock.invocationCallOrder[0]).toBeLessThan(
mocks.canCheckout.mock.invocationCallOrder[0]
)
expect(mocks.canCheckout).toHaveBeenCalledWith('/repo', 'app', 'main', {})
expect(mocks.branchConflict).toHaveBeenCalledWith('/repo', 'app', 'main', {}, undefined)
})
})
describe('runtime create setup decision', () => {
@@ -12,8 +12,6 @@ vi.mock('../../../workspace-hooks/worktree-runner-script', () => ({
createSetupRunnerScript: runner.createSetupRunnerScript,
resolveSetupRunnerShell: () => undefined
}))
const PRIMARY_YAML = 'scripts:\n setup: pnpm install\n'
const PRIMARY_TRUST_CONTENT = 'pnpm install'
const sha256 = (text: string): string => createHash('sha256').update(text).digest('hex')
@@ -50,14 +48,6 @@ afterEach(() => {
})
describe('created worktree setup trust', () => {
it('runs setup when the created branch matches the approved content', async () => {
writeFileSync(join(worktreePath, 'orca.yaml'), PRIMARY_YAML)
const result = await prepare(sha256(PRIMARY_TRUST_CONTENT))
expect(runner.createSetupRunnerScript).toHaveBeenCalledOnce()
expect(result.setup).toBeDefined()
expect(result.setupTrustMismatch).toBeUndefined()
})
it('skips setup and default tab commands and reports the branch content when it differs', async () => {
writeFileSync(
join(worktreePath, 'orca.yaml'),
@@ -86,13 +76,6 @@ describe('created worktree setup trust', () => {
expect(result.setupTrustMismatch).toBeUndefined()
})
it('skips setup a branch adds when the primary checkout had none', async () => {
writeFileSync(join(worktreePath, 'orca.yaml'), PRIMARY_YAML)
const result = await prepare(sha256(''))
expect(runner.createSetupRunnerScript).not.toHaveBeenCalled()
expect(result.setupTrustMismatch?.scriptContent).toBe(PRIMARY_TRUST_CONTENT)
})
it('runs the created branch setup as before when an older client sends no approval', async () => {
writeFileSync(join(worktreePath, 'orca.yaml'), 'scripts:\n setup: make bootstrap\n')
const result = await prepare()
@@ -75,13 +75,6 @@ describe('adoptRuntimeProvisionedRoot', () => {
expect(setWorktreeMeta).not.toHaveBeenCalled()
})
it('refuses a project whose root is not the recipe root', async () => {
const { value } = ports([checkout()])
await expect(
adoptRuntimeProvisionedRoot('repo-vm', { name: 'fix-login', expectedPath: '/other' }, value)
).rejects.toThrow('does not match')
})
it('refuses a project a WSL move is taking before reading its checkout', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'wsl-move-adopt-'))
try {
@@ -27,32 +27,6 @@ beforeEach(() => {
})
describe('explicit local branch reuse on the execution host', () => {
it('routes all probes through the WSL distro and does not compare or reset the remote tip', async () => {
git.mockResolvedValueOnce({ stdout: 'local-ahead\n' })
git.mockResolvedValueOnce({ stdout: 'refs/heads/feature/test\n' })
worktrees.mockResolvedValue([])
await expect(
canCheckoutExistingLocalBranch(
'/repo',
'feature/test',
'origin/feature/test',
{ wslDistro: 'Ubuntu' },
true
)
).resolves.toBe(true)
expect(git.mock.calls).toEqual([
[
['rev-parse', '--verify', '--quiet', 'refs/heads/feature/test^{commit}'],
{ cwd: '/repo', wslDistro: 'Ubuntu' }
],
[
['for-each-ref', '--format=%(refname)', 'refs/heads/feature/test'],
{ cwd: '/repo', wslDistro: 'Ubuntu' }
]
])
expect(worktrees).toHaveBeenCalledWith('/repo', { wslDistro: 'Ubuntu' })
})
it('rejects a filesystem alias before attempting checkout through WSL', async () => {
git.mockResolvedValueOnce({ stdout: 'local-ahead\n' })
git.mockResolvedValueOnce({ stdout: '' })
@@ -5,7 +5,7 @@ import { RpcDispatcher } from '../../rpc/dispatcher'
import type { RpcRequest } from '../../rpc/core'
import { REPO_METHODS } from '../../rpc/methods/projects/repo'
import { WORKTREE_METHODS } from '../../rpc/methods/workspaces/worktree'
import { agentLaunchWorkspaceFactory } from '../../rpc/methods/agent-launch/agent-launch-worktree-creation'
import '../../rpc/methods/agent-launch/agent-launch-worktree-creation'
import { OrcaRuntimeService } from '../../orca-runtime'
import { partialRuntimeStore } from '../../orca-runtime-tests/runtime-store-partial-fixture'
@@ -93,29 +93,4 @@ describe('a repo id set up on two hosts, named with repoHostId', () => {
expect(reached).toHaveBeenCalledTimes(1)
expect(reached.mock.calls[0]?.[1]).toMatchObject({ path: '/laptop/dup' })
})
it('agent.launch create-worktree keeps the host', async () => {
const reached = stopAtResolvedProject()
const runtime = createRuntime()
const factory = agentLaunchWorkspaceFactory({ runtime }, 'claude')
await expect(
factory.createWorktree({
create: { repo: 'id:dup', repoHostId: 'ssh:gpu', name: 'x' },
startupAgent: 'claude'
})
).rejects.toThrow('stop after resolution')
expect(reached.mock.calls[0]?.[1]).toMatchObject({ path: '/remote/dup' })
})
it('a retried create for one host never coalesces with the same retry for another', async () => {
const runtime = createRuntime()
const run = vi.fn(async () => 'created')
await runtime.dedupeWorktreeCreate({ repo: 'id:dup', repoHostId: 'local' }, 'retry-1', run)
await runtime.dedupeWorktreeCreate({ repo: 'id:dup', repoHostId: 'ssh:gpu' }, 'retry-1', run)
expect(run).toHaveBeenCalledTimes(2)
})
})
@@ -85,13 +85,6 @@ describe('WorkspaceLifecycleQueue', () => {
expect(queue.hints()).toEqual({})
})
it('runs a repeated command again rather than merging it into the first', async () => {
const queue = new WorkspaceLifecycleQueue()
let runs = 0
const sleep: LifecycleCommand<number> = { kind: 'sleep', run: async () => ++runs }
expect(await Promise.all([queue.submit(KEY, sleep), queue.submit(KEY, sleep)])).toEqual([1, 2])
})
it('keeps separate workspaces apart, including one id on two hosts', async () => {
const queue = new WorkspaceLifecycleQueue()
const log: string[] = []
@@ -193,30 +186,6 @@ describe('WorkspaceLifecycleQueue', () => {
expect(queue.hints()).toEqual({})
})
it('re-runs a repeated command once contact is back', async () => {
const { queue } = manualRetryQueue()
let reachable = false
let sleeps = 0
const sleep: LifecycleCommand<boolean> = {
kind: 'sleep',
run: async () => {
sleeps += 1
return reachable
},
outcome: (ok) => (ok ? 'settled' : 'unverifiable'),
observe: async () => {
if (!reachable) {
throw new Error('lost')
}
}
}
expect(await queue.submit(KEY, sleep)).toBe(false)
reachable = true
expect(await queue.submit(KEY, sleep)).toBe(true)
expect(sleeps).toBe(2)
expect(queue.pausedKeys().includes(KEY)).toBe(false)
})
it('pauses on lost contact mid-run, but not on a definite failure', async () => {
const queue = new WorkspaceLifecycleQueue()
const lost = (message: string): LifecycleCommand<never> => ({
@@ -333,62 +302,9 @@ describe('WorkspaceLifecycleQueue', () => {
expect(await other).toBe('woken')
expect(clock).toBe(500)
})
it('never cancels a command because its requester went away', async () => {
const queue = new WorkspaceLifecycleQueue()
const log: string[] = []
const sleep = gated(log, 'sleep', 'sleep', 'slept')
// A disconnected requester must not cancel the running command.
void queue.submit(KEY, sleep.command).catch(() => {})
await flush()
sleep.release()
await flush()
expect(log).toEqual(['start sleep', 'end sleep'])
})
it('leaves nothing pending in a fresh queue, as after a restart', () => {
const queue = new WorkspaceLifecycleQueue()
expect(queue.hints()).toEqual({})
expect(queue.creationsSnapshot().creations).toEqual({})
})
})
describe('WorkspaceLifecycleQueue creates', () => {
it('lists a creation once opened, names its workspace, and drops it on close', () => {
const queue = new WorkspaceLifecycleQueue()
const creation = queue.openCreate({ creationId: 'c1', repoId: 'repo-1' })
expect(queue.creationsSnapshot().creations).toEqual({ c1: { repoId: 'repo-1' } })
const opened = queue.creationsSnapshot().revision
creation.named('repo-1::/wt')
expect(queue.creationsSnapshot().creations).toEqual({
c1: { repoId: 'repo-1', worktreeId: 'repo-1::/wt' }
})
expect(queue.creationsSnapshot().revision).toBeGreaterThan(opened)
creation.close()
expect(queue.creationsSnapshot().creations).toEqual({})
})
it('mints an id for a caller that sends none', () => {
const queue = new WorkspaceLifecycleQueue()
const creation = queue.openCreate({ repoId: 'repo-1' })
const ids = Object.keys(queue.creationsSnapshot().creations)
expect(ids).toHaveLength(1)
expect(ids[0]).not.toBe('')
creation.close()
expect(queue.creationsSnapshot().creations).toEqual({})
})
it('leaves a concurrent retry under the same id listed when the first create closes', () => {
const queue = new WorkspaceLifecycleQueue()
const first = queue.openCreate({ creationId: 'c1', repoId: 'repo-1' })
const second = queue.openCreate({ creationId: 'c1', repoId: 'repo-1' })
first.named('repo-1::/stale')
first.close()
expect(queue.creationsSnapshot().creations).toEqual({ c1: { repoId: 'repo-1' } })
second.close()
expect(queue.creationsSnapshot().creations).toEqual({})
})
it('frees the workspace queue when a create closes after a retry took over its id', async () => {
const queue = new WorkspaceLifecycleQueue()
const first = queue.openCreate({ creationId: 'c1', repoId: 'repo-1', keyOf: () => KEY })
@@ -400,15 +316,6 @@ describe('WorkspaceLifecycleQueue creates', () => {
retry.close()
})
it('closes once', () => {
const queue = new WorkspaceLifecycleQueue()
const creation = queue.openCreate({ creationId: 'c1', repoId: 'repo-1' })
creation.close()
const closed = queue.creationsSnapshot().revision
creation.close()
expect(queue.creationsSnapshot().revision).toBe(closed)
})
it('binds to the workspace once named, so commands on the Creating row wait behind it', async () => {
const queue = new WorkspaceLifecycleQueue()
const log: string[] = []
@@ -494,22 +401,4 @@ describe('WorkspaceLifecycleQueue membership', () => {
// Publish membership and creation changes together in one revision.
expect(bumps).toHaveLength(1)
})
it('drops a failed create at once, with nothing to list', () => {
const queue = new WorkspaceLifecycleQueue()
const rows = listing()
queue.setMembership(rows.membership)
queue.openCreate({ creationId: 'c1', repoId: 'repo-1' }).close()
expect(queue.creationsSnapshot().creations).toEqual({})
})
it('shows a finished delete until a fresh listing drops the row', async () => {
const queue = new WorkspaceLifecycleQueue()
const rows = listing()
queue.setMembership(rows.membership)
expect(await queue.submit(KEY, { kind: 'delete', run: async () => 'deleted' })).toBe('deleted')
expect(queue.hints()).toEqual({ [KEY]: { kind: 'delete', queuedBehind: 0 } })
rows.applyFreshScan()
expect(queue.hints()).toEqual({})
})
})
@@ -1,7 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { ALL_TUI_AGENTS } from '../../../../shared/agent-launch/catalog/tui-agent-display-names'
import '../../../../shared/agent-launch/catalog/tui-agent-display-names'
import type { CliWorktreeBuilder } from '../../rpc/methods/agent-launch/agent-launch-cwd-cli.test-fixture'
import { toSshExecutionHostId } from '../../../../shared/execution-host'
import '../../../../shared/execution-host'
import { WorktreeCreate } from '../../../../shared/rpc-contract/worktree-create-params'
import { WorktreeCreate as PreObservationCreate } from '../../../../shared/rpc-contract/worktree-create-params.pre-observation.test-fixture'
import { AgentLaunch, AgentLaunchReplay } from '../../../../shared/rpc-contract/agent-launch-params'
@@ -10,7 +10,7 @@ import {
AgentLaunchReplay as PreObservationReplay
} from '../../../../shared/rpc-contract/agent-launch-params.pre-observation.test-fixture'
import { RuntimeWorktreeLineageController } from './runtime-worktree-lineage-controller'
import type { FolderWorkspace } from '../../../../shared/folder-workspaces/folder-workspace-types'
import type { ResolvedWorktree } from '../catalog/runtime-worktree-path-identity'
import {
formatAmbiguousCwdParentMessage,
@@ -104,80 +104,6 @@ function parseVersionedCreate(
afterEach(() => vi.restoreAllMocks())
describe('consumed cwd parent at the production authority boundary', () => {
it.each(['terminal', 'env', 'task', 'conflict', 'cross-host', 'exact', 'no-parent'] as const)(
'preserves %s authority/provenance and warnings when choices are unused',
async (kind) => {
const { controller, db, showTerminal, resolveWorktree } = fixture()
const input: WorktreeLineageInput = { ...unresolved }
if (kind === 'env') {
input.envParentWorkspace = 'id:outside-cwd'
}
if (kind === 'task') {
input.comment = 'created for task_creator'
vi.spyOn(db, 'getDispatchContext').mockReturnValue(undefined)
vi.spyOn(db, 'getTask').mockReturnValue(
taskLineageRow({ id: 'task_creator', created_by_terminal_handle: 'term_creator' })
)
showTerminal.mockImplementation(async (handle) => {
if (handle !== 'term_creator') {
throw new Error('terminal_not_found')
}
return { worktreeId: root.id }
})
} else if (kind !== 'terminal' && kind !== 'conflict') {
showTerminal.mockRejectedValue(new Error('terminal_not_found'))
}
if (kind === 'conflict') {
input.envParentWorkspace = choices[0]
}
if (kind === 'cross-host') {
resolveWorktree.mockImplementation(async (selector) => ({
...worktree(selector.slice(3)),
hostId: toSshExecutionHostId('other')
}))
}
if (kind === 'exact') {
input.parentWorktree = choices[1]
}
if (kind === 'no-parent') {
input.noParent = true
}
const withoutChoices = { ...input, cwdParentAmbiguous: undefined }
const before = await controller.resolveCreate(withoutChoices, 'local')
const after = await controller.resolveCreate(input, 'local')
expect(after).toEqual(before)
if (kind === 'conflict' || kind === 'cross-host') {
expect(after).toMatchObject({
kind: 'none',
warnings: [{ code: 'LINEAGE_PARENT_CONTEXT_CONFLICT' }]
})
} else if (kind === 'no-parent') {
expect(after).toEqual({ kind: 'none', warnings: [] })
} else {
const source =
kind === 'env'
? 'env-workspace'
: kind === 'task'
? 'orchestration-context'
: kind === 'exact'
? 'explicit-cli-flag'
: 'terminal-context'
expect(after).toMatchObject({
kind: 'lineage',
origin: kind === 'task' ? 'orchestration' : 'cli',
capture: { source, confidence: kind === 'exact' ? 'explicit' : 'inferred' },
parent: { worktree: { id: kind === 'env' ? 'outside-cwd' : root.id } }
})
if (kind === 'terminal') {
expect(after).toHaveProperty('createdByTerminalHandle', 'term_R')
}
if (kind === 'task') {
expect(after).toHaveProperty('taskId', 'task_creator')
}
}
}
)
it.each(['absent', 'stale-terminal', 'stale-env', 'stale-task', 'terminal-disappears'])(
'refuses a retained positive cwd tie (%s), preserving exact RPC recovery',
async (kind) => {
@@ -225,29 +151,6 @@ describe('consumed cwd parent at the production authority boundary', () => {
}
)
it('retains unique/legacy cwd and missing-parent behavior without inventing authority', async () => {
const { controller, showTerminal } = fixture()
showTerminal.mockRejectedValue(new Error('terminal_not_found'))
for (const cwdParentAmbiguous of [undefined]) {
expect(
await controller.resolveCreate({ ...unresolved, cwdParentAmbiguous }, 'local')
).toMatchObject({
kind: 'lineage',
origin: 'cli',
capture: { source: 'cwd-context', confidence: 'inferred' },
parent: { worktree: { id: siblingId } }
})
}
expect(
await controller.resolveCreate({ ...unresolved, cwdParentWorktree: 'id:stale' }, 'local')
).toMatchObject({
kind: 'none',
warnings: expect.arrayContaining([
expect.objectContaining({ code: 'LINEAGE_PARENT_CONTEXT_MISSING' })
])
})
})
it.each([false, true])(
'executes old/new client and host schema compatibility (newClient=%s)',
async (newClient) => {
@@ -286,222 +189,4 @@ describe('consumed cwd parent at the production authority boundary', () => {
}
}
)
it('retains complete historical transforms, refinements and actual launch embeddings', () => {
for (const newHost of [false, true]) {
for (const route of ['create', 'launch', 'replay'] as const) {
for (const value of [undefined, true, false, null, 'true', 1, [], {}]) {
const request = { repo: 'id:repo-a', cwdParentAmbiguous: value, futureField: 1 }
if (newHost && value !== undefined && value !== true) {
expect(() => parseVersionedCreate(newHost, route, request)).toThrow()
} else {
const parsed = parseVersionedCreate(newHost, route, request)
expect(parsed).not.toHaveProperty('futureField')
expect('cwdParentAmbiguous' in parsed ? parsed.cwdParentAmbiguous : undefined).toBe(
newHost ? value : undefined
)
}
}
for (const invalid of [
{ repo: 123 },
{ parentWorktree: 'id:p', noParent: true },
{ parentWorkspace: 'folder:p', noParent: true },
{ parentWorkspace: 'folder:p', parentWorktree: 'id:p' },
{ startupPrompt: 'hello' },
{ linkedWorkItem: { invalid: true }, linkedTaskSourceContext: { invalid: true } }
]) {
expect(() =>
parseVersionedCreate(newHost, route, { repo: 'id:repo-a', ...invalid })
).toThrow()
}
const linkedWorkItem = {
provider: 'github',
type: 'issue',
number: 1,
title: 'Issue',
url: 'https://github.com/example/repo/issues/1'
}
const linkedTaskSourceContext = {
provider: 'github',
projectId: 'project',
hostId: 'local'
}
expect(
parseVersionedCreate(newHost, route, {
repo: 'id:repo-a',
linkedWorkItem,
linkedTaskSourceContext
})
).toMatchObject({ linkedWorkItem, linkedTaskSourceContext })
expect(() =>
parseVersionedCreate(newHost, route, {
repo: 'id:repo-a',
linkedWorkItem,
linkedTaskSourceContext: { ...linkedTaskSourceContext, provider: 'gitlab' }
})
).toThrow('Linked work item and source context identities must match')
expect(
parseVersionedCreate(newHost, route, {
repo: 'id:repo-a',
setupDecision: 'unknown',
telemetrySource: 'invalid-telemetry-source',
createdWithAgent: 'unknown',
startupAgent: 'codex',
startupPrompt: 'hello'
})
).toMatchObject({
repo: 'id:repo-a',
setupDecision: undefined,
telemetrySource: undefined,
createdWithAgent: undefined,
startupAgent: 'codex',
startupPrompt: 'hello'
})
expect(
parseVersionedCreate(newHost, route, {
repo: 'id:repo-a',
telemetrySource: 'unknown'
})
).toMatchObject({ repo: 'id:repo-a', telemetrySource: 'unknown' })
}
}
for (const schema of [
PreObservationLaunch,
PreObservationReplay,
AgentLaunch,
AgentLaunchReplay
]) {
expect(
schema.safeParse({
agent: 'codex',
operationId: 'malformed',
target: { kind: 'existing', worktree: 'id:r' }
}).success
).toBe(false)
expect(() =>
schema.parse({
agent: 'codex',
operationId: `${Date.now()}-${'a'.repeat(32)}`,
sessionId: 'claude_session',
target: { kind: 'existing', worktree: 'id:r' }
})
).toThrow('Launch session id must be named for its agent')
}
expect(
PreObservationReplay.safeParse({
agent: 'codex',
target: { kind: 'existing', worktree: 'id:r' }
}).success
).toBe(false)
})
it.each([false, null, 'true', 1, [], {}])(
'rejects malformed positive literal %j before runtime reads',
(value) => {
const { resolveWorktree, showTerminal } = fixture()
expect(
WorktreeCreate.safeParse({ repo: 'id:repo-a', name: 'child', cwdParentAmbiguous: value })
.success
).toBe(false)
expect(resolveWorktree).not.toHaveBeenCalled()
expect(showTerminal).not.toHaveBeenCalled()
}
)
it('does not recheck terminal liveness after its existing validation read', async () => {
const { controller, showTerminal } = fixture()
showTerminal.mockImplementationOnce(async () => {
showTerminal.mockRejectedValue(new Error('terminal_not_found'))
return { worktreeId: root.id }
})
expect(await controller.resolveCreate(unresolved, 'local')).toMatchObject({
capture: { source: 'terminal-context' },
createdByTerminalHandle: 'term_R'
})
expect(showTerminal).toHaveBeenCalledTimes(1)
})
it('keeps the complete original refusal bounded for plain creation and every finite agent', () => {
const codex = [
'Orca did not create the workspace or start codex because more than one workspace matched the current directory when this request was prepared.',
'Run `orca worktree list --limit 10000 --json` with the same server and host selection. Match path and host; if truncated, repeat with a larger --limit using totalCount. Choose id:<id> (or identity:<identity.key> for a shared ID), then add --parent-worktree <selector> to your original invocation, or deliberately use --no-parent. Pass the selector as one argument quoted for your shell.',
'To choose a parent, start a fresh request without --retry-launch; retrying the old ID returns this refusal.'
].join('\n')
expect(formatAmbiguousCwdParentMessage('codex')).toBe(codex)
expect(formatAmbiguousCwdParentMessage()).toBe(codex.replace(' or start codex', ''))
expect(formatAmbiguousCwdParentMessage()).toHaveLength(622)
expect(
Math.max(...ALL_TUI_AGENTS.map((agent) => formatAmbiguousCwdParentMessage(agent).length))
).toBe(650)
for (const agent of ALL_TUI_AGENTS) {
expect(formatAmbiguousCwdParentMessage(agent)).toBe(
codex.replace('or start codex', `or start ${agent}`)
)
expect(formatAmbiguousCwdParentMessage(agent).length).toBeLessThan(4224)
}
})
})
describe('caller parent for a create on another machine', () => {
it('skips a stale terminal and keeps the valid environment parent, as a same-machine create does', async () => {
const { controller, showTerminal } = fixture()
showTerminal.mockRejectedValue(new Error('terminal_not_found'))
await expect(
controller.resolveCallerParentForAnotherStore(
{
envParentWorkspace: 'id:repo-a::/tmp/env-parent',
callerTerminalHandle: 'term_stale'
},
'mac-install'
)
).resolves.toEqual({
parent: {
installationId: 'mac-install',
workspaceKey: 'worktree:repo-a::/tmp/env-parent',
instanceId: 'repo-a::/tmp/env-parent'
},
source: 'env-workspace'
})
})
})
describe('a task id in the note of a create on another machine', () => {
it('names the folder workspace the task was made in', async () => {
const db = makeRuntimeOrchestrationDb()
vi.spyOn(db, 'getDispatchContext').mockReturnValue(undefined)
vi.spyOn(db, 'getTask').mockReturnValue(
taskLineageRow({ id: 'task_folder', created_by_terminal_handle: 'term_folder' })
)
const controller = new RuntimeWorktreeLineageController({
getStore: () => null,
getDb: () => db,
showTerminal: async () => ({ worktreeId: 'folder:f-1' }),
resolveWorktree: async () => {
throw new Error('selector_not_found')
}
})
const folderWorkspace: FolderWorkspace = {
id: 'f-1',
projectGroupId: 'group-1',
name: 'Release',
folderPath: '/work/multi',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
createdAt: 0,
updatedAt: 0
}
vi.spyOn(controller, 'resolveParent').mockImplementation(async (selector) => {
if (selector !== 'id:folder:f-1') {
throw new Error('selector_not_found')
}
return { type: 'folder', workspaceKey: 'folder:f-1', folderWorkspace, instanceId: null }
})
await expect(
controller.resolveCallerParentForAnotherStore({ comment: 'for task_folder' }, 'desktop-1')
).resolves.toEqual({
parent: { installationId: 'desktop-1', workspaceKey: 'folder:f-1', instanceId: 'f-1' },
source: 'orchestration-context'
})
})
})
@@ -1,34 +0,0 @@
import { describe, expect, it } from 'vitest'
import { worktreeWorkspaceKey } from '../../../../shared/workspace-scope'
import { resolveRuntimeWorktreeCreateLineage } from './runtime-worktree-lineage-resolution'
const unused = async (): Promise<never> => {
throw new Error('not reached')
}
describe('resolveRuntimeWorktreeCreateLineage', () => {
it('records an app-picked parent on another machine as manual', async () => {
const parent = {
installationId: 'mac-install',
workspaceKey: worktreeWorkspaceKey('repo::/src/parent'),
instanceId: 'parent-instance'
}
await expect(
resolveRuntimeWorktreeCreateLineage(
{ crossStoreParent: parent, parentWorkspaceOrigin: 'manual' },
{
resolveParent: unused,
resolveWorktreeParent: unused,
resolveTaskCandidate: unused,
resolveCaller: unused
}
)
).resolves.toEqual({
kind: 'crossStore',
parent,
origin: 'manual',
capture: { source: 'manual-action', confidence: 'explicit' }
})
})
})
@@ -100,13 +100,6 @@ describe('updateRuntimeManagedWorktreeMetadata under a WSL move', () => {
expect(p.notifyChanged).not.toHaveBeenCalled()
})
it('refuses a user edit', async () => {
await expect(update(MOVING, { comment: 'mine' })).rejects.toBeInstanceOf(
WslMoveProjectFencedError
)
expect(store.getWorktreeMeta(MOVING)).toBeUndefined()
})
it('refuses a lineage change even alongside passive fields', async () => {
await expect(
update(MOVING, { isUnread: true, lineage: { noParent: true } })
@@ -120,18 +120,6 @@ describe('main places the swept workspace’s live terminals, layout-owner build
expect(listed?.tabId).not.toMatch(/^pty:/)
})
it('a listing that does not ask places nothing', async () => {
const { store, runtime } = await hostWith([session('pty-swept')])
const context = { runtime }
await terminalListHandler()(
{ worktree: `id:${SWEPT}`, limit: 200, requireFreshPtyLiveness: true },
context
)
expect(boundPtyIds(store, SWEPT)).toEqual([])
})
it('leaves a terminal whose registration is still binding its pane, as a census does', async () => {
const { store, runtime } = await hostWith([session('pty-registering')])
runtime.beginPtyRegistration('pty-registering', 'pty-registering-inc')
@@ -1,269 +0,0 @@
import { describe, expect, it } from 'vitest'
import { buildRuntimeWorktreePsSummaries } from './runtime-worktree-ps-summaries'
import type { ResolvedWorktree } from '../catalog/runtime-worktree-path-identity'
import type { RuntimeStore } from '../../runtime-store-contract'
describe('buildRuntimeWorktreePsSummaries', () => {
it('preserves persisted host ownership over the resolved row fallback', () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the summary builder reads only these worktree fields.
const worktree = {
id: 'repo-1::/workspace/app',
repoId: 'repo-1',
hostId: 'ssh:resolved-host',
path: '/workspace/app',
branch: 'feature',
isArchived: false,
isMainWorktree: false,
parentWorktreeId: null,
childWorktreeIds: [],
lineage: null,
lastActivityAt: 0
} as unknown as ResolvedWorktree
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the summary builder reads only these store methods.
const store = {
getRepos: () => [],
getWorktreeMeta: () => ({
hostId: 'ssh:persisted-host',
linkedPR: 42,
linkedItems: [{ provider: 'github', type: 'pr', number: 57 }]
}),
getAllWorktreeMeta: () => ({}),
getFolderWorkspaces: () => [],
getProjectGroups: () => []
} as unknown as RuntimeStore
const summary = buildRuntimeWorktreePsSummaries({
store,
resolvedWorktrees: [worktree],
platformByRepoId: new Map()
}).get(worktree.id)
expect(summary?.hostId).toBe('ssh:persisted-host')
expect(summary?.linkedItems).toEqual([
{ provider: 'github', type: 'pr', number: 57 },
{ provider: 'github', type: 'pr', number: 42 }
])
})
it("sends a renamed workspace's older ids so phone history still finds its sessions", () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the builder reads only these fields for a git row.
const worktree = {
id: 'repo-1::/workspace/app',
repoId: 'repo-1',
path: '/workspace/app',
branch: 'feature',
isArchived: false,
isMainWorktree: false,
parentWorktreeId: null,
childWorktreeIds: [],
lineage: null,
lastActivityAt: 0
} as unknown as ResolvedWorktree
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the summary builder reads only these store methods.
const store = {
getRepos: () => [],
getWorktreeMeta: () => ({ priorWorktreeIds: ['repo-1::/workspace/old-app'] }),
getAllWorktreeMeta: () => ({}),
getFolderWorkspaces: () => [],
getProjectGroups: () => []
} as unknown as RuntimeStore
const summary = buildRuntimeWorktreePsSummaries({
store,
resolvedWorktrees: [worktree],
platformByRepoId: new Map()
}).get(worktree.id)
expect(summary?.priorWorktreeIds).toEqual(['repo-1::/workspace/old-app'])
})
it('carries the error of a failed delete the host still lists', () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the builder reads only these fields for a git row.
const worktree = {
id: 'repo-1::/workspace/app',
repoId: 'repo-1',
path: '/workspace/app',
branch: 'feature',
isArchived: false,
isMainWorktree: false,
parentWorktreeId: null,
childWorktreeIds: [],
lineage: null,
lastActivityAt: 0,
removalError: 'Operation not permitted'
} as unknown as ResolvedWorktree
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the builder reads only these store members.
const store = {
getRepos: () => [],
getWorktreeMeta: () => undefined,
getAllWorktreeMeta: () => ({}),
getFolderWorkspaces: () => [],
getProjectGroups: () => []
} as unknown as RuntimeStore
const summary = buildRuntimeWorktreePsSummaries({
store,
resolvedWorktrees: [worktree],
platformByRepoId: new Map()
}).get(worktree.id)
expect(summary?.removalError).toBe('Operation not permitted')
})
it('sends the Workspace options row facts the sidebar reads', () => {
const row = (id: string, fields: Record<string, unknown>) =>
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the builder reads only these fields for a git row.
({
id,
repoId: 'repo-1',
path: `/workspace/${id}`,
branch: 'feature',
head: 'abc1234',
isArchived: false,
isMainWorktree: false,
parentWorktreeId: null,
childWorktreeIds: [],
lineage: null,
lastActivityAt: 0,
...fields
}) as unknown as ResolvedWorktree
const worktrees = [
row('main', { isMainWorktree: true, branch: 'refs/heads/main' }),
// The phone's own guess calls both of these default-branch rows; the host says not.
row('main-detached', { isMainWorktree: true, branch: '' }),
row('provisioned', { isMainWorktree: true, ephemeralVmCheckoutMode: 'provisioned-root' }),
row('automation', { automationProvenance: { kind: 'created-by-automation' } }),
row('cli', { cliProvenance: { kind: 'created-by-cli', createdAt: 0 } }),
row('folder-root', { repoId: 'folder-repo', isMainWorktree: true, branch: '', head: '' })
]
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the builder reads only these store members.
const store = {
getRepos: () => [
{ id: 'repo-1', path: '/r', displayName: 'Repo', badgeColor: '#000', addedAt: 0 },
{
id: 'folder-repo',
path: '/f',
displayName: 'Folder',
badgeColor: '#000',
addedAt: 0,
kind: 'folder'
}
],
getWorktreeMeta: () => undefined,
getAllWorktreeMeta: () => ({}),
getFolderWorkspaces: () => [],
getProjectGroups: () => []
} as unknown as RuntimeStore
const summaries = buildRuntimeWorktreePsSummaries({
store,
resolvedWorktrees: worktrees,
platformByRepoId: new Map()
})
const summary = (id: string) => summaries.get(id)
expect(summary('main')?.defaultBranch).toBe(true)
expect(summary('folder-root')?.defaultBranch).toBe(false)
expect(summary('main-detached')?.defaultBranch).toBe(false)
expect(summary('main-detached')?.detachedHead).toBe(true)
expect(summary('provisioned')?.defaultBranch).toBe(false)
expect(summary('automation')?.automationCreated).toBe(true)
expect(summary('cli')?.cliCreated).toBe(true)
expect(summary('main')).not.toHaveProperty('automationCreated')
expect(summary('main')).not.toHaveProperty('cliCreated')
expect(summary('main')).not.toHaveProperty('detachedHead')
expect(summary('folder-root')).not.toHaveProperty('detachedHead')
})
it("names the automation that made a row, and carries the branch PR's title, link and checks", () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the builder reads only these fields for a git row.
const worktree = {
id: 'repo-1::/workspace/app',
repoId: 'repo-1',
path: '/workspace/app',
branch: 'refs/heads/feature',
head: 'abc1234',
isArchived: false,
isMainWorktree: false,
parentWorktreeId: null,
childWorktreeIds: [],
lineage: null,
lastActivityAt: 0,
automationProvenance: {
kind: 'created-by-automation',
automationNameSnapshot: ' Nightly dependency bump '
}
} as unknown as ResolvedWorktree
const pr = {
number: 7,
title: 'Bump deps',
state: 'open',
url: 'https://github.com/o/r/pull/7',
checksStatus: 'success',
updatedAt: '',
mergeable: 'MERGEABLE',
headSha: 'not-sent'
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the builder reads only these store members.
const store = {
getRepos: () => [
{ id: 'repo-1', path: '/r', displayName: 'Repo', badgeColor: '#000', addedAt: 0 }
],
getWorktreeMeta: () => undefined,
getAllWorktreeMeta: () => ({}),
getFolderWorkspaces: () => [],
getProjectGroups: () => [],
getGitHubCache: () => ({ pr: { 'repo-1::feature': { data: pr, fetchedAt: 1 } }, issue: {} })
} as unknown as RuntimeStore
const summary = buildRuntimeWorktreePsSummaries({
store,
resolvedWorktrees: [worktree],
platformByRepoId: new Map()
}).get(worktree.id)
expect(summary?.automationName).toBe('Nightly dependency bump')
expect(summary?.linkedPR).toEqual({
number: 7,
state: 'open',
title: 'Bump deps',
url: 'https://github.com/o/r/pull/7',
checksStatus: 'success'
})
})
it('leaves the automation name off when the record has none, as older hosts do', () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the builder reads only these fields for a git row.
const worktree = {
id: 'repo-1::/workspace/app',
repoId: 'repo-1',
path: '/workspace/app',
branch: 'feature',
isArchived: false,
isMainWorktree: false,
parentWorktreeId: null,
childWorktreeIds: [],
lineage: null,
lastActivityAt: 0,
// Blank reads as no name.
automationProvenance: { kind: 'created-by-automation', automationNameSnapshot: ' ' }
} as unknown as ResolvedWorktree
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the builder reads only these store members.
const store = {
getRepos: () => [],
getWorktreeMeta: () => undefined,
getAllWorktreeMeta: () => ({}),
getFolderWorkspaces: () => [],
getProjectGroups: () => []
} as unknown as RuntimeStore
const summary = buildRuntimeWorktreePsSummaries({
store,
resolvedWorktrees: [worktree],
platformByRepoId: new Map()
}).get(worktree.id)
expect(summary?.automationCreated).toBe(true)
expect(summary).not.toHaveProperty('automationName')
})
})
@@ -1,24 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { RuntimeWorktreePsSummary } from '../../../../shared/runtime-worktree-contracts'
import { applyRuntimeWorktreePsUnverifiableTerminals } from './runtime-worktree-ps-unverifiable-terminals'
describe('unverifiable terminal attribution', () => {
it('follows the host PTY record, not a stale pane, as the live pass does', () => {
const summaries = new Map<string, Partial<RuntimeWorktreePsSummary>>([
['wt-old', {}],
['wt-new', {}]
])
applyRuntimeWorktreePsUnverifiableTerminals({
countedPtyIds: new Set(),
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the pass reads only ptyId and worktreeId.
leaves: [{ ptyId: 'pty-1', worktreeId: 'wt-old' } as never],
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the pass reads only ptyId and worktreeId.
ptysById: new Map([['pty-1', { ptyId: 'pty-1', worktreeId: 'wt-new' } as never]]),
getLivenessVerdict: () => ({ status: 'unverifiable', reason: 'lost contact' }),
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only the counters this pass writes are read back.
getSummary: (id) => (summaries.get(id) as RuntimeWorktreePsSummary | undefined) ?? null
})
expect(summaries.get('wt-new')?.unverifiableTerminalCount).toBe(1)
expect(summaries.get('wt-old')?.unverifiableTerminalCount).toBeUndefined()
})
})
@@ -22,7 +22,7 @@ vi.mock('../../../telemetry/cohort-classifier', () => ({
/** Exercise journal to worktree-status liveness: feed summaries never retract, but the store drops closed chats, including unanswered approvals whose retained status stays attention. */
const WORKTREE_ID = 'repo-1::/workspace/app'
const SESSION = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'
const SUBJECT = makeStructuredAgentStatusSubject(
makeStructuredAgentStatusSubject(
{
executionHostId: 'local',
wslDistro: null,
@@ -164,29 +164,4 @@ describe('worktree ps and a closed structured chat', () => {
expect(worktreeFor(store).agents).toHaveLength(0)
})
it('keeps an aged host-held approval state authoritative', async () => {
const { store, published } = await awaitingApproval()
const aged = {
...published.at(-1)!,
hostExecutionOwned: true as const,
updatedAt: Date.now() - 30 * 60 * 1000 - 1
}
store.ingestStructuredStatus(aged, SUBJECT)
const row = worktreeFor(store)
expect(row.agents).toHaveLength(1)
expect(row.agents[0]?.state).toBe('blocked')
expect(row.status).toBe('permission')
expect(row.agents[0]?.updatedAt).toBe(aged.updatedAt)
})
it('lets an aged approval decay once the host no longer owns the child', async () => {
const { store, published } = await awaitingApproval()
const { hostExecutionOwned: _owned, ...held } = published.at(-1)!
store.ingestStructuredStatus({ ...held, updatedAt: Date.now() - 30 * 60 * 1000 - 1 }, SUBJECT)
const row = worktreeFor(store)
expect(row.agents).toHaveLength(1)
expect(row.agents[0]?.state).toBe('blocked')
expect(row.status).toBe('inactive')
})
})
@@ -313,25 +313,6 @@ describe('successful scan cache on timeout', () => {
}
)
it('does not reuse failed cached local rows on timeout', async () => {
vi.useFakeTimers()
try {
const { runtime, repo, successfulScan } = cachedScanFixture(null)
runtime.scanGit
.mockResolvedValueOnce({ ...successfulScan, ok: false })
.mockImplementation(neverSettles)
await runtime.scanRows(repo)
await vi.advanceTimersByTimeAsync(31_000)
const result = await advancePastRepoScanBudget(runtime.scanRows(repo))
expect(result.rows[0]).toMatchObject({ branch: '', head: '', displayName: 'app' })
expect(result.ok).toBe(false)
} finally {
vi.useRealTimers()
}
})
it('keeps a failed-removal checkout while the successful Git result awaits its listing overlay', async () => {
vi.useFakeTimers()
const checkoutExists = vi
@@ -374,45 +355,6 @@ describe('worktree.ps on a degraded repo scan', () => {
listWorktreesStrictMock.mockResolvedValue([])
})
it('keeps persisted worktrees when a remote scan stalls past the per-repo budget', async () => {
vi.useFakeTimers()
try {
const runtime = new OrcaRuntimeService(
partialRuntimeStore(makeStore({ connectionId: 'ssh-remote-1' }))
)
const result = await advancePastRepoScanBudget(runtime.getWorktreePs(10_000))
expect(result.worktrees.map((worktree) => worktree.worktreeId)).toContain(WORKTREE_ID)
} finally {
vi.useRealTimers()
}
})
it('keeps persisted worktrees when a remote repo is unreachable', async () => {
const runtime = new OrcaRuntimeService(
partialRuntimeStore(makeStore({ connectionId: 'ssh-remote-1' }))
)
const result = await runtime.getWorktreePs(10_000)
expect(result.worktrees.map((worktree) => worktree.worktreeId)).toContain(WORKTREE_ID)
})
it('keeps persisted worktrees when a local scan stalls past the per-repo budget', async () => {
vi.useFakeTimers()
try {
listWorktreesStrictMock.mockImplementation(neverSettles)
const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore()))
const result = await advancePastRepoScanBudget(runtime.getWorktreePs(10_000))
expect(result.worktrees.map((worktree) => worktree.worktreeId)).toContain(WORKTREE_ID)
} finally {
vi.useRealTimers()
}
})
// withTimeout also falls back on rejection; absorb real scan failures before they can restore stale rows.
it('treats a rejected scan as a real answer instead of restoring persisted worktrees', async () => {
listWorktreesStrictMock.mockRejectedValue(new Error('git unavailable'))
@@ -423,47 +365,6 @@ describe('worktree.ps on a degraded repo scan', () => {
expect(result.worktrees.map((worktree) => worktree.worktreeId)).not.toContain(WORKTREE_ID)
})
it('still reports selector_not_found for a local worktree when the scan rejects', async () => {
listWorktreesStrictMock.mockRejectedValue(new Error('git unavailable'))
const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore()))
await expect(runtime.showManagedWorktree(`id:${WORKTREE_ID}`)).rejects.toThrow(
'selector_not_found'
)
})
it('caches a zero-row local scan instead of rescanning on every poll', async () => {
vi.useFakeTimers()
try {
listWorktreesStrictMock.mockResolvedValue([])
const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore()))
await runtime.getWorktreePs(10_000)
await vi.advanceTimersByTimeAsync(2_000)
await runtime.getWorktreePs(10_000)
expect(listWorktreesStrictMock).toHaveBeenCalledTimes(1)
} finally {
vi.useRealTimers()
}
})
it('caches a failed local scan instead of respawning Git on every poll', async () => {
vi.useFakeTimers()
try {
listWorktreesStrictMock.mockRejectedValue(new Error('spawn git EAGAIN'))
const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore()))
await runtime.getWorktreePs(10_000)
await vi.advanceTimersByTimeAsync(2_000)
await runtime.getWorktreePs(10_000)
expect(listWorktreesStrictMock).toHaveBeenCalledTimes(1)
} finally {
vi.useRealTimers()
}
})
it('does not overwrite another execution host’s healthy rows when this host’s scan stalls', async () => {
vi.useFakeTimers()
try {
@@ -529,21 +430,6 @@ describe('worktree.ps on a degraded repo scan', () => {
}
})
it('does not re-spawn the git scan on every poll while a scan stays stalled', async () => {
vi.useFakeTimers()
try {
listWorktreesStrictMock.mockImplementation(neverSettles)
const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore()))
await advancePastRepoScanBudget(runtime.getWorktreePs(10_000))
await advancePastRepoScanBudget(runtime.getWorktreePs(10_000))
expect(listWorktreesStrictMock).toHaveBeenCalledTimes(1)
} finally {
vi.useRealTimers()
}
})
it('drops and prunes a worktree that a healthy scan no longer reports', async () => {
const removeWorktreeLineage = vi.fn()
listWorktreesStrictMock.mockResolvedValue([
@@ -578,24 +464,6 @@ describe('worktree.ps on a degraded repo scan', () => {
}
})
it('does not re-read persisted worktree metadata on a healthy scan', async () => {
listWorktreesStrictMock.mockResolvedValue([
{ path: REPO_PATH, head: 'abc', branch: 'main', isBare: false, isMainWorktree: true }
])
const store = makeStore()
const getWorktreeMetaForHost = vi.fn((id: string, hostId: string) =>
hostId === 'local' ? store.getWorktreeMeta(id) : undefined
)
const runtime = new OrcaRuntimeService(
partialRuntimeStore({ ...store, getWorktreeMetaForHost })
)
await runtime.getWorktreePs(10_000)
expect(getWorktreeMetaForHost).toHaveBeenCalledWith(MAIN_WORKTREE_ID, 'local')
expect(store.getAllWorktreeMeta).toHaveBeenCalledTimes(1)
})
it('lists restored worktrees while a scan is stalled and still hides agent scratch', async () => {
vi.useFakeTimers()
try {
@@ -105,15 +105,6 @@ describe('worktree.ps host coverage', () => {
])
})
it('names every host the page covers', async () => {
const runtime = new OrcaRuntimeService(makeStore() as never)
const result = await runtime.getWorktreePs(10_000)
expect(result.hostScope?.hostIds).toEqual(['local', `ssh:${SSH_CONNECTION_ID}`])
expect(result.hostScope?.omittedHostIds).toEqual([])
})
it('keeps a remote row in the page when the cap cannot hold every local row', async () => {
const runtime = new OrcaRuntimeService(makeStore() as never)
@@ -81,42 +81,6 @@ describe('stopMissingWorktreeTerminals', () => {
expect(localProvider.shutdown).not.toHaveBeenCalled()
})
it('still stops graph-visible sessions when the owning provider is unavailable', async () => {
const deletedId = 'repo-1::/workspace/deleted'
const runtime = createRuntime()
const result = await stopMissingWorktreeTerminals(
{ ...localRepo, connectionId: 'ssh-1' },
[deletedId],
[],
{
runtime,
getLocalProvider: () => null
}
)
expect(result).toEqual({ stoppedWorktreeIds: [deletedId] })
expect(runtime.stopTerminalsForWorktree).toHaveBeenCalledWith(deletedId, {
resolvedWorktreeId: deletedId,
resolvedConnectionId: 'ssh-1'
})
})
// Per-workspace scans cost O(N) round trips carrying O(N²) rows; share one host inventory.
it('enumerates the host once for the whole sweep', async () => {
const ids = Array.from({ length: 25 }, (_, index) => `repo-1::/workspace/wt-${index}`)
const provider = createProvider(ids.map((id) => `${id}@@session`))
const result = await stopMissingWorktreeTerminals(localRepo, ids, [], {
runtime: createRuntime(),
getLocalProvider: () => provider
})
expect(result.stoppedWorktreeIds).toHaveLength(ids.length)
expect(provider.listProcesses).toHaveBeenCalledTimes(1)
expect(provider.shutdown).toHaveBeenCalledTimes(ids.length)
})
it.each([false, true])(
'stops multiple workspaces through scoped daemon listings (unrelated duplicate: %s)',
async (duplicate) => {
@@ -148,63 +112,6 @@ describe('stopMissingWorktreeTerminals', () => {
}
)
it('keeps provider method binding intact while batching', async () => {
class PrototypeProvider {
listCalls = 0
shutdownCalls: string[] = []
private readonly sessions: { id: string; cwd: string; title: string }[]
constructor(sessionIds: string[]) {
this.sessions = sessionIds.map((id) => ({ id, cwd: '/workspace', title: 'shell' }))
}
async listProcesses(): Promise<{ id: string; cwd: string; title: string }[]> {
this.listCalls += 1
return this.sessions
}
async shutdown(sessionId: string): Promise<void> {
this.shutdownCalls.push(sessionId)
}
}
const ids = ['repo-1::/workspace/a', 'repo-1::/workspace/b', 'repo-1::/workspace/c']
const provider = new PrototypeProvider(ids.map((id) => `${id}@@session`))
const result = await stopMissingWorktreeTerminals(localRepo, ids, [], {
runtime: createRuntime(),
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the sweep calls only listProcesses and shutdown, which this class implements.
getLocalProvider: () => provider as unknown as IPtyProvider
})
expect(result.stoppedWorktreeIds).toHaveLength(ids.length)
expect(provider.listCalls).toBe(1)
expect(provider.shutdownCalls).toHaveLength(ids.length)
})
// Provider-internal this.listProcesses must bypass the sweep cache and read the live host.
it('does not serve the shared snapshot to provider-internal listProcesses', async () => {
class SelfListingProvider {
listCalls = 0
constructor(private readonly sessions: { id: string; cwd: string; title: string }[]) {}
async listProcesses(): Promise<{ id: string; cwd: string; title: string }[]> {
this.listCalls += 1
return this.sessions
}
async shutdown(): Promise<void> {
await this.listProcesses()
}
}
const ids = ['repo-1::/workspace/a', 'repo-1::/workspace/b']
const provider = new SelfListingProvider(
ids.map((id) => ({ id: `${id}@@session`, cwd: '/workspace', title: 'shell' }))
)
await stopMissingWorktreeTerminals(localRepo, ids, [], {
runtime: createRuntime(),
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the sweep calls only listProcesses and shutdown, which this class implements.
getLocalProvider: () => provider as unknown as IPtyProvider
})
expect(provider.listCalls).toBe(1 + ids.length)
})
it('does not reuse a failed process scan', async () => {
const ids = ['repo-1::/workspace/a', 'repo-1::/workspace/b']
const listProcesses = vi
@@ -262,16 +262,6 @@ describe.skipIf(process.platform !== 'darwin')('a worktree delete Git fails part
beginTerminalInstall(worktreePath)()
})
it('keeps the leftover listed with Git’s error after an in-session delete fails', async () => {
const error = await failInSession()
expect(String(error)).toMatch(/Operation not permitted/)
expect(await isRegistered(worktreePath)).toBe(false)
expect(await listedRows()).toEqual([
{ path: worktreePath, removalError: expect.stringMatching(/Operation not permitted/) }
])
})
it('does not retry it at the next start', async () => {
await failStartupFinish()
_resetPendingWorktreeRemovalsForTests()
@@ -332,25 +322,6 @@ describe.skipIf(process.platform !== 'darwin')('a worktree delete Git fails part
])
})
it('deletes the merged branch when the delete fails, and only the folder is left to the user', async () => {
await failInSession()
expect(await git(['branch', '--list', 'feature'])).toBe('')
expect(existsSync(lockedFile)).toBe(true)
expect(removeHostTree).not.toHaveBeenCalled()
expect(await listedRows()).toEqual([
{ path: worktreePath, removalError: expect.stringMatching(/Operation not permitted/) }
])
// Once the folder is gone, listing finishes the remaining record.
await setImmutable(false)
await rm(worktreePath, { recursive: true })
expect(await listedRows()).toEqual([])
await vi.waitFor(async () => expect(await readWorktreeRemovalRecords(recordsDir)).toEqual([]))
expect(await git(['branch', '--list', 'feature'])).toBe('')
expect(await git(['worktree', 'list', '--porcelain'])).not.toContain(worktreePath)
})
it('leaves another worktree registered while its folder is missing, as on an unmounted drive', async () => {
const parent = join(scratchDir, 'volume')
const other = join(parent, 'other')
@@ -406,15 +377,6 @@ describe.skipIf(process.platform !== 'darwin')('a worktree delete Git fails part
expect(existsSync(lockedFile)).toBe(true)
})
it('removes the push-target remote Orca added when the startup finish fails', async () => {
const pushTarget = await addOrcaPushTargetRemote()
expect(String(await finishAtStartup([], pushTarget))).toMatch(LOCKED_FILE_ERROR)
expect(await git(['remote'])).not.toContain(pushTarget.remoteName)
expect(existsSync(lockedFile)).toBe(true)
})
it('keeps an unmerged branch when the delete fails, as a normal delete does', async () => {
await writeFile(join(worktreePath, 'work.txt'), 'work\n')
await git(['add', 'work.txt'], worktreePath)
@@ -1,28 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
// Fragments stay side-effect ordered: mocks, then lifecycle, then fixtures.
await import('../../orca-runtime-tests/runtime-test-module-registration.spec')
const { OrcaRuntimeService } = await import('../../orca-runtime')
await import('../../orca-runtime-tests/orca-runtime-test-lifecycle.spec')
const { createFolderWorkspaceRuntimeStore, makeFolderWorkspace } =
await import('../../orca-runtime-tests/orca-runtime-test-fixtures.spec')
describe('deleting a folder workspace', () => {
it('announces the removal to worktree lifecycle listeners under its folder key', async () => {
const folderWorkspace = makeFolderWorkspace({ folderPath: '/tmp/folder-ws' })
const runtime = new OrcaRuntimeService({
...createFolderWorkspaceRuntimeStore(folderWorkspace),
removeFolderWorkspace: () => true
})
const listener = vi.fn()
runtime.onWorktreeLifecycle(listener)
await expect(runtime.deleteFolderWorkspace(folderWorkspace.id)).resolves.toEqual({
deleted: true
})
expect(listener).toHaveBeenCalledWith({
kind: 'removed',
worktreeId: `folder:${folderWorkspace.id}`,
path: '/tmp/folder-ws'
})
})
})
@@ -783,17 +783,6 @@ describe('finishing an interrupted worktree removal after a restart', () => {
expect(records).toMatchObject([{ failure: { message: expect.any(String) } }])
})
it('deletes the branch when Git finished the checkout but the quit came before the branch', async () => {
await git(['worktree', 'remove', worktreePath])
const { outcome, purged, records } = await finishAfterRestart()
expect(outcome).toMatchObject({ status: 'removed' })
expect(records).toEqual([])
expect(await git(['branch', '--list', 'feature'])).toBe('')
expect(purged).toEqual([`repo-1::${worktreePath}`])
})
it('keeps an unmerged branch, as a normal removal does', async () => {
await writeFile(join(worktreePath, 'work.txt'), 'work\n')
await git(['add', '-A'], worktreePath)
@@ -818,21 +807,6 @@ describe('finishing an interrupted worktree removal after a restart', () => {
)
})
it('treats a removal that fully finished as done', async () => {
const head = (await git(['rev-parse', 'feature'])).trim()
await git(['worktree', 'remove', worktreePath])
await git(['branch', '-d', 'feature'])
const { outcome, purged, records } = await finishAfterRestart({ head })
expect(records).toEqual([])
expect(outcome).toMatchObject({ status: 'removed' })
expect(
outcome && 'preservedBranch' in outcome ? outcome.preservedBranch : undefined
).toBeUndefined()
expect(purged).toEqual([`repo-1::${worktreePath}`])
})
it('drops a record whose repo Orca no longer has', async () => {
vi.spyOn(console, 'warn').mockImplementation(() => {})
@@ -17,10 +17,9 @@ const {
isProvenLiveStructuredSessionRemovalError,
isUnstoppedPtyRemovalError
} = await import('../../../../shared/worktree/removal')
const { listStructuredSessionsForWorktree } = await import('./structured-session-worktree-teardown')
await import('./structured-session-worktree-teardown')
const WORKTREE = 'repo_1::/tmp/wt-a'
const OTHER_WORKTREE = 'repo_1::/tmp/wt-b'
function record(
sessionId: string,
@@ -188,39 +187,6 @@ describe('worktree teardown and structured agent sessions', () => {
hostRef.current = null
})
it('finds sessions by workspace, and ignores a sibling worktree', () => {
installHost({ records: [record('s1', WORKTREE), record('s2', OTHER_WORKTREE)] })
expect(listStructuredSessionsForWorktree(WORKTREE, {})).toEqual({
members: [{ sessionId: 's1', agent: 'claude' }],
live: [{ sessionId: 's1', agent: 'claude' }]
})
})
it('counts a chat with no attached child as a member but never as live', () => {
// Chats at rest remain workspace members even when no child is attached.
installHost({ records: [record('s1', WORKTREE)], detached: new Set(['s1']) })
expect(listStructuredSessionsForWorktree(WORKTREE, {})).toEqual({
members: [{ sessionId: 's1', agent: 'claude' }],
live: []
})
})
it('closes a live session on an ordinary removal instead of refusing it', async () => {
// Structured sessions appear on none of the three PTY surfaces; ordinary removal must close them separately.
const host = installHost({ records: [record('s1', WORKTREE)] })
await expect(killAllProcessesForWorktree(WORKTREE, destructiveDeps())).resolves.toMatchObject({
structuredStopped: 1
})
expect(host.closed).toEqual(['s1'])
})
it('refuses only when the close does not settle', async () => {
installHost({ records: [record('s1', WORKTREE)], stuck: new Set(['s1']) })
await expect(killAllProcessesForWorktree(WORKTREE, destructiveDeps())).rejects.toThrow(
/still live: 1 agent session \(claude\)/
)
})
it('puts the chat tab back when the removal refuses over the session', async () => {
// A refused removal preserves the workspace and must restore its chat tabs.
const host = installHost({
@@ -247,74 +213,6 @@ describe('worktree teardown and structured agent sessions', () => {
warn.mockRestore()
})
it('leaves the chat tab dropped for a folder-workspace removal, which never refuses', async () => {
// Folder removal cannot refuse, so do not restore tabs for the forgotten workspace.
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const host = installHost({
records: [record('s1', WORKTREE)],
stuck: new Set(['s1']),
visible: ['s1']
})
await killAllProcessesForWorktree(WORKTREE, {
localProvider,
includeProviderInventory: false as const,
includeLocalRegistry: false as const,
closeStructuredSessions: true
})
expect([...host.visible]).toEqual([])
warn.mockRestore()
})
it('names the force escape hatch in the refusal, like the unstopped-PTY gate', async () => {
installHost({ records: [record('s1', WORKTREE)], stuck: new Set(['s1']) })
await expect(killAllProcessesForWorktree(WORKTREE, destructiveDeps())).rejects.toThrow(/force/i)
})
it('classifies for the desktop Force Delete button, not just the CLI', async () => {
// Ordinary desktop deletion already passes force for dirty files; the classifier must separately offer the running-session waiver (#11960).
installHost({ records: [record('s1', WORKTREE)], stuck: new Set(['s1']) })
const error = await killAllProcessesForWorktree(WORKTREE, destructiveDeps()).catch(
(thrown: Error) => thrown.message
)
expect(classifyWorktreeForceDeleteReason(error as string, true)).toBe('running-agent-session')
// Once waived, suppress the button just as for unstopped PTYs.
expect(classifyWorktreeForceDeleteReason(error as string, true, true)).toBeNull()
})
it('keeps session ids out of a message users and agents read', async () => {
// Session ids lead to mailbox-gating pane keys; CLI/toast text must expose only counts and providers.
installHost({ records: [record('s1', WORKTREE)], stuck: new Set(['s1']) })
const error = await killAllProcessesForWorktree(WORKTREE, destructiveDeps()).catch(
(thrown: Error) => thrown.message
)
expect(error).not.toContain('s1')
expect(error).toContain('1 agent session (claude)')
})
it('closes best-effort for a folder-workspace removal, which requires no stop proof', async () => {
// Folder paths forget shared roots without stop proof; close best-effort without refusing removal.
const host = installHost({ records: [record('s1', WORKTREE)] })
await expect(
killAllProcessesForWorktree(WORKTREE, {
localProvider,
includeProviderInventory: false,
includeLocalRegistry: false,
closeStructuredSessions: true
})
).resolves.toMatchObject({ structuredStopped: 1 })
expect(host.closed).toEqual(['s1'])
})
it('closes them under force instead of orphaning the child', async () => {
const host = installHost({ records: [record('s1', WORKTREE), record('s2', WORKTREE)] })
const result = await killAllProcessesForWorktree(
WORKTREE,
destructiveDeps({ allowUnverifiedStop: true })
)
expect(host.closed).toEqual(['s1', 's2'])
expect(result.structuredStopped).toBe(2)
})
it('still removes under force when a close does not settle, and says so', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const retired: string[] = []
@@ -358,18 +256,6 @@ describe('worktree teardown and structured agent sessions', () => {
expect(retired).toEqual(['s1'])
})
it('leaves the best-effort reconciliation paths alone', async () => {
// Those callers repair state and delete nothing, so a refusal there would wedge a repair.
installHost({ records: [record('s1', WORKTREE)] })
await expect(
killAllProcessesForWorktree(WORKTREE, {
localProvider,
includeProviderInventory: false,
includeLocalRegistry: false
})
).resolves.toMatchObject({ runtimeStopped: 0 })
})
it('leaves a same-id workspace on another execution host alone', async () => {
// A workspace id is `repoId::path` with no host component, so the local, SSH and paired-runtime
// copies of one id are DIFFERENT workspaces. Unfenced, deleting the local one closed a chat
@@ -383,25 +269,6 @@ describe('worktree teardown and structured agent sessions', () => {
expect(host.closed).toEqual([])
})
it('reads an explicit local fence the way the PTY sweeps do', () => {
// This helper reuses the PTY fence's own type, so the two cannot answer `null` differently:
// there it means this machine, and it has to mean this machine here. ABSENT is the one
// deliberate difference — no fence at all for the PTY sweeps, narrowed to local here, because
// a single-host-id comparison cannot express match-all and closing every host's chats is
// destructive. Latent today only because `WorktreeTeardownDeps` cannot yet carry the `null`.
installHost({
records: [record('s1', WORKTREE, { executionHostId: 'ssh:host-a' }), record('s2', WORKTREE)]
})
const local = {
members: [{ sessionId: 's2', agent: 'claude' }],
live: [{ sessionId: 's2', agent: 'claude' }]
}
expect(listStructuredSessionsForWorktree(WORKTREE, { resolvedConnectionId: null })).toEqual(
local
)
expect(listStructuredSessionsForWorktree(WORKTREE, {})).toEqual(local)
})
it('closes only the session on the host the removal resolved to', async () => {
const host = installHost({
records: [record('s1', WORKTREE, { executionHostId: 'ssh:host-a' }), record('s2', WORKTREE)]
@@ -415,21 +282,6 @@ describe('worktree teardown and structured agent sessions', () => {
expect(host.closed).toEqual(['s1'])
})
it('names only the sessions that stayed, and every provider still there', async () => {
installHost({
records: [
record('s1', WORKTREE),
record('s2', WORKTREE, { provider: 'codex' }),
record('s3', WORKTREE)
],
stuck: new Set(['s2', 's3'])
})
const error = await killAllProcessesForWorktree(WORKTREE, destructiveDeps()).catch(
(thrown: Error) => thrown.message
)
expect(error).toContain('still live: 2 agent sessions (claude, codex)')
})
it('names the unconfirmed sessions too, instead of counting only the live ones', async () => {
// The PTY sibling may drop everything outside its live list because a fresh inventory PROVED
// those exited. Nothing proves that here: an `unverifiable` session is unclosed as well, so
@@ -449,46 +301,6 @@ describe('worktree teardown and structured agent sessions', () => {
expect(isProvenLiveStructuredSessionRemovalError(error as string)).toBe(true)
})
it('still reports what it closed when a forced removal skips the PTY verdict', async () => {
// A sweep that fails outright short-circuits the per-PTY verdict — but not the structured
// close that already ran, so the count has to survive that return or the removal log claims
// `structured=0` for chats it just ended.
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const runtime: TeardownRuntime = {
...presentationRuntime(),
getPtyLivenessVerdict: () => null,
stopTerminalsForWorktree: async () => {
throw new Error('the terminal sweep died')
}
}
const host = installHost({ records: [record('s1', WORKTREE)] })
const result = await killAllProcessesForWorktree(WORKTREE, {
...destructiveDeps({ allowUnverifiedStop: true }),
runtime
})
expect(host.closed).toEqual(['s1'])
expect(result.structuredStopped).toBe(1)
warn.mockRestore()
})
it('separates a close it could not confirm from one it watched stay attached', async () => {
// `src/shared/worktree/removal.ts` keeps these two apart on purpose: a user waiving "we could
// not confirm" is making a different decision than one discarding a conversation Orca just saw
// running. The toast branches on this marker, so flattening them makes one of the two a lie.
installHost({ records: [record('s1', WORKTREE)], unverifiable: new Set(['s1']) })
const unconfirmed = await killAllProcessesForWorktree(WORKTREE, destructiveDeps()).catch(
(thrown: Error) => thrown.message
)
expect(unconfirmed).toContain('could not confirm these closed: 1 agent session (claude)')
expect(isProvenLiveStructuredSessionRemovalError(unconfirmed as string)).toBe(false)
installHost({ records: [record('s1', WORKTREE)], stuck: new Set(['s1']) })
const live = await killAllProcessesForWorktree(WORKTREE, destructiveDeps()).catch(
(thrown: Error) => thrown.message
)
expect(isProvenLiveStructuredSessionRemovalError(live as string)).toBe(true)
})
it('refuses in agent-session wording when the close outlives the sweep budget', async () => {
vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'] })
try {
@@ -530,61 +342,6 @@ describe('worktree teardown and structured agent sessions', () => {
}
})
it('names only the sessions still open when the budget expires mid-close', async () => {
vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'] })
try {
// Record serial-loop progress before timeout so already-closed children are not reported as unclosed.
installHost({
records: [record('s1', WORKTREE), record('s2', WORKTREE, { provider: 'codex' })],
closeGates: { s2: new Promise<void>(() => {}) }
})
const outcome = killAllProcessesForWorktree(
WORKTREE,
destructiveDeps({ timeoutMs: 40 })
).catch((thrown: Error) => thrown.message)
await vi.advanceTimersByTimeAsync(40)
const error = await outcome
expect(error).toContain('could not confirm these closed: 1 agent session (codex)')
expect(error).not.toContain('claude')
} finally {
vi.useRealTimers()
}
})
it('counts the closes that landed before the budget expired', async () => {
vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'] })
// Preserve completed-close counts in timeout logs.
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const slowClose = new Promise<void>((resolve) => {
setTimeout(resolve, 300)
})
try {
installHost({
records: [record('s1', WORKTREE), record('s2', WORKTREE, { provider: 'codex' })],
closeGates: { s2: slowClose }
})
const outcome = killAllProcessesForWorktree(
WORKTREE,
destructiveDeps({ allowUnverifiedStop: true, timeoutMs: 40 })
)
void outcome.catch(() => undefined)
await vi.advanceTimersByTimeAsync(300)
const result = await outcome
expect(result.structuredStopped).toBe(1)
expect(structuredSessionWarning(warn)).toContain(
'could not confirm these closed: 1 agent session (codex)'
)
} finally {
try {
await vi.advanceTimersByTimeAsync(300)
await slowClose
} finally {
warn.mockRestore()
vi.useRealTimers()
}
}
})
it('stops issuing new closes once the budget is spent', async () => {
// Stop issuing closes at deadline; the uncancellable in-flight close and untouched tail both remain unverified.
let releaseFirstClose: () => void = () => {}
@@ -611,25 +368,6 @@ describe('worktree teardown and structured agent sessions', () => {
}
})
it('leaves the terminals already stopped when it refuses over a stuck session', async () => {
// PTY sweeps run concurrently to share the deadline: structured refusal can follow already-stopped PTYs.
// Serializing again would starve terminal stops and let force-delete remove files with open PTY handles.
let terminalSweeps = 0
const runtime: TeardownRuntime = {
...presentationRuntime(),
getPtyLivenessVerdict: () => null,
stopTerminalsForWorktree: async () => {
terminalSweeps += 1
return { stopped: 2 }
}
}
installHost({ records: [record('s1', WORKTREE)], stuck: new Set(['s1']) })
await expect(
killAllProcessesForWorktree(WORKTREE, { ...destructiveDeps(), runtime })
).rejects.toThrow(/still live: 1 agent session \(claude\)/)
expect(terminalSweeps).toBe(1)
})
it('starts the terminal sweeps while the structured close is still in flight', async () => {
// Starting PTY sweeps after serial structured closes would spend their shared budget before they can attempt a stop.
let releaseClose: () => void = () => {}
@@ -654,20 +392,6 @@ describe('worktree teardown and structured agent sessions', () => {
await expect(removal).resolves.toMatchObject({ structuredStopped: 1 })
})
it('retires the chat tab of a chat that had no child to close', async () => {
// Chats at rest are absent from the close list; their durable tab index must not resurrect a deleted workspace.
const host = installHost({
records: [record('s1', WORKTREE)],
detached: new Set(['s1']),
visible: ['s1']
})
await expect(killAllProcessesForWorktree(WORKTREE, destructiveDeps())).resolves.toMatchObject({
runtimeStopped: 0
})
expect(host.closed).toEqual([])
expect([...host.visible]).toEqual([])
})
it('retires it from the live tab snapshot as well as the durable index', async () => {
// Durable index hiding alone leaves the live snapshot tab able to reattach this session.
const retired: string[] = []
@@ -734,32 +458,6 @@ describe('worktree teardown and structured agent sessions', () => {
expect([...host.visible]).toEqual(['s1'])
})
it('retires the chat tab under force, which deletes the workspace anyway', async () => {
const host = installHost({
records: [record('s1', WORKTREE)],
detached: new Set(['s1']),
visible: ['s1']
})
await killAllProcessesForWorktree(WORKTREE, destructiveDeps({ allowUnverifiedStop: true }))
expect([...host.visible]).toEqual([])
})
it('retires the chat tab for a folder-workspace removal too', async () => {
// Forgetting folder metadata must retire detached tabs even though the shared checkout remains.
const host = installHost({
records: [record('s1', WORKTREE)],
detached: new Set(['s1']),
visible: ['s1']
})
await killAllProcessesForWorktree(WORKTREE, {
localProvider,
includeProviderInventory: false as const,
includeLocalRegistry: false as const,
closeStructuredSessions: true
})
expect([...host.visible]).toEqual([])
})
it('retires a live snapshot tab when folder cleanup cannot close its child', async () => {
const retired: string[] = []
const host = installHost({
@@ -849,11 +547,4 @@ describe('worktree teardown and structured agent sessions', () => {
await killAllProcessesForWorktree(WORKTREE, destructiveDeps())
expect([...host.visible]).toEqual(['s1'])
})
it('does not block removal when no structured host is installed', async () => {
// Reading persisted storage would force-install a host during teardown; an unavailable host proves no child.
await expect(killAllProcessesForWorktree(WORKTREE, destructiveDeps())).resolves.toMatchObject({
runtimeStopped: 0
})
})
})