mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
test: remove low-value terminal runtime tests (#38544)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -999,7 +999,6 @@
|
||||
2 test-no-wall-clock:src/main/rate-limits/claude-profile-usage.test.ts
|
||||
2 test-no-wall-clock:src/main/rate-limits/service-cursor-usage.test.ts
|
||||
2 test-no-wall-clock:src/main/runtime/agent-session/structured-agent-session-held-signal.test.ts
|
||||
2 test-no-wall-clock:src/main/runtime/terminal/surface/orca-runtime-terminal-close-records.test.ts
|
||||
2 test-no-wall-clock:src/main/runtime/orchestration-codex-real-pty.integration.test.ts
|
||||
2 test-no-wall-clock:src/main/runtime/orchestration/worker-output-archive.test.ts
|
||||
2 test-no-wall-clock:src/main/runtime/remote-runtime-request-connection.integration.test.ts
|
||||
|
||||
@@ -2498,24 +2498,23 @@
|
||||
],
|
||||
"platforms": ["macos", "linux", "windows"],
|
||||
"providers": ["local", "daemon", "ssh", "remote-runtime"],
|
||||
"coveredPlatforms": ["macos", "linux"],
|
||||
"coveredProviders": ["local", "daemon", "ssh", "remote-runtime"],
|
||||
"coverageNotes": "Shared primitive and status/title parser tests cover provider-independent string behavior on macOS. The status pending-storage suite now retains only its raw UTF-16 parser fixture; its site ownership spy, large-chunk replay and segmented pending-cap fixture were removed. The removed terminal normalizer suite has no replacement-equivalence claim. Native execution, process ownership, wire formats and mobile UI were not validated by this audit. Audit1287 removes the mixed-terminator/malformed-frame example from agent-status-osc.test.ts. Its earliest mixed BEL/ST, malformed-frame removal and individual payload-offset alarms are intentionally lost. The every-split and pending-cap registrations remain; no equivalent replacement is claimed. Audit1313: src/main/runtime/terminal-tail-buffer.test.ts: Random warm/cold comparisons use the same production function as oracle; remaining case counts String.prototype charCodeAt against a private work threshold. Lose cache-total equivalence and prefix-work alarm without replacement. Reduced selectors require fresh validation; prior full-suite receipts do not validate removed or changed coverage. No equivalent replacement or physical-provider claim. Audit1318: src/shared/agent-status/contracts/agent-status-types.test.ts: Schema/field/truncation/enum inventories and JSON.parse call counts characterize status normalization over supplied values. Intentionally lose oversized question validity, Unicode surrogate clipping, stale-field rejection, freshness and dispatch-marker parsing alarms; no producer delivery or rendered interaction and no equivalent replacement. Reduced selectors need fresh validation; historical full-suite receipts do not validate the changed coverage. No equivalent replacement or physical-provider claim.",
|
||||
"coveredPlatforms": ["linux"],
|
||||
"coveredProviders": [],
|
||||
"coverageNotes": "Audit1496 deletes terminal-tail-whitespace.test.ts: the wall-clock padded redraw smoke is intentionally retired, including its quadratic-whitespace alarm. No equivalent replacement or fresh physical-provider/budget evidence is claimed. Shared primitive and status/title parser tests cover provider-independent string behavior on macOS. The status pending-storage suite now retains only its raw UTF-16 parser fixture; its site ownership spy, large-chunk replay and segmented pending-cap fixture were removed. The removed terminal normalizer suite has no replacement-equivalence claim. Native execution, process ownership, wire formats and mobile UI were not validated by this audit. Audit1287 removes the mixed-terminator/malformed-frame example from agent-status-osc.test.ts. Its earliest mixed BEL/ST, malformed-frame removal and individual payload-offset alarms are intentionally lost. The every-split and pending-cap registrations remain; no equivalent replacement is claimed. Audit1313: src/main/runtime/terminal-tail-buffer.test.ts: Random warm/cold comparisons use the same production function as oracle; remaining case counts String.prototype charCodeAt against a private work threshold. Lose cache-total equivalence and prefix-work alarm without replacement. Reduced selectors require fresh validation; prior full-suite receipts do not validate removed or changed coverage. No equivalent replacement or physical-provider claim. Audit1318: src/shared/agent-status/contracts/agent-status-types.test.ts: Schema/field/truncation/enum inventories and JSON.parse call counts characterize status normalization over supplied values. Intentionally lose oversized question validity, Unicode surrogate clipping, stale-field rejection, freshness and dispatch-marker parsing alarms; no producer delivery or rendered interaction and no equivalent replacement. Reduced selectors need fresh validation; historical full-suite receipts do not validate the changed coverage. No equivalent replacement or physical-provider claim. Audit1496 validates the five remaining sources locally on Linux:20 tests pass; no provider execution mapping or padded redraw-budget claim.",
|
||||
"motivatingLinks": [
|
||||
"https://github.com/stablyai/orca/blob/main/src/shared/agent-status/transport/agent-status-osc.test.ts"
|
||||
],
|
||||
"invariant": "Small incomplete status, ANSI and title controls must not retain the larger consumed output strings they were sliced from. Every retained control fragment is routed through ownRetainedString, which preserves exact UTF-16, current caps and trimming, payload order and clean-output offsets on both the Buffer and the code-unit fallback copier.",
|
||||
"oracle": "The primitive forced-GC fixture measures retained backing storage. The remaining title-site spy and raw UTF-16 status/title parser fidelity checks do not establish status-site or terminal-normalizer backing-storage retention. The status ownership spy, large-chunk replay and segmented pending-cap fixture, along with the earlier normalizer per-site spy and fidelity suite, were removed as low-value during the test audit. The retained status UTF-16 fixture checks exact prompt bytes and offsets across BEL/ST completion; it does not measure retained memory.",
|
||||
"commands": [
|
||||
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts src/main/runtime/terminal/output/tail/terminal-tail-whitespace.test.ts"
|
||||
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts"
|
||||
],
|
||||
"testFiles": [
|
||||
"src/shared/own-retained-string.test.ts",
|
||||
"src/shared/agent-status/transport/agent-status-osc.test.ts",
|
||||
"src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts",
|
||||
"src/shared/osc-title-scan-tail-retention.test.ts",
|
||||
"src/shared/osc-title-scan-tail.test.ts",
|
||||
"src/main/runtime/terminal/output/tail/terminal-tail-whitespace.test.ts"
|
||||
"src/shared/osc-title-scan-tail.test.ts"
|
||||
],
|
||||
"assertionRefs": [
|
||||
{
|
||||
@@ -2543,14 +2542,14 @@
|
||||
],
|
||||
"evidenceRuns": [
|
||||
{
|
||||
"date": "2026-11-04",
|
||||
"date": "2026-11-05",
|
||||
"runner": "local",
|
||||
"platform": "linux",
|
||||
"result": "passed",
|
||||
"command": "ORCA_BACKGROUND_LAUNCH=1 pnpm --config.verifyDepsBeforeRun=false test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts src/main/runtime/terminal-tail-whitespace.test.ts --maxWorkers=2",
|
||||
"currentCommand": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts src/main/runtime/terminal/output/tail/terminal-tail-whitespace.test.ts",
|
||||
"durationSeconds": 3.6,
|
||||
"summary": "Test Files 6 passed (6); Tests 22 passed (22). Focused unit selector; native adapters remain mocked where present. Single local execution, not a p95 or cross-platform measurement. Source head bb0888828cc11d69b4463514e7f8cb121771b14f."
|
||||
"command": "ORCA_BACKGROUND_LAUNCH=1 /home/neil/.local/share/mise/installs/node/24.22.0/bin/node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts --maxWorkers=2 --reporter=default --reporter=json --outputFile=/home/neil/orca/workspaces/orca/comprehensive-test-audit-10/build/audits/audit1496-renderer-next/runtime-batch/gate-validation-attempts/1793882954439602905/vitest.json",
|
||||
"currentCommand": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts",
|
||||
"durationSeconds": 1.416,
|
||||
"summary": "20 tests pass across the five remaining sources on an isolated display. Native adapters remain mocked where present. Single local execution; no p95, cross-platform/provider or padded-redraw-work conclusion. Audit1496 candidate based on6e91427ee70d3118d5b78343f16a22d2bd3255e1."
|
||||
}
|
||||
],
|
||||
"runtimeBudget": {
|
||||
@@ -2582,7 +2581,10 @@
|
||||
"Audit1313 retired the complete dated receipt after source coverage changed. Its result does not validate the reduced selector. Original receipt verbatim:\n{\n \"date\": \"2026-11-04\",\n \"runner\": \"local\",\n \"platform\": \"linux\",\n \"command\": \"ORCA_BACKGROUND_LAUNCH=1 pnpm --config.verifyDepsBeforeRun=false test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/agent-status/contracts/agent-status-types.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts src/main/runtime/terminal-tail-buffer.test.ts src/main/runtime/terminal-tail-whitespace.test.ts --maxWorkers=2\",\n \"currentCommand\": \"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/agent-status/contracts/agent-status-types.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts src/main/runtime/terminal-tail-buffer.test.ts src/main/runtime/terminal-tail-whitespace.test.ts\",\n \"result\": \"passed\",\n \"durationSeconds\": 4.64,\n \"summary\": \"Test Files 8 passed (8); Tests 48 passed (48). Fixture correctness only; no timing, work-count or retained-heap conclusion.\"\n }",
|
||||
"Audit1318: src/shared/agent-status/contracts/agent-status-types.test.ts: Schema/field/truncation/enum inventories and JSON.parse call counts characterize status normalization over supplied values. Intentionally lose oversized question validity, Unicode surrogate clipping, stale-field rejection, freshness and dispatch-marker parsing alarms; no producer delivery or rendered interaction and no equivalent replacement. Reduced selectors need fresh validation; historical full-suite receipts do not validate the changed coverage. No equivalent replacement or physical-provider claim.",
|
||||
"Audit1318 retired the complete dated receipt after source coverage changed. Its result does not validate the reduced selector. Original receipt verbatim:\n{\n \"date\": \"2026-11-04\",\n \"runner\": \"local\",\n \"platform\": \"linux\",\n \"command\": \"ORCA_BACKGROUND_LAUNCH=1 pnpm --config.verifyDepsBeforeRun=false test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/agent-status/contracts/agent-status-types.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts src/main/runtime/terminal-tail-buffer.test.ts src/main/runtime/terminal-tail-whitespace.test.ts --maxWorkers=2\",\n \"currentCommand\": \"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/agent-status/contracts/agent-status-types.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts src/main/runtime/terminal-tail-buffer.test.ts src/main/runtime/terminal-tail-whitespace.test.ts\",\n \"result\": \"passed\",\n \"durationSeconds\": 4.64,\n \"summary\": \"Test Files 8 passed (8); Tests 48 passed (48). Fixture correctness only; no timing, work-count or retained-heap conclusion.\"\n }",
|
||||
"Retired during audit1319 selector cleanup; original complete execution receipt (unchanged): {\"date\":\"2026-11-04\",\"runner\":\"local\",\"platform\":\"linux\",\"result\":\"passed\",\"command\":\"ORCA_BACKGROUND_LAUNCH=1 pnpm --config.verifyDepsBeforeRun=false test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/agent-status/contracts/agent-status-types.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts src/main/runtime/terminal-tail-whitespace.test.ts --maxWorkers=2\",\"currentCommand\":\"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/agent-status/contracts/agent-status-types.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts src/main/runtime/terminal-tail-whitespace.test.ts\",\"durationSeconds\":3.39,\"summary\":\"Test Files 7 passed (7); Tests 39 passed (39). Focused unit selector; native adapters remain mocked where present. Single local execution, not a p95 or cross-platform measurement. Source head 574edb34b8e5448541e7a69aac098958d91ffb06.\"}"
|
||||
"Retired during audit1319 selector cleanup; original complete execution receipt (unchanged): {\"date\":\"2026-11-04\",\"runner\":\"local\",\"platform\":\"linux\",\"result\":\"passed\",\"command\":\"ORCA_BACKGROUND_LAUNCH=1 pnpm --config.verifyDepsBeforeRun=false test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/agent-status/contracts/agent-status-types.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts src/main/runtime/terminal-tail-whitespace.test.ts --maxWorkers=2\",\"currentCommand\":\"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/agent-status/contracts/agent-status-types.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts src/main/runtime/terminal-tail-whitespace.test.ts\",\"durationSeconds\":3.39,\"summary\":\"Test Files 7 passed (7); Tests 39 passed (39). Focused unit selector; native adapters remain mocked where present. Single local execution, not a p95 or cross-platform measurement. Source head 574edb34b8e5448541e7a69aac098958d91ffb06.\"}",
|
||||
"Audit1496 deletes terminal-tail-whitespace.test.ts: the wall-clock padded redraw smoke is intentionally retired, including its quadratic-whitespace alarm. No equivalent replacement or fresh physical-provider/budget evidence is claimed.",
|
||||
"Audit1496 retires this complete historical receipt after selector deletion; it does not validate the reduced selector. Original receipt verbatim:\n{\n \"date\": \"2026-11-04\",\n \"runner\": \"local\",\n \"platform\": \"linux\",\n \"result\": \"passed\",\n \"command\": \"ORCA_BACKGROUND_LAUNCH=1 pnpm --config.verifyDepsBeforeRun=false test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts src/main/runtime/terminal-tail-whitespace.test.ts --maxWorkers=2\",\n \"currentCommand\": \"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/own-retained-string.test.ts src/shared/agent-status/transport/agent-status-osc.test.ts src/shared/agent-status/transport/agent-status-osc-pending-retention.test.ts src/shared/osc-title-scan-tail-retention.test.ts src/shared/osc-title-scan-tail.test.ts src/main/runtime/terminal/output/tail/terminal-tail-whitespace.test.ts\",\n \"durationSeconds\": 3.6,\n \"summary\": \"Test Files 6 passed (6); Tests 22 passed (22). Focused unit selector; native adapters remain mocked where present. Single local execution, not a p95 or cross-platform measurement. Source head bb0888828cc11d69b4463514e7f8cb121771b14f.\"\n}",
|
||||
"Audit1496 previous mixed selector platform/provider claims are historical only: {\"coveredPlatforms\":[\"macos\",\"linux\"],\"coveredProviders\":[\"local\",\"daemon\",\"ssh\",\"remote-runtime\"]}"
|
||||
],
|
||||
"promotionCriteria": [
|
||||
"Complete CI soak with zero unexplained flakes and preserve the observable oracle."
|
||||
@@ -2663,46 +2665,22 @@
|
||||
"id": "terminal-performance.padded-fullscreen-redraw",
|
||||
"title": "Fullscreen redraw padding does not stall terminal delivery",
|
||||
"maturity": "experimental",
|
||||
"protection": "partial",
|
||||
"protection": "none",
|
||||
"owner": "terminal-runtime",
|
||||
"layer": "runtime-unit-and-electron-cdp",
|
||||
"surfaces": ["terminal transcript preview", "fullscreen TUI scrolling"],
|
||||
"platforms": ["macos", "linux", "windows"],
|
||||
"providers": ["local", "daemon", "ssh", "remote-runtime"],
|
||||
"coveredPlatforms": ["macos", "linux"],
|
||||
"coveredProviders": ["local", "daemon"],
|
||||
"coverageNotes": "The trim operation is platform-independent and preserves the same spaces/tabs policy for all providers. Real Pi 0.84.2 was exercised in a hidden macOS Electron renderer through CDP using a folder workspace. Audit1313: src/main/runtime/terminal-tail-buffer.test.ts: Random warm/cold comparisons use the same production function as oracle; remaining case counts String.prototype charCodeAt against a private work threshold. Lose cache-total equivalence and prefix-work alarm without replacement. Reduced selectors require fresh validation; prior full-suite receipts do not validate removed or changed coverage. No equivalent replacement or physical-provider claim.",
|
||||
"coveredPlatforms": [],
|
||||
"coveredProviders": [],
|
||||
"coverageNotes": "Audit1496 deletes terminal-tail-whitespace.test.ts: the wall-clock padded redraw smoke is intentionally retired, including its quadratic-whitespace alarm. No equivalent replacement or fresh physical-provider/budget evidence is claimed. The trim operation is platform-independent and preserves the same spaces/tabs policy for all providers. Real Pi 0.84.2 was exercised in a hidden macOS Electron renderer through CDP using a folder workspace. Audit1313: src/main/runtime/terminal-tail-buffer.test.ts: Random warm/cold comparisons use the same production function as oracle; remaining case counts String.prototype charCodeAt against a private work threshold. Lose cache-total equivalence and prefix-work alarm without replacement. Reduced selectors require fresh validation; prior full-suite receipts do not validate removed or changed coverage. No equivalent replacement or physical-provider claim.",
|
||||
"motivatingLinks": ["https://github.com/stablyai/orca/issues/14770"],
|
||||
"invariant": "Transcript preview trimming preserves internal whitespace and terminal read contents without quadratic main-process work on padded fullscreen redraws.",
|
||||
"oracle": "Preserve 32,000 spaces before a marker while trimming trailing spaces/tabs in both retained-row and carried-prefix redraw paths; four redraws must finish within 500 ms. Existing tail equivalence tests preserve cursor, retention, and pagination behavior.",
|
||||
"commands": [
|
||||
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/runtime/terminal/output/tail/terminal-tail-whitespace.test.ts src/main/runtime/retained-tail-redraw-window.equivalence.test.ts"
|
||||
],
|
||||
"testFiles": [
|
||||
"src/main/runtime/terminal/output/tail/terminal-tail-whitespace.test.ts",
|
||||
"src/main/runtime/retained-tail-redraw-window.equivalence.test.ts"
|
||||
],
|
||||
"assertionRefs": [
|
||||
{
|
||||
"file": "src/main/runtime/terminal/output/tail/terminal-tail-whitespace.test.ts",
|
||||
"assertions": [
|
||||
"handles padded redraws across %i retained rows without stalling",
|
||||
"preserves terminal text while trimming spaces and tabs: %j"
|
||||
]
|
||||
}
|
||||
],
|
||||
"evidenceRuns": [
|
||||
{
|
||||
"date": "2026-11-04",
|
||||
"runner": "local",
|
||||
"platform": "linux",
|
||||
"result": "passed",
|
||||
"command": "ORCA_BACKGROUND_LAUNCH=1 pnpm --config.verifyDepsBeforeRun=false test src/main/runtime/terminal-tail-whitespace.test.ts src/main/runtime/retained-tail-redraw-window.equivalence.test.ts --maxWorkers=2",
|
||||
"currentCommand": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/runtime/terminal/output/tail/terminal-tail-whitespace.test.ts src/main/runtime/retained-tail-redraw-window.equivalence.test.ts",
|
||||
"durationSeconds": 2.82,
|
||||
"summary": "Test Files 2 passed (2); Tests 4 passed (4). Focused unit selector; native adapters remain mocked where present. Single local execution, not a p95 or cross-platform measurement. Source head 574edb34b8e5448541e7a69aac098958d91ffb06."
|
||||
}
|
||||
],
|
||||
"oracle": "The padded-redraw whitespace timing suite and its assertion references were removed by audit1496. This gate has no current test protection. Historical timing, red-green and performance observations remain historical only; no equivalent replacement is claimed.",
|
||||
"commands": [],
|
||||
"testFiles": [],
|
||||
"assertionRefs": [],
|
||||
"evidenceRuns": [],
|
||||
"runtimeBudget": {
|
||||
"p95Seconds": 30,
|
||||
"scope": "Three unit test files; padding cases allow 500 ms for four redraws. New focused Linux selector completed once; historical p95 target is unchanged and not measured by this execution."
|
||||
@@ -2723,7 +2701,11 @@
|
||||
"The user manually compared the fixed dev app with production and confirmed improved responsiveness. A live Terminal.app comparison was not exercised; timing measurements used CDP wheel events.",
|
||||
"Linux, Windows, and live SSH rendering were not exercised; the shared trimming behavior is covered by unit tests.",
|
||||
"Audit1313: src/main/runtime/terminal-tail-buffer.test.ts: Random warm/cold comparisons use the same production function as oracle; remaining case counts String.prototype charCodeAt against a private work threshold. Lose cache-total equivalence and prefix-work alarm without replacement. Reduced selectors require fresh validation; prior full-suite receipts do not validate removed or changed coverage. No equivalent replacement or physical-provider claim.",
|
||||
"Audit1313 retired the complete dated receipt after source coverage changed. Its result does not validate the reduced selector. Original receipt verbatim:\n{\n \"date\": \"2026-09-06\",\n \"runner\": \"local\",\n \"platform\": \"macos\",\n \"command\": \"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/runtime/terminal-tail-whitespace.test.ts src/main/runtime/terminal-tail-buffer.test.ts src/main/runtime/retained-tail-redraw-window.equivalence.test.ts\",\n \"result\": \"passed\",\n \"durationSeconds\": 3.96,\n \"summary\": \"17 tests passed. Before the fix both padding budget cases failed, taking approximately 1.7 seconds each.\"\n }"
|
||||
"Audit1313 retired the complete dated receipt after source coverage changed. Its result does not validate the reduced selector. Original receipt verbatim:\n{\n \"date\": \"2026-09-06\",\n \"runner\": \"local\",\n \"platform\": \"macos\",\n \"command\": \"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/runtime/terminal-tail-whitespace.test.ts src/main/runtime/terminal-tail-buffer.test.ts src/main/runtime/retained-tail-redraw-window.equivalence.test.ts\",\n \"result\": \"passed\",\n \"durationSeconds\": 3.96,\n \"summary\": \"17 tests passed. Before the fix both padding budget cases failed, taking approximately 1.7 seconds each.\"\n }",
|
||||
"Audit1496 deletes terminal-tail-whitespace.test.ts: the wall-clock padded redraw smoke is intentionally retired, including its quadratic-whitespace alarm. No equivalent replacement or fresh physical-provider/budget evidence is claimed.",
|
||||
"Audit1496 retires this complete historical receipt after selector deletion; it does not validate the reduced selector. Original receipt verbatim:\n{\n \"date\": \"2026-11-04\",\n \"runner\": \"local\",\n \"platform\": \"linux\",\n \"result\": \"passed\",\n \"command\": \"ORCA_BACKGROUND_LAUNCH=1 pnpm --config.verifyDepsBeforeRun=false test src/main/runtime/terminal-tail-whitespace.test.ts src/main/runtime/retained-tail-redraw-window.equivalence.test.ts --maxWorkers=2\",\n \"currentCommand\": \"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/runtime/terminal/output/tail/terminal-tail-whitespace.test.ts src/main/runtime/retained-tail-redraw-window.equivalence.test.ts\",\n \"durationSeconds\": 2.82,\n \"summary\": \"Test Files 2 passed (2); Tests 4 passed (4). Focused unit selector; native adapters remain mocked where present. Single local execution, not a p95 or cross-platform measurement. Source head 574edb34b8e5448541e7a69aac098958d91ffb06.\"\n}",
|
||||
"Audit1496 previous mixed selector platform/provider claims are historical only: {\"coveredPlatforms\":[\"macos\",\"linux\"],\"coveredProviders\":[\"local\",\"daemon\"]}",
|
||||
"Audit1496 demotes protection to none after deleting the sole padded-work assertion suite. Previous remaining selector was historical context, not equivalent protection: {\"commands\":[\"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/runtime/retained-tail-redraw-window.equivalence.test.ts\"],\"testFiles\":[\"src/main/runtime/retained-tail-redraw-window.equivalence.test.ts\"]}"
|
||||
],
|
||||
"promotionCriteria": [
|
||||
"Complete CI soak requirements and retain the padding budget and tail equivalence oracles."
|
||||
|
||||
@@ -4835,7 +4835,6 @@
|
||||
"src/main/runtime/headless-tab-group-selection.test.ts": 40,
|
||||
"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/output/headless-terminal-query-reply-policy.test.ts": 44,
|
||||
"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,
|
||||
@@ -5791,12 +5790,10 @@
|
||||
"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-row.equivalence.test.ts": 1478,
|
||||
"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,
|
||||
"src/main/runtime/terminal/output/tail/terminal-tail-whitespace.test.ts": 145,
|
||||
"src/main/runtime/terminal-update-admission-census.test.ts": 4182,
|
||||
"src/main/runtime/terminal-vertical-control-scan.test.ts": 168,
|
||||
"src/main/runtime/terminal/wait/terminal-wait-detection.test.ts": 220,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { shouldForwardHeadlessTerminalQueryReply } from './headless-terminal-query-reply-policy'
|
||||
|
||||
describe('shouldForwardHeadlessTerminalQueryReply', () => {
|
||||
const xtVersion = '\x1bP>|xterm.js(6.1.0-beta.287)\x1b\\'
|
||||
const oscColorReply = '\x1b]10;rgb:2e2e/3434/3434\x1b\\'
|
||||
|
||||
it('suppresses XTVERSION for a hidden Grok terminal', () => {
|
||||
expect(shouldForwardHeadlessTerminalQueryReply('grok', xtVersion)).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps other Grok terminal query replies', () => {
|
||||
expect(shouldForwardHeadlessTerminalQueryReply('grok', '\x1b[?1;2c')).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps XTVERSION replies for other agents', () => {
|
||||
expect(shouldForwardHeadlessTerminalQueryReply('codex', xtVersion)).toBe(true)
|
||||
})
|
||||
|
||||
it('suppresses OSC color replies for a hidden Jcode terminal', () => {
|
||||
expect(shouldForwardHeadlessTerminalQueryReply('jcode', oscColorReply)).toBe(false)
|
||||
expect(
|
||||
shouldForwardHeadlessTerminalQueryReply('jcode', '\x1b]11;rgb:ffff/ffff/ffff\x1b\\')
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps non-color replies for a hidden Jcode terminal', () => {
|
||||
expect(shouldForwardHeadlessTerminalQueryReply('jcode', xtVersion)).toBe(true)
|
||||
expect(shouldForwardHeadlessTerminalQueryReply('jcode', '\x1b[?1;2c')).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps OSC color replies for other agents', () => {
|
||||
expect(shouldForwardHeadlessTerminalQueryReply('codex', oscColorReply)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,53 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { normalizeTerminalChunk } from '../terminal-ansi-normalization'
|
||||
import { appendNormalizedToTailBuffer } from './terminal-tail-buffer'
|
||||
import type { RetainedTailRedrawCursor } from './terminal-tail-redraw-buffer'
|
||||
|
||||
import { MAX_TAIL_PARTIAL_CHARS } from './terminal-tail-limits'
|
||||
import { readPtyTail, runtimeWithLeaf } from '../../../orca-runtime-pty-leaf.test-fixture'
|
||||
|
||||
function ingestChunks(chunks: string[]) {
|
||||
let lines: string[] = []
|
||||
let partialLine = ''
|
||||
let pendingAnsi = ''
|
||||
let redrawCursor: RetainedTailRedrawCursor | null = null
|
||||
let completeLines = 0
|
||||
const completed: string[] = []
|
||||
for (const chunk of chunks) {
|
||||
const normalized = normalizeTerminalChunk(chunk, pendingAnsi)
|
||||
pendingAnsi = normalized.pendingAnsi
|
||||
const next = appendNormalizedToTailBuffer(lines, partialLine, normalized.text, redrawCursor)
|
||||
lines = next.lines
|
||||
partialLine = next.partialLine
|
||||
redrawCursor = next.redrawCursor
|
||||
completeLines += next.newCompleteLines
|
||||
completed.push(...next.newlyCompletedLines)
|
||||
expect(next.truncated).toBe(false)
|
||||
}
|
||||
return { lines, partialLine, pendingAnsi, completeLines, completed }
|
||||
}
|
||||
|
||||
describe('terminal tail CRLF chunk boundaries', () => {
|
||||
it('retains the reported base64 line when CR and LF arrive separately', () => {
|
||||
expect(ingestChunks(['QUJDRA==\r', '\n'])).toEqual({
|
||||
lines: ['QUJDRA=='],
|
||||
partialLine: '',
|
||||
pendingAnsi: '',
|
||||
completeLines: 1,
|
||||
completed: ['QUJDRA==']
|
||||
})
|
||||
})
|
||||
|
||||
it('retains content and line counts at every boundary, including empty chunks', () => {
|
||||
const output = '\r\nfirst\r\n\r\nsecond\r\nlast'
|
||||
const expected = ingestChunks([output])
|
||||
for (let boundary = 0; boundary <= output.length; boundary += 1) {
|
||||
expect(ingestChunks([output.slice(0, boundary), '', output.slice(boundary)])).toEqual(
|
||||
expected
|
||||
)
|
||||
}
|
||||
expect(ingestChunks([...output])).toEqual(expected)
|
||||
})
|
||||
|
||||
it('keeps partial retention bounded while a final CR is pending', () => {
|
||||
const normalized = normalizeTerminalChunk(`${'x'.repeat(MAX_TAIL_PARTIAL_CHARS * 2)}\r`)
|
||||
const partial = appendNormalizedToTailBuffer([], '', normalized.text)
|
||||
|
||||
-253
@@ -1,253 +0,0 @@
|
||||
// Test-only: the redraw row model as it was before #11315 (one string rebuild per character),
|
||||
// vendored so wall-time and equivalence tests can measure the current model against it.
|
||||
import {
|
||||
hasCanonicalNumericCsiParams,
|
||||
parseAnsiControlSequence
|
||||
} from '../terminal-ansi-normalization'
|
||||
import { ownRetainedString } from '../../../../../shared/own-retained-string'
|
||||
import { clampTerminalPreviewCursor, trimTerminalLineRight } from './terminal-tail-line-controls'
|
||||
import { MAX_TAIL_CHARS, MAX_TAIL_LINES, MAX_TAIL_PARTIAL_CHARS } from './terminal-tail-limits'
|
||||
import type { RetainedTailRedrawCursor } from './terminal-tail-redraw-buffer'
|
||||
|
||||
export function appendPerCharacterRedrawReference(
|
||||
previousLines: string[],
|
||||
boundedPreviousPartialLine: string,
|
||||
normalizedChunk: string,
|
||||
previousPartialWasCapped: boolean,
|
||||
previousRedrawCursor: RetainedTailRedrawCursor | null
|
||||
): {
|
||||
lines: string[]
|
||||
partialLine: string
|
||||
redrawCursor: RetainedTailRedrawCursor | null
|
||||
truncated: boolean
|
||||
newCompleteLines: number
|
||||
newlyCompletedLines: string[]
|
||||
} {
|
||||
const rows: RetainedTerminalRow[] = [
|
||||
...previousLines.map((line) => ({ text: line, completed: true })),
|
||||
{ text: boundedPreviousPartialLine, completed: false }
|
||||
]
|
||||
let cursorRow = previousRedrawCursor
|
||||
? Math.max(0, rows.length - 1 - previousRedrawCursor.rowFromEnd)
|
||||
: rows.length - 1
|
||||
let cursorColumn = previousRedrawCursor?.column ?? boundedPreviousPartialLine.length
|
||||
let newCompleteLines = 0
|
||||
const newlyCompletedLines: string[] = []
|
||||
let newlyCompletedLineCharacters = 0
|
||||
let newlyCompletedLineStart = 0
|
||||
let truncated = previousPartialWasCapped
|
||||
|
||||
const retainNewlyCompletedLine = (line: string): void => {
|
||||
newlyCompletedLines.push(line)
|
||||
newlyCompletedLineCharacters += line.length
|
||||
while (
|
||||
newlyCompletedLines.length - newlyCompletedLineStart > MAX_TAIL_LINES ||
|
||||
newlyCompletedLineCharacters > MAX_TAIL_CHARS
|
||||
) {
|
||||
newlyCompletedLineCharacters -= newlyCompletedLines[newlyCompletedLineStart]!.length
|
||||
newlyCompletedLineStart += 1
|
||||
}
|
||||
// Why: a single PTY chunk can carry unbounded newlines; compact in batches while retaining the suffix needed for stable pagination.
|
||||
if (newlyCompletedLineStart >= MAX_TAIL_LINES) {
|
||||
newlyCompletedLines.splice(0, newlyCompletedLineStart)
|
||||
newlyCompletedLineStart = 0
|
||||
}
|
||||
}
|
||||
|
||||
const ensureCursorRow = (): void => {
|
||||
while (cursorRow >= rows.length) {
|
||||
rows.push({ text: '', completed: false })
|
||||
}
|
||||
}
|
||||
const trimRows = (): void => {
|
||||
const maxRows = MAX_TAIL_LINES + 1
|
||||
if (rows.length <= maxRows) {
|
||||
return
|
||||
}
|
||||
const removeCount = rows.length - maxRows
|
||||
rows.splice(0, removeCount)
|
||||
cursorRow = Math.max(0, cursorRow - removeCount)
|
||||
truncated = true
|
||||
}
|
||||
const moveCursorToColumn = (nextColumn: number): void => {
|
||||
cursorColumn = clampTerminalPreviewCursor(nextColumn)
|
||||
}
|
||||
const markCursorRowRewritten = (): void => {
|
||||
ensureCursorRow()
|
||||
rows[cursorRow]!.completed = false
|
||||
}
|
||||
const writeChar = (char: string): void => {
|
||||
ensureCursorRow()
|
||||
markCursorRowRewritten()
|
||||
const row = rows[cursorRow]!
|
||||
if (cursorColumn > row.text.length) {
|
||||
row.text = `${row.text}${' '.repeat(cursorColumn - row.text.length)}`
|
||||
}
|
||||
row.text =
|
||||
cursorColumn >= row.text.length
|
||||
? `${row.text}${char}`
|
||||
: `${row.text.slice(0, cursorColumn)}${char}${row.text.slice(cursorColumn + 1)}`
|
||||
cursorColumn += 1
|
||||
}
|
||||
const eraseLine = (mode: number): void => {
|
||||
ensureCursorRow()
|
||||
markCursorRowRewritten()
|
||||
const row = rows[cursorRow]!
|
||||
if (mode === 0) {
|
||||
row.text = row.text.slice(0, cursorColumn)
|
||||
} else if (mode === 1) {
|
||||
const deleteCount = Math.min(cursorColumn + 1, row.text.length)
|
||||
row.text = `${' '.repeat(deleteCount)}${row.text.slice(deleteCount)}`
|
||||
} else if (mode === 2) {
|
||||
row.text = ''
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = 0; index < normalizedChunk.length; index += 1) {
|
||||
const char = normalizedChunk[index]
|
||||
if (char === '\n') {
|
||||
ensureCursorRow()
|
||||
rows[cursorRow]!.completed = true
|
||||
newCompleteLines += 1
|
||||
retainNewlyCompletedLine(trimTerminalLineRight(rows[cursorRow]!.text))
|
||||
cursorRow += 1
|
||||
cursorColumn = 0
|
||||
ensureCursorRow()
|
||||
trimRows()
|
||||
continue
|
||||
}
|
||||
if (char === '\r') {
|
||||
cursorColumn = 0
|
||||
continue
|
||||
}
|
||||
if (char === '\u0008') {
|
||||
cursorColumn = Math.max(0, cursorColumn - 1)
|
||||
continue
|
||||
}
|
||||
if (char === '\u001b') {
|
||||
const parsed = parseAnsiControlSequence(normalizedChunk, index)
|
||||
if (!parsed) {
|
||||
continue
|
||||
}
|
||||
index = parsed.endIndex
|
||||
if (parsed.kind !== 'csi' || !hasCanonicalNumericCsiParams(parsed.params)) {
|
||||
continue
|
||||
}
|
||||
const firstParam = parsed.firstParam ?? 1
|
||||
if (parsed.final === 'A') {
|
||||
cursorRow = Math.max(0, cursorRow - firstParam)
|
||||
rows.splice(cursorRow + 1)
|
||||
} else if (parsed.final === 'K') {
|
||||
eraseLine(parsed.firstParam ?? 0)
|
||||
} else if (parsed.final === 'G' || parsed.final === '`') {
|
||||
moveCursorToColumn(firstParam - 1)
|
||||
} else if (parsed.final === 'D') {
|
||||
cursorColumn = Math.max(0, cursorColumn - firstParam)
|
||||
} else if (parsed.final === 'C') {
|
||||
moveCursorToColumn(cursorColumn + firstParam)
|
||||
}
|
||||
continue
|
||||
}
|
||||
writeChar(char)
|
||||
}
|
||||
|
||||
return finalizeRetainedTerminalRows(
|
||||
rows,
|
||||
cursorRow,
|
||||
cursorColumn,
|
||||
truncated,
|
||||
newCompleteLines,
|
||||
newlyCompletedLineStart > 0
|
||||
? newlyCompletedLines.slice(newlyCompletedLineStart)
|
||||
: newlyCompletedLines
|
||||
)
|
||||
}
|
||||
|
||||
type RetainedTerminalRow = {
|
||||
text: string
|
||||
completed: boolean
|
||||
}
|
||||
|
||||
function finalizeRetainedTerminalRows(
|
||||
rows: RetainedTerminalRow[],
|
||||
cursorRow: number,
|
||||
cursorColumn: number,
|
||||
initialTruncated: boolean,
|
||||
newCompleteLines: number,
|
||||
newlyCompletedLines: string[]
|
||||
): {
|
||||
lines: string[]
|
||||
partialLine: string
|
||||
redrawCursor: RetainedTailRedrawCursor | null
|
||||
truncated: boolean
|
||||
newCompleteLines: number
|
||||
newlyCompletedLines: string[]
|
||||
} {
|
||||
let truncated = initialTruncated
|
||||
let retainedRows = rows.map((row) => ({ ...row, text: trimTerminalLineRight(row.text) }))
|
||||
|
||||
if (retainedRows.length > MAX_TAIL_LINES + 1) {
|
||||
const removeCount = retainedRows.length - (MAX_TAIL_LINES + 1)
|
||||
retainedRows = retainedRows.slice(removeCount)
|
||||
cursorRow = Math.max(0, cursorRow - removeCount)
|
||||
truncated = true
|
||||
}
|
||||
|
||||
let totalChars = retainedRows.reduce((sum, row) => sum + row.text.length, 0)
|
||||
let trimStartIndex = 0
|
||||
while (trimStartIndex < retainedRows.length - 1 && totalChars > MAX_TAIL_CHARS) {
|
||||
totalChars -= retainedRows[trimStartIndex]!.text.length
|
||||
trimStartIndex += 1
|
||||
}
|
||||
if (trimStartIndex > 0) {
|
||||
retainedRows = retainedRows.slice(trimStartIndex)
|
||||
cursorRow = Math.max(0, cursorRow - trimStartIndex)
|
||||
truncated = true
|
||||
}
|
||||
while (
|
||||
retainedRows.length > 1 &&
|
||||
cursorRow < retainedRows.length - 1 &&
|
||||
retainedRows.at(-1)?.completed === false &&
|
||||
retainedRows.at(-1)?.text.length === 0
|
||||
) {
|
||||
retainedRows.pop()
|
||||
}
|
||||
|
||||
const lastRow = retainedRows.at(-1)
|
||||
let partialLine = lastRow && !lastRow.completed ? lastRow.text : ''
|
||||
let lines = (lastRow && !lastRow.completed ? retainedRows.slice(0, -1) : retainedRows).map(
|
||||
(row) => row.text
|
||||
)
|
||||
|
||||
if (partialLine.length > MAX_TAIL_PARTIAL_CHARS) {
|
||||
partialLine = partialLine.slice(-MAX_TAIL_PARTIAL_CHARS)
|
||||
truncated = true
|
||||
}
|
||||
if (lines.length > MAX_TAIL_LINES) {
|
||||
lines = lines.slice(lines.length - MAX_TAIL_LINES)
|
||||
truncated = true
|
||||
}
|
||||
const outputRowCount = lines.length + 1
|
||||
const defaultCursorRow = outputRowCount - 1
|
||||
const defaultCursorColumn = partialLine.length
|
||||
const redrawCursor =
|
||||
cursorRow === defaultCursorRow && cursorColumn === defaultCursorColumn
|
||||
? null
|
||||
: {
|
||||
rowFromEnd: Math.max(0, outputRowCount - 1 - cursorRow),
|
||||
column: clampTerminalPreviewCursor(cursorColumn)
|
||||
}
|
||||
|
||||
return {
|
||||
lines,
|
||||
// Why only the partial: redraw rows are built character by character and never sliced from
|
||||
// the chunk, but the partial is re-sliced from its own row on every chunk, so it alone can
|
||||
// accumulate a backing string across frames. Owning the rows too costs 20-36% on TUI floods
|
||||
// for no measured retention.
|
||||
partialLine: ownRetainedString(partialLine),
|
||||
redrawCursor,
|
||||
truncated,
|
||||
newCompleteLines,
|
||||
newlyCompletedLines
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mulberry32 } from '../../../../../shared/agent-tui-ansi-fuzz-stream'
|
||||
|
||||
import { appendNormalizedToMultilineTailBufferUnwindowed } from './terminal-tail-redraw-buffer'
|
||||
import { appendPerCharacterRedrawReference } from './terminal-tail-per-character-redraw.test-fixture'
|
||||
|
||||
describe('retained terminal row equivalence', () => {
|
||||
it('matches the per-character redraw model across 2,000 randomized chunk streams', () => {
|
||||
const rng = mulberry32(0x263160)
|
||||
const pick = <T>(items: T[]): T => items[Math.floor(rng() * items.length)]!
|
||||
const tokens: (() => string)[] = [
|
||||
() => 'abc y'.slice(0, 1 + Math.floor(rng() * 5)),
|
||||
() => 'x'.repeat(Math.floor(rng() * 300)),
|
||||
() => pick([' ', '\t']).repeat(Math.floor(rng() * 200)),
|
||||
() => pick(['\n', '\r', '\b', '\r\n']),
|
||||
() => `\x1b[${pick(['', '1', '2', '3'])}A`,
|
||||
() => `\x1b[${pick(['', '0', '1', '2', '3'])}K`,
|
||||
() => `\x1b[${Math.floor(rng() * 400)}${pick(['G', 'C', 'D'])}`,
|
||||
() => `\x1b[1A\r${pick(['y', 'y'.repeat(33), 'panel'])}\n`
|
||||
]
|
||||
type TailState = ReturnType<typeof appendNormalizedToMultilineTailBufferUnwindowed>
|
||||
const advance = (
|
||||
model: typeof appendPerCharacterRedrawReference,
|
||||
state: TailState,
|
||||
chunk: string
|
||||
): TailState => model(state.lines, state.partialLine, chunk, false, state.redrawCursor)
|
||||
for (let stream = 0; stream < 2_000; stream += 1) {
|
||||
const empty: TailState = {
|
||||
lines: [],
|
||||
partialLine: '',
|
||||
redrawCursor: null,
|
||||
truncated: false,
|
||||
newCompleteLines: 0,
|
||||
newlyCompletedLines: []
|
||||
}
|
||||
let current = empty
|
||||
let reference = empty
|
||||
for (let chunkIndex = 0; chunkIndex < 6; chunkIndex += 1) {
|
||||
let chunk = ''
|
||||
const count = Math.floor(rng() * 40)
|
||||
for (let token = 0; token < count; token += 1) {
|
||||
chunk += pick(tokens)()
|
||||
}
|
||||
current = advance(appendNormalizedToMultilineTailBufferUnwindowed, current, chunk)
|
||||
reference = advance(appendPerCharacterRedrawReference, reference, chunk)
|
||||
expect(current, `stream ${stream} chunk ${chunkIndex}`).toEqual(reference)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -2,39 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import { appendNormalizedToTailBuffer } from './terminal-tail-buffer'
|
||||
|
||||
describe('retained terminal redraw text spans', () => {
|
||||
it.each([
|
||||
['shorter overwrite', '\x1b[1A\rXY\n', 'XYcdef'],
|
||||
['extending overwrite', '\x1b[1A\r123456789\n', '123456789'],
|
||||
['erase to end', '\x1b[1A\rXY\x1b[KZ\n', 'XYZ'],
|
||||
['erase to start', '\x1b[1A\rXY\x1b[2C!\x1b[1KZ\n', ' Z'],
|
||||
['erase whole line', '\x1b[1A\rXY\x1b[2KZ\n', ' Z'],
|
||||
['backspace', '\x1b[1A\rXY\bZ\n', 'XZcdef'],
|
||||
['carriage return', '\x1b[1A\rXY\rZ\n', 'ZYcdef'],
|
||||
['split surrogate overwrite', '\x1b[1A\r😀\bZ\n', '\ud83dZcdef'],
|
||||
[
|
||||
'Unicode and non-special controls',
|
||||
'\x1b[1A\r漢😀e\u0301\ud800\t\u0000\udfff\bZ\n',
|
||||
'漢😀e\u0301\ud800\t\u0000Z'
|
||||
]
|
||||
])('preserves %s', (_name, chunk, expected) => {
|
||||
const result = appendNormalizedToTailBuffer(['abcdef'], '', chunk)
|
||||
expect(result).toEqual({
|
||||
lines: [expected],
|
||||
partialLine: '',
|
||||
redrawCursor: null,
|
||||
truncated: false,
|
||||
newCompleteLines: 1,
|
||||
newlyCompletedLines: [expected]
|
||||
})
|
||||
})
|
||||
|
||||
it('fills a cursor gap before a text span and retains its code-unit column', () => {
|
||||
const result = appendNormalizedToTailBuffer(['abcdef'], '', '\x1b[1A\x1b[9G😀')
|
||||
expect(result.lines).toEqual([])
|
||||
expect(result.partialLine).toBe('abcdef 😀')
|
||||
expect(result.redrawCursor).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps completed trailing spaces available to a later cursor-up rewrite', () => {
|
||||
const result = appendNormalizedToTailBuffer(['abcdef'], '', '\x1b[1A\rab \n\x1b[1A\x1b[5GX\n')
|
||||
expect(result.lines).toEqual(['ab Xf'])
|
||||
|
||||
@@ -3,7 +3,6 @@ import { appendNormalizedToTailBuffer } from './terminal-tail-buffer'
|
||||
import { MAX_TAIL_CHARS, MAX_TAIL_LINES } from './terminal-tail-limits'
|
||||
import { buildPreview } from './terminal-tail-state'
|
||||
import {
|
||||
getTerminalTailSentinelFullScanCount,
|
||||
getTerminalTailSentinelMatches,
|
||||
tailMayContainBlockedSignal
|
||||
} from './terminal-tail-sentinel-index'
|
||||
@@ -12,14 +11,6 @@ import { terminalWaitBlockedSentinelRe } from '../../../agent-state-rules/blocke
|
||||
import type { RetainedTailRedrawCursor } from './terminal-tail-redraw-buffer'
|
||||
|
||||
// Independent full scan defines whether any retained/partial line matches the blocked sentinel.
|
||||
function referenceMayContainBlockedSignal(lines: string[], partialLine: string): boolean {
|
||||
for (const line of lines) {
|
||||
if (terminalWaitBlockedSentinelRe().test(line)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return terminalWaitBlockedSentinelRe().test(partialLine)
|
||||
}
|
||||
|
||||
function indexedMayContainBlockedSignal(lines: string[], partialLine: string): boolean {
|
||||
return tailMayContainBlockedSignal(lines) || terminalWaitBlockedSentinelRe().test(partialLine)
|
||||
@@ -49,15 +40,6 @@ function unindexed(sim: TailSim): string[] {
|
||||
return [...sim.lines]
|
||||
}
|
||||
|
||||
function assertMatchesFullScan(sim: TailSim): void {
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(
|
||||
referenceMayContainBlockedSignal(sim.lines, sim.partialLine)
|
||||
)
|
||||
expect(computeTerminalTailWaitState(sim.lines, sim.partialLine, sim.preview)).toEqual(
|
||||
computeTerminalTailWaitState(unindexed(sim), sim.partialLine, sim.preview)
|
||||
)
|
||||
}
|
||||
|
||||
const BLOCKED_LINE = 'Update available! Press Enter to continue.'
|
||||
const ESC = String.fromCharCode(27)
|
||||
|
||||
@@ -122,101 +104,10 @@ describe('terminal tail sentinel index', () => {
|
||||
// 20 appended lines + one partial-line test per compute call.
|
||||
expect(incrementalTests).toBeLessThanOrEqual(25)
|
||||
})
|
||||
|
||||
it('keeps a retained sentinel visible and drops it exactly when it is evicted', () => {
|
||||
const sim = saturatedSim()
|
||||
feed(sim, `${BLOCKED_LINE}\n`)
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(true)
|
||||
assertMatchesFullScan(sim)
|
||||
|
||||
// Push the prompt to the very last retained slot.
|
||||
for (let index = 0; index < MAX_TAIL_LINES - 1; index += 1) {
|
||||
feed(sim, `after prompt ${index}\n`)
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(true)
|
||||
}
|
||||
expect(sim.lines[0]).toBe(BLOCKED_LINE)
|
||||
|
||||
feed(sim, 'evicting line\n')
|
||||
expect(sim.lines.includes(BLOCKED_LINE)).toBe(false)
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(false)
|
||||
assertMatchesFullScan(sim)
|
||||
|
||||
for (let index = 0; index < 200; index += 1) {
|
||||
feed(sim, `long after eviction ${index}\n`)
|
||||
}
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(false)
|
||||
assertMatchesFullScan(sim)
|
||||
})
|
||||
|
||||
it('drops a sentinel evicted by the retained-character cap', () => {
|
||||
const sim = newSim()
|
||||
feed(sim, `${BLOCKED_LINE}\n`)
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(true)
|
||||
const bulkLine = `${'x'.repeat(4000)}\n`
|
||||
for (let index = 0; index * 4001 < MAX_TAIL_CHARS + 20000; index += 1) {
|
||||
feed(sim, bulkLine)
|
||||
}
|
||||
expect(sim.lines.includes(BLOCKED_LINE)).toBe(false)
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(false)
|
||||
assertMatchesFullScan(sim)
|
||||
})
|
||||
|
||||
it('finds a sentinel split across two chunks once the line completes', () => {
|
||||
const sim = saturatedSim()
|
||||
feed(sim, 'Codex asks: press ent')
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(false)
|
||||
assertMatchesFullScan(sim)
|
||||
|
||||
feed(sim, 'er to confirm')
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(true)
|
||||
assertMatchesFullScan(sim)
|
||||
|
||||
feed(sim, '\n')
|
||||
expect(sim.partialLine).toBe('')
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(true)
|
||||
assertMatchesFullScan(sim)
|
||||
})
|
||||
|
||||
it('full-scans a tail array the index has never seen (seed/restore path)', () => {
|
||||
// primeWaitBlockedBaselineFromSeededTail reads whatever tail the restore seed installed.
|
||||
const seeded = ['boot log', BLOCKED_LINE, 'trailing']
|
||||
expect(tailMayContainBlockedSignal(seeded)).toBe(true)
|
||||
const state = computeTerminalTailWaitState(seeded, '', '')
|
||||
expect(state.fromTail).toBe(true)
|
||||
expect(state.signal?.reason).toBe('agent-update-prompt')
|
||||
|
||||
const clean = ['boot log', 'no prompt here', 'trailing']
|
||||
expect(tailMayContainBlockedSignal(clean)).toBe(false)
|
||||
expect(computeTerminalTailWaitState(clean, '', '').signal).toBeNull()
|
||||
})
|
||||
|
||||
it('reports fromTail from a blank tail without consulting the index', () => {
|
||||
const sim = newSim()
|
||||
feed(sim, ' \n\t\n')
|
||||
expect(computeTerminalTailWaitState(sim.lines, sim.partialLine, '').fromTail).toBe(false)
|
||||
feed(sim, 'now visible\n')
|
||||
expect(computeTerminalTailWaitState(sim.lines, sim.partialLine, '').fromTail).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// Compare constructor carry windows with independent scans; boolean-only checks miss shifted, duplicated or out-of-bounds positions.
|
||||
describe('terminal tail sentinel index carried window', () => {
|
||||
it('drops a carried match the moment the constructor evicts its row (no stale match)', () => {
|
||||
const sim = saturatedSim()
|
||||
feed(sim, `${BLOCKED_LINE}\n`)
|
||||
// At saturation each append evicts one row; verify the carried match shifts exactly once.
|
||||
for (let index = 0; index < MAX_TAIL_LINES - 1; index += 1) {
|
||||
feed(sim, `after prompt ${index}\n`)
|
||||
expect(getTerminalTailSentinelMatches(sim.lines)).toEqual([MAX_TAIL_LINES - 2 - index])
|
||||
}
|
||||
expect(sim.lines[0]).toBe(BLOCKED_LINE)
|
||||
|
||||
feed(sim, 'evicting line\n')
|
||||
expect(sim.lines.includes(BLOCKED_LINE)).toBe(false)
|
||||
assertIndexedPositionsAreExact(sim.lines)
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(false)
|
||||
})
|
||||
|
||||
it('finds a match a redraw writes into rows the carried prefix does not cover', () => {
|
||||
const sim = saturatedSim()
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(false)
|
||||
@@ -236,25 +127,6 @@ describe('terminal tail sentinel index carried window', () => {
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(true)
|
||||
})
|
||||
|
||||
it('shifts every carried position by exactly the number of rows evicted', () => {
|
||||
const sim = newSim()
|
||||
feed(sim, `first\n${BLOCKED_LINE}\nsecond\n${BLOCKED_LINE}\nthird\n`)
|
||||
expect(getTerminalTailSentinelMatches(sim.lines)).toEqual([1, 3])
|
||||
|
||||
// Saturate so the line cap evicts exactly one row per single-line append.
|
||||
for (let index = 0; index < MAX_TAIL_LINES - 5; index += 1) {
|
||||
feed(sim, `pad ${index}\n`)
|
||||
}
|
||||
expect(sim.lines.length).toBe(MAX_TAIL_LINES)
|
||||
expect(getTerminalTailSentinelMatches(sim.lines)).toEqual([1, 3])
|
||||
|
||||
feed(sim, 'evict one\n')
|
||||
expect(getTerminalTailSentinelMatches(sim.lines)).toEqual([0, 2])
|
||||
feed(sim, 'evict two\n')
|
||||
expect(getTerminalTailSentinelMatches(sim.lines)).toEqual([1])
|
||||
assertIndexedPositionsAreExact(sim.lines)
|
||||
})
|
||||
|
||||
it('stays in bounds when a single chunk evicts the whole carried window and part of itself', () => {
|
||||
const sim = saturatedSim()
|
||||
feed(sim, `${BLOCKED_LINE}\n`)
|
||||
@@ -288,124 +160,11 @@ describe('terminal tail sentinel index carried window', () => {
|
||||
assertIndexedPositionsAreExact(sim.lines)
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(false)
|
||||
})
|
||||
|
||||
it('never leaves a produced tail unindexed, on any append path', () => {
|
||||
const sim = saturatedSim()
|
||||
computeTerminalTailWaitState(sim.lines, sim.partialLine, sim.preview)
|
||||
const fullScansBefore = getTerminalTailSentinelFullScanCount()
|
||||
|
||||
feed(sim, `${BLOCKED_LINE}\n`)
|
||||
for (let index = 0; index < 200; index += 1) {
|
||||
feed(sim, `after prompt ${index}\n`)
|
||||
}
|
||||
feed(sim, 'partial with no newline')
|
||||
feed(sim, ' and its completion\n')
|
||||
feed(sim, '\rspinner 40%')
|
||||
feed(sim, `${ESC}[3A${ESC}[2Kredrawn\n`)
|
||||
feed(sim, `${ESC}[2500A${ESC}[2Kdeep redraw\n`)
|
||||
feed(sim, 'trailing spaces here \n')
|
||||
feed(sim, `${'z'.repeat(5000)}\n`)
|
||||
feed(sim, `multi\nline\nchunk\n`)
|
||||
feed(sim, '')
|
||||
// Reading the verdict must never trigger a scan of an array the constructor produced.
|
||||
computeTerminalTailWaitState(sim.lines, sim.partialLine, sim.preview)
|
||||
|
||||
expect(getTerminalTailSentinelFullScanCount()).toBe(fullScansBefore)
|
||||
assertIndexedPositionsAreExact(sim.lines)
|
||||
})
|
||||
})
|
||||
|
||||
// Deterministic PRNG so a divergence is reproducible from the seed alone.
|
||||
function mulberry32(seed: number): () => number {
|
||||
let state = seed >>> 0
|
||||
return () => {
|
||||
state = (state + 0x6d2b79f5) >>> 0
|
||||
let t = Math.imul(state ^ (state >>> 15), 1 | state)
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `streaming` saturates and evicts the retained tail; `tui` trades saturation for redraw
|
||||
* coverage (cursor-up rewrites of retained rows, and reaches past the redraw window).
|
||||
*/
|
||||
function randomChunk(random: () => number, profile: 'streaming' | 'tui'): string {
|
||||
const roll = random()
|
||||
if (roll < 0.2) {
|
||||
const lines: string[] = []
|
||||
for (let index = 0; index < 30; index += 1) {
|
||||
lines.push(`burst line ${Math.floor(random() * 1e6)}`)
|
||||
}
|
||||
return `${lines.join('\n')}\n`
|
||||
}
|
||||
if (roll < 0.42) {
|
||||
return `plain output ${Math.floor(random() * 1e6)}\n`
|
||||
}
|
||||
if (roll < 0.48) {
|
||||
return `${' '.repeat(Math.floor(random() * 3))}\n`
|
||||
}
|
||||
if (roll < 0.54) {
|
||||
return `${BLOCKED_LINE}\n`
|
||||
}
|
||||
if (roll < 0.58) {
|
||||
return 'do you trust the files in this folder?\n'
|
||||
}
|
||||
if (roll < 0.63) {
|
||||
// Sentinel split across a chunk boundary.
|
||||
return random() < 0.5 ? 'Codex asks: press ent' : 'er to confirm\n'
|
||||
}
|
||||
if (roll < 0.7) {
|
||||
// TUI redraw: move the cursor up a few rows and rewrite them.
|
||||
const rows = 1 + Math.floor(random() * 12)
|
||||
return `${ESC}[${rows}A${ESC}[2Kredrawn row ${Math.floor(random() * 1000)}\n`
|
||||
}
|
||||
if (roll < (profile === 'tui' ? 0.76 : 0.7)) {
|
||||
// Deep redraw that outruns the window and forces the unwindowed path.
|
||||
return `${ESC}[${1500 + Math.floor(random() * 800)}A${ESC}[2Kdeep redraw\n`
|
||||
}
|
||||
if (roll < 0.82) {
|
||||
return `\rspinner ${Math.floor(random() * 100)}%`
|
||||
}
|
||||
if (roll < 0.87) {
|
||||
return 'trailing spaces here \n'
|
||||
}
|
||||
if (roll < 0.91) {
|
||||
return `${'y'.repeat(3000)}\n`
|
||||
}
|
||||
if (roll < 0.95) {
|
||||
return `multi\nline\nchunk ${Math.floor(random() * 1000)}\n`
|
||||
}
|
||||
if (roll < 0.97) {
|
||||
return ''
|
||||
}
|
||||
return `no newline ${Math.floor(random() * 1000)}`
|
||||
}
|
||||
|
||||
describe('terminal tail sentinel index property', () => {
|
||||
for (const profile of ['streaming', 'tui'] as const) {
|
||||
for (const seed of [1, 7, 42, 1337]) {
|
||||
it(`matches a full scan on every step of a random ${profile} sequence (seed ${seed})`, () => {
|
||||
const random = mulberry32(seed)
|
||||
const sim = newSim()
|
||||
let sawSentinel = false
|
||||
let sawSaturation = false
|
||||
for (let step = 0; step < 1200; step += 1) {
|
||||
feed(sim, randomChunk(random, profile))
|
||||
const expected = referenceMayContainBlockedSignal(sim.lines, sim.partialLine)
|
||||
expect(indexedMayContainBlockedSignal(sim.lines, sim.partialLine)).toBe(expected)
|
||||
expect(computeTerminalTailWaitState(sim.lines, sim.partialLine, sim.preview)).toEqual(
|
||||
computeTerminalTailWaitState(unindexed(sim), sim.partialLine, sim.preview)
|
||||
)
|
||||
sawSentinel = sawSentinel || expected
|
||||
sawSaturation = sawSaturation || sim.lines.length >= MAX_TAIL_LINES
|
||||
}
|
||||
// Guard against a vacuous pass.
|
||||
expect(sawSentinel).toBe(true)
|
||||
if (profile === 'streaming') {
|
||||
expect(sawSaturation).toBe(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { appendNormalizedToTailBuffer } from './terminal-tail-buffer'
|
||||
|
||||
describe('terminal redraw whitespace', () => {
|
||||
it.each([2, 20])('handles padded redraws across %i retained rows without stalling', (rows) => {
|
||||
const padded = `${' '.repeat(32_000)}marker \t`
|
||||
const previousLines = Array.from({ length: rows }, (_, index) =>
|
||||
index === 0 ? padded : `row ${index}`
|
||||
)
|
||||
const start = performance.now()
|
||||
let result: ReturnType<typeof appendNormalizedToTailBuffer> | undefined
|
||||
for (let frame = 0; frame < 4; frame += 1) {
|
||||
result = appendNormalizedToTailBuffer(previousLines, 'footer', '\x1b[1A\rupdated')
|
||||
}
|
||||
const elapsedMs = performance.now() - start
|
||||
expect(result?.lines[0]).toBe(`${' '.repeat(32_000)}marker`)
|
||||
// Interior padding made the trailing-whitespace regex backtrack quadratically.
|
||||
expect(elapsedMs).toBeLessThan(500)
|
||||
})
|
||||
})
|
||||
@@ -20,43 +20,6 @@ describe('PtyShellOwnershipMirror', () => {
|
||||
expect(confirm).toHaveBeenCalledTimes(6)
|
||||
})
|
||||
|
||||
it('does not arm from a seed on the normal buffer', async () => {
|
||||
const confirm = vi.fn(async () => true)
|
||||
const mirror = new PtyShellOwnershipMirror(confirm)
|
||||
|
||||
mirror.seedOwner(undefined, { alternateScreen: false })
|
||||
mirror.scan('\x1b]133;D;0\x07')
|
||||
|
||||
expect(confirm).not.toHaveBeenCalled()
|
||||
expect(mirror.owner).toBeUndefined()
|
||||
})
|
||||
|
||||
it('revokes a seeded owner on later TUI bytes', () => {
|
||||
const mirror = new PtyShellOwnershipMirror(async () => true)
|
||||
mirror.seedOwner('shell', { alternateScreen: false })
|
||||
expect(mirror.owner).toBe('shell')
|
||||
|
||||
mirror.scan('\x1b[?1003h')
|
||||
expect(mirror.owner).toBeUndefined()
|
||||
})
|
||||
|
||||
it('retires a hung confirmation so a later candidate can still prove ownership', async () => {
|
||||
let calls = 0
|
||||
const mirror = new PtyShellOwnershipMirror(() => {
|
||||
calls += 1
|
||||
return calls === 1 ? new Promise(() => {}) : Promise.resolve(true)
|
||||
})
|
||||
mirror.seedOwner(undefined, { alternateScreen: true })
|
||||
mirror.scan('\x1b]133;D;137\x07')
|
||||
await mirror.settle()
|
||||
expect(mirror.owner).toBeUndefined()
|
||||
|
||||
mirror.scan('\x1b[?1049hTUI\x1b]133;D;137\x07')
|
||||
await vi.waitFor(() => expect(calls).toBe(2))
|
||||
await mirror.settle()
|
||||
expect(mirror.owner).toBe('shell')
|
||||
})
|
||||
|
||||
it('contains a synchronously throwing confirm callback', async () => {
|
||||
let calls = 0
|
||||
const mirror = new PtyShellOwnershipMirror(() => {
|
||||
|
||||
@@ -11,10 +11,6 @@ export function registerTerminalListExecutionHostScopeTests(): void {
|
||||
// A scoped list must identify its hosts so omitting a live remote worker cannot be mistaken for exit.
|
||||
|
||||
const LOCAL_WORKTREE_ID = 'repo-local::/tmp/local-worktree'
|
||||
const SSH_WORKTREE_ID = 'repo-ssh::/remote/ssh-worktree'
|
||||
const LOCAL_LEAF_ID = '11111111-1111-4111-8111-111111111111'
|
||||
const SSH_LEAF_ID = '22222222-2222-4222-8222-222222222222'
|
||||
const REMOTE_LEAF_ID = '33333333-3333-4333-8333-333333333333'
|
||||
|
||||
const REPOS = [
|
||||
{
|
||||
@@ -114,143 +110,7 @@ export function registerTerminalListExecutionHostScopeTests(): void {
|
||||
return runtime
|
||||
}
|
||||
|
||||
describe('listTerminals execution-host identity', () => {
|
||||
it('names the SSH connection a remote terminal runs on instead of local', async () => {
|
||||
const runtime = makeRuntime([
|
||||
{ worktreeId: LOCAL_WORKTREE_ID, leafId: LOCAL_LEAF_ID, ptyId: 'pty-local-1' },
|
||||
{ worktreeId: SSH_WORKTREE_ID, leafId: SSH_LEAF_ID, ptyId: 'ssh:box-1@@pty-7' }
|
||||
])
|
||||
|
||||
const { terminals } = await runtime.listTerminals()
|
||||
|
||||
const sshRow = terminals.find((terminal) => terminal.ptyId === 'ssh:box-1@@pty-7')
|
||||
const localRow = terminals.find((terminal) => terminal.ptyId === 'pty-local-1')
|
||||
expect(sshRow?.executionHostId).toBe('ssh:box-1')
|
||||
expect(localRow?.executionHostId).toBe('local')
|
||||
})
|
||||
|
||||
it('names the paired runtime environment a mirrored terminal belongs to', async () => {
|
||||
const runtime = makeRuntime([
|
||||
{
|
||||
worktreeId: LOCAL_WORKTREE_ID,
|
||||
leafId: REMOTE_LEAF_ID,
|
||||
ptyId: 'remote:env-7@@handle-1'
|
||||
}
|
||||
])
|
||||
|
||||
const { terminals } = await runtime.listTerminals()
|
||||
|
||||
expect(terminals[0]?.executionHostId).toBe('runtime:env-7')
|
||||
})
|
||||
|
||||
it('leaves the host unset — never local — for a paired PTY that names no environment', async () => {
|
||||
const runtime = makeRuntime([
|
||||
{ worktreeId: LOCAL_WORKTREE_ID, leafId: REMOTE_LEAF_ID, ptyId: 'remote:handle-1' }
|
||||
])
|
||||
|
||||
const { hostScope, terminals } = await runtime.listTerminals()
|
||||
|
||||
expect(terminals[0]?.executionHostId).toBeUndefined()
|
||||
expect(hostScope?.hostIds).toEqual(['local'])
|
||||
})
|
||||
|
||||
it.each([
|
||||
'remote:env@@%E0%A4%A',
|
||||
'remote:%20@@terminal%3Aone',
|
||||
'ssh:%E0%A4%A@@pty-7',
|
||||
'ssh:%20@@pty-7'
|
||||
])('leaves the host unset for a malformed foreign PTY id: %s', async (ptyId) => {
|
||||
const runtime = makeRuntime([
|
||||
{ worktreeId: LOCAL_WORKTREE_ID, leafId: REMOTE_LEAF_ID, ptyId }
|
||||
])
|
||||
|
||||
const { hostScope, terminals } = await runtime.listTerminals()
|
||||
|
||||
expect(terminals[0]?.executionHostId).toBeUndefined()
|
||||
expect(hostScope?.hostIds).toEqual(['local'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('listTerminals scope declaration', () => {
|
||||
it('declares every host an unscoped listing covers', async () => {
|
||||
const runtime = makeRuntime([
|
||||
{ worktreeId: LOCAL_WORKTREE_ID, leafId: LOCAL_LEAF_ID, ptyId: 'pty-local-1' },
|
||||
{ worktreeId: SSH_WORKTREE_ID, leafId: SSH_LEAF_ID, ptyId: 'ssh:box-1@@pty-7' }
|
||||
])
|
||||
|
||||
const result = await runtime.listTerminals()
|
||||
|
||||
expect(result.hostScope?.hostIds).toEqual(['local', 'ssh:box-1'])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual([])
|
||||
})
|
||||
|
||||
it('does not claim a paired runtime was covered from a mirrored row', async () => {
|
||||
const runtime = makeRuntime([
|
||||
{
|
||||
worktreeId: LOCAL_WORKTREE_ID,
|
||||
leafId: REMOTE_LEAF_ID,
|
||||
ptyId: 'remote:env-7@@handle-1'
|
||||
}
|
||||
])
|
||||
|
||||
const result = await runtime.listTerminals()
|
||||
|
||||
expect(result.hostScope?.hostIds).toEqual(['local'])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['runtime:env-7', 'ssh:box-1'])
|
||||
})
|
||||
|
||||
it('keeps a repo-known paired runtime omitted without a mirrored row', async () => {
|
||||
const baseStore = makeStore()
|
||||
const repos = [
|
||||
REPOS[0]!,
|
||||
{
|
||||
id: 'repo-runtime',
|
||||
path: '/remote/runtime-worktree',
|
||||
displayName: 'runtime',
|
||||
badgeColor: '#000000',
|
||||
addedAt: 0,
|
||||
executionHostId: 'runtime:env-9' as const
|
||||
}
|
||||
]
|
||||
const runtime = makeRuntime([], {
|
||||
...baseStore,
|
||||
getRepos: vi.fn(() => repos),
|
||||
getRepo: vi.fn((id: string) => repos.find((repo) => repo.id === id)),
|
||||
getWorkspaceSessionHostIds: vi.fn(() => ['local'])
|
||||
})
|
||||
|
||||
const result = await runtime.listTerminals()
|
||||
|
||||
expect(result.hostScope?.hostIds).toEqual(['local'])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['runtime:env-9'])
|
||||
})
|
||||
|
||||
it('keeps a repo-known paired runtime omitted in a worktree-scoped listing', async () => {
|
||||
const baseStore = makeStore()
|
||||
const repos = [
|
||||
REPOS[0]!,
|
||||
{
|
||||
id: 'repo-runtime',
|
||||
path: '/remote/runtime-worktree',
|
||||
displayName: 'runtime',
|
||||
badgeColor: '#000000',
|
||||
addedAt: 0,
|
||||
executionHostId: 'runtime:env-9' as const
|
||||
}
|
||||
]
|
||||
const runtime = makeRuntime([], {
|
||||
...baseStore,
|
||||
getRepos: vi.fn(() => repos),
|
||||
getRepo: vi.fn((id: string) => repos.find((repo) => repo.id === id)),
|
||||
getWorkspaceSessionHostIds: vi.fn(() => ['local'])
|
||||
})
|
||||
|
||||
const result = await runtime.listTerminals(`id:${LOCAL_WORKTREE_ID}`)
|
||||
|
||||
expect(result.hostScope?.hostIds).toEqual(['local'])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['runtime:env-9'])
|
||||
})
|
||||
|
||||
it('covers a queried SSH host used only by a folder workspace', async () => {
|
||||
const baseStore = makeStore()
|
||||
const folderWorkspace = makeSshFolderWorkspace()
|
||||
@@ -278,31 +138,6 @@ export function registerTerminalListExecutionHostScopeTests(): void {
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['local'])
|
||||
})
|
||||
|
||||
it('keeps a disconnected folder-workspace SSH host omitted', 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 () => {
|
||||
throw new Error('relay unavailable')
|
||||
})
|
||||
} as never)
|
||||
|
||||
const result = await runtime.listTerminals(`id:${folderWorkspaceKey(folderWorkspace.id)}`)
|
||||
|
||||
expect(result.hostScope?.hostIds).toEqual([])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['local', 'ssh:box-2'])
|
||||
})
|
||||
|
||||
it('does not claim local coverage for a paired-runtime folder workspace', async () => {
|
||||
const baseStore = makeStore()
|
||||
const folderWorkspace = makeRuntimeFolderWorkspace()
|
||||
@@ -320,38 +155,6 @@ export function registerTerminalListExecutionHostScopeTests(): void {
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['local', 'runtime:env-9'])
|
||||
})
|
||||
|
||||
it('keeps a paired-runtime folder owner omitted in an unscoped listing', 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()
|
||||
|
||||
expect(result.hostScope?.hostIds).toEqual(['local'])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['runtime:env-9'])
|
||||
})
|
||||
|
||||
it('keeps a disconnected SSH host omitted when only local inventory answered', async () => {
|
||||
const runtime = makeRuntime([])
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn(async () => ({ id: 'never' })),
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
listProcesses: vi.fn(async () => [])
|
||||
} as never)
|
||||
|
||||
const result = await runtime.listTerminals()
|
||||
|
||||
expect(result.hostScope?.hostIds).toEqual(['local'])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['ssh:box-1'])
|
||||
})
|
||||
|
||||
it('marks every known host omitted when process inventory is unverifiable', async () => {
|
||||
const runtime = makeRuntime([])
|
||||
runtime.setPtyController({
|
||||
@@ -399,17 +202,5 @@ export function registerTerminalListExecutionHostScopeTests(): void {
|
||||
expect((await current).hostScope?.hostIds).toEqual(['local'])
|
||||
expect(listProcessesWithHostScope).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('reports the hosts a worktree-scoped listing skipped, so an empty result is not absolute', async () => {
|
||||
const runtime = makeRuntime([
|
||||
{ worktreeId: SSH_WORKTREE_ID, leafId: SSH_LEAF_ID, ptyId: 'ssh:box-1@@pty-7' }
|
||||
])
|
||||
|
||||
const result = await runtime.listTerminals(`id:${LOCAL_WORKTREE_ID}`)
|
||||
|
||||
expect(result.terminals).toEqual([])
|
||||
expect(result.hostScope?.hostIds).toEqual(['local'])
|
||||
expect(result.hostScope?.omittedHostIds).toEqual(['ssh:box-1'])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
-157
@@ -162,84 +162,6 @@ describe('mobile session terminal persistence retirement', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('fences an absent exact leaf without deleting its live sibling parent', () => {
|
||||
const session = {
|
||||
...getDefaultWorkspaceSession(),
|
||||
tabsByWorktree: {
|
||||
[WORKTREE_ID]: [
|
||||
{
|
||||
id: 'terminal',
|
||||
ptyId: 'pty-right',
|
||||
worktreeId: WORKTREE_ID,
|
||||
title: 'Terminal',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
terminalLayoutsByTabId: {
|
||||
terminal: {
|
||||
root: { type: 'leaf' as const, leafId: 'right' },
|
||||
activeLeafId: 'right',
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { right: 'pty-right' }
|
||||
}
|
||||
},
|
||||
terminalPtyIncarnationsByPaneKey: { 'terminal:left': 'incarnation-left' },
|
||||
remoteSessionIdsByTabId: { terminal: 'pty-right' }
|
||||
}
|
||||
|
||||
const result = retireTerminalSurfaceFromPersistence(session, {
|
||||
worktreeId: WORKTREE_ID,
|
||||
parentTabId: 'terminal',
|
||||
leafId: 'left',
|
||||
ptyId: 'pty-left',
|
||||
incarnationId: 'incarnation-left',
|
||||
retiredAt: 42
|
||||
})
|
||||
|
||||
expect(result.tabsByWorktree[WORKTREE_ID]).toEqual(session.tabsByWorktree[WORKTREE_ID])
|
||||
expect(result.terminalLayoutsByTabId.terminal).toEqual(session.terminalLayoutsByTabId.terminal)
|
||||
expect(result.remoteSessionIdsByTabId).toEqual({ terminal: 'pty-right' })
|
||||
expect(result.terminalPtyIncarnationsByPaneKey?.['terminal:left']).toBeUndefined()
|
||||
expect(result.terminalSurfaceTombstonesByPaneKey).toEqual({})
|
||||
expect(result.terminalTopologyRevisionByRepoId?.[REPO_ID]).toBe(1)
|
||||
})
|
||||
|
||||
it('does not treat a sibling parent PTY as the exact leaf when layout is unavailable', () => {
|
||||
const session = {
|
||||
...getDefaultWorkspaceSession(),
|
||||
tabsByWorktree: {
|
||||
[WORKTREE_ID]: [
|
||||
{
|
||||
id: 'terminal',
|
||||
ptyId: 'pty-right',
|
||||
worktreeId: WORKTREE_ID,
|
||||
title: 'Terminal',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const result = retireTerminalSurfaceFromPersistence(session, {
|
||||
worktreeId: WORKTREE_ID,
|
||||
parentTabId: 'terminal',
|
||||
leafId: 'left',
|
||||
ptyId: 'pty-left',
|
||||
incarnationId: 'incarnation-left'
|
||||
})
|
||||
|
||||
expect(result.tabsByWorktree[WORKTREE_ID]).toEqual(session.tabsByWorktree[WORKTREE_ID])
|
||||
expect(result.terminalSurfaceTombstonesByPaneKey).toEqual({})
|
||||
expect(result.terminalTopologyRevisionByRepoId?.[REPO_ID]).toBe(1)
|
||||
})
|
||||
|
||||
it('rebases stale writes onto durable host membership without retaining pane history', () => {
|
||||
const staleSession = {
|
||||
...getDefaultWorkspaceSession(),
|
||||
@@ -295,47 +217,6 @@ describe('mobile session terminal persistence retirement', () => {
|
||||
expect(untrustedReplacement.terminalPtyIncarnationsByPaneKey).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rebases a host partition that no longer carries the closed web-terminal layout', () => {
|
||||
const tabId = 'web-terminal-24aa462c-589c-45fa-b332-6aa233cd84cf'
|
||||
const prior = {
|
||||
...getDefaultWorkspaceSession(),
|
||||
tabsByWorktree: {
|
||||
[WORKTREE_ID]: [
|
||||
{
|
||||
id: tabId,
|
||||
ptyId: 'pty-worker',
|
||||
worktreeId: WORKTREE_ID,
|
||||
title: 'Worker',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
terminalLayoutsByTabId: {
|
||||
[tabId]: {
|
||||
root: { type: 'leaf' as const, leafId: 'worker' },
|
||||
activeLeafId: 'worker',
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { worker: 'pty-worker' }
|
||||
}
|
||||
},
|
||||
terminalTopologyRevisionByRepoId: { [REPO_ID]: 1 }
|
||||
}
|
||||
const incoming = {
|
||||
...getDefaultWorkspaceSession(),
|
||||
tabsByWorktree: { [WORKTREE_ID]: [] }
|
||||
}
|
||||
const partialIncoming: Partial<typeof incoming> = incoming
|
||||
delete partialIncoming.terminalLayoutsByTabId
|
||||
|
||||
const result = sanitizeWorkspaceSessionTerminalRetirements(incoming, prior)
|
||||
|
||||
expect(result.tabsByWorktree[WORKTREE_ID]).toEqual(prior.tabsByWorktree[WORKTREE_ID])
|
||||
expect(result.terminalLayoutsByTabId[tabId]).toEqual(prior.terminalLayoutsByTabId[tabId])
|
||||
})
|
||||
|
||||
it('replays a legacy retirement when its host partition omits terminal layouts', () => {
|
||||
const tabId = 'web-terminal-24aa462c-589c-45fa-b332-6aa233cd84cf'
|
||||
const incoming = {
|
||||
@@ -375,42 +256,4 @@ describe('mobile session terminal persistence retirement', () => {
|
||||
expect(result.terminalSurfaceTombstonesByPaneKey).toEqual({})
|
||||
expect(result.terminalTopologyRevisionByRepoId).toEqual({ [REPO_ID]: 1 })
|
||||
})
|
||||
|
||||
it('migrates legacy tombstones into one repo watermark', () => {
|
||||
const stale = {
|
||||
...getDefaultWorkspaceSession(),
|
||||
terminalSurfaceTombstonesByPaneKey: {
|
||||
'terminal:left': {
|
||||
worktreeId: WORKTREE_ID,
|
||||
parentTabId: 'terminal',
|
||||
leafId: 'left',
|
||||
ptyId: 'pty-left',
|
||||
incarnationId: 'incarnation-a',
|
||||
retiredAt: 42
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const migrated = sanitizeWorkspaceSessionTerminalRetirements(stale, stale)
|
||||
|
||||
expect(migrated.terminalSurfaceTombstonesByPaneKey).toEqual({})
|
||||
expect(migrated.terminalTopologyRevisionByRepoId).toEqual({ [REPO_ID]: 1 })
|
||||
})
|
||||
|
||||
it('keeps retirement state proportional to repos across many worktrees and closed panes', () => {
|
||||
let session = getDefaultWorkspaceSession()
|
||||
for (let index = 0; index < 1_000; index += 1) {
|
||||
session = retireTerminalSurfaceFromPersistence(session, {
|
||||
worktreeId: `${REPO_ID}::/worktree-${index}`,
|
||||
parentTabId: `terminal-${index}`,
|
||||
leafId: `leaf-${index}`,
|
||||
ptyId: `pty-${index}`,
|
||||
incarnationId: `incarnation-${index}`
|
||||
})
|
||||
}
|
||||
|
||||
expect(session.terminalSurfaceTombstonesByPaneKey).toEqual({})
|
||||
expect(Object.keys(session.terminalTopologyRevisionByRepoId ?? {})).toEqual([REPO_ID])
|
||||
expect(session.terminalTopologyRevisionByRepoId?.[REPO_ID]).toBe(1_000)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
|
||||
import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
|
||||
import { registerBoundPty } from '../../bind-first-pane.test-fixture'
|
||||
|
||||
import {
|
||||
resetAgentLaunchPanesForTests,
|
||||
trackRunningAgentLaunchPane
|
||||
@@ -13,19 +13,14 @@ import {
|
||||
CANARY_TAB_ID,
|
||||
INCARNATION_ID,
|
||||
LEAF_ID,
|
||||
OTHER_WORKTREE_ID,
|
||||
PTY_ID,
|
||||
RUNTIME_OWNED_PTY_ID,
|
||||
SIBLING_INCARNATION_ID,
|
||||
SIBLING_LEAF_ID,
|
||||
SIBLING_PTY_ID,
|
||||
STALE_TAB_ID,
|
||||
TAB_ID,
|
||||
WORKTREE_ID
|
||||
} from '../../__fixtures__/orca-runtime-terminal-close-continuity-state-fixture'
|
||||
import {
|
||||
createHarness,
|
||||
createPtyBackedPublishedSurfaceHarness,
|
||||
createStaleTabCloseHarness
|
||||
} from '../../__fixtures__/orca-runtime-terminal-close-continuity-fixtures'
|
||||
import { expectDesktopDropsPane } from '../../__fixtures__/main-layout-close-fixture'
|
||||
@@ -61,174 +56,7 @@ async function closeLastPane(
|
||||
return close
|
||||
}
|
||||
|
||||
function refuseTabClose(harness: CloseContinuityHarness): void {
|
||||
if (!ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
harness.rejectTerminalTabClose(new Error('terminal_tab_pinned'))
|
||||
return
|
||||
}
|
||||
harness.editSession((session) => ({
|
||||
...session,
|
||||
tabsByWorktree: {
|
||||
[WORKTREE_ID]: (session.tabsByWorktree[WORKTREE_ID] ?? []).map((tab) => ({
|
||||
...tab,
|
||||
isPinned: true
|
||||
}))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
describe('terminal close and handle incarnation continuity', () => {
|
||||
it('delegates a stale spawn-time tab through its current PTY-backed renderer surface', async () => {
|
||||
const harness = await createStaleTabCloseHarness()
|
||||
const { terminal } = harness
|
||||
|
||||
await expect(harness.runtime.closeTerminalTab(terminal.handle)).resolves.toMatchObject({
|
||||
handle: terminal.handle,
|
||||
tabId: TAB_ID,
|
||||
closeMode: 'tab'
|
||||
})
|
||||
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
expect(harness.closeTerminalTab).not.toHaveBeenCalled()
|
||||
expect(harness.getSession().tabsByWorktree[WORKTREE_ID]).toEqual([])
|
||||
} else {
|
||||
expect(harness.closeTerminalTab).toHaveBeenCalledWith(TAB_ID)
|
||||
}
|
||||
})
|
||||
|
||||
// L259a: expected failure until main-owned close handles stale spawn-time tabs.
|
||||
;(ORCA_LAYOUT_OWNER_IS_MAIN ? it.fails : it)(
|
||||
'kills and removes a stale spawn-time tab through its current headless surface',
|
||||
async () => {
|
||||
const harness = await createStaleTabCloseHarness({ headless: true })
|
||||
const { terminal } = harness
|
||||
const published = vi.fn()
|
||||
const unsubscribe = harness.runtime.onMobileSessionTabsChanged(published)
|
||||
|
||||
await expect(harness.runtime.closeTerminalTab(terminal.handle)).resolves.toMatchObject({
|
||||
handle: terminal.handle,
|
||||
tabId: TAB_ID,
|
||||
closeMode: 'tab'
|
||||
})
|
||||
|
||||
expect(harness.kill).toHaveBeenCalledWith(RUNTIME_OWNED_PTY_ID)
|
||||
expect(harness.getSession().tabsByWorktree[WORKTREE_ID]).toEqual([])
|
||||
expect(harness.flushOrThrow.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
harness.kill.mock.invocationCallOrder[0]!
|
||||
)
|
||||
expect(harness.flushOrThrow.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
published.mock.invocationCallOrder[0]!
|
||||
)
|
||||
await expect(
|
||||
harness.runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)
|
||||
).resolves.toMatchObject({
|
||||
retiredTerminalSurfaces: [
|
||||
{
|
||||
parentTabId: TAB_ID,
|
||||
leafId: LEAF_ID,
|
||||
ptyId: RUNTIME_OWNED_PTY_ID,
|
||||
terminal: terminal.handle,
|
||||
incarnationId: INCARNATION_ID
|
||||
}
|
||||
],
|
||||
tabs: []
|
||||
})
|
||||
unsubscribe()
|
||||
}
|
||||
)
|
||||
|
||||
// L259a: expected failure until main-owned close handles stale spawn-time tabs.
|
||||
;(ORCA_LAYOUT_OWNER_IS_MAIN ? it.fails : it)(
|
||||
'still kills and keeps the removal when the durable headless close fails to flush',
|
||||
async () => {
|
||||
const harness = await createStaleTabCloseHarness({ headless: true })
|
||||
const errorSpy = vi
|
||||
.spyOn(console, ORCA_LAYOUT_OWNER_IS_MAIN ? 'warn' : 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
harness.rejectPersistenceFlush(new Error('disk-full'))
|
||||
|
||||
await expect(
|
||||
harness.runtime.closeTerminalTab(harness.terminal.handle)
|
||||
).resolves.toMatchObject({
|
||||
tabId: TAB_ID,
|
||||
closeMode: 'tab'
|
||||
})
|
||||
|
||||
// Flush failure cannot undo the user's stop; retain in-memory retirement for the next write.
|
||||
expect(harness.kill).toHaveBeenCalledWith(RUNTIME_OWNED_PTY_ID)
|
||||
expect(harness.getSession().tabsByWorktree[WORKTREE_ID]).toEqual([])
|
||||
const snapshot = await harness.runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)
|
||||
expect(snapshot.tabs).toEqual([])
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
ORCA_LAYOUT_OWNER_IS_MAIN
|
||||
? '[layout] a terminal close is applied but not yet saved; saving again later:'
|
||||
: '[runtime] failed to persist terminal close:',
|
||||
expect.any(Error)
|
||||
)
|
||||
errorSpy.mockRestore()
|
||||
}
|
||||
)
|
||||
|
||||
// L255: expected failure until main-owned close publishes leaf-specific handles.
|
||||
;(ORCA_LAYOUT_OWNER_IS_MAIN ? it.fails : it)(
|
||||
'publishes each split leaf retirement with its own terminal handle',
|
||||
async () => {
|
||||
const harness = createHarness({ publishMobileSurface: true, registerPtyBacked: true })
|
||||
harness.syncSplitFixtureGraph()
|
||||
const before = await harness.runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)
|
||||
const terminalsByLeafId = new Map(
|
||||
before.tabs.flatMap((tab) =>
|
||||
tab.type === 'terminal' && tab.terminal ? [[tab.leafId, tab.terminal] as const] : []
|
||||
)
|
||||
)
|
||||
expect(terminalsByLeafId.size).toBe(2)
|
||||
harness.syncEmptyGraph()
|
||||
|
||||
await expect(
|
||||
harness.runtime.closeMobileSessionTab(`id:${WORKTREE_ID}`, TAB_ID, { reason: 'user' })
|
||||
).resolves.toMatchObject({ closed: true })
|
||||
|
||||
const after = await harness.runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)
|
||||
expect(after.tabs).toEqual([])
|
||||
expect(after.retiredTerminalSurfaces).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
leafId: LEAF_ID,
|
||||
ptyId: PTY_ID,
|
||||
terminal: terminalsByLeafId.get(LEAF_ID),
|
||||
incarnationId: INCARNATION_ID
|
||||
}),
|
||||
expect.objectContaining({
|
||||
leafId: SIBLING_LEAF_ID,
|
||||
ptyId: SIBLING_PTY_ID,
|
||||
terminal: terminalsByLeafId.get(SIBLING_LEAF_ID),
|
||||
incarnationId: SIBLING_INCARNATION_ID
|
||||
})
|
||||
])
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// L259b: expected failure until main-owned close accepts exact hot-state surfaces without persistence.
|
||||
;(ORCA_LAYOUT_OWNER_IS_MAIN ? it.fails : it).each(['pane', 'tab'] as const)(
|
||||
'closes an exact hot-state %s whose failed reveal left no persisted row',
|
||||
async (closeMode) => {
|
||||
const harness = await createStaleTabCloseHarness({ headless: true })
|
||||
harness.retirePersistedTab()
|
||||
|
||||
await expect(
|
||||
closeMode === 'tab'
|
||||
? harness.runtime.closeTerminalTab(harness.terminal.handle)
|
||||
: harness.runtime.closeTerminal(harness.terminal.handle)
|
||||
).resolves.toMatchObject({ handle: harness.terminal.handle, tabId: TAB_ID })
|
||||
|
||||
expect(harness.kill).toHaveBeenCalledWith(RUNTIME_OWNED_PTY_ID)
|
||||
await expect(
|
||||
harness.runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)
|
||||
).resolves.toMatchObject({ tabs: [] })
|
||||
}
|
||||
)
|
||||
|
||||
it('does not let a colliding PTY id close a different persisted incarnation', async () => {
|
||||
const harness = await createStaleTabCloseHarness({ headless: true })
|
||||
harness.replacePersistedIncarnation(SIBLING_INCARNATION_ID)
|
||||
@@ -243,37 +71,6 @@ describe('terminal close and handle incarnation continuity', () => {
|
||||
expect(harness.getSession().tabsByWorktree[WORKTREE_ID]).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('does not let a PTY handle cross its recorded worktree boundary', async () => {
|
||||
const harness = await createStaleTabCloseHarness({ headless: true })
|
||||
const { terminal } = harness
|
||||
const register = () =>
|
||||
ORCA_LAYOUT_OWNER_IS_MAIN
|
||||
? // New spawn commit binds across workspaces; main's terminalHoldsPane fence must reject it (L256).
|
||||
registerBoundPty(harness.runtime, RUNTIME_OWNED_PTY_ID, OTHER_WORKTREE_ID, null, {
|
||||
tabId: 'tab-close-continuity-crossing',
|
||||
leafId: '44444444-4444-4444-8444-444444444444',
|
||||
incarnationId: INCARNATION_ID
|
||||
})
|
||||
: harness.runtime.registerPty(RUNTIME_OWNED_PTY_ID, OTHER_WORKTREE_ID, null, {
|
||||
tabId: STALE_TAB_ID,
|
||||
leafId: LEAF_ID,
|
||||
incarnationId: INCARNATION_ID
|
||||
})
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
expect(register).toThrow('pane_unbound_at_registration')
|
||||
} else {
|
||||
register()
|
||||
}
|
||||
|
||||
await expect(harness.runtime.closeTerminalTab(terminal.handle)).rejects.toThrow(
|
||||
'terminal_handle_stale'
|
||||
)
|
||||
|
||||
expect(harness.kill).not.toHaveBeenCalled()
|
||||
expect(harness.closeTerminalTab).not.toHaveBeenCalled()
|
||||
expect(harness.getSession().tabsByWorktree[WORKTREE_ID]).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('does not acknowledge final-pane close before durable tab retirement', async () => {
|
||||
const harness = createHarness()
|
||||
const [{ handle }] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
@@ -395,180 +192,6 @@ describe('terminal close and handle incarnation continuity', () => {
|
||||
expect(canary.ptyId).toBe(survivor.ptyId)
|
||||
})
|
||||
|
||||
it('does not kill the final PTY when durable tab retirement is rejected', async () => {
|
||||
const harness = createHarness()
|
||||
const [{ handle }] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
refuseTabClose(harness)
|
||||
|
||||
await expect(harness.runtime.closeTerminal(handle)).rejects.toThrow('terminal_tab_pinned')
|
||||
|
||||
expect(harness.kill).not.toHaveBeenCalled()
|
||||
expect(harness.getSession().tabsByWorktree[WORKTREE_ID]).toHaveLength(1)
|
||||
})
|
||||
|
||||
// Main-owned layout decides close scope; terminal-surface-replies-main.test.ts covers graphless tab close.
|
||||
it.skipIf(ORCA_LAYOUT_OWNER_IS_MAIN)(
|
||||
'requests a stop for every live tab PTY after retirement when the renderer graph is stale',
|
||||
async () => {
|
||||
const harness = createHarness()
|
||||
harness.syncSplitFixtureGraph()
|
||||
const terminal = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals.find(
|
||||
(candidate) => candidate.ptyId === PTY_ID
|
||||
)!
|
||||
harness.syncFixtureGraph()
|
||||
|
||||
const closing = harness.runtime.closeTerminal(terminal.handle)
|
||||
await vi.waitFor(() => expect(harness.closeTerminalTab).toHaveBeenCalled())
|
||||
expect(harness.kill).not.toHaveBeenCalled()
|
||||
|
||||
harness.retirePersistedTab()
|
||||
harness.acknowledged.resolve()
|
||||
await expect(closing).resolves.toMatchObject({ ptyKilled: false })
|
||||
expect(harness.kill).toHaveBeenCalledWith(PTY_ID)
|
||||
expect(harness.kill).toHaveBeenCalledWith(SIBLING_PTY_ID)
|
||||
}
|
||||
)
|
||||
|
||||
it('uses verified teardown after retirement before falling back to kill', async () => {
|
||||
const harness = createHarness()
|
||||
const [{ handle }] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
harness.setVerifiedStopResult(true)
|
||||
|
||||
const closing = closeLastPane(harness, handle)
|
||||
await expect(closing).resolves.toMatchObject({ ptyKilled: true })
|
||||
expect(harness.stopAndWait).toHaveBeenCalledWith(PTY_ID, {
|
||||
deadlineMs: expect.any(Number)
|
||||
})
|
||||
expect(harness.kill).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports an unconfirmed stop on the close receipt rather than a bare uncertain false', async () => {
|
||||
const harness = createHarness()
|
||||
const [{ handle }] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
// Match lost SSH provider: tombstone the lease, report unverified stop and lost contact, never a proven kill.
|
||||
harness.kill.mockReturnValue(false)
|
||||
harness.setVerifiedStopResult(false)
|
||||
harness.runtime.markPtyLivenessUnverifiable(PTY_ID, 'its SSH provider is no longer registered')
|
||||
|
||||
const closing = closeLastPane(harness, handle)
|
||||
|
||||
await expect(closing).resolves.toMatchObject({
|
||||
ptyKilled: false,
|
||||
ptyStopVerdict: 'unverifiable',
|
||||
ptyStopReason: 'its SSH provider is no longer registered'
|
||||
})
|
||||
expect(harness.kill).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('downgrades a live verdict after issuing an unverified follow-up stop', async () => {
|
||||
const harness = createHarness()
|
||||
const [{ handle }] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
harness.setVerifiedStopResult(false)
|
||||
harness.runtime.markPtyLivenessLive(PTY_ID)
|
||||
|
||||
const closing = closeLastPane(harness, handle)
|
||||
|
||||
await expect(closing).resolves.toMatchObject({
|
||||
ptyKilled: false,
|
||||
ptyStopVerdict: 'unverifiable',
|
||||
ptyStopReason: 'a follow-up stop was issued but its outcome could not be verified'
|
||||
})
|
||||
expect(harness.kill).toHaveBeenCalledWith(PTY_ID)
|
||||
})
|
||||
|
||||
it('leaves a confirmed kill receipt free of any stop verdict', async () => {
|
||||
const harness = createHarness()
|
||||
const [{ handle }] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
harness.setVerifiedStopResult(true)
|
||||
|
||||
const closing = closeLastPane(harness, handle)
|
||||
|
||||
const close = await closing
|
||||
expect(close.ptyKilled).toBe(true)
|
||||
expect(close.ptyStopVerdict).toBeUndefined()
|
||||
expect(close.ptyStopReason).toBeUndefined()
|
||||
})
|
||||
|
||||
it('reports an unconfirmed stop when verified teardown rejects after retirement', async () => {
|
||||
const harness = createHarness()
|
||||
const [{ handle }] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
harness.setVerifiedStopResult(new Error('provider_unavailable'))
|
||||
|
||||
const closing = closeLastPane(harness, handle)
|
||||
|
||||
await expect(closing).resolves.toMatchObject({
|
||||
ptyKilled: false,
|
||||
ptyStopVerdict: 'unverifiable',
|
||||
ptyStopReason: 'provider_unavailable'
|
||||
})
|
||||
expect(harness.stopAndWait).toHaveBeenCalledWith(PTY_ID, {
|
||||
deadlineMs: expect.any(Number)
|
||||
})
|
||||
expect(harness.kill).toHaveBeenCalledWith(PTY_ID)
|
||||
})
|
||||
|
||||
it('finishes PTY teardown when the session store disappears after retirement', async () => {
|
||||
const harness = createPtyBackedPublishedSurfaceHarness()
|
||||
const closeMobileSessionTab = vi.spyOn(harness.runtime, 'closeMobileSessionTab')
|
||||
const [terminal] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
expect(terminal).toMatchObject({ ptyId: RUNTIME_OWNED_PTY_ID })
|
||||
const { handle } = terminal
|
||||
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
harness.setStopAndWaitAction(() => harness.makeSessionUnavailable())
|
||||
await expect(harness.runtime.closeTerminal(handle)).resolves.toMatchObject({
|
||||
handle,
|
||||
tabId: TAB_ID,
|
||||
ptyKilled: false
|
||||
})
|
||||
expect(closeMobileSessionTab).not.toHaveBeenCalled()
|
||||
expect(harness.closeTerminalTab).not.toHaveBeenCalled()
|
||||
} else {
|
||||
const closing = harness.runtime.closeTerminal(handle)
|
||||
await vi.waitFor(() => expect(harness.closeTerminalTab).toHaveBeenCalled())
|
||||
expect(closeMobileSessionTab).toHaveBeenCalled()
|
||||
expect(harness.kill).not.toHaveBeenCalled()
|
||||
|
||||
harness.retirePersistedTab()
|
||||
harness.makeSessionUnavailable()
|
||||
harness.acknowledged.resolve()
|
||||
|
||||
await expect(closing).resolves.toMatchObject({ handle, tabId: TAB_ID, ptyKilled: false })
|
||||
expect(harness.closeTerminal).toHaveBeenCalledWith(TAB_ID)
|
||||
}
|
||||
expect(harness.stopAndWait).toHaveBeenCalledWith(RUNTIME_OWNED_PTY_ID, {
|
||||
deadlineMs: expect.any(Number)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not tear down a published PTY when retirement fails before acknowledgement', async () => {
|
||||
const harness = createPtyBackedPublishedSurfaceHarness()
|
||||
refuseTabClose(harness)
|
||||
const [terminal] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
|
||||
await expect(harness.runtime.closeTerminal(terminal.handle)).rejects.toThrow(
|
||||
'terminal_tab_pinned'
|
||||
)
|
||||
expect(harness.closeTerminal).not.toHaveBeenCalled()
|
||||
expect(harness.stopAndWait).not.toHaveBeenCalled()
|
||||
expect(harness.kill).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps a handle valid when renderer reload preserves the PTY incarnation', async () => {
|
||||
const harness = createHarness()
|
||||
const [before] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
|
||||
harness.runtime.markRendererReloading(1)
|
||||
harness.syncFixtureGraph()
|
||||
|
||||
const [after] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
expect(after).toMatchObject({ handle: before.handle, incarnationId: INCARNATION_ID })
|
||||
await expect(harness.runtime.readTerminal(before.handle)).resolves.toMatchObject({
|
||||
handle: before.handle,
|
||||
status: 'running'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a handle through an intermediate empty reload graph', async () => {
|
||||
const harness = createHarness()
|
||||
const [before] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
@@ -596,28 +219,6 @@ describe('terminal close and handle incarnation continuity', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves a retained handle waiter when idle arrives during renderer reload', async () => {
|
||||
const harness = createHarness()
|
||||
const [before] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
harness.runtime.onPtyData(PTY_ID, '\x1b]0;\u280b Working on task\x07output\n', 100)
|
||||
const waiting = harness.runtime.waitForTerminal(before.handle, {
|
||||
condition: 'tui-idle',
|
||||
timeoutMs: 1_000
|
||||
})
|
||||
|
||||
harness.runtime.markRendererReloading(1)
|
||||
harness.runtime.onPtyData(PTY_ID, '\x1b]0;\u2733 Task complete\x07done\n', 200)
|
||||
|
||||
await expect(waiting).resolves.toMatchObject({
|
||||
handle: before.handle,
|
||||
condition: 'tui-idle',
|
||||
satisfied: true
|
||||
})
|
||||
harness.syncFixtureGraph()
|
||||
const [after] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
expect(after.handle).toBe(before.handle)
|
||||
})
|
||||
|
||||
it('stales the old handle when the same PTY id names a new incarnation', async () => {
|
||||
const harness = createHarness()
|
||||
const [before] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
@@ -634,76 +235,6 @@ describe('terminal close and handle incarnation continuity', () => {
|
||||
'terminal_handle_stale'
|
||||
)
|
||||
})
|
||||
|
||||
it('stales a retained handle after the renderer graph becomes unavailable', async () => {
|
||||
const harness = createHarness()
|
||||
const [before] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
|
||||
harness.runtime.markGraphUnavailable(1)
|
||||
harness.runtime.attachWindow(1)
|
||||
harness.syncFixtureGraph()
|
||||
|
||||
const [after] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
expect(after.handle).not.toBe(before.handle)
|
||||
await expect(harness.runtime.readTerminal(before.handle)).rejects.toThrow(
|
||||
'terminal_handle_stale'
|
||||
)
|
||||
})
|
||||
|
||||
it('stales a renderer handle superseded by a preallocated handle', async () => {
|
||||
const harness = createHarness()
|
||||
const [before] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
const preallocated = 'term_preallocated-close-continuity'
|
||||
|
||||
harness.runtime.registerPreAllocatedHandleForPty(PTY_ID, preallocated)
|
||||
await expect(harness.runtime.readTerminal(before.handle)).rejects.toThrow(
|
||||
'terminal_handle_stale'
|
||||
)
|
||||
await expect(harness.runtime.readTerminal(preallocated)).resolves.toMatchObject({
|
||||
handle: preallocated,
|
||||
status: 'running'
|
||||
})
|
||||
harness.syncFixtureGraph()
|
||||
|
||||
const [after] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
expect(after.handle).toBe(preallocated)
|
||||
await expect(harness.runtime.readTerminal(preallocated)).resolves.toMatchObject({
|
||||
handle: preallocated,
|
||||
status: 'running'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a renderer handle when the controller adopts that same handle', async () => {
|
||||
const harness = createHarness()
|
||||
const [before] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
|
||||
harness.runtime.registerPreAllocatedHandleForPty(PTY_ID, before.handle)
|
||||
harness.syncFixtureGraph()
|
||||
|
||||
const [after] = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals
|
||||
expect(after.handle).toBe(before.handle)
|
||||
await expect(harness.runtime.readTerminal(before.handle)).resolves.toMatchObject({
|
||||
handle: before.handle,
|
||||
status: 'running'
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects a preallocated-handle waiter when its PTY is invalidated during reload', async () => {
|
||||
const harness = createHarness()
|
||||
const preallocated = 'term_preallocated-reload-invalidation'
|
||||
harness.runtime.registerPreAllocatedHandleForPty(PTY_ID, preallocated)
|
||||
harness.syncFixtureGraph()
|
||||
harness.runtime.onPtyData(PTY_ID, '\x1b]0;\u280b Working on task\x07output\n', 100)
|
||||
const waiting = harness.runtime.waitForTerminal(preallocated, {
|
||||
condition: 'tui-idle',
|
||||
timeoutMs: 100
|
||||
})
|
||||
|
||||
harness.runtime.markRendererReloading(1)
|
||||
harness.runtime['invalidateAllHandlesForPty'](PTY_ID)
|
||||
|
||||
await expect(waiting).rejects.toThrow('terminal_handle_stale')
|
||||
})
|
||||
})
|
||||
|
||||
function launchIn(leafId: string) {
|
||||
@@ -738,42 +269,4 @@ describe("a pane close main starts (a phone's, the CLI's)", () => {
|
||||
})
|
||||
})
|
||||
|
||||
async function closeByPty(harness: CloseContinuityHarness): Promise<unknown> {
|
||||
const terminal = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals.find(
|
||||
(candidate) => candidate.ptyId === PTY_ID
|
||||
)
|
||||
if (!terminal) {
|
||||
throw new Error('fixture pane has no terminal handle')
|
||||
}
|
||||
return harness.runtime.closeTerminal(terminal.handle)
|
||||
}
|
||||
|
||||
/** CLI/headless path: PTY-backed tab without a paired-viewer surface. */
|
||||
function headlessPtyTab(): CloseContinuityHarness {
|
||||
const harness = createHarness({ registerPtyBacked: true })
|
||||
harness.syncFixtureTabWithoutLeaf()
|
||||
harness.setVerifiedStopResult(true)
|
||||
return harness
|
||||
}
|
||||
|
||||
describe('closing a terminal by PTY on a host without a renderer tab', () => {
|
||||
it('stops the PTY without asking a missing renderer to close the tab', async () => {
|
||||
const harness = headlessPtyTab()
|
||||
harness.syncEmptyGraph()
|
||||
|
||||
await expect(closeByPty(harness)).resolves.toMatchObject({ tabId: TAB_ID })
|
||||
|
||||
expect(harness.closeTerminalTab).not.toHaveBeenCalled()
|
||||
expect(harness.stopAndWait).toHaveBeenCalledWith(PTY_ID, expect.anything())
|
||||
})
|
||||
|
||||
it('does not fail the close when the advisory renderer notification throws', async () => {
|
||||
const harness = headlessPtyTab()
|
||||
harness.syncEmptyGraph()
|
||||
harness.closeTerminal.mockImplementation(() => {
|
||||
throw new Error('renderer gone')
|
||||
})
|
||||
|
||||
await expect(closeByPty(harness)).resolves.toMatchObject({ tabId: TAB_ID })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,8 +13,6 @@ import { expectDesktopDropsPane } from '../../__fixtures__/main-layout-close-fix
|
||||
import type { CloseContinuityHarness } from '../../__fixtures__/orca-runtime-terminal-close-continuity-fixtures'
|
||||
import { retireTerminalSurfaceFromPersistence } from './mobile-session-terminal-persistence-retirement'
|
||||
|
||||
const emptyLayout = { root: null, activeLeafId: null, expandedLeafId: null }
|
||||
|
||||
/** All explicit pane-close paths must retain live siblings and send no tab-level close. */
|
||||
|
||||
type Entry = 'renderer' | 'cli-graph' | 'cli-pty' | 'phone-desktop' | 'phone-headless'
|
||||
@@ -279,7 +277,6 @@ const awaitsL255 = (entry: string) => ORCA_LAYOUT_OWNER_IS_MAIN && entry === 'ph
|
||||
|
||||
describe('an explicit close of one pane never widens into its tab', () => {
|
||||
it.each(rows.filter(([entry]) => !awaitsL255(entry)))('%s close, %s', closesOnlyThatPane)
|
||||
it.fails.each(rows.filter(([entry]) => awaitsL255(entry)))('%s close, %s', closesOnlyThatPane)
|
||||
})
|
||||
|
||||
async function closesOnlyThatPane(
|
||||
@@ -317,16 +314,6 @@ async function closesOnlyThatPane(
|
||||
}
|
||||
|
||||
/** Last-pane tab close uses legacy renderer acknowledgement or main-owned layout without renderer notice. */
|
||||
function expectDesktopClosesTab(harness: CloseContinuityHarness): void {
|
||||
expect(harness.closeTerminalPane).not.toHaveBeenCalled()
|
||||
if (!ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
expect(harness.closeTerminalTab.mock.calls.map((call) => call[0])).toEqual([TAB_ID])
|
||||
return
|
||||
}
|
||||
expect(harness.closeTerminalTab).not.toHaveBeenCalled()
|
||||
expect(harness.getSession().tabsByWorktree[WORKTREE_ID]).toEqual([])
|
||||
expect(harness.getSession().terminalLayoutsByTabId[TAB_ID]).toBeUndefined()
|
||||
}
|
||||
|
||||
describe("a close of a tab's last pane still closes the tab", () => {
|
||||
it('on a host with no desktop window, closes the whole tab in main and for paired clients', async () => {
|
||||
@@ -342,52 +329,4 @@ describe("a close of a tab's last pane still closes the tab", () => {
|
||||
expect((await harness.runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)).tabs).toEqual([])
|
||||
expect(harness.kill).toHaveBeenCalledWith(PTY_ID)
|
||||
})
|
||||
|
||||
// Main-owned bind-before-register makes live tabs without saved panes impossible; covered by main tab-close cases.
|
||||
it.skipIf(ORCA_LAYOUT_OWNER_IS_MAIN).each([
|
||||
['no saved layout', () => ({})],
|
||||
['a layout saved before its pane mounted', () => ({ [TAB_ID]: emptyLayout })]
|
||||
] as const)(
|
||||
'on a host with no desktop window, closes an unsplit tab with %s',
|
||||
async (_name, layouts) => {
|
||||
const harness = createHarness({ publishMobileSurface: true, registerPtyBacked: true })
|
||||
harness.runtime.setNotifier(null)
|
||||
harness.syncEmptyGraph()
|
||||
harness.editSession((session) => ({ ...session, terminalLayoutsByTabId: layouts() }))
|
||||
|
||||
await harness.runtime.closeMobileSessionTab(`id:${WORKTREE_ID}`, `${TAB_ID}::${LEAF_ID}`, {
|
||||
reason: 'user'
|
||||
})
|
||||
|
||||
expect(harness.getSession().tabsByWorktree[WORKTREE_ID]).toEqual([])
|
||||
expect(harness.kill).toHaveBeenCalledWith(PTY_ID)
|
||||
}
|
||||
)
|
||||
|
||||
it('from the CLI by PTY, closes an unsplit tab whose renderer graph lists no panes yet', async () => {
|
||||
const harness = createHarness({ publishMobileSurface: true, registerPtyBacked: true })
|
||||
harness.syncFixtureTabWithoutLeaf()
|
||||
harness.setVerifiedStopResult(true)
|
||||
harness.setCloseTerminalTabAction(() => harness.retirePersistedTab())
|
||||
|
||||
await closePane('cli-pty', harness)
|
||||
|
||||
expectDesktopClosesTab(harness)
|
||||
})
|
||||
|
||||
it.each(['phone-desktop', 'cli-graph'] as const)(
|
||||
'from %s, goes through the renderer tab close so its pin guard still runs',
|
||||
async (entry) => {
|
||||
const harness = createHarness({
|
||||
publishMobileSurface: true,
|
||||
registerPtyBacked: entry === 'phone-desktop'
|
||||
})
|
||||
harness.setVerifiedStopResult(true)
|
||||
harness.setCloseTerminalTabAction(() => harness.retirePersistedTab())
|
||||
|
||||
await closePane(entry, harness)
|
||||
|
||||
expectDesktopClosesTab(harness)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { getDefaultWorkspaceSession } from '../../../../shared/constants'
|
||||
import {
|
||||
CLOSED_TERMINAL_TAB_TOMBSTONE_TTL_MS,
|
||||
MAX_CLOSED_TERMINAL_TAB_TOMBSTONES
|
||||
} from '../../../../shared/workspace-layout/closed-terminal-tab-tombstones'
|
||||
|
||||
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
|
||||
import { Store } from '../../../persistence/loading-store/store'
|
||||
import {
|
||||
@@ -23,8 +20,6 @@ import {
|
||||
makeSession
|
||||
} from '../../__fixtures__/orca-runtime-terminal-close-continuity-state-fixture'
|
||||
import { advanceTerminalTopologyRevision } from '../../../persistence/terminal-topology/terminal-topology-membership'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
|
||||
import { RuntimeMachineName } from '../../runtime-machine-name'
|
||||
|
||||
const SSH_REPO_ID = 'ssh-repo'
|
||||
const SSH_HOST_ID = 'ssh:target-1'
|
||||
@@ -106,115 +101,6 @@ describe('close records', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// Store preserves omitted close maps; explicit main-owned maps replace them.
|
||||
it("keeps main's own record writes across later store writes", async () => {
|
||||
const { store, runtime } = createPersistedRuntime()
|
||||
|
||||
await runtime.closeTerminalSurfaceFromRenderer({
|
||||
worktreeId: WORKTREE_ID,
|
||||
target: { kind: 'tab', tabId: TAB_ID }
|
||||
})
|
||||
await runtime.closeTerminalSurfaceFromRenderer({
|
||||
worktreeId: WORKTREE_ID,
|
||||
target: { kind: 'tab', tabId: LATE_TAB_ID }
|
||||
})
|
||||
store.setWorkspaceSession(rendererSave(store.getWorkspaceSession()))
|
||||
|
||||
expect(
|
||||
Object.keys(store.getWorkspaceSession().closedTerminalTabTombstonesByTabId ?? {}).sort()
|
||||
).toEqual([LATE_TAB_ID, TAB_ID].sort())
|
||||
})
|
||||
|
||||
it("keeps a close's first reason when the renderer's echo closes the same tab again", async () => {
|
||||
const { store, runtime } = createPersistedRuntime()
|
||||
|
||||
await runtime.closeTerminalSurfaceFromRenderer({
|
||||
worktreeId: WORKTREE_ID,
|
||||
target: { kind: 'tab', tabId: LATE_TAB_ID },
|
||||
reason: 'cleanup'
|
||||
})
|
||||
const first = store.getWorkspaceSession().closedTerminalTabTombstonesByTabId?.[LATE_TAB_ID]
|
||||
await runtime.closeTerminalSurfaceFromRenderer({
|
||||
worktreeId: WORKTREE_ID,
|
||||
target: { kind: 'tab', tabId: LATE_TAB_ID },
|
||||
reason: 'user'
|
||||
})
|
||||
|
||||
expect(first?.reason).toBe('cleanup')
|
||||
expect(store.getWorkspaceSession().closedTerminalTabTombstonesByTabId?.[LATE_TAB_ID]).toEqual(
|
||||
first
|
||||
)
|
||||
})
|
||||
|
||||
it('drops a listed tab its partition already recorded as closed, and keeps the record', async () => {
|
||||
const earlier = Date.now() - 24 * 60 * 60 * 1000
|
||||
const record = { closedAt: earlier, worktreeId: WORKTREE_ID, reason: 'cleanup' as const }
|
||||
const { store } = createPersistedRuntime({
|
||||
...makeSession(),
|
||||
closedTerminalTabTombstonesByTabId: { [TAB_ID]: record }
|
||||
})
|
||||
|
||||
expect(store.getWorkspaceSession().tabsByWorktree[WORKTREE_ID]).toEqual([])
|
||||
expect(store.getWorkspaceSession().closedTerminalTabTombstonesByTabId?.[TAB_ID]).toEqual(record)
|
||||
})
|
||||
|
||||
it('admits a late spawn for a tab whose close record is past the TTL', async () => {
|
||||
const expired = {
|
||||
[LATE_TAB_ID]: {
|
||||
closedAt: Date.now() - CLOSED_TERMINAL_TAB_TOMBSTONE_TTL_MS - 60_000,
|
||||
worktreeId: WORKTREE_ID,
|
||||
reason: 'user' as const
|
||||
}
|
||||
}
|
||||
const { store } = createPersistedRuntime({
|
||||
...makeSession(),
|
||||
closedTerminalTabTombstonesByTabId: expired
|
||||
})
|
||||
|
||||
expect(
|
||||
await store.persistPtyBinding({
|
||||
worktreeId: WORKTREE_ID,
|
||||
tabId: LATE_TAB_ID,
|
||||
leafId: LEAF_ID,
|
||||
ptyId: 'late-pty',
|
||||
incarnationId: 'late-incarnation'
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('records nothing for a split pane close, which leaves its tab open', async () => {
|
||||
const { store, runtime } = createPersistedRuntime()
|
||||
|
||||
await runtime.closeTerminalSurfaceFromRenderer({
|
||||
worktreeId: WORKTREE_ID,
|
||||
target: {
|
||||
kind: 'pane',
|
||||
tabId: 'unknown-tab',
|
||||
leafId: LEAF_ID
|
||||
}
|
||||
})
|
||||
|
||||
expect(store.getWorkspaceSession().closedTerminalTabTombstonesByTabId).toBeUndefined()
|
||||
})
|
||||
|
||||
it("records nothing for a pane close aimed at its tab's only pane", async () => {
|
||||
const { store, runtime } = createPersistedRuntime()
|
||||
|
||||
await runtime.closeTerminalSurfaceFromRenderer({
|
||||
worktreeId: WORKTREE_ID,
|
||||
target: {
|
||||
kind: 'pane',
|
||||
tabId: TAB_ID,
|
||||
leafId: LEAF_ID
|
||||
}
|
||||
})
|
||||
|
||||
expect(store.getWorkspaceSession().closedTerminalTabTombstonesByTabId).toBeUndefined()
|
||||
expect(store.getWorkspaceSession().tabsByWorktree[WORKTREE_ID]?.map((tab) => tab.id)).toEqual([
|
||||
TAB_ID
|
||||
])
|
||||
})
|
||||
|
||||
// Durable close must reject in-flight spawns committing after crash/reload, even if the tab was never listed.
|
||||
it('refuses a closed tab whose spawn commits after a crash and reload', async () => {
|
||||
const { runtime, reload } = createPersistedRuntime()
|
||||
@@ -268,110 +154,4 @@ describe('close records', () => {
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('prunes each host partition alone, so local churn evicts no SSH record', async () => {
|
||||
const { store, runtime } = createPersistedRuntime()
|
||||
store.setWorkspaceSession(
|
||||
{ ...getDefaultWorkspaceSession(), tabsByWorktree: { [SSH_WORKTREE_ID]: [] } },
|
||||
SSH_HOST_ID
|
||||
)
|
||||
await runtime.closeTerminalSurfaceFromRenderer({
|
||||
worktreeId: SSH_WORKTREE_ID,
|
||||
target: {
|
||||
kind: 'tab',
|
||||
tabId: 'ssh-tab'
|
||||
}
|
||||
})
|
||||
|
||||
for (let index = 0; index <= MAX_CLOSED_TERMINAL_TAB_TOMBSTONES; index += 1) {
|
||||
await runtime.closeTerminalSurfaceFromRenderer({
|
||||
worktreeId: WORKTREE_ID,
|
||||
target: {
|
||||
kind: 'tab',
|
||||
tabId: `local-${index}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.keys(store.getWorkspaceSession().closedTerminalTabTombstonesByTabId ?? {})
|
||||
).toHaveLength(MAX_CLOSED_TERMINAL_TAB_TOMBSTONES)
|
||||
expect(
|
||||
store.getWorkspaceSession(SSH_HOST_ID).closedTerminalTabTombstonesByTabId?.['ssh-tab']
|
||||
).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('terminal resume cleanup characterization', () => {
|
||||
class ResumeRecordsRuntime extends OrcaRuntimeService {
|
||||
clearResumeRecords() {
|
||||
return this['clearWorktreeTerminalResumeRecords']('folder-proof', LOCAL_EXECUTION_HOST_ID, [
|
||||
'closed-tab'
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(RuntimeMachineName.prototype, 'start').mockImplementation(() => {})
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
function persistedRuntime() {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-stop-resume-'))
|
||||
directories.push(directory)
|
||||
const store = createSqliteTestStore(Store, { dataFile: join(directory, 'orca-data.json') })
|
||||
store.setWorkspaceSession({
|
||||
...store.getWorkspaceSession(),
|
||||
terminalPtyIncarnationsByPaneKey: { 'closed-tab:left': 'incarnation-proof' }
|
||||
})
|
||||
store.flushOrThrow()
|
||||
return { store, runtime: new ResumeRecordsRuntime(store) }
|
||||
}
|
||||
|
||||
function removeSessionReader(store: Store) {
|
||||
const original = store.getWorkspaceSession
|
||||
Object.defineProperty(store, 'getWorkspaceSession', { configurable: true, value: undefined })
|
||||
return () => {
|
||||
Object.defineProperty(store, 'getWorkspaceSession', { configurable: true, value: original })
|
||||
}
|
||||
}
|
||||
|
||||
describe('terminal resume cleanup guarded Store accesses', () => {
|
||||
it('removes stale pane incarnations through the real durable Store callback', async () => {
|
||||
const { store, runtime } = persistedRuntime()
|
||||
expect(store.getWorkspaceSession().terminalPtyIncarnationsByPaneKey).toEqual({
|
||||
'closed-tab:left': 'incarnation-proof'
|
||||
})
|
||||
await expect(runtime.clearResumeRecords()).resolves.toBeUndefined()
|
||||
expect(store.getWorkspaceSession().terminalPtyIncarnationsByPaneKey).toEqual({})
|
||||
})
|
||||
|
||||
it('refuses a missing capability before the durable callback', async () => {
|
||||
const { store, runtime } = persistedRuntime()
|
||||
const durable = vi.spyOn(store, 'runDurableMutation')
|
||||
const restore = removeSessionReader(store)
|
||||
try {
|
||||
await expect(runtime.clearResumeRecords()).rejects.toThrow('workspace_session_unavailable')
|
||||
expect(durable).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
restore()
|
||||
}
|
||||
})
|
||||
})
|
||||
it('uses the session reader with its Store receiver and local host partition', async () => {
|
||||
const { store, runtime } = persistedRuntime()
|
||||
const original = store.getWorkspaceSession
|
||||
const reader = vi.spyOn(store, 'getWorkspaceSession').mockImplementation(function (
|
||||
this: Store,
|
||||
...args: Parameters<Store['getWorkspaceSession']>
|
||||
) {
|
||||
expect(this).toBe(store)
|
||||
expect(args).toEqual([LOCAL_EXECUTION_HOST_ID])
|
||||
return original.apply(this, args)
|
||||
})
|
||||
await expect(runtime.clearResumeRecords()).resolves.toBeUndefined()
|
||||
expect(reader).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -98,36 +98,6 @@ export function registerOrcaRuntimeTerminalCloseSpillPartitionTests(): void {
|
||||
}
|
||||
|
||||
describe('terminal close across local and runtime session partitions (#18202)', () => {
|
||||
it('closes a tab persisted only in a runtime spill partition', async () => {
|
||||
const local = sessionWithTab('unrelated', 'unrelated-pty')
|
||||
const sessions = new Map<ExecutionHostId, WorkspaceSessionState>([
|
||||
[LOCAL_EXECUTION_HOST_ID, local],
|
||||
[SPILL_HOST_ID, sessionWithTab('target', 'target-pty')]
|
||||
])
|
||||
const { runtime, kill } = harness(sessions)
|
||||
runtime.registerPty('target-pty', WORKTREE_ID, null, {
|
||||
tabId: 'target',
|
||||
leafId: LEAF,
|
||||
incarnationId: 'target-incarnation'
|
||||
})
|
||||
|
||||
const terminal = (await runtime.listTerminals()).terminals.find(
|
||||
(candidate) => candidate.tabId === 'target'
|
||||
)!
|
||||
await expect(runtime.closeTerminalTab(terminal.handle)).resolves.toMatchObject({
|
||||
tabId: 'target'
|
||||
})
|
||||
|
||||
const spill = sessions.get(SPILL_HOST_ID)!
|
||||
expect(spill.tabsByWorktree[WORKTREE_ID]).toEqual([])
|
||||
expect(spill.terminalLayoutsByTabId.target).toBeUndefined()
|
||||
expect(hasClosedTerminalTabRecord(spill.closedTerminalTabTombstonesByTabId, 'target')).toBe(
|
||||
true
|
||||
)
|
||||
expect(kill).toHaveBeenCalledWith('target-pty')
|
||||
expect(sessions.get(LOCAL_EXECUTION_HOST_ID)).toBe(local)
|
||||
})
|
||||
|
||||
it('closes and records every copy when legacy reattach left one in two partitions', async () => {
|
||||
const sessions = new Map<ExecutionHostId, WorkspaceSessionState>([
|
||||
[LOCAL_EXECUTION_HOST_ID, sessionWithTab('unrelated', 'unrelated-pty')],
|
||||
@@ -195,23 +165,5 @@ export function registerOrcaRuntimeTerminalCloseSpillPartitionTests(): void {
|
||||
expect(setSession).not.toHaveBeenCalled()
|
||||
expect(onClosed).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps the pin guard of the partition that holds the tab', async () => {
|
||||
const spill = sessionWithTab('target', 'target-pty')
|
||||
spill.tabsByWorktree[WORKTREE_ID] = [
|
||||
{ ...terminalTab('target', 'target-pty'), isPinned: true }
|
||||
]
|
||||
const sessions = new Map<ExecutionHostId, WorkspaceSessionState>([
|
||||
[LOCAL_EXECUTION_HOST_ID, sessionWithTab('unrelated', 'unrelated-pty')],
|
||||
[SPILL_HOST_ID, spill]
|
||||
])
|
||||
const { runtime, kill } = harness(sessions)
|
||||
|
||||
await expect(runtime.closeMobileSessionTab(`id:${WORKTREE_ID}`, 'target')).rejects.toThrow(
|
||||
'terminal_tab_pinned'
|
||||
)
|
||||
expect(sessions.get(SPILL_HOST_ID)).toBe(spill)
|
||||
expect(kill).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
-342
@@ -5,7 +5,6 @@ import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../share
|
||||
import type { RuntimeMobileSessionTabsSnapshot } from '../../../../shared/runtime-session-contracts'
|
||||
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
|
||||
import { OrcaRuntimeService } from '../../orca-runtime'
|
||||
import { RuntimeWorkspaceSessionController } from '../../runtime-workspace-session-controller'
|
||||
|
||||
export function registerOrcaRuntimeTerminalRetirementHostPartitionTests(): void {
|
||||
const CONNECTION_ID = 'conn-1'
|
||||
@@ -177,73 +176,6 @@ export function registerOrcaRuntimeTerminalRetirementHostPartitionTests(): void
|
||||
}
|
||||
|
||||
describe('OrcaRuntimeService terminal retirement host partitioning (STA-3463)', () => {
|
||||
it('routes a stale catalog owner to the unique persisted session owner', async () => {
|
||||
const staleHostId: ExecutionHostId = 'runtime:stale-host'
|
||||
const persistedTab = {
|
||||
id: 'tab',
|
||||
ptyId: 'persisted-pty',
|
||||
worktreeId: SSH_WORKTREE_ID,
|
||||
title: 'Terminal',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
const localSession: WorkspaceSessionState = {
|
||||
...getDefaultWorkspaceSession(),
|
||||
tabsByWorktree: { [SSH_WORKTREE_ID]: [persistedTab] },
|
||||
terminalLayoutsByTabId: {
|
||||
tab: {
|
||||
root: { type: 'leaf', leafId: 'leaf' },
|
||||
activeLeafId: 'leaf',
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { leaf: 'persisted-pty' }
|
||||
}
|
||||
}
|
||||
}
|
||||
const sessions = new Map<ExecutionHostId, WorkspaceSessionState>([
|
||||
[LOCAL_EXECUTION_HOST_ID, localSession],
|
||||
[
|
||||
staleHostId,
|
||||
{
|
||||
...getDefaultWorkspaceSession(),
|
||||
tabsByWorktree: { [SSH_WORKTREE_ID]: [] }
|
||||
}
|
||||
]
|
||||
])
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This runtime fixture supplies the persistence and graph methods exercised by the test.
|
||||
const store = withDurableRuntimeStore({
|
||||
getRepos: () => [{ ...SSH_REPO, executionHostId: staleHostId }],
|
||||
getRepo: () => ({ ...SSH_REPO, executionHostId: staleHostId }),
|
||||
getWorktreeMeta: () => undefined,
|
||||
getAllWorktreeMeta: () => ({}),
|
||||
getWorkspaceSessionHostIds: () => [...sessions.keys()],
|
||||
getWorkspaceSession: (hostId?: ExecutionHostId) =>
|
||||
sessions.get(hostId ?? LOCAL_EXECUTION_HOST_ID) ?? getDefaultWorkspaceSession(),
|
||||
setWorkspaceSession: (session: WorkspaceSessionState, hostId?: ExecutionHostId) =>
|
||||
sessions.set(hostId ?? LOCAL_EXECUTION_HOST_ID, session),
|
||||
flushOrThrow: vi.fn()
|
||||
}) as never
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: vi.fn(() => true),
|
||||
getForegroundProcess: async () => null
|
||||
})
|
||||
runtime.registerPty('persisted-pty', SSH_WORKTREE_ID, null, {
|
||||
tabId: 'tab',
|
||||
leafId: 'leaf'
|
||||
})
|
||||
|
||||
await expect(
|
||||
runtime.closeMobileSessionTab(`id:${SSH_WORKTREE_ID}`, 'tab')
|
||||
).resolves.toMatchObject({
|
||||
closed: true
|
||||
})
|
||||
expect(sessions.get(LOCAL_EXECUTION_HOST_ID)?.tabsByWorktree[SSH_WORKTREE_ID]).toEqual([])
|
||||
expect(sessions.get(staleHostId)?.tabsByWorktree[SSH_WORKTREE_ID]).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps a same-id local workspace out of an SSH workspace close', async () => {
|
||||
const localTab = {
|
||||
id: 'local-tab',
|
||||
@@ -330,157 +262,6 @@ export function registerOrcaRuntimeTerminalRetirementHostPartitionTests(): void
|
||||
expect(Object.keys(local.sleepingAgentSessionsByPaneKey ?? {})).toEqual(['local-tab:leaf'])
|
||||
})
|
||||
|
||||
it('clears resume records from the partition that owned the tabs when the catalog owner rotated', async () => {
|
||||
const staleHostId: ExecutionHostId = 'runtime:stale-host'
|
||||
const sessions = new Map<ExecutionHostId, WorkspaceSessionState>([
|
||||
[
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
{
|
||||
...makePersistedSshSession(),
|
||||
terminalPtyIncarnationsByPaneKey: { 'tab:left': 'incarnation-1' }
|
||||
}
|
||||
],
|
||||
[
|
||||
staleHostId,
|
||||
{ ...getDefaultWorkspaceSession(), tabsByWorktree: { [SSH_WORKTREE_ID]: [] } }
|
||||
]
|
||||
])
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This runtime fixture supplies the persistence and graph methods exercised by the test.
|
||||
const store = withDurableRuntimeStore({
|
||||
getRepos: () => [{ ...SSH_REPO, executionHostId: staleHostId }],
|
||||
getRepo: () => ({ ...SSH_REPO, executionHostId: staleHostId }),
|
||||
getWorktreeMeta: () => ({}),
|
||||
getAllWorktreeMeta: () => ({ [SSH_WORKTREE_ID]: {} }),
|
||||
setWorktreeMeta: vi.fn(),
|
||||
getWorkspaceSessionHostIds: () => [...sessions.keys()],
|
||||
getWorkspaceSession: (hostId?: ExecutionHostId) =>
|
||||
sessions.get(hostId ?? LOCAL_EXECUTION_HOST_ID) ?? getDefaultWorkspaceSession(),
|
||||
setWorkspaceSession: (session: WorkspaceSessionState, hostId?: ExecutionHostId) =>
|
||||
sessions.set(hostId ?? LOCAL_EXECUTION_HOST_ID, session),
|
||||
flushOrThrow: vi.fn(),
|
||||
persistPtyBinding: vi.fn()
|
||||
}) as never
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: vi.fn(() => true),
|
||||
stopAndWait: vi.fn(async () => true),
|
||||
getForegroundProcess: async () => null
|
||||
})
|
||||
runtime.attachWindow(1)
|
||||
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
|
||||
runtime.registerPty(SSH_PTY_LEFT, SSH_WORKTREE_ID, null, { tabId: 'tab', leafId: 'left' })
|
||||
|
||||
await expect(
|
||||
runtime.closeTerminalsForWorktree(`id:${SSH_WORKTREE_ID}`)
|
||||
).resolves.toMatchObject({ closed: 1 })
|
||||
expect(sessions.get(LOCAL_EXECUTION_HOST_ID)?.tabsByWorktree[SSH_WORKTREE_ID]).toEqual([])
|
||||
expect(sessions.get(LOCAL_EXECUTION_HOST_ID)?.terminalPtyIncarnationsByPaneKey).toEqual({})
|
||||
})
|
||||
|
||||
it('hydrates the persisted owner when a folder host is absent from the host index', () => {
|
||||
const folderWorktreeId = 'folder:folder-1'
|
||||
const localSession = {
|
||||
...getDefaultWorkspaceSession(),
|
||||
tabsByWorktree: {
|
||||
[folderWorktreeId]: [
|
||||
{
|
||||
id: 'folder-tab',
|
||||
ptyId: null,
|
||||
worktreeId: folderWorktreeId,
|
||||
title: 'Folder'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
const folderHostId: ExecutionHostId = 'runtime:folder-host'
|
||||
const store = {
|
||||
getRepos: () => [],
|
||||
getFolderWorkspaces: () => [
|
||||
{
|
||||
id: 'folder-1',
|
||||
projectGroupId: 'project-1',
|
||||
name: 'Folder',
|
||||
folderPath: '/tmp/folder',
|
||||
executionHostId: folderHostId
|
||||
}
|
||||
],
|
||||
getWorkspaceSessionHostIds: () => [LOCAL_EXECUTION_HOST_ID],
|
||||
getWorkspaceSession: (hostId?: ExecutionHostId) =>
|
||||
hostId === LOCAL_EXECUTION_HOST_ID ? localSession : getDefaultWorkspaceSession()
|
||||
} as never
|
||||
const controller = new RuntimeWorkspaceSessionController({
|
||||
getStore: () => store,
|
||||
resolveFolderConnectionId: () => null,
|
||||
hasRuntimeOwnedPtyCandidate: () => false
|
||||
})
|
||||
|
||||
const targets = controller.getHydrationTargets(true)
|
||||
|
||||
expect(targets.get(folderWorktreeId)).toBe(localSession)
|
||||
})
|
||||
|
||||
it.each<[string, string, Record<string, unknown>, ExecutionHostId]>([
|
||||
['local repo', 'repo::/wt', { id: 'repo' }, LOCAL_EXECUTION_HOST_ID],
|
||||
['SSH repo', 'repo::/wt', { id: 'repo', connectionId: 'c1' }, 'ssh:c1'],
|
||||
['runtime repo', 'repo::/wt', { id: 'repo', executionHostId: 'runtime:e1' }, 'runtime:e1'],
|
||||
['local folder', 'folder:f1', { folderId: 'f1' }, LOCAL_EXECUTION_HOST_ID],
|
||||
['SSH folder', 'folder:f1', { folderId: 'f1', connectionId: 'c1' }, 'ssh:c1'],
|
||||
['explicit SSH folder', 'folder:f1', { folderId: 'f1', executionHostId: 'ssh:c2' }, 'ssh:c2'],
|
||||
[
|
||||
'runtime folder',
|
||||
'folder:f1',
|
||||
{ folderId: 'f1', executionHostId: 'runtime:e1' },
|
||||
'runtime:e1'
|
||||
]
|
||||
])('reads a %s own saved partition', (_label, worktreeId, owner, ownerHostId) => {
|
||||
const listing = (id: string) => ({
|
||||
...getDefaultWorkspaceSession(),
|
||||
tabsByWorktree: {
|
||||
[worktreeId]: [
|
||||
{
|
||||
id,
|
||||
ptyId: null,
|
||||
worktreeId,
|
||||
title: id,
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
const sessions = new Map<ExecutionHostId, WorkspaceSessionState>([
|
||||
[ownerHostId, listing('own')],
|
||||
['runtime:e0', listing('rotated')]
|
||||
])
|
||||
const { folderId, ...repoOrFolder } = owner
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the controller reads only repos, folder workspaces and workspace sessions.
|
||||
const store = {
|
||||
getRepos: () => (folderId ? [] : [repoOrFolder]),
|
||||
getRepo: () => (folderId ? undefined : repoOrFolder),
|
||||
getFolderWorkspaces: () => (folderId ? [{ ...repoOrFolder, id: folderId }] : []),
|
||||
getWorkspaceSessionHostIds: () => [...sessions.keys()],
|
||||
getWorkspaceSession: (hostId: ExecutionHostId) =>
|
||||
sessions.get(hostId) ?? getDefaultWorkspaceSession()
|
||||
} as never
|
||||
const controller = new RuntimeWorkspaceSessionController({
|
||||
getStore: () => store,
|
||||
resolveFolderConnectionId: (workspace) => workspace.connectionId ?? null,
|
||||
hasRuntimeOwnedPtyCandidate: () => false
|
||||
})
|
||||
|
||||
expect(controller.getOwnPartition(worktreeId)).toBe(sessions.get(ownerHostId))
|
||||
expect(controller.get(worktreeId)).toBe(sessions.get(ownerHostId))
|
||||
|
||||
sessions.set(ownerHostId, getDefaultWorkspaceSession())
|
||||
const fellBack = controller.get(worktreeId) === sessions.get('runtime:e0')
|
||||
// Runtime owners may rotate onto another copy but must never mistake it for their own partition.
|
||||
expect(fellBack).toBe(ownerHostId.startsWith('runtime:'))
|
||||
expect(controller.getOwnPartition(worktreeId)?.tabsByWorktree[worktreeId]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('waits for provider retirement on a direct worktree stop', async () => {
|
||||
const harness = partitionedStore()
|
||||
const runtime = new OrcaRuntimeService(harness.store)
|
||||
@@ -587,128 +368,5 @@ export function registerOrcaRuntimeTerminalRetirementHostPartitionTests(): void
|
||||
expect.objectContaining({ id: 'tab::right' })
|
||||
])
|
||||
})
|
||||
|
||||
it('still retires a local pane from the local partition', async () => {
|
||||
const harness = partitionedStore()
|
||||
const sshBefore = harness.sessions.get(SSH_HOST_ID)!
|
||||
const localRepo = {
|
||||
id: 'local-repo',
|
||||
path: '/worktree',
|
||||
displayName: 'local-repo',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1
|
||||
} as const
|
||||
const localWorktreeId = 'local-repo::/worktree'
|
||||
const localSession: WorkspaceSessionState = {
|
||||
...getDefaultWorkspaceSession(),
|
||||
tabsByWorktree: {
|
||||
[localWorktreeId]: [
|
||||
{
|
||||
id: 'tab',
|
||||
ptyId: 'pty-left',
|
||||
worktreeId: localWorktreeId,
|
||||
title: 'Terminal',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
terminalLayoutsByTabId: {
|
||||
tab: {
|
||||
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: null,
|
||||
ptyIdsByLeafId: { left: 'pty-left', right: 'pty-right' }
|
||||
}
|
||||
}
|
||||
}
|
||||
harness.sessions.set(LOCAL_EXECUTION_HOST_ID, localSession)
|
||||
const store = {
|
||||
...(harness.store as object),
|
||||
getRepos: () => [SSH_REPO, localRepo],
|
||||
getRepo: (id: string) =>
|
||||
id === SSH_REPO_ID ? SSH_REPO : id === 'local-repo' ? localRepo : undefined
|
||||
} as never
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
runtime.attachWindow(1)
|
||||
const snapshot = makeSshSnapshot()
|
||||
runtime.syncWindowGraph(1, {
|
||||
tabs: [
|
||||
{
|
||||
tabId: 'tab',
|
||||
worktreeId: localWorktreeId,
|
||||
title: 'Terminal',
|
||||
activeLeafId: 'left',
|
||||
layout:
|
||||
snapshot.tabs[0]?.type === 'terminal'
|
||||
? (snapshot.tabs[0].parentLayout?.root ?? null)
|
||||
: null
|
||||
}
|
||||
],
|
||||
leaves: [
|
||||
{
|
||||
tabId: 'tab',
|
||||
worktreeId: localWorktreeId,
|
||||
leafId: 'left',
|
||||
paneRuntimeId: 1,
|
||||
ptyId: 'pty-left'
|
||||
},
|
||||
{
|
||||
tabId: 'tab',
|
||||
worktreeId: localWorktreeId,
|
||||
leafId: 'right',
|
||||
paneRuntimeId: 2,
|
||||
ptyId: 'pty-right'
|
||||
}
|
||||
],
|
||||
mobileSessionTabs: [
|
||||
{
|
||||
...snapshot,
|
||||
worktree: localWorktreeId,
|
||||
tabs: snapshot.tabs.map((tab) =>
|
||||
tab.type === 'terminal'
|
||||
? {
|
||||
...tab,
|
||||
ptyId: tab.leafId === 'left' ? 'pty-left' : 'pty-right',
|
||||
parentLayout: tab.parentLayout
|
||||
? {
|
||||
...tab.parentLayout,
|
||||
ptyIdsByLeafId: { left: 'pty-left', right: 'pty-right' }
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
: tab
|
||||
)
|
||||
}
|
||||
]
|
||||
})
|
||||
runtime.registerPty('pty-left', localWorktreeId, null, {
|
||||
tabId: 'tab',
|
||||
leafId: 'left',
|
||||
incarnationId: 'incarnation-a'
|
||||
})
|
||||
|
||||
await runtime.onPtyExit('pty-left', 0, 'incarnation-a')
|
||||
|
||||
expect(harness.writes.map((write) => write.hostId ?? LOCAL_EXECUTION_HOST_ID)).toEqual([
|
||||
LOCAL_EXECUTION_HOST_ID
|
||||
])
|
||||
expect(
|
||||
harness.sessions.get(LOCAL_EXECUTION_HOST_ID)?.terminalLayoutsByTabId.tab
|
||||
).toMatchObject({
|
||||
root: { type: 'leaf', leafId: 'right' }
|
||||
})
|
||||
expect(harness.sessions.get(SSH_HOST_ID)).toBe(sshBefore)
|
||||
expect((await runtime.listMobileSessionTabs(`id:${localWorktreeId}`)).tabs).toEqual([
|
||||
expect.objectContaining({ id: 'tab::right' })
|
||||
])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { withDurableRuntimeStore } from '../../runtime-durable-store-fixture'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { getDefaultWorkspaceSession } from '../../../../shared/constants'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
|
||||
|
||||
import type {
|
||||
RuntimeMobileSessionTabsResult,
|
||||
RuntimeMobileSessionTabsSnapshot
|
||||
} from '../../../../shared/runtime-session-contracts'
|
||||
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
|
||||
import { sanitizeWorkspaceSessionTerminalRetirements } from './mobile-session-terminal-persistence-retirement'
|
||||
|
||||
import { OrcaRuntimeService } from '../../orca-runtime'
|
||||
|
||||
const WORKTREE_ID = 'repo::/worktree'
|
||||
@@ -147,42 +147,6 @@ function makePersistedSplitSession(): WorkspaceSessionState {
|
||||
}
|
||||
|
||||
describe('OrcaRuntimeService terminal surface retirement', () => {
|
||||
it('releases each early-exit fence after its matching registration is rejected', async () => {
|
||||
const runtime = new OrcaRuntimeService()
|
||||
const internals = runtime as unknown as {
|
||||
pendingPtyRegistrationIncarnations: Map<string, string | null>
|
||||
}
|
||||
|
||||
for (let index = 0; index < 1_000; index += 1) {
|
||||
const ptyId = `pty-early-${index}`
|
||||
const incarnationId = `incarnation-${index}`
|
||||
runtime.beginPtyRegistration(ptyId, incarnationId)
|
||||
await runtime.onPtyExit(ptyId, 0, incarnationId)
|
||||
expect(() => runtime.assertPtyRegistrationAllowed(ptyId, incarnationId)).toThrow(
|
||||
'agent_session_exited_during_start'
|
||||
)
|
||||
runtime.releaseRejectedPtyRegistrationFence(ptyId, incarnationId)
|
||||
}
|
||||
|
||||
expect(internals.pendingPtyRegistrationIncarnations.size).toBe(0)
|
||||
})
|
||||
|
||||
it('does not retain fences for completed surface-less lifecycles', async () => {
|
||||
const runtime = new OrcaRuntimeService()
|
||||
const internals = runtime as unknown as {
|
||||
pendingPtyRegistrationIncarnations: Map<string, string | null>
|
||||
}
|
||||
|
||||
for (let index = 0; index < 1_000; index += 1) {
|
||||
runtime.onPtySpawned(`pty-headless-${index}`, `incarnation-${index}`, {
|
||||
awaitsRegistration: false
|
||||
})
|
||||
await runtime.onPtyExit(`pty-headless-${index}`, 0, `incarnation-${index}`)
|
||||
}
|
||||
|
||||
expect(internals.pendingPtyRegistrationIncarnations.size).toBe(0)
|
||||
})
|
||||
|
||||
it('fences an early-exited replacement even when its pane already exists', async () => {
|
||||
const runtime = new OrcaRuntimeService()
|
||||
runtime.attachWindow(1)
|
||||
@@ -253,235 +217,6 @@ describe('OrcaRuntimeService terminal surface retirement', () => {
|
||||
expect(afterStaleFrame.tabs.map((tab) => tab.id)).toEqual(['tab::right'])
|
||||
})
|
||||
|
||||
it('rejects one stale shared-PTY surface without removing its live sibling', () => {
|
||||
const session = makePersistedSplitSession()
|
||||
session.tabsByWorktree[WORKTREE_ID]![0]!.ptyId = 'pty-shared'
|
||||
session.terminalLayoutsByTabId.tab = {
|
||||
root: { type: 'leaf', leafId: 'right' },
|
||||
activeLeafId: 'right',
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { right: 'pty-shared' }
|
||||
}
|
||||
session.terminalPtyIncarnationsByPaneKey = { 'tab:right': 'incarnation-current' }
|
||||
session.terminalTopologyRevisionByRepoId = { [REPO_ID]: 1 }
|
||||
const runtime = new OrcaRuntimeService(runtimeStore({ getWorkspaceSession: () => session }))
|
||||
runtime.attachWindow(1)
|
||||
runtime.registerPty('pty-shared', WORKTREE_ID, null, {
|
||||
tabId: 'tab',
|
||||
leafId: 'right',
|
||||
incarnationId: 'incarnation-current'
|
||||
})
|
||||
const snapshot = makeSplitSnapshot()
|
||||
const incoming = {
|
||||
...snapshot,
|
||||
tabs: snapshot.tabs.map((tab) =>
|
||||
tab.type === 'terminal'
|
||||
? {
|
||||
...tab,
|
||||
ptyId: 'pty-shared',
|
||||
parentLayout: tab.parentLayout
|
||||
? {
|
||||
...tab.parentLayout,
|
||||
ptyIdsByLeafId: { left: 'pty-shared', right: 'pty-shared' }
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
: tab
|
||||
)
|
||||
}
|
||||
type IncomingTerminalTab = Extract<(typeof incoming.tabs)[number], { type: 'terminal' }>
|
||||
const rightTab = incoming.tabs.find(
|
||||
(tab): tab is IncomingTerminalTab => tab.type === 'terminal' && tab.leafId === 'right'
|
||||
)!
|
||||
const hostSnapshot: RuntimeMobileSessionTabsSnapshot = {
|
||||
...incoming,
|
||||
activeTabId: rightTab.id,
|
||||
tabs: [
|
||||
{
|
||||
...rightTab,
|
||||
parentLayout: {
|
||||
root: { type: 'leaf', leafId: 'right' },
|
||||
activeLeafId: 'right',
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { right: 'pty-shared' }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
runtime.syncWindowGraph(1, {
|
||||
tabs: [
|
||||
{
|
||||
tabId: 'tab',
|
||||
worktreeId: WORKTREE_ID,
|
||||
title: 'Terminal',
|
||||
activeLeafId: 'right',
|
||||
layout: { type: 'leaf', leafId: 'right' }
|
||||
}
|
||||
],
|
||||
leaves: [
|
||||
{
|
||||
tabId: 'tab',
|
||||
worktreeId: WORKTREE_ID,
|
||||
leafId: 'right',
|
||||
paneRuntimeId: 2,
|
||||
ptyId: 'pty-shared'
|
||||
}
|
||||
],
|
||||
mobileSessionTabs: [hostSnapshot]
|
||||
})
|
||||
;(
|
||||
runtime as unknown as {
|
||||
mobileSessionTabsByWorktree: Map<string, RuntimeMobileSessionTabsSnapshot>
|
||||
}
|
||||
).mobileSessionTabsByWorktree.set(WORKTREE_ID, hostSnapshot)
|
||||
|
||||
runtime.syncWindowGraph(1, {
|
||||
tabs: [
|
||||
{
|
||||
tabId: 'tab',
|
||||
worktreeId: WORKTREE_ID,
|
||||
title: 'Terminal',
|
||||
activeLeafId: 'right',
|
||||
layout: incoming.tabs[0]?.type === 'terminal' ? incoming.tabs[0].parentLayout!.root : null
|
||||
}
|
||||
],
|
||||
leaves: [
|
||||
{
|
||||
tabId: 'tab',
|
||||
worktreeId: WORKTREE_ID,
|
||||
leafId: 'left',
|
||||
paneRuntimeId: 1,
|
||||
ptyId: 'pty-shared'
|
||||
},
|
||||
{
|
||||
tabId: 'tab',
|
||||
worktreeId: WORKTREE_ID,
|
||||
leafId: 'right',
|
||||
paneRuntimeId: 2,
|
||||
ptyId: 'pty-shared'
|
||||
}
|
||||
],
|
||||
mobileSessionTabs: [incoming]
|
||||
})
|
||||
|
||||
const internalSnapshot = (
|
||||
runtime as unknown as {
|
||||
mobileSessionTabsByWorktree: Map<string, RuntimeMobileSessionTabsSnapshot>
|
||||
}
|
||||
).mobileSessionTabsByWorktree.get(WORKTREE_ID)
|
||||
expect(internalSnapshot?.tabs).toEqual([
|
||||
expect.objectContaining({ id: 'tab::right', ptyId: 'pty-shared' })
|
||||
])
|
||||
})
|
||||
|
||||
it('honors a legacy persisted tombstone before its first migration write', async () => {
|
||||
const session = makePersistedSplitSession()
|
||||
session.tabsByWorktree[WORKTREE_ID]![0]!.ptyId = 'pty-right'
|
||||
session.terminalLayoutsByTabId.tab = {
|
||||
root: { type: 'leaf', leafId: 'right' },
|
||||
activeLeafId: 'right',
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { right: 'pty-right' }
|
||||
}
|
||||
Object.assign(session, {
|
||||
terminalSurfaceTombstonesByPaneKey: {
|
||||
'tab:left': {
|
||||
worktreeId: WORKTREE_ID,
|
||||
parentTabId: 'tab',
|
||||
leafId: 'left',
|
||||
ptyId: 'pty-left',
|
||||
incarnationId: 'incarnation-left',
|
||||
retiredAt: 42
|
||||
}
|
||||
}
|
||||
})
|
||||
const runtime = new OrcaRuntimeService(runtimeStore({ getWorkspaceSession: () => session }))
|
||||
runtime.attachWindow(1)
|
||||
runtime.registerPty('pty-right', WORKTREE_ID, null, {
|
||||
tabId: 'tab',
|
||||
leafId: 'right',
|
||||
incarnationId: 'incarnation-right'
|
||||
})
|
||||
|
||||
syncSplit(runtime)
|
||||
|
||||
const tabs = (await runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)).tabs
|
||||
expect(tabs).toHaveLength(1)
|
||||
expect(tabs[0]).toMatchObject({ type: 'terminal', ptyId: 'pty-right' })
|
||||
})
|
||||
|
||||
it('publishes the host-rebased layout after a stale client pane update', async () => {
|
||||
let session = makePersistedSplitSession()
|
||||
session.tabsByWorktree[WORKTREE_ID]![0]!.ptyId = 'pty-right'
|
||||
session.terminalLayoutsByTabId.tab = {
|
||||
root: { type: 'leaf', leafId: 'right' },
|
||||
activeLeafId: 'right',
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { right: 'pty-right' }
|
||||
}
|
||||
Object.assign(session, {
|
||||
terminalTopologyRevisionByRepoId: { [REPO_ID]: 1 }
|
||||
})
|
||||
const runtime = new OrcaRuntimeService(
|
||||
runtimeStore({
|
||||
getWorkspaceSession: () => session,
|
||||
setWorkspaceSession: (incoming: WorkspaceSessionState) => {
|
||||
session = sanitizeWorkspaceSessionTerminalRetirements(incoming, session)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)
|
||||
await runtime.updateMobileSessionPaneLayout(`id:${WORKTREE_ID}`, {
|
||||
tabId: 'tab',
|
||||
root: {
|
||||
type: 'split',
|
||||
direction: 'vertical',
|
||||
first: { type: 'leaf', leafId: 'left' },
|
||||
second: { type: 'leaf', leafId: 'right' }
|
||||
},
|
||||
expandedLeafId: null,
|
||||
titlesByLeafId: { right: 'Survivor' }
|
||||
})
|
||||
|
||||
const tabs = (await runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)).tabs
|
||||
expect(tabs).toHaveLength(1)
|
||||
expect(tabs[0]).toMatchObject({
|
||||
type: 'terminal',
|
||||
ptyId: 'pty-right',
|
||||
parentLayout: {
|
||||
root: { type: 'leaf', leafId: 'right' }
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('retires a permanently exited surface despite a stale sleeping record', async () => {
|
||||
const session = {
|
||||
...getDefaultWorkspaceSession(),
|
||||
sleepingAgentSessionsByPaneKey: { 'tab:left': {} as never }
|
||||
}
|
||||
const runtime = new OrcaRuntimeService(
|
||||
runtimeStore(
|
||||
withDurableRuntimeStore({
|
||||
getWorkspaceSession: () => session,
|
||||
setWorkspaceSession: vi.fn(),
|
||||
flushOrThrow: vi.fn()
|
||||
})
|
||||
)
|
||||
)
|
||||
runtime.attachWindow(1)
|
||||
syncSplit(runtime)
|
||||
|
||||
await runtime.onPtyExit('pty-left', 0)
|
||||
|
||||
const result = await runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)
|
||||
expect(result.tabs.find((tab) => tab.id === 'tab::left')).toBeUndefined()
|
||||
expect(result.tabs.find((tab) => tab.id === 'tab::right')).toMatchObject({
|
||||
status: 'ready'
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores a delayed exit from an older incarnation of a reused PTY id', async () => {
|
||||
const setWorkspaceSession = vi.fn()
|
||||
const runtime = new OrcaRuntimeService(
|
||||
@@ -512,45 +247,6 @@ describe('OrcaRuntimeService terminal surface retirement', () => {
|
||||
expect(setWorkspaceSession).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('retires a durable surface after reconnect proves a newer incarnation', async () => {
|
||||
const session = makePersistedSplitSession()
|
||||
const setWorkspaceSession = vi.fn()
|
||||
const runtime = new OrcaRuntimeService(
|
||||
runtimeStore(
|
||||
withDurableRuntimeStore({
|
||||
getWorkspaceSession: () => session,
|
||||
setWorkspaceSession,
|
||||
flushOrThrow: vi.fn()
|
||||
})
|
||||
)
|
||||
)
|
||||
runtime.attachWindow(1)
|
||||
syncSplit(runtime)
|
||||
runtime.registerPty('pty-left', WORKTREE_ID, null, {
|
||||
tabId: 'tab',
|
||||
leafId: 'left',
|
||||
incarnationId: 'incarnation-before-reconnect'
|
||||
})
|
||||
|
||||
runtime.acceptPtyIncarnationForExit('pty-left', 'incarnation-after-reconnect')
|
||||
await runtime.onPtyExit('pty-left', 0, 'incarnation-after-reconnect')
|
||||
|
||||
expect((await runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)).tabs).toEqual([
|
||||
expect.objectContaining({ id: 'tab::right', status: 'ready' })
|
||||
])
|
||||
expect(setWorkspaceSession).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
terminalLayoutsByTabId: {
|
||||
tab: expect.objectContaining({
|
||||
root: { type: 'leaf', leafId: 'right' },
|
||||
ptyIdsByLeafId: { right: 'pty-right' }
|
||||
})
|
||||
}
|
||||
}),
|
||||
LOCAL_EXECUTION_HOST_ID
|
||||
)
|
||||
})
|
||||
|
||||
it('publishes only same-repo retirements individually accepted by persistence', async () => {
|
||||
let session = makePersistedSplitSession()
|
||||
session.terminalLayoutsByTabId.tab.ptyIdsByLeafId = {
|
||||
@@ -627,102 +323,6 @@ describe('OrcaRuntimeService terminal surface retirement', () => {
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('de-persists an exact surface even when there is no mobile snapshot', async () => {
|
||||
const session = makePersistedSplitSession()
|
||||
const setWorkspaceSession = vi.fn()
|
||||
const flushOrThrow = vi.fn()
|
||||
const runtime = new OrcaRuntimeService(
|
||||
runtimeStore(
|
||||
withDurableRuntimeStore({
|
||||
getWorkspaceSession: () => session,
|
||||
setWorkspaceSession,
|
||||
flushOrThrow
|
||||
})
|
||||
)
|
||||
)
|
||||
runtime.attachWindow(1)
|
||||
runtime.syncWindowGraph(1, {
|
||||
tabs: [
|
||||
{
|
||||
tabId: 'tab',
|
||||
worktreeId: WORKTREE_ID,
|
||||
title: 'Terminal',
|
||||
activeLeafId: 'left',
|
||||
layout: { type: 'leaf', leafId: 'left' }
|
||||
}
|
||||
],
|
||||
leaves: [
|
||||
{
|
||||
tabId: 'tab',
|
||||
worktreeId: WORKTREE_ID,
|
||||
leafId: 'left',
|
||||
paneRuntimeId: 1,
|
||||
ptyId: 'pty-left'
|
||||
}
|
||||
]
|
||||
})
|
||||
runtime.registerPty('pty-left', WORKTREE_ID, null, {
|
||||
tabId: 'tab',
|
||||
leafId: 'left',
|
||||
incarnationId: 'incarnation-a'
|
||||
})
|
||||
|
||||
await runtime.onPtyExit('pty-left', 0, 'incarnation-a')
|
||||
|
||||
expect(setWorkspaceSession).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
terminalLayoutsByTabId: {
|
||||
tab: expect.objectContaining({
|
||||
root: { type: 'leaf', leafId: 'right' },
|
||||
ptyIdsByLeafId: { right: 'pty-right' }
|
||||
})
|
||||
},
|
||||
terminalSurfaceTombstonesByPaneKey: {},
|
||||
terminalTopologyRevisionByRepoId: { [REPO_ID]: 1 }
|
||||
}),
|
||||
LOCAL_EXECUTION_HOST_ID
|
||||
)
|
||||
expect(flushOrThrow).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('publishes absence and reports when the durable retirement flush fails', async () => {
|
||||
const session = makePersistedSplitSession()
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
|
||||
const runtime = new OrcaRuntimeService(
|
||||
runtimeStore(
|
||||
withDurableRuntimeStore({
|
||||
getWorkspaceSession: () => session,
|
||||
setWorkspaceSession: vi.fn(),
|
||||
flushOrThrow: vi.fn(() => {
|
||||
throw new Error('disk unavailable')
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
runtime.attachWindow(1)
|
||||
syncSplit(runtime)
|
||||
runtime.registerPty('pty-left', WORKTREE_ID, null, {
|
||||
tabId: 'tab',
|
||||
leafId: 'left',
|
||||
incarnationId: 'incarnation-a'
|
||||
})
|
||||
const events: unknown[] = []
|
||||
const unsubscribe = runtime.onMobileSessionTabsChanged((event) => events.push(event))
|
||||
|
||||
await runtime.onPtyExit('pty-left', 0, 'incarnation-a')
|
||||
|
||||
expect((await runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)).tabs).toEqual([
|
||||
expect.objectContaining({ id: 'tab::right' })
|
||||
])
|
||||
expect(events).not.toEqual([])
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'[runtime] terminal retirement is not yet durable:',
|
||||
expect.any(Error)
|
||||
)
|
||||
unsubscribe()
|
||||
errorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('keeps the in-memory retirement when the durable flush fails', async () => {
|
||||
let session = makePersistedSplitSession()
|
||||
const original = structuredClone(session)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
|
||||
|
||||
import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
|
||||
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
|
||||
import { Store } from '../../../persistence/loading-store/store'
|
||||
@@ -183,52 +183,6 @@ describe('renderer close intents', () => {
|
||||
})
|
||||
|
||||
describe('CLI close of one pane in a split tab', () => {
|
||||
it('commits the pane removal without waiting for its process exit', async () => {
|
||||
const harness = createHarness()
|
||||
harness.syncSplitFixtureGraph()
|
||||
// Verified stop can lack an exit event; retirement must not depend on it.
|
||||
harness.setVerifiedStopResult(true)
|
||||
const terminal = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals.find(
|
||||
(candidate) => candidate.ptyId === PTY_ID
|
||||
)!
|
||||
|
||||
await expect(harness.runtime.closeTerminal(terminal.handle)).resolves.toMatchObject({
|
||||
tabId: TAB_ID,
|
||||
ptyKilled: true
|
||||
})
|
||||
|
||||
const session = harness.getSession()
|
||||
expect(session.tabsByWorktree[WORKTREE_ID]).toEqual([expect.objectContaining({ id: TAB_ID })])
|
||||
expect(session.terminalLayoutsByTabId[TAB_ID]).toMatchObject({
|
||||
root: { type: 'leaf', leafId: SIBLING_LEAF_ID },
|
||||
ptyIdsByLeafId: { [SIBLING_LEAF_ID]: SIBLING_PTY_ID }
|
||||
})
|
||||
expectDesktopDropsPane(harness, TAB_ID, LEAF_ID)
|
||||
expect(harness.closeTerminalTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('commits the pane removal when the handle names a live PTY', async () => {
|
||||
const harness = createHarness({ publishMobileSurface: true, registerPtyBacked: true })
|
||||
harness.syncSplitFixtureGraph()
|
||||
harness.syncFixtureTabWithoutLeaf()
|
||||
harness.setVerifiedStopResult(true)
|
||||
const terminal = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals.find(
|
||||
(candidate) => candidate.ptyId === PTY_ID
|
||||
)!
|
||||
|
||||
await expect(harness.runtime.closeTerminal(terminal.handle)).resolves.toMatchObject({
|
||||
tabId: TAB_ID,
|
||||
ptyKilled: true
|
||||
})
|
||||
|
||||
expect(harness.getSession().terminalLayoutsByTabId[TAB_ID]?.root).toEqual({
|
||||
type: 'leaf',
|
||||
leafId: SIBLING_LEAF_ID
|
||||
})
|
||||
expectDesktopDropsPane(harness, TAB_ID, LEAF_ID)
|
||||
expect(harness.closeTerminalTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['reports no exit', false],
|
||||
['throws, as an unreachable SSH host does', new Error('ssh_host_unreachable')]
|
||||
@@ -270,33 +224,6 @@ describe('CLI close of one pane in a split tab', () => {
|
||||
)
|
||||
|
||||
// L255: expected main-owned headless pane-close failure until its fix lands.
|
||||
;(ORCA_LAYOUT_OWNER_IS_MAIN ? it.fails : it)(
|
||||
'drops only that pane from paired clients on a host with no renderer listing the tab',
|
||||
async () => {
|
||||
const harness = createHarness({ publishMobileSurface: true, registerPtyBacked: true })
|
||||
harness.syncSplitFixtureGraph()
|
||||
harness.syncFixtureTabWithoutLeaf()
|
||||
const terminal = (await harness.runtime.listTerminals(`id:${WORKTREE_ID}`)).terminals.find(
|
||||
(candidate) => candidate.ptyId === PTY_ID
|
||||
)!
|
||||
harness.syncEmptyGraph()
|
||||
harness.setVerifiedStopResult(new Error('ssh_host_unreachable'))
|
||||
|
||||
await harness.runtime.closeTerminal(terminal.handle)
|
||||
|
||||
const snapshot = await harness.runtime.listMobileSessionTabs(`id:${WORKTREE_ID}`)
|
||||
expect(snapshot.tabs).toEqual([
|
||||
expect.objectContaining({ parentTabId: TAB_ID, leafId: SIBLING_LEAF_ID })
|
||||
])
|
||||
expect(snapshot.retiredTerminalSurfaces).toEqual([
|
||||
expect.objectContaining({ parentTabId: TAB_ID, leafId: LEAF_ID, ptyId: PTY_ID })
|
||||
])
|
||||
expect(harness.getSession().terminalLayoutsByTabId[TAB_ID]?.root).toEqual({
|
||||
type: 'leaf',
|
||||
leafId: SIBLING_LEAF_ID
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps the sibling when the stop delivers the exit before the pane commit', async () => {
|
||||
const harness = createHarness()
|
||||
@@ -317,56 +244,3 @@ describe('CLI close of one pane in a split tab', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mobile close of one pane in a split tab', () => {
|
||||
it('commits the pane removal alongside its kill', async () => {
|
||||
const harness = createHarness({ publishMobileSurface: true, registerPtyBacked: true })
|
||||
harness.syncSplitFixtureGraph()
|
||||
|
||||
await expect(
|
||||
harness.runtime.closeMobileSessionTab(`id:${WORKTREE_ID}`, `${TAB_ID}::${LEAF_ID}`, {
|
||||
reason: 'user'
|
||||
})
|
||||
).resolves.toMatchObject({ closed: true })
|
||||
|
||||
expect(harness.kill).toHaveBeenCalledWith(PTY_ID)
|
||||
expect(harness.getSession().terminalLayoutsByTabId[TAB_ID]).toMatchObject({
|
||||
root: { type: 'leaf', leafId: SIBLING_LEAF_ID }
|
||||
})
|
||||
expectDesktopDropsPane(harness, TAB_ID, LEAF_ID)
|
||||
})
|
||||
})
|
||||
|
||||
describe('mobile close of a tab the desktop renderer lists', () => {
|
||||
// Legacy renderer owns pin presentation and must be allowed to refuse close.
|
||||
it('still asks the renderer, whose pin guard can refuse it', async () => {
|
||||
const harness = createHarness({ publishMobileSurface: true, registerPtyBacked: true })
|
||||
harness.rejectTerminalTabClose(new Error('terminal_tab_pinned'))
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
// Main-owned layout has its own pin guard, requiring no renderer acknowledgement.
|
||||
harness.editSession((session) => ({
|
||||
...session,
|
||||
tabsByWorktree: {
|
||||
[WORKTREE_ID]: (session.tabsByWorktree[WORKTREE_ID] ?? []).map((tab) => ({
|
||||
...tab,
|
||||
isPinned: true
|
||||
}))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
await expect(
|
||||
harness.runtime.closeMobileSessionTab(`id:${WORKTREE_ID}`, TAB_ID, { reason: 'user' })
|
||||
).rejects.toThrow('terminal_tab_pinned')
|
||||
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
expect(harness.closeTerminalTab).not.toHaveBeenCalled()
|
||||
} else {
|
||||
expect(harness.closeTerminalTab).toHaveBeenCalledWith(TAB_ID)
|
||||
}
|
||||
expect(harness.kill).not.toHaveBeenCalled()
|
||||
expect(harness.getSession().tabsByWorktree[WORKTREE_ID]).toEqual([
|
||||
expect.objectContaining({ id: TAB_ID })
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import '../../orca-runtime-test-lifecycle.spec'
|
||||
import { getDefaultWorkspaceSession } from '../../../../shared/constants'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createMobileCreateTestNotifier } from '../../orca-runtime-test-scenario-builders.spec'
|
||||
|
||||
import { OrcaRuntimeService } from '../../orca-runtime-test-mocks.spec'
|
||||
import {
|
||||
HEADLESS_LEAF_ID,
|
||||
@@ -10,12 +10,11 @@ import {
|
||||
makeRuntimeStoreWithWorkspaceSession,
|
||||
makeWorkspaceSessionWithHeadlessTerminal
|
||||
} from '../../orca-runtime-test-fixtures.spec'
|
||||
import type { RuntimeNotifier } from '../../runtime-notifier-contract'
|
||||
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
|
||||
import { bindPaneFirst, committingSpawn } from '../../bind-first-pane.test-fixture'
|
||||
import { committingSpawn } from '../../bind-first-pane.test-fixture'
|
||||
import { buildRuntimeMobileAgentStatus } from '../../runtime-mobile-agent-status-builder'
|
||||
import type { RuntimePtyWorktreeRecord } from '../records/runtime-terminal-state-records'
|
||||
import { partialRuntimeStore } from '../../runtime-store-partial-fixture'
|
||||
|
||||
describe('terminal rename before renderer graph hydration', () => {
|
||||
it.each(['Media Engine Orch', null])(
|
||||
@@ -72,78 +71,9 @@ describe('terminal rename before renderer graph hydration', () => {
|
||||
expect(getSession().tabsByWorktree[TEST_WORKTREE_ID][0].customTitle).toBe(title)
|
||||
}
|
||||
)
|
||||
it('does not recreate a closed persisted tab from a surviving PTY record', async () => {
|
||||
const session = makeWorkspaceSessionWithHeadlessTerminal()
|
||||
const { runtimeStore, getSession, setSession } = makeRuntimeStoreWithWorkspaceSession(session)
|
||||
const runtime = new OrcaRuntimeService(runtimeStore)
|
||||
const notifier = createMobileCreateTestNotifier(vi.fn())
|
||||
runtime.setNotifier(notifier)
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn(committingSpawn(runtime, async () => ({ id: 'surviving-pty' }))),
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null
|
||||
})
|
||||
const created = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, {
|
||||
tabId: 'host-tab',
|
||||
leafId: HEADLESS_LEAF_ID
|
||||
})
|
||||
setSession({ ...getSession(), tabsByWorktree: { [TEST_WORKTREE_ID]: [] } })
|
||||
runtimeStore.setWorkspaceSession.mockClear()
|
||||
|
||||
await runtime.renameTerminal(created.handle, 'Late rename')
|
||||
|
||||
expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([])
|
||||
expect(runtimeStore.setWorkspaceSession).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('terminal rename display', () => {
|
||||
it('reveals a renamed background terminal under its name', async () => {
|
||||
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(getDefaultWorkspaceSession())
|
||||
const runtime = new OrcaRuntimeService(runtimeStore)
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn(async () => ({ id: 'background-pty' })),
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null
|
||||
})
|
||||
const notifier = createMobileCreateTestNotifier(vi.fn())
|
||||
runtime.setNotifier(notifier)
|
||||
const created = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, {
|
||||
tabId: 'host-tab',
|
||||
leafId: HEADLESS_LEAF_ID,
|
||||
presentation: 'background'
|
||||
})
|
||||
runtime.onPtyData('background-pty', '\x1b]0;bash\x07', Date.now())
|
||||
|
||||
await runtime.renameTerminal(created.handle, 'Review worker')
|
||||
await runtime.focusTerminal(created.handle)
|
||||
|
||||
expect(notifier.revealTerminalSession.mock.calls.at(-1)?.[1]).toMatchObject({
|
||||
title: 'Review worker'
|
||||
})
|
||||
})
|
||||
|
||||
it('shows an orphan terminal under its new name', async () => {
|
||||
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(getDefaultWorkspaceSession())
|
||||
const runtime = new OrcaRuntimeService(runtimeStore)
|
||||
runtime.registerPty('orphan-pty', TEST_WORKTREE_ID)
|
||||
runtime.onPtyData('orphan-pty', '\x1b]0;bash\x07', Date.now())
|
||||
const orphan = (await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`)).terminals.find(
|
||||
(terminal) => terminal.ptyId === 'orphan-pty'
|
||||
)
|
||||
if (!orphan?.orphaned) {
|
||||
throw new Error('expected an orphan terminal')
|
||||
}
|
||||
|
||||
await runtime.renameTerminal(orphan.handle, 'Recovered worker')
|
||||
|
||||
await expect(runtime.showTerminal(orphan.handle)).resolves.toMatchObject({
|
||||
title: 'Recovered worker'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps an orphan terminal name once a tab adopts it', async () => {
|
||||
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession({
|
||||
...getDefaultWorkspaceSession(),
|
||||
@@ -195,111 +125,6 @@ describe('terminal rename display', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('terminal view rename characterization', () => {
|
||||
function notifier(renameTerminal: RuntimeNotifier['renameTerminal']): RuntimeNotifier {
|
||||
return {
|
||||
worktreesChanged() {},
|
||||
reposChanged() {},
|
||||
activateWorktree() {},
|
||||
createTerminal() {},
|
||||
splitTerminal() {},
|
||||
renameTerminal,
|
||||
focusTerminal() {},
|
||||
closeTerminal() {},
|
||||
terminalFitOverrideChanged() {},
|
||||
terminalDriverChanged() {}
|
||||
}
|
||||
}
|
||||
it('keeps rename persistence before the current notifier with its own receiver', async () => {
|
||||
// Switch-on, main's layout says which tab holds the terminal; that layout lives in a store.
|
||||
const runtime = new OrcaRuntimeService(
|
||||
ORCA_LAYOUT_OWNER_IS_MAIN ? partialRuntimeStore() : undefined
|
||||
)
|
||||
bindPaneFirst(runtime, {
|
||||
worktreeId: 'folder-workspace',
|
||||
tabId: 'host-tab',
|
||||
leafId: '00000000-0000-4000-8000-000000000002',
|
||||
ptyId: 'rename-pty'
|
||||
})
|
||||
const pty = runtime['recordPtyWorktree']('rename-pty', 'folder-workspace', {
|
||||
tabId: 'host-tab'
|
||||
})
|
||||
const handle = runtime['issuePtyHandle'](pty)
|
||||
const receivers: OrcaRuntimeService[] = []
|
||||
const events: string[] = []
|
||||
const replacement = notifier(function (this: RuntimeNotifier, tabId, title) {
|
||||
expect(this).toBe(replacement)
|
||||
expect([tabId, title]).toEqual(['host-tab', 'Pinned'])
|
||||
events.push('replacement')
|
||||
})
|
||||
runtime.setNotifier(notifier(() => events.push('original')))
|
||||
runtime['persistHeadlessTerminalTitle'] = function (this: OrcaRuntimeService, ...args) {
|
||||
receivers.push(this)
|
||||
expect(args).toEqual(['folder-workspace', 'host-tab', 'Pinned'])
|
||||
events.push('persist')
|
||||
runtime.setNotifier(replacement)
|
||||
}
|
||||
runtime['touchMobileSessionSnapshotsForPty'] = function (this: OrcaRuntimeService, ptyId) {
|
||||
receivers.push(this)
|
||||
expect(ptyId).toBe('rename-pty')
|
||||
expect(pty.title).toBeNull()
|
||||
events.push('touch')
|
||||
}
|
||||
await expect(runtime.renameTerminal(handle, 'Pinned')).resolves.toEqual({
|
||||
handle,
|
||||
tabId: 'host-tab',
|
||||
title: 'Pinned'
|
||||
})
|
||||
expect(events).toEqual(['touch', 'persist', 'replacement'])
|
||||
expect(receivers).toEqual([runtime, runtime])
|
||||
runtime.onPtyExit('rename-pty', 0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('terminal query erased boundary regressions', () => {
|
||||
// Pane getter changes per read to catch internally inconsistent summaries (L80); ordinary records do not.
|
||||
it('summarizes from one read of the pane, though a later read would differ', () => {
|
||||
class SummaryRuntime extends OrcaRuntimeService {
|
||||
summarize() {
|
||||
const pty = this.recordPtyWorktree('changing-binding', 'missing', { connected: false })
|
||||
this.terminalIdentity.setRecordedSurface(pty, 'tab', pty.paneKey ?? null, 1)
|
||||
let reads = 0
|
||||
Object.defineProperty(pty, 'paneKey', {
|
||||
get: () => (++reads === 1 ? null : 'tab:00000000-0000-4000-8000-000000000001')
|
||||
})
|
||||
const issueHandle = vi.fn(() => 'handle')
|
||||
Object.defineProperty(this, 'issuePtyHandle', { value: issueHandle })
|
||||
expect(this.buildPtyTerminalSummary(pty, new Map())).toMatchObject({
|
||||
orphaned: true,
|
||||
tabId: 'pty:changing-binding'
|
||||
})
|
||||
expect(reads).toBe(ORCA_LAYOUT_OWNER_IS_MAIN ? 0 : 1)
|
||||
expect(issueHandle).toHaveBeenCalledExactlyOnceWith(pty)
|
||||
}
|
||||
}
|
||||
new SummaryRuntime(null).summarize()
|
||||
})
|
||||
|
||||
it('dispatches destructive removal through the Root provider override', async () => {
|
||||
class TeardownRuntime extends OrcaRuntimeService {
|
||||
readonly providerReads = vi.fn()
|
||||
override getLocalProvider() {
|
||||
this.providerReads(this, arguments.length)
|
||||
return null
|
||||
}
|
||||
remove(connectionId?: string) {
|
||||
return this.stopPtysForDestructiveWorktreeRemoval('folder:notes', { connectionId })
|
||||
}
|
||||
}
|
||||
const runtime = new TeardownRuntime(null)
|
||||
await expect(runtime.remove()).rejects.toThrow('PTY provider unavailable')
|
||||
expect(runtime.providerReads).toHaveBeenCalledExactlyOnceWith(runtime, 0)
|
||||
runtime.providerReads.mockClear()
|
||||
await expect(runtime.remove('ssh-host')).rejects.toThrow('PTY provider unavailable')
|
||||
expect(runtime.providerReads).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('first-turn identity under recorded title clocks (#32030)', () => {
|
||||
const launchAt = 1_800_000_000_000
|
||||
const hookAt = launchAt + 2_000
|
||||
@@ -343,23 +168,6 @@ describe('first-turn identity under recorded title clocks (#32030)', () => {
|
||||
return new OrcaRuntimeService(runtimeStore)
|
||||
}
|
||||
|
||||
it('keeps the first clock of a shell title that inventory replays after the hook', () => {
|
||||
const runtime = makeRuntime()
|
||||
const now = vi.spyOn(Date, 'now').mockReturnValue(launchAt)
|
||||
try {
|
||||
const pty = runtime['recordPtyWorktree']('replay-pty', TEST_WORKTREE_ID, { title: 'zsh' })
|
||||
pty.lastAgentStatus = 'idle'
|
||||
now.mockReturnValue(hookAt + 1_000)
|
||||
runtime['recordPtyWorktree']('replay-pty', TEST_WORKTREE_ID, { title: 'zsh' })
|
||||
expect(freshHookSession(pty)).toEqual(providerSession)
|
||||
|
||||
runtime['recordPtyWorktree']('replay-pty', TEST_WORKTREE_ID, { title: 'bash' })
|
||||
expect(freshHookSession(pty)).toBeUndefined()
|
||||
} finally {
|
||||
now.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['none', 'recorded', 'osc'] as const)(
|
||||
'dates the launch title before spawn, with %s title evidence during spawn',
|
||||
async (observation) => {
|
||||
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
} from '../../orca-runtime-test-fixtures.spec'
|
||||
import { electronMocks } from '../../orca-runtime-test-mocks/setup.spec'
|
||||
import { createMobileCreateTestNotifier } from '../../orca-runtime-test-scenario-builders.spec'
|
||||
import { RpcDispatcher } from '../../rpc/dispatcher'
|
||||
import { TERMINAL_QUERY_METHODS } from '../../rpc/methods/terminal/terminal-query-methods'
|
||||
|
||||
async function createPaneTitleRuntime() {
|
||||
electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false })
|
||||
@@ -23,23 +21,6 @@ async function createPaneTitleRuntime() {
|
||||
}
|
||||
|
||||
describe('pane-scoped terminal title', () => {
|
||||
it('uses the durable leaf and trims or clears without changing the tab', async () => {
|
||||
const { runtime, handle, setPaneTitle, renameTerminal } = await createPaneTitleRuntime()
|
||||
await expect(runtime.setPaneTitle(handle, ' REVIEWER ')).resolves.toEqual({
|
||||
handle,
|
||||
tabId: 'tab-1',
|
||||
leafId: 'pane:1',
|
||||
title: 'REVIEWER'
|
||||
})
|
||||
expect(setPaneTitle).toHaveBeenCalledExactlyOnceWith('tab-1', 'pane:1', 'REVIEWER')
|
||||
await runtime.setPaneTitle(handle, ' ')
|
||||
expect(setPaneTitle).toHaveBeenLastCalledWith('tab-1', 'pane:1', null)
|
||||
expect(renameTerminal).not.toHaveBeenCalled()
|
||||
await runtime.renameTerminal(handle, 'Tab name')
|
||||
expect(renameTerminal).toHaveBeenCalledWith('tab-1', 'Tab name')
|
||||
expect(setPaneTitle).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('rejects stale handles after a leaf is replaced, even if its numeric id is reused', async () => {
|
||||
const { runtime, handle, setPaneTitle } = await createPaneTitleRuntime()
|
||||
runtime.syncWindowGraph(1, {
|
||||
@@ -69,40 +50,4 @@ describe('pane-scoped terminal title', () => {
|
||||
)
|
||||
expect(setPaneTitle).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refuses a desktop without the new notification handler', async () => {
|
||||
const { runtime, handle, notifier } = await createPaneTitleRuntime()
|
||||
const { setPaneTitle: _unused, ...legacyNotifier } = notifier
|
||||
runtime.setNotifier(legacyNotifier)
|
||||
await expect(runtime.setPaneTitle(handle, 'REVIEWER')).rejects.toThrow(
|
||||
'require a running Orca desktop'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not report success when the desktop stops accepting notifications', async () => {
|
||||
const { runtime, handle, setPaneTitle, renameTerminal } = await createPaneTitleRuntime()
|
||||
setPaneTitle.mockReturnValue(false)
|
||||
await expect(runtime.setPaneTitle(handle, 'REVIEWER')).rejects.toThrow('runtime_unavailable')
|
||||
expect(setPaneTitle).toHaveBeenCalledExactlyOnceWith('tab-1', 'pane:1', 'REVIEWER')
|
||||
expect(renameTerminal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns an RPC failure receipt when title notification delivery fails', async () => {
|
||||
const { runtime, handle, setPaneTitle } = await createPaneTitleRuntime()
|
||||
setPaneTitle.mockReturnValue(false)
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: TERMINAL_QUERY_METHODS })
|
||||
await expect(
|
||||
dispatcher.dispatch({
|
||||
id: 'pane-title-request',
|
||||
authToken: 'test',
|
||||
method: 'terminal.setPaneTitle',
|
||||
params: { terminal: handle, title: 'REVIEWER' }
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
id: 'pane-title-request',
|
||||
ok: false,
|
||||
error: { code: 'runtime_unavailable' }
|
||||
})
|
||||
expect(setPaneTitle).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user