diff --git a/config/flaky-test-pattern-baseline.txt b/config/flaky-test-pattern-baseline.txt index 6b7e1e34ae7..ca48c2a641a 100644 --- a/config/flaky-test-pattern-baseline.txt +++ b/config/flaky-test-pattern-baseline.txt @@ -23,7 +23,6 @@ 1 test-no-in-test-import-actual:src/main/workspaces/removal/local-worktree-removal-recovery.test.ts 1 test-no-in-test-import-actual:src/renderer/src/components/terminal-pane/pty-connection/notifications/pty-connection-hook-idle-arbitration.test.ts 3 test-no-in-test-import-actual:src/renderer/src/components/terminal-pane/snapshots/remote-snapshot-alt-screen-host-chain.test.ts -1 test-no-in-test-import-actual:src/renderer/src/hooks/useIpcEvents-agent-status-pane-teardown.test.ts 1 test-no-polling-loop:src/main/browser/cookies/browser-cookie-import-concurrency.test.ts 1 test-no-polling-loop:src/main/codex/hooks/reconciliation/codex-hook-toggle-off-wins.test.ts 1 test-no-polling-loop:src/main/codex/structured/launch/codex-structured-real-cli-command-env.test.ts diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 33e9b4f22e9..0244c4773db 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -3381,9 +3381,9 @@ ], "platforms": ["macos", "linux", "windows"], "providers": ["local", "ssh"], - "coveredPlatforms": ["macos"], + "coveredPlatforms": ["linux"], "coveredProviders": [], - "coverageNotes": "Command-runner layout policy retires the paired external-worktree E2E: rendered two-client discovery and transport-continuity coverage is lost, with no equivalent replacement. The archived dated receipt does not validate current source. Audit3417 composes root-adopted3415v2/independent3416v2: intentionally removes117authored/173potential expanded registrations from12semantic sources, deleting4whole conventional suites and3registration modules and retaining4authored/5expanded cases, without equivalent replacement or fresh execution. The whole main-window attach-services suite is removed: runtime/window attach, notifier wiring, reload/teardown and permission-handler mock orchestration alarms are intentionally lost. Prune its exact deleted command selectors, testFiles entries and notifier assertionRef where present; complete prior selectors/ref are archived. Unrelated dated receipts remain exact and retain their original scope; no fresh reduced-selector, native-window or remote execution is claimed. Module layout SSH policy: Retire reset/private-arithmetic generation rollover registrations. The two retained local-versus-SSH mutation refusals do not establish process-scope rollover or sibling-token invalidation; remove this unrelated gate selector and its rollover assertion claim. Unchanged unrelated execution receipts retain only their original scope. No replacement, fresh rollover execution or cross-platform proof is claimed. Historical prior account (preserved verbatim): Audit2003: Remove the standalone one-client shared-runtime/headless-publication example. The retained two-client callback (real WebSocket/runtime coordination with supplied Git listings and watcher notifications) and invalidated scan/reconnect races keep their exact owning scope, without replacement-equivalence or rendered-headless proof. Retire the entire dated38-case receipt through its effective currentCommand. Preserve the unrelated SSH bridge receipt and paired Electron receipt within their original commands. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, physical provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1645 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1412 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Local deterministic evidence covers git-common classification, desktop watcher debounce counts, non-overlapping poller semantics, macOS native-watch fallback, paired-runtime publication to two independent clients with reconnect and stale-cache checks, and process generation-scope rollover across sibling targets. A headed macOS Electron host and separate paired Electron client both rendered a worktree created through the real Git binary and watcher entry path without reconnect. Linux/Windows desktop clients, multi-target live fanout, and WSL runs remain gaps. Audit1309: src/main/ipc/worktrees/creation/worktree-head-identity-reader.test.ts: Keep real filesystem HEAD/loose/packed/relative metadata reads and traversal refusal. Remove invalid-oid/nonstandard-layout characterization. Lose malformed object-id and bare-dir omission alarms; no Git-produced layout claim. Reduced selectors require fresh validation; prior full-suite receipts do not validate removed or changed coverage. No equivalent replacement or physical-provider claim. Audit1381 removes the two mock-forwarding registrations in src/renderer/src/components/right-sidebar/source-control/sync/git-status-push-signal-refresh.test.ts. Sibling attribution suppression and missing/unknown attribution refresh callback alarms are intentionally lost; no equivalent replacement or real hook lifecycle/status-update coverage is claimed. Remaining exact receipts, platform/provider scope, and budgets remain limited to their unchanged selectors; no new execution result is claimed. Audit1412: src/main/ssh/ssh-provider-authority.test.ts removed. Loses real AbortController revocation/target isolation/reentrancy outcomes, recreated-target generation-floor/session rollover behavior, stale mutation refusal and settled-registration cleanup alarms. These useful alarms are deliberately lost; no equivalent cancellation coverage is claimed. No equivalent replacement coverage is claimed.", + "coverageNotes": "Audit4953: only the five retained Linux Git metadata/event-filter cases have a current execution receipt here. No current remote, paired-client or renderer execution is claimed; prior scopes below are historical. Command-runner layout policy retires the paired external-worktree E2E: rendered two-client discovery and transport-continuity coverage is lost, with no equivalent replacement. The archived dated receipt does not validate current source. Audit3417 composes root-adopted3415v2/independent3416v2: intentionally removes117authored/173potential expanded registrations from12semantic sources, deleting4whole conventional suites and3registration modules and retaining4authored/5expanded cases, without equivalent replacement or fresh execution. The whole main-window attach-services suite is removed: runtime/window attach, notifier wiring, reload/teardown and permission-handler mock orchestration alarms are intentionally lost. Prune its exact deleted command selectors, testFiles entries and notifier assertionRef where present; complete prior selectors/ref are archived. Unrelated dated receipts remain exact and retain their original scope; no fresh reduced-selector, native-window or remote execution is claimed. Module layout SSH policy: Retire reset/private-arithmetic generation rollover registrations. The two retained local-versus-SSH mutation refusals do not establish process-scope rollover or sibling-token invalidation; remove this unrelated gate selector and its rollover assertion claim. Unchanged unrelated execution receipts retain only their original scope. No replacement, fresh rollover execution or cross-platform proof is claimed. Historical prior account (preserved verbatim): Audit2003: Remove the standalone one-client shared-runtime/headless-publication example. The retained two-client callback (real WebSocket/runtime coordination with supplied Git listings and watcher notifications) and invalidated scan/reconnect races keep their exact owning scope, without replacement-equivalence or rendered-headless proof. Retire the entire dated38-case receipt through its effective currentCommand. Preserve the unrelated SSH bridge receipt and paired Electron receipt within their original commands. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, physical provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1645 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1412 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Local deterministic evidence covers git-common classification, desktop watcher debounce counts, non-overlapping poller semantics, macOS native-watch fallback, paired-runtime publication to two independent clients with reconnect and stale-cache checks, and process generation-scope rollover across sibling targets. A headed macOS Electron host and separate paired Electron client both rendered a worktree created through the real Git binary and watcher entry path without reconnect. Linux/Windows desktop clients, multi-target live fanout, and WSL runs remain gaps. Audit1309: src/main/ipc/worktrees/creation/worktree-head-identity-reader.test.ts: Keep real filesystem HEAD/loose/packed/relative metadata reads and traversal refusal. Remove invalid-oid/nonstandard-layout characterization. Lose malformed object-id and bare-dir omission alarms; no Git-produced layout claim. Reduced selectors require fresh validation; prior full-suite receipts do not validate removed or changed coverage. No equivalent replacement or physical-provider claim. Audit1381 removes the two mock-forwarding registrations in src/renderer/src/components/right-sidebar/source-control/sync/git-status-push-signal-refresh.test.ts. Sibling attribution suppression and missing/unknown attribution refresh callback alarms are intentionally lost; no equivalent replacement or real hook lifecycle/status-update coverage is claimed. Remaining exact receipts, platform/provider scope, and budgets remain limited to their unchanged selectors; no new execution result is claimed. Audit1412: src/main/ssh/ssh-provider-authority.test.ts removed. Loses real AbortController revocation/target isolation/reentrancy outcomes, recreated-target generation-floor/session rollover behavior, stale mutation refusal and settled-registration cleanup alarms. These useful alarms are deliberately lost; no equivalent cancellation coverage is claimed. No equivalent replacement coverage is claimed.", "motivatingLinks": [ "https://github.com/stablyai/orca/pull/7086", "https://github.com/stablyai/orca/issues/16477" @@ -3391,15 +3391,13 @@ "invariant": "Index-only Git activity below the common Git directory must not emit worktrees:changed, invalidate worktree caches, or trigger fetchWorktrees fanout; structural add/remove/HEAD/gitdir/locked/config.worktree changes must still refresh worktrees and nudge Source Control. When the authoritative paired host accepts an external structural change for a uniquely identified server-local repo, each live client for that host must receive one existing host-scoped worktreesChanged invalidation and converge to the same repo/worktree identity without reconnect; duplicate watcher events must coalesce, stale cached or in-flight catalogs must not survive invalidation, and event-ignoring clients must remain compatible. Colliding local/SSH repo IDs must not publish the host-blind event until existing semantics can carry owner identity. External head moves (commit, amend, reset) must reach background worktree rows through spawn-free metadata reads, never through structural fanout. A process generation-scope rollover revokes every direct SSH target and old-scope provider request, not only the target whose counter exhausted.", "oracle": "Audit3417 composes root-adopted3415v2/independent3416v2: intentionally removes117authored/173potential expanded registrations from12semantic sources, deleting4whole conventional suites and3registration modules and retaining4authored/5expanded cases, without equivalent replacement or fresh execution. The whole main-window attach-services suite is removed: runtime/window attach, notifier wiring, reload/teardown and permission-handler mock orchestration alarms are intentionally lost. Prune its exact deleted command selectors, testFiles entries and notifier assertionRef where present; complete prior selectors/ref are archived. Unrelated dated receipts remain exact and retain their original scope; no fresh reduced-selector, native-window or remote execution is claimed. Module layout SSH policy: Retire reset/private-arithmetic generation rollover registrations. The two retained local-versus-SSH mutation refusals do not establish process-scope rollover or sibling-token invalidation; remove this unrelated gate selector and its rollover assertion claim. Unchanged unrelated execution receipts retain only their original scope. No replacement, fresh rollover execution or cross-platform proof is claimed. Historical prior account (preserved verbatim): Audit2003: Remove the standalone one-client shared-runtime/headless-publication example. The retained two-client callback (real WebSocket/runtime coordination with supplied Git listings and watcher notifications) and invalidated scan/reconnect races keep their exact owning scope, without replacement-equivalence or rendered-headless proof. Retire the entire dated38-case receipt through its effective currentCommand. Preserve the unrelated SSH bridge receipt and paired Electron receipt within their original commands. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, physical provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): Classify exact git-common paths as structural, status-only, or ignored; count notifications from debounced watcher events; force the Linux/Windows poll path to emit allowlisted leaf events, detect linked HEAD rewrites independent of entry-directory mtime, and surface in-place index rewrites via the backstop re-stat; diff head identities from metadata-file reads and notify only real head moves. Join the real runtime RPC server to two independently authenticated E2EE clients plus an event-ignoring client, advance the host Git inventory, trigger duplicate structural watcher callbacks, and require one host renderer notification plus one runtime event and a refreshed catalog per subscribed client; require the event-ignoring client to refresh safely on explicit read. Hold a scan after it captures old inventory, join it from a second paired client, publish the watcher invalidation, advance Git independently, and require both overlapping RPCs to retry or join one fresh scan before returning. Disconnect one client, advance inventory again, and require reconnect to read current state without stale event replay. Give a watcher a local repo ID colliding with an SSH owner and require the host renderer refresh while the unqualified runtime event stays absent. Force the real path in isolated Electron apps with git worktree add, a host Git inventory assertion, store-subscription barriers, matching host/client worktree identity, and visible rows in both renderers. For direct SSH, roll one exhausted target into a fresh process generation scope while invalidating sibling target tokens, and fill a partial pushed authority before clearing that target's PTY bindings once. Audit1381 removes the two mock-forwarding registrations in src/renderer/src/components/right-sidebar/source-control/sync/git-status-push-signal-refresh.test.ts. Sibling attribution suppression and missing/unknown attribution refresh callback alarms are intentionally lost; no equivalent replacement or real hook lifecycle/status-update coverage is claimed. Remaining exact receipts, platform/provider scope, and budgets remain limited to their unchanged selectors; no new execution result is claimed.", "commands": [ - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/worktrees/creation/worktree-base-directory-event-filter.test.ts src/main/ipc/worktrees/creation/worktree-head-identity-reader.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/workspaces/catalog/external-worktree-paired-client-discovery.integration.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts --reporter=dot" + "ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/worktrees/creation/worktree-base-directory-event-filter.test.ts src/main/ipc/worktrees/creation/worktree-head-identity-reader.test.ts --maxWorkers=2", + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/workspaces/catalog/external-worktree-paired-client-discovery.integration.test.ts" ], "testFiles": [ "src/main/ipc/worktrees/creation/worktree-base-directory-event-filter.test.ts", "src/main/ipc/worktrees/creation/worktree-head-identity-reader.test.ts", - "src/main/runtime/workspaces/catalog/external-worktree-paired-client-discovery.integration.test.ts", - "src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts" + "src/main/runtime/workspaces/catalog/external-worktree-paired-client-discovery.integration.test.ts" ], "assertionRefs": [ { @@ -3409,12 +3407,6 @@ "traversal-shaped or backslash/colon symrefs are rejected before any path join and only hex object ids are ever emitted" ] }, - { - "file": "src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts", - "assertions": [ - "a filled partial SSH authority clears the target's PTY bindings once and a conflicting reply is rejected" - ] - }, { "file": "src/main/runtime/workspaces/catalog/external-worktree-paired-client-discovery.integration.test.ts", "assertions": [ @@ -3428,13 +3420,13 @@ ], "evidenceRuns": [ { - "date": "2026-10-11", + "date": "2026-11-14", "runner": "local", - "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts --reporter=dot", + "platform": "linux", + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/worktrees/creation/worktree-base-directory-event-filter.test.ts src/main/ipc/worktrees/creation/worktree-head-identity-reader.test.ts --maxWorkers=2", "result": "passed", - "durationSeconds": 1.21, - "summary": "The direct SSH state-bridge wiring file passed with 6 tests after the reconnect coordinator, scheduler and telemetry were deleted." + "durationSeconds": 0.369, + "summary": "Audit4953: five tests across the two retained Git metadata/event-filter files passed. Duration is the Vitest-reported run time; no paired-client integration, renderer, SSH, Windows or macOS execution is claimed." } ], "runtimeBudget": { @@ -3484,7 +3476,8 @@ "Exact prior command before deleted attach-main-window-services selector removal: \"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/workspaces/catalog/external-worktree-paired-client-discovery.integration.test.ts src/main/window/attach-main-window-services.test.ts\"", "Exact retired testFiles selector: \"src/main/window/attach-main-window-services.test.ts\"", "Complete prior assertionRef retired as historical data only: {\n \"file\": \"src/main/window/attach-main-window-services.test.ts\",\n \"assertions\": [\n \"main-window wiring installs the runtime as the watched-worktree remote-client notifier alongside the existing repo notifier\"\n ]\n }", - "Command-runner layout policy: historical paired external-worktree receipt, preserved verbatim; it no longer validates current source:\n{\n \"date\": \"2026-08-26\",\n \"runner\": \"local\",\n \"platform\": \"macos\",\n \"command\": \"ORCA_E2E_FORCE_HEADFUL=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-external-worktree-discovery.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1\",\n \"result\": \"passed\",\n \"durationSeconds\": 17.96,\n \"summary\": \"One forced-visible paired Electron test passed: real Git inventory, matching visible host/client rows, and one unchanged main-process shared-control transport epoch during convergence.\"\n }" + "Command-runner layout policy: historical paired external-worktree receipt, preserved verbatim; it no longer validates current source:\n{\n \"date\": \"2026-08-26\",\n \"runner\": \"local\",\n \"platform\": \"macos\",\n \"command\": \"ORCA_E2E_FORCE_HEADFUL=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-external-worktree-discovery.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1\",\n \"result\": \"passed\",\n \"durationSeconds\": 17.96,\n \"summary\": \"One forced-visible paired Electron test passed: real Git inventory, matching visible host/client rows, and one unchanged main-process shared-control transport epoch during convergence.\"\n }", + "Audit4953 deletes the mock-only SSH-authority suite. Its authority reconciliation and port-snapshot ordering alarms are intentionally lost; no replacement coverage or fresh remote execution is claimed. Obsolete selectors and the retired dated receipt are archived in the audit packet." ], "demotionRule": "Keep experimental or demote if the focused gate flakes without a product or harness bug, if index-only churn can emit worktrees:changed, or if structural add/remove/HEAD/lock changes fail to converge." }, @@ -12961,12 +12954,9 @@ "The live run reconnects an existing app session but does not restart the packaged application from disk.", "The live topology uses an SSH Git worktree; folder-workspace behavior is covered by target-scoped renderer reconciliation rather than a second headed topology.", "Audit1400: src/main/ipc/ssh/ssh.test.ts reduced. Lose trivial missing target and mocked publication inventory. Preexisting unresolved ../ssh/ssh-port-scanner mock is recorded, not fixed. No equivalent replacement coverage is claimed.", - "Audit1400 retires this complete dated receipt because a source in its selector was deleted or reduced. It does not validate the reduced selector. Original receipt verbatim:\n{\n \"date\": \"2026-07-30\",\n \"runner\": \"local\",\n \"platform\": \"macos\",\n \"command\": \"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts src/main/ssh/port-forwarding/ssh-port-forward.test.ts src/main/ssh/system-transport/system-ssh-forward-process.test.ts src/main/ipc/ssh/ssh.test.ts --reporter=dot\",\n \"result\": \"passed\",\n \"durationSeconds\": 6.77,\n \"summary\": \"Six renderer, main-process, and barrier lifecycle files passed with 215 tests, including deterministic stale-snapshot, rejected-stream, partial-authority, and single-capture isolation plus existing collision, delayed-exit, reconnect, persistence, re-registration, and unrelated-forward contracts.\"\n }", "Audit1400 historical platform/provider scope before source changes: {\"coveredPlatforms\":[\"macos\"],\"coveredProviders\":[\"ssh2\",\"system-ssh\"]}. Remaining provider scope applies only to unchanged-source receipts.", - "Audit1400 retained but unvalidated assertion inventory pending a new run of the reduced command:\n[\n {\n \"file\": \"src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts\",\n \"assertions\": [\n \"does not let initial SSH port snapshots overwrite newer push events\"\n ]\n },\n {\n \"file\": \"src/main/ssh/port-forwarding/ssh-port-forward.test.ts\",\n \"assertions\": [\n \"lists forwards filtered by connectionId\",\n \"removes a forward by id\"\n ]\n },\n {\n \"file\": \"src/main/ssh/system-transport/system-ssh-forward-process.test.ts\",\n \"assertions\": [\n \"does not spawn ssh when the requested local forward port is already in use\",\n \"sends SIGTERM then SIGKILL when the process does not exit\",\n \"does not resolve stop until the process exits\"\n ]\n }\n]", "Audit1400 historical assertion inventory for deleted/reduced sources; not current validated assertions:\n[\n {\n \"file\": \"src/main/ipc/ssh/ssh.test.ts\",\n \"assertions\": [\n \"preserves active port forwards and live connections across handler re-registration\",\n \"persists desired forwards and broadcasts when an active forward closes unexpectedly\"\n ]\n }\n]", - "Audit1400 archived original active assertionRefs verbatim after retirement of all passed receipts:\n[\n {\n \"file\": \"src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts\",\n \"assertions\": [\"does not let initial SSH port snapshots overwrite newer push events\"]\n },\n {\n \"file\": \"src/main/ssh/port-forwarding/ssh-port-forward.test.ts\",\n \"assertions\": [\"lists forwards filtered by connectionId\", \"removes a forward by id\"]\n },\n {\n \"file\": \"src/main/ssh/system-transport/system-ssh-forward-process.test.ts\",\n \"assertions\": [\n \"does not spawn ssh when the requested local forward port is already in use\",\n \"sends SIGTERM then SIGKILL when the process does not exit\",\n \"does not resolve stop until the process exits\"\n ]\n },\n {\n \"file\": \"src/main/ipc/ssh/ssh.test.ts\",\n \"assertions\": [\n \"preserves active port forwards and live connections across handler re-registration\",\n \"persists desired forwards and broadcasts when an active forward closes unexpectedly\"\n ]\n }\n ]", - "Audit1400 archived active selectors after retirement of all passed receipts: {\"commands\":[\"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts src/main/ssh/port-forwarding/ssh-port-forward.test.ts src/main/ssh/system-transport/system-ssh-forward-process.test.ts src/main/ipc/ssh/ssh.test.ts --reporter=dot\",\"pnpm exec electron-vite build --mode e2e\"],\"testFiles\":[\"src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts\",\"src/main/ssh/port-forwarding/ssh-port-forward.test.ts\",\"src/main/ssh/system-transport/system-ssh-forward-process.test.ts\",\"src/main/ipc/ssh/ssh.test.ts\"]}" + "Audit4953 deletes the mock-only SSH-authority suite. Its authority reconciliation and port-snapshot ordering alarms are intentionally lost; no replacement coverage or fresh remote execution is claimed. Obsolete selectors and the retired dated receipt are archived in the audit packet." ], "demotionRule": "Keep experimental or demote if same-authority snapshots can overwrite newer pushes, reconnect silently loses persisted intent, one forward operation disturbs unrelated forwards, live HTTP diverges from renderer/main inventory, or either transport topology flakes without an identified product or harness fault." }, diff --git a/src/renderer/src/components/terminal-pane/pty-connection/startup/agent-pre-ready-typed-draft.test.ts b/src/renderer/src/components/terminal-pane/pty-connection/startup/agent-pre-ready-typed-draft.test.ts deleted file mode 100644 index e192c770dd7..00000000000 --- a/src/renderer/src/components/terminal-pane/pty-connection/startup/agent-pre-ready-typed-draft.test.ts +++ /dev/null @@ -1,398 +0,0 @@ -import type * as React from 'react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { connectPanePty } from '../connect-pane-pty' -import { makePaneKey } from '../../../../../../shared/workspace-layout/stable-pane-id' -import { flushAsyncTicks } from '../test-fixtures/pty-connection-test-async' -import { - createManager, - createMockTransport, - createPane, - leafIdForPane, - type MockTransport -} from '../test-fixtures/pty-connection-test-pane-fixtures' -import { - buildPaneConnectionDeps, - type PtyConnectionAgentTitleMocks, - bindPaneAgentTitleMocks -} from '../test-fixtures/pty-connection-test-deps' -import { - installTerminalTestGlobals, - restoreTerminalTestGlobals -} from '../test-fixtures/pty-connection-test-environment' -import { createInitialStoreState } from '../test-fixtures/pty-connection-test-store-fixtures' -import type { StoreState } from '../test-fixtures/pty-connection-test-store-state' -import type * as TerminalWorktreeRouteModule from '@/lib/workspace/ownership/terminal-worktree-route' -const agentTitleMocks = await vi.importMock( - '@/lib/agent-status/aggregation/worktree-agent-status' -) - -const { - notifyCodexPaneBoundForStaleSweep, - scheduleRuntimeGraphSync, - shouldSeedCacheTimerOnInitialTitle, - toastInfo, - waitForAgentDraftInputReady, - sendAgentDraftPasteContent, - onPasteTimeout, - pasteTimeoutNotice -} = vi.hoisted(() => ({ - waitForAgentDraftInputReady: vi.fn(), - sendAgentDraftPasteContent: vi.fn(async () => true), - onPasteTimeout: vi.fn(), - pasteTimeoutNotice: vi.fn(), - notifyCodexPaneBoundForStaleSweep: vi.fn(), - scheduleRuntimeGraphSync: vi.fn(), - shouldSeedCacheTimerOnInitialTitle: vi.fn(() => false), - toastInfo: vi.fn() -})) - -let mockStoreState: StoreState -let transportFactoryQueue: MockTransport[] = [] -let createdTransportOptions: Record[] = [] -let storeSubscribers: ((state: StoreState) => void)[] = [] - -const layoutOwner = vi.hoisted(() => ({ isMain: false })) - -vi.mock('../../../../../../shared/layout-owner-switch', () => ({ - get ORCA_LAYOUT_OWNER_IS_MAIN() { - return layoutOwner.isMain - } -})) - -vi.mock('@/runtime/sync-runtime-graph/graph-sync-scheduler', () => ({ scheduleRuntimeGraphSync })) - -vi.mock('@/lib/agent-session/messaging/paste/agent-draft-readiness', () => ({ - waitForAgentDraftInputReady -})) - -const { requestTerminalPaneRecovery } = vi.hoisted(() => ({ - requestTerminalPaneRecovery: vi.fn(async () => undefined) -})) - -vi.mock('../../render-health/recovery/terminal-pane-recovery', async (importOriginal) => ({ - ...(await importOriginal>()), - requestTerminalPaneRecovery -})) - -const pairedRoute = vi.hoisted(() => { - const route: { runtimeEnvironmentId: string | null } = { runtimeEnvironmentId: null } - return route -}) - -vi.mock('@/lib/workspace/ownership/terminal-worktree-route', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - resolveTerminalWorktreeRoute: ( - ...args: Parameters - ) => - pairedRoute.runtimeEnvironmentId - ? { runtimeEnvironmentId: pairedRoute.runtimeEnvironmentId } - : actual.resolveTerminalWorktreeRoute(...args) - } -}) - -vi.mock('../../pty-transport/remote-runtime/remote-runtime-pty-transport', () => ({ - createRemoteRuntimePtyTransport: vi.fn( - (_environmentId: string, options: Record) => { - createdTransportOptions.push(options) - const nextTransport = transportFactoryQueue.shift() - if (!nextTransport) { - throw new Error('No mock transport queued') - } - return nextTransport - } - ) -})) - -vi.mock('@/lib/agent-session/messaging/paste/agent-draft-paste-content', () => ({ - sendAgentDraftPasteContent -})) - -vi.mock('@/lib/agent-session/launch/prompt/launch-agent-paste-timeout-notice', () => ({ - createPasteReadinessTimeoutNotice: (args: unknown) => { - pasteTimeoutNotice(args) - return { onTimeout: onPasteTimeout, wasNotified: () => false } - } -})) - -vi.mock('@/store/index', () => ({ - useAppStore: { - getState: () => mockStoreState, - subscribe: (listener: (state: StoreState) => void) => { - storeSubscribers.push(listener) - return () => { - storeSubscribers = storeSubscribers.filter((candidate) => candidate !== listener) - } - } - } -})) - -vi.mock('@/lib/agent-status/aggregation/worktree-agent-status', async (importOriginal) => { - const { buildAgentStatusModuleMock } = - await import('../test-fixtures/pty-connection-test-environment') - return buildAgentStatusModuleMock(await importOriginal>()) -}) - -vi.mock('../../titles/cache-timer-seeding', () => ({ shouldSeedCacheTimerOnInitialTitle })) - -vi.mock('sonner', () => ({ toast: { info: toastInfo } })) - -vi.mock('@/lib/agent-session/accounts/codex/codex-stale-pane-sweep', () => ({ - notifyCodexPaneBoundForStaleSweep -})) - -vi.mock('react', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useCallback: unknown>(fn: T): T => fn - } -}) - -vi.mock('../../pty-transport/ipc/pty-transport', () => ({ - createIpcPtyTransport: vi.fn((options: Record) => { - createdTransportOptions.push(options) - const nextTransport = transportFactoryQueue.shift() - if (!nextTransport) { - throw new Error('No mock transport queued') - } - return nextTransport - }) -})) - -type ConnectedAgentPane = { - paneKey: string - transport: MockTransport - type: (data: string) => void - bind: (ptyId: string) => void - unbind: () => void - resolveConnect: (result: unknown) => void - dispose: () => void -} - -function connectAgentPane(overrides: Record = {}): ConnectedAgentPane { - const transport = createMockTransport() - let ptyId: string | null = null - transport.getPtyId.mockImplementation(() => ptyId) - transport.sendInput.mockImplementation(() => ptyId !== null) - let resolveConnect: (result: unknown) => void = () => {} - transport.connect.mockImplementation(() => new Promise((resolve) => (resolveConnect = resolve))) - transportFactoryQueue.push(transport) - const pane = createPane(1) - const userInputListeners: (() => void)[] = [] - Object.assign(pane.terminal, { - _core: { - coreService: { - onUserInput: (listener: () => void) => { - userInputListeners.push(listener) - return { dispose: vi.fn() } - } - } - } - }) - const connectArgs = [ - pane, - createManager(1), - buildPaneConnectionDeps(() => mockStoreState, { - startup: { command: 'claude', launchAgent: 'claude' }, - ...overrides - }) - ] - bindPaneAgentTitleMocks(connectArgs[2], agentTitleMocks) - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared pane fixtures implement the members connectPanePty touches. - const binding = connectPanePty(...(connectArgs as unknown as Parameters)) - return { - paneKey: makePaneKey('tab-1', leafIdForPane(1)), - transport, - type: (data) => { - for (const listener of userInputListeners) { - listener() - } - for (const [listener] of pane.terminal.onData.mock.calls) { - listener(data) - } - }, - bind: (id) => { - ptyId = id - const onPtySpawn = createdTransportOptions[0]?.onPtySpawn - if (typeof onPtySpawn === 'function') { - onPtySpawn(id) - } - }, - unbind: () => { - ptyId = null - }, - resolveConnect: (result) => { - if (result && typeof result === 'object' && 'id' in result && typeof result.id === 'string') { - ptyId = result.id - } - resolveConnect(result) - }, - dispose: () => binding.dispose() - } -} - -describe('agent pane keys typed before its PTY binds', () => { - beforeEach(async () => { - vi.clearAllMocks() - transportFactoryQueue = [] - createdTransportOptions = [] - storeSubscribers = [] - mockStoreState = createInitialStoreState(() => mockStoreState) - await installTerminalTestGlobals() - }) - - afterEach(async () => { - pairedRoute.runtimeEnvironmentId = null - await restoreTerminalTestGlobals() - }) - - it('pastes them as an unsubmitted draft once the agent is ready', async () => { - waitForAgentDraftInputReady.mockResolvedValue(true) - const pane = connectAgentPane() - await flushAsyncTicks(20) - - pane.type('make the tests pass') - pane.type('\r') - expect(pane.transport.sendInput).not.toHaveBeenCalled() - - pane.bind('pty-1') - await flushAsyncTicks(20) - - expect(waitForAgentDraftInputReady).toHaveBeenCalledWith( - 'pty-1', - expect.any(Number), - expect.any(String) - ) - expect(sendAgentDraftPasteContent).toHaveBeenCalledWith( - 'pty-1', - 'make the tests pass', - 'driving', - expect.any(Function) - ) - pane.dispose() - }) - - it('drops them with a notice when the agent never reports ready', async () => { - waitForAgentDraftInputReady.mockResolvedValue(false) - const pane = connectAgentPane() - await flushAsyncTicks(20) - - pane.type('hello') - pane.bind('pty-1') - await flushAsyncTicks(20) - - expect(sendAgentDraftPasteContent).not.toHaveBeenCalled() - expect(onPasteTimeout).toHaveBeenCalledOnce() - // The typed text exists nowhere else, so the notice must carry it back to the user. - expect(pasteTimeoutNotice).toHaveBeenCalledWith( - expect.objectContaining({ droppedText: 'hello' }) - ) - pane.dispose() - }) - - it('sends keys typed after the bind live and drops the draft rather than pasting over them', async () => { - let ready!: (value: boolean) => void - waitForAgentDraftInputReady.mockReturnValue( - new Promise((resolve) => (ready = resolve)) - ) - const pane = connectAgentPane() - await flushAsyncTicks(20) - - pane.type('fix it') - pane.bind('pty-1') - pane.type('y') - expect(pane.transport.sendInput).toHaveBeenCalledWith('y', 'driving') - - ready(true) - await flushAsyncTicks(20) - - expect(sendAgentDraftPasteContent).not.toHaveBeenCalled() - expect(onPasteTimeout).toHaveBeenCalledOnce() - pane.dispose() - }) - - it('drops the held draft when the pane closes before the bind', async () => { - const pane = connectAgentPane() - await flushAsyncTicks(20) - - pane.type('lost on close') - pane.dispose() - pane.bind('pty-1') - await flushAsyncTicks(20) - - expect(waitForAgentDraftInputReady).not.toHaveBeenCalled() - expect(sendAgentDraftPasteContent).not.toHaveBeenCalled() - }) - - it('discards the draft when the first bind is a reattach result, then sends keys live', async () => { - const pane = connectAgentPane() - await flushAsyncTicks(20) - - waitForAgentDraftInputReady.mockResolvedValue(true) - pane.type('held') - pane.resolveConnect({ id: 'pty-1', isReattach: true }) - await flushAsyncTicks(40) - // A later fresh spawn (the cold-restore resume) must not receive the old draft either. - pane.unbind() - pane.bind('pty-2') - await flushAsyncTicks(20) - pane.type('z') - - expect(waitForAgentDraftInputReady).not.toHaveBeenCalled() - expect(sendAgentDraftPasteContent).not.toHaveBeenCalled() - expect(pane.transport.sendInput).toHaveBeenCalledWith('z', 'driving') - pane.dispose() - }) - - // Why: the paired-server transport holds and delivers type-ahead itself, Ctrl+C included. - it('leaves a paired-server agent pane resuming a live entry on its own transport', async () => { - pairedRoute.runtimeEnvironmentId = 'env-1' - const paneKey = makePaneKey('tab-1', leafIdForPane(1)) - mockStoreState.agentStatusByPaneKey[paneKey] = { - state: 'working', - prompt: '', - agentType: 'claude', - paneKey, - updatedAt: 1_000, - stateStartedAt: 1_000, - stateHistory: [], - providerSession: { key: 'session_id', id: 'session-1' } - } - const pane = connectAgentPane({ startup: undefined }) - await flushAsyncTicks(20) - - pane.type('before reattach') - pane.type('\x03') - - expect(pane.transport.sendInput).toHaveBeenCalledWith('before reattach', 'driving') - // Ctrl+C takes the acknowledged-write path. - expect(pane.transport.sendInputAccepted).toHaveBeenCalledWith('\x03', 'driving') - pane.bind('pty-1') - await flushAsyncTicks(20) - expect(waitForAgentDraftInputReady).not.toHaveBeenCalled() - expect(sendAgentDraftPasteContent).not.toHaveBeenCalled() - pane.dispose() - }) - - // Why: a first connect hung past its grace must still let typing arm recovery. - it('still requests recovery for keys held while the first connect hangs past its grace', async () => { - const pane = connectAgentPane() - await flushAsyncTicks(20) - - pane.type('a') - expect(requestTerminalPaneRecovery).not.toHaveBeenCalled() - - const now = Date.now() - const clock = vi.spyOn(Date, 'now').mockReturnValue(now + 61_000) - try { - pane.type('b') - expect(requestTerminalPaneRecovery).toHaveBeenCalledTimes(1) - } finally { - clock.mockRestore() - } - pane.dispose() - }) -}) diff --git a/src/renderer/src/components/terminal-pane/pty-connection/startup/main-pane-resume-banner.test.ts b/src/renderer/src/components/terminal-pane/pty-connection/startup/main-pane-resume-banner.test.ts deleted file mode 100644 index febb72759b7..00000000000 --- a/src/renderer/src/components/terminal-pane/pty-connection/startup/main-pane-resume-banner.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { TerminalPaneResumeOutcome } from '@/store/terminals/terminal-state' -import type { TerminalLayoutSnapshot } from '../../../../../../shared/workspace-layout/terminal-tab-types' -import type { ConnectPanePtySession } from '../connect-pane-pty-session' -import type { showMainPaneResumeOutcome as ShowOutcome } from '../spawn/main-pane-resume-banner' - -type Outcomes = Record -type StoreState = { - terminalPaneResumeOutcomesByPaneKey: Outcomes - terminalLayoutsByTabId: Record -} - -const store = vi.hoisted(() => { - let state: StoreState = { terminalPaneResumeOutcomesByPaneKey: {}, terminalLayoutsByTabId: {} } - const listeners = new Set<(next: StoreState) => void>() - return { - getState: () => state, - subscribe: (listener: (next: StoreState) => void) => { - listeners.add(listener) - return () => listeners.delete(listener) - }, - publish: (patch: Partial) => { - state = { ...state, ...patch } - for (const listener of listeners) { - listener(state) - } - }, - reset: () => { - state = { terminalPaneResumeOutcomesByPaneKey: {}, terminalLayoutsByTabId: {} } - listeners.clear() - } - } -}) - -vi.mock('@/store/index', () => ({ useAppStore: store })) -vi.mock('../spawn/main-starts-pane', () => ({ mainStartsPane: () => true })) - -const PANE_KEY = 'tab-1:leaf-1' - -function attachedView() { - const view = { - cacheKey: PANE_KEY, - disposed: false, - deps: { tabId: 'tab-1' }, - pane: { leafId: 'leaf-1' }, - waitTeardowns: new Array<() => void>(), - showSessionRestoredBanner: vi.fn() - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the banner reads only these session fields; mainStartsPane is mocked. - return { view, session: view as unknown as ConnectPanePtySession } -} - -/** A fresh module, as a renderer reload loads it; session storage survives the reload. */ -async function loadBannerModule(): Promise { - vi.resetModules() - const { showMainPaneResumeOutcome, acceptMainPaneResumeStart } = - await import('../spawn/main-pane-resume-banner') - const { admitTerminalNoticeLifetime } = - await import('../../provider-notices/terminal-notice-lifetime') - return (session, ptyId) => { - admitTerminalNoticeLifetime(ptyId) - acceptMainPaneResumeStart(session, ptyId) - showMainPaneResumeOutcome(session, ptyId) - } -} - -describe("the window's banner for main's start of a pane", () => { - let storage: Map - - beforeEach(() => { - store.reset() - storage = new Map() - vi.stubGlobal('sessionStorage', { - getItem: (key: string) => storage.get(key) ?? null, - setItem: (key: string, value: string) => storage.set(key, value) - }) - }) - - afterEach(() => { - vi.unstubAllGlobals() - }) - - it('shows once per start: a later start under the same terminal id shows again', async () => { - const showMainPaneResumeOutcome = await loadBannerModule() - const publish = (ptyGeneration: number, outcome: TerminalPaneResumeOutcome['outcome']) => - store.publish({ - terminalPaneResumeOutcomesByPaneKey: { - [PANE_KEY]: { ptyId: 'pty-1', ptyGeneration, outcome } - } - }) - publish(1, 'restored') - const first = attachedView() - showMainPaneResumeOutcome(first.session, 'pty-1') - // A respawn kept the daemon id; its bind raised the generation, so this is a new start. - publish(2, 'resume-unavailable') - const respawned = attachedView() - showMainPaneResumeOutcome(respawned.session, 'pty-1') - expect(respawned.view.showSessionRestoredBanner).toHaveBeenCalledExactlyOnceWith( - 'resume-unavailable' - ) - const again = attachedView() - showMainPaneResumeOutcome(again.session, 'pty-1') - expect(again.view.showSessionRestoredBanner).not.toHaveBeenCalled() - }) - - it('shows nothing again after a renderer reload reattaches the same start', async () => { - store.publish({ - terminalPaneResumeOutcomesByPaneKey: { - [PANE_KEY]: { ptyId: 'pty-1', ptyGeneration: 1, outcome: 'resume-unavailable' } - } - }) - const before = attachedView() - ;(await loadBannerModule())(before.session, 'pty-1') - expect(before.view.showSessionRestoredBanner).toHaveBeenCalledOnce() - - const reloaded = attachedView() - ;(await loadBannerModule())(reloaded.session, 'pty-1') - expect(reloaded.view.showSessionRestoredBanner).not.toHaveBeenCalled() - }) - - it('retains a shown start across a publication gap', async () => { - const attach = await loadBannerModule() - const publish = (ptyId: string, ptyGeneration: number) => - store.publish({ - terminalPaneResumeOutcomesByPaneKey: { - [PANE_KEY]: { ptyId, ptyGeneration, outcome: 'restored' } - } - }) - publish('pty-a', 3) - const first = attachedView() - attach(first.session, 'pty-a') - expect(first.view.showSessionRestoredBanner).toHaveBeenCalledExactlyOnceWith('restored') - store.publish({ terminalPaneResumeOutcomesByPaneKey: {} }) - publish('pty-b', 4) - attach(attachedView().session, 'pty-b') - publish('pty-a', 3) - const remounted = attachedView() - attach(remounted.session, 'pty-a') - expect(remounted.view.showSessionRestoredBanner).not.toHaveBeenCalled() - // A late older fact cannot replace the accepted start or reset suppression. - publish('pty-a', 2) - attach(remounted.session, 'pty-a') - publish('pty-a', 3) - attach(remounted.session, 'pty-a') - expect(remounted.view.showSessionRestoredBanner).not.toHaveBeenCalled() - }) - - it('retains a shown start across a publication gap without session storage', async () => { - vi.stubGlobal('sessionStorage', { - getItem: () => { - throw new Error('storage unavailable') - }, - setItem: () => { - throw new Error('storage unavailable') - } - }) - const attach = await loadBannerModule() - const publish = (ptyId: string, ptyGeneration: number) => - store.publish({ - terminalPaneResumeOutcomesByPaneKey: { - [PANE_KEY]: { ptyId, ptyGeneration, outcome: 'restored' } - } - }) - publish('pty-a', 3) - const first = attachedView() - attach(first.session, 'pty-a') - expect(first.view.showSessionRestoredBanner).toHaveBeenCalledExactlyOnceWith('restored') - store.publish({ terminalPaneResumeOutcomesByPaneKey: {} }) - publish('pty-b', 4) - attach(attachedView().session, 'pty-b') - publish('pty-a', 3) - const remounted = attachedView() - attach(remounted.session, 'pty-a') - expect(remounted.view.showSessionRestoredBanner).not.toHaveBeenCalled() - // A late older fact cannot replace the accepted start or reset suppression. - publish('pty-a', 2) - attach(remounted.session, 'pty-a') - publish('pty-a', 3) - attach(remounted.session, 'pty-a') - expect(remounted.view.showSessionRestoredBanner).not.toHaveBeenCalled() - }) - - it('shows nothing for an outcome naming another terminal', async () => { - const showMainPaneResumeOutcome = await loadBannerModule() - store.publish({ - terminalPaneResumeOutcomesByPaneKey: { - [PANE_KEY]: { ptyId: 'pty-old', ptyGeneration: 1, outcome: 'restored' } - } - }) - const { view, session } = attachedView() - showMainPaneResumeOutcome(session, 'pty-1') - store.publish({ - terminalPaneResumeOutcomesByPaneKey: { - [PANE_KEY]: { ptyId: 'pty-old', ptyGeneration: 2, outcome: 'resume-unavailable' } - } - }) - expect(view.showSessionRestoredBanner).not.toHaveBeenCalled() - }) -}) diff --git a/src/renderer/src/hooks/ipc-events-ssh-authority-test-fixtures.ts b/src/renderer/src/hooks/ipc-events-ssh-authority-test-fixtures.ts deleted file mode 100644 index 16719970b83..00000000000 --- a/src/renderer/src/hooks/ipc-events-ssh-authority-test-fixtures.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { vi } from 'vitest' -import { buildStoreState } from './ipc-events-agent-status-store-test-fixtures' -import type { StoreLike } from './ipc-events-agent-status-store-test-fixtures' -import { buildWindowApi } from './ipc-events-agent-status-window-test-fixtures' - -export function buildSshAuthorityReconciliationHarness(args: { - partialAuthority: { providerEpoch?: string; connectionGeneration?: number } - latestAuthority: { providerEpoch: string; connectionGeneration: number } -}): { - emitPartialState: () => void - getState: ReturnType - setSshConnectionState: ReturnType - storeState: StoreLike - storedState: () => Record | undefined -} { - const targetId = 'target-reconciliation' - const baseState = { - targetId, - status: 'connected' as const, - error: null, - reconnectAttempt: 0 - } - const partialState = { ...baseState, ...args.partialAuthority } - const latestState = { ...baseState, ...args.latestAuthority } - const sshConnectionStates = new Map>() - let sshStateListener: ((data: { targetId: string; state: unknown }) => void) | undefined - const getState = vi.fn(() => Promise.resolve(latestState)) - const setSshConnectionState = vi.fn((nextTargetId: string, state: Record) => { - sshConnectionStates.set(nextTargetId, state) - }) - const storeState = buildStoreState({ - sshTargetLabels: new Map([[targetId, 'Reconciliation Target']]), - sshConnectionStates, - setSshConnectionState, - setSshTargetsMetadata: vi.fn(), - setRemovedSshTargetLabels: vi.fn(), - clearRemoteDetectedAgents: vi.fn(), - clearRemovedSshTargetState: vi.fn() - }) - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: () => () => {}, - ssh: { - getState, - onStateChanged: (listener: (data: { targetId: string; state: unknown }) => void) => { - sshStateListener = listener - return () => {} - } - } - }) - ) - - return { - emitPartialState: () => { - if (!sshStateListener) { - throw new Error('Expected SSH state listener') - } - sshStateListener({ targetId, state: partialState }) - }, - getState, - setSshConnectionState, - storeState, - storedState: () => sshConnectionStates.get(targetId) - } -} diff --git a/src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts b/src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts deleted file mode 100644 index cf5de59d92a..00000000000 --- a/src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts +++ /dev/null @@ -1,326 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as EditorAutosaveModule from '@/components/editor/autosave/editor-autosave' -import type { FsChangedPayload } from '../../../shared/file-watch/filesystem-change-event' - -vi.mock('@/store', () => ({ - useAppStore: { getState: vi.fn() } -})) -vi.mock('@/components/editor/autosave/editor-autosave', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - notifyEditorExternalFileChange: vi.fn(), - getOpenFilesForExternalFileChange: vi.fn(() => []) - } -}) - -import { - buildEditorExternalWatchEventHandler as createExternalWatchEventHandler, - EXTERNAL_MUTATION_DEBOUNCE_MS -} from './editor-external-watch-event-reconciliation' -import { verifyLatchedMoveDestinations } from './useEditorExternalWatch' -import { useAppStore } from '@/store' -import type { OpenFile } from '@/store/slices/editor/types/open-file' -import { createTestStore } from '@/store/testing/app-store-test-fixture' -import { getOpenFilesForExternalFileChange } from '@/components/editor/autosave/editor-autosave' -import { __clearSelfWriteRegistryForTests } from '@/components/editor/external-changes/editor-self-write-registry' -import { - __clearEditorPathMovesForTests, - beginEditorPathMove, - settleEditorPathMove -} from '@/components/editor/editor-path-move-inflight' -import { getDiskBaselineSignature } from '../../../shared/file-write/disk-baseline-signature' - -const findTarget = (worktreePath: string, runtimeEnvironmentId: string | null = null) => - worktreePath === '/repo' - ? { worktreeId: 'wt-1', worktreePath: '/repo', connectionId: undefined, runtimeEnvironmentId } - : undefined - -function payload(events: FsChangedPayload['events']): FsChangedPayload { - return { worktreePath: '/repo', events } -} - -describe('self-move source suppression (in-flight)', () => { - const setExternalMutation = vi.fn() - const fileNotes = { - id: 'file-notes', - worktreeId: 'wt-1', - worktreePath: '/repo', - filePath: '/repo/notes.md', - relativePath: 'notes.md', - language: 'markdown', - mode: 'edit' as const, - isDirty: false - } - - beforeEach(() => { - vi.clearAllMocks() - vi.useFakeTimers() - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [fileNotes], - setExternalMutation - }) - }) - - afterEach(() => { - vi.useRealTimers() - vi.unstubAllGlobals() - __clearSelfWriteRegistryForTests() - __clearEditorPathMovesForTests() - }) - - function beginMove(): void { - beginEditorPathMove({ - operationId: 'op-1', - worktreeId: 'wt-1', - runtimeEnvironmentId: null, - sourcePaths: ['/repo/notes.md'] - }) - } - - it('does not tombstone the source of an in-flight move (tab still at old path)', () => { - beginMove() - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payload([{ kind: 'delete', absolutePath: '/repo/notes.md' }])) - vi.advanceTimersByTime(EXTERNAL_MUTATION_DEBOUNCE_MS) - - expect(setExternalMutation).not.toHaveBeenCalledWith('file-notes', 'deleted') - expect(setExternalMutation).not.toHaveBeenCalledWith('file-notes', 'renamed') - dispose() - }) - - it('does not tombstone a tab opened UNDER a moving directory mid-rename', () => { - // Root move provenance must cover files opened during the move, even absent from its original tab list. - const lateTab = { - id: 'file-late', - worktreeId: 'wt-1', - worktreePath: '/repo', - filePath: '/repo/src/late.md', - relativePath: 'src/late.md', - language: 'markdown', - mode: 'edit' as const, - isDirty: true - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [lateTab], - setExternalMutation - }) - beginEditorPathMove({ - operationId: 'op-dir', - worktreeId: 'wt-1', - runtimeEnvironmentId: null, - sourcePaths: ['/repo/src'] - }) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged( - payload([ - { kind: 'delete', absolutePath: '/repo/src/late.md' }, - { kind: 'create', absolutePath: '/repo/dst/late.md' } - ]) - ) - vi.advanceTimersByTime(EXTERNAL_MUTATION_DEBOUNCE_MS) - - expect(setExternalMutation).not.toHaveBeenCalledWith('file-late', 'renamed') - expect(setExternalMutation).not.toHaveBeenCalledWith('file-late', 'deleted') - dispose() - }) - - it('tombstones a genuine delete once the move has settled', () => { - beginMove() - settleEditorPathMove('op-1') - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payload([{ kind: 'delete', absolutePath: '/repo/notes.md' }])) - vi.advanceTimersByTime(EXTERNAL_MUTATION_DEBOUNCE_MS) - - expect(setExternalMutation).toHaveBeenCalledWith('file-notes', 'deleted') - dispose() - }) -}) - -describe('self-move echo verification (content identity)', () => { - const setExternalMutation = vi.fn() - const clearSelfMoveEcho = vi.fn() - - const BASELINE_CONTENT = 'the file on disk\n' - const baselineSignature = getDiskBaselineSignature(BASELINE_CONTENT) - const TARGET_PATH = '/repo/subdir/notes.md' - - function movedDirtyTab(overrides: Partial = {}): OpenFile { - return { - id: 'file-notes', - worktreeId: 'wt-1', - filePath: TARGET_PATH, - relativePath: 'subdir/notes.md', - language: 'markdown', - mode: 'edit' as const, - isDirty: true, - lastKnownDiskSignature: baselineSignature, - pendingSelfMoveEcho: { operationId: 'op-1', targetPath: TARGET_PATH }, - ...overrides - } - } - - function mockState(file: OpenFile): void { - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [file], - setExternalMutation, - clearSelfMoveEcho - }) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([file]) - } - - function payloadFor(): FsChangedPayload { - return { worktreePath: '/repo', events: [{ kind: 'update', absolutePath: TARGET_PATH }] } - } - - function stubDiskRead(result: unknown): void { - vi.stubGlobal('window', { api: { fs: { readFile: vi.fn().mockResolvedValue(result) } } }) - } - - function stubDiskReadError(error: Error): void { - vi.stubGlobal('window', { api: { fs: { readFile: vi.fn().mockRejectedValue(error) } } }) - } - - beforeEach(() => { - vi.clearAllMocks() - vi.useFakeTimers() - __clearSelfWriteRegistryForTests() - __clearEditorPathMovesForTests() - }) - - afterEach(() => { - vi.useRealTimers() - vi.unstubAllGlobals() - }) - - it('suppresses when disk matches the baseline', async () => { - mockState(movedDirtyTab()) - stubDiskRead({ isBinary: false, content: BASELINE_CONTENT }) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payloadFor()) - await vi.advanceTimersByTimeAsync(100) - - expect(setExternalMutation).not.toHaveBeenCalledWith('file-notes', 'changed') - expect(clearSelfMoveEcho).toHaveBeenCalledWith('file-notes') - dispose() - }) - - it('marks changed when a genuine external write differs from the baseline', async () => { - mockState(movedDirtyTab()) - stubDiskRead({ isBinary: false, content: 'an agent rewrote this file\n' }) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payloadFor()) - await vi.advanceTimersByTimeAsync(100) - - expect(setExternalMutation).toHaveBeenCalledWith('file-notes', 'changed') - dispose() - }) - - it('fails closed (marks changed) when the tab has no disk baseline', async () => { - mockState(movedDirtyTab({ lastKnownDiskSignature: undefined })) - stubDiskRead({ isBinary: false, content: BASELINE_CONTENT }) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payloadFor()) - await vi.advanceTimersByTimeAsync(100) - - expect(setExternalMutation).toHaveBeenCalledWith('file-notes', 'changed') - dispose() - }) - - it('fails closed when the destination read errors', async () => { - mockState(movedDirtyTab()) - stubDiskReadError(new Error('EACCES')) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payloadFor()) - await vi.advanceTimersByTimeAsync(100) - - expect(setExternalMutation).toHaveBeenCalledWith('file-notes', 'changed') - dispose() - }) - - it('fails closed when the destination is binary', async () => { - mockState(movedDirtyTab()) - stubDiskRead({ isBinary: true, content: '' }) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payloadFor()) - await vi.advanceTimersByTimeAsync(100) - - expect(setExternalMutation).toHaveBeenCalledWith('file-notes', 'changed') - dispose() - }) - - it('does not verify an event with no move provenance (normal changed-on-disk path)', async () => { - mockState(movedDirtyTab({ pendingSelfMoveEcho: undefined })) - stubDiskRead({ isBinary: false, content: BASELINE_CONTENT }) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payloadFor()) - await vi.advanceTimersByTimeAsync(100) - - expect(setExternalMutation).toHaveBeenCalledWith('file-notes', 'changed') - dispose() - }) -}) - -describe('verifyLatchedMoveDestinations (proactive, cross-worktree path)', () => { - const clearSelfMoveEcho = vi.fn() - const setExternalMutation = vi.fn() - - const CONTENT = 'the echoed file on disk\n' - const baseline = getDiskBaselineSignature(CONTENT) - // Floating tabs use their own root-relative paths, never the initiating workspace root. - const FLOATING_FILE_PATH = '/elsewhere/notes/readme.md' - const floatingTab = { - id: 'file-x', - worktreeId: 'floating', - worktreePath: '/elsewhere', - filePath: FLOATING_FILE_PATH, - relativePath: '../notes/readme.md', - runtimeEnvironmentId: null, - language: 'markdown', - mode: 'edit' as const, - isDirty: true, - lastKnownDiskSignature: baseline, - pendingSelfMoveEcho: { operationId: 'op-1', targetPath: FLOATING_FILE_PATH } - } - let readFile: ReturnType - - beforeEach(() => { - vi.clearAllMocks() - vi.useFakeTimers() - readFile = vi.fn().mockResolvedValue({ isBinary: false, content: CONTENT }) - vi.stubGlobal('window', { api: { fs: { readFile } } }) - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [floatingTab], - clearSelfMoveEcho, - setExternalMutation - }) - }) - - afterEach(() => { - vi.useRealTimers() - vi.unstubAllGlobals() - }) - - it('reads the tab own filePath, not the initiating worktree joined with its relativePath', async () => { - verifyLatchedMoveDestinations('/initiating-worktree', undefined, ['file-x']) - await vi.advanceTimersByTimeAsync(50) - - expect(readFile).toHaveBeenCalledWith(expect.objectContaining({ filePath: FLOATING_FILE_PATH })) - expect(setExternalMutation).not.toHaveBeenCalledWith('file-x', 'changed') - // Proactive verification must retain provenance for later watcher echoes. - expect(clearSelfMoveEcho).not.toHaveBeenCalled() - }) -}) diff --git a/src/renderer/src/hooks/useEditorExternalWatch-subscriptions.test.tsx b/src/renderer/src/hooks/useEditorExternalWatch-subscriptions.test.tsx deleted file mode 100644 index 8fd792b1898..00000000000 --- a/src/renderer/src/hooks/useEditorExternalWatch-subscriptions.test.tsx +++ /dev/null @@ -1,203 +0,0 @@ -// @vitest-environment happy-dom - -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { act, createElement } from 'react' -import { createRoot, type Root } from 'react-dom/client' - -type TestWatchTarget = { - worktreeId: string - worktreePath: string - connectionId: string | undefined - runtimeEnvironmentId: string | null - allowLocalWindowsWslAliases?: true -} - -const subscriptionState = vi.hoisted(() => ({ - snapshot: { targets: [] as TestWatchTarget[], targetsKey: '' }, - subscribeRuntimeFileChanges: vi.fn(), - disposeEventHandler: vi.fn(), - handleFsChanged: vi.fn(), - contacts: new Map void>(), - unsubscribeContact: vi.fn() -})) - -vi.mock('@/store', () => ({ - useAppStore: Object.assign( - (selector: (state: unknown) => unknown) => selector({ openFiles: [] }), - { - getState: () => ({ openFiles: [] }) - } - ) -})) -vi.mock('@/runtime/hosts/connections/runtime-host-contact-regained', () => ({ - subscribeRuntimeHostContactRegained: (id: string, listener: () => void) => { - subscriptionState.contacts.set(id, listener) - return subscriptionState.unsubscribeContact - } -})) -vi.mock('@/runtime/files/watching/runtime-file-watch-client', () => ({ - subscribeRuntimeFileChanges: subscriptionState.subscribeRuntimeFileChanges -})) -vi.mock('./editor-external-watch-targets', () => ({ - selectEditorExternalWatchTargets: () => ({ - unwatchedTargetKeys: new Set(), - unwatchedTargetsKey: '', - ...subscriptionState.snapshot - }), - getEditorExternalWatchTargetKey: (target: TestWatchTarget) => - [ - target.worktreeId, - target.worktreePath, - target.connectionId ?? 'local', - target.runtimeEnvironmentId ?? 'client', - target.allowLocalWindowsWslAliases ? 'wsl-aliases' : 'literal' - ].join('::') -})) -vi.mock('./editor-external-watch-event-reconciliation', () => ({ - buildEditorExternalWatchEventHandler: vi.fn(() => ({ - handleFsChanged: subscriptionState.handleFsChanged, - dispose: subscriptionState.disposeEventHandler - })), - collectOverflowEditorExternalReloadTargets: vi.fn() -})) -vi.mock('./editor-external-watch-disk-verification', () => ({ - verifyLatchedEditorMoveDestinations: vi.fn() -})) - -import { useEditorExternalWatch } from './useEditorExternalWatch' - -function WatchProbe(): null { - useEditorExternalWatch() - return null -} - -function runtimeTarget(): TestWatchTarget { - return { - worktreeId: 'wt-runtime', - worktreePath: '/runtime/repo', - connectionId: 'nested-ssh', - runtimeEnvironmentId: 'runtime-1' - } -} - -function deferredRuntimeSubscription(): { - promise: Promise<() => void> - resolve: (unsubscribe: () => void) => void -} { - let resolve!: (unsubscribe: () => void) => void - const promise = new Promise<() => void>((settle) => { - resolve = settle - }) - return { promise, resolve } -} - -describe('useEditorExternalWatch subscriptions', () => { - let previousApi: unknown - let container: HTMLDivElement - let root: Root - let watchWorktree: ReturnType - let unwatchWorktree: ReturnType - let unsubscribeFsEvents: ReturnType - - beforeEach(() => { - vi.clearAllMocks() - subscriptionState.contacts.clear() - subscriptionState.subscribeRuntimeFileChanges.mockReset() - subscriptionState.snapshot = { targets: [], targetsKey: '' } - watchWorktree = vi.fn().mockResolvedValue(undefined) - unwatchWorktree = vi.fn().mockResolvedValue(undefined) - unsubscribeFsEvents = vi.fn() - previousApi = (window as unknown as { api?: unknown }).api - ;(window as unknown as { api: unknown }).api = { - fs: { - watchWorktree, - unwatchWorktree, - onFsChanged: vi.fn(() => unsubscribeFsEvents) - } - } - container = document.body.appendChild(document.createElement('div')) - root = createRoot(container) - }) - - afterEach(async () => { - await act(async () => root.unmount()) - container.remove() - ;(window as unknown as { api?: unknown }).api = previousApi - }) - - it('cannot let an old runtime subscribe resolution replace a re-added watch', async () => { - const stalePending = deferredRuntimeSubscription() - const currentPending = deferredRuntimeSubscription() - const unsubscribeStale = vi.fn() - const unsubscribeCurrent = vi.fn() - subscriptionState.subscribeRuntimeFileChanges - .mockReturnValueOnce(stalePending.promise) - .mockReturnValueOnce(currentPending.promise) - subscriptionState.snapshot = { - targets: [runtimeTarget()], - targetsKey: 'runtime-watch-1' - } - await act(async () => root.render(createElement(WatchProbe))) - - subscriptionState.snapshot = { targets: [], targetsKey: 'no-runtime-watch' } - await act(async () => root.render(createElement(WatchProbe))) - subscriptionState.snapshot = { - targets: [runtimeTarget()], - targetsKey: 'runtime-watch-2' - } - await act(async () => root.render(createElement(WatchProbe))) - - currentPending.resolve(unsubscribeCurrent) - await act(async () => currentPending.promise) - stalePending.resolve(unsubscribeStale) - await act(async () => stalePending.promise) - expect(unsubscribeStale).toHaveBeenCalledTimes(1) - expect(unsubscribeCurrent).not.toHaveBeenCalled() - - await act(async () => root.unmount()) - expect(unsubscribeCurrent).toHaveBeenCalledTimes(1) - }) - - it('retries a failed initial watch when the same host answers again', async () => { - const warning = vi.spyOn(console, 'warn').mockImplementation(() => {}) - subscriptionState.subscribeRuntimeFileChanges - .mockRejectedValueOnce(new Error('connection lost')) - .mockResolvedValueOnce(vi.fn()) - subscriptionState.snapshot = { targets: [runtimeTarget()], targetsKey: 'recovering-watch' } - try { - await act(async () => root.render(createElement(WatchProbe))) - await act(async () => subscriptionState.contacts.get('runtime-1')?.()) - expect(subscriptionState.subscribeRuntimeFileChanges).toHaveBeenCalledTimes(2) - expect(subscriptionState.handleFsChanged).toHaveBeenCalledWith( - { - worktreePath: '/runtime/repo', - events: [{ kind: 'overflow', absolutePath: '/runtime/repo' }], - connectionId: 'nested-ssh', - resync: true - }, - 'runtime-1', - runtimeTarget() - ) - } finally { - warning.mockRestore() - } - }) - - it('replaces a lost watch and stops contact recovery when the editor unmounts', async () => { - const firstStop = vi.fn() - const secondStop = vi.fn() - subscriptionState.subscribeRuntimeFileChanges - .mockResolvedValueOnce(firstStop) - .mockResolvedValueOnce(secondStop) - subscriptionState.snapshot = { targets: [runtimeTarget()], targetsKey: 'live-watch' } - await act(async () => root.render(createElement(WatchProbe))) - await act(async () => subscriptionState.contacts.get('runtime-1')?.()) - expect(firstStop).toHaveBeenCalledOnce() - expect(subscriptionState.subscribeRuntimeFileChanges).toHaveBeenCalledTimes(2) - await act(async () => root.unmount()) - expect(secondStop).toHaveBeenCalledOnce() - expect(subscriptionState.unsubscribeContact).toHaveBeenCalledOnce() - subscriptionState.contacts.get('runtime-1')?.() - expect(subscriptionState.subscribeRuntimeFileChanges).toHaveBeenCalledTimes(2) - }) -}) diff --git a/src/renderer/src/hooks/useEditorExternalWatch.test.ts b/src/renderer/src/hooks/useEditorExternalWatch.test.ts deleted file mode 100644 index e0a87cb44af..00000000000 --- a/src/renderer/src/hooks/useEditorExternalWatch.test.ts +++ /dev/null @@ -1,590 +0,0 @@ -import { beforeEach, describe, expect, it, vi, afterEach } from 'vitest' -import type * as EditorAutosaveModule from '@/components/editor/autosave/editor-autosave' -import type { FsChangedPayload } from '../../../shared/file-watch/filesystem-change-event' - -vi.mock('@/store', () => ({ - useAppStore: { - getState: vi.fn() - } -})) -// Stub autosave’s window-event exports so the handler runs in headless Node. -vi.mock('@/components/editor/autosave/editor-autosave', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - notifyEditorExternalFileChange: vi.fn(), - getOpenFilesForExternalFileChange: vi.fn(() => []) - } -}) - -import { - buildEditorExternalWatchEventHandler as createExternalWatchEventHandler, - EXTERNAL_MUTATION_DEBOUNCE_MS -} from './editor-external-watch-event-reconciliation' -import { useAppStore } from '@/store' -import { createTestStore } from '@/store/testing/app-store-test-fixture' -import { - getOpenFilesForExternalFileChange, - notifyEditorExternalFileChange -} from '@/components/editor/autosave/editor-autosave' -import { - __clearSelfWriteRegistryForTests, - recordSelfWrite -} from '@/components/editor/external-changes/editor-self-write-registry' -import { __clearEditorPathMovesForTests } from '@/components/editor/editor-path-move-inflight' - -describe('createExternalWatchEventHandler tombstone coalescing', () => { - const setExternalMutation = vi.fn() - const findTarget = ( - worktreePath: string, - runtimeEnvironmentId: string | null = null - ): - | { - worktreeId: string - worktreePath: string - connectionId: string | undefined - runtimeEnvironmentId: string | null - } - | undefined => - worktreePath === '/repo' - ? { - worktreeId: 'wt-1', - worktreePath: '/repo', - connectionId: undefined, - runtimeEnvironmentId - } - : undefined - - const fileNotes = { - id: 'file-notes', - worktreeId: 'wt-1', - worktreePath: '/repo', - filePath: '/repo/notes.md', - relativePath: 'notes.md', - language: 'markdown', - mode: 'edit' as const, - isDirty: false - } - - beforeEach(() => { - vi.clearAllMocks() - vi.useFakeTimers() - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [fileNotes], - setExternalMutation - }) - }) - - afterEach(() => { - vi.useRealTimers() - vi.unstubAllGlobals() - __clearSelfWriteRegistryForTests() - __clearEditorPathMovesForTests() - }) - - function payload(events: FsChangedPayload['events']): FsChangedPayload { - return { worktreePath: '/repo', events } - } - - it('does not flash deleted when a create at the same path arrives in the next payload', () => { - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - handleFsChanged(payload([{ kind: 'delete', absolutePath: '/repo/notes.md' }])) - - vi.advanceTimersByTime(10) - expect(setExternalMutation).not.toHaveBeenCalledWith('file-notes', 'deleted') - - handleFsChanged(payload([{ kind: 'create', absolutePath: '/repo/notes.md' }])) - - vi.advanceTimersByTime(500) - expect(setExternalMutation).not.toHaveBeenCalledWith('file-notes', 'deleted') - - dispose() - }) - - it('still sets deleted after the debounce window for a naked delete', () => { - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - handleFsChanged(payload([{ kind: 'delete', absolutePath: '/repo/notes.md' }])) - - expect(setExternalMutation).not.toHaveBeenCalled() - vi.advanceTimersByTime(EXTERNAL_MUTATION_DEBOUNCE_MS) - expect(setExternalMutation).toHaveBeenCalledWith('file-notes', 'deleted') - - dispose() - }) - - it('resolves batched delete+create in a single payload synchronously as renamed', () => { - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - handleFsChanged( - payload([ - { kind: 'delete', absolutePath: '/repo/notes.md' }, - { kind: 'create', absolutePath: '/repo/subdir/notes.md' } - ]) - ) - - // Same-payload delete/create already proves recreation; reconcile without deferral. - expect(setExternalMutation).toHaveBeenCalledWith('file-notes', 'renamed') - - dispose() - }) - - it('reloads Windows editor tabs when watcher event casing differs', () => { - const file = { - id: 'file-win', - worktreeId: 'wt-win', - worktreePath: 'C:\\Repo', - filePath: 'C:\\Repo\\notes.md', - relativePath: 'notes.md', - language: 'markdown', - mode: 'edit' as const, - isDirty: false, - runtimeEnvironmentId: 'env-1' - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [file], - setExternalMutation - }) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([file] as never) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(() => ({ - worktreeId: 'wt-win', - worktreePath: 'C:\\Repo', - connectionId: undefined, - runtimeEnvironmentId: 'env-1' - })) - - handleFsChanged({ - worktreePath: 'c:\\repo', - events: [{ kind: 'update', absolutePath: 'c:\\repo\\notes.md', isDirectory: false }] - }) - vi.advanceTimersByTime(100) - - expect(notifyEditorExternalFileChange).toHaveBeenCalledWith({ - worktreeId: 'wt-win', - worktreePath: 'C:\\Repo', - relativePath: 'notes.md', - runtimeEnvironmentId: 'env-1' - }) - dispose() - }) - - it('reloads UNC editor tabs without collapsing the share root', () => { - const file = { - id: 'file-unc', - worktreeId: 'wt-unc', - worktreePath: '//Server/Share/Repo', - filePath: '//Server/Share/Repo/notes.md', - relativePath: 'notes.md', - language: 'markdown', - mode: 'edit' as const, - isDirty: false, - runtimeEnvironmentId: 'env-1' - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [file], - setExternalMutation - }) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([file] as never) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(() => ({ - worktreeId: 'wt-unc', - worktreePath: '//Server/Share/Repo', - connectionId: undefined, - runtimeEnvironmentId: 'env-1' - })) - - handleFsChanged({ - worktreePath: '//server/share/repo', - events: [{ kind: 'update', absolutePath: '//server/share/repo/notes.md', isDirectory: false }] - }) - vi.advanceTimersByTime(100) - - expect(notifyEditorExternalFileChange).toHaveBeenCalledWith({ - worktreeId: 'wt-unc', - worktreePath: '//Server/Share/Repo', - relativePath: 'notes.md', - runtimeEnvironmentId: 'env-1' - }) - dispose() - }) - - it('does not drop external edits that arrive inside the self-write TTL', async () => { - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [fileNotes], - setExternalMutation - }) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([fileNotes] as never) - const readFile = vi.fn().mockResolvedValue({ content: 'agent edit', isBinary: false }) - vi.stubGlobal('window', { api: { fs: { readFile } } }) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - recordSelfWrite('/repo/notes.md', 'orca save') - handleFsChanged(payload([{ kind: 'update', absolutePath: '/repo/notes.md' }])) - await vi.advanceTimersByTimeAsync(100) - - expect(notifyEditorExternalFileChange).toHaveBeenCalledWith({ - worktreeId: 'wt-1', - worktreePath: '/repo', - relativePath: 'notes.md', - runtimeEnvironmentId: null - }) - dispose() - }) - - it('still suppresses the watcher echo from Orca self-writes', async () => { - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [fileNotes], - setExternalMutation - }) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([fileNotes] as never) - const readFile = vi.fn().mockResolvedValue({ content: 'orca save', isBinary: false }) - vi.stubGlobal('window', { api: { fs: { readFile } } }) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - recordSelfWrite('/repo/notes.md', 'orca save') - handleFsChanged(payload([{ kind: 'update', absolutePath: '/repo/notes.md' }])) - await vi.advanceTimersByTimeAsync(100) - - expect(notifyEditorExternalFileChange).not.toHaveBeenCalled() - dispose() - }) - - it('does not let a local self-write stamp suppress a runtime owner update', async () => { - const runtimeFile = { - ...fileNotes, - runtimeEnvironmentId: 'env-1' - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [runtimeFile], - setExternalMutation - }) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([runtimeFile] as never) - const readFile = vi.fn().mockResolvedValue({ content: 'local save', isBinary: false }) - vi.stubGlobal('window', { api: { fs: { readFile } } }) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - recordSelfWrite('/repo/notes.md', 'local save', null) - handleFsChanged(payload([{ kind: 'update', absolutePath: '/repo/notes.md' }]), 'env-1') - await vi.advanceTimersByTimeAsync(100) - - expect(notifyEditorExternalFileChange).toHaveBeenCalledWith({ - worktreeId: 'wt-1', - worktreePath: '/repo', - relativePath: 'notes.md', - runtimeEnvironmentId: 'env-1' - }) - dispose() - }) - - it('routes local and runtime watch events to the matching editor owner', () => { - const localFile = fileNotes - const runtimeFile = { - ...fileNotes, - id: 'runtime-notes', - runtimeEnvironmentId: 'env-1' - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [localFile, runtimeFile], - setExternalMutation - }) - vi.mocked(getOpenFilesForExternalFileChange).mockImplementation((_files, notification) => - notification.runtimeEnvironmentId === 'env-1' - ? ([runtimeFile] as never) - : ([localFile] as never) - ) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payload([{ kind: 'update', absolutePath: '/repo/notes.md' }]), null) - handleFsChanged(payload([{ kind: 'update', absolutePath: '/repo/notes.md' }]), 'env-1') - vi.advanceTimersByTime(100) - - expect(notifyEditorExternalFileChange).toHaveBeenCalledWith({ - worktreeId: 'wt-1', - worktreePath: '/repo', - relativePath: 'notes.md', - runtimeEnvironmentId: null - }) - expect(notifyEditorExternalFileChange).toHaveBeenCalledWith({ - worktreeId: 'wt-1', - worktreePath: '/repo', - relativePath: 'notes.md', - runtimeEnvironmentId: 'env-1' - }) - dispose() - }) - - it('reloads a combined Changes tab even though no single-file tab matches', () => { - const combinedDiffTab = { - id: 'wt-1::all-diffs::uncommitted', - worktreeId: 'wt-1', - worktreePath: '/repo', - filePath: '/repo', - relativePath: 'Changes', - language: 'markdown', - mode: 'diff' as const, - diffSource: 'combined-uncommitted' as const, - isDirty: false - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [combinedDiffTab], - setExternalMutation - }) - // Why: combined tabs match no single path, so the per-path lookup stays empty. - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([]) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payload([{ kind: 'update', absolutePath: '/repo/src/foo.ts' }])) - vi.advanceTimersByTime(100) - - expect(notifyEditorExternalFileChange).toHaveBeenCalledWith({ - worktreeId: 'wt-1', - worktreePath: '/repo', - relativePath: 'src/foo.ts', - runtimeEnvironmentId: null - }) - dispose() - }) - - it('reloads a combined Changes tab when the matching single-file tab is dirty', () => { - const dirtyFile = { - ...fileNotes, - isDirty: true - } - const combinedDiffTab = { - id: 'wt-1::all-diffs::uncommitted', - worktreeId: 'wt-1', - worktreePath: '/repo', - filePath: '/repo', - relativePath: 'Changes', - language: 'markdown', - mode: 'diff' as const, - diffSource: 'combined-uncommitted' as const, - isDirty: false - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [dirtyFile, combinedDiffTab], - setExternalMutation - }) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyFile] as never) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payload([{ kind: 'update', absolutePath: '/repo/notes.md' }])) - vi.advanceTimersByTime(100) - - expect(notifyEditorExternalFileChange).toHaveBeenCalledWith({ - worktreeId: 'wt-1', - worktreePath: '/repo', - relativePath: 'notes.md', - runtimeEnvironmentId: null - }) - dispose() - }) - - it('marks a dirty edit tab changed-on-disk instead of reloading it', () => { - const dirtyFile = { - ...fileNotes, - isDirty: true - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [dirtyFile], - setExternalMutation - }) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyFile] as never) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payload([{ kind: 'update', absolutePath: '/repo/notes.md' }])) - vi.advanceTimersByTime(EXTERNAL_MUTATION_DEBOUNCE_MS) - - expect(setExternalMutation).toHaveBeenCalledWith('file-notes', 'changed') - expect(notifyEditorExternalFileChange).not.toHaveBeenCalled() - dispose() - }) - - it('still reloads a clean sibling diff tab when the edit tab for the path is dirty', () => { - const dirtyFile = { - ...fileNotes, - isDirty: true - } - const cleanDiffTab = { - id: 'diff-notes', - worktreeId: 'wt-1', - worktreePath: '/repo', - filePath: '/repo/notes.md', - relativePath: 'notes.md', - language: 'markdown', - mode: 'diff' as const, - diffSource: 'unstaged' as const, - isDirty: false - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [dirtyFile, cleanDiffTab], - setExternalMutation - }) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyFile, cleanDiffTab] as never) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payload([{ kind: 'update', absolutePath: '/repo/notes.md' }])) - vi.advanceTimersByTime(EXTERNAL_MUTATION_DEBOUNCE_MS) - - expect(setExternalMutation).toHaveBeenCalledWith('file-notes', 'changed') - expect(notifyEditorExternalFileChange).toHaveBeenCalledWith({ - worktreeId: 'wt-1', - worktreePath: '/repo', - relativePath: 'notes.md', - runtimeEnvironmentId: null - }) - dispose() - }) - - it('does not mark a dirty tab for the echo of Orca’s own save', async () => { - const dirtyFile = { - ...fileNotes, - isDirty: true - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [dirtyFile], - setExternalMutation - }) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyFile] as never) - const readFile = vi.fn().mockResolvedValue({ content: 'orca save', isBinary: false }) - vi.stubGlobal('window', { api: { fs: { readFile } } }) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - recordSelfWrite('/repo/notes.md', 'orca save') - handleFsChanged(payload([{ kind: 'update', absolutePath: '/repo/notes.md' }])) - await vi.advanceTimersByTimeAsync(100) - - expect(setExternalMutation).not.toHaveBeenCalledWith('file-notes', 'changed') - dispose() - }) - - it('marks a dirty tab when a genuine external write lands inside the self-write TTL', async () => { - const dirtyFile = { - ...fileNotes, - isDirty: true - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [dirtyFile], - setExternalMutation - }) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyFile] as never) - const readFile = vi.fn().mockResolvedValue({ content: 'agent content', isBinary: false }) - vi.stubGlobal('window', { api: { fs: { readFile } } }) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - recordSelfWrite('/repo/notes.md', 'orca save') - handleFsChanged(payload([{ kind: 'update', absolutePath: '/repo/notes.md' }])) - await vi.advanceTimersByTimeAsync(100) - - expect(setExternalMutation).toHaveBeenCalledWith('file-notes', 'changed') - dispose() - }) - - it('marks a lone dirty unstaged-diff tab changed-on-disk', () => { - const dirtyDiffTab = { - id: 'diff-notes', - worktreeId: 'wt-1', - worktreePath: '/repo', - filePath: '/repo/notes.md', - relativePath: 'notes.md', - language: 'markdown', - mode: 'diff' as const, - diffSource: 'unstaged' as const, - isDirty: true - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [dirtyDiffTab], - setExternalMutation - }) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyDiffTab] as never) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payload([{ kind: 'update', absolutePath: '/repo/notes.md' }])) - vi.advanceTimersByTime(EXTERNAL_MUTATION_DEBOUNCE_MS) - - expect(setExternalMutation).toHaveBeenCalledWith('diff-notes', 'changed') - expect(notifyEditorExternalFileChange).not.toHaveBeenCalled() - dispose() - }) - - it('does not let an update event clear a changed-on-disk mark while the tab stays dirty', () => { - const dirtyChangedFile = { - ...fileNotes, - isDirty: true, - externalMutation: 'changed' as const - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [dirtyChangedFile], - setExternalMutation - }) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyChangedFile] as never) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payload([{ kind: 'update', absolutePath: '/repo/notes.md' }])) - vi.advanceTimersByTime(EXTERNAL_MUTATION_DEBOUNCE_MS) - - expect(setExternalMutation).not.toHaveBeenCalledWith('file-notes', null) - dispose() - }) - - it('does not reload a branch-compare combined diff for working-tree changes', () => { - const branchDiffTab = { - id: 'wt-1::all-diffs::branch', - worktreeId: 'wt-1', - worktreePath: '/repo', - filePath: '/repo', - relativePath: 'Branch Changes', - language: 'markdown', - mode: 'diff' as const, - diffSource: 'combined-branch' as const, - isDirty: false - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [branchDiffTab], - setExternalMutation - }) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([]) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payload([{ kind: 'update', absolutePath: '/repo/src/foo.ts' }])) - vi.advanceTimersByTime(100) - - expect(notifyEditorExternalFileChange).not.toHaveBeenCalled() - dispose() - }) - - it('tombstones only the matching owner for same-path delete events', () => { - const localFile = fileNotes - const runtimeFile = { - ...fileNotes, - id: 'runtime-notes', - runtimeEnvironmentId: 'env-1' - } - vi.mocked(useAppStore.getState).mockReturnValue({ - ...createTestStore().getState(), - openFiles: [localFile, runtimeFile], - setExternalMutation - }) - const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) - - handleFsChanged(payload([{ kind: 'delete', absolutePath: '/repo/notes.md' }]), 'env-1') - vi.advanceTimersByTime(EXTERNAL_MUTATION_DEBOUNCE_MS) - - expect(setExternalMutation).toHaveBeenCalledWith('runtime-notes', 'deleted') - expect(setExternalMutation).not.toHaveBeenCalledWith('file-notes', 'deleted') - dispose() - }) -}) diff --git a/src/renderer/src/hooks/useIpcEvents-agent-status-connection-attribution.test.ts b/src/renderer/src/hooks/useIpcEvents-agent-status-connection-attribution.test.ts deleted file mode 100644 index d527927a11b..00000000000 --- a/src/renderer/src/hooks/useIpcEvents-agent-status-connection-attribution.test.ts +++ /dev/null @@ -1,480 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { - buildStoreState, - expectWorktreeRouting, - FUTURE_LEAF_ID, - ORPHAN_LEAF_ID, - TAB_1_LEAF_ID, - STALE_PANE_KEY, - ORPHAN_PANE_KEY, - TAB_1_PANE_KEY, - type AgentStatusSetData, - type StoreLike, - type StoreSubscribeListener -} from './ipc-events-agent-status-store-test-fixtures' -import { - buildWindowApi, - stubReactSyncEffect, - stubAuxiliaryModules -} from './ipc-events-agent-status-window-test-fixtures' - -describe('useIpcEvents agent status snapshot integration', () => { - beforeEach(() => { - vi.resetModules() - vi.unstubAllGlobals() - }) - - it('pulls the snapshot once workspace session is ready even before settings load', async () => { - const setAgentStatus = vi.fn() - const getSnapshot = vi.fn(() => Promise.resolve([])) - const subscribeListenerRef: { current: StoreSubscribeListener | null } = { current: null } - - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - tabsByWorktree: {}, - workspaceSessionReady: true, - settings: null - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn((listener: StoreSubscribeListener) => { - subscribeListenerRef.current = listener - return () => { - subscribeListenerRef.current = null - } - }), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - getSnapshot, - onSet: () => () => {} - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - - useIpcEvents() - await Promise.resolve() - expect(getSnapshot).toHaveBeenCalledTimes(1) - expect(setAgentStatus).not.toHaveBeenCalled() - }) - - it('silently discards snapshot entries whose tabs are still unknown', async () => { - const setAgentStatus = vi.fn() - const getSnapshot = vi.fn(() => - Promise.resolve([ - { - paneKey: ORPHAN_PANE_KEY, - state: 'done' as const, - prompt: 'p', - agentType: 'claude', - receivedAt: 1_700_000_000_000, - stateStartedAt: 1_699_999_999_000 - } - ]) - ) - - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - tabsByWorktree: { - 'wt-1': [{ id: 'tab-other', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'Other' }] - }, - terminalLayoutsByTabId: { - 'tab-orphan': { - root: { type: 'leaf', leafId: ORPHAN_LEAF_ID }, - activeLeafId: ORPHAN_LEAF_ID, - expandedLeafId: null - } - }, - workspaceSessionReady: true - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - getSnapshot, - onSet: () => () => {} - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - - useIpcEvents() - await Promise.resolve() - await Promise.resolve() - - expect(setAgentStatus).not.toHaveBeenCalled() - }) - - it('silently discards stale worktree-attributed snapshots for unknown panes', async () => { - const setAgentStatus = vi.fn() - const getSnapshot = vi.fn(() => - Promise.resolve([ - { - paneKey: ORPHAN_PANE_KEY, - state: 'done' as const, - prompt: 'old copilot turn', - agentType: 'copilot', - worktreeId: 'wt-1', - receivedAt: 1_700_000_000_000, - stateStartedAt: 1_699_999_999_000 - } - ]) - ) - - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - repos: [{ id: 'repo-1', connectionId: null }], - worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] }, - tabsByWorktree: { - 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'Copilot' }] - }, - terminalLayoutsByTabId: { - 'tab-future': { - root: { type: 'leaf', leafId: FUTURE_LEAF_ID }, - activeLeafId: FUTURE_LEAF_ID, - expandedLeafId: null - } - }, - workspaceSessionReady: true - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - getSnapshot, - onSet: () => () => {} - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - - useIpcEvents() - await Promise.resolve() - await Promise.resolve() - - expect(setAgentStatus).not.toHaveBeenCalled() - }) - - it('applies worktree-attributed child snapshots when runtime identity is present', async () => { - const setAgentStatus = vi.fn() - const getSnapshot = vi.fn(() => - Promise.resolve([ - { - paneKey: ORPHAN_PANE_KEY, - state: 'working' as const, - prompt: 'child task', - agentType: 'codex', - worktreeId: 'wt-1', - terminalHandle: 'term-child', - orchestration: { - taskId: 'task-child', - dispatchId: 'dispatch-child', - parentTerminalHandle: 'term-parent' - }, - receivedAt: 1_700_000_000_000, - stateStartedAt: 1_699_999_999_000 - } - ]) - ) - - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - repos: [{ id: 'repo-1', connectionId: null }], - worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] }, - tabsByWorktree: { - 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'Codex' }] - }, - terminalLayoutsByTabId: { - 'tab-future': { - root: { type: 'leaf', leafId: FUTURE_LEAF_ID }, - activeLeafId: FUTURE_LEAF_ID, - expandedLeafId: null - } - }, - workspaceSessionReady: true - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - getSnapshot, - onSet: () => () => {} - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - - useIpcEvents() - await Promise.resolve() - await Promise.resolve() - - expect(setAgentStatus).toHaveBeenCalledTimes(1) - expect(setAgentStatus).toHaveBeenCalledWith( - ORPHAN_PANE_KEY, - expect.objectContaining({ - state: 'working', - prompt: 'child task', - agentType: 'codex', - orchestration: expect.objectContaining({ taskId: 'task-child' }) - }), - undefined, - { updatedAt: 1_700_000_000_000, stateStartedAt: 1_699_999_999_000 }, - expect.objectContaining({ worktreeId: 'wt-1', terminalHandle: 'term-child' }), - undefined - ) - }) - - it('silently discards valid paneKeys whose leaf is not in the current layout', async () => { - const setAgentStatus = vi.fn() - const getSnapshot = vi.fn(() => - Promise.resolve([ - { - paneKey: STALE_PANE_KEY, - state: 'done' as const, - prompt: 'p', - agentType: 'claude', - receivedAt: 1_700_000_000_000, - stateStartedAt: 1_699_999_999_000 - } - ]) - ) - - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - tabsByWorktree: { - 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'Future Tab' }] - }, - terminalLayoutsByTabId: { - 'tab-future': { - root: { type: 'leaf', leafId: FUTURE_LEAF_ID }, - activeLeafId: FUTURE_LEAF_ID, - expandedLeafId: null - } - }, - workspaceSessionReady: true - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - getSnapshot, - onSet: () => () => {} - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - - useIpcEvents() - await Promise.resolve() - await Promise.resolve() - - expect(setAgentStatus).not.toHaveBeenCalled() - }) - - it('forwards events whose connectionId matches the live repo connection', async () => { - const setAgentStatus = vi.fn() - const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = { - current: null - } - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - workspaceSessionReady: true, - repos: [{ id: 'repo-1', connectionId: 'conn-1' }], - worktreesByRepo: { - 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] - }, - tabsByWorktree: { - 'wt-1': [{ id: 'tab-1', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'Terminal 1' }] - }, - terminalLayoutsByTabId: { - 'tab-1': { - root: { type: 'leaf', leafId: TAB_1_LEAF_ID }, - activeLeafId: TAB_1_LEAF_ID, - expandedLeafId: null - } - } - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { subscribe: vi.fn(() => () => {}), getState: () => storeState } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: (cb) => { - onSetListenerRef.current = cb - return () => {} - } - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - useIpcEvents() - await Promise.resolve() - - onSetListenerRef.current?.({ - paneKey: TAB_1_PANE_KEY, - connectionId: 'conn-1', - state: 'working', - receivedAt: 1_700_000_000_100, - stateStartedAt: 1_699_999_999_100 - }) - - expect(setAgentStatus).toHaveBeenCalledWith( - TAB_1_PANE_KEY, - expect.objectContaining({ state: 'working' }), - 'Terminal 1', - { updatedAt: 1_700_000_000_100, stateStartedAt: 1_699_999_999_100 }, - expectWorktreeRouting('wt-1'), - undefined - ) - }) - - it('drops events whose connectionId no longer matches the live local repo', async () => { - const setAgentStatus = vi.fn() - const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = { - current: null - } - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - workspaceSessionReady: true, - repos: [{ id: 'repo-1', connectionId: null }], - worktreesByRepo: { - 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] - }, - tabsByWorktree: { - 'wt-1': [{ id: 'tab-1', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'Terminal 1' }] - }, - terminalLayoutsByTabId: { - 'tab-1': { - root: { type: 'leaf', leafId: TAB_1_LEAF_ID }, - activeLeafId: TAB_1_LEAF_ID, - expandedLeafId: null - } - } - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { subscribe: vi.fn(() => () => {}), getState: () => storeState } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: (cb) => { - onSetListenerRef.current = cb - return () => {} - } - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - useIpcEvents() - await Promise.resolve() - - onSetListenerRef.current?.({ - paneKey: TAB_1_PANE_KEY, - connectionId: 'conn-stale', - state: 'working', - receivedAt: 1_700_000_000_100, - stateStartedAt: 1_699_999_999_100 - }) - - expect(setAgentStatus).not.toHaveBeenCalled() - }) - - it('accepts events without a stamped connectionId for preload compatibility', async () => { - const setAgentStatus = vi.fn() - const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = { - current: null - } - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - workspaceSessionReady: true, - repos: [{ id: 'repo-1', connectionId: 'conn-1' }], - worktreesByRepo: { - 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] - }, - tabsByWorktree: { - 'wt-1': [{ id: 'tab-1', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'Terminal 1' }] - }, - terminalLayoutsByTabId: { - 'tab-1': { - root: { type: 'leaf', leafId: TAB_1_LEAF_ID }, - activeLeafId: TAB_1_LEAF_ID, - expandedLeafId: null - } - } - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { subscribe: vi.fn(() => () => {}), getState: () => storeState } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: (cb) => { - onSetListenerRef.current = cb - return () => {} - } - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - useIpcEvents() - await Promise.resolve() - - onSetListenerRef.current?.({ - paneKey: TAB_1_PANE_KEY, - state: 'working', - receivedAt: 1_700_000_000_100, - stateStartedAt: 1_699_999_999_100 - }) - - expect(setAgentStatus).toHaveBeenCalledTimes(1) - }) -}) diff --git a/src/renderer/src/hooks/useIpcEvents-agent-status-pane-teardown.test.ts b/src/renderer/src/hooks/useIpcEvents-agent-status-pane-teardown.test.ts deleted file mode 100644 index 7c8f5cbd762..00000000000 --- a/src/renderer/src/hooks/useIpcEvents-agent-status-pane-teardown.test.ts +++ /dev/null @@ -1,444 +0,0 @@ -import type * as ReactModule from 'react' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { AgentStatusClearIpcPayload } from '../../../shared/agent-status/contracts/agent-status-ipc-payload' -import { - buildStoreState, - expectWorktreeRouting, - FUTURE_LEAF_ID, - FUTURE_PANE_KEY, - type AgentStatusSetData, - type StoreLike -} from './ipc-events-agent-status-store-test-fixtures' -import { - buildWindowApi, - stubReactSyncEffect, - stubAuxiliaryModules -} from './ipc-events-agent-status-window-test-fixtures' - -describe('useIpcEvents agent status snapshot integration', () => { - beforeEach(() => { - vi.resetModules() - vi.unstubAllGlobals() - }) - - it('does not send an out-of-order hook event to completion lifecycle tracking', async () => { - const setAgentStatus = vi.fn() - const updateTabTitle = vi.fn() - const observeAgentHookCompletionForNotification = vi.fn() - const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = { - current: null - } - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - updateTabTitle, - workspaceSessionReady: true, - settings: { terminalFontSize: 13, notifications: { enabled: true, agentTaskComplete: true } }, - tabsByWorktree: { - 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'Claude' }] - }, - agentStatusByPaneKey: { - [FUTURE_PANE_KEY]: { - state: 'working', - prompt: 'newer turn', - agentType: 'claude', - updatedAt: 1_700_000_000_500, - stateStartedAt: 1_700_000_000_400 - } - } - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - vi.doMock('./agent-hook-completion-notifications', () => ({ - observeAgentHookCompletionForNotification, - resetAgentHookCompletionNotificationCoordinators: vi.fn(), - syncAgentHookCompletionNotificationSettings: vi.fn() - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: (cb) => { - onSetListenerRef.current = cb - return () => {} - } - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - useIpcEvents() - await Promise.resolve() - - if (typeof onSetListenerRef.current !== 'function') { - throw new Error('Expected agentStatus.onSet listener to be registered') - } - onSetListenerRef.current({ - paneKey: FUTURE_PANE_KEY, - tabId: 'tab-future', - worktreeId: 'wt-1', - state: 'done', - prompt: 'older turn', - agentType: 'claude', - receivedAt: 1_700_000_000_300, - stateStartedAt: 1_700_000_000_200 - }) - - expect(setAgentStatus).not.toHaveBeenCalled() - expect(updateTabTitle).not.toHaveBeenCalled() - expect(observeAgentHookCompletionForNotification).not.toHaveBeenCalled() - }) - - it('keeps missing-tab runtime attribution for tabs that were never explicitly closed', async () => { - const setAgentStatus = vi.fn() - const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = { - current: null - } - - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - workspaceSessionReady: true, - recentlyClosedAgentStatusTabIds: {}, - settings: { terminalFontSize: 13, notifications: { enabled: false } }, - repos: [{ id: 'repo-1', connectionId: null }], - worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] }, - tabsByWorktree: { 'wt-1': [] }, - terminalLayoutsByTabId: {} - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: (cb) => { - onSetListenerRef.current = cb - return () => {} - } - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - - useIpcEvents() - await Promise.resolve() - - if (typeof onSetListenerRef.current !== 'function') { - throw new Error('Expected agentStatus.onSet listener to be registered') - } - - onSetListenerRef.current({ - paneKey: FUTURE_PANE_KEY, - state: 'working', - prompt: 'runtime child', - agentType: 'codex', - worktreeId: 'wt-1', - receivedAt: 1_700_000_000_200, - stateStartedAt: 1_700_000_000_000, - orchestration: { - parentPaneKey: 'parent-tab:11111111-1111-4111-8111-111111111111' - } - }) - - expect(setAgentStatus).toHaveBeenCalledTimes(1) - expect(setAgentStatus).toHaveBeenCalledWith( - FUTURE_PANE_KEY, - expect.objectContaining({ state: 'working', prompt: 'runtime child', agentType: 'codex' }), - undefined, - { updatedAt: 1_700_000_000_200, stateStartedAt: 1_700_000_000_000 }, - expectWorktreeRouting('wt-1'), - undefined - ) - }) - - it('clears a worktree-attributed live row when main reports pane teardown', async () => { - const setAgentStatus = vi.fn() - const removeAgentStatus = vi.fn() - const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = { - current: null - } - const onClearListenerRef: { - current: ((data: AgentStatusClearIpcPayload) => void) | null - } = { - current: null - } - - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - removeAgentStatus, - workspaceSessionReady: true, - settings: { terminalFontSize: 13, notifications: { enabled: false } }, - repos: [{ id: 'repo-1', connectionId: null }], - worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] }, - tabsByWorktree: { 'wt-1': [] }, - terminalLayoutsByTabId: {}, - agentStatusByPaneKey: { - [FUTURE_PANE_KEY]: { - state: 'working', - prompt: 'hidden worker', - agentType: 'codex', - updatedAt: 1_700_000_000_200, - stateStartedAt: 1_700_000_000_000, - paneKey: FUTURE_PANE_KEY, - worktreeId: 'wt-1', - stateHistory: [] - } - } - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: (cb) => { - onSetListenerRef.current = cb - return () => {} - }, - onClear: (cb) => { - onClearListenerRef.current = cb - return () => {} - } - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - - useIpcEvents() - await Promise.resolve() - - if (typeof onSetListenerRef.current !== 'function') { - throw new Error('Expected agentStatus.onSet listener to be registered') - } - if (typeof onClearListenerRef.current !== 'function') { - throw new Error('Expected agentStatus.onClear listener to be registered') - } - - onSetListenerRef.current({ - paneKey: FUTURE_PANE_KEY, - state: 'working', - prompt: 'hidden worker', - agentType: 'codex', - worktreeId: 'wt-1', - receivedAt: 1_700_000_000_200, - stateStartedAt: 1_700_000_000_000, - orchestration: { - parentPaneKey: 'parent-tab:11111111-1111-4111-8111-111111111111' - } - }) - - expect(setAgentStatus).toHaveBeenCalledTimes(1) - expect(setAgentStatus).toHaveBeenCalledWith( - FUTURE_PANE_KEY, - expect.objectContaining({ state: 'working', prompt: 'hidden worker', agentType: 'codex' }), - undefined, - { updatedAt: 1_700_000_000_200, stateStartedAt: 1_700_000_000_000 }, - expectWorktreeRouting('wt-1'), - undefined - ) - - onClearListenerRef.current({ paneKey: FUTURE_PANE_KEY }) - - expect(removeAgentStatus).toHaveBeenCalledTimes(1) - expect(removeAgentStatus).toHaveBeenCalledWith(FUTURE_PANE_KEY) - }) - - it('blocks cleared snapshots across remount and accepts newer reconnect replay', async () => { - let resolveOldSnapshot!: (entries: AgentStatusSetData[]) => void - let resolveCurrentSnapshot!: (entries: AgentStatusSetData[]) => void - const oldSnapshot = new Promise((resolve) => { - resolveOldSnapshot = resolve - }) - const currentSnapshot = new Promise((resolve) => { - resolveCurrentSnapshot = resolve - }) - const effectCleanups: (() => void)[] = [] - const setAgentStatus = vi.fn() - const clearTransientAgentStatuses = vi.fn() - const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = { - current: null - } - const onClearListenerRef: { - current: ((data: AgentStatusClearIpcPayload) => void) | null - } = { current: null } - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - clearTransientAgentStatuses, - workspaceSessionReady: true, - settings: { terminalFontSize: 13, notifications: { enabled: false } }, - repos: [{ id: 'repo-1', connectionId: 'ssh-a' }], - worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] }, - tabsByWorktree: { - 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'Remote agent' }] - }, - terminalLayoutsByTabId: { - 'tab-future': { root: { type: 'leaf', leafId: FUTURE_LEAF_ID } } - } - }) - - vi.doMock('react', async () => { - const actual = await vi.importActual('react') - return { - ...actual, - useEffect: (effect: () => void | (() => void)) => { - const cleanup = effect() - effectCleanups.push(typeof cleanup === 'function' ? cleanup : () => {}) - } - } - }) - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: (callback) => { - onSetListenerRef.current = callback - return () => {} - }, - onClear: (callback) => { - onClearListenerRef.current = callback - return () => {} - }, - getSnapshot: vi.fn().mockReturnValueOnce(oldSnapshot).mockReturnValueOnce(currentSnapshot) - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - useIpcEvents() - await Promise.resolve() - effectCleanups[0]?.() - useIpcEvents() - await Promise.resolve() - if (!onSetListenerRef.current || !onClearListenerRef.current) { - throw new Error('Expected agent status listeners to be registered') - } - - expect(() => - onClearListenerRef.current?.(null as unknown as AgentStatusClearIpcPayload) - ).not.toThrow() - expect(clearTransientAgentStatuses).not.toHaveBeenCalled() - - onClearListenerRef.current({ - transient: true, - connectionId: 'ssh-a', - clearedAt: 100 - }) - const staleEntry: AgentStatusSetData = { - paneKey: FUTURE_PANE_KEY, - state: 'working', - prompt: 'stale snapshot', - agentType: 'codex', - worktreeId: 'wt-1', - connectionId: 'ssh-a', - receivedAt: 100, - stateStartedAt: 90 - } - resolveOldSnapshot([staleEntry]) - resolveCurrentSnapshot([staleEntry]) - await Promise.resolve() - await Promise.resolve() - - expect(clearTransientAgentStatuses).toHaveBeenCalledWith('ssh-a', 100) - expect(setAgentStatus).not.toHaveBeenCalled() - - onSetListenerRef.current({ - paneKey: FUTURE_PANE_KEY, - state: 'working', - prompt: 'replayed', - agentType: 'codex', - worktreeId: 'wt-1', - connectionId: 'ssh-a', - receivedAt: 101, - stateStartedAt: 101 - }) - - expect(setAgentStatus).toHaveBeenCalledOnce() - expect(setAgentStatus).toHaveBeenCalledWith( - FUTURE_PANE_KEY, - expect.objectContaining({ prompt: 'replayed' }), - 'Remote agent', - { updatedAt: 101, stateStartedAt: 101 }, - expect.objectContaining({ worktreeId: 'wt-1', connectionId: 'ssh-a' }), - undefined - ) - }) - - it('keeps a completed worktree-attributed row when main reports pane teardown', async () => { - const removeAgentStatus = vi.fn() - const onClearListenerRef: { - current: ((data: AgentStatusClearIpcPayload) => void) | null - } = { - current: null - } - - const storeState: StoreLike = buildStoreState({ - removeAgentStatus, - workspaceSessionReady: true, - agentStatusByPaneKey: { - [FUTURE_PANE_KEY]: { - state: 'done', - prompt: 'hidden worker', - agentType: 'codex', - updatedAt: 1_700_000_000_200, - stateStartedAt: 1_700_000_000_000, - paneKey: FUTURE_PANE_KEY, - worktreeId: 'wt-1', - stateHistory: [] - } - } - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: () => () => {}, - onClear: (cb) => { - onClearListenerRef.current = cb - return () => {} - } - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - - useIpcEvents() - await Promise.resolve() - - if (typeof onClearListenerRef.current !== 'function') { - throw new Error('Expected agentStatus.onClear listener to be registered') - } - - onClearListenerRef.current({ paneKey: FUTURE_PANE_KEY }) - - expect(removeAgentStatus).not.toHaveBeenCalled() - }) -}) diff --git a/src/renderer/src/hooks/useIpcEvents-agent-status-queue-ordering.test.ts b/src/renderer/src/hooks/useIpcEvents-agent-status-queue-ordering.test.ts deleted file mode 100644 index 373bda1dbab..00000000000 --- a/src/renderer/src/hooks/useIpcEvents-agent-status-queue-ordering.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { AgentStatusUpdate } from '../store/slices/agent-status/agent-status-contract' -import { - buildStoreState, - FUTURE_LEAF_ID, - FUTURE_PANE_KEY, - type AgentStatusSetData, - type StoreLike, - type StoreSubscribeListener -} from './ipc-events-agent-status-store-test-fixtures' -import { - buildWindowApi, - stubReactSyncEffect, - stubAuxiliaryModules -} from './ipc-events-agent-status-window-test-fixtures' - -describe('useIpcEvents agent status snapshot integration', () => { - beforeEach(() => { - vi.resetModules() - vi.unstubAllGlobals() - }) - - afterEach(() => { - vi.useRealTimers() - }) - - it('preserves queued set-clear order for working removal and done retention', async () => { - vi.useFakeTimers() - let storeState: StoreLike - const applyStatusUpdate = ( - paneKey: string, - payload: { state: string }, - timing: { updatedAt?: number } | undefined - ): void => { - storeState.agentStatusByPaneKey = { - ...(storeState.agentStatusByPaneKey as Record), - [paneKey]: { ...payload, updatedAt: timing?.updatedAt } - } - } - const setAgentStatus = vi.fn( - ( - paneKey: string, - payload: { state: string }, - _title: unknown, - timing: { updatedAt?: number } | undefined - ) => { - applyStatusUpdate(paneKey, payload, timing) - } - ) - const setAgentStatuses = vi.fn((updates: readonly AgentStatusUpdate[]) => { - for (const update of updates) { - applyStatusUpdate(update.paneKey, update.payload, update.timing) - } - return updates.map(() => true) - }) - const removeAgentStatus = vi.fn((paneKey: string) => { - const next = { ...(storeState.agentStatusByPaneKey as Record) } - delete next[paneKey] - storeState.agentStatusByPaneKey = next - }) - const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = { - current: null - } - const onClearListenerRef: { current: ((data: { paneKey: string }) => void) | null } = { - current: null - } - storeState = buildStoreState({ - setAgentStatus, - setAgentStatuses, - removeAgentStatus, - workspaceSessionReady: true, - tabsByWorktree: { - 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'Future Tab' }] - }, - terminalLayoutsByTabId: { - 'tab-future': { - root: { type: 'leaf', leafId: FUTURE_LEAF_ID }, - activeLeafId: FUTURE_LEAF_ID, - expandedLeafId: null - } - } - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: (cb) => { - onSetListenerRef.current = cb - return () => {} - }, - onClear: (cb) => { - onClearListenerRef.current = cb as (data: { paneKey: string }) => void - return () => {} - } - }) - ) - - try { - const { useIpcEvents } = await import('./useIpcEvents') - useIpcEvents() - if ( - typeof onSetListenerRef.current !== 'function' || - typeof onClearListenerRef.current !== 'function' - ) { - throw new Error('Expected agentStatus listeners to be registered') - } - const emit = ( - receivedAt: number, - prompt: string, - state: AgentStatusSetData['state'] = 'working' - ): void => { - onSetListenerRef.current!({ - paneKey: FUTURE_PANE_KEY, - state, - prompt, - agentType: 'claude', - receivedAt, - stateStartedAt: receivedAt - }) - } - - emit(1_700_000_000_000, 'leading') - emit(1_700_000_000_001, 'queued') - expect(setAgentStatus).toHaveBeenCalledTimes(1) - - onClearListenerRef.current({ paneKey: FUTURE_PANE_KEY }) - expect(removeAgentStatus).toHaveBeenCalledWith(FUTURE_PANE_KEY) - expect(storeState.agentStatusByPaneKey).toEqual({}) - - vi.advanceTimersByTime(40) - expect(setAgentStatus).toHaveBeenCalledTimes(1) - expect(setAgentStatuses).toHaveBeenCalledTimes(1) - expect(storeState.agentStatusByPaneKey).toEqual({}) - - emit(1_700_000_000_002, 'task') - emit(1_700_000_000_003, 'task', 'done') - expect(setAgentStatus).toHaveBeenCalledTimes(2) - - onClearListenerRef.current({ paneKey: FUTURE_PANE_KEY }) - - expect(setAgentStatus).toHaveBeenCalledTimes(2) - expect(setAgentStatuses).toHaveBeenCalledTimes(2) - expect(setAgentStatuses.mock.calls[1][0][0]).toEqual( - expect.objectContaining({ payload: expect.objectContaining({ state: 'done' }) }) - ) - expect(removeAgentStatus).toHaveBeenCalledTimes(1) - expect(storeState.agentStatusByPaneKey).toEqual({ - [FUTURE_PANE_KEY]: expect.objectContaining({ state: 'done' }) - }) - vi.advanceTimersByTime(40) - expect(setAgentStatus).toHaveBeenCalledTimes(2) - expect(setAgentStatuses).toHaveBeenCalledTimes(2) - } finally { - vi.useRealTimers() - } - }) - - it('does not recurse when flushing a pending status re-enters via the store subscriber', async () => { - // Model synchronous Zustand notifications: pending flush once reentered itself with the same queued event until stack overflow (crash 9fc89529). - const subscribeListenerRef: { current: StoreSubscribeListener | null } = { current: null } - const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = { - current: null - } - let setAgentStatusCalls = 0 - const notify = (previousState: StoreLike = storeState): void => { - const listener = subscribeListenerRef.current - if (listener) { - listener(storeState, previousState) - } - } - const storeState: StoreLike = buildStoreState({ - setAgentStatus: (paneKey: string, entry: unknown) => { - setAgentStatusCalls += 1 - storeState.agentStatusByPaneKey = { - ...(storeState.agentStatusByPaneKey as Record), - [paneKey]: entry - } - notify() - }, - workspaceSessionReady: true, - settings: { terminalFontSize: 13, notifications: { enabled: false } }, - tabsByWorktree: {}, - terminalLayoutsByTabId: {} - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn((listener: StoreSubscribeListener) => { - subscribeListenerRef.current = listener - return () => { - subscribeListenerRef.current = null - } - }), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: (cb) => { - onSetListenerRef.current = cb - return () => {} - } - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - useIpcEvents() - await Promise.resolve() - - if (typeof onSetListenerRef.current !== 'function') { - throw new Error('Expected agentStatus.onSet listener to be registered') - } - if (typeof subscribeListenerRef.current !== 'function') { - throw new Error('Expected useAppStore.subscribe listener to be registered') - } - - onSetListenerRef.current({ - paneKey: FUTURE_PANE_KEY, - state: 'working', - prompt: 'p', - agentType: 'claude', - receivedAt: 1_700_000_000_100, - stateStartedAt: 1_699_999_999_100 - }) - expect(setAgentStatusCalls).toBe(0) - - // Hydration retries synchronously; the re-entry guard must apply this event once without recursion. - const beforeHydration = { ...storeState } - storeState.tabsByWorktree = { - 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'Future Tab' }] - } - storeState.terminalLayoutsByTabId = { - 'tab-future': { - root: { type: 'leaf', leafId: FUTURE_LEAF_ID }, - activeLeafId: FUTURE_LEAF_ID, - expandedLeafId: null - } - } - - expect(() => notify(beforeHydration)).not.toThrow() - expect(setAgentStatusCalls).toBe(1) - }) -}) diff --git a/src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-replay.test.ts b/src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-replay.test.ts deleted file mode 100644 index 4f51dba4dee..00000000000 --- a/src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-replay.test.ts +++ /dev/null @@ -1,364 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { - buildStoreState, - expectWorktreeRouting, - FUTURE_LEAF_ID, - FUTURE_PANE_KEY, - type AgentStatusSetData, - type StoreLike, - type StoreSubscribeListener -} from './ipc-events-agent-status-store-test-fixtures' -import { - buildWindowApi, - stubReactSyncEffect, - stubAuxiliaryModules -} from './ipc-events-agent-status-window-test-fixtures' - -describe('useIpcEvents agent status snapshot integration', () => { - beforeEach(() => { - vi.resetModules() - vi.unstubAllGlobals() - }) - - it('queues replayed startup snapshots until the pane hydrates', async () => { - const setAgentStatus = vi.fn() - const subscribeListenerRef: { current: StoreSubscribeListener | null } = { current: null } - let resolveSnapshot!: (entries: AgentStatusSetData[]) => void - const getSnapshot = vi.fn( - () => - new Promise((resolve) => { - resolveSnapshot = resolve - }) - ) - - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - workspaceSessionReady: true, - tabsByWorktree: {}, - terminalLayoutsByTabId: {}, - repos: [], - worktreesByRepo: {} - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn((listener: StoreSubscribeListener) => { - subscribeListenerRef.current = listener - return () => { - subscribeListenerRef.current = null - } - }), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - getSnapshot, - onSet: () => () => {} - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - - useIpcEvents() - await Promise.resolve() - - resolveSnapshot([ - { - paneKey: FUTURE_PANE_KEY, - tabId: 'tab-future', - worktreeId: 'wt-1', - connectionId: 'ssh-1', - state: 'working', - prompt: 'PreToolUse: Bash', - agentType: 'codex', - toolName: 'Bash', - toolInput: 'pnpm test', - terminalHandle: 'term-future', - receivedAt: 1_700_000_000_000, - stateStartedAt: 1_699_999_999_000, - // A reload starts with no state history; the host's turn start must arrive with the row. - turnStartedAt: 1_699_999_990_000 - } - ]) - await Promise.resolve() - await Promise.resolve() - - expect(setAgentStatus).not.toHaveBeenCalled() - - const beforeHydration = { ...storeState } - Object.assign(storeState, { - tabsByWorktree: { - 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'SSH Tab' }] - }, - terminalLayoutsByTabId: { - 'tab-future': { - root: { type: 'leaf', leafId: FUTURE_LEAF_ID }, - activeLeafId: FUTURE_LEAF_ID, - expandedLeafId: null - } - } - }) - subscribeListenerRef.current?.(storeState, beforeHydration) - - expect(setAgentStatus).toHaveBeenCalledTimes(1) - expect(setAgentStatus).toHaveBeenCalledWith( - FUTURE_PANE_KEY, - expect.objectContaining({ - state: 'working', - prompt: 'PreToolUse: Bash', - agentType: 'codex', - toolName: 'Bash', - toolInput: 'pnpm test' - }), - 'SSH Tab', - { - updatedAt: 1_700_000_000_000, - stateStartedAt: 1_699_999_999_000, - turnStartedAt: 1_699_999_990_000 - }, - expectWorktreeRouting('wt-1'), - undefined - ) - }) - - it('suppresses notifications for replayed done snapshots after pane hydration', async () => { - const setAgentStatus = vi.fn() - const observeAgentHookCompletionForNotification = vi.fn() - const subscribeListenerRef: { current: StoreSubscribeListener | null } = { current: null } - let resolveSnapshot!: (entries: AgentStatusSetData[]) => void - const getSnapshot = vi.fn( - () => - new Promise((resolve) => { - resolveSnapshot = resolve - }) - ) - - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - workspaceSessionReady: true, - settings: { terminalFontSize: 13, notifications: { enabled: true, agentTaskComplete: true } }, - tabsByWorktree: {}, - terminalLayoutsByTabId: {}, - repos: [], - worktreesByRepo: {} - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn((listener: StoreSubscribeListener) => { - subscribeListenerRef.current = listener - return () => { - subscribeListenerRef.current = null - } - }), - getState: () => storeState - } - })) - vi.doMock('./agent-hook-completion-notifications', () => ({ - observeAgentHookCompletionForNotification, - resetAgentHookCompletionNotificationCoordinators: vi.fn(), - syncAgentHookCompletionNotificationSettings: vi.fn(), - syncAgentHookCompletionNotificationsForStoreUpdate: vi.fn() - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - getSnapshot, - onSet: () => () => {} - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - - useIpcEvents() - await Promise.resolve() - - resolveSnapshot([ - { - paneKey: FUTURE_PANE_KEY, - tabId: 'tab-future', - worktreeId: 'wt-1', - connectionId: 'ssh-1', - state: 'done', - prompt: 'remote completion', - agentType: 'codex', - lastAssistantMessage: 'queued completion', - terminalHandle: 'term-future', - receivedAt: 1_700_000_000_000, - stateStartedAt: 1_699_999_999_000 - } - ]) - await Promise.resolve() - await Promise.resolve() - - expect(setAgentStatus).not.toHaveBeenCalled() - - const beforeHydration = { ...storeState } - Object.assign(storeState, { - tabsByWorktree: { - 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'SSH Tab' }] - }, - terminalLayoutsByTabId: { - 'tab-future': { - root: { type: 'leaf', leafId: FUTURE_LEAF_ID }, - activeLeafId: FUTURE_LEAF_ID, - expandedLeafId: null - } - } - }) - subscribeListenerRef.current?.(storeState, beforeHydration) - - expect(setAgentStatus).toHaveBeenCalledWith( - FUTURE_PANE_KEY, - expect.objectContaining({ - state: 'done', - prompt: 'remote completion', - agentType: 'codex', - lastAssistantMessage: 'queued completion' - }), - 'SSH Tab', - { updatedAt: 1_700_000_000_000, stateStartedAt: 1_699_999_999_000 }, - expectWorktreeRouting('wt-1'), - undefined - ) - expect(observeAgentHookCompletionForNotification).not.toHaveBeenCalled() - }) - - it('accepts WSL-relayed status events for a local repo (wsl:* is transport provenance, not ownership)', async () => { - const setAgentStatus = vi.fn() - const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = { - current: null - } - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - workspaceSessionReady: true, - tabsByWorktree: { - 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'WSL Tab' }] - }, - terminalLayoutsByTabId: { - 'tab-future': { - root: { type: 'leaf', leafId: FUTURE_LEAF_ID }, - activeLeafId: FUTURE_LEAF_ID, - expandedLeafId: null - } - }, - repos: [{ id: 'repo-1', connectionId: null }], - worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] } - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: (cb) => { - onSetListenerRef.current = cb - return () => {} - } - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - - useIpcEvents() - await Promise.resolve() - if (typeof onSetListenerRef.current !== 'function') { - throw new Error('Expected agentStatus.onSet listener to be registered') - } - - onSetListenerRef.current({ - paneKey: FUTURE_PANE_KEY, - state: 'working', - prompt: 'wsl p', - agentType: 'claude', - worktreeId: 'wt-1', - connectionId: 'wsl:Ubuntu', - receivedAt: 1_700_000_000_000, - stateStartedAt: 1_699_999_999_000 - }) - - expect(setAgentStatus).toHaveBeenCalledTimes(1) - expect(setAgentStatus).toHaveBeenCalledWith( - FUTURE_PANE_KEY, - expect.objectContaining({ state: 'working', prompt: 'wsl p', agentType: 'claude' }), - 'WSL Tab', - { updatedAt: 1_700_000_000_000, stateStartedAt: 1_699_999_999_000 }, - expectWorktreeRouting('wt-1'), - undefined - ) - }) - - it('still rejects WSL-relayed status events against an SSH-owned repo', async () => { - const setAgentStatus = vi.fn() - const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = { - current: null - } - const storeState: StoreLike = buildStoreState({ - setAgentStatus, - workspaceSessionReady: true, - tabsByWorktree: { - 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'SSH Tab' }] - }, - terminalLayoutsByTabId: { - 'tab-future': { - root: { type: 'leaf', leafId: FUTURE_LEAF_ID }, - activeLeafId: FUTURE_LEAF_ID, - expandedLeafId: null - } - }, - repos: [{ id: 'repo-1', connectionId: 'ssh-1' }], - worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] } - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: (cb) => { - onSetListenerRef.current = cb - return () => {} - } - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - - useIpcEvents() - await Promise.resolve() - if (typeof onSetListenerRef.current !== 'function') { - throw new Error('Expected agentStatus.onSet listener to be registered') - } - - onSetListenerRef.current({ - paneKey: FUTURE_PANE_KEY, - state: 'working', - prompt: 'wsl p', - agentType: 'claude', - worktreeId: 'wt-1', - connectionId: 'wsl:Ubuntu', - receivedAt: 1_700_000_000_000, - stateStartedAt: 1_699_999_999_000 - }) - - expect(setAgentStatus).not.toHaveBeenCalled() - }) -}) diff --git a/src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts b/src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts deleted file mode 100644 index 872584e3abd..00000000000 --- a/src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { buildStoreState } from './ipc-events-agent-status-store-test-fixtures' -import type { StoreLike } from './ipc-events-agent-status-store-test-fixtures' -import { - buildWindowApi, - stubReactSyncEffect, - stubAuxiliaryModules -} from './ipc-events-agent-status-window-test-fixtures' -import { buildSshAuthorityReconciliationHarness } from './ipc-events-ssh-authority-test-fixtures' - -function stubDirectSshModules(args: { storeState: StoreLike }): void { - stubReactSyncEffect() - stubAuxiliaryModules() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => args.storeState - } - })) -} - -// Pull main’s durable status snapshot through the real hook only after tabs are ready, recovering startup pushes received before local state. -describe('useIpcEvents agent status snapshot integration', () => { - beforeEach(() => { - vi.resetModules() - vi.unstubAllGlobals() - }) - - it.each([ - { partialAuthority: { providerEpoch: 'epoch-current' } }, - { partialAuthority: { connectionGeneration: 7 } } - ])('fills a same-watermark partial SSH authority', async ({ partialAuthority }) => { - const harness = buildSshAuthorityReconciliationHarness({ - partialAuthority, - latestAuthority: { - providerEpoch: 'epoch-current', - connectionGeneration: 7 - } - }) - stubDirectSshModules({ storeState: harness.storeState }) - const { useIpcEvents } = await import('./useIpcEvents') - useIpcEvents() - - harness.emitPartialState() - - await vi.waitFor(() => { - expect(harness.setSshConnectionState).toHaveBeenCalledTimes(2) - }) - expect(harness.getState).toHaveBeenCalledOnce() - expect(harness.storedState()).toEqual({ - targetId: 'target-reconciliation', - status: 'connected', - error: null, - reconnectAttempt: 0, - providerEpoch: 'epoch-current', - connectionGeneration: 7 - }) - }) - - it.each([ - { partialAuthority: { providerEpoch: 'epoch-conflict' } }, - { partialAuthority: { connectionGeneration: 6 } } - ])( - 'rejects a reconciliation reply that conflicts with present authority', - async ({ partialAuthority }) => { - const harness = buildSshAuthorityReconciliationHarness({ - partialAuthority, - latestAuthority: { - providerEpoch: 'epoch-current', - connectionGeneration: 7 - } - }) - stubDirectSshModules({ storeState: harness.storeState }) - const { useIpcEvents } = await import('./useIpcEvents') - useIpcEvents() - - harness.emitPartialState() - - await vi.waitFor(() => { - expect(harness.getState).toHaveBeenCalledOnce() - }) - await Promise.resolve() - expect(harness.setSshConnectionState).toHaveBeenCalledOnce() - expect(harness.storedState()).toEqual( - expect.objectContaining({ - targetId: 'target-reconciliation', - ...partialAuthority - }) - ) - } - ) - - it('does not let initial SSH port snapshots overwrite newer push events', async () => { - const targetId = 'target-ports' - const secondTargetId = 'target-ports-second' - const rejectingTargetId = 'target-ports-rejecting' - const partialTargetId = 'target-ports-partial' - const connectedState = { - targetId, - status: 'connected' as const, - error: null, - reconnectAttempt: 0, - providerEpoch: 'epoch-ports', - connectionGeneration: 3 - } - const secondConnectedState = { - ...connectedState, - targetId: secondTargetId, - providerEpoch: 'epoch-ports-second' - } - const rejectingConnectedState = { - ...connectedState, - targetId: rejectingTargetId, - providerEpoch: 'epoch-ports-rejecting' - } - const partialConnectedState = { - ...connectedState, - targetId: partialTargetId, - providerEpoch: null, - connectionGeneration: undefined - } - const reconciledPartialState = { - ...connectedState, - targetId: partialTargetId, - providerEpoch: 'epoch-ports-partial', - connectionGeneration: 4 - } - const liveForward = { - id: 'forward-live', - targetId, - localPort: 17860, - remoteHost: '127.0.0.1', - remotePort: 7860, - status: 'active' as const - } - const secondForward = { - ...liveForward, - id: 'forward-second', - targetId: secondTargetId, - localPort: 17861, - remotePort: 7861 - } - const rejectingTargetForward = { - ...liveForward, - id: 'forward-rejecting-target', - targetId: rejectingTargetId, - localPort: 17862, - remotePort: 7862 - } - const partialTargetForward = { - ...liveForward, - id: 'forward-partial-target', - targetId: partialTargetId, - localPort: 17863, - remotePort: 7863 - } - const detectedPort = { - port: 7860, - pid: 42, - processName: 'python', - command: 'python -m http.server 7860' - } - const secondDetectedPort = { - ...detectedPort, - port: 7861, - pid: 43, - command: 'python -m http.server 7861' - } - let resolveForwards: (value: []) => void = () => {} - let resolveDetected: (value: (typeof detectedPort)[]) => void = () => {} - let resolveSecondForwards: (value: (typeof secondForward)[]) => void = () => {} - let resolveSecondDetected: (value: []) => void = () => {} - const forwardsSnapshot = new Promise<[]>((resolve) => { - resolveForwards = resolve - }) - const detectedSnapshot = new Promise<(typeof detectedPort)[]>((resolve) => { - resolveDetected = resolve - }) - const secondForwardsSnapshot = new Promise<(typeof secondForward)[]>((resolve) => { - resolveSecondForwards = resolve - }) - const secondDetectedSnapshot = new Promise<[]>((resolve) => { - resolveSecondDetected = resolve - }) - const listPortForwards = vi.fn(({ targetId: requestedTargetId }: { targetId: string }) => { - if (requestedTargetId === rejectingTargetId) { - return Promise.resolve([rejectingTargetForward]) - } - if (requestedTargetId === partialTargetId) { - return Promise.resolve([partialTargetForward]) - } - return requestedTargetId === targetId ? forwardsSnapshot : secondForwardsSnapshot - }) - const listDetectedPorts = vi.fn(({ targetId: requestedTargetId }: { targetId: string }) => { - if (requestedTargetId === rejectingTargetId) { - return Promise.reject(new Error('detected snapshot unavailable')) - } - if (requestedTargetId === partialTargetId) { - return Promise.resolve([]) - } - return requestedTargetId === targetId ? detectedSnapshot : secondDetectedSnapshot - }) - let forwardListener: - | ((data: { targetId: string; forwards: (typeof liveForward)[] }) => void) - | undefined - let detectedListener: - | ((data: { targetId: string; ports: (typeof detectedPort)[] }) => void) - | undefined - const setPortForwards = vi.fn() - const setDetectedPorts = vi.fn() - const sshConnectionStates = new Map< - string, - | typeof connectedState - | typeof secondConnectedState - | typeof rejectingConnectedState - | typeof partialConnectedState - | typeof reconciledPartialState - >() - const storeState = buildStoreState({ - sshTargetLabels: new Map([ - [targetId, 'Ports Target'], - [secondTargetId, 'Second Ports Target'], - [rejectingTargetId, 'Rejecting Ports Target'], - [partialTargetId, 'Partial Ports Target'] - ]), - sshConnectionStates, - setSshConnectionState: ( - nextTargetId: string, - state: - | typeof connectedState - | typeof secondConnectedState - | typeof rejectingConnectedState - | typeof partialConnectedState - | typeof reconciledPartialState - ) => { - sshConnectionStates.set(nextTargetId, state) - }, - setPortForwards, - setUnrestoredPortForwards: vi.fn(), - setDetectedPorts, - setSshTargetsMetadata: vi.fn(), - setRemovedSshTargetLabels: vi.fn(), - fetchRuntimeEnvironmentRepos: vi.fn(async () => []), - fetchWorktreeLineage: vi.fn(async () => undefined), - clearRemoteDetectedAgents: vi.fn(), - clearRemovedSshTargetState: vi.fn() - }) - let partialTargetStateCalls = 0 - - stubDirectSshModules({ storeState }) - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: () => () => {}, - ssh: { - listTargets: () => - Promise.resolve([ - { id: rejectingTargetId, label: 'Rejecting Ports Target' }, - { id: partialTargetId, label: 'Partial Ports Target' }, - { id: targetId, label: 'Ports Target' }, - { id: secondTargetId, label: 'Second Ports Target' } - ]), - listRemovedTargetLabels: () => Promise.resolve({}), - getState: ({ targetId: requestedTargetId }: { targetId: string }) => { - if (requestedTargetId === partialTargetId) { - partialTargetStateCalls += 1 - return Promise.resolve( - partialTargetStateCalls === 1 ? partialConnectedState : reconciledPartialState - ) - } - return Promise.resolve( - requestedTargetId === rejectingTargetId - ? rejectingConnectedState - : requestedTargetId === targetId - ? connectedState - : secondConnectedState - ) - }, - listPortForwards, - listUnrestoredPortForwards: () => Promise.resolve([]), - listDetectedPorts, - onPortForwardsChanged: ( - listener: (data: { targetId: string; forwards: (typeof liveForward)[] }) => void - ) => { - forwardListener = listener - return () => {} - }, - onDetectedPortsChanged: ( - listener: (data: { targetId: string; ports: (typeof detectedPort)[] }) => void - ) => { - detectedListener = listener - return () => {} - }, - onTargetRemoved: () => () => {} - } - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - useIpcEvents() - - await vi.waitFor(() => { - expect(forwardListener).toBeTypeOf('function') - expect(detectedListener).toBeTypeOf('function') - expect(partialTargetStateCalls).toBe(2) - expect(sshConnectionStates.get(partialTargetId)).toEqual(reconciledPartialState) - expect(listPortForwards).toHaveBeenCalledTimes(4) - expect(listDetectedPorts).toHaveBeenCalledTimes(4) - expect( - setPortForwards.mock.calls.filter( - ([requestedTargetId]) => requestedTargetId === rejectingTargetId - ) - ).toEqual([[rejectingTargetId, [rejectingTargetForward]]]) - expect( - setPortForwards.mock.calls.filter( - ([requestedTargetId]) => requestedTargetId === partialTargetId - ) - ).toEqual([[partialTargetId, [partialTargetForward]]]) - }) - forwardListener?.({ targetId, forwards: [liveForward] }) - resolveForwards([]) - resolveDetected([detectedPort]) - - await vi.waitFor(() => { - expect( - setPortForwards.mock.calls.filter(([requestedTargetId]) => requestedTargetId === targetId) - ).toEqual([[targetId, [liveForward]]]) - expect( - setDetectedPorts.mock.calls.filter(([requestedTargetId]) => requestedTargetId === targetId) - ).toEqual([[targetId, [detectedPort]]]) - expect(listPortForwards).toHaveBeenCalledTimes(4) - expect(listDetectedPorts).toHaveBeenCalledTimes(4) - }) - forwardListener?.({ targetId, forwards: [liveForward] }) - detectedListener?.({ targetId: secondTargetId, ports: [secondDetectedPort] }) - resolveSecondForwards([secondForward]) - resolveSecondDetected([]) - - await vi.waitFor(() => { - expect( - setPortForwards.mock.calls.filter( - ([requestedTargetId]) => requestedTargetId === secondTargetId - ) - ).toEqual([[secondTargetId, [secondForward]]]) - expect( - setDetectedPorts.mock.calls.filter( - ([requestedTargetId]) => requestedTargetId === secondTargetId - ) - ).toEqual([[secondTargetId, [secondDetectedPort]]]) - }) - }) -}) diff --git a/src/shared/agent-launch/choices/agent-launch-settings-auto-projection.test.ts b/src/shared/agent-launch/choices/agent-launch-settings-auto-projection.test.ts index e4224ed5d5c..efe8a71d50f 100644 --- a/src/shared/agent-launch/choices/agent-launch-settings-auto-projection.test.ts +++ b/src/shared/agent-launch/choices/agent-launch-settings-auto-projection.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { agentLaunchSettingsMutationUpdates } from './agent-launch-settings' -import { applyAgentPermissionMode, AUTO_TUI_AGENT_ARGS } from '../permissions/tui-agent-permissions' +import { applyAgentPermissionMode } from '../permissions/tui-agent-permissions' describe('agent launch permission projection compatibility', () => { it('changes only agents Auto does not cover', () => { @@ -13,7 +13,6 @@ describe('agent launch permission projection compatibility', () => { { type: 'permissions', mode: 'manual', exceptAuto: true }, 'darwin' ) - expect(updates.agentDefaultArgs).toMatchObject(AUTO_TUI_AGENT_ARGS) expect(updates.agentDefaultArgs?.gemini).toBe('') expect(() => agentLaunchSettingsMutationUpdates( diff --git a/src/shared/agent-launch/startup/hermes-startup-query.windows.test.ts b/src/shared/agent-launch/startup/hermes-startup-query.windows.test.ts index 37781bec1ca..58740ecdba8 100644 --- a/src/shared/agent-launch/startup/hermes-startup-query.windows.test.ts +++ b/src/shared/agent-launch/startup/hermes-startup-query.windows.test.ts @@ -33,7 +33,6 @@ windowsIt('preserves the startup query and spaced custom args in native Windows }) const encodedCommand = plan?.command.match(/-EncodedCommand\s+(\S+)/)?.[1] - expect(encodedCommand).toBeDefined() execFileSync('powershell.exe', ['-NoProfile', '-EncodedCommand', encodedCommand!], { cwd: tempDir, env: { ...process.env, ...plan?.env } diff --git a/src/shared/agent-launch/startup/omp-draft-launch-shell.test.ts b/src/shared/agent-launch/startup/omp-draft-launch-shell.test.ts index 85fcbb091a9..5959e4a4361 100644 --- a/src/shared/agent-launch/startup/omp-draft-launch-shell.test.ts +++ b/src/shared/agent-launch/startup/omp-draft-launch-shell.test.ts @@ -56,13 +56,9 @@ it.each( env: { ...process.env, ...plan.env, ORCA_OMP_FRESH_CONFIG: config, CAPTURE: calls } }) expect(result.code, result.stderr).toBe(present ? 17 : 1) - expect(result.stderr).toContain( - present ? 'agent-stderr' : 'fresh OMP settings are unavailable' - ) if (present) { expect(result.stderr).toBe('agent-stderr') } - expect(result.stderr).not.toContain('command substitutions not allowed') expect(await readFile(calls, 'utf8')).toBe('task with spaces\n') } } finally { diff --git a/src/shared/agent-launch/startup/omp-fresh-launch-shell.test.ts b/src/shared/agent-launch/startup/omp-fresh-launch-shell.test.ts index f3c2eb04198..091bb5f6743 100644 --- a/src/shared/agent-launch/startup/omp-fresh-launch-shell.test.ts +++ b/src/shared/agent-launch/startup/omp-fresh-launch-shell.test.ts @@ -34,7 +34,6 @@ it.each(shells)( platform: 'linux', isRemote: true }) - expect(plan).not.toBeNull() const define = shell === 'fish' ? 'function omp; printf "%s\\n" $argv >> "$CAPTURE"; return 17; end; '