From fdcf887bf069b12cf211f606a46f235ce61163cc Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Sat, 24 Oct 2026 19:37:19 -0700 Subject: [PATCH] chore(lint): fix suppressed lint violations instead of ignoring them (#33294) * chore(lint): remove disable directives that suppress nothing Each was reported unused by the oxlint config that owns its rule (root, casting, react-doctor, anti-slop, native-plugins), including 17 file-level react-doctor blanket disables whose rule no longer fires. An unused react-hooks disable was masking a react/purity finding in useAgentBucketCounts (compiler rules skip functions with hook suppressions); suppress that one explicitly where the code explains it. * test(tab-bar): use typed tab/file fixtures instead of @ts-expect-error partials * chore(lint): fix misc suppressed lint violations instead of disabling - consistent-type-imports: the "vi.importActual requires inline import()" rationale was wrong; use typed vi.mock(import(...), importOriginal) or a type-only namespace import. - no-control-regex: source-control commit/push failure summaries, shell PATH hydration, and ipynb ANSI parsing reuse the shared ansi-escape-sequences stripper and control-character pattern instead of private copies. - no-explicit-any / no-useless-spread / no-this-alias: typed `this` in bridge test mocks, drop blanket file-level disables, drop a this-alias. * refactor(main): replace avoidable type assertions with checked narrowing 17 production cast suppressions in main/wsl-guest removed: untrusted IPC and RPC payloads (Jira issue update, WSL guest plugin install, handshake type) now narrow through isRecord/type guards and build typed results; redundant casts dropped; hand-rolled object checks and errno casts use the existing isRecord / hasErrorCode helpers. * refactor(renderer,mobile): replace avoidable type assertions with checked types 13 production cast suppressions removed: RPC failures read through the `ok` discriminant instead of casting to RpcFailure; react-native-web host refs narrowed with instanceof HTMLElement; redundant reply/diff casts become checked annotations; the TUI agent param uses z.custom(isTuiAgent) like its sibling schemas; the agent-status slice cast that hid missing actions is gone so the creator's return type checks the whole slice. * chore(lint): drop dead tiptap wrapper; scope anti-slop mocking rule to test support - safe-react-node-view-renderer patched handleSelectionUpdate, which @tiptap/react 3.31.3 no longer has, so it always returned early. Use the stock ReactNodeViewRenderer. - The anti-slop test override only matched *.test.{ts,tsx}, forcing file-level no-module-mocking disables in test harnesses and fixtures. Match *-test-harness, *-test-fixture(s), .test-fixture and *.test.mjs, then drop those disables and the now-redundant per-file exemptions. * test: replace repeated cast suppressions with typed fixtures (main/shared/e2e) Net -119 cast directives. Typed fixtures replace recurring casts: unusedSshConnection/unusedOrcadHostTransport placeholders, a typed readPersistedState harness read (exposed a test treating optional httpProxyUrl as always set), real Response objects in fetch spies, an updater main-window fixture, a CDP webContents adapter, one shared releaseModuleExport for cross-version tests, and requireOverlayDir in place of `as string` on possibly-undefined overlay paths. * fix(runtime): validate loaded device rows and host scope instead of casting Two cast suppressions claimed checks that did not happen: - device-registry-file-read: "normalizeLoadedDeviceRow re-validates every field" was false; deviceId, token, name and timestamps were never checked. Rows without an identity are now dropped and the file is set aside through the existing corrupt-file path (registry reads as doubtful, so no revocation verdicts); a non-string token is stripped so the row stays listed but cannot authenticate. - client-ui: "only optionality differs" was false; any string reached persisted workspaceHostScope. Narrow it with isWorkspaceHostScope and ignore unknown scopes (wire schema unchanged for mixed versions). The remaining cast now states what it actually covers. * test(runtime,ipc): drop redundant cast suppressions; typed runtime store fixture -98 cast directives across runtime/ipc tests: 61 casts that typecheck without the assertion; a typed vi.fn for scoped invalidation; a partialRuntimeStore fixture in place of `store as never` for OrcaRuntimeService; typed worker terminal/authority builders in the incarnation-fallback suite (the old stub lacked runtimeId, ptyId, worktreeId and launchTokenHash) plus removal of stacked duplicate directives there. * chore(lint): typed vi.mock imports and explicit listener snapshot in shared-bucket tests Remaining files from the misc-rule pass that overlapped other buckets: codex-accounts mocks use the typed vi.mock(import(...)) form instead of a consistent-type-imports disable; the daemon late-exit fixture copies its listener set with Array.from instead of a no-useless-spread disable. * refactor(github,mobile): narrow routing settings type; drop remaining avoidable casts - work-item-routing cast a one-field object to AppState['settings'] under a claim that readers only consume activeRuntimeEnvironmentId. Return the existing RuntimeFocusSettings pick instead and narrow the cache-key chain's parameters, so the compiler enforces that claim. - mobile file preview reads refusals through the RpcResponse `ok` discriminant rather than a cast. - terminal document host-notify accepts `unknown` errors, removing the message-bridge cast. - native chat readability drops a redundant repo-list cast. * test(renderer): replace repeated cast suppressions with typed fixtures -140 cast directives (and ~150 unsuppressed casts of the same shapes): - mockPanePtyArgs owns the connectPanePty fixture casts, so a wrong argument order or fixture is now a type error; drop binding casts for members PanePtyBinding already declares. - Seed AppState tab rows with the typed makeTerminalTab; this exposed a split direction 'row' that is not a TerminalPaneSplitDirection. - Partial GlobalSettings use getDefaultSettings overrides; Jira-only stores use createTestStore; runtime environment rows/statuses come from a typed paired-runtime-environment fixture. * chore(lint): drop no-module-mocking disable now covered by the test-fixture glob * test(e2e): fix all 221 grandfathered e2e type errors; baseline to zero The e2e typecheck baseline grandfathered 221 errors in 119 files. About a third were tests drifted from the current API, several of which would fail or mislead at runtime: - terminal-shortcuts called a nonexistent xterm buffer method; - the daemon-lifecycle fake lacked terminateOwnedTree, hidden by importing its type from a module that does not export it; - arguments in the wrong position (listProcesses(hostId, opts), a dropped pty.write inputKind, an extra toWebTerminalSurfaceTabId argument); - removed enum values ('drawer', 'idle', 'editor', 'session-insecure-origin') and a launch_source the telemetry schema rejects; - missing required fields (turnScope, childWorkOpen, providerIdentity, reviewLookupOutcome, runtimeServiceAccount behind an `as RelayConfig`); - dead reads (tab.customTitle, bucket.targets, changedPixels). Also lifts variables that narrowed to `never` after poll-callback assignment, and adds a typed xterm render-dimensions accessor. Includes the cross-version suites' shared releaseModuleExport adoption. * chore(typecheck): gate the now-clean e2e typecheck in local lint The e2e baseline is empty, so the ratchet now rejects any tests/ type error. CI already runs it via run-ratchets; add it to pnpm lint's explicit ratchet list and replace the stale 'not yet clean' note. * test(mobile): typecheck every mobile test; baseline 105 files -> 0 mobile/tests-typecheck-baseline.txt grandfathered 105 test files (531 errors) whose types CI never checked. Contract changes had landed without these tests following, and type pins in them silently checked nothing. - Root cause: salvagedOptional (src/shared/zod-salvage.ts) cast away zod's optional marker, so every reply type built on it claimed salvaged fields were always present. Return a real ZodOptional; runtime is unchanged (the outer optional short-circuits undefined). - Production now takes the port it uses: RpcOperationSender instead of RpcClient for send-only callers, a narrow fetch signature, and only the request fields push-receive reads. - findAllByHostType/findByHostType replace `'Text' as never` lookups; stubRpcClient gives tests a typed RpcClient. - Real test bugs: settings-host-client-lifecycle "relay" hosts were direct hosts (relayHostId no longer exists); two toEqualTypeOf pins could never hold; a recovery hook ran with an undefined scope key; a create-PR test cast to a never-imported type; "no URL" assertions passed on null; plus stale required props, args and enum values. * test(e2e): parse workerStart result instead of casting it * test(runtime): typed partial runtime stores with production-shaped fixture data Route every partial store passed to OrcaRuntimeService through partialRuntimeStore with typed row builders, removing 60 cast directives that hid fixture data production never produces: - paired-client-navigation-test-harness keyed setWorktreeMeta writes by `[object Object]` (it took an id where the store takes a target), so reads never saw them; - invalid worktree lineage origin/capture and incomplete workspace lineage; - getWorkspaceSession returning null where the store returns the default session; getGitHubCache returning undefined; - orcad migration states missing migrationId/manifest/receipt, with a toEqual pinned to that incomplete shape; - partial repos, metadata, folders, project groups and notification rows. * refactor(renderer): resolve suppressed React hook lint instead of disabling it 109 of 187 react-hooks/react-doctor suppressions fixed: - ~95 exhaustive-deps disables only hid stable refs and setters passed through controller objects; list them (identity never changes, so nothing re-runs), plus a few real stale closures (tabId/worktreeId). - Effects keyed on one value that read the latest props now use useEffectEvent (WebConnect, skill freshness, Bitbucket dialog seeding, Landing preflight, automation catalog, structured-session recovery, file explorer). - The conditional layout-owner hook is selected once at module load from the build constant instead of a rules-of-hooks disable that also hid the whole controller from compiler and react-doctor rules. - A hook disable was masking a react/purity Date.now() in the palette's recent-tabs memo; sample it when the snapshot is captured. - Three identical debounced-search effects share useTaskSearchDebouncedApply; layout snapshot merge helpers move out to stay under max-lines. Remaining suppressions are mount-once resources, invalidation epochs, deliberately keyed fields, and test harnesses that mock the dispatcher. * chore(ratchet): exempt test harnesses and fixtures from the focus-read ratchet The ratchet skipped *.test-fixture.ts but scanned *-test-harness, *-test-fixture(s) and *.test-support files. Those seed settings with getDefaultSettings (which the ratchet discovers as a focus reader) and never route host work, so 17 baseline entries were false positives, and replacing a test's `settings: {...} as never` with typed defaults tripped it. Match the repo's test-support names and prune (which also drops one stale entry), and rename the frame-ordering harness to *-test-harness like its siblings. A blanket "-harness" match would also exempt e2e-terminal-pty-harness, which production imports, so the names stay explicit. * test: merge duplicate shared/constants imports * revert(file-explorer): restore original tree load effects pending SSH e2e investigation * test(runtime): drop RuntimeStore cast now that the fixture is typed --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- config/e2e-typecheck-baseline.txt | 119 ----- config/focus-setting-read-baseline.txt | 29 +- config/oxlint-anti-slop.json | 28 +- config/scripts/check-focus-read-ratchet.mjs | 6 +- .../scripts/check-focus-read-ratchet.test.mjs | 5 + .../headless-serve-shutdown-matrix.test.mjs | 4 - config/scripts/moved-code-matcher.test.mjs | 2 - config/tsconfig.e2e.json | 5 +- .../use-mobile-agent-history-state.ts | 1 - .../browser-command-reply-schema.test.ts | 6 +- .../mobile-browser-frameless-stream.test.tsx | 27 +- .../mobile-browser-pane-frame-layers.test.tsx | 10 +- ...e-browser-commands-click-fallback.test.tsx | 5 +- .../components/CodexResetCreditAction.test.ts | 11 +- .../src/components/HostProtocolGate.test.ts | 27 +- .../components/MobileHomeQuickActions.test.ts | 3 +- .../MobileHostCard-truthful-status.test.tsx | 7 +- mobile/src/components/MobileHostCard.test.ts | 480 +++++++++--------- .../MobileMarkdown.file-links.test.ts | 2 +- .../components/MobileRichMarkdownEditor.tsx | 6 +- .../src/components/NewWorktreeModal.test.tsx | 109 ++-- .../PickerModal.accessibility.test.ts | 3 +- .../src/components/WorktreeAgentList.test.tsx | 17 +- mobile/src/components/WorktreeListRow.test.ts | 27 +- .../agent-monitoring-indicators.test.ts | 62 ++- .../bottom-drawer-close-lifecycle.test.ts | 31 +- .../bottom-drawer-window-handback.test.ts | 15 +- .../codex-reset-credit-capability.test.ts | 5 +- ...le-diff-review-send-sheet-stacking.test.ts | 22 +- .../mobile-markdown-mermaid-routing.test.ts | 5 +- .../components/pr-sidebar/MermaidDiagram.tsx | 6 +- .../pr-sidebar/PRCommentsSection.test.ts | 27 +- .../connection-diagnostics-submission.ts | 2 +- .../src/dictation/mobile-dictation-setup.ts | 15 +- mobile/src/files/MobileFileExplorerPanel.tsx | 7 +- .../files/MobileFileMarkdownPreview.test.ts | 9 +- .../src/files/mobile-file-preview-request.ts | 12 +- .../mobile-dictation-desktop-start.test.ts | 6 +- .../src/host-edit-route-accessibility.test.ts | 3 +- .../host-list-first-frame.test.tsx | 5 +- .../host-worktree-child-navigation.test.tsx | 17 +- mobile/src/mock-server-key-pair.test.ts | 14 +- .../NotificationDeliverySection.test.tsx | 3 +- .../notification-consent-ownership.test.ts | 9 +- .../push-dismissal-native-races.test.ts | 13 +- mobile/src/notifications/push-receive.ts | 8 +- .../notifications/push-registration.test.ts | 12 +- mobile/src/notifications/push-registration.ts | 4 +- mobile/src/notifications/push-token.test.ts | 10 +- .../onboarding/MobileOnboardingPage.test.ts | 18 +- .../NotificationOnboardingPreview.test.ts | 8 +- .../legacy-notification-opt-in-route.test.ts | 4 +- .../mobile-onboarding-screen.test.ts | 5 +- .../src/session/MobileMarkdownReader.test.tsx | 11 +- mobile/src/session/MobileNativeChatAsk.tsx | 8 +- .../session/MobileNativeChatComposer.test.ts | 89 ++-- .../MobileNativeChatFullContent.test.tsx | 7 +- .../session/MobileNativeChatMessage.test.ts | 30 +- .../session/MobileNativeChatOverlay.test.ts | 14 +- .../MobileNativeChatPermission.test.ts | 30 +- .../session/MobileNativeChatQuestion.test.tsx | 27 +- ...bileNativeChatSessionOptionPickers.test.ts | 114 ++--- .../MobileNativeChatSessionOptionRows.test.ts | 9 +- .../MobileNativeChatTurnStatus.test.ts | 17 +- mobile/src/session/QuickCommandsList.test.ts | 5 +- mobile/src/session/QuickCommandsSheet.test.ts | 106 ++-- .../session/ai-vault-resume-launch.test.ts | 5 +- .../mobile-file-search-owner-cleanup.test.tsx | 4 +- .../src/session/mobile-file-tap-open.test.ts | 32 +- .../mobile-native-chat-open-file.test.ts | 18 +- .../session/mobile-new-tab-agent-loader.ts | 3 +- .../mobile-session-tab-activation.test.ts | 3 +- ...tured-agent-session-thrown-refusal.test.ts | 16 +- ...ile-structured-session-status-feed.test.ts | 6 +- .../mobile-worker-takeover-send-sites.test.ts | 2 +- ...ding-terminal-handle-recovery-poll.test.ts | 3 +- .../src/session/session-reply-schema.test.ts | 6 +- ...se-mobile-diff-review-send-actions.test.ts | 15 +- .../use-mobile-file-tap-handlers.test.ts | 8 +- ...use-mobile-native-chat-answer-send.test.ts | 67 ++- ...mobile-native-chat-message-detail.test.tsx | 5 +- .../use-mobile-native-chat-readability.ts | 5 +- .../use-mobile-native-chat-visual.test.tsx | 5 +- .../session/use-mobile-pr-ai-triage.test.ts | 4 +- .../use-mobile-pr-branch-context.test.ts | 1 + .../use-mobile-pr-sidebar-controller.test.ts | 7 +- .../use-mobile-session-accessory-selection.ts | 5 +- ...le-session-terminal-create-actions.test.ts | 3 +- ...e-mobile-structured-agent-session.test.tsx | 16 +- ...obile-structured-prompt-responses.test.tsx | 8 +- .../src/session/use-mobile-terminal-paste.ts | 2 - ...ve-notification-delivery-settings.test.tsx | 3 +- .../notification-display-test.test.tsx | 3 +- .../settings/notification-display-test.tsx | 4 +- .../settings/settings-screen-state.test.tsx | 3 +- .../voice-settings-poller-refresh.test.tsx | 7 +- .../mobile-commit-during-generation.test.tsx | 5 +- ...use-mobile-commit-failure-recovery.test.ts | 4 +- ...e-mobile-commit-message-generation.test.ts | 5 +- .../use-mobile-source-control-loaders.test.ts | 4 +- ...use-mobile-source-control-openers.test.tsx | 5 +- .../use-mobile-source-control-openers.ts | 3 +- .../tasks/agent-launch-mobile-replay.test.ts | 17 +- .../src/tasks/source-workspace-create.test.ts | 2 +- .../task-source-search-reply-schema.test.ts | 2 +- mobile/src/terminal/TerminalWebView.web.tsx | 6 +- ...uffered-terminal-draft-restoration.test.ts | 7 +- mobile/src/terminal/document/host-notify.ts | 10 +- .../src/terminal/document/message-bridge.ts | 5 +- .../terminal/mobile-terminal-query-reply.ts | 4 +- .../terminal-caret-rendering-oracle.test.ts | 8 +- .../terminal-live-accessory-raw-send.ts | 4 +- .../terminal-live-text-commit.test.ts | 2 +- .../terminal-webview-engine-error-state.tsx | 1 - .../terminal-webview-engine-error.test.ts | 13 +- .../terminal-webview-init-surface.test.ts | 1 - .../terminal-webview-notification-dispatch.ts | 2 - ...rminal-webview-query-reply-routing.test.ts | 3 +- .../terminal-webview-tap-routing.test.ts | 1 - .../terminal-webview-wheel-scroll.test.ts | 10 +- .../use-buffered-terminal-drafts.test.tsx | 2 +- .../worker-terminal-takeover-report.ts | 4 +- .../react-test-renderer-host-nodes.ts | 11 + .../native-mounting-substitutes.test.ts | 2 +- .../operation-module-loader.test.ts | 4 +- .../screen-native-substitutes.test.ts | 15 +- mobile/src/transport/client-context.test.ts | 2 +- mobile/src/transport/client-context.tsx | 1 - .../transport/host-credential-cleanup.test.ts | 8 +- .../transport/host-edit-navigation.test.ts | 3 +- .../transport/host-removal-lifecycle.test.ts | 8 +- ...ost-status-capability-ignorability.test.ts | 4 +- .../src/transport/host-status-gates.test.ts | 32 +- mobile/src/transport/host-store.test.ts | 4 +- .../mobile-endpoint-supervisor-nudge.test.ts | 2 +- .../mobile-endpoint-supervisor-test-fakes.ts | 8 +- .../mobile-relay-background-lifecycle.test.ts | 2 +- .../mobile-relay-direct-upgrade.test.ts | 2 +- .../transport/mobile-relay-e2ee-link.test.ts | 2 +- .../mobile-relay-pairing-recovery.test.ts | 21 +- .../mobile-relay-physical-client.test.ts | 4 +- .../mobile-relay-reconnect-controller.test.ts | 7 +- .../mobile-relay-resume-director.test.ts | 4 +- .../transport/mobile-relay-resume-director.ts | 2 +- .../mobile-relay-rpc-session-liveness.test.ts | 6 +- .../mobile-relay-rpc-session.test.ts | 18 +- .../mobile-relay-runtime-failover.test.ts | 4 +- .../transport/pairing-relay-candidate.test.ts | 4 +- .../pre-profile-pairing-coordinator.test.ts | 5 +- ...ient-synthesized-close-diagnostics.test.ts | 6 +- mobile/src/transport/rpc-client-test-stub.ts | 17 + .../rpc-session-liveness-watchdog.test.ts | 2 +- .../runtime-capability-probe.test.ts | 2 +- .../settings-host-client-lifecycle.test.ts | 33 +- mobile/src/transport/workspace-server.test.ts | 4 +- .../worktree-catalog-snapshot-client.ts | 4 +- mobile/tests-typecheck-baseline.txt | 99 +--- package.json | 2 +- .../agent-hooks/wsl-guest-plugin-install.ts | 21 +- .../ai-vault-search/session-search-store.ts | 45 +- .../session-scanner-opencode2-sqlite-list.ts | 5 +- .../session-scanner-opencode2-sqlite.ts | 12 +- .../runtime-automation-service.test.ts | 40 +- ...-browser-bridge-capture-paint-hold.test.ts | 6 +- ...t-browser-bridge-command-transport.test.ts | 6 +- ...gent-browser-bridge-keypress-input.test.ts | 6 +- .../agent-browser-bridge-mouse-input.test.ts | 6 +- .../agent-browser-bridge-navigation.test.ts | 6 +- ...agent-browser-bridge-pointer-input.test.ts | 6 +- ...t-browser-bridge-session-lifecycle.test.ts | 6 +- .../agent-browser-bridge-tab-routing.test.ts | 6 +- .../agent-browser-bridge-text-input.test.ts | 6 +- .../browser-session-ua-cdp-collector.ts | 7 +- .../browser/cdp-ws-proxy-focus-replay.test.ts | 4 +- .../cdp-ws-proxy-host-admission.test.ts | 8 +- .../browser/cdp-ws-proxy-input-focus.test.ts | 16 +- src/main/browser/cdp-ws-proxy-origin.test.ts | 4 +- src/main/browser/cdp-ws-proxy-test-harness.ts | 7 + src/main/browser/cdp-ws-proxy.test.ts | 4 +- .../claude-accounts/runtime-paths.test.ts | 5 +- .../claude/claude-diff-panel-auto-open.ts | 6 +- src/main/cli/cli-installer.ts | 1 - src/main/cli/wsl-cli-registration-command.ts | 6 +- .../runtime-home-legacy-migration.test.ts | 4 +- ...runtime-home-managed-auth-recovery.test.ts | 4 +- .../runtime-home-mirror-login-copy.test.ts | 4 +- .../runtime-home-per-account-homes.test.ts | 4 +- ...e-home-read-only-launch-resolution.test.ts | 17 +- ...untime-home-real-home-lane-routing.test.ts | 4 +- ...runtime-home-resume-selection-gate.test.ts | 4 +- ...time-home-retained-auth-provenance.test.ts | 4 +- ...ome-system-default-mirror-readback.test.ts | 4 +- ...ntime-home-system-default-snapshot.test.ts | 4 +- ...me-system-resource-materialization.test.ts | 4 +- .../runtime-home-wsl-managed-accounts.test.ts | 4 +- .../runtime-home-wsl-session-bridge.test.ts | 4 +- .../runtime-home-wsl-system-default.test.ts | 4 +- .../service-abandoned-login-supersede.test.ts | 4 +- .../service-account-add-login.test.ts | 4 +- ...vice-account-selection-and-removal.test.ts | 4 +- .../service-add-account-from-home.test.ts | 4 +- .../service-device-code-login.test.ts | 4 +- .../service-login-process-teardown.test.ts | 4 +- ...rvice-managed-home-removal-retries.test.ts | 9 +- .../service-quota-refresh-decoupling.test.ts | 4 +- .../service-reauthenticate-activation.test.ts | 4 +- .../service-reset-credit-durability.test.ts | 4 +- ...ervice-reset-credit-home-ownership.test.ts | 4 +- ...service-reset-credit-retry-routing.test.ts | 4 +- ...ervice-reset-credit-target-routing.test.ts | 4 +- .../service-system-default-identity.test.ts | 4 +- .../service-wsl-accounts.test.ts | 4 +- src/main/codex-accounts/service.test.ts | 4 +- ...sta-4422-transient-ownership-error.test.ts | 4 +- ...ollback-deletes-authenticated-home.test.ts | 4 +- ...a-4735-unreadable-host-lane-writes.test.ts | 4 +- ...codex-structured-launch-resolution.test.ts | 1 - ...dex-trust-config-concurrent-launch.test.ts | 5 +- .../desktop-script-provider-test-harness.ts | 3 - .../daemon/degraded-daemon-pty-provider.ts | 4 +- .../session-ingest-throughput.bench.test.ts | 1 - .../readers/opencode-binder-sessions.ts | 19 +- .../github/work-item-search-test-harness.ts | 3 - .../ipc/filesystem-search-rg-timeout.test.ts | 3 +- src/main/ipc/jira/ipc-params.ts | 91 ++-- .../ipc/pty-launch-parity.test-fixture.ts | 16 +- .../ipc/pty-spawn-env-terminal-basics.test.ts | 3 +- .../ipc/pty/daemon-late-exit-test-fixture.ts | 4 +- ...worktree-roots-scoped-invalidation.test.ts | 9 +- ...ent-session-tabs-delta.integration.test.ts | 4 +- src/main/ipc/runtime.ts | 5 +- .../ssh-host-server-on-connect-wiring.test.ts | 3 +- ...ktree-base-directory-watch-targets.test.ts | 2 +- .../worktree-base-directory-watcher.test.ts | 2 +- ...d-provider-listing-catalog-version.test.ts | 9 +- ...ed-provider-listing-overtaken-scan.test.ts | 9 +- .../detected-worktree-classification.test.ts | 2 +- .../detected-worktree-scan-superseded.test.ts | 9 +- src/main/jcode/hook-gate-script.test.ts | 5 +- src/main/jcode/hook-service.test.ts | 5 +- .../orcad-activation-fence-generation.test.ts | 10 +- .../orcad-state-mutation-timeout.test.ts | 5 +- .../orcad-remote-install-termination.test.ts | 5 +- .../install/orcad-remote-log-tail.test.ts | 4 +- ...-remote-node-runtime-host-fallback.test.ts | 5 +- .../orcad-remote-node-runtime-noexec.test.ts | 5 +- ...remote-node-runtime-promotion-lock.test.ts | 5 +- ...remote-node-runtime-recorded-probe.test.ts | 5 +- ...rcad-remote-node-runtime-slow-hash.test.ts | 5 +- ...emote-install-gc-claim-termination.test.ts | 5 +- .../remote-install-gc-termination.test.ts | 5 +- .../remote-install-lock-termination.test.ts | 5 +- .../remote-node-runtime-store-gc.test.ts | 5 +- ...de-runtime-store-lock-host-release.test.ts | 5 +- .../remote-node-runtime-store-windows.test.ts | 5 +- ...mote-versioned-install-termination.test.ts | 5 +- .../orcad-managed-bound-port.test.ts | 5 +- ...cad-managed-pairing-repair-restart.test.ts | 5 +- .../orcad-managed-pairing-repair.test.ts | 5 +- .../orcad-managed-tunnel-target.test.ts | 11 +- .../orcad-managed-version-gc.test.ts | 4 +- .../agent-model-catalog-persistence.ts | 4 +- .../hook-plugin-module-contract.test.ts | 87 +--- ...untime-daemon-adoption.integration.test.ts | 5 +- src/main/orcad/orcad-node-slot-fixture.ts | 11 +- .../worktree-meta-alias-projection.test.ts | 13 +- ...re-interaction-broadcast.benchmark.test.ts | 6 +- ...rsistence-loading-store-extraction.test.ts | 17 +- .../persistence-proxy-secret-recovery.test.ts | 18 +- .../persistence-single-serialize.test.ts | 6 +- .../persistence/persistence-test-harness.ts | 14 +- ...tence-workspace-session-scrollback.test.ts | 8 +- ...tence-worktree-lineage-and-backups.test.ts | 5 +- .../provider-process-teardown.ts | 4 +- .../agent-detection-host-resolution.test.ts | 17 +- .../construction/runtime-construction.ts | 18 +- ...elegated-mobile-device.integration.test.ts | 4 +- .../runtime/device-registry-file-read.test.ts | 43 ++ src/main/runtime/device-registry-file-read.ts | 17 +- .../runtime/device-registry-row-validation.ts | 44 +- src/main/runtime/dsb-terminal-runtime.test.ts | 10 +- .../runtime/editor-open-active-group.test.ts | 4 +- .../folder-upgrade-runtime-listing.test.ts | 28 +- ...older-workspace-mixed-host-routing.test.ts | 14 +- ...less-close-keeps-publication-epoch.spec.ts | 4 +- ...dless-pane-layout-noop-publication.test.ts | 4 +- ...placeholder-graph-surface-standing.spec.ts | 4 +- .../runtime/hook-status-resume-record.test.ts | 8 +- .../hook-status-resume-sleep-ordering.test.ts | 5 +- .../runtime/host-account-launch-env.spec.ts | 4 +- .../host-pane-remount-terminal-handle.test.ts | 3 +- .../runtime/host-sleep-resume-record.test.ts | 32 +- ...t-command-pane-starts-layout-owner.test.ts | 6 +- .../managed-worktree-create-registry.test.ts | 13 +- .../managed-worktree-create-telemetry.test.ts | 13 +- .../relay-auth-coordinator-readiness.test.ts | 6 +- ...ile-relay-host-catalog.integration.test.ts | 4 +- .../omp-user-bubble-launch-authority.test.ts | 4 +- .../opencode-wsl-folder-launch-host.test.ts | 31 +- ...-runtime-agent-session-account-env.test.ts | 4 +- ...e-cold-restore-terminal-membership.test.ts | 3 +- ...ca-runtime-exited-split-pane-close.test.ts | 18 +- ...ntime-hook-agent-status-projection.test.ts | 8 +- ...-provider-reattach-launch-identity.test.ts | 4 +- ...orca-runtime-serve-browser-restore.test.ts | 4 +- ...ca-runtime-session-tabs-path-alias.test.ts | 69 +-- ...-structured-launch-settings-wiring.test.ts | 7 +- ...-runtime-terminal-agent-phone-push.test.ts | 30 +- ...untime-terminal-handle-incarnation.test.ts | 12 +- ...-runtime-terminal-rename-retention.test.ts | 16 +- .../orca-runtime-test-lifecycle.spec.ts | 3 +- .../headless-pane-title-terminal-list.spec.ts | 3 +- ...adless-terminal-rename-publication.spec.ts | 3 +- .../lineage-and-scan-cache-part-02.spec.ts | 3 +- .../mobile-session-tabs-part-02.spec.ts | 6 +- .../mobile-session-tabs-part-09.spec.ts | 6 +- ...session-tabs-targeted-agent-resume.spec.ts | 11 +- ...ssion-tabs-windowless-sleep-resume.spec.ts | 9 +- .../mobile-session-tabs.spec.ts | 6 +- ...nal-creation-and-readiness-part-04.spec.ts | 4 +- ...ntime-worktree-change-invalidation.test.ts | 4 +- .../orcad-migration-catalog-import.test.ts | 49 +- .../structured-session-mail-target.test.ts | 3 +- .../paired-client-navigation-test-harness.ts | 17 +- ...retirement-proof-publication-order.spec.ts | 1 - .../remote-desktop-headless-park.test.ts | 7 +- .../agent-launch-settings-work.test.ts | 4 +- .../runtime/rpc/methods/agent-launch.test.ts | 3 +- .../rpc/methods/automatic-work.test.ts | 3 +- src/main/runtime/rpc/methods/client-ui.ts | 8 +- ...n-structured-worker-start-deadline.test.ts | 3 +- ...orker-release-incarnation-fallback.test.ts | 186 +++---- .../federation-agent-death-liveness.test.ts | 9 +- .../publish-capability-settings.test.ts | 4 +- .../rpc/methods/skills-bundled-orca.test.ts | 3 +- ...status-machine-name-publish-budget.test.ts | 5 +- src/main/runtime/rpc/methods/status.test.ts | 5 +- ...ructured-agent-session-attachments.test.ts | 3 +- ...ructured-agent-session-unsubscribe.test.ts | 6 +- .../methods/workspace-layout-apply.test.ts | 52 +- ...minal-multiplex-flow-control.bench.test.ts | 1 - ...nt-settings-linear-team-projection.spec.ts | 3 +- ...time-desktop-pane-title-forwarding.test.ts | 6 +- .../runtime-extraction-regressions.test.ts | 5 +- ...untime-local-create-rearm-ordering.test.ts | 4 +- .../runtime-remove-project-host-scope.spec.ts | 53 +- .../runtime/runtime-store-partial-fixture.ts | 117 +++++ .../runtime-terminal-set-pane-title.test.ts | 3 +- .../runtime-worktree-create-repo-host.test.ts | 26 +- ...ime-worktree-terminal-provisioning.test.ts | 22 +- .../session-graph/session-window-graph.ts | 3 +- ...close-by-split-handle-layout-owner.test.ts | 12 +- ...ld-headless-mobile-session-browser-tabs.ts | 3 +- .../main-layout-agent-launch-tabs.test.ts | 7 +- .../mobile-session-reconciliation.ts | 3 +- .../orca-runtime-project-catalog.test.ts | 4 +- .../orca-runtime-repo-icons.test.ts | 4 +- .../orca-runtime-sort-stamps.test.ts | 29 +- .../orca-runtime-terminal-use.test.ts | 19 +- .../orca-runtime-workspace-unread.test.ts | 53 +- .../sidebar-inputs-feed-announcements.test.ts | 8 +- .../workspace-rows-publish-cost.test.ts | 4 +- .../structured-agent-launch-parity.test.ts | 10 +- ...ent-session-windowless-layout-host.test.ts | 12 +- ...ed-agent-session-windowless-layout.test.ts | 19 +- .../terminal-chat-owner-persistence.test.ts | 4 +- .../terminal-list-surface-lost-orphan.spec.ts | 4 +- .../terminal-update-admission-census.test.ts | 3 +- .../adopted-pane-handle-race-main.test.ts | 3 +- .../identity/main-pane-record-arrival.test.ts | 3 +- .../lifecycle/main-pane-liveness.test.ts | 6 +- .../closed-pane-exit-cleanup-main.test.ts | 3 +- .../terminal-surface-replies.test-fixture.ts | 10 +- .../terminal/view/mobile-terminal-surface.ts | 3 +- ...dle-name-only-real-pty.integration.test.ts | 6 +- .../runtime/tui-idle-wait-test-harness.ts | 19 +- .../sweep-terminal-placement.test.ts | 6 +- .../workspace-host-notifications.test.ts | 4 +- .../worktree-ps-degraded-repo-scan.test.ts | 157 +++--- ...worktree-ps-unverifiable-terminals.test.ts | 7 +- .../worktree-scan-repeated-path.test.ts | 17 +- .../ssh-connection-attribution.test.ts | 4 +- .../legacy-relay-bridge-restart-stop.test.ts | 5 +- .../legacy-relay-bridge-runtime-exit.test.ts | 5 +- .../legacy-relay-bridge-teardown.test.ts | 5 +- .../ssh-host-relay-census-for-target.test.ts | 4 +- .../ssh-relay-endpoint-incumbent.test.ts | 5 +- ...cad-exited-own-fence-mutation-race.test.ts | 4 +- src/main/ssh/orcad-exited-own-fence.test.ts | 8 +- .../ssh/orcad-exited-own-install-lock.test.ts | 4 +- .../ssh/orcad-exited-own-lock-windows.test.ts | 7 +- .../port-forwarding/ssh-port-forward.test.ts | 102 ++-- .../ssh/unused-host-transport.test-fixture.ts | 14 + src/main/startup/hydrate-shell-path.ts | 8 +- .../updater-sqlite-profile-downgrade.test.ts | 7 +- src/main/updater-test-main-window.ts | 8 + src/main/updater.check-preflight.test.ts | 4 +- .../updater.feed-attempt-lifetime.test.ts | 5 +- .../updater.headless-serve-install.test.ts | 9 +- src/main/updater.mac-install.test.ts | 34 +- src/main/updater.quit-and-install.test.ts | 5 +- src/main/wsl/wsl-orcad-incumbent.test.ts | 5 +- src/renderer/src/components/Landing.tsx | 35 +- .../src/components/LinearItemDrawer.tsx | 3 +- .../activity/ActivityPrototypePage.tsx | 1 - .../automations-page-test-harness.tsx | 3 - .../use-automation-host-catalog.ts | 8 +- .../dashboard/useAgentBucketCounts.ts | 2 +- .../editor/EditorPanelHeaderPath.test.tsx | 3 +- .../components/editor/ImageViewer.test.tsx | 3 +- .../src/components/editor/ImageViewer.tsx | 1 - .../src/components/editor/MonacoEditor.tsx | 1 - .../components/editor/NotesSendMenu.test.tsx | 3 +- .../src/components/editor/PdfViewer.tsx | 1 - .../ReviewNotesSendMenuContent.test.tsx | 7 +- ...editor-autosave-controller-test-fixture.ts | 2 +- .../src/components/editor/ipynb-ansi.ts | 7 +- .../editor/rich-markdown-extensions.ts | 4 +- .../editor/safe-react-node-view-renderer.ts | 72 --- .../use-markdown-preview-components.tsx | 1 - .../editor/use-monaco-markdown-annotations.ts | 9 +- .../editor/useContextualCopySetup.tsx | 3 +- .../editor/useEditorPanelDiffContentLoader.ts | 4 +- .../feature-wall/TasksAnimatedVisual.tsx | 1 - .../OrchestrationPage.tsx | 1 - .../agents-orchestration/StatusesPage.tsx | 1 - .../agents-orchestration/UsagePage.tsx | 1 - .../FloatingTerminalPanel.behavior.test.tsx | 3 +- .../FloatingTerminalToggleButton.test.tsx | 3 +- .../slug-dialog/SlugDialogBody.tsx | 1 - .../use-gitlab-item-dialog-effects.ts | 1 - .../use-gitlab-item-dialog-state.ts | 1 - ...nding-preflight-unverifiable-host.test.tsx | 5 +- .../src/components/mobile/PhoneCarousel.tsx | 2 +- ...veChatQueuedMessageList.in-flight.test.tsx | 8 +- .../NativeChatQueuedMessageList.test.tsx | 4 +- ...cturedAgentSessionAttentionBridge.test.tsx | 6 +- .../use-host-model-catalog-upgrade.test.tsx | 3 +- ...model-catalog-upgrade.unavailable.test.tsx | 3 +- ...agent-session-options.first-frame.test.tsx | 4 +- ...t-session-options.learned-default.test.tsx | 3 +- ...-session-options.preloaded-agents.test.tsx | 4 +- .../use-structured-agent-session-sends.ts | 20 +- .../use-smart-workspace-secondary-searches.ts | 5 +- .../src/components/onboarding/ThemeStep.tsx | 1 - .../src/components/quick-open-file-list.ts | 1 - ...ebar-titlebar-drag-regions.render.test.tsx | 3 +- .../right-sidebar/useFileDuplicate.ts | 1 - .../src/components/settings/BaseRefPicker.tsx | 1 - .../settings/NodeDistMirrorDialog.test.tsx | 4 +- .../TerminalAppearanceSection.ghostty.test.ts | 3 +- .../settings/TerminalPane.pwsh.test.ts | 3 +- ...TerminalSettingsPreview.lifecycle.test.tsx | 3 +- .../TerminalThemeSections.lifecycle.test.ts | 3 +- .../settings/bitbucket-credentials-dialog.tsx | 14 +- ...time-environment-mutation-actions.test.tsx | 5 +- .../settings/useGhosttyImport.test.ts | 3 +- .../settings/useWarpThemeImport.test.ts | 3 +- .../setup-guide/use-setup-guide-progress.ts | 3 - .../sidebar/SidebarFeedbackDialog.tsx | 1 - .../sidebar/WorkspaceKanbanLaneCardList.tsx | 3 +- ...eeTitleInlineRename.begin-editing.test.tsx | 3 +- .../status-bar-provider-menu-focus.test.tsx | 4 +- .../use-workspace-space-manager-panel.ts | 8 +- .../status-bar/workspace-space-metrics.tsx | 1 - .../components/tab-bar/BrowserTab.test.tsx | 3 +- .../components/tab-bar/EditorFileTab.test.tsx | 3 +- .../SortableTab.rename-shortcut.test.tsx | 3 +- .../TabBarQuickCommandsMenu.keyboard.test.ts | 3 +- .../tab-bar/group-tab-order.test.ts | 29 +- ...b-bar-windows-shell-launch-render-stubs.ts | 3 +- .../tab-bar/use-omnibox-browser-history.ts | 3 +- .../useTabGroupWorkspaceModel.focus.test.ts | 4 +- ...ge-jira-saved-filter-load-failure.test.tsx | 17 +- .../gitlab/use-task-page-gitlab-loading.ts | 11 +- ...use-task-page-linear-collection-effects.ts | 24 +- ...se-task-page-linear-custom-view-effects.ts | 6 +- .../use-task-page-linear-list-effects.ts | 29 +- .../use-task-search-debounced-apply.ts | 17 + .../MobileDriverOverlay.test.tsx | 3 +- .../launch-parity-window-tab.test.ts | 4 +- .../pending-split-close-test-fixture.ts | 5 - ...ection-cold-restore-resume-command.spec.ts | 9 +- ...onnection-command-finished-cleanup.spec.ts | 106 ++-- ...nection-command-finished-late-done.test.ts | 6 +- ...onnection-foreground-agent-routing.spec.ts | 108 ++-- ...connection-foreground-process-exit.spec.ts | 8 +- ...y-connection-foreground-write-path.spec.ts | 34 +- ...-hibernation-kill-shell-foreground.test.ts | 6 +- .../pty-connection-hibernation-wake.spec.ts | 88 ++-- ...y-connection-hidden-atlas-recovery.spec.ts | 220 ++++---- ...tion-hidden-backlog-reconciliation.spec.ts | 22 +- ...ty-connection-hidden-codex-queries.spec.ts | 186 ++++--- ...-connection-hidden-sleeping-resume.spec.ts | 9 +- ...-connection-kitty-keyboard-restore.spec.ts | 7 +- .../pty-connection-layout-owner-wake.test.ts | 6 +- ...connection-main-pane-restore-start.test.ts | 6 +- .../pty-connection-main-pane-start.test.ts | 11 +- .../pty-connection-mocks.spec.ts | 1 - ...y-connection-pi-synchronized-input.test.ts | 5 +- ...connection-process-exit-row-retire.test.ts | 21 +- ...pty-connection-reattach-mode-reset.spec.ts | 51 +- ...onnection-remote-pause-drop-notice.spec.ts | 4 +- ...pty-connection-restart-replacement.spec.ts | 4 +- .../pty-connection-session-liveness.spec.ts | 40 +- ...ty-connection-spawn-deferred-sleep.test.ts | 25 +- ...connection-spawn-left-pane-unbound.test.ts | 28 +- .../pty-connection-split-spawn.spec.ts | 56 +- ...-connection-startup-draft-delivery.spec.ts | 30 +- ...-connection-task-complete-dispatch.spec.ts | 27 +- .../pty-connection-test-connect-mock-pane.ts | 24 +- .../pty-connection-unserved-ssh-pane.test.ts | 7 +- ...-connection-visibility-resume-size.spec.ts | 35 +- ...ote-park-reveal-unverifiable-retry.test.ts | 16 +- ...ote-reconnect-unimaged-pane-redraw.test.ts | 4 +- ...mote-runtime-pty-transport-test-harness.ts | 3 - ...ote-snapshot-alt-screen-host-chain.test.ts | 4 +- .../remote-snapshot-alt-screen-replay.test.ts | 4 +- .../terminal-pane-layout-snapshot-merge.ts | 47 ++ .../use-layout-owner-pane-tree.ts | 4 +- .../use-terminal-container-fit-sync.ts | 3 +- .../use-terminal-pane-chat-state.ts | 34 +- .../use-terminal-pane-close-actions.ts | 27 +- .../use-terminal-pane-context-actions.ts | 42 +- .../use-terminal-pane-controller.ts | 11 +- .../use-terminal-pane-global-effects.ts | 14 +- .../use-terminal-pane-global-listeners.ts | 6 +- .../use-terminal-pane-layout-bindings.ts | 3 +- .../use-terminal-pane-layout-persistence.ts | 229 ++++----- .../use-terminal-pane-mobile-actions.ts | 28 +- .../use-terminal-pane-process-exit-actions.ts | 10 +- .../use-terminal-pane-projection.ts | 3 +- .../use-terminal-pane-reconciliation.ts | 12 +- .../use-terminal-pane-startup-actions.ts | 37 +- .../use-terminal-pane-title-effects.ts | 33 +- .../use-terminal-pane-title-state.ts | 6 +- .../use-activation-deferred-tab-admission.ts | 10 +- .../terminal/use-active-terminal-repair.ts | 10 +- .../use-task-page-jira-list-effects.ts | 15 +- .../use-task-page-provider-metadata.ts | 1 - .../use-terminal-browser-retention.ts | 7 +- ...se-terminal-editor-close-dialog-actions.ts | 25 +- .../use-terminal-editor-close-queue.ts | 36 +- .../use-terminal-parking-foundation.ts | 6 +- .../components/use-terminal-parking-pass.ts | 7 +- .../use-terminal-watcher-effects.ts | 4 +- .../use-terminal-window-lifecycle.ts | 3 +- .../use-worktree-jump-palette-list-entries.ts | 3 +- ...use-worktree-jump-palette-quick-actions.ts | 2 +- .../use-worktree-jump-palette-recent-tabs.ts | 49 +- ...worktree-jump-palette-selection-actions.ts | 58 +-- ...rktree-jump-palette-selection-lifecycle.ts | 40 +- ...vents-agent-status-window-test-fixtures.ts | 3 - .../ipc-events-close-routing-test-harness.ts | 3 - ...ipc-events-terminal-create-test-harness.ts | 3 - .../src/hooks/ipc-events-test-harness.ts | 3 - .../src/hooks/useSkillFreshness.test.tsx | 37 +- src/renderer/src/hooks/useSkillFreshness.ts | 13 +- src/renderer/src/hooks/useUnreadDockBadge.ts | 1 - ...tualizedScrollAnchor.listener-deps.test.ts | 2 - ...dScrollAnchor.marks-restore-signal.test.ts | 5 - .../src/lib/execution-host-facts.test.ts | 4 +- .../lib/host-mirror-handle-gap-drain.test.ts | 21 +- ...st-mirror-handle-gap-landed-handle.test.ts | 27 +- .../lib/host-mirror-handle-gap-resume.test.ts | 39 +- ...r-handle-gap-subscription-lifetime.test.ts | 17 +- .../host-mirror-handle-gap-teardown.test.ts | 18 +- ...st-mirror-handle-gap-verdict-union.test.ts | 13 +- .../option-as-alt-probe.test.ts | 56 +- ...-output-scheduler-throughput.bench.test.ts | 1 - ...eping-agent-session-provider-claim.test.ts | 25 +- .../lib/worktree-activation-reveal.test.ts | 1 - .../host-session-mirror-frame-fixtures.ts | 6 +- ...ion-mirror-frame-ordering-test-harness.ts} | 22 +- ...n-mirror-hydration-frame-ordering.test.tsx | 2 +- ...sion-mirror-settle-receipt-frames.test.tsx | 2 +- ...paired-runtime-environment-test-fixture.ts | 37 ++ .../server-layout-frames.test-fixture.ts | 13 +- ...ime-session-mirror-environment-key.test.ts | 43 +- ...-tabs-retired-epoch-census-repair.test.tsx | 16 +- ...ssion-tabs-stream-end-resubscribe.test.tsx | 16 +- ...bs-sync-initial-terminal-relaunch.test.tsx | 13 +- ...n-tabs-sync-reconnect-resubscribe.test.tsx | 7 +- ...ssion-tabs-sync-unverifiable-host.test.tsx | 7 +- ...on-tabs-sync-visibility-collision.test.tsx | 18 +- ...ssion-tabs-sync-window-visibility.test.tsx | 18 +- ...session-tabs-sync-worktree-switch.test.tsx | 12 +- .../src/store/github/cache-identity.ts | 10 +- .../src/store/github/pr-result-cache.ts | 3 +- .../src/store/github/pr-result-routing.ts | 3 +- .../store/github/pull-request-execution.ts | 3 +- .../src/store/github/work-item-routing.ts | 14 +- src/renderer/src/store/slices/agent-status.ts | 3 +- src/renderer/src/store/slices/browser.test.ts | 5 +- .../slices/editor-close-file-cleanup.test.ts | 10 +- .../editor-markdown-link-activation.test.ts | 7 +- .../src/store/slices/github-checks.ts | 7 +- .../src/store/slices/hosted-review.test.ts | 8 +- .../slices/jira-connection-identity.test.ts | 11 +- .../slices/jira-sprint-filter-cache.test.ts | 9 +- .../slices/jira-status-filter-cache.test.ts | 9 +- .../slices/jira-web-client-status.test.ts | 11 +- ...ktrees-fetch-stale-catalog-version.test.ts | 31 +- src/renderer/src/web/WebConnect.tsx | 7 +- src/shared/agent-title-decoration.ts | 1 - src/shared/agent-title-glyphs.ts | 1 - src/shared/ansi-escape-sequences.ts | 3 +- src/shared/github/github-cache-key.ts | 5 +- src/shared/jcode-session-files.test.ts | 5 +- .../rpc-contract/agent-launch-params.ts | 10 +- src/shared/source-control-commit-failure.ts | 16 +- src/shared/source-control-push-failure.ts | 16 +- src/shared/zod-salvage.ts | 7 +- src/wsl-guest/protocol.ts | 16 +- .../wsl-install-plugins-handler.test.ts | 44 +- .../e2e/__mocks__/native-chat-ssh-delivery.ts | 13 +- .../structured-attention-read-surfaces.ts | 3 +- .../e2e/agent-dashboard-status-burst.spec.ts | 8 +- ...ent-session-live-force-exit-resume.spec.ts | 4 +- .../agent-session-log-tail-stability.spec.ts | 1 - .../ai-vault-typing-bench-renderer-probe.ts | 5 +- tests/e2e/app-menu-paste-ownership.spec.ts | 2 +- ...ificial-opencode-main-pressure-scenario.ts | 6 +- ...mation-hidden-terminal-first-mount.spec.ts | 3 +- .../browser-floating-shortcut-scope.spec.ts | 6 +- tests/e2e/browser-tab.spec.ts | 15 +- .../e2e/chinese-ime-chat-input-repro.spec.ts | 9 +- .../e2e/codex-account-switch-dismiss.spec.ts | 6 +- ...x-child-approval-activity-row.unit.test.ts | 20 +- tests/e2e/codex-ctrl-c-status.spec.ts | 2 +- .../e2e/combined-diff-scroll-restore.spec.ts | 2 +- ...completed-worker-retirement-resume.spec.ts | 11 +- ...eted-worker-retirement-resume.unit.test.ts | 33 +- ...-handle-replacement-downgrade.unit.test.ts | 27 +- ...on-interrupted-tool-downgrade.unit.test.ts | 30 +- ...nt-session-newer-chat-refusal.unit.test.ts | 23 +- ...ssion-resume-marker-downgrade.unit.test.ts | 6 +- ...-session-stop-event-downgrade.unit.test.ts | 46 +- ...oss-version-registered-agents.unit.test.ts | 25 +- .../cross-version-stop-note-test-rig.ts | 16 +- .../cross-version-web-device-key.unit.test.ts | 8 +- ...ht-workspace-scoped-downgrade.unit.test.ts | 18 +- .../cross-version-wire/release-checkout.ts | 10 + ...untime-source-fence-downgrade.unit.test.ts | 19 +- ...on-search-agent-set-downgrade.unit.test.ts | 21 +- .../structured-agent-session-host-fixture.ts | 14 +- ...ructured-agent-session-surface-manifest.ts | 13 +- ...ubmission-positions-downgrade.unit.test.ts | 25 +- .../versioned-agent-session-wire.ts | 8 +- ...e-ports-host-scoped-downgrade.unit.test.ts | 22 +- .../editable-context-paste-ownership.spec.ts | 2 +- ...file-explorer-linked-watch-refresh.spec.ts | 6 +- .../daemon-generation-legacy-close-client.ts | 54 +- .../daemon-generation-reconnect-client.ts | 5 +- tests/e2e/fixtures/daemon-lifecycle-entry.ts | 3 +- tests/e2e/github-cli-stall-repro.spec.ts | 14 +- ...github-created-issue-start-prefill.spec.ts | 9 +- ...paired-remote-terminal-reveal-grid.spec.ts | 2 +- tests/e2e/helpers/electron-home-isolation.ts | 13 +- .../helpers/headless-paired-runtime-host.ts | 27 +- tests/e2e/helpers/native-chat-view.ts | 2 +- .../nested-runtime-ssh-client-route.ts | 4 +- tests/e2e/helpers/orcad-managed-workspace.ts | 3 +- .../paired-terminal-cold-activation-oracle.ts | 14 +- tests/e2e/helpers/runtime-types.ts | 2 + tests/e2e/helpers/shortcuts.ts | 2 +- tests/e2e/helpers/xterm-render-dimensions.ts | 9 + ...nch-settings-hook-bookkeeping.unit.test.ts | 2 +- .../issue-12656-terminal-link-tooltip.spec.ts | 9 +- ...an-ime-terminal-shift-enter-commit.spec.ts | 5 +- tests/e2e/large-diff-freeze-repro.spec.ts | 5 +- .../e2e/linear-issue-view-persistence.spec.ts | 5 +- ...ackground-terminal-mount-authority.spec.ts | 26 +- ...worktree-visibility-runtime-active.spec.ts | 11 +- tests/e2e/markdown-tab-scroll-restore.spec.ts | 2 +- .../multi-client-navigation-isolation.spec.ts | 7 +- ...native-chat-history-prepend-anchor.spec.ts | 6 +- ...e-chat-startup-option-pickers.unit.test.ts | 3 +- .../nested-agent-idle-session-owner.spec.ts | 2 +- tests/e2e/omp-model-picker.spec.ts | 4 +- ...y-worker-missing-terminal-recovery.spec.ts | 10 +- ...tion-worker-settlement-release-cli.spec.ts | 13 +- ...tration-worker-terminal-visibility.spec.ts | 13 +- ...ration-worker-transcript-providers.spec.ts | 13 +- .../e2e/paired-quick-open-large-tree.spec.ts | 47 +- .../paired-remote-pane-layout-retry.spec.ts | 22 +- ...aired-remote-terminal-browser-link.spec.ts | 15 +- ...minal-host-restart-background-sync.spec.ts | 2 +- ...rminal-parked-reveal-interactivity.spec.ts | 2 +- ...erminal-parked-scrollback-survives.spec.ts | 4 +- ...red-remote-terminal-stall-recovery.spec.ts | 7 +- .../e2e/paired-startup-exec-readiness.spec.ts | 3 +- tests/e2e/plugin-marketplace-content.spec.ts | 17 +- ...r11346-selected-runtime-identity-oracle.ts | 42 +- tests/e2e/project-group-manual-sort.spec.ts | 12 +- .../e2e/relay-region-correction.unit.test.ts | 4 +- ...ote-session-bulk-open-freeze-repro.spec.ts | 4 +- tests/e2e/remote-terminal-input-model-link.ts | 7 +- ...emote-terminal-tab-retirement.unit.test.ts | 15 +- ...erer-crash-recovery-terminal-input.spec.ts | 1 - tests/e2e/repo-registration-async.spec.ts | 2 +- ...ored-terminal-input-readiness.unit.test.ts | 2 +- tests/e2e/setup-guide-sidebar.spec.ts | 27 +- ...ce-control-create-pr-intent-switch.spec.ts | 2 + tests/e2e/source-control-create-pr.spec.ts | 10 +- tests/e2e/ssh-orcad-auto-convert.spec.ts | 9 +- .../ssh-orcad-browser-capabilities.spec.ts | 5 +- tests/e2e/ssh-orcad-browser-routing.spec.ts | 5 +- .../ssh-orcad-browser-service-status.spec.ts | 3 +- .../ssh-orcad-explorer-selected-host.spec.ts | 5 +- .../ssh-orcad-terminal-network-outage.spec.ts | 2 +- tests/e2e/staging-skill-sharing.spec.ts | 19 +- ...ured-agent-session-read-owner.unit.test.ts | 20 +- ...attention-read-retirement.test-fixture.tsx | 2 +- ...red-attention-read-retirement.unit.test.ts | 2 +- ...-chat-owner-status-activation.unit.test.ts | 4 +- tests/e2e/tab-rename-paste-ownership.spec.ts | 2 +- tests/e2e/tab-rename.spec.ts | 5 +- tests/e2e/tasks-page.spec.ts | 12 +- ...terminal-codex-cursor-jitter-repro.spec.ts | 6 +- ...al-codex-hidden-startup-background.spec.ts | 13 +- ...erminal-codex-local-typing-latency.spec.ts | 5 +- ...codex-skill-preview-artifact-repro.spec.ts | 105 ++-- ...minal-foreground-confirmation.unit.test.ts | 3 +- .../terminal-foreground-redraw-freeze.spec.ts | 2 +- .../terminal-ime-preedit-cell-grid.spec.ts | 15 +- ...terminal-long-table-scroll-restore.spec.ts | 5 +- ...nal-opencode-emoji-table-rendering.spec.ts | 5 +- .../terminal-osc8-cold-park-restore.spec.ts | 3 + ...terminal-pane-divider-capture-loss.spec.ts | 6 +- ...nal-raw-emoji-table-scroll-restore.spec.ts | 3 +- ...erminal-reveal-paused-render-repro.spec.ts | 4 +- tests/e2e/terminal-search-results.spec.ts | 6 +- tests/e2e/terminal-shortcuts.spec.ts | 8 +- tests/e2e/terminal-sleep-wake-restore.spec.ts | 2 +- ...terminal-tab-switch-visual-restore.spec.ts | 5 +- tests/e2e/terminal-tui-wheel-drain.spec.ts | 6 +- tests/e2e/terminal-tui-wheel-reports.spec.ts | 11 +- ...al-warm-reattach-split-live-output.spec.ts | 17 +- tests/e2e/workspace-space-git-status.spec.ts | 2 +- .../daemon-relocation-spike/ndjson-client.mjs | 1 - .../omp-child-history-rendered/fixture.tsx | 2 + tests/tools/omp-fresh-launch-windows-smoke.ts | 2 +- .../omp-remote-sidebar-rendered/fixture.tsx | 3 +- .../tools/omp-transcript-rendered/fixture.tsx | 2 +- 745 files changed, 5093 insertions(+), 4935 deletions(-) create mode 100644 mobile/src/test-support/react-test-renderer-host-nodes.ts create mode 100644 mobile/src/transport/rpc-client-test-stub.ts create mode 100644 src/main/runtime/device-registry-file-read.test.ts create mode 100644 src/main/runtime/runtime-store-partial-fixture.ts create mode 100644 src/main/ssh/unused-host-transport.test-fixture.ts create mode 100644 src/main/updater-test-main-window.ts delete mode 100644 src/renderer/src/components/editor/safe-react-node-view-renderer.ts create mode 100644 src/renderer/src/components/task-page/use-task-search-debounced-apply.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-layout-snapshot-merge.ts rename src/renderer/src/runtime/{host-session-mirror-frame-ordering-harness.ts => host-session-mirror-frame-ordering-test-harness.ts} (92%) create mode 100644 src/renderer/src/runtime/paired-runtime-environment-test-fixture.ts create mode 100644 tests/e2e/helpers/xterm-render-dimensions.ts diff --git a/config/e2e-typecheck-baseline.txt b/config/e2e-typecheck-baseline.txt index b3864f07fc2..1782c09536f 100644 --- a/config/e2e-typecheck-baseline.txt +++ b/config/e2e-typecheck-baseline.txt @@ -1,122 +1,3 @@ # Type errors per file under tests/ (tsc -p config/tsconfig.e2e.json). # This is a RATCHET: counts may only go DOWN. Fix the errors in a file you touch. # Prune after a fix: pnpm check:e2e-typecheck-ratchet --prune -2 tests/e2e/__mocks__/native-chat-ssh-delivery.ts -1 tests/e2e/agent-dashboard-status-burst.spec.ts -1 tests/e2e/agent-session-live-force-exit-resume.spec.ts -1 tests/e2e/ai-vault-typing-bench-renderer-probe.ts -1 tests/e2e/app-menu-paste-ownership.spec.ts -1 tests/e2e/artificial-opencode-main-pressure-scenario.ts -1 tests/e2e/automation-hidden-terminal-first-mount.spec.ts -1 tests/e2e/browser-floating-shortcut-scope.spec.ts -4 tests/e2e/browser-tab.spec.ts -3 tests/e2e/chinese-ime-chat-input-repro.spec.ts -1 tests/e2e/codex-account-switch-dismiss.spec.ts -3 tests/e2e/codex-child-approval-activity-row.unit.test.ts -1 tests/e2e/codex-ctrl-c-status.spec.ts -2 tests/e2e/combined-diff-scroll-restore.spec.ts -2 tests/e2e/completed-worker-retirement-resume.spec.ts -1 tests/e2e/completed-worker-retirement-resume.unit.test.ts -4 tests/e2e/cross-version-wire/agent-session-handle-replacement-downgrade.unit.test.ts -1 tests/e2e/cross-version-wire/agent-session-interrupted-tool-downgrade.unit.test.ts -2 tests/e2e/cross-version-wire/agent-session-newer-chat-refusal.unit.test.ts -1 tests/e2e/cross-version-wire/agent-session-resume-marker-downgrade.unit.test.ts -3 tests/e2e/cross-version-wire/cross-version-stop-note-test-rig.ts -3 tests/e2e/cross-version-wire/cross-version-web-device-key.unit.test.ts -2 tests/e2e/cross-version-wire/session-search-agent-set-downgrade.unit.test.ts -1 tests/e2e/cross-version-wire/structured-agent-session-host-fixture.ts -1 tests/e2e/cross-version-wire/submission-positions-downgrade.unit.test.ts -1 tests/e2e/cross-version-wire/versioned-agent-session-wire.ts -1 tests/e2e/editable-context-paste-ownership.spec.ts -1 tests/e2e/file-explorer-linked-watch-refresh.spec.ts -5 tests/e2e/fixtures/daemon-generation-legacy-close-client.ts -2 tests/e2e/fixtures/daemon-generation-reconnect-client.ts -1 tests/e2e/fixtures/daemon-lifecycle-entry.ts -2 tests/e2e/github-cli-stall-repro.spec.ts -1 tests/e2e/github-created-issue-start-prefill.spec.ts -1 tests/e2e/headless-paired-remote-terminal-reveal-grid.spec.ts -1 tests/e2e/headless-serve-desktop-activation.spec.ts -5 tests/e2e/helpers/headless-paired-runtime-host.ts -1 tests/e2e/helpers/native-chat-view.ts -2 tests/e2e/helpers/nested-runtime-ssh-client-route.ts -1 tests/e2e/helpers/orcad-managed-workspace.ts -2 tests/e2e/helpers/paired-terminal-cold-activation-oracle.ts -2 tests/e2e/helpers/shortcuts.ts -1 tests/e2e/host-agent-launch-settings-hook-bookkeeping.unit.test.ts -4 tests/e2e/issue-12656-terminal-link-tooltip.spec.ts -1 tests/e2e/korean-ime-terminal-shift-enter-commit.spec.ts -1 tests/e2e/large-diff-freeze-repro.spec.ts -1 tests/e2e/linear-issue-view-persistence.spec.ts -2 tests/e2e/live-background-terminal-mount-authority.spec.ts -1 tests/e2e/local-worktree-visibility-runtime-active.spec.ts -1 tests/e2e/markdown-tab-scroll-restore.spec.ts -2 tests/e2e/multi-client-navigation-isolation.spec.ts -2 tests/e2e/native-chat-history-prepend-anchor.spec.ts -2 tests/e2e/native-chat-startup-option-pickers.unit.test.ts -1 tests/e2e/nested-agent-idle-session-owner.spec.ts -2 tests/e2e/omp-model-picker.spec.ts -2 tests/e2e/orchestration-legacy-worker-missing-terminal-recovery.spec.ts -1 tests/e2e/orchestration-worker-settlement-release-cli.spec.ts -1 tests/e2e/orchestration-worker-terminal-visibility.spec.ts -1 tests/e2e/orchestration-worker-transcript-providers.spec.ts -6 tests/e2e/paired-quick-open-large-tree.spec.ts -3 tests/e2e/paired-remote-pane-layout-retry.spec.ts -3 tests/e2e/paired-remote-terminal-browser-link.spec.ts -1 tests/e2e/paired-remote-terminal-host-restart-background-sync.spec.ts -1 tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts -4 tests/e2e/paired-remote-terminal-parked-scrollback-survives.spec.ts -3 tests/e2e/paired-remote-terminal-stall-recovery.spec.ts -1 tests/e2e/paired-startup-exec-readiness.spec.ts -1 tests/e2e/plugin-marketplace-content.spec.ts -2 tests/e2e/pr11346-selected-runtime-identity-oracle.ts -5 tests/e2e/project-group-manual-sort.spec.ts -1 tests/e2e/relay-region-correction.unit.test.ts -2 tests/e2e/remote-session-bulk-open-freeze-repro.spec.ts -2 tests/e2e/remote-terminal-input-delivery-model.unit.test.ts -4 tests/e2e/remote-terminal-input-model-link.ts -1 tests/e2e/remote-terminal-tab-retirement.unit.test.ts -2 tests/e2e/repo-registration-async.spec.ts -1 tests/e2e/restored-terminal-input-readiness.unit.test.ts -5 tests/e2e/setup-guide-sidebar.spec.ts -1 tests/e2e/source-control-create-pr-intent-switch.spec.ts -2 tests/e2e/source-control-create-pr.spec.ts -4 tests/e2e/ssh-orcad-auto-convert.spec.ts -2 tests/e2e/ssh-orcad-browser-capabilities.spec.ts -2 tests/e2e/ssh-orcad-browser-routing.spec.ts -1 tests/e2e/ssh-orcad-browser-service-status.spec.ts -2 tests/e2e/ssh-orcad-explorer-selected-host.spec.ts -1 tests/e2e/ssh-orcad-terminal-network-outage.spec.ts -5 tests/e2e/staging-skill-sharing.spec.ts -3 tests/e2e/structured-agent-session-read-owner.unit.test.ts -2 tests/e2e/structured-attention-prompt-relay.unit.test.ts -1 tests/e2e/structured-attention-read-retirement.test-fixture.tsx -1 tests/e2e/structured-attention-read-retirement.unit.test.ts -1 tests/e2e/structured-chat-owner-status-activation.unit.test.ts -1 tests/e2e/tab-rename-paste-ownership.spec.ts -1 tests/e2e/tab-rename.spec.ts -3 tests/e2e/tasks-page.spec.ts -1 tests/e2e/terminal-codex-cursor-jitter-repro.spec.ts -4 tests/e2e/terminal-codex-hidden-startup-background.spec.ts -1 tests/e2e/terminal-codex-local-typing-latency.spec.ts -2 tests/e2e/terminal-codex-skill-preview-artifact-repro.spec.ts -1 tests/e2e/terminal-foreground-confirmation.unit.test.ts -2 tests/e2e/terminal-foreground-redraw-freeze.spec.ts -2 tests/e2e/terminal-ime-preedit-cell-grid.spec.ts -1 tests/e2e/terminal-long-table-scroll-restore.spec.ts -1 tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts -4 tests/e2e/terminal-osc8-cold-park-restore.spec.ts -2 tests/e2e/terminal-pane-divider-capture-loss.spec.ts -3 tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts -2 tests/e2e/terminal-reveal-paused-render-repro.spec.ts -2 tests/e2e/terminal-search-results.spec.ts -2 tests/e2e/terminal-shortcuts.spec.ts -1 tests/e2e/terminal-sleep-wake-restore.spec.ts -1 tests/e2e/terminal-tab-switch-visual-restore.spec.ts -1 tests/e2e/terminal-tui-wheel-drain.spec.ts -2 tests/e2e/terminal-tui-wheel-reports.spec.ts -1 tests/e2e/terminal-warm-reattach-split-live-output.spec.ts -1 tests/e2e/workspace-space-git-status.spec.ts -1 tests/tools/omp-child-history-rendered/fixture.tsx -1 tests/tools/omp-fresh-launch-windows-smoke.ts -1 tests/tools/omp-remote-sidebar-rendered/fixture.tsx -1 tests/tools/omp-transcript-rendered/fixture.tsx diff --git a/config/focus-setting-read-baseline.txt b/config/focus-setting-read-baseline.txt index a138f65f46f..7e7f901c675 100644 --- a/config/focus-setting-read-baseline.txt +++ b/config/focus-setting-read-baseline.txt @@ -34,8 +34,6 @@ 1 src/renderer/src/components/editor/EditorMarkdownFileSurface.tsx 1 src/renderer/src/components/editor/combined-diff/CombinedDiffViewer.tsx 1 src/renderer/src/components/editor/csv/csv-link-routing.ts -2 src/renderer/src/components/editor/editor-autosave-controller-test-fixture.ts -1 src/renderer/src/components/editor/editor-model-lifetime-fixture.ts 1 src/renderer/src/components/editor/markdown-preview-link-actions.ts 1 src/renderer/src/components/editor/markdown-preview-system-link-action.ts 1 src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.ts @@ -61,8 +59,6 @@ 2 src/renderer/src/components/linear-item-drawer-comment-footer.tsx 6 src/renderer/src/components/linear-item-drawer-edit-controller.tsx 1 src/renderer/src/components/linear-item-drawer-edit-section.tsx -2 src/renderer/src/components/native-chat-resume-modal.test-support.tsx -1 src/renderer/src/components/native-chat-resume-tree.test-support.ts 1 src/renderer/src/components/native-chat/NativeChatVisualFrame.tsx 2 src/renderer/src/components/native-chat/native-chat-http-link-source-owner.ts 1 src/renderer/src/components/native-chat/native-chat-link-target-actions.ts @@ -72,7 +68,6 @@ 1 src/renderer/src/components/new-workspace/use-smart-workspace-secondary-searches.ts 2 src/renderer/src/components/new-workspace/use-work-item-source-foundation.ts 1 src/renderer/src/components/new-workspace/use-work-item-source-search.ts -1 src/renderer/src/components/quick-open-file-list-test-harness.ts 1 src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.tsx 3 src/renderer/src/components/right-sidebar/checks-panel-hosted-review-click-routing.ts 1 src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-create-review.tsx @@ -99,7 +94,6 @@ 3 src/renderer/src/components/settings/jira-integration-card.tsx 1 src/renderer/src/components/settings/linear-agent-skill-install-cta.tsx 1 src/renderer/src/components/settings/provider-account-scope.ts -1 src/renderer/src/components/settings/settings-persistence-test-fixture.ts 2 src/renderer/src/components/settings/settings-setup-guide-progress.ts 1 src/renderer/src/components/settings/source-control-preflight-card-status.ts 2 src/renderer/src/components/settings/task-tracker-integration-cards.tsx @@ -166,17 +160,20 @@ 1 src/renderer/src/components/status-bar/use-workspace-space-manager-panel.ts 1 src/renderer/src/components/status-bar/workspace-space-decision-details.ts 1 src/renderer/src/components/tab-bar/TabBar.tsx -3 src/renderer/src/components/tab-bar/tab-bar-windows-shell-launch-test-harness.ts 2 src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts 1 src/renderer/src/components/tab-bar/web-runtime-tab-move-mirror.ts 1 src/renderer/src/components/tab-bar/windows-shell-menu-visibility.ts 3 src/renderer/src/components/tab-group/useTabGroupCreationCommands.ts 1 src/renderer/src/components/task-page-jira-status-order.ts -2 src/renderer/src/components/task-page/linear/task-page-linear-issue-model.tsx 9 src/renderer/src/components/task-page/TaskPage.tsx 2 src/renderer/src/components/task-page/jira/IssueAssigneeField.tsx 1 src/renderer/src/components/task-page/linear/Filters.tsx -1 src/renderer/src/components/terminal-pane/pending-split-close-test-fixture.ts +2 src/renderer/src/components/task-page/linear/task-page-linear-issue-model.tsx +4 src/renderer/src/components/task-page/linear/use-task-page-linear-board.ts +6 src/renderer/src/components/task-page/linear/use-task-page-linear-creation-state.ts +4 src/renderer/src/components/task-page/linear/use-task-page-linear-issue-creation.ts +2 src/renderer/src/components/task-page/linear/use-task-page-linear-project-creation.ts +2 src/renderer/src/components/task-page/use-task-page-source-availability.ts 1 src/renderer/src/components/terminal-pane/pty-connection/agent-idle-working-handlers.ts 1 src/renderer/src/components/terminal-pane/retire-unbound-runtime-terminal-pane.ts 1 src/renderer/src/components/terminal-pane/terminal-fit-restore.ts @@ -194,12 +191,7 @@ 4 src/renderer/src/components/use-task-page-jira-creation-metadata.ts 4 src/renderer/src/components/use-task-page-jira-issue-creation.ts 1 src/renderer/src/components/use-task-page-jira-list-effects.ts -4 src/renderer/src/components/task-page/linear/use-task-page-linear-board.ts -6 src/renderer/src/components/task-page/linear/use-task-page-linear-creation-state.ts -4 src/renderer/src/components/task-page/linear/use-task-page-linear-issue-creation.ts -2 src/renderer/src/components/task-page/linear/use-task-page-linear-project-creation.ts 2 src/renderer/src/components/use-task-page-provider-metadata.ts -2 src/renderer/src/components/task-page/use-task-page-source-availability.ts 2 src/renderer/src/components/use-task-page-store-bindings.ts 5 src/renderer/src/components/use-terminal-create-actions.ts 2 src/renderer/src/components/use-worktree-jump-palette-controller.ts @@ -214,9 +206,6 @@ 1 src/renderer/src/hooks/composer-state/runtime-target-selection.ts 1 src/renderer/src/hooks/editor-external-watch-target-rows.ts 2 src/renderer/src/hooks/editor-external-watch-targets.ts -1 src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts -1 src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts -6 src/renderer/src/hooks/ipc-events-test-harness.ts 2 src/renderer/src/hooks/ipc-events/app-lifetime-ipc-bridge.ts 1 src/renderer/src/hooks/ipc-events/runtime-environment-subscription-selection.ts 1 src/renderer/src/hooks/ipc-events/settings-sidebar-ipc-bridge.ts @@ -232,7 +221,6 @@ 7 src/renderer/src/hooks/useSettingsNavigationMetadata.ts 1 src/renderer/src/hooks/useUnreadDockBadge.ts 2 src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.ts -1 src/renderer/src/lib/agent-hibernation-coordinator-test-fixture.ts 1 src/renderer/src/lib/client-creation-action-error.ts 2 src/renderer/src/lib/client-creation-action-policy.ts 1 src/renderer/src/lib/default-creation-host.ts # the sanctioned reader of the default host @@ -245,7 +233,6 @@ 1 src/renderer/src/lib/linear-issue-url-lookup.ts 1 src/renderer/src/lib/local-preflight-context.ts 2 src/renderer/src/lib/provider-runtime-context.ts -1 src/renderer/src/lib/remote-workspace-drop-test-fixtures.ts 2 src/renderer/src/lib/repo-runtime-owner.ts 1 src/renderer/src/lib/runtime-session-mirror-owners.ts 1 src/renderer/src/lib/runtime-session-mirror-targets.ts @@ -284,7 +271,6 @@ 2 src/renderer/src/store/repos/repo-add-actions.ts 1 src/renderer/src/store/repos/repo-update.ts 1 src/renderer/src/store/repos/runtime-repo-catalog-actions.ts -1 src/renderer/src/store/slices/browser-slice-test-harness.ts 16 src/renderer/src/store/slices/browser/browser-cookie-import-actions.ts 1 src/renderer/src/store/slices/browser/browser-host-actions.ts 7 src/renderer/src/store/slices/browser/browser-host-state.ts @@ -293,7 +279,6 @@ 4 src/renderer/src/store/slices/browser/browser-tab-actions.ts 1 src/renderer/src/store/slices/browser/create-browser-slice.ts 1 src/renderer/src/store/slices/browser/paired-browser-tab-creator.ts -2 src/renderer/src/store/slices/editor-slice-test-harness.ts 1 src/renderer/src/store/slices/editor/actions/markdown-link-action.ts 1 src/renderer/src/store/slices/editor/actions/markdown-preview-actions.ts 1 src/renderer/src/store/slices/editor/actions/open-file-action.ts @@ -305,7 +290,6 @@ 8 src/renderer/src/store/slices/jira-issue-read-actions.ts 6 src/renderer/src/store/slices/jira-read-coordination.ts 3 src/renderer/src/store/slices/jira.ts -1 src/renderer/src/store/slices/linear-slice-test-harness.ts 11 src/renderer/src/store/slices/linear.ts 5 src/renderer/src/store/slices/linear/linear-custom-view-detail-actions.ts 5 src/renderer/src/store/slices/linear/linear-custom-view-issue-actions.ts @@ -334,7 +318,6 @@ 3 src/renderer/src/store/slices/workspace-attention/workspace-unread-characterization-fixture.ts 1 src/renderer/src/store/slices/worktree-visibility-owner-settings.ts 3 src/renderer/src/store/slices/worktree-visibility-settings-write.ts -3 src/renderer/src/store/slices/worktrees-slice-test-harness.ts 5 src/renderer/src/store/slices/worktrees.ts 1 src/renderer/src/store/slices/worktrees/create/create-worktree.ts 1 src/renderer/src/store/slices/worktrees/create/created-worktree-state-merge.ts diff --git a/config/oxlint-anti-slop.json b/config/oxlint-anti-slop.json index c9ed54f137b..cfe05973059 100644 --- a/config/oxlint-anti-slop.json +++ b/config/oxlint-anti-slop.json @@ -47,10 +47,13 @@ "overrides": [ { "files": [ - "**/*.test.{ts,tsx}", + "**/*.test.{ts,tsx,mjs}", "**/*.spec.{ts,tsx}", - // Extracted React Native mocks remain test-only. - "**/mobile/src/session/MobileNativeChatView.harness.test-fixture.ts", + // Vitest support modules shared by split suites; only test files import them. + "**/*-test-harness.{ts,tsx}", + "**/*-test-fixture.{ts,tsx}", + "**/*-test-fixtures.{ts,tsx}", + "**/*.test-fixture.{ts,tsx}", "tests/**/*.{ts,tsx}", "**/__mocks__/**" ], @@ -67,24 +70,9 @@ "anti-slop/no-module-mocking": "off" } }, - // Test-only builtin adapters publish Node-compatible behavior and instrument OS metrics. + // Test-only builtin adapter publishing Node-compatible behavior. { - "files": ["**/vitest-bun-node-builtins.ts", "**/hang-watchdog-process-metrics.test.mjs"], - "rules": { - "anti-slop/no-module-mocking": "off" - } - }, - // Test-only side effect for explicit RPC registries; real catalog tests never import it. - { - "files": ["**/src/main/runtime/rpc/unused-default-rpc-methods.test-fixture.ts"], - "rules": { - "anti-slop/no-module-mocking": "off" - } - }, - // Shared render harness for the split MobileNativeChatView suites; it registers the same - // hoisted mocks every suite needs and is only imported by those test files. - { - "files": ["**/mobile/src/session/MobileNativeChatView.harness.test-fixture.ts"], + "files": ["**/vitest-bun-node-builtins.ts"], "rules": { "anti-slop/no-module-mocking": "off" } diff --git a/config/scripts/check-focus-read-ratchet.mjs b/config/scripts/check-focus-read-ratchet.mjs index 06042a12c31..0e8d1147c58 100644 --- a/config/scripts/check-focus-read-ratchet.mjs +++ b/config/scripts/check-focus-read-ratchet.mjs @@ -354,8 +354,12 @@ const BASELINE_HEADER = [ const ADVICE = "Route by the resource's owner. Only a creation flow with no source row may use defaultCreationHost." +// Why test support too: harnesses and fixtures seed settings for tests; they never route host work. +const TEST_SUPPORT_PATH = + /(?:\.(?:test|spec)(?:-fixture|-support)?|-test-(?:harness|fixtures?))\.tsx?$/ + export function isScannedPath(rel) { - return /\.(ts|tsx)$/.test(rel) && !/\.(test|spec)(?:-fixture)?\.tsx?$/.test(rel) + return /\.(ts|tsx)$/.test(rel) && !TEST_SUPPORT_PATH.test(rel) } // One read and one discovery per root per run; `--prune` then `main` reuse them. diff --git a/config/scripts/check-focus-read-ratchet.test.mjs b/config/scripts/check-focus-read-ratchet.test.mjs index 790b5a8b405..4c10a7434c1 100644 --- a/config/scripts/check-focus-read-ratchet.test.mjs +++ b/config/scripts/check-focus-read-ratchet.test.mjs @@ -289,5 +289,10 @@ describe('isScannedPath', () => { expect(isScannedPath('src/renderer/src/a.test.ts')).toBe(false) expect(isScannedPath('src/renderer/src/a.spec.tsx')).toBe(false) expect(isScannedPath('src/renderer/src/a.test-fixture.ts')).toBe(false) + expect(isScannedPath('src/renderer/src/a.test-support.tsx')).toBe(false) + expect(isScannedPath('src/renderer/src/a-test-harness.ts')).toBe(false) + expect(isScannedPath('src/renderer/src/a-test-fixture.ts')).toBe(false) + expect(isScannedPath('src/renderer/src/a-test-fixtures.ts')).toBe(false) + expect(isScannedPath('src/renderer/src/editor-model-lifetime-fixture.ts')).toBe(true) }) }) diff --git a/config/scripts/headless-serve-shutdown-matrix.test.mjs b/config/scripts/headless-serve-shutdown-matrix.test.mjs index fa9a9c6943f..37e8ccce495 100644 --- a/config/scripts/headless-serve-shutdown-matrix.test.mjs +++ b/config/scripts/headless-serve-shutdown-matrix.test.mjs @@ -1,7 +1,3 @@ -/* oxlint-disable anti-slop/no-module-mocking -- This IS the Vitest spec for run-headless-serve-shutdown-docker.mjs, but the rule's test-file - override globs only .ts/.tsx, so a .test.mjs spec slips through. The script under test is a - top-level CLI module driven via vi.resetModules() + await import(); the only other way to observe - its docker argv is to spawn real docker. */ import { createHash } from 'node:crypto' import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' diff --git a/config/scripts/moved-code-matcher.test.mjs b/config/scripts/moved-code-matcher.test.mjs index fce7fa1378e..b9d4a33e23b 100644 --- a/config/scripts/moved-code-matcher.test.mjs +++ b/config/scripts/moved-code-matcher.test.mjs @@ -5,9 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import * as quality from './check-changed-code-quality.mjs' const processCalls = vi.hoisted(() => ({ execFileSync: vi.fn(), spawnSync: vi.fn() })) -// oxlint-disable-next-line anti-slop/no-module-mocking -- Test-only external process boundary; actual main executes. vi.mock('node:child_process', () => processCalls) -// oxlint-disable-next-line anti-slop/no-module-mocking -- Resolve only the external executable used by this fixture. vi.mock('./oxlint-cli-invocation.mjs', () => ({ resolveOxlintInvocation: () => ({ command: 'fixture-oxlint', prefixArgs: [] }) })) diff --git a/config/tsconfig.e2e.json b/config/tsconfig.e2e.json index 2f30279e502..20ba08e8535 100644 --- a/config/tsconfig.e2e.json +++ b/config/tsconfig.e2e.json @@ -1,7 +1,6 @@ { - // Ratcheted, not yet clean: tests/ was never typechecked, so the per-file error counts in - // config/e2e-typecheck-baseline.txt may only go down (check-e2e-typecheck-ratchet.mjs). Run - // `pnpm typecheck:e2e`, fix a file, prune; when the count reaches zero, add it to `typecheck`. + // Clean: check-e2e-typecheck-ratchet.mjs (CI via run-ratchets, local via `pnpm lint`) fails on any + // tests/ error. Not in `typecheck`, which would also surface out-of-scope cloud/ and mobile/ errors. "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", // Why: specs import renderer and main modules directly, so the ambient declarations those // modules rely on (vite client types, build-time defines) have to be in the program too. diff --git a/mobile/src/agent-history/use-mobile-agent-history-state.ts b/mobile/src/agent-history/use-mobile-agent-history-state.ts index 1973ecf631e..4cdefc6fe80 100644 --- a/mobile/src/agent-history/use-mobile-agent-history-state.ts +++ b/mobile/src/agent-history/use-mobile-agent-history-state.ts @@ -170,7 +170,6 @@ export function useMobileAgentHistoryState(params: MobileAgentHistoryStateParams // (force:false) so a tab tap is cheap; only an explicit refresh bypasses it. useEffect(() => { void loadSessions({ scope, force: false }) - // eslint-disable-next-line react-hooks/exhaustive-deps -- scope handled by onSelectScope }, [loadSessions]) const onSelectScope = useCallback( diff --git a/mobile/src/browser/browser-command-reply-schema.test.ts b/mobile/src/browser/browser-command-reply-schema.test.ts index d593ec2ee1b..ca14d11cf1d 100644 --- a/mobile/src/browser/browser-command-reply-schema.test.ts +++ b/mobile/src/browser/browser-command-reply-schema.test.ts @@ -12,14 +12,14 @@ function reads(schema: z.ZodType, value: unknown): T { describe('a navigation reports the URL it settled on', () => { it('reads the settled URL the address bar takes', () => { - expect(reads(browserNavigationSettledSchema, { url: 'https://example.test/' }).url).toBe( + expect(reads(browserNavigationSettledSchema, { url: 'https://example.test/' })?.url).toBe( 'https://example.test/' ) }) it('leaves the address bar alone for a reply that names no URL', () => { - expect(reads(browserNavigationSettledSchema, {}).url).toBe(undefined) - expect(reads(browserNavigationSettledSchema, { url: 7 }).url).toBe(undefined) + expect(reads(browserNavigationSettledSchema, {})).toEqual({}) + expect(reads(browserNavigationSettledSchema, { url: 7 })).toEqual({}) }) it('leaves the address bar alone for a null or absent result, as the pane always did', () => { diff --git a/mobile/src/browser/mobile-browser-frameless-stream.test.tsx b/mobile/src/browser/mobile-browser-frameless-stream.test.tsx index 0952502030c..9dae03d02bb 100644 --- a/mobile/src/browser/mobile-browser-frameless-stream.test.tsx +++ b/mobile/src/browser/mobile-browser-frameless-stream.test.tsx @@ -9,6 +9,7 @@ import { import type { RpcClient } from '../transport/rpc-client' import { MobileBrowserPane, type MobileBrowserTab } from './MobileBrowserPane' import { useBrowserBinaryScreencastGrant } from './use-browser-binary-screencast-grant' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' // The seam ruling 5 put between the pane and a shell that may have no encoder behind the lane the // pane would ask for. Native answers yes always; mocked here so both answers are reachable. @@ -64,7 +65,7 @@ function makeFrame(): BrowserScreencastFrame { } function spinnerCount(renderer: ReactTestRenderer): number { - return renderer.root.findAllByType('ActivityIndicator').length + return findAllByHostType(renderer.root, 'ActivityIndicator').length } async function renderPane(): Promise<{ @@ -100,9 +101,9 @@ async function renderPane(): Promise<{ isActive: true } - let renderer: ReactTestRenderer + const rendered: { renderer?: ReactTestRenderer } = {} await act(async () => { - renderer = create( + rendered.renderer = create( createElement(MobileBrowserPane, { client, // Why: unique worktree id keeps each test on a cold module-level frame cache. @@ -117,10 +118,13 @@ async function renderPane(): Promise<{ ) await Promise.resolve() }) - const mounted: ReactTestRenderer = renderer - const viewport = mounted.root - .findAllByType('View') - .find((node) => typeof node.props.onLayout === 'function') + const mounted = rendered.renderer + if (!mounted) { + throw new Error('MobileBrowserPane did not mount') + } + const viewport = findAllByHostType(mounted.root, 'View').find( + (node) => typeof node.props.onLayout === 'function' + ) if (!viewport) { throw new Error('Viewport with onLayout not found') } @@ -142,9 +146,9 @@ async function renderStreamingPane(): Promise<{ } function errorMessages(renderer: ReactTestRenderer): string[] { - return renderer.root - .findAllByType('Text') - .flatMap((node) => (typeof node.props.children === 'string' ? [node.props.children] : [])) + return findAllByHostType(renderer.root, 'Text').flatMap((node) => + typeof node.props.children === 'string' ? [node.props.children] : [] + ) } describe('MobileBrowserPane with a stream that reports ready but sends no frames', () => { @@ -174,8 +178,7 @@ describe('MobileBrowserPane with a stream that reports ready but sends no frames expect(errorMessages(renderer)).not.toContain( 'Update the Orca app to stream browser tabs here.' ) - const source = renderer.root - .findAllByType('Image') + const source = findAllByHostType(renderer.root, 'Image') .map((image) => (image.props.source as { uri?: string } | null)?.uri) .find((uri) => typeof uri === 'string') expect(source).toContain(Buffer.from(makeFrame().image).toString('base64')) diff --git a/mobile/src/browser/mobile-browser-pane-frame-layers.test.tsx b/mobile/src/browser/mobile-browser-pane-frame-layers.test.tsx index e3f38f2ef2e..c478627762c 100644 --- a/mobile/src/browser/mobile-browser-pane-frame-layers.test.tsx +++ b/mobile/src/browser/mobile-browser-pane-frame-layers.test.tsx @@ -9,9 +9,9 @@ import { BrowserScreencastOpcode, type BrowserScreencastFrame } from '../transport/browser-screencast-protocol' -import type { RpcClient } from '../transport/rpc-client' import { MOBILE_BROWSER_FRAME_MIN_INTERVAL_MS } from './browser-screencast-request' import { MobileBrowserPane, type MobileBrowserTab } from './MobileBrowserPane' +import { stubRpcClient } from '../transport/rpc-client-test-stub' vi.mock('./use-browser-binary-screencast-grant', () => ({ useBrowserBinaryScreencastGrant: vi.fn(() => true) @@ -195,8 +195,7 @@ let pageCounter = 0 async function renderPane() { pageCounter += 1 const frames: { push: ((frame: BrowserScreencastFrame) => void) | null } = { push: null } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the pane only subscribes here; no request is sent in these cases. - const client = { + const client = stubRpcClient({ subscribe: ( _method: string, _params: unknown, @@ -205,9 +204,8 @@ async function renderPane() { ) => { frames.push = options?.onBinaryFrame ?? null return () => {} - }, - request: vi.fn() - } as unknown as RpcClient + } + }) const tab: MobileBrowserTab = { type: 'browser', id: `tab-${pageCounter}`, diff --git a/mobile/src/browser/use-mobile-browser-commands-click-fallback.test.tsx b/mobile/src/browser/use-mobile-browser-commands-click-fallback.test.tsx index 2fec0fb223c..d00b22c6e35 100644 --- a/mobile/src/browser/use-mobile-browser-commands-click-fallback.test.tsx +++ b/mobile/src/browser/use-mobile-browser-commands-click-fallback.test.tsx @@ -7,9 +7,9 @@ import { createElement } from 'react' import { act, create } from 'react-test-renderer' import { beforeEach, describe, expect, it, vi } from 'vitest' import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity' -import type { RpcClient } from '../transport/rpc-client' import { useMobileBrowserCommands } from './use-mobile-browser-commands' import { useMobileBrowserRequest } from './use-mobile-browser-request' +import { stubRpcClient } from '../transport/rpc-client-test-stub' const { sent, clickFailure, clickReply } = vi.hoisted(() => { const failure: { current: Error | null } = { current: null } @@ -47,8 +47,7 @@ vi.mock('./mobile-browser-command-operations', () => { type Commands = ReturnType function mountCommands(): Commands { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the operations module is mocked, so the client is only passed through and never called. - const client = {} as RpcClient + const client = stubRpcClient() const held: { commands: Commands | null } = { commands: null } function Screen(): null { const { pageParams, sendBrowserRequest } = useMobileBrowserRequest({ diff --git a/mobile/src/components/CodexResetCreditAction.test.ts b/mobile/src/components/CodexResetCreditAction.test.ts index 0c1a6ea57b4..78349e141ed 100644 --- a/mobile/src/components/CodexResetCreditAction.test.ts +++ b/mobile/src/components/CodexResetCreditAction.test.ts @@ -2,6 +2,7 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { describe, expect, it, vi } from 'vitest' import { CodexResetCreditAction } from './CodexResetCreditAction' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ ActivityIndicator: 'ActivityIndicator', @@ -41,7 +42,7 @@ function renderAction(busy: boolean, disabled: boolean): ReactTestRenderer { describe('CodexResetCreditAction', () => { it('exposes a 44pt touch target and enabled accessibility state', () => { const renderer = renderAction(false, false) - const button = renderer.root.findByType('Pressable') + const button = findByHostType(renderer.root, 'Pressable') expect(button.props.accessibilityLabel).toBe('Use Codex rate-limit reset') expect(button.props.accessibilityState).toEqual({ busy: false, disabled: false }) @@ -53,10 +54,10 @@ describe('CodexResetCreditAction', () => { it('announces progress and visually dims a busy disabled action', () => { const renderer = renderAction(true, true) - const button = renderer.root.findByType('Pressable') - const text = renderer.root - .findAllByType('Text') - .map((node) => node.children.filter((child) => typeof child === 'string').join('')) + const button = findByHostType(renderer.root, 'Pressable') + const text = findAllByHostType(renderer.root, 'Text').map((node) => + node.children.filter((child) => typeof child === 'string').join('') + ) expect(button.props.accessibilityLabel).toBe('Resetting Codex rate limits') expect(button.props.accessibilityState).toEqual({ busy: true, disabled: true }) diff --git a/mobile/src/components/HostProtocolGate.test.ts b/mobile/src/components/HostProtocolGate.test.ts index 0e0220694ee..0351c36bffd 100644 --- a/mobile/src/components/HostProtocolGate.test.ts +++ b/mobile/src/components/HostProtocolGate.test.ts @@ -1,9 +1,10 @@ -import { createElement, useEffect } from 'react' +import { createElement, useEffect, type ComponentProps } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { RpcClient } from '../transport/rpc-client' import { HostProtocolGate } from './HostProtocolGate' import { useHostProtocolGates } from './host-protocol-gates-context' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' const nativeTestState = vi.hoisted(() => ({ openUrl: vi.fn(), @@ -64,11 +65,16 @@ function MountProbe() { } function gateElement() { - return createElement( - HostProtocolGate, - { hostId: 'host-1' }, - createElement('HostContent', null, createElement(GateConsumer), createElement(MountProbe)) - ) + const props: ComponentProps = { + hostId: 'host-1', + children: createElement( + 'HostContent', + null, + createElement(GateConsumer), + createElement(MountProbe) + ) + } + return createElement(HostProtocolGate, props) } async function renderGate(): Promise { @@ -127,7 +133,8 @@ describe('HostProtocolGate', () => { expect(output).toContain('Open GitHub Releases') expect(output).not.toContain('mobile app store') expect(output).not.toContain('HostContent') - act(() => renderer?.root.findAllByType('Pressable')[0]?.props.onPress()) + const [openReleases] = findAllByHostType(renderer.root, 'Pressable') + act(() => openReleases?.props.onPress()) expect(nativeTestState.openUrl).toHaveBeenCalledWith( 'https://github.com/stablyai/orca/releases' ) @@ -203,9 +210,9 @@ describe('HostProtocolGate', () => { expect(output).toContain('Checking host compatibility') // Why: the cold-start remount this replaces is exactly what destroys in-flight deep navigation. expect(probeMounts.count).toBe(1) - const overlay = renderer.root - .findAllByType('View') - .find((node) => node.props.accessibilityViewIsModal === true) + const overlay = findAllByHostType(renderer.root, 'View').find( + (node) => node.props.accessibilityViewIsModal === true + ) expect(overlay?.props.pointerEvents).toBe('auto') }) diff --git a/mobile/src/components/MobileHomeQuickActions.test.ts b/mobile/src/components/MobileHomeQuickActions.test.ts index 478776acac6..45fdc7702ef 100644 --- a/mobile/src/components/MobileHomeQuickActions.test.ts +++ b/mobile/src/components/MobileHomeQuickActions.test.ts @@ -3,6 +3,7 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import type { HostProfile } from '../transport/types' import { MobileHomeQuickActions } from './MobileHomeQuickActions' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ Pressable: 'Pressable', @@ -42,7 +43,7 @@ describe('MobileHomeQuickActions', () => { } function newWorkspaceButton() { - return renderer!.root.findAllByType('Pressable')[1] + return findAllByHostType(renderer!.root, 'Pressable')[1] } it('disables workspace creation without a connected host', async () => { diff --git a/mobile/src/components/MobileHostCard-truthful-status.test.tsx b/mobile/src/components/MobileHostCard-truthful-status.test.tsx index 2e8bfab862a..6c3a198db9d 100644 --- a/mobile/src/components/MobileHostCard-truthful-status.test.tsx +++ b/mobile/src/components/MobileHostCard-truthful-status.test.tsx @@ -9,6 +9,7 @@ import { type HostWorktreeInfo } from '../worktree/home-worktree-info' import { MobileHostCard } from './MobileHostCard' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ Pressable: 'Pressable', @@ -68,9 +69,9 @@ describe('MobileHostCard', () => { }) ) }) - return renderer!.root - .findAllByType('Text') - .flatMap((node) => node.children.filter((child) => typeof child === 'string')) + return findAllByHostType(renderer!.root, 'Text').flatMap((node) => + node.children.filter((child) => typeof child === 'string') + ) } it('renders the counts the host proved', async () => { diff --git a/mobile/src/components/MobileHostCard.test.ts b/mobile/src/components/MobileHostCard.test.ts index 5786a87afc4..46fb69e5cdb 100644 --- a/mobile/src/components/MobileHostCard.test.ts +++ b/mobile/src/components/MobileHostCard.test.ts @@ -1,4 +1,4 @@ -import { createElement } from 'react' +import { createElement, type ReactElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import { MobileHostCard } from './MobileHostCard' @@ -6,6 +6,7 @@ import { recordHostDescriptor, resetHostDescriptorStoreForTests } from '../transport/host-descriptor-store' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ Pressable: 'Pressable', @@ -31,6 +32,19 @@ function suppressRendererDeprecation() { }) } +async function render(element: ReactElement): Promise { + const consoleError = suppressRendererDeprecation() + const mounted: { renderer?: ReactTestRenderer } = {} + await act(async () => { + mounted.renderer = create(element) + }) + consoleError.mockRestore() + if (!mounted.renderer) { + throw new Error('MobileHostCard did not mount') + } + return mounted.renderer +} + describe('MobileHostCard', () => { let renderer: ReactTestRenderer | null = null @@ -45,30 +59,26 @@ describe('MobileHostCard', () => { const onPress = vi.fn() const onLongPress = vi.fn() const onOpenActions = vi.fn() - const consoleError = suppressRendererDeprecation() - await act(async () => { - renderer = create( - createElement(MobileHostCard, { - host: { - id: 'desk', - name: 'Desk', - endpoint: 'ws://192.168.1.2:6768', - deviceToken: 'token', - publicKeyB64: 'key', - lastConnected: 1 - }, - state: 'disconnected', - verdict: { kind: 'normal', label: 'Disconnected' }, - path: 'lan', - onPress, - onLongPress, - onOpenActions - }) - ) - }) - consoleError.mockRestore() + renderer = await render( + createElement(MobileHostCard, { + host: { + id: 'desk', + name: 'Desk', + endpoint: 'ws://192.168.1.2:6768', + deviceToken: 'token', + publicKeyB64: 'key', + lastConnected: 1 + }, + state: 'disconnected', + verdict: { kind: 'normal', label: 'Disconnected' }, + path: 'lan', + onPress, + onLongPress, + onOpenActions + }) + ) - const buttons = renderer.root.findAllByType('Pressable') + const buttons = findAllByHostType(renderer.root, 'Pressable') expect(buttons).toHaveLength(2) expect(buttons[0].props.accessibilityRole).toBe('button') expect(buttons[0].props.accessibilityLabel).toBe('Open Desk, Disconnected') @@ -86,102 +96,90 @@ describe('MobileHostCard', () => { }) it('announces the connection path without the visual separator', async () => { - const consoleError = suppressRendererDeprecation() - await act(async () => { - renderer = create( - createElement(MobileHostCard, { - host: { - id: 'desk', - name: 'Desk', - endpoint: 'ws://192.168.1.2:6768', - deviceToken: 'token', - publicKeyB64: 'key', - lastConnected: 1 - }, - state: 'connected', - verdict: { kind: 'normal', label: 'Connected' }, - path: 'tailscale', - worktreeInfo: { - hostId: 'desk', - totalWorktrees: 3, - activeCount: 2, - lastActiveWorktree: null, - countsProvenAt: Date.now() - }, - onPress: vi.fn(), - onLongPress: vi.fn(), - onOpenActions: vi.fn() - }) - ) - }) - consoleError.mockRestore() + renderer = await render( + createElement(MobileHostCard, { + host: { + id: 'desk', + name: 'Desk', + endpoint: 'ws://192.168.1.2:6768', + deviceToken: 'token', + publicKeyB64: 'key', + lastConnected: 1 + }, + state: 'connected', + verdict: { kind: 'normal', label: 'Connected' }, + path: 'tailscale', + worktreeInfo: { + hostId: 'desk', + totalWorktrees: 3, + activeCount: 2, + lastActiveWorktree: null, + countsProvenAt: Date.now() + }, + onPress: vi.fn(), + onLongPress: vi.fn(), + onOpenActions: vi.fn() + }) + ) - const navigationButton = renderer.root.findAllByType('Pressable')[0] + const navigationButton = findAllByHostType(renderer.root, 'Pressable')[0] expect(navigationButton.props.accessibilityLabel).toBe( 'Open Desk, Connected, Direct via Tailscale, 3 worktrees, 2 active' ) }) it('keeps a phone rename above a disagreeing machine descriptor', async () => { - const consoleError = suppressRendererDeprecation() recordHostDescriptor('desk', { machineName: 'm4airs-Air', platform: 'darwin' }) - await act(async () => { - renderer = create( - createElement(MobileHostCard, { - host: { - id: 'desk', - name: 'Windows-Low Spec', - personalName: 'Windows-Low Spec', - endpoint: 'ws://192.168.1.2:6768', - deviceToken: 'token', - publicKeyB64: 'key', - lastConnected: 1 - }, - state: 'connected', - verdict: { kind: 'normal', label: 'Connected' }, - path: 'lan', - onPress: vi.fn(), - onLongPress: vi.fn(), - onOpenActions: vi.fn() - }) - ) - }) - consoleError.mockRestore() + renderer = await render( + createElement(MobileHostCard, { + host: { + id: 'desk', + name: 'Windows-Low Spec', + personalName: 'Windows-Low Spec', + endpoint: 'ws://192.168.1.2:6768', + deviceToken: 'token', + publicKeyB64: 'key', + lastConnected: 1 + }, + state: 'connected', + verdict: { kind: 'normal', label: 'Connected' }, + path: 'lan', + onPress: vi.fn(), + onLongPress: vi.fn(), + onOpenActions: vi.fn() + }) + ) - const texts = renderer.root.findAllByType('Text').map((node) => node.children.join('')) + const texts = findAllByHostType(renderer.root, 'Text').map((node) => node.children.join('')) expect(texts).toContain('Windows-Low Spec') expect(texts).toContain('macOS · m4airs-Air') }) it('titles an unrenamed host with the live machine name before its stored name catches up', async () => { // The desktop was renamed; the stored `name` still holds the old machine name until reload. - const consoleError = suppressRendererDeprecation() recordHostDescriptor('desk', { machineName: 'Studio 2', platform: 'darwin' }) - await act(async () => { - renderer = create( - createElement(MobileHostCard, { - host: { - id: 'desk', - name: 'Studio', - lastKnownMachineName: 'Studio', - lastKnownHostPlatform: 'darwin', - endpoint: 'ws://192.168.1.2:6768', - deviceToken: 'token', - publicKeyB64: 'key', - lastConnected: 1 - }, - state: 'connected', - verdict: { kind: 'normal', label: 'Connected' }, - path: 'lan', - onPress: vi.fn(), - onLongPress: vi.fn(), - onOpenActions: vi.fn() - }) - ) - }) - consoleError.mockRestore() + renderer = await render( + createElement(MobileHostCard, { + host: { + id: 'desk', + name: 'Studio', + lastKnownMachineName: 'Studio', + lastKnownHostPlatform: 'darwin', + endpoint: 'ws://192.168.1.2:6768', + deviceToken: 'token', + publicKeyB64: 'key', + lastConnected: 1 + }, + state: 'connected', + verdict: { kind: 'normal', label: 'Connected' }, + path: 'lan', + onPress: vi.fn(), + onLongPress: vi.fn(), + onOpenActions: vi.fn() + }) + ) - const texts = renderer.root.findAllByType('Text').map((node) => node.children.join('')) + const texts = findAllByHostType(renderer.root, 'Text').map((node) => node.children.join('')) expect(texts).toContain('Studio 2') expect(texts).toContain('macOS') expect(texts).not.toContain('Studio') @@ -189,183 +187,163 @@ describe('MobileHostCard', () => { it('shows the stored descriptor when the host is offline, as after a restart', async () => { // No live descriptor is recorded: the row has only what the stored profile carries. - const consoleError = suppressRendererDeprecation() - await act(async () => { - renderer = create( - createElement(MobileHostCard, { - host: { - id: 'desk', - name: 'Desk', - personalName: 'Desk', - lastKnownMachineName: 'm4airs-Air', - lastKnownHostPlatform: 'darwin', - endpoint: 'ws://192.168.1.2:6768', - deviceToken: 'token', - publicKeyB64: 'key', - lastConnected: 1 - }, - state: 'disconnected', - verdict: { kind: 'normal', label: 'Disconnected' }, - path: 'lan', - onPress: vi.fn(), - onLongPress: vi.fn(), - onOpenActions: vi.fn() - }) - ) - }) - consoleError.mockRestore() - - expect(renderer.root.findAllByType('Text').map((node) => node.children.join(''))).toContain( - 'macOS · m4airs-Air' + renderer = await render( + createElement(MobileHostCard, { + host: { + id: 'desk', + name: 'Desk', + personalName: 'Desk', + lastKnownMachineName: 'm4airs-Air', + lastKnownHostPlatform: 'darwin', + endpoint: 'ws://192.168.1.2:6768', + deviceToken: 'token', + publicKeyB64: 'key', + lastConnected: 1 + }, + state: 'disconnected', + verdict: { kind: 'normal', label: 'Disconnected' }, + path: 'lan', + onPress: vi.fn(), + onLongPress: vi.fn(), + onOpenActions: vi.fn() + }) ) + + expect( + findAllByHostType(renderer.root, 'Text').map((node) => node.children.join('')) + ).toContain('macOS · m4airs-Air') }) it('collapses the machine name into the OS line when it is the shown name', async () => { - const consoleError = suppressRendererDeprecation() recordHostDescriptor('desk', { machineName: 'Desk', platform: 'darwin' }) - await act(async () => { - renderer = create( - createElement(MobileHostCard, { - host: { - id: 'desk', - name: 'Desk', - endpoint: 'ws://192.168.1.2:6768', - deviceToken: 'token', - publicKeyB64: 'key', - lastConnected: 1 - }, - state: 'connected', - verdict: { kind: 'normal', label: 'Connected' }, - path: 'lan', - onPress: vi.fn(), - onLongPress: vi.fn(), - onOpenActions: vi.fn() - }) - ) - }) - consoleError.mockRestore() + renderer = await render( + createElement(MobileHostCard, { + host: { + id: 'desk', + name: 'Desk', + endpoint: 'ws://192.168.1.2:6768', + deviceToken: 'token', + publicKeyB64: 'key', + lastConnected: 1 + }, + state: 'connected', + verdict: { kind: 'normal', label: 'Connected' }, + path: 'lan', + onPress: vi.fn(), + onLongPress: vi.fn(), + onOpenActions: vi.fn() + }) + ) - const texts = renderer.root.findAllByType('Text').map((node) => node.children.join('')) + const texts = findAllByHostType(renderer.root, 'Text').map((node) => node.children.join('')) expect(texts).toContain('macOS') expect(texts).not.toContain('macOS · Desk') }) it('preserves the connected worktree-catalog failure state', async () => { - const consoleError = suppressRendererDeprecation() - await act(async () => { - renderer = create( - createElement(MobileHostCard, { - host: { - id: 'desk', - name: 'Desk', - endpoint: 'ws://192.168.1.2:6768', - deviceToken: 'token', - publicKeyB64: 'key', - lastConnected: 1 - }, - state: 'connected', - verdict: { kind: 'normal', label: 'Connected' }, - path: 'relay', - worktreeInfo: { - hostId: 'desk', - totalWorktrees: 0, - activeCount: 0, - lastActiveWorktree: null, - catalogUnavailable: true - }, - onPress: vi.fn(), - onLongPress: vi.fn(), - onOpenActions: vi.fn() - }) - ) - }) - consoleError.mockRestore() + renderer = await render( + createElement(MobileHostCard, { + host: { + id: 'desk', + name: 'Desk', + endpoint: 'ws://192.168.1.2:6768', + deviceToken: 'token', + publicKeyB64: 'key', + lastConnected: 1 + }, + state: 'connected', + verdict: { kind: 'normal', label: 'Connected' }, + path: 'relay', + worktreeInfo: { + hostId: 'desk', + totalWorktrees: 0, + activeCount: 0, + lastActiveWorktree: null, + catalogUnavailable: true + }, + onPress: vi.fn(), + onLongPress: vi.fn(), + onOpenActions: vi.fn() + }) + ) - const navigationButton = renderer.root.findAllByType('Pressable')[0] + const navigationButton = findAllByHostType(renderer.root, 'Pressable')[0] expect(navigationButton.props.accessibilityLabel).toBe( 'Open Desk, Connected, Orca Relay, Worktree list unavailable' ) expect( - renderer.root - .findAllByType('Text') - .some((node) => node.children.includes('Worktree list unavailable')) + findAllByHostType(renderer.root, 'Text').some((node) => + node.children.includes('Worktree list unavailable') + ) ).toBe(true) }) it('includes visible offline recovery guidance in the navigation label', async () => { - const consoleError = suppressRendererDeprecation() - await act(async () => { - renderer = create( - createElement(MobileHostCard, { - host: { - id: 'desk', - name: 'Desk', - endpoint: 'ws://192.168.1.2:6768', - deviceToken: 'token', - publicKeyB64: 'key', - lastConnected: 1 - }, - state: 'reconnecting', - verdict: { - kind: 'unreachable', - label: "Can't reach desktop", - reason: 'never-connected' - }, - path: 'lan', - onPress: vi.fn(), - onLongPress: vi.fn(), - onOpenActions: vi.fn() - }) - ) - }) - consoleError.mockRestore() + renderer = await render( + createElement(MobileHostCard, { + host: { + id: 'desk', + name: 'Desk', + endpoint: 'ws://192.168.1.2:6768', + deviceToken: 'token', + publicKeyB64: 'key', + lastConnected: 1 + }, + state: 'reconnecting', + verdict: { + kind: 'unreachable', + label: "Can't reach desktop", + reason: 'never-connected' + }, + path: 'lan', + onPress: vi.fn(), + onLongPress: vi.fn(), + onOpenActions: vi.fn() + }) + ) - const navigationButton = renderer.root.findAllByType('Pressable')[0] + const navigationButton = findAllByHostType(renderer.root, 'Pressable')[0] expect(navigationButton.props.accessibilityLabel).toBe( "Open Desk, Can't reach desktop, Update desktop Orca and sign in to connect from anywhere" ) }) it('renders the verdict detail as a second line and announces it', async () => { - const consoleError = suppressRendererDeprecation() - await act(async () => { - renderer = create( - createElement(MobileHostCard, { - host: { - id: 'desk', - name: 'Host 1', - endpoint: 'ws://192.168.1.2:6768', - deviceToken: 'token', - publicKeyB64: 'key', - lastConnected: 1, - relay: { - v: 1 as const, - directorUrl: 'https://relay-staging.onorca.dev', - cellUrl: 'https://c1.relay-staging.onorca.dev', - assignmentEpoch: 4, - relayHostId: 'AbCdEf0123_-xyZ9', - e2eeFraming: 2 as const - } - }, - state: 'connecting', - verdict: { - kind: 'unreachable', - label: 'Host 1 is offline', - reason: 'never-connected', - detail: "Check it's awake, Orca is running, and you're signed in" - }, - path: 'lan', - onPress: vi.fn(), - onLongPress: vi.fn(), - onOpenActions: vi.fn() - }) - ) - }) - consoleError.mockRestore() + renderer = await render( + createElement(MobileHostCard, { + host: { + id: 'desk', + name: 'Host 1', + endpoint: 'ws://192.168.1.2:6768', + deviceToken: 'token', + publicKeyB64: 'key', + lastConnected: 1, + relay: { + v: 1 as const, + directorUrl: 'https://relay-staging.onorca.dev', + cellUrl: 'https://c1.relay-staging.onorca.dev', + assignmentEpoch: 4, + relayHostId: 'AbCdEf0123_-xyZ9', + e2eeFraming: 2 as const + } + }, + state: 'connecting', + verdict: { + kind: 'unreachable', + label: 'Host 1 is offline', + reason: 'never-connected', + detail: "Check it's awake, Orca is running, and you're signed in" + }, + path: 'lan', + onPress: vi.fn(), + onLongPress: vi.fn(), + onOpenActions: vi.fn() + }) + ) - const texts = renderer.root.findAllByType('Text').map((node) => node.props.children) + const texts = findAllByHostType(renderer.root, 'Text').map((node) => node.props.children) expect(texts).toContainEqual("Check it's awake, Orca is running, and you're signed in") - expect(renderer.root.findAllByType('Pressable')[0]?.props.accessibilityLabel).toBe( + expect(findAllByHostType(renderer.root, 'Pressable')[0]?.props.accessibilityLabel).toBe( "Open Host 1, Host 1 is offline, Check it's awake, Orca is running, and you're signed in" ) }) diff --git a/mobile/src/components/MobileMarkdown.file-links.test.ts b/mobile/src/components/MobileMarkdown.file-links.test.ts index f2eea6eaab7..2f1976c7b9e 100644 --- a/mobile/src/components/MobileMarkdown.file-links.test.ts +++ b/mobile/src/components/MobileMarkdown.file-links.test.ts @@ -3,7 +3,7 @@ import { act, create, type ReactTestRenderer, type ReactTestInstance } from 'rea import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { MobileMarkdown } from './MobileMarkdown' -const openURL = vi.fn(() => Promise.resolve()) +const openURL = vi.fn((_url: string) => Promise.resolve()) vi.mock('react-native', () => ({ Linking: { openURL: (url: string) => openURL(url) }, diff --git a/mobile/src/components/MobileRichMarkdownEditor.tsx b/mobile/src/components/MobileRichMarkdownEditor.tsx index bef4a51c8a5..918cf11f6cc 100644 --- a/mobile/src/components/MobileRichMarkdownEditor.tsx +++ b/mobile/src/components/MobileRichMarkdownEditor.tsx @@ -171,9 +171,9 @@ function MobileRichMarkdownEditorWebInner( ) useEffect(() => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: react-native-web renders View as a div and forwards the ref to it; this module only ever runs in that build. - const host = hostRef.current as unknown as HTMLElement | null - if (!host) { + // react-native-web renders View as a div and forwards the ref to it. + const host = hostRef.current + if (!(host instanceof HTMLElement)) { return } const live = mountRichMarkdownWebDocument(host, { diff --git a/mobile/src/components/NewWorktreeModal.test.tsx b/mobile/src/components/NewWorktreeModal.test.tsx index 6b9e3ab7c31..b5209694432 100644 --- a/mobile/src/components/NewWorktreeModal.test.tsx +++ b/mobile/src/components/NewWorktreeModal.test.tsx @@ -3,6 +3,7 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { z } from 'zod' import type { RpcClient } from '../transport/rpc-client' +import type { RpcResponse } from '../transport/types' const asyncStorage = vi.hoisted(() => ({ getItem: vi.fn().mockResolvedValue(null), @@ -48,11 +49,15 @@ vi.mock('./TaskProviderLogo', () => ({ TaskProviderLogo: 'TaskProviderLogo' })) import { setCachedRepos } from '../cache/repo-cache' import { recordHostDescriptor } from '../transport/host-descriptor-store' import { FakeSession } from '../transport/mobile-endpoint-supervisor-test-fakes' -import { getLocalExecutionHostLabel } from '../../../src/shared/execution-host' +import { + getLocalExecutionHostLabel, + type ExecutionHostId +} from '../../../src/shared/execution-host' import { MOBILE_DESKTOP_RELAY_RUNTIME_CAPABILITY } from '../../../src/shared/mobile-desktop-relay-contract' import { MOBILE_RELAY_HOSTS_LIST_METHOD } from '../../../src/shared/mobile-relay-hosts-contract' import { NewWorktreeModal } from './NewWorktreeModal' import { NewWorktreeModalController } from './NewWorktreeModalController' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' const RelayedOptionsSchema = z.object({ envelope: z.object({ executionHost: z.string() }) }) @@ -72,15 +77,14 @@ function pickerItems( renderer: ReactTestRenderer, title: string ): { label: string; detail: string }[] { - const pickers = renderer.root.findAll((node) => node.type === 'PickerListDrawer') + const pickers = findAllByHostType(renderer.root, 'PickerListDrawer') const picker = pickers.find((node) => node.props.title === title) return picker?.props.items ?? [] } function sourceInputs(renderer: ReactTestRenderer) { - return renderer.root.findAll( - (node) => - node.type === 'TextInput' && node.props.placeholder === 'Type a name or search a source' + return findAllByHostType(renderer.root, 'TextInput').filter( + (node) => node.props.placeholder === 'Type a name or search a source' ) } @@ -90,10 +94,14 @@ function requestsOnly(sendRequest: RpcClient['sendRequest']): RpcClient { return client } +function replied(result: unknown): Promise { + return Promise.resolve({ id: 'reply', ok: true, result }) +} + function runTargetPicker(renderer: ReactTestRenderer) { - return renderer.root - .findAll((node) => node.type === 'PickerListDrawer') - .find((node) => node.props.title === 'Run on')! + return findAllByHostType(renderer.root, 'PickerListDrawer').find( + (node) => node.props.title === 'Run on' + )! } async function flushUpdates(): Promise { @@ -134,7 +142,8 @@ describe('NewWorktreeModal project targets', () => { client, hostId: 'host-1', onCreated: () => {}, - onClose: () => {} + onClose: () => {}, + openExternalUrl: () => {} }) ) await Promise.resolve() @@ -183,7 +192,8 @@ describe('NewWorktreeModal project targets', () => { client, hostId: 'host-1', onCreated: () => {}, - onClose: () => {} + onClose: () => {}, + openExternalUrl: () => {} }) ) await Promise.resolve() @@ -254,7 +264,8 @@ describe('NewWorktreeModal project targets', () => { client, hostId: 'host-ssh', onCreated: () => {}, - onClose: () => {} + onClose: () => {}, + openExternalUrl: () => {} }) ) }) @@ -327,7 +338,8 @@ describe('NewWorktreeModal project targets', () => { client, hostId: 'host-folder', onCreated: () => {}, - onClose: () => {} + onClose: () => {}, + openExternalUrl: () => {} }) ) }) @@ -382,21 +394,20 @@ describe('NewWorktreeModal project targets', () => { const serverSshRepo = { ...serverRepo, id: 'server-ssh', connectionId: 'its-own-ssh' } const reply = (method: string, listed: unknown[], hostPlatform: string) => { if (method === 'repo.list') { - return Promise.resolve({ ok: true, result: { repos: listed } }) + return replied({ repos: listed }) } if (method === 'status.get') { - return Promise.resolve({ ok: true, result: { hostPlatform } }) + return replied({ hostPlatform }) } if (method === 'worktree.create') { - return Promise.resolve({ - ok: true, - result: { worktree: { id: 'srv-wt', displayName: 'srv-wt', path: '/srv/orca-wt' } } + return replied({ + worktree: { id: 'srv-wt', displayName: 'srv-wt', path: '/srv/orca-wt' } }) } if (method === 'settings.get') { - return Promise.resolve({ ok: true, result: { settings: {} } }) + return replied({ settings: {} }) } - return Promise.resolve({ ok: true, result: {} }) + return replied({}) } const desktop = vi.fn((method: string) => reply(method, repos, 'darwin')) const server = vi.fn((method: string) => reply(method, [serverRepo, serverSshRepo], 'linux')) @@ -408,11 +419,14 @@ describe('NewWorktreeModal project targets', () => { createElement(NewWorktreeModal, { visible: true, client: requestsOnly(desktop), - serverClients: new Map([['runtime:env-1', requestsOnly(server)]]), + serverClients: new Map([ + ['runtime:env-1', requestsOnly(server)] + ]), hostId: 'host-1', - hostLabelById: new Map([['runtime:env-1', 'scratch-server']]), + hostLabelById: new Map([['runtime:env-1', 'scratch-server']]), onCreated, - onClose: () => {} + onClose: () => {}, + openExternalUrl: () => {} }) ) }) @@ -438,7 +452,7 @@ describe('NewWorktreeModal project targets', () => { await act(async () => serverTarget.props.onSelect(serverTarget.props.items[1])) await flushUpdates() // The Run on field shows the picked server's glyph, as its row does. - expect(renderer.root.findAll((node) => node.type === 'Server')).toHaveLength(1) + expect(findAllByHostType(renderer.root, 'Server')).toHaveLength(1) expect(pickerItems(renderer, 'Run on')[0]?.label).toBe(LOCAL_HOST_LABEL) act(() => sourceInputs(renderer)[0]!.props.onChangeText('srv-wt')) const form = renderer.root.find((node) => typeof node.props.onCreate === 'function') @@ -508,21 +522,22 @@ describe('NewWorktreeModal project targets', () => { it("lists the desktop's repos at once while a server has not answered", async () => { const desktop = vi.fn((method: string) => - method === 'repo.list' - ? Promise.resolve({ ok: true, result: { repos } }) - : new Promise(() => {}) + method === 'repo.list' ? replied({ repos }) : new Promise(() => {}) ) - const hungServer = vi.fn(() => new Promise(() => {})) + const hungServer = vi.fn(() => new Promise(() => {})) await act(async () => { renderer = create( createElement(NewWorktreeModal, { visible: true, client: requestsOnly(desktop), - serverClients: new Map([['runtime:env-1', requestsOnly(hungServer)]]), + serverClients: new Map([ + ['runtime:env-1', requestsOnly(hungServer)] + ]), hostId: 'host-cold', onCreated: () => {}, - onClose: () => {} + onClose: () => {}, + openExternalUrl: () => {} }) ) }) @@ -536,9 +551,7 @@ describe('NewWorktreeModal project targets', () => { const serverRepo = { ...repos[0]!, path: '/srv/orca' } const listing = (listed: unknown[]) => vi.fn((method: string) => - method === 'repo.list' - ? Promise.resolve({ ok: true, result: { repos: listed } }) - : new Promise(() => {}) + method === 'repo.list' ? replied({ repos: listed }) : new Promise(() => {}) ) await act(async () => { @@ -546,10 +559,13 @@ describe('NewWorktreeModal project targets', () => { createElement(NewWorktreeModal, { visible: true, client: requestsOnly(listing(repos)), - serverClients: new Map([['runtime:env-1', requestsOnly(listing([serverRepo]))]]), + serverClients: new Map([ + ['runtime:env-1', requestsOnly(listing([serverRepo]))] + ]), hostId: 'host-1', onCreated: () => {}, - onClose: () => {} + onClose: () => {}, + openExternalUrl: () => {} }) ) }) @@ -593,7 +609,8 @@ describe('NewWorktreeModal project targets', () => { visible: true, hostId: 'host-1', onCreated: () => {}, - onClose: () => {} + onClose: () => {}, + openExternalUrl: () => {} } await act(async () => { @@ -635,7 +652,13 @@ describe('NewWorktreeModal project targets', () => { it('keeps the form when a host switch renders but never commits', async () => { setCachedRepos('host-2', repos) const client = requestsOnly(vi.fn().mockImplementation(() => new Promise(() => {}))) - const modalProps = { visible: true, client, onCreated: () => {}, onClose: () => {} } + const modalProps = { + visible: true, + client, + onCreated: () => {}, + onClose: () => {}, + openExternalUrl: () => {} + } let frozen = false const tree = (hostId: string) => suspendableTree(() => frozen, createElement(NewWorktreeModal, { ...modalProps, hostId })) @@ -664,7 +687,8 @@ describe('NewWorktreeModal project targets', () => { client, hostId: 'host-1', onCreated: () => {}, - onClose: () => {} + onClose: () => {}, + openExternalUrl: () => {} } let frozen = false const tree = (visible: boolean) => @@ -694,7 +718,13 @@ describe('NewWorktreeModal project targets', () => { it('starts a fresh form session when the modal switches hosts', async () => { setCachedRepos('host-2', repos) const client = requestsOnly(vi.fn().mockImplementation(() => new Promise(() => {}))) - const modalProps = { visible: true, client, onCreated: () => {}, onClose: () => {} } + const modalProps = { + visible: true, + client, + onCreated: () => {}, + onClose: () => {}, + openExternalUrl: () => {} + } await act(async () => { renderer = create(createElement(NewWorktreeModal, { ...modalProps, hostId: 'host-1' })) @@ -715,7 +745,8 @@ describe('NewWorktreeModal project targets', () => { client, hostId: 'host-1', onCreated: () => {}, - onClose: () => {} + onClose: () => {}, + openExternalUrl: () => {} } await act(async () => { diff --git a/mobile/src/components/PickerModal.accessibility.test.ts b/mobile/src/components/PickerModal.accessibility.test.ts index 46610a6e631..1ad62b21dac 100644 --- a/mobile/src/components/PickerModal.accessibility.test.ts +++ b/mobile/src/components/PickerModal.accessibility.test.ts @@ -2,6 +2,7 @@ import { createElement, type ReactNode } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { PickerModal } from './PickerModal' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ Pressable: 'Pressable', @@ -54,7 +55,7 @@ describe('PickerModal accessibility', () => { ) }) - const rows = renderer!.root.findAllByType('Pressable') + const rows = findAllByHostType(renderer!.root, 'Pressable') expect(rows.map((row) => row.props.accessible)).toEqual([true, true]) expect(rows.map((row) => row.props.accessibilityRole)).toEqual(['button', 'button']) expect(rows.map((row) => row.props.accessibilityState)).toEqual([ diff --git a/mobile/src/components/WorktreeAgentList.test.tsx b/mobile/src/components/WorktreeAgentList.test.tsx index baa6457994f..3cb14c4eb67 100644 --- a/mobile/src/components/WorktreeAgentList.test.tsx +++ b/mobile/src/components/WorktreeAgentList.test.tsx @@ -3,6 +3,7 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import type { RuntimeWorktreeAgentRow } from '../../../src/shared/runtime-types' import { WorktreeAgentList } from './WorktreeAgentList' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ Pressable: 'Pressable', @@ -82,7 +83,7 @@ describe('WorktreeAgentList', () => { ) }) - const summary = renderer!.root.findByType('Pressable') + const summary = findByHostType(renderer!.root, 'Pressable') expect(summary.props.accessibilityLabel).toBe('Expand 3 agents') expect(summary.props.accessibilityState).toEqual({ expanded: false }) expect(renderer!.root.findAll((node) => node.props.agent !== undefined)).toHaveLength(0) @@ -100,10 +101,10 @@ describe('WorktreeAgentList', () => { name: 'Review tests', displayState: 'working' }) - await act(async () => rows[1].findByType('Pressable').props.onPress({ stopPropagation })) + await act(async () => findByHostType(rows[1], 'Pressable').props.onPress({ stopPropagation })) expect(onAgentPress).toHaveBeenCalledWith('agent-1') - expect(rows[0].findAllByType('Text').at(-1)?.props.children).toBe('1m') - expect(rows[1].findAllByType('Text').at(-1)?.props.children).toBe(age) + expect(findAllByHostType(rows[0], 'Text').at(-1)?.props.children).toBe('1m') + expect(findAllByHostType(rows[1], 'Text').at(-1)?.props.children).toBe(age) }) it("shows every agent row with no summary under the desktop's Full list layout", async () => { @@ -131,7 +132,7 @@ describe('WorktreeAgentList', () => { const rows = renderer!.root.findAll((node) => node.props.agent !== undefined) expect(rows).toHaveLength(4) expect(rows[1].props.childRow.id).toBe('child') - const childPress = renderer!.root.findByType('Pressable') + const childPress = findByHostType(renderer!.root, 'Pressable') await act(async () => childPress.props.onPress({ stopPropagation: vi.fn() })) expect(onAgentPress).toHaveBeenCalledWith('agent-1') }) @@ -147,7 +148,7 @@ describe('WorktreeAgentList', () => { ) }) - expect(renderer!.root.findAllByType('Pressable')).toHaveLength(0) + expect(findAllByHostType(renderer!.root, 'Pressable')).toHaveLength(0) expect(renderer!.root.findAll((node) => node.props.agent !== undefined)).toHaveLength(1) }) @@ -162,6 +163,8 @@ describe('WorktreeAgentList', () => { ) }) - expect(renderer!.root.findByType('Pressable').props.accessibilityLabel).toBe('Expand 2 agents') + expect(findByHostType(renderer!.root, 'Pressable').props.accessibilityLabel).toBe( + 'Expand 2 agents' + ) }) }) diff --git a/mobile/src/components/WorktreeListRow.test.ts b/mobile/src/components/WorktreeListRow.test.ts index d263adbc2dc..7dcf8723461 100644 --- a/mobile/src/components/WorktreeListRow.test.ts +++ b/mobile/src/components/WorktreeListRow.test.ts @@ -18,6 +18,7 @@ import { getWorktreeCardDetails, type WorktreeCardDetails } from '../../../src/shared/worktree/card-properties' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' const { agentSpinnerRender, agentStateDotRender, metaGlyphsRender } = vi.hoisted(() => ({ agentSpinnerRender: vi.fn(), @@ -319,8 +320,7 @@ describe('memoized worktree rows', () => { it('names the host with a glyph that matches the host kind', async () => { const textNodes = (): string[] => - renderer!.root - .findAllByType('Text' as never) + findAllByHostType(renderer!.root, 'Text') .flatMap((node) => node.props.children) .filter((child): child is string => typeof child === 'string') @@ -334,7 +334,7 @@ describe('memoized worktree rows', () => { }) expect(textNodes()).toContain('openclaw') expect(textNodes().some((text) => text.includes('Disconnected'))).toBe(false) - expect(renderer!.root.findAllByType('Server' as never)).toHaveLength(1) + expect(findAllByHostType(renderer!.root, 'Server')).toHaveLength(1) await act(async () => renderer!.update( @@ -351,15 +351,15 @@ describe('memoized worktree rows', () => { ) // Separate nodes: the name truncates while the health word stays whole. expect(textNodes()).toEqual(expect.arrayContaining(['openclaw', '· Disconnected'])) - const healthText = renderer!.root - .findAllByType('Text' as never) - .find((node) => node.props.children === '· Disconnected') + const healthText = findAllByHostType(renderer!.root, 'Text').find( + (node) => node.props.children === '· Disconnected' + ) expect([healthText?.props.style].flat()).toContainEqual({ flexShrink: 0 }) const announcedBadges = renderer!.root.findAll( (node) => node.props.accessibilityLabel === 'openclaw, Disconnected' ) expect(announcedBadges).toHaveLength(1) - expect(renderer!.root.findAllByType('Monitor' as never)).toHaveLength(0) + expect(findAllByHostType(renderer!.root, 'Monitor')).toHaveLength(0) await act(async () => renderer!.update( @@ -390,13 +390,13 @@ describe('memoized worktree rows', () => { ) ) expect(textNodes()).toContain('Local Mac') - expect(renderer!.root.findAllByType('Monitor' as never)).toHaveLength(1) + expect(findAllByHostType(renderer!.root, 'Monitor')).toHaveLength(1) await act(async () => renderer!.update(createElement(ListRowHarness, { item: baseItem, now: 2_000 })) ) expect(textNodes()).not.toContain('Local Mac') - expect(renderer!.root.findAllByType('Monitor' as never)).toHaveLength(0) + expect(findAllByHostType(renderer!.root, 'Monitor')).toHaveLength(0) }) it("leaves out the PR, host and agents the desktop's card properties turn off", async () => { @@ -432,8 +432,7 @@ describe('memoized worktree rows', () => { describe('Compact', () => { const texts = (): string[] => - renderer!.root - .findAllByType('Text' as never) + findAllByHostType(renderer!.root, 'Text') .flatMap((node) => node.props.children) .filter((child): child is string => typeof child === 'string') const repoChips = () => @@ -532,9 +531,9 @@ describe('memoized worktree rows', () => { expect(renderer!.root.findAll((node) => String(node.type) === 'GitPullRequest')).toHaveLength( 1 ) - const badgeTexts = renderer!.root - .findAllByType('Text' as never) - .map((node) => [node.props.children].flat().join('')) + const badgeTexts = findAllByHostType(renderer!.root, 'Text').map((node) => + [node.props.children].flat().join('') + ) expect(badgeTexts).toContain('#7') }) diff --git a/mobile/src/components/agent-monitoring-indicators.test.ts b/mobile/src/components/agent-monitoring-indicators.test.ts index 1a08cff4a7c..2caa6273669 100644 --- a/mobile/src/components/agent-monitoring-indicators.test.ts +++ b/mobile/src/components/agent-monitoring-indicators.test.ts @@ -1,18 +1,22 @@ -import { createElement } from 'react' -import { act, create } from 'react-test-renderer' +import { createElement, type ReactElement } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { AgentSpinner } from './AgentSpinner' import { AgentStateDot } from './AgentStateDot' import { colors } from '../theme/mobile-theme' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' const DESKTOP_WORKING_COLOR = '#eab308' -type MonitoringTestRenderer = { - readonly root: { - findByType(type: string): { props: Record } - findAllByType(type: string): { props: Record }[] +async function mount(element: ReactElement): Promise { + const mounted: { renderer?: ReactTestRenderer } = {} + await act(async () => { + mounted.renderer = create(element) + }) + if (!mounted.renderer) { + throw new Error('indicator did not mount') } - unmount(): void + return mounted.renderer } const { animationLoop, animationTiming, setValue } = vi.hoisted(() => ({ @@ -38,7 +42,7 @@ vi.mock('react-native', () => ({ })) describe('mobile monitoring indicators', () => { - let renderer: MonitoringTestRenderer | null = null + let renderer: ReactTestRenderer | null = null beforeEach(() => { animationLoop.mockClear() @@ -52,11 +56,9 @@ describe('mobile monitoring indicators', () => { }) it('renders a static Activity heartbeat for a monitoring agent', async () => { - await act(async () => { - renderer = create(createElement(AgentStateDot, { state: 'monitoring' })) - }) + renderer = await mount(createElement(AgentStateDot, { state: 'monitoring' })) - expect(renderer?.root.findByType('Activity').props).toMatchObject({ + expect(findByHostType(renderer.root, 'Activity').props).toMatchObject({ color: DESKTOP_WORKING_COLOR, size: 10 }) @@ -65,13 +67,11 @@ describe('mobile monitoring indicators', () => { }) it('renders a static Activity heartbeat for an all-monitoring workspace', async () => { - await act(async () => { - renderer = create( - createElement(AgentSpinner, { status: 'working', workingMode: 'monitoring' }) - ) - }) + renderer = await mount( + createElement(AgentSpinner, { status: 'working', workingMode: 'monitoring' }) + ) - expect(renderer?.root.findByType('Activity').props).toMatchObject({ + expect(findByHostType(renderer.root, 'Activity').props).toMatchObject({ color: DESKTOP_WORKING_COLOR, size: 12 }) @@ -80,23 +80,19 @@ describe('mobile monitoring indicators', () => { }) it('keeps the spinner fallback when workingMode is absent', async () => { - await act(async () => { - renderer = create(createElement(AgentSpinner, { status: 'working' })) - }) + renderer = await mount(createElement(AgentSpinner, { status: 'working' })) expect(animationTiming).toHaveBeenCalledOnce() expect(animationLoop).toHaveBeenCalledOnce() }) it('keeps both native working rings off the interaction queue', async () => { - await act(async () => { - renderer = create( - createElement('View', null, [ - createElement(AgentSpinner, { key: 'spinner', status: 'working' }), - createElement(AgentStateDot, { key: 'dot', state: 'working' }) - ]) - ) - }) + renderer = await mount( + createElement('View', null, [ + createElement(AgentSpinner, { key: 'spinner', status: 'working' }), + createElement(AgentStateDot, { key: 'dot', state: 'working' }) + ]) + ) expect(animationTiming).toHaveBeenCalledTimes(2) for (const call of animationTiming.mock.calls) { @@ -112,11 +108,11 @@ describe('mobile monitoring indicators', () => { ['interrupted', colors.textMuted], ['failed', '#ef4444'] ] as const)('draws %s with its own dot colour', async (state, color) => { - await act(async () => { - renderer = create(createElement(AgentStateDot, { state })) - }) + renderer = await mount(createElement(AgentStateDot, { state })) - const dot = renderer?.root.findAllByType('View').find((view) => Array.isArray(view.props.style)) + const dot = findAllByHostType(renderer.root, 'View').find((view) => + Array.isArray(view.props.style) + ) expect(dot?.props.style).toContainEqual({ backgroundColor: color }) }) }) diff --git a/mobile/src/components/bottom-drawer-close-lifecycle.test.ts b/mobile/src/components/bottom-drawer-close-lifecycle.test.ts index 2e993af225c..4c8690dcea9 100644 --- a/mobile/src/components/bottom-drawer-close-lifecycle.test.ts +++ b/mobile/src/components/bottom-drawer-close-lifecycle.test.ts @@ -1,12 +1,23 @@ -import { createElement } from 'react' +import { createElement, type ComponentProps } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { BottomDrawer } from './BottomDrawer' +import { findByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('./mounted-bottom-drawer', () => ({ MountedBottomDrawer: 'MountedBottomDrawer' })) +function drawerElement(visible: boolean, onClose: () => void, onAfterClose: () => void) { + const props: ComponentProps = { + visible, + onClose, + onAfterClose, + children: createElement('DrawerContent') + } + return createElement(BottomDrawer, props) +} + function renderDrawer( visible: boolean, onClose: () => void, @@ -14,13 +25,7 @@ function renderDrawer( ): ReactTestRenderer { let renderer: ReactTestRenderer | null = null act(() => { - renderer = create( - createElement( - BottomDrawer, - { visible, onClose, onAfterClose }, - createElement('DrawerContent') - ) - ) + renderer = create(drawerElement(visible, onClose, onAfterClose)) }) if (!renderer) { throw new Error('Bottom drawer did not render') @@ -35,18 +40,12 @@ function updateDrawer( onAfterClose: () => void ): void { act(() => { - renderer.update( - createElement( - BottomDrawer, - { visible, onClose, onAfterClose }, - createElement('DrawerContent') - ) - ) + renderer.update(drawerElement(visible, onClose, onAfterClose)) }) } function mountedDrawer(renderer: ReactTestRenderer) { - return renderer.root.findByType('MountedBottomDrawer') + return findByHostType(renderer.root, 'MountedBottomDrawer') } describe('BottomDrawer close lifecycle', () => { diff --git a/mobile/src/components/bottom-drawer-window-handback.test.ts b/mobile/src/components/bottom-drawer-window-handback.test.ts index a1d9c790a69..9a31131f318 100644 --- a/mobile/src/components/bottom-drawer-window-handback.test.ts +++ b/mobile/src/components/bottom-drawer-window-handback.test.ts @@ -1,4 +1,4 @@ -import { createElement, useEffect } from 'react' +import { createElement, useEffect, type ComponentProps } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' @@ -88,11 +88,14 @@ function SheetBody() { } function drawer(interactive: boolean) { - return createElement( - MountedBottomDrawer, - { visible: true, interactive, onClose: noop, onHidden: noop }, - createElement(SheetBody) - ) + const props: ComponentProps = { + visible: true, + interactive, + onClose: noop, + onHidden: noop, + children: createElement(SheetBody) + } + return createElement(MountedBottomDrawer, props) } function render(interactive: boolean): ReactTestRenderer { diff --git a/mobile/src/components/codex-reset-credit-capability.test.ts b/mobile/src/components/codex-reset-credit-capability.test.ts index df8762d7f36..079155185ff 100644 --- a/mobile/src/components/codex-reset-credit-capability.test.ts +++ b/mobile/src/components/codex-reset-credit-capability.test.ts @@ -16,6 +16,7 @@ import { readCodexResetCreditCapability, useCodexResetCreditCapability } from './codex-reset-credit-capability' +import { findByHostType } from '../test-support/react-test-renderer-host-nodes' afterEach(() => { vi.restoreAllMocks() @@ -71,10 +72,10 @@ describe('useCodexResetCreditCapability', () => { act(() => { renderer = create(createElement(Harness)) }) - expect(renderer!.root.findByType('CapabilityResult').props.supported).toBe(false) + expect(findByHostType(renderer!.root, 'CapabilityResult').props.supported).toBe(false) act(() => publish?.([MOBILE_CODEX_RESET_CREDIT_CAPABILITY])) - expect(renderer!.root.findByType('CapabilityResult').props.supported).toBe(true) + expect(findByHostType(renderer!.root, 'CapabilityResult').props.supported).toBe(true) act(() => renderer!.unmount()) expect(cancel).toHaveBeenCalledOnce() diff --git a/mobile/src/components/mobile-diff-review-send-sheet-stacking.test.ts b/mobile/src/components/mobile-diff-review-send-sheet-stacking.test.ts index d1034335e1e..4fbcb10ab43 100644 --- a/mobile/src/components/mobile-diff-review-send-sheet-stacking.test.ts +++ b/mobile/src/components/mobile-diff-review-send-sheet-stacking.test.ts @@ -4,6 +4,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { DiffComment } from '../../../src/shared/diff-comment-types' import type { RpcClient } from '../transport/rpc-client' import type { ReviewScreenState } from '../session/mobile-diff-review-screen-model' +import { stubRpcClient } from '../transport/rpc-client-test-stub' +import type { RpcResponse } from '../transport/types' // iOS cannot present a review sheet while another is still on screen (even mid-close): the second // presentation silently fails and every later tap on the screen is swallowed. These drive the real @@ -58,11 +60,11 @@ const { useMobileDiffReviewController } = await import('../session/use-mobile-diff-review-controller') type Controller = ReturnType -type Deferred = { promise: Promise; resolve: (value: unknown) => void } +type Deferred = { promise: Promise; resolve: (value: RpcResponse) => void } function deferred(): Deferred { - let resolve: (value: unknown) => void = () => {} - const promise = new Promise((settle) => { + let resolve: (value: RpcResponse) => void = () => {} + const promise = new Promise((settle) => { resolve = settle }) return { promise, resolve } @@ -92,13 +94,18 @@ const SNAPSHOT: ReviewScreenState = { reviewState: { version: 1, files: {} } } -const TABS_REPLY = { +const TABS_REPLY: RpcResponse = { id: 'tabs', ok: true, result: { tabs: [{ type: 'terminal', id: 'tab-1', terminal: 'terminal-1', title: 'codex' }] }, _meta: { runtimeId: 'runtime' } } -const SAVE_REPLY = { id: 'save', ok: true, result: {}, _meta: { runtimeId: 'runtime' } } +const SAVE_REPLY: RpcResponse = { + id: 'save', + ok: true, + result: {}, + _meta: { runtimeId: 'runtime' } +} let renderer: ReactTestRenderer | null = null let controller: Controller @@ -135,14 +142,13 @@ async function mountScreen(snapshot: ReviewScreenState = SNAPSHOT): Promise { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the operations under test only call `sendRequest`. - renderer = create(createElement(Screen, { client: { sendRequest } as unknown as RpcClient })) + renderer = create(createElement(Screen, { client: stubRpcClient({ sendRequest }) })) await Promise.resolve() }) expect(controller.currentItem).not.toBeNull() } -async function answer(method: string, reply: unknown): Promise { +async function answer(method: string, reply: RpcResponse): Promise { const pending = replies.get(method) expect(pending, `${method} was never requested`).toBeDefined() await act(async () => { diff --git a/mobile/src/components/mobile-markdown-mermaid-routing.test.ts b/mobile/src/components/mobile-markdown-mermaid-routing.test.ts index 833064ee3fc..e6e305edcf9 100644 --- a/mobile/src/components/mobile-markdown-mermaid-routing.test.ts +++ b/mobile/src/components/mobile-markdown-mermaid-routing.test.ts @@ -2,6 +2,7 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import { MobileMarkdown } from './MobileMarkdown' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ Linking: { openURL: vi.fn() }, @@ -30,11 +31,11 @@ function render(content: string): ReactTestRenderer { } function mermaidCount(tree: ReactTestRenderer): number { - return tree.root.findAllByType('MermaidDiagram' as never).length + return findAllByHostType(tree.root, 'MermaidDiagram').length } function firstMermaid(tree: ReactTestRenderer) { - return tree.root.findByType('MermaidDiagram' as never) + return findByHostType(tree.root, 'MermaidDiagram') } describe('MobileMarkdown mermaid routing', () => { diff --git a/mobile/src/components/pr-sidebar/MermaidDiagram.tsx b/mobile/src/components/pr-sidebar/MermaidDiagram.tsx index 1557f35c552..fddbd2627fc 100644 --- a/mobile/src/components/pr-sidebar/MermaidDiagram.tsx +++ b/mobile/src/components/pr-sidebar/MermaidDiagram.tsx @@ -38,9 +38,9 @@ export const MermaidDiagram = memo(function MermaidDiagram({ source, base }: Mer const failed = failedSource === source useEffect(() => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: react-native-web renders View as a div and forwards the ref to it; this module only ever runs in that build. - const host = hostRef.current as unknown as HTMLElement | null - if (!host) { + // react-native-web renders View as a div and forwards the ref to it. + const host = hostRef.current + if (!(host instanceof HTMLElement)) { return } let disposed = false diff --git a/mobile/src/components/pr-sidebar/PRCommentsSection.test.ts b/mobile/src/components/pr-sidebar/PRCommentsSection.test.ts index bf28570770c..31f07ca9ce9 100644 --- a/mobile/src/components/pr-sidebar/PRCommentsSection.test.ts +++ b/mobile/src/components/pr-sidebar/PRCommentsSection.test.ts @@ -1,9 +1,10 @@ import { createElement } from 'react' -import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { act, create, type ReactTestInstance, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import type { PRComment } from '../../../../src/shared/github/comment-types' import type { GitHubWorkItemDetails } from '../../../../src/shared/github/work-item-types' import { PRCommentsSection } from './PRCommentsSection' +import { findAllByHostType } from '../../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ ActivityIndicator: 'ActivityIndicator', @@ -68,22 +69,22 @@ async function renderComments(details: GitHubWorkItemDetails): Promise node.props.accessibilityState !== undefined) + return findAllByHostType(renderer.root, 'Pressable').filter( + (node) => node.props.accessibilityState !== undefined + ) } function showMoreButton(renderer: ReactTestRenderer) { - const button = renderer.root - .findAllByType('Pressable') - .find((node) => node.props.accessibilityState === undefined) + const button = findAllByHostType(renderer.root, 'Pressable').find( + (node) => node.props.accessibilityState === undefined + ) if (!button) { throw new Error('Show more button not found') } return button } -async function press(node: { props: { onPress: () => void } }): Promise { +async function press(node: ReactTestInstance): Promise { await act(async () => { node.props.onPress() }) @@ -99,20 +100,20 @@ describe('PRCommentsSection', () => { it('resets pagination only when the user chooses a different audience filter', async () => { renderer = await renderComments(detailsWithComments(25)) - expect(renderer.root.findAllByType('PRCommentCard')).toHaveLength(12) + expect(findAllByHostType(renderer.root, 'PRCommentCard')).toHaveLength(12) await press(showMoreButton(renderer)) - expect(renderer.root.findAllByType('PRCommentCard')).toHaveLength(24) + expect(findAllByHostType(renderer.root, 'PRCommentCard')).toHaveLength(24) // The second tab is Humans; moving from All to Humans resets the page limit. await press(audienceTabs(renderer)[1]) - expect(renderer.root.findAllByType('PRCommentCard')).toHaveLength(12) + expect(findAllByHostType(renderer.root, 'PRCommentCard')).toHaveLength(12) await press(showMoreButton(renderer)) - expect(renderer.root.findAllByType('PRCommentCard')).toHaveLength(24) + expect(findAllByHostType(renderer.root, 'PRCommentCard')).toHaveLength(24) // Retapping the active tab used to leave the page size alone; retain that behavior. await press(audienceTabs(renderer)[1]) - expect(renderer.root.findAllByType('PRCommentCard')).toHaveLength(24) + expect(findAllByHostType(renderer.root, 'PRCommentCard')).toHaveLength(24) }) }) diff --git a/mobile/src/diagnostics/connection-diagnostics-submission.ts b/mobile/src/diagnostics/connection-diagnostics-submission.ts index 4952fd8c881..29028fc90c1 100644 --- a/mobile/src/diagnostics/connection-diagnostics-submission.ts +++ b/mobile/src/diagnostics/connection-diagnostics-submission.ts @@ -15,7 +15,7 @@ export type ConnectionDiagnosticsSubmissionResult = { ok: true } | { ok: false; /** Sends only the already-redacted, bounded report after an explicit user tap. */ export async function submitConnectionDiagnostics( submission: ConnectionDiagnosticsSubmission, - fetchImpl: typeof fetch = fetch + fetchImpl: (url: string, init: RequestInit) => Promise = fetch ): Promise { const report = boundConnectionDiagnosticsReport(submission.report) const controller = new AbortController() diff --git a/mobile/src/dictation/mobile-dictation-setup.ts b/mobile/src/dictation/mobile-dictation-setup.ts index 8fed4a9aeef..18fcf96ac96 100644 --- a/mobile/src/dictation/mobile-dictation-setup.ts +++ b/mobile/src/dictation/mobile-dictation-setup.ts @@ -1,4 +1,4 @@ -import type { RpcClient } from '../transport/rpc-client' +import type { RpcOperationSender } from '../transport/rpc-operation-sender' import type { RpcResponse } from '../transport/types' import { interpretOrThrowRefusalMessage } from '../transport/rpc-refusal-message' import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client' @@ -53,7 +53,7 @@ export function isDictationSetupRequiredError(message: string): boolean { return SETUP_REQUIRED_CODES.has(message) || message.startsWith('voice_model_not_ready:') } -export async function fetchDictationSetup(client: RpcClient): Promise { +export async function fetchDictationSetup(client: RpcOperationSender): Promise { const reply = await requestDictationSetupReply(client) if (isLegacyDesktopSpeechSetupReply(reply)) { throw new Error(LEGACY_DESKTOP_SPEECH_SETUP_MESSAGE) @@ -67,7 +67,7 @@ export async function fetchDictationSetup(client: RpcClient): Promise { +async function requestDictationSetupReply(client: RpcOperationSender): Promise { try { return await dictationSetupRead.request(client, null) } catch (error) { @@ -80,7 +80,10 @@ async function requestDictationSetupReply(client: RpcClient): Promise { +export async function downloadDictationModel( + client: RpcOperationSender, + modelId: string +): Promise { const reply = await dictationModelDownload.request(client, { modelId }) interpretOrThrowRefusalMessage( () => dictationModelDownload.interpret(reply), @@ -89,7 +92,7 @@ export async function downloadDictationModel(client: RpcClient, modelId: string) } export async function deleteDictationModel( - client: RpcClient, + client: RpcOperationSender, modelId: string ): Promise { const reply = await dictationModelDelete.request(client, { modelId }) @@ -100,7 +103,7 @@ export async function deleteDictationModel( } export async function setDictationConfig( - client: RpcClient, + client: RpcOperationSender, params: { enabled?: boolean; modelId?: string; dictationMode?: 'toggle' | 'hold' } ): Promise { const reply = await dictationConfigWrite.request(client, params) diff --git a/mobile/src/files/MobileFileExplorerPanel.tsx b/mobile/src/files/MobileFileExplorerPanel.tsx index cb07684c739..460ac802c0c 100644 --- a/mobile/src/files/MobileFileExplorerPanel.tsx +++ b/mobile/src/files/MobileFileExplorerPanel.tsx @@ -21,7 +21,6 @@ import { type DirectoryCache, type FileExplorerRow } from './file-tree' -import type { RpcFailure } from '../transport/types' import { colors, spacing } from '../theme/mobile-theme' import { beginDirectoryLoad, @@ -116,8 +115,7 @@ export function MobileFileExplorerPanel(props: { }) const directory = fileDirectoryRead.interpret(response) if (!directory.accepted) { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this policy skips only a refusal, so an unaccepted reply is a failure envelope. - const refusal = (response as RpcFailure).error + const refusal = response.ok ? undefined : response.error // Why: desktops that predate the files.readDir mobile allowlist // entry still serve the capped files.list; fall back so the Files // tab keeps working until the desktop updates. @@ -144,8 +142,7 @@ export function MobileFileExplorerPanel(props: { return } throw new Error( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this policy skips only a refusal, so an unaccepted reply is a failure envelope. - (legacyReply as RpcFailure).error?.message || + (legacyReply.ok ? undefined : legacyReply.error)?.message || refusal?.message || 'Unable to load files' ) diff --git a/mobile/src/files/MobileFileMarkdownPreview.test.ts b/mobile/src/files/MobileFileMarkdownPreview.test.ts index 41d6287204f..9b0f95ee881 100644 --- a/mobile/src/files/MobileFileMarkdownPreview.test.ts +++ b/mobile/src/files/MobileFileMarkdownPreview.test.ts @@ -2,6 +2,7 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { MobileFileMarkdownPreview } from './MobileFileMarkdownPreview' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ Alert: { alert }, @@ -67,9 +68,9 @@ async function updatePreview(renderer: ReactTestRenderer, props: PreviewProps): } function modeToggle(renderer: ReactTestRenderer, label: string) { - const toggle = renderer.root - .findAllByType('Pressable') - .find((node) => node.props.accessibilityLabel === label) + const toggle = findAllByHostType(renderer.root, 'Pressable').find( + (node) => node.props.accessibilityLabel === label + ) if (!toggle) { throw new Error(`Missing ${label} toggle`) } @@ -131,7 +132,7 @@ describe('MobileFileMarkdownPreview', () => { }) const copy = modeToggle(renderer, 'Copy loaded Markdown source') expect( - renderer.root.findAllByType('Text').some((node) => node.props.children === 'Copy loaded') + findAllByHostType(renderer.root, 'Text').some((node) => node.props.children === 'Copy loaded') ).toBe(true) await act(async () => copy.props.onPress()) expect(writeText.mock.calls).toEqual([[source]]) diff --git a/mobile/src/files/mobile-file-preview-request.ts b/mobile/src/files/mobile-file-preview-request.ts index 67fbdc18b47..4f2c7580466 100644 --- a/mobile/src/files/mobile-file-preview-request.ts +++ b/mobile/src/files/mobile-file-preview-request.ts @@ -132,10 +132,14 @@ function settlePreviewSend( interpret: (reply: RpcResponse) => RpcAcceptedResult ): MobileFilePreviewOutcome { const verdict = interpret(reply) - return verdict.accepted - ? { accepted: true, payload: verdict.value } - : // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this policy skips only a refusal, so an unaccepted reply is a failure envelope. - { accepted: false, refusal: (reply as RpcFailure).error } + if (verdict.accepted) { + return { accepted: true, payload: verdict.value } + } + // The skip policy declines only a refusal; an ok reply is accepted or throws as incompatible. + if (reply.ok) { + throw new Error('Preview reply was neither accepted nor refused') + } + return { accepted: false, refusal: reply.error } } export async function loadMobileFilePreview( diff --git a/mobile/src/hooks/mobile-dictation-desktop-start.test.ts b/mobile/src/hooks/mobile-dictation-desktop-start.test.ts index 8166028d2dd..0dd2533fbad 100644 --- a/mobile/src/hooks/mobile-dictation-desktop-start.test.ts +++ b/mobile/src/hooks/mobile-dictation-desktop-start.test.ts @@ -60,7 +60,7 @@ function createStartHarness(options: StartHarnessOptions = {}) { describe('startMobileDictationDesktopSession', () => { it('cancels a start a newer one superseded while the desktop session opened', async () => { - let setNewerStart = () => undefined + let setNewerStart: () => void = () => {} const harness = createStartHarness({ sendRequest: async (method) => { if (method === 'speech.dictation.start') { @@ -83,7 +83,7 @@ describe('startMobileDictationDesktopSession', () => { }) it('returns to idle when a disable makes the start stale', async () => { - let setDisabled = () => undefined + let setDisabled: () => void = () => {} const harness = createStartHarness({ sendRequest: async (method) => { if (method === 'speech.dictation.start') { @@ -125,7 +125,7 @@ describe('startMobileDictationDesktopSession', () => { }) it('leaves the capture alone when the failure is no longer the current start', async () => { - let setNewerStart = () => undefined + let setNewerStart: () => void = () => {} const harness = createStartHarness({ sendRequest: async (method) => { if (method === 'speech.dictation.start') { diff --git a/mobile/src/host-edit-route-accessibility.test.ts b/mobile/src/host-edit-route-accessibility.test.ts index 3526db9b1b4..568ef5ff523 100644 --- a/mobile/src/host-edit-route-accessibility.test.ts +++ b/mobile/src/host-edit-route-accessibility.test.ts @@ -2,6 +2,7 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import EditHostScreen from '../app/h/[hostId]/edit' +import { findAllByHostType } from './test-support/react-test-renderer-host-nodes' const dependencies = vi.hoisted(() => ({ back: vi.fn(), @@ -79,7 +80,7 @@ describe('edit host route accessibility', () => { it('exposes stable accessible names for both editable fields', async () => { const renderer = await renderEditHostRoute() - const inputs = renderer.root.findAllByType('TextInput') + const inputs = findAllByHostType(renderer.root, 'TextInput') expect(inputs).toHaveLength(2) expect(inputs.map((input) => input.props.accessibilityLabel)).toEqual(['Name', 'Address']) diff --git a/mobile/src/host-screen/host-list-first-frame.test.tsx b/mobile/src/host-screen/host-list-first-frame.test.tsx index b262172b27a..8fea186bd02 100644 --- a/mobile/src/host-screen/host-list-first-frame.test.tsx +++ b/mobile/src/host-screen/host-list-first-frame.test.tsx @@ -3,7 +3,6 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { SIDEBAR_INPUTS_SEMANTICS_VERSION } from '../../../src/shared/sidebar-list/sidebar-inputs-feed-contract' import type { InstalledSidebarInputs } from '../../../src/shared/sidebar-list/sidebar-inputs-feed-installer' -import type { RpcClient } from '../transport/rpc-client' import type { Section } from '../worktree/workspace-list-types' import type { SidebarFeedReaderRead } from '../../../src/shared/sidebar-feed-client/sidebar-feed-reader' import { installSidebarFeedSnapshot } from '../../../src/shared/sidebar-feed-client/sidebar-feed-snapshot' @@ -36,6 +35,7 @@ vi.mock('../worktree/use-last-visited-worktree-on-host', () => ({ import { useHostListSections } from './use-host-list-sections' import type { HostScreenState } from './use-host-screen-state' import { forgetRememberedSidebarFeeds, useHostSidebarFeed } from './use-host-sidebar-feed' +import { stubRpcClient } from '../transport/rpc-client-test-stub' /** worktree.ps's rows: flat, unpinned, in its own order — not the shape the desktop draws. */ const LISTED = [ @@ -72,8 +72,7 @@ const STATE = { showPinnedInGroups: false } as unknown as HostScreenState -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked reader never sends. -const CLIENT = {} as RpcClient +const CLIENT = stubRpcClient() let drawn: Section[] = [] let kind = '' diff --git a/mobile/src/host-screen/host-worktree-child-navigation.test.tsx b/mobile/src/host-screen/host-worktree-child-navigation.test.tsx index 10cca794ac7..81aa8ec5d44 100644 --- a/mobile/src/host-screen/host-worktree-child-navigation.test.tsx +++ b/mobile/src/host-screen/host-worktree-child-navigation.test.tsx @@ -6,11 +6,12 @@ import { structuredAgentSessionPaneKey, structuredAgentSessionTabId } from '../../../src/shared/structured-agent-session-projection' -import type { RpcClient } from '../transport/rpc-client' import { MOBILE_DESKTOP_RELAY_RUNTIME_CAPABILITY } from '../../../src/shared/mobile-desktop-relay-contract' import type { Worktree } from '../worktree/workspace-list-types' import { useHostScreenState } from './use-host-screen-state' import { useHostWorktreeActions } from './use-host-worktree-actions' +import { stubRpcClient } from '../transport/rpc-client-test-stub' +import type { RpcResponse } from '../transport/types' const renderers = new Set>() function create(...args: Parameters): ReturnType { @@ -62,12 +63,14 @@ describe('a workspace child opens its parent pane', () => { async (paneKey, executionHost) => { const push = vi.fn() const replace = vi.fn() - const sendRequest = vi.fn(async (_method: string, _params: unknown, _options?: unknown) => ({ - ok: true, - result: {} - })) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this action only sends activation; the scoped client never uses its other methods. - const client = { sendRequest } as unknown as RpcClient + const sendRequest = vi.fn( + async (_method: string, _params: unknown, _options?: unknown): Promise => ({ + id: 'activate', + ok: true, + result: {} + }) + ) + const client = stubRpcClient({ sendRequest }) const row = { ...item, ...(executionHost ? { hostId: executionHost } : {}) } const held: { open?: (item: Worktree, paneKey?: string) => void } = {} function Probe() { diff --git a/mobile/src/mock-server-key-pair.test.ts b/mobile/src/mock-server-key-pair.test.ts index dbe29567162..1db9757e1e2 100644 --- a/mobile/src/mock-server-key-pair.test.ts +++ b/mobile/src/mock-server-key-pair.test.ts @@ -9,6 +9,18 @@ import { loadOrCreateMockServerKeyPair } from '../scripts/mock-server-key-pair' const temporaryDirectories: string[] = [] +// Why: this file runs under Node, but the program's RN libs type a timer handle as a number. +function unrefTimer(timer: unknown): void { + if ( + typeof timer === 'object' && + timer !== null && + 'unref' in timer && + typeof timer.unref === 'function' + ) { + timer.unref() + } +} + type ConcurrentCreator = { ready: Promise locked: Promise @@ -75,7 +87,7 @@ function runConcurrentCreator(keyFile: string): ConcurrentCreator { resolveClosed = resolve }) const timeout = setTimeout(() => child.kill(), 5_000) - timeout.unref() + unrefTimer(timeout) child.stdout.on('data', (chunk) => { stdout += chunk.toString() if (stdout.includes('READY\n')) { diff --git a/mobile/src/notifications/NotificationDeliverySection.test.tsx b/mobile/src/notifications/NotificationDeliverySection.test.tsx index f51614a821f..87a92a9f3c6 100644 --- a/mobile/src/notifications/NotificationDeliverySection.test.tsx +++ b/mobile/src/notifications/NotificationDeliverySection.test.tsx @@ -3,6 +3,7 @@ import { act, create } from 'react-test-renderer' import { expect, it, vi } from 'vitest' import { NotificationDeliverySection } from './NotificationDeliverySection' import { DEFAULT_NOTIFICATION_DELIVERY } from './notification-delivery-preferences' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('@react-native-async-storage/async-storage', () => ({ default: {} })) vi.mock('react-native', () => ({ @@ -20,7 +21,7 @@ it('shows only phone-specific controls while desktop owns category eligibility', createElement(NotificationDeliverySection, { value: DEFAULT_NOTIFICATION_DELIVERY, onChange }) ) }) - const switches = () => renderer.root.findAllByType('Switch' as never) + const switches = () => findAllByHostType(renderer.root, 'Switch') expect(switches().map((node) => node.props.accessibilityLabel)).toEqual([ 'Only when away from desktop', 'Notification sound', diff --git a/mobile/src/notifications/notification-consent-ownership.test.ts b/mobile/src/notifications/notification-consent-ownership.test.ts index c72d3d07c68..d71fa673c4e 100644 --- a/mobile/src/notifications/notification-consent-ownership.test.ts +++ b/mobile/src/notifications/notification-consent-ownership.test.ts @@ -13,6 +13,7 @@ import { startPushTokenSync } from './push-registration' import { getDevicePushToken } from './push-token' +import { findByHostType } from '../test-support/react-test-renderer-host-nodes' const mocks = vi.hoisted(() => ({ storage: new Map(), replace: vi.fn() })) vi.mock('@react-native-async-storage/async-storage', () => ({ @@ -132,16 +133,16 @@ async function choose(entry: string) { await act(async () => layoutOnboarding()) await act(async () => { if (entry === 'settings') { - renderer!.root.findByType('Switch').props.onValueChange(true) + findByHostType(renderer!.root, 'Switch').props.onValueChange(true) } else { - renderer!.root.findByType('Page').props.onNotificationChoice('enable') + findByHostType(renderer!.root, 'Page').props.onNotificationChoice('enable') } }) } function expectChoiceComplete(entry: string) { expect(mocks.storage.get('orca:pushServiceNotificationsEnabled')).toBe('true') if (entry === 'settings') { - expect(renderer!.root.findByType('Switch').props).toMatchObject({ + expect(findByHostType(renderer!.root, 'Switch').props).toMatchObject({ value: true, disabled: false }) @@ -192,7 +193,7 @@ it('remembers Not now without registering and does not ask again', async () => { }) await act(async () => layoutOnboarding()) await act(async () => { - renderer!.root.findByType('Page').props.onNotificationChoice('skip') + findByHostType(renderer!.root, 'Page').props.onNotificationChoice('skip') }) await drain() await expect(shouldPresentNotificationOptIn()).resolves.toBe(false) diff --git a/mobile/src/notifications/push-dismissal-native-races.test.ts b/mobile/src/notifications/push-dismissal-native-races.test.ts index 35a51748906..fd577f0e4ff 100644 --- a/mobile/src/notifications/push-dismissal-native-races.test.ts +++ b/mobile/src/notifications/push-dismissal-native-races.test.ts @@ -106,13 +106,12 @@ it('suppresses presentation when dismissal completes during the handler sound re }) return { sound: true } as Awaited> }) - const pending = foregroundNotificationBehavior({ - request: { - identifier: 'foreground-alert', - trigger: null, - content: { title: null, subtitle: null, body: null, sound: null, data: { orca: payload } } - } - }) + const foregroundAlert = { + identifier: 'foreground-alert', + trigger: null, + content: { title: null, subtitle: null, body: null, sound: null, data: { orca: payload } } + } + const pending = foregroundNotificationBehavior({ request: foregroundAlert }) await vi.waitFor(() => expect(finish).toBeDefined()) await foregroundNotificationBehavior({ request: { content: { data: { orca: { ...fence, kind: 'dismiss' } } } } diff --git a/mobile/src/notifications/push-receive.ts b/mobile/src/notifications/push-receive.ts index 92b4c376080..03b50639a75 100644 --- a/mobile/src/notifications/push-receive.ts +++ b/mobile/src/notifications/push-receive.ts @@ -5,7 +5,7 @@ import { loadPushNotificationsEnabled } from '../storage/preferences' import { loadHostCatalog } from '../transport/host-store' import { resolveHostIdForFingerprint } from './push-host-fingerprint' import { readOrcaPushPayload, type OrcaPushPayload } from './push-payload' -import type { Notification, NotificationBehavior } from 'expo-notifications' +import type { NotificationBehavior } from 'expo-notifications' import { readNativeNotificationData } from './native-notification-data' import { loadNotificationDeliveryPreferences } from './notification-delivery-preferences' @@ -45,9 +45,9 @@ export function resetForegroundPushClaimsForTests(): void { recentForegroundPushes.clear() } -export async function foregroundNotificationBehavior( - notification: Pick -): Promise { +export async function foregroundNotificationBehavior(notification: { + request: Parameters[0] +}): Promise { const data = readNativeNotificationData(notification.request) const payload = readOrcaPushPayload(data) const preferences = await loadNotificationDeliveryPreferences() diff --git a/mobile/src/notifications/push-registration.test.ts b/mobile/src/notifications/push-registration.test.ts index 5708eb9da65..db8b2ba1b10 100644 --- a/mobile/src/notifications/push-registration.test.ts +++ b/mobile/src/notifications/push-registration.test.ts @@ -228,9 +228,9 @@ describe('push registration capability gating', () => { describe('push registration token changes', () => { it('re-registers every connected host when the provider rolls the token', async () => { - let onTokenChange: ((token: MobilePushToken) => void) | null = null + const tokenListener: { onChange?: (token: MobilePushToken) => void } = {} vi.mocked(addPushTokenListener).mockImplementation((listener) => { - onTokenChange = listener + tokenListener.onChange = listener return () => {} }) const { client, sent } = makeClient([NOTIFICATIONS_REMOTE_PUSH_CAPABILITY]) @@ -242,7 +242,7 @@ describe('push registration token changes', () => { await flush() const stop = startPushTokenSync() - onTokenChange?.({ platform: 'ios', token: 'b'.repeat(64), apnsEnvironment: 'sandbox' }) + tokenListener.onChange?.({ platform: 'ios', token: 'b'.repeat(64), apnsEnvironment: 'sandbox' }) await flush() const registers = sent.filter((request) => request.method === 'notifications.registerPush') @@ -391,7 +391,7 @@ describe('push unregistration', () => { it('re-arms the unregister when the switch goes off while a register is in flight', async () => { const sent: string[] = [] - let releaseRegister: (() => void) | null = null + const register: { release?: () => void } = {} const client = { sendRequest: vi.fn(async (method: string) => { sent.push(method) @@ -400,7 +400,7 @@ describe('push unregistration', () => { } if (method === 'notifications.registerPush') { await new Promise((resolve) => { - releaseRegister = resolve + register.release = resolve }) return ok({ registered: true, registrationId: 'registration-1' }) } @@ -414,7 +414,7 @@ describe('push unregistration', () => { // The sweep snapshots `registered` while this host is still only in flight. const switchedOff = setRemotePushEnabled(false) await flush() - releaseRegister?.() + register.release?.() await switchedOff await flush() diff --git a/mobile/src/notifications/push-registration.ts b/mobile/src/notifications/push-registration.ts index 07476cf9a9c..04b6b0cdf99 100644 --- a/mobile/src/notifications/push-registration.ts +++ b/mobile/src/notifications/push-registration.ts @@ -12,7 +12,7 @@ import type { MobilePushRegisterInput } from '../../../src/shared/mobile-push-contract' import { NOTIFICATIONS_REMOTE_PUSH_RUNTIME_CAPABILITY } from '../../../src/shared/protocol-version' -import type { RpcClient } from '../transport/rpc-client' +import type { RpcOperationSender } from '../transport/rpc-operation-sender' import { startRuntimeCapabilityProbe } from '../transport/runtime-capability-probe' import { pushRouteRegister, pushRouteUnregister } from './mobile-push-registration-operations' import { @@ -25,7 +25,7 @@ import { addPushTokenListener, getDevicePushToken, type MobilePushToken } from ' export const NOTIFICATIONS_REMOTE_PUSH_CAPABILITY = NOTIFICATIONS_REMOTE_PUSH_RUNTIME_CAPABILITY -type PushClient = RpcClient +type PushClient = RpcOperationSender const REQUEST_TIMEOUT_MS = 5_000 const REMOVAL_TIMEOUT_MS = 2_000 diff --git a/mobile/src/notifications/push-token.test.ts b/mobile/src/notifications/push-token.test.ts index 2a193430ac6..98299ab2425 100644 --- a/mobile/src/notifications/push-token.test.ts +++ b/mobile/src/notifications/push-token.test.ts @@ -66,16 +66,16 @@ describe('getDevicePushToken', () => { describe('addPushTokenListener', () => { it('forwards a rolled native token and removes the subscription on teardown', () => { const remove = vi.fn() - let emit: ((raw: unknown) => void) | null = null + const native: { emit?: Parameters[0] } = {} vi.mocked(Notifications.addPushTokenListener).mockImplementation((listener) => { - emit = listener as (raw: unknown) => void - return { remove } as never + native.emit = listener + return { remove } }) const seen: unknown[] = [] const stop = addPushTokenListener((token) => seen.push(token)) - emit?.({ type: 'android', data: 'rolled' }) - emit?.({ type: 'web', data: {} }) + native.emit?.({ type: 'android', data: 'rolled' }) + native.emit?.({ type: 'web', data: {} }) stop() expect(seen).toEqual([{ platform: 'android', token: 'rolled' }]) diff --git a/mobile/src/onboarding/MobileOnboardingPage.test.ts b/mobile/src/onboarding/MobileOnboardingPage.test.ts index fec0ef53169..99861014800 100644 --- a/mobile/src/onboarding/MobileOnboardingPage.test.ts +++ b/mobile/src/onboarding/MobileOnboardingPage.test.ts @@ -1,7 +1,8 @@ -import { createElement } from 'react' +import { createElement, type ReactNode } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import { MobileOnboardingPage } from './MobileOnboardingPage' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', async () => { const React = await import('react') @@ -29,7 +30,7 @@ vi.mock('react-native', async () => { Easing: { cubic: (t: number) => t, in: (e: unknown) => e, out: (e: unknown) => e }, Image: 'Image', Pressable: 'Pressable', - ScrollView: ({ children, ...props }: { children?: unknown }) => + ScrollView: ({ children, ...props }: { children?: ReactNode }) => React.createElement('ScrollView', props, children), StyleSheet: { create: (styles: unknown) => styles }, Text: 'Text', @@ -81,14 +82,17 @@ describe('MobileOnboardingPage', () => { } function button(label: string) { - return renderer!.root.find( - (node) => node.type === 'Pressable' && node.props.accessibilityLabel === label + const match = findAllByHostType(renderer!.root, 'Pressable').find( + (node) => node.props.accessibilityLabel === label ) + if (!match) { + throw new Error(`No ${label} button`) + } + return match } function collectText(): string { - return renderer!.root - .findAllByType('Text') + return findAllByHostType(renderer!.root, 'Text') .flatMap((node) => node.props.children) .join(' ') } @@ -133,7 +137,7 @@ describe('MobileOnboardingPage', () => { it('hides an off-screen page from assistive technology', async () => { await renderPage('notifications', { active: false }) - const scrollView = renderer!.root.findByType('ScrollView') + const scrollView = findByHostType(renderer!.root, 'ScrollView') expect(scrollView.props.accessibilityElementsHidden).toBe(true) expect(scrollView.props.importantForAccessibility).toBe('no-hide-descendants') diff --git a/mobile/src/onboarding/NotificationOnboardingPreview.test.ts b/mobile/src/onboarding/NotificationOnboardingPreview.test.ts index 5016ba38bd6..fa400485a9f 100644 --- a/mobile/src/onboarding/NotificationOnboardingPreview.test.ts +++ b/mobile/src/onboarding/NotificationOnboardingPreview.test.ts @@ -1,7 +1,8 @@ -import { createElement } from 'react' +import { createElement, type ReactNode } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import { NotificationOnboardingPreview } from './NotificationOnboardingPreview' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' const mocks = vi.hoisted(() => { const anim = () => ({ start: vi.fn(), stop: vi.fn() }) @@ -30,7 +31,7 @@ vi.mock('react-native', async () => { }, Animated: { Value: AnimatedValue, - View: ({ children, ...props }: { children?: unknown }) => + View: ({ children, ...props }: { children?: ReactNode }) => React.createElement('AnimatedView', props, children), delay: vi.fn(() => ({ start: vi.fn(), stop: vi.fn() })), loop: mocks.loop, @@ -75,8 +76,7 @@ describe('NotificationOnboardingPreview', () => { it('shows sample agent-done and needs-input banners as a decorative mock', async () => { await renderPreview() const preview = renderer!.root.findByProps({ testID: 'notification-onboarding-preview' }) - const copy = renderer!.root - .findAllByType('Text') + const copy = findAllByHostType(renderer!.root, 'Text') .flatMap((node) => node.props.children) .join(' ') diff --git a/mobile/src/onboarding/legacy-notification-opt-in-route.test.ts b/mobile/src/onboarding/legacy-notification-opt-in-route.test.ts index c7efaaefd86..38c0bf70de9 100644 --- a/mobile/src/onboarding/legacy-notification-opt-in-route.test.ts +++ b/mobile/src/onboarding/legacy-notification-opt-in-route.test.ts @@ -24,11 +24,11 @@ describe('legacy notification opt-in destination', () => { }) }) - it.each([ + it.each<[string[] | undefined, string | undefined]>([ [['first-host', 'second-host'], 'first-host'], [[], undefined], [undefined, undefined] - ] as const)('normalizes hostId %j to %s', (hostId, expectedHostId) => { + ])('normalizes hostId %j to %s', (hostId, expectedHostId) => { expect(legacyNotificationOptInDestination({ hostId })).toEqual({ pathname: '/mobile-onboarding', params: { diff --git a/mobile/src/onboarding/mobile-onboarding-screen.test.ts b/mobile/src/onboarding/mobile-onboarding-screen.test.ts index f9e61ce8d17..99770d28c50 100644 --- a/mobile/src/onboarding/mobile-onboarding-screen.test.ts +++ b/mobile/src/onboarding/mobile-onboarding-screen.test.ts @@ -2,6 +2,7 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import MobileOnboardingScreen from '../../app/mobile-onboarding' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' const mocks = vi.hoisted(() => ({ params: { hostId: 'paired-host', steps: 'session-view,notifications' }, @@ -102,7 +103,7 @@ describe('MobileOnboardingScreen', () => { } function trackStyle() { - return renderer!.root.findByType('AnimatedView').props.style[1] + return findByHostType(renderer!.root, 'AnimatedView').props.style[1] } function trackWidth(): number { @@ -116,7 +117,7 @@ describe('MobileOnboardingScreen', () => { } function pages() { - return renderer!.root.findAllByType('MobileOnboardingPage') + return findAllByHostType(renderer!.root, 'MobileOnboardingPage') } it('advances from session view to notifications before opening the paired host', async () => { diff --git a/mobile/src/session/MobileMarkdownReader.test.tsx b/mobile/src/session/MobileMarkdownReader.test.tsx index bea3d8adcf7..124a3002946 100644 --- a/mobile/src/session/MobileMarkdownReader.test.tsx +++ b/mobile/src/session/MobileMarkdownReader.test.tsx @@ -1,8 +1,9 @@ -import { createElement, forwardRef, useImperativeHandle } from 'react' +import { createElement, forwardRef, useImperativeHandle, type ReactNode } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import { MobileMarkdownReader } from './MobileMarkdownReader' import type { MarkdownDocState } from './mobile-session-route-types' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' const mocks = vi.hoisted(() => ({ dismissKeyboard: vi.fn(), @@ -14,7 +15,7 @@ vi.mock('react-native', async () => { return { ActivityIndicator: 'ActivityIndicator', Platform: { OS: 'ios', select: (choices: Record) => choices.ios }, - Pressable: ({ children, ...props }: { children?: unknown }) => + Pressable: ({ children, ...props }: { children?: ReactNode }) => React.createElement('Pressable', props, children), StyleSheet: { create: (styles: unknown) => styles, hairlineWidth: 1 }, Text: 'Text', @@ -100,18 +101,18 @@ describe('MobileMarkdownReader', () => { const instance = render(0) expect(dismissButtons(instance)).toHaveLength(0) - expect(instance.root.findAllByType('Text')).toHaveLength(0) + expect(findAllByHostType(instance.root, 'Text')).toHaveLength(0) }) it('keeps document actions available without offering dismissal when the keyboard is closed', () => { const instance = render(0, { ...readyDoc, localContent: '# Edited', isDirty: true }) expect(dismissButtons(instance)).toHaveLength(0) - expect(instance.root.findAllByType('Text').map((node) => node.props.children)).toEqual([ + expect(findAllByHostType(instance.root, 'Text').map((node) => node.props.children)).toEqual([ 'Discard', 'Save' ]) - const actions = instance.root.findAllByType('Pressable') + const actions = findAllByHostType(instance.root, 'Pressable') expect(actions).toHaveLength(2) expect(actions[1]?.props.disabled).not.toBe(true) act(() => actions[0]?.props.onPress()) diff --git a/mobile/src/session/MobileNativeChatAsk.tsx b/mobile/src/session/MobileNativeChatAsk.tsx index afc6ff214b8..b7f11ff6c6d 100644 --- a/mobile/src/session/MobileNativeChatAsk.tsx +++ b/mobile/src/session/MobileNativeChatAsk.tsx @@ -83,14 +83,9 @@ export function MobileNativeChatAsk({ const total = prompt.questions.length const isLast = index === total - 1 - const currentAnswered = useMemo( - () => isAnswered(index), - // eslint-disable-next-line react-hooks/exhaustive-deps - [selections, otherText, index] - ) + const currentAnswered = useMemo(() => isAnswered(index), [selections, otherText, index]) const allAnswered = useMemo( () => prompt.questions.every((_, i) => isAnswered(i)), - // eslint-disable-next-line react-hooks/exhaustive-deps [otherText, prompt.questions, selections] ) const canAdvance = !submitting && (isLast ? allAnswered : currentAnswered) @@ -133,6 +128,7 @@ export function MobileNativeChatAsk({ > {prompt.questions.map((qq, i) => ( 0 @@ -25,7 +26,7 @@ vi.mock('react-native', async () => { Keyboard: { dismiss: vi.fn() }, Platform: { OS: 'ios' }, Pressable: 'Pressable', - ScrollView: ({ children, ...props }: { children?: unknown }) => + ScrollView: ({ children, ...props }: { children?: ReactNode }) => React.createElement('ScrollView', props, children), StyleSheet: { create: (styles: unknown) => styles, @@ -93,13 +94,13 @@ describe('MobileNativeChatComposer', () => { }) } - function sendButton(): { props: { onPress: () => Promise } } { + function sendButton(): ReactTestInstance { if (!renderer) { throw new Error('Composer was not rendered') } - return renderer.root.find( - (node) => node.type === 'Pressable' && node.props.accessibilityLabel === 'Send message' - ) as { props: { onPress: () => Promise } } + return findAllByHostType(renderer.root, 'Pressable').find( + (node) => node.props.accessibilityLabel === 'Send message' + )! } it('reports an accepted send without owning route-scoped draft cleanup', async () => { @@ -121,7 +122,7 @@ describe('MobileNativeChatComposer', () => { const composer = renderer!.root.findByProps({ testID: 'native-chat-composer' }) const inset = renderer!.root.findByProps({ testID: 'native-chat-composer-inset' }) const actions = renderer!.root.findByProps({ testID: 'native-chat-composer-actions' }) - expect(composer.findAllByType('TextInput')).toHaveLength(1) + expect(findAllByHostType(composer, 'TextInput')).toHaveLength(1) expect(composer.children.at(-1)).toBe(actions) expect(inset.props.style).toMatchObject({ paddingBottom: spacing.sm }) expect(composer.props.style).toMatchObject({ borderWidth: 1, borderRadius: radii.composer }) @@ -169,6 +170,7 @@ describe('MobileNativeChatComposer', () => { ] }, valueSource: 'unknown' as const, + transport: 'catalog' as const, settable: true } ], @@ -191,10 +193,10 @@ describe('MobileNativeChatComposer', () => { await act(async () => { renderer = create(draw('run the tests')) }) - const modelPill = (): { props: { accessibilityState: { disabled: boolean } } } => - renderer!.root.find( - (node) => node.type === 'Pressable' && node.props.accessibilityLabel === 'Model, Model' - ) as { props: { accessibilityState: { disabled: boolean } } } + const modelPill = () => + findAllByHostType(renderer!.root, 'Pressable').find( + (node) => node.props.accessibilityLabel === 'Model, Model' + )! expect(modelPill().props.accessibilityState).toMatchObject({ disabled: false }) // Start the send but don't await it — it stays in flight on purpose. let pressed!: Promise @@ -210,7 +212,7 @@ describe('MobileNativeChatComposer', () => { ) expect(onStop).toHaveBeenCalledTimes(1) expect(onSend).toHaveBeenCalledTimes(1) - expect(renderer!.root.findByType('TextInput').props.value).toBe('next draft') + expect(findByHostType(renderer!.root, 'TextInput').props.value).toBe('next draft') expect(modelPill().props.accessibilityState).toMatchObject({ disabled: true }) await act(async () => { releaseSend?.(true) @@ -284,11 +286,7 @@ describe('MobileNativeChatComposer', () => { await act(async () => { renderer = create(draw(false)) }) - const input = () => - renderer!.root.find((node) => { - const type: unknown = node.type - return type === 'TextInput' - }) + const input = () => findByHostType(renderer!.root, 'TextInput') const originalInput = input() await act(async () => input().props.onFocus()) await act(async () => renderer!.update(draw(true))) @@ -357,7 +355,7 @@ describe('MobileNativeChatComposer', () => { }) const bar = () => renderer!.root.findByProps({ testID: 'native-chat-composer' }) const labelled = (label: string) => renderer!.root.findAllByProps({ accessibilityLabel: label }) - const field = () => renderer!.root.findByType('TextInput') + const field = () => findByHostType(renderer!.root, 'TextInput') const unfocused = bar().props.style expect(labelled('Stop the agent')).toHaveLength(1) expect(labelled('Send message')).toHaveLength(0) @@ -384,6 +382,7 @@ describe('MobileNativeChatComposer', () => { category: id === 'model' ? ('model' as const) : ('thought_level' as const), kind: { type: 'select' as const, choices: [] }, valueSource: 'unknown' as const, + transport: 'catalog' as const, settable: true })), pendingId: null, @@ -406,32 +405,32 @@ describe('MobileNativeChatComposer', () => { createNodeMock: (element) => (element.type === 'TextInput' ? { focus } : null) }) }) - const field = () => renderer!.root.findByType('TextInput') + const field = () => findByHostType(renderer!.root, 'TextInput') const press = async (label: string) => act(async () => renderer!.root.findByProps({ accessibilityLabel: label }).props.onPress()) - expect(renderer!.root.findAllByType('BottomDrawer')).toHaveLength(0) + expect(findAllByHostType(renderer!.root, 'BottomDrawer')).toHaveLength(0) await act(async () => field().props.onFocus()) await press('Model, Model effort') await act(async () => field().props.onBlur()) await press('effort, Not set') await press('Back to models') await press('Close picker') - await act(async () => renderer!.root.findByType('BottomDrawer').props.onHidden()) + await act(async () => findByHostType(renderer!.root, 'BottomDrawer').props.onHidden()) expect(focus).toHaveBeenCalledTimes(1) for (const id of ['model', 'effort']) { await act(async () => field().props.onFocus()) controller.optionPickerRequest = { id, sequence: controller.optionPickerRequest.sequence + 1 } await act(async () => renderer!.update(draw())) await act(async () => field().props.onBlur()) - await act(async () => renderer!.root.findByType('BottomDrawer').props.onClose()) - await act(async () => renderer!.root.findByType('BottomDrawer').props.onHidden()) + await act(async () => findByHostType(renderer!.root, 'BottomDrawer').props.onClose()) + await act(async () => findByHostType(renderer!.root, 'BottomDrawer').props.onHidden()) expect(focus).toHaveBeenCalledTimes(1) } await act(async () => renderer!.unmount()) await act(async () => { renderer = create(draw()) }) - expect(renderer!.root.findAllByType('BottomDrawer')).toHaveLength(0) + expect(findAllByHostType(renderer!.root, 'BottomDrawer')).toHaveLength(0) }) it('renders a removable thumbnail for each pending image attachment', async () => { @@ -452,14 +451,12 @@ describe('MobileNativeChatComposer', () => { }) ) }) - const thumbs = renderer!.root.findAll((node) => node.type === 'Image') as Array<{ - props: { source: { uri: string } } - }> + const thumbs = findAllByHostType(renderer!.root, 'Image') expect(thumbs.map((t) => t.props.source.uri)).toEqual(['file:///a.png', 'file:///b.png']) - const remove = renderer!.root.findAll( - (node) => node.type === 'Pressable' && node.props.accessibilityLabel === 'Remove image' - ) as Array<{ props: { onPress: () => void } }> + const remove = findAllByHostType(renderer!.root, 'Pressable').filter( + (node) => node.props.accessibilityLabel === 'Remove image' + ) remove[1].props.onPress() expect(onRemoveAttachment).toHaveBeenCalledWith('img-2') }) @@ -497,13 +494,7 @@ describe('MobileNativeChatComposer', () => { }) ) }) - const input = () => - renderer!.root.find((node) => node.type === 'TextInput') as { - props: { - selection?: { start: number; end: number } - onSelectionChange: (e: { nativeEvent: { selection: { end: number } } }) => void - } - } + const input = () => findByHostType(renderer!.root, 'TextInput') // Uncontrolled selection until a suggestion is applied. expect(input().props.selection).toBeUndefined() // Place the caret at the end so the slash trigger is active and suggestions render. @@ -537,13 +528,9 @@ describe('MobileNativeChatComposer', () => { }) ) }) - const input = renderer!.root.find((node) => node.type === 'TextInput') as { - props: { onSelectionChange: (e: { nativeEvent: { selection: { end: number } } }) => void } - } + const input = findByHostType(renderer!.root, 'TextInput') await act(async () => input.props.onSelectionChange({ nativeEvent: { selection: { end: 1 } } })) - const texts = renderer!.root - .findAll((node) => node.type === 'Text') - .map((node) => (node.props as { children?: unknown }).children) + const texts = findAllByHostType(renderer!.root, 'Text').map((node) => node.props.children) // Codex-only commands from the shared catalog, with their description rows — // and none of the old hardcoded provider-agnostic list's phantom entries. expect(texts).toContain('/permissions') @@ -556,9 +543,9 @@ describe('MobileNativeChatComposer', () => { const onMicPressIn = vi.fn() const onMicPressOut = vi.fn() const mic = () => - renderer!.root.find( - (node) => node.type === 'Pressable' && node.props.accessibilityLabel === 'Dictate' - ) as { props: { onPress?: unknown; onPressIn?: unknown; onPressOut?: unknown } } + findAllByHostType(renderer!.root, 'Pressable').find( + (node) => node.props.accessibilityLabel === 'Dictate' + )! await act(async () => { renderer = create( @@ -696,9 +683,7 @@ describe('MobileNativeChatComposer', () => { pendingSend = sendButton().props.onPress() await Promise.resolve() }) - const input = renderer!.root.find((node) => node.type === 'TextInput') as { - props: { onChangeText: (text: string) => void } - } + const input = findByHostType(renderer!.root, 'TextInput') await act(async () => input.props.onChangeText('newer draft')) await act(async () => { resolveSend?.(true) @@ -733,9 +718,7 @@ describe('MobileNativeChatComposer', () => { }) ) }) - const input = renderer!.root.find((node) => node.type === 'TextInput') as { - props: { onSelectionChange: (event: { nativeEvent: { selection: { end: number } } }) => void } - } + const input = findByHostType(renderer!.root, 'TextInput') await act(async () => input.props.onSelectionChange({ nativeEvent: { selection: { end: 2 } } })) let pendingSend!: Promise diff --git a/mobile/src/session/MobileNativeChatFullContent.test.tsx b/mobile/src/session/MobileNativeChatFullContent.test.tsx index 6788b5af2ee..bd0eab004b1 100644 --- a/mobile/src/session/MobileNativeChatFullContent.test.tsx +++ b/mobile/src/session/MobileNativeChatFullContent.test.tsx @@ -6,6 +6,7 @@ import type { RpcClient } from '../transport/rpc-client' import { MobileNativeChatItemSourceContext } from './mobile-native-chat-item-read' import { useMobileNativeChatFullMessage } from './MobileNativeChatFullContent' import { MobileNativeChatUserMessageFold } from './MobileNativeChatUserMessageFold' +import { stubRpcClient } from '../transport/rpc-client-test-stub' vi.mock('react-native', () => ({ Platform: { OS: 'ios' }, @@ -23,10 +24,8 @@ const message: NativeChatMessage = { shortened: true, blocks: [{ type: 'text', text: 'head' }] } -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Only client identity is used; this fixture performs no RPC. -const first = {} as RpcClient -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Only client identity is used; this fixture performs no RPC. -const second = {} as RpcClient +const first = stubRpcClient() +const second = stubRpcClient() let renderer: ReactTestRenderer | undefined afterEach(() => { act(() => renderer?.unmount()) diff --git a/mobile/src/session/MobileNativeChatMessage.test.ts b/mobile/src/session/MobileNativeChatMessage.test.ts index 9c731c34d90..3d54b61afea 100644 --- a/mobile/src/session/MobileNativeChatMessage.test.ts +++ b/mobile/src/session/MobileNativeChatMessage.test.ts @@ -1,4 +1,4 @@ -import { createElement } from 'react' +import { createElement, type ReactNode } from 'react' import { act, create, type ReactTestInstance, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import type { AgentJournalStatusItem } from '../../../src/shared/agent-session-journal-types' @@ -22,7 +22,7 @@ import { styles } from './mobile-native-chat-message-styles' vi.mock('react-native', async () => { const React = await import('react') - const Text = ({ children, ...props }: { children?: unknown }): unknown => + const Text = ({ children, ...props }: { children?: ReactNode }): unknown => React.createElement('Text', props, children) return { ActivityIndicator: 'ActivityIndicator', @@ -38,10 +38,10 @@ vi.mock('react-native', async () => { Image: 'Image', Platform: { OS: 'ios' }, Pressable: 'Pressable', - ScrollView: ({ children, ...props }: { children?: unknown }) => + ScrollView: ({ children, ...props }: { children?: ReactNode }) => React.createElement('ScrollView', props, children), Text, - View: ({ children, ...props }: { children?: unknown }) => + View: ({ children, ...props }: { children?: ReactNode }) => React.createElement('View', props, children), StyleSheet: { create: (styles: unknown) => styles, hairlineWidth: 1 }, useWindowDimensions: () => ({ width: 400, height: 800 }) @@ -79,6 +79,7 @@ vi.mock('./MobileNativeChatMessageActionsSheet', () => ({ })) import { MobileNativeChatMessage } from './MobileNativeChatMessage' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' function userMessage(blocks: NativeChatMessage['blocks']): NativeChatMessage { return { id: 'u1', role: 'user', blocks, timestamp: null, source: 'transcript' } @@ -130,7 +131,7 @@ describe('MobileNativeChatMessage', () => { .findAll((child) => String(child.type) === 'MobileMarkdown') .map((markdown) => markdown.props.content) const textIn = (node: ReactTestInstance): string[] => - node.findAllByType('Text' as never).map((text) => String(text.children.join(''))) + findAllByHostType(node, 'Text').map((text) => String(text.children.join(''))) it('renders the shared clear boundary as one muted divider with prior history retained', () => { const message = projectStructuredItemToNativeChat({ @@ -376,7 +377,7 @@ describe('MobileNativeChatMessage', () => { } }) - it.each([ + it.each([ [ 'an API retry as a warning', statusRow({ @@ -462,6 +463,7 @@ describe('MobileNativeChatMessage', () => { id: 'notice', role, timestamp: 1, + source: 'transcript', blocks: [ { type: 'text', text: 'provider fallback', presentation: 'history-item-too-large' } ] @@ -550,7 +552,7 @@ describe('MobileNativeChatMessage', () => { it('renders a loadable preview URI as an image thumbnail', () => { const tree = render(userMessage([{ type: 'image-ref', url: 'file:///a.jpg', alt: 'a photo' }])) - const image = tree.root.findByType('Image' as never) + const image = findByHostType(tree.root, 'Image') expect(image.props.source).toEqual({ uri: 'file:///a.jpg' }) expect(image.parent?.props).toMatchObject({ accessibilityRole: 'imagebutton', @@ -620,7 +622,7 @@ describe('MobileNativeChatMessage', () => { const tree = render( userMessage([{ type: 'image-ref', url: 'file:///local.jpg', path: '/tmp/host.png' }]) ) - expect(tree.root.findByType('Image' as never).props.source).toEqual({ + expect(findByHostType(tree.root, 'Image').props.source).toEqual({ uri: 'file:///local.jpg' }) }) @@ -628,16 +630,14 @@ describe('MobileNativeChatMessage', () => { it('falls back to a text placeholder for a bare host path', () => { // A host temp path (e.g. on an SSH host) is not loadable on the device. const tree = render(userMessage([{ type: 'image-ref', path: '/tmp/host.png' }])) - expect(tree.root.findAllByType('Image' as never)).toHaveLength(0) - const texts = tree.root - .findAllByType('Text' as never) - .map((node) => String(node.children.join(''))) + expect(findAllByHostType(tree.root, 'Image')).toHaveLength(0) + const texts = findAllByHostType(tree.root, 'Text').map((node) => String(node.children.join(''))) expect(texts.some((text) => text.includes('/tmp/host.png'))).toBe(true) }) it('draws your message as selectable Markdown in the bubble’s dark-on-light colours', () => { const tree = render(userMessage([{ type: 'text', text: '**Fix** `src/a.ts`' }])) - const markdown = tree.root.findByType('MobileMarkdown' as never) + const markdown = findByHostType(tree.root, 'MobileMarkdown') expect(markdown.props).toMatchObject({ content: '**Fix** `src/a.ts`', tone: 'onBright', @@ -648,7 +648,7 @@ describe('MobileNativeChatMessage', () => { it('routes assistant prose through selectable Markdown', () => { const tree = render(toolMessage([{ type: 'text', text: 'Agent reply prose' }])) - const markdown = tree.root.findByType('MobileMarkdown' as never) + const markdown = findByHostType(tree.root, 'MobileMarkdown') expect(markdown.props.content).toBe('Agent reply prose') expect(markdown.props.rangeSelectable).toBe(true) }) @@ -673,7 +673,7 @@ describe('MobileNativeChatMessage', () => { activeTurnIsWorking: true }) expect(textIn(tree.root)).toEqual(expect.arrayContaining(['Running 1 command', 'npm test'])) - expect(tree.root.findAllByType('SquareTerminal' as never)).toHaveLength(1) + expect(findAllByHostType(tree.root, 'SquareTerminal')).toHaveLength(1) // A finished call, and a Codex diff that carries no state, keep the same header. act(() => tree.update( diff --git a/mobile/src/session/MobileNativeChatOverlay.test.ts b/mobile/src/session/MobileNativeChatOverlay.test.ts index 00cc3728880..b3b71a8803e 100644 --- a/mobile/src/session/MobileNativeChatOverlay.test.ts +++ b/mobile/src/session/MobileNativeChatOverlay.test.ts @@ -4,6 +4,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import type { NativeChatMessage } from '../../../src/shared/native-chat-types' import { MobileNativeChatOverlay } from './MobileNativeChatOverlay' import type { MobileNativeChatController } from './use-mobile-native-chat-controller' +import type { MobileImageSource } from '../platform/media-picker-contract' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ StyleSheet: { create: (styles: unknown) => styles, absoluteFillObject: {} }, @@ -40,7 +42,7 @@ type Tick = { structured?: boolean activeTabId?: string | null setTabView?: (tabId: string, view: 'terminal') => void - attachImage?: (source: 'library' | 'camera') => Promise + attachImage?: (source: MobileImageSource) => Promise } function overlayElement(tick: Tick): ReturnType { @@ -123,7 +125,7 @@ describe('MobileNativeChatOverlay streaming gate', () => { /** The bubble text handed to the chat list, or `'hidden'` when chat is off. */ function streaming(): string | null | 'hidden' { - const views = renderer!.root.findAll((node) => node.type === 'ChatView') + const views = findAllByHostType(renderer!.root, 'ChatView') return views.length === 0 ? 'hidden' : (views[0].props.streaming as string | null) } @@ -231,7 +233,7 @@ describe("MobileNativeChatOverlay while a person's Stop ends the turn", () => { await act(async () => { renderer = create(overlayElement(tick)) }) - const view = renderer!.root.find((node) => node.type === 'ChatView') + const view = findByHostType(renderer!.root, 'ChatView') // The tray's second child is the queued box; the first is the child-work strip. return view.props.composerTray?.content?.props?.children?.[1]?.props?.steerHeld } @@ -258,7 +260,7 @@ describe('MobileNativeChatOverlay attach button', () => { await act(async () => { renderer = create(overlayElement({ agent: 'omp', activeTabId: 'tab-omp', setTabView })) }) - const view = renderer!.root.find((node) => node.type === 'ChatView') + const view = findByHostType(renderer!.root, 'ChatView') expect(view.props.askRequiresTerminal).toBe(true) act(() => view.props.onAnswerInTerminal()) expect(setTabView).toHaveBeenCalledExactlyOnceWith('tab-omp', 'terminal') @@ -276,7 +278,7 @@ describe('MobileNativeChatOverlay attach button', () => { }) ) }) - const view = renderer!.root.find((node) => node.type === 'ChatView') + const view = findByHostType(renderer!.root, 'ChatView') expect(view.props.askRequiresTerminal).toBe(scenario === 'missing tab') if (scenario === 'missing tab') { expect(view.props.onAnswerInTerminal).toBeUndefined() @@ -289,7 +291,7 @@ describe('MobileNativeChatOverlay attach button', () => { await act(async () => { renderer = create(overlayElement({ acceptsImages: accepts, attachImage })) }) - const view = renderer!.root.find((node) => node.type === 'ChatView') + const view = findByHostType(renderer!.root, 'ChatView') expect(typeof view.props.onAttachImage === 'function').toBe(accepts) if (accepts) { await act(async () => view.props.onAttachImage()) diff --git a/mobile/src/session/MobileNativeChatPermission.test.ts b/mobile/src/session/MobileNativeChatPermission.test.ts index fc1dd916474..10f2db7cf5d 100644 --- a/mobile/src/session/MobileNativeChatPermission.test.ts +++ b/mobile/src/session/MobileNativeChatPermission.test.ts @@ -3,6 +3,7 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import { MobileNativeChatPermission } from './MobileNativeChatPermission' import type { MobileChatPermission } from './mobile-native-chat-permission' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ Pressable: 'Pressable', @@ -40,7 +41,7 @@ describe('MobileNativeChatPermission', () => { }) ) }) - const button = renderer.root.findByType('Pressable') + const button = findByHostType(renderer!.root, 'Pressable') act(() => { button.props.onPress() @@ -66,7 +67,7 @@ describe('MobileNativeChatPermission', () => { }) ) }) - const cancel = renderer.root.findByProps({ accessibilityLabel: 'Cancel' }) + const cancel = renderer!.root.findByProps({ accessibilityLabel: 'Cancel' }) await act(async () => cancel.props.onPress()) expect(onCancel).toHaveBeenCalledWith({ itemId: 'approval-1', expectedRevision: 4 }) }) @@ -88,11 +89,11 @@ describe('MobileNativeChatPermission', () => { ) }) - const card = renderer.root.findByProps({ testID: 'native-chat-approval-card' }) - const title = renderer.root.findByProps({ testID: 'native-chat-approval-title' }) - const content = renderer.root.findByProps({ testID: 'native-chat-approval-content' }) - const actions = renderer.root.findByProps({ testID: 'native-chat-approval-actions' }) - const contentText = content.findAllByType('Text') + const card = renderer!.root.findByProps({ testID: 'native-chat-approval-card' }) + const title = renderer!.root.findByProps({ testID: 'native-chat-approval-title' }) + const content = renderer!.root.findByProps({ testID: 'native-chat-approval-content' }) + const actions = renderer!.root.findByProps({ testID: 'native-chat-approval-actions' }) + const contentText = findAllByHostType(content, 'Text') const containsText = (value: string): boolean => contentText.some((node) => { const children = Array.isArray(node.props.children) @@ -155,9 +156,8 @@ describe('MobileNativeChatPermission', () => { }) ) }) - const content = renderer.root.findByProps({ testID: 'native-chat-approval-content' }) - return content - .findAllByType('Text') + const content = renderer!.root.findByProps({ testID: 'native-chat-approval-content' }) + return findAllByHostType(content, 'Text') .flatMap((node) => [node.props.children].flat()) .filter((child): child is string => typeof child === 'string') .join('') @@ -179,14 +179,14 @@ describe('MobileNativeChatPermission', () => { ) }) - const content = renderer.root.findByProps({ testID: 'native-chat-approval-content' }) - const actions = renderer.root.findByProps({ testID: 'native-chat-approval-actions' }) + const content = renderer!.root.findByProps({ testID: 'native-chat-approval-content' }) + const actions = renderer!.root.findByProps({ testID: 'native-chat-approval-actions' }) // Same shared region as every other context row, so it inherits the cap. expect(content.props.style).toMatchObject({ maxHeight: 240, minHeight: 0, flexShrink: 1 }) - expect(content.findByType('MobileMarkdown').props.content).toBe(planText) + expect(findByHostType(content, 'MobileMarkdown').props.content).toBe(planText) // The path renders as an interpolated child, so match within the children. - const planFileShown = content.findAllByType('Text').some((node) => { + const planFileShown = findAllByHostType(content, 'Text').some((node) => { const children = Array.isArray(node.props.children) ? node.props.children : [node.props.children] @@ -224,7 +224,7 @@ describe('MobileNativeChatPermission', () => { ) }) const actions = renderer!.root.findByProps({ testID: 'native-chat-approval-actions' }) - return { onRespond, onCancel, options: actions.findAllByType('Pressable') } + return { onRespond, onCancel, options: findAllByHostType(actions, 'Pressable') } } it('with no detail: says so, answers nothing, and only its cancel reaches the host', async () => { diff --git a/mobile/src/session/MobileNativeChatQuestion.test.tsx b/mobile/src/session/MobileNativeChatQuestion.test.tsx index c0499d4fdbc..dd2e122d544 100644 --- a/mobile/src/session/MobileNativeChatQuestion.test.tsx +++ b/mobile/src/session/MobileNativeChatQuestion.test.tsx @@ -2,6 +2,7 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import { MobileNativeChatQuestion } from './MobileNativeChatQuestion' +import { findByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ Pressable: 'Pressable', @@ -46,9 +47,9 @@ describe('MobileNativeChatQuestion', () => { ) }) - const choices = renderer.root.findAllByProps({ accessibilityRole: 'checkbox' }) + const choices = renderer!.root.findAllByProps({ accessibilityRole: 'checkbox' }) await act(async () => choices[1]!.props.onPress()) - const submit = renderer.root.findByProps({ accessibilityLabel: 'Submit selected options' }) + const submit = renderer!.root.findByProps({ accessibilityLabel: 'Submit selected options' }) await act(async () => submit.props.onPress()) expect(onAnswer).toHaveBeenCalledWith('second-token') @@ -72,7 +73,7 @@ describe('MobileNativeChatQuestion', () => { ) }) - const choices = renderer.root.findAllByProps({ accessibilityRole: 'button' }) + const choices = renderer!.root.findAllByProps({ accessibilityRole: 'button' }) await act(async () => choices[1]!.props.onPress()) expect(onAnswer).toHaveBeenCalledWith('Choice') @@ -97,11 +98,11 @@ describe('MobileNativeChatQuestion', () => { ) }) - const choices = renderer.root.findAllByProps({ accessibilityRole: 'checkbox' }) + const choices = renderer!.root.findAllByProps({ accessibilityRole: 'checkbox' }) await act(async () => choices[0]!.props.onPress()) - const input = renderer.root.findByType('TextInput') + const input = findByHostType(renderer!.root, 'TextInput') await act(async () => input.props.onChangeText('ap-south')) - const submit = renderer.root.findByProps({ accessibilityLabel: 'Submit selected options' }) + const submit = renderer!.root.findByProps({ accessibilityLabel: 'Submit selected options' }) await act(async () => submit.props.onPress()) expect(onAnswer).toHaveBeenCalledWith('east-token, other-token:ap-south') @@ -125,7 +126,7 @@ describe('MobileNativeChatQuestion', () => { }) ) }) - const cancel = renderer.root.findByProps({ accessibilityLabel: 'Cancel' }) + const cancel = renderer!.root.findByProps({ accessibilityLabel: 'Cancel' }) await act(async () => cancel.props.onPress()) expect(onCancel).toHaveBeenCalledWith({ itemId: 'question-1', expectedRevision: 7 }) }) @@ -153,14 +154,14 @@ describe('MobileNativeChatQuestion', () => { ) }) - const input = renderer.root.findByType('TextInput') + const input = findByHostType(renderer!.root, 'TextInput') expect(input.props).toMatchObject({ value: ' draft\n', placeholder: 'Write here', multiline: true }) await act(async () => input.props.onChangeText(' \n ')) - const send = renderer.root.findByProps({ accessibilityLabel: 'Send reply' }) + const send = renderer!.root.findByProps({ accessibilityLabel: 'Send reply' }) expect(send.props.disabled).toBe(false) await act(async () => send.props.onPress()) expect(onAnswer).toHaveBeenCalledWith(`editor-token:${encodeURIComponent(' \n ')}`) @@ -184,16 +185,16 @@ describe('MobileNativeChatQuestion', () => { }) ) }) - const send = renderer.root.findByProps({ accessibilityLabel: 'Send reply' }) - expect(renderer.root.findByType('TextInput').props.multiline).toBe(false) + const send = renderer!.root.findByProps({ accessibilityLabel: 'Send reply' }) + expect(findByHostType(renderer!.root, 'TextInput').props.multiline).toBe(false) expect(send.props.disabled).toBe(false) await act(async () => send.props.onPress()) expect(onAnswer).toHaveBeenCalledWith('input-token:') await act(async () => - renderer.update(createElement(MobileNativeChatQuestion, { question, onAnswer })) + renderer!.update(createElement(MobileNativeChatQuestion, { question, onAnswer })) ) - expect(renderer.root.findByProps({ accessibilityLabel: 'Send reply' }).props.disabled).toBe( + expect(renderer!.root.findByProps({ accessibilityLabel: 'Send reply' }).props.disabled).toBe( true ) }) diff --git a/mobile/src/session/MobileNativeChatSessionOptionPickers.test.ts b/mobile/src/session/MobileNativeChatSessionOptionPickers.test.ts index b30edd0347b..bf269df4973 100644 --- a/mobile/src/session/MobileNativeChatSessionOptionPickers.test.ts +++ b/mobile/src/session/MobileNativeChatSessionOptionPickers.test.ts @@ -1,14 +1,18 @@ -import { createElement, useState } from 'react' +import { createElement, useState, type ReactNode } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { AccessibilityInfo, Platform } from 'react-native' import { colors } from '../theme/mobile-theme' -import type { SessionOptionDescriptor } from '../../../src/shared/native-chat-session-options' +import type { + SessionOptionDescriptor, + SessionOptionSelectChoice +} from '../../../src/shared/native-chat-session-options' import { MobileNativeChatSessionOptionPickers, type MobileNativeChatPickerPresentation } from './MobileNativeChatSessionOptionPickers' import type { MobileNativeChatSessionOptionsController } from './use-mobile-native-chat-session-options' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ AccessibilityInfo: { announceForAccessibilityWithOptions: vi.fn() }, @@ -36,7 +40,7 @@ vi.mock('lucide-react-native', () => ({ vi.mock('../components/BottomDrawer', async () => { const React = await import('react') return { - BottomDrawer: ({ visible, children }: { visible: boolean; children?: unknown }) => + BottomDrawer: ({ visible, children }: { visible: boolean; children?: ReactNode }) => visible ? React.createElement('BottomDrawer', { visible }, children) : null } }) @@ -118,65 +122,30 @@ describe('MobileNativeChatSessionOptionPickers', () => { } } - const pill = ( - name: string - ): { - props: { - onPress: () => void - accessibilityLabel?: string - disabled?: boolean - accessibilityRole?: string - accessibilityState?: { disabled?: boolean } - } - } => + const pill = (name: string) => renderer!.root.find( (node) => - node.type === 'Pressable' && + String(node.type) === 'Pressable' && typeof node.props.accessibilityLabel === 'string' && node.props.accessibilityLabel.startsWith(name) - ) as { - props: { - onPress: () => void - accessibilityLabel?: string - disabled?: boolean - accessibilityRole?: string - accessibilityState?: { disabled?: boolean } - } - } + ) - const rowByText = ( - text: string - ): { - props: { - onPress: () => void - accessibilityRole?: string - accessibilityLabel?: string - accessibilityState?: { checked?: boolean; disabled?: boolean } - } - } => { + const rowByText = (text: string) => { // The sheet's rows only: the pill names the model too. - const label = renderer!.root - .findByType('BottomDrawer') - .findAll((node) => node.type === 'Text') - .find((node) => (node.props as { children?: unknown }).children === text) + const label = findAllByHostType(findByHostType(renderer!.root, 'BottomDrawer'), 'Text').find( + (node) => node.props.children === text + ) if (!label) { throw new Error(`No row labeled ${text}`) } let parent = label.parent - while (parent && parent.type !== 'Pressable') { + while (parent && String(parent.type) !== 'Pressable') { parent = parent.parent } if (!parent) { throw new Error(`No pressable row for ${text}`) } - return parent as unknown as { - props: { - onPress: () => void - accessibilityRole?: string - accessibilityLabel?: string - accessibilityState?: { checked?: boolean; disabled?: boolean } - } - } + return parent } beforeEach(() => { @@ -210,13 +179,15 @@ describe('MobileNativeChatSessionOptionPickers', () => { it('lets the pill shrink so a long label never pushes the composer buttons off the row', () => { mount([MODEL_DESCRIPTOR, EFFORT_DESCRIPTOR]) // Style-level only: the test renderer runs no layout. - expect(renderer!.root.findAllByType('View')[0]!.props.style).toMatchObject({ flexShrink: 1 }) + expect(findAllByHostType(renderer!.root, 'View')[0]!.props.style).toMatchObject({ + flexShrink: 1 + }) }) it('opens the model sheet and applies a picked model', async () => { mount([MODEL_DESCRIPTOR, EFFORT_DESCRIPTOR]) await act(async () => pill('Model').props.onPress()) - expect(renderer!.root.findByType('BottomDrawer').props.visible).toBe(true) + expect(findByHostType(renderer!.root, 'BottomDrawer').props.visible).toBe(true) await act(async () => rowByText('Opus 4.8').props.onPress()) expect(setOption).toHaveBeenCalledWith('model', 'opus') }) @@ -293,12 +264,9 @@ describe('MobileNativeChatSessionOptionPickers', () => { { ...EFFORT_DESCRIPTOR, valueSource: 'dispatched', transport: scenario.transport } ]) await act(async () => pill('Model').props.onPress()) - const captions = renderer!.root - .findAll((node) => node.type === 'Text') - .filter( - (node) => - (node.props as { children?: unknown }).children === 'Sent to the agent — not confirmed' - ) + const captions = findAllByHostType(renderer!.root, 'Text').filter( + (node) => node.props.children === 'Sent to the agent — not confirmed' + ) expect(captions.length > 0).toBe(scenario.caption) }) @@ -308,13 +276,9 @@ describe('MobileNativeChatSessionOptionPickers', () => { mount([MODEL_DESCRIPTOR, { ...EFFORT_DESCRIPTOR, valueSource: 'reported', transport }]) await act(async () => pill('Model').props.onPress()) expect( - renderer!.root - .findAll((node) => node.type === 'Text') - .some( - (node) => - (node.props as { children?: unknown }).children === - 'Sent to the agent — not confirmed' - ) + findAllByHostType(renderer!.root, 'Text').some( + (node) => node.props.children === 'Sent to the agent — not confirmed' + ) ).toBe(false) } ) @@ -375,7 +339,7 @@ describe('MobileNativeChatSessionOptionPickers', () => { MODEL_DESCRIPTOR, { ...PERMISSION_DESCRIPTOR, - kind: { ...PERMISSION_DESCRIPTOR.kind, currentValue: 'full-access' } + kind: { type: 'select', currentValue: 'full-access', choices: PERMISSION_CHOICES } } ]) expect(pill('Model').props.accessibilityLabel).toBe('Model, Sonnet 5, full access') @@ -405,13 +369,13 @@ describe('MobileNativeChatSessionOptionPickers', () => { accessibilityLabel: `Permissions, ${label}`, disabled: false }) - expect(renderer!.root.findAllByType('Text')).toHaveLength(icon ? 0 : 2) - expect(renderer!.root.findAllByType('ShieldAlert')).toHaveLength( + expect(findAllByHostType(renderer!.root, 'Text')).toHaveLength(icon ? 0 : 2) + expect(findAllByHostType(renderer!.root, 'ShieldAlert')).toHaveLength( value === 'full-access' ? 1 : 0 ) if (icon) { - expect(renderer!.root.findAllByType(icon)).toHaveLength(1) - expect(renderer!.root.findByType(icon).props.color).toBe( + expect(findAllByHostType(renderer!.root, icon)).toHaveLength(1) + expect(findByHostType(renderer!.root, icon).props.color).toBe( value === 'full-access' ? colors.statusAmber : colors.textSecondary ) } @@ -420,23 +384,21 @@ describe('MobileNativeChatSessionOptionPickers', () => { expect(rowByText(nextLabel).props.accessibilityRole).toBe('radio') await act(async () => rowByText(nextLabel).props.onPress()) expect(setOption).toHaveBeenCalledWith('permissionMode', nextValue) - expect(renderer!.root.findAllByType('BottomDrawer')).toEqual([]) + expect(findAllByHostType(renderer!.root, 'BottomDrawer')).toEqual([]) } ) }) +const PERMISSION_CHOICES: SessionOptionSelectChoice[] = [ + { value: 'ask', label: 'Ask for approval' }, + { value: 'full-access', label: 'Full access' } +] + const PERMISSION_DESCRIPTOR: SessionOptionDescriptor = { id: 'permissionMode', label: 'Permissions', category: 'mode', - kind: { - type: 'select', - currentValue: 'ask', - choices: [ - { value: 'ask', label: 'Ask for approval' }, - { value: 'full-access', label: 'Full access' } - ] - }, + kind: { type: 'select', currentValue: 'ask', choices: PERMISSION_CHOICES }, valueSource: 'applied', transport: 'agent-session', settable: true diff --git a/mobile/src/session/MobileNativeChatSessionOptionRows.test.ts b/mobile/src/session/MobileNativeChatSessionOptionRows.test.ts index 241b1a6434c..025997b6125 100644 --- a/mobile/src/session/MobileNativeChatSessionOptionRows.test.ts +++ b/mobile/src/session/MobileNativeChatSessionOptionRows.test.ts @@ -3,6 +3,7 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { describe, expect, it, vi } from 'vitest' import type { SessionOptionDescriptor } from '../../../src/shared/native-chat-session-options' import { DescriptorRows } from './MobileNativeChatSessionOptionRows' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ Pressable: 'Pressable', @@ -46,7 +47,7 @@ function renderRows(descriptor: SessionOptionDescriptor): ReactTestRenderer { } const textOf = (renderer: ReactTestRenderer): string[] => - renderer.root.findAllByType('Text').flatMap((node) => { + findAllByHostType(renderer.root, 'Text').flatMap((node) => { const children = node.props.children return typeof children === 'string' ? [children] : [] }) @@ -54,7 +55,7 @@ const textOf = (renderer: ReactTestRenderer): string[] => describe('DescriptorRows boolean', () => { it('renders one switch and no unknown-value caption', () => { const renderer = renderRows({ ...FAST, valueSource: 'unknown' }) - expect(renderer.root.findAllByType('Switch')).toHaveLength(1) + expect(findAllByHostType(renderer.root, 'Switch')).toHaveLength(1) expect(textOf(renderer)).not.toContain('Current value unknown') }) @@ -80,7 +81,7 @@ describe('DescriptorRows boolean', () => { expect(textOf(renderer)).toContain(shown) expect(textOf(renderer)).not.toContain(hidden) // The marker qualifies the value; it must not become part of the control's name. - expect(renderer.root.findByType('Switch').props.accessibilityLabel).toBe('Fast mode') + expect(findByHostType(renderer.root, 'Switch').props.accessibilityLabel).toBe('Fast mode') }) it('drops the marker once something has picked the value', () => { @@ -95,6 +96,6 @@ describe('DescriptorRows boolean', () => { kind: { type: 'boolean', currentValue: true }, valueSource: 'unknown' }) - expect(renderer.root.findByType('Switch').props.value).toBe(true) + expect(findByHostType(renderer.root, 'Switch').props.value).toBe(true) }) }) diff --git a/mobile/src/session/MobileNativeChatTurnStatus.test.ts b/mobile/src/session/MobileNativeChatTurnStatus.test.ts index 2506ab6e311..c87e174b39b 100644 --- a/mobile/src/session/MobileNativeChatTurnStatus.test.ts +++ b/mobile/src/session/MobileNativeChatTurnStatus.test.ts @@ -1,18 +1,18 @@ -import { createElement } from 'react' +import { createElement, type ReactNode } from 'react' import { act, create, type ReactTestInstance, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('react-native', async () => { const React = await import('react') - const Text = ({ children, ...props }: { children?: unknown }): unknown => + const Text = ({ children, ...props }: { children?: ReactNode }): unknown => React.createElement('Text', props, children) return { ActivityIndicator: (props: Record) => React.createElement('ActivityIndicator', props), - Pressable: ({ children, ...props }: { children?: unknown }) => + Pressable: ({ children, ...props }: { children?: ReactNode }) => React.createElement('Pressable', props, children), Text, - View: ({ children, ...props }: { children?: unknown }) => + View: ({ children, ...props }: { children?: ReactNode }) => React.createElement('View', props, children), StyleSheet: { create: (styles: unknown) => styles, hairlineWidth: 1 } } @@ -20,12 +20,13 @@ vi.mock('react-native', async () => { vi.mock('lucide-react-native', () => ({ ChevronRight: 'ChevronRight' })) import { MobileNativeChatTurnStatus } from './MobileNativeChatTurnStatus' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' const labels = (node: ReactTestInstance): string[] => - node.findAllByType('Text' as never).map((text) => String(text.children.join(''))) + findAllByHostType(node, 'Text').map((text) => String(text.children.join(''))) const spinners = (node: ReactTestInstance): ReactTestInstance[] => - node.findAllByType('ActivityIndicator' as never) + findAllByHostType(node, 'ActivityIndicator') let renderer: ReactTestRenderer | null = null @@ -68,7 +69,7 @@ describe('MobileNativeChatTurnStatus', () => { const onToggleExpanded = vi.fn() const tree = render({ startedAt: Date.now(), workedSeconds: 184, onToggleExpanded }) expect(labels(tree.root)).toEqual(['Worked for 3m 4s']) - const button = tree.root.findByType('Pressable' as never) + const button = findByHostType(tree.root, 'Pressable') expect(button.props.accessibilityLabel).toBe('Toggle turn details') expect(button.props.accessibilityState).toEqual({ expanded: false }) act(() => button.props.onPress()) @@ -96,7 +97,7 @@ describe('MobileNativeChatTurnStatus', () => { it('stays a plain row when the settled turn has nothing to disclose', () => { const tree = render({ startedAt: Date.now(), workedSeconds: 5 }) - expect(tree.root.findAllByType('Pressable' as never)).toHaveLength(0) + expect(findAllByHostType(tree.root, 'Pressable')).toHaveLength(0) expect(labels(tree.root)).toEqual(['Worked for 5s']) }) diff --git a/mobile/src/session/QuickCommandsList.test.ts b/mobile/src/session/QuickCommandsList.test.ts index 7b49006c517..9d15638fdad 100644 --- a/mobile/src/session/QuickCommandsList.test.ts +++ b/mobile/src/session/QuickCommandsList.test.ts @@ -2,6 +2,7 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import { QUICK_COMMAND_SEARCH_QUERY_MAX_LENGTH, QuickCommandsList } from './QuickCommandsList' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ ActivityIndicator: 'ActivityIndicator', @@ -66,7 +67,7 @@ describe('QuickCommandsList search', () => { ) }) - const search = renderer!.root.findByType('TextInput') + const search = findByHostType(renderer!.root, 'TextInput') expect(search.props.value).toBe('missing') expect(search.props.maxLength).toBe(QUICK_COMMAND_SEARCH_QUERY_MAX_LENGTH) act(() => search.props.onChangeText('')) @@ -93,6 +94,6 @@ describe('QuickCommandsList search', () => { ) }) - expect(renderer!.root.findAllByType('TextInput')).toHaveLength(0) + expect(findAllByHostType(renderer!.root, 'TextInput')).toHaveLength(0) }) }) diff --git a/mobile/src/session/QuickCommandsSheet.test.ts b/mobile/src/session/QuickCommandsSheet.test.ts index b54b2cdd74d..0825cc8bdbc 100644 --- a/mobile/src/session/QuickCommandsSheet.test.ts +++ b/mobile/src/session/QuickCommandsSheet.test.ts @@ -2,10 +2,11 @@ import { createElement, Fragment, startTransition, Suspense, type ReactNode } fr import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types' -import type { RpcClient } from '../transport/rpc-client' import { QuickCommandsSheet } from './QuickCommandsSheet' import { createStableLogicalRpcClient } from '../transport/stable-logical-rpc-client' import { FakeSession } from '../transport/mobile-endpoint-supervisor-test-fakes' +import { stubRpcClient } from '../transport/rpc-client-test-stub' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' const mocks = vi.hoisted(() => ({ alert: vi.fn(), @@ -94,7 +95,7 @@ describe('QuickCommandsSheet', () => { createElement(QuickCommandsSheet, { visible: true, onClose, - client: {} as RpcClient, + client: stubRpcClient(), repoId: 'repo-1', repoName: 'Repo', agentPromptMaxLength: null, @@ -103,7 +104,7 @@ describe('QuickCommandsSheet', () => { ) }) - act(() => renderer!.root.findByType(quickCommandsList).props.onLaunch(command)) + act(() => findByHostType(renderer!.root, quickCommandsList).props.onLaunch(command)) expect(onLaunch).toHaveBeenCalledWith(command) expect(onClose).not.toHaveBeenCalled() @@ -117,7 +118,7 @@ describe('QuickCommandsSheet', () => { createElement(QuickCommandsSheet, { visible: true, onClose: vi.fn(), - client: {} as RpcClient, + client: stubRpcClient(), repoId: 'repo-1', repoName: 'Repo', agentPromptMaxLength: null, @@ -126,13 +127,13 @@ describe('QuickCommandsSheet', () => { ) }) - act(() => renderer!.root.findByType(quickCommandsList).props.onAdd()) - const editor = renderer!.root.findByType(quickCommandEditorForm) + act(() => findByHostType(renderer!.root, quickCommandsList).props.onAdd()) + const editor = findByHostType(renderer!.root, quickCommandEditorForm) act(() => { editor.props.onChange({ label: 'Test' }) editor.props.onChange({ command: 'pnpm test' }) }) - const readyEditor = renderer!.root.findByType(quickCommandEditorForm) + const readyEditor = findByHostType(renderer!.root, quickCommandEditorForm) act(() => { readyEditor.props.onSave() readyEditor.props.onSave() @@ -172,14 +173,14 @@ describe('QuickCommandsSheet', () => { act(() => { renderer = create(render()) }) - act(() => renderer!.root.findByType(quickCommandsList).props.onAdd()) + act(() => findByHostType(renderer!.root, quickCommandsList).props.onAdd()) act(() => - renderer!.root.findByType(quickCommandEditorForm).props.onChange({ + findByHostType(renderer!.root, quickCommandEditorForm).props.onChange({ label: 'Test', command: 'pnpm test' }) ) - act(() => renderer!.root.findByType(quickCommandEditorForm).props.onSave()) + act(() => findByHostType(renderer!.root, quickCommandEditorForm).props.onSave()) suspendedRender.mockClear() act(() => { startTransition(() => renderer?.update(render(true))) @@ -189,8 +190,8 @@ describe('QuickCommandsSheet', () => { save.resolve(true) await save.promise }) - expect(renderer!.root.findAllByType(quickCommandEditorForm)).toHaveLength(0) - expect(renderer!.root.findAllByType(quickCommandsList)).toHaveLength(1) + expect(findAllByHostType(renderer!.root, quickCommandEditorForm)).toHaveLength(0) + expect(findAllByHostType(renderer!.root, quickCommandsList)).toHaveLength(1) }) it.each(['new editor', 'newer edits'])( @@ -211,30 +212,34 @@ describe('QuickCommandsSheet', () => { }) ) }) - act(() => renderer!.root.findByType(quickCommandsList).props.onAdd()) + act(() => findByHostType(renderer!.root, quickCommandsList).props.onAdd()) act(() => - renderer!.root - .findByType(quickCommandEditorForm) - .props.onChange({ label: 'First', command: 'first' }) + findByHostType(renderer!.root, quickCommandEditorForm).props.onChange({ + label: 'First', + command: 'first' + }) ) - act(() => renderer!.root.findByType(quickCommandEditorForm).props.onSave()) + act(() => findByHostType(renderer!.root, quickCommandEditorForm).props.onSave()) if (change === 'new editor') { - act(() => renderer!.root.findByType(quickCommandEditorForm).props.onCancel()) - act(() => renderer!.root.findByType(quickCommandsList).props.onAdd()) + act(() => findByHostType(renderer!.root, quickCommandEditorForm).props.onCancel()) + act(() => findByHostType(renderer!.root, quickCommandsList).props.onAdd()) } act(() => - renderer!.root - .findByType(quickCommandEditorForm) - .props.onChange({ label: 'Second', command: 'second' }) + findByHostType(renderer!.root, quickCommandEditorForm).props.onChange({ + label: 'Second', + command: 'second' + }) ) await act(async () => { save.resolve(true) await save.promise }) - expect(renderer!.root.findAllByType(quickCommandEditorForm)).toHaveLength(1) - expect(renderer!.root.findByType(quickCommandEditorForm).props.draft.label).toBe('Second') + expect(findAllByHostType(renderer!.root, quickCommandEditorForm)).toHaveLength(1) + expect(findByHostType(renderer!.root, quickCommandEditorForm).props.draft.label).toBe( + 'Second' + ) if (change === 'newer edits') { - act(() => renderer!.root.findByType(quickCommandEditorForm).props.onSave()) + act(() => findByHostType(renderer!.root, quickCommandEditorForm).props.onSave()) expect(mocks.persist.mock.calls[1]?.[0].command).toMatchObject({ id: mocks.persist.mock.calls[0]?.[0].command.id, label: 'Second', @@ -260,15 +265,15 @@ describe('QuickCommandsSheet', () => { act(() => { renderer = create(createElement(QuickCommandsSheet, props)) }) - act(() => renderer!.root.findByType(quickCommandsList).props.onAdd()) + act(() => findByHostType(renderer!.root, quickCommandsList).props.onAdd()) act(() => - renderer!.root - .findByType(quickCommandEditorForm) - .props.onChange({ label: 'First', command: 'first' }) + findByHostType(renderer!.root, quickCommandEditorForm).props.onChange({ + label: 'First', + command: 'first' + }) ) - act(() => renderer!.root.findByType(quickCommandEditorForm).props.onSave()) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The settings hook is mocked; this client is used only as an identity by the sheet. - const nextClient = {} as RpcClient + act(() => findByHostType(renderer!.root, quickCommandEditorForm).props.onSave()) + const nextClient = stubRpcClient() act(() => renderer!.update( createElement(QuickCommandsSheet, { @@ -278,19 +283,20 @@ describe('QuickCommandsSheet', () => { }) ) ) - act(() => renderer!.root.findByType(quickCommandsList).props.onAdd()) + act(() => findByHostType(renderer!.root, quickCommandsList).props.onAdd()) act(() => - renderer!.root - .findByType(quickCommandEditorForm) - .props.onChange({ label: 'Second', command: 'second' }) + findByHostType(renderer!.root, quickCommandEditorForm).props.onChange({ + label: 'Second', + command: 'second' + }) ) - act(() => renderer!.root.findByType(quickCommandEditorForm).props.onSave()) + act(() => findByHostType(renderer!.root, quickCommandEditorForm).props.onSave()) expect(mocks.persist).toHaveBeenCalledTimes(2) await act(async () => { first.resolve(true) await first.promise }) - const editor = renderer!.root.findByType(quickCommandEditorForm) + const editor = findByHostType(renderer!.root, quickCommandEditorForm) expect(editor.props.draft.label).toBe('Second') expect(editor.props.saving).toBe(true) act(() => editor.props.onSave()) @@ -299,7 +305,7 @@ describe('QuickCommandsSheet', () => { second.resolve(true) await second.promise }) - expect(renderer!.root.findAllByType(quickCommandEditorForm)).toHaveLength(0) + expect(findAllByHostType(renderer!.root, quickCommandEditorForm)).toHaveLength(0) }) it('keeps newer edits and the pending save when the same computer changes connection paths', async () => { @@ -318,8 +324,8 @@ describe('QuickCommandsSheet', () => { act(() => { renderer = create(createElement(QuickCommandsSheet, props)) }) - act(() => renderer!.root.findByType(quickCommandsList).props.onAdd()) - const editor = () => renderer!.root.findByType(quickCommandEditorForm) + act(() => findByHostType(renderer!.root, quickCommandsList).props.onAdd()) + const editor = () => findByHostType(renderer!.root, quickCommandEditorForm) act(() => editor().props.onChange({ label: 'First', command: 'first' })) act(() => editor().props.onSave()) act(() => editor().props.onChange({ label: 'Newer', command: 'newer' })) @@ -351,7 +357,7 @@ describe('QuickCommandsSheet', () => { createElement(QuickCommandsSheet, { visible: true, onClose: vi.fn(), - client: {} as RpcClient, + client: stubRpcClient(), repoId: 'repo-1', repoName: 'Repo', agentPromptMaxLength: null, @@ -360,9 +366,9 @@ describe('QuickCommandsSheet', () => { ) }) - const list = renderer!.root.findByType(quickCommandsList) + const list = findByHostType(renderer!.root, quickCommandsList) act(() => list.props.onAdd()) - const editor = () => renderer!.root.findByType(quickCommandEditorForm) + const editor = () => findByHostType(renderer!.root, quickCommandEditorForm) act(() => editor().props.onChange({ label: 'Another command', command: 'echo saved' })) mocks.persist.mockResolvedValue(true) await act(async () => editor().props.onSave()) @@ -370,7 +376,7 @@ describe('QuickCommandsSheet', () => { type: 'upsert', command: expect.objectContaining({ label: 'Another command', command: 'echo saved' }) }) - expect(renderer!.root.findAllByType(quickCommandEditorForm)).toHaveLength(0) + expect(findAllByHostType(renderer!.root, quickCommandEditorForm)).toHaveLength(0) }) it('shows only global commands and defaults new commands to global without a repo', async () => { @@ -385,7 +391,7 @@ describe('QuickCommandsSheet', () => { createElement(QuickCommandsSheet, { visible: true, onClose: vi.fn(), - client: {} as RpcClient, + client: stubRpcClient(), repoId: null, repoName: 'Folder workspace', agentPromptMaxLength: null, @@ -394,11 +400,11 @@ describe('QuickCommandsSheet', () => { ) }) - const list = renderer!.root.findByType(quickCommandsList) + const list = findByHostType(renderer!.root, quickCommandsList) expect(list.props.globalCommands).toEqual([command]) expect(list.props.repoCommands).toEqual([]) act(() => list.props.onAdd()) - expect(renderer!.root.findByType(quickCommandEditorForm).props.draft.scope).toEqual({ + expect(findByHostType(renderer!.root, quickCommandEditorForm).props.draft.scope).toEqual({ type: 'global' }) }) @@ -409,7 +415,7 @@ describe('QuickCommandsSheet', () => { createElement(QuickCommandsSheet, { visible: true, onClose: vi.fn(), - client: {} as RpcClient, + client: stubRpcClient(), repoId: 'repo-1', repoName: 'Repo', agentPromptMaxLength: null, @@ -418,7 +424,7 @@ describe('QuickCommandsSheet', () => { ) }) - act(() => renderer!.root.findByType(quickCommandsList).props.onDelete(command)) + act(() => findByHostType(renderer!.root, quickCommandsList).props.onDelete(command)) expect(mocks.persist).not.toHaveBeenCalled() const actions = mocks.alert.mock.calls[0]?.[2] as diff --git a/mobile/src/session/ai-vault-resume-launch.test.ts b/mobile/src/session/ai-vault-resume-launch.test.ts index 915e9eb3be3..aea759dd28d 100644 --- a/mobile/src/session/ai-vault-resume-launch.test.ts +++ b/mobile/src/session/ai-vault-resume-launch.test.ts @@ -345,10 +345,7 @@ describe('resumeAiVaultSessionInTerminal', () => { agentEnv: { ANTHROPIC_BASE_URL: 'http://localhost:3000' } }, launchAgent: 'claude', - clientMutationId: 'resume-1', - activate: false, - select: true, - navigation: 'caller' + clientMutationId: 'resume-1' }) ).resolves.toMatchObject({ id: 'tab-1', terminal: 'pty-1' }) expect(sendRequest).toHaveBeenNthCalledWith( diff --git a/mobile/src/session/mobile-file-search-owner-cleanup.test.tsx b/mobile/src/session/mobile-file-search-owner-cleanup.test.tsx index 91aa30fbc57..3087ecf3bbc 100644 --- a/mobile/src/session/mobile-file-search-owner-cleanup.test.tsx +++ b/mobile/src/session/mobile-file-search-owner-cleanup.test.tsx @@ -4,6 +4,7 @@ import { afterEach, beforeEach, expect, it, vi } from 'vitest' import type { RpcClient } from '../transport/rpc-client' import { MobileNativeChatComposer } from './MobileNativeChatComposer' import { useMobileNativeChatFileSearch } from './use-mobile-native-chat-file-search' +import { stubRpcClient } from '../transport/rpc-client-test-stub' vi.mock('react-native', async () => { const React = await import('react') @@ -75,8 +76,7 @@ function pendingReply() { return { promise, resolve } } function mount(sendRequest: RpcClient['sendRequest'], strict = false, getGeneration = () => 1) { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The actual hook only reads these two RPC members at the fake transport boundary. - const client = { sendRequest, getGeneration } as RpcClient + const client = stubRpcClient({ sendRequest, getGeneration }) let search: ReturnType | undefined function Route({ showChat }: { showChat: boolean }) { search = useMobileNativeChatFileSearch({ client, worktreeId: 'folder:remote' }) diff --git a/mobile/src/session/mobile-file-tap-open.test.ts b/mobile/src/session/mobile-file-tap-open.test.ts index ba90a33d1eb..a6c5a72b8b5 100644 --- a/mobile/src/session/mobile-file-tap-open.test.ts +++ b/mobile/src/session/mobile-file-tap-open.test.ts @@ -1,13 +1,23 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { RpcResponse } from '../transport/types' +import type { UnvalidatedRpcRequestPort } from '../transport/unvalidated-rpc-request-port' import { openMobileFileTap } from './mobile-file-tap-open' -function ok(result: unknown) { - return { ok: true, result, _meta: { runtimeId: 'runtime-1' } } +type SendRequest = UnvalidatedRpcRequestPort['sendRequest'] + +function ok(result: unknown): RpcResponse { + return { id: 'rpc', ok: true, result, _meta: { runtimeId: 'runtime-1' } } } -function createClient(responses: unknown[]) { +function createClient(responses: RpcResponse[]) { return { - sendRequest: vi.fn(async () => responses.shift()) + sendRequest: vi.fn(async (method) => { + const response = responses.shift() + if (!response) { + throw new Error(`no scripted response for ${method}`) + } + return response + }) } } @@ -394,9 +404,9 @@ describe('openMobileFileTap', () => { }) it('does not navigate an absolute artifact after the user leaves the source terminal', async () => { - let resolveRequest: (value: unknown) => void = () => {} + let resolveRequest: (value: RpcResponse) => void = () => {} const client = { - sendRequest: vi.fn( + sendRequest: vi.fn( () => new Promise((resolve) => { resolveRequest = resolve @@ -465,7 +475,7 @@ describe('openMobileFileTap', () => { absolutePath: '/repo/src/index.ts' } }), - { ok: false, error: { message: 'nope' } } + { id: 'rpc', ok: false, error: { code: 'runtime_error', message: 'nope' } } ]) const onOpenFailed = vi.fn() @@ -676,7 +686,7 @@ describe('openMobileFileTap', () => { async function tapOnHostWithNoDesktopWindow(options: { leaveDuringOpen: boolean }) { let latestActivationSeq = 1 const client = { - sendRequest: vi.fn(async (method: string) => { + sendRequest: vi.fn(async (method) => { if (method !== 'files.open') { return ok({ worktree: 'wt-1', @@ -695,7 +705,11 @@ describe('openMobileFileTap', () => { if (options.leaveDuringOpen) { latestActivationSeq = 2 } - return { ok: false, error: { code: 'runtime_error', message: 'renderer_unavailable' } } + return { + id: 'rpc', + ok: false, + error: { code: 'runtime_error', message: 'renderer_unavailable' } + } }) } const pushPreviewRoute = vi.fn() diff --git a/mobile/src/session/mobile-native-chat-open-file.test.ts b/mobile/src/session/mobile-native-chat-open-file.test.ts index 7a75f690148..97924b648c1 100644 --- a/mobile/src/session/mobile-native-chat-open-file.test.ts +++ b/mobile/src/session/mobile-native-chat-open-file.test.ts @@ -1,8 +1,10 @@ import { describe, expect, it, vi } from 'vitest' +import type { RpcResponse } from '../transport/types' +import type { UnvalidatedRpcRequestPort } from '../transport/unvalidated-rpc-request-port' import { openMobileNativeChatFileTap } from './mobile-native-chat-open-file' -function ok(result: unknown) { - return { ok: true, result, _meta: { runtimeId: 'runtime-1' } } +function ok(result: unknown): RpcResponse { + return { id: 'rpc', ok: true, result, _meta: { runtimeId: 'runtime-1' } } } function activationState(activated: boolean) { @@ -16,7 +18,7 @@ function activationState(activated: boolean) { } } -function baseOptions(client: { sendRequest: ReturnType }) { +function baseOptions(client: UnvalidatedRpcRequestPort) { return { client, hostId: 'host-1', @@ -173,8 +175,14 @@ describe('openMobileNativeChatFileTap', () => { }) it('opens a plain path through files.open with tab activation', async () => { - const responses: unknown[] = [worktreeFileResolution('src/app.ts'), ok({ opened: true })] - const sendRequest = vi.fn(async () => responses.shift()) + const responses = [worktreeFileResolution('src/app.ts'), ok({ opened: true })] + const sendRequest = vi.fn(async (method: string) => { + const response = responses.shift() + if (!response) { + throw new Error(`no scripted response for ${method}`) + } + return response + }) const openedTab = { id: 'tab-2', relativePath: 'src/app.ts' } const switchSessionTab = vi.fn() const options = { diff --git a/mobile/src/session/mobile-new-tab-agent-loader.ts b/mobile/src/session/mobile-new-tab-agent-loader.ts index 8a513339f29..04d953ac624 100644 --- a/mobile/src/session/mobile-new-tab-agent-loader.ts +++ b/mobile/src/session/mobile-new-tab-agent-loader.ts @@ -134,8 +134,7 @@ async function loadDetectedAgents( return loadFolderDetectedAgents(client, worktreeId, host) } const repoResponse = await newTabRepoListRead.request(client) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. - const repos = (newTabRepoListRead.interpret(repoResponse) as MobileRuntimeRepoSummary[]) ?? [] + const repos: MobileRuntimeRepoSummary[] = newTabRepoListRead.interpret(repoResponse) const repoId = getRepoIdFromMobileWorktreeId(worktreeId) const repo = repos.find((candidate) => candidate.id === repoId) if (!repo) { diff --git a/mobile/src/session/mobile-session-tab-activation.test.ts b/mobile/src/session/mobile-session-tab-activation.test.ts index a84004649e8..3c9e41533fa 100644 --- a/mobile/src/session/mobile-session-tab-activation.test.ts +++ b/mobile/src/session/mobile-session-tab-activation.test.ts @@ -76,7 +76,8 @@ describe('mobile session tab activation', () => { worktree: 'id:worktree-1', tabId: 'tab-1', notifyClients: false, - navigation: 'caller' + navigation: 'caller', + intent: 'user' }) ).rejects.toBeInstanceOf(LogicalClientCutoverError) expect(sendRequest).toHaveBeenCalledTimes(2) diff --git a/mobile/src/session/mobile-structured-agent-session-thrown-refusal.test.ts b/mobile/src/session/mobile-structured-agent-session-thrown-refusal.test.ts index 0a682f1c937..c99ffb5a83d 100644 --- a/mobile/src/session/mobile-structured-agent-session-thrown-refusal.test.ts +++ b/mobile/src/session/mobile-structured-agent-session-thrown-refusal.test.ts @@ -6,6 +6,8 @@ import { callAgentSession, requestStructuredAgentSessionMutation } from './mobile-structured-agent-session-rpc' +import { stubRpcClient } from '../transport/rpc-client-test-stub' +import type { RpcResponse } from '../transport/types' // As `mapRuntimeError` sends a thrown refusal (pinned in `src/main/runtime/rpc/errors.test.ts`): // its message is the bare code, and its reason rides in data. @@ -21,9 +23,12 @@ const THROWN_JOURNAL_REFUSAL = { } function refusingClient(): RpcClient { - const sendRequest = async () => ({ id: 'req-1', ok: false, error: THROWN_JOURNAL_REFUSAL }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the paths under test reach only `sendRequest`. - return { sendRequest } as unknown as RpcClient + const sendRequest = async (): Promise => ({ + id: 'req-1', + ok: false, + error: THROWN_JOURNAL_REFUSAL + }) + return stubRpcClient({ sendRequest }) } describe('a refusal the host threw', () => { @@ -79,7 +84,7 @@ describe('a refusal the host threw', () => { describe('a write refused on a journal a newer Orca wrote', () => { // As the host answers it (pinned in `journal-open-failure.test.ts`). const newerOrcaClient = (): RpcClient => { - const sendRequest = async () => ({ + const sendRequest = async (): Promise => ({ id: 'req-1', ok: true, result: { @@ -91,8 +96,7 @@ describe('a write refused on a journal a newer Orca wrote', () => { } } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the paths under test reach only `sendRequest`. - return { sendRequest } as unknown as RpcClient + return stubRpcClient({ sendRequest }) } it.each([ diff --git a/mobile/src/session/mobile-structured-session-status-feed.test.ts b/mobile/src/session/mobile-structured-session-status-feed.test.ts index 3b8deaf5dfd..971e679efc8 100644 --- a/mobile/src/session/mobile-structured-session-status-feed.test.ts +++ b/mobile/src/session/mobile-structured-session-status-feed.test.ts @@ -4,6 +4,7 @@ import type { RpcClient } from '../transport/rpc-client' import type { ConnectionState } from '../transport/types' import { createStableLogicalRpcClient } from '../transport/stable-logical-rpc-client' import { mobileStructuredSessionStatusFeed } from './mobile-structured-session-status-feed' +import { stubRpcClient } from '../transport/rpc-client-test-stub' const REFUSAL = "Method 'agentSession.subscribeStatus' is not available to mobile clients" const FORBIDDEN = { @@ -34,8 +35,7 @@ describe("the phone's status stream", () => { frames = [] stateListeners = [] // A fresh client per test: the stream is one per client for its life. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the feed reaches the client only through subscribe and onStateChange. - client = { + client = stubRpcClient({ subscribe: vi.fn((_method: string, _params: unknown, onData: (value: unknown) => void) => { frames.push(onData) return () => {} @@ -44,7 +44,7 @@ describe("the phone's status stream", () => { stateListeners.push(listener) return () => {} } - } as unknown as RpcClient + }) }) function read(): AgentSessionStatusSummary | undefined { diff --git a/mobile/src/session/mobile-worker-takeover-send-sites.test.ts b/mobile/src/session/mobile-worker-takeover-send-sites.test.ts index 2b7d60bad3d..eacc786297d 100644 --- a/mobile/src/session/mobile-worker-takeover-send-sites.test.ts +++ b/mobile/src/session/mobile-worker-takeover-send-sites.test.ts @@ -31,7 +31,7 @@ const ref = (current: T) => ({ current }) const renderers: ReactTestRenderer[] = [] function clientFixture() { return { - sendRequest: vi.fn(async (method: string) => ({ + sendRequest: vi.fn(async (method: string, _params?: unknown) => ({ id: 'rpc', ok: true as const, result: diff --git a/mobile/src/session/pending-terminal-handle-recovery-poll.test.ts b/mobile/src/session/pending-terminal-handle-recovery-poll.test.ts index d5b1df752fb..2e92681b28f 100644 --- a/mobile/src/session/pending-terminal-handle-recovery-poll.test.ts +++ b/mobile/src/session/pending-terminal-handle-recovery-poll.test.ts @@ -75,7 +75,7 @@ function makeHarness() { ) const client = { sendRequest, subscribe } as unknown as RpcClient const consumeAcceptedSessionTabs = () => {} - const fetchTerminals = async () => {} + const fetchTerminals = async () => true const getPendingTerminalRecoveryContextKey = () => contextKey const hasRecoveryNeed = () => otherRecoveryNeeded const onPendingTerminalRecoveryParked = (key: string | null) => parkedContexts.push(key) @@ -89,6 +89,7 @@ function makeHarness() { client, connState, worktreeId: 'repo::worktree', + terminalInventoryRecoveryScopeKey: 'repo::worktree', applySessionTabs, consumeAcceptedSessionTabs, fetchTerminals, diff --git a/mobile/src/session/session-reply-schema.test.ts b/mobile/src/session/session-reply-schema.test.ts index 606c3153f74..64e78475484 100644 --- a/mobile/src/session/session-reply-schema.test.ts +++ b/mobile/src/session/session-reply-schema.test.ts @@ -153,7 +153,7 @@ describe('required members', () => { { path: 'c.ts' } ] }) - expect(compare.entries.map((entry) => entry.path)).toEqual(['a.ts']) + expect(compare.entries?.map((entry) => entry.path)).toEqual(['a.ts']) }) it('requires the three members a markdown tab publishes into its ready state', () => { @@ -339,7 +339,7 @@ describe('closed arm sets drop rather than default', () => { { path: 'b.ts', status: 'teleported' } ] }) - expect(projection.entries.map((entry) => entry.path)).toEqual(['a.ts']) + expect(projection.entries?.map((entry) => entry.path)).toEqual(['a.ts']) }) it('refuses a diff whose kind names no arm the screen can render', () => { @@ -399,7 +399,7 @@ describe('tri-state flags keep an explicit null', () => { it('keeps a null autoMergeAllowed and mergeQueueRequired on the PR and drops a non-boolean', () => { const readsPr = (value: unknown) => { const found = reads(githubPrForBranchSchema, { kind: 'found', pr: value }) - return found.kind === 'found' ? found.pr : null + return found?.kind === 'found' ? found.pr : null } const nulled = readsPr({ ...pr, autoMergeAllowed: null, mergeQueueRequired: null }) expect(nulled?.autoMergeAllowed).toBeNull() diff --git a/mobile/src/session/use-mobile-diff-review-send-actions.test.ts b/mobile/src/session/use-mobile-diff-review-send-actions.test.ts index a3fb5aa2c98..ee020c9178a 100644 --- a/mobile/src/session/use-mobile-diff-review-send-actions.test.ts +++ b/mobile/src/session/use-mobile-diff-review-send-actions.test.ts @@ -29,6 +29,7 @@ function requestPortRpcClient(sendRequest: RpcClient['sendRequest']): RpcClient } type SendActions = ReturnType +type SendActionArgs = Parameters[0] vi.mock('../platform/haptics', () => ({ triggerSuccess: vi.fn() })) // Resolving `true`, which is what the pasteboard answers when it took the text: the seam reads @@ -98,9 +99,15 @@ function launchClient( return { client: requestPortRpcClient(sendRequest), sendRequest } } -const READY: ReviewScreenState = { +const READY: Extract = { kind: 'ready', - status: { entries: [], conflictOperation: 'none' }, + status: { + entries: [], + conflictOperation: 'unknown', + branch: undefined, + head: undefined, + upstreamStatus: undefined + }, branchCompare: null, comments: [COMMENT], reviewState: { version: 1, files: {} } @@ -110,13 +117,13 @@ describe('useMobileDiffReviewSendActions', () => { let renderer: ReactTestRenderer | null = null let actions: SendActions | null = null let mountedClient: RpcClient | null = null - let setActionError: ReturnType + let setActionError: Mock let sheets: { openSheet: Mock closeSheet: Mock updateSendSheet: Mock } - let saveCommentsAndReviewState: ReturnType + let saveCommentsAndReviewState: Mock let screenState: ReviewScreenState = READY beforeEach(() => { diff --git a/mobile/src/session/use-mobile-file-tap-handlers.test.ts b/mobile/src/session/use-mobile-file-tap-handlers.test.ts index 6d0adbbf8df..93514021d30 100644 --- a/mobile/src/session/use-mobile-file-tap-handlers.test.ts +++ b/mobile/src/session/use-mobile-file-tap-handlers.test.ts @@ -1,6 +1,8 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { RpcResponse } from '../transport/types' +import type { UnvalidatedRpcRequestPort } from '../transport/unvalidated-rpc-request-port' import { useMobileFileTapHandlers } from './use-mobile-file-tap-handlers' const push = vi.fn() @@ -10,8 +12,8 @@ vi.mock('../platform/haptics', () => ({ triggerSelection: vi.fn() })) type Handlers = ReturnType -function ok(result: unknown) { - return { ok: true, result, _meta: { runtimeId: 'runtime-1' } } +function ok(result: unknown): RpcResponse { + return { id: 'rpc', ok: true, result, _meta: { runtimeId: 'runtime-1' } } } describe('useMobileFileTapHandlers', () => { @@ -28,7 +30,7 @@ describe('useMobileFileTapHandlers', () => { handlers = null }) - function createOptions(sendRequest: ReturnType) { + function createOptions(sendRequest: UnvalidatedRpcRequestPort['sendRequest']) { return { client: { sendRequest }, hostId: 'host-1', diff --git a/mobile/src/session/use-mobile-native-chat-answer-send.test.ts b/mobile/src/session/use-mobile-native-chat-answer-send.test.ts index 743488bac9f..b179c6aa3ad 100644 --- a/mobile/src/session/use-mobile-native-chat-answer-send.test.ts +++ b/mobile/src/session/use-mobile-native-chat-answer-send.test.ts @@ -23,6 +23,7 @@ import { } from './mobile-native-chat-terminal-write-lock' import { useMobileNativeChatAnswerSend } from './use-mobile-native-chat-answer-send' import { useNativeChatAcceptedAction } from './use-native-chat-action-outcomes' +import { stubRpcClient } from '../transport/rpc-client-test-stub' type AnswerSend = ReturnType @@ -112,8 +113,7 @@ describe('useMobileNativeChatAnswerSend', () => { '%s single-select: sends the picked option number without a trailing Enter', async (agent) => { const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This fixture exercises only the scripted sendRequest port. - await mount({ sendRequest } as unknown as RpcClient, vi.fn(), agent) + await mount(stubRpcClient({ sendRequest }), vi.fn(), agent) // Spaces is option 2 — the STA-1860 case where label text committed Tabs. await expect(answerSend?.answerAsk(TABS_OR_SPACES, [{ indices: [1] }])).resolves.toBe(true) @@ -126,8 +126,7 @@ describe('useMobileNativeChatAnswerSend', () => { '%s multi-select: toggles option numbers and confirms in paced steps', async (agent) => { const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This fixture exercises only the scripted sendRequest port. - await mount({ sendRequest } as unknown as RpcClient, vi.fn(), agent) + await mount(stubRpcClient({ sendRequest }), vi.fn(), agent) const prompt: AskPrompt = { questions: [ @@ -163,7 +162,7 @@ describe('useMobileNativeChatAnswerSend', () => { it('multi-question: option numbers auto-advance, one final submit Enter', async () => { const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn()) + await mount(stubRpcClient({ sendRequest }), vi.fn()) const prompt: AskPrompt = { questions: [ @@ -195,7 +194,7 @@ describe('useMobileNativeChatAnswerSend', () => { vi.advanceTimersByTime(6_000) return acceptedResponse() }) - await mount({ sendRequest } as unknown as RpcClient, vi.fn()) + await mount(stubRpcClient({ sendRequest }), vi.fn()) const prompt: AskPrompt = { questions: [ @@ -222,7 +221,7 @@ describe('useMobileNativeChatAnswerSend', () => { it('free text: opens "Type something", types the sanitized answer, then Enter', async () => { const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn()) + await mount(stubRpcClient({ sendRequest }), vi.fn()) let result: Promise | undefined await act(async () => { @@ -243,7 +242,7 @@ describe('useMobileNativeChatAnswerSend', () => { it('answers OpenClaude asks with Claude selector keystrokes', async () => { const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn(), 'openclaude') + await mount(stubRpcClient({ sendRequest }), vi.fn(), 'openclaude') await expect(answerSend?.answerAsk(TABS_OR_SPACES, [{ indices: [1] }])).resolves.toBe(true) expect(sendRequest).toHaveBeenCalledTimes(1) @@ -252,7 +251,7 @@ describe('useMobileNativeChatAnswerSend', () => { it('submits a Codex answer by option-number keystroke like Claude', async () => { const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn(), 'codex') + await mount(stubRpcClient({ sendRequest }), vi.fn(), 'codex') await expect(answerSend?.answerAsk(TABS_OR_SPACES, [{ indices: [1] }])).resolves.toBe(true) // Codex's request_user_input card ignores pasted labels; the digit selects AND commits. @@ -262,7 +261,7 @@ describe('useMobileNativeChatAnswerSend', () => { it('does not send a trailing Enter after Codex submits a multi-question answer', async () => { const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn(), 'codex') + await mount(stubRpcClient({ sendRequest }), vi.fn(), 'codex') const prompt: AskPrompt = { questions: [ { question: 'q1', multiSelect: false, options: [{ label: 'A' }, { label: 'B' }] }, @@ -287,7 +286,7 @@ describe('useMobileNativeChatAnswerSend', () => { it('sends a Codex async answer as one ordinary message, not selector keystrokes', async () => { const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn(), 'codex') + await mount(stubRpcClient({ sendRequest }), vi.fn(), 'codex') const prompt: AskPrompt = { replyAsMessage: true, questions: [{ question: 'Which color?', multiSelect: false, options: [{ label: 'Red' }] }] @@ -301,7 +300,7 @@ describe('useMobileNativeChatAnswerSend', () => { it('submits a non-selector answer as pasted label text with a single Enter', async () => { const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn(), 'grok') + await mount(stubRpcClient({ sendRequest }), vi.fn(), 'grok') await expect(answerSend?.answerAsk(TABS_OR_SPACES, [{ indices: [1] }])).resolves.toBe(true) // Grok's question tool commits the pasted answer: label text + one Enter. @@ -311,7 +310,7 @@ describe('useMobileNativeChatAnswerSend', () => { it('clears an orphaned image paste before an answer that commits with Enter (#10228)', async () => { const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn(), 'grok') + await mount(stubRpcClient({ sendRequest }), vi.fn(), 'grok') // An earlier image send left its path on this terminal's composer line. markMobileNativeChatInputStale('terminal') @@ -326,7 +325,7 @@ describe('useMobileNativeChatAnswerSend', () => { it('keeps the marker for a selector answer, which cannot submit the composer', async () => { const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn(), 'claude') + await mount(stubRpcClient({ sendRequest }), vi.fn(), 'claude') markMobileNativeChatInputStale('terminal') await expect(answerSend?.answerAsk(TABS_OR_SPACES, [{ indices: [1] }])).resolves.toBe(true) @@ -346,7 +345,7 @@ describe('useMobileNativeChatAnswerSend', () => { result: { send: { accepted: false } }, _meta: { runtimeId: 'runtime' } }) - await mount({ sendRequest } as unknown as RpcClient, onSendError, 'grok') + await mount(stubRpcClient({ sendRequest }), onSendError, 'grok') markMobileNativeChatInputStale('terminal') await expect(answerSend?.answerAsk(TABS_OR_SPACES, [{ indices: [1] }])).resolves.toBe(false) @@ -365,7 +364,7 @@ describe('useMobileNativeChatAnswerSend', () => { result: { send: { accepted: false } }, _meta: { runtimeId: 'runtime' } }) - await mount({ sendRequest } as unknown as RpcClient, onSendError) + await mount(stubRpcClient({ sendRequest }), onSendError) await expect(answerSend?.answerAsk(TABS_OR_SPACES, [{ indices: [1] }])).resolves.toBe(false) expect(sendRequest).toHaveBeenCalledTimes(1) @@ -380,7 +379,7 @@ describe('useMobileNativeChatAnswerSend', () => { vi.advanceTimersByTime(16_000) return acceptedResponse() }) - await mount({ sendRequest } as unknown as RpcClient, onSendError) + await mount(stubRpcClient({ sendRequest }), onSendError) const prompt: AskPrompt = { questions: [ @@ -407,7 +406,7 @@ describe('useMobileNativeChatAnswerSend', () => { const sendRequest = vi .fn() .mockRejectedValue(markRpcDeliveryUnknown(new Error('Connection closed'))) - await mount({ sendRequest } as unknown as RpcClient, onSendError) + await mount(stubRpcClient({ sendRequest }), onSendError) await expect(answerSend?.answerAsk(TABS_OR_SPACES, [{ indices: [1] }])).resolves.toBe(false) expect(onSendError).toHaveBeenCalledWith('Answer unconfirmed — check chat before retrying') @@ -415,7 +414,7 @@ describe('useMobileNativeChatAnswerSend', () => { it('rejects an empty selection without writing anything', async () => { const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn()) + await mount(stubRpcClient({ sendRequest }), vi.fn()) await expect(answerSend?.answerAsk(TABS_OR_SPACES, [{ indices: [] }])).resolves.toBe(false) expect(sendRequest).not.toHaveBeenCalled() @@ -423,7 +422,7 @@ describe('useMobileNativeChatAnswerSend', () => { it('cancels delayed keystrokes when the acknowledged input lease is lost', async () => { const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn()) + await mount(stubRpcClient({ sendRequest }), vi.fn()) const prompt: AskPrompt = { questions: [ @@ -449,7 +448,7 @@ describe('useMobileNativeChatAnswerSend', () => { it('rejects an answer while another composed write holds the terminal', async () => { const onSendError = vi.fn() const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, onSendError) + await mount(stubRpcClient({ sendRequest }), onSendError) // An image paste sequence is mid-flight into the same PTY. expect(acquireMobileNativeChatTerminalWrite('terminal')).toBe(true) @@ -465,7 +464,7 @@ describe('useMobileNativeChatAnswerSend', () => { it('answers again on the same handle after an earlier answer already landed', async () => { const onSendError = vi.fn() const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, onSendError) + await mount(stubRpcClient({ sendRequest }), onSendError) await expect(answerSend?.answerAsk(TABS_OR_SPACES, [{ indices: [0] }])).resolves.toBe(true) // A landed answer resolves its turn FALSE — correct for a queued successor, @@ -479,7 +478,7 @@ describe('useMobileNativeChatAnswerSend', () => { it('fences a superseding answer after the cancelled chain moved the selector', async () => { const sendRequest = vi.fn().mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn()) + await mount(stubRpcClient({ sendRequest }), vi.fn()) const prompt: AskPrompt = { questions: [ @@ -520,7 +519,7 @@ describe('useMobileNativeChatAnswerSend', () => { }) ) .mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn()) + await mount(stubRpcClient({ sendRequest }), vi.fn()) const prompt: AskPrompt = { questions: [ @@ -561,7 +560,7 @@ describe('useMobileNativeChatAnswerSend', () => { }) ) .mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn()) + await mount(stubRpcClient({ sendRequest }), vi.fn()) let first: Promise | undefined let second: Promise | undefined @@ -599,7 +598,7 @@ describe('useMobileNativeChatAnswerSend', () => { }) ) .mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, onSendError) + await mount(stubRpcClient({ sendRequest }), onSendError) let first: Promise | undefined let second: Promise | undefined @@ -634,7 +633,7 @@ describe('useMobileNativeChatAnswerSend', () => { settle.push(resolve) }) ) - await mount({ sendRequest } as unknown as RpcClient, onSendError) + await mount(stubRpcClient({ sendRequest }), onSendError) let first: Promise | undefined let second: Promise | undefined @@ -671,7 +670,7 @@ describe('useMobileNativeChatAnswerSend', () => { settle.push(resolve) }) ) - await mount({ sendRequest } as unknown as RpcClient, vi.fn()) + await mount(stubRpcClient({ sendRequest }), vi.fn()) let first: Promise | undefined let second: Promise | undefined @@ -714,7 +713,7 @@ describe('useMobileNativeChatAnswerSend', () => { settle.push(resolve) }) ) - await mount({ sendRequest } as unknown as RpcClient, onSendError) + await mount(stubRpcClient({ sendRequest }), onSendError) let first: Promise | undefined let second: Promise | undefined @@ -751,7 +750,7 @@ describe('useMobileNativeChatAnswerSend', () => { settle.push(resolve) }) ) - await mount({ sendRequest } as unknown as RpcClient, onSendError, 'grok') + await mount(stubRpcClient({ sendRequest }), onSendError, 'grok') let first: Promise | undefined let second: Promise | undefined @@ -785,7 +784,7 @@ describe('useMobileNativeChatAnswerSend', () => { settle.push(resolve) }) ) - await mount({ sendRequest } as unknown as RpcClient, onSendError) + await mount(stubRpcClient({ sendRequest }), onSendError) let first: Promise | undefined await act(async () => { @@ -817,7 +816,7 @@ describe('useMobileNativeChatAnswerSend', () => { settle.push(resolve) }) ) - await mount({ sendRequest } as unknown as RpcClient, onSendError, 'grok') + await mount(stubRpcClient({ sendRequest }), onSendError, 'grok') let first: Promise | undefined await act(async () => { @@ -846,7 +845,7 @@ describe('useMobileNativeChatAnswerSend', () => { settle.push(resolve) }) ) - await mount({ sendRequest } as unknown as RpcClient, onSendError) + await mount(stubRpcClient({ sendRequest }), onSendError) let first: Promise | undefined let second: Promise | undefined @@ -888,7 +887,7 @@ describe('useMobileNativeChatAnswerSend', () => { settle.push(resolve) }) ) - await mount({ sendRequest } as unknown as RpcClient, vi.fn()) + await mount(stubRpcClient({ sendRequest }), vi.fn()) let first: Promise | undefined let second: Promise | undefined diff --git a/mobile/src/session/use-mobile-native-chat-message-detail.test.tsx b/mobile/src/session/use-mobile-native-chat-message-detail.test.tsx index 236d2bc98b9..0baf09ac793 100644 --- a/mobile/src/session/use-mobile-native-chat-message-detail.test.tsx +++ b/mobile/src/session/use-mobile-native-chat-message-detail.test.tsx @@ -3,14 +3,13 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, expect, it, vi } from 'vitest' import type { NativeChatMessage } from '../../../src/shared/native-chat-types' import type { AgentJournalRenderItem } from '../../../src/shared/agent-session-journal-types' -import type { RpcClient } from '../transport/rpc-client' import { foldMobileNativeChatMessages } from './mobile-native-chat-render-data' import { useMobileNativeChatMessageDetail } from './use-mobile-native-chat-message-detail' +import { stubRpcClient } from '../transport/rpc-client-test-stub' const reads = vi.hoisted(() => vi.fn()) vi.mock('./mobile-native-chat-item-read', () => ({ readMobileNativeChatItem: reads })) -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: RPC is mocked; only this client's object identity is used. -const source = { client: {} as RpcClient, sessionId: 'session' } +const source = { client: stubRpcClient(), sessionId: 'session' } let latest: ReturnType | undefined let renderer: ReactTestRenderer | undefined function Probe({ messages }: { messages: NativeChatMessage[] }) { diff --git a/mobile/src/session/use-mobile-native-chat-readability.ts b/mobile/src/session/use-mobile-native-chat-readability.ts index c8c67f62119..10c69fad0a8 100644 --- a/mobile/src/session/use-mobile-native-chat-readability.ts +++ b/mobile/src/session/use-mobile-native-chat-readability.ts @@ -64,10 +64,7 @@ export function useMobileNativeChatReadability( return } const accepted = nativeChatRepoListRead.interpret(response) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. - const repos = accepted.accepted - ? ((accepted.value as MobileRuntimeRepoSummary[]) ?? []) - : [] + const repos: MobileRuntimeRepoSummary[] = accepted.accepted ? accepted.value : [] const repoId = getRepoIdFromMobileWorktreeId(worktreeId) const repo = repos.find((candidate) => candidate.id === repoId) setState({ diff --git a/mobile/src/session/use-mobile-native-chat-visual.test.tsx b/mobile/src/session/use-mobile-native-chat-visual.test.tsx index 21e8cde258c..0126d6dcfb3 100644 --- a/mobile/src/session/use-mobile-native-chat-visual.test.tsx +++ b/mobile/src/session/use-mobile-native-chat-visual.test.tsx @@ -1,7 +1,6 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { RpcClient } from '../transport/rpc-client' import type { MobileNativeChatVisualRead } from './mobile-native-chat-visual-read' const reads = vi.hoisted(() => { @@ -21,9 +20,9 @@ import { useMobileNativeChatVisual, type MobileNativeChatVisualState } from './use-mobile-native-chat-visual' +import { stubRpcClient } from '../transport/rpc-client-test-stub' -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the read module is mocked; the client is only an identity here. -const source = { client: {} as RpcClient, sessionId: 'session-1' } +const source = { client: stubRpcClient(), sessionId: 'session-1' } let latest: { state: MobileNativeChatVisualState; retry: () => void } | null = null function Probe() { diff --git a/mobile/src/session/use-mobile-pr-ai-triage.test.ts b/mobile/src/session/use-mobile-pr-ai-triage.test.ts index d3e284fe882..88709f07a7e 100644 --- a/mobile/src/session/use-mobile-pr-ai-triage.test.ts +++ b/mobile/src/session/use-mobile-pr-ai-triage.test.ts @@ -7,6 +7,7 @@ import type { ConnectionState, RpcResponse } from '../transport/types' vi.mock('../platform/haptics', () => ({ triggerError: vi.fn(), triggerSuccess: vi.fn() })) import { useMobilePrAiTriage, type MobilePrAiTriage } from './use-mobile-pr-ai-triage' +import { stubRpcClient } from '../transport/rpc-client-test-stub' const LAUNCH_CAPABILITIES = [ 'agent.launch.v2', @@ -45,8 +46,7 @@ function hostClient(launchReply: RpcResponse = LAUNCHED_WITHOUT_PROMPT): RpcClie } return launchReply }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook reaches the client only through sendRequest. - return { sendRequest } as unknown as RpcClient + return stubRpcClient({ sendRequest }) } describe('useMobilePrAiTriage', () => { diff --git a/mobile/src/session/use-mobile-pr-branch-context.test.ts b/mobile/src/session/use-mobile-pr-branch-context.test.ts index 6973292d725..7e17ea83bd4 100644 --- a/mobile/src/session/use-mobile-pr-branch-context.test.ts +++ b/mobile/src/session/use-mobile-pr-branch-context.test.ts @@ -13,6 +13,7 @@ function status(overrides: Partial): MobileGitStatusResul conflictOperation: 'unknown', branch: undefined, head: undefined, + upstreamStatus: undefined, ...overrides } } diff --git a/mobile/src/session/use-mobile-pr-sidebar-controller.test.ts b/mobile/src/session/use-mobile-pr-sidebar-controller.test.ts index 0e41b58d02a..6afb6faa114 100644 --- a/mobile/src/session/use-mobile-pr-sidebar-controller.test.ts +++ b/mobile/src/session/use-mobile-pr-sidebar-controller.test.ts @@ -44,7 +44,12 @@ const CHECKS: PRCheckDetail[] = [ // A host reply that settled fine at the transport but carries no result — the // `result-absent` partition of the reply matrix. -const RESULT_ABSENT_REPLY: RpcResponse = { id: 'x', ok: true, _meta: { runtimeId: 'r' } } +const RESULT_ABSENT_REPLY: RpcResponse = { + id: 'x', + ok: true, + result: undefined, + _meta: { runtimeId: 'r' } +} function ghInfo(over: Partial = {}): HostedReviewInfo { return { diff --git a/mobile/src/session/use-mobile-session-accessory-selection.ts b/mobile/src/session/use-mobile-session-accessory-selection.ts index 3ef450d19dd..3aa57a6c444 100644 --- a/mobile/src/session/use-mobile-session-accessory-selection.ts +++ b/mobile/src/session/use-mobile-session-accessory-selection.ts @@ -133,7 +133,6 @@ export function useMobileSessionAccessorySelection(scope: MobileSessionTerminalI } catch (e) { triggerError() const err = e as { name?: string; message?: string } - // eslint-disable-next-line no-console console.warn('[mobile-clip] setString failed', { name: err.name, message: err.message @@ -163,7 +162,6 @@ export function useMobileSessionAccessorySelection(scope: MobileSessionTerminalI if (handle !== activeHandleRef.current) { return } - // eslint-disable-next-line no-console console.warn('[mobile-clip] selection evicted') showToast('Selection cleared (scrolled out of buffer)', 1500) setSelectModeActive(false) @@ -208,8 +206,7 @@ export function useMobileSessionAccessorySelection(scope: MobileSessionTerminalI } const repoId = getRepoIdFromMobileWorktreeId(worktreeId) const repoResponse = await newTabRepoListRead.request(client) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. - const repos = (newTabRepoListRead.interpret(repoResponse) as MobileRuntimeRepoSummary[]) ?? [] + const repos: MobileRuntimeRepoSummary[] = newTabRepoListRead.interpret(repoResponse) return repos.find((repo) => repo.id === repoId)?.connectionId?.trim() || null }, [client, isFloatingWorkspaceRoute, worktreeId]) diff --git a/mobile/src/session/use-mobile-session-terminal-create-actions.test.ts b/mobile/src/session/use-mobile-session-terminal-create-actions.test.ts index fdb3e6a29db..c1372063aae 100644 --- a/mobile/src/session/use-mobile-session-terminal-create-actions.test.ts +++ b/mobile/src/session/use-mobile-session-terminal-create-actions.test.ts @@ -61,12 +61,13 @@ function terminalCreateResponse(): RpcResponse { function createScope(client: RpcClient) { const sessionTabs: MobileSessionTab[] = [] + const hostCapabilities: string[] = [] return { nativeChatController: { setTabView: vi.fn() }, hostId: 'host-1', worktreeId: 'workspace-1', client, - hostCapabilities: [], + hostCapabilities, connState: 'connected', setTerminals: vi.fn(), terminalsRef: { current: [] }, diff --git a/mobile/src/session/use-mobile-structured-agent-session.test.tsx b/mobile/src/session/use-mobile-structured-agent-session.test.tsx index cc8f223217d..401e3fa1bab 100644 --- a/mobile/src/session/use-mobile-structured-agent-session.test.tsx +++ b/mobile/src/session/use-mobile-structured-agent-session.test.tsx @@ -373,12 +373,12 @@ describe('useMobileStructuredAgentSession', () => { throw new Error('hook not ready') } - await vi.waitFor(() => expect(hook.permission).not.toBeNull()) - await vi.waitFor(() => expect(hook.question).not.toBeNull()) + await vi.waitFor(() => expect(hook!.permission).not.toBeNull()) + await vi.waitFor(() => expect(hook!.question).not.toBeNull()) // The seed paints first; the session's own list is what a pick is checked against. await vi.waitFor(() => { - expect(listedModelIds(hook.optionSnapshot)).toContain('gpt-slow') - expect(hook.optionSnapshot.find((descriptor) => descriptor.id === 'model')).toMatchObject({ + expect(listedModelIds(hook!.optionSnapshot)).toContain('gpt-slow') + expect(hook!.optionSnapshot.find((descriptor) => descriptor.id === 'model')).toMatchObject({ settable: true }) }) @@ -400,7 +400,7 @@ describe('useMobileStructuredAgentSession', () => { expect(hook.optionSurface.getSnapshot()).toEqual(hook.optionSnapshot) await act(async () => { - expect(await hook.setStructuredOption('model', 'gpt-fast')).toBe(true) + expect(await hook!.setStructuredOption('model', 'gpt-fast')).toBe(true) }) expect(sendRequest).toHaveBeenCalledWith( 'agentSession.setOption', @@ -423,7 +423,7 @@ describe('useMobileStructuredAgentSession', () => { }) await act(async () => { - expect(await hook.respondPermission(hook.permission!.options[0]!.send)).toBe(true) + expect(await hook!.respondPermission(hook!.permission!.options[0]!.send)).toBe(true) }) expect(sendRequest).toHaveBeenCalledWith( 'agentSession.respondToApproval', @@ -440,7 +440,7 @@ describe('useMobileStructuredAgentSession', () => { await act(async () => { expect( - await hook.respondQuestion(formatQuestionFreeTextAnswer(hook.question!, 'custom answer')) + await hook!.respondQuestion(formatQuestionFreeTextAnswer(hook!.question!, 'custom answer')) ).toBe(true) }) expect(sendRequest).toHaveBeenCalledWith( @@ -466,7 +466,7 @@ describe('useMobileStructuredAgentSession', () => { let outcome: MobileNativeChatSendOutcome = 'rejected' await act(async () => { - outcome = await hook.sendWithOutcome('look at this', undefined, undefined, [ + outcome = await hook!.sendWithOutcome('look at this', undefined, undefined, [ { path: '/tmp/a.png', previewUri: 'file:///a.jpg' } ]) }) diff --git a/mobile/src/session/use-mobile-structured-prompt-responses.test.tsx b/mobile/src/session/use-mobile-structured-prompt-responses.test.tsx index 3e78ed2847c..609caa67d6a 100644 --- a/mobile/src/session/use-mobile-structured-prompt-responses.test.tsx +++ b/mobile/src/session/use-mobile-structured-prompt-responses.test.tsx @@ -24,7 +24,7 @@ type PromptResponses = ReturnType let currentHook: PromptResponses | null = null let renderer: ReactTestRenderer | null = null -function groupedPrompt(itemId: string, revision: number): AgentJournalRenderItem { +function groupedPrompt(itemId: string, revision: number): StructuredQuestionItem { return { itemId, revision, @@ -63,7 +63,7 @@ function sessionState(prompt: AgentJournalRenderItem): StructuredAgentSessionSta return { ...EMPTY_STRUCTURED_AGENT_SESSION, status: 'ready', items: [prompt] } } -function projectedResponse(prompt: AgentJournalRenderItem, draft: PromptResponses['groupedDraft']) { +function projectedResponse(prompt: StructuredQuestionItem, draft: PromptResponses['groupedDraft']) { const projected = projectStructuredQuestion(prompt, draft) const response = projected?.optionTokens[0] if (!response) { @@ -371,7 +371,7 @@ describe('useMobileStructuredPromptResponses', () => { throw new Error('expected a grouped question') } const [first, second] = base.body.questions - const prompt: AgentJournalRenderItem = { + const prompt: StructuredQuestionItem = { ...base, body: { ...base.body, questions: [first!, { ...second!, freeTextQuestionId: 'q2' }] } } @@ -410,7 +410,7 @@ describe('useMobileStructuredPromptResponses', () => { throw new Error('expected a grouped question') } const [first, second] = base.body.questions - const prompt: AgentJournalRenderItem = { + const prompt: StructuredQuestionItem = { ...base, body: { ...base.body, questions: [{ ...first!, freeTextQuestionId: 'q1' }, second!] } } diff --git a/mobile/src/session/use-mobile-terminal-paste.ts b/mobile/src/session/use-mobile-terminal-paste.ts index 5ec8775273d..54713f01107 100644 --- a/mobile/src/session/use-mobile-terminal-paste.ts +++ b/mobile/src/session/use-mobile-terminal-paste.ts @@ -104,7 +104,6 @@ export function useMobileTerminalPaste({ const wrappedBytes = new TextEncoder().encode(payload).byteLength if (wrappedBytes > 256 * 1024) { onError() - // eslint-disable-next-line no-console console.warn('[mobile-clip] paste oversized', { wrappedBytes }) showToast('Paste too large (max 256 KiB)', 1500) return @@ -147,7 +146,6 @@ export function useMobileTerminalPaste({ onError() const err = e as { name?: string; message?: string } const isDisconnected = connState !== 'connected' - // eslint-disable-next-line no-console console.warn('[mobile-clip] paste failed', { name: err.name, message: err.message }) if (isDisconnected) { showToast('Paste failed (disconnected)', 1500) diff --git a/mobile/src/settings/native-notification-delivery-settings.test.tsx b/mobile/src/settings/native-notification-delivery-settings.test.tsx index bf94b75649e..210d074ed29 100644 --- a/mobile/src/settings/native-notification-delivery-settings.test.tsx +++ b/mobile/src/settings/native-notification-delivery-settings.test.tsx @@ -2,6 +2,7 @@ import { createElement, useEffect } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { NativeNotificationDeliverySettings } from './native-notification-delivery-settings' +import { findByHostType } from '../test-support/react-test-renderer-host-nodes' const mocks = vi.hoisted(() => ({ load: vi.fn(), @@ -48,7 +49,7 @@ beforeEach(() => { afterEach(() => { act(() => renderer?.unmount()) }) -const section = () => renderer.root.findByType('Delivery').props +const section = () => findByHostType(renderer.root, 'Delivery').props it('keeps stored controls visible but disabled without consent and explains an old host', async () => { await act(async () => { renderer = create(createElement(NativeNotificationDeliverySettings, { enabled: false })) diff --git a/mobile/src/settings/notification-display-test.test.tsx b/mobile/src/settings/notification-display-test.test.tsx index ea5128c7fb5..29df616f072 100644 --- a/mobile/src/settings/notification-display-test.test.tsx +++ b/mobile/src/settings/notification-display-test.test.tsx @@ -2,6 +2,7 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { NotificationDisplayTest } from './notification-display-test' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' const mocks = vi.hoisted(() => ({ loadHosts: vi.fn(), @@ -27,7 +28,7 @@ async function send() { await act(async () => { renderer = create(createElement(NotificationDisplayTest, { onTroubleshoot: vi.fn() })) }) - await act(async () => renderer.root.findAllByType('Pressable')[0].props.onPress()) + await act(async () => findAllByHostType(renderer.root, 'Pressable')[0].props.onPress()) } it.each([ diff --git a/mobile/src/settings/notification-display-test.tsx b/mobile/src/settings/notification-display-test.tsx index 2f3719bf949..f2b3f258484 100644 --- a/mobile/src/settings/notification-display-test.tsx +++ b/mobile/src/settings/notification-display-test.tsx @@ -3,7 +3,6 @@ import { Pressable, StyleSheet, Text, View } from 'react-native' import { useAllHostClients } from '../transport/use-all-host-clients' import { loadHostCatalog } from '../transport/host-store' import { pushDeliveryTest } from '../notifications/mobile-push-delivery-test-operations' -import type { RpcFailure } from '../transport/types' import { colors, spacing, typography } from '../theme/mobile-theme' export function NotificationDisplayTest({ onTroubleshoot }: { onTroubleshoot: () => void }) { @@ -40,8 +39,7 @@ export function NotificationDisplayTest({ onTroubleshoot }: { onTroubleshoot: () }) const delivered = pushDeliveryTest.interpret(reply) if (!delivered.accepted) { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this policy skips only a refusal, so an unaccepted reply is a failure envelope. - const code = (reply as RpcFailure).error?.code + const code = reply.ok ? undefined : reply.error?.code if (code === 'forbidden' || code === 'method_not_found') { continue } diff --git a/mobile/src/settings/settings-screen-state.test.tsx b/mobile/src/settings/settings-screen-state.test.tsx index a0ec8801f13..7ca13d38024 100644 --- a/mobile/src/settings/settings-screen-state.test.tsx +++ b/mobile/src/settings/settings-screen-state.test.tsx @@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import NotificationsScreen from './notification-settings-screen' import VoiceSettingsScreen from './voice-settings-screen' import type { VoiceSettingsOperations } from './voice-settings-operations' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ View: 'View', @@ -99,7 +100,7 @@ describe('shared settings screen state', () => { ) }) expect(JSON.stringify(renderer.toJSON())).not.toContain('Failed to load voice settings') - expect(renderer.root.findAllByType('ActivityIndicator')).toHaveLength(1) + expect(findAllByHostType(renderer.root, 'ActivityIndicator')).toHaveLength(1) }) it('drops a poll that resolves after a toggle instead of clobbering it', async () => { const loaded = { diff --git a/mobile/src/settings/voice-settings-poller-refresh.test.tsx b/mobile/src/settings/voice-settings-poller-refresh.test.tsx index 6a44f4f4415..262694fd699 100644 --- a/mobile/src/settings/voice-settings-poller-refresh.test.tsx +++ b/mobile/src/settings/voice-settings-poller-refresh.test.tsx @@ -3,6 +3,7 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import VoiceSettingsScreen from './voice-settings-screen' import type { VoiceSettingsOperations } from './voice-settings-operations' +import { findAllByHostType } from '../test-support/react-test-renderer-host-nodes' // Why: the sibling state test stubs the poller, so it can only prove the screen's local // machine. This one runs the real useDictationSetupPoller, whose refreshNow is gated on @@ -85,7 +86,7 @@ describe('voice settings poller integration', () => { // The poller is gated on focus, so no read runs and only the initial flag decides // this paint. Base started it false and flashed the error card here. expect(operations.load).not.toHaveBeenCalled() - expect(renderer.root.findAllByType('ActivityIndicator')).toHaveLength(1) + expect(findAllByHostType(renderer.root, 'ActivityIndicator')).toHaveLength(1) expect(JSON.stringify(renderer.toJSON())).not.toContain('Failed to load voice settings') }) @@ -105,7 +106,7 @@ describe('voice settings poller integration', () => { }) // The failed read leaves the error card up, exactly as base did. expect(JSON.stringify(renderer.toJSON())).toContain('Desktop unavailable') - expect(renderer.root.findAllByType('ActivityIndicator')).toHaveLength(0) + expect(findAllByHostType(renderer.root, 'ActivityIndicator')).toHaveLength(0) await act(async () => { renderer.update(createElement(VoiceSettingsScreen, { operations, focused: false, onBack })) @@ -115,7 +116,7 @@ describe('voice settings poller integration', () => { }) expect((operations.load as ReturnType).mock.calls.length).toBe(2) // Base re-showed the spinner on re-entry; the stale error must not sit there during the retry. - expect(renderer.root.findAllByType('ActivityIndicator')).toHaveLength(1) + expect(findAllByHostType(renderer.root, 'ActivityIndicator')).toHaveLength(1) expect(JSON.stringify(renderer.toJSON())).not.toContain('Desktop unavailable') }) diff --git a/mobile/src/source-control/mobile-commit-during-generation.test.tsx b/mobile/src/source-control/mobile-commit-during-generation.test.tsx index 7fb8a351e41..797b7237274 100644 --- a/mobile/src/source-control/mobile-commit-during-generation.test.tsx +++ b/mobile/src/source-control/mobile-commit-during-generation.test.tsx @@ -1,7 +1,6 @@ import { createElement, useRef, useState } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi, type Mock } from 'vitest' -import type { RpcClient } from '../transport/rpc-client' import type { MobileGenerateCommitMessageResult } from './mobile-commit-message-ai' // Why: react-native is Flow source vitest will not parse; haptics reaches expo-haptics through it. @@ -39,9 +38,9 @@ vi.mock('./mobile-commit-message-ai', () => ({ import { useRouteHandoff } from '../navigation/route-handoff' import { useMobileSourceControlRunners } from './use-mobile-source-control-runners' +import { stubRpcClient } from '../transport/rpc-client-test-stub' -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked AI module and the injected senders are the only things that would reach the client. -const client = {} as RpcClient +const client = stubRpcClient() type Runners = ReturnType const view: { message: string diff --git a/mobile/src/source-control/use-mobile-commit-failure-recovery.test.ts b/mobile/src/source-control/use-mobile-commit-failure-recovery.test.ts index d1058a32b89..f50634cb9c3 100644 --- a/mobile/src/source-control/use-mobile-commit-failure-recovery.test.ts +++ b/mobile/src/source-control/use-mobile-commit-failure-recovery.test.ts @@ -19,6 +19,7 @@ vi.mock('../components/host-protocol-gates-context', () => ({ })) import { useMobileCommitFailureRecovery } from './use-mobile-commit-failure-recovery' +import { stubRpcClient } from '../transport/rpc-client-test-stub' type Recovery = ReturnType @@ -47,8 +48,7 @@ function hostClient(launchReply: () => Promise) { } return launchReply() }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook reaches the client only through sendRequest. - return { client: { sendRequest } as unknown as RpcClient, sendRequest } + return { client: stubRpcClient({ sendRequest }), sendRequest } } const FAILURE: MobileCommitFailureRecovery = { diff --git a/mobile/src/source-control/use-mobile-commit-message-generation.test.ts b/mobile/src/source-control/use-mobile-commit-message-generation.test.ts index 7f3d2941162..c6bf1c907c3 100644 --- a/mobile/src/source-control/use-mobile-commit-message-generation.test.ts +++ b/mobile/src/source-control/use-mobile-commit-message-generation.test.ts @@ -1,7 +1,6 @@ import { createElement, startTransition, Suspense, useRef, useState } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { RpcClient } from '../transport/rpc-client' import type { MobileGenerateCommitMessageResult } from './mobile-commit-message-ai' vi.mock('../platform/haptics', () => ({ triggerError: vi.fn(), triggerSuccess: vi.fn() })) @@ -23,9 +22,9 @@ vi.mock('./mobile-commit-message-ai', () => ({ })) import { useMobileCommitMessageGeneration } from './use-mobile-commit-message-generation' +import { stubRpcClient } from '../transport/rpc-client-test-stub' -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked request module never reaches the client. -const client = {} as RpcClient +const client = stubRpcClient() const view: { generating: boolean message: string diff --git a/mobile/src/source-control/use-mobile-source-control-loaders.test.ts b/mobile/src/source-control/use-mobile-source-control-loaders.test.ts index 7e22420681d..d80587d96f3 100644 --- a/mobile/src/source-control/use-mobile-source-control-loaders.test.ts +++ b/mobile/src/source-control/use-mobile-source-control-loaders.test.ts @@ -6,6 +6,7 @@ import { createFakeRpcClient } from '../mobile-web-shell/bridge-host-test-fakes' import type { RpcClient } from '../transport/rpc-client' import type { ConnectionState, RpcResponse } from '../transport/types' import { useMobileSourceControlLoaders } from './use-mobile-source-control-loaders' +import { stubRpcClient } from '../transport/rpc-client-test-stub' vi.mock('expo-router', async () => { const React = await import('react') @@ -76,8 +77,7 @@ function fakeClient(calls: PendingCall[]): RpcClient { new Promise((resolve) => { calls.push({ method, params, settle: resolve }) }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these loaders reach `sendRequest` and nothing else on the client; the owner's scope holds the instance by identity without calling it. - return { sendRequest } as RpcClient + return stubRpcClient({ sendRequest }) } /** Settles the oldest unanswered call for `method`, which is what orders one attempt against another. */ diff --git a/mobile/src/source-control/use-mobile-source-control-openers.test.tsx b/mobile/src/source-control/use-mobile-source-control-openers.test.tsx index 3f02de94272..98965c1c650 100644 --- a/mobile/src/source-control/use-mobile-source-control-openers.test.tsx +++ b/mobile/src/source-control/use-mobile-source-control-openers.test.tsx @@ -3,7 +3,6 @@ import { act, create } from 'react-test-renderer' import { describe, expect, it, vi } from 'vitest' import { normalizeExecutionHostId } from '../../../src/shared/execution-host' import { WorkspaceExecutionHostContext } from '../navigation/workspace-execution-host' -import type { RpcClient } from '../transport/rpc-client' import type { RpcResponse } from '../transport/types' const route = vi.hoisted(() => { @@ -23,6 +22,7 @@ vi.mock('../navigation/route-handoff', () => ({ useRouteHandoff: () => route.rou vi.mock('../platform/haptics', () => ({ triggerError: vi.fn(), triggerSelection: vi.fn() })) import { useMobileSourceControlOpeners } from './use-mobile-source-control-openers' +import { stubRpcClient } from '../transport/rpc-client-test-stub' const ENTRY = { path: 'src/app.ts', status: 'modified', area: 'unstaged' } as const @@ -45,8 +45,7 @@ async function tapChangedFile(args: { } return args.openDiffReply } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the opener reaches `sendRequest` and nothing else on the client. - const client = { sendRequest } as RpcClient + const client = stubRpcClient({ sendRequest }) const held: { openFile: ((entry: typeof ENTRY) => Promise) | null } = { openFile: null } function Probe(): null { held.openFile = useMobileSourceControlOpeners({ diff --git a/mobile/src/source-control/use-mobile-source-control-openers.ts b/mobile/src/source-control/use-mobile-source-control-openers.ts index 7b94d25d7ae..94bf676ffc2 100644 --- a/mobile/src/source-control/use-mobile-source-control-openers.ts +++ b/mobile/src/source-control/use-mobile-source-control-openers.ts @@ -65,14 +65,13 @@ export function useMobileSourceControlOpeners(params: Params) { (filePath: string, area: MobileReviewRouteArea) => { triggerSelection() router.push( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the builder only emits the declared /h/[hostId]/review/[worktreeId] route. buildMobileReviewFileRoute({ hostId, worktreeId, worktreeName: name, filePath, area - }) as Parameters[0] + }) ) }, [hostId, name, router, worktreeId] diff --git a/mobile/src/tasks/agent-launch-mobile-replay.test.ts b/mobile/src/tasks/agent-launch-mobile-replay.test.ts index 51d790b85dd..9e9a7b3c8d7 100644 --- a/mobile/src/tasks/agent-launch-mobile-replay.test.ts +++ b/mobile/src/tasks/agent-launch-mobile-replay.test.ts @@ -3,7 +3,6 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createWorktreeWithNameRetry } from './worktree-create-retry' -import type { RpcClient } from '../transport/rpc-client' import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity' import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client' import { @@ -26,6 +25,7 @@ import { launchAgentInExistingWorkspace, reserveMobileAgentLaunch } from '../session/mobile-existing-agent-launch' +import { stubRpcClient } from '../transport/rpc-client-test-stub' const createStructuredSession = vi.fn() vi.mock('../../../src/main/runtime/rpc/methods/structured-agent-session-create', () => ({ @@ -70,8 +70,7 @@ function mobileLaunch( ) const operationId = `${Date.now()}-000000000000000000000000000000aa` const attempts: unknown[] = [] - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mobile retry loop reaches only these transport members; requests use the real host dispatcher. - const client = { + const client = stubRpcClient({ getState: () => 'connected', sendRequest: async (method: string, params: unknown) => { attempts.push(params) @@ -92,7 +91,7 @@ function mobileLaunch( } return response } - } as unknown as RpcClient + }) const result = createWorktreeWithNameRetry({ client, baseName: 'otter', @@ -181,8 +180,7 @@ describe('a + menu launch into an open workspace', () => { methods: AGENT_LAUNCH_METHODS }) const attempts: unknown[] = [] - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the replay loop reaches only these transport members; requests use the real host dispatcher. - const client = { + const client = stubRpcClient({ getState: () => 'connected', sendRequest: async (method: string, params: unknown) => { attempts.push(params) @@ -197,7 +195,7 @@ describe('a + menu launch into an open workspace', () => { } return response } - } as unknown as RpcClient + }) const reservation = reserveMobileAgentLaunch('aider') const launched = await launchAgentInExistingWorkspace({ @@ -233,12 +231,11 @@ describe('a + menu launch into an open workspace', () => { runtime: runtime as unknown as OrcaRuntimeService, methods: AGENT_LAUNCH_METHODS }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the launch reaches only these transport members; requests use the real host dispatcher. - const client = { + const client = stubRpcClient({ getState: () => 'connected', sendRequest: async (method: string, params: unknown) => dispatcher.dispatch({ id: 'request-1', authToken: 'token', method, params }) - } as unknown as RpcClient + }) const reservation = reserveMobileAgentLaunch('claude') const launched = await launchAgentInExistingWorkspace({ diff --git a/mobile/src/tasks/source-workspace-create.test.ts b/mobile/src/tasks/source-workspace-create.test.ts index 73064c4412c..a5d0c8ac191 100644 --- a/mobile/src/tasks/source-workspace-create.test.ts +++ b/mobile/src/tasks/source-workspace-create.test.ts @@ -37,7 +37,7 @@ const baseArgs = { note: undefined, worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT, // Existing cases keep pinning the legacy create; the launch cases opt in explicitly. - agentLaunchSupported: false + agentLaunchSupported: false as const } describe('createWorkspaceFromComposerSource', () => { diff --git a/mobile/src/tasks/task-source-search-reply-schema.test.ts b/mobile/src/tasks/task-source-search-reply-schema.test.ts index 1dfd4bc5f1e..07f773b2dee 100644 --- a/mobile/src/tasks/task-source-search-reply-schema.test.ts +++ b/mobile/src/tasks/task-source-search-reply-schema.test.ts @@ -163,7 +163,7 @@ describe('the Linear issue list takes both shapes the picker has always accepted // Adopting the item half's `linearIssueRowSchema` adds five requirements to this path. Each one // is non-optional on the host's own `LinearIssue`, and each is rendered by the picker row. - it.each(['identifier', 'title', 'url', 'updatedAt', 'labels'])( + it.each(['identifier', 'title', 'url', 'updatedAt', 'labels'] as const)( 'drops an issue with no %s, which the host type declares non-optional', (member) => { const { [member]: _dropped, ...without } = linearRow diff --git a/mobile/src/terminal/TerminalWebView.web.tsx b/mobile/src/terminal/TerminalWebView.web.tsx index 38de971d587..d8acf8b3a58 100644 --- a/mobile/src/terminal/TerminalWebView.web.tsx +++ b/mobile/src/terminal/TerminalWebView.web.tsx @@ -69,9 +69,9 @@ export const TerminalWebView = forwardRef( receiveRef.current = receive }, [receive]) useEffect(() => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: react-native-web renders View as a div and forwards the ref to it; this module only ever runs in that build. - const host = hostRef.current as unknown as HTMLElement | null - if (!host) { + // react-native-web renders View as a div and forwards the ref to it. + const host = hostRef.current + if (!(host instanceof HTMLElement)) { return } let live diff --git a/mobile/src/terminal/buffered-terminal-draft-restoration.test.ts b/mobile/src/terminal/buffered-terminal-draft-restoration.test.ts index 218feba117c..76cff8319c2 100644 --- a/mobile/src/terminal/buffered-terminal-draft-restoration.test.ts +++ b/mobile/src/terminal/buffered-terminal-draft-restoration.test.ts @@ -51,7 +51,7 @@ describe('buffered terminal draft restoration', () => { const rejectedDraft = 'rejected command' const pendingRestorations = new Map() const token = beginBufferedTerminalDraftRestoration(pendingRestorations, terminal) - let drafts = { [terminal]: rejectedDraft } + let drafts: Record = { [terminal]: rejectedDraft } drafts = updateBufferedTerminalDraft(drafts, terminal, '') invalidateBufferedTerminalDraftRestoration(pendingRestorations, terminal) drafts = updateBufferedTerminalDraft(drafts, terminal, 'new command') @@ -71,7 +71,10 @@ describe('buffered terminal draft restoration', () => { const sendOrigin = activeHandle const pendingRestorations = new Map() const token = beginBufferedTerminalDraftRestoration(pendingRestorations, sendOrigin) - let drafts = { [terminalA]: rejectedDraft, [terminalB]: 'new command for B' } + let drafts: Record = { + [terminalA]: rejectedDraft, + [terminalB]: 'new command for B' + } drafts = updateBufferedTerminalDraft(drafts, sendOrigin, '') activeHandle = terminalB diff --git a/mobile/src/terminal/document/host-notify.ts b/mobile/src/terminal/document/host-notify.ts index 43c1042b5a9..b7b48c12038 100644 --- a/mobile/src/terminal/document/host-notify.ts +++ b/mobile/src/terminal/document/host-notify.ts @@ -16,14 +16,18 @@ export function notify(scope: TerminalDocumentScope, msg: Record { vi.spyOn(owner, 'addEventListener').mockImplementation(function ( this: EventTarget, type: string, - listener: EventListenerOrEventListenerObject, + listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions ) { addEventListener.call(this, type, listener, options) - if (!activeRegistration(this, type, listener, options)) { + if (listener && !activeRegistration(this, type, listener, options)) { registrations.push({ capture: capture(options), listener, @@ -95,11 +95,11 @@ function trackEventListenerCleanup(): () => { vi.spyOn(owner, 'removeEventListener').mockImplementation(function ( this: EventTarget, type: string, - listener: EventListenerOrEventListenerObject, + listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions ) { removeEventListener.call(this, type, listener, options) - const registration = activeRegistration(this, type, listener, options) + const registration = listener && activeRegistration(this, type, listener, options) if (registration) { registration.removed = true } diff --git a/mobile/src/terminal/terminal-live-accessory-raw-send.ts b/mobile/src/terminal/terminal-live-accessory-raw-send.ts index 2133d1855ac..fdb6cd28e8f 100644 --- a/mobile/src/terminal/terminal-live-accessory-raw-send.ts +++ b/mobile/src/terminal/terminal-live-accessory-raw-send.ts @@ -2,11 +2,11 @@ import { reportWorkerTerminalUserInput } from './worker-terminal-takeover-report import { getTerminalLiveAccessoryRawSendTarget } from './terminal-live-accessory-raw-send-target' import { buildTerminalSendParams, TERMINAL_INPUT_SEND_OPTIONS } from './terminal-send-request' import { terminalInputSend } from './mobile-terminal-operations' -import type { RpcClient } from '../transport/rpc-client' +import type { RpcOperationSender } from '../transport/rpc-operation-sender' import type { ConnectionState } from '../transport/types' type TerminalLiveAccessoryRawSendArgs = { - readonly client: RpcClient | null + readonly client: RpcOperationSender | null readonly targetHandle: string readonly activeHandle: string | null readonly activeSessionTabType: string | null diff --git a/mobile/src/terminal/terminal-live-text-commit.test.ts b/mobile/src/terminal/terminal-live-text-commit.test.ts index ac177542be8..6261671a218 100644 --- a/mobile/src/terminal/terminal-live-text-commit.test.ts +++ b/mobile/src/terminal/terminal-live-text-commit.test.ts @@ -129,7 +129,7 @@ describe('terminal live accessory local edit text', () => { let visits = 0 const iterator = vi.spyOn(String.prototype, Symbol.iterator).mockImplementation(function* ( this: string - ) { + ): Generator { for (const codePoint of originalIterator.call(this)) { visits += 1 yield codePoint diff --git a/mobile/src/terminal/terminal-webview-engine-error-state.tsx b/mobile/src/terminal/terminal-webview-engine-error-state.tsx index 0ba3e79db8d..4cb292c0477 100644 --- a/mobile/src/terminal/terminal-webview-engine-error-state.tsx +++ b/mobile/src/terminal/terminal-webview-engine-error-state.tsx @@ -26,7 +26,6 @@ export function useTerminalWebViewEngineErrorState(onEngineError?: (message: str const reportEngineError = useCallback( (message: string, fatal: boolean) => { onEngineError?.(message) - // eslint-disable-next-line no-console console.warn('[terminal-webview] engine error', message) if (fatal) { // Why: the first fatal report is the root cause; later cascades (e.g. the diff --git a/mobile/src/terminal/terminal-webview-engine-error.test.ts b/mobile/src/terminal/terminal-webview-engine-error.test.ts index 2a1ba5df93d..792c016ec46 100644 --- a/mobile/src/terminal/terminal-webview-engine-error.test.ts +++ b/mobile/src/terminal/terminal-webview-engine-error.test.ts @@ -4,6 +4,7 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import { TerminalWebView } from './TerminalWebView' import type { TerminalWebViewHandle } from './terminal-webview-contract' +import { findAllByHostType, findByHostType } from '../test-support/react-test-renderer-host-nodes' const nativeWebViewMethods = vi.hoisted(() => ({ postMessage: vi.fn<(message: string) => void>(), @@ -49,10 +50,11 @@ function createTerminalWebViewRenderer( onEngineError = vi.fn(), props: Record = {} ) { - let renderer: ReactTestRenderer | null = null + const mounted: { renderer?: ReactTestRenderer } = {} act(() => { - renderer = create(createElement(TerminalWebView, { onEngineError, ...props })) + mounted.renderer = create(createElement(TerminalWebView, { onEngineError, ...props })) }) + const renderer = mounted.renderer if (!renderer) { throw new Error('TerminalWebView did not render') } @@ -61,15 +63,14 @@ function createTerminalWebViewRenderer( } function postWebViewMessage(renderer: ReactTestRenderer, payload: Record) { - const webView = renderer.root.findByType('WebView') + const webView = findByHostType(renderer.root, 'WebView') act(() => { webView.props.onMessage({ nativeEvent: { data: JSON.stringify(payload) } }) }) } function renderedText(renderer: ReactTestRenderer): string { - return renderer.root - .findAllByType('Text') + return findAllByHostType(renderer.root, 'Text') .flatMap((node) => node.props.children) .join(' ') } @@ -238,7 +239,7 @@ describe('TerminalWebView engine errors', () => { }) postWebViewMessage(renderer, { type: 'web-ready' }) nativeWebViewMethods.postMessage.mockClear() - const webView = renderer.root.findByType('WebView') + const webView = findByHostType(renderer.root, 'WebView') act(() => { webView.props.onContentProcessDidTerminate({ nativeEvent: {} }) diff --git a/mobile/src/terminal/terminal-webview-init-surface.test.ts b/mobile/src/terminal/terminal-webview-init-surface.test.ts index 1ac3c89fc18..2418f6773bc 100644 --- a/mobile/src/terminal/terminal-webview-init-surface.test.ts +++ b/mobile/src/terminal/terminal-webview-init-surface.test.ts @@ -123,7 +123,6 @@ describe('terminal WebView init surface replacement', () => { } as unknown as new (options: TerminalOptions) => TerminalStub webWindow.ReactNativeWebView = { postMessage: vi.fn() } document.body.innerHTML = bodyMarkup() - // eslint-disable-next-line no-new-func new Function(TERMINAL_DOCUMENT_SCRIPT)() }) diff --git a/mobile/src/terminal/terminal-webview-notification-dispatch.ts b/mobile/src/terminal/terminal-webview-notification-dispatch.ts index fb2bf780c98..de6bad9537d 100644 --- a/mobile/src/terminal/terminal-webview-notification-dispatch.ts +++ b/mobile/src/terminal/terminal-webview-notification-dispatch.ts @@ -19,7 +19,6 @@ export function dispatchTerminalWebViewNotification( if (msg.type === 'log') { // Surface fit-scale diagnostics in the RN/Metro console. const tag = typeof msg.tag === 'string' ? msg.tag : '[fit]' - // eslint-disable-next-line no-console console.log(tag, msg.payload) } else if (msg.type === 'error') { const message = typeof msg.message === 'string' ? msg.message : 'Unknown terminal error' @@ -86,7 +85,6 @@ export function dispatchTerminalWebViewNotification( handlers.onTextScaleChange?.(scale) } } else if (msg.type === 'mobile-clip-cancel-by-pinch') { - // eslint-disable-next-line no-console console.warn('[mobile-clip] selection cancelled by pinch') } } diff --git a/mobile/src/terminal/terminal-webview-query-reply-routing.test.ts b/mobile/src/terminal/terminal-webview-query-reply-routing.test.ts index af53772d481..689efae00cf 100644 --- a/mobile/src/terminal/terminal-webview-query-reply-routing.test.ts +++ b/mobile/src/terminal/terminal-webview-query-reply-routing.test.ts @@ -2,6 +2,7 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import { TerminalWebView } from './TerminalWebView' +import { findByHostType } from '../test-support/react-test-renderer-host-nodes' vi.mock('react-native', () => ({ Platform: { OS: 'ios' }, @@ -37,7 +38,7 @@ function postWebViewMessage(payload: Record): void { if (!renderer) { throw new Error('TerminalWebView did not render') } - const webView = renderer.root.findByType('WebView') + const webView = findByHostType(renderer.root, 'WebView') act(() => { webView.props.onMessage({ nativeEvent: { data: JSON.stringify(payload) } }) }) diff --git a/mobile/src/terminal/terminal-webview-tap-routing.test.ts b/mobile/src/terminal/terminal-webview-tap-routing.test.ts index 439f3446e56..05f1c7b6139 100644 --- a/mobile/src/terminal/terminal-webview-tap-routing.test.ts +++ b/mobile/src/terminal/terminal-webview-tap-routing.test.ts @@ -89,7 +89,6 @@ function boot( } } document.body.innerHTML = bodyMarkup() - // eslint-disable-next-line no-new-func // The bundle the WebView loads, run as the WebView runs it. new Function(TERMINAL_DOCUMENT_SCRIPT)() window.dispatchEvent( diff --git a/mobile/src/terminal/terminal-webview-wheel-scroll.test.ts b/mobile/src/terminal/terminal-webview-wheel-scroll.test.ts index f5e6be8fdb5..a3b71d60db6 100644 --- a/mobile/src/terminal/terminal-webview-wheel-scroll.test.ts +++ b/mobile/src/terminal/terminal-webview-wheel-scroll.test.ts @@ -1,5 +1,5 @@ // @vitest-environment happy-dom -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest' import { TERMINAL_DOCUMENT_SCRIPT } from './terminal-webview-document-script.generated' import { TERMINAL_DOCUMENT_MARKUP } from './terminal-webview-html' @@ -115,9 +115,9 @@ function terminalInputBytes(postMessage: ReturnType): string { describe('terminal WebView external pointer wheel scrolling', () => { let animationFrames: Array<() => void> let buffer: BufferState - let postMessage: ReturnType + let postMessage: Mock<(data: string) => void> let registeredWindowListeners: RegisteredWindowListener[] - let scrollLines: ReturnType + let scrollLines: Mock<(lines: number) => void> let terminals: TerminalStub[] function boot(): void { @@ -145,7 +145,7 @@ describe('terminal WebView external pointer wheel scrolling', () => { animationFrames = [] buffer = { baseY: 0, type: 'normal', viewportY: 0 } registeredWindowListeners = [] - scrollLines = vi.fn() + scrollLines = vi.fn<(lines: number) => void>() terminals = [] const addWindowEventListener = window.addEventListener.bind(window) vi.spyOn(window, 'addEventListener').mockImplementation((( @@ -163,7 +163,7 @@ describe('terminal WebView external pointer wheel scrolling', () => { vi.stubGlobal('cancelAnimationFrame', () => {}) Object.defineProperty(window, 'innerWidth', { value: 381, configurable: true }) Object.defineProperty(window, 'innerHeight', { value: 612, configurable: true }) - postMessage = vi.fn() + postMessage = vi.fn<(data: string) => void>() const webWindow = window as unknown as { Terminal: new () => TerminalStub ReactNativeWebView: { postMessage: (data: string) => void } diff --git a/mobile/src/terminal/use-buffered-terminal-drafts.test.tsx b/mobile/src/terminal/use-buffered-terminal-drafts.test.tsx index f41e71ed3e9..60a86a74ac1 100644 --- a/mobile/src/terminal/use-buffered-terminal-drafts.test.tsx +++ b/mobile/src/terminal/use-buffered-terminal-drafts.test.tsx @@ -182,7 +182,7 @@ describe('useBufferedTerminalDrafts', () => { renderer = create(createElement(Probe, { activeHandle: 'terminal' })) }) act(() => hook().setInput('rejected command')) - let send: ReturnType + let send!: ReturnType act(() => { send = hook().beginBufferedTerminalDraftSend('terminal', hook().input) }) diff --git a/mobile/src/terminal/worker-terminal-takeover-report.ts b/mobile/src/terminal/worker-terminal-takeover-report.ts index 91151123687..bf2f2d6a0ac 100644 --- a/mobile/src/terminal/worker-terminal-takeover-report.ts +++ b/mobile/src/terminal/worker-terminal-takeover-report.ts @@ -1,7 +1,7 @@ -import type { RpcClient } from '../transport/rpc-client' +import type { RpcOperationSender } from '../transport/rpc-operation-sender' import { workerTerminalTakeoverReport } from './mobile-terminal-operations' -type ReportClient = RpcClient +type ReportClient = RpcOperationSender const REPORT_INTERVAL_MS = 30_000 const REPORT_RETRY_DELAY_MS = 250 let reportsByClient = new WeakMap>() diff --git a/mobile/src/test-support/react-test-renderer-host-nodes.ts b/mobile/src/test-support/react-test-renderer-host-nodes.ts new file mode 100644 index 00000000000..f1325696fef --- /dev/null +++ b/mobile/src/test-support/react-test-renderer-host-nodes.ts @@ -0,0 +1,11 @@ +import type { ReactTestInstance } from 'react-test-renderer' + +// Mocked react-native modules render host components as bare names ('Pressable', 'Text'), which +// findAllByType's ElementType parameter cannot express. +export function findAllByHostType(root: ReactTestInstance, hostType: string): ReactTestInstance[] { + return root.findAll((node) => node.type === hostType) +} + +export function findByHostType(root: ReactTestInstance, hostType: string): ReactTestInstance { + return root.find((node) => node.type === hostType) +} diff --git a/mobile/src/test-support/rpc-recording/native-mounting-substitutes.test.ts b/mobile/src/test-support/rpc-recording/native-mounting-substitutes.test.ts index 28564a7062e..c912ab6e859 100644 --- a/mobile/src/test-support/rpc-recording/native-mounting-substitutes.test.ts +++ b/mobile/src/test-support/rpc-recording/native-mounting-substitutes.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { nativeMountingSubstitutes } from './native-mounting-substitutes' /** TypeScript's emitted interop helper, verbatim: what every `import X from` in a mounted module runs. */ -function importDefault(module: unknown): { default: unknown } { +function importDefault(module: unknown): { default?: unknown } { // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: mirrors the emit, which reads the marker off an untyped module record. const record = module as { __esModule?: unknown; default?: unknown } return record?.__esModule ? record : { default: module } diff --git a/mobile/src/test-support/rpc-recording/operation-module-loader.test.ts b/mobile/src/test-support/rpc-recording/operation-module-loader.test.ts index 3e1927989f9..54303d3f5f5 100644 --- a/mobile/src/test-support/rpc-recording/operation-module-loader.test.ts +++ b/mobile/src/test-support/rpc-recording/operation-module-loader.test.ts @@ -1,7 +1,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { createElement } from 'react' +import { createElement, type ReactNode } from 'react' import { act, create } from 'react-test-renderer' import { afterAll, describe, expect, it } from 'vitest' import { operationModuleLoader } from './operation-module-loader' @@ -130,7 +130,7 @@ describe('the mounted module loader', () => { } ` }) - const { Screen } = modules.load<{ Screen: (props: { label: string }) => unknown }>( + const { Screen } = modules.load<{ Screen: (props: { label: string }) => ReactNode }>( 'mobile/src/screen.tsx' ) let rendered: ReturnType | undefined diff --git a/mobile/src/test-support/rpc-recording/screen-native-substitutes.test.ts b/mobile/src/test-support/rpc-recording/screen-native-substitutes.test.ts index a16a9e25763..425ac1aa2d5 100644 --- a/mobile/src/test-support/rpc-recording/screen-native-substitutes.test.ts +++ b/mobile/src/test-support/rpc-recording/screen-native-substitutes.test.ts @@ -56,11 +56,16 @@ describe('the inert screen substitutes', () => { let rendered: ReturnType | undefined act(() => { rendered = create( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the substitute is an inert element component by construction. - createElement(member('react-native', 'Pressable') as ElementType, {}, () => { - invoked++ - return null - }) + createElement( + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the substitute is an inert element component by construction. + member('react-native', 'Pressable') as ElementType, + {}, + // @ts-expect-error -- deliberately invalid function children; asserts the substitute drops them + () => { + invoked++ + return null + } + ) ) }) expect(invoked).toBe(0) diff --git a/mobile/src/transport/client-context.test.ts b/mobile/src/transport/client-context.test.ts index 1147be9a501..df36a807f52 100644 --- a/mobile/src/transport/client-context.test.ts +++ b/mobile/src/transport/client-context.test.ts @@ -542,7 +542,7 @@ describe('useHostClient', () => { expect(connectMock).not.toHaveBeenCalled() expect(fake.closeMock).not.toHaveBeenCalled() - act(() => renderer.unmount()) + act(() => renderer?.unmount()) }) it('does not open a client after provider unmount during an in-flight lookup', async () => { diff --git a/mobile/src/transport/client-context.tsx b/mobile/src/transport/client-context.tsx index c765f7a0daf..518d6727e41 100644 --- a/mobile/src/transport/client-context.tsx +++ b/mobile/src/transport/client-context.tsx @@ -290,7 +290,6 @@ export function RpcClientProvider({ children }: { children: ReactNode }) { ) // Close all clients on provider unmount. Empty deps: [closeEntry] would let Fast Refresh tear down all live sockets. - // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { const store = storeRef.current return () => { diff --git a/mobile/src/transport/host-credential-cleanup.test.ts b/mobile/src/transport/host-credential-cleanup.test.ts index c85b034c007..a5764f9daa3 100644 --- a/mobile/src/transport/host-credential-cleanup.test.ts +++ b/mobile/src/transport/host-credential-cleanup.test.ts @@ -119,7 +119,7 @@ describe('host credential cleanup', () => { it('clears a timed-out pending entry when the native delete later succeeds', async () => { vi.useFakeTimers() - let resolveDelete: (() => void) | null = null + let resolveDelete: (() => void) | undefined const deleteCredential = vi.fn( () => new Promise((resolve) => { @@ -132,7 +132,7 @@ describe('host credential cleanup', () => { expect(deleteCredential).toHaveBeenCalledOnce() await vi.advanceTimersByTimeAsync(3_000) await expect(loadPendingHostCredentialCleanupIds()).resolves.toEqual(['host-1']) - expect(resolveDelete).not.toBeNull() + expect(resolveDelete).toBeDefined() resolveDelete?.() await vi.waitFor(async () => { @@ -141,7 +141,7 @@ describe('host credential cleanup', () => { }) it('joins a concurrently observed delete instead of stacking native calls', async () => { - let resolveDelete: (() => void) | null = null + let resolveDelete: (() => void) | undefined const deleteCredential = vi.fn( () => new Promise((resolve) => { @@ -233,7 +233,7 @@ describe('host credential cleanup', () => { }) it('clears the fallback handle once the native delete finally succeeds', async () => { - let resolveDelete: (() => void) | null = null + let resolveDelete: (() => void) | undefined const deleteCredential = vi.fn( () => new Promise((resolve) => { diff --git a/mobile/src/transport/host-edit-navigation.test.ts b/mobile/src/transport/host-edit-navigation.test.ts index bcc4c36181e..89c232e6c1c 100644 --- a/mobile/src/transport/host-edit-navigation.test.ts +++ b/mobile/src/transport/host-edit-navigation.test.ts @@ -119,7 +119,8 @@ describe('mobile host edit navigation', () => { { push: () => { throw error - } + }, + replace: vi.fn() }, 'host-1' ) diff --git a/mobile/src/transport/host-removal-lifecycle.test.ts b/mobile/src/transport/host-removal-lifecycle.test.ts index a30ea66b15a..aa23d3f592f 100644 --- a/mobile/src/transport/host-removal-lifecycle.test.ts +++ b/mobile/src/transport/host-removal-lifecycle.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const removeHostMock = vi.hoisted(() => vi.fn()) -const unregisterPushMock = vi.hoisted(() => vi.fn(async () => vi.fn())) -const forgetUpdateFailuresMock = vi.hoisted(() => vi.fn(async () => undefined)) -const deletePageCacheMock = vi.hoisted(() => vi.fn(async () => undefined)) +const unregisterPushMock = vi.hoisted(() => vi.fn(async (_hostId: string) => vi.fn())) +const forgetUpdateFailuresMock = vi.hoisted(() => vi.fn(async (_hostId: string) => undefined)) +const deletePageCacheMock = vi.hoisted(() => vi.fn(async (_hostId: string) => undefined)) vi.mock('./host-store', () => ({ removeHost: (hostId: string) => removeHostMock(hostId) @@ -29,7 +29,7 @@ describe('host removal lifecycle', () => { }) it('closes the client only after metadata removal commits', async () => { - let commitRemoval: (() => void) | null = null + let commitRemoval: (() => void) | undefined removeHostMock.mockReturnValue(new Promise((resolve) => (commitRemoval = resolve))) const closeHostClient = vi.fn() const removal = removeHostAndCloseClient('host-1', closeHostClient) diff --git a/mobile/src/transport/host-status-capability-ignorability.test.ts b/mobile/src/transport/host-status-capability-ignorability.test.ts index 912d006b064..c908f1d0710 100644 --- a/mobile/src/transport/host-status-capability-ignorability.test.ts +++ b/mobile/src/transport/host-status-capability-ignorability.test.ts @@ -17,6 +17,7 @@ import { import type { HostStatusReply } from './host-status-reply-schema' import type { RpcClient } from './rpc-client' import type { RpcResponse } from './types' +import { stubRpcClient } from './rpc-client-test-stub' const recordHostAppVersionMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)) @@ -65,8 +66,7 @@ function statusReply(capabilities: readonly string[]): RpcResponse { /** Answers every method with the one status reply, which is all any reader under test asks for. */ function clientAnswering(reply: RpcResponse): RpcClient { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every reader below reaches this client only through an rpc operation's `request`, which uses sendRequest alone; the rest of RpcClient is streaming and lifecycle none of them touch. - return { sendRequest: vi.fn().mockResolvedValue(reply) } as unknown as RpcClient + return stubRpcClient({ sendRequest: vi.fn().mockResolvedValue(reply) }) } async function renderGates(client: RpcClient): Promise { diff --git a/mobile/src/transport/host-status-gates.test.ts b/mobile/src/transport/host-status-gates.test.ts index 04de1f0e4fe..d473f7a9ea9 100644 --- a/mobile/src/transport/host-status-gates.test.ts +++ b/mobile/src/transport/host-status-gates.test.ts @@ -35,9 +35,9 @@ describe('useHostStatusGates', () => { } }) const newClient = { sendRequest: newSendRequest } as unknown as RpcClient - let gates: HostStatusGates | null = null + let gates: HostStatusGates | undefined const firstRenderByHost = new Map() - let renderer: ReactTestRenderer | null = null + let renderer: ReactTestRenderer | undefined function Probe({ hostId, client }: { hostId: string; client: RpcClient }): null { gates = useHostStatusGates({ hostId, client, connState: 'connected' }) @@ -99,8 +99,8 @@ describe('useHostStatusGates', () => { } }) const client = { sendRequest } as unknown as RpcClient - let gates: HostStatusGates | null = null - let renderer: ReactTestRenderer | null = null + let gates: HostStatusGates | undefined + let renderer: ReactTestRenderer | undefined function Probe({ hostId }: { hostId: string }): null { gates = useHostStatusGates({ hostId, client, connState: 'connected' }) @@ -142,8 +142,8 @@ describe('useHostStatusGates', () => { }) .mockReturnValueOnce(pendingReconnect) const client = { sendRequest } as unknown as RpcClient - let gates: HostStatusGates | null = null - let renderer: ReactTestRenderer | null = null + let gates: HostStatusGates | undefined + let renderer: ReactTestRenderer | undefined function Probe({ connState }: { connState: 'connected' | 'disconnected' }): null { gates = useHostStatusGates({ hostId: 'host-1', client, connState }) @@ -203,8 +203,8 @@ describe('useHostStatusGates', () => { const secondClient = { sendRequest: vi.fn().mockReturnValue(new Promise(() => {})) } as unknown as RpcClient - let gates: HostStatusGates | null = null - let renderer: ReactTestRenderer | null = null + let gates: HostStatusGates | undefined + let renderer: ReactTestRenderer | undefined function Probe({ client }: { client: RpcClient }): null { gates = useHostStatusGates({ hostId: 'host-1', client, connState: 'connected' }) @@ -241,8 +241,8 @@ describe('useHostStatusGates', () => { } }) const client = { sendRequest } as unknown as RpcClient - let gates: HostStatusGates | null = null - let renderer: ReactTestRenderer | null = null + let gates: HostStatusGates | undefined + let renderer: ReactTestRenderer | undefined function Probe(): null { gates = useHostStatusGates({ hostId: 'host-1', client, connState: 'connected' }) @@ -288,8 +288,8 @@ describe('useHostStatusGates', () => { .mockResolvedValueOnce({ ok: false, error: { code: 'unavailable', message: 'busy' } }) .mockResolvedValueOnce({ ok: true, result: { capabilities: ['browser.screencast.v1'] } }) const client = { sendRequest } as unknown as RpcClient - let gates: HostStatusGates | null = null - let renderer: ReactTestRenderer | null = null + let gates: HostStatusGates | undefined + let renderer: ReactTestRenderer | undefined function Probe(): null { gates = useHostStatusGates({ hostId: 'host-1', client, connState: 'connected' }) @@ -321,8 +321,8 @@ describe('useHostStatusGates', () => { vi.useFakeTimers() const sendRequest = vi.fn().mockResolvedValue({ ok: true, result: 'not a status' }) const client = { sendRequest } as unknown as RpcClient - let gates: HostStatusGates | null = null - let renderer: ReactTestRenderer | null = null + let gates: HostStatusGates | undefined + let renderer: ReactTestRenderer | undefined function Probe(): null { gates = useHostStatusGates({ hostId: 'host-1', client, connState: 'connected' }) @@ -349,7 +349,7 @@ describe('useHostStatusGates', () => { vi.useFakeTimers() const sendRequest = vi.fn().mockRejectedValue(new Error('request timed out')) const client = { sendRequest } as unknown as RpcClient - let renderer: ReactTestRenderer | null = null + let renderer: ReactTestRenderer | undefined function Probe(): null { useHostStatusGates({ hostId: 'host-1', client, connState: 'connected' }) @@ -363,7 +363,7 @@ describe('useHostStatusGates', () => { }) await act(async () => { renderer?.unmount() - renderer = null + renderer = undefined }) await act(async () => { await vi.advanceTimersByTimeAsync(60_000) diff --git a/mobile/src/transport/host-store.test.ts b/mobile/src/transport/host-store.test.ts index 60269c01d5e..e79bd095a12 100644 --- a/mobile/src/transport/host-store.test.ts +++ b/mobile/src/transport/host-store.test.ts @@ -573,7 +573,7 @@ describe('host-store list mutations', () => { }) it('awaits cleanup scheduling after metadata commit', async () => { - let resolveSchedule: (() => void) | null = null + let resolveSchedule: (() => void) | undefined scheduleCleanupMock.mockReturnValue( new Promise((resolve) => { resolveSchedule = resolve @@ -598,7 +598,7 @@ describe('host-store list mutations', () => { }) it('applies concurrent rename and remove without clobbering either', async () => { - let releaseReads: (() => void) | null = null + let releaseReads: (() => void) | undefined const readsReleased = new Promise((resolve) => { releaseReads = resolve }) diff --git a/mobile/src/transport/mobile-endpoint-supervisor-nudge.test.ts b/mobile/src/transport/mobile-endpoint-supervisor-nudge.test.ts index 6fd90b8f86d..e0690801366 100644 --- a/mobile/src/transport/mobile-endpoint-supervisor-nudge.test.ts +++ b/mobile/src/transport/mobile-endpoint-supervisor-nudge.test.ts @@ -138,7 +138,7 @@ describe('mobile endpoint supervisor nudges', () => { it('queues a network replacement that lands while another dial owns the mutex', async () => { const logical = new FakeLogicalClient('disconnected', 'lan') - let resolveWrite: (() => void) | null = null + let resolveWrite: (() => void) | undefined const deps = dependencies({ openDirect: vi.fn(() => new FakeSession('disconnected')), writeBundle: vi diff --git a/mobile/src/transport/mobile-endpoint-supervisor-test-fakes.ts b/mobile/src/transport/mobile-endpoint-supervisor-test-fakes.ts index 85e320618ce..21f2fa7e6ff 100644 --- a/mobile/src/transport/mobile-endpoint-supervisor-test-fakes.ts +++ b/mobile/src/transport/mobile-endpoint-supervisor-test-fakes.ts @@ -4,14 +4,18 @@ import type { MobileRelayRpcSession } from './mobile-relay-rpc-session' import { RelayDialStageTracker, type RelayDialStage } from './relay-dial-stage' import { defaultCancelTimer, defaultScheduleTimer } from './timer-scheduler' import type { MobileEndpointSupervisorDependencies } from './mobile-endpoint-supervisor' -import type { RpcClient } from './rpc-client' +import type { RpcClient, SendRequestOptions } from './rpc-client' import type { RelayHostReachability } from './relay-host-reachability' import type { MobileConnectionPath, StableLogicalRpcClient } from './stable-logical-rpc-client' import type { ConnectionState, HostProfile, RpcResponse } from './types' export class FakeSession implements RpcClient { readonly sendRequest = vi.fn( - async (_method: string, _params?: unknown): Promise => ({ + async ( + _method: string, + _params?: unknown, + _options?: SendRequestOptions + ): Promise => ({ id: 'rpc-1', ok: true, result: {}, diff --git a/mobile/src/transport/mobile-relay-background-lifecycle.test.ts b/mobile/src/transport/mobile-relay-background-lifecycle.test.ts index 9df36203aa1..74068c6ea35 100644 --- a/mobile/src/transport/mobile-relay-background-lifecycle.test.ts +++ b/mobile/src/transport/mobile-relay-background-lifecycle.test.ts @@ -165,7 +165,7 @@ describe('mobile Relay background lifecycle', () => { }) it('arms lease rotation when confirmation persistence finishes during the grace', async () => { - let finishWrite: (() => void) | null = null + let finishWrite: (() => void) | undefined const writeStarted = new Promise((resolve) => { finishWrite = resolve }) diff --git a/mobile/src/transport/mobile-relay-direct-upgrade.test.ts b/mobile/src/transport/mobile-relay-direct-upgrade.test.ts index d76e887b299..af34cb54092 100644 --- a/mobile/src/transport/mobile-relay-direct-upgrade.test.ts +++ b/mobile/src/transport/mobile-relay-direct-upgrade.test.ts @@ -107,7 +107,7 @@ describe('existing direct pairing relay upgrade', () => { expect(journal).not.toBeNull() expect(deps.writeJournal.mock.invocationCallOrder[0]).toBeLessThan( - client.sendRequest.mock.invocationCallOrder[0]! + vi.mocked(client.sendRequest).mock.invocationCallOrder[0]! ) expect(client.sendRequest).toHaveBeenNthCalledWith(2, 'pairing.provisionRelay', { reqId: journal!.reqId, diff --git a/mobile/src/transport/mobile-relay-e2ee-link.test.ts b/mobile/src/transport/mobile-relay-e2ee-link.test.ts index 8cd5393b553..e076f801f84 100644 --- a/mobile/src/transport/mobile-relay-e2ee-link.test.ts +++ b/mobile/src/transport/mobile-relay-e2ee-link.test.ts @@ -28,7 +28,7 @@ class ThrowingSocket { onmessage: ((event: { data: unknown }) => void) | null = null onerror: (() => void) | null = null onclose: ((event: { code: number; reason?: string }) => void) | null = null - send = vi.fn(() => { + send = vi.fn((_frame: string): void => { throw new Error('relay auth write failed') }) close = vi.fn() diff --git a/mobile/src/transport/mobile-relay-pairing-recovery.test.ts b/mobile/src/transport/mobile-relay-pairing-recovery.test.ts index 30aaac8ca59..1bb4a01471d 100644 --- a/mobile/src/transport/mobile-relay-pairing-recovery.test.ts +++ b/mobile/src/transport/mobile-relay-pairing-recovery.test.ts @@ -6,7 +6,11 @@ import { resetMobileRelayPairingRecoveryForTests, settleMobileRelayPairingRecovery } from './mobile-relay-pairing-recovery' -import { RelayOuterError, type PairingCandidateClient } from './mobile-relay-physical-client' +import { + RelayOuterError, + type connectMobileRelayForPairing, + type PairingCandidateClient +} from './mobile-relay-physical-client' import type { HostProfile, PairingOffer, RpcResponse } from './types' vi.mock('react-native', () => ({ Platform: { OS: 'ios' } })) @@ -36,10 +40,9 @@ function response(result: unknown): RpcResponse { return { id: 'rpc-1', ok: true, result, _meta: { runtimeId: 'runtime-1' } } } -function installed( - journal: ReturnType, - mode: 'authenticated-direct' | 'relay-basis' -) { +type TestJournal = ReturnType + +function installed(journal: TestJournal, mode: 'authenticated-direct' | 'relay-basis') { return { v: 1 as const, reqId: journal.metadata.installReqId, @@ -50,7 +53,7 @@ function installed( } function endpoints( - journal: ReturnType, + journal: TestJournal, state: { state: 'not-found' } | { state: 'committed'; result: ReturnType } ) { return { @@ -90,8 +93,8 @@ function client(handler: (method: string, params: unknown) => Promise - connectRelay: ReturnType + journal: TestJournal + connectRelay: typeof connectMobileRelayForPairing bundle?: MobileRelayCredentialBundle | null hosts?: HostProfile[] }) { @@ -103,7 +106,7 @@ function dependencies(args: { clearJournal: vi.fn(async () => {}), claimJournal: vi.fn(async () => () => {}), readCredentialBundle: vi.fn(async () => args.bundle ?? null), - writeCredentialBundle: vi.fn(async () => {}), + writeCredentialBundle: vi.fn(async (_bundle: MobileRelayCredentialBundle) => {}), loadHosts: vi.fn(async (): Promise => args.hosts ?? []), savePairedHost: vi.fn(async () => {}), connectRelay: args.connectRelay, diff --git a/mobile/src/transport/mobile-relay-physical-client.test.ts b/mobile/src/transport/mobile-relay-physical-client.test.ts index a75a8b55640..e5ab2bd2b00 100644 --- a/mobile/src/transport/mobile-relay-physical-client.test.ts +++ b/mobile/src/transport/mobile-relay-physical-client.test.ts @@ -7,7 +7,7 @@ const fakes = vi.hoisted(() => ({ }, start: vi.fn(), handleMessage: vi.fn(), - sendText: vi.fn(() => true), + sendText: vi.fn((_frame: string) => true), dispose: vi.fn() })) @@ -102,7 +102,7 @@ describe('mobile relay physical pairing client', () => { fakes.channelOptions!.onAuthenticated() const responsePromise = client.sendRequest('status.get') await vi.waitFor(() => expect(fakes.sendText).toHaveBeenCalledOnce()) - const request = JSON.parse(fakes.sendText.mock.calls[0]![0] as string) + const request = JSON.parse(fakes.sendText.mock.calls[0]![0]) expect(request).toEqual({ id: 'relay-pair-1', deviceToken: 'device-token', diff --git a/mobile/src/transport/mobile-relay-reconnect-controller.test.ts b/mobile/src/transport/mobile-relay-reconnect-controller.test.ts index 0bca568e6a9..f8e1805b910 100644 --- a/mobile/src/transport/mobile-relay-reconnect-controller.test.ts +++ b/mobile/src/transport/mobile-relay-reconnect-controller.test.ts @@ -321,9 +321,10 @@ describe('relay reconnect controller', () => { it('resets backoff after an authenticated relay remains stable', () => { const onRetry = vi.fn() const reconnect = createController(onRetry) - const session = { - getFailure: () => new RelayOuterError(4408) - } as MobileRelayRpcSession + const session: Pick = { + getFailure: () => new RelayOuterError(4408), + probeAfterBackground: () => {} + } const logical = { getActivePath: () => 'relay' } as StableLogicalRpcClient diff --git a/mobile/src/transport/mobile-relay-resume-director.test.ts b/mobile/src/transport/mobile-relay-resume-director.test.ts index 4bacb6759cb..88322b2c6a1 100644 --- a/mobile/src/transport/mobile-relay-resume-director.test.ts +++ b/mobile/src/transport/mobile-relay-resume-director.test.ts @@ -13,7 +13,7 @@ const relay = { describe('mobile relay resume director', () => { it('uses a bounded POST body and never puts the bearer in the URL', async () => { const fetchImpl = vi.fn( - async () => + async (_url: string, _init: RequestInit) => new Response( JSON.stringify({ v: 1, @@ -32,7 +32,7 @@ describe('mobile relay resume director', () => { expect(url).toBe('https://relay.onorca.dev/v1/resolve') expect(url).not.toContain('A'.repeat(43)) expect(init).toMatchObject({ method: 'POST' }) - expect(JSON.parse(init!.body as string)).toEqual({ + expect(JSON.parse(String(init.body))).toEqual({ v: 1, relayHostId: relay.relayHostId, resumeToken: 'A'.repeat(43) diff --git a/mobile/src/transport/mobile-relay-resume-director.ts b/mobile/src/transport/mobile-relay-resume-director.ts index 9f6135c9927..8a698bd093e 100644 --- a/mobile/src/transport/mobile-relay-resume-director.ts +++ b/mobile/src/transport/mobile-relay-resume-director.ts @@ -35,7 +35,7 @@ const ResolveResponseSchema = z export async function resolveMobileRelayEndpoint(args: { relay: MobileRelayEndpoint resumeToken: string - fetchImpl?: typeof fetch + fetchImpl?: (url: string, init: RequestInit) => Promise timeoutMs?: number }): Promise { const controller = new AbortController() diff --git a/mobile/src/transport/mobile-relay-rpc-session-liveness.test.ts b/mobile/src/transport/mobile-relay-rpc-session-liveness.test.ts index c9880f26719..8ea740bb1e4 100644 --- a/mobile/src/transport/mobile-relay-rpc-session-liveness.test.ts +++ b/mobile/src/transport/mobile-relay-rpc-session-liveness.test.ts @@ -6,7 +6,7 @@ const fakes = vi.hoisted(() => ({ onText(value: string): void onBinary(value: Uint8Array): void }, - sendText: vi.fn(() => true), + sendText: vi.fn((_frame: string) => true), close: vi.fn() })) @@ -90,8 +90,8 @@ async function authenticateSession(onLog?: ConnectionLogSink) { } function sentRequests(): Array<{ id: string; method: string }> { - return fakes.sendText.mock.calls.map( - ([value]) => JSON.parse(value as string) as { id: string; method: string } + return fakes.sendText.mock.calls.map(([value]): { id: string; method: string } => + JSON.parse(value) ) } diff --git a/mobile/src/transport/mobile-relay-rpc-session.test.ts b/mobile/src/transport/mobile-relay-rpc-session.test.ts index 544c39e9231..377d0cb317a 100644 --- a/mobile/src/transport/mobile-relay-rpc-session.test.ts +++ b/mobile/src/transport/mobile-relay-rpc-session.test.ts @@ -19,7 +19,7 @@ const fakes = vi.hoisted(() => ({ onBinary(value: Uint8Array): void onError(error: Error): void }, - sendText: vi.fn(() => true), + sendText: vi.fn((_frame: string) => true), close: vi.fn() })) @@ -70,11 +70,11 @@ async function confirmResume() { expect(session.getState()).toBe('handshaking') fakes.linkOptions!.onAuthenticated() await vi.waitFor(() => expect(fakes.sendText).toHaveBeenCalledOnce()) - const request = JSON.parse(fakes.sendText.mock.calls[0]![0] as string) as { + const request: { id: string method: string params: unknown - } + } = JSON.parse(fakes.sendText.mock.calls[0]![0]) fakes.linkOptions!.onText( JSON.stringify({ id: request.id, @@ -95,12 +95,12 @@ async function confirmResume() { }) ) await vi.waitFor(() => expect(fakes.sendText).toHaveBeenCalledTimes(2)) - const capabilityRequest = JSON.parse(fakes.sendText.mock.calls[1]![0] as string) as { + const capabilityRequest: { id: string method: string deviceToken: string params: { clientCapabilities?: string[] } - } + } = JSON.parse(fakes.sendText.mock.calls[1]![0]) return { session, confirmationRequest: request, capabilityRequest } } @@ -142,7 +142,7 @@ describe('mobile relay RPC session', () => { const listener = vi.fn() session.subscribe('runtime.clientEvents.subscribe', {}, listener) await Promise.resolve() - const request = JSON.parse(fakes.sendText.mock.calls[0]![0] as string) as { id: string } + const request: { id: string } = JSON.parse(fakes.sendText.mock.calls[0]![0]) fakes.linkOptions!.onText( JSON.stringify({ id: request.id, @@ -259,9 +259,9 @@ describe('mobile relay RPC session', () => { const terminalListener = vi.fn() session.subscribe('terminal.subscribe', { terminal: 'term-1' }, terminalListener) await vi.waitFor(() => expect(fakes.sendText).toHaveBeenCalledOnce()) - const terminalRequest = JSON.parse(fakes.sendText.mock.calls[0]![0] as string) as { + const terminalRequest: { id: string - } + } = JSON.parse(fakes.sendText.mock.calls[0]![0]) fakes.linkOptions!.onText( JSON.stringify({ id: terminalRequest.id, @@ -288,7 +288,7 @@ describe('mobile relay RPC session', () => { const onBinaryFrame = vi.fn() session.subscribe('browser.screencast', {}, vi.fn(), { onBinaryFrame }) await vi.waitFor(() => expect(fakes.sendText).toHaveBeenCalledOnce()) - const browserRequest = JSON.parse(fakes.sendText.mock.calls[0]![0] as string) as { id: string } + const browserRequest: { id: string } = JSON.parse(fakes.sendText.mock.calls[0]![0]) fakes.linkOptions!.onText( JSON.stringify({ id: browserRequest.id, diff --git a/mobile/src/transport/mobile-relay-runtime-failover.test.ts b/mobile/src/transport/mobile-relay-runtime-failover.test.ts index 70779f4a046..aabb6b3f580 100644 --- a/mobile/src/transport/mobile-relay-runtime-failover.test.ts +++ b/mobile/src/transport/mobile-relay-runtime-failover.test.ts @@ -280,7 +280,9 @@ describe('relay runtime recovery without direct connectivity', () => { it('recovers when the credential bundle was unreadable at supervisor start', async () => { const logical = new FakeLogicalClient('disconnected', 'lan') const readBundle = vi - .fn(async () => bundleWith(2, Number.MAX_SAFE_INTEGER)) + .fn(async (): Promise => + bundleWith(2, Number.MAX_SAFE_INTEGER) + ) .mockResolvedValueOnce(null) const deps = dependencies({ readBundle }) const supervisor = new MobileEndpointSupervisor(logical, host.id, relay, deps) diff --git a/mobile/src/transport/pairing-relay-candidate.test.ts b/mobile/src/transport/pairing-relay-candidate.test.ts index dbab0bad2d5..0e0986afc82 100644 --- a/mobile/src/transport/pairing-relay-candidate.test.ts +++ b/mobile/src/transport/pairing-relay-candidate.test.ts @@ -168,7 +168,7 @@ describe('recovering pairing relay candidate', () => { const stale = client(Promise.reject(new RelayOuterError(1006))) const target = client(Promise.resolve(success())) const persistMove = vi.fn() - const sleep = vi.fn(async () => {}) + const sleep = vi.fn(async (_delayMs: number) => {}) const resolveDirector = vi.fn(async (relay) => { throw new RelayDirectorMoveNotNewerError({ cellUrl: 'https://relay-c2.onorca.dev', @@ -307,7 +307,7 @@ describe('recovering pairing relay candidate', () => { cellUrl: 'https://relay-c2.onorca.dev', assignmentEpoch: 8 })) - const sleep = vi.fn(async () => {}) + const sleep = vi.fn(async (_delayMs: number) => {}) let connects = 0 const candidate = createRecoveringPairingRelayCandidate({ journal, diff --git a/mobile/src/transport/pre-profile-pairing-coordinator.test.ts b/mobile/src/transport/pre-profile-pairing-coordinator.test.ts index 3217658584d..3968603eddf 100644 --- a/mobile/src/transport/pre-profile-pairing-coordinator.test.ts +++ b/mobile/src/transport/pre-profile-pairing-coordinator.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import type { MobileRelayCredentialBundle } from './mobile-relay-credential-bundle' +import type { connectMobileRelayForPairing } from './mobile-relay-physical-client' import type { MobileRelayPairingJournal } from './mobile-relay-pairing-journal' import { racePairingCandidates } from './pairing-candidate-race' import { startPreProfilePairing } from './pre-profile-pairing-coordinator' @@ -100,7 +101,9 @@ function dependencies(client: RpcClient, events: string[]) { connectDirect: vi.fn( (..._args: Parameters) => (events.push('connect'), client) ), - connectRelay: vi.fn(() => unavailableRelay), + connectRelay: vi.fn( + (..._args: Parameters) => unavailableRelay + ), resolveInviteDirector: vi.fn(async () => { throw new Error('director unavailable') }), diff --git a/mobile/src/transport/rpc-client-synthesized-close-diagnostics.test.ts b/mobile/src/transport/rpc-client-synthesized-close-diagnostics.test.ts index 1231a200ebf..99f03109be8 100644 --- a/mobile/src/transport/rpc-client-synthesized-close-diagnostics.test.ts +++ b/mobile/src/transport/rpc-client-synthesized-close-diagnostics.test.ts @@ -1,7 +1,7 @@ // Why: RN can swallow onclose for a wedged iOS transport, so the client synthesizes the close. // These cover the diagnostic clocks that synthesis has to keep honest, and prove synthesis // never costs us the auth-failed latch when the real close lands late. -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest' import { connect } from './rpc-client' import { diagnoseConnection } from '../diagnostics/connection-diagnostics-analysis' import type { ConnectionLogEntry } from './types' @@ -79,14 +79,14 @@ function lastSocket(): WedgedWebSocket { return sockets[sockets.length - 1]! } -function openConnectionLogs(spy: ReturnType): Record[] { +function openConnectionLogs(spy: MockInstance): Record[] { return spy.mock.calls .filter((call) => call[0] === '[net] openConnection') .map((call) => call[1] as Record) } describe('synthesized close diagnostics', () => { - let logSpy: ReturnType + let logSpy: MockInstance beforeEach(() => { vi.useFakeTimers() diff --git a/mobile/src/transport/rpc-client-test-stub.ts b/mobile/src/transport/rpc-client-test-stub.ts new file mode 100644 index 00000000000..f24b46e965b --- /dev/null +++ b/mobile/src/transport/rpc-client-test-stub.ts @@ -0,0 +1,17 @@ +import type { RpcClient } from './rpc-client' + +/** A connected RpcClient whose behaviour is only the members a test supplies; an unscripted request rejects. */ +export function stubRpcClient(members: Partial = {}): RpcClient { + return { + sendRequest: (method) => Promise.reject(new Error(`unscripted request: ${method}`)), + subscribe: () => () => {}, + updateTerminalSubscriptionViewport: () => {}, + getState: () => 'connected', + getReconnectAttempt: () => 0, + getLastConnectedAt: () => null, + onStateChange: () => () => {}, + notifyForeground: () => {}, + close: () => {}, + ...members + } +} diff --git a/mobile/src/transport/rpc-session-liveness-watchdog.test.ts b/mobile/src/transport/rpc-session-liveness-watchdog.test.ts index fc79154515a..f55418d1fc5 100644 --- a/mobile/src/transport/rpc-session-liveness-watchdog.test.ts +++ b/mobile/src/transport/rpc-session-liveness-watchdog.test.ts @@ -112,7 +112,7 @@ describe('RpcSessionLivenessWatchdog', () => { it('does not charge a scheduler-stalled probe window', () => { let now = 0 - let callback: (() => void) | null = null + let callback: (() => void) | undefined const terminate = vi.fn() const watchdog = new RpcSessionLivenessWatchdog({ transport: 'relay', diff --git a/mobile/src/transport/runtime-capability-probe.test.ts b/mobile/src/transport/runtime-capability-probe.test.ts index 3b20649ab35..1b6876678bd 100644 --- a/mobile/src/transport/runtime-capability-probe.test.ts +++ b/mobile/src/transport/runtime-capability-probe.test.ts @@ -174,7 +174,7 @@ describe('startRuntimeCapabilityProbe', () => { }) it('ignores a success that resolves after cancellation', async () => { - let resolveRequest: ((response: RpcResponse) => void) | null = null + let resolveRequest: ((response: RpcResponse) => void) | undefined const client = { sendRequest: () => new Promise((resolve) => { diff --git a/mobile/src/transport/settings-host-client-lifecycle.test.ts b/mobile/src/transport/settings-host-client-lifecycle.test.ts index b158a8aeaa1..3aa98ad8fbd 100644 --- a/mobile/src/transport/settings-host-client-lifecycle.test.ts +++ b/mobile/src/transport/settings-host-client-lifecycle.test.ts @@ -6,13 +6,11 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { beforeEach, describe, expect, it, vi } from 'vitest' import { useAllHostClients } from './use-all-host-clients' import { useFocusedSettingsHostClients } from './settings-host-client-connections' -import { - RpcClientProvider, - useRpcClientContext, - type RpcClientContextValue -} from './client-context' +import type { MobileRelayEndpoint } from '../../../src/shared/mobile-relay-credential-contract' +import { RpcClientProvider, useRpcClientContext } from './client-context' import { useHostClient, usePrimeHosts } from './host-client-hooks' import { selectHomeAutoConnectHostIds } from './home-host-auto-connect' +import type { RpcClientContextValue } from './rpc-client-context-contract' import type { RpcClient } from './rpc-client' import type { ConnectionState, HostProfile } from './types' @@ -23,7 +21,7 @@ const routeFocus = vi.hoisted(() => ({ })) // Why: the opener starts a descriptor status probe per connection; these fakes have no RPC surface. -const descriptorProbe = vi.hoisted(() => ({ start: vi.fn(() => vi.fn()) })) +const descriptorProbe = vi.hoisted(() => ({ start: vi.fn((..._args: unknown[]) => vi.fn()) })) vi.mock('./runtime-status-probe', () => ({ startRuntimeStatusProbe: (...args: unknown[]) => descriptorProbe.start(...args) })) @@ -83,9 +81,18 @@ function host(id: string, lastConnected: number, extra: Partial = { } } +const RELAY: MobileRelayEndpoint = { + v: 1, + directorUrl: 'https://relay-staging.onorca.dev', + cellUrl: 'https://c1.relay-staging.onorca.dev', + assignmentEpoch: 4, + relayHostId: 'AbCdEf0123_-xyZ9', + e2eeFraming: 2 +} + const HOSTS = [ host('direct-recent', 50), - host('relay-recent', 40, { relayHostId: 'AbCdEf0123_-xyZ9' }), + host('relay-recent', 40, { relay: RELAY }), host('ssh-provider-recent', 30), host('folder-workspace-host', 20), host('offline-host', 10) @@ -281,7 +288,7 @@ describe('settings host client lifecycle', () => { ) } - act(() => renderer.unmount()) + act(() => renderer?.unmount()) }) it('leaves no zero-reference clients after repeated settings navigation', async () => { @@ -373,7 +380,7 @@ describe('settings host client lifecycle', () => { expect(clients.get(removedHostId)?.closeMock).toHaveBeenCalledOnce() expect(clients.get(addedHostId)?.closeMock).not.toHaveBeenCalled() - act(() => renderer.unmount()) + act(() => renderer?.unmount()) expect(clients.get(retainedHostId)?.closeMock).toHaveBeenCalledOnce() expect(clients.get(sharedHostId)?.closeMock).toHaveBeenCalledOnce() expect(clients.get(addedHostId)?.closeMock).toHaveBeenCalledOnce() @@ -445,7 +452,7 @@ describe('settings host client lifecycle', () => { resolveInitial?.([retryHost]) await initialLookup }) - if (!context) { + if (!context?.forceReconnect) { throw new Error('client context was not captured') } @@ -598,7 +605,7 @@ describe('settings host client lifecycle', () => { }) it('releases a manual host after Home demotes or stops tracking it', async () => { - const manualHost = host('manual-relay-host', 1, { relayHostId: 'AbCdEf0123_-xyZ9' }) + const manualHost = host('manual-relay-host', 1, { relay: RELAY }) const clients: FakeClient[] = [] connectMock.mockImplementation(() => { const client = makeFakeClient('reconnecting') @@ -641,7 +648,7 @@ describe('settings host client lifecycle', () => { throw new Error('manual connection harness did not initialize') } await act(async () => { - await context?.forceReconnect(manualHost.id) + await context?.forceReconnect?.(manualHost.id) }) expect(activeHostIds()).toEqual([manualHost.id]) @@ -660,7 +667,7 @@ describe('settings host client lifecycle', () => { expect(clients[0]?.closeMock).toHaveBeenCalledOnce() await act(async () => { - await context?.forceReconnect(manualHost.id) + await context?.forceReconnect?.(manualHost.id) }) expect(activeHostIds()).toEqual([manualHost.id]) expect(clients).toHaveLength(2) diff --git a/mobile/src/transport/workspace-server.test.ts b/mobile/src/transport/workspace-server.test.ts index 4a9887a60c0..b2b65a0935a 100644 --- a/mobile/src/transport/workspace-server.test.ts +++ b/mobile/src/transport/workspace-server.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it, vi } from 'vitest' import type { MobileRelayHost } from '../../../src/shared/mobile-relay-hosts-contract' import type { RpcClient } from './rpc-client' import type { ConnectionState } from './types' +import { stubRpcClient } from './rpc-client-test-stub' const feed = vi.hoisted((): { hosts: readonly MobileRelayHost[] | null } => ({ hosts: null })) const gatesConnStates = vi.hoisted((): ConnectionState[] => []) @@ -30,8 +31,7 @@ function server(relay: MobileRelayHost['relay']): MobileRelayHost { describe('useWorkspaceServer', () => { it("re-reads the server's status when the server reconnects under a connected desktop", async () => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked route and gates never call the client. - const client = {} as RpcClient + const client = stubRpcClient() function Probe(): null { useWorkspaceServer({ client, diff --git a/mobile/src/worktree/worktree-catalog-snapshot-client.ts b/mobile/src/worktree/worktree-catalog-snapshot-client.ts index 13de6453a6d..38ebe182b75 100644 --- a/mobile/src/worktree/worktree-catalog-snapshot-client.ts +++ b/mobile/src/worktree/worktree-catalog-snapshot-client.ts @@ -1,5 +1,4 @@ import type { RpcClient } from '../transport/rpc-client' -import type { RpcFailure } from '../transport/types' import type { Worktree } from './workspace-list-sections' import { worktreeCatalogRead } from './worktree-catalog-operations' @@ -79,8 +78,7 @@ export class WorktreeCatalogSnapshotClient { const catalog = worktreeCatalogRead.interpret(reply) if (!catalog.accepted) { // The refusal code the caller reports lives on the envelope; no acceptance policy carries it. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this policy skips only a refusal, so an unaccepted reply is a failure envelope. - const code = (reply as RpcFailure).error?.code + const code = reply.ok ? undefined : reply.error?.code return { kind: 'request_failed', code: typeof code === 'string' && code.length > 0 ? code : 'request_failed' diff --git a/mobile/tests-typecheck-baseline.txt b/mobile/tests-typecheck-baseline.txt index 790d75f6b4a..567b57566c4 100644 --- a/mobile/tests-typecheck-baseline.txt +++ b/mobile/tests-typecheck-baseline.txt @@ -2,101 +2,4 @@ # This is a RATCHET: the list may only SHRINK. Do NOT add entries to get CI green — # an unchecked test is one whose type-level pins prove nothing. # Regenerate/prune: node scripts/check-tests-typecheck-ratchet.mjs --prune -src/browser/browser-command-reply-schema.test.ts -src/browser/mobile-browser-frameless-stream.test.tsx -src/components/CodexResetCreditAction.test.ts -src/components/HostProtocolGate.test.ts -src/components/MobileHomeQuickActions.test.ts -src/components/MobileHostCard-truthful-status.test.tsx -src/components/MobileHostCard.test.ts -src/components/MobileMarkdown.file-links.test.ts -src/components/NewWorktreeModal.test.tsx -src/components/PickerModal.accessibility.test.ts -src/components/WorktreeAgentList.test.tsx -src/components/agent-monitoring-indicators.test.ts -src/components/bottom-drawer-close-lifecycle.test.ts -src/components/bottom-drawer-window-handback.test.ts -src/components/codex-reset-credit-capability.test.ts -src/components/pr-sidebar/PRCommentsSection.test.ts -src/diagnostics/connection-diagnostics-submission.test.ts -src/dictation/mobile-dictation-setup.test.ts -src/files/MobileFileMarkdownPreview.test.ts -src/hooks/mobile-dictation-desktop-start.test.ts -src/host-edit-route-accessibility.test.ts -src/mock-server-key-pair.test.ts -src/notifications/notification-consent-ownership.test.ts -src/notifications/push-dismissal-native-races.test.ts -src/notifications/push-receive.test.ts -src/notifications/push-registration.test.ts -src/notifications/push-token.test.ts -src/onboarding/MobileOnboardingPage.test.ts -src/onboarding/NotificationOnboardingPreview.test.ts -src/onboarding/legacy-notification-opt-in-route.test.ts -src/onboarding/mobile-onboarding-screen.test.ts -src/session/MobileMarkdownReader.test.tsx -src/session/MobileNativeChatComposer.test.ts -src/session/MobileNativeChatMessage.test.ts -src/session/MobileNativeChatOverlay.test.ts -src/session/MobileNativeChatPermission.test.ts -src/session/MobileNativeChatQuestion.test.tsx -src/session/MobileNativeChatSessionOptionPickers.test.ts -src/session/MobileNativeChatSessionOptionRows.test.ts -src/session/MobileNativeChatTurnStatus.test.ts -src/session/QuickCommandsList.test.ts -src/session/QuickCommandsSheet.test.ts -src/session/ai-vault-resume-launch.test.ts -src/session/mobile-file-tap-open.test.ts -src/session/mobile-native-chat-open-file.test.ts -src/session/mobile-session-tab-activation.test.ts -src/session/mobile-worker-takeover-send-sites.test.ts -src/session/pending-terminal-handle-recovery-poll.test.ts -src/session/session-reply-schema.test.ts -src/session/use-mobile-diff-review-send-actions.test.ts -src/session/use-mobile-file-tap-handlers.test.ts -src/session/use-mobile-pr-branch-context.test.ts -src/session/use-mobile-pr-sidebar-controller.test.ts -src/session/use-mobile-session-terminal-create-actions.test.ts -src/session/use-mobile-structured-agent-session.test.tsx -src/session/use-mobile-structured-prompt-responses.test.tsx -src/settings/native-notification-delivery-settings.test.tsx -src/settings/notification-display-test.test.tsx -src/settings/settings-screen-state.test.tsx -src/settings/voice-settings-poller-refresh.test.tsx -src/tasks/source-workspace-create.test.ts -src/tasks/task-source-search-reply-schema.test.ts -src/terminal/buffered-terminal-draft-restoration.test.ts -src/terminal/terminal-caret-rendering-oracle.test.ts -src/terminal/terminal-live-accessory-raw-send.test.ts -src/terminal/terminal-live-text-commit.test.ts -src/terminal/terminal-webview-engine-error.test.ts -src/terminal/terminal-webview-query-reply-routing.test.ts -src/terminal/terminal-webview-wheel-scroll.test.ts -src/terminal/use-buffered-terminal-drafts.test.tsx -src/terminal/worker-terminal-takeover-report.test.ts -src/test-support/rpc-recording/native-mounting-substitutes.test.ts -src/test-support/rpc-recording/operation-module-loader.test.ts -src/test-support/rpc-recording/screen-native-substitutes.test.ts -src/transport/client-context.test.ts -src/transport/host-credential-cleanup.test.ts -src/transport/host-edit-navigation.test.ts -src/transport/host-removal-lifecycle.test.ts -src/transport/host-status-gates.test.ts -src/transport/host-store.test.ts -src/transport/mobile-endpoint-supervisor-nudge.test.ts -src/transport/mobile-relay-background-lifecycle.test.ts -src/transport/mobile-relay-direct-upgrade.test.ts -src/transport/mobile-relay-e2ee-link.test.ts -src/transport/mobile-relay-pairing-recovery.test.ts -src/transport/mobile-relay-physical-client.test.ts -src/transport/mobile-relay-reconnect-controller.test.ts -src/transport/mobile-relay-resume-director.test.ts -src/transport/mobile-relay-rpc-session-liveness.test.ts -src/transport/mobile-relay-rpc-session.test.ts -src/transport/mobile-relay-runtime-failover.test.ts -src/transport/pairing-relay-candidate.test.ts -src/transport/pre-profile-pairing-coordinator.test.ts -src/transport/rpc-client-synthesized-close-diagnostics.test.ts -src/transport/rpc-operation.test.ts -src/transport/rpc-session-liveness-watchdog.test.ts -src/transport/runtime-capability-probe.test.ts -src/transport/settings-host-client-lifecycle.test.ts + diff --git a/package.json b/package.json index 1c5c36dc19e..d58b95a02cc 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "test:perf:contracts": "vitest run --config config/vitest.performance.config.ts", "test:perf:benchmarks": "vitest bench --run --config config/vitest.performance.config.ts", "format": "oxfmt --write .", - "lint": "oxlint && pnpm run audit:anti-slop && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:dead-classes && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:focus-read-ratchet && pnpm run check:layout-writer-ratchets && pnpm run check:path-literal-ratchet && pnpm run check:legacy-relay-fence-ratchet && pnpm run check:unproven-pty-retirement-ratchet && pnpm run check:id-only-store-write-ratchet && pnpm run check:repo-host-read-ratchet && pnpm run check:workspace-record-read-ratchet && pnpm run check:host-list-render-ratchet && pnpm run check:readme-local-links && pnpm run check:node-runtime-pin && pnpm run check:test-file-runners && pnpm run check:mobile-shared-dependency-versions && pnpm run verify:rpc-params-catalog && pnpm run verify:acp-protocol && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalogs && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage && pnpm run check:process-host-imports && pnpm run check:main-sync-spawn-ratchet && pnpm run check:compat-boundary-imports", + "lint": "oxlint && pnpm run audit:anti-slop && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:dead-classes && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:e2e-typecheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:focus-read-ratchet && pnpm run check:layout-writer-ratchets && pnpm run check:path-literal-ratchet && pnpm run check:legacy-relay-fence-ratchet && pnpm run check:unproven-pty-retirement-ratchet && pnpm run check:id-only-store-write-ratchet && pnpm run check:repo-host-read-ratchet && pnpm run check:workspace-record-read-ratchet && pnpm run check:host-list-render-ratchet && pnpm run check:readme-local-links && pnpm run check:node-runtime-pin && pnpm run check:test-file-runners && pnpm run check:mobile-shared-dependency-versions && pnpm run verify:rpc-params-catalog && pnpm run verify:acp-protocol && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalogs && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage && pnpm run check:process-host-imports && pnpm run check:main-sync-spawn-ratchet && pnpm run check:compat-boundary-imports", "audit:code-quality": "pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run audit:react-doctor", "audit:code-quality:native": "oxlint --config config/oxlint-code-quality-native-plugins.json src config tests mobile --deny-warnings", "audit:code-quality:type-aware": "oxlint --type-aware --config config/oxlint-code-quality-type-aware.json src config tests --deny-warnings", diff --git a/src/main/agent-hooks/wsl-guest-plugin-install.ts b/src/main/agent-hooks/wsl-guest-plugin-install.ts index af38e5c87a1..f082667b9d3 100644 --- a/src/main/agent-hooks/wsl-guest-plugin-install.ts +++ b/src/main/agent-hooks/wsl-guest-plugin-install.ts @@ -1,5 +1,6 @@ import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer' import { AGENT_HOOK_INSTALL_PLUGINS_METHOD } from '../../shared/agent-hook-relay' +import { isRecord } from '../../shared/agent-status-child-work-value-guards' import { selectOpenCodePluginSources } from './opencode-plugin-settings' import type { ManagedHookDetectionSettings } from './managed-hook-detection-commands' import type { PluginSources } from '../../wsl-guest/plugin-overlay' @@ -23,17 +24,16 @@ export async function requestGuestOpenCodeOverlayDir( launchKind?: 'pi' | 'omp' ): Promise { try { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - const res = (await mux.request(AGENT_HOOK_INSTALL_PLUGINS_METHOD, { + const res = await mux.request(AGENT_HOOK_INSTALL_PLUGINS_METHOD, { ...selectOpenCodePluginSources(deps.pluginSources(), deps.managedHookSettings?.() ?? null), ...(launchKind ? { launchKind } : {}) - })) as { - overlayDirs?: { opencode?: unknown; opencode2?: unknown; pi?: unknown; omp?: unknown } - } - const dir = res?.overlayDirs?.opencode - const dir2 = res?.overlayDirs?.opencode2 - const piDir = res?.overlayDirs?.pi - const ompDir = res?.overlayDirs?.omp + }) + const overlayDirs: Record = + isRecord(res) && isRecord(res.overlayDirs) ? res.overlayDirs : {} + const dir = overlayDirs.opencode + const dir2 = overlayDirs.opencode2 + const piDir = overlayDirs.pi + const ompDir = overlayDirs.omp const opencodeDir = typeof dir === 'string' && dir.length > 0 ? dir : undefined const opencode2Dir = typeof dir2 === 'string' && dir2.length > 0 ? dir2 : undefined const guestPiDir = typeof piDir === 'string' && piDir.length > 0 ? piDir : undefined @@ -49,8 +49,7 @@ export async function requestGuestOpenCodeOverlayDir( : { kind: 'none' } } catch (err) { // Older guests lack the handler (-32601); connection loss and disposal are routine teardown. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - const code = (err as { code?: unknown })?.code + const code = isRecord(err) ? err.code : undefined if (code === -32601 || code === 'CONNECTION_LOST' || code === 'DISPOSED' || mux.isDisposed()) { return { kind: 'unavailable' } } diff --git a/src/main/ai-vault-search/session-search-store.ts b/src/main/ai-vault-search/session-search-store.ts index 4e640e8002c..f611722895a 100644 --- a/src/main/ai-vault-search/session-search-store.ts +++ b/src/main/ai-vault-search/session-search-store.ts @@ -172,33 +172,30 @@ export class SessionSearchStore { files(): SessionSearchFileRow[] { return ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The files schema and SELECT aliases define this row; REAL casts return numeric IDs or null. - ( - this.db - .prepare( - // Numeric stat IDs may exceed SQLite's safe INTEGER-to-number read range. - `SELECT path, CAST(dev AS REAL) AS dev, CAST(ino AS REAL) AS ino, + this.db + .prepare( + // Numeric stat IDs may exceed SQLite's safe INTEGER-to-number read range. + `SELECT path, CAST(dev AS REAL) AS dev, CAST(ino AS REAL) AS ino, mtime_ms AS mtimeMs, size_bytes AS sizeBytes, state, fail_count AS failCount, failed_mtime_ms AS failedMtimeMs FROM files` - ) - .all() as (Omit & { - dev: number | null - ino: number | null - })[] - ).map((row) => ({ - path: row.path, - identity: - typeof row.dev === 'number' && typeof row.ino === 'number' - ? { dev: row.dev, ino: row.ino } - : null, - mtimeMs: row.mtimeMs, - sizeBytes: row.sizeBytes, - state: row.state, - failCount: row.failCount, - failedMtimeMs: row.failedMtimeMs - })) - ) + ) + .all() as (Omit & { + dev: number | null + ino: number | null + })[] + ).map((row) => ({ + path: row.path, + identity: + typeof row.dev === 'number' && typeof row.ino === 'number' + ? { dev: row.dev, ino: row.ino } + : null, + mtimeMs: row.mtimeMs, + sizeBytes: row.sizeBytes, + state: row.state, + failCount: row.failCount, + failedMtimeMs: row.failedMtimeMs + })) } setFileState(path: string, state: SessionSearchFileState, atMtimeMs?: number): void { diff --git a/src/main/ai-vault/session-scanner-opencode2-sqlite-list.ts b/src/main/ai-vault/session-scanner-opencode2-sqlite-list.ts index 1f1f20c42b3..18bfd5f70ac 100644 --- a/src/main/ai-vault/session-scanner-opencode2-sqlite-list.ts +++ b/src/main/ai-vault/session-scanner-opencode2-sqlite-list.ts @@ -1,4 +1,4 @@ -import type { AiVaultAgent, AiVaultScanIssue } from '../../shared/ai-vault-types' +import type { AiVaultScanIssue } from '../../shared/ai-vault-types' import { buildOpenCodeSqliteCandidatePath, splitOpenCodeSqliteCandidate @@ -57,8 +57,7 @@ function rowToCandidate(row: SessionRow, dbPath: string): SessionFileCandidate { ? row.time_updated : row.time_created return { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - agent: 'opencode2' as AiVaultAgent, + agent: 'opencode2', file: { path: buildOpenCodeSqliteCandidatePath(dbPath, row.id), mtimeMs, diff --git a/src/main/ai-vault/session-scanner-opencode2-sqlite.ts b/src/main/ai-vault/session-scanner-opencode2-sqlite.ts index 4db4a860f96..b004014ed35 100644 --- a/src/main/ai-vault/session-scanner-opencode2-sqlite.ts +++ b/src/main/ai-vault/session-scanner-opencode2-sqlite.ts @@ -15,6 +15,7 @@ import { shouldCaptureFullFirstUserPrompt } from './session-scanner-first-user-prompt' import { normalizeTitleText } from './session-scanner-values' +import { asRecord } from './session-scanner-record-value' import { extractOpenCode2MessageText, decodeOpenCode2Message, @@ -122,13 +123,7 @@ function extractModelId(modelText: string | null): string | null { return trimmed } try { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - const parsed = JSON.parse(trimmed) as unknown - const record = - parsed && typeof parsed === 'object' && !Array.isArray(parsed) - ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - (parsed as Record) - : null + const record = asRecord(JSON.parse(trimmed)) if (typeof record?.id === 'string' && record.id.trim()) { return record.id } @@ -162,8 +157,6 @@ function readFirstUserPromptFromDb(db: SyncDatabase, sessionId: string): string ORDER BY time_created ASC, seq ASC LIMIT 1` ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the SELECT projects one string data column and better-sqlite3 returns rows synchronously. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. .all(sessionId) as { data: string }[] const text = rows[0] ? extractOpenCode2MessageText(rows[0].data) : null return text ? normalizeFullFirstUserPromptText(text) : null @@ -229,7 +222,6 @@ export async function parseOpenCode2SqliteSession(args: { if (!canReadOpenCode2Sessions(db)) { return null } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the query selects the SessionRow columns and the schema guard ran immediately above. // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. const row = db.prepare(buildSessionQuery(db)).get(sessionId) as SessionRow | undefined if (!row || row.id !== sessionId) { diff --git a/src/main/automations/runtime-automation-service.test.ts b/src/main/automations/runtime-automation-service.test.ts index 965ae0a61a4..fa3fdffa12a 100644 --- a/src/main/automations/runtime-automation-service.test.ts +++ b/src/main/automations/runtime-automation-service.test.ts @@ -3,10 +3,12 @@ import type { AutomationRun } from '../../shared/automations-types' import type { HeadlessAutomationDispatcher } from './headless-dispatch' import type { AutomationRunTerminalObserver } from './run-completion-watcher' -const captured = vi.hoisted((): { dispatcher: unknown; observer: unknown } => ({ - dispatcher: null, - observer: null -})) +type CapturedServiceOptions = { + headlessDispatcher?: HeadlessAutomationDispatcher + terminalObserver?: AutomationRunTerminalObserver +} + +const captured = vi.hoisted((): { options: CapturedServiceOptions } => ({ options: {} })) const { deliverTerminalAgentLaunchPrompt } = vi.hoisted(() => ({ deliverTerminalAgentLaunchPrompt: vi.fn(async () => true) @@ -19,18 +21,24 @@ vi.mock('./service', () => ({ AutomationService: class { start(): void {} stop(): void {} - constructor( - _store: unknown, - opts: { headlessDispatcher?: unknown; terminalObserver?: unknown } - ) { - captured.dispatcher = opts.headlessDispatcher - captured.observer = opts.terminalObserver + constructor(_store: unknown, opts: CapturedServiceOptions) { + captured.options = opts } } })) import { createRuntimeAutomationService } from './runtime-automation-service' +function capturedOption( + key: K +): NonNullable { + const value = captured.options[key] + if (value === undefined) { + throw new Error(`the service was constructed without ${key}`) + } + return value +} + describe('headless automation dispatch', () => { it('hands the launched run to its watcher instead of awaiting one tui-idle wait itself', async () => { const runtime = { @@ -55,8 +63,7 @@ describe('headless automation dispatch', () => { runtime: runtime as never, headless: true }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: captured from the mocked constructor above. - const dispatcher = captured.dispatcher as HeadlessAutomationDispatcher + const dispatcher = capturedOption('headlessDispatcher') const launch = await dispatcher({ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a reuse-workspace automation; only these fields are read. automation: { @@ -78,8 +85,7 @@ describe('headless automation dispatch', () => { title: 'Nightly' }) expect(runtime.waitForTerminal).not.toHaveBeenCalled() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: captured from the mocked constructor above. - const observer = captured.observer as AutomationRunTerminalObserver + const observer = capturedOption('terminalObserver') // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: resolution reads only the pane key. expect(observer.resolveRunTerminal({ terminalPaneKey: 'tab-1:pane-1' } as never)).toBe( 'terminal-1' @@ -110,8 +116,7 @@ describe('headless automation dispatch with extra agent args', () => { runtime: runtime as never, headless: true }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: captured from the mocked constructor above. - const dispatcher = captured.dispatcher as HeadlessAutomationDispatcher + const dispatcher = capturedOption('headlessDispatcher') const automation = { workspaceMode: 'existing', workspaceId: 'wt-1', @@ -164,8 +169,7 @@ describe('headless reuse-session dispatch', () => { runtime: runtime as never, headless: true }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: captured from the mocked constructor above. - const dispatcher = captured.dispatcher as HeadlessAutomationDispatcher + const dispatcher = capturedOption('headlessDispatcher') const dispatch = (id: string) => dispatcher({ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only these fields are read. diff --git a/src/main/browser/agent-browser-bridge-capture-paint-hold.test.ts b/src/main/browser/agent-browser-bridge-capture-paint-hold.test.ts index 8871e481d6c..2809a3d30cb 100644 --- a/src/main/browser/agent-browser-bridge-capture-paint-hold.test.ts +++ b/src/main/browser/agent-browser-bridge-capture-paint-hold.test.ts @@ -26,8 +26,10 @@ vi.mock('electron', () => { }) const { CdpWsProxyMock } = vi.hoisted(() => { const instances: unknown[] = [] - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const MockClass = vi.fn().mockImplementation(function (this: any, _wc: unknown) { + const MockClass = vi.fn().mockImplementation(function ( + this: Record, + _wc: unknown + ) { this._wc = _wc this.start = vi.fn(async () => 'ws://127.0.0.1:9222') this.stop = vi.fn(async () => {}) diff --git a/src/main/browser/agent-browser-bridge-command-transport.test.ts b/src/main/browser/agent-browser-bridge-command-transport.test.ts index d8ca1a60b48..7df8aa68958 100644 --- a/src/main/browser/agent-browser-bridge-command-transport.test.ts +++ b/src/main/browser/agent-browser-bridge-command-transport.test.ts @@ -26,8 +26,10 @@ vi.mock('electron', () => { }) const { CdpWsProxyMock } = vi.hoisted(() => { const instances: unknown[] = [] - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const MockClass = vi.fn().mockImplementation(function (this: any, _wc: unknown) { + const MockClass = vi.fn().mockImplementation(function ( + this: Record, + _wc: unknown + ) { this._wc = _wc this.start = vi.fn(async () => 'ws://127.0.0.1:9222') this.stop = vi.fn(async () => {}) diff --git a/src/main/browser/agent-browser-bridge-keypress-input.test.ts b/src/main/browser/agent-browser-bridge-keypress-input.test.ts index 2998c4afd77..e709c2029f5 100644 --- a/src/main/browser/agent-browser-bridge-keypress-input.test.ts +++ b/src/main/browser/agent-browser-bridge-keypress-input.test.ts @@ -29,8 +29,10 @@ vi.mock('electron', () => { }) const { CdpWsProxyMock } = vi.hoisted(() => { const instances: unknown[] = [] - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const MockClass = vi.fn().mockImplementation(function (this: any, _wc: unknown) { + const MockClass = vi.fn().mockImplementation(function ( + this: Record, + _wc: unknown + ) { this._wc = _wc this.start = vi.fn(async () => 'ws://127.0.0.1:9222') this.stop = vi.fn(async () => {}) diff --git a/src/main/browser/agent-browser-bridge-mouse-input.test.ts b/src/main/browser/agent-browser-bridge-mouse-input.test.ts index 82ad53e796b..332d12d7460 100644 --- a/src/main/browser/agent-browser-bridge-mouse-input.test.ts +++ b/src/main/browser/agent-browser-bridge-mouse-input.test.ts @@ -26,8 +26,10 @@ vi.mock('electron', () => { }) const { CdpWsProxyMock } = vi.hoisted(() => { const instances: unknown[] = [] - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const MockClass = vi.fn().mockImplementation(function (this: any, _wc: unknown) { + const MockClass = vi.fn().mockImplementation(function ( + this: Record, + _wc: unknown + ) { this._wc = _wc this.start = vi.fn(async () => 'ws://127.0.0.1:9222') this.stop = vi.fn(async () => {}) diff --git a/src/main/browser/agent-browser-bridge-navigation.test.ts b/src/main/browser/agent-browser-bridge-navigation.test.ts index 99c58c44edd..32d3aa1bbbd 100644 --- a/src/main/browser/agent-browser-bridge-navigation.test.ts +++ b/src/main/browser/agent-browser-bridge-navigation.test.ts @@ -26,8 +26,10 @@ vi.mock('electron', () => { }) const { CdpWsProxyMock } = vi.hoisted(() => { const instances: unknown[] = [] - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const MockClass = vi.fn().mockImplementation(function (this: any, _wc: unknown) { + const MockClass = vi.fn().mockImplementation(function ( + this: Record, + _wc: unknown + ) { this._wc = _wc this.start = vi.fn(async () => 'ws://127.0.0.1:9222') this.stop = vi.fn(async () => {}) diff --git a/src/main/browser/agent-browser-bridge-pointer-input.test.ts b/src/main/browser/agent-browser-bridge-pointer-input.test.ts index a3f1db902bd..371d2ed30e0 100644 --- a/src/main/browser/agent-browser-bridge-pointer-input.test.ts +++ b/src/main/browser/agent-browser-bridge-pointer-input.test.ts @@ -30,8 +30,10 @@ vi.mock('electron', () => { }) const { CdpWsProxyMock } = vi.hoisted(() => { const instances: unknown[] = [] - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const MockClass = vi.fn().mockImplementation(function (this: any, _wc: unknown) { + const MockClass = vi.fn().mockImplementation(function ( + this: Record, + _wc: unknown + ) { this._wc = _wc this.start = vi.fn(async () => 'ws://127.0.0.1:9222') this.stop = vi.fn(async () => {}) diff --git a/src/main/browser/agent-browser-bridge-session-lifecycle.test.ts b/src/main/browser/agent-browser-bridge-session-lifecycle.test.ts index ed94c49608e..b609e5e9f03 100644 --- a/src/main/browser/agent-browser-bridge-session-lifecycle.test.ts +++ b/src/main/browser/agent-browser-bridge-session-lifecycle.test.ts @@ -34,8 +34,10 @@ vi.mock('electron', () => { }) const { CdpWsProxyMock } = vi.hoisted(() => { const instances: unknown[] = [] - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const MockClass = vi.fn().mockImplementation(function (this: any, _wc: unknown) { + const MockClass = vi.fn().mockImplementation(function ( + this: Record, + _wc: unknown + ) { this._wc = _wc this.start = vi.fn(async () => 'ws://127.0.0.1:9222') this.stop = vi.fn(async () => {}) diff --git a/src/main/browser/agent-browser-bridge-tab-routing.test.ts b/src/main/browser/agent-browser-bridge-tab-routing.test.ts index 67e01583cc3..6f447b87a7c 100644 --- a/src/main/browser/agent-browser-bridge-tab-routing.test.ts +++ b/src/main/browser/agent-browser-bridge-tab-routing.test.ts @@ -26,8 +26,10 @@ vi.mock('electron', () => { }) const { CdpWsProxyMock } = vi.hoisted(() => { const instances: unknown[] = [] - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const MockClass = vi.fn().mockImplementation(function (this: any, _wc: unknown) { + const MockClass = vi.fn().mockImplementation(function ( + this: Record, + _wc: unknown + ) { this._wc = _wc this.start = vi.fn(async () => 'ws://127.0.0.1:9222') this.stop = vi.fn(async () => {}) diff --git a/src/main/browser/agent-browser-bridge-text-input.test.ts b/src/main/browser/agent-browser-bridge-text-input.test.ts index de04dec375f..dc7b1973b1e 100644 --- a/src/main/browser/agent-browser-bridge-text-input.test.ts +++ b/src/main/browser/agent-browser-bridge-text-input.test.ts @@ -26,8 +26,10 @@ vi.mock('electron', () => { }) const { CdpWsProxyMock } = vi.hoisted(() => { const instances: unknown[] = [] - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const MockClass = vi.fn().mockImplementation(function (this: any, _wc: unknown) { + const MockClass = vi.fn().mockImplementation(function ( + this: Record, + _wc: unknown + ) { this._wc = _wc this.start = vi.fn(async () => 'ws://127.0.0.1:9222') this.stop = vi.fn(async () => {}) diff --git a/src/main/browser/browser-session-ua-cdp-collector.ts b/src/main/browser/browser-session-ua-cdp-collector.ts index c12b13b447a..024f5af3819 100644 --- a/src/main/browser/browser-session-ua-cdp-collector.ts +++ b/src/main/browser/browser-session-ua-cdp-collector.ts @@ -1,5 +1,6 @@ import WebSocket from 'ws' import { cancelUnreadResponseBody } from '../lib/unread-response-body' +import { isRecord } from '../../shared/agent-status-child-work-value-guards' export type BrowserSessionUaCdpRequest = Readonly<{ targetType: string @@ -17,11 +18,7 @@ type PendingRequest = { } function readRecord(value: unknown): Record | undefined { - if (typeof value !== 'object' || value === null) { - return undefined - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: guarded by the object/null check above; every member is read back through its own typeof check. - return value as Record + return isRecord(value) ? value : undefined } function readString(record: Record | undefined, key: string): string | undefined { diff --git a/src/main/browser/cdp-ws-proxy-focus-replay.test.ts b/src/main/browser/cdp-ws-proxy-focus-replay.test.ts index fd856060ba8..a04478466da 100644 --- a/src/main/browser/cdp-ws-proxy-focus-replay.test.ts +++ b/src/main/browser/cdp-ws-proxy-focus-replay.test.ts @@ -6,6 +6,7 @@ import { getSendCommandCalls, getSendCommandMethods, sendAndReceive, + mockAsWebContents, type MockWebContents } from './cdp-ws-proxy-test-harness' @@ -21,8 +22,7 @@ describe('CdpWsProxy DOM.focus replay', () => { beforeEach(async () => { mock = createMockWebContents() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements every WebContents member the proxy calls. - proxy = new CdpWsProxy(mock.webContents as never, () => () => {}) + proxy = new CdpWsProxy(mockAsWebContents(mock), () => () => {}) endpoint = await proxy.start() }) diff --git a/src/main/browser/cdp-ws-proxy-host-admission.test.ts b/src/main/browser/cdp-ws-proxy-host-admission.test.ts index 710406d5e8e..18795ffa7be 100644 --- a/src/main/browser/cdp-ws-proxy-host-admission.test.ts +++ b/src/main/browser/cdp-ws-proxy-host-admission.test.ts @@ -8,6 +8,7 @@ import { createMockWebContents, getSendCommandMethods, sendAndReceive, + mockAsWebContents, type MockWebContents } from './cdp-ws-proxy-test-harness' @@ -42,10 +43,8 @@ describe('CdpWsProxy under a host command', () => { beforeEach(async () => { mock = createMockWebContents() guardFocus = async () => () => {} - const page = { ...mock.webContents, id: PAGE_ID } proxy = new CdpWsProxy( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements every WebContents member the proxy calls. - page as never, + mockAsWebContents(mock, PAGE_ID), () => () => {}, () => guardFocus() ) @@ -118,8 +117,7 @@ describe('CdpWsProxy under a host command', () => { }) function holdPage(): { revoke: () => void; release: () => Promise } { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements every WebContents member the release path calls. - const page = { ...mock.webContents, id: PAGE_ID } as never + const page = mockAsWebContents(mock, PAGE_ID) let revoked = false let finish = (): void => {} const held = runWithRemoteCliPageAdmission( diff --git a/src/main/browser/cdp-ws-proxy-input-focus.test.ts b/src/main/browser/cdp-ws-proxy-input-focus.test.ts index fbc95932e98..2dee61215c9 100644 --- a/src/main/browser/cdp-ws-proxy-input-focus.test.ts +++ b/src/main/browser/cdp-ws-proxy-input-focus.test.ts @@ -5,6 +5,7 @@ import { createMockWebContents, getSendCommandCalls, getSendCommandMethods, + mockAsWebContents, sendAndReceive } from './cdp-ws-proxy-test-harness' @@ -24,8 +25,7 @@ describe('CdpWsProxy agent input focus guard', () => { ) const mock = createMockWebContents() mock.webContents.getType.mockReturnValue('webview') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements every WebContents member the proxy calls. - const proxy = new CdpWsProxy(mock.webContents as never, () => () => {}, guardFocus) + const proxy = new CdpWsProxy(mockAsWebContents(mock), () => () => {}, guardFocus) const client = await connect(await proxy.start()) mock.webContents.debugger.sendCommand.mockClear() const replies: unknown[] = [] @@ -70,8 +70,7 @@ describe('CdpWsProxy agent input focus guard', () => { mock.webContents.getType.mockReturnValue('webview') const release = vi.fn() const proxy = new CdpWsProxy( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements every WebContents member the proxy calls. - mock.webContents as never, + mockAsWebContents(mock), () => () => {}, async () => release ) @@ -96,8 +95,7 @@ describe('CdpWsProxy agent input focus guard', () => { it('forwards presses on pages outside an Orca window without a guard', async () => { const mock = createMockWebContents() const guardFocus = vi.fn(async () => () => {}) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements every WebContents member the proxy calls. - const proxy = new CdpWsProxy(mock.webContents as never, () => () => {}, guardFocus) + const proxy = new CdpWsProxy(mockAsWebContents(mock), () => () => {}, guardFocus) const client = await connect(await proxy.start()) await sendAndReceive(client, { @@ -124,8 +122,7 @@ describe('CdpWsProxy agent input focus guard', () => { }) const release = vi.fn() const proxy = new CdpWsProxy( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements every WebContents member the proxy calls. - mock.webContents as never, + mockAsWebContents(mock), () => () => {}, async () => release ) @@ -148,8 +145,7 @@ describe('CdpWsProxy agent input focus guard', () => { const mock = createMockWebContents() mock.webContents.getType.mockReturnValue('webview') const proxy = new CdpWsProxy( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements every WebContents member the proxy calls. - mock.webContents as never, + mockAsWebContents(mock), () => () => {}, async () => { throw new Error('focus guard unavailable') diff --git a/src/main/browser/cdp-ws-proxy-origin.test.ts b/src/main/browser/cdp-ws-proxy-origin.test.ts index 26168ed0667..238ae12156c 100644 --- a/src/main/browser/cdp-ws-proxy-origin.test.ts +++ b/src/main/browser/cdp-ws-proxy-origin.test.ts @@ -7,6 +7,7 @@ import { createMockWebContents, getSendCommandMethods, sendAndReceive, + mockAsWebContents, type MockWebContents } from './cdp-ws-proxy-test-harness' @@ -47,8 +48,7 @@ describe('CdpWsProxy loopback request guard', () => { beforeEach(async () => { mock = createMockWebContents() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements every WebContents member the proxy calls. - proxy = new CdpWsProxy(mock.webContents as never, () => () => {}) + proxy = new CdpWsProxy(mockAsWebContents(mock), () => () => {}) endpoint = await proxy.start() }) diff --git a/src/main/browser/cdp-ws-proxy-test-harness.ts b/src/main/browser/cdp-ws-proxy-test-harness.ts index f3a153aaacd..0c9868b73fd 100644 --- a/src/main/browser/cdp-ws-proxy-test-harness.ts +++ b/src/main/browser/cdp-ws-proxy-test-harness.ts @@ -1,4 +1,5 @@ import { vi, type Mock } from 'vitest' +import type { WebContents } from 'electron' import WebSocket from 'ws' type DebuggerListener = (...args: unknown[]) => void @@ -98,6 +99,12 @@ export function createMockWebContents(): MockWebContents { } } +export function mockAsWebContents(mock: MockWebContents, id?: number): WebContents { + const contents = id === undefined ? mock.webContents : { ...mock.webContents, id } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements every WebContents member the CDP proxy and page-input paths call. + return contents as unknown as WebContents +} + export type SendCommandCall = [string, Record?, string?] export function connect(endpoint: string): Promise { diff --git a/src/main/browser/cdp-ws-proxy.test.ts b/src/main/browser/cdp-ws-proxy.test.ts index 16688765d72..4d16c97ac4a 100644 --- a/src/main/browser/cdp-ws-proxy.test.ts +++ b/src/main/browser/cdp-ws-proxy.test.ts @@ -7,6 +7,7 @@ import { getSendCommandCalls, getSendCommandMethods, sendAndReceive, + mockAsWebContents, type MockWebContents } from './cdp-ws-proxy-test-harness' @@ -21,8 +22,7 @@ describe('CdpWsProxy', () => { beforeEach(async () => { mock = createMockWebContents() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements every WebContents member the proxy calls. - proxy = new CdpWsProxy(mock.webContents as never, () => () => {}) + proxy = new CdpWsProxy(mockAsWebContents(mock), () => () => {}) endpoint = await proxy.start() }) diff --git a/src/main/claude-accounts/runtime-paths.test.ts b/src/main/claude-accounts/runtime-paths.test.ts index 9dd2a6b223e..398cde7b7de 100644 --- a/src/main/claude-accounts/runtime-paths.test.ts +++ b/src/main/claude-accounts/runtime-paths.test.ts @@ -14,9 +14,8 @@ vi.mock('node:fs', async (importOriginal) => { return { ...actual, mkdirSync: vi.fn(actual.mkdirSync) } }) -vi.mock('node:os', async () => { - // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() - const actual = await vi.importActual('node:os') +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/claude/claude-diff-panel-auto-open.ts b/src/main/claude/claude-diff-panel-auto-open.ts index fa4da4787d5..4359ad7a341 100644 --- a/src/main/claude/claude-diff-panel-auto-open.ts +++ b/src/main/claude/claude-diff-panel-auto-open.ts @@ -5,6 +5,7 @@ import { homedir } from 'node:os' import { join } from 'node:path' import { resolveClaudeGlobalConfigFile } from './claude-folder-trust-file' import { claudeVersionReaches } from './claude-hook-event-versions' +import { isRecord } from '../../shared/agent-status-child-work-value-guards' export const CLAUDE_DIFF_PANEL_FIRST_VERSION = '2.1.260' @@ -23,10 +24,7 @@ export function claudeDiffPanelStorePath(configDir: string): string { async function readJsonObject(path: string): Promise | null> { try { const parsed: unknown = JSON.parse(await readFile(path, 'utf8')) - return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) - ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a parsed non-array object is a string-keyed record. - (parsed as Record) - : null + return isRecord(parsed) ? parsed : null } catch { return null } diff --git a/src/main/cli/cli-installer.ts b/src/main/cli/cli-installer.ts index 492e9a1221c..1eb644c17ff 100644 --- a/src/main/cli/cli-installer.ts +++ b/src/main/cli/cli-installer.ts @@ -134,7 +134,6 @@ export class CliInstaller extends CliPathRegistration { ) } - // eslint-disable-next-line unicorn/prefer-ternary -- Why: the install path performs async side effects and is easier to audit as an explicit branch than as an awaited ternary. if (status.installMethod === 'symlink') { await this.installSymlink(status) await this.removeLegacyLinuxCommandIfManaged(status.launcherPath) diff --git a/src/main/cli/wsl-cli-registration-command.ts b/src/main/cli/wsl-cli-registration-command.ts index 333f80e7bcd..9c73e964213 100644 --- a/src/main/cli/wsl-cli-registration-command.ts +++ b/src/main/cli/wsl-cli-registration-command.ts @@ -20,12 +20,12 @@ export function buildWslCliInstallCommand( status: CliInstallStatus & { commandPath: string; launcherPath: string } ): string { const bridgePath = getBridgePathFromCommandPath(status.commandPath) - const legacyCommandPath = `${getPosixDirname(status.commandPath)}/${LEGACY_WSL_COMMAND_NAME}` + const commandDirectory = getPosixDirname(status.commandPath) + const legacyCommandPath = `${commandDirectory}/${LEGACY_WSL_COMMAND_NAME}` return [ // Runs under sh; Ubuntu 20.04 dash lacks pipefail, and no pipeline here needs it. 'set -eu', - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: WSL status sets pathDirectory to dirname(commandPath) whenever commandPath is set, and the caller requires commandPath. - `mkdir -p ${quotePosixShellDoubleQuotedApostrophe(status.pathDirectory as string)}`, + `mkdir -p ${quotePosixShellDoubleQuotedApostrophe(commandDirectory)}`, `mkdir -p ${quotePosixShellDoubleQuotedApostrophe(getPosixDirname(bridgePath))}`, buildRegistrationLockPrelude(status.commandPath), `command_tmp=${quotePosixShellDoubleQuotedApostrophe(`${status.commandPath}.tmp`)}.$$`, diff --git a/src/main/codex-accounts/runtime-home-legacy-migration.test.ts b/src/main/codex-accounts/runtime-home-legacy-migration.test.ts index 0e015ea5c15..e5293cfc270 100644 --- a/src/main/codex-accounts/runtime-home-legacy-migration.test.ts +++ b/src/main/codex-accounts/runtime-home-legacy-migration.test.ts @@ -32,8 +32,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/runtime-home-managed-auth-recovery.test.ts b/src/main/codex-accounts/runtime-home-managed-auth-recovery.test.ts index 8f5c397d80f..e5487f3baf2 100644 --- a/src/main/codex-accounts/runtime-home-managed-auth-recovery.test.ts +++ b/src/main/codex-accounts/runtime-home-managed-auth-recovery.test.ts @@ -27,8 +27,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/runtime-home-mirror-login-copy.test.ts b/src/main/codex-accounts/runtime-home-mirror-login-copy.test.ts index 874d67158ff..a8a3c256453 100644 --- a/src/main/codex-accounts/runtime-home-mirror-login-copy.test.ts +++ b/src/main/codex-accounts/runtime-home-mirror-login-copy.test.ts @@ -28,8 +28,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/runtime-home-per-account-homes.test.ts b/src/main/codex-accounts/runtime-home-per-account-homes.test.ts index 199104bd3fb..e81c78c99f8 100644 --- a/src/main/codex-accounts/runtime-home-per-account-homes.test.ts +++ b/src/main/codex-accounts/runtime-home-per-account-homes.test.ts @@ -24,8 +24,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/runtime-home-read-only-launch-resolution.test.ts b/src/main/codex-accounts/runtime-home-read-only-launch-resolution.test.ts index a3559dee4e4..1eb9a999689 100644 --- a/src/main/codex-accounts/runtime-home-read-only-launch-resolution.test.ts +++ b/src/main/codex-accounts/runtime-home-read-only-launch-resolution.test.ts @@ -2,6 +2,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { installFakeAppEnvironment } from '../../../config/scripts/vitest-host-ports-setup' import { realpathSync, writeFileSync } from 'node:fs' import type * as NodeFs from 'node:fs' +import type * as CodexConfigMirror from '../codex/codex-config-mirror' +import type * as CodexHomePaths from '../codex/codex-home-paths' +import type * as CodexSessionBackfillMarker from '../codex/codex-session-backfill-marker' import { join } from 'node:path' import { createSettings } from './runtime-home-settings-test-fixtures' import { @@ -72,8 +75,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir @@ -87,15 +90,11 @@ async function importServiceWithSideEffectSpies() { const accountBridge = vi.fn(async () => {}) const backfillMarker = vi.fn(() => false) vi.doMock('../codex/codex-home-paths', async () => ({ - ...(await vi.importActual( // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() - '../codex/codex-home-paths' - )), + ...(await vi.importActual('../codex/codex-home-paths')), syncSystemCodexResourcesIntoManagedHome: syncResources })) vi.doMock('../codex/codex-config-mirror', async () => ({ - ...(await vi.importActual( // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() - '../codex/codex-config-mirror' - )), + ...(await vi.importActual('../codex/codex-config-mirror')), syncSystemConfigIntoManagedCodexHome: syncConfig })) vi.doMock('../codex/codex-session-bridge', () => ({ @@ -105,7 +104,7 @@ async function importServiceWithSideEffectSpies() { startCodexAccountSessionBridgeInBackground: accountBridge })) vi.doMock('../codex/codex-session-backfill-marker', async () => ({ - ...(await vi.importActual( // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + ...(await vi.importActual( '../codex/codex-session-backfill-marker' )), markCodexSessionBackfillMarkerPending: backfillMarker diff --git a/src/main/codex-accounts/runtime-home-real-home-lane-routing.test.ts b/src/main/codex-accounts/runtime-home-real-home-lane-routing.test.ts index c3a9a84cfa7..708958fb405 100644 --- a/src/main/codex-accounts/runtime-home-real-home-lane-routing.test.ts +++ b/src/main/codex-accounts/runtime-home-real-home-lane-routing.test.ts @@ -33,8 +33,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/runtime-home-resume-selection-gate.test.ts b/src/main/codex-accounts/runtime-home-resume-selection-gate.test.ts index 0adea7932c3..6d8a9b34f35 100644 --- a/src/main/codex-accounts/runtime-home-resume-selection-gate.test.ts +++ b/src/main/codex-accounts/runtime-home-resume-selection-gate.test.ts @@ -71,8 +71,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/runtime-home-retained-auth-provenance.test.ts b/src/main/codex-accounts/runtime-home-retained-auth-provenance.test.ts index 9d804121375..a0d060b9be1 100644 --- a/src/main/codex-accounts/runtime-home-retained-auth-provenance.test.ts +++ b/src/main/codex-accounts/runtime-home-retained-auth-provenance.test.ts @@ -30,8 +30,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/runtime-home-system-default-mirror-readback.test.ts b/src/main/codex-accounts/runtime-home-system-default-mirror-readback.test.ts index a76cd0a19dd..cfb70abc6d0 100644 --- a/src/main/codex-accounts/runtime-home-system-default-mirror-readback.test.ts +++ b/src/main/codex-accounts/runtime-home-system-default-mirror-readback.test.ts @@ -22,8 +22,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/runtime-home-system-default-snapshot.test.ts b/src/main/codex-accounts/runtime-home-system-default-snapshot.test.ts index 89f8d74bd08..9739717b83e 100644 --- a/src/main/codex-accounts/runtime-home-system-default-snapshot.test.ts +++ b/src/main/codex-accounts/runtime-home-system-default-snapshot.test.ts @@ -33,8 +33,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/runtime-home-system-resource-materialization.test.ts b/src/main/codex-accounts/runtime-home-system-resource-materialization.test.ts index d48daf3b034..b4389c0eea1 100644 --- a/src/main/codex-accounts/runtime-home-system-resource-materialization.test.ts +++ b/src/main/codex-accounts/runtime-home-system-resource-materialization.test.ts @@ -25,8 +25,8 @@ vi.mock('electron', () => ({ } })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/runtime-home-wsl-managed-accounts.test.ts b/src/main/codex-accounts/runtime-home-wsl-managed-accounts.test.ts index 0868e0afcf5..1d7baf1ea2b 100644 --- a/src/main/codex-accounts/runtime-home-wsl-managed-accounts.test.ts +++ b/src/main/codex-accounts/runtime-home-wsl-managed-accounts.test.ts @@ -23,8 +23,8 @@ vi.mock('electron', () => ({ } })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/runtime-home-wsl-session-bridge.test.ts b/src/main/codex-accounts/runtime-home-wsl-session-bridge.test.ts index e33a8285417..5cb635350db 100644 --- a/src/main/codex-accounts/runtime-home-wsl-session-bridge.test.ts +++ b/src/main/codex-accounts/runtime-home-wsl-session-bridge.test.ts @@ -23,8 +23,8 @@ vi.mock('electron', () => ({ } })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/runtime-home-wsl-system-default.test.ts b/src/main/codex-accounts/runtime-home-wsl-system-default.test.ts index 25bd2d5d75e..ad177f2e199 100644 --- a/src/main/codex-accounts/runtime-home-wsl-system-default.test.ts +++ b/src/main/codex-accounts/runtime-home-wsl-system-default.test.ts @@ -18,8 +18,8 @@ vi.mock('electron', () => ({ } })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service-abandoned-login-supersede.test.ts b/src/main/codex-accounts/service-abandoned-login-supersede.test.ts index 071fdfe8c78..7b01a9f62dc 100644 --- a/src/main/codex-accounts/service-abandoned-login-supersede.test.ts +++ b/src/main/codex-accounts/service-abandoned-login-supersede.test.ts @@ -19,8 +19,8 @@ vi.mock('electron', () => ({ } })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service-account-add-login.test.ts b/src/main/codex-accounts/service-account-add-login.test.ts index 2b55b4a5729..374e1626fd6 100644 --- a/src/main/codex-accounts/service-account-add-login.test.ts +++ b/src/main/codex-accounts/service-account-add-login.test.ts @@ -31,8 +31,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service-account-selection-and-removal.test.ts b/src/main/codex-accounts/service-account-selection-and-removal.test.ts index 02a20fc91b8..d797882425c 100644 --- a/src/main/codex-accounts/service-account-selection-and-removal.test.ts +++ b/src/main/codex-accounts/service-account-selection-and-removal.test.ts @@ -20,8 +20,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service-add-account-from-home.test.ts b/src/main/codex-accounts/service-add-account-from-home.test.ts index 8247e5fadd0..0779fbd9f6f 100644 --- a/src/main/codex-accounts/service-add-account-from-home.test.ts +++ b/src/main/codex-accounts/service-add-account-from-home.test.ts @@ -18,8 +18,8 @@ vi.mock('electron', () => ({ } })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service-device-code-login.test.ts b/src/main/codex-accounts/service-device-code-login.test.ts index b63aef230ee..d9814212bb9 100644 --- a/src/main/codex-accounts/service-device-code-login.test.ts +++ b/src/main/codex-accounts/service-device-code-login.test.ts @@ -21,8 +21,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // oxlint-disable-line typescript/consistent-type-imports -- vi.importActual needs an inline import() type +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service-login-process-teardown.test.ts b/src/main/codex-accounts/service-login-process-teardown.test.ts index fe7e8112ef2..5a030bd161d 100644 --- a/src/main/codex-accounts/service-login-process-teardown.test.ts +++ b/src/main/codex-accounts/service-login-process-teardown.test.ts @@ -19,8 +19,8 @@ vi.mock('electron', () => ({ } })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service-managed-home-removal-retries.test.ts b/src/main/codex-accounts/service-managed-home-removal-retries.test.ts index 77c5f1c845f..2d4b31dbe80 100644 --- a/src/main/codex-accounts/service-managed-home-removal-retries.test.ts +++ b/src/main/codex-accounts/service-managed-home-removal-retries.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest' import { installFakeAppEnvironment } from '../../../config/scripts/vitest-host-ports-setup' import { existsSync } from 'node:fs' +import type * as NodeFs from 'node:fs' import { createManagedHome, createRateLimits, @@ -18,8 +19,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir @@ -31,7 +32,7 @@ describe('CodexAccountService config sync', () => { it('removes managed homes with bounded rm retries for transient Windows locks', async () => { vi.resetModules() - const actualFs = await vi.importActual('node:fs') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actualFs = await vi.importActual('node:fs') const rmSyncSpy = vi.fn(actualFs.rmSync) vi.doMock('node:fs', () => ({ ...actualFs, rmSync: rmSyncSpy })) @@ -71,7 +72,7 @@ describe('CodexAccountService config sync', () => { it('does not throw when managed home removal keeps failing on a held handle', async () => { vi.resetModules() - const actualFs = await vi.importActual('node:fs') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actualFs = await vi.importActual('node:fs') let managedHomePath = '' const lockedError = Object.assign(new Error('ENOTEMPTY: directory not empty'), { code: 'ENOTEMPTY' diff --git a/src/main/codex-accounts/service-quota-refresh-decoupling.test.ts b/src/main/codex-accounts/service-quota-refresh-decoupling.test.ts index cf2e5ea4724..f3ad5aa9883 100644 --- a/src/main/codex-accounts/service-quota-refresh-decoupling.test.ts +++ b/src/main/codex-accounts/service-quota-refresh-decoupling.test.ts @@ -20,8 +20,8 @@ vi.mock('electron', () => ({ } })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service-reauthenticate-activation.test.ts b/src/main/codex-accounts/service-reauthenticate-activation.test.ts index 9db69e7da5c..5056b0d7af6 100644 --- a/src/main/codex-accounts/service-reauthenticate-activation.test.ts +++ b/src/main/codex-accounts/service-reauthenticate-activation.test.ts @@ -23,8 +23,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service-reset-credit-durability.test.ts b/src/main/codex-accounts/service-reset-credit-durability.test.ts index 0079ae5162f..f8ce50735c9 100644 --- a/src/main/codex-accounts/service-reset-credit-durability.test.ts +++ b/src/main/codex-accounts/service-reset-credit-durability.test.ts @@ -23,8 +23,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service-reset-credit-home-ownership.test.ts b/src/main/codex-accounts/service-reset-credit-home-ownership.test.ts index 9447f25c66f..c6ce8a7d106 100644 --- a/src/main/codex-accounts/service-reset-credit-home-ownership.test.ts +++ b/src/main/codex-accounts/service-reset-credit-home-ownership.test.ts @@ -78,8 +78,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service-reset-credit-retry-routing.test.ts b/src/main/codex-accounts/service-reset-credit-retry-routing.test.ts index 080db1443e2..b7518e1040b 100644 --- a/src/main/codex-accounts/service-reset-credit-retry-routing.test.ts +++ b/src/main/codex-accounts/service-reset-credit-retry-routing.test.ts @@ -23,8 +23,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service-reset-credit-target-routing.test.ts b/src/main/codex-accounts/service-reset-credit-target-routing.test.ts index d026111c8a1..2846503affa 100644 --- a/src/main/codex-accounts/service-reset-credit-target-routing.test.ts +++ b/src/main/codex-accounts/service-reset-credit-target-routing.test.ts @@ -23,8 +23,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service-system-default-identity.test.ts b/src/main/codex-accounts/service-system-default-identity.test.ts index 9256f9fb065..dffce9e4375 100644 --- a/src/main/codex-accounts/service-system-default-identity.test.ts +++ b/src/main/codex-accounts/service-system-default-identity.test.ts @@ -20,8 +20,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service-wsl-accounts.test.ts b/src/main/codex-accounts/service-wsl-accounts.test.ts index 5da4c5e43da..b2e237e01e7 100644 --- a/src/main/codex-accounts/service-wsl-accounts.test.ts +++ b/src/main/codex-accounts/service-wsl-accounts.test.ts @@ -27,8 +27,8 @@ vi.mock('electron', () => ({ } })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/service.test.ts b/src/main/codex-accounts/service.test.ts index 7926146ec1a..e54d9043ee0 100644 --- a/src/main/codex-accounts/service.test.ts +++ b/src/main/codex-accounts/service.test.ts @@ -26,8 +26,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/sta-4422-transient-ownership-error.test.ts b/src/main/codex-accounts/sta-4422-transient-ownership-error.test.ts index d245bb6f96e..e9dae685612 100644 --- a/src/main/codex-accounts/sta-4422-transient-ownership-error.test.ts +++ b/src/main/codex-accounts/sta-4422-transient-ownership-error.test.ts @@ -88,8 +88,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/sta-4734-login-rollback-deletes-authenticated-home.test.ts b/src/main/codex-accounts/sta-4734-login-rollback-deletes-authenticated-home.test.ts index 5d12fcc4548..e6827b53edd 100644 --- a/src/main/codex-accounts/sta-4734-login-rollback-deletes-authenticated-home.test.ts +++ b/src/main/codex-accounts/sta-4734-login-rollback-deletes-authenticated-home.test.ts @@ -21,8 +21,8 @@ vi.mock('electron', () => ({ })) beforeEach(() => installFakeAppEnvironment({ getPath: () => testState.userDataDir })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir diff --git a/src/main/codex-accounts/sta-4735-unreadable-host-lane-writes.test.ts b/src/main/codex-accounts/sta-4735-unreadable-host-lane-writes.test.ts index 654fc120132..fcf3ccc80bc 100644 --- a/src/main/codex-accounts/sta-4735-unreadable-host-lane-writes.test.ts +++ b/src/main/codex-accounts/sta-4735-unreadable-host-lane-writes.test.ts @@ -76,8 +76,8 @@ vi.mock('node:fs', async (importOriginal) => { vi.mock('electron', () => ({ app: { getPath: () => testState.userDataDir } })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir } }) diff --git a/src/main/codex/codex-structured-launch-resolution.test.ts b/src/main/codex/codex-structured-launch-resolution.test.ts index 7bfd837396b..16687aef0eb 100644 --- a/src/main/codex/codex-structured-launch-resolution.test.ts +++ b/src/main/codex/codex-structured-launch-resolution.test.ts @@ -314,7 +314,6 @@ describe('codex structured launch resolution', () => { it('resumes the durable thread ID under its pinned account home', async () => { const launch = await resolverFor( record({ - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads only each link's handle, so the link's other fields stay unset. launchArgs: ['--enable', 'unified_exec', '-c', 'model_reasoning_effort=high'], providerHandleChain: [ { handle: codexProviderHandle('thread-current') } diff --git a/src/main/codex/codex-trust-config-concurrent-launch.test.ts b/src/main/codex/codex-trust-config-concurrent-launch.test.ts index 1319ef7e0bb..9c29b9f6bfd 100644 --- a/src/main/codex/codex-trust-config-concurrent-launch.test.ts +++ b/src/main/codex/codex-trust-config-concurrent-launch.test.ts @@ -26,9 +26,8 @@ const testState = { previousUserDataPath: undefined as string | undefined } -vi.mock('node:os', async () => { - // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() - const actual = await vi.importActual('node:os') +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: () => testState.fakeHomeDir } }) diff --git a/src/main/computer/desktop-script-provider-test-harness.ts b/src/main/computer/desktop-script-provider-test-harness.ts index 7ef19c0cc1e..af46e702447 100644 --- a/src/main/computer/desktop-script-provider-test-harness.ts +++ b/src/main/computer/desktop-script-provider-test-harness.ts @@ -1,6 +1,3 @@ -/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 8 desktop-script-provider specs, not shipped code, and it falls - outside the *.test / *.spec / tests glob set. The stubs replace node builtins (child_process, fs/promises) for a provider - that shells out; inlining them would duplicate the vi.hoisted fixture into all 8 specs. */ import { expect, vi } from 'vitest' import type { DesktopScriptRuntimeHost } from './desktop-script-runtime-host' diff --git a/src/main/daemon/degraded-daemon-pty-provider.ts b/src/main/daemon/degraded-daemon-pty-provider.ts index 18fd762b3f6..c7552d110ec 100644 --- a/src/main/daemon/degraded-daemon-pty-provider.ts +++ b/src/main/daemon/degraded-daemon-pty-provider.ts @@ -317,8 +317,8 @@ export class DegradedDaemonPtyProvider implements IPtyProvider { for (const id of this.getSessionIdsRoutedTo('current-daemon')) { this.ownerRecovery.forgetRoute(id) // Restart also kills listed sessions the adapter never tracked as active. - // oxlint-disable-next-line unicorn/no-useless-spread -- copy-safe: listeners may unsubscribe during iteration - for (const listener of [...this.exitListeners]) { + // Why: listeners may unsubscribe during iteration. + for (const listener of Array.from(this.exitListeners)) { listener({ id, code, ...exit }) } } diff --git a/src/main/daemon/session-ingest-throughput.bench.test.ts b/src/main/daemon/session-ingest-throughput.bench.test.ts index 9138c64477f..b57deb0d9dc 100644 --- a/src/main/daemon/session-ingest-throughput.bench.test.ts +++ b/src/main/daemon/session-ingest-throughput.bench.test.ts @@ -113,7 +113,6 @@ describe.skipIf(!benchEnabled)('daemon session ingest throughput', () => { `${name}: ${rate.toFixed(1)} MB/s (${ms.toFixed(0)}ms for ${(bytes / 1024 / 1024).toFixed(1)}MB)` ) } - // eslint-disable-next-line no-console -- bench harness output console.log(`\n[session-ingest] ${COLS}x${ROWS}\n ${rows.join('\n ')}`) expect(rows.length).toBe(2) }) diff --git a/src/main/foreign-sqlite-readers/readers/opencode-binder-sessions.ts b/src/main/foreign-sqlite-readers/readers/opencode-binder-sessions.ts index e12d468e074..28687f14593 100644 --- a/src/main/foreign-sqlite-readers/readers/opencode-binder-sessions.ts +++ b/src/main/foreign-sqlite-readers/readers/opencode-binder-sessions.ts @@ -1,6 +1,7 @@ import { statSync } from 'node:fs' import { readOpenCodeDatabase } from '../../ai-vault/session-scanner-opencode-sqlite-open' import { columnExists, tableExists } from '../../opencode-usage/schema-helpers' +import { isRecord } from '../../../shared/agent-status-child-work-value-guards' import type { BinderSessionRow, OpenCodeSessionCursor } from '../opencode-binder-sessions-result' /** Composite cursor pagination preserves same-millisecond rows; unknown schemas or absent stores return empty. */ @@ -31,23 +32,21 @@ export function readOpenCodeBinderSessions( .all(cursor.ms, cursor.ms, cursor.id) const sessions: BinderSessionRow[] = [] for (const row of rows) { - if (typeof row !== 'object' || row === null) { + if (!isRecord(row)) { continue } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: node:sqlite returns plain row objects; the object check above plus the per-field validation below reject anything else. - const record = row as Record if ( - typeof record.id !== 'string' || - typeof record.directory !== 'string' || - typeof record.time_created !== 'number' + typeof row.id !== 'string' || + typeof row.directory !== 'string' || + typeof row.time_created !== 'number' ) { continue } sessions.push({ - id: record.id, - directory: record.directory, - createdAtMs: record.time_created, - parentId: typeof record.parent_id === 'string' ? record.parent_id : null + id: row.id, + directory: row.directory, + createdAtMs: row.time_created, + parentId: typeof row.parent_id === 'string' ? row.parent_id : null }) } return sessions diff --git a/src/main/github/work-item-search-test-harness.ts b/src/main/github/work-item-search-test-harness.ts index d36732e9eb1..d3783f0afe1 100644 --- a/src/main/github/work-item-search-test-harness.ts +++ b/src/main/github/work-item-search-test-harness.ts @@ -1,6 +1,3 @@ -/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 6 work-item-search specs, not shipped code, and it falls outside - the *.test / *.spec / tests glob set. One consumer lives in tests/e2e, where the relative mock ids ('../git/...') resolve to - different modules, so moving these calls into the specs would silently stop mocking there. */ import { afterEach, beforeEach, vi } from 'vitest' import type { Mock } from 'vitest' import { randomUUID } from 'node:crypto' diff --git a/src/main/ipc/filesystem-search-rg-timeout.test.ts b/src/main/ipc/filesystem-search-rg-timeout.test.ts index 734f03825b7..31b2fccc64d 100644 --- a/src/main/ipc/filesystem-search-rg-timeout.test.ts +++ b/src/main/ipc/filesystem-search-rg-timeout.test.ts @@ -329,11 +329,10 @@ describe('filesystem rg search timeout', () => { parseWslPathMock.mockReturnValue({ distro: 'Ubuntu' }) registerFilesystemHandlers({} as never) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: fs:search handlers return Promise. const promise = handlers.get('fs:search')!( { sender: { id: 7 } }, { rootPath: '\\\\wsl.localhost\\Ubuntu\\repo', query: 'ok' } - ) as Promise + ) await flushMicrotasks() child.emit('close', 1, null) diff --git a/src/main/ipc/jira/ipc-params.ts b/src/main/ipc/jira/ipc-params.ts index be6ec2230fd..73ebff9dbcd 100644 --- a/src/main/ipc/jira/ipc-params.ts +++ b/src/main/ipc/jira/ipc-params.ts @@ -1,4 +1,5 @@ import type { JiraIssueUpdate, JiraSiteSelection } from '../../../shared/jira/jira-types' +import { isRecord } from '../../../shared/agent-status-child-work-value-guards' export function normalizeJiraSiteId(value: unknown): string | undefined { return typeof value === 'string' && value.trim() ? value.trim() : undefined @@ -23,56 +24,70 @@ function normalizeStringArray(value: unknown): string[] | undefined { /** Narrows an untrusted IPC payload to the issue-update fields the host accepts. */ export function normalizeJiraIssueUpdate(value: unknown): JiraIssueUpdate | null { - if (!value || typeof value !== 'object' || Array.isArray(value)) { + if (!isRecord(value)) { return null } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The following checks reject every host-accepted field with an invalid value. - const input = value as JiraIssueUpdate - if (input.title !== undefined && typeof input.title !== 'string') { - return null + const update: JiraIssueUpdate = {} + if (value.title !== undefined) { + if (typeof value.title !== 'string') { + return null + } + update.title = value.title } - if (input.labels !== undefined && normalizeStringArray(input.labels) === undefined) { - return null + if (value.labels !== undefined) { + const labels = normalizeStringArray(value.labels) + if (labels === undefined) { + return null + } + update.labels = labels } - if (input.labelOperations !== undefined) { - const operations = input.labelOperations + if (value.labelOperations !== undefined) { + const operations = value.labelOperations + if (!isRecord(operations)) { + return null + } + const add = normalizeStringArray(operations.add) + const remove = normalizeStringArray(operations.remove) if ( - !operations || - typeof operations !== 'object' || - Array.isArray(operations) || - (operations.add !== undefined && normalizeStringArray(operations.add) === undefined) || - (operations.remove !== undefined && normalizeStringArray(operations.remove) === undefined) + (operations.add !== undefined && add === undefined) || + (operations.remove !== undefined && remove === undefined) ) { return null } + update.labelOperations = { + ...(add !== undefined ? { add } : {}), + ...(remove !== undefined ? { remove } : {}) + } } - if ( - input.assigneeAccountId !== undefined && - input.assigneeAccountId !== null && - typeof input.assigneeAccountId !== 'string' - ) { - return null + if (value.assigneeAccountId !== undefined) { + if (value.assigneeAccountId !== null && typeof value.assigneeAccountId !== 'string') { + return null + } + update.assigneeAccountId = value.assigneeAccountId } - if ( - input.priorityId !== undefined && - input.priorityId !== null && - typeof input.priorityId !== 'string' - ) { - return null + if (value.priorityId !== undefined) { + if (value.priorityId !== null && typeof value.priorityId !== 'string') { + return null + } + update.priorityId = value.priorityId } - if (input.transitionId !== undefined && typeof input.transitionId !== 'string') { - return null + if (value.transitionId !== undefined) { + if (typeof value.transitionId !== 'string') { + return null + } + update.transitionId = value.transitionId } - if ( - input.transitionFields !== undefined && - (input.transitionFields === null || - typeof input.transitionFields !== 'object' || - Array.isArray(input.transitionFields)) - ) { - return null + if (value.transitionFields !== undefined) { + if (!isRecord(value.transitionFields)) { + return null + } + update.transitionFields = value.transitionFields } - if (input.transitionComment !== undefined && typeof input.transitionComment !== 'string') { - return null + if (value.transitionComment !== undefined) { + if (typeof value.transitionComment !== 'string') { + return null + } + update.transitionComment = value.transitionComment } - return input + return update } diff --git a/src/main/ipc/pty-launch-parity.test-fixture.ts b/src/main/ipc/pty-launch-parity.test-fixture.ts index 48a0bd53292..27f6fd5eeaa 100644 --- a/src/main/ipc/pty-launch-parity.test-fixture.ts +++ b/src/main/ipc/pty-launch-parity.test-fixture.ts @@ -14,6 +14,11 @@ import { } from '../../shared/launch-parity-window-request.test-fixture' import { isHiddenRendererPty } from './pty-hidden-delivery-gate' import { HOST_TAB_ID } from './pty-launch-parity-host.test-cases' +import { + partialRuntimeStore, + storeProject, + storeProjectGroup +} from '../runtime/runtime-store-partial-fixture' const WINDOWS_ENV = { COMSPEC: 'C:\\Windows\\system32\\cmd.exe', @@ -155,13 +160,13 @@ export function startParityLanes( ...bindingStore, getSettings: () => settings, getProjects: () => [ - { + storeProject({ id: 'project-1', sourceRepoIds: ['repo-1'], ...(args.workspace.projectRuntime ? { localWindowsRuntimePreference: args.workspace.projectRuntime } : {}) - } + }) ], getRepo: (id: string) => (repo && id === repo.id ? repo : undefined), getRepos: () => (repo ? [repo] : []), @@ -169,10 +174,11 @@ export function startParityLanes( getWorktreeMeta: () => undefined, getFolderWorkspace: () => folderWorkspace ?? undefined, getFolderWorkspaces: () => (folderWorkspace ? [folderWorkspace] : []), - getProjectGroups: () => [{ id: 'pg-1', name: 'group', parentPath: null, connectionId: null }] + getProjectGroups: () => [ + storeProjectGroup({ id: 'pg-1', name: 'group', parentPath: null, connectionId: null }) + ] } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: launches read only the store members defined above; the rest of the persistence store is unreachable here. - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) const provider = vi.fn(async (options: { sessionId?: string }) => { if (args.spawnError) { throw args.spawnError diff --git a/src/main/ipc/pty-spawn-env-terminal-basics.test.ts b/src/main/ipc/pty-spawn-env-terminal-basics.test.ts index c91125c218f..c5ee171093b 100644 --- a/src/main/ipc/pty-spawn-env-terminal-basics.test.ts +++ b/src/main/ipc/pty-spawn-env-terminal-basics.test.ts @@ -405,8 +405,7 @@ describe('registerPtyHandlers', () => { const tabId = 'tab-1' const paneKey = makePaneKey(tabId, leafId) handlers.clear() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the suite's mock BrowserWindow, widened the same way every other registerPtyHandlers call in this file does; the handler only touches webContents.send. - registerPtyHandlers(mainWindow as never) + registerPtyHandlers(mainWindow) await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24, diff --git a/src/main/ipc/pty/daemon-late-exit-test-fixture.ts b/src/main/ipc/pty/daemon-late-exit-test-fixture.ts index c59573ff654..d81b1fb7062 100644 --- a/src/main/ipc/pty/daemon-late-exit-test-fixture.ts +++ b/src/main/ipc/pty/daemon-late-exit-test-fixture.ts @@ -164,8 +164,8 @@ export async function startLateExitHarness() { }, id: PTY_ID, deliverProviderExit: (event: { id: string; code: number; incarnationId?: string }) => { - // oxlint-disable-next-line unicorn/no-useless-spread -- copy-safe: exit listeners may unsubscribe while receiving the event. - for (const listener of [...harness.adapter['exitListeners']]) { + // Why: exit listeners may unsubscribe while receiving the event. + for (const listener of Array.from(harness.adapter['exitListeners'])) { listener(event) } }, diff --git a/src/main/ipc/registered-worktree-roots-scoped-invalidation.test.ts b/src/main/ipc/registered-worktree-roots-scoped-invalidation.test.ts index 2fc8eb56219..9b34810c6b6 100644 --- a/src/main/ipc/registered-worktree-roots-scoped-invalidation.test.ts +++ b/src/main/ipc/registered-worktree-roots-scoped-invalidation.test.ts @@ -3,7 +3,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Store } from '../persistence' import type { Repo } from '../../shared/repo-types' -const mocks = vi.hoisted(() => ({ graph: vi.fn(), stat: vi.fn(), realpath: vi.fn() })) +const mocks = vi.hoisted(() => ({ + graph: vi.fn<(repo: Repo) => Promise<{ path: string }[]>>(), + stat: vi.fn(), + realpath: vi.fn() +})) vi.mock('node:fs', () => ({ realpathSync: (path: string) => path, statSync: mocks.stat })) vi.mock('node:fs/promises', () => ({ stat: mocks.stat, realpath: mocks.realpath })) vi.mock('../repo-worktrees', () => ({ @@ -44,8 +48,7 @@ function fixture(): Store { } function listedRepoPaths(): string[] { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this file installs the listRepoWorktreeGraph mock, so every call's first argument is the Repo it was handed. - return mocks.graph.mock.calls.map((call) => (call[0] as Repo).path) + return mocks.graph.mock.calls.map((call) => call[0].path) } describe('authorized-roots invalidation scope', () => { diff --git a/src/main/ipc/runtime-environments/runtime-environment-session-tabs-delta.integration.test.ts b/src/main/ipc/runtime-environments/runtime-environment-session-tabs-delta.integration.test.ts index cd778db8fbc..fa38d1a7d83 100644 --- a/src/main/ipc/runtime-environments/runtime-environment-session-tabs-delta.integration.test.ts +++ b/src/main/ipc/runtime-environments/runtime-environment-session-tabs-delta.integration.test.ts @@ -16,6 +16,7 @@ import { OrcaRuntimeRpcServer } from '../../runtime/runtime-rpc' import { makeStore, SESSION_WORKTREE_ID } from '../../runtime/paired-client-navigation-test-harness' import { resetSharedControlSupport } from './runtime-environment-shared-control-support' import { subscribeRuntimeEnvironment } from './runtime-environment-transport-routing' +import { partialRuntimeStore } from '../../runtime/runtime-store-partial-fixture' vi.mock('electron', () => ({ BrowserWindow: { getAllWindows: () => [] } })) @@ -76,8 +77,7 @@ function recorder(): Received & { onResponse: (response: RuntimeRpcResponse { it('serves an old desktop full frames and a new desktop the same frames from deltas', async () => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared paired-client harness store covers every store read this host makes. - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) runtime.attachWindow(1) runtime.markGraphReady(1) publish(runtime, 1, -1) diff --git a/src/main/ipc/runtime.ts b/src/main/ipc/runtime.ts index bfe30abc29f..f11318a9cfe 100644 --- a/src/main/ipc/runtime.ts +++ b/src/main/ipc/runtime.ts @@ -73,8 +73,7 @@ export function registerRuntimeHandlers(runtime: OrcaRuntimeService): void { if (event.senderFrame !== event.sender.mainFrame) { throw new Error('Runtime RPC call must originate from the current main frame') } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the dispatcher's RpcSuccess/RpcFailure union is the same envelope RuntimeRpcResponse describes; only the `result` generic differs, and this call site declares it as unknown. - return (await new RpcDispatcher({ runtime, methods: ALL_RPC_METHODS }).dispatch( + return new RpcDispatcher({ runtime, methods: ALL_RPC_METHODS }).dispatch( { id: 'desktop-ipc', authToken: 'desktop-ipc', @@ -88,7 +87,7 @@ export function registerRuntimeHandlers(runtime: OrcaRuntimeService): void { connectionId: desktopSenders.connectionIdFor(event.sender), clientCapabilities: DESKTOP_RENDERER_RUNTIME_CLIENT_CAPABILITIES } - )) as RuntimeRpcResponse + ) } ) diff --git a/src/main/ipc/ssh/ssh-host-server-on-connect-wiring.test.ts b/src/main/ipc/ssh/ssh-host-server-on-connect-wiring.test.ts index 4a0c8b7bf20..c5934c0886f 100644 --- a/src/main/ipc/ssh/ssh-host-server-on-connect-wiring.test.ts +++ b/src/main/ipc/ssh/ssh-host-server-on-connect-wiring.test.ts @@ -62,8 +62,7 @@ beforeEach(() => { userDataPath = mkdtempSync(join(tmpdir(), 'host-server-wiring-')) store = createSqliteTestStore(Store, { dataFile: join(userDataPath, 'orca-data.json') }) store.addSshTarget(TARGET) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: SshConnectionStore wraps the real test store it is given. - mocks.registry.current = new SshConnectionStore(store as never) + mocks.registry.current = new SshConnectionStore(store) }) afterEach(async () => { diff --git a/src/main/ipc/worktree-base-directory-watch-targets.test.ts b/src/main/ipc/worktree-base-directory-watch-targets.test.ts index 6434685839e..4b9d7f1e831 100644 --- a/src/main/ipc/worktree-base-directory-watch-targets.test.ts +++ b/src/main/ipc/worktree-base-directory-watch-targets.test.ts @@ -46,7 +46,7 @@ function makeRepo(index: number, overrides: Partial = {}): Repo { badgeColor: '#000000', addedAt: index, ...overrides - } as Repo + } } function makeStore(repos: Repo[]) { diff --git a/src/main/ipc/worktree-base-directory-watcher.test.ts b/src/main/ipc/worktree-base-directory-watcher.test.ts index 320fd391bca..c8883c5ee2f 100644 --- a/src/main/ipc/worktree-base-directory-watcher.test.ts +++ b/src/main/ipc/worktree-base-directory-watcher.test.ts @@ -78,7 +78,7 @@ function makeRepo(overrides: Partial = {}): Repo { badgeColor: '#000000', addedAt: 1, ...overrides - } as Repo + } } function makeStore(repos: Repo[]) { diff --git a/src/main/ipc/worktrees/listing/detected-provider-listing-catalog-version.test.ts b/src/main/ipc/worktrees/listing/detected-provider-listing-catalog-version.test.ts index 8af4f16023d..cc494df539c 100644 --- a/src/main/ipc/worktrees/listing/detected-provider-listing-catalog-version.test.ts +++ b/src/main/ipc/worktrees/listing/detected-provider-listing-catalog-version.test.ts @@ -33,8 +33,13 @@ const { __resetDetectedWorktreeScanCacheForTests, invalidateDetectedWorktreeScan const { getLocalWorktreeCatalogVersion, getLocalWorktreeScanGeneration } = await import('../../../local-worktree-scan-generation') -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the listing reads only id, path, displayName and connectionId off the repo row. -const localRepo = { id: 'repo-local', path: '/repos/local', displayName: 'local' } as Repo +const localRepo: Repo = { + id: 'repo-local', + path: '/repos/local', + displayName: 'local', + badgeColor: '#000', + addedAt: 0 +} function createStore(repo: Repo) { // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the listing reads only these store methods; the pruning that would need more is mocked above. diff --git a/src/main/ipc/worktrees/listing/detected-provider-listing-overtaken-scan.test.ts b/src/main/ipc/worktrees/listing/detected-provider-listing-overtaken-scan.test.ts index abb3c08f388..3d15fc745a2 100644 --- a/src/main/ipc/worktrees/listing/detected-provider-listing-overtaken-scan.test.ts +++ b/src/main/ipc/worktrees/listing/detected-provider-listing-overtaken-scan.test.ts @@ -31,8 +31,13 @@ const { listDetectedWorktreesForCapturedRepo } = await import('./detected-provid const { __resetDetectedWorktreeScanCacheForTests, invalidateDetectedWorktreeScanCache } = await import('./detected-worktree-scan-cache') -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the listing reads only id, path, displayName and connectionId off the repo row. -const localRepo = { id: 'repo-local', path: '/repos/local', displayName: 'local' } as Repo +const localRepo: Repo = { + id: 'repo-local', + path: '/repos/local', + displayName: 'local', + badgeColor: '#000', + addedAt: 0 +} function createStore(repo: Repo) { // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the listing reads only these store methods; the pruning that would need more is mocked above. diff --git a/src/main/ipc/worktrees/listing/detected-worktree-classification.test.ts b/src/main/ipc/worktrees/listing/detected-worktree-classification.test.ts index f22aa1df679..0e9ab76f811 100644 --- a/src/main/ipc/worktrees/listing/detected-worktree-classification.test.ts +++ b/src/main/ipc/worktrees/listing/detected-worktree-classification.test.ts @@ -49,7 +49,7 @@ const repo: Repo = { displayName: 'repo', badgeColor: '#000', addedAt: 0 -} as Repo +} const ownershipMeta = getProjectHostSetupWorktreeMeta([], repo) diff --git a/src/main/ipc/worktrees/listing/detected-worktree-scan-superseded.test.ts b/src/main/ipc/worktrees/listing/detected-worktree-scan-superseded.test.ts index ca631989cb1..f3dd40563b8 100644 --- a/src/main/ipc/worktrees/listing/detected-worktree-scan-superseded.test.ts +++ b/src/main/ipc/worktrees/listing/detected-worktree-scan-superseded.test.ts @@ -29,8 +29,13 @@ const { listDetectedGitWorktrees } = await import('./detected-worktree-scan-cache') -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the scan reads only id, path and connectionId off the repo row. -const repo = { id: 'repo-1', path: '/repos/one', displayName: 'one' } as Repo +const repo: Repo = { + id: 'repo-1', + path: '/repos/one', + displayName: 'one', + badgeColor: '#000', + addedAt: 0 +} // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the scan calls only the expectation capture; the metadata prune it feeds is mocked above. const store = { captureNativeLocalWorktreeMetadataScanExpectation: () => ({ repo: { id: repo.id }, metadata: [] }) diff --git a/src/main/jcode/hook-gate-script.test.ts b/src/main/jcode/hook-gate-script.test.ts index 25ddc0228e1..b0762f99276 100644 --- a/src/main/jcode/hook-gate-script.test.ts +++ b/src/main/jcode/hook-gate-script.test.ts @@ -8,9 +8,8 @@ import { removeTreeSync } from '../../shared/windows-transient-lock-removal' import { runProcess } from '@orca/process-host' const { homedirMock } = vi.hoisted(() => ({ homedirMock: vi.fn<() => string>() })) -vi.mock('os', async () => { - // oxlint-disable-next-line typescript/consistent-type-imports -- vi.importActual needs the inline import() to type the real module - const actual = await vi.importActual('os') +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: homedirMock } }) diff --git a/src/main/jcode/hook-service.test.ts b/src/main/jcode/hook-service.test.ts index f9927865a5c..525eb35433f 100644 --- a/src/main/jcode/hook-service.test.ts +++ b/src/main/jcode/hook-service.test.ts @@ -7,9 +7,8 @@ const { homedirMock } = vi.hoisted(() => ({ homedirMock: vi.fn<() => string>() })) -vi.mock('os', async () => { - // oxlint-disable-next-line typescript/consistent-type-imports -- vi.importActual needs the inline import() to type the real module - const actual = await vi.importActual('os') +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: homedirMock } }) diff --git a/src/main/managed-server/activation/orcad-activation-fence-generation.test.ts b/src/main/managed-server/activation/orcad-activation-fence-generation.test.ts index f187ccfee96..e9d865c1042 100644 --- a/src/main/managed-server/activation/orcad-activation-fence-generation.test.ts +++ b/src/main/managed-server/activation/orcad-activation-fence-generation.test.ts @@ -12,6 +12,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import type * as ExecCommandModule from '../../ssh/remote-exec/remote-exec-command' +import { unusedOrcadHostTransport } from '../../ssh/unused-host-transport.test-fixture' // Execute generated host guards in real local shells. const shell = vi.hoisted((): { env: NodeJS.ProcessEnv | undefined } => ({ env: undefined })) @@ -53,8 +54,7 @@ describe.skipIf(process.platform === 'win32')('a superseded activation fence hol const fence = join(root, '.install-lock') const journal = join(root, 'transaction.json') const launched = join(home, 'launched-by-stale-holder') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: execCommand is mocked to a local shell, so the connection is never used. - const conn = {} as never + const conn = unusedOrcadHostTransport() const options = { conn, host: getRemoteHostPlatform('linux-x64'), remoteHome: home } let resumeStale!: () => void @@ -120,8 +120,7 @@ describe.skipIf(process.platform === 'win32')('a superseded activation fence hol `#!/bin/sh\nif [ "$1" = '${journal}' ] && [ ! -f '${ready}' ]; then touch '${ready}'; while [ ! -f '${resume}' ]; do sleep 0.05; done; fi\nexec /bin/mv "$@"\n` ) chmodSync(join(bin, 'mv'), 0o755) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: execCommand is mocked to a local shell, so the connection is never used. - const conn = {} as never + const conn = unusedOrcadHostTransport() const options = { conn, host: getRemoteHostPlatform('linux-x64'), remoteHome: home } shell.env = { ...process.env, PATH: `${bin}:/usr/bin:/bin` } let finishSuccessor: (() => void) | undefined @@ -170,8 +169,7 @@ describe.skipIf(process.platform === 'win32')('a superseded activation fence hol mkdirSync(join(root, 'profiles'), { recursive: true }) writeFileSync(join(lockDir, '.orca-fence-owner'), 'successor') writeFileSync(join(root, 'profiles', 'state.json'), 'successor-state') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: execCommand is mocked to a local shell, so the connection is never used. - const conn = {} as never + const conn = unusedOrcadHostTransport() const options = { conn, host: getRemoteHostPlatform('linux-x64'), remoteHome: home } await expect( runWithOrcadFence({ lockDir, token: 'stale' }, () => diff --git a/src/main/managed-server/host-state/orcad-state-mutation-timeout.test.ts b/src/main/managed-server/host-state/orcad-state-mutation-timeout.test.ts index 40bf9540b3f..9bf747df1f0 100644 --- a/src/main/managed-server/host-state/orcad-state-mutation-timeout.test.ts +++ b/src/main/managed-server/host-state/orcad-state-mutation-timeout.test.ts @@ -43,7 +43,6 @@ import { deployOrcad } from '../../ssh/orcad-remote-deploy' import { rollbackOrcad } from '../install/orcad-remote-rollback' import { recoverInterruptedOrcadActivation } from '../activation/orcad-activation-recovery' import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform' -import type { OrcadHostTransport } from '../../ssh/orcad-host-transport' import { BUILD_HASH, FakeOrcadHost, @@ -51,6 +50,7 @@ import { OLD } from '../activation/orcad-activation-host-test-harness' import { isSnapshotCaptureCommand } from '../../ssh/orcad-snapshot-capture-command' +import { unusedOrcadHostTransport } from '../../ssh/unused-host-transport.test-fixture' let host = new FakeOrcadHost() type StateMutation = 'capture' | 'restore' @@ -58,8 +58,7 @@ let slow: StateMutation | null = null let running = false const slot = { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: execCommand is mocked, so nothing reads the connection. - conn: {} as OrcadHostTransport, + conn: unusedOrcadHostTransport(), host: getRemoteHostPlatform('linux-x64'), remoteHome: '/home/u', nodePath: '/usr/bin/node', diff --git a/src/main/managed-server/install/orcad-remote-install-termination.test.ts b/src/main/managed-server/install/orcad-remote-install-termination.test.ts index 5037ef906da..7de0b69783b 100644 --- a/src/main/managed-server/install/orcad-remote-install-termination.test.ts +++ b/src/main/managed-server/install/orcad-remote-install-termination.test.ts @@ -17,7 +17,6 @@ vi.mock('./orcad-remote-node-runtime', () => ({ ensureRemoteOrcadNodeRuntime: vi.fn().mockResolvedValue(undefined) })) -import type { OrcadHostTransport } from '../../ssh/orcad-host-transport' import { execCommand } from '../../ssh/remote-exec/remote-exec-command' import { uploadRemoteInstallDirectory, writeRemoteInstallFile } from './remote-install-transfers' import { installOrcadBundle } from './orcad-remote-install' @@ -31,9 +30,9 @@ import { import { mkdtempSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { unusedOrcadHostTransport } from '../../ssh/unused-host-transport.test-fixture' -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: All connection operations are mocked. -const conn = {} as OrcadHostTransport +const conn = unusedOrcadHostTransport() const mockExec = vi.mocked(execCommand) const mockUpload = vi.mocked(uploadRemoteInstallDirectory) const mockWrite = vi.mocked(writeRemoteInstallFile) diff --git a/src/main/managed-server/install/orcad-remote-log-tail.test.ts b/src/main/managed-server/install/orcad-remote-log-tail.test.ts index 7827d56999b..f6c35bf7cff 100644 --- a/src/main/managed-server/install/orcad-remote-log-tail.test.ts +++ b/src/main/managed-server/install/orcad-remote-log-tail.test.ts @@ -13,6 +13,7 @@ import { } from './orcad-remote-log-tail' import { ORCAD_WINDOWS_HOST_SCRIPT } from '../host-state/orcad-windows-host-script' import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform' +import { unusedOrcadHostTransport } from '../../ssh/unused-host-transport.test-fixture' const execCommand = vi.hoisted(() => vi.fn()) vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ({ @@ -24,8 +25,7 @@ const linux = getRemoteHostPlatform('linux-x64') const windows = getRemoteHostPlatform('win32-x64') const SLOT = '/home/me/.orca-remote/orcad-1.0.0' const WINDOWS_SLOT = 'C:\\Users\\me\\.orca-remote\\orcad-1.0.0' -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: execCommand is mocked; the connection is never used. -const conn = {} as never +const conn = unusedOrcadHostTransport() let dir = '' beforeEach(() => { diff --git a/src/main/managed-server/install/orcad-remote-node-runtime-host-fallback.test.ts b/src/main/managed-server/install/orcad-remote-node-runtime-host-fallback.test.ts index b5e69217ef2..42087af1588 100644 --- a/src/main/managed-server/install/orcad-remote-node-runtime-host-fallback.test.ts +++ b/src/main/managed-server/install/orcad-remote-node-runtime-host-fallback.test.ts @@ -3,7 +3,6 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { OrcadHostTransport } from '../../ssh/orcad-host-transport' import { ensureRemoteOrcadNodeRuntime } from './orcad-remote-node-runtime' import { REMOTE_NODE_RUNTIME_MISSING, @@ -20,6 +19,7 @@ import { HOST_NODE_DOWNLOAD_OK, NodeRuntimeUnreachableError } from './remote-node-runtime-host-download' +import { unusedOrcadHostTransport } from '../../ssh/unused-host-transport.test-fixture' vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ({ ...(await importOriginal()), @@ -29,8 +29,7 @@ vi.mock('./remote-install-transfers', () => ({ uploadRemoteInstallDirectory: vi.fn(async () => {}) })) -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every remote call goes through the mocked execCommand. -const conn = {} as OrcadHostTransport +const conn = unusedOrcadHostTransport() const host = getRemoteHostPlatform('linux-x64') const TARGET = 'linux-x64-glibc' const OFFLINE_PATH = '/Users/me/Library/Application Support/Orca/orcad-artifacts/node/archives/x' diff --git a/src/main/managed-server/install/orcad-remote-node-runtime-noexec.test.ts b/src/main/managed-server/install/orcad-remote-node-runtime-noexec.test.ts index 529e34ce318..9974f2eb9d5 100644 --- a/src/main/managed-server/install/orcad-remote-node-runtime-noexec.test.ts +++ b/src/main/managed-server/install/orcad-remote-node-runtime-noexec.test.ts @@ -12,7 +12,6 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { runProcessSync } from '@orca/process-host' -import type { OrcadHostTransport } from '../../ssh/orcad-host-transport' import { createExecProbedStageCommand, ensureRemoteOrcadNodeRuntime @@ -27,6 +26,7 @@ import { classifyOrcadHostUnavailable } from '../../orcad/orcad-host-unavailable import { execCommand } from '../../ssh/remote-exec/remote-exec-command' import { uploadRemoteInstallDirectory } from './remote-install-transfers' import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform' +import { unusedOrcadHostTransport } from '../../ssh/unused-host-transport.test-fixture' vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ({ ...(await importOriginal()), @@ -36,8 +36,7 @@ vi.mock('./remote-install-transfers', () => ({ uploadRemoteInstallDirectory: vi.fn(async () => {}) })) -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every remote call goes through the mocked execCommand. -const conn = {} as OrcadHostTransport +const conn = unusedOrcadHostTransport() const host = getRemoteHostPlatform('linux-x64') // Captured dash output for executing from a noexec home filesystem. const NOEXEC_PROBE_REPLY = [ diff --git a/src/main/managed-server/install/orcad-remote-node-runtime-promotion-lock.test.ts b/src/main/managed-server/install/orcad-remote-node-runtime-promotion-lock.test.ts index c731245a440..f90c90629d3 100644 --- a/src/main/managed-server/install/orcad-remote-node-runtime-promotion-lock.test.ts +++ b/src/main/managed-server/install/orcad-remote-node-runtime-promotion-lock.test.ts @@ -3,7 +3,6 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { OrcadHostTransport } from '../../ssh/orcad-host-transport' import { ensureRemoteOrcadNodeRuntime, NODE_RUNTIME_PROMOTE_TIMEOUT_MS @@ -15,6 +14,7 @@ import { import { RUNTIME_STORE_LOCK_NAME } from './remote-node-runtime-store-lock' import { execCommand } from '../../ssh/remote-exec/remote-exec-command' import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform' +import { unusedOrcadHostTransport } from '../../ssh/unused-host-transport.test-fixture' vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ({ ...(await importOriginal()), @@ -24,8 +24,7 @@ vi.mock('./remote-install-transfers', () => ({ uploadRemoteInstallDirectory: vi.fn(async () => {}) })) -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every remote call goes through the mocked execCommand. -const conn = {} as OrcadHostTransport +const conn = unusedOrcadHostTransport() const host = getRemoteHostPlatform('linux-x64') const store = '/home/u/.orca-remote/runtimes' const lock = `${store}/${RUNTIME_STORE_LOCK_NAME}` diff --git a/src/main/managed-server/install/orcad-remote-node-runtime-recorded-probe.test.ts b/src/main/managed-server/install/orcad-remote-node-runtime-recorded-probe.test.ts index 6c132f7cd20..f58aa9f809b 100644 --- a/src/main/managed-server/install/orcad-remote-node-runtime-recorded-probe.test.ts +++ b/src/main/managed-server/install/orcad-remote-node-runtime-recorded-probe.test.ts @@ -12,7 +12,6 @@ import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { runProcess } from '@orca/process-host' import { pinnedNodeRuntimeAsset } from '../../../shared/node-runtime-pin' -import type { OrcadHostTransport } from '../../ssh/orcad-host-transport' import { ensureRemoteOrcadNodeRuntime, posixNodeRuntimeExecutable, @@ -31,14 +30,14 @@ import { import { execCommand } from '../../ssh/remote-exec/remote-exec-command' import type * as ExecCommandModule from '../../ssh/remote-exec/remote-exec-command' import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform' +import { unusedOrcadHostTransport } from '../../ssh/unused-host-transport.test-fixture' vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ({ ...(await importOriginal()), execCommand: vi.fn() })) -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every remote call goes through the mocked execCommand, which runs it in a local shell. -const conn = {} as OrcadHostTransport +const conn = unusedOrcadHostTransport() const host = getRemoteHostPlatform('linux-x64') const target = 'linux-x64-glibc' const pin = pinnedNodeRuntimeAsset(target).executableSha256 diff --git a/src/main/managed-server/install/orcad-remote-node-runtime-slow-hash.test.ts b/src/main/managed-server/install/orcad-remote-node-runtime-slow-hash.test.ts index 9f25ffe3a50..1fd85ad15d4 100644 --- a/src/main/managed-server/install/orcad-remote-node-runtime-slow-hash.test.ts +++ b/src/main/managed-server/install/orcad-remote-node-runtime-slow-hash.test.ts @@ -5,7 +5,6 @@ import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { pinnedNodeRuntimeAsset } from '../../../shared/node-runtime-pin' import { runProcess } from '@orca/process-host' -import type { OrcadHostTransport } from '../../ssh/orcad-host-transport' import { ensureRemoteOrcadNodeRuntime, posixNodeRuntimeExecutable, @@ -19,6 +18,7 @@ import { RUNTIME_STORE_LOCK_NAME, withRuntimeStoreLock } from './remote-node-run import { execCommand } from '../../ssh/remote-exec/remote-exec-command' import { uploadRemoteInstallDirectory } from './remote-install-transfers' import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform' +import { unusedOrcadHostTransport } from '../../ssh/unused-host-transport.test-fixture' vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ({ ...(await importOriginal()), @@ -26,8 +26,7 @@ vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ( })) vi.mock('./remote-install-transfers', () => ({ uploadRemoteInstallDirectory: vi.fn() })) -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every remote call goes through the mocked execCommand, which runs it in a local shell. -const conn = {} as OrcadHostTransport +const conn = unusedOrcadHostTransport() const host = getRemoteHostPlatform('linux-x64') const target = 'linux-x64-glibc' let root: string diff --git a/src/main/managed-server/install/remote-install-gc-claim-termination.test.ts b/src/main/managed-server/install/remote-install-gc-claim-termination.test.ts index 0770f89b7cf..d644a27196e 100644 --- a/src/main/managed-server/install/remote-install-gc-claim-termination.test.ts +++ b/src/main/managed-server/install/remote-install-gc-claim-termination.test.ts @@ -6,7 +6,6 @@ vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ( execCommand: vi.fn() })) -import type { SshConnection } from '../../ssh/connection/ssh-connection' import { execCommand } from '../../ssh/remote-exec/remote-exec-command' import { isRelayGcClaimOwned, @@ -16,9 +15,9 @@ import { waitForRelayGcClaimRelease } from './remote-install-gc-claim' import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform' +import { unusedSshConnection } from '../../ssh/unused-host-transport.test-fixture' -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mocked execCommand never reads the connection. -const conn = {} as SshConnection +const conn = unusedSshConnection() const mockExec = vi.mocked(execCommand) const token = 'claim-owner' diff --git a/src/main/managed-server/install/remote-install-gc-termination.test.ts b/src/main/managed-server/install/remote-install-gc-termination.test.ts index 4a594d47a68..3efa393d9fa 100644 --- a/src/main/managed-server/install/remote-install-gc-termination.test.ts +++ b/src/main/managed-server/install/remote-install-gc-termination.test.ts @@ -6,14 +6,13 @@ vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ( execCommand: vi.fn() })) -import type { SshConnection } from '../../ssh/connection/ssh-connection' import { gcOldRemoteInstallVersions } from './remote-install-gc' import { ORCAD_INSTALL_MODEL } from './remote-install-model' import { execCommand } from '../../ssh/remote-exec/remote-exec-command' import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform' +import { unusedSshConnection } from '../../ssh/unused-host-transport.test-fixture' -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: all connection access is replaced by execCommand's mock. -const conn = {} as SshConnection +const conn = unusedSshConnection() const host = getRemoteHostPlatform('linux-x64') const home = '/home/u' const currentDir = `${home}/.orca-remote/orcad-0.1.0+bbb` diff --git a/src/main/managed-server/install/remote-install-lock-termination.test.ts b/src/main/managed-server/install/remote-install-lock-termination.test.ts index 3ba84bbe515..9a8780920e5 100644 --- a/src/main/managed-server/install/remote-install-lock-termination.test.ts +++ b/src/main/managed-server/install/remote-install-lock-termination.test.ts @@ -6,13 +6,12 @@ vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ( execCommand: vi.fn() })) -import type { SshConnection } from '../../ssh/connection/ssh-connection' import { execCommand } from '../../ssh/remote-exec/remote-exec-command' import { acquireInstallLock, isRelayInstallLockStale } from './remote-install-lock' import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform' +import { unusedSshConnection } from '../../ssh/unused-host-transport.test-fixture' -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mocked execCommand never reads the connection. -const conn = {} as SshConnection +const conn = unusedSshConnection() const mockExec = vi.mocked(execCommand) const nextAcquisition = ['OPEN', '', 'OK', 'OPEN'] diff --git a/src/main/managed-server/install/remote-node-runtime-store-gc.test.ts b/src/main/managed-server/install/remote-node-runtime-store-gc.test.ts index e5c8200c7ef..624b9c476c3 100644 --- a/src/main/managed-server/install/remote-node-runtime-store-gc.test.ts +++ b/src/main/managed-server/install/remote-node-runtime-store-gc.test.ts @@ -20,7 +20,6 @@ vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ( execCommand: vi.fn() })) -import type { SshConnection } from '../../ssh/connection/ssh-connection' import { gcRemoteNodeRuntimeStore, parseSweptRuntimeStages, @@ -36,9 +35,9 @@ import { } from './remote-node-runtime-store-inventory' import { execCommand } from '../../ssh/remote-exec/remote-exec-command' import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform' +import { unusedSshConnection } from '../../ssh/unused-host-transport.test-fixture' -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: all connection access is replaced by execCommand's mock. -const conn = {} as SshConnection +const conn = unusedSshConnection() const host = getRemoteHostPlatform('linux-x64') const mockExec = vi.mocked(execCommand) const sha = (c: string): string => c.repeat(64) diff --git a/src/main/managed-server/install/remote-node-runtime-store-lock-host-release.test.ts b/src/main/managed-server/install/remote-node-runtime-store-lock-host-release.test.ts index 16787925163..33b3533fe66 100644 --- a/src/main/managed-server/install/remote-node-runtime-store-lock-host-release.test.ts +++ b/src/main/managed-server/install/remote-node-runtime-store-lock-host-release.test.ts @@ -4,7 +4,6 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { runProcess, spawnProcess } from '@orca/process-host' -import type { SshConnection } from '../../ssh/connection/ssh-connection' import { RUNTIME_STORE_LOCK_NAME, RUNTIME_STORE_LOCK_OWNER_FILENAME, @@ -13,14 +12,14 @@ import { import { execCommand } from '../../ssh/remote-exec/remote-exec-command' import { tryCreateInstallLockCommand } from './remote-install-lock-commands' import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform' +import { unusedSshConnection } from '../../ssh/unused-host-transport.test-fixture' vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ({ ...(await importOriginal()), execCommand: vi.fn() })) -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every remote call goes through the mocked execCommand, which runs it in a local shell. -const conn = {} as SshConnection +const conn = unusedSshConnection() const host = getRemoteHostPlatform('linux-x64') let store: string let lock: string diff --git a/src/main/managed-server/install/remote-node-runtime-store-windows.test.ts b/src/main/managed-server/install/remote-node-runtime-store-windows.test.ts index 634acfeccf8..82e23e99c69 100644 --- a/src/main/managed-server/install/remote-node-runtime-store-windows.test.ts +++ b/src/main/managed-server/install/remote-node-runtime-store-windows.test.ts @@ -10,7 +10,6 @@ vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ( execCommand: vi.fn() })) -import type { SshConnection } from '../../ssh/connection/ssh-connection' import { gcRemoteNodeRuntimeStore, parseSweptRuntimeStages } from './remote-node-runtime-store-gc' import { RUNTIME_STORE_LOCK_NAME } from './remote-node-runtime-store-lock' import { parseRuntimeStoreInventory } from './remote-node-runtime-store-inventory' @@ -22,9 +21,9 @@ import { execCommand } from '../../ssh/remote-exec/remote-exec-command' import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform' import { decodeRemotePowerShellScript } from '../../ssh/ssh-remote-powershell' import { removeTreeSync } from '../../../shared/windows-transient-lock-removal' +import { unusedSshConnection } from '../../ssh/unused-host-transport.test-fixture' -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: all connection access is replaced by execCommand's mock. -const conn = {} as SshConnection +const conn = unusedSshConnection() const host = getRemoteHostPlatform('win32-x64') const mockExec = vi.mocked(execCommand) const sha = (c: string): string => c.repeat(64) diff --git a/src/main/managed-server/install/remote-versioned-install-termination.test.ts b/src/main/managed-server/install/remote-versioned-install-termination.test.ts index fff6cb21aa8..fe230df931b 100644 --- a/src/main/managed-server/install/remote-versioned-install-termination.test.ts +++ b/src/main/managed-server/install/remote-versioned-install-termination.test.ts @@ -6,7 +6,6 @@ vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ( execCommand: vi.fn() })) -import type { SshConnection } from '../../ssh/connection/ssh-connection' import { REMOTE_INSTALL_MODELS } from './remote-install-model' import { execCommand } from '../../ssh/remote-exec/remote-exec-command' import { @@ -15,9 +14,9 @@ import { isRemoteInstallComplete } from './remote-versioned-install' import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform' +import { unusedSshConnection } from '../../ssh/unused-host-transport.test-fixture' -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mocked execCommand never reads the connection. -const conn = {} as SshConnection +const conn = unusedSshConnection() const mockExec = vi.mocked(execCommand) beforeEach(() => { diff --git a/src/main/managed-server/lifecycle/orcad-managed-bound-port.test.ts b/src/main/managed-server/lifecycle/orcad-managed-bound-port.test.ts index c998b42100e..62bce426893 100644 --- a/src/main/managed-server/lifecycle/orcad-managed-bound-port.test.ts +++ b/src/main/managed-server/lifecycle/orcad-managed-bound-port.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { OrcadHostTransport } from '../../ssh/orcad-host-transport' import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform' const execOrcadRemote = vi.hoisted(() => vi.fn()) @@ -10,6 +9,7 @@ vi.mock('../install/orcad-remote-context', () => ({ resolveOrcadRemoteContext }) import { orcadBoundPort, resolveManagedOrcadTunnelPort } from './orcad-managed-bound-port' import type { KnownRuntimeEnvironment } from '../../../shared/runtime-environments' import type { SshTarget } from '../../../shared/ssh-types' +import { unusedOrcadHostTransport } from '../../ssh/unused-host-transport.test-fixture' function readinessLine(boundEndpoint: string | null): string { return `${JSON.stringify({ type: 'orca_server_ready', runtimeId: 'runtime-1', boundEndpoint })}\n` @@ -38,8 +38,7 @@ describe('resolveManagedOrcadTunnelPort', () => { environment: { [kind]: link } as unknown as KnownRuntimeEnvironment, // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: passed through to the mocked context. target: { id: 'ssh-1' } as SshTarget, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: passed through to the mocked context. - connection: {} as OrcadHostTransport + connection: unusedOrcadHostTransport() } } diff --git a/src/main/managed-server/lifecycle/orcad-managed-pairing-repair-restart.test.ts b/src/main/managed-server/lifecycle/orcad-managed-pairing-repair-restart.test.ts index bf56f665597..e323866252e 100644 --- a/src/main/managed-server/lifecycle/orcad-managed-pairing-repair-restart.test.ts +++ b/src/main/managed-server/lifecycle/orcad-managed-pairing-repair-restart.test.ts @@ -15,12 +15,12 @@ import { OrcaRuntimeRpcServer } from '../../runtime/runtime-rpc' import { verifyRuntimePairingIdentity } from '../../runtime/runtime-environment-identity-verification' import { buildServePairingReadiness } from '../../server/serve-pairing-readiness' import type { ServeReadiness } from '../../server/serve-readiness' -import type { OrcadHostTransport } from '../../ssh/orcad-host-transport' import { ManagedOrcadPairingRepairs } from './orcad-managed-pairing-repair' import { OrcadManagedIdentityError, verifyManagedOrcadTunnelIdentity } from './orcad-managed-tunnel-identity' +import { unusedOrcadHostTransport } from '../../ssh/unused-host-transport.test-fixture' vi.mock('../../git/worktree', () => ({ listWorktrees: vi.fn().mockResolvedValue([]), @@ -88,8 +88,7 @@ function repairsAgainst(saved: KnownRuntimeEnvironment, readiness: ServeReadines connect: async () => ({ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only passed to readReadiness, which ignores it here. target: {} as SshTarget, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only passed to readReadiness, which ignores it here. - connection: {} as OrcadHostTransport + connection: unusedOrcadHostTransport() }), readReadiness: async () => readiness, refresh, diff --git a/src/main/managed-server/lifecycle/orcad-managed-pairing-repair.test.ts b/src/main/managed-server/lifecycle/orcad-managed-pairing-repair.test.ts index 20b7661dc6a..70c762853be 100644 --- a/src/main/managed-server/lifecycle/orcad-managed-pairing-repair.test.ts +++ b/src/main/managed-server/lifecycle/orcad-managed-pairing-repair.test.ts @@ -10,7 +10,6 @@ import { } from '../../../shared/runtime-environments' import type { SshTarget } from '../../../shared/ssh-types' import type { ServeReadiness } from '../../server/serve-readiness' -import type { OrcadHostTransport } from '../../ssh/orcad-host-transport' import { ManagedOrcadPairingRepairs, MAX_PAIRING_REPAIRS_PER_WINDOW, @@ -24,6 +23,7 @@ import { OrcadManagedPairingRejectedError } from './orcad-managed-tunnel-identity' import { buildWithPairingRepair } from './orcad-managed-tunnel-pairing-recovery' +import { unusedOrcadHostTransport } from '../../ssh/unused-host-transport.test-fixture' const KEY = 'pinned-host-key' @@ -85,8 +85,7 @@ function setup(published: () => ServeReadiness | null) { connect: vi.fn(async () => ({ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only handed to readReadiness, a double here. target: {} as SshTarget, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only handed to readReadiness, a double here. - connection: {} as OrcadHostTransport + connection: unusedOrcadHostTransport() })), readReadiness: vi.fn(async () => published()), refresh: vi.fn((_id: string, _code: string) => { diff --git a/src/main/managed-server/lifecycle/orcad-managed-tunnel-target.test.ts b/src/main/managed-server/lifecycle/orcad-managed-tunnel-target.test.ts index c27da6c42f3..0cad3474d4c 100644 --- a/src/main/managed-server/lifecycle/orcad-managed-tunnel-target.test.ts +++ b/src/main/managed-server/lifecycle/orcad-managed-tunnel-target.test.ts @@ -1,7 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { SshTarget } from '../../../shared/ssh-types' import type { KnownRuntimeEnvironment } from '../../../shared/runtime-environments' -import type { OrcadHostTransport } from '../../ssh/orcad-host-transport' import type { SshConnection } from '../../ssh/connection/ssh-connection' import type { SshPortForwardManager } from '../../ssh/port-forwarding/ssh-port-forward' import { @@ -14,6 +13,10 @@ import { forwardToVerifiedOrcad, MANAGED_ORCAD_TUNNEL_TARGETING } from './orcad-managed-tunnel-target' +import { + unusedOrcadHostTransport, + unusedSshConnection +} from '../../ssh/unused-host-transport.test-fixture' const mocks = vi.hoisted(() => ({ wake: vi.fn(), resolveContext: vi.fn() })) vi.mock('./orcad-managed-wake', () => ({ wakeStoppedManagedOrcad: mocks.wake })) @@ -36,8 +39,7 @@ function setup(verdicts: OrcadTunnelIdentity[], rereadPort: number) { const rereadRemotePort = vi.fn().mockResolvedValue(rereadPort) const args = { targetId: 'ssh-1', - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the forward manager double never reads it. - connection: {} as SshConnection, + connection: unusedSshConnection(), // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a test double for the two members forwarding calls. forwards: { addForward, removeForwardAndWait } as unknown as SshPortForwardManager, localPort: 46_768, @@ -141,8 +143,7 @@ describe('environmentForwardChecks', () => { } as KnownRuntimeEnvironment, // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: passed through to the mocked context resolver only. target: { id: 'ssh-1' } as SshTarget, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: passed through to the mocked context resolver only. - connection: {} as OrcadHostTransport + connection: unusedOrcadHostTransport() } beforeEach(() => { diff --git a/src/main/managed-server/lifecycle/orcad-managed-version-gc.test.ts b/src/main/managed-server/lifecycle/orcad-managed-version-gc.test.ts index 11730bb7ad3..e2c97214ffe 100644 --- a/src/main/managed-server/lifecycle/orcad-managed-version-gc.test.ts +++ b/src/main/managed-server/lifecycle/orcad-managed-version-gc.test.ts @@ -13,6 +13,7 @@ vi.mock('../activation/orcad-activation-record-store', () => ({ })) import { provenLiveDaemonVersion, pruneManagedOrcadVersions } from './orcad-managed-version-gc' +import { unusedOrcadHostTransport } from '../../ssh/unused-host-transport.test-fixture' function readiness(terminalDaemon: unknown): ServeReadiness { // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only health.terminalDaemon is read. @@ -20,8 +21,7 @@ function readiness(terminalDaemon: unknown): ServeReadiness { } const slot = { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: GC is mocked and never touches the connection. - conn: {} as never, + conn: unusedOrcadHostTransport(), // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: GC is mocked; the slot's host is only passed through. host: { os: 'linux', pathFlavor: 'posix', commandDialect: 'posix' } as never, remoteHome: '/home/u', diff --git a/src/main/native-chat/agent-model-catalog/agent-model-catalog-persistence.ts b/src/main/native-chat/agent-model-catalog/agent-model-catalog-persistence.ts index a957c275627..f1c1a4cda1e 100644 --- a/src/main/native-chat/agent-model-catalog/agent-model-catalog-persistence.ts +++ b/src/main/native-chat/agent-model-catalog/agent-model-catalog-persistence.ts @@ -10,6 +10,7 @@ import { type AgentModelCatalogListing } from './agent-model-catalog-entry' import { isStructuredAgentId } from '../../../shared/agent-session-provider-handle-encoding' +import { isRecord } from '../../../shared/agent-status-child-work-value-guards' // Older catalogs lack serviceTiers and cannot safely construct Fast requests. const SCHEMA_VERSION = 3 @@ -24,8 +25,7 @@ export type AgentModelCatalogPersistence = { } function asRecord(value: unknown): Record | null { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a non-null `typeof === 'object'` value is indexable by string key. - return typeof value === 'object' && value !== null ? (value as Record) : null + return isRecord(value) ? value : null } function text(value: unknown): string | null { diff --git a/src/main/opencode/hook-plugin-module-contract.test.ts b/src/main/opencode/hook-plugin-module-contract.test.ts index 977e9ef18c9..a0dc6a8ab12 100644 --- a/src/main/opencode/hook-plugin-module-contract.test.ts +++ b/src/main/opencode/hook-plugin-module-contract.test.ts @@ -75,6 +75,23 @@ describe('OpenCode status plugin module contract', () => { rmSync(tempDir, { recursive: true, force: true }) }) + type HookPost = { url: string; body: Record } + + function captureHookPosts(): HookPost[] { + const posts: HookPost[] = [] + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { + posts.push({ url: String(input), body: JSON.parse(String(init?.body ?? '{}')) }) + return new Response(null) + }) + return posts + } + + function hookEventName(payload: unknown): unknown { + return typeof payload === 'object' && payload !== null && 'hook_event_name' in payload + ? payload.hook_event_name + : undefined + } + async function loadPluginModule( source = _internals.getOpenCodePluginSource() ): Promise { @@ -122,14 +139,7 @@ describe('OpenCode status plugin module contract', () => { it('reports a session lifecycle event through the hook endpoint when driven via the default export', async () => { process.env.ORCA_PANE_KEY = 'tab-1:leaf-1' - const posts: { url: string; body: unknown }[] = [] - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mocked fetch is assigned to the standard Fetch API shape. - globalThis.fetch = vi.fn(async (input: unknown, init?: { body?: unknown }) => { - posts.push({ url: String(input), body: JSON.parse(String(init?.body ?? '{}')) }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - return { ok: true } as Response - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - }) as unknown as typeof globalThis.fetch + const posts = captureHookPosts() const module = await loadPluginModule() const hooks = await module.default?.server?.({ @@ -161,14 +171,7 @@ describe('OpenCode status plugin module contract', () => { it('keeps OpenCode 2 busy across steps until the session becomes idle', async () => { process.env.ORCA_PANE_KEY = 'tab-1:leaf-1' - const posts: { body: Record }[] = [] - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mocked fetch is assigned to the standard Fetch API shape. - globalThis.fetch = vi.fn(async (_input: unknown, init?: { body?: unknown }) => { - posts.push({ body: JSON.parse(String(init?.body ?? '{}')) }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mocked fetch returns the Response shape the plugin checks. - return { ok: true } as Response - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mocked fetch is assigned to the standard Fetch API shape. - }) as unknown as typeof globalThis.fetch + const posts = captureHookPosts() const module = await loadPluginModule(_internals.getOpenCode2PluginSource()) const hooks = await module.default?.server?.({ @@ -195,27 +198,14 @@ describe('OpenCode status plugin module contract', () => { ) await hooks?.event({ event: { type: 'session.idle', properties: { sessionID: 'ses_root' } } }) await new Promise((resolve) => setTimeout(resolve, 50)) - const hookEvents = posts.map((post) => { - const payload = post.body.payload - return typeof payload === 'object' && payload !== null - ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The generated plugin payload is an object with a hook_event_name field. - (payload as { hook_event_name?: unknown }).hook_event_name - : undefined - }) + const hookEvents = posts.map((post) => hookEventName(post.body.payload)) expect(hookEvents).toContain('SessionBusy') expect(hookEvents).toContain('SessionIdle') }) it('maps OpenCode 2 permission.v2 events to the existing permission card contract', async () => { process.env.ORCA_PANE_KEY = 'tab-1:leaf-1' - const posts: { body: Record }[] = [] - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mocked fetch is assigned to the standard Fetch API shape. - globalThis.fetch = vi.fn(async (_input: unknown, init?: { body?: unknown }) => { - posts.push({ body: JSON.parse(String(init?.body ?? '{}')) }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - return { ok: true } as Response - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - }) as unknown as typeof globalThis.fetch + const posts = captureHookPosts() const module = await loadPluginModule(_internals.getOpenCode2PluginSource()) const hooks = await module.default?.server?.({ @@ -234,23 +224,10 @@ describe('OpenCode status plugin module contract', () => { }) await new Promise((resolve) => setTimeout(resolve, 50)) - const hookEvents = posts.map((post) => - typeof post.body.payload === 'object' && post.body.payload !== null - ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - (post.body.payload as { hook_event_name?: unknown }).hook_event_name - : undefined - ) + const hookEvents = posts.map((post) => hookEventName(post.body.payload)) expect(hookEvents).toContain('PermissionRequest') expect( - posts.find((post) => { - const payload = post.body.payload - return ( - typeof payload === 'object' && - payload !== null && - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - (payload as { hook_event_name?: unknown }).hook_event_name === 'PermissionRequest' - ) - })?.body + posts.find((post) => hookEventName(post.body.payload) === 'PermissionRequest')?.body ).toMatchObject({ payload: { permission: 'bash', @@ -261,14 +238,7 @@ describe('OpenCode status plugin module contract', () => { it('forwards admitted prompts and completed streamed text once', async () => { process.env.ORCA_PANE_KEY = 'tab-1:leaf-1' - const posts: { body: Record }[] = [] - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mocked fetch is assigned to the standard Fetch API shape. - globalThis.fetch = vi.fn(async (_input: unknown, init?: { body?: unknown }) => { - posts.push({ body: JSON.parse(String(init?.body ?? '{}')) }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - return { ok: true } as Response - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - }) as unknown as typeof globalThis.fetch + const posts = captureHookPosts() const module = await loadPluginModule(_internals.getOpenCode2PluginSource()) const hooks = await module.default?.server?.({ @@ -311,14 +281,7 @@ describe('OpenCode status plugin module contract', () => { it('maps question.v2 blockers and replies through the waiting lifecycle', async () => { process.env.ORCA_PANE_KEY = 'tab-1:leaf-1' - const posts: { body: Record }[] = [] - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mocked fetch is assigned to the standard Fetch API shape. - globalThis.fetch = vi.fn(async (_input: unknown, init?: { body?: unknown }) => { - posts.push({ body: JSON.parse(String(init?.body ?? '{}')) }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - return { ok: true } as Response - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - }) as unknown as typeof globalThis.fetch + const posts = captureHookPosts() const module = await loadPluginModule(_internals.getOpenCode2PluginSource()) const hooks = await module.default?.server?.({ diff --git a/src/main/orcad/orcad-cross-runtime-daemon-adoption.integration.test.ts b/src/main/orcad/orcad-cross-runtime-daemon-adoption.integration.test.ts index 81d332e32e3..26f30b349c2 100644 --- a/src/main/orcad/orcad-cross-runtime-daemon-adoption.integration.test.ts +++ b/src/main/orcad/orcad-cross-runtime-daemon-adoption.integration.test.ts @@ -40,7 +40,6 @@ import { getOrcaProfileIndexPath, getOrcaProfileStateDatabaseFile } from '../orca-profiles/profile-storage-paths' -import type { SshConnection } from '../ssh/connection/ssh-connection' import { emptyOrcadActivationRecord, orcadGcPinnedDirNames @@ -65,6 +64,7 @@ import { hostServerTarget, locatePinnedNodeForTests } from './orcad-node-slot-fixture' +import { unusedSshConnection } from '../ssh/unused-host-transport.test-fixture' // GC runs its real commands, against this machine instead of an SSH host. const { execMock } = vi.hoisted(() => ({ execMock: vi.fn() })) @@ -447,8 +447,7 @@ worker.on('error', (error) => { console.error(error); process.exitCode = 1 }) const record = { ...emptyOrcadActivationRecord(), active: incoming.version } expect(orcadGcPinnedDirNames(record)).not.toContain(`orcad-${outgoing.version}`) await gcOldOrcadVersions({ - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: execCommand is replaced by a local shell for every connection access. - conn: {} as SshConnection, + conn: unusedSshConnection(), host, remoteHome: caseRoot, currentDirAbsPath: incoming.dir, diff --git a/src/main/orcad/orcad-node-slot-fixture.ts b/src/main/orcad/orcad-node-slot-fixture.ts index ef211377b7d..7071d48013d 100644 --- a/src/main/orcad/orcad-node-slot-fixture.ts +++ b/src/main/orcad/orcad-node-slot-fixture.ts @@ -6,7 +6,11 @@ import type * as NodePty from 'node-pty' import { join, resolve } from 'node:path' import { it } from 'vitest' import { runProcessSync } from '@orca/process-host' -import { NODE_RUNTIME_ASSETS, type ServerTarget } from '../../shared/node-runtime-pin' +import { + NODE_RUNTIME_ASSETS, + SERVER_TARGETS, + type ServerTarget +} from '../../shared/node-runtime-pin' import { ORCAD_NODE_PTY_DIR, ORCAD_NODE_RUNTIME_MARKER_FILENAME, @@ -17,12 +21,11 @@ import { detectNativeHostAbi, nativeSlotName } from './native-host-abi' export function hostServerTarget(): ServerTarget { const slot = nativeSlotName(detectNativeHostAbi()) - const target = Object.keys(NODE_RUNTIME_ASSETS).find((candidate) => candidate === slot) + const target = SERVER_TARGETS.find((candidate) => candidate === slot) if (!target) { throw new Error(`No pinned Node runtime for host slot ${slot}`) } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: found among NODE_RUNTIME_ASSETS keys, which are ServerTargets. - return target as ServerTarget + return target } /** The pinned Node the runner (run-node-server-tests.mjs) or `build:orcad` provided, if any. */ diff --git a/src/main/persistence/loading-store/worktree-meta-alias-projection.test.ts b/src/main/persistence/loading-store/worktree-meta-alias-projection.test.ts index ee119f7d0e8..0c64b8d2bb9 100644 --- a/src/main/persistence/loading-store/worktree-meta-alias-projection.test.ts +++ b/src/main/persistence/loading-store/worktree-meta-alias-projection.test.ts @@ -1,6 +1,7 @@ import { closeTestStores, createSqliteTestStore, + readPersistedState, readPersistedStateJson } from '../persistence-test-harness' /** @@ -283,8 +284,7 @@ describe('worktree meta alias projection', () => { // What a build without this change does with that file: parse it, run the metadata normalizer // it already ships (untouched here), write the result back. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const downgraded = JSON.parse(readPersistedStateJson(dataFile)) as PersistedState + const downgraded = readPersistedState(dataFile) normalizeWorktreeLinkedItemMetadata(downgraded) expect(Object.keys(downgraded.worktreeMeta).sort()).toEqual(Object.keys(before.meta).sort()) // It drops the aliases whose identity row is not there; it never touches a locator row. @@ -306,8 +306,7 @@ describe('worktree meta alias projection', () => { before.meta[worktreeId(0)] ) rolledBack.flush() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const reminted = JSON.parse(readPersistedStateJson(rollbackDataFile)) as PersistedState + const reminted = readPersistedState(rollbackDataFile) expect( reminted.worktreeIdentityAliases?.[composeWorktreeHostIdentity(LOCAL, worktreeId(0))] ).toEqual([ @@ -375,8 +374,7 @@ describe('worktree meta alias projection', () => { ) expect(store.getAllWorktreeMeta()[worktreeId(0)]?.displayName).toBe('workspace-0') store.flush() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const onDisk = JSON.parse(readPersistedStateJson(dataFile)) as PersistedState + const onDisk = readPersistedState(dataFile) expect(Object.keys(onDisk.worktreeMeta).length).toBe( Object.keys(fixture.state.worktreeMeta).length ) @@ -419,8 +417,7 @@ describe('worktree meta alias projection', () => { expect(Object.keys(store.getAllWorktreeMeta())).toEqual([worktreeId(0)]) store.flush() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const onDisk = JSON.parse(readPersistedStateJson(dataFile)) as PersistedState + const onDisk = readPersistedState(dataFile) expect(Object.hasOwn(onDisk, 'worktreeMetaByIdentity')).toBe(false) // The locator map and its lineage companions are all still there, untouched by the projection. expect(Object.keys(onDisk.worktreeMeta)).toEqual([worktreeId(0)]) diff --git a/src/main/persistence/persistence-feature-interaction-broadcast.benchmark.test.ts b/src/main/persistence/persistence-feature-interaction-broadcast.benchmark.test.ts index 6c25d001186..8969de42e96 100644 --- a/src/main/persistence/persistence-feature-interaction-broadcast.benchmark.test.ts +++ b/src/main/persistence/persistence-feature-interaction-broadcast.benchmark.test.ts @@ -1,7 +1,7 @@ import { closeTestStores, createSqliteTestStore, - readPersistedStateJson + readPersistedState } from './persistence-test-harness' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' @@ -9,7 +9,6 @@ import { join } from 'node:path' import { performance } from 'node:perf_hooks' import { afterEach, describe, expect, it, vi } from 'vitest' import { normalizeFeatureInteractions } from '../../shared/feature-interactions' -import type { PersistedState } from '../../shared/persisted-state-types' vi.mock('electron', () => ({ app: { getPath: () => tmpdir() }, @@ -86,8 +85,7 @@ describe('feature interaction UI broadcast benchmark', () => { 'agent-orchestration' ]?.interactionCount ).toBe(INTERACTIONS) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const persisted = JSON.parse(readPersistedStateJson(dataFile)) as PersistedState + const persisted = readPersistedState(dataFile) expect(persisted.featureInteractionTelemetryBuckets?.['agent-orchestration']).toBe( 'count_200_499' ) diff --git a/src/main/persistence/persistence-loading-store-extraction.test.ts b/src/main/persistence/persistence-loading-store-extraction.test.ts index b415e3b9af1..f42d2172ed8 100644 --- a/src/main/persistence/persistence-loading-store-extraction.test.ts +++ b/src/main/persistence/persistence-loading-store-extraction.test.ts @@ -1,6 +1,7 @@ import { closeTestStores, createSqliteTestStore, + readPersistedState, readPersistedStateJson, writePersistedStateJson, createStore, @@ -23,7 +24,6 @@ import { import { tmpdir } from 'node:os' import { join } from 'node:path' import { getDefaultPersistedState, getDefaultWorkspaceSession } from '../../shared/constants' -import type { PersistedState } from '../../shared/persisted-state-types' import type { Store as PersistenceStore } from './loading-store/store' const { trackMock, getCohortAtEmitMock } = vi.hoisted(() => ({ @@ -223,8 +223,7 @@ describe('loading Store extraction seams', () => { expect(store.getWorkspaceSession(' ').activeRepoId).toBe('from-blank-patch') expect(store.getWorkspaceSessionHostIds()).toEqual(['local']) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const persisted = JSON.parse(readPersistedStateJson(dataFile())) as PersistedState + const persisted = readPersistedState(dataFile()) expect(persisted.workspaceSession?.activeRepoId).toBe('from-blank-patch') expect(persisted.workspaceSessionsByHostId).toEqual({}) }) @@ -246,10 +245,8 @@ describe('loading Store extraction seams', () => { first.flushOrThrow() second.flushOrThrow() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const firstState = JSON.parse(readPersistedStateJson(dataFile())) as PersistedState - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const secondState = JSON.parse(readPersistedStateJson(secondDataFile)) as PersistedState + const firstState = readPersistedState(dataFile()) + const secondState = readPersistedState(secondDataFile) expect( firstState.migrationUnsupportedPtyEntries?.some( (entry) => entry.ptyId === 'listener-owner-pty' @@ -270,8 +267,7 @@ describe('loading Store extraction seams', () => { store.updateUI({ sidebarWidth: 732 }) expect(store.flushAsync()).toBe(finalFlush) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const persisted = JSON.parse(readPersistedStateJson(dataFile())) as PersistedState + const persisted = readPersistedState(dataFile()) expect(persisted.ui.sidebarWidth).toBe(731) expect(store.getUI().sidebarWidth).toBe(732) }) @@ -284,8 +280,7 @@ describe('loading Store extraction seams', () => { store.updateUI({ sidebarWidth: 742 }) await finalFlush - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const persisted = JSON.parse(readPersistedStateJson(dataFile())) as PersistedState + const persisted = readPersistedState(dataFile()) expect(persisted.ui.sidebarWidth).toBe(742) }) diff --git a/src/main/persistence/persistence-proxy-secret-recovery.test.ts b/src/main/persistence/persistence-proxy-secret-recovery.test.ts index ededf976ba1..5d4f2dd8fb1 100644 --- a/src/main/persistence/persistence-proxy-secret-recovery.test.ts +++ b/src/main/persistence/persistence-proxy-secret-recovery.test.ts @@ -1,6 +1,7 @@ import { closeTestStores, createSqliteTestStore, + readPersistedState, readPersistedStateJson } from './persistence-test-harness' // Keep undecryptable proxy secrets sealed and unapplied; preserve plaintext upgrades and unrelated saves (STA-3442). @@ -101,12 +102,9 @@ describe('httpProxyUrl secret recovery (STA-3442)', () => { it('persists the configured proxy across a restart and applies it as fixed_servers', async () => { await seedConfiguredProxy() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const persisted = JSON.parse(readPersistedStateJson(dataFile())) as { - settings: { httpProxyUrl: string; httpProxyBypassRules: string } - } + const persisted = readPersistedState(dataFile()) expect(persisted.settings.httpProxyUrl).not.toBe(PROXY_URL) - expect(Buffer.from(persisted.settings.httpProxyUrl, 'base64').toString('utf-8')).toMatch( + expect(Buffer.from(persisted.settings.httpProxyUrl ?? '', 'base64').toString('utf-8')).toMatch( /^enc:/ ) expect(persisted.settings.httpProxyBypassRules).toBe(BYPASS_RULES) @@ -149,10 +147,7 @@ describe('httpProxyUrl secret recovery (STA-3442)', () => { reloaded.updateSettings({ httpProxyBypassRules: 'localhost' }) vi.advanceTimersByTime(2000) await reloaded.waitForPendingWrite() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const persisted = JSON.parse(readPersistedStateJson(dataFile())) as { - settings: { httpProxyUrl: string } - } + const persisted = readPersistedState(dataFile()) expect(persisted.settings.httpProxyUrl).toBe(originalCiphertext) }) @@ -178,10 +173,7 @@ describe('httpProxyUrl secret recovery (STA-3442)', () => { await store.waitForPendingWrite() expect(existsSync(join(testState.dir, 'profile-state.db'))).toBe(true) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const persisted = JSON.parse(readPersistedStateJson(dataFile())) as { - settings: { httpProxyUrl: string; httpProxyBypassRules: string } - } + const persisted = readPersistedState(dataFile()) expect(persisted.settings.httpProxyUrl).toBe('') expect(persisted.settings.httpProxyBypassRules).toBe(BYPASS_RULES) diff --git a/src/main/persistence/persistence-single-serialize.test.ts b/src/main/persistence/persistence-single-serialize.test.ts index e979538337e..9d49fa515ba 100644 --- a/src/main/persistence/persistence-single-serialize.test.ts +++ b/src/main/persistence/persistence-single-serialize.test.ts @@ -1,6 +1,7 @@ import { closeTestStores, createSqliteTestStore, + readPersistedState, readPersistedStateJson } from './persistence-test-harness' // Random-IV ciphertext changes on every encryption; identical state must still skip redundant writes. @@ -168,10 +169,7 @@ describe('persistence single-serialize save guard', () => { vi.advanceTimersByTime(1000) await store.waitForPendingWrite() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const persisted = JSON.parse(readPersistedStateJson(dataFile())) as { - settings: { opencodeSessionCookie: string; httpProxyUrl: string } - } + const persisted = readPersistedState(dataFile()) expect(persisted.settings.opencodeSessionCookie).toBe('') expect(persisted.settings.httpProxyUrl).toBe('') diff --git a/src/main/persistence/persistence-test-harness.ts b/src/main/persistence/persistence-test-harness.ts index c89a7f186ae..c72216b0701 100644 --- a/src/main/persistence/persistence-test-harness.ts +++ b/src/main/persistence/persistence-test-harness.ts @@ -119,10 +119,18 @@ export function writeDataFile(data: unknown): void { writeFileSync(dataFile(), json, 'utf-8') } -/** Restore aliases omitted by serialization, as Store.load does; use readPersistedStateJson for raw state. */ +/** Raw stored state, without the aliases Store.load restores. */ +export function readPersistedState( + path = dataFile(), + profileId = 'persistence-test' +): PersistedState { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: tests read back a snapshot their own Store serialized from PersistedState. + return JSON.parse(readPersistedStateJson(path, profileId)) as PersistedState +} + +/** Restore aliases omitted by serialization, as Store.load does; use readPersistedState for raw state. */ export function readDataFile(): unknown { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Store exports use PersistedState; this fixture reader restores only its omitted aliases. - const parsed = JSON.parse(readPersistedStateJson()) as PersistedState + const parsed = readPersistedState() hydrateWorktreeMetaAliasProjection(parsed) return parsed } diff --git a/src/main/persistence/tracking-repos/persistence-workspace-session-scrollback.test.ts b/src/main/persistence/tracking-repos/persistence-workspace-session-scrollback.test.ts index 4488b343bd8..fead61cd029 100644 --- a/src/main/persistence/tracking-repos/persistence-workspace-session-scrollback.test.ts +++ b/src/main/persistence/tracking-repos/persistence-workspace-session-scrollback.test.ts @@ -12,6 +12,7 @@ import { createStore, dataFile, makeRepo, + readPersistedState, readPersistedStateJson, testState, workspaceRepos, @@ -316,12 +317,7 @@ describe('Store', () => { }) expect(existsSync(join(testState.dir, 'terminal-scrollback', `${ref}.bin`))).toBe(false) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const stillPublished = JSON.parse(readPersistedStateJson(dataFile())) as { - workspaceSession: { - terminalLayoutsByTabId: Record }> - } - } + const stillPublished = readPersistedState(dataFile()) expect( stillPublished.workspaceSession.terminalLayoutsByTabId['remote-tab'] ?.scrollbackRefsByLeafId?.[TEST_LEAF_2] diff --git a/src/main/persistence/tracking-repos/persistence-worktree-lineage-and-backups.test.ts b/src/main/persistence/tracking-repos/persistence-worktree-lineage-and-backups.test.ts index 4a80165c3fa..725f26b630e 100644 --- a/src/main/persistence/tracking-repos/persistence-worktree-lineage-and-backups.test.ts +++ b/src/main/persistence/tracking-repos/persistence-worktree-lineage-and-backups.test.ts @@ -1,6 +1,6 @@ import { closeTestStores, - readPersistedStateJson, + readPersistedState, testState, createStore, dataFile, @@ -423,8 +423,7 @@ describe('Store', () => { vi.advanceTimersByTime(1000) await store.waitForPendingWrite() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The preceding Store save produced the PersistedState snapshot read by this test. - const persisted = JSON.parse(readPersistedStateJson()) as { repos: Repo[] } + const persisted = readPersistedState() expect(persisted.repos.map((r) => r.id).sort()).toEqual(['first', 'second']) } finally { vi.useRealTimers() diff --git a/src/main/provider-process/provider-process-teardown.ts b/src/main/provider-process/provider-process-teardown.ts index c177fda2ac5..fba025b6a54 100644 --- a/src/main/provider-process/provider-process-teardown.ts +++ b/src/main/provider-process/provider-process-teardown.ts @@ -6,6 +6,7 @@ import { } from '../pty-descendant-exit-verification' import { terminateWindowsProcessTree } from '../windows-process-tree-kill' import { recordSelfInitiatedTreeKill } from '../crash-reporting/self-initiated-tree-kill-log' +import { hasErrorCode } from '../daemon/daemon-process-inspection' /** What the teardown observed of the descendants; null when it signalled but observed nothing. */ export type ProviderProcessTeardownVerdict = DescendantTreeVerdict | null @@ -37,8 +38,7 @@ function terminateDedicatedPosixGroup( signalGroup(rootPid, 'SIGKILL') } catch (error) { // ESRCH says only that the group is empty; a descendant that left it, or a root that never led it, may live. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Node process.kill errors expose an optional errno code; only that field is read. - return (error as NodeJS.ErrnoException).code === 'ESRCH' ? null : 'unverifiable' + return hasErrorCode(error, 'ESRCH') ? null : 'unverifiable' } // Outside the try: that catch is the ESRCH contract, not a breadcrumb handler. recordSelfInitiatedTreeKill({ diff --git a/src/main/runtime/agent-detection-host-resolution.test.ts b/src/main/runtime/agent-detection-host-resolution.test.ts index 43e6630abde..27612af081f 100644 --- a/src/main/runtime/agent-detection-host-resolution.test.ts +++ b/src/main/runtime/agent-detection-host-resolution.test.ts @@ -20,18 +20,19 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants' import type { Repo } from '../../shared/repo-types' import type { Project } from '../../shared/project-types' import type { ResolvedWorktree } from './runtime-worktree-path-identity' +import { partialRuntimeStore } from './runtime-store-partial-fixture' const originalPlatform = process.platform function makeRuntime(repos: Repo[], projects: Project[] = []) { - const store = { - getRepo: (id: string) => repos.find((repo) => repo.id === id), - getRepos: () => repos, - getProjects: () => projects, - getSettings: () => ({ localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' } }) - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: detection resolution reads only the repo, project and settings members supplied here. - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService( + partialRuntimeStore({ + getRepo: (id: string) => repos.find((repo) => repo.id === id), + getRepos: () => repos, + getProjects: () => projects, + getSettings: () => ({ localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' } }) + }) + ) const selector = vi.fn<(selector: string) => Promise>() // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: replaces the protected worktree lookup this method awaits; its signature is restated above. ;(runtime as unknown as { resolveWorktreeSelector: typeof selector }).resolveWorktreeSelector = diff --git a/src/main/runtime/construction/runtime-construction.ts b/src/main/runtime/construction/runtime-construction.ts index 6a4e03bee53..e9065d26da0 100644 --- a/src/main/runtime/construction/runtime-construction.ts +++ b/src/main/runtime/construction/runtime-construction.ts @@ -116,11 +116,9 @@ export abstract class OrcaRuntimeWithStateFields extends OrcaRuntimeWithLinearCo this.store = store this.machineName.start() this.prepareClaudeAuth = deps?.prepareClaudeAuth - // oxlint-disable-next-line typescript/no-this-alias -- SAFETY: Preserve the constructor receiver captured by callbacks. - const runtime: this = this - installRuntimeFileCommandSurface(runtime, this.fileCommands) - installRuntimeGitCommandSurface(runtime, this.gitCommands) - installRuntimeRepositoryCommandSurface(runtime, { + installRuntimeFileCommandSurface(this, this.fileCommands) + installRuntimeGitCommandSurface(this, this.gitCommands) + installRuntimeRepositoryCommandSurface(this, { projectHostSetups: this.projectHostSetups, projectGroups: this.projectGroups, nestedRepoImport: this.nestedRepoImport, @@ -135,7 +133,7 @@ export abstract class OrcaRuntimeWithStateFields extends OrcaRuntimeWithLinearCo repositoryHooks: this.repositoryHooks, repositoryIssueCommand: this.repositoryIssueCommand }) - installRuntimeReviewCommandSurface(runtime, { + installRuntimeReviewCommandSurface(this, { gitLabQueries: this.gitLabQueryCommands, gitLabMutations: this.gitLabMutationCommands, gitHubReviewQueries: this.gitHubReviewQueries, @@ -143,7 +141,7 @@ export abstract class OrcaRuntimeWithStateFields extends OrcaRuntimeWithLinearCo gitHubIssueComments: this.gitHubIssueComments, gitHubProjects: this.gitHubProjectCommands }) - installRuntimeServiceCommandSurface(runtime, { + installRuntimeServiceCommandSurface(this, { aiVault: this.aiVault, sessionSearchSettings: new RuntimeSessionSearchSettingsController( store, @@ -183,13 +181,13 @@ export abstract class OrcaRuntimeWithStateFields extends OrcaRuntimeWithLinearCo } ) }) - installRuntimeSkillCommandSurface(runtime, this.skillCommands) + installRuntimeSkillCommandSurface(this, this.skillCommands) Object.assign(this, this.edgeCommands.surface) this.clientSettings = new RuntimeClientSettingsController(store, () => this.notifyReposChanged() ) this.automation = new RuntimeAutomationController(store, { - showRepo: (selector) => runtime.showRepo(selector), + showRepo: (selector) => this.showRepo(selector), showManagedWorktree: (selector) => this.showManagedWorktree(selector) }) // Why: per-device tab selections must survive host restarts, or every phone snaps back to the first tab on return. @@ -203,7 +201,7 @@ export abstract class OrcaRuntimeWithStateFields extends OrcaRuntimeWithLinearCo this.orchestrationEnvironmentTransport = deps?.orchestrationEnvironmentTransport ?? null this.workspaceSleepPlatform = deps?.workspaceSleepPlatform ?? null this.orchestrationFederation = new RuntimeOrchestrationFederation( - runtime, + this, this.orchestrationEnvironmentTransport ) if (stats) { diff --git a/src/main/runtime/delegated-mobile-device.integration.test.ts b/src/main/runtime/delegated-mobile-device.integration.test.ts index 101da75a1cd..a1a6a30035c 100644 --- a/src/main/runtime/delegated-mobile-device.integration.test.ts +++ b/src/main/runtime/delegated-mobile-device.integration.test.ts @@ -23,6 +23,7 @@ import { waitForWsClose } from './runtime-rpc-mobile-ws-test-harness' import { makeStore } from './runtime-rpc-worktree-store-fixtures' +import { partialRuntimeStore } from './runtime-store-partial-fixture' vi.mock('../git/worktree', () => ({ listWorktrees: vi.fn().mockResolvedValue([]), @@ -40,8 +41,7 @@ describe('pairing.delegatedMobileDevice.sync', () => { }) async function startServer(userDataPath = mkdtempSync(join(tmpdir(), 'orca-delegated-'))) { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store carries the surface these RPCs read, as in the sibling WebSocket tests. - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, diff --git a/src/main/runtime/device-registry-file-read.test.ts b/src/main/runtime/device-registry-file-read.test.ts new file mode 100644 index 00000000000..5182ae062b7 --- /dev/null +++ b/src/main/runtime/device-registry-file-read.test.ts @@ -0,0 +1,43 @@ +import { mkdtempSync, readdirSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { DeviceRegistry } from './device-registry' +import { DEVICE_REGISTRY_FILENAME } from './mobile-pairing-files' + +function registryWith(rows: unknown[]): { userDataPath: string; registry: DeviceRegistry } { + const userDataPath = mkdtempSync(join(tmpdir(), 'orca-device-registry-read-')) + writeFileSync(join(userDataPath, DEVICE_REGISTRY_FILENAME), JSON.stringify(rows)) + return { userDataPath, registry: new DeviceRegistry(userDataPath) } +} + +const goodRow = { + deviceId: 'good', + name: 'Phone', + token: 'good-token', + scope: 'mobile', + pairedAt: 1, + lastSeenAt: 2 +} + +describe('device registry load validation', () => { + it('keeps a row whose token is not a string listed but unable to authenticate', () => { + const { registry } = registryWith([goodRow, { ...goodRow, deviceId: 'bad-token', token: 7 }]) + expect(registry.listDevices().map((device) => device.deviceId)).toEqual(['good', 'bad-token']) + expect(registry.getDevice('bad-token')?.token).toBeUndefined() + expect(registry.validateToken('good-token')?.deviceId).toBe('good') + }) + + it('drops a row without a string deviceId and sets the file aside before rewriting', () => { + const { userDataPath, registry } = registryWith([ + goodRow, + { ...goodRow, deviceId: 42, token: 'orphan-token' }, + { ...goodRow, deviceId: undefined, token: 'missing-id-token' } + ]) + expect(registry.listDevices().map((device) => device.deviceId)).toEqual(['good']) + expect(registry.validateToken('orphan-token')).toBeNull() + expect(registry.lookupDeviceKey('42')).toEqual({ kind: 'unavailable' }) + registry.getOrCreatePendingDevice('Re-pair', 'mobile') + expect(readdirSync(userDataPath).some((name) => name.includes('.corrupt-'))).toBe(true) + }) +}) diff --git a/src/main/runtime/device-registry-file-read.ts b/src/main/runtime/device-registry-file-read.ts index 30bab126e83..42256defb87 100644 --- a/src/main/runtime/device-registry-file-read.ts +++ b/src/main/runtime/device-registry-file-read.ts @@ -20,13 +20,20 @@ export function readDeviceRegistryFile(registryPath: string): DeviceRegistryFile const doubtful = corruptRegistryCopyExists(registryPath) try { hardenExistingSecureFile(registryPath) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: normalizeLoadedDeviceRow re-validates every field; a non-array throws below and reads as corrupt. - const parsed = JSON.parse(readFileSync(registryPath, 'utf-8')) as DeviceEntry[] + const parsed: unknown = JSON.parse(readFileSync(registryPath, 'utf-8')) + if (!Array.isArray(parsed)) { + throw new Error('Device registry is not an array') + } + const devices = parsed + .map(normalizeLoadedDeviceRow) + .filter((device): device is DeviceEntry => device !== null) + // Why: a dropped row may still be some client's credential; set the file aside like a corrupt one. + const droppedRows = devices.length !== parsed.length return { - devices: parsed.map(normalizeLoadedDeviceRow), + devices, unreadable: false, - doubtful, - corruptFilePending: false + doubtful: doubtful || droppedRows, + corruptFilePending: droppedRows } } catch (error) { if (isMissingFileError(error)) { diff --git a/src/main/runtime/device-registry-row-validation.ts b/src/main/runtime/device-registry-row-validation.ts index ed0f25bc2d5..dab9be1f359 100644 --- a/src/main/runtime/device-registry-row-validation.ts +++ b/src/main/runtime/device-registry-row-validation.ts @@ -4,6 +4,7 @@ import type { DeviceEntry } from './device-registry' import { parseMobilePushRegistration } from '../../shared/mobile-push-contract' import { isDevicePublicKeyB64 } from '../../shared/device-key-proof' import { RUNTIME_DEVICE_GRANTS, type RuntimeDeviceGrant } from './rpc/rpc-method-permission' +import { isRecord } from '../../shared/agent-status-child-work-value-guards' const GRANTABLE: ReadonlySet = new Set(RUNTIME_DEVICE_GRANTS) @@ -23,27 +24,46 @@ export function validRelayBinding( value: unknown, deviceId: string ): RelayDeviceBinding | undefined { - if (!value || typeof value !== 'object') { + if (!isRecord(value)) { return undefined } - const binding = value as Partial - return binding.relayDeviceId === deviceId && - typeof binding.relayHostId === 'string' && - typeof binding.ownerIdentityKey === 'string' + return value.relayDeviceId === deviceId && + typeof value.relayHostId === 'string' && + typeof value.ownerIdentityKey === 'string' ? { - relayHostId: binding.relayHostId, - relayDeviceId: binding.relayDeviceId, - ownerIdentityKey: binding.ownerIdentityKey, - ...(typeof binding.inviteExpiresAt === 'number' && Number.isFinite(binding.inviteExpiresAt) - ? { inviteExpiresAt: binding.inviteExpiresAt } + relayHostId: value.relayHostId, + relayDeviceId: deviceId, + ownerIdentityKey: value.ownerIdentityKey, + ...(typeof value.inviteExpiresAt === 'number' && Number.isFinite(value.inviteExpiresAt) + ? { inviteExpiresAt: value.inviteExpiresAt } : {}) } : undefined } -export function normalizeLoadedDeviceRow(device: DeviceEntry): DeviceEntry { +function isTimestamp(value: unknown): value is number { + return typeof value === 'number' && Number.isFinite(value) +} + +/** Null for a row without a usable identity; the caller must treat the registry as doubtful. */ +export function normalizeLoadedDeviceRow(device: unknown): DeviceEntry | null { + if ( + !isRecord(device) || + typeof device.deviceId !== 'string' || + !device.deviceId || + !isTimestamp(device.pairedAt) || + !isTimestamp(device.lastSeenAt) + ) { + return null + } return { ...device, + deviceId: device.deviceId, + name: typeof device.name === 'string' ? device.name : '', + // Why: a non-string bearer can never match, so the row stays listed but inert. + token: typeof device.token === 'string' ? device.token : undefined, + pairedAt: device.pairedAt, + lastSeenAt: device.lastSeenAt, // Why: older registries only existed for phone pairing. Treat missing // scope as mobile so legacy device tokens do not gain new CLI powers. scope: device.scope === 'runtime' ? 'runtime' : 'mobile', @@ -75,7 +95,7 @@ export function normalizeLoadedDeviceRow(device: DeviceEntry): DeviceEntry { // Why: a malformed keyed row stays keyed (so it never falls back to a token) but cannot authenticate. function validDeviceKeyFields( - device: DeviceEntry + device: Record ): Pick { if (device.credentialKind !== 'device-key') { return { diff --git a/src/main/runtime/dsb-terminal-runtime.test.ts b/src/main/runtime/dsb-terminal-runtime.test.ts index 644e0e1095d..50e71f4bf92 100644 --- a/src/main/runtime/dsb-terminal-runtime.test.ts +++ b/src/main/runtime/dsb-terminal-runtime.test.ts @@ -6,6 +6,7 @@ import type { RuntimeTerminalSummary } from '../../shared/runtime-terminal-contr import type { TerminalAgent } from '../../shared/terminal-agent' import { OrcaRuntimeService } from './orca-runtime' import { makeStore } from './runtime-rpc-worktree-store-fixtures' +import { partialRuntimeStore } from './runtime-store-partial-fixture' vi.mock('../git/worktree', () => ({ listWorktrees: vi.fn().mockResolvedValue([ @@ -32,8 +33,7 @@ describe('manually started DeepSeek Build terminals', () => { afterEach(() => vi.useRealTimers()) it('publishes title-only identity from captured release PTY bytes', async () => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This fixture supplies the store subset used by terminal observations. - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) runtime.setPtyController({ spawn: async () => ({ id: 'pty-dsb' }), write: () => true, @@ -55,8 +55,7 @@ describe('manually started DeepSeek Build terminals', () => { it.each(['✦', '⏲', '◇', '✋'])( 'preserves Build title and identity through OSC normalization with task glyph %s', async (glyph) => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This fixture supplies the store subset used by terminal observations. - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) runtime.setPtyController({ spawn: async () => ({ id: 'pty-dsb' }), write: () => true, @@ -75,8 +74,7 @@ describe('manually started DeepSeek Build terminals', () => { it('publishes observed DSB identity and sends a prompt with generic input behavior', async () => { vi.useFakeTimers() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This fixture supplies the store subset exercised by terminal recognition and prompt submission. - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) const writes: string[] = [] runtime.setPtyController({ spawn: async () => ({ id: 'pty-dsb' }), diff --git a/src/main/runtime/editor-open-active-group.test.ts b/src/main/runtime/editor-open-active-group.test.ts index 7106f1488b9..93e2c1f8e20 100644 --- a/src/main/runtime/editor-open-active-group.test.ts +++ b/src/main/runtime/editor-open-active-group.test.ts @@ -1,6 +1,5 @@ import './orca-runtime-test-lifecycle.spec' import { describe, expect, it, vi } from 'vitest' -import type { RuntimeStore } from './runtime-store-contract' import { OrcaRuntimeService } from './orca-runtime-test-mocks.spec' import { TEST_WORKTREE_ID, @@ -30,8 +29,7 @@ function twoGroupHost() { ]) const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(saved) Object.assign(runtimeStore, { getWorkspaceSessionHostIds: () => ['local'] }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The shared fixture supplies RuntimeStore methods; its legacy Mock return type loses callable signatures. - const runtime = new RuntimeWithEditorOpenGroup(runtimeStore as RuntimeStore) + const runtime = new RuntimeWithEditorOpenGroup(runtimeStore) const open = () => openEditorFileThroughLayoutOwner( { diff --git a/src/main/runtime/folder-upgrade-runtime-listing.test.ts b/src/main/runtime/folder-upgrade-runtime-listing.test.ts index 68e26e6ff42..c3be69b584d 100644 --- a/src/main/runtime/folder-upgrade-runtime-listing.test.ts +++ b/src/main/runtime/folder-upgrade-runtime-listing.test.ts @@ -27,6 +27,13 @@ vi.mock('./repo-worktree-admin-fingerprint', () => ({ import { OrcaRuntimeService } from './orca-runtime' import type { HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity' +import { + partialRuntimeStore, + storeRepo, + storeWorktreeMeta, + type PartialRuntimeStore +} from './runtime-store-partial-fixture' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' const REPO_ID = 'repo-upgraded' const FOLDER_PATH = 'D:\\project\\a' @@ -34,11 +41,15 @@ const GIT_PATH = 'D:/project/a' const FOLDER_WORKTREE_ID = `${REPO_ID}::${FOLDER_PATH}` function makeRuntime(): OrcaRuntimeService { - const metaById: Record> = { - [FOLDER_WORKTREE_ID]: { displayName: 'Draft', hostId: 'local', instanceId: 'kept' } + const metaById: Record = { + [FOLDER_WORKTREE_ID]: storeWorktreeMeta({ + displayName: 'Draft', + hostId: 'local', + instanceId: 'kept' + }) } const repos = [ - { + storeRepo({ id: REPO_ID, path: FOLDER_PATH, displayName: 'a', @@ -46,15 +57,15 @@ function makeRuntime(): OrcaRuntimeService { addedAt: 1, kind: 'git', folderUpgradeGitRootPath: GIT_PATH - } + }) ] - const store = { + const store: PartialRuntimeStore = { getRepo: (id: string) => repos.find((repo) => repo.id === id), getRepos: () => repos, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (id: string) => metaById[id], - setWorktreeMeta: ({ id }: HostWorktreeTarget, meta: Record) => { - metaById[id] = { ...metaById[id], ...meta } + setWorktreeMeta: ({ id }: HostWorktreeTarget, meta: Partial) => { + metaById[id] = { ...storeWorktreeMeta(), ...metaById[id], ...meta } return metaById[id] }, removeWorktreeMeta: ({ id }: HostWorktreeTarget) => { @@ -71,8 +82,7 @@ function makeRuntime(): OrcaRuntimeService { }), getProjects: () => [] } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub carries the repo, meta, lineage and settings reads a worktree listing makes; the rest of Store is unreached. - return new OrcaRuntimeService(store as never) + return new OrcaRuntimeService(partialRuntimeStore(store)) } describe('folder-upgraded checkout in runtime listings', () => { diff --git a/src/main/runtime/folder-workspace-mixed-host-routing.test.ts b/src/main/runtime/folder-workspace-mixed-host-routing.test.ts index 132892f91ca..0ba14cd696d 100644 --- a/src/main/runtime/folder-workspace-mixed-host-routing.test.ts +++ b/src/main/runtime/folder-workspace-mixed-host-routing.test.ts @@ -9,6 +9,7 @@ import { } from '../../shared/folder-workspace-host-routing.test-fixture' import type { ProjectGroup } from '../../shared/project-group-types' import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' +import type { LayoutApplyOutcome } from '../../shared/workspace-layout/workspace-layout-command-types' import { localDesktopSession } from '../../shared/workspace-layout/workspace-layout-profile.test-fixture' import { emptySession, @@ -18,6 +19,7 @@ import { import { OrcaRuntimeService } from './orca-runtime' import { WorkspaceLayoutStream } from './workspace-layout-stream' import { collectLayoutEvents } from './workspace-layout-stream-events.test-fixture' +import { partialRuntimeStore, storeRepo } from './runtime-store-partial-fixture' const mocks = vi.hoisted(() => ({ layoutOwnerIsMain: true })) vi.mock('../../shared/layout-owner-switch', () => ({ @@ -30,24 +32,26 @@ function runtimeWithMixedFolder( folder: FolderWorkspace = MIXED_FOLDER, groups: readonly ProjectGroup[] = [MIXED_GROUP] ) { - const repos = ROUTING_REPOS + const repos = ROUTING_REPOS.map(storeRepo) const partitions = new Map([ ['local', localDesktopSession()], ['ssh:target-1', emptySession()] ]) - const applyLayoutCommand = vi.fn(async () => ({ ok: true, result: {} })) + const applyLayoutCommand = vi.fn(async (): Promise => ({ + ok: true, + result: {} + })) const store = { getRepos: () => repos, getRepo: (id: string) => repos.find((repo) => repo.id === id), - getProjectGroups: () => groups, + getProjectGroups: () => [...groups], getFolderWorkspaces: () => [folder], getWorkspaceSessionHostIds: () => [...partitions.keys()], getWorkspaceSession: (hostId: ExecutionHostId) => partitions.get(hostId) ?? emptySession(), onWorkspaceSessionWritten: () => () => {}, applyLayoutCommand } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: routing, the stream and apply read only these store methods. - return { runtime: new OrcaRuntimeService(store as never), applyLayoutCommand } + return { runtime: new OrcaRuntimeService(partialRuntimeStore(store)), applyLayoutCommand } } const RATIOS = { diff --git a/src/main/runtime/headless-close-keeps-publication-epoch.spec.ts b/src/main/runtime/headless-close-keeps-publication-epoch.spec.ts index 528feb697af..8f0c3ffc79c 100644 --- a/src/main/runtime/headless-close-keeps-publication-epoch.spec.ts +++ b/src/main/runtime/headless-close-keeps-publication-epoch.spec.ts @@ -6,6 +6,7 @@ import type { RuntimeMobileSessionTabsSnapshot, RuntimeMobileSessionTerminalTab } from '../../shared/runtime-types' +import { partialRuntimeStore } from './runtime-store-partial-fixture' export function registerHeadlessCloseKeepsPublicationEpochTests(): void { /** @@ -73,8 +74,7 @@ export function registerHeadlessCloseKeepsPublicationEpochTests(): void { } async function closeOneTab(): Promise { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: makeStore covers the reads this suite drives. - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) const closedTab = terminalTab('tab-a', LEAF_ID) const snapshot = storedSnapshot([closedTab, terminalTab('tab-b', LEAF_ID)]) // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: closeHeadlessMobileTerminalTab is protected; reaching it is the only way to drive a headless close. diff --git a/src/main/runtime/headless-pane-layout-noop-publication.test.ts b/src/main/runtime/headless-pane-layout-noop-publication.test.ts index 9f598282427..4af06218b6d 100644 --- a/src/main/runtime/headless-pane-layout-noop-publication.test.ts +++ b/src/main/runtime/headless-pane-layout-noop-publication.test.ts @@ -11,7 +11,6 @@ import { makeRuntimeStoreWithWorkspaceSession, makeWorkspaceSessionWithHeadlessTerminal } from './orca-runtime-test-fixtures.spec' -import type { RuntimeStore } from './runtime-store-contract' import type { TerminalPaneLayoutNode } from '../../shared/terminal-tab-types' const REMOUNTS = 20 @@ -20,8 +19,7 @@ async function setup() { const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The shared fixture implements RuntimeStore; its annotation erases the Vitest mock call signatures. - const runtime = new OrcaRuntimeService(runtimeStore as RuntimeStore) + const runtime = new OrcaRuntimeService(runtimeStore) // Hydrate the live snapshot the way the first list/subscribe does. await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) const published: unknown[] = [] diff --git a/src/main/runtime/headless-placeholder-graph-surface-standing.spec.ts b/src/main/runtime/headless-placeholder-graph-surface-standing.spec.ts index e72d37e0fab..488a89a392a 100644 --- a/src/main/runtime/headless-placeholder-graph-surface-standing.spec.ts +++ b/src/main/runtime/headless-placeholder-graph-surface-standing.spec.ts @@ -7,6 +7,7 @@ import { SURFACE_CLAIM_WITHOUT_STANDING } from './pty-recorded-surface-topology' import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch' import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { bindPaneFirst } from './bind-first-pane.test-fixture' +import { partialRuntimeStore } from './runtime-store-partial-fixture' export function registerHeadlessPlaceholderGraphSurfaceStandingTests(): void { // #18191: a headless server publishes one empty placeholder graph at launch so status clients see @@ -48,8 +49,7 @@ export function registerHeadlessPlaceholderGraphSurfaceStandingTests(): void { /** A headless host: no renderer ever attaches, and the only graph is the launch placeholder. */ function makeHeadlessRuntime(): OrcaRuntimeService { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: makeStore returns the repo and session reads this suite drives; the rest of Store is unreached. - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub carries the members this suite drives; the PTY stays live throughout. runtime.setPtyController({ spawn: vi.fn(async () => ({ id: 'never' })), diff --git a/src/main/runtime/hook-status-resume-record.test.ts b/src/main/runtime/hook-status-resume-record.test.ts index b9bd340338f..4bc2b5a5370 100644 --- a/src/main/runtime/hook-status-resume-record.test.ts +++ b/src/main/runtime/hook-status-resume-record.test.ts @@ -38,10 +38,7 @@ function makeRuntime() { return { ok: true, result: {} } } ) - const runtime = new OrcaRuntimeService( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements the runtime store methods these tests reach. - Object.assign(runtimeStore, { applyLayoutCommand }) as never - ) + const runtime = new OrcaRuntimeService(Object.assign(runtimeStore, { applyLayoutCommand })) // No window holds any tab. runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) return { runtime, getSession, applyLayoutCommand } @@ -145,8 +142,7 @@ describe('main records a held pane’s resume record from its hook status', () = const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements the runtime store methods these tests reach. - const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, { + const runtime = new OrcaRuntimeService(runtimeStore, undefined, { getAgentProviderSessionSnapshot: () => rows }) diff --git a/src/main/runtime/hook-status-resume-sleep-ordering.test.ts b/src/main/runtime/hook-status-resume-sleep-ordering.test.ts index 6e03d15180b..9082bb1020d 100644 --- a/src/main/runtime/hook-status-resume-sleep-ordering.test.ts +++ b/src/main/runtime/hook-status-resume-sleep-ordering.test.ts @@ -66,10 +66,7 @@ function makeRuntimeWithSlowFirstWrite() { return { ok: true, result: {} } } ) - const runtime = new OrcaRuntimeService( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements the runtime store methods these tests reach. - Object.assign(runtimeStore, { applyLayoutCommand }) as never - ) + const runtime = new OrcaRuntimeService(Object.assign(runtimeStore, { applyLayoutCommand })) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) return { runtime, getSession, releaseFirstWrite } } diff --git a/src/main/runtime/host-account-launch-env.spec.ts b/src/main/runtime/host-account-launch-env.spec.ts index 2ab13439b59..543bc95b877 100644 --- a/src/main/runtime/host-account-launch-env.spec.ts +++ b/src/main/runtime/host-account-launch-env.spec.ts @@ -4,6 +4,7 @@ import { prepareLocalCommitMessageAgentEnv } from '../text-generation/commit-mes import { RuntimeAccountController, type RuntimeAccountServices } from './runtime-account-controller' import { withHostAccountLaunchEnv } from './runtime-agent-terminal-startup' import { OrcaRuntimeService } from './orca-runtime' +import { partialRuntimeStore } from './runtime-store-partial-fixture' export function registerHostAccountLaunchEnvTests(): void { const LAUNCH_ENV = HostAccountLaunchEnvParams.parse({ @@ -97,8 +98,7 @@ export function registerHostAccountLaunchEnvTests(): void { } }) } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these paths read only getSettings(). - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) runtime.setHostAccountLaunchEnv(LAUNCH_ENV) return runtime } diff --git a/src/main/runtime/host-pane-remount-terminal-handle.test.ts b/src/main/runtime/host-pane-remount-terminal-handle.test.ts index e0e8453725b..bc871c82387 100644 --- a/src/main/runtime/host-pane-remount-terminal-handle.test.ts +++ b/src/main/runtime/host-pane-remount-terminal-handle.test.ts @@ -46,8 +46,7 @@ function graph(ptyId: string | null, extraLeafPtyId?: string) { function makeHost(options: { livePtyIds: Set }) { const writes: string[] = [] - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Graph sync and terminal send read no store fields. - const runtime = new OrcaRuntimeService(null as never) + const runtime = new OrcaRuntimeService(null) runtime.setPtyController({ write: (_ptyId, data) => { writes.push(data) diff --git a/src/main/runtime/host-sleep-resume-record.test.ts b/src/main/runtime/host-sleep-resume-record.test.ts index 7b9371585a4..65465683fa4 100644 --- a/src/main/runtime/host-sleep-resume-record.test.ts +++ b/src/main/runtime/host-sleep-resume-record.test.ts @@ -1,6 +1,7 @@ // A paired client's sleep stops the host's PTYs; the host must keep the session to resume on open. import { describe, expect, it, vi } from 'vitest' import type { AgentStatusIpcPayload } from '../../shared/agent-status-ipc-payload' +import { partialRuntimeStore } from './runtime-store-partial-fixture' // Fragments stay side-effect ordered: mocks, then lifecycle, then fixtures. const { OrcaRuntimeService } = await import('./orca-runtime-test-mocks.spec') @@ -21,6 +22,14 @@ const SESSION_ID = 'slept-provider-session' type Verdict = 'exited' | 'unverifiable' | null +/** A hook store row as the agent hook server records it. */ +function hookRow( + fields: Partial & + Pick +): AgentStatusIpcPayload { + return { state: 'done', stateStartedAt: fields.receivedAt, connectionId: null, ...fields } +} + function makeRuntime(initialVerdict: Verdict, holderVerdict: Verdict = null) { let verdict = initialVerdict const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( @@ -33,8 +42,7 @@ function makeRuntime(initialVerdict: Verdict, holderVerdict: Verdict = null) { live.add(id) return { id } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook row reads only the fields the sleep capture uses. - const row = { + const row = hookRow({ paneKey: PANE_KEY, agentType: 'claude', providerSession: { key: 'session_id', id: SESSION_ID }, @@ -43,10 +51,9 @@ function makeRuntime(initialVerdict: Verdict, holderVerdict: Verdict = null) { receivedAt: 1, stateStartedAt: 1, connectionId: null - } as AgentStatusIpcPayload + }) let rows: AgentStatusIpcPayload[] = [] - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements the runtime store methods these tests reach. - const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, { + const runtime = new OrcaRuntimeService(partialRuntimeStore(runtimeStore), undefined, { getAgentProviderSessionSnapshot: () => rows, checkHookAgentPresence: async (paneKey: string) => paneKey === HOLDER_PANE_KEY ? holderVerdict : verdict @@ -176,14 +183,13 @@ describe('host-driven worktree sleep keeps the slept agent resumable', () => { ['codex', false] ] as const)('keeps a %s-launched PTY recipe: %s', async (launchAgent, kept) => { const config = { agentCommand: '/opt/claude-custom', agentArgs: '--x', agentEnv: {} } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the capture reads only these row fields. - const launchedRow = { + const launchedRow = hookRow({ paneKey: PANE_KEY, agentType: 'claude', providerSession: { key: 'session_id', id: SESSION_ID }, prompt: '', receivedAt: 1 - } as AgentStatusIpcPayload + }) const capture = await captureWorktreeSleepResumeRecords({ worktreeId: TEST_WORKTREE_ID, paneKeysByPtyId: new Map([['pty', [PANE_KEY]]]), @@ -196,13 +202,12 @@ describe('host-driven worktree sleep keeps the slept agent resumable', () => { }) it('retires an older capture when the hook store names a non-resumable agent in the pane', async () => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the capture reads only these row fields. - const row = { + const row = hookRow({ paneKey: PANE_KEY, agentType: 'amp', prompt: '', receivedAt: 1 - } as AgentStatusIpcPayload + }) const capture = await captureWorktreeSleepResumeRecords({ worktreeId: TEST_WORKTREE_ID, paneKeysByPtyId: new Map([['pty', [PANE_KEY]]]), @@ -219,15 +224,14 @@ describe('host-driven worktree sleep keeps the slept agent resumable', () => { ['wsl:Ubuntu', null], ['ssh-target', 'ssh-target'] ] as const)('stamps hook connection %s as host %s', async (rowConnectionId, stamped) => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the capture reads only these row fields. - const row = { + const row = hookRow({ paneKey: PANE_KEY, agentType: 'claude', providerSession: { key: 'session_id', id: SESSION_ID }, prompt: '', receivedAt: 1, connectionId: rowConnectionId - } as AgentStatusIpcPayload + }) const capture = await captureWorktreeSleepResumeRecords({ worktreeId: TEST_WORKTREE_ID, paneKeysByPtyId: new Map([['pty', [PANE_KEY]]]), diff --git a/src/main/runtime/layout-command-pane-starts-layout-owner.test.ts b/src/main/runtime/layout-command-pane-starts-layout-owner.test.ts index 2b7890eb628..8eab62626f0 100644 --- a/src/main/runtime/layout-command-pane-starts-layout-owner.test.ts +++ b/src/main/runtime/layout-command-pane-starts-layout-owner.test.ts @@ -89,8 +89,7 @@ async function desktopRuntime( // The tab was launched as the agent its record resumes. session.tabsByWorktree[TEST_WORKTREE_ID]![0]!.launchAgent = 'claude' const fixture = makeRuntimeStoreWithWorkspaceSession(session) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements the runtime store methods these tests reach. - const runtime = new StatusRuntime(fixture.runtimeStore as never, undefined, { + const runtime = new StatusRuntime(fixture.runtimeStore, undefined, { getDesktopWindowStatus: () => 'openable' }) const started = deferred<{ id: string }>() @@ -107,8 +106,7 @@ async function desktopRuntime( // The desktop window is open: before C1c it started this pane itself. runtime.attachWindow(TEST_WINDOW_ID) runtime.markGraphReady(TEST_WINDOW_ID) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the runtime reads only isDestroyed from the window mock. - electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false } as never) + electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false }) if (opts.activate !== false) { await runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`, { notifyClients: false, diff --git a/src/main/runtime/managed-worktree-create-registry.test.ts b/src/main/runtime/managed-worktree-create-registry.test.ts index cce1a24e6d2..fec290ffe08 100644 --- a/src/main/runtime/managed-worktree-create-registry.test.ts +++ b/src/main/runtime/managed-worktree-create-registry.test.ts @@ -45,6 +45,7 @@ vi.mock('../ipc/registered-worktree-roots-scoped-invalidation', () => ({ })) import { OrcaRuntimeService } from './orca-runtime' +import { partialRuntimeStore } from './runtime-store-partial-fixture' const gitRepo = { id: 'repo-1', @@ -68,12 +69,12 @@ type RuntimeInternals = { } function makeRuntime(repo: Record = gitRepo): OrcaRuntimeService { - const store = { - getSettings: () => ({ disabledTuiAgents: ['codex'], workspaceDir: '/worktrees' }), - getProjectHostSetups: () => [] - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every store method this create path reaches is supplied above. - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService( + partialRuntimeStore({ + getSettings: () => ({ disabledTuiAgents: ['codex'], workspaceDir: '/worktrees' }), + getProjectHostSetups: () => [] + }) + ) // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the named members all exist on the service; the cast only exposes non-public ones to the spies. const internals = runtime as unknown as RuntimeInternals vi.spyOn(internals, 'resolveRepoSelector').mockResolvedValue(repo) diff --git a/src/main/runtime/managed-worktree-create-telemetry.test.ts b/src/main/runtime/managed-worktree-create-telemetry.test.ts index 49113c80feb..6bd0c5c15c4 100644 --- a/src/main/runtime/managed-worktree-create-telemetry.test.ts +++ b/src/main/runtime/managed-worktree-create-telemetry.test.ts @@ -51,6 +51,7 @@ import { beginPreparationWork, type PreparationWork } from '../worktree-create-concurrency' +import { partialRuntimeStore } from './runtime-store-partial-fixture' const gitRepo = { id: 'repo-1', @@ -74,12 +75,12 @@ type RuntimeInternals = { } function makeRuntime(repo: Record = gitRepo): OrcaRuntimeService { - const store = { - getSettings: () => ({ disabledTuiAgents: ['codex'], workspaceDir: '/worktrees' }), - getProjectHostSetups: () => [] - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every store method this create path reaches is supplied above. - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService( + partialRuntimeStore({ + getSettings: () => ({ disabledTuiAgents: ['codex'], workspaceDir: '/worktrees' }), + getProjectHostSetups: () => [] + }) + ) // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the named members all exist on the service; the cast only exposes non-public ones to the spies. const internals = runtime as unknown as RuntimeInternals vi.spyOn(internals, 'resolveRepoSelector').mockResolvedValue(repo) diff --git a/src/main/runtime/mobile-cloud-relay/relay-auth-coordinator-readiness.test.ts b/src/main/runtime/mobile-cloud-relay/relay-auth-coordinator-readiness.test.ts index 2f136636d17..55837261f71 100644 --- a/src/main/runtime/mobile-cloud-relay/relay-auth-coordinator-readiness.test.ts +++ b/src/main/runtime/mobile-cloud-relay/relay-auth-coordinator-readiness.test.ts @@ -14,8 +14,7 @@ const context: RelayAuthContext = { } function fakeSessionBroker(): RelaySessionBroker { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the service only calls the stubbed members below. - const broker = Object.create(RelaySessionBroker.prototype) as RelaySessionBroker + const broker = Object.create(RelaySessionBroker.prototype) Object.defineProperties(broker, { closeNow: { value: vi.fn() }, isLive: { value: () => true }, @@ -28,8 +27,7 @@ function fakeSessionBroker(): RelaySessionBroker { } function serviceWith(coordinator: RelayAuthCoordinator): DesktopRelayService { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: createPairingRelay touches only the fields assigned here. - const service = Object.create(DesktopRelayService.prototype) as DesktopRelayService + const service = Object.create(DesktopRelayService.prototype) return Object.assign(service, { coordinator, stopped: false, diff --git a/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.integration.test.ts b/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.integration.test.ts index 6b7f0506805..946549ca7de 100644 --- a/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.integration.test.ts +++ b/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.integration.test.ts @@ -21,6 +21,7 @@ import { } from '../runtime-rpc-mobile-ws-test-harness' import { makeStore } from '../runtime-rpc-worktree-store-fixtures' import type { MobileDesktopRelayHosts } from './mobile-desktop-relay-hosts' +import { partialRuntimeStore } from '../runtime-store-partial-fixture' vi.mock('../../git/worktree', () => { const worktrees = [ @@ -49,8 +50,7 @@ describe('mobile relay hosts: phone -> desktop lists the servers it shows', () = }) async function startRuntime() { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store carries the surface these RPCs read, as in the sibling relay tests. - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath: mkdtempSync(join(tmpdir(), 'orca-relay-hosts-')), diff --git a/src/main/runtime/omp-user-bubble-launch-authority.test.ts b/src/main/runtime/omp-user-bubble-launch-authority.test.ts index ff345285191..b2a435b00b9 100644 --- a/src/main/runtime/omp-user-bubble-launch-authority.test.ts +++ b/src/main/runtime/omp-user-bubble-launch-authority.test.ts @@ -5,6 +5,7 @@ import { OrcaRuntimeService } from './orca-runtime' import { makeStore } from './runtime-rpc-worktree-store-fixtures' import { spawnedPtyEnv } from './spawned-pty-env-fixtures' import { committingSpawn } from './bind-first-pane.test-fixture' +import { partialRuntimeStore } from './runtime-store-partial-fixture' // #10923: every OMP user bubble ends with OSC 133 `B C D;0`. Read as the shell's command // finishing, that retired the pane's hook authority mid-turn, which deleted its status row and @@ -34,8 +35,7 @@ async function launchOmpPane(): Promise<{ id: 'pty-omp', incarnationId: 'incarnation-1' })) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: makeStore serves the repo reads terminal creation makes; the rest of Store is unreached. - const runtime = new OrcaRuntimeService(makeStore() as never, undefined, { + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore()), undefined, { attestAgentHookCompatibilityAuthority: (candidate) => ({ paneKey: candidate.paneKey, source: 'current_hook' as const diff --git a/src/main/runtime/opencode-wsl-folder-launch-host.test.ts b/src/main/runtime/opencode-wsl-folder-launch-host.test.ts index 5738ed3db03..4a2c0cc51c3 100644 --- a/src/main/runtime/opencode-wsl-folder-launch-host.test.ts +++ b/src/main/runtime/opencode-wsl-folder-launch-host.test.ts @@ -5,6 +5,14 @@ import { RpcDispatcher } from './rpc/dispatcher' import { AGENT_SESSION_METHODS } from './rpc/methods/agent-session' import { _resetWslCachesForTests, _setWslCachesForTests } from '../wsl' import { PROJECT_RUNTIME_REPAIR_REQUIRED_CODE } from '../../shared/project-runtime-repair-message' +import { + partialRuntimeStore, + storeFolderWorkspace, + storeProject, + storeProjectGroup, + storeRepo, + type PartialRuntimeStore +} from './runtime-store-partial-fixture' vi.mock('../opencode/opencode-launch-capabilities', () => ({ probeOpenCodeLaunchCapabilities: async () => ({ @@ -18,11 +26,11 @@ vi.mock('../opencode/opencode-model-availability', () => ({ resolveOpenCodeDirectModelExecutable: async () => 'opencode' })) -const repo = { id: 'r', path: 'C:\\repo', kind: 'git', projectGroupId: 'g' } -const share = { id: 's', path: '\\\\wsl$\\Ubuntu\\s', kind: 'git', projectGroupId: 'g' } -const folder = { id: 'f', projectGroupId: 'g', folderPath: 'C:\\work' } -const wsl = { kind: 'wsl', distro: 'Ubuntu' } -const store = { +const repo = storeRepo({ id: 'r', path: 'C:\\repo', kind: 'git', projectGroupId: 'g' }) +const share = storeRepo({ id: 's', path: '\\\\wsl$\\Ubuntu\\s', kind: 'git', projectGroupId: 'g' }) +const folder = storeFolderWorkspace({ id: 'f', projectGroupId: 'g', folderPath: 'C:\\work' }) +const wsl = { kind: 'wsl', distro: 'Ubuntu' } as const +const store: PartialRuntimeStore = { // A WSL Terminal shell must not move the model check into WSL, as it did once. getSettings: () => ({ disabledTuiAgents: [], @@ -30,13 +38,17 @@ const store = { terminalWindowsWslDistro: 'Ubuntu' }), getProjects: () => [ - { id: 'p', sourceRepoIds: ['r'], localWindowsRuntimePreference: wsl }, - { id: 'q', sourceRepoIds: ['s'], localWindowsRuntimePreference: { kind: 'windows-host' } } + storeProject({ id: 'p', sourceRepoIds: ['r'], localWindowsRuntimePreference: wsl }), + storeProject({ + id: 'q', + sourceRepoIds: ['s'], + localWindowsRuntimePreference: { kind: 'windows-host' } + }) ], getRepo: (id: string) => [repo, share].find((entry) => entry.id === id), getRepos: () => [repo], getFolderWorkspaces: () => [folder], - getProjectGroups: () => [{ id: 'g' }] + getProjectGroups: () => [storeProjectGroup({ id: 'g' })] } const scope = { id: 'f', path: 'C:\\work', connectionId: null, repo: null, folderWorkspace: folder } const clientOperationId = `${Date.now()}-0123456789abcdef0123456789abcdef` @@ -47,8 +59,7 @@ const setPlatform = (value: string) => Object.defineProperty(process, 'platform', { configurable: true, value }) beforeEach(() => { setPlatform('win32') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these paths read only the members above. - runtime = new OrcaRuntimeService(store as never) + runtime = new OrcaRuntimeService(partialRuntimeStore(store)) Object.assign(runtime, { resolveTerminalWorkspaceLaunchScope: async () => scope, executionOwnerSupportsAgentSessionOperation: async () => true diff --git a/src/main/runtime/orca-runtime-agent-session-account-env.test.ts b/src/main/runtime/orca-runtime-agent-session-account-env.test.ts index a98e2b89e7b..c639baac995 100644 --- a/src/main/runtime/orca-runtime-agent-session-account-env.test.ts +++ b/src/main/runtime/orca-runtime-agent-session-account-env.test.ts @@ -6,6 +6,7 @@ import { import { HostAccountLaunchEnvParams } from '../../shared/host-account-launch-env' import { OrcaRuntimeService } from './orca-runtime' import type { TerminalCreateOptions } from './runtime-terminal-contracts' +import { partialRuntimeStore } from './runtime-store-partial-fixture' const ACCOUNT_ENV = { CLAUDE_CONFIG_DIR: '/home/u/.orca/claude-accounts/a/config', @@ -38,8 +39,7 @@ function createRuntime() { agentDefaultEnv: { codex: { KEEP: '1', CODEX_HOME: '/stale' } } }) } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the launch paths under test read only getSettings(). - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) Object.assign(runtime, { resolveTerminalWorkspaceLaunchScope: vi.fn(async () => ({ id: 'worktree-1', diff --git a/src/main/runtime/orca-runtime-cold-restore-terminal-membership.test.ts b/src/main/runtime/orca-runtime-cold-restore-terminal-membership.test.ts index d53d076f282..fa86fbb604b 100644 --- a/src/main/runtime/orca-runtime-cold-restore-terminal-membership.test.ts +++ b/src/main/runtime/orca-runtime-cold-restore-terminal-membership.test.ts @@ -489,7 +489,6 @@ describe('a surface the host retired stays retired when a restored sibling regis sessions.set(SSH_HOST.hostId, makeSshSession(['tab-x'])) }) } as never) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the close path kills and inventories only; the remote kill lands asynchronously. runtime.setPtyController({ write: () => true, kill, @@ -497,7 +496,7 @@ describe('a surface the host retired stays retired when a restored sibling regis [SSH_PTY_X, SSH_PTY_Y].map((id) => ({ id, cwd: '/worktree', title: 'shell' })) ), getForegroundProcess: async () => null - } as never) + }) publishSshRendererFrame(runtime) registerBound(runtime, SSH_HOST.hostId, SSH_PTY_Y, 'ssh-1', { tabId: 'tab-y', diff --git a/src/main/runtime/orca-runtime-exited-split-pane-close.test.ts b/src/main/runtime/orca-runtime-exited-split-pane-close.test.ts index 452e9d39a9b..6444a7fc954 100644 --- a/src/main/runtime/orca-runtime-exited-split-pane-close.test.ts +++ b/src/main/runtime/orca-runtime-exited-split-pane-close.test.ts @@ -4,6 +4,7 @@ import { collectLeafIds } from '../../shared/terminal-pane-layout-tree' import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import type { RuntimeMobileSessionTabsSnapshot } from '../../shared/runtime-types' import { OrcaRuntimeService } from './orca-runtime' +import { partialRuntimeStore } from './runtime-store-partial-fixture' vi.mock('../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true })) @@ -81,14 +82,15 @@ function phoneSnapshot(session: WorkspaceSessionState): RuntimeMobileSessionTabs /** A runtime over one stored session, with the window's two split panes registered. */ function splitRuntime(): { runtime: OrcaRuntimeService; session: () => WorkspaceSessionState } { let session = splitSession() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the exit path reads only these store members. - const runtime = new OrcaRuntimeService({ - getRepos: () => [LIVE_REPO], - getWorkspaceSession: () => session, - setWorkspaceSession: (next: WorkspaceSessionState) => { - session = next - } - } as never) + const runtime = new OrcaRuntimeService( + partialRuntimeStore({ + getRepos: () => [LIVE_REPO], + getWorkspaceSession: () => session, + setWorkspaceSession: (next: WorkspaceSessionState) => { + session = next + } + }) + ) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ diff --git a/src/main/runtime/orca-runtime-hook-agent-status-projection.test.ts b/src/main/runtime/orca-runtime-hook-agent-status-projection.test.ts index da822047a2b..1dd671f585e 100644 --- a/src/main/runtime/orca-runtime-hook-agent-status-projection.test.ts +++ b/src/main/runtime/orca-runtime-hook-agent-status-projection.test.ts @@ -9,6 +9,7 @@ import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch' import type { AgentStatusIpcPayload } from '../../shared/agent-status-types' import { makePaneKey } from '../../shared/stable-pane-id' import { committingSpawn } from './bind-first-pane.test-fixture' +import { partialRuntimeStore } from './runtime-store-partial-fixture' vi.mock('electron', () => ({ BrowserWindow: { fromId: vi.fn(() => null) }, @@ -70,17 +71,16 @@ async function createRuntimeWithHookRows( : (): AgentStatusIpcPayload[] => rows // Switch-on, main's layout answers which pane holds the PTY, so the runtime needs a store. const store = ORCA_LAYOUT_OWNER_IS_MAIN - ? { + ? partialRuntimeStore({ getRepos: () => [], getRepo: () => undefined, getAllWorktreeMeta: () => ({}), getWorktreeMeta: () => undefined, getProjects: () => [], getSettings: () => ({}) - } + }) : null - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the create and list paths read only repos, settings and the session bindPaneFirst adds. - const runtime = new OrcaRuntimeService(store as never, undefined, { + const runtime = new OrcaRuntimeService(store, undefined, { ...(statusWiring ? { onTerminalAgentStatus: statusWiring.deps.onTerminalAgentStatus, diff --git a/src/main/runtime/orca-runtime-provider-reattach-launch-identity.test.ts b/src/main/runtime/orca-runtime-provider-reattach-launch-identity.test.ts index a60051aa7fd..9d2d36f5077 100644 --- a/src/main/runtime/orca-runtime-provider-reattach-launch-identity.test.ts +++ b/src/main/runtime/orca-runtime-provider-reattach-launch-identity.test.ts @@ -3,6 +3,7 @@ import { makePaneKey } from '../../shared/stable-pane-id' import { OrcaRuntimeService } from './orca-runtime' import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch' import { registerBoundPty } from './bind-first-pane.test-fixture' +import { partialRuntimeStore } from './runtime-store-partial-fixture' vi.mock('electron', () => ({ app: { getPath: vi.fn(() => '/tmp'), isPackaged: false }, @@ -33,8 +34,7 @@ function getPty(runtime: OrcaRuntimeService, ptyId: string): RuntimePtyLaunchIde /** Switch-on, main's layout answers which pane holds a terminal, so the runtime needs a store. */ function createRuntime(): OrcaRuntimeService { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: registration reads only the session bindPaneFirst adds to this store. - return new OrcaRuntimeService(ORCA_LAYOUT_OWNER_IS_MAIN ? ({} as never) : null) + return new OrcaRuntimeService(ORCA_LAYOUT_OWNER_IS_MAIN ? partialRuntimeStore() : null) } function paneKeyOf(runtime: OrcaRuntimeService, ptyId: string): string | null { diff --git a/src/main/runtime/orca-runtime-serve-browser-restore.test.ts b/src/main/runtime/orca-runtime-serve-browser-restore.test.ts index cd7e7425728..b5621dd13cb 100644 --- a/src/main/runtime/orca-runtime-serve-browser-restore.test.ts +++ b/src/main/runtime/orca-runtime-serve-browser-restore.test.ts @@ -1,5 +1,4 @@ import './orca-runtime-test-lifecycle.spec' -import type { RuntimeStore } from './runtime-store-contract' import { describe, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime-test-mocks.spec' import { @@ -18,8 +17,7 @@ function restartedServe(session: WorkspaceSessionState = savedServeTabs()) { const saved = session const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(saved) Object.assign(runtimeStore, { getWorkspaceSessionHostIds: () => ['local'] }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The shared fixture supplies RuntimeStore methods; its legacy Mock return type loses callable signatures. - const runtime = new OrcaRuntimeService(runtimeStore as RuntimeStore) + const runtime = new OrcaRuntimeService(runtimeStore) const livePages: string[] = [] const createTab = vi.fn(async ({ browserPageId }: { browserPageId?: string }) => { livePages.push(browserPageId!) diff --git a/src/main/runtime/orca-runtime-session-tabs-path-alias.test.ts b/src/main/runtime/orca-runtime-session-tabs-path-alias.test.ts index 7088c80eb72..0cde7a86e3d 100644 --- a/src/main/runtime/orca-runtime-session-tabs-path-alias.test.ts +++ b/src/main/runtime/orca-runtime-session-tabs-path-alias.test.ts @@ -11,6 +11,7 @@ import { RpcDispatcher } from './rpc/dispatcher' import { SESSION_TAB_METHODS } from './rpc/methods/session-tabs' import { getRepoExecutionHostId } from '../../shared/execution-host' import { bindPaneFirst } from './bind-first-pane.test-fixture' +import { partialRuntimeStore, storeRepo } from './runtime-store-partial-fixture' vi.mock('electron', () => ({ BrowserWindow: { fromId: vi.fn(() => null) }, @@ -133,12 +134,13 @@ describe('session tab workspace path aliases', () => { )( 'keeps live alias adoption within the execution host: %j', async ({ connectionId, ptyConnectionId, ownsSession, binding }) => { - const repo = { id: 'repo', path: 'G:\\git\\example', connectionId } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this fixture only needs the registered repository's execution host. - const runtime = new OrcaRuntimeService({ - getRepo: () => repo, - getRepos: () => [repo] - } as never) + const repo = storeRepo({ id: 'repo', path: 'G:\\git\\example', connectionId }) + const runtime = new OrcaRuntimeService( + partialRuntimeStore({ + getRepo: () => repo, + getRepos: () => [repo] + }) + ) runtime.attachWindow(1) const ptyId = ptyConnectionId ? toAppSshPtyId(ptyConnectionId, 'pty-existing') @@ -187,17 +189,18 @@ describe('session tab workspace path aliases', () => { )( 'keeps runtime aliases in the owning environment without trusting nested SSH names: %j', async ({ ptyId, ownsSession, binding }) => { - const repo = { + const repo = storeRepo({ id: 'repo', path: 'G:\\git\\example', executionHostId: 'runtime:env-a', connectionId: 'nested-target' - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture supplies the runtime-owned repo row; no remote services are called. - const runtime = new OrcaRuntimeService({ - getRepo: () => repo, - getRepos: () => [repo] - } as never) + }) + const runtime = new OrcaRuntimeService( + partialRuntimeStore({ + getRepo: () => repo, + getRepos: () => [repo] + }) + ) runtime.attachWindow(1) runtime.registerPty(ptyId, WORKTREE, null, { tabId: 'tab', leafId: LEAF }) if (binding !== 'unpublished') { @@ -229,12 +232,13 @@ describe('session tab workspace path aliases', () => { ) it('refuses alias adoption when repository rows disagree about the execution host', async () => { - const local = { id: 'repo', path: 'G:\\git\\example' } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: duplicate repo rows model an ambiguous host without any external services. - const runtime = new OrcaRuntimeService({ - getRepo: () => local, - getRepos: () => [local, { ...local, connectionId: 'ssh-host' }] - } as never) + const local = storeRepo({ id: 'repo', path: 'G:\\git\\example' }) + const runtime = new OrcaRuntimeService( + partialRuntimeStore({ + getRepo: () => local, + getRepos: () => [local, { ...local, connectionId: 'ssh-host' }] + }) + ) runtime.attachWindow(1) publishGraph(runtime) await expect(runtime.listMobileSessionTabs(`id:${ALIAS}`)).rejects.toThrow( @@ -267,8 +271,7 @@ describe('session tab workspace path aliases', () => { 'recovers a surviving renderer PTY without pinning an empty alias (discovered during refresh: %s)', async (duringRefresh) => { // Why a store: main's layout binds the surviving pane in the store's workspace session. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: no repository rows; the fixture binds the session methods main's layout reads. - const runtime = new OrcaRuntimeService({ getRepos: () => [] } as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore({ getRepos: () => [] })) runtime.attachWindow(1) const register = async () => { registerBoundPty(runtime, 'pty-existing', null) @@ -440,14 +443,15 @@ describe('session tab workspace path aliases', () => { ] } } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only repository and saved-session reads are reached; no process controller or filesystem scan is installed. - const runtime = new OrcaRuntimeService({ - getRepo: (id: string) => (id === repo.id ? repo : undefined), - getRepos: () => [repo], - getWorkspaceSessionHostIds: () => ['local', 'ssh:ssh-host'], - getWorkspaceSession: (hostId = 'local') => - hostId === persistedHostId ? session : getDefaultWorkspaceSession() - } as never) + const runtime = new OrcaRuntimeService( + partialRuntimeStore({ + getRepo: (id: string) => (id === repo.id ? repo : undefined), + getRepos: () => [repo], + getWorkspaceSessionHostIds: () => ['local', 'ssh:ssh-host'], + getWorkspaceSession: (hostId = 'local') => + hostId === persistedHostId ? session : getDefaultWorkspaceSession() + }) + ) const listed = await runtime.listMobileSessionTabs(`id:${ALIAS}`) @@ -469,9 +473,10 @@ describe('session tab workspace path aliases', () => { }) it('refuses alias adoption of an unattributed remote PTY for a local repository', async () => { - const repo = { id: 'repo', path: 'G:\\git\\example' } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture only needs the local repository row. - const runtime = new OrcaRuntimeService({ getRepo: () => repo, getRepos: () => [repo] } as never) + const repo = storeRepo({ id: 'repo', path: 'G:\\git\\example' }) + const runtime = new OrcaRuntimeService( + partialRuntimeStore({ getRepo: () => repo, getRepos: () => [repo] }) + ) runtime.attachWindow(1) publishGraph(runtime, WORKTREE, [], toRemoteRuntimePtyId('term_1')) await expect(runtime.listMobileSessionTabs(`id:${ALIAS}`)).rejects.toThrow( diff --git a/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts b/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts index 51546084761..99b0106e6b6 100644 --- a/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts +++ b/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts @@ -20,6 +20,7 @@ vi.mock('electron', () => ({ })) import { OrcaRuntimeService } from './orca-runtime' +import { partialRuntimeStore } from './runtime-store-partial-fixture' describe('execution host structured launch settings wiring', () => { it('reads saved Command and Arguments from the host settings and rereads changes', async () => { @@ -28,8 +29,7 @@ describe('execution host structured launch settings wiring', () => { agentCmdOverrides: { claude: `"${process.execPath}"`, codex: `"${process.execPath}"` }, agentDefaultArgs: { claude: '--model "model one"', codex: '-c model_reasoning_effort=high' } } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked installer only reads the store's getSettings method. - const runtime = new OrcaRuntimeService({ getSettings: () => settings } as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore({ getSettings: () => settings })) await runtime.ensureStructuredAgentSessionHost() const deps = installed.mock.calls[0]?.[0] expect(deps).toBeDefined() @@ -72,8 +72,7 @@ describe('execution host structured launch settings wiring', () => { const settings: Partial = { agentCmdOverrides: { claude: 'custom-claude', codex: 'custom-codex' } } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked installer only reads the store's getSettings method. - const runtime = new OrcaRuntimeService({ getSettings: () => settings } as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore({ getSettings: () => settings })) await runtime.ensureStructuredAgentSessionHost() const deps = installed.mock.calls[0]?.[0] const childLookup = { pathEnv: bin, homePath: bin } diff --git a/src/main/runtime/orca-runtime-terminal-agent-phone-push.test.ts b/src/main/runtime/orca-runtime-terminal-agent-phone-push.test.ts index 722da1a4fba..118914c7809 100644 --- a/src/main/runtime/orca-runtime-terminal-agent-phone-push.test.ts +++ b/src/main/runtime/orca-runtime-terminal-agent-phone-push.test.ts @@ -2,10 +2,18 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { AGENT_DONE_QUIET_MS } from '../../shared/agent-transitions' import { HEADLESS_RUNTIME_WINDOW_ID } from '../../shared/runtime-session-contracts' +import { getDefaultNotificationSettings } from '../../shared/notification-settings-defaults' import { AgentHookServer } from '../agent-hooks/server' import { OrcaRuntimeService } from './orca-runtime' import type { MobileNotificationEvent } from './runtime-mobile-notification-controller' -import type { RuntimeStore } from './runtime-store-contract' +import { + partialRuntimeStore, + storeFolderWorkspace, + storeProjectGroup, + storeRepo, + storeWorktreeMeta, + type PartialRuntimeStore +} from './runtime-store-partial-fixture' // Switched on, as it ships next: that build's host reads hookless agents' titles itself. vi.mock('../../shared/sidebar-inputs-switch', () => ({ ORCA_SIDEBAR_INPUTS_IS_MAIN: true })) @@ -14,18 +22,22 @@ const WT = 'repo-1::/tmp/worktree-a' const PANE = 'tab-1:11111111-1111-4111-8111-111111111111' function createHost(windowId: number | null = HEADLESS_RUNTIME_WINDOW_ID) { - const store = { + const store: PartialRuntimeStore = { getRepos: () => [], getRepo: (id: string) => - id === 'repo-1' ? { id, connectionId: null, displayName: 'orca' } : undefined, - getSettings: () => ({ notifications: { enabled: true, agentTaskComplete: true } }), - getWorktreeMeta: (id: string) => (id === WT ? { displayName: 'feature' } : undefined), + id === 'repo-1' + ? storeRepo({ id, path: '/repo-1', connectionId: null, displayName: 'orca' }) + : undefined, + getSettings: () => ({ notifications: getDefaultNotificationSettings() }), + getWorktreeMeta: (id: string) => + id === WT ? storeWorktreeMeta({ displayName: 'feature' }) : undefined, getAllWorktreeMeta: () => ({}), - getFolderWorkspaces: () => [{ id: 'f1', name: 'Notes', projectGroupId: 'g1' }], - getProjectGroups: () => [{ id: 'g1', name: 'Research' }] + getFolderWorkspaces: () => [ + storeFolderWorkspace({ id: 'f1', name: 'Notes', projectGroupId: 'g1', folderPath: '/notes' }) + ], + getProjectGroups: () => [storeProjectGroup({ id: 'g1', name: 'Research' })] } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the push path reads only these members. - const runtime = new OrcaRuntimeService(store as unknown as RuntimeStore) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) if (windowId !== null) { runtime.syncWindowGraph(windowId, { tabs: [], leaves: [] }) } diff --git a/src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts b/src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts index c9fc4de8530..daddee5a30e 100644 --- a/src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts +++ b/src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts @@ -3,6 +3,7 @@ import { OrcaRuntimeService } from './orca-runtime' import { makePaneKey } from '../../shared/stable-pane-id' import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch' import { registerBoundPty } from './bind-first-pane.test-fixture' +import { partialRuntimeStore } from './runtime-store-partial-fixture' const PTY_ID = 'ssh:target@@relay-pty' const WORKTREE_ID = 'repo::/worktree' @@ -16,9 +17,8 @@ function makeRuntime(opts: { paneLayout?: boolean } = {}): { } { const writes: string[] = [] // Switch-on, main's layout answers which pane holds a terminal; that layout lives in a store. - const store = opts.paneLayout && ORCA_LAYOUT_OWNER_IS_MAIN ? {} : null - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolve path reads only the session bindPaneFirst adds to this store. - const runtime = new OrcaRuntimeService(store as never) + const store = opts.paneLayout && ORCA_LAYOUT_OWNER_IS_MAIN ? partialRuntimeStore() : null + const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ write: (_ptyId, data) => { writes.push(data) @@ -79,7 +79,6 @@ describe('runtime terminal handle incarnation fencing', () => { }) expect(runtime.markRendererReloading(1)).not.toBeNull() // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. expect((runtime as unknown as { handles: Map }).handles.has(handle)).toBe( false ) @@ -103,8 +102,6 @@ describe('runtime terminal handle incarnation fencing', () => { }) }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. it('treats a null-to-known incarnation as the same un-fenced PTY', async () => { const { runtime } = makeRuntime() const handle = runtime.preAllocateHandleForPty(PTY_ID) @@ -147,8 +144,6 @@ describe('runtime terminal handle incarnation fencing', () => { const [listed] = (await runtime.listTerminals()).terminals // Rotate the record directly so reconcile is the only fence exercised. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: test reaches the runtime's protected pty record map to bypass the registerPty fence. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. const internals = runtime as unknown as { ptysById: Map @@ -237,7 +232,6 @@ describe('runtime terminal handle incarnation fencing', () => { runtime.registerPreAllocatedHandleForPty(PTY_ID, replacementHandle) syncGraph(runtime) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. const internals = runtime as unknown as { handles: Map diff --git a/src/main/runtime/orca-runtime-terminal-rename-retention.test.ts b/src/main/runtime/orca-runtime-terminal-rename-retention.test.ts index 9501c74fb84..9c65ce1df73 100644 --- a/src/main/runtime/orca-runtime-terminal-rename-retention.test.ts +++ b/src/main/runtime/orca-runtime-terminal-rename-retention.test.ts @@ -1,5 +1,4 @@ import './orca-runtime-test-lifecycle.spec' -import type { RuntimeStore } from './runtime-store-contract' import { getDefaultWorkspaceSession } from '../../shared/constants' import { describe, expect, it, vi } from 'vitest' import { createMobileCreateTestNotifier } from './orca-runtime-test-scenario-builders.spec' @@ -16,6 +15,7 @@ import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch' import { bindPaneFirst, committingSpawn } from './bind-first-pane.test-fixture' import { buildRuntimeMobileAgentStatus } from './runtime-mobile-agent-status-builder' import type { RuntimePtyWorktreeRecord } from './runtime-terminal-state-records' +import { partialRuntimeStore } from './runtime-store-partial-fixture' describe('terminal rename before renderer graph hydration', () => { it.each(['Media Engine Orch', null])( @@ -24,8 +24,7 @@ describe('terminal rename before renderer graph hydration', () => { const session = makeWorkspaceSessionWithHeadlessTerminal() session.tabsByWorktree[TEST_WORKTREE_ID][0].customTitle = 'Previous name' const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(session) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The shared fixture supplies RuntimeStore methods; its legacy Mock return type loses callable signatures. - const checkedStore = runtimeStore as RuntimeStore + const checkedStore = runtimeStore const runtime = new OrcaRuntimeService(checkedStore) const renameTerminal = vi.fn() runtime.setPtyController({ @@ -77,8 +76,7 @@ describe('terminal rename before renderer graph hydration', () => { it('does not recreate a closed persisted tab from a surviving PTY record', async () => { const session = makeWorkspaceSessionWithHeadlessTerminal() const { runtimeStore, getSession, setSession } = makeRuntimeStoreWithWorkspaceSession(session) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Shared fixture implements RuntimeStore; its legacy Mock typing loses callable signatures. - const runtime = new OrcaRuntimeService(runtimeStore as RuntimeStore) + const runtime = new OrcaRuntimeService(runtimeStore) const notifier = createMobileCreateTestNotifier(vi.fn()) runtime.setNotifier(notifier) runtime.setPtyController({ @@ -216,8 +214,9 @@ describe('terminal view rename characterization', () => { } it('keeps rename persistence before the current notifier with its own receiver', async () => { // Switch-on, main's layout says which tab holds the terminal; that layout lives in a store. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the rename path reads only the session bindPaneFirst adds to this store. - const runtime = new OrcaRuntimeService(ORCA_LAYOUT_OWNER_IS_MAIN ? ({} as never) : undefined) + const runtime = new OrcaRuntimeService( + ORCA_LAYOUT_OWNER_IS_MAIN ? partialRuntimeStore() : undefined + ) bindPaneFirst(runtime, { worktreeId: 'folder-workspace', tabId: 'host-tab', @@ -345,8 +344,7 @@ describe('first-turn identity under recorded title clocks (#32030)', () => { const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Shared fixture implements RuntimeStore; its legacy Mock typing loses callable signatures. - return new OrcaRuntimeService(runtimeStore as RuntimeStore) + return new OrcaRuntimeService(runtimeStore) } it('keeps the first clock of a shell title that inventory replays after the hook', () => { diff --git a/src/main/runtime/orca-runtime-test-lifecycle.spec.ts b/src/main/runtime/orca-runtime-test-lifecycle.spec.ts index dbda9598482..4a1dc806b04 100644 --- a/src/main/runtime/orca-runtime-test-lifecycle.spec.ts +++ b/src/main/runtime/orca-runtime-test-lifecycle.spec.ts @@ -64,8 +64,7 @@ const { updateGitHubIssueMock } = mocks const { updateGitHubPRDetailsMock, updateGitHubPRStateMock, updateGitHubPRTitleMock } = mocks const { updateGitLabIssueMock, updateGitLabMRMock, updateGitLabMRReviewersMock, vi } = mocks -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the prototype carries the removal method the wrapper replaces. -awaitBackgroundRemovalsInRuntimeTests(mocks.OrcaRuntimeService.prototype as never) +awaitBackgroundRemovalsInRuntimeTests(mocks.OrcaRuntimeService.prototype) commitFakeSpawnsInRuntimeTests(mocks.OrcaRuntimeService.prototype) function resetRuntimeTestMocks(): void { diff --git a/src/main/runtime/orca-runtime-tests/headless-pane-title-terminal-list.spec.ts b/src/main/runtime/orca-runtime-tests/headless-pane-title-terminal-list.spec.ts index a1d972e7616..564359ad135 100644 --- a/src/main/runtime/orca-runtime-tests/headless-pane-title-terminal-list.spec.ts +++ b/src/main/runtime/orca-runtime-tests/headless-pane-title-terminal-list.spec.ts @@ -12,8 +12,7 @@ async function headlessRuntimeWithLivePane() { const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements every method these headless paths call; it is the shared runtime spec fixture. - const runtime = new OrcaRuntimeService(runtimeStore as never) + const runtime = new OrcaRuntimeService(runtimeStore) runtime.setPtyController({ spawn: vi.fn(async () => ({ id: 'pane-title-pty' })), write: () => true, diff --git a/src/main/runtime/orca-runtime-tests/headless-terminal-rename-publication.spec.ts b/src/main/runtime/orca-runtime-tests/headless-terminal-rename-publication.spec.ts index 72cfa933b87..95cbcbfeeae 100644 --- a/src/main/runtime/orca-runtime-tests/headless-terminal-rename-publication.spec.ts +++ b/src/main/runtime/orca-runtime-tests/headless-terminal-rename-publication.spec.ts @@ -13,8 +13,7 @@ function headlessRuntime() { const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements every method these headless paths call; it is the shared runtime spec fixture. - return { runtime: new OrcaRuntimeService(runtimeStore as never), getSession } + return { runtime: new OrcaRuntimeService(runtimeStore), getSession } } describe('headless terminal rename publication', () => { diff --git a/src/main/runtime/orca-runtime-tests/lineage-and-scan-cache-part-02.spec.ts b/src/main/runtime/orca-runtime-tests/lineage-and-scan-cache-part-02.spec.ts index 643080b98da..cbf00d42d2a 100644 --- a/src/main/runtime/orca-runtime-tests/lineage-and-scan-cache-part-02.spec.ts +++ b/src/main/runtime/orca-runtime-tests/lineage-and-scan-cache-part-02.spec.ts @@ -630,7 +630,6 @@ describe('OrcaRuntimeService', () => { createdAt: 1 } } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial store mock; the code under test reads only the members it defines. const runtime = new OrcaRuntimeService({ ...store, getRepos: () => [folderRepo], @@ -642,7 +641,7 @@ describe('OrcaRuntimeService', () => { return metaById[worktreeId] }, getAllWorktreeLineage: () => lineageById - } as never) + }) const result = await runtime.listDetectedManagedWorktrees(`id:${folderRepo.id}`) diff --git a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-02.spec.ts b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-02.spec.ts index 02ed94d0b50..9dfda85fd7c 100644 --- a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-02.spec.ts +++ b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-02.spec.ts @@ -147,14 +147,13 @@ describe('OrcaRuntimeService', () => { } ) const worktreesChanged = vi.fn() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial store mock; the code under test reads only the members it defines. const runtime = new OrcaRuntimeService({ ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], getWorktreeMetaForHost: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta - } as never) + }) runtime.setNotifier({ worktreesChanged, reposChanged: vi.fn(), @@ -203,14 +202,13 @@ describe('OrcaRuntimeService', () => { return metaById[worktreeId] } ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial store mock; the code under test reads only the members it defines. const runtime = new OrcaRuntimeService({ ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], getWorktreeMetaForHost: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta - } as never) + }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), diff --git a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-09.spec.ts b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-09.spec.ts index 4cd6d55c64a..bb7d9c0f7bd 100644 --- a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-09.spec.ts +++ b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-09.spec.ts @@ -21,6 +21,7 @@ import { makeWorkspaceSessionWithHeadlessTerminal, store } from '../orca-runtime-test-fixtures.spec' +import type { RuntimeStore } from '../runtime-store-contract' describe('OrcaRuntimeService', () => { describe('deliberately parked pane activation (STA-3465)', () => { @@ -52,7 +53,7 @@ describe('OrcaRuntimeService', () => { } function makeParkedRuntime( - runtimeStore: unknown, + runtimeStore: RuntimeStore, host: { deps?: ConstructorParameters[2] livePtyIds?: string[] @@ -62,8 +63,7 @@ describe('OrcaRuntimeService', () => { spawn: ReturnType } { const spawn = vi.fn().mockResolvedValue({ id: 'persisted-pty' }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements the runtime store methods these tests reach. - const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, host.deps) + const runtime = new OrcaRuntimeService(runtimeStore, undefined, host.deps) runtime.setPtyController({ spawn, write: () => true, diff --git a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-targeted-agent-resume.spec.ts b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-targeted-agent-resume.spec.ts index e5973a5e39e..7c4b3c90c66 100644 --- a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-targeted-agent-resume.spec.ts +++ b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-targeted-agent-resume.spec.ts @@ -85,8 +85,7 @@ function makeRuntime( const spawn = vi.fn<(args: unknown) => Promise<{ id: string }>>( opts.spawn ?? (() => Promise.resolve({ id: 'resumed-pty' })) ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements the runtime store methods these tests reach. - const runtime = new OrcaRuntimeService(fixture.runtimeStore as never, undefined, { + const runtime = new OrcaRuntimeService(fixture.runtimeStore, undefined, { getDesktopWindowStatus: () => 'openable', ...(opts.rows ? { getAgentProviderSessionSnapshot: opts.rows } : {}), ...(opts.checkHookAgentPresence ? { checkHookAgentPresence: opts.checkHookAgentPresence } : {}), @@ -106,10 +105,10 @@ function makeRuntime( const attachWindow = (webContents?: { send: (...args: unknown[]) => void }): void => { runtime.attachWindow(TEST_WINDOW_ID) runtime.markGraphReady(TEST_WINDOW_ID) - electronMocks.BrowserWindow.fromId.mockReturnValue( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the runtime reads only isDestroyed and webContents.send from the window mock. - { isDestroyed: () => false, ...(webContents ? { webContents } : {}) } as never - ) + electronMocks.BrowserWindow.fromId.mockReturnValue({ + isDestroyed: () => false, + ...(webContents ? { webContents } : {}) + }) } const resume = ( overrides: { diff --git a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-windowless-sleep-resume.spec.ts b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-windowless-sleep-resume.spec.ts index 10925887aad..89c15823aad 100644 --- a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-windowless-sleep-resume.spec.ts +++ b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-windowless-sleep-resume.spec.ts @@ -10,6 +10,7 @@ import { makeRuntimeStoreWithWorkspaceSession, makeWorkspaceSessionWithHeadlessTerminal } from '../orca-runtime-test-fixtures.spec' +import type { RuntimeStore } from '../runtime-store-contract' const PANE_KEY = `host-tab:${HEADLESS_LEAF_ID}` // The host's own pane-owned capture form (#11598), which no wake resumes a second time. @@ -55,13 +56,12 @@ function withClaudeLaunchedTab(fixture: ReturnType } function openPane( - runtimeStore: unknown, + runtimeStore: RuntimeStore, windowStatus: 'blocked' | 'openable', opts: { spawn?: () => Promise<{ id: string }>; withWindow?: boolean; tap?: boolean } = {} ) { const spawn = vi.fn(opts.spawn ?? (() => Promise.resolve({ id: 'persisted-pty' }))) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements the runtime store methods these tests reach. - const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, { + const runtime = new OrcaRuntimeService(runtimeStore, undefined, { getDesktopWindowStatus: () => windowStatus }) runtime.setPtyController({ @@ -77,8 +77,7 @@ function openPane( if (opts.withWindow) { runtime.attachWindow(TEST_WINDOW_ID) runtime.markGraphReady(TEST_WINDOW_ID) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the runtime reads only isDestroyed from the window mock. - electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false } as never) + electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false }) // A paired desktop client opening the worktree hydrates the host's tabs without a wake. await runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`, { notifyClients: false, diff --git a/src/main/runtime/orca-runtime-tests/mobile-session-tabs.spec.ts b/src/main/runtime/orca-runtime-tests/mobile-session-tabs.spec.ts index ba0d1c36678..7792bcfdf79 100644 --- a/src/main/runtime/orca-runtime-tests/mobile-session-tabs.spec.ts +++ b/src/main/runtime/orca-runtime-tests/mobile-session-tabs.spec.ts @@ -548,14 +548,13 @@ describe('OrcaRuntimeService', () => { ) const activateWorktree = vi.fn() const worktreesChanged = vi.fn() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial store mock; the code under test reads only the members it defines. const runtime = new OrcaRuntimeService({ ...store, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (worktreeId: string) => metaById[worktreeId], getWorktreeMetaForHost: (worktreeId: string) => metaById[worktreeId], setWorktreeMeta - } as never) + }) runtime.setNotifier({ worktreesChanged, reposChanged: vi.fn(), @@ -591,7 +590,6 @@ describe('OrcaRuntimeService', () => { } const activateWorktree = vi.fn() const resumeSleepingAgents = vi.fn() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial store mock; the code under test reads only the members it defines. const runtime = new OrcaRuntimeService({ ...store, getAllWorktreeMeta: () => metaById, @@ -614,7 +612,7 @@ describe('OrcaRuntimeService', () => { } } }) - } as never) + }) runtime.setNotifier({ worktreesChanged: vi.fn(), reposChanged: vi.fn(), diff --git a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-04.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-04.spec.ts index 3123720a9af..7781b5cb954 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-04.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-04.spec.ts @@ -18,7 +18,6 @@ import { spawnedPtyEnv, store } from '../orca-runtime-test-fixtures.spec' -import type { RuntimeStore } from '../runtime-store-contract' import { wireHeadlessServeRuntime } from '../orca-runtime-test-scenario-builders.spec' describe('OrcaRuntimeService', () => { @@ -716,8 +715,7 @@ function titledHeadlessRuntime() { activeRepoId: TEST_REPO_ID, activeWorktreeId: TEST_WORKTREE_ID }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The shared fixture supplies RuntimeStore methods; its legacy Mock return type loses callable signatures. - const runtime = new OrcaRuntimeService(runtimeStore as RuntimeStore) + const runtime = new OrcaRuntimeService(runtimeStore) runtime.setPtyController({ spawn: vi.fn(async () => ({ id: 'titled-pty' })), write: () => true, diff --git a/src/main/runtime/orca-runtime-worktree-change-invalidation.test.ts b/src/main/runtime/orca-runtime-worktree-change-invalidation.test.ts index 50115ecb99d..b8bdf549e10 100644 --- a/src/main/runtime/orca-runtime-worktree-change-invalidation.test.ts +++ b/src/main/runtime/orca-runtime-worktree-change-invalidation.test.ts @@ -33,6 +33,7 @@ vi.mock('./repo-worktree-admin-fingerprint', () => ({ import { OrcaRuntimeService } from './orca-runtime' import { runWorktreeChangeInvalidators } from '../ipc/worktree-change-invalidators' import { getLocalWorktreeScanGeneration } from '../local-worktree-scan-generation' +import { partialRuntimeStore } from './runtime-store-partial-fixture' const REPO_ID = 'repo-local' const REPO_PATH = '/Users/me/dev/app' @@ -74,8 +75,7 @@ function makeStore() { } function makeRuntimeService(): OrcaRuntimeService { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every store member the local listing reaches is supplied above. - return new OrcaRuntimeService(makeStore() as never) + return new OrcaRuntimeService(partialRuntimeStore(makeStore())) } function makeRuntime(): () => Promise { diff --git a/src/main/runtime/orcad-migration-catalog-import.test.ts b/src/main/runtime/orcad-migration-catalog-import.test.ts index 4559220a2bc..231ee46e9c5 100644 --- a/src/main/runtime/orcad-migration-catalog-import.test.ts +++ b/src/main/runtime/orcad-migration-catalog-import.test.ts @@ -2,9 +2,9 @@ import { describe, expect, it, vi } from 'vitest' import type { OrcadMigrationCatalogAbortResult, OrcadMigrationCatalogReferenceAbortResult, - OrcadMigrationCatalogState, - OrcadMigrationManifest + OrcadMigrationCatalogState } from '../../shared/orcad-migration/orcad-migration-manifest' +import { orcadMigrationCutoverFixture } from '../ssh/orcad-migration-cutover-fixture' import { abortStagedOrcadMigrationCatalogByReferenceDurably, abortStagedOrcadMigrationCatalogDurably, @@ -12,11 +12,17 @@ import { stageOrcadMigrationCatalogDurably } from './orcad-migration-catalog-import' +const { manifest } = orcadMigrationCutoverFixture() +const reference = { migrationId: manifest.migrationId, manifestSha256: manifest.manifestSha256 } + describe('durable orcad migration catalog import', () => { it('does not acknowledge staging until its dormant manifest is durable', async () => { const diskError = new Error('disk full') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the durable wrappers only pass these values through to the stubbed store. - const staged = { state: 'staged' } as OrcadMigrationCatalogState + const staged: OrcadMigrationCatalogState = { + state: 'staged', + ...reference, + stagedAt: '2026-10-01T00:00:01.000Z' + } const stage = vi.fn().mockReturnValue(staged) const flush = vi.fn().mockRejectedValueOnce(diskError).mockResolvedValueOnce(undefined) const args = { @@ -24,8 +30,7 @@ describe('durable orcad migration catalog import', () => { stageOrcadMigrationCatalog: stage, flushPendingOrThrowAsync: flush }, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the durable wrappers only pass these values through to the stubbed store. - manifest: {} as OrcadMigrationManifest + manifest } await expect(stageOrcadMigrationCatalogDurably(args)).rejects.toBe(diskError) @@ -36,8 +41,19 @@ describe('durable orcad migration catalog import', () => { it('publishes a committed catalog only after its receipt is durable', async () => { const diskError = new Error('disk full') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the durable wrappers only pass these values through to the stubbed store. - const committed = { state: 'committed' } as OrcadMigrationCatalogState + const committed: OrcadMigrationCatalogState = { + state: 'committed', + ...reference, + receipt: { + version: manifest.version, + ...reference, + source: manifest.source, + importedAt: '2026-10-01T00:00:02.000Z', + repositoryIds: [], + projectGroupIds: [], + folderWorkspaceIds: [] + } + } const commit = vi.fn().mockReturnValue(committed) const flush = vi.fn().mockRejectedValueOnce(diskError).mockResolvedValueOnce(undefined) const onDurableCommit = vi.fn() @@ -46,8 +62,7 @@ describe('durable orcad migration catalog import', () => { commitStagedOrcadMigrationCatalog: commit, flushPendingOrThrowAsync: flush }, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the durable wrappers only pass these values through to the stubbed store. - manifest: {} as OrcadMigrationManifest, + manifest, onDurableCommit } @@ -58,10 +73,12 @@ describe('durable orcad migration catalog import', () => { }) it('reflushes an already-absent abort after the first flush failed', async () => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the durable wrappers only pass these values through to the stubbed store. - const aborted = { state: 'absent', aborted: true } as OrcadMigrationCatalogAbortResult - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the durable wrappers only pass these values through to the stubbed store. - const unchanged = { state: 'absent', aborted: false } as OrcadMigrationCatalogAbortResult + const aborted: OrcadMigrationCatalogAbortResult = { + state: 'absent', + ...reference, + aborted: true + } + const unchanged: OrcadMigrationCatalogAbortResult = { ...aborted, aborted: false } const abort = vi.fn().mockReturnValueOnce(aborted).mockReturnValueOnce(unchanged) const flush = vi.fn().mockRejectedValueOnce(new Error('disk full')).mockResolvedValue(undefined) const args = { @@ -69,8 +86,7 @@ describe('durable orcad migration catalog import', () => { abortStagedOrcadMigrationCatalog: abort, flushPendingOrThrowAsync: flush }, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the durable wrappers only pass these values through to the stubbed store. - manifest: {} as OrcadMigrationManifest + manifest } await expect(abortStagedOrcadMigrationCatalogDurably(args)).rejects.toThrow('disk full') @@ -82,7 +98,6 @@ describe('durable orcad migration catalog import', () => { }) it('answers an abort by reference absent only once its flush succeeds', async () => { - const reference = { migrationId: 'migration-1', manifestSha256: 'a'.repeat(64) } const aborted: OrcadMigrationCatalogReferenceAbortResult = { ...reference, state: 'absent', diff --git a/src/main/runtime/orchestration/structured-session-mail-target.test.ts b/src/main/runtime/orchestration/structured-session-mail-target.test.ts index 826c5869b57..9f581f386ac 100644 --- a/src/main/runtime/orchestration/structured-session-mail-target.test.ts +++ b/src/main/runtime/orchestration/structured-session-mail-target.test.ts @@ -79,12 +79,11 @@ function installStore(record: AgentSessionRecord | null, visible = true): Store let db: InstanceType function probe(extra: Record = {}): MailTargetProbe { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a prototype-only probe; every field the methods read is assigned below. return Object.assign(Object.create(MailTargetProbe.prototype), { _orchestrationDb: db, ptysById: new Map(), ...extra - }) as MailTargetProbe + }) } function chatCoordinatedRun(): string { diff --git a/src/main/runtime/paired-client-navigation-test-harness.ts b/src/main/runtime/paired-client-navigation-test-harness.ts index 1f361178057..144381146cb 100644 --- a/src/main/runtime/paired-client-navigation-test-harness.ts +++ b/src/main/runtime/paired-client-navigation-test-harness.ts @@ -3,6 +3,9 @@ import WebSocket from 'ws' import { parsePairingCode } from '../../shared/pairing' import type { RuntimeMobileSessionTabsResult } from '../../shared/runtime-types' import type { OrcaRuntimeService } from './orca-runtime' +import type { Repo } from '../../shared/repo-types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' +import type { HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity' import { decrypt, deriveSharedKey, encrypt, generateKeyPair } from '../../shared/e2ee-crypto' export const REPO_ID = 'repo-1' @@ -28,7 +31,7 @@ export type ResponseReader = { } export function makeStore() { - const worktreeMeta = Object.fromEntries( + const worktreeMeta: Record = Object.fromEntries( [ HOST_WORKTREE_ID, CLIENT_A_WORKTREE_ID, @@ -52,7 +55,7 @@ export function makeStore() { } ]) ) - const repos = [ + const repos: Repo[] = [ { id: REPO_ID, path: '/tmp/repo', @@ -74,13 +77,13 @@ export function makeStore() { getRepo: (id: string) => repos.find((repo) => repo.id === id), getRepos: () => repos, addRepo: () => {}, - updateRepo: () => undefined as never, + updateRepo: () => null, getAllWorktreeMeta: () => worktreeMeta, getWorktreeMeta: (id: string) => worktreeMeta[id], - setWorktreeMeta: (id: string, patch: Record) => { - const next = { ...worktreeMeta[id], ...patch } - worktreeMeta[id] = next as (typeof worktreeMeta)[string] - return next as never + setWorktreeMeta: ({ id }: HostWorktreeTarget, patch: Partial) => { + const next = { ...worktreeMeta[id]!, ...patch } + worktreeMeta[id] = next + return next }, removeWorktreeMeta: () => {}, getSettings: () => ({ diff --git a/src/main/runtime/paired-close-retirement-proof-publication-order.spec.ts b/src/main/runtime/paired-close-retirement-proof-publication-order.spec.ts index f4ed3168dc7..1111a9b265f 100644 --- a/src/main/runtime/paired-close-retirement-proof-publication-order.spec.ts +++ b/src/main/runtime/paired-close-retirement-proof-publication-order.spec.ts @@ -93,7 +93,6 @@ export function registerPairedCloseRetirementProofPublicationOrderTests(): void retirePersistedSurface: () => Promise } { let session = makePersistedSession() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the store stub carries the four members this publication-order suite drives; the rest of Store is unreached. const runtime = new OrcaRuntimeService( // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This runtime fixture supplies the persistence and graph methods exercised by the test. withDurableRuntimeStore({ diff --git a/src/main/runtime/remote-desktop-headless-park.test.ts b/src/main/runtime/remote-desktop-headless-park.test.ts index ec6d497f1c2..c0575f21c5d 100644 --- a/src/main/runtime/remote-desktop-headless-park.test.ts +++ b/src/main/runtime/remote-desktop-headless-park.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' import { publishHeadlessRuntimeGraph } from './headless-runtime-graph' +import { partialRuntimeStore } from './runtime-store-partial-fixture' // A parked paired pane closes its stream, the PTY's last viewer. On a windowless host the PTY must // keep that viewer's grid, while other viewers and phones can still take it (#15997). @@ -15,10 +16,8 @@ function createRuntime({ headless }: { headless: boolean }) { getWorktreeMeta: () => undefined, getSettings: () => ({ workspaceDir: '/tmp/workspaces', mobileAutoRestoreFitMs: 5_000 }) } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub covers the settings and repo reads the viewer floor reaches; the rest of Store is unreached. - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) const sizes = new Map([[PTY, { ...SPAWN_GRID }]]) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: resize and getSize are the only controller members the layout queue drives here. runtime.setPtyController({ write: () => true, kill: () => true, @@ -28,7 +27,7 @@ function createRuntime({ headless }: { headless: boolean }) { return true }, getSize: (ptyId: string) => sizes.get(ptyId) ?? null - } as never) + }) if (headless) { publishHeadlessRuntimeGraph(runtime) } else { diff --git a/src/main/runtime/rpc/methods/agent-launch-settings-work.test.ts b/src/main/runtime/rpc/methods/agent-launch-settings-work.test.ts index d5f911c31e3..40f7b3e31a1 100644 --- a/src/main/runtime/rpc/methods/agent-launch-settings-work.test.ts +++ b/src/main/runtime/rpc/methods/agent-launch-settings-work.test.ts @@ -17,6 +17,7 @@ import { expectSettingsWorkScale, createGlobalSettingsFixture } from '../../../../shared/global-settings-test-fixture' +import { partialRuntimeStore } from '../../runtime-store-partial-fixture' vi.mock('../../../codex/managed-wsl-home-shell-preflight', () => ({ prepareManagedWslCodexHomeBeforeShellLaunch: async ({ @@ -56,8 +57,7 @@ it.each([20, 200])('projects %s repos without launch work', async (n) => { updateSettings: (updates) => Object.assign(settings, updates), runDurableMutation: async (mutate) => mutate().value } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these settings and offline-label paths use only the supplied store methods. - const host = new OrcaRuntimeService(store as unknown as RuntimeStore) + const host = new OrcaRuntimeService(partialRuntimeStore(store)) vi.spyOn(host, 'configureNotificationDismissalStore').mockImplementation(() => {}) vi.spyOn(host.pairedServerRows!, 'attach').mockReturnValue(() => {}) // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: invoking captured label callbacks; only relay retirement subscribes, no transport is used. diff --git a/src/main/runtime/rpc/methods/agent-launch.test.ts b/src/main/runtime/rpc/methods/agent-launch.test.ts index 71ff6665115..32a41b91eb7 100644 --- a/src/main/runtime/rpc/methods/agent-launch.test.ts +++ b/src/main/runtime/rpc/methods/agent-launch.test.ts @@ -649,8 +649,7 @@ describe('launch inputs that cross the wire', () => { if (!options) { throw new Error('createTerminal was not called') } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub records whatever options the method passed; each assertion below checks a field before reading it. - return options as Record + return options } it('carries agentArgs and cwd through to the terminal create', async () => { diff --git a/src/main/runtime/rpc/methods/automatic-work.test.ts b/src/main/runtime/rpc/methods/automatic-work.test.ts index 6a3c37cf1c6..09f345aee38 100644 --- a/src/main/runtime/rpc/methods/automatic-work.test.ts +++ b/src/main/runtime/rpc/methods/automatic-work.test.ts @@ -7,8 +7,7 @@ import type { RpcContext } from '../core' import type { RuntimePtyController } from '../../runtime-pty-controller-contract' function context(runtime: OrcaRuntimeService): RpcContext { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these handlers read only `runtime` from the dispatch context. - return { runtime } as unknown as RpcContext + return { runtime } } function method(name: string) { diff --git a/src/main/runtime/rpc/methods/client-ui.ts b/src/main/runtime/rpc/methods/client-ui.ts index 8792a3f6e2b..dd762308f48 100644 --- a/src/main/runtime/rpc/methods/client-ui.ts +++ b/src/main/runtime/rpc/methods/client-ui.ts @@ -10,6 +10,7 @@ import { type PairedUiState } from '../../../../shared/pairing-local-ui-fields' import type { PersistedUIState } from '../../../../shared/persisted-ui-state-types' +import { isWorkspaceHostScope } from '../../../../shared/orcad-migration/orcad-migration-client-state-value-validation' import { defineMethod, type RpcContext } from '../core' import { isHostOwnPhone } from '../host-own-phone' import { @@ -171,9 +172,12 @@ export const CLIENT_UI_METHODS = [ isHostOwnPhone(context) && params.hideWorkspacesFromOtherDevices !== undefined ? { hideWorkspacesFromOtherDevices: params.hideWorkspacesFromOtherDevices } : {} - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: UiUpdate is checked key-for-key against PersistedUIState (ui-state-schema-parity-checks.ts); only optionality differs. + // Why: the wire schema keeps workspaceHostScope a plain string for older clients; drop unknown scopes. + const { workspaceHostScope, ...uiUpdates } = omitPairingLocalUiFields(params) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: keys match PersistedUIState (ui-state-schema-parity-checks.ts); wider wire values such as host-id lists are not narrowed here. const updates = { - ...omitPairingLocalUiFields(params), + ...uiUpdates, + ...(isWorkspaceHostScope(workspaceHostScope) ? { workspaceHostScope } : {}), ...ownPhoneFilter } as Partial return { ui: uiForCaller(context.runtime.updateUIState(updates), context) } diff --git a/src/main/runtime/rpc/methods/orchestration-structured-worker-start-deadline.test.ts b/src/main/runtime/rpc/methods/orchestration-structured-worker-start-deadline.test.ts index 7948feb4930..58735fdd90d 100644 --- a/src/main/runtime/rpc/methods/orchestration-structured-worker-start-deadline.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-structured-worker-start-deadline.test.ts @@ -88,8 +88,7 @@ it('leaves the preamble only what the setup gate did not use of the caller’s t } const receipt = await startLocalWorker({ - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fields this start reads; the rest are optional. - params: { from: 'term_c', timeoutMs: 300_000, agent: 'claude' } as never, + params: { from: 'term_c', timeoutMs: 300_000, agent: 'claude' }, mode: { mode: 'structured', preferred: 'structured', diff --git a/src/main/runtime/rpc/methods/orchestration-worker-release-incarnation-fallback.test.ts b/src/main/runtime/rpc/methods/orchestration-worker-release-incarnation-fallback.test.ts index e839c2e017d..0b0fce5d1e5 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-release-incarnation-fallback.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-release-incarnation-fallback.test.ts @@ -1,9 +1,14 @@ import { afterEach, describe, expect, it, vi, type MockInstance } from 'vitest' +import { z } from 'zod' import { ORCHESTRATION_METHODS } from './orchestration' import { eraseRpcMethods, type RpcContext } from '../core' import { OrchestrationDb } from '../../orchestration/db' import { OrcaRuntimeService } from '../../orca-runtime' import { completeWorkerTerminalRelease } from './orchestration/worker/worker-release-completion' +import type { RuntimeTerminalShow } from '../../../../shared/runtime-terminal-contracts' +import type { OrchestrationCompatibilityTerminalAuthority } from '../../runtime-terminal-contracts' + +const workerStartResult = z.object({ dispatchId: z.string(), state: z.string() }) describe('orchestration worker release incarnation fallback', () => { let db: OrchestrationDb @@ -18,6 +23,38 @@ describe('orchestration worker release incarnation fallback', () => { const coordinatorPaneKey = 'tab_coord:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' const workerPaneKey = 'tab_worker:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb' + function workerTerminal(handle: string): RuntimeTerminalShow { + return { + handle, + ptyId: 'pty_worker', + paneRuntimeId: 1, + rendererGraphEpoch: 1, + worktreeId: 'repo::worktree', + worktreePath: '/repo/worktree', + branch: 'worktree', + tabId: 'tab-worker', + leafId: 'leaf-worker', + title: 'worker', + connected: true, + writable: true, + lastOutputAt: null, + preview: '' + } + } + + function workerAuthority(handle: string): OrchestrationCompatibilityTerminalAuthority { + return { + runtimeId: 'runtime_test', + terminalHandle: handle, + ptyId: 'pty_worker', + worktreeId: 'repo::worktree', + paneKey: workerPaneKey, + processIncarnation: 'runtime_test:term_worker:1', + launchTokenHash: null, + hostScope: { kind: 'local', hostId: 'local' } + } + } + /** Fresh in-memory db and a fully-stubbed runtime for one worker-release scenario. */ function setup(): void { db = new OrchestrationDb(':memory:') @@ -38,29 +75,13 @@ describe('orchestration worker release incarnation fallback', () => { handle === 'term_worker' || handle === 'term_reminted' ? 'runtime_test:term_worker:1' : null ) vi.spyOn(runtime, 'getOrchestrationDispatchAuthority').mockImplementation((handle) => - handle === 'term_worker' || handle === 'term_reminted' - ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This test fixture is deliberately shaped to exercise the private/runtime boundary. - ({ - terminalHandle: handle, - paneKey: workerPaneKey, - processIncarnation: 'runtime_test:term_worker:1', - hostScope: { kind: 'local', hostId: 'local' } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - } as never) - : null + handle === 'term_worker' || handle === 'term_reminted' ? workerAuthority(handle) : null ) vi.spyOn(runtime, 'validateOrchestrationAgentLauncher').mockImplementation(() => {}) - vi.spyOn(runtime, 'showTerminal').mockImplementation( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - async (handle) => ({ handle, worktreeId: 'repo::worktree', status: 'running' }) as never - ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. + vi.spyOn(runtime, 'showTerminal').mockImplementation(async (handle) => workerTerminal(handle)) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: worker start reads only the managed worktree's id. vi.spyOn(runtime, 'showManagedTerminalWorkspace').mockResolvedValue({ id: 'repo::worktree' - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. } as never) vi.spyOn(runtime, 'createTerminal').mockResolvedValue({ handle: 'term_worker', @@ -88,17 +109,12 @@ describe('orchestration worker release incarnation fallback', () => { tail: ['worker output line 1', 'worker output line 2'], truncated: false, nextCursor: '2' - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. vi.spyOn(runtime, 'closeTerminal').mockResolvedValue({ handle: 'term_worker', tabId: 'tab-worker', ptyKilled: true - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - } as never) + }) vi.spyOn(runtime, 'notifyMessageArrived').mockImplementation(() => {}) activeRunId = db.createRun({ objective: 'Release test Run', @@ -137,24 +153,18 @@ describe('orchestration worker release incarnation fallback', () => { taskId: string dispatchId: string }> { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. const task = db.createTask({ spec: 'release fixture task', runId: activeRunId }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const result = (await call('orchestration.workerStart', { - task: task.id, - from: 'term_coord', - ...(options.terminal ? { terminal: options.terminal } : { agent: 'codex' }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - })) as { dispatchId: string; state: string } + const result = workerStartResult.parse( + await call('orchestration.workerStart', { + task: task.id, + from: 'term_coord', + ...(options.terminal ? { terminal: options.terminal } : { agent: 'codex' }) + }) + ) expect(result.state).toBe('ready') return { taskId: task.id, dispatchId: result.dispatchId } } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. /** Record the worker's report as settled — the precondition for release. */ function settle(taskId: string, dispatchId: string, outcome: 'succeeded' | 'failed'): void { const settlement = db.settleWorkerReport({ @@ -183,10 +193,7 @@ describe('orchestration worker release incarnation fallback', () => { vi.mocked(runtime.showTerminal).mockImplementation(async (handle) => handle === 'term_worker' ? Promise.reject(new Error('terminal_handle_stale')) - : // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This test fixture is deliberately shaped to exercise the private/runtime boundary. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ({ handle, worktreeId: 'repo::worktree', status: 'running' } as never) + : workerTerminal(handle) ) // ...but the recorded process incarnation still names a live PTY, re-minted to a fresh handle. const resolveByIncarnation = vi.fn().mockReturnValue('term_reminted') @@ -194,12 +201,7 @@ describe('orchestration worker release incarnation fallback', () => { resolveByIncarnation ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerRelease', { dispatch: dispatchId })) as { - state: string - processAction: string - } + const receipt = await call('orchestration.workerRelease', { dispatch: dispatchId }) expect(receipt).toMatchObject({ state: 'released', processAction: 'closed_agent_terminal' }) expect(resolveByIncarnation).toHaveBeenCalledWith( @@ -225,13 +227,9 @@ describe('orchestration worker release incarnation fallback', () => { resolveByIncarnation ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerRelease', { dispatch: dispatchId })) as { - state: string - } + const receipt = await call('orchestration.workerRelease', { dispatch: dispatchId }) - expect(receipt.state).toBe('release_unknown') + expect(receipt).toMatchObject({ state: 'release_unknown' }) expect(resolveByIncarnation).toHaveBeenCalledWith( 'runtime_test:term_worker:1', JSON.stringify({ kind: 'local', hostId: 'local' }) @@ -250,12 +248,7 @@ describe('orchestration worker release incarnation fallback', () => { ) inspectProcessLiveness.mockResolvedValue('exited') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerRelease', { dispatch: dispatchId })) as { - state: string - processAction: string - } + const receipt = await call('orchestration.workerRelease', { dispatch: dispatchId }) expect(receipt).toMatchObject({ state: 'release_unknown', processAction: 'none' }) expect(inspectProcessLiveness).toHaveBeenCalled() @@ -310,8 +303,6 @@ describe('orchestration worker release incarnation fallback', () => { setup() const { dispatchId } = await startSettledWorker() vi.mocked(runtime.showTerminal).mockRejectedValue(new Error('terminal_handle_stale')) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. const resolveByIncarnation = vi.fn().mockReturnValue(null) vi.spyOn(runtime, 'resolveTerminalHandleByProcessIncarnation').mockImplementation( resolveByIncarnation @@ -319,21 +310,15 @@ describe('orchestration worker release incarnation fallback', () => { vi.mocked(runtime.getOrchestrationDispatchAuthority).mockReturnValue(null) inspectProcessLiveness.mockResolvedValue('exited') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerRelease', { dispatch: dispatchId })) as { - state: string - processAction: string - } + const receipt = await call('orchestration.workerRelease', { dispatch: dispatchId }) expect(receipt).toMatchObject({ state: 'release_unknown', processAction: 'none' }) - expect(receipt.state).not.toBe('retained') + expect(receipt).not.toMatchObject({ state: 'retained' }) expect(inspectProcessLiveness).toHaveBeenCalled() expect(runtime.closeTerminal).not.toHaveBeenCalled() }) it('concedes release_unknown before the lease check when a gone worker has no live authority and liveness is unproven', async () => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. setup() const { dispatchId } = await startSettledWorker() vi.mocked(runtime.showTerminal).mockRejectedValue(new Error('terminal_handle_stale')) @@ -346,14 +331,10 @@ describe('orchestration worker release incarnation fallback', () => { // than retain or guess at a live process. inspectProcessLiveness.mockResolvedValue('live') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerRelease', { dispatch: dispatchId })) as { - state: string - } + const receipt = await call('orchestration.workerRelease', { dispatch: dispatchId }) - expect(receipt.state).toBe('release_unknown') - expect(receipt.state).not.toBe('retained') + expect(receipt).toMatchObject({ state: 'release_unknown' }) + expect(receipt).not.toMatchObject({ state: 'retained' }) expect(runtime.closeTerminal).not.toHaveBeenCalled() expect(db.getWorkerTerminalResourceByOwner(dispatchId)?.release_state).toBe('unknown') }) @@ -365,24 +346,16 @@ describe('orchestration worker release incarnation fallback', () => { vi.mocked(runtime.showTerminal).mockImplementation(async (handle) => handle === 'term_worker' ? Promise.reject(new Error('terminal_handle_stale')) - : // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This test fixture is deliberately shaped to exercise the private/runtime boundary. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ({ handle, worktreeId: 'repo::worktree', status: 'running' } as never) + : workerTerminal(handle) ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. const resolveByIncarnation = vi.fn().mockReturnValue('term_reminted') vi.spyOn(runtime, 'resolveTerminalHandleByProcessIncarnation').mockImplementation( resolveByIncarnation ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerStop', { dispatch: dispatchId })) as { - processAction: string - } + const receipt = await call('orchestration.workerStop', { dispatch: dispatchId }) - expect(receipt.processAction).toBe('closed_agent_terminal') + expect(receipt).toMatchObject({ processAction: 'closed_agent_terminal' }) // The kill targeted exactly the reminted live handle, never the stale durable one — closing // the stale handle would throw terminal_handle_stale and leak the PTY. expect(runtime.closeTerminal).toHaveBeenCalledTimes(1) @@ -392,28 +365,17 @@ describe('orchestration worker release incarnation fallback', () => { it('workerRead reads a live worker via the reminted handle when the durable handle is stale', async () => { setup() const { dispatchId } = await startWorker() - vi.mocked(runtime.showTerminal).mockImplementation( - async (handle) => - handle === 'term_worker' - ? Promise.reject(new Error('terminal_handle_stale')) - : // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This test fixture is deliberately shaped to exercise the private/runtime boundary. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ({ handle, worktreeId: 'repo::worktree', status: 'running' } as never) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. + vi.mocked(runtime.showTerminal).mockImplementation(async (handle) => + handle === 'term_worker' + ? Promise.reject(new Error('terminal_handle_stale')) + : workerTerminal(handle) ) const resolveByIncarnation = vi.fn().mockReturnValue('term_reminted') vi.spyOn(runtime, 'resolveTerminalHandleByProcessIncarnation').mockImplementation( resolveByIncarnation ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const output = (await call('orchestration.workerRead', { dispatch: dispatchId })) as { - terminal?: { tail: string[] } - } + const output = await call('orchestration.workerRead', { dispatch: dispatchId }) // Both the exact-session probe and the terminal read addressed the reminted handle. expect(runtime.getExactWorkerProviderSession).toHaveBeenCalledWith( @@ -421,11 +383,11 @@ describe('orchestration worker release incarnation fallback', () => { expect.any(Number) ) expect(runtime.readTerminal).toHaveBeenCalledWith('term_reminted', expect.anything()) - expect(output.terminal?.tail).toEqual(['worker output line 1', 'worker output line 2']) + expect(output).toMatchObject({ + terminal: { tail: ['worker output line 1', 'worker output line 2'] } + }) }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. it('recovery-mode: defers when exited missing has no archive rather than plain-settling', async () => { // Proof of death still runs settleDead first; when it retains (no archive), recovery must // stay release_pending — not plain-settle, not unknown. @@ -459,14 +421,9 @@ describe('orchestration worker release incarnation fallback', () => { }) it('recovery-mode: settles released before the defer when exited missing already has an archive', async () => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. setup() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. const { dispatchId } = await startSettledWorker() vi.mocked(runtime.showTerminal).mockRejectedValue(new Error('terminal_handle_stale')) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. const resolveByIncarnation = vi.fn().mockReturnValue(null) vi.spyOn(runtime, 'resolveTerminalHandleByProcessIncarnation').mockImplementation( resolveByIncarnation @@ -501,13 +458,10 @@ describe('orchestration worker release incarnation fallback', () => { }) }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. it('recovery-mode: defers release_pending when liveness is unverifiable rather than provably exited', async () => { setup() const { dispatchId } = await startSettledWorker() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. vi.mocked(runtime.showTerminal).mockRejectedValue(new Error('terminal_handle_stale')) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. const resolveByIncarnation = vi.fn().mockReturnValue(null) vi.spyOn(runtime, 'resolveTerminalHandleByProcessIncarnation').mockImplementation( resolveByIncarnation @@ -527,7 +481,7 @@ describe('orchestration worker release incarnation fallback', () => { mode: 'recovery' }) - expect(receipt.state).toBe('release_pending') + expect(receipt).toMatchObject({ state: 'release_pending' }) expect(runtime.closeTerminal).not.toHaveBeenCalled() expect(db.getWorkerTerminalResourceByOwner(dispatchId)?.release_state).not.toBe('released') }) diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federation-agent-death-liveness.test.ts b/src/main/runtime/rpc/methods/orchestration/federation/federation-agent-death-liveness.test.ts index cac33666362..9ea9e7b1e99 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federation-agent-death-liveness.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federation-agent-death-liveness.test.ts @@ -80,11 +80,10 @@ describe('federation host agent-death liveness', () => { name === 'orchestration.federationFleetSnapshot' ? { dispatchIds: [DISPATCH_ID] } : { dispatchId: DISPATCH_ID } - return method.handler( - method.params!.parse(params), - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these handlers read only the runtime and caller. - { runtime, authenticatedCallerFingerprint: HOME_FINGERPRINT } as never - ) + return method.handler(method.params!.parse(params), { + runtime, + authenticatedCallerFingerprint: HOME_FINGERPRINT + }) } async function snapshotObservation() { diff --git a/src/main/runtime/rpc/methods/publish-capability-settings.test.ts b/src/main/runtime/rpc/methods/publish-capability-settings.test.ts index bb4593b1b11..a17d912a2ce 100644 --- a/src/main/runtime/rpc/methods/publish-capability-settings.test.ts +++ b/src/main/runtime/rpc/methods/publish-capability-settings.test.ts @@ -3,6 +3,7 @@ import { getDefaultSettings } from '../../../../shared/constants' import { SettingsUpdate } from '../../../../shared/rpc-contract/client-settings-params' import { OrcaRuntimeService } from '../../orca-runtime' import type { RuntimeStore } from '../../runtime-store-contract' +import { partialRuntimeStore } from '../../runtime-store-partial-fixture' vi.mock('electron', () => ({ app: { getPath: () => '/orca-state', isPackaged: true } @@ -22,8 +23,7 @@ function runtimeWithSharing( return settings } } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: construction and getClientSettings require only getSettings from this fixture. - return new OrcaRuntimeService(store as RuntimeStore) + return new OrcaRuntimeService(partialRuntimeStore(store)) } // A client may read the host's publish permissions, but cannot grant them over RPC. diff --git a/src/main/runtime/rpc/methods/skills-bundled-orca.test.ts b/src/main/runtime/rpc/methods/skills-bundled-orca.test.ts index 8a8ff3ec815..a61743d8163 100644 --- a/src/main/runtime/rpc/methods/skills-bundled-orca.test.ts +++ b/src/main/runtime/rpc/methods/skills-bundled-orca.test.ts @@ -40,8 +40,7 @@ function method(name: string) { async function call(name: string, params: unknown, context: RpcContext): Promise { const entry = method(name) const parsed = entry.params ? entry.params.parse(params) : undefined - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: `parsed` came from this method's own params schema, which is what the erased handler accepts. - const result = await entry.handler(parsed as never, context) + const result = await entry.handler(parsed, context) // Why JSON: the result crosses the wire, so the client only ever sees its serialised form. const wire: T = JSON.parse(JSON.stringify(result)) return wire diff --git a/src/main/runtime/rpc/methods/status-machine-name-publish-budget.test.ts b/src/main/runtime/rpc/methods/status-machine-name-publish-budget.test.ts index 268c928c0c8..c8eae48b00f 100644 --- a/src/main/runtime/rpc/methods/status-machine-name-publish-budget.test.ts +++ b/src/main/runtime/rpc/methods/status-machine-name-publish-budget.test.ts @@ -3,8 +3,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { normalizeMachineName } from '../../../../shared/machine-name' import { OrcaRuntimeService } from '../../orca-runtime' import { MACHINE_NAME_PUBLISH_WAIT_MS } from '../../runtime-machine-name' -import type { RuntimeStore } from '../../runtime-store-contract' import { STATUS_METHODS } from './status' +import { partialRuntimeStore } from '../../runtime-store-partial-fixture' // Why its own file: the lookup memo is process-wide, and `status.test.ts` already lands it; this // case needs a process whose first lookup is still running when the publish budget runs out. @@ -38,8 +38,7 @@ describe('status.get publish budget', () => { finishLookup = resolve }) ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the runtime reads only `getSettings` off the store during construction and status. - const runtime = new OrcaRuntimeService({ getSettings: () => ({}) } as RuntimeStore) + const runtime = new OrcaRuntimeService(partialRuntimeStore({ getSettings: () => ({}) })) expect(finishLookup).toBeTypeOf('function') const hostname = normalizeMachineName(os.hostname()) diff --git a/src/main/runtime/rpc/methods/status.test.ts b/src/main/runtime/rpc/methods/status.test.ts index 7b15975c3bc..8772eba089d 100644 --- a/src/main/runtime/rpc/methods/status.test.ts +++ b/src/main/runtime/rpc/methods/status.test.ts @@ -2,8 +2,8 @@ import os from 'node:os' import { afterEach, describe, expect, it, vi } from 'vitest' import { normalizeMachineName } from '../../../../shared/machine-name' import { OrcaRuntimeService } from '../../orca-runtime' -import type { RuntimeStore } from '../../runtime-store-contract' import { STATUS_METHODS } from './status' +import { partialRuntimeStore } from '../../runtime-store-partial-fixture' // Why mocked: the friendly-name lookup is the one path that spawns `scutil`; the test decides when it lands. const runProcessMock = vi.hoisted(() => vi.fn()) @@ -33,8 +33,7 @@ describe('status.get', () => { finishLookup = resolve }) ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the runtime reads only `getSettings` off the store during construction and status. - const runtime = new OrcaRuntimeService({ getSettings: () => ({}) } as RuntimeStore) + const runtime = new OrcaRuntimeService(partialRuntimeStore({ getSettings: () => ({}) })) expect(finishLookup).toBeTypeOf('function') expect(runtime.readMachineName()).toBe(normalizeMachineName(os.hostname())) diff --git a/src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts b/src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts index c8102bc7de6..440d681c5d6 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts @@ -56,8 +56,7 @@ async function call( const replies: RpcResponse[] = [] await dispatcher.dispatchStreaming( { id: 'request-1', authToken: 'token', method, params }, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the dispatcher writes serialized RpcResponse frames. - (raw) => replies.push(JSON.parse(raw) as RpcResponse), + (raw) => replies.push(JSON.parse(raw)), client ) if (!replies[0]) { diff --git a/src/main/runtime/rpc/methods/structured-agent-session-unsubscribe.test.ts b/src/main/runtime/rpc/methods/structured-agent-session-unsubscribe.test.ts index 22d4d7c6174..1eace644087 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-unsubscribe.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-unsubscribe.test.ts @@ -35,8 +35,7 @@ function subscriberCount(): number { async function stream(id: string, frames: RpcResponse[]): Promise { await dispatcher.dispatchStreaming( { id, authToken: 'token', method: 'agentSession.subscribe', params: { sessionId: SESSION } }, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the dispatcher writes RpcResponse JSON. - (raw) => frames.push(JSON.parse(raw) as RpcResponse), + (raw) => frames.push(JSON.parse(raw)), CLIENT ) } @@ -76,8 +75,7 @@ it('drops the subscriber a client unsubscribes, and keeps its sibling (U-02)', a method: 'agentSession.unsubscribe', params: { sessionId: SESSION, subscriptionId: 'frame-a' } }, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the dispatcher writes RpcResponse JSON. - (raw) => replies.push(JSON.parse(raw) as RpcResponse), + (raw) => replies.push(JSON.parse(raw)), CLIENT ) expect(replies[0]).toMatchObject({ ok: true }) diff --git a/src/main/runtime/rpc/methods/workspace-layout-apply.test.ts b/src/main/runtime/rpc/methods/workspace-layout-apply.test.ts index 2c4859b17d0..82acdfbe47e 100644 --- a/src/main/runtime/rpc/methods/workspace-layout-apply.test.ts +++ b/src/main/runtime/rpc/methods/workspace-layout-apply.test.ts @@ -1,10 +1,15 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' +import type { ExecutionHostId } from '../../../../shared/execution-host' +import type { LayoutApplyOutcome } from '../../../../shared/workspace-layout/workspace-layout-command-types' +import type { Tab } from '../../../../shared/tab-types' +import type { RuntimeStore } from '../../runtime-store-contract' import { RUNTIME_CAPABILITIES } from '../../../../shared/protocol-version' import { MOBILE_RPC_METHOD_ALLOWLIST } from '../../runtime-rpc/runtime-rpc-mobile-method-allowlist' import { OrcaRuntimeService } from '../../orca-runtime' import { eraseRpcMethods, isStreamingMethod, type RpcContext } from '../core' import { LAYOUT_APPLY_METHODS } from './workspace-layout' +import { partialRuntimeStore, type PartialRuntimeStore } from '../../runtime-store-partial-fixture' const frozen = vi.hoisted(() => ({ workspaces: new Set() })) vi.mock( @@ -56,19 +61,25 @@ function sessionListing(tabId: string): WorkspaceSessionState { } } -function runtimeHolding(hostId: string) { - const applyLayoutCommand = vi.fn(async () => ({ ok: true, result: { leafId: 'minted' } })) +function runtimeHolding(hostId: ExecutionHostId) { + const applyLayoutCommand = vi.fn(async (): Promise => ({ + ok: true, + result: { leafId: 'minted' } + })) const applyTabRenames = vi.fn(async () => []) const empty: WorkspaceSessionState = { ...sessionListing('other'), tabsByWorktree: {} } - const store = { + const store: PartialRuntimeStore = { getRepo: () => undefined, getWorkspaceSessionHostIds: () => ['local', hostId], getWorkspaceSession: (host: string) => (host === hostId ? sessionListing('tab-1') : empty), applyLayoutCommand, applyTabRenames } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the apply path reads only these store methods. - return { runtime: new OrcaRuntimeService(store as never), applyLayoutCommand, applyTabRenames } + return { + runtime: new OrcaRuntimeService(partialRuntimeStore(store)), + applyLayoutCommand, + applyTabRenames + } } afterEach(() => { @@ -660,11 +671,11 @@ describe('layout.apply', () => { }) it('gives each title in its tab’s partition in one write, naming a paired server’s tab', async () => { - const applyTabRenames = vi.fn(async (batches: { renames: { tabId: string }[] }[]) => [ + const applyTabRenames = vi.fn>(async (batches) => [ { rename: batches[0]!.renames[0]!, code: 'tab_not_found' } ]) const hosts: Record = { 'tab-1': 'local', 'tab-2': 'ssh:target-1' } - const store = { + const store: PartialRuntimeStore = { getRepo: () => undefined, getWorkspaceSessionHostIds: () => ['local', 'ssh:target-1', 'runtime:env-1'], getWorkspaceSession: (host: string) => ({ @@ -673,13 +684,23 @@ describe('layout.apply', () => { unifiedTabs: { [WORKTREE]: Object.entries({ ...hosts, 'tab-3': 'runtime:env-1' }) .filter(([, owner]) => owner === host) - .map(([id]) => ({ id, entityId: id, contentType: 'terminal' })) + .map(([id]): Tab => ({ + id, + entityId: id, + groupId: 'group-1', + worktreeId: WORKTREE, + contentType: 'terminal', + label: id, + customLabel: null, + color: null, + sortOrder: 0, + createdAt: 0 + })) } }), applyTabRenames } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the apply path reads only these store methods. - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) const titled = (tabId: string) => ({ workspace: WORKTREE, tabId, title: `Title ${tabId}` }) const reply = await runtime.applyLayoutCommand({ @@ -754,24 +775,23 @@ describe('layout.apply', () => { it('names the stream position of the frame its write produced, sent before the reply', async () => { let session = sessionListing('tab-1') - const written = new Set<(hostId: string | null) => void>() + const written = new Set<(hostId: ExecutionHostId | null) => void>() const order: string[] = [] - const store = { + const store: PartialRuntimeStore = { getRepo: () => undefined, getWorkspaceSessionHostIds: () => ['local'], getWorkspaceSession: () => session, - onWorkspaceSessionWritten: (listener: (hostId: string | null) => void) => { + onWorkspaceSessionWritten: (listener: (hostId: ExecutionHostId | null) => void) => { written.add(listener) return () => written.delete(listener) }, - applyLayoutCommand: vi.fn(async () => { + applyLayoutCommand: vi.fn(async (): Promise => { session = { ...session, activeTabId: null, tabsByWorktree: {} } written.forEach((listener) => listener('local')) return { ok: true, result: { leafId: 'minted' } } }) } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the apply and stream paths read only these store methods. - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) const sent: number[] = [] const { seq } = runtime.subscribeWorkspaceLayouts((event) => { order.push('frame') diff --git a/src/main/runtime/rpc/terminal-multiplex-flow-control.bench.test.ts b/src/main/runtime/rpc/terminal-multiplex-flow-control.bench.test.ts index ed6c94be67f..fbbed8858f0 100644 --- a/src/main/runtime/rpc/terminal-multiplex-flow-control.bench.test.ts +++ b/src/main/runtime/rpc/terminal-multiplex-flow-control.bench.test.ts @@ -188,7 +188,6 @@ describe.skipIf(!benchEnabled)('terminal multiplex parsed-credit benchmark', () } }) ) - // eslint-disable-next-line no-console -- opt-in benchmark evidence console.table(rows) }) }) diff --git a/src/main/runtime/runtime-client-settings-linear-team-projection.spec.ts b/src/main/runtime/runtime-client-settings-linear-team-projection.spec.ts index c26eecb3d95..5df4848db9a 100644 --- a/src/main/runtime/runtime-client-settings-linear-team-projection.spec.ts +++ b/src/main/runtime/runtime-client-settings-linear-team-projection.spec.ts @@ -11,11 +11,10 @@ function projectionOf(settings: Partial) { } function hostSettings(overrides: Record): Partial { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the override deliberately carries the malformed on-disk shape the projection must tolerate. return { ...createGlobalSettingsFixture({ workspaceDir: '/w' }), ...overrides - } as Partial + } } describe('RuntimeClientSettingsController Linear team selection projection', () => { diff --git a/src/main/runtime/runtime-desktop-pane-title-forwarding.test.ts b/src/main/runtime/runtime-desktop-pane-title-forwarding.test.ts index b36a762d14d..41a4c4f6c0f 100644 --- a/src/main/runtime/runtime-desktop-pane-title-forwarding.test.ts +++ b/src/main/runtime/runtime-desktop-pane-title-forwarding.test.ts @@ -21,13 +21,11 @@ const SINGLE_ROOT: TerminalPaneLayoutNode = { type: 'leaf', leafId: HEADLESS_LEA // #30386: a desktop host's window owns its layout, so a client's pane rename must reach the window. function desktopHostRuntime() { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: window routing only checks that the mocked desktop window is not destroyed. - electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false } as never) + electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false }) const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements every method these paths call; it is the shared runtime spec fixture. - const runtime = new OrcaRuntimeService(runtimeStore as never) + const runtime = new OrcaRuntimeService(runtimeStore) runtime.attachWindow(1) const setPaneTitle = vi.fn(() => true) const renameTerminal = vi.fn() diff --git a/src/main/runtime/runtime-extraction-regressions.test.ts b/src/main/runtime/runtime-extraction-regressions.test.ts index 50e88aea9e4..165926598ff 100644 --- a/src/main/runtime/runtime-extraction-regressions.test.ts +++ b/src/main/runtime/runtime-extraction-regressions.test.ts @@ -1,8 +1,8 @@ import { join } from 'node:path' import { describe, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' -import type { RuntimeStore } from './runtime-store-contract' import { SESSION_TABS_AUTHORITATIVE_INVENTORY_RUNTIME_CAPABILITY } from '../../shared/protocol-version' +import { partialRuntimeStore } from './runtime-store-partial-fixture' vi.mock('electron', () => ({ BrowserWindow: { fromId: vi.fn(() => null) }, @@ -52,8 +52,7 @@ describe('runtime extraction regressions', () => { it('reads a machine rename the same way status publishes it', () => { const settings = { machineName: 'Build server' } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this test exercises status with only the store surface the runtime reads during construction. - const runtime = new OrcaRuntimeService({ getSettings: () => settings } as RuntimeStore) + const runtime = new OrcaRuntimeService(partialRuntimeStore({ getSettings: () => settings })) expect(runtime.readMachineName()).toBe('Build server') expect(runtime.getStatus().hostPlatform).toBe(process.platform) settings.machineName = 'Renamed desk' diff --git a/src/main/runtime/runtime-local-create-rearm-ordering.test.ts b/src/main/runtime/runtime-local-create-rearm-ordering.test.ts index 8272a014d12..aaaa7bd2a87 100644 --- a/src/main/runtime/runtime-local-create-rearm-ordering.test.ts +++ b/src/main/runtime/runtime-local-create-rearm-ordering.test.ts @@ -41,6 +41,7 @@ vi.mock('../ipc/registered-worktree-roots-scoped-invalidation', () => ({ })) import { OrcaRuntimeService } from './orca-runtime' +import { partialRuntimeStore } from './runtime-store-partial-fixture' const repo = { id: 'repo-1', path: '/repo', displayName: 'Repo', badgeColor: 'blue', kind: 'git' } @@ -63,8 +64,7 @@ function makeRuntime(): OrcaRuntimeService { getSettings: () => ({ disabledTuiAgents: [], workspaceDir: '/worktrees' }), getProjectHostSetups: () => [] } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every store method this create path reaches is supplied above. - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the named members all exist on the service; the cast only exposes non-public ones to the spies. const internals = runtime as unknown as RuntimeInternals vi.spyOn(internals, 'resolveRepoSelector').mockResolvedValue(repo) diff --git a/src/main/runtime/runtime-remove-project-host-scope.spec.ts b/src/main/runtime/runtime-remove-project-host-scope.spec.ts index dbf002bd7a2..a9fe118704c 100644 --- a/src/main/runtime/runtime-remove-project-host-scope.spec.ts +++ b/src/main/runtime/runtime-remove-project-host-scope.spec.ts @@ -11,6 +11,7 @@ import { OrcaRuntimeService } from './orca-runtime' import { findRepoRowForHostScopedWrite } from '../persistence/tracking-repos/repo-update-operations' import { getRepoExecutionHostId } from '../../shared/execution-host' import type { RepoHostTarget } from '../../shared/repo-host-identity' +import { partialRuntimeStore } from './runtime-store-partial-fixture' export function registerRuntimeRemoveProjectHostScopeTests(): void { function makeRepos(): Repo[] { @@ -22,7 +23,7 @@ export function registerRuntimeRemoveProjectHostScopeTests(): void { badgeColor: '#000', addedAt: 1, executionHostId: 'local' - } as Repo, + }, { id: 'dup', path: '/remote/dup', @@ -30,7 +31,7 @@ export function registerRuntimeRemoveProjectHostScopeTests(): void { badgeColor: '#000', addedAt: 2, connectionId: 'ssh-1' - } as Repo + } ] } @@ -48,26 +49,34 @@ export function registerRuntimeRemoveProjectHostScopeTests(): void { } } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the store stub covers every read and write repo.rm and repo.update drive. - const runtime = new OrcaRuntimeService({ - getRepos: () => [...repos], - getRepo: (id: string) => repos.find((repo) => repo.id === id) ?? null, - getAllWorktreeMeta: () => ({}), - getWorktreeMeta: () => null, - setWorktreeMeta: vi.fn(), - removeWorktreeMeta: vi.fn(), - getGitHubCache: () => null, - // Same row lookup the real store's host-scoped write uses. - updateRepo: (target: RepoHostTarget, updates: Partial) => { - const row = findRepoRowForHostScopedWrite(repos, target.id, getRepoExecutionHostId(target)) - return row ? Object.assign(row, updates) : null - }, - moveProjectToGroup: (target: RepoHostTarget, groupId: string | null) => { - const row = findRepoRowForHostScopedWrite(repos, target.id, getRepoExecutionHostId(target)) - return row ? Object.assign(row, { projectGroupId: groupId }) : null - }, - removeProjectForHost - } as never) + const runtime = new OrcaRuntimeService( + partialRuntimeStore({ + getRepos: () => [...repos], + getRepo: (id: string) => repos.find((repo) => repo.id === id), + getAllWorktreeMeta: () => ({}), + getWorktreeMeta: () => undefined, + setWorktreeMeta: vi.fn(), + removeWorktreeMeta: vi.fn(), + // Same row lookup the real store's host-scoped write uses. + updateRepo: (target: RepoHostTarget, updates: Partial) => { + const row = findRepoRowForHostScopedWrite( + repos, + target.id, + getRepoExecutionHostId(target) + ) + return row ? Object.assign(row, updates) : null + }, + moveProjectToGroup: (target: RepoHostTarget, groupId: string | null) => { + const row = findRepoRowForHostScopedWrite( + repos, + target.id, + getRepoExecutionHostId(target) + ) + return row ? Object.assign(row, { projectGroupId: groupId }) : null + }, + removeProjectForHost + }) + ) return { runtime, repos, removeProjectForHost } } diff --git a/src/main/runtime/runtime-store-partial-fixture.ts b/src/main/runtime/runtime-store-partial-fixture.ts new file mode 100644 index 00000000000..04720fd8e05 --- /dev/null +++ b/src/main/runtime/runtime-store-partial-fixture.ts @@ -0,0 +1,117 @@ +import type { RuntimeStore } from './runtime-store-contract' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { Repo } from '../../shared/repo-types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' +import type { Project } from '../../shared/project-types' +import type { ProjectGroup } from '../../shared/project-group-types' +import type { FolderWorkspace } from '../../shared/folder-workspace-types' + +type RuntimeSettings = ReturnType + +export type PartialRuntimeStore = Partial> & { + // Any persisted setting may be stubbed: the store returns all of them, not just the runtime's. + getSettings?: () => Partial & + Partial> +} + +function unstubbedWrite(member: string): () => never { + return () => { + throw new Error(`runtime store fake does not stub ${member}`) + } +} + +/** A runtime store fake: empty reads and refused writes beyond the members a test supplies. */ +export function partialRuntimeStore(overrides: PartialRuntimeStore = {}): RuntimeStore { + const { getSettings, ...members } = overrides + return { + getRepos: () => [], + getRepo: () => undefined, + getAllWorktreeMeta: () => ({}), + getWorktreeMeta: () => undefined, + getGitHubCache: () => ({ pr: {}, issue: {} }), + addRepo: unstubbedWrite('addRepo'), + updateRepo: unstubbedWrite('updateRepo'), + setWorktreeMeta: unstubbedWrite('setWorktreeMeta'), + removeWorktreeMeta: unstubbedWrite('removeWorktreeMeta'), + ...members, + getSettings: () => ({ + workspaceDir: '', + nestWorkspaces: false, + refreshLocalBaseRefOnWorktreeCreate: false, + branchPrefix: 'none', + branchPrefixCustom: '', + ...getSettings?.() + }) + } +} + +/** A persisted repo row as the store returns it. */ +export function storeRepo(fields: Partial & Pick): Repo { + return { displayName: fields.id, badgeColor: '#737373', addedAt: 0, ...fields } +} + +/** A persisted worktree meta row as the store returns it. */ +export function storeWorktreeMeta(overrides: Partial = {}): WorktreeMeta { + return { + displayName: '', + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0, + ...overrides + } +} + +/** A persisted project row as the store returns it. */ +export function storeProject(fields: Partial & Pick): Project { + return { + displayName: fields.id, + badgeColor: '#737373', + sourceRepoIds: [], + createdAt: 0, + updatedAt: 0, + ...fields + } +} + +/** A persisted project group row as the store returns it. */ +export function storeProjectGroup( + fields: Partial & Pick +): ProjectGroup { + return { + name: fields.id, + parentPath: null, + parentGroupId: null, + createdFrom: 'manual', + tabOrder: 0, + isCollapsed: false, + color: null, + createdAt: 0, + updatedAt: 0, + ...fields + } +} + +/** A persisted folder workspace row as the store returns it. */ +export function storeFolderWorkspace( + fields: Partial & Pick +): FolderWorkspace { + return { + name: fields.id, + linkedTask: null, + comment: '', + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0, + createdAt: 0, + updatedAt: 0, + ...fields + } +} diff --git a/src/main/runtime/runtime-terminal-set-pane-title.test.ts b/src/main/runtime/runtime-terminal-set-pane-title.test.ts index bfa61d02b47..a14d6360831 100644 --- a/src/main/runtime/runtime-terminal-set-pane-title.test.ts +++ b/src/main/runtime/runtime-terminal-set-pane-title.test.ts @@ -7,8 +7,7 @@ import { RpcDispatcher } from './rpc/dispatcher' import { TERMINAL_QUERY_METHODS } from './rpc/methods/terminal/terminal-query-methods' async function createPaneTitleRuntime() { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: runtime title routing only checks that the mocked desktop window is not destroyed. - electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false } as never) + electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false }) const runtime = createRuntime() syncSinglePty(runtime) const setPaneTitle = vi.fn(() => true) diff --git a/src/main/runtime/runtime-worktree-create-repo-host.test.ts b/src/main/runtime/runtime-worktree-create-repo-host.test.ts index d8e3e584432..280f80d74fc 100644 --- a/src/main/runtime/runtime-worktree-create-repo-host.test.ts +++ b/src/main/runtime/runtime-worktree-create-repo-host.test.ts @@ -2,11 +2,12 @@ import './rpc/unused-default-rpc-methods.test-fixture' import { afterEach, describe, expect, it, vi } from 'vitest' import type { Repo } from '../../shared/repo-types' import { RpcDispatcher } from './rpc/dispatcher' -import type { RpcContext, RpcRequest } from './rpc/core' +import type { RpcRequest } from './rpc/core' import { REPO_METHODS } from './rpc/methods/repo' import { WORKTREE_METHODS } from './rpc/methods/worktree' import { agentLaunchWorkspaceFactory } from './rpc/methods/agent-launch-worktree-creation' import { OrcaRuntimeService } from './orca-runtime' +import { partialRuntimeStore } from './runtime-store-partial-fixture' // Why twins: a project copied between hosts keeps its repo id (Codex review 6 #6). const REPOS: Repo[] = [ @@ -31,16 +32,16 @@ const REPOS: Repo[] = [ ] function createRuntime(): OrcaRuntimeService { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these paths stop right after repo resolution, which reads only these store methods. - return new OrcaRuntimeService({ - getRepos: () => REPOS, - getRepo: (id: string) => REPOS.find((repo) => repo.id === id) ?? null, - getAllWorktreeMeta: () => ({}), - getWorktreeMeta: () => null, - setWorktreeMeta: vi.fn(), - getGitHubCache: () => null, - getSettings: () => ({}) - } as never) + return new OrcaRuntimeService( + partialRuntimeStore({ + getRepos: () => REPOS, + getRepo: (id: string) => REPOS.find((repo) => repo.id === id), + getAllWorktreeMeta: () => ({}), + getWorktreeMeta: () => undefined, + setWorktreeMeta: vi.fn(), + getSettings: () => ({}) + }) + ) } /** Stops every create at the step that receives the resolved repo, and records that repo. */ @@ -96,8 +97,7 @@ describe('a repo id set up on two hosts, named with repoHostId', () => { it('agent.launch create-worktree keeps the host', async () => { const reached = stopAtResolvedProject() const runtime = createRuntime() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the create reads only `runtime` and the creator-provenance fields, which default when absent. - const factory = agentLaunchWorkspaceFactory({ runtime } as unknown as RpcContext, 'claude') + const factory = agentLaunchWorkspaceFactory({ runtime }, 'claude') await expect( factory.createWorktree({ diff --git a/src/main/runtime/runtime-worktree-terminal-provisioning.test.ts b/src/main/runtime/runtime-worktree-terminal-provisioning.test.ts index 51202405031..280efa46173 100644 --- a/src/main/runtime/runtime-worktree-terminal-provisioning.test.ts +++ b/src/main/runtime/runtime-worktree-terminal-provisioning.test.ts @@ -28,19 +28,15 @@ function fakeHost() { } function provision(host: ReturnType, startup: boolean) { - return provisionWorktreeTerminals( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fake implements every host member provisioning reaches for default tabs with no setup. - host as unknown as WorktreeTerminalProvisioningHost, - { - worktreeSelector: 'id:wt-1', - worktreeId: 'wt-1', - worktreePath: '/worktrees/wt-1', - defaultTabs: DEFAULT_TABS, - primaryTerminalHandle: startup ? 'term_agent' : null, - hasStartupTerminal: startup, - setupCommandPlatform: 'posix' - } - ) + return provisionWorktreeTerminals(host, { + worktreeSelector: 'id:wt-1', + worktreeId: 'wt-1', + worktreePath: '/worktrees/wt-1', + defaultTabs: DEFAULT_TABS, + primaryTerminalHandle: startup ? 'term_agent' : null, + hasStartupTerminal: startup, + setupCommandPlatform: 'posix' + }) } describe('default tabs beside the agent a create started', () => { diff --git a/src/main/runtime/session-graph/session-window-graph.ts b/src/main/runtime/session-graph/session-window-graph.ts index 6cdcdf43a17..bad5bdeedd4 100644 --- a/src/main/runtime/session-graph/session-window-graph.ts +++ b/src/main/runtime/session-graph/session-window-graph.ts @@ -1,4 +1,3 @@ -/* eslint-disable unicorn/no-useless-spread */ import { OrcaRuntimeWithAttachWindow } from './session-graph-promotion' import type { RuntimeRendererSyncWindowGraph, @@ -350,7 +349,7 @@ export abstract class OrcaRuntimeWithSyncWindowGraph extends OrcaRuntimeWithAtta // Why: createTerminal waits for the renderer's graph sync to populate the // new leaf so it can return a handle. Drain callbacks after leaves update. - for (const cb of [...this.graphSyncCallbacks]) { + for (const cb of Array.from(this.graphSyncCallbacks)) { cb() } diff --git a/src/main/runtime/session-lifecycle/close/close-by-split-handle-layout-owner.test.ts b/src/main/runtime/session-lifecycle/close/close-by-split-handle-layout-owner.test.ts index 608a42f3022..7d1523d1d59 100644 --- a/src/main/runtime/session-lifecycle/close/close-by-split-handle-layout-owner.test.ts +++ b/src/main/runtime/session-lifecycle/close/close-by-split-handle-layout-owner.test.ts @@ -14,6 +14,7 @@ import { OrcaRuntimeService } from '../../orca-runtime' import type { ExecutionHostId } from '../../../../shared/execution-host' import type { LayoutCommand } from '../../../../shared/workspace-layout/workspace-layout-command-types' import { applyCommandToSession } from '../../../persistence/terminal-topology/terminal-pane-admission' +import { partialRuntimeStore } from '../../runtime-store-partial-fixture' vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true })) @@ -127,7 +128,7 @@ function setup(listing: WindowListing, pinned = false) { } } // The Store tells its write listeners after each write, as main's terminal list relies on. - const written = new Set<(hostId: string | null) => void>() + const written = new Set<(hostId: ExecutionHostId | null) => void>() const write = (next: WorkspaceSessionState): void => { session = next for (const listener of written) { @@ -146,8 +147,8 @@ function setup(listing: WindowListing, pinned = false) { getRepos: () => [repo], getRepo: (id: string) => (id === REPO_ID ? repo : undefined), getWorkspaceSession: () => session, - getWorkspaceSessionHostIds: () => ['local'], - onWorkspaceSessionWritten: (listener: (hostId: string | null) => void) => { + getWorkspaceSessionHostIds: (): ExecutionHostId[] => ['local'], + onWorkspaceSessionWritten: (listener: (hostId: ExecutionHostId | null) => void) => { written.add(listener) return () => written.delete(listener) }, @@ -162,12 +163,11 @@ function setup(listing: WindowListing, pinned = false) { } return change.value }, - persistPtyBinding: () => true, + persistPtyBinding: async () => true, flushOrThrow: () => undefined }) ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fake carries every store method a split and a tab close reach. - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) Object.assign(runtime, { resolveTerminalWorkspaceLaunchScope: vi.fn(async () => ({ id: WORKTREE_ID, diff --git a/src/main/runtime/session-lifecycle/projection/build-headless-mobile-session-browser-tabs.ts b/src/main/runtime/session-lifecycle/projection/build-headless-mobile-session-browser-tabs.ts index 1e2d923cafe..ebca24ad888 100644 --- a/src/main/runtime/session-lifecycle/projection/build-headless-mobile-session-browser-tabs.ts +++ b/src/main/runtime/session-lifecycle/projection/build-headless-mobile-session-browser-tabs.ts @@ -149,8 +149,7 @@ export abstract class OrcaRuntimeWithBuildHeadlessMobileSessionBrowserTabs exten worktreeId, target, hostIds(), - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The existing guard requires this getter; preserve late-bound store access. - store as Parameters[3], + { getWorkspaceSession: (hostId) => store.getWorkspaceSession!(hostId) }, fenced ), ownerMatches: () => !acknowledgeTabRetirement || acknowledgeTabRetirement().matches, diff --git a/src/main/runtime/session-tabs/main-layout-agent-launch-tabs.test.ts b/src/main/runtime/session-tabs/main-layout-agent-launch-tabs.test.ts index f61bf9aacde..f66772ba4a8 100644 --- a/src/main/runtime/session-tabs/main-layout-agent-launch-tabs.test.ts +++ b/src/main/runtime/session-tabs/main-layout-agent-launch-tabs.test.ts @@ -4,7 +4,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { WORKTREE_ID, setupMainOwnedWorkspace } from '../main-owned-workspace-runtime-fixture' import { publishAgentLaunchTabEarly } from '../rpc/methods/agent-launch-tab-publication' import { resetAgentLaunchPanesForTests } from '../../agent-launch/agent-launch-pane-attachment' -import type { RpcContext } from '../rpc/core' vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true })) @@ -18,8 +17,7 @@ function launchHost() { Object.assign(fixture.runtime, { showTerminalWorkspaceLaunchScope: async () => ({ id: WORKTREE_ID, path: '/workspace' }) }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the early tab reads only the runtime off its context. - const context = { runtime: fixture.runtime } as unknown as RpcContext + const context = { runtime: fixture.runtime } const launch = () => publishAgentLaunchTabEarly( { @@ -78,8 +76,7 @@ describe("an agent launch's early tab in a workspace main owns, with a window at // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the early tab reads only this notice. fixture.runtime.setNotifier({ publishAgentLaunchTab } as never) Object.assign(fixture.runtime, { getAvailableAuthoritativeWindow: () => ({}) }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the early tab reads only the runtime off its context. - const context = { runtime: fixture.runtime } as unknown as RpcContext + const context = { runtime: fixture.runtime } await publishAgentLaunchTabEarly( { diff --git a/src/main/runtime/session-tabs/mobile-session-reconciliation.ts b/src/main/runtime/session-tabs/mobile-session-reconciliation.ts index 66b3b21b710..28996fca53c 100644 --- a/src/main/runtime/session-tabs/mobile-session-reconciliation.ts +++ b/src/main/runtime/session-tabs/mobile-session-reconciliation.ts @@ -1,6 +1,5 @@ import type { OrcaRuntimeWithMergePreservedHeadlessMobileSessionTabs as MergePreservedHeadlessMobileSessionTabsMethods } from './mobile-session-preservation' import type { OrcaRuntimeWithStoredMobileSnapshotHasStalePreservedTab as StoredMobileSnapshotHasStalePreservedTabMethods } from './mobile-session-publication-preservation' -/* eslint-disable unicorn/no-useless-spread */ import { OrcaRuntimeWithWriteOrchestrationPointerPty } from '../orchestration/terminal-authority/write-orchestration-pointer-pty' import type { RuntimeMobileSessionTabsSnapshot } from '../../../shared/runtime-types' import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' @@ -215,7 +214,7 @@ export abstract class OrcaRuntimeWithSyncMobileSessionTabs extends OrcaRuntimeWi ) }) } - for (const [worktreeId, existing] of [...this.mobileSessionTabsByWorktree.entries()]) { + for (const [worktreeId, existing] of Array.from(this.mobileSessionTabsByWorktree)) { if (!nextWorktrees.has(worktreeId)) { const preserved = this.buildPreservedHeadlessMobileSessionSnapshot(existing) if (preserved) { diff --git a/src/main/runtime/sidebar-inputs/orca-runtime-project-catalog.test.ts b/src/main/runtime/sidebar-inputs/orca-runtime-project-catalog.test.ts index da7aa397611..0372771fc86 100644 --- a/src/main/runtime/sidebar-inputs/orca-runtime-project-catalog.test.ts +++ b/src/main/runtime/sidebar-inputs/orca-runtime-project-catalog.test.ts @@ -10,6 +10,7 @@ import { ALL_RPC_METHODS } from '../rpc/methods' import type { RuntimeStore } from '../runtime-store-contract' import { requireSwitchedOn } from './require-switched-on' import { TestPhone } from './sidebar-inputs-feed.test-fixture' +import { partialRuntimeStore } from '../runtime-store-partial-fixture' vi.mock('../../../shared/sidebar-inputs-switch', () => ({ ORCA_SIDEBAR_INPUTS_IS_MAIN: true })) @@ -36,8 +37,7 @@ function setup() { getRepos: () => repos, getProjectGroups: () => [group('a'), group('school')] } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the catalog reads only repos and groups; other domains fail soft. - const runtime = new OrcaRuntimeService(store as RuntimeStore) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) const events: RuntimeClientEvent[] = [] runtime.onClientEvent((event) => events.push(event)) const feed = requireSwitchedOn(runtime.sidebarInputsFeed) diff --git a/src/main/runtime/sidebar-inputs/orca-runtime-repo-icons.test.ts b/src/main/runtime/sidebar-inputs/orca-runtime-repo-icons.test.ts index be8b57ababa..ea62eefb51e 100644 --- a/src/main/runtime/sidebar-inputs/orca-runtime-repo-icons.test.ts +++ b/src/main/runtime/sidebar-inputs/orca-runtime-repo-icons.test.ts @@ -6,6 +6,7 @@ import type { RuntimeStore } from '../runtime-store-contract' import { MAX_REPO_ICON_IDS_PER_READ } from '../../../shared/sidebar-list/repo-icon-data-urls-contract' import { repoIconId } from './repo-icon-data-urls' import { pngHeader } from './repo-icon-image-fixture' +import { partialRuntimeStore } from '../runtime-store-partial-fixture' vi.mock('../../../shared/sidebar-inputs-switch', () => ({ ORCA_SIDEBAR_INPUTS_IS_MAIN: true })) @@ -29,8 +30,7 @@ describe('sidebarInputs.repoIcons', () => { const store: Pick = { getRepos: () => [{ ...repo, repoIcon: upload }] } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the read touches only repos. - const runtime = new OrcaRuntimeService(store as RuntimeStore) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) const { method, params } = repoIconsMethod() const id = repoIconId(upload) diff --git a/src/main/runtime/sidebar-inputs/orca-runtime-sort-stamps.test.ts b/src/main/runtime/sidebar-inputs/orca-runtime-sort-stamps.test.ts index 82b2b866928..6ec6b02fb68 100644 --- a/src/main/runtime/sidebar-inputs/orca-runtime-sort-stamps.test.ts +++ b/src/main/runtime/sidebar-inputs/orca-runtime-sort-stamps.test.ts @@ -4,11 +4,11 @@ import { HEADLESS_RUNTIME_WINDOW_ID } from '../../../shared/runtime-session-cont import type { WorktreeMeta } from '../../../shared/worktree/meta-types' import { AgentHookServer } from '../../agent-hooks/server' import { OrcaRuntimeService } from '../orca-runtime' -import type { RuntimeStore } from '../runtime-store-contract' import { hostWorktreeTarget, type HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity' +import { partialRuntimeStore, storeRepo, storeWorktreeMeta } from '../runtime-store-partial-fixture' vi.mock('../../../shared/sidebar-inputs-switch', () => ({ ORCA_SIDEBAR_INPUTS_IS_MAIN: true })) @@ -17,17 +17,13 @@ const WT = 'repo-1::/tmp/worktree-a' function createHeadlessRuntime( stored: Pick = { lastActivityAt: 0 } ) { - const meta: Record< - string, - Pick & { isUnread?: boolean } - > = { - [WT]: stored - } + const meta: Record = { [WT]: storeWorktreeMeta(stored) } const written = new Set<(domains: readonly string[] | undefined) => void>() + const repo = storeRepo({ id: 'repo-1', path: '/tmp/repo-1', connectionId: null }) const store = { // An unstamped row's writes resolve their host from its repo's only registration. - getRepos: () => [{ id: 'repo-1', connectionId: null }], - getRepo: (id: string) => (id === 'repo-1' ? { id, connectionId: null } : undefined), + getRepos: () => [repo], + getRepo: (id: string) => (id === repo.id ? repo : undefined), getSettings: () => ({}), getWorktreeMeta: (id: string) => meta[id], getAllWorktreeMeta: () => meta, @@ -41,8 +37,7 @@ function createHeadlessRuntime( return () => written.delete(listener) } } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stamp path reads only these members. - const runtime = new OrcaRuntimeService(store as unknown as RuntimeStore) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) // No window: a server with no client attached must still see its own bells and titles. runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [ @@ -137,7 +132,7 @@ describe('host sort stamps on a headless runtime', () => { expect(boundary).toMatchObject({ smartTier: 'done', smartAt: 76_000 }) // No client is looking: a finish is news, a clear or a compaction is not. expect(finishUnread).toBe(true) - expect(boundaryUnread).toBeUndefined() + expect(boundaryUnread).toBe(false) }) it('stamps agent transitions from the attached hook store', () => { @@ -153,7 +148,7 @@ describe('host sort stamps on a headless runtime', () => { payload: { state: 'waiting', prompt: 'task', agentType: 'claude' } }) // No client is looking, so the request for input also raises the unread flag. - expect(meta[WT]).toEqual({ + expect(meta[WT]).toMatchObject({ lastActivityAt: 70_000, smartTier: 'needs-you', smartAt: 70_000, @@ -168,14 +163,18 @@ describe('host sort stamps on a headless runtime', () => { const live = createHeadlessRuntime({ lastActivityAt: 5, smartTier: 'done', smartAt: 4 }) live.runtime.attachAgentStatusStore(new AgentHookServer()) await live.runtime.sidebarFacts?.reconcileSortStampTiers({ probePtyLiveness: async () => true }) - expect(live.meta[WT]).toEqual({ lastActivityAt: 5, smartTier: 'done', smartAt: 4 }) + expect(live.meta[WT]).toMatchObject({ lastActivityAt: 5, smartTier: 'done', smartAt: 4 }) const gone = createHeadlessRuntime({ lastActivityAt: 5, smartTier: 'done', smartAt: 4 }) gone.runtime.attachAgentStatusStore(new AgentHookServer()) await gone.runtime.sidebarFacts?.reconcileSortStampTiers({ probePtyLiveness: async () => false }) - expect(gone.meta[WT]).toEqual({ lastActivityAt: 5, smartTier: undefined, smartAt: undefined }) + expect(gone.meta[WT]).toMatchObject({ + lastActivityAt: 5, + smartTier: undefined, + smartAt: undefined + }) }) it('re-tiers a read Done beside a Working pane when any writer clears the flag', () => { diff --git a/src/main/runtime/sidebar-inputs/orca-runtime-terminal-use.test.ts b/src/main/runtime/sidebar-inputs/orca-runtime-terminal-use.test.ts index d6780999f9f..a17c3d58f73 100644 --- a/src/main/runtime/sidebar-inputs/orca-runtime-terminal-use.test.ts +++ b/src/main/runtime/sidebar-inputs/orca-runtime-terminal-use.test.ts @@ -8,9 +8,14 @@ import { isWorkspaceOldForCleanup } from '../../../shared/workspace-cleanup' import { OrcaRuntimeService } from '../orca-runtime' -import type { RuntimeStore } from '../runtime-store-contract' import { TERMINAL_USE_THROTTLE_MS } from './terminal-use-producer' import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity' +import { + partialRuntimeStore, + storeRepo, + storeWorktreeMeta, + type PartialRuntimeStore +} from '../runtime-store-partial-fixture' vi.mock('../../../shared/sidebar-inputs-switch', () => ({ ORCA_SIDEBAR_INPUTS_IS_MAIN: true })) @@ -19,12 +24,13 @@ const DAY_MS = 24 * 60 * 60 * 1000 function createHeadlessRuntime(ptyId = 'pty-1') { // Untouched for 45 days: no agent ran, so no Recent stamp moved. - const meta: Record> = { [WT]: { lastActivityAt: 1 } } + const meta: Record = { [WT]: storeWorktreeMeta({ lastActivityAt: 1 }) } const writes: Partial[] = [] - const store = { + const repo = storeRepo({ id: 'repo-1', path: '/tmp/repo-1', connectionId: null }) + const store: PartialRuntimeStore = { // An unstamped row's writes resolve their host from its repo's only registration. - getRepos: () => [{ id: 'repo-1', connectionId: null }], - getRepo: (id: string) => (id === 'repo-1' ? { id, connectionId: null } : undefined), + getRepos: () => [repo], + getRepo: (id: string) => (id === repo.id ? repo : undefined), getSettings: () => ({}), getWorktreeMeta: (id: string) => meta[id], getAllWorktreeMeta: () => meta, @@ -34,8 +40,7 @@ function createHeadlessRuntime(ptyId = 'pty-1') { return meta[id] } } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the terminal-use path reads only these members. - const runtime = new OrcaRuntimeService(store as unknown as RuntimeStore) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) // No desktop window: the phone is the only client. runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [{ tabId: 'tab-1', worktreeId: WT, title: 'zsh', activeLeafId: 'pane:1', layout: null }], diff --git a/src/main/runtime/sidebar-inputs/orca-runtime-workspace-unread.test.ts b/src/main/runtime/sidebar-inputs/orca-runtime-workspace-unread.test.ts index 87e6c7f365b..fdbf1e8989b 100644 --- a/src/main/runtime/sidebar-inputs/orca-runtime-workspace-unread.test.ts +++ b/src/main/runtime/sidebar-inputs/orca-runtime-workspace-unread.test.ts @@ -5,7 +5,6 @@ import { HEADLESS_RUNTIME_WINDOW_ID } from '../../../shared/runtime-session-cont import { folderWorkspaceKey } from '../../../shared/workspace-scope' import { AgentHookServer } from '../../agent-hooks/server' import { OrcaRuntimeService } from '../orca-runtime' -import type { RuntimeStore } from '../runtime-store-contract' import { RpcDispatcher } from '../rpc/dispatcher' import type { RpcDispatchStreamingOptions } from '../rpc/dispatcher-stream-options' import { SESSION_TAB_METHODS } from '../rpc/methods/session-tabs' @@ -20,6 +19,16 @@ import { BROWSER_NETWORK_TUNNEL_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity' +import { + partialRuntimeStore, + storeFolderWorkspace, + storeProjectGroup, + storeRepo, + storeWorktreeMeta, + type PartialRuntimeStore +} from '../runtime-store-partial-fixture' +import type { WorktreeMeta } from '../../../shared/worktree/meta-types' +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' vi.mock('../../../shared/sidebar-inputs-switch', () => ({ ORCA_SIDEBAR_INPUTS_IS_MAIN: true })) @@ -32,8 +41,6 @@ const LEAF = '22222222-2222-4222-8222-222222222222' const PANE = `tab-1:${LEAF}` const PHONE: RpcDispatchStreamingOptions = { clientKind: 'mobile', connectionId: 'phone-1' } -type Flag = { lastActivityAt: number; isUnread: boolean } - /** What each kind of desktop socket sends: the request and subscription sockets, the browser host * lease and network tunnel (which add only browser capabilities to the shared base), and the CLI. */ function currentDesktopSockets() { @@ -52,28 +59,37 @@ function currentDesktopSockets() { } function createHeadlessHost() { - const meta: Record = { [WT]: { lastActivityAt: 0, isUnread: false } } - const sshMeta: Record = { [SSH_WT]: { lastActivityAt: 0, isUnread: false } } + const meta: Record = { [WT]: storeWorktreeMeta() } + const sshMeta: Record = { [SSH_WT]: storeWorktreeMeta() } // A same-id project this host registered under a server's id and locally. - const hubMeta: Record = { [HUB_WT]: { lastActivityAt: 0, isUnread: false } } - meta[HUB_WT] = { lastActivityAt: 0, isUnread: false } - const folders: Record = { - [FOLDER_ID]: { lastActivityAt: 0, isUnread: false, projectGroupId: 'group-1' } + const hubMeta: Record = { [HUB_WT]: storeWorktreeMeta() } + meta[HUB_WT] = storeWorktreeMeta() + const folders: Record = { + [FOLDER_ID]: storeFolderWorkspace({ + id: FOLDER_ID, + projectGroupId: 'group-1', + folderPath: '/tmp/folder' + }) } const repos = [ - { id: 'repo-1', connectionId: null }, - { id: 'repo-ssh', connectionId: 'build-box' }, - { id: 'repo-hub', connectionId: null, executionHostId: 'runtime:hub' }, - { id: 'repo-hub', connectionId: null } + storeRepo({ id: 'repo-1', path: '/tmp/repo-1', connectionId: null }), + storeRepo({ id: 'repo-ssh', path: '/home/user/repo-ssh', connectionId: 'build-box' }), + storeRepo({ + id: 'repo-hub', + path: '/srv/repo-hub', + connectionId: null, + executionHostId: 'runtime:hub' + }), + storeRepo({ id: 'repo-hub', path: '/tmp/repo-hub', connectionId: null }) ] - const store = { + const store: PartialRuntimeStore = { getRepos: () => repos, getRepo: (id: string) => repos.find((repo) => repo.id === id), - getProjectGroups: () => [{ id: 'group-1', connectionId: null }], + getProjectGroups: () => [storeProjectGroup({ id: 'group-1', connectionId: null })], getSettings: () => ({}), getWorktreeMeta: (id: string) => meta[id], getAllWorktreeMeta: () => meta, - setWorktreeMeta: ({ id, hostId }: HostWorktreeTarget, updates: Partial) => + setWorktreeMeta: ({ id, hostId }: HostWorktreeTarget, updates: Partial) => Object.assign( hostId === 'ssh:build-box' ? sshMeta[id] @@ -85,11 +101,10 @@ function createHeadlessHost() { getWorktreeMetaForHost: (id: string, hostId: string) => hostId === 'ssh:build-box' ? sshMeta[id] : hostId === 'runtime:hub' ? hubMeta[id] : meta[id], getFolderWorkspace: (id: string) => folders[id], - updateFolderWorkspace: (id: string, updates: Partial) => + updateFolderWorkspace: (id: string, updates: Partial) => Object.assign(folders[id], updates) } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the unread path reads only these members. - const runtime = new OrcaRuntimeService(store as unknown as RuntimeStore) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [{ tabId: 'tab-1', worktreeId: WT, title: 'Agent', activeLeafId: LEAF, layout: null }], leaves: [ diff --git a/src/main/runtime/sidebar-inputs/sidebar-inputs-feed-announcements.test.ts b/src/main/runtime/sidebar-inputs/sidebar-inputs-feed-announcements.test.ts index 0d086fb453c..bf96089ae9a 100644 --- a/src/main/runtime/sidebar-inputs/sidebar-inputs-feed-announcements.test.ts +++ b/src/main/runtime/sidebar-inputs/sidebar-inputs-feed-announcements.test.ts @@ -4,8 +4,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { RuntimeClientEvent } from '../../../shared/runtime-client-events' import type { Repo } from '../../../shared/repo-types' import type { PRInfo } from '../../../shared/github/pull-request-types' -import type { ResolvedWorktree } from '../runtime-worktree-path-identity' -import type { RuntimeStore } from '../runtime-store-contract' import type { WslHostService } from '../../wsl/wsl-host-service' import { setWslHostService } from '../../wsl/wsl-host-service-instance' import { getGitHubPRCacheKeyCandidates } from '../../../shared/github/github-cache-key' @@ -94,13 +92,11 @@ async function host() { workspaceRows: { revision: revisions.rows, snapshot: () => ({ revision: 0, rows: {} }), - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: review facts read only these fields. - resolvedWorktrees: () => [worktree as unknown as ResolvedWorktree], + resolvedWorktrees: () => [worktree], ready: async () => {} } as unknown as SidebarInputsFeedSources['workspaceRows'], repoIcons: { idsByRepo: () => ({}) }, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the real Store is a RuntimeStore. - store: () => store as unknown as RuntimeStore, + store: () => store, onClientEvent: (listener) => { clientEvents.add(listener) return () => clientEvents.delete(listener) diff --git a/src/main/runtime/sidebar-inputs/workspace-rows-publish-cost.test.ts b/src/main/runtime/sidebar-inputs/workspace-rows-publish-cost.test.ts index 6e4d1fdad32..7f6087bf606 100644 --- a/src/main/runtime/sidebar-inputs/workspace-rows-publish-cost.test.ts +++ b/src/main/runtime/sidebar-inputs/workspace-rows-publish-cost.test.ts @@ -9,7 +9,6 @@ import { getRepoExecutionHostId } from '../../../shared/execution-host' import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity' import { fixture } from '../../persistence/loading-store/profile-state-delayed-authority-fixture' import { OrcaRuntimeService } from '../orca-runtime' -import type { RuntimeStore } from '../runtime-store-contract' import type { ResolvedWorktreeRepoScans } from '../workspaces/resolved-worktree-scan-join' import { requireSwitchedOn } from './require-switched-on' import { TestPhone } from './sidebar-inputs-feed.test-fixture' @@ -52,8 +51,7 @@ describe('workspace rows publish cost', () => { store.addRepo(repo) // Checkouts outside the workspace directory count as external; show them, as a long-time user does. store.updateSettings({ worktreeVisibilityDefaults: { external: 'show' } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the real Store is a RuntimeStore. - const runtime = new CachedGitRuntime(store as unknown as RuntimeStore) + const runtime = new CachedGitRuntime(store) const feed = requireSwitchedOn(runtime.sidebarInputsFeed) const phone = new TestPhone(feed, 'conn-1') phone.refresh() diff --git a/src/main/runtime/structured-agent-launch-parity.test.ts b/src/main/runtime/structured-agent-launch-parity.test.ts index d84efdd0cbf..f9f255c9469 100644 --- a/src/main/runtime/structured-agent-launch-parity.test.ts +++ b/src/main/runtime/structured-agent-launch-parity.test.ts @@ -153,6 +153,7 @@ import { structuredWorkerIdentities, structuredWorkerProcessIncarnation } from './structured-worker-identity' +import { partialRuntimeStore } from './runtime-store-partial-fixture' // A new agent goes last: its cases then shift only the configured ones' session ids. const AGENTS = [ @@ -582,10 +583,11 @@ beforeAll(async () => { process.env = inheritedEnvironment() installAppEnvironment() settings = defaultSettings() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the host wiring reads only the store's getSettings method. - const runtime = new OrcaRuntimeService({ - getSettings: () => settings - } as never) + const runtime = new OrcaRuntimeService( + partialRuntimeStore({ + getSettings: () => settings + }) + ) await runtime.ensureStructuredAgentSessionHost() const production = capture.productionDeps.at(-1) if (!production) { diff --git a/src/main/runtime/structured-agent-session-windowless-layout-host.test.ts b/src/main/runtime/structured-agent-session-windowless-layout-host.test.ts index b0cd339ecca..1fbedbe6aff 100644 --- a/src/main/runtime/structured-agent-session-windowless-layout-host.test.ts +++ b/src/main/runtime/structured-agent-session-windowless-layout-host.test.ts @@ -10,6 +10,8 @@ import { } from '../../shared/agent-session-record.test-fixture' import { structuredAgentSessionTabId } from '../../shared/structured-agent-session-projection' import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' +import { getDefaultWorkspaceSession } from '../../shared/constants' +import { normalizeExecutionHostId } from '../../shared/execution-host' import { closeTestJournalHostDatabases } from '../native-chat/agent-session-journal/journal-host-database-test-support' import { recordingStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support' import { seedTestAgentSessionRecordStore } from './agent-session-record-store-test-harness' @@ -23,6 +25,7 @@ import { ensureStructuredAgentSessionHost, stopStructuredAgentSessionRuntime } from './structured-agent-session-runtime' +import { partialRuntimeStore } from './runtime-store-partial-fixture' const WT = 'workspace-1' // Session ids are 8 to 128 characters; a shorter one is not a readable record. @@ -80,9 +83,11 @@ async function runtimeWithSavedChats( const store = { getRepos: () => [], getSettings: () => ({}), + // An unsaved partition reads as the default layout, as the Store answers it. getWorkspaceSession: (hostId = 'local') => - hostId === 'local' ? state.session : (partitions.get(hostId) ?? null), - getWorkspaceSessionHostIds: () => ['local', ...partitions.keys()], + hostId === 'local' ? state.session : (partitions.get(hostId) ?? getDefaultWorkspaceSession()), + getWorkspaceSessionHostIds: () => + ['local', ...partitions.keys()].flatMap((id) => normalizeExecutionHostId(id) ?? []), setWorkspaceSession: (next: WorkspaceSessionState, hostId = 'local') => { if (hostId === 'local') { state.session = next @@ -95,8 +100,7 @@ async function runtimeWithSavedChats( (next, hostId) => store.setWorkspaceSession(next, hostId) ) } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the runtime reads only these store methods on these paths. - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) for (const sessionId of sessionIds) { await runtime.publishStructuredAgentSessionTab({ workspaceId: WT, diff --git a/src/main/runtime/structured-agent-session-windowless-layout.test.ts b/src/main/runtime/structured-agent-session-windowless-layout.test.ts index 6943612b0a2..a73c427ef4e 100644 --- a/src/main/runtime/structured-agent-session-windowless-layout.test.ts +++ b/src/main/runtime/structured-agent-session-windowless-layout.test.ts @@ -2,12 +2,15 @@ // opening a chat saves it, closing removes it, and a chat the index restores without one gets one. import { afterEach, describe, expect, it, vi } from 'vitest' import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' +import { getDefaultWorkspaceSession } from '../../shared/constants' +import { normalizeExecutionHostId } from '../../shared/execution-host' import { setStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' import { OrcaRuntimeService } from './orca-runtime' import { withChatTabChanges } from './agent-session/agent-session-tab-layout' import { structuredAgentSessionTabId } from '../../shared/structured-agent-session-projection' import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch' import { chatTabCommandsSettled, layoutApplyOn } from './session-layout-apply.test-fixture' +import { partialRuntimeStore } from './runtime-store-partial-fixture' const WT = 'repo-1::/tmp/worktree-a' @@ -23,8 +26,7 @@ function createRuntime() { terminalLayoutsByTabId: {} } } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the runtime reads only these store methods on these paths. - const runtime = new OrcaRuntimeService(runtimeStore(state) as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(runtimeStore(state))) return { runtime, state } } @@ -142,8 +144,7 @@ describe('a chat tab on a host with no window', () => { await settled() expect(savedChatIds(state)).toEqual(['chat-1']) // A fresh runtime lists no chats, as when the close RPC or a worker stop runs before a restore. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the runtime reads only these store methods on these paths. - const fresh = new OrcaRuntimeService(runtimeStore(state) as never) + const fresh = new OrcaRuntimeService(partialRuntimeStore(runtimeStore(state))) useChatHost({ getPersistedVisibleSessionTabIndex: () => ({ present: true, sessionIds: [] }) }) fresh.onStructuredSessionTabHidden('chat-1') @@ -181,8 +182,11 @@ describe('a chat tab on a host with no window', () => { const store = { getRepos: () => [], getSettings: () => ({}), - getWorkspaceSessionHostIds: () => [...sessions.keys()], - getWorkspaceSession: (hostId = 'local') => sessions.get(hostId) ?? null, + getWorkspaceSessionHostIds: () => + [...sessions.keys()].flatMap((id) => normalizeExecutionHostId(id) ?? []), + // An unsaved partition reads as the default layout, as the Store answers it. + getWorkspaceSession: (hostId = 'local') => + sessions.get(hostId) ?? getDefaultWorkspaceSession(), setWorkspaceSession: (next: WorkspaceSessionState, hostId = 'local') => { sessions.set(hostId, next) }, @@ -191,8 +195,7 @@ describe('a chat tab on a host with no window', () => { (next, hostId) => sessions.set(hostId, next) ) } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the runtime reads only these store methods on these paths. - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) useChatHost({ getPersistedVisibleSessionTabIndex: () => ({ present: true, sessionIds: [] }) }) runtime.onStructuredSessionTabHidden('chat-1') diff --git a/src/main/runtime/terminal-chat-owner-persistence.test.ts b/src/main/runtime/terminal-chat-owner-persistence.test.ts index a489285d27e..397e9037549 100644 --- a/src/main/runtime/terminal-chat-owner-persistence.test.ts +++ b/src/main/runtime/terminal-chat-owner-persistence.test.ts @@ -7,15 +7,13 @@ import { makeWorkspaceSessionWithHeadlessTerminal } from './orca-runtime-test-fixtures.spec' import { UpdatePaneLayout } from '../../shared/rpc-contract/session-tabs-schemas-params' -import type { RuntimeStore } from './runtime-store-contract' describe('remote terminal chat ownership', () => { it('preserves, changes, and explicitly clears the owner across the RPC and restart boundary', async () => { const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( makeWorkspaceSessionWithHeadlessTerminal() ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The shared fixture implements RuntimeStore; its annotation erases the Vitest mock call signatures. - const runtime = new OrcaRuntimeService(runtimeStore as RuntimeStore) + const runtime = new OrcaRuntimeService(runtimeStore) const update = (owner: string | null | undefined) => { const params = UpdatePaneLayout.parse({ worktree: `id:${TEST_WORKTREE_ID}`, diff --git a/src/main/runtime/terminal-list-surface-lost-orphan.spec.ts b/src/main/runtime/terminal-list-surface-lost-orphan.spec.ts index b7e94989a96..cbe32c1f427 100644 --- a/src/main/runtime/terminal-list-surface-lost-orphan.spec.ts +++ b/src/main/runtime/terminal-list-surface-lost-orphan.spec.ts @@ -7,6 +7,7 @@ import { makePaneKey } from '../../shared/stable-pane-id' import { spawnSurfaceClaimSequence } from './pty-recorded-surface-topology' import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch' import { bindPaneFirst } from './bind-first-pane.test-fixture' +import { partialRuntimeStore } from './runtime-store-partial-fixture' export function registerTerminalListSurfaceLostOrphanTests(): void { // #18191: a terminal whose pane the graph dropped kept reporting `orphaned: false` with a @@ -114,8 +115,7 @@ export function registerTerminalListSurfaceLostOrphanTests(): void { /** Both PTYs stay live on the host throughout; only the graph changes. */ function makeRuntime(session?: WorkspaceSessionState): OrcaRuntimeService { const store = makeStore(session) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: makeStore returns the repo and session reads this suite drives; the rest of Store is unreached. - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) storesByRuntime.set(runtime, store) // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub carries the four controller members this suite drives; both PTYs stay live throughout. runtime.setPtyController({ diff --git a/src/main/runtime/terminal-update-admission-census.test.ts b/src/main/runtime/terminal-update-admission-census.test.ts index 5fa6442b7b6..5e428c93d8f 100644 --- a/src/main/runtime/terminal-update-admission-census.test.ts +++ b/src/main/runtime/terminal-update-admission-census.test.ts @@ -30,8 +30,7 @@ type CensusRequest = Omit, 'acti function census(runtime: InstanceType, params: CensusRequest) { // Parsed as the server parses it, so a field the server does not know is dropped here too. const parsed = OrcadTerminalCensusParamsSchema.parse({ activatedAt: 1, ...params }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handler reads only `runtime` from its context. - return ORCAD_TERMINAL_CENSUS_METHODS[0]!.handler(parsed, { runtime } as never) + return ORCAD_TERMINAL_CENSUS_METHODS[0]!.handler(parsed, { runtime }) } // What the SSH and WSL updates send as the last count before the stop. diff --git a/src/main/runtime/terminal/identity/adopted-pane-handle-race-main.test.ts b/src/main/runtime/terminal/identity/adopted-pane-handle-race-main.test.ts index 4b25e486761..47cd30f25ea 100644 --- a/src/main/runtime/terminal/identity/adopted-pane-handle-race-main.test.ts +++ b/src/main/runtime/terminal/identity/adopted-pane-handle-race-main.test.ts @@ -43,8 +43,7 @@ it('gives a restored pane one handle: none before its adoption registers, then t terminalPtyIncarnationsByPaneKey: { [`${TAB}:${HEADLESS_LEAF_ID}`]: 'inc-restored' } }) ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements every method these paths call. - const runtime = new OrcaRuntimeService(harness.runtimeStore as never) + const runtime = new OrcaRuntimeService(harness.runtimeStore) runtime.setPtyController({ write: () => true, kill: () => true, diff --git a/src/main/runtime/terminal/identity/main-pane-record-arrival.test.ts b/src/main/runtime/terminal/identity/main-pane-record-arrival.test.ts index 07ad9bc918b..6f13ad0a4cf 100644 --- a/src/main/runtime/terminal/identity/main-pane-record-arrival.test.ts +++ b/src/main/runtime/terminal/identity/main-pane-record-arrival.test.ts @@ -24,8 +24,7 @@ function restartedHost(stamp: string, listedByHost: () => Listed[]) { terminalPtyIncarnationsByPaneKey: { [`host-tab:${HEADLESS_LEAF_ID}`]: stamp } }) ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared runtime spec fixture store implements every method these paths call. - const runtime = new OrcaRuntimeService(runtimeStore as never) + const runtime = new OrcaRuntimeService(runtimeStore) runtime.setPtyController({ write: () => true, kill: () => true, diff --git a/src/main/runtime/terminal/lifecycle/main-pane-liveness.test.ts b/src/main/runtime/terminal/lifecycle/main-pane-liveness.test.ts index 7f10de5b440..edf584e249f 100644 --- a/src/main/runtime/terminal/lifecycle/main-pane-liveness.test.ts +++ b/src/main/runtime/terminal/lifecycle/main-pane-liveness.test.ts @@ -87,8 +87,7 @@ function savedPaneHost(ptyId: string, opts: { windowed?: boolean; splitPtyId?: s } }) ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared runtime spec fixture store implements every method these paths call. - const runtime = new OrcaRuntimeService(runtimeStore as never) + const runtime = new OrcaRuntimeService(runtimeStore) runtime.setPtyController(hostController(host, TEST_WORKTREE_PATH, TEST_WORKTREE_ID)) const graph = { tabs: [ @@ -219,12 +218,11 @@ describe('a pane main owns does not vouch for its terminal (switch on)', () => { } }) const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(session) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared runtime spec fixture store implements every method these paths call. const runtime = new OrcaRuntimeService({ ...runtimeStore, getFolderWorkspaces: () => [makeFolderWorkspace()], getProjectGroups: () => [makeFolderProjectGroup()] - } as never) + }) runtime.setPtyController( hostController(host, TEST_FOLDER_WORKSPACE_PATH, TEST_FOLDER_WORKSPACE_KEY) ) diff --git a/src/main/runtime/terminal/surface/closed-pane-exit-cleanup-main.test.ts b/src/main/runtime/terminal/surface/closed-pane-exit-cleanup-main.test.ts index 417fa0c92b1..f6ac1f9bffe 100644 --- a/src/main/runtime/terminal/surface/closed-pane-exit-cleanup-main.test.ts +++ b/src/main/runtime/terminal/surface/closed-pane-exit-cleanup-main.test.ts @@ -135,8 +135,7 @@ for (const host of ['no window', 'a window'] as const) { await close(harness.runtime) // The relaunch reattaches the closed pane's live terminal; no pane holds it any more. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the same fixture store the first run wrote. - const relaunched = new OrcaRuntimeService(harness.runtime['store'] as never) + const relaunched = new OrcaRuntimeService(harness.runtime['store']) relaunched.setPtyController({ write: () => true, kill: () => true, diff --git a/src/main/runtime/terminal/surface/terminal-surface-replies.test-fixture.ts b/src/main/runtime/terminal/surface/terminal-surface-replies.test-fixture.ts index 34998b9d5d8..3ec4bb8bd69 100644 --- a/src/main/runtime/terminal/surface/terminal-surface-replies.test-fixture.ts +++ b/src/main/runtime/terminal/surface/terminal-surface-replies.test-fixture.ts @@ -17,6 +17,7 @@ import type { WorkspaceSessionState } from '../../../../shared/workspace-session import type { ExecutionHostId } from '../../../../shared/execution-host' import type { LayoutCommand } from '../../../../shared/workspace-layout/workspace-layout-command-types' import { applyCommandToSession } from '../../../persistence/terminal-topology/terminal-pane-admission' +import { partialRuntimeStore } from '../../runtime-store-partial-fixture' export const SURFACE_TAB_ID = '44444444-4444-4444-8444-444444444444' export const SURFACE_PTYS = { [HEADLESS_LEAF_ID]: 'pty-a', [HEADLESS_SECOND_LEAF_ID]: 'pty-b' } @@ -67,7 +68,7 @@ export function surfaceRuntime( }) ) // The Store's write notice, which main's terminal list follows. - const written = new Set<(hostId: string | null) => void>() + const written = new Set<(hostId: ExecutionHostId | null) => void>() const setSession = (next: WorkspaceSessionState): void => { harness.setSession(next) for (const listener of written) { @@ -78,7 +79,7 @@ export function surfaceRuntime( ...harness.runtimeStore, // The runtime's own session write, as the Store applies it: then tells its write listeners. setWorkspaceSession: vi.fn((next: WorkspaceSessionState) => setSession(next)), - getWorkspaceSessionHostIds: () => ['local'], + getWorkspaceSessionHostIds: (): ExecutionHostId[] => ['local'], // A terminal's binding write, as the Store applies it: then tells its write listeners. persistPtyBinding: async ( ...args: Parameters @@ -89,7 +90,7 @@ export function surfaceRuntime( } return persisted }, - onWorkspaceSessionWritten: (listener: (hostId: string | null) => void) => { + onWorkspaceSessionWritten: (listener: (hostId: ExecutionHostId | null) => void) => { written.add(listener) return () => written.delete(listener) }, @@ -102,8 +103,7 @@ export function surfaceRuntime( return change.value } } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared runtime spec fixture store implements every method these paths call. - const runtime = new OrcaRuntimeService(runtimeStore as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(runtimeStore)) const killed: string[] = [] runtime.setPtyController({ write: () => true, diff --git a/src/main/runtime/terminal/view/mobile-terminal-surface.ts b/src/main/runtime/terminal/view/mobile-terminal-surface.ts index 22dac20c010..7b6b2e8f1a7 100644 --- a/src/main/runtime/terminal/view/mobile-terminal-surface.ts +++ b/src/main/runtime/terminal/view/mobile-terminal-surface.ts @@ -1,4 +1,3 @@ -/* eslint-disable unicorn/no-useless-spread */ import { OrcaRuntimeWithCreateRuntimeOwnedMobileSessionTerminal } from '../launch/mobile-terminal-owned-create' import type { OrcaRuntimeWithResolveTerminalSplitSourceAuthority } from '../launch/terminal-split-source-authority' import type { @@ -218,7 +217,7 @@ export abstract class OrcaRuntimeWithWaitForMobileTerminalSurface extends OrcaRu ...(pending.viewMode ? { viewMode: pending.viewMode } : {}) }) // Why: check closures normally drain only inside syncWindowGraph; a main-side publish must drain them too or the pending wait misses the insertion. - for (const cb of [...this.graphSyncCallbacks]) { + for (const cb of Array.from(this.graphSyncCallbacks)) { cb() } return this.findMobileTerminalSurface(worktreeId, tabId) diff --git a/src/main/runtime/tui-idle-name-only-real-pty.integration.test.ts b/src/main/runtime/tui-idle-name-only-real-pty.integration.test.ts index 0f5efd8c7b3..eeedf894def 100644 --- a/src/main/runtime/tui-idle-name-only-real-pty.integration.test.ts +++ b/src/main/runtime/tui-idle-name-only-real-pty.integration.test.ts @@ -86,11 +86,7 @@ async function terminalWait( ): Promise<{ satisfied: boolean; elapsedMs: number }> { const startedAt = Date.now() try { - const result = await waitMethod.handler( - { terminal, for: 'tui-idle', timeoutMs }, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: terminal.wait reads only `runtime` off its context; the rest is request plumbing this fixture has no use for. - { runtime } as Parameters[1] - ) + const result = await waitMethod.handler({ terminal, for: 'tui-idle', timeoutMs }, { runtime }) return { satisfied: result.wait.satisfied === true, elapsedMs: Date.now() - startedAt } } catch (error) { // Why only `timeout`: an unsatisfied wait is the outcome under test, but any other diff --git a/src/main/runtime/tui-idle-wait-test-harness.ts b/src/main/runtime/tui-idle-wait-test-harness.ts index cbf6f1cbe80..443407553b4 100644 --- a/src/main/runtime/tui-idle-wait-test-harness.ts +++ b/src/main/runtime/tui-idle-wait-test-harness.ts @@ -121,17 +121,14 @@ export function makeTuiIdleRuntime(options: TuiIdleRuntimeOptions): OrcaRuntimeS // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: partial store double; the wait path reads only the members defined above. makeStore(options.repoPath) as never ) - runtime.setPtyController( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: partial controller double; the wait path calls only the members listed here. - { - spawn: async () => ({ id: 'fixture-pty' }), - write: () => true, - kill: () => true, - getForegroundProcess: options.getForegroundProcess, - listProcesses: async () => [], - hasPty: () => true - } as never - ) + runtime.setPtyController({ + spawn: async () => ({ id: 'fixture-pty' }), + write: () => true, + kill: () => true, + getForegroundProcess: options.getForegroundProcess, + listProcesses: async () => [], + hasPty: () => true + }) return runtime } diff --git a/src/main/runtime/workspaces/persisted-surfaces/sweep-terminal-placement.test.ts b/src/main/runtime/workspaces/persisted-surfaces/sweep-terminal-placement.test.ts index f3747f320d1..20e316777bc 100644 --- a/src/main/runtime/workspaces/persisted-surfaces/sweep-terminal-placement.test.ts +++ b/src/main/runtime/workspaces/persisted-surfaces/sweep-terminal-placement.test.ts @@ -106,8 +106,7 @@ describe('main places the swept workspace’s live terminals, layout-owner build it('the sweep’s listing places before it lists, so it names the placed terminal’s pane', async () => { const { runtime } = await hostWith([session('pty-swept')]) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handler reads only `runtime` from its context. - const context = { runtime } as never + const context = { runtime } const listing = await terminalListHandler()( { worktree: `id:${SWEPT}`, @@ -127,8 +126,7 @@ describe('main places the swept workspace’s live terminals, layout-owner build it('a listing that does not ask places nothing', async () => { const { store, runtime } = await hostWith([session('pty-swept')]) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handler reads only `runtime` from its context. - const context = { runtime } as never + const context = { runtime } await terminalListHandler()( { worktree: `id:${SWEPT}`, limit: 200, requireFreshPtyLiveness: true }, context diff --git a/src/main/runtime/workspaces/workspace-host-notifications.test.ts b/src/main/runtime/workspaces/workspace-host-notifications.test.ts index e05cd0779ed..3fa69a47eab 100644 --- a/src/main/runtime/workspaces/workspace-host-notifications.test.ts +++ b/src/main/runtime/workspaces/workspace-host-notifications.test.ts @@ -3,11 +3,11 @@ import type { Repo } from '../../../shared/repo-types' import type { RuntimeClientEvent } from '../../../shared/runtime-client-events' import { OrcaRuntimeService } from '../orca-runtime' import type { RuntimeStore } from '../runtime-store-contract' +import { partialRuntimeStore } from '../runtime-store-partial-fixture' function catalogEventsFor(repo: Repo): RuntimeClientEvent[] { const store: Pick = { getRepos: () => [repo] } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the catalog notification reads only repos. - const runtime = new OrcaRuntimeService(store as RuntimeStore) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) const events: RuntimeClientEvent[] = [] runtime.onClientEvent((event) => events.push(event)) runtime.notifyWorktreeCatalogChangedForRemoteClients(repo.id) diff --git a/src/main/runtime/worktree-ps-degraded-repo-scan.test.ts b/src/main/runtime/worktree-ps-degraded-repo-scan.test.ts index 09d4761a467..438d043b8c0 100644 --- a/src/main/runtime/worktree-ps-degraded-repo-scan.test.ts +++ b/src/main/runtime/worktree-ps-degraded-repo-scan.test.ts @@ -21,12 +21,16 @@ vi.mock('../git/worktree', async (importOriginal) => ({ listWorktreesStrict: listWorktreesStrictMock })) -import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' +import { LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId } from '../../shared/execution-host' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' +import type { WorktreeLineage } from '../../shared/worktree/lineage-types' +import { worktreeWorkspaceKey } from '../../shared/workspace-scope' import { OrcaRuntimeService } from './orca-runtime' import { hostWorktreeTarget, type HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity' +import { partialRuntimeStore, storeRepo, storeWorktreeMeta } from './runtime-store-partial-fixture' const REPO_ID = 'repo-remote' const REPO_PATH = '/home/user/projects/app' @@ -34,39 +38,30 @@ const WORKTREE_PATH = '/home/user/projects/app-feature' const WORKTREE_ID = `${REPO_ID}::${WORKTREE_PATH}` const SCRATCH_ID = `${REPO_ID}::${REPO_PATH}/.claude/worktrees/scratch` const SSH_CONNECTION_ID = 'ssh-remote-1' -const SSH_HOST_ID = `ssh:${SSH_CONNECTION_ID}` +const SSH_HOST_ID = toSshExecutionHostId(SSH_CONNECTION_ID) const LOCAL_REPO_PATH = '/Users/me/dev/app' -function makeMeta(overrides: Record = {}) { - return { +function makeMeta(overrides: Partial = {}): WorktreeMeta { + return storeWorktreeMeta({ displayName: 'feature', - comment: '', - linkedIssue: null, - linkedPR: null, - linkedLinearIssue: null, linkedGitLabMR: null, linkedGitLabIssue: null, - isArchived: false, - isUnread: false, - isPinned: false, - sortOrder: 0, - lastActivityAt: 0, ...overrides - } + }) } const MAIN_WORKTREE_ID = `${REPO_ID}::${REPO_PATH}` // Why: prune assertions only discriminate when the repo actually owns lineage rows a scan verdict could delete. -function makeLineage() { +function makeLineage(): Record { return { [WORKTREE_ID]: { worktreeId: WORKTREE_ID, worktreeInstanceId: 'child-instance', parentWorktreeId: MAIN_WORKTREE_ID, parentWorktreeInstanceId: 'parent-instance', - origin: 'agent' as const, - capture: 'env-workspace' as const, + origin: 'cli', + capture: { source: 'env-workspace', confidence: 'inferred' }, createdAt: 1 } } @@ -74,8 +69,8 @@ function makeLineage() { type StoreOptions = { connectionId?: string - metaById?: Record> - removeWorktreeLineage?: ReturnType + metaById?: Record + removeWorktreeLineage?: (target: HostWorktreeTarget) => void /** Extra `getRepos()` rows appended after the primary one, for same-id-on-two-hosts coverage. */ coHostedRepos?: { id: string; path: string; connectionId?: string }[] } @@ -89,32 +84,38 @@ function makeStore(options: StoreOptions = {}) { const store = { getRepo: (id: string) => store.getRepos().find((repo) => repo.id === id), getRepos: () => [ - { + storeRepo({ id: REPO_ID, path: REPO_PATH, displayName: 'app', badgeColor: 'blue', addedAt: 1, ...(options.connectionId === undefined ? {} : { connectionId: options.connectionId }) - }, - ...(options.coHostedRepos ?? []).map((repo) => ({ - displayName: 'app', - badgeColor: 'blue', - addedAt: 1, - ...repo - })) + }), + ...(options.coHostedRepos ?? []).map((repo) => + storeRepo({ displayName: 'app', badgeColor: 'blue', addedAt: 1, ...repo }) + ) ], getAllWorktreeMeta: vi.fn(() => metaById), getWorktreeMeta: (id: string) => metaById[id], - setWorktreeMeta: ({ id }: HostWorktreeTarget, meta: Record) => { - metaById[id] = { ...(metaById[id] ?? makeMeta()), ...meta } as never + setWorktreeMeta: ({ id }: HostWorktreeTarget, meta: Partial) => { + metaById[id] = { ...(metaById[id] ?? makeMeta()), ...meta } return metaById[id] }, removeWorktreeMeta: () => {}, getAllWorktreeLineage: () => lineageById, - getAllWorkspaceLineage: () => ({ [`worktree:${WORKTREE_ID}`]: { parentWorkspaceKey: null } }), + getAllWorkspaceLineage: () => ({ + [worktreeWorkspaceKey(WORKTREE_ID)]: { + childWorkspaceKey: worktreeWorkspaceKey(WORKTREE_ID), + childInstanceId: 'child-instance', + parentWorkspaceKey: worktreeWorkspaceKey(MAIN_WORKTREE_ID), + parentInstanceId: 'parent-instance', + origin: 'cli' as const, + capture: { source: 'env-workspace' as const, confidence: 'inferred' as const }, + createdAt: 1 + } + }), removeWorktreeLineage: options.removeWorktreeLineage ?? vi.fn(), - getGitHubCache: () => undefined as never, getSettings: () => ({ workspaceDir: '/tmp/workspaces', nestWorkspaces: false, @@ -145,7 +146,9 @@ describe('worktree.ps on a degraded repo scan', () => { it('keeps persisted worktrees when a remote scan stalls past the per-repo budget', async () => { vi.useFakeTimers() try { - const runtime = new OrcaRuntimeService(makeStore({ connectionId: 'ssh-remote-1' }) as never) + const runtime = new OrcaRuntimeService( + partialRuntimeStore(makeStore({ connectionId: 'ssh-remote-1' })) + ) const result = await advancePastRepoScanBudget(runtime.getWorktreePs(10_000)) @@ -156,7 +159,9 @@ describe('worktree.ps on a degraded repo scan', () => { }) it('keeps persisted worktrees when a remote repo is unreachable', async () => { - const runtime = new OrcaRuntimeService(makeStore({ connectionId: 'ssh-remote-1' }) as never) + const runtime = new OrcaRuntimeService( + partialRuntimeStore(makeStore({ connectionId: 'ssh-remote-1' })) + ) const result = await runtime.getWorktreePs(10_000) @@ -167,7 +172,7 @@ describe('worktree.ps on a degraded repo scan', () => { vi.useFakeTimers() try { listWorktreesStrictMock.mockImplementation(neverSettles) - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) const result = await advancePastRepoScanBudget(runtime.getWorktreePs(10_000)) @@ -181,7 +186,7 @@ describe('worktree.ps on a degraded repo scan', () => { // selector resolution for local repos the way a stall does. it('treats a rejected scan as a real answer instead of restoring persisted worktrees', async () => { listWorktreesStrictMock.mockRejectedValue(new Error('git unavailable')) - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) const result = await runtime.getWorktreePs(10_000) @@ -190,7 +195,7 @@ describe('worktree.ps on a degraded repo scan', () => { it('still reports selector_not_found for a local worktree when the scan rejects', async () => { listWorktreesStrictMock.mockRejectedValue(new Error('git unavailable')) - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) await expect(runtime.showManagedWorktree(`id:${WORKTREE_ID}`)).rejects.toThrow( 'selector_not_found' @@ -202,7 +207,7 @@ describe('worktree.ps on a degraded repo scan', () => { vi.useFakeTimers() try { listWorktreesStrictMock.mockResolvedValue([]) - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) await runtime.getWorktreePs(10_000) await vi.advanceTimersByTimeAsync(2_000) @@ -218,7 +223,7 @@ describe('worktree.ps on a degraded repo scan', () => { vi.useFakeTimers() try { listWorktreesStrictMock.mockRejectedValue(new Error('spawn git EAGAIN')) - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) await runtime.getWorktreePs(10_000) await vi.advanceTimersByTimeAsync(2_000) @@ -236,18 +241,20 @@ describe('worktree.ps on a degraded repo scan', () => { try { listWorktreesStrictMock.mockImplementation(neverSettles) const runtime = new OrcaRuntimeService( - makeStore({ - connectionId: SSH_CONNECTION_ID, - coHostedRepos: [{ id: REPO_ID, path: LOCAL_REPO_PATH }], - metaById: { - [WORKTREE_ID]: makeMeta({ hostId: SSH_HOST_ID }), - [MAIN_WORKTREE_ID]: makeMeta({ - displayName: 'main', - instanceId: 'parent-instance', - hostId: SSH_HOST_ID - }) - } - }) as never + partialRuntimeStore( + makeStore({ + connectionId: SSH_CONNECTION_ID, + coHostedRepos: [{ id: REPO_ID, path: LOCAL_REPO_PATH }], + metaById: { + [WORKTREE_ID]: makeMeta({ hostId: SSH_HOST_ID }), + [MAIN_WORKTREE_ID]: makeMeta({ + displayName: 'main', + instanceId: 'parent-instance', + hostId: SSH_HOST_ID + }) + } + }) + ) ) const result = await advancePastRepoScanBudget(runtime.getWorktreePs(10_000)) @@ -265,16 +272,18 @@ describe('worktree.ps on a degraded repo scan', () => { try { listWorktreesStrictMock.mockImplementation(neverSettles) const runtime = new OrcaRuntimeService( - makeStore({ - metaById: { - [WORKTREE_ID]: makeMeta({ hostId: LOCAL_EXECUTION_HOST_ID }), - [MAIN_WORKTREE_ID]: makeMeta({ - displayName: 'main', - instanceId: 'parent-instance', - hostId: LOCAL_EXECUTION_HOST_ID - }) - } - }) as never + partialRuntimeStore( + makeStore({ + metaById: { + [WORKTREE_ID]: makeMeta({ hostId: LOCAL_EXECUTION_HOST_ID }), + [MAIN_WORKTREE_ID]: makeMeta({ + displayName: 'main', + instanceId: 'parent-instance', + hostId: LOCAL_EXECUTION_HOST_ID + }) + } + }) + ) ) const result = await advancePastRepoScanBudget(runtime.getWorktreePs(10_000)) @@ -299,7 +308,7 @@ describe('worktree.ps on a degraded repo scan', () => { vi.useFakeTimers() try { listWorktreesStrictMock.mockImplementation(neverSettles) - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) await advancePastRepoScanBudget(runtime.getWorktreePs(10_000)) await advancePastRepoScanBudget(runtime.getWorktreePs(10_000)) @@ -315,8 +324,9 @@ describe('worktree.ps on a degraded repo scan', () => { listWorktreesStrictMock.mockResolvedValue([ { path: REPO_PATH, head: 'abc', branch: 'main', isBare: false, isMainWorktree: true } ]) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial store mock; the code under test reads only the members it defines. - const runtime = new OrcaRuntimeService(makeStore({ removeWorktreeLineage }) as never) + const runtime = new OrcaRuntimeService( + partialRuntimeStore(makeStore({ removeWorktreeLineage })) + ) const result = await runtime.getWorktreePs(10_000) @@ -331,8 +341,9 @@ describe('worktree.ps on a degraded repo scan', () => { try { const removeWorktreeLineage = vi.fn() listWorktreesStrictMock.mockImplementation(neverSettles) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial store mock; the code under test reads only the members it defines. - const runtime = new OrcaRuntimeService(makeStore({ removeWorktreeLineage }) as never) + const runtime = new OrcaRuntimeService( + partialRuntimeStore(makeStore({ removeWorktreeLineage })) + ) await advancePastRepoScanBudget(runtime.getWorktreePs(10_000)) @@ -347,7 +358,7 @@ describe('worktree.ps on a degraded repo scan', () => { { path: REPO_PATH, head: 'abc', branch: 'main', isBare: false, isMainWorktree: true } ]) const store = makeStore() - const runtime = new OrcaRuntimeService(store as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(store)) await runtime.getWorktreePs(10_000) @@ -359,12 +370,14 @@ describe('worktree.ps on a degraded repo scan', () => { try { listWorktreesStrictMock.mockImplementation(neverSettles) const runtime = new OrcaRuntimeService( - makeStore({ - metaById: { - [WORKTREE_ID]: makeMeta(), - [SCRATCH_ID]: makeMeta({ displayName: 'scratch' }) - } - }) as never + partialRuntimeStore( + makeStore({ + metaById: { + [WORKTREE_ID]: makeMeta(), + [SCRATCH_ID]: makeMeta({ displayName: 'scratch' }) + } + }) + ) ) const listed = await advancePastRepoScanBudget(runtime.listManagedWorktrees(`id:${REPO_ID}`)) diff --git a/src/main/runtime/worktree-ps-unverifiable-terminals.test.ts b/src/main/runtime/worktree-ps-unverifiable-terminals.test.ts index 2499c879653..4422fc22403 100644 --- a/src/main/runtime/worktree-ps-unverifiable-terminals.test.ts +++ b/src/main/runtime/worktree-ps-unverifiable-terminals.test.ts @@ -9,6 +9,7 @@ vi.mock('electron', () => ({ })) import { OrcaRuntimeService } from './orca-runtime' +import { partialRuntimeStore, storeWorktreeMeta } from './runtime-store-partial-fixture' // BUG-9: a network drop to a relay host made `worktree ps` print `live:0 pty:no` for a terminal // that was still running. Lost contact is `unverifiable`, never zero. @@ -35,13 +36,12 @@ function makeStore() { getRepos: vi.fn(() => [REPO]), getRepo: vi.fn((id: string) => (id === REPO.id ? REPO : undefined)), // Why: this process serves no SSH git, so the SSH repo lists its stored rows. - getAllWorktreeMeta: vi.fn(() => ({ [WORKTREE_ID]: {} })), + getAllWorktreeMeta: vi.fn(() => ({ [WORKTREE_ID]: storeWorktreeMeta() })), getWorktreeMeta: vi.fn(() => undefined), setWorktreeMeta: vi.fn(), removeWorktreeMeta: vi.fn(), getAllWorktreeLineage: vi.fn(() => ({})), getAllWorkspaceLineage: vi.fn(() => ({})), - getGitHubCache: vi.fn(() => undefined), getSettings: vi.fn(() => ({ workspaceDir: '/tmp/workspaces' })), getProjects: vi.fn(() => []) } @@ -51,8 +51,7 @@ function makeRuntime(controller: { hasPty: () => boolean | null listProcesses: () => Promise<{ id: string; worktreeId: string }[]> }): OrcaRuntimeService { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the runtime reads only the store methods this fixture defines. - const runtime = new OrcaRuntimeService(makeStore() as never) + const runtime = new OrcaRuntimeService(partialRuntimeStore(makeStore())) runtime.setPtyController( // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: worktree.ps reaches only listProcesses and hasPty on this controller. { diff --git a/src/main/runtime/worktree-scan-repeated-path.test.ts b/src/main/runtime/worktree-scan-repeated-path.test.ts index 3f977f79452..4437c33e40b 100644 --- a/src/main/runtime/worktree-scan-repeated-path.test.ts +++ b/src/main/runtime/worktree-scan-repeated-path.test.ts @@ -28,6 +28,12 @@ vi.mock('./repo-worktree-admin-fingerprint', () => ({ import { OrcaRuntimeService } from './orca-runtime' import type { GitWorktreeInfo } from '../../shared/worktree/types' import type { HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity' +import { + partialRuntimeStore, + storeWorktreeMeta, + type PartialRuntimeStore +} from './runtime-store-partial-fixture' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' const REPO_ID = 'repo-local' const REPO_PATH = '/home/me/fileLoc' @@ -49,7 +55,7 @@ const OTHER_SPELLING_ROW = { } function makeRuntime(options: { connectionId?: string } = {}): OrcaRuntimeService { - const metaById: Record> = {} + const metaById: Record = {} const repos = [ { id: REPO_ID, @@ -60,13 +66,13 @@ function makeRuntime(options: { connectionId?: string } = {}): OrcaRuntimeServic ...options } ] - const store = { + const store: PartialRuntimeStore = { getRepo: (id: string) => repos.find((repo) => repo.id === id), getRepos: () => repos, getAllWorktreeMeta: () => metaById, getWorktreeMeta: (id: string) => metaById[id], - setWorktreeMeta: ({ id }: HostWorktreeTarget, meta: Record) => { - metaById[id] = { ...metaById[id], ...meta } + setWorktreeMeta: ({ id }: HostWorktreeTarget, meta: Partial) => { + metaById[id] = { ...storeWorktreeMeta(), ...metaById[id], ...meta } return metaById[id] }, getAllWorktreeLineage: () => ({}), @@ -80,8 +86,7 @@ function makeRuntime(options: { connectionId?: string } = {}): OrcaRuntimeServic }), getProjects: () => [] } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub carries the repo, meta, lineage and settings reads a worktree listing makes; the rest of Store is unreached. - return new OrcaRuntimeService(store as never) + return new OrcaRuntimeService(partialRuntimeStore(store)) } function gitLists(worktrees: GitWorktreeInfo[]): void { diff --git a/src/main/ssh/connection/ssh-connection-attribution.test.ts b/src/main/ssh/connection/ssh-connection-attribution.test.ts index 16551135010..08785feccec 100644 --- a/src/main/ssh/connection/ssh-connection-attribution.test.ts +++ b/src/main/ssh/connection/ssh-connection-attribution.test.ts @@ -6,9 +6,9 @@ import { recordSshConnectionOpened, runAttributedToSshOwner } from './ssh-connection-attribution' +import { unusedSshConnection } from '../unused-host-transport.test-fixture' -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: attribution keys on identity only. -const transport = (): SshConnection => ({}) as SshConnection +const transport = (): SshConnection => unusedSshConnection() describe('SSH transport attribution', () => { it('lets a cancelled attempt own only the transport its own work opened', async () => { diff --git a/src/main/ssh/legacy-relay/legacy-relay-bridge-restart-stop.test.ts b/src/main/ssh/legacy-relay/legacy-relay-bridge-restart-stop.test.ts index 2221aeb683a..bd1a3e0e669 100644 --- a/src/main/ssh/legacy-relay/legacy-relay-bridge-restart-stop.test.ts +++ b/src/main/ssh/legacy-relay/legacy-relay-bridge-restart-stop.test.ts @@ -1,8 +1,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { toAppSshPtyId } from '../../../shared/ssh-pty-id' import type { SshRemotePtyLease } from '../../../shared/ssh-types' -import type { SshConnection } from '../connection/ssh-connection' import { getRemoteHostPlatform } from '../ssh-remote-platform' +import { unusedSshConnection } from '../unused-host-transport.test-fixture' const TARGET = 'ssh-box' const RELAY_PTY = 'pty2:epoch:1' @@ -61,8 +61,7 @@ let leases: SshRemotePtyLease[] let connected: boolean function installHost(): void { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked endpoints and client never touch the connection. - const connection = {} as SshConnection + const connection = unusedSshConnection() registry.installLegacyRelayBridge({ leases: () => leases, retireLease: (_target, relayPtyId) => { diff --git a/src/main/ssh/legacy-relay/legacy-relay-bridge-runtime-exit.test.ts b/src/main/ssh/legacy-relay/legacy-relay-bridge-runtime-exit.test.ts index 45697916195..78fed8b8ee6 100644 --- a/src/main/ssh/legacy-relay/legacy-relay-bridge-runtime-exit.test.ts +++ b/src/main/ssh/legacy-relay/legacy-relay-bridge-runtime-exit.test.ts @@ -5,11 +5,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { toAppSshPtyId } from '../../../shared/ssh-pty-id' import type { SshRemotePtyLease } from '../../../shared/ssh-types' -import type { SshConnection } from '../connection/ssh-connection' import { getRemoteHostPlatform } from '../ssh-remote-platform' import type { PtyRuntimeControllerDeps } from '../../ipc/pty/runtime/controller-deps' import type { OrcaRuntimeService } from '../../runtime/orca-runtime' import { LEGACY_RELAY_BRIDGE_CHANNELS } from '../../../shared/legacy-relay-bridge-contract' +import { unusedSshConnection } from '../unused-host-transport.test-fixture' const TARGET = 'ssh-box' const RELAY_PTY = 'pty2:epoch:1' @@ -83,8 +83,7 @@ let runtime: { } async function startBridge(): Promise { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked endpoints and client never touch the connection. - const connection = {} as SshConnection + const connection = unusedSshConnection() registerLegacyRelayBridgeIpc({ store: { getSshRemotePtyLeases: () => leases, diff --git a/src/main/ssh/legacy-relay/legacy-relay-bridge-teardown.test.ts b/src/main/ssh/legacy-relay/legacy-relay-bridge-teardown.test.ts index bcc86aa430e..f4dd786f1d7 100644 --- a/src/main/ssh/legacy-relay/legacy-relay-bridge-teardown.test.ts +++ b/src/main/ssh/legacy-relay/legacy-relay-bridge-teardown.test.ts @@ -2,9 +2,9 @@ import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest' import type { LegacyRelayBridgedPane } from '../../../shared/legacy-relay-bridge-contract' import { toAppSshPtyId } from '../../../shared/ssh-pty-id' import type { SshRemotePtyLease } from '../../../shared/ssh-types' -import type { SshConnection } from '../connection/ssh-connection' import type { LegacyRelayTargetBridgeDeps } from './legacy-relay-target-bridge' import type { LegacyRelayBridgeHost } from './legacy-relay-bridge-registry' +import { unusedSshConnection } from '../unused-host-transport.test-fixture' const TARGET = 'ssh-box' const PTY = toAppSshPtyId(TARGET, 'pty-1') @@ -152,8 +152,7 @@ describe('a hung-up legacy relay bridge (loss of contact is not exit)', () => { describe('attaching a bridged pane', () => { it('sends a reattach screen in output order and only once', async () => { const replays: string[] = [] - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the attach only checks a connection exists. - const connection = {} as SshConnection + const connection = unusedSshConnection() registry.installLegacyRelayBridge({ leases: () => leases, retireLease: vi.fn(), diff --git a/src/main/ssh/legacy-relay/ssh-host-relay-census-for-target.test.ts b/src/main/ssh/legacy-relay/ssh-host-relay-census-for-target.test.ts index d65b8d272ec..861425e7a53 100644 --- a/src/main/ssh/legacy-relay/ssh-host-relay-census-for-target.test.ts +++ b/src/main/ssh/legacy-relay/ssh-host-relay-census-for-target.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { SshConnection } from '../connection/ssh-connection' import type { SshTarget } from '../../../shared/ssh-types' +import { unusedSshConnection } from '../unused-host-transport.test-fixture' const mocks = vi.hoisted(() => ({ connect: vi.fn(), @@ -26,8 +27,7 @@ const { censusHostRelayTerminalsFor } = await import('./ssh-host-relay-census-fo const attribution = await import('../connection/ssh-connection-attribution') const target: SshTarget = { id: 'ssh-1', label: 'Box', host: 'box', port: 22, username: 'me' } -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: attribution keys on identity only. -const transport = (): SshConnection => ({}) as SshConnection +const transport = (): SshConnection => unusedSshConnection() beforeEach(() => { vi.clearAllMocks() diff --git a/src/main/ssh/legacy-relay/ssh-relay-endpoint-incumbent.test.ts b/src/main/ssh/legacy-relay/ssh-relay-endpoint-incumbent.test.ts index 85d4267ec58..caad7d1d801 100644 --- a/src/main/ssh/legacy-relay/ssh-relay-endpoint-incumbent.test.ts +++ b/src/main/ssh/legacy-relay/ssh-relay-endpoint-incumbent.test.ts @@ -16,11 +16,10 @@ import { probeRelayEndpointIncumbent, relayEndpointIncumbentProbeCommand } from './ssh-relay-endpoint-incumbent' -import type { SshConnection } from '../connection/ssh-connection' import { getRemoteHostPlatform } from '../ssh-remote-platform' +import { unusedSshConnection } from '../unused-host-transport.test-fixture' -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mocked execCommand never dereferences the connection; the Windows path returns before using it. -const connection = {} as SshConnection +const connection = unusedSshConnection() const SOCK = '/home/u/.orca-remote/relay-0.1.0+aaaa/relay-deadbeef.sock' const POSIX_HOST = getRemoteHostPlatform('linux-x64') diff --git a/src/main/ssh/orcad-exited-own-fence-mutation-race.test.ts b/src/main/ssh/orcad-exited-own-fence-mutation-race.test.ts index dbc2a47acde..46a64871fe3 100644 --- a/src/main/ssh/orcad-exited-own-fence-mutation-race.test.ts +++ b/src/main/ssh/orcad-exited-own-fence-mutation-race.test.ts @@ -12,6 +12,7 @@ import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { runProcess, spawnProcess } from '@orca/process-host' import type * as ExecCommandModule from './remote-exec/remote-exec-command' +import { unusedOrcadHostTransport } from './unused-host-transport.test-fixture' // The proof's remote commands run in a real local shell. vi.mock('./remote-exec/remote-exec-command', async (importOriginal) => { @@ -71,8 +72,7 @@ async function fenceOfExitedHolder() { ) const host = getRemoteHostPlatform('linux-x64') const proof = exitedOwnLockProof( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: execCommand is mocked to a local shell, so the connection is never used. - { conn: {} as never, host }, + { conn: unusedOrcadHostTransport(), host }, { baseDir: dir, guardsStateMutation: true } ) const token = await proof.find(lock) diff --git a/src/main/ssh/orcad-exited-own-fence.test.ts b/src/main/ssh/orcad-exited-own-fence.test.ts index a5425af61e0..853f49e980c 100644 --- a/src/main/ssh/orcad-exited-own-fence.test.ts +++ b/src/main/ssh/orcad-exited-own-fence.test.ts @@ -12,6 +12,7 @@ import { hostname, tmpdir, uptime } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import type * as ExecCommandModule from './remote-exec/remote-exec-command' +import { unusedOrcadHostTransport } from './unused-host-transport.test-fixture' // Every remote command runs in a real local shell, so the host-side checks are the real ones. const shell = vi.hoisted((): { env: NodeJS.ProcessEnv | undefined } => ({ env: undefined })) @@ -65,8 +66,11 @@ function hostWithFence(owner: string, held: ReturnType[]) { initOrcadHeldFenceTokenFile(join(home, 'data', 'orca-data.json')) const store = join(home, 'data', ORCAD_HELD_FENCE_TOKENS_FILE_NAME) writeFileSync(store, JSON.stringify(held)) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: execCommand is mocked to a local shell, so the connection is never used. - const options = { conn: {} as never, host: getRemoteHostPlatform('linux-x64'), remoteHome: home } + const options = { + conn: unusedOrcadHostTransport(), + host: getRemoteHostPlatform('linux-x64'), + remoteHome: home + } return { home, fence, store, options } } diff --git a/src/main/ssh/orcad-exited-own-install-lock.test.ts b/src/main/ssh/orcad-exited-own-install-lock.test.ts index 4da144676c9..9e98e81ac58 100644 --- a/src/main/ssh/orcad-exited-own-install-lock.test.ts +++ b/src/main/ssh/orcad-exited-own-install-lock.test.ts @@ -12,6 +12,7 @@ import { hostname, tmpdir, uptime } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import type * as ExecCommandModule from './remote-exec/remote-exec-command' +import { unusedOrcadHostTransport } from './unused-host-transport.test-fixture' // Every remote command runs in a real local shell, so the host-side checks are the real ones. vi.mock('./remote-exec/remote-exec-command', async (importOriginal) => { @@ -64,8 +65,7 @@ function versionDirWithLock(owner: string, heldToken: string) { { token: heldToken, pid: EXITED_PID, host: hostname(), bootedAt, at: Date.now() } ]) ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: execCommand is mocked to a local shell, so the connection is never used. - const target = { conn: {} as never, host: getRemoteHostPlatform('linux-x64') } + const target = { conn: unusedOrcadHostTransport(), host: getRemoteHostPlatform('linux-x64') } const proof = exitedOwnLockProof(target, { baseDir: join(home, '.orca-remote'), guardsStateMutation: false diff --git a/src/main/ssh/orcad-exited-own-lock-windows.test.ts b/src/main/ssh/orcad-exited-own-lock-windows.test.ts index 0aa79ce53fb..fe6ffb468a2 100644 --- a/src/main/ssh/orcad-exited-own-lock-windows.test.ts +++ b/src/main/ssh/orcad-exited-own-lock-windows.test.ts @@ -3,6 +3,7 @@ import { hostname, tmpdir, uptime } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as ExecCommandModule from './remote-exec/remote-exec-command' +import { unusedOrcadHostTransport } from './unused-host-transport.test-fixture' // The host script's own checks are covered against the real script; here only the client's part. const remote = vi.hoisted((): { commands: string[]; reply: string } => ({ @@ -48,8 +49,7 @@ function hold(...entries: { token: string; pid: number }[]): void { } function find(guardsStateMutation: boolean): Promise { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: execCommand is mocked, so the connection is never used. - const target = { conn: {} as never, host } + const target = { conn: unusedOrcadHostTransport(), host } return findExitedOwnLockToken(target, lockDir, { baseDir, guardsStateMutation }) } @@ -89,8 +89,7 @@ describe('reclaiming this desktop’s exited lock on a Windows host', () => { hold({ token: 't-exited', pid: EXITED_PID }) remote.reply = 'EXITED_OWNER t-exited\n' const deep = `C:/Users/me/${'nested-folder/'.repeat(200)}.orca-remote` - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: execCommand is mocked, so the connection is never used. - const target = { conn: {} as never, host } + const target = { conn: unusedOrcadHostTransport(), host } await expect( findExitedOwnLockToken(target, `${deep}/.install-lock`, { baseDir: deep, diff --git a/src/main/ssh/port-forwarding/ssh-port-forward.test.ts b/src/main/ssh/port-forwarding/ssh-port-forward.test.ts index daa41118243..a301477bbc3 100644 --- a/src/main/ssh/port-forwarding/ssh-port-forward.test.ts +++ b/src/main/ssh/port-forwarding/ssh-port-forward.test.ts @@ -2,6 +2,7 @@ import { EventEmitter } from 'node:events' import { createServer } from 'node:net' import { describe, expect, it, vi, beforeEach } from 'vitest' import { PortForwardTornDownError, SshPortForwardManager } from './ssh-port-forward' +import type { SshConnection } from '../connection/ssh-connection' const { startSystemSshPortForwardProcessMock } = vi.hoisted(() => ({ startSystemSshPortForwardProcessMock: vi.fn() @@ -55,6 +56,16 @@ function createSystemSshConn() { } } +type ConnectionStub = Pick< + ReturnType, + 'getClient' | 'isDisposed' | 'usesSystemSshTransport' +> + +function asSshConnection(conn: ConnectionStub): SshConnection { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the manager and its providers read only the members these stubs define. + return conn as unknown as SshConnection +} + function createFakeSystemSshForward(localPort = 3000) { const process = Object.assign(new EventEmitter(), { stderr: new EventEmitter() }) return { @@ -127,15 +138,14 @@ describe('SshPortForwardManager', () => { it('reports the port the OS bound when an ssh2 forward asks for port 0', async () => { const conn = createMockConn() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements the getClient/usesSystemSshTransport surface the ssh2 provider reads. - const entry = await manager.addForward('conn-1', conn as never, 0, '127.0.0.1', 6768) + const entry = await manager.addForward('conn-1', asSshConnection(conn), 0, '127.0.0.1', 6768) expect(entry.localPort).toBe(43210) }) it('adds an ssh2 port forward and returns entry', async () => { const conn = createMockConn() - const entry = await manager.addForward('conn-1', conn as never, 3000, 'localhost', 8080) + const entry = await manager.addForward('conn-1', asSshConnection(conn), 3000, 'localhost', 8080) expect(entry).toMatchObject({ connectionId: 'conn-1', @@ -153,7 +163,7 @@ describe('SshPortForwardManager', () => { usesSystemSshTransport: vi.fn().mockReturnValue(false) } await expect( - manager.addForward('conn-1', conn as never, 3000, 'localhost', 8080) + manager.addForward('conn-1', asSshConnection(conn), 3000, 'localhost', 8080) ).rejects.toThrow('SSH connection is not established') }) @@ -162,7 +172,7 @@ describe('SshPortForwardManager', () => { startSystemSshPortForwardProcessMock.mockReturnValue(forward) const conn = createSystemSshConn() - const entry = await manager.addForward('conn-1', conn as never, 3000, '127.0.0.1', 8080) + const entry = await manager.addForward('conn-1', asSshConnection(conn), 3000, '127.0.0.1', 8080) expect(startSystemSshPortForwardProcessMock).toHaveBeenCalledWith( conn.getTarget(), @@ -185,8 +195,7 @@ describe('SshPortForwardManager', () => { startSystemSshPortForwardProcessMock.mockReturnValue(createFakeSystemSshForward(49800)) const conn = createSystemSshConn() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements the system-SSH surface the provider reads. - const entry = await manager.addForward('conn-1', conn as never, 0, '127.0.0.1', 6768) + const entry = await manager.addForward('conn-1', asSshConnection(conn), 0, '127.0.0.1', 6768) expect(entry.localPort).toBe(49800) }) @@ -209,7 +218,7 @@ describe('SshPortForwardManager', () => { } }) - await manager.addForward('conn-1', conn as never, 3000, '127.0.0.1', 8080) + await manager.addForward('conn-1', asSshConnection(conn), 3000, '127.0.0.1', 8080) expect(startSystemSshPortForwardProcessMock).toHaveBeenCalledWith( conn.getTarget(), @@ -233,7 +242,7 @@ describe('SshPortForwardManager', () => { const conn = createSystemSshConn() await expect( - manager.addForward('conn-1', conn as never, 3000, '127.0.0.1', 8080) + manager.addForward('conn-1', asSshConnection(conn), 3000, '127.0.0.1', 8080) ).rejects.toThrow('bind: Address already in use') expect(manager.listForwards('conn-1')).toHaveLength(0) }) @@ -243,7 +252,7 @@ describe('SshPortForwardManager', () => { startSystemSshPortForwardProcessMock.mockReturnValue(forward) const conn = createSystemSshConn() - const entry = await manager.addForward('conn-1', conn as never, 3000, '127.0.0.1', 8080) + const entry = await manager.addForward('conn-1', asSshConnection(conn), 3000, '127.0.0.1', 8080) expect(manager.removeForward(entry.id)).toMatchObject({ id: entry.id }) expect(forward.dispose).toHaveBeenCalled() @@ -261,7 +270,7 @@ describe('SshPortForwardManager', () => { startSystemSshPortForwardProcessMock.mockReturnValue(forward) const conn = createSystemSshConn() - await manager.addForward('conn-1', conn as never, 3000, '127.0.0.1', 8080) + await manager.addForward('conn-1', asSshConnection(conn), 3000, '127.0.0.1', 8080) let resolved = false const removal = manager.removeAllForwards('conn-1').then(() => { @@ -286,8 +295,7 @@ describe('SshPortForwardManager', () => { startSystemSshPortForwardProcessMock.mockReturnValue(forward) const entry = await manager.addForward( 'conn-1', - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the system SSH provider reads only the members createSystemSshConn stubs. - createSystemSshConn() as never, + asSshConnection(createSystemSshConn()), 3000, '127.0.0.1', 8080 @@ -315,8 +323,7 @@ describe('SshPortForwardManager', () => { ) const adding = manager.addForward( 'conn-1', - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the system SSH provider reads only the members createSystemSshConn stubs. - createSystemSshConn() as never, + asSshConnection(createSystemSshConn()), 3000, '127.0.0.1', 8080 @@ -342,8 +349,7 @@ describe('SshPortForwardManager', () => { finishStart = resolve }) ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the system SSH provider reads only the members createSystemSshConn stubs. - const conn = createSystemSshConn() as never + const conn = asSshConnection(createSystemSshConn()) const adding = manager.addForward('conn-1', conn, 3000, '127.0.0.1', 8080) adding.catch(() => undefined) await vi.waitFor(() => expect(startSystemSshPortForwardProcessMock).toHaveBeenCalled()) @@ -363,8 +369,7 @@ describe('SshPortForwardManager', () => { it('does not roll an edit back onto a connection torn down while it started', async () => { const first = createFakeSystemSshForward() startSystemSshPortForwardProcessMock.mockReturnValueOnce(first) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the system SSH provider reads only the members createSystemSshConn stubs. - const conn = createSystemSshConn() as never + const conn = asSshConnection(createSystemSshConn()) const entry = await manager.addForward('conn-1', conn, 3000, '127.0.0.1', 8080) let finishStart!: (value: ReturnType) => void startSystemSshPortForwardProcessMock.mockReturnValueOnce( @@ -392,8 +397,7 @@ describe('SshPortForwardManager', () => { usesSystemSshTransport: vi.fn().mockReturnValue(false) } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the system SSH provider reads only the members createSystemSshConn stubs. - const adding = manager.addForward('conn-1', conn as never, 3000, '127.0.0.1', 8080) + const adding = manager.addForward('conn-1', asSshConnection(conn), 3000, '127.0.0.1', 8080) await expect(adding).rejects.toThrow(PortForwardTornDownError) expect(startSystemSshPortForwardProcessMock).not.toHaveBeenCalled() @@ -410,8 +414,7 @@ describe('SshPortForwardManager', () => { ) startSystemSshPortForwardProcessMock.mockReturnValue(first) const conn = createSystemSshConn() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the system SSH provider reads only the members createSystemSshConn stubs. - const session = conn as never + const session = asSshConnection(conn) const entry = await manager.addForward('conn-1', session, 3000, '127.0.0.1', 8080) const editing = manager.updateForward(entry.id, session, 3001, '127.0.0.1', 8080) @@ -434,7 +437,7 @@ describe('SshPortForwardManager', () => { startSystemSshPortForwardProcessMock.mockReturnValue(forward) const conn = createSystemSshConn() - const entry = await manager.addForward('conn-1', conn as never, 3000, '127.0.0.1', 8080) + const entry = await manager.addForward('conn-1', asSshConnection(conn), 3000, '127.0.0.1', 8080) forward.process.stderr.emit('data', Buffer.from('channel open failed')) forward.process.emit('exit', 255) @@ -455,7 +458,7 @@ describe('SshPortForwardManager', () => { startSystemSshPortForwardProcessMock.mockReturnValue(forward) const conn = createSystemSshConn() - await manager.addForward('conn-1', conn as never, 3000, '127.0.0.1', 8080) + await manager.addForward('conn-1', asSshConnection(conn), 3000, '127.0.0.1', 8080) // A long-lived forward against a chatty remote sshd: emit >64 KB of stderr. forward.process.stderr.emit('data', Buffer.from(`HEAD_MARKER${'x'.repeat(70 * 1024)}`)) forward.process.stderr.emit('data', Buffer.from('TAIL_MARKER')) @@ -471,9 +474,9 @@ describe('SshPortForwardManager', () => { it('lists forwards filtered by connectionId', async () => { const conn = createMockConn() - await manager.addForward('conn-1', conn as never, 3000, 'localhost', 8080) - await manager.addForward('conn-2', conn as never, 3001, 'localhost', 8081) - await manager.addForward('conn-1', conn as never, 3002, 'localhost', 8082) + await manager.addForward('conn-1', asSshConnection(conn), 3000, 'localhost', 8080) + await manager.addForward('conn-2', asSshConnection(conn), 3001, 'localhost', 8081) + await manager.addForward('conn-1', asSshConnection(conn), 3002, 'localhost', 8082) expect(manager.listForwards('conn-1')).toHaveLength(2) expect(manager.listForwards('conn-2')).toHaveLength(1) @@ -482,7 +485,7 @@ describe('SshPortForwardManager', () => { it('removes a forward by id', async () => { const conn = createMockConn() - const entry = await manager.addForward('conn-1', conn as never, 3000, 'localhost', 8080) + const entry = await manager.addForward('conn-1', asSshConnection(conn), 3000, 'localhost', 8080) const removed = manager.removeForward(entry.id) expect(removed).toMatchObject({ id: entry.id, localPort: 3000 }) @@ -491,7 +494,7 @@ describe('SshPortForwardManager', () => { it('awaits forward close when removing a forward for user actions', async () => { const conn = createMockConn() - const entry = await manager.addForward('conn-1', conn as never, 3000, 'localhost', 8080) + const entry = await manager.addForward('conn-1', asSshConnection(conn), 3000, 'localhost', 8080) await expect(manager.removeForwardAndWait(entry.id)).resolves.toMatchObject({ id: entry.id, @@ -502,7 +505,7 @@ describe('SshPortForwardManager', () => { it('destroys local sockets that emit errors', async () => { const conn = createMockConn() - await manager.addForward('conn-1', conn as never, 3000, 'localhost', 8080) + await manager.addForward('conn-1', asSshConnection(conn), 3000, 'localhost', 8080) const server = getLastMockServer() const socket = createFakeSocket() @@ -517,8 +520,7 @@ describe('SshPortForwardManager', () => { conn.mockClient.forwardOut.mockImplementation(() => { throw new Error('Not connected') }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements the getClient/usesSystemSshTransport surface the ssh2 provider reads. - await manager.addForward('conn-1', conn as never, 3000, 'localhost', 8080) + await manager.addForward('conn-1', asSshConnection(conn), 3000, 'localhost', 8080) const socket = createFakeSocket() expect(() => getLastMockServer()?._connectionHandler(socket)).not.toThrow() @@ -542,7 +544,7 @@ describe('SshPortForwardManager', () => { isDisposed: vi.fn().mockReturnValue(false), usesSystemSshTransport: vi.fn().mockReturnValue(false) } - const entry = await manager.addForward('conn-1', conn as never, 3000, 'localhost', 8080) + const entry = await manager.addForward('conn-1', asSshConnection(conn), 3000, 'localhost', 8080) const server = getLastMockServer() const socket = createFakeSocket() @@ -560,9 +562,9 @@ describe('SshPortForwardManager', () => { it('removes all forwards for a connection', async () => { const conn = createMockConn() - await manager.addForward('conn-1', conn as never, 3000, 'localhost', 8080) - await manager.addForward('conn-1', conn as never, 3001, 'localhost', 8081) - await manager.addForward('conn-2', conn as never, 3002, 'localhost', 8082) + await manager.addForward('conn-1', asSshConnection(conn), 3000, 'localhost', 8080) + await manager.addForward('conn-1', asSshConnection(conn), 3001, 'localhost', 8081) + await manager.addForward('conn-2', asSshConnection(conn), 3002, 'localhost', 8082) await manager.removeAllForwards('conn-1') expect(manager.listForwards()).toHaveLength(1) @@ -571,8 +573,7 @@ describe('SshPortForwardManager', () => { it('closes only the ssh2 forwards bound to a client the connection replaced', async () => { const conn = createMockConn() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements the getClient/usesSystemSshTransport surface the ssh2 provider reads. - const sshConn = conn as never + const sshConn = asSshConnection(conn) await manager.addForward('conn-1', sshConn, 3000, 'localhost', 8080) await manager.addForward('conn-2', sshConn, 3002, 'localhost', 8082) @@ -597,8 +598,7 @@ describe('SshPortForwardManager', () => { ) const second = createFakeSystemSshForward(3001) startSystemSshPortForwardProcessMock.mockReturnValueOnce(first).mockReturnValueOnce(second) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock implements the system-SSH surface the provider reads. - const conn = createSystemSshConn() as never + const conn = asSshConnection(createSystemSshConn()) await manager.addForward('conn-1', conn, 3000, '127.0.0.1', 8080) await manager.addForward('conn-2', conn, 3001, '127.0.0.1', 8081) @@ -619,8 +619,8 @@ describe('SshPortForwardManager', () => { it('dispose removes all forwards', async () => { const conn = createMockConn() - await manager.addForward('conn-1', conn as never, 3000, 'localhost', 8080) - await manager.addForward('conn-2', conn as never, 3001, 'localhost', 8081) + await manager.addForward('conn-1', asSshConnection(conn), 3000, 'localhost', 8080) + await manager.addForward('conn-2', asSshConnection(conn), 3001, 'localhost', 8081) manager.dispose() expect(manager.listForwards()).toHaveLength(0) @@ -630,7 +630,7 @@ describe('SshPortForwardManager', () => { const conn = createMockConn() const entry = await manager.addForward( 'conn-1', - conn as never, + asSshConnection(conn), 3000, 'localhost', 8080, @@ -642,9 +642,15 @@ describe('SshPortForwardManager', () => { it('preserves the existing id when updating a forward succeeds', async () => { const conn = createMockConn() - const entry = await manager.addForward('conn-1', conn as never, 3000, 'localhost', 8080) + const entry = await manager.addForward('conn-1', asSshConnection(conn), 3000, 'localhost', 8080) - const updated = await manager.updateForward(entry.id, conn as never, 3001, 'localhost', 8081) + const updated = await manager.updateForward( + entry.id, + asSshConnection(conn), + 3001, + 'localhost', + 8081 + ) expect(updated.id).toBe(entry.id) expect(updated.localPort).toBe(3001) @@ -661,10 +667,10 @@ describe('SshPortForwardManager', () => { .mockReturnValueOnce(failedForward) .mockReturnValueOnce(rollbackForward) const conn = createSystemSshConn() - const entry = await manager.addForward('conn-1', conn as never, 3000, '127.0.0.1', 8080) + const entry = await manager.addForward('conn-1', asSshConnection(conn), 3000, '127.0.0.1', 8080) await expect( - manager.updateForward(entry.id, conn as never, 3001, '127.0.0.1', 8081) + manager.updateForward(entry.id, asSshConnection(conn), 3001, '127.0.0.1', 8081) ).rejects.toThrow('bind failed') expect(initialForward.close).toHaveBeenCalled() diff --git a/src/main/ssh/unused-host-transport.test-fixture.ts b/src/main/ssh/unused-host-transport.test-fixture.ts new file mode 100644 index 00000000000..bc0c09e7e22 --- /dev/null +++ b/src/main/ssh/unused-host-transport.test-fixture.ts @@ -0,0 +1,14 @@ +import type { SshConnection } from './connection/ssh-connection' +import type { OrcadHostTransport } from './orcad-host-transport' + +// Placeholders for tests that mock every reader of the connection, so it is only forwarded. + +export function unusedSshConnection(): SshConnection { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: callers mock all code that would read the connection; it is only passed through. + return {} as SshConnection +} + +export function unusedOrcadHostTransport(): OrcadHostTransport { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: callers mock all code that would read the transport; it is only passed through. + return {} as OrcadHostTransport +} diff --git a/src/main/startup/hydrate-shell-path.ts b/src/main/startup/hydrate-shell-path.ts index 9e8bbd866df..cd739369d9a 100644 --- a/src/main/startup/hydrate-shell-path.ts +++ b/src/main/startup/hydrate-shell-path.ts @@ -1,6 +1,7 @@ import type { SpawnedProcess } from '@orca/process-host/process-spec' import { delimiter, win32 as pathWin32 } from 'node:path' import { spawnProcess } from '@orca/process-host' +import { stripAnsiEscapeSequences } from '../../shared/ansi-escape-sequences' import type { ShellHydrationFailureReason } from '../../shared/shell-path-hydration-types' import { resolveWindowsShellStartupFamily } from '../../shared/windows-terminal-shell' import { SHELL_PATH_DELIMITER as DELIMITER, shellPathProbe } from './shell-path-probe' @@ -25,10 +26,6 @@ import { WindowsShellPathOwnership, createWindowsPathKey } from './windows-shell // those users were otherwise stuck with a wrong PATH for the whole session. const SPAWN_TIMEOUT_MS = 10_000 -// ANSI escape sequences can leak into the captured output when the user's rc -// files print banners or set colored prompts. Strip them before parsing. -const ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g // eslint-disable-line no-control-regex - // Why: the discriminator lets telemetry classify *why* hydration failed, not // just whether it did. Five resolve sites in this file each tag the result // with the right reason. The shared alias keeps the enum in lockstep with the @@ -101,7 +98,8 @@ export const resolveProfileLoadingFallbackShell = (): string | null => configuredWindowsFallbackShell function parseCapturedPath(stdout: string, pathDelimiter: string = delimiter): string[] { - const cleaned = stdout.replace(ANSI_RE, '') + // Why: rc-file banners and colored prompts can leak escapes into the captured output. + const cleaned = stripAnsiEscapeSequences(stdout) const first = cleaned.indexOf(DELIMITER) if (first === -1) { return [] diff --git a/src/main/updater-sqlite-profile-downgrade.test.ts b/src/main/updater-sqlite-profile-downgrade.test.ts index f7a2934ef71..790dddc0448 100644 --- a/src/main/updater-sqlite-profile-downgrade.test.ts +++ b/src/main/updater-sqlite-profile-downgrade.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { loadUpdaterModule, warmUpdaterModule } from './updater-test-module-loader' +import { updaterMainWindow } from './updater-test-main-window' const { appMock, autoUpdaterMock, chooseLocalBuildMock, moduleFactories, resetUpdaterMocks } = await vi.hoisted(async () => (await import('./updater-test-harness')).createUpdaterMocks()) @@ -29,8 +30,7 @@ describe('updater SQLite downgrade protection', () => { appMock.getVersion.mockReturnValue('1.4.221') const send = vi.fn() const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mocked updater only reads webContents.send from this window fixture. - setupAutoUpdater({ webContents: { send } } as never, { + setupAutoUpdater(updaterMainWindow(send), { getLastUpdateCheckAt: () => Date.now() }) autoUpdaterMock.setFeedURL.mockClear() @@ -58,8 +58,7 @@ describe('updater SQLite downgrade protection', () => { chooseLocalBuildMock.mockResolvedValue({ version: '1.4.213', close }) const send = vi.fn() const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mocked updater only reads webContents.send from this window fixture. - setupAutoUpdater({ webContents: { send } } as never, { + setupAutoUpdater(updaterMainWindow(send), { getLastUpdateCheckAt: () => Date.now() }) checkForUpdatesFromMenu({ localBuild: true }) diff --git a/src/main/updater-test-main-window.ts b/src/main/updater-test-main-window.ts new file mode 100644 index 00000000000..17e8db84e03 --- /dev/null +++ b/src/main/updater-test-main-window.ts @@ -0,0 +1,8 @@ +import type { BrowserWindow } from 'electron' + +export function updaterMainWindow( + send: (channel: string, ...args: unknown[]) => void +): BrowserWindow { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the updater reaches its window only through webContents.send. + return { webContents: { send } } as unknown as BrowserWindow +} diff --git a/src/main/updater.check-preflight.test.ts b/src/main/updater.check-preflight.test.ts index a55338c265c..6595790dbf0 100644 --- a/src/main/updater.check-preflight.test.ts +++ b/src/main/updater.check-preflight.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { loadUpdaterModule, warmUpdaterModule } from './updater-test-module-loader' +import { updaterMainWindow } from './updater-test-main-window' const { appMock, @@ -60,8 +61,7 @@ describe('updater', () => { quitAndInstall, getUpdateStatus } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The updater reads only webContents.send from this window fixture. - setupAutoUpdater({ webContents: { send } } as never, { + setupAutoUpdater(updaterMainWindow(send), { getLastUpdateCheckAt: () => Date.now(), onBeforeQuit, onBeforeQuitFailure: 'abort' diff --git a/src/main/updater.feed-attempt-lifetime.test.ts b/src/main/updater.feed-attempt-lifetime.test.ts index c323a599e53..6210888f18e 100644 --- a/src/main/updater.feed-attempt-lifetime.test.ts +++ b/src/main/updater.feed-attempt-lifetime.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { loadUpdaterModule, warmUpdaterModule } from './updater-test-module-loader' +import { updaterMainWindow } from './updater-test-main-window' const { autoUpdaterMock, fetchNewerReleaseTagsMock, moduleFactories, resetUpdaterMocks } = await vi.hoisted(async () => (await import('./updater-test-harness')).createUpdaterMocks()) @@ -42,9 +43,7 @@ function holdFirstPreflight(): (value: FeedResult) => void { async function setup() { const updater = await loadUpdaterModule() - const mainWindow = { webContents: { send: vi.fn() } } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Only webContents.send is used; all native Electron APIs are mocked. - updater.setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => Date.now() }) + updater.setupAutoUpdater(updaterMainWindow(vi.fn()), { getLastUpdateCheckAt: () => Date.now() }) return updater } diff --git a/src/main/updater.headless-serve-install.test.ts b/src/main/updater.headless-serve-install.test.ts index 670478012f0..3afc91a411d 100644 --- a/src/main/updater.headless-serve-install.test.ts +++ b/src/main/updater.headless-serve-install.test.ts @@ -1,12 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { loadUpdaterModule, warmUpdaterModule } from './updater-test-module-loader' import { MAC_DEFERRED_STAGING_TIMEOUT_MS } from './updater/updater-state' -import type { BrowserWindow } from 'electron' - -function windowSending(send: ReturnType): BrowserWindow { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the updater only reaches the window through webContents.send. - return { webContents: { send } } as unknown as BrowserWindow -} +import { updaterMainWindow } from './updater-test-main-window' const { appMock, @@ -478,7 +473,7 @@ describe('headless serve update install handoff', () => { const { checkForUpdatesFromMenu, downloadUpdate, quitAndInstall, setupAutoUpdater } = await loadUpdaterModule() - setupAutoUpdater(windowSending(send), { + setupAutoUpdater(updaterMainWindow(send), { getLastUpdateCheckAt: () => Date.now(), installMode: 'supervised-headless-serve' }) diff --git a/src/main/updater.mac-install.test.ts b/src/main/updater.mac-install.test.ts index d3094de116a..bfe1d5d8139 100644 --- a/src/main/updater.mac-install.test.ts +++ b/src/main/updater.mac-install.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as ServeSupervisorInstallPark from './updater/serve-supervisor-install-park' import { loadUpdaterModule, warmUpdaterModule } from './updater-test-module-loader' +import { updaterMainWindow } from './updater-test-main-window' const { appMock, @@ -285,8 +286,7 @@ describe('updater mac install handoff', () => { checkForUpdatesFromMenu, getUpdateStatus } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The updater reads only webContents.send from this window fixture. - setupAutoUpdater({ webContents: { send } } as never) + setupAutoUpdater(updaterMainWindow(send)) await vi.waitFor(() => expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledOnce()) autoUpdaterMock.emit('checking-for-update') autoUpdaterMock.emit('update-available', { version: '1.0.61' }) @@ -463,8 +463,7 @@ describe('updater mac install handoff', () => { autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined) const { setupAutoUpdater, downloadUpdate, quitAndInstall, isQuittingForUpdate } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The updater only reads webContents.send from this window fixture. - setupAutoUpdater({ webContents: { send } } as never, { onBeforeQuit }) + setupAutoUpdater(updaterMainWindow(send), { onBeforeQuit }) await prepareStagedMacUpdate(downloadUpdate) getMacUpdateRunningInstancesMock.mockResolvedValue([9654, 10718]) let finishDescription: () => void = () => undefined @@ -524,8 +523,7 @@ describe('updater mac install handoff', () => { const onBeforeQuit = vi.fn() autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined) const { setupAutoUpdater, downloadUpdate, quitAndInstall } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Only webContents.send is read from this window fixture. - setupAutoUpdater({ webContents: { send } } as never, { onBeforeQuit }) + setupAutoUpdater(updaterMainWindow(send), { onBeforeQuit }) await prepareStagedMacUpdate(downloadUpdate) getMacUpdateRunningInstancesMock.mockResolvedValue([9654]) recoverBrowserSidecarsMock.mockResolvedValue([]) @@ -557,8 +555,7 @@ describe('updater mac install handoff', () => { autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined) const { setupAutoUpdater, downloadUpdate, quitAndInstall, isQuittingForUpdate } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The updater only reads webContents.send from this window fixture. - setupAutoUpdater({ webContents: { send } } as never, { onBeforeQuit }) + setupAutoUpdater(updaterMainWindow(send), { onBeforeQuit }) await prepareStagedMacUpdate(downloadUpdate) getMacUpdateRunningInstancesMock.mockRejectedValue(new Error('probe failed')) quitAndInstall() @@ -591,8 +588,7 @@ describe('updater mac install handoff', () => { const onBeforeQuit = vi.fn() autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined) const { setupAutoUpdater, downloadUpdate } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The updater only reads webContents.send from this window fixture. - setupAutoUpdater({ webContents: { send: vi.fn() } } as never, { onBeforeQuit }) + setupAutoUpdater(updaterMainWindow(vi.fn()), { onBeforeQuit }) await prepareStagedMacUpdate(downloadUpdate) const preventDefault = vi.fn() appMock.emit('before-quit', { preventDefault }) @@ -612,8 +608,7 @@ describe('updater mac install handoff', () => { const onBeforeQuit = vi.fn() autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined) const { setupAutoUpdater, downloadUpdate, quitAndInstall } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The updater only reads webContents.send from this window fixture. - setupAutoUpdater({ webContents: { send: vi.fn() } } as never, { onBeforeQuit }) + setupAutoUpdater(updaterMainWindow(vi.fn()), { onBeforeQuit }) await prepareStagedMacUpdate(downloadUpdate) let finishProbe: (pids: number[]) => void = () => {} @@ -643,8 +638,7 @@ describe('updater mac install handoff', () => { vi.useFakeTimers() autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined) const { setupAutoUpdater, downloadUpdate, quitAndInstall } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The updater only reads webContents.send from this window fixture. - setupAutoUpdater({ webContents: { send: vi.fn() } } as never) + setupAutoUpdater(updaterMainWindow(vi.fn())) await prepareStagedMacUpdate(downloadUpdate) getMacUpdateRunningInstancesMock.mockResolvedValue([9654]) quitAndInstall() @@ -672,8 +666,7 @@ describe('updater mac install handoff', () => { ) autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined) const { setupAutoUpdater, downloadUpdate, quitAndInstall } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The updater only reads webContents.send from this window fixture. - setupAutoUpdater({ webContents: { send: vi.fn() } } as never, { onBeforeQuit }) + setupAutoUpdater(updaterMainWindow(vi.fn()), { onBeforeQuit }) await prepareStagedMacUpdate(downloadUpdate) quitAndInstall() await vi.advanceTimersByTimeAsync(1000) @@ -697,8 +690,7 @@ describe('updater mac install handoff', () => { vi.useFakeTimers() autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined) const { setupAutoUpdater, downloadUpdate, quitAndInstall } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The updater only reads webContents.send from this window fixture. - setupAutoUpdater({ webContents: { send: vi.fn() } } as never) + setupAutoUpdater(updaterMainWindow(vi.fn())) await prepareStagedMacUpdate(downloadUpdate) getMacUpdateRunningInstancesMock.mockResolvedValue([9654]) quitAndInstall() @@ -731,8 +723,7 @@ describe('updater mac install handoff', () => { autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined) const onBeforeQuit = vi.fn().mockRejectedValue(new Error('required cleanup failed')) const { setupAutoUpdater, downloadUpdate, quitAndInstall } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The updater only reads webContents.send from this window fixture. - setupAutoUpdater({ webContents: { send: vi.fn() } } as never, { + setupAutoUpdater(updaterMainWindow(vi.fn()), { onBeforeQuit, onBeforeQuitFailure: 'abort' }) @@ -761,8 +752,7 @@ describe('updater mac install handoff', () => { checkForUpdatesFromMenu, getUpdateStatus } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The updater only reads webContents.send from this window fixture. - setupAutoUpdater({ webContents: { send: vi.fn() } } as never) + setupAutoUpdater(updaterMainWindow(vi.fn())) await prepareStagedMacUpdate(downloadUpdate) getMacUpdateRunningInstancesMock.mockResolvedValue([9654]) quitAndInstall() diff --git a/src/main/updater.quit-and-install.test.ts b/src/main/updater.quit-and-install.test.ts index 8868675508e..ff0b150d80e 100644 --- a/src/main/updater.quit-and-install.test.ts +++ b/src/main/updater.quit-and-install.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { PRE_COMMIT_INSTALL_FAILURE } from './updater-test-harness' import { loadUpdaterModule, warmUpdaterModule } from './updater-test-module-loader' +import { updaterMainWindow } from './updater-test-main-window' const { nativeUpdaterMock, @@ -231,10 +232,8 @@ describe('updater', () => { it('allows a required profile export to finish beyond the optional cleanup budget', async () => { vi.useFakeTimers() const onBeforeQuit = vi.fn(() => new Promise((resolve) => setTimeout(resolve, 5_000))) - const mainWindow = { webContents: { send: vi.fn() } } const { setupAutoUpdater, quitAndInstall } = await loadUpdaterModule() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: updater only reads the mocked webContents.send in this lifecycle test. - setupAutoUpdater(mainWindow as never, { onBeforeQuit, onBeforeQuitFailure: 'abort' }) + setupAutoUpdater(updaterMainWindow(vi.fn()), { onBeforeQuit, onBeforeQuitFailure: 'abort' }) quitAndInstall() await vi.advanceTimersByTimeAsync(2_600) expect(autoUpdaterMock.quitAndInstall).not.toHaveBeenCalled() diff --git a/src/main/wsl/wsl-orcad-incumbent.test.ts b/src/main/wsl/wsl-orcad-incumbent.test.ts index 834bbb6bf9d..297c6980b22 100644 --- a/src/main/wsl/wsl-orcad-incumbent.test.ts +++ b/src/main/wsl/wsl-orcad-incumbent.test.ts @@ -7,13 +7,13 @@ import { pairedRuntimeConnectionToken } from '../../shared/paired-runtime-connec import { DeviceRegistry } from '../runtime/device-registry' import { ORCAD_UPDATE_ADMISSION_FENCE_MS } from '../../shared/orcad-terminal-census' import { emptyOrcadActivationRecord } from '../managed-server/activation/orcad-activation-record' -import type { OrcadHostTransport } from '../ssh/orcad-host-transport' import type { OrcadRemoteContext } from '../managed-server/install/orcad-remote-context' import type { ServeReadiness } from '../server/serve-readiness' import { getRemoteHostPlatform } from '../ssh/ssh-remote-platform' import type { StartedPortForward } from '../ssh/port-forwarding/ssh-port-forward-provider' import { readWslOrcadIncumbent, type WslOrcadIncumbentDependencies } from './wsl-orcad-incumbent' import type { WslOrcadHostTarget } from './wsl-orcad-host-target' +import { unusedOrcadHostTransport } from '../ssh/unused-host-transport.test-fixture' const host = getRemoteHostPlatform('linux-x64') const KEY = Buffer.alloc(32, 7).toString('base64') @@ -53,8 +53,7 @@ const target: WslOrcadHostTarget = { distro: 'Ubuntu', label: 'Ubuntu' } -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every host call is a dependency stub. -const conn = {} as OrcadHostTransport +const conn = unusedOrcadHostTransport() function context(active: string | null = '1.4.0+aa'): OrcadRemoteContext { return { diff --git a/src/renderer/src/components/Landing.tsx b/src/renderer/src/components/Landing.tsx index eb309af405e..e78d4cd4f7e 100644 --- a/src/renderer/src/components/Landing.tsx +++ b/src/renderer/src/components/Landing.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useEffectEvent, useMemo, useRef, useState } from 'react' import { AlertTriangle, ExternalLink, FolderPlus, GitBranchPlus, Star, X } from 'lucide-react' import { cn } from '../lib/utils' import { useAppStore } from '../store' @@ -141,6 +141,14 @@ function GitHubStarButton({ ) } +function dismissedPreflightIssueIds(issues: PreflightIssue[], repos: readonly Repo[]): Set { + return new Set( + issues + .filter((issue) => issue.dismissible && isPreflightIssueDismissed(issue.id, repos)) + .map((issue) => issue.id) + ) +} + function PreflightBanner({ issues, repos @@ -152,26 +160,17 @@ function PreflightBanner({ // GitHub project (which changes the key) re-evaluates dismissals, so a lapsed // dismissal re-surfaces the nudge without a manual reset. const githubKey = githubProjectKeys(repos).join('|') - const [dismissed, setDismissed] = useState>( - () => - new Set( - issues - .filter((issue) => issue.dismissible && isPreflightIssueDismissed(issue.id, repos)) - .map((issue) => issue.id) - ) + const [dismissed, setDismissed] = useState>(() => + dismissedPreflightIssueIds(issues, repos) ) + // Why an event: re-seed only when the GitHub project set changes; issues identity is + // stable per render and would otherwise reset transient dismiss state. + const reseedDismissed = useEffectEvent(() => + setDismissed(dismissedPreflightIssueIds(issues, repos)) + ) useEffect(() => { - setDismissed( - new Set( - issues - .filter((issue) => issue.dismissible && isPreflightIssueDismissed(issue.id, repos)) - .map((issue) => issue.id) - ) - ) - // Why: re-seed only when the GitHub project set changes; issues identity is - // stable per render and would otherwise reset transient dismiss state. - // oxlint-disable-next-line react-hooks/exhaustive-deps + reseedDismissed() }, [githubKey]) const visibleIssues = issues.filter((issue) => !dismissed.has(issue.id)) diff --git a/src/renderer/src/components/LinearItemDrawer.tsx b/src/renderer/src/components/LinearItemDrawer.tsx index 21469c4846b..0d493237267 100644 --- a/src/renderer/src/components/LinearItemDrawer.tsx +++ b/src/renderer/src/components/LinearItemDrawer.tsx @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: Linear drawer state hydrates full issue details and comments from provider IPC for the selected issue. */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { findLinearIssueWorkspaceAttachment } from '@/lib/linear-issue-workspace-attachment' @@ -63,6 +62,7 @@ export default function LinearItemDrawer({ // Why: the list view may not include the full description. Re-fetch // the issue by ID and its comments to populate the drawer. + // oxlint-disable-next-line react-doctor/no-derived-state-effect -- Why: resets drafts as it starts the fenced IPC fetch for a new issue id. useEffect(() => { if (!issue) { setFullIssue(null) @@ -128,7 +128,6 @@ export default function LinearItemDrawer({ // Why: same pointer-events fix as GitHubItemDialog — Radix may leave // pointer-events: none on body when overlays transition. - // oxlint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { if (!issue?.id) { return diff --git a/src/renderer/src/components/activity/ActivityPrototypePage.tsx b/src/renderer/src/components/activity/ActivityPrototypePage.tsx index 2cf9c3d2b38..674fa063ccc 100644 --- a/src/renderer/src/components/activity/ActivityPrototypePage.tsx +++ b/src/renderer/src/components/activity/ActivityPrototypePage.tsx @@ -220,7 +220,6 @@ export default function ActivityPrototypePage(): React.JSX.Element { // Why useLayoutEffect (not useEffect): publish before paint so Terminal's portal subscriber rerenders in the same commit, else the stale target flashes on screen. // Why no cleanup-to-null on each change: it forces the portal through null on every switch, flashing the workspace pane; null only on unmount (effect below). - // oxlint-disable-next-line react-doctor/no-derived-state-effect -- Why: this publishes portal descriptors to Terminal's external portal store before paint. useLayoutEffect(() => { setActivityTerminalPortals(portalDescriptors) }, [portalDescriptors]) diff --git a/src/renderer/src/components/automations/automations-page-test-harness.tsx b/src/renderer/src/components/automations/automations-page-test-harness.tsx index e1bdd4d91b0..3634d8b729d 100644 --- a/src/renderer/src/components/automations/automations-page-test-harness.tsx +++ b/src/renderer/src/components/automations/automations-page-test-harness.tsx @@ -1,6 +1,3 @@ -/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 10 AutomationsPage specs, not shipped code, and it falls outside - the *.test / *.spec / tests glob set. Inlining these 13 stubs would duplicate them into all 10 specs and push the largest - past the max-lines ratchet. */ /** * The mount rig for AutomationsPage tests: child stand-ins, the preload API * double, and the per-test store reset. diff --git a/src/renderer/src/components/automations/use-automation-host-catalog.ts b/src/renderer/src/components/automations/use-automation-host-catalog.ts index f67ba085855..9ce0bb91fa6 100644 --- a/src/renderer/src/components/automations/use-automation-host-catalog.ts +++ b/src/renderer/src/components/automations/use-automation-host-catalog.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from 'react' import { useAppStore } from '@/store' import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides' import { getLocalExecutionHostLabel } from '../../../../shared/execution-host' @@ -194,9 +194,11 @@ export function useAutomationHostCatalog( ].join('\n'), [queryCatalog, selectedStableKey] ) + const applyCatalog = useEffectEvent( + () => void controller.applyCatalog(queryCatalog, { selectedStableKey }) + ) useEffect(() => { - void controller.applyCatalog(queryCatalog, { selectedStableKey }) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- applySignature is the apply policy: it covers the generation fingerprint plus every other field the scheduler reads, and keying off the catalog object would re-apply (and cancel in-flight work) on a label or health change. + applyCatalog() }, [applySignature, controller]) const catalog = useMemo( diff --git a/src/renderer/src/components/dashboard/useAgentBucketCounts.ts b/src/renderer/src/components/dashboard/useAgentBucketCounts.ts index 96f7b737253..fc81765a015 100644 --- a/src/renderer/src/components/dashboard/useAgentBucketCounts.ts +++ b/src/renderer/src/components/dashboard/useAgentBucketCounts.ts @@ -105,7 +105,7 @@ export function useAgentBucketCounts(): AgentBucketCounts { // Why Date.now() is read here and not a dep: idle-decay tracks agentStatusEpoch // ticks (carried in `state`), matching useDashboardData. That epoch doubles as the // cache generation, so a stale-boundary tick recounts every decayed bucket. + // oxlint-disable-next-line react/purity -- wall time is sampled per epoch tick, as explained above. return buildDashboardBucketCounts(state, Date.now(), cacheRef.current, state.agentStatusEpoch) - // eslint-disable-next-line react-hooks/exhaustive-deps }, [state]) } diff --git a/src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx b/src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx index 79411956204..92a9b715d65 100644 --- a/src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx +++ b/src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx @@ -1,6 +1,7 @@ // @vitest-environment happy-dom import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import type * as I18nModule from '@/i18n/i18n' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { OpenFile } from '@/store/slices/editor' import { EditorPanelHeaderPath } from './EditorPanelHeaderPath' @@ -25,7 +26,7 @@ vi.mock('@/hooks/useShortcutLabel', () => ({ })) vi.mock('@/i18n/i18n', async (importOriginal) => { - const actual = await importOriginal() // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.mock requires an inline import + const actual = await importOriginal() return { ...actual, translate: (_key: string, fallback: string, options?: { value0?: string }) => diff --git a/src/renderer/src/components/editor/ImageViewer.test.tsx b/src/renderer/src/components/editor/ImageViewer.test.tsx index 5e73b4ef4c2..0c25bf1177e 100644 --- a/src/renderer/src/components/editor/ImageViewer.test.tsx +++ b/src/renderer/src/components/editor/ImageViewer.test.tsx @@ -1,3 +1,4 @@ +import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' const reactHookRuntime = vi.hoisted(() => ({ @@ -6,7 +7,7 @@ const reactHookRuntime = vi.hoisted(() => ({ })) vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useMemo(factory: () => T) { diff --git a/src/renderer/src/components/editor/ImageViewer.tsx b/src/renderer/src/components/editor/ImageViewer.tsx index b44fc845d10..cb38979f333 100644 --- a/src/renderer/src/components/editor/ImageViewer.tsx +++ b/src/renderer/src/components/editor/ImageViewer.tsx @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: image surface size is measured with ResizeObserver and DOM refs, which are external layout systems outside render derivation. */ import { Image as ImageIcon, RotateCcw, ZoomIn, ZoomOut } from 'lucide-react' import { type JSX, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { cn } from '@/lib/utils' diff --git a/src/renderer/src/components/editor/MonacoEditor.tsx b/src/renderer/src/components/editor/MonacoEditor.tsx index 37d6c88f7da..59d22e528c3 100644 --- a/src/renderer/src/components/editor/MonacoEditor.tsx +++ b/src/renderer/src/components/editor/MonacoEditor.tsx @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: selection annotations are synchronized from Monaco editor selection and layout APIs, not derived React props. */ import React, { useRef, useEffect, useLayoutEffect, useMemo, useState } from 'react' import Editor from '@monaco-editor/react' import type { editor } from 'monaco-editor' diff --git a/src/renderer/src/components/editor/NotesSendMenu.test.tsx b/src/renderer/src/components/editor/NotesSendMenu.test.tsx index b8758850895..e26481a06f9 100644 --- a/src/renderer/src/components/editor/NotesSendMenu.test.tsx +++ b/src/renderer/src/components/editor/NotesSendMenu.test.tsx @@ -1,4 +1,5 @@ import React from 'react' +import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { buildNotesSendTargetModeId, NotesSendMenu } from './NotesSendMenu' import type { DiffCommentDeliverySnapshot } from '@/store/slices/diffComments' @@ -30,7 +31,7 @@ const storeMocks = vi.hoisted(() => ({ })) vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useCallback unknown>(callback: T): T { diff --git a/src/renderer/src/components/editor/PdfViewer.tsx b/src/renderer/src/components/editor/PdfViewer.tsx index af76a4e5a1d..2e09a858859 100644 --- a/src/renderer/src/components/editor/PdfViewer.tsx +++ b/src/renderer/src/components/editor/PdfViewer.tsx @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: PDF loading drives pdf.js document/viewer instances and decode errors through an external worker lifecycle. */ import { type JSX, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { Image as ImageIcon, RotateCcw, Search, ZoomIn, ZoomOut } from 'lucide-react' import * as pdfjsLib from 'pdfjs-dist' diff --git a/src/renderer/src/components/editor/ReviewNotesSendMenuContent.test.tsx b/src/renderer/src/components/editor/ReviewNotesSendMenuContent.test.tsx index af4c1e98445..cb532d86a31 100644 --- a/src/renderer/src/components/editor/ReviewNotesSendMenuContent.test.tsx +++ b/src/renderer/src/components/editor/ReviewNotesSendMenuContent.test.tsx @@ -1,5 +1,6 @@ import React from 'react' import { toast } from 'sonner' +import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { AgentStatusEntry, AgentStatusState } from '../../../../shared/agent-status-types' import type { TuiAgent } from '../../../../shared/tui-agent' @@ -35,7 +36,7 @@ const harness = vi.hoisted(() => ({ })) vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useCallback unknown>(callback: T): T { @@ -162,8 +163,8 @@ vi.mock('@/lib/focus-terminal-tab-surface', () => ({ })) // Why: real sonner state, so tests see which toast type a settled toast ends up with. -vi.mock('sonner', async () => { - const actual = await vi.importActual('sonner') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('sonner'), async (importOriginal) => { + const actual = await importOriginal() const realMessage = actual.toast.message const message: typeof realMessage = (...args) => { harness.toastMessage(...args) diff --git a/src/renderer/src/components/editor/editor-autosave-controller-test-fixture.ts b/src/renderer/src/components/editor/editor-autosave-controller-test-fixture.ts index ad68582e0c8..0a184293b96 100644 --- a/src/renderer/src/components/editor/editor-autosave-controller-test-fixture.ts +++ b/src/renderer/src/components/editor/editor-autosave-controller-test-fixture.ts @@ -99,7 +99,7 @@ export function stubEditorWindow(): ReturnType { export function createEditorStore(): StoreApi { // Why the record middleware: views of one file share a record only through it, as in the app. - // oxlint-disable-next-line typescript/no-explicit-any, typescript/consistent-type-assertions -- SAFETY: a partial store; tests read only the slices it builds. + // oxlint-disable-next-line typescript/no-explicit-any -- a partial store; tests read only the slices it builds. const create = withEditorBufferRecords( (...args: any[]) => ({ ...emptyTabLayoutState(), diff --git a/src/renderer/src/components/editor/ipynb-ansi.ts b/src/renderer/src/components/editor/ipynb-ansi.ts index 61b33c787f4..6de28ac6778 100644 --- a/src/renderer/src/components/editor/ipynb-ansi.ts +++ b/src/renderer/src/components/editor/ipynb-ansi.ts @@ -1,4 +1,7 @@ -import { stripAnsiEscapeSequences } from '../../../../shared/ansi-escape-sequences' +import { + ANSI_INTRODUCER_PATTERN, + stripAnsiEscapeSequences +} from '../../../../shared/ansi-escape-sequences' /** 0-15 index the themed ANSI palette; strings are explicit CSS colors. */ export type AnsiColor = number | string @@ -15,8 +18,6 @@ export type AnsiSegment = AnsiStyle & { text: string } // oxlint-disable-next-line no-control-regex -- matches the ESC byte that introduces SGR sequences. const SGR_PATTERN = /\u001b\[([0-9;]*)m/g -// oxlint-disable-next-line no-control-regex -- introducers accepted by the SGR parser and shared ANSI stripping patterns. -const ANSI_INTRODUCER_PATTERN = /[\u001b\u0090\u0098\u009b\u009d-\u009f]/ function xterm256Color(index: number): AnsiColor { if (index < 16) { diff --git a/src/renderer/src/components/editor/rich-markdown-extensions.ts b/src/renderer/src/components/editor/rich-markdown-extensions.ts index 28b763b2d4f..2fa180e7332 100644 --- a/src/renderer/src/components/editor/rich-markdown-extensions.ts +++ b/src/renderer/src/components/editor/rich-markdown-extensions.ts @@ -29,7 +29,7 @@ import { } from './rich-markdown-details-extension' import { createMarkdownDocLink } from './rich-markdown-doc-link' import { RichMarkdownCodeBlock } from './RichMarkdownCodeBlock' -import { safeReactNodeViewRenderer } from './safe-react-node-view-renderer' +import { ReactNodeViewRenderer } from '@tiptap/react' import { positionStableNodeViewUpdate } from './position-stable-node-view-update' import { DragSelectionGuard } from './drag-selection-guard' import { createRichMarkdownAnnotationHighlightExtension } from './rich-markdown-annotation-highlight' @@ -88,7 +88,7 @@ export function createRichMarkdownExtensions({ addNodeView() { // Why: RichMarkdownCodeBlock never reads getPos, so it must not re-render // just because earlier edits shifted this block's document position. - return safeReactNodeViewRenderer(RichMarkdownCodeBlock, { + return ReactNodeViewRenderer(RichMarkdownCodeBlock, { update: positionStableNodeViewUpdate }) } diff --git a/src/renderer/src/components/editor/safe-react-node-view-renderer.ts b/src/renderer/src/components/editor/safe-react-node-view-renderer.ts deleted file mode 100644 index 5aa00295cf7..00000000000 --- a/src/renderer/src/components/editor/safe-react-node-view-renderer.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { ComponentType } from 'react' -import { ReactNodeViewRenderer } from '@tiptap/react' -import type { ReactNodeViewProps, ReactNodeViewRendererOptions } from '@tiptap/react' -import type { NodeViewRenderer } from '@tiptap/core' -import { NodeSelection } from '@tiptap/pm/state' - -/** - * Workaround for Tiptap #7647: ReactNodeViewRenderer's handleSelectionUpdate - * incorrectly calls selectNode() for *any* selection that encompasses the node - * view — including TextSelection and AllSelection from mouse drag. The - * selectNode() call triggers a React re-render that mutates the DOM during an - * active drag, causing ProseMirror to lose the native browser selection. - * - * This wrapper patches handleSelectionUpdate on each created NodeView instance - * so selectNode/deselectNode only fire for actual NodeSelections (the user - * clicking a node with the modifier key to select it as a whole). - * - * Safe to remove once Tiptap merges PR #7691. - */ -export function safeReactNodeViewRenderer( - component: ComponentType>, - options?: Partial -): NodeViewRenderer { - const factory = ReactNodeViewRenderer(component, options) - - return (props) => { - const nodeView = factory(props) - - // Why: the factory returns an empty object when editor.contentComponent - // is not set (SSR / immediatelyRender: false initial pass). In that case - // there is no handleSelectionUpdate to patch. - if (!('handleSelectionUpdate' in nodeView)) { - return nodeView - } - - // Why: the constructor binds handleSelectionUpdate and registers it via - // editor.on('selectionUpdate', ...). We must unregister the original bound - // reference before replacing, otherwise the event emitter still calls the - // original and our patch is a no-op. On destroy(), the class calls - // editor.off('selectionUpdate', this.handleSelectionUpdate), so storing - // the patched function back on the property ensures clean teardown. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const nv = nodeView as any - const originalBound = nv.handleSelectionUpdate - nv.editor.off('selectionUpdate', originalBound) - - nv.handleSelectionUpdate = function patchedHandleSelectionUpdate( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this: any - ): void { - // Why: only NodeSelection means the user intentionally selected this - // specific node (e.g. Ctrl/Cmd-click on an atom node). Text and All - // selections that happen to span across the node should not trigger - // selectNode(), because that causes a React re-render mid-drag which - // disrupts the browser's native selection tracking. - if (this.editor.state.selection instanceof NodeSelection) { - originalBound() - } else { - // Why: if a previous NodeSelection had set selected=true, clear it - // now that the selection is no longer a NodeSelection. - if (this.renderer?.props?.selected) { - this.deselectNode() - } - } - } - - nv.handleSelectionUpdate = nv.handleSelectionUpdate.bind(nv) - nv.editor.on('selectionUpdate', nv.handleSelectionUpdate) - - return nodeView - } -} diff --git a/src/renderer/src/components/editor/use-markdown-preview-components.tsx b/src/renderer/src/components/editor/use-markdown-preview-components.tsx index 809f30c7152..3e71af8a4a3 100644 --- a/src/renderer/src/components/editor/use-markdown-preview-components.tsx +++ b/src/renderer/src/components/editor/use-markdown-preview-components.tsx @@ -282,7 +282,6 @@ export function useMarkdownPreviewComponents({ ) } - // eslint-disable-next-line react-hooks/exhaustive-deps -- the image override is a hook component; listed inputs preserve its identity. }, [ filePath, activateMarkdownLink, diff --git a/src/renderer/src/components/editor/use-monaco-markdown-annotations.ts b/src/renderer/src/components/editor/use-monaco-markdown-annotations.ts index 9cb070fe3c0..552c58d4dd7 100644 --- a/src/renderer/src/components/editor/use-monaco-markdown-annotations.ts +++ b/src/renderer/src/components/editor/use-monaco-markdown-annotations.ts @@ -136,12 +136,14 @@ export function useMonacoMarkdownAnnotations(params: { onPendingScrollConsumed: () => setScrollToDiffCommentId(null) }) + // Why the line number alone: match DiffViewer, so popover top updates don't resubscribe. + const popoverLineNumber = commentPopover?.lineNumber useEffect(() => { - if (!mountedEditor || !commentPopover) { + if (!mountedEditor || popoverLineNumber === undefined) { return } const update = (): void => { - const top = getDiffCommentPopoverTop(mountedEditor, commentPopover.lineNumber, undefined) + const top = getDiffCommentPopoverTop(mountedEditor, popoverLineNumber, undefined) const left = getDiffCommentPopoverLeft(mountedEditor, editorContainerRef.current) setCommentPopover((prev) => prev ? { ...prev, top: top ?? prev.top, left: left == null ? prev.left : left } : prev @@ -155,8 +157,7 @@ export function useMonacoMarkdownAnnotations(params: { contentSub.dispose() layoutSub.dispose() } - // eslint-disable-next-line react-hooks/exhaustive-deps -- match DiffViewer: don't resubscribe on top updates. - }, [mountedEditor, commentPopover?.lineNumber]) + }, [mountedEditor, popoverLineNumber, editorContainerRef]) useEffect(() => { if (!mountedEditor || !shouldShowMarkdownAnnotations || commentPopover) { diff --git a/src/renderer/src/components/editor/useContextualCopySetup.tsx b/src/renderer/src/components/editor/useContextualCopySetup.tsx index e115422f88b..7f0324d3ea8 100644 --- a/src/renderer/src/components/editor/useContextualCopySetup.tsx +++ b/src/renderer/src/components/editor/useContextualCopySetup.tsx @@ -10,8 +10,7 @@ export function useContextualCopySetup() { const setupCopy = useCallback( ( editorInstance: editor.IStandaloneCodeEditor, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - _monaco: any, + _monaco: unknown, filePath: string, propsRef: React.MutableRefObject<{ relativePath: string diff --git a/src/renderer/src/components/editor/useEditorPanelDiffContentLoader.ts b/src/renderer/src/components/editor/useEditorPanelDiffContentLoader.ts index 12a32132ec0..4d6ba8a0d3b 100644 --- a/src/renderer/src/components/editor/useEditorPanelDiffContentLoader.ts +++ b/src/renderer/src/components/editor/useEditorPanelDiffContentLoader.ts @@ -96,8 +96,7 @@ export function useEditorPanelDiffContentLoader({ } let pending = inFlightDiffReads.get(key) if (!pending) { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every branch resolves a git diff result, which DiffContent mirrors. - const promise = ( + const promise: Promise = effectiveDiffSource === 'commit' ? commitCompare ? getRuntimeGitCommitDiff( @@ -147,7 +146,6 @@ export function useEditorPanelDiffContentLoader({ compareAgainstHead } ) - ) as Promise pending = { externalEventGeneration: options?.externalEventGeneration, promise } inFlightDiffReads.set(key, pending) queueMicrotask(() => { diff --git a/src/renderer/src/components/feature-wall/TasksAnimatedVisual.tsx b/src/renderer/src/components/feature-wall/TasksAnimatedVisual.tsx index af04080f4de..d2061d5e24b 100644 --- a/src/renderer/src/components/feature-wall/TasksAnimatedVisual.tsx +++ b/src/renderer/src/components/feature-wall/TasksAnimatedVisual.tsx @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: this visual is a timed storyboard; phase and cursor state intentionally advance from animation effects and reduced-motion gates. */ import { useEffect, useLayoutEffect, useRef, useState } from 'react' import type { JSX } from 'react' import { ArrowRight, CircleDot } from 'lucide-react' diff --git a/src/renderer/src/components/feature-wall/agents-orchestration/OrchestrationPage.tsx b/src/renderer/src/components/feature-wall/agents-orchestration/OrchestrationPage.tsx index e43901ef28b..0a7ec3c28f7 100644 --- a/src/renderer/src/components/feature-wall/agents-orchestration/OrchestrationPage.tsx +++ b/src/renderer/src/components/feature-wall/agents-orchestration/OrchestrationPage.tsx @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: this page is a timed storyboard; row state resets are part of replaying the animation when the active step changes. */ import { useCallback, useEffect, useRef, useState } from 'react' import type { JSX } from 'react' import { ChevronDown, Workflow } from 'lucide-react' diff --git a/src/renderer/src/components/feature-wall/agents-orchestration/StatusesPage.tsx b/src/renderer/src/components/feature-wall/agents-orchestration/StatusesPage.tsx index dd4412cf282..c3f5db7f408 100644 --- a/src/renderer/src/components/feature-wall/agents-orchestration/StatusesPage.tsx +++ b/src/renderer/src/components/feature-wall/agents-orchestration/StatusesPage.tsx @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: this page is a timed storyboard; reveal state intentionally resets when the active/reduced-motion gates change. */ import { useEffect, useRef, useState } from 'react' import type { JSX, ReactNode } from 'react' import { Wrench } from 'lucide-react' diff --git a/src/renderer/src/components/feature-wall/agents-orchestration/UsagePage.tsx b/src/renderer/src/components/feature-wall/agents-orchestration/UsagePage.tsx index a7f73f3b9ce..7377d25e109 100644 --- a/src/renderer/src/components/feature-wall/agents-orchestration/UsagePage.tsx +++ b/src/renderer/src/components/feature-wall/agents-orchestration/UsagePage.tsx @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: this page is a timed storyboard; phase state intentionally advances from animation effects and reduced-motion gates. */ import { useEffect, useState } from 'react' import type { JSX } from 'react' import { ClaudeIcon, OpenAIIcon } from '../../status-bar/icons' diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.behavior.test.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.behavior.test.tsx index c73f830cc71..149c1096022 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.behavior.test.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.behavior.test.tsx @@ -1,3 +1,4 @@ +import type * as ReactModule from 'react' import { vi } from 'vitest' import './FloatingTerminalPanel.bounds.spec' import './FloatingTerminalPanel.focus.spec' @@ -17,7 +18,7 @@ vi.mock('zustand/react/shallow', () => ({ useShallow: (selector: unknown) => selector })) vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') const { createReactHookOverrides } = await import('./floating-terminal-panel-test-module-mocks') return { ...actual, ...createReactHookOverrides() } }) diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalToggleButton.test.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalToggleButton.test.tsx index 0db0de4966c..3c10fcfad09 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalToggleButton.test.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalToggleButton.test.tsx @@ -1,3 +1,4 @@ +import type * as ReactModule from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { FLOATING_TERMINAL_TRIGGER_POSITION_STORAGE_KEY } from './floating-terminal-trigger-position' @@ -16,7 +17,7 @@ const hookRuntime = vi.hoisted(() => ({ })) vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useCallback: (callback: T) => callback, diff --git a/src/renderer/src/components/github-project/slug-dialog/SlugDialogBody.tsx b/src/renderer/src/components/github-project/slug-dialog/SlugDialogBody.tsx index 45ee227c4a4..b9359d63963 100644 --- a/src/renderer/src/components/github-project/slug-dialog/SlugDialogBody.tsx +++ b/src/renderer/src/components/github-project/slug-dialog/SlugDialogBody.tsx @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: slug dialog details are loaded through GitHub IPC and must clear stale request state before each lookup. */ import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { CircleDot, ExternalLink, GitPullRequest, LoaderCircle, X } from 'lucide-react' diff --git a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-item-dialog-effects.ts b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-item-dialog-effects.ts index cea2c0e557f..c05a82d55b8 100644 --- a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-item-dialog-effects.ts +++ b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-item-dialog-effects.ts @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: GitLab item dialogs reset draft/provider state and hydrate details from GitLab IPC when the selected item identity changes. */ import { gitLabApiFor } from '@/runtime/gitlab-owner-api' import { useEffect } from 'react' import type { GitLabWorkItem, GitLabWorkItemDetails } from '../../../../shared/gitlab-types' diff --git a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-item-dialog-state.ts b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-item-dialog-state.ts index b1175afe21c..78ffe18fb81 100644 --- a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-item-dialog-state.ts +++ b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-item-dialog-state.ts @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: GitLab item dialogs reset draft/provider state and hydrate details from GitLab IPC when the selected item identity changes. */ import { useCallback, useState } from 'react' import { useMountedRef } from '@/hooks/useMountedRef' import type { GitLabAssignableUser, GitLabWorkItemDetails } from '../../../../shared/gitlab-types' diff --git a/src/renderer/src/components/landing-preflight-unverifiable-host.test.tsx b/src/renderer/src/components/landing-preflight-unverifiable-host.test.tsx index 6903f29c0d4..1a406a9596e 100644 --- a/src/renderer/src/components/landing-preflight-unverifiable-host.test.tsx +++ b/src/renderer/src/components/landing-preflight-unverifiable-host.test.tsx @@ -6,8 +6,8 @@ import type { RuntimeHostStatusSnapshot } from '../../../shared/runtime-host-sta import type { RuntimeStatus } from '../../../shared/runtime-types' import { useAppStore } from '../store' import type { RuntimeEnvironmentStatus } from '../store/slices/runtime-status-types' -import type { AppState } from '../store/types' import { useLandingPreflightRuntime } from './landing-preflight-runtime' +import { getDefaultSettings } from '../../../shared/constants' const ENVIRONMENT_ID = 'environment-a' const initialState = useAppStore.getInitialState() @@ -51,8 +51,7 @@ describe('landing preflight under an unverifiable host probe', () => { useAppStore.setState( { ...initialState, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook under test reads only activeRuntimeEnvironmentId; the rest of GlobalSettings never reaches it. - settings: { activeRuntimeEnvironmentId: ENVIRONMENT_ID } as AppState['settings'], + settings: { ...getDefaultSettings('/tmp'), activeRuntimeEnvironmentId: ENVIRONMENT_ID }, invalidatePreflightStatus: invalidate, refreshPreflightStatus: refresh }, diff --git a/src/renderer/src/components/mobile/PhoneCarousel.tsx b/src/renderer/src/components/mobile/PhoneCarousel.tsx index 9d801d2e16d..fedcf1c981b 100644 --- a/src/renderer/src/components/mobile/PhoneCarousel.tsx +++ b/src/renderer/src/components/mobile/PhoneCarousel.tsx @@ -16,6 +16,7 @@ export function PhoneCarousel(): React.JSX.Element { const [tappingSlide, setTappingSlide] = useState(null) const containerRef = useRef(null) + // oxlint-disable-next-line react-doctor/effect-needs-cleanup -- Cleanup clears all four timers the schedule chain re-arms. useEffect(() => { if (typeof window === 'undefined') { return @@ -68,7 +69,6 @@ export function PhoneCarousel(): React.JSX.Element { }, DWELL_MS) } - // oxlint-disable-next-line react-doctor/no-initialize-state -- Why: the first tap pulse is intentionally delayed until after the initial dwell. schedule(0) return () => { diff --git a/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.in-flight.test.tsx b/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.in-flight.test.tsx index b5b57e43c38..6e2ddd018ed 100644 --- a/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.in-flight.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.in-flight.test.tsx @@ -22,10 +22,7 @@ import { toast } from 'sonner' import { TooltipProvider } from '@/components/ui/tooltip' import { NativeChatQueuedMessageList } from './NativeChatQueuedMessageList' import { useStructuredAgentSessionQueuedMessages } from './use-structured-agent-session-queued-messages' -import type { - StructuredAgentSessionMutate, - StructuredAgentSessionWrite -} from './use-structured-agent-session-mutate' +import type { StructuredAgentSessionWrite } from './use-structured-agent-session-mutate' import type { AgentSessionQueuedMessage } from '../../../../shared/agent-session-wire' function waitingDraft(messageId: string, position: number): AgentSessionQueuedMessage { @@ -54,8 +51,7 @@ function renderHeldQueue( hasPendingPrompt: false, isWorking: false, composerScopeKey: undefined, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the list only awaits mutate; its answer is never read. - mutate: mutate as StructuredAgentSessionMutate, + mutate: mutate, editTransport: { target: { kind: 'local' }, sessionId: 'session', capable: false, write } }) return diff --git a/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx b/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx index 6ca34cfa942..af50b24ec73 100644 --- a/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx @@ -32,7 +32,6 @@ import { useStructuredAgentSessionQueuedMessages, type StructuredAgentSessionQueuedMessagesController } from './use-structured-agent-session-queued-messages' -import type { StructuredAgentSessionMutate } from './use-structured-agent-session-mutate' import type { AgentSessionQueuedMessage } from '../../../../shared/agent-session-wire' import type { AgentSessionQueuePause } from '../../../../shared/agent-session-queued-message-wire' import type { AgentSessionFailureFact } from '../../../../shared/agent-session-failure' @@ -127,8 +126,7 @@ function renderHeldQueue( hasPendingPrompt, isWorking: false, composerScopeKey: undefined, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the list only awaits mutate; its answer is never read. - mutate: mutate as StructuredAgentSessionMutate, + mutate: mutate, editTransport: { target: { kind: 'local' }, sessionId: 'session', diff --git a/src/renderer/src/components/native-chat/StructuredAgentSessionAttentionBridge.test.tsx b/src/renderer/src/components/native-chat/StructuredAgentSessionAttentionBridge.test.tsx index 5f969b87eed..469e49a1363 100644 --- a/src/renderer/src/components/native-chat/StructuredAgentSessionAttentionBridge.test.tsx +++ b/src/renderer/src/components/native-chat/StructuredAgentSessionAttentionBridge.test.tsx @@ -17,7 +17,6 @@ import type { import type { NotificationDispatchRequest } from '../../../../shared/notification-settings-types' import { structuredAgentSessionPaneKey } from '../../../../shared/structured-agent-session-projection' import type { Tab } from '../../../../shared/tab-types' -import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { AppState } from '@/store/types' import type * as RuntimeRpcClientModule from '@/runtime/runtime-rpc-client' @@ -75,6 +74,7 @@ import { } from '@/runtime/structured-agent-session-status-feed' import { makeTabGroup, makeUnifiedTab } from '../../store/slices/store-session-test-harness' import { makeWorktree, TEST_REPO } from '../../store/slices/worktrees-slice-test-fixtures' +import { getDefaultSettings } from '../../../../shared/constants' // Worktree ids encode their repo (`repoId::path`); the unread reducer buckets by that prefix. const WORKSPACE = 'repo1::/tmp/wt' @@ -227,9 +227,7 @@ describe('StructuredAgentSessionAttentionBridge', () => { unreadTerminalTabs: {}, unreadTerminalPanes: {}, unreadAgentCompletionPanes: {}, - // The attention dispatch reads exactly one field; GlobalSettings has no test factory. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only field read. - settings: { experimentalTerminalAttention: true } as GlobalSettings + settings: { ...getDefaultSettings('/tmp'), experimentalTerminalAttention: true } }) }) diff --git a/src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.test.tsx b/src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.test.tsx index 7ee82b8228d..442e6d27239 100644 --- a/src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.test.tsx +++ b/src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.test.tsx @@ -38,8 +38,7 @@ const HOST_CATALOG = { fetchedAt: 1_000 } -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: no test here sends a pick over a fence, so mutate is never called. -const mutate = vi.fn(async () => null) as unknown as StructuredAgentSessionMutate +const mutate: StructuredAgentSessionMutate = vi.fn(async () => null) type Props = { hidden?: boolean; attached?: boolean; sessionId?: string } diff --git a/src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.unavailable.test.tsx b/src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.unavailable.test.tsx index fb92c938c20..b407722b4b8 100644 --- a/src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.unavailable.test.tsx +++ b/src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.unavailable.test.tsx @@ -40,8 +40,7 @@ const HOST_CATALOG = { fetchedAt: 1_000 } -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: no test here sends a pick, so mutate is never called. -const mutate = vi.fn(async () => null) as unknown as StructuredAgentSessionMutate +const mutate: StructuredAgentSessionMutate = vi.fn(async () => null) let sessionId = '' let sessionCount = 0 diff --git a/src/renderer/src/components/native-chat/use-structured-agent-session-options.first-frame.test.tsx b/src/renderer/src/components/native-chat/use-structured-agent-session-options.first-frame.test.tsx index 662d4c46d65..e1700bffd26 100644 --- a/src/renderer/src/components/native-chat/use-structured-agent-session-options.first-frame.test.tsx +++ b/src/renderer/src/components/native-chat/use-structured-agent-session-options.first-frame.test.tsx @@ -34,7 +34,6 @@ import { preloadHostModelCatalogSnapshots, resetHostModelCatalogSnapshotsForTests } from '@/runtime/host-model-catalog-snapshots' -import type { StructuredAgentSessionMutate } from './use-structured-agent-session-mutate' import { useStructuredAgentSessionOptions } from './use-structured-agent-session-options' const LOCAL_TARGET = { kind: 'local' } as const @@ -113,8 +112,7 @@ function renderNewChat(args: { fence: null, turnId: null, unloadedTurnRevisions: undefined, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: no pick is made, so mutate is never called. - mutate: vi.fn() as unknown as StructuredAgentSessionMutate, + mutate: vi.fn(), launch: { kind: 'new', heldOptions: {}, diff --git a/src/renderer/src/components/native-chat/use-structured-agent-session-options.learned-default.test.tsx b/src/renderer/src/components/native-chat/use-structured-agent-session-options.learned-default.test.tsx index c2ef2e7d9c2..46eec730d3a 100644 --- a/src/renderer/src/components/native-chat/use-structured-agent-session-options.learned-default.test.tsx +++ b/src/renderer/src/components/native-chat/use-structured-agent-session-options.learned-default.test.tsx @@ -87,8 +87,7 @@ function pill(snapshot: readonly SessionOptionDescriptor[]): Pill { const LOCAL = { kind: 'local' } as const const NOT_STARTED: { running: { fence: number } | null } = { running: null } const NO_HELD = {} -// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: no pick is made, so mutate is never called. -const NO_MUTATE = vi.fn() as unknown as StructuredAgentSessionMutate +const NO_MUTATE: StructuredAgentSessionMutate = vi.fn() function renderChat(agent: string, sessionId: string, worktree: string) { const frames: Pill[] = [] diff --git a/src/renderer/src/components/native-chat/use-structured-agent-session-options.preloaded-agents.test.tsx b/src/renderer/src/components/native-chat/use-structured-agent-session-options.preloaded-agents.test.tsx index f97c4b1eecc..49f705ef8f4 100644 --- a/src/renderer/src/components/native-chat/use-structured-agent-session-options.preloaded-agents.test.tsx +++ b/src/renderer/src/components/native-chat/use-structured-agent-session-options.preloaded-agents.test.tsx @@ -41,7 +41,6 @@ import { getDefaultSettings } from '../../../../shared/constants' import { useAppStore } from '@/store' import { resetHostModelCatalogSnapshotsForTests } from '@/runtime/host-model-catalog-snapshots' import { installHostModelCatalogSnapshotsSync } from '@/runtime/host-model-catalog-snapshots-sync' -import type { StructuredAgentSessionMutate } from './use-structured-agent-session-mutate' import { useStructuredAgentSessionOptions } from './use-structured-agent-session-options' // Grok, OpenCode and OMP chats were used, but none has a saved model pick: the user's report was @@ -110,8 +109,7 @@ function renderNewChat(agent: string) { fence: null, turnId: null, unloadedTurnRevisions: undefined, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: no pick is made, so mutate is never called. - mutate: vi.fn() as unknown as StructuredAgentSessionMutate, + mutate: vi.fn(), launch: { kind: 'new', heldOptions: {}, worktree: 'id:wt-1' } }) frames.push(pill(options.optionSnapshot)) diff --git a/src/renderer/src/components/native-chat/use-structured-agent-session-sends.ts b/src/renderer/src/components/native-chat/use-structured-agent-session-sends.ts index d2f3275399c..407e33f468f 100644 --- a/src/renderer/src/components/native-chat/use-structured-agent-session-sends.ts +++ b/src/renderer/src/components/native-chat/use-structured-agent-session-sends.ts @@ -1,7 +1,7 @@ import { useStructuredAgentSessionHostCapabilityState } from '@/runtime/structured-agent-session-host-capability' import { AGENT_SESSION_SEND_SKILLS_RUNTIME_CAPABILITY } from '../../../../shared/agent-session-create-capabilities' import type { StructuredAgentSessionSendRequest } from './structured-agent-session-send-slot' -import { useCallback, useEffect, useSyncExternalStore } from 'react' +import { useCallback, useEffect, useEffectEvent, useSyncExternalStore } from 'react' import type { AgentJournalCursor, AgentJournalSubmission @@ -85,17 +85,19 @@ export function useStructuredAgentSessionSends(args: { }, [cursor, pending, queuedMessageIds, sessionId, submissions]) const recoverLegacy = historyLoaded && fence !== null + // Why an event: once per open, from the first loaded history; later batches add nothing it needs. + const recoverLegacyOutbox = useEffectEvent(() => { + void recoverLegacyStructuredAgentSessionOutbox({ + sessionId, + target, + submissions, + queuedMessageIds + }) + }) useEffect(() => { if (recoverLegacy) { - void recoverLegacyStructuredAgentSessionOutbox({ - sessionId, - target, - submissions, - queuedMessageIds - }) + recoverLegacyOutbox() } - // Why: once per open, from the first loaded history; later batches add nothing it needs. - // eslint-disable-next-line react-hooks/exhaustive-deps }, [recoverLegacy, sessionId, target]) /** Whether the message was taken; 'queued' when it waits as a queued card, not a bubble. */ diff --git a/src/renderer/src/components/new-workspace/use-smart-workspace-secondary-searches.ts b/src/renderer/src/components/new-workspace/use-smart-workspace-secondary-searches.ts index 625d6acf1f8..aa613af715e 100644 --- a/src/renderer/src/components/new-workspace/use-smart-workspace-secondary-searches.ts +++ b/src/renderer/src/components/new-workspace/use-smart-workspace-secondary-searches.ts @@ -240,8 +240,6 @@ export function useSmartWorkspaceSecondarySearches({ return () => { stale = true } - // Why: list/search are stable store methods; unrelated store writes must not refetch. - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ disabled, linearConnected, @@ -250,6 +248,9 @@ export function useSmartWorkspaceSecondarySearches({ linearSourceContext, linearWorkspaceIdOverride, linearUrlIntent, + setLinearIssues, + setLinearLoading, + setLinearReadError, setSettledLinearUrlQuery, shouldQueryLinear ]) diff --git a/src/renderer/src/components/onboarding/ThemeStep.tsx b/src/renderer/src/components/onboarding/ThemeStep.tsx index 8e9bafeb7df..bffb794fe3f 100644 --- a/src/renderer/src/components/onboarding/ThemeStep.tsx +++ b/src/renderer/src/components/onboarding/ThemeStep.tsx @@ -77,7 +77,6 @@ export function ThemeStep({ theme, onThemeChange, settings, updateSettings }: Th return } let cancelled = false - // oxlint-disable-next-line react-doctor/no-initialize-state -- Why: non-Mac intentionally remains idle; only Mac enters detecting before IPC. setDiscovery({ status: 'detecting' }) void window.api.settings .previewGhosttyImport() diff --git a/src/renderer/src/components/quick-open-file-list.ts b/src/renderer/src/components/quick-open-file-list.ts index 22ca5b12f36..dcb45763db7 100644 --- a/src/renderer/src/components/quick-open-file-list.ts +++ b/src/renderer/src/components/quick-open-file-list.ts @@ -12,7 +12,6 @@ import { clearQuickOpenRecentCache, useQuickOpenRecentCache } from './quick-open-recent-validation' -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: quick-open file lists are fetched over local or SSH runtime IPC, so loading/error/results track the request lifecycle. */ import { useEffect, useMemo, useRef, useState } from 'react' import { useShallow } from 'zustand/react/shallow' export { isNestedWorktreePath, getNestedWorktreeExcludePaths } from './quick-open-nested-worktrees' diff --git a/src/renderer/src/components/right-sidebar/right-sidebar-titlebar-drag-regions.render.test.tsx b/src/renderer/src/components/right-sidebar/right-sidebar-titlebar-drag-regions.render.test.tsx index 829d0e68469..6825e7927bf 100644 --- a/src/renderer/src/components/right-sidebar/right-sidebar-titlebar-drag-regions.render.test.tsx +++ b/src/renderer/src/components/right-sidebar/right-sidebar-titlebar-drag-regions.render.test.tsx @@ -1,5 +1,6 @@ // @vitest-environment happy-dom +import type * as ReactModule from 'react' import { act, cloneElement, isValidElement, type ReactElement, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { renderToStaticMarkup } from 'react-dom/server' @@ -61,7 +62,7 @@ vi.mock('@/hooks/useShortcutLabel', () => ({ })) vi.mock('@/store', async () => { - const React = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const React = await vi.importActual('react') const getSnapshot = (selector: (state: Record) => unknown): unknown => { if (mockAppState.snapshotCache.has(selector)) { return mockAppState.snapshotCache.get(selector) diff --git a/src/renderer/src/components/right-sidebar/useFileDuplicate.ts b/src/renderer/src/components/right-sidebar/useFileDuplicate.ts index 432dd586eb6..7451d20b328 100644 --- a/src/renderer/src/components/right-sidebar/useFileDuplicate.ts +++ b/src/renderer/src/components/right-sidebar/useFileDuplicate.ts @@ -76,7 +76,6 @@ export function useFileDuplicate({ // in degenerate scenarios. const MAX_RETRIES = 10 let retries = 0 - // eslint-disable-next-line no-constant-condition while (true) { try { operationGuard.assertCurrent() diff --git a/src/renderer/src/components/settings/BaseRefPicker.tsx b/src/renderer/src/components/settings/BaseRefPicker.tsx index 49037b623bd..2c291c25a83 100644 --- a/src/renderer/src/components/settings/BaseRefPicker.tsx +++ b/src/renderer/src/components/settings/BaseRefPicker.tsx @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: base-ref defaults and search results come from runtime repo IPC and must clear stale repo results before new requests resolve. */ import { useEffect, useRef, useState } from 'react' import { Button } from '../ui/button' import { Input } from '../ui/input' diff --git a/src/renderer/src/components/settings/NodeDistMirrorDialog.test.tsx b/src/renderer/src/components/settings/NodeDistMirrorDialog.test.tsx index 5c92a059a0c..ab444d84185 100644 --- a/src/renderer/src/components/settings/NodeDistMirrorDialog.test.tsx +++ b/src/renderer/src/components/settings/NodeDistMirrorDialog.test.tsx @@ -13,6 +13,7 @@ vi.mock('sonner', () => ({ toast: { error: toastError } })) import NodeDistMirrorDialog from './NodeDistMirrorDialog' import { SshHostNodeMirrorAction } from './SshHostNodeMirrorAction' import { toastSshConnectFailure } from '@/ssh/ssh-connect-failure-toast' +import { getDefaultSettings } from '../../../../shared/constants' const blocked: SshManagedServerStatus = { kind: 'relay', @@ -28,8 +29,7 @@ const settingsSet = vi.fn(async (updates: Partial) => ({ const connect = vi.fn(async () => ({ targetId: 'ssh-1', status: 'connected' })) function settingsWithMirror(nodeDistMirrorUrl: string): GlobalSettings { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these components read only nodeDistMirrorUrl. - return { nodeDistMirrorUrl } as GlobalSettings + return { ...getDefaultSettings('/tmp'), nodeDistMirrorUrl } } function Host({ status }: { status: SshManagedServerStatus }) { diff --git a/src/renderer/src/components/settings/TerminalAppearanceSection.ghostty.test.ts b/src/renderer/src/components/settings/TerminalAppearanceSection.ghostty.test.ts index b8162992118..d14ea5d4b7c 100644 --- a/src/renderer/src/components/settings/TerminalAppearanceSection.ghostty.test.ts +++ b/src/renderer/src/components/settings/TerminalAppearanceSection.ghostty.test.ts @@ -1,3 +1,4 @@ +import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' const mockStateValues: unknown[] = [] @@ -9,7 +10,7 @@ function resetMockState() { } vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useState: (initial: unknown) => { diff --git a/src/renderer/src/components/settings/TerminalPane.pwsh.test.ts b/src/renderer/src/components/settings/TerminalPane.pwsh.test.ts index cb9af4f9fbb..090c44927c6 100644 --- a/src/renderer/src/components/settings/TerminalPane.pwsh.test.ts +++ b/src/renderer/src/components/settings/TerminalPane.pwsh.test.ts @@ -1,3 +1,4 @@ +import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' const mockStateValues: unknown[] = [] @@ -8,7 +9,7 @@ function resetMockState() { } vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useState: (initial: unknown) => { diff --git a/src/renderer/src/components/settings/TerminalSettingsPreview.lifecycle.test.tsx b/src/renderer/src/components/settings/TerminalSettingsPreview.lifecycle.test.tsx index 6c194137ef9..6130ae0a0fa 100644 --- a/src/renderer/src/components/settings/TerminalSettingsPreview.lifecycle.test.tsx +++ b/src/renderer/src/components/settings/TerminalSettingsPreview.lifecycle.test.tsx @@ -1,3 +1,4 @@ +import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Mock } from 'vitest' import type { GlobalSettings } from '../../../../shared/global-settings-types' @@ -37,7 +38,7 @@ const mockLigaturesAddon = vi.hoisted(() => ({ })) vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useEffect: (effect: () => void | Cleanup) => { diff --git a/src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts b/src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts index 1a3fd0e4607..fed924cfceb 100644 --- a/src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts +++ b/src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts @@ -1,3 +1,4 @@ +import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { UseWarpThemeImportReturn } from './useWarpThemeImport' @@ -5,7 +6,7 @@ import type { UseWarpThemeImportReturn } from './useWarpThemeImport' let themeTarget: 'dark' | 'light' | undefined = 'dark' vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useState: (initial: T | (() => T)) => { diff --git a/src/renderer/src/components/settings/bitbucket-credentials-dialog.tsx b/src/renderer/src/components/settings/bitbucket-credentials-dialog.tsx index 124cda8a761..422b3285fcb 100644 --- a/src/renderer/src/components/settings/bitbucket-credentials-dialog.tsx +++ b/src/renderer/src/components/settings/bitbucket-credentials-dialog.tsx @@ -1,4 +1,4 @@ -import { useId, useLayoutEffect, useRef, useState } from 'react' +import { useEffectEvent, useId, useLayoutEffect, useRef, useState } from 'react' import { ExternalLink, LoaderCircle, Lock } from 'lucide-react' import type { BitbucketAuthMode } from '../../../../shared/bitbucket-credentials' import { useAppStore } from '@/store' @@ -71,10 +71,8 @@ export function BitbucketCredentialsDialog({ // Re-sync from the latest stored metadata on every open, not just on mount, so // the Edit flow never shows values from a previous connection. Secrets always // start empty: the main process never hands them back. - useLayoutEffect(() => { - if (!open) { - return - } + // Why an event: keyed on `open` alone, so a status refresh mid-edit must not overwrite typing. + const seedFromStoredMetadata = useEffectEvent(() => { const seedEmail = initialEmail ?? '' const seedBaseUrl = initialBaseUrl ?? '' baselineRef.current = { email: seedEmail, baseUrl: seedBaseUrl } @@ -85,7 +83,11 @@ export function BitbucketCredentialsDialog({ setAccessToken('') setConnectState('idle') setConnectError(null) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- keyed on `open` alone: a status refresh mid-edit must not overwrite what the user is typing. + }) + useLayoutEffect(() => { + if (open) { + seedFromStoredMetadata() + } }, [open]) const remoteRuntime = hasRemoteProviderRuntime(settings) diff --git a/src/renderer/src/components/settings/use-runtime-environment-mutation-actions.test.tsx b/src/renderer/src/components/settings/use-runtime-environment-mutation-actions.test.tsx index 28c4d8a7530..5c65d5c7722 100644 --- a/src/renderer/src/components/settings/use-runtime-environment-mutation-actions.test.tsx +++ b/src/renderer/src/components/settings/use-runtime-environment-mutation-actions.test.tsx @@ -1,7 +1,6 @@ // @vitest-environment happy-dom import { act, cleanup, renderHook } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments' const { toastSuccess, toastError } = vi.hoisted(() => ({ @@ -11,6 +10,7 @@ const { toastSuccess, toastError } = vi.hoisted(() => ({ vi.mock('sonner', () => ({ toast: { success: toastSuccess, error: toastError } })) import { useRuntimeEnvironmentMutationActions } from './use-runtime-environment-mutation-actions' +import { getDefaultSettings } from '../../../../shared/constants' const verifyAndAddFromPairingCode = vi.fn() const remove = vi.fn() @@ -41,8 +41,7 @@ afterEach(() => cleanup()) function renderActions(connectEnvironment: () => Promise, name = 'Studio over relay') { const view = renderHook(() => useRuntimeEnvironmentMutationActions({ - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: addEnvironment reads no settings. - settings: {} as GlobalSettings, + settings: getDefaultSettings('/tmp'), allowLocalRuntime: false, mountedRef: { current: true }, setAddServerFormOpen: vi.fn(), diff --git a/src/renderer/src/components/settings/useGhosttyImport.test.ts b/src/renderer/src/components/settings/useGhosttyImport.test.ts index ee3cb6560b6..2ed207e92e0 100644 --- a/src/renderer/src/components/settings/useGhosttyImport.test.ts +++ b/src/renderer/src/components/settings/useGhosttyImport.test.ts @@ -1,3 +1,4 @@ +import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { GhosttyImportPreview, GlobalSettings } from '../../../../shared/global-settings-types' @@ -28,7 +29,7 @@ function resetMockState() { } vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useEffect: (effect: () => void | (() => void)) => { diff --git a/src/renderer/src/components/settings/useWarpThemeImport.test.ts b/src/renderer/src/components/settings/useWarpThemeImport.test.ts index c76850cfbda..a446b6cdb49 100644 --- a/src/renderer/src/components/settings/useWarpThemeImport.test.ts +++ b/src/renderer/src/components/settings/useWarpThemeImport.test.ts @@ -1,3 +1,4 @@ +import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { GlobalSettings } from '../../../../shared/global-settings-types' import { @@ -29,7 +30,7 @@ function resetMockState() { } vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useEffect: (effect: () => void | (() => void)) => { diff --git a/src/renderer/src/components/setup-guide/use-setup-guide-progress.ts b/src/renderer/src/components/setup-guide/use-setup-guide-progress.ts index 496d98bfba1..b2e07ed78bf 100644 --- a/src/renderer/src/components/setup-guide/use-setup-guide-progress.ts +++ b/src/renderer/src/components/setup-guide/use-setup-guide-progress.ts @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: setup-guide readiness is driven by bounded IPC probes and browser focus events; the state cannot be derived synchronously from render inputs. */ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react' import { useAppStore } from '@/store' import { isGitRepoKind } from '../../../../shared/repo-kind' @@ -199,7 +198,6 @@ export function useSetupGuideProgress( return } const permissionState = getComputerUsePermissionSetupState(status) - // oxlint-disable-next-line react-doctor/no-adjust-state-on-prop-change -- Why: async permission checks update setup progress after external OS state changes. setComputerUsePermissionStatusChecked(true) setComputerUsePermissionsReady(permissionState.ready) setComputerUseUnavailable(permissionState.unavailable) @@ -218,7 +216,6 @@ export function useSetupGuideProgress( const refreshComputerUsePermissions = (): void => { void readComputerUsePermissions(() => stale) } - // oxlint-disable-next-line react-doctor/no-adjust-state-on-prop-change -- Why: refresh the setup checklist when the permission step becomes active. refreshComputerUsePermissions() const handleFocus = (): void => { void refreshComputerUsePermissions() diff --git a/src/renderer/src/components/sidebar/SidebarFeedbackDialog.tsx b/src/renderer/src/components/sidebar/SidebarFeedbackDialog.tsx index 7be16e4d9ea..eed06f014bc 100644 --- a/src/renderer/src/components/sidebar/SidebarFeedbackDialog.tsx +++ b/src/renderer/src/components/sidebar/SidebarFeedbackDialog.tsx @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: feedback viewer details are loaded through GitHub IPC after the dialog receives the issue URL. */ import React, { useRef, useState } from 'react' import { ExternalLink, Github } from 'lucide-react' import { toast } from 'sonner' diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.tsx index b811b98784a..6c0de3936da 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.tsx @@ -77,8 +77,7 @@ function WorkspaceKanbanLaneCardList({ getWorktreeIds: () => items.map((item) => item.id), getMeasurements: () => virtualizer.measurementsCache }) - // eslint-disable-next-line react-hooks/exhaustive-deps -- virtualizer is a stable instance (useVirtualizer holds it in useState), and getMeasurements reads measurementsCache off it live. - }, [itemIds, items, scrollRef]) + }, [itemIds, items, scrollRef, virtualizer]) return (
({ @@ -6,7 +7,7 @@ const reactHookRuntime = vi.hoisted(() => ({ })) vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useCallback(callback: T) { diff --git a/src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx b/src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx index a72761322a5..5b0b1a4c23d 100644 --- a/src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx +++ b/src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx @@ -3,8 +3,8 @@ import { describe, expect, it, vi } from 'vitest' const recordFeatureInteractionMock = vi.fn() const usagePercentageDisplayMock = 'used' -vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('react'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, memo: function memo(component: T): T { diff --git a/src/renderer/src/components/status-bar/use-workspace-space-manager-panel.ts b/src/renderer/src/components/status-bar/use-workspace-space-manager-panel.ts index e589cf0af98..7e43f5bab18 100644 --- a/src/renderer/src/components/status-bar/use-workspace-space-manager-panel.ts +++ b/src/renderer/src/components/status-bar/use-workspace-space-manager-panel.ts @@ -151,8 +151,12 @@ export function useWorkspaceSpaceManagerPanel() { } ) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Extracted state setters keep their original stable identities. - [removeWorkspaceSpaceWorktrees] + [ + removeWorkspaceSpaceWorktrees, + setInspectedWorktreeId, + setSelectedIds, + setTreemapZoomWorktreeId + ] ) const deleteWorktrees = useCallback( diff --git a/src/renderer/src/components/status-bar/workspace-space-metrics.tsx b/src/renderer/src/components/status-bar/workspace-space-metrics.tsx index ccd605f8e09..2bb0cda44c4 100644 --- a/src/renderer/src/components/status-bar/workspace-space-metrics.tsx +++ b/src/renderer/src/components/status-bar/workspace-space-metrics.tsx @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: the relative time clock advances from a wall-clock interval, which is an external timer rather than render-derived state. */ import { useEffect, useState } from 'react' import { installWindowVisibilityInterval } from '@/lib/window-visibility-interval' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/tab-bar/BrowserTab.test.tsx b/src/renderer/src/components/tab-bar/BrowserTab.test.tsx index a6139c2c46c..929770867a9 100644 --- a/src/renderer/src/components/tab-bar/BrowserTab.test.tsx +++ b/src/renderer/src/components/tab-bar/BrowserTab.test.tsx @@ -1,3 +1,4 @@ +import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { isValidElement } from 'react' import type { BrowserTab as BrowserTabState } from '../../../../shared/browser-workspace-types' @@ -8,7 +9,7 @@ const reactHookRuntime = vi.hoisted(() => ({ })) vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useEffect: () => {}, diff --git a/src/renderer/src/components/tab-bar/EditorFileTab.test.tsx b/src/renderer/src/components/tab-bar/EditorFileTab.test.tsx index 14fd4d4ba6f..e1b5b0ec414 100644 --- a/src/renderer/src/components/tab-bar/EditorFileTab.test.tsx +++ b/src/renderer/src/components/tab-bar/EditorFileTab.test.tsx @@ -1,3 +1,4 @@ +import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { OpenFile } from '../../store/slices/editor' @@ -22,7 +23,7 @@ const appStoreMocks = vi.hoisted(() => ({ const renameFileOnDiskMock = vi.hoisted(() => vi.fn()) vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useEffect: () => {}, diff --git a/src/renderer/src/components/tab-bar/SortableTab.rename-shortcut.test.tsx b/src/renderer/src/components/tab-bar/SortableTab.rename-shortcut.test.tsx index 8668ab3500b..4396869e81e 100644 --- a/src/renderer/src/components/tab-bar/SortableTab.rename-shortcut.test.tsx +++ b/src/renderer/src/components/tab-bar/SortableTab.rename-shortcut.test.tsx @@ -1,3 +1,4 @@ +import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { requestTerminalTabRename } from './terminal-tab-rename-request' @@ -33,7 +34,7 @@ const storeState = vi.hoisted( ) vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useCallback(callback: T) { diff --git a/src/renderer/src/components/tab-bar/TabBarQuickCommandsMenu.keyboard.test.ts b/src/renderer/src/components/tab-bar/TabBarQuickCommandsMenu.keyboard.test.ts index 12011007847..f657e6f81a3 100644 --- a/src/renderer/src/components/tab-bar/TabBarQuickCommandsMenu.keyboard.test.ts +++ b/src/renderer/src/components/tab-bar/TabBarQuickCommandsMenu.keyboard.test.ts @@ -1,3 +1,4 @@ +import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' // Capture window keydown listeners so tests can fire them directly. @@ -25,7 +26,7 @@ const reactRuntime = vi.hoisted(() => ({ })) vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useState(initial: T | (() => T)) { diff --git a/src/renderer/src/components/tab-bar/group-tab-order.test.ts b/src/renderer/src/components/tab-bar/group-tab-order.test.ts index ca693893893..70a4694f988 100644 --- a/src/renderer/src/components/tab-bar/group-tab-order.test.ts +++ b/src/renderer/src/components/tab-bar/group-tab-order.test.ts @@ -3,6 +3,8 @@ import type { Tab, TabGroup } from '../../../../shared/tab-types' import type { AppState } from '../../store/types' import { getActiveTabNavOrder, getGroupVisibleTabOrder } from './group-tab-order' import { buildOrderedTabItems } from './tab-bar-item-model' +import { makeOpenFile } from '../../store/slices/store-session-test-harness' +import { makeTerminalTab } from '../../store/slices/worktrees-slice-test-fixtures' function terminalTab(id: string, groupId: string, entityId: string, sortOrder: number): Tab { return { @@ -339,8 +341,11 @@ describe('getActiveTabNavOrder', () => { // Stale legacy order still in insertion order: tabBarOrderByWorktree: { wt: ['term-1', 'term-2', 'term-3'] }, tabsByWorktree: { - // @ts-expect-error — minimal TerminalTab shape; nav helper only reads `id` - wt: [{ id: 'term-1' }, { id: 'term-2' }, { id: 'term-3' }] + wt: [ + makeTerminalTab({ id: 'term-1', worktreeId: 'wt' }), + makeTerminalTab({ id: 'term-2', worktreeId: 'wt' }), + makeTerminalTab({ id: 'term-3', worktreeId: 'wt' }) + ] } }) expect(getActiveTabNavOrder(state, 'wt').map((t) => t.id)).toEqual([ @@ -374,13 +379,12 @@ describe('getActiveTabNavOrder', () => { groupsByWorktree: { wt: [activeGroup, otherGroup] }, unifiedTabsByWorktree: { wt: tabs }, tabsByWorktree: { - // @ts-expect-error — minimal shape for terminal presence only - wt: [{ id: 'term-1' }, { id: 'term-2' }] + wt: [ + makeTerminalTab({ id: 'term-1', worktreeId: 'wt' }), + makeTerminalTab({ id: 'term-2', worktreeId: 'wt' }) + ] }, - openFiles: [ - // @ts-expect-error — minimal OpenFile shape; nav helper only reads `id` and `worktreeId` - { id: '/repo/file.md', worktreeId: 'wt' } - ] + openFiles: [makeOpenFile({ id: '/repo/file.md', worktreeId: 'wt' })] }) expect(getActiveTabNavOrder(state, 'wt')).toEqual([ @@ -394,11 +398,12 @@ describe('getActiveTabNavOrder', () => { tabBarOrderByWorktree: { wt: ['term-1', 'sim-1', 'e1', 'term-2'] }, unifiedTabsByWorktree: { wt: [simulatorTab('sim-1', 'g1', 3)] }, tabsByWorktree: { - // @ts-expect-error — minimal shape - wt: [{ id: 'term-1' }, { id: 'term-2' }] + wt: [ + makeTerminalTab({ id: 'term-1', worktreeId: 'wt' }), + makeTerminalTab({ id: 'term-2', worktreeId: 'wt' }) + ] }, - // @ts-expect-error — minimal shape - openFiles: [{ id: 'e1', worktreeId: 'wt' }] + openFiles: [makeOpenFile({ id: 'e1', worktreeId: 'wt' })] }) expect(getActiveTabNavOrder(state, 'wt')).toEqual([ { type: 'terminal', id: 'term-1' }, diff --git a/src/renderer/src/components/tab-bar/tab-bar-windows-shell-launch-render-stubs.ts b/src/renderer/src/components/tab-bar/tab-bar-windows-shell-launch-render-stubs.ts index 68591f0ddec..51385d55793 100644 --- a/src/renderer/src/components/tab-bar/tab-bar-windows-shell-launch-render-stubs.ts +++ b/src/renderer/src/components/tab-bar/tab-bar-windows-shell-launch-render-stubs.ts @@ -1,8 +1,9 @@ +import type * as ReactModule from 'react' import { vi } from 'vitest' // Headless React: hooks are stubbed so TabBar can be invoked as a plain function. export async function stubHeadlessReact(): Promise> { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, memo: (component: T) => component, diff --git a/src/renderer/src/components/tab-bar/use-omnibox-browser-history.ts b/src/renderer/src/components/tab-bar/use-omnibox-browser-history.ts index 9b1d9947a08..f1e9483c0c2 100644 --- a/src/renderer/src/components/tab-bar/use-omnibox-browser-history.ts +++ b/src/renderer/src/components/tab-bar/use-omnibox-browser-history.ts @@ -95,8 +95,7 @@ export function useOmniboxBrowserHistory({ ) const prepared = useMemo( () => (history ? prepareBrowserHistoryEntries(history) : null), - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Snapshot per menu open; a background navigation must not reshuffle rows mid-keystroke. - [enabled] + [history] ) const deferredQuery = useDeferredValue(query) const deferredRows = useMemo(() => matchRows(prepared, deferredQuery), [deferredQuery, prepared]) diff --git a/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts b/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts index ed45228a70f..70ba760fc59 100644 --- a/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts +++ b/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts @@ -43,8 +43,8 @@ const storeBox = vi.hoisted(() => ({ state: null as Record | null })) -vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() +vi.mock(import('react'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, useCallback: (callback: T) => callback, diff --git a/src/renderer/src/components/task-page-jira-saved-filter-load-failure.test.tsx b/src/renderer/src/components/task-page-jira-saved-filter-load-failure.test.tsx index 2fe8147b9e2..16e4a4d3fb9 100644 --- a/src/renderer/src/components/task-page-jira-saved-filter-load-failure.test.tsx +++ b/src/renderer/src/components/task-page-jira-saved-filter-load-failure.test.tsx @@ -1,13 +1,11 @@ // @vitest-environment happy-dom import { useCallback, useRef, useState } from 'react' -import { create } from 'zustand' import { act, cleanup, renderHook, waitFor } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import type { JiraIssue } from '../../../shared/jira/jira-types' import type { JiraSavedFilterState } from '../../../shared/jira/jira-saved-filters' -import type { AppState } from '@/store/types' import type { TaskPageLinearCollectionEffectsModel } from './task-page/linear/use-task-page-linear-collection-effects' -import { createJiraSlice } from '@/store/slices/jira' +import { createTestStore } from '@/store/slices/store-test-helpers' import { useTaskPageJiraSavedFilters } from './use-task-page-jira-saved-filters' import { useTaskPageJiraListEffects } from './use-task-page-jira-list-effects' import type { JiraStatusCatalogItem } from './task-page-jira-status-catalog' @@ -47,14 +45,7 @@ const selectedStatuses: string[] = [] const noop = () => {} const suspendedRead = new Promise(() => {}) -function createStore() { - return create()( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Jira collection actions read only the settings, connection status, and caches initialized here. - (...args) => ({ settings: null, ...createJiraSlice(...args) }) as AppState - ) -} - -function useTaskPageFixture(store: ReturnType, remote: boolean) { +function useTaskPageFixture(store: ReturnType, remote: boolean) { const renders = useRef(0) renders.current += 1 const [jiraIssues, setJiraIssues] = useState([]) @@ -147,7 +138,7 @@ it.each([ ])( 'settles $cache Jira results after $failure and keeps query/refresh working', async (testCase) => { - const store = createStore() + const store = createTestStore() store.setState({ jiraStatus: { connected: true, viewer: null, selectedSiteId: 'site-1' } }) await store.getState().listJiraIssues('assigned', 50) if (testCase.cache === 'stale') { @@ -205,7 +196,7 @@ it.each([ ) it('recovers from a failed owner read and restores the active exact JQL on retry', async () => { - const store = createStore() + const store = createTestStore() store.setState({ jiraStatus: { connected: true, viewer: null, selectedSiteId: 'site-1' } }) const savedState: JiraSavedFilterState = { filters: [{ id: 'restored', name: 'Restored', jql: 'filter = 12345' }], diff --git a/src/renderer/src/components/task-page/gitlab/use-task-page-gitlab-loading.ts b/src/renderer/src/components/task-page/gitlab/use-task-page-gitlab-loading.ts index 4e4fe3be6a1..6ea62831d8d 100644 --- a/src/renderer/src/components/task-page/gitlab/use-task-page-gitlab-loading.ts +++ b/src/renderer/src/components/task-page/gitlab/use-task-page-gitlab-loading.ts @@ -183,7 +183,16 @@ export function useTaskPageGitLabLoading(model stale = true } // eslint-disable-next-line react-hooks/exhaustive-deps -- selectedReposKey covers every selectedRepos field read above (see its GitHub-scoped-context note); keying off the array ref would re-run on every parent render. - }, [taskSource, gitlabView, activeGitlabFilter, gitlabRefreshNonce, selectedReposKey]) + }, [ + taskSource, + gitlabView, + activeGitlabFilter, + gitlabRefreshNonce, + selectedReposKey, + setGitlabError, + setGitlabItems, + setGitlabLoading + ]) // Why: Todos fetch has its own effect — different trigger (no chip filter) and data path (gl.todos is user-scoped, not repo-scoped). useEffect(() => { diff --git a/src/renderer/src/components/task-page/linear/use-task-page-linear-collection-effects.ts b/src/renderer/src/components/task-page/linear/use-task-page-linear-collection-effects.ts index ec186d310c7..8807dab74c9 100644 --- a/src/renderer/src/components/task-page/linear/use-task-page-linear-collection-effects.ts +++ b/src/renderer/src/components/task-page/linear/use-task-page-linear-collection-effects.ts @@ -1,6 +1,7 @@ import type { TaskPageLinearInOrcaEffectsModel } from './use-task-page-linear-in-orca-effects' import { useEffect } from 'react' -import { TASK_SEARCH_DEBOUNCE_MS, LINEAR_ITEM_LIMIT } from '../../task-page-source-context' +import { LINEAR_ITEM_LIMIT } from '../../task-page-source-context' +import { useTaskSearchDebouncedApply } from '../use-task-search-debounced-apply' import { clampLinearIssueListLimit } from '../../../../../shared/linear/issue-read-limits' import { useTaskPageLinearCustomViewEffects } from './use-task-page-linear-custom-view-effects' @@ -36,15 +37,11 @@ export function useTaskPageLinearCollectionEffectsPrelude(model: TaskPageLinearI setLinearProjectIssuesError, setLinearProjectParentView } = model - useEffect(() => { - if (!taskResumeApplied) { - return - } - const timeout = window.setTimeout(() => { - setAppliedLinearProjectSearch(linearProjectSearchInput) - }, TASK_SEARCH_DEBOUNCE_MS) - return () => window.clearTimeout(timeout) - }, [linearProjectSearchInput, taskResumeApplied, setAppliedLinearProjectSearch]) + useTaskSearchDebouncedApply( + linearProjectSearchInput, + taskResumeApplied, + setAppliedLinearProjectSearch + ) useEffect(() => { if (!taskResumeApplied || taskSource !== 'linear' || linearMode !== 'projects') { return @@ -84,7 +81,6 @@ export function useTaskPageLinearCollectionEffectsPrelude(model: TaskPageLinearI return () => { cancelled = true } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ taskResumeApplied, taskSource, @@ -95,7 +91,11 @@ export function useTaskPageLinearCollectionEffectsPrelude(model: TaskPageLinearI appliedLinearProjectSearch, linearRefreshNonce, getCachedLinearProjects, - linearTaskSourceContext + listLinearProjectsFromStore, + linearTaskSourceContext, + setLinearProjectsError, + setLinearProjectsLoading, + setLinearProjectsResult ]) useEffect(() => { if (!selectedLinearProject?.workspaceId) { diff --git a/src/renderer/src/components/task-page/linear/use-task-page-linear-custom-view-effects.ts b/src/renderer/src/components/task-page/linear/use-task-page-linear-custom-view-effects.ts index 979a642704a..02f876447ad 100644 --- a/src/renderer/src/components/task-page/linear/use-task-page-linear-custom-view-effects.ts +++ b/src/renderer/src/components/task-page/linear/use-task-page-linear-custom-view-effects.ts @@ -87,7 +87,6 @@ export function useTaskPageLinearCustomViewEffects(model: TaskPageLinearInOrcaEf return () => { cancelled = true } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ taskResumeApplied, taskSource, @@ -98,7 +97,10 @@ export function useTaskPageLinearCustomViewEffects(model: TaskPageLinearInOrcaEf linearRefreshNonce, getCachedLinearCustomViews, listLinearCustomViews, - linearTaskSourceContext + linearTaskSourceContext, + setLinearCustomViewsError, + setLinearCustomViewsLoading, + setLinearCustomViewsResult ]) useEffect(() => { if (!selectedLinearCustomView?.workspaceId) { diff --git a/src/renderer/src/components/task-page/linear/use-task-page-linear-list-effects.ts b/src/renderer/src/components/task-page/linear/use-task-page-linear-list-effects.ts index f27304be819..dc7f9fff94d 100644 --- a/src/renderer/src/components/task-page/linear/use-task-page-linear-list-effects.ts +++ b/src/renderer/src/components/task-page/linear/use-task-page-linear-list-effects.ts @@ -17,7 +17,8 @@ import type { LinearIssue } from '../../../../../shared/linear/issue-types' import type { LinearCollectionResult } from '../../../../../shared/linear/workspace-types' import { linearIssueAttributeFilterSignature } from '../../../../../shared/linear/issue-attribute-filter' import { reconcileTaskPageLinearIssuesAfterLandingRefresh } from './task-page-linear-cache-selectors' -import { LINEAR_ITEM_LIMIT, TASK_SEARCH_DEBOUNCE_MS } from '../../task-page-source-context' +import { LINEAR_ITEM_LIMIT } from '../../task-page-source-context' +import { useTaskSearchDebouncedApply } from '../use-task-search-debounced-apply' export function useTaskPageLinearListEffects(model: TaskPageGlobalEffectsModel) { const { setTaskResumeState, @@ -59,16 +60,8 @@ export function useTaskPageLinearListEffects(model: TaskPageGlobalEffectsModel) lastLinearRequestRef, landingLinearRefreshKeysRef } = model - // Why: debounce the Linear search input so we don't fire a request per keystroke (300ms, matching GitHub search). - useEffect(() => { - if (!taskResumeApplied) { - return - } - const timeout = window.setTimeout(() => { - setAppliedLinearSearch(linearSearchInput) - }, TASK_SEARCH_DEBOUNCE_MS) - return () => window.clearTimeout(timeout) - }, [linearSearchInput, taskResumeApplied, setAppliedLinearSearch]) + // Why: debounce the Linear search input so we don't fire a request per keystroke (matching GitHub search). + useTaskSearchDebouncedApply(linearSearchInput, taskResumeApplied, setAppliedLinearSearch) useEffect(() => { if (!taskResumeApplied) { return @@ -280,8 +273,6 @@ export function useTaskPageLinearListEffects(model: TaskPageGlobalEffectsModel) return () => { cancelled = true } - // Why: searchLinearIssues/listLinearIssues are stable selectors; adding them would re-run the effect on unrelated store updates. - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ taskSource, linearMode, @@ -291,10 +282,20 @@ export function useTaskPageLinearListEffects(model: TaskPageGlobalEffectsModel) linearIssueLimit, linearRefreshNonce, linearAttributeFilter, + linearAttributeFilterWorkspaceId, linearListInvalidationVersionForSource, taskResumeApplied, getCachedLinearIssues, - linearTaskSourceContext + searchLinearIssues, + listLinearIssues, + linearTaskSourceContext, + linearAttributeFilterReadRef, + lastLinearRequestRef, + landingLinearRefreshKeysRef, + setLinearError, + setLinearIssues, + setLinearIssuesHasMore, + setLinearLoading ]) // Why: Has Worktree loads Linear tickets linked on local worktrees, not a Linear list/search query. diff --git a/src/renderer/src/components/task-page/use-task-search-debounced-apply.ts b/src/renderer/src/components/task-page/use-task-search-debounced-apply.ts new file mode 100644 index 00000000000..c6a1732e2d5 --- /dev/null +++ b/src/renderer/src/components/task-page/use-task-search-debounced-apply.ts @@ -0,0 +1,17 @@ +import { useEffect } from 'react' +import { TASK_SEARCH_DEBOUNCE_MS } from '../task-page-source-context' + +/** Applies a task search input after the shared debounce, once resume state has landed. */ +export function useTaskSearchDebouncedApply( + input: string, + ready: boolean, + apply: (value: string) => void +): void { + useEffect(() => { + if (!ready) { + return + } + const timeout = window.setTimeout(() => apply(input), TASK_SEARCH_DEBOUNCE_MS) + return () => window.clearTimeout(timeout) + }, [input, ready, apply]) +} diff --git a/src/renderer/src/components/terminal-pane/MobileDriverOverlay.test.tsx b/src/renderer/src/components/terminal-pane/MobileDriverOverlay.test.tsx index dea8f870679..9541777e03a 100644 --- a/src/renderer/src/components/terminal-pane/MobileDriverOverlay.test.tsx +++ b/src/renderer/src/components/terminal-pane/MobileDriverOverlay.test.tsx @@ -1,4 +1,5 @@ import type React from 'react' +import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { MobileDriverOverlay } from './MobileDriverOverlay' @@ -23,7 +24,7 @@ const hookRuntime = vi.hoisted(() => ({ })) vi.mock('react', async () => { - const actual = await vi.importActual('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import() + const actual = await vi.importActual('react') return { ...actual, useCallback unknown>(callback: T): T { diff --git a/src/renderer/src/components/terminal-pane/launch-parity-window-tab.test.ts b/src/renderer/src/components/terminal-pane/launch-parity-window-tab.test.ts index 5ae2070cd99..2ec45683440 100644 --- a/src/renderer/src/components/terminal-pane/launch-parity-window-tab.test.ts +++ b/src/renderer/src/components/terminal-pane/launch-parity-window-tab.test.ts @@ -28,6 +28,7 @@ import { windowSpawnRequest, type WindowLaunchCase } from '../../../../shared/launch-parity-window-request.test-fixture' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' // Main's answer to the window's layout commands, as a switch-on build sends them. vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) => @@ -193,8 +194,7 @@ async function paneSpawnRequest(c: WindowLaunchCase): Promise { ...managerOptions.terminalOptions?.(pane.id) } pane.terminal.options.vtExtensions = { kittyKeyboard: vtExtensions?.kittyKeyboard === true } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the pane fixtures model every member connectPanePty reads, as in pty-connection-startup-command-delivery.test.ts. - modules.pane.connectPanePty(pane as never, createManager(1) as never, deps as never) + modules.pane.connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(20) const [request] = spawn.mock.calls[0] ?? [] // Fixed ids so the table can name them; the color replies are this harness's theme. diff --git a/src/renderer/src/components/terminal-pane/pending-split-close-test-fixture.ts b/src/renderer/src/components/terminal-pane/pending-split-close-test-fixture.ts index 12182fc2aeb..79bf788d526 100644 --- a/src/renderer/src/components/terminal-pane/pending-split-close-test-fixture.ts +++ b/src/renderer/src/components/terminal-pane/pending-split-close-test-fixture.ts @@ -1,8 +1,3 @@ -/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the sibling - pending-split-close specs, not shipped code, and it falls outside the *.test / *.spec / tests - glob set this rule is already switched off for. The calls have to live in one shared module: - `vi.mock` is registered per importing spec, so the alternative is copying all twelve into every - spec, where they would drift apart. */ import { afterEach, beforeEach, vi } from 'vitest' import type { AppState } from '@/store/types' import type { TerminalTabRetirementState } from '@/store/slices/terminal-tab-retirement' diff --git a/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-resume-command.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-resume-command.spec.ts index 8aba9596428..ec8b0c4c0ff 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-resume-command.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-resume-command.spec.ts @@ -24,6 +24,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionColdRestoreResumeCommandTests(): void { let mockStoreState: StoreState @@ -130,7 +131,7 @@ export function registerPtyConnectionColdRestoreResumeCommandTests(): void { .mockResolvedValueOnce(1) .mockResolvedValueOnce(2) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(20) for (const fn of pendingTimeouts) { fn() @@ -258,7 +259,7 @@ export function registerPtyConnectionColdRestoreResumeCommandTests(): void { .mockResolvedValueOnce(1) .mockResolvedValueOnce(2) - connectPanePty(createPane(2) as never, createManager(2) as never, deps as never) + connectPanePty(...mockPanePtyArgs(createPane(2), createManager(2), deps)) await flushAsyncTicks(20) for (const fn of pendingTimeouts) { fn() @@ -336,7 +337,7 @@ export function registerPtyConnectionColdRestoreResumeCommandTests(): void { restoredPtyIdByLeafId: { [LEAF_2]: 'restored-session' } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(20) expect(transport.connect).toHaveBeenCalledTimes(1) @@ -401,7 +402,7 @@ export function registerPtyConnectionColdRestoreResumeCommandTests(): void { restoredPtyIdByLeafId: { [LEAF_2]: 'restored-session' } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(20) expect(transport.connect).toHaveBeenCalledTimes(1) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.spec.ts index 8366f6ef1c7..c339f27a73f 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.spec.ts @@ -28,6 +28,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionCommandFinishedCleanupTests(): void { let mockStoreState: StoreState @@ -83,9 +84,11 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { transportFactoryQueue.push(transport) connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ tabId, isVisibleRef: { current: false } }) as never + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ tabId, isVisibleRef: { current: false } }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() @@ -133,9 +136,11 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { transportFactoryQueue.push(transport) connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ tabId, isVisibleRef: { current: false } }) as never + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ tabId, isVisibleRef: { current: false } }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() @@ -169,9 +174,11 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { const paneKey = makePaneKey('tab-1', LEAF_1) connectPanePty( - createPane(1) as never, - createManager(1) as never, - createDeps({ isVisibleRef: { current: false } }) as never + ...mockPanePtyArgs( + createPane(1), + createManager(1), + createDeps({ isVisibleRef: { current: false } }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() @@ -220,9 +227,11 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { const paneKey = makePaneKey('tab-1', LEAF_1) connectPanePty( - createPane(1) as never, - createManager(1) as never, - createDeps({ isVisibleRef: { current: false } }) as never + ...mockPanePtyArgs( + createPane(1), + createManager(1), + createDeps({ isVisibleRef: { current: false } }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() @@ -270,9 +279,11 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { const paneKey = makePaneKey('tab-1', LEAF_1) connectPanePty( - createPane(1) as never, - createManager(1) as never, - createDeps({ isVisibleRef: { current: false } }) as never + ...mockPanePtyArgs( + createPane(1), + createManager(1), + createDeps({ isVisibleRef: { current: false } }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() @@ -328,9 +339,11 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { const paneKey = makePaneKey('tab-1', LEAF_1) connectPanePty( - createPane(1) as never, - createManager(1) as never, - createDeps({ isVisibleRef: { current: false } }) as never + ...mockPanePtyArgs( + createPane(1), + createManager(1), + createDeps({ isVisibleRef: { current: false } }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() @@ -383,9 +396,11 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { const paneKey = makePaneKey('tab-1', LEAF_1) connectPanePty( - createPane(1) as never, - createManager(1) as never, - createDeps({ isVisibleRef: { current: false } }) as never + ...mockPanePtyArgs( + createPane(1), + createManager(1), + createDeps({ isVisibleRef: { current: false } }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() @@ -430,9 +445,11 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { const paneKey = makePaneKey('tab-1', LEAF_1) connectPanePty( - createPane(1) as never, - createManager(1) as never, - createDeps({ isVisibleRef: { current: false } }) as never + ...mockPanePtyArgs( + createPane(1), + createManager(1), + createDeps({ isVisibleRef: { current: false } }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() @@ -475,12 +492,7 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { const { writes } = captureCallbackTerminalWrites(pane) const deps = createDeps({ isVisibleRef: { current: false } }) - connectPanePty( - pane as never, - createManager(1) as never, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: fixture types intentionally model the production connection boundary. - deps as never - ) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() mockStoreState.agentLaunchConfigByPaneKey[paneKey] = { @@ -530,12 +542,7 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { const { writes } = captureCallbackTerminalWrites(pane) const deps = createDeps({ isVisibleRef: { current: false } }) - connectPanePty( - pane as never, - createManager(1) as never, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: fixture types intentionally model the production connection boundary. - deps as never - ) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() mockStoreState.agentLaunchConfigByPaneKey[paneKey] = { @@ -582,12 +589,7 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { const { writes } = captureCallbackTerminalWrites(pane) const deps = createDeps({ isVisibleRef: { current: false } }) - connectPanePty( - pane as never, - createManager(1) as never, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: fixture types intentionally model the production connection boundary. - deps as never - ) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() mockStoreState.agentLaunchConfigByPaneKey[paneKey] = { @@ -625,9 +627,11 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { const paneKey = makePaneKey(tabId, LEAF_1) connectPanePty( - createPane(1) as never, - createManager(1) as never, - createDeps({ tabId, isVisibleRef: { current: false } }) as never + ...mockPanePtyArgs( + createPane(1), + createManager(1), + createDeps({ tabId, isVisibleRef: { current: false } }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() @@ -667,9 +671,11 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { transportFactoryQueue.push(transport) const paneKey = makePaneKey('tab-1', LEAF_1) connectPanePty( - createPane(1) as never, - createManager(1) as never, - createDeps({ isVisibleRef: { current: false } }) as never + ...mockPanePtyArgs( + createPane(1), + createManager(1), + createDeps({ isVisibleRef: { current: false } }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() @@ -759,7 +765,7 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { return { dispose: vi.fn() } }) as typeof pane.terminal.onData) - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) vi.advanceTimersByTime(1_000) await flushAsyncTicks() expect(capturedDataCallback.current).not.toBeNull() @@ -835,7 +841,7 @@ export function registerPtyConnectionCommandFinishedCleanupTests(): void { return { dispose: vi.fn() } }) as typeof pane.terminal.onData) - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) vi.advanceTimersByTime(1_000) await flushAsyncTicks() if (!onDataHandler) { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-command-finished-late-done.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-command-finished-late-done.test.ts index a02088f35f4..472ac96592f 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-command-finished-late-done.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-command-finished-late-done.test.ts @@ -18,6 +18,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' const { resetAndRefreshAllTerminalWebglAtlases, @@ -168,12 +169,11 @@ describe('command-finished drop and a Done that lands after it', () => { transportFactoryQueue.push(transport) const paneKey = makePaneKey('tab-1', LEAF_1) const pane = createPane(1) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. - const args = [ + const args = mockPanePtyArgs( pane, createManager(1), createDeps({ isVisibleRef: { current: false } }) - ] as unknown as Parameters + ) connectPanePty(...args) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() diff --git a/src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.spec.ts index cb827213299..f9f73139d3f 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.spec.ts @@ -26,6 +26,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionForegroundAgentRoutingTests(): void { let mockStoreState: StoreState @@ -96,7 +97,7 @@ export function registerPtyConnectionForegroundAgentRoutingTests(): void { const manager = createManager(1) const deps = createDeps({ isVisibleRef: { current: false } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) capturedDataCallback.current?.('\x1b]133;D;130\x07thebr ~/repo $ ') await flushAsyncTicks() @@ -130,19 +131,21 @@ export function registerPtyConnectionForegroundAgentRoutingTests(): void { } as StoreState connectPanePty( - createPane(1) as never, - createManager(1) as never, - createDeps({ - startup: { - command: "codex '--dangerously-bypass-approvals-and-sandbox'", - launchConfig: { - agentArgs: '--dangerously-bypass-approvals-and-sandbox', - agentEnv: {} + ...mockPanePtyArgs( + createPane(1), + createManager(1), + createDeps({ + startup: { + command: "codex '--dangerously-bypass-approvals-and-sandbox'", + launchConfig: { + agentArgs: '--dangerously-bypass-approvals-and-sandbox', + agentEnv: {} + }, + launchAgent: 'codex' }, - launchAgent: 'codex' - }, - restoredPtyIdByLeafId: {} - }) as never + restoredPtyIdByLeafId: {} + }) + ) ) expect(mockStoreState.registerAgentLaunchConfig).toHaveBeenCalledWith( @@ -184,9 +187,11 @@ export function registerPtyConnectionForegroundAgentRoutingTests(): void { const paneKey = makePaneKey(tabId, LEAF_1) connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ tabId, isVisibleRef: { current: false } }) as never + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ tabId, isVisibleRef: { current: false } }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() @@ -218,9 +223,11 @@ export function registerPtyConnectionForegroundAgentRoutingTests(): void { transportFactoryQueue.push(createMockTransport(ptyId)) connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ tabId, isVisibleRef: { current: false } }) as never + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ tabId, isVisibleRef: { current: false } }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() @@ -253,12 +260,14 @@ export function registerPtyConnectionForegroundAgentRoutingTests(): void { transportFactoryQueue.push(createMockTransport(ptyId)) connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ - tabId, - startup: { command: 'droid', launchAgent: 'droid' } - }) as never + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ + tabId, + startup: { command: 'droid', launchAgent: 'droid' } + }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() @@ -299,9 +308,11 @@ export function registerPtyConnectionForegroundAgentRoutingTests(): void { transportFactoryQueue.push(createMockTransport(ptyId)) connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ tabId, startup: { command: 'droid', launchAgent: 'droid' } }) as never + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ tabId, startup: { command: 'droid', launchAgent: 'droid' } }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() @@ -339,10 +350,12 @@ export function registerPtyConnectionForegroundAgentRoutingTests(): void { transportFactoryQueue.push(createMockTransport(ptyId)) const binding = connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ tabId, startup: { command: 'droid', launchAgent: 'droid' } }) as never - ) as unknown as { sampleForegroundAgentOnFocus: () => void } + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ tabId, startup: { command: 'droid', launchAgent: 'droid' } }) + ) + ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Captured transport options expose onPtySpawn as an optional PTY-id callback. @@ -387,12 +400,11 @@ export function registerPtyConnectionForegroundAgentRoutingTests(): void { const paneKey = makePaneKey(tabId, LEAF_1) transportFactoryQueue.push(createMockTransport(ptyId)) const binding = connectPanePty( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The pane fixture implements the terminal APIs exercised by this connection test. - createPane(1) as never, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The manager fixture supplies this single pane and its connection callbacks. - createManager(1) as never, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The connection dependency fixture supplies all APIs used by the test with the explicit startup override. - createDeps({ tabId, startup: { command: 'sleep 30 && claude' } }) as never + ...mockPanePtyArgs( + createPane(1), + createManager(1), + createDeps({ tabId, startup: { command: 'sleep 30 && claude' } }) + ) ) await vi.advanceTimersByTimeAsync(30_000) expect(mockStoreState.paneForegroundAgentByPaneKey[paneKey]?.agent).not.toBe('claude') @@ -423,7 +435,7 @@ export function registerPtyConnectionForegroundAgentRoutingTests(): void { const paneKey = makePaneKey(tabId, LEAF_1) transportFactoryQueue.push(createMockTransport(ptyId)) - connectPanePty(pane as never, createManager(1) as never, createDeps({ tabId }) as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps({ tabId }))) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() mockStoreState.paneForegroundAgentByPaneKey[paneKey] = { @@ -467,10 +479,12 @@ export function registerPtyConnectionForegroundAgentRoutingTests(): void { transportFactoryQueue.push(createMockTransport(ptyId)) const binding = connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ tabId, startup: { command: 'pi', launchAgent: 'pi' } }) as never - ) as unknown as { requestWindowsShiftEnterReconfirmation: () => void } + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ tabId, startup: { command: 'pi', launchAgent: 'pi' } }) + ) + ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Captured transport options expose onPtySpawn as an optional PTY-id callback. @@ -530,9 +544,11 @@ export function registerPtyConnectionForegroundAgentRoutingTests(): void { transportFactoryQueue.push(transport) connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ tabId, isVisibleRef: { current: false } }) as never + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ tabId, isVisibleRef: { current: false } }) + ) ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() diff --git a/src/renderer/src/components/terminal-pane/pty-connection-foreground-process-exit.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-foreground-process-exit.spec.ts index 9d796f0879e..4ce20287b03 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-foreground-process-exit.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-foreground-process-exit.spec.ts @@ -16,6 +16,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionForegroundProcessExitTests(): void { let mockStoreState: StoreState @@ -67,12 +68,7 @@ export function registerPtyConnectionForegroundProcessExitTests(): void { restoredPtyIdByLeafId: { [LEAF_1]: ptyId }, isVisibleRef: { current: visible } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: connectPanePty returns the pane binding, which exposes sampleForegroundAgentOnFocus. - const binding = connectPanePty( - createPane(1) as never, - createManager(1) as never, - deps as never - ) as unknown as { sampleForegroundAgentOnFocus: () => void } + const binding = connectPanePty(...mockPanePtyArgs(createPane(1), createManager(1), deps)) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks(20) return { cacheKey: makePaneKey(tabId, LEAF_1), binding } diff --git a/src/renderer/src/components/terminal-pane/pty-connection-foreground-write-path.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-foreground-write-path.spec.ts index 9fb2332b8ff..0a83a06546a 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-foreground-write-path.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-foreground-write-path.spec.ts @@ -19,6 +19,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionForegroundWritePathTests(): void { let mockStoreState: StoreState @@ -67,7 +68,7 @@ export function registerPtyConnectionForegroundWritePathTests(): void { ) transportFactoryQueue.push(transport) - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) await flushAsyncTicks() expect(capturedDataCallback.current).not.toBeNull() @@ -92,7 +93,7 @@ export function registerPtyConnectionForegroundWritePathTests(): void { ) transportFactoryQueue.push(transport) - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) await flushAsyncTicks() const onDataMock = pane.terminal.onData as unknown as { mock: { calls: [[(data: string) => void] | []] } @@ -120,7 +121,7 @@ export function registerPtyConnectionForegroundWritePathTests(): void { ) transportFactoryQueue.push(transport) - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) await flushAsyncTicks() const onDataMock = pane.terminal.onData as unknown as { mock: { calls: [[(data: string) => void] | []] } @@ -145,12 +146,7 @@ export function registerPtyConnectionForegroundWritePathTests(): void { transportFactoryQueue.push(transport) const deferPtyInput = vi.fn() - connectPanePty( - pane as never, - createManager(1) as never, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The dependency fixture provides the input-forwarding hooks exercised by this test. - createDeps({ deferPtyInput }) as never - ) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps({ deferPtyInput }))) await flushAsyncTicks() sendTerminalInputThroughPane(pane, 'a') @@ -168,7 +164,7 @@ export function registerPtyConnectionForegroundWritePathTests(): void { const transport = createMockTransport('pty-1') transportFactoryQueue.push(transport) - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) await flushAsyncTicks() sendTerminalInputThroughPane(pane, 'a') @@ -188,11 +184,7 @@ export function registerPtyConnectionForegroundWritePathTests(): void { ) transportFactoryQueue.push(transport) - const binding = connectPanePty( - pane as never, - createManager(1) as never, - createDeps() as never - ) as unknown as { markShortcutTerminalInputSent: () => void } + const binding = connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) await flushAsyncTicks() binding.markShortcutTerminalInputSent() @@ -216,7 +208,7 @@ export function registerPtyConnectionForegroundWritePathTests(): void { ) transportFactoryQueue.push(transport) - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) await flushAsyncTicks() expect(capturedDataCallback.current).not.toBeNull() @@ -261,7 +253,7 @@ export function registerPtyConnectionForegroundWritePathTests(): void { transportFactoryQueue.push(transport) const pane = createPane(1) - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) await flushAsyncTicks(6) vi.useFakeTimers() @@ -307,7 +299,7 @@ export function registerPtyConnectionForegroundWritePathTests(): void { transportFactoryQueue.push(transport) const pane = createPane(1) - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) await flushAsyncTicks(6) vi.useFakeTimers() @@ -353,7 +345,7 @@ export function registerPtyConnectionForegroundWritePathTests(): void { transportFactoryQueue.push(transport) const pane = createPane(1) - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) await flushAsyncTicks(6) vi.useFakeTimers() @@ -397,7 +389,7 @@ export function registerPtyConnectionForegroundWritePathTests(): void { transportFactoryQueue.push(transport) const pane = createPane(1) - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) await flushAsyncTicks(6) vi.useFakeTimers() @@ -451,7 +443,7 @@ export function registerPtyConnectionForegroundWritePathTests(): void { transportFactoryQueue.push(transport) const pane = createPane(1) - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) await flushAsyncTicks(6) vi.useFakeTimers() diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hibernation-kill-shell-foreground.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hibernation-kill-shell-foreground.test.ts index 20c187dd681..498447ef891 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hibernation-kill-shell-foreground.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hibernation-kill-shell-foreground.test.ts @@ -18,6 +18,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' const { resetAndRefreshAllTerminalWebglAtlases, @@ -165,8 +166,7 @@ describe('shell showing through a hibernation kill', () => { }) transportFactoryQueue.push(transport) const paneKey = makePaneKey('tab-1', LEAF_1) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. - const args = [ + const args = mockPanePtyArgs( createPane(1), createManager(1), createDeps({ @@ -177,7 +177,7 @@ describe('shell showing through a hibernation kill', () => { return suppressed }) }) - ] as unknown as Parameters + ) connectPanePty(...args) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hibernation-wake.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-hibernation-wake.spec.ts index 53241b533f3..2fe6b0f4a65 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hibernation-wake.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hibernation-wake.spec.ts @@ -25,6 +25,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionHibernationWakeTests(): void { let mockStoreState: StoreState @@ -94,10 +95,7 @@ export function registerPtyConnectionHibernationWakeTests(): void { } mockStoreState.suppressedPtyExitIds['tab-pty'] = true - const binding = connectPanePty(pane as never, manager as never, deps as never) as unknown as { - noteVisibilityResume: () => void - dispose: () => void - } + const binding = connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() const onPtyExit = createdTransportOptions[0]?.onPtyExit as @@ -177,8 +175,7 @@ export function registerPtyConnectionHibernationWakeTests(): void { origin: 'live' } mockStoreState.suppressedPtyExitIds['tab-pty'] = true - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. - const args = [pane, createManager(1), deps] as unknown as Parameters + const args = mockPanePtyArgs(pane, createManager(1), deps) const binding = connectPanePty(...args) await flushAsyncTicks() const onPtyExit = createdTransportOptions[0]?.onPtyExit @@ -238,10 +235,7 @@ export function registerPtyConnectionHibernationWakeTests(): void { } mockStoreState.suppressedPtyExitIds['tab-pty'] = true - const binding = connectPanePty(pane as never, manager as never, deps as never) as unknown as { - wakeHibernatedAgentIfArmed: (claimedProviderSessions?: Set) => string | null - dispose: () => void - } + const binding = connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() const onPtyExit = createdTransportOptions[0]?.onPtyExit as @@ -301,13 +295,8 @@ export function registerPtyConnectionHibernationWakeTests(): void { isVisibleRef: { current: false } }) const pane = createPane(2) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. - const args = [pane, createManager(1), deps] as unknown as Parameters - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the pane binding exposes the wake hook this test drives. - const binding = connectPanePty(...args) as unknown as { - wakeHibernatedAgentIfArmed: (claimedProviderSessions?: Set) => string | null - dispose: () => void - } + const args = mockPanePtyArgs(pane, createManager(1), deps) + const binding = connectPanePty(...args) await flushAsyncTicks() expect(transport.connect).toHaveBeenCalledTimes(1) @@ -384,8 +373,7 @@ export function registerPtyConnectionHibernationWakeTests(): void { restoredLeafId: LEAF_2, paneTransportsRef: { current: new Map([[1, createMockTransport('pty-pane-1')]]) } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. - const args = [createPane(2), manager, deps] as unknown as Parameters + const args = mockPanePtyArgs(createPane(2), manager, deps) connectPanePty(...args) const onPtyExit = createdTransportOptions[0]?.onPtyExit expect(onPtyExit).toBeTypeOf('function') @@ -416,10 +404,7 @@ export function registerPtyConnectionHibernationWakeTests(): void { startup: { command: 'codex', launchAgent: 'codex' }, replacesPtyId: 'pty-replaced' }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. - const args = [createPane(2), createManager(1), deps] as unknown as Parameters< - typeof connectPanePty - > + const args = mockPanePtyArgs(createPane(2), createManager(1), deps) // Closing the tab (or parking it) disposes the pane before its deferred connect runs. connectPanePty(...args).dispose() await flushAsyncTicks() @@ -459,10 +444,7 @@ export function registerPtyConnectionHibernationWakeTests(): void { } mockStoreState.suppressedPtyExitIds['tab-pty'] = true - const binding = connectPanePty(pane as never, manager as never, deps as never) as unknown as { - wakeHibernatedAgentIfArmed: (claimedProviderSessions?: Set) => string | null - dispose: () => void - } + const binding = connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() expect((transport.getPtyId as unknown as () => string | null)()).toBe('tab-pty') @@ -518,13 +500,7 @@ export function registerPtyConnectionHibernationWakeTests(): void { updatedAt: 1, origin: 'worktree-sleep' } - const binding = connectPanePty( - createPane(2) as never, - createManager(1) as never, - deps as never - ) as unknown as { - wakeHibernatedAgentIfArmed: () => string | null - } + const binding = connectPanePty(...mockPanePtyArgs(createPane(2), createManager(1), deps)) await flushAsyncTicks() const onPtyExit = createdTransportOptions[0]?.onPtyExit as @@ -581,13 +557,15 @@ export function registerPtyConnectionHibernationWakeTests(): void { origin: 'worktree-sleep' } const binding = connectPanePty( - createPane(2) as never, - createManager(1) as never, - createDeps({ - consumeSuppressedPtyExit: vi.fn(() => true), - isVisibleRef: { current: false } - }) as never - ) as unknown as { wakeHibernatedAgentIfArmed: () => string | null } + ...mockPanePtyArgs( + createPane(2), + createManager(1), + createDeps({ + consumeSuppressedPtyExit: vi.fn(() => true), + isVisibleRef: { current: false } + }) + ) + ) await flushAsyncTicks() const onPtyExit = createdTransportOptions[0]?.onPtyExit as @@ -638,12 +616,12 @@ export function registerPtyConnectionHibernationWakeTests(): void { origin: 'worktree-sleep' } const binding = connectPanePty( - createPane(2) as never, - createManager(1) as never, - createDeps({ isVisibleRef: { current: false } }) as never - ) as unknown as { - wakeHibernatedAgentIfArmed: () => string | null - } + ...mockPanePtyArgs( + createPane(2), + createManager(1), + createDeps({ isVisibleRef: { current: false } }) + ) + ) await flushAsyncTicks() expect(binding.wakeHibernatedAgentIfArmed()).toBeNull() @@ -680,10 +658,7 @@ export function registerPtyConnectionHibernationWakeTests(): void { origin: 'worktree-sleep' } - const binding = connectPanePty(pane as never, manager as never, deps as never) as unknown as { - noteVisibilityResume: () => void - dispose: () => void - } + const binding = connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() const onPtyExit = createdTransportOptions[0]?.onPtyExit as @@ -748,10 +723,7 @@ export function registerPtyConnectionHibernationWakeTests(): void { origin: 'worktree-sleep' } - const binding = connectPanePty(pane as never, manager as never, deps as never) as unknown as { - noteVisibilityResume: () => void - dispose: () => void - } + const binding = connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() const onPtyExit = createdTransportOptions[0]?.onPtyExit as @@ -795,9 +767,7 @@ export function registerPtyConnectionHibernationWakeTests(): void { } } - const binding = connectPanePty(pane as never, manager as never, deps as never) as unknown as { - dispose: () => void - } + const binding = connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() expect(userInputListeners.size).toBeGreaterThan(0) ;(mockStoreState.recordTerminalInput as ReturnType).mockClear() @@ -827,7 +797,7 @@ export function registerPtyConnectionHibernationWakeTests(): void { const deps = createDeps() const pane = createPane(2) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() ;(mockStoreState.recordTerminalInput as ReturnType).mockClear() diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.spec.ts index 789ecbb4753..1b3bf402d56 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.spec.ts @@ -16,6 +16,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { const { resetAndRefreshAllTerminalWebglAtlases, scheduleTerminalWebglAtlasRecovery } = @@ -36,19 +37,6 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { expect(resetAndRefreshAllTerminalWebglAtlases).not.toHaveBeenCalled() } - // Why: connectPanePty takes the real pane/manager/deps types and every test in this file - // passes structural fakes. Funnelling the widening through one helper keeps the casts in - // a single reviewable place instead of three per test. - function connectFakePane( - connect: (pane: never, manager: never, deps: never) => { dispose: () => void }, - pane: unknown, - manager: unknown, - deps: Parameters[0] - ): { dispose: () => void } { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the three values are this suite's structural fakes for pane/manager/deps; connectPanePty only reads the fields they define. - return connect(pane as never, manager as never, createDeps(deps) as never) - } - describe('connectPanePty', () => { beforeEach(async () => { vi.resetModules() @@ -89,11 +77,13 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { const { writes, parseCallbacks } = captureCallbackTerminalWrites(pane) connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ - isVisibleRef: { current: false } - }) as never + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ + isVisibleRef: { current: false } + }) + ) ) await flushAsyncTicks(6) @@ -135,11 +125,13 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { const { writes, parseCallbacks } = captureCallbackTerminalWrites(pane) connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ - isVisibleRef: { current: false } - }) as never + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ + isVisibleRef: { current: false } + }) + ) ) await flushAsyncTicks(6) @@ -177,11 +169,13 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { const { parseCallbacks } = captureCallbackTerminalWrites(pane) connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ - isVisibleRef: { current: false } - }) as never + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ + isVisibleRef: { current: false } + }) + ) ) await flushAsyncTicks(6) @@ -217,11 +211,13 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { const { parseCallbacks } = captureCallbackTerminalWrites(pane) connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ - isVisibleRef: { current: false } - }) as never + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ + isVisibleRef: { current: false } + }) + ) ) await flushAsyncTicks(6) @@ -254,11 +250,13 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { const { writes, parseCallbacks } = captureCallbackTerminalWrites(pane) connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ - isVisibleRef: { current: false } - }) as never + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ + isVisibleRef: { current: false } + }) + ) ) await flushAsyncTicks(6) @@ -302,11 +300,13 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { const isVisibleRef = { current: false } connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ - isVisibleRef - }) as never + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ + isVisibleRef + }) + ) ) await flushAsyncTicks(6) @@ -355,7 +355,7 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { isVisibleRef: { current: true } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(6) expect(capturedDataCallback.current).not.toBeNull() @@ -390,7 +390,7 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { isVisibleRef: { current: true } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(6) expect(capturedDataCallback.current).not.toBeNull() @@ -425,7 +425,7 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { isVisibleRef: { current: true } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(6) expect(capturedDataCallback.current).not.toBeNull() @@ -456,19 +456,21 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef: { current: false }, - startup: { - command: 'wrapped-agent', - telemetry: { - agent_kind: 'codex', - launch_source: 'tab_bar_quick_launch', - request_kind: 'new' + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef: { current: false }, + startup: { + command: 'wrapped-agent', + telemetry: { + agent_kind: 'codex', + launch_source: 'tab_bar_quick_launch', + request_kind: 'new' + } } - } - }) as never + }) + ) ) await flushAsyncTicks(6) @@ -498,18 +500,24 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { ) transportFactoryQueue.push(transport) - const binding = connectFakePane(connectPanePty, createPane(1), createManager(1), { - isVisibleRef: { current: false }, - startup: { - command: 'jcode', - launchAgent: 'jcode', - telemetry: { - agent_kind: 'jcode', - launch_source: 'tab_bar_quick_launch', - request_kind: 'new' - } - } - }) + const binding = connectPanePty( + ...mockPanePtyArgs( + createPane(1), + createManager(1), + createDeps({ + isVisibleRef: { current: false }, + startup: { + command: 'jcode', + launchAgent: 'jcode', + telemetry: { + agent_kind: 'jcode', + launch_source: 'tab_bar_quick_launch', + request_kind: 'new' + } + } + }) + ) + ) await flushAsyncTicks(6) expect(capturedDataCallback.current).not.toBeNull() @@ -538,19 +546,21 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef: { current: false }, - startup: { - command: 'wrapped-agent', - telemetry: { - agent_kind: 'grok', - launch_source: 'tab_bar_quick_launch', - request_kind: 'new' + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef: { current: false }, + startup: { + command: 'wrapped-agent', + telemetry: { + agent_kind: 'grok', + launch_source: 'tab_bar_quick_launch', + request_kind: 'new' + } } - } - }) as never + }) + ) ) await flushAsyncTicks(6) @@ -583,12 +593,14 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef: { current: false }, - startup: { command: 'codex' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef: { current: false }, + startup: { command: 'codex' } + }) + ) ) await flushAsyncTicks(6) @@ -621,12 +633,14 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef: { current: false }, - startup: { command: '/Users/me/.grok/bin/grok --permission-mode bypassPermissions' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef: { current: false }, + startup: { command: '/Users/me/.grok/bin/grok --permission-mode bypassPermissions' } + }) + ) ) await flushAsyncTicks(6) @@ -659,12 +673,14 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef: { current: false }, - startup: { command: 'printf noisy startup' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef: { current: false }, + startup: { command: 'printf noisy startup' } + }) + ) ) await flushAsyncTicks(6) @@ -699,9 +715,7 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ isVisibleRef: { current: false } }) as never + ...mockPanePtyArgs(pane, manager, createDeps({ isVisibleRef: { current: false } })) ) await flushAsyncTicks(6) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.spec.ts index 921088e8383..c8baf496b91 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.spec.ts @@ -16,6 +16,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionHiddenBacklogReconciliationTests(): void { let mockStoreState: StoreState @@ -87,10 +88,7 @@ export function registerPtyConnectionHiddenBacklogReconciliationTests(): void { transportFactoryQueue.push(transport) const pane = createPane(1) const manager = createManager(1) - const binding = connectPanePty(pane as never, manager as never, deps as never) as { - syncProcessTracking: () => void - dispose: () => void - } + const binding = connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(6) expect(capturedDataCallback.current).not.toBeNull() return { transport, pane, dataCallback: capturedDataCallback.current!, binding } @@ -128,9 +126,7 @@ export function registerPtyConnectionHiddenBacklogReconciliationTests(): void { onPtySpawn?: (ptyId: string) => void } transportOptions.onPtySpawn?.('pty-id') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: installTerminalTestGlobals installs getMainBufferSnapshot as a vi.fn mock. - const getMainBufferSnapshot = window.api.pty - .getMainBufferSnapshot as unknown as ReturnType + const getMainBufferSnapshot = vi.mocked(window.api.pty.getMainBufferSnapshot) const firstSnapshot = createDeferred<{ data: string cols: number @@ -289,9 +285,7 @@ export function registerPtyConnectionHiddenBacklogReconciliationTests(): void { onPtySpawn?: (ptyId: string) => void } transportOptions.onPtySpawn?.('pty-id') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: installTerminalTestGlobals installs getMainBufferSnapshot as a vi.fn mock. - const getMainBufferSnapshot = window.api.pty - .getMainBufferSnapshot as unknown as ReturnType + const getMainBufferSnapshot = vi.mocked(window.api.pty.getMainBufferSnapshot) getMainBufferSnapshot.mockResolvedValue({ data: 'hidden reveal snapshot\r\n', cols: 100, @@ -330,9 +324,7 @@ export function registerPtyConnectionHiddenBacklogReconciliationTests(): void { onPtySpawn?: (ptyId: string) => void } transportOptions.onPtySpawn?.('pty-id') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: installTerminalTestGlobals installs getMainBufferSnapshot as a vi.fn mock. - const getMainBufferSnapshot = window.api.pty - .getMainBufferSnapshot as unknown as ReturnType + const getMainBufferSnapshot = vi.mocked(window.api.pty.getMainBufferSnapshot) getMainBufferSnapshot.mockResolvedValue({ data: 'restored snapshot\r\n', cols: 100, @@ -453,9 +445,7 @@ export function registerPtyConnectionHiddenBacklogReconciliationTests(): void { onPtySpawn?: (ptyId: string) => void } transportOptions.onPtySpawn?.('pty-id') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: installTerminalTestGlobals installs getMainBufferSnapshot as a vi.fn mock. - const getMainBufferSnapshot = window.api.pty - .getMainBufferSnapshot as unknown as ReturnType + const getMainBufferSnapshot = vi.mocked(window.api.pty.getMainBufferSnapshot) // Visible prompt echo metered in main's cumulative seq domain. dataCallback('$ node frame-script.mjs\r\n', { seq: 2_315, rawLength: 25 }) // Pane hides mid-stream; main drops the hidden frame and marks restore. diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-codex-queries.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-codex-queries.spec.ts index 7a9efa07d24..a3bd3b7221b 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-codex-queries.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-codex-queries.spec.ts @@ -24,6 +24,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionHiddenCodexQueriesTests(): void { let mockStoreState: StoreState @@ -74,12 +75,14 @@ export function registerPtyConnectionHiddenCodexQueriesTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef: { current: false }, - startup: { command: 'codex' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef: { current: false }, + startup: { command: 'codex' } + }) + ) ) await flushAsyncTicks(6) @@ -115,13 +118,14 @@ export function registerPtyConnectionHiddenCodexQueriesTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The connection fixture supplies the hidden pane APIs with this explicit startup command. - createDeps({ - isVisibleRef: { current: false }, - startup: { command } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef: { current: false }, + startup: { command } + }) + ) ) await flushAsyncTicks(6) @@ -148,12 +152,14 @@ export function registerPtyConnectionHiddenCodexQueriesTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef: { current: false }, - startup: { command: 'codex' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef: { current: false }, + startup: { command: 'codex' } + }) + ) ) await flushAsyncTicks(6) @@ -181,12 +187,14 @@ export function registerPtyConnectionHiddenCodexQueriesTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef: { current: false }, - startup: { command: 'codex' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef: { current: false }, + startup: { command: 'codex' } + }) + ) ) await flushAsyncTicks(6) @@ -219,12 +227,14 @@ export function registerPtyConnectionHiddenCodexQueriesTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef: { current: false }, - startup: { command: 'codex' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef: { current: false }, + startup: { command: 'codex' } + }) + ) ) await flushAsyncTicks(6) @@ -257,12 +267,14 @@ export function registerPtyConnectionHiddenCodexQueriesTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef: { current: false }, - startup: { command: 'codex' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef: { current: false }, + startup: { command: 'codex' } + }) + ) ) await flushAsyncTicks(6) @@ -293,12 +305,14 @@ export function registerPtyConnectionHiddenCodexQueriesTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef: { current: false }, - startup: { command: 'codex' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef: { current: false }, + startup: { command: 'codex' } + }) + ) ) await flushAsyncTicks(6) @@ -330,12 +344,14 @@ export function registerPtyConnectionHiddenCodexQueriesTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef, - startup: { command: 'codex' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef, + startup: { command: 'codex' } + }) + ) ) try { await flushAsyncTicks(6) @@ -374,12 +390,14 @@ export function registerPtyConnectionHiddenCodexQueriesTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef, - startup: { command: 'codex' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef, + startup: { command: 'codex' } + }) + ) ) try { await flushAsyncTicks(6) @@ -436,12 +454,14 @@ export function registerPtyConnectionHiddenCodexQueriesTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef, - startup: { command: 'codex' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef, + startup: { command: 'codex' } + }) + ) ) try { await flushAsyncTicks(6) @@ -506,9 +526,11 @@ export function registerPtyConnectionHiddenCodexQueriesTests(): void { const isVisibleRef = { current: false } const pane = createPane(1) const binding = connectPanePty( - pane as never, - createManager(1) as never, - createDeps({ isVisibleRef, startup: { command: 'codex' } }) as never + ...mockPanePtyArgs( + pane, + createManager(1), + createDeps({ isVisibleRef, startup: { command: 'codex' } }) + ) ) try { await flushAsyncTicks(6) @@ -555,12 +577,14 @@ export function registerPtyConnectionHiddenCodexQueriesTests(): void { const pane = createPane(1) const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef, - startup: { command: 'codex' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef, + startup: { command: 'codex' } + }) + ) ) try { await flushAsyncTicks(6) @@ -612,12 +636,14 @@ export function registerPtyConnectionHiddenCodexQueriesTests(): void { pane.terminal.buffer.active.type = 'alternate' const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ - isVisibleRef, - startup: { command: 'codex' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + isVisibleRef, + startup: { command: 'codex' } + }) + ) ) try { await flushAsyncTicks(6) @@ -667,9 +693,11 @@ export function registerPtyConnectionHiddenCodexQueriesTests(): void { pane.terminal.buffer.active.type = 'alternate' const manager = createManager(1) const binding = connectPanePty( - pane as never, - manager as never, - createDeps({ isVisibleRef, startup: { command: 'codex' } }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ isVisibleRef, startup: { command: 'codex' } }) + ) ) try { await flushAsyncTicks(6) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-sleeping-resume.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-sleeping-resume.spec.ts index ba34593e67f..a149b8713cd 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-sleeping-resume.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-sleeping-resume.spec.ts @@ -25,6 +25,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' type HiddenPaneBinding = { noteVisibilityResume: () => void @@ -108,12 +109,8 @@ export function registerPtyConnectionHiddenSleepingResumeTests(): void { } function connectPane(deps: ReturnType): HiddenPaneBinding { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. - const args = [createPane(2), createManager(2), deps] as unknown as Parameters< - typeof connectPanePty - > - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: connectPanePty returns the PanePtyBinding, which has both methods. - return connectPanePtyRef!(...args) as unknown as HiddenPaneBinding + const args = mockPanePtyArgs(createPane(2), createManager(2), deps) + return connectPanePtyRef!(...args) } function setHostLiveness(isLive: boolean | null): void { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-kitty-keyboard-restore.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-kitty-keyboard-restore.spec.ts index 9e8ca0e2b7c..bfc3f059bf3 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-kitty-keyboard-restore.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-kitty-keyboard-restore.spec.ts @@ -21,6 +21,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionKittyKeyboardRestoreTests(): void { let mockStoreState: StoreState @@ -77,8 +78,7 @@ export function registerPtyConnectionKittyKeyboardRestoreTests(): void { restoredLeafId: LEAF_1, restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: pane-connection mocks cover the members connectPanePty reads. - connectPanePty(pane as never, createManager(1) as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(20) return { pane, mirror: deps.paneKittyKeyboardModesRef.current.get(pane.id) } } @@ -115,8 +115,7 @@ export function registerPtyConnectionKittyKeyboardRestoreTests(): void { mirror.scan('\x1b[?1049h\x1b[>5u') deps.paneKittyKeyboardModesRef.current.set(pane.id, mirror) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: pane-connection mocks cover the members connectPanePty reads. - connectPanePty(pane as never, createManager(91) as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(91), deps)) await flushAsyncTicks(20) expect(pane.terminal.write).toHaveBeenCalledWith( diff --git a/src/renderer/src/components/terminal-pane/pty-connection-layout-owner-wake.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-layout-owner-wake.test.ts index cbf9e258908..8e528db8b1f 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-layout-owner-wake.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-layout-owner-wake.test.ts @@ -22,6 +22,7 @@ import { mainLayoutApplyTransport, mainStartsPaneAs } from './pty-connection-test-main-pane-start-fixtures' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' const mocks = vi.hoisted(() => ({ wake: vi.fn() })) @@ -98,10 +99,7 @@ describe('wake through main in a layout-owner build', () => { 'sess-other' ) mockStoreState.suppressedPtyExitIds['tab-pty'] = true - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. - const args = [createPane(2), createManager(1), deps] as unknown as Parameters< - typeof connectPanePty - > + const args = mockPanePtyArgs(createPane(2), createManager(1), deps) // Main's real wake must find the pane still sleeping: the window's record drop follows it. let recordHeldAtWake: boolean | undefined mocks.wake.mockImplementation(() => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-main-pane-restore-start.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-main-pane-restore-start.test.ts index 3877b7b186b..cd050bf63e3 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-main-pane-restore-start.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-main-pane-restore-start.test.ts @@ -23,6 +23,7 @@ import { restoreTerminalTestGlobals } from './pty-connection-test-environment' import { PANE_NOT_LIVE_ERROR } from '../../../../shared/pane-attach-only-view' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true })) @@ -88,10 +89,7 @@ async function connectBound( restoredPtyIdByLeafId: { [LEAF]: 'pty-old' }, isVisibleRef: { current: visible } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. - const args = [createPane(1), createManager(1), deps] as unknown as Parameters< - typeof connectPanePty - > + const args = mockPanePtyArgs(createPane(1), createManager(1), deps) const connection = connectPanePty(...args) await flushAsyncTicks() return { transport, deps, connection } diff --git a/src/renderer/src/components/terminal-pane/pty-connection-main-pane-start.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-main-pane-start.test.ts index 1ff5b2fdeaa..8b953bcdfd7 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-main-pane-start.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-main-pane-start.test.ts @@ -24,6 +24,7 @@ import { mainLayoutApplyTransport, mainStartsPaneAs } from './pty-connection-test-main-pane-start-fixtures' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true })) // Main's answer to the window's layout commands, as a switch-on build sends them. @@ -67,10 +68,7 @@ async function connectWithStart(start: TerminalPaneStart | undefined) { terminalPaneStartsByPaneKey: start ? { [PANE_KEY]: start } : {} }) const deps = buildPaneConnectionDeps(() => mockStoreState, { tabId: TAB }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. - const args = [createPane(1), createManager(1), deps] as unknown as Parameters< - typeof connectPanePty - > + const args = mockPanePtyArgs(createPane(1), createManager(1), deps) connectPanePty(...args) await flushAsyncTicks() return { transport, deps } @@ -143,10 +141,7 @@ describe('a pane main is starting a terminal for', () => { tabId: TAB, paneTransportsRef: { current: new Map([[1, source]]) } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. - const args = [createPane(2), createManager(2), deps] as unknown as Parameters< - typeof connectPanePty - > + const args = mockPanePtyArgs(createPane(2), createManager(2), deps) connectPanePty(...args) await flushAsyncTicks() expect(transport.connect).not.toHaveBeenCalled() diff --git a/src/renderer/src/components/terminal-pane/pty-connection-mocks.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-mocks.spec.ts index d5188e87ed0..be307fb141b 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-mocks.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-mocks.spec.ts @@ -1,4 +1,3 @@ -// oxlint-disable anti-slop/no-module-mocking -- Test-only mocks shared by PTY connection registration suites. import type * as React from 'react' import { vi } from 'vitest' import type { Mock } from 'vitest' diff --git a/src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts index 66a6ff1c4fd..9a850570a0b 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts @@ -19,6 +19,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' const { resetAndRefreshAllTerminalWebglAtlases, @@ -164,7 +165,6 @@ describe('connectPanePty', () => { const pane = createPane(1) const synchronizedOutput = { synchronizedOutput: false } const renderRows = vi.fn() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: test fixture exposes xterm's private render state to observe the DEC 2026 present. ;( pane.terminal as unknown as { _core: { @@ -194,8 +194,7 @@ describe('connectPanePty', () => { callback?.() }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: fixture types intentionally model the production connection boundary. - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) await flushAsyncTicks(6) vi.useFakeTimers() diff --git a/src/renderer/src/components/terminal-pane/pty-connection-process-exit-row-retire.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-process-exit-row-retire.test.ts index 302ae5aac95..653338764b5 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-process-exit-row-retire.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-process-exit-row-retire.test.ts @@ -22,6 +22,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' const { coordinatorOptionsByPaneKey, @@ -147,16 +148,16 @@ async function connectAgentPane(args: { transportFactoryQueue.push(transport) const tabId = args.tabId ?? 'tab-1' const paneId = args.paneId ?? 1 - const connectArgs = [ - createPane(paneId), - createManager(paneId), - buildPaneConnectionDeps(() => mockStoreState, { - tabId, - isVisibleRef: { current: args.visible ?? false } - }) - ] - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared pane fixtures implement the members connectPanePty touches. - connectPanePty(...(connectArgs as unknown as Parameters)) + connectPanePty( + ...mockPanePtyArgs( + createPane(paneId), + createManager(paneId), + buildPaneConnectionDeps(() => mockStoreState, { + tabId, + isVisibleRef: { current: args.visible ?? false } + }) + ) + ) await vi.advanceTimersByTimeAsync(20) await flushAsyncTicks() const paneKey = makePaneKey(tabId, leafIdForPane(paneId)) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.spec.ts index cc784b26142..21590cbb057 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.spec.ts @@ -43,6 +43,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionReattachModeResetTests(): void { let mockStoreState: StoreState @@ -115,7 +116,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { pane.terminal.buffer.active.viewportY = pane.terminal.buffer.active.baseY }) - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) await flushAsyncTicks() expect(pane.terminal.buffer.active.viewportY).toBe(40) expect(renderListener.current).not.toBeNull() @@ -156,7 +157,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(20) const tailWriteCall = pane.terminal.write.mock.invocationCallOrder.find( @@ -179,10 +180,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { 'pty-live': { previousAccountLabel: 'A', nextAccountLabel: 'B' } } const pane = createPane(1) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: fixtures implement the connection's pane, manager and dependency contract. - const args = [pane, createManager(1), createDeps()] as unknown as Parameters< - typeof connectPanePty - > + const args = mockPanePtyArgs(pane, createManager(1), createDeps()) const binding = connectPanePty(...args) transport.getPtyId.mockReturnValue('pty-live') mockStoreState.codexRestartNoticeByPtyId = { @@ -205,7 +203,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { const manager = createManager(1, 1) const deps = createDeps() - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() // xterm answers CSI 6n natively with a CPR via onData; it must take the immediate path (skips the remote 8ms debounce that corrupted it). @@ -266,7 +264,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { const manager = createManager(1, 1) const deps = createDeps() - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(20) expect(replayCallback.current).toBeTypeOf('function') @@ -308,7 +306,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(20) expect(transport.sendInput).toHaveBeenCalledWith('\x1b[I', 'query-reply') @@ -344,12 +342,14 @@ export function registerPtyConnectionReattachModeResetTests(): void { const pane = createPane(1) const manager = createManager(1) connectPanePty( - pane as never, - manager as never, - createDeps({ - restoredLeafId: LEAF_1, - restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' } - }) as never + ...mockPanePtyArgs( + pane, + manager, + createDeps({ + restoredLeafId: LEAF_1, + restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' } + }) + ) ) await flushAsyncTicks(20) @@ -394,7 +394,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(20) const writes = (pane.terminal.write as ReturnType).mock.calls.map( @@ -460,7 +460,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { }) } - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(30) expect(transport.connect).toHaveBeenCalledTimes(2) @@ -514,7 +514,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(20) expect(pane.terminal.write).toHaveBeenCalledWith( @@ -547,7 +547,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(20) expect(transport.sendInput.mock.calls.map(([data]) => data)).not.toContain('\x1b[I') @@ -583,7 +583,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(20) expect(transport.sendInput.mock.calls.map(([data]) => data)).not.toContain('\x1b[I') @@ -618,7 +618,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { restoredLeafId: LEAF_1, restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(20) return (pane.terminal.write as ReturnType).mock.calls .map((call) => String(call[0])) @@ -668,7 +668,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(20) expect(transport.sendInput.mock.calls.map(([data]) => data)).not.toContain('\x1b[I') @@ -706,7 +706,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(20) expect(transport.sendInput.mock.calls.map(([data]) => data)).not.toContain('\x1b[I') @@ -747,7 +747,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(20) expect(transport.sendInput.mock.calls.map(([data]) => data)).not.toContain('\x1b[I') @@ -802,8 +802,7 @@ export function registerPtyConnectionReattachModeResetTests(): void { restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: fixtures implement the connection's pane, manager and dependency contract. - const args = [pane, manager, deps] as unknown as Parameters + const args = mockPanePtyArgs(pane, manager, deps) const binding = connectPanePty(...args) await flushAsyncTicks(20) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-remote-pause-drop-notice.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-remote-pause-drop-notice.spec.ts index aac178c3ec9..bdecaa91ecb 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-remote-pause-drop-notice.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-remote-pause-drop-notice.spec.ts @@ -15,6 +15,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' // Why: switching back to a remote pane cost a 5000-row snapshot even when the host dropped nothing (#24863). export function registerPtyConnectionRemotePauseDropNoticeTests(): void { @@ -76,8 +77,7 @@ export function registerPtyConnectionRemotePauseDropNoticeTests(): void { const deps = buildPaneConnectionDeps(() => mockStoreState, { isVisibleRef: { current: true } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures model the pane, manager and deps surface connectPanePty reads. - const binding = connectPanePty(pane as never, createManager(1) as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(6) const setVisible = async (visible: boolean): Promise => { deps.isVisibleRef.current = visible diff --git a/src/renderer/src/components/terminal-pane/pty-connection-restart-replacement.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-restart-replacement.spec.ts index 4b18aa93773..3b02087ead3 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-restart-replacement.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-restart-replacement.spec.ts @@ -22,6 +22,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionRestartReplacementTests(): void { let mockStoreState: StoreState @@ -86,8 +87,7 @@ export function registerPtyConnectionRestartReplacementTests(): void { ptyId: 'pty-main', bind: bindsPaneInStore(() => mockStoreState, 'tab-1', leafIdForPane(2)) }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. - const args = [pane, createManager(1), deps] as unknown as Parameters + const args = mockPanePtyArgs(pane, createManager(1), deps) const binding = connectPanePty(...args) await flushAsyncTicks() const onPtyExit = createdTransportOptions[0]?.onPtyExit diff --git a/src/renderer/src/components/terminal-pane/pty-connection-session-liveness.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-session-liveness.spec.ts index 635d2ea7725..3f3bbebabb0 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-session-liveness.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-session-liveness.spec.ts @@ -22,6 +22,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionSessionLivenessTests(): void { let mockStoreState: StoreState @@ -101,7 +102,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { }) const hasPty = vi.fn(async () => false) - const binding = connectPanePty(createPane(2) as never, manager as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(createPane(2), manager, deps)) capturedDataCallback.current?.('shell prompt') binding.reconcileIfSessionMissing(hasPty) @@ -126,7 +127,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { paneTransportsRef: { current: new Map([[1, createMockTransport('pty-pane-1')]]) } }) - const binding = connectPanePty(createPane(2) as never, manager as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(createPane(2), manager, deps)) binding.reconcileIfSessionMissing(vi.fn(async () => true)) binding.reconcileIfSessionMissing(vi.fn(async () => null)) @@ -155,7 +156,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { paneTransportsRef: { current: new Map([[1, createMockTransport('pty-pane-1')]]) } }) - const binding = connectPanePty(createPane(2) as never, manager as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(createPane(2), manager, deps)) binding.reconcileIfSessionMissing(hasPty) transport.getPtyId.mockReturnValue('pty-pane-2-reattached') resolveHasPty(false) @@ -183,7 +184,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { paneTransportsRef: { current: new Map([[1, createMockTransport('pty-pane-1')]]) } }) - const binding = connectPanePty(createPane(2) as never, manager as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(createPane(2), manager, deps)) // Why: clear the freshly-split early-return guard so the only remaining protection is the freshness guard under test. capturedDataCallback.current?.('shell prompt') const onPtySpawn = createdTransportOptions[0]?.onPtySpawn as @@ -220,7 +221,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { paneTransportsRef: { current: new Map([[1, createMockTransport('pty-pane-1')]]) } }) - const binding = connectPanePty(createPane(2) as never, manager as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(createPane(2), manager, deps)) // Why: clear the freshly-split early-return guard so onExit reaches close. capturedDataCallback.current?.('shell prompt') const onPtySpawn = createdTransportOptions[0]?.onPtySpawn as @@ -249,7 +250,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { const manager = createManager(1) const deps = createDeps() - const binding = connectPanePty(createPane(1) as never, manager as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(createPane(1), manager, deps)) // The last pane reattaches to the tab's persisted ptyId ('tab-pty'); let that bind first. await flushAsyncTicks() @@ -272,7 +273,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { paneTransportsRef: { current: new Map([[1, createMockTransport('pty-pane-1')]]) } }) - const binding = connectPanePty(createPane(2) as never, manager as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(createPane(2), manager, deps)) binding.reconcileIfSessionMissing(async () => false) await flushAsyncTicks() @@ -292,7 +293,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { paneTransportsRef: { current: new Map([[1, createMockTransport('pty-pane-1')]]) } }) - const binding = connectPanePty(createPane(2) as never, manager as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(createPane(2), manager, deps)) binding.reconcileIfSessionMissing(async () => false) await flushAsyncTicks() @@ -312,7 +313,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { paneTransportsRef: { current: new Map([[1, createMockTransport('pty-pane-1')]]) } }) - const binding = connectPanePty(createPane(2) as never, manager as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(createPane(2), manager, deps)) binding.reconcileIfSessionMissing(async () => false) await flushAsyncTicks() @@ -346,7 +347,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { paneTransportsRef: { current: new Map([[1, createMockTransport('pty-pane-1')]]) } }) - connectPanePty(createPane(2) as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(createPane(2), manager, deps)) capturedDataCallback.current?.('shell prompt') const onPtyExit = createdTransportOptions[0]?.onPtyExit as | ((ptyId: string) => void) @@ -385,7 +386,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { paneTransportsRef: { current: new Map([[1, createMockTransport('pty-pane-1')]]) } }) - const binding = connectPanePty(createPane(2) as never, manager as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(createPane(2), manager, deps)) capturedDataCallback.current?.('shell prompt') const onPtyExit = createdTransportOptions[0]?.onPtyExit as | ((ptyId: string) => void) @@ -426,7 +427,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { paneTransportsRef: { current: new Map([[1, createMockTransport('pty-pane-1')]]) } }) - const binding = connectPanePty(createPane(2) as never, manager as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(createPane(2), manager, deps)) capturedDataCallback.current?.('shell prompt') const onPtyExit = createdTransportOptions[0]?.onPtyExit as | ((ptyId: string) => void) @@ -457,7 +458,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { transportFactoryQueue.push(transport) const deps = seedUnverifiableRestoredPane() - connectPanePty(createPane(2) as never, createManager(2) as never, deps as never) + connectPanePty(...mockPanePtyArgs(createPane(2), createManager(2), deps)) await flushAsyncTicks() expect(transport.connect).toHaveBeenCalledTimes(1) @@ -479,7 +480,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { transportFactoryQueue.push(transport) const deps = seedUnverifiableRestoredPane() - connectPanePty(createPane(2) as never, createManager(2) as never, deps as never) + connectPanePty(...mockPanePtyArgs(createPane(2), createManager(2), deps)) await flushAsyncTicks() expect(transport.connect).toHaveBeenCalledTimes(1) @@ -506,7 +507,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { const cleared = vi.fn() deps.onPtyErrorClearedRef = { current: cleared } - connectPanePty(createPane(2) as never, createManager(2) as never, deps as never) + connectPanePty(...mockPanePtyArgs(createPane(2), createManager(2), deps)) await flushAsyncTicks() expect(transport.connect).toHaveBeenCalledTimes(1) @@ -557,14 +558,7 @@ export function registerPtyConnectionSessionLivenessTests(): void { paneTransportsRef: { current: new Map([[1, createMockTransport('pty-pane-1')]]) } }) const pane = createPane(2) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The real PanePtyBinding exposes noteVisibilityResume; this fixture narrows it to that method. - const binding = connectPanePty( - pane as never, - manager as never, - deps as never - ) as unknown as { - noteVisibilityResume: () => void - } + const binding = connectPanePty(...mockPanePtyArgs(pane, manager, deps)) return { binding, transport, diff --git a/src/renderer/src/components/terminal-pane/pty-connection-spawn-deferred-sleep.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-spawn-deferred-sleep.test.ts index 19caaa24f28..792ea9b7df1 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-spawn-deferred-sleep.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-spawn-deferred-sleep.test.ts @@ -16,6 +16,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' const { resetAndRefreshAllTerminalWebglAtlases, @@ -247,8 +248,7 @@ describe('connectPanePty: a restore the host kept asleep', () => { } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared pane fixtures stand in for the xterm-backed pane, manager and deps connectPanePty reads, as in the sibling connectPanePty specs. - const binding = connectPanePty(pane as never, createManager(1) as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(20) if (ORCA_LAYOUT_OWNER_IS_MAIN) { await expectRestoreAskedMainNotDeferred(transport, makePaneKey('tab-1', pane.leafId)) @@ -296,8 +296,7 @@ describe('connectPanePty: a restore the host kept asleep', () => { } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared pane fixtures stand in for the xterm-backed pane, manager and deps connectPanePty reads, as in the sibling connectPanePty specs. - const binding = connectPanePty(pane as never, createManager(1) as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(20) if (ORCA_LAYOUT_OWNER_IS_MAIN) { await expectRestoreAskedMainNotDeferred(transport, makePaneKey('tab-1', pane.leafId)) @@ -337,8 +336,7 @@ describe('connectPanePty: a restore the host kept asleep', () => { } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared pane fixtures stand in for the xterm-backed pane, manager and deps connectPanePty reads, as in the sibling connectPanePty specs. - const binding = connectPanePty(pane as never, createManager(1) as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(20) if (ORCA_LAYOUT_OWNER_IS_MAIN) { await expectRestoreAskedMainNotDeferred(transport, makePaneKey('tab-1', pane.leafId)) @@ -367,8 +365,7 @@ describe('connectPanePty: a restore the host kept asleep', () => { const pane = createPane(1) const paneKey = makePaneKey('tab-1', pane.leafId) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared pane fixtures stand in for the xterm-backed pane, manager and deps connectPanePty reads, as in the sibling connectPanePty specs. - const binding = connectPanePty(pane as never, createManager(1) as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(20) if (ORCA_LAYOUT_OWNER_IS_MAIN) { await expectRestoreAskedMainNotDeferred(transport, makePaneKey('tab-1', pane.leafId)) @@ -398,8 +395,7 @@ describe('connectPanePty: a restore the host kept asleep', () => { } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared pane fixtures stand in for the xterm-backed pane, manager and deps connectPanePty reads, as in the sibling connectPanePty specs. - const binding = connectPanePty(pane as never, createManager(1) as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(20) if (ORCA_LAYOUT_OWNER_IS_MAIN) { await expectRestoreAskedMainNotDeferred(transport, makePaneKey('tab-1', pane.leafId)) @@ -453,8 +449,7 @@ describe('connectPanePty: a restore the host kept asleep', () => { } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared pane fixtures stand in for the xterm-backed pane, manager and deps connectPanePty reads, as in the sibling connectPanePty specs. - const binding = connectPanePty(pane as never, createManager(1) as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(20) if (ORCA_LAYOUT_OWNER_IS_MAIN) { await expectRestoreAskedMainNotDeferred(transport, makePaneKey('tab-1', pane.leafId)) @@ -495,8 +490,7 @@ describe('connectPanePty: a restore the host kept asleep', () => { } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared pane fixtures stand in for the xterm-backed pane, manager and deps connectPanePty reads, as in the sibling connectPanePty specs. - const binding = connectPanePty(pane as never, createManager(1) as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(20) if (ORCA_LAYOUT_OWNER_IS_MAIN) { await expectRestoreAskedMainNotDeferred(transport, makePaneKey('tab-1', pane.leafId)) @@ -530,8 +524,7 @@ describe('connectPanePty: a restore the host kept asleep', () => { } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared pane fixtures stand in for the xterm-backed pane, manager and deps connectPanePty reads, as in the sibling connectPanePty specs. - const binding = connectPanePty(pane as never, createManager(1) as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(20) if (ORCA_LAYOUT_OWNER_IS_MAIN) { await expectRestoreAskedMainNotDeferred(transport, makePaneKey('tab-1', pane.leafId)) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts index 40605cda45d..cf11e6f85d5 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts @@ -14,6 +14,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' const { resetAndRefreshAllTerminalWebglAtlases, @@ -146,9 +147,11 @@ describe('fresh spawn leaves a local pane unbound', () => { transportFactoryQueue.push(transport) connectPanePty( - createPane(1) as never, - createManager(1) as never, - createDeps({ tabId: 'tab-unbound-spawn' }) as never + ...mockPanePtyArgs( + createPane(1), + createManager(1), + createDeps({ tabId: 'tab-unbound-spawn' }) + ) ) await flushAsyncTicks(40) @@ -170,8 +173,7 @@ describe('fresh spawn leaves a local pane unbound', () => { transportFactoryQueue.push(transport) const deps = createDeps({ tabId: 'tab-transport-retry' }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocks cover every pane, manager, and deps member the connect path reads. - connectPanePty(createPane(1) as never, createManager(1) as never, deps as never) + connectPanePty(...mockPanePtyArgs(createPane(1), createManager(1), deps)) await flushAsyncTicks(40) expect(transport.connect).toHaveBeenCalled() @@ -187,9 +189,7 @@ describe('fresh spawn leaves a local pane unbound', () => { transportFactoryQueue.push(transport) connectPanePty( - createPane(1) as never, - createManager(1) as never, - createDeps({ tabId: 'tab-bound-spawn' }) as never + ...mockPanePtyArgs(createPane(1), createManager(1), createDeps({ tabId: 'tab-bound-spawn' })) ) await flushAsyncTicks(40) @@ -211,9 +211,7 @@ describe('fresh spawn leaves a local pane unbound', () => { transportFactoryQueue.push(transport) connectPanePty( - createPane(1) as never, - createManager(1) as never, - createDeps({ tabId: 'tab-bound-spawn' }) as never + ...mockPanePtyArgs(createPane(1), createManager(1), createDeps({ tabId: 'tab-bound-spawn' })) ) await flushAsyncTicks(40) @@ -232,9 +230,11 @@ describe('fresh spawn leaves a local pane unbound', () => { transportFactoryQueue.push(transport) connectPanePty( - createPane(1) as never, - createManager(1) as never, - createDeps({ tabId: 'tab-unbound-spawn' }) as never + ...mockPanePtyArgs( + createPane(1), + createManager(1), + createDeps({ tabId: 'tab-unbound-spawn' }) + ) ) await flushAsyncTicks(40) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-split-spawn.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-split-spawn.spec.ts index 8ee723cab67..c9c1182c82b 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-split-spawn.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-split-spawn.spec.ts @@ -21,6 +21,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionSplitSpawnTests(): void { const { notifyCodexPaneBoundForStaleSweep } = ptyConnectionTestMocks @@ -66,11 +67,7 @@ export function registerPtyConnectionSplitSpawnTests(): void { transportFactoryQueue.push(transport) const pane = createPane(1) - const binding = connectPanePty( - pane as never, - createManager(1) as never, - createDeps() as never - ) + const binding = connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) const onPtySpawn = createdTransportOptions[0]?.onPtySpawn as | ((ptyId: string) => void) | undefined @@ -114,7 +111,7 @@ export function registerPtyConnectionSplitSpawnTests(): void { }) setFitOverride('pty-fit', 'mobile-fit', 49, 20) - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) const onPtySpawn = createdTransportOptions[0]?.onPtySpawn as | ((ptyId: string) => void) | undefined @@ -157,7 +154,7 @@ export function registerPtyConnectionSplitSpawnTests(): void { pane.terminal.cols = 80 pane.terminal.rows = 24 - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) runNextFrame() await flushAsyncTicks() @@ -237,8 +234,7 @@ export function registerPtyConnectionSplitSpawnTests(): void { mockStoreState.repos[0].executionHostId = 'runtime:env-1' mockStoreState.worktreesByRepo.repo1[0].runtimeOwnerEnvironmentId = 'env-1' } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the pane, manager and deps doubles implement every member connectPanePty reads on this startup path. - const args = [pane, createManager(1), deps] as unknown as Parameters + const args = mockPanePtyArgs(pane, createManager(1), deps) if (scenario === 'metrics') { applyOrDeferPaneMetricOptions(args[0], { fontSize: 24 }, false) } @@ -352,8 +348,7 @@ export function registerPtyConnectionSplitSpawnTests(): void { const primaryCwd = createDeferred() const sharedTransportsRef = { current: new Map() } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: pane, manager and dependency doubles implement every member reached by fresh split connection. - const mainArgs = [ + const mainArgs = mockPanePtyArgs( mainPane, manager, createDeps({ @@ -363,10 +358,9 @@ export function registerPtyConnectionSplitSpawnTests(): void { paneTransportsRef: sharedTransportsRef, updateTabPtyId: updateStoreTabPtyId }) - ] as unknown as Parameters + ) connectPanePty(...mainArgs) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the sibling uses the same checked fixture shape with its own command and transport. - const issueArgs = [ + const issueArgs = mockPanePtyArgs( issuePane, manager, createDeps({ @@ -375,7 +369,7 @@ export function registerPtyConnectionSplitSpawnTests(): void { paneTransportsRef: sharedTransportsRef, updateTabPtyId: updateStoreTabPtyId }) - ] as unknown as Parameters + ) connectPanePty(...issueArgs) await flushAsyncTicks(20) @@ -417,22 +411,26 @@ export function registerPtyConnectionSplitSpawnTests(): void { const sharedTransportsRef = { current: new Map() } connectPanePty( - createPane(1) as never, - createManager(2) as never, - createDeps({ paneTransportsRef: sharedTransportsRef }) as never + ...mockPanePtyArgs( + createPane(1), + createManager(2), + createDeps({ paneTransportsRef: sharedTransportsRef }) + ) ) connectPanePty( - createPane(2) as never, - createManager(2) as never, - createDeps({ - startup: { command: 'bash setup-runner.sh' }, - paneTransportsRef: sharedTransportsRef - }) as never + ...mockPanePtyArgs( + createPane(2), + createManager(2), + createDeps({ + startup: { command: 'bash setup-runner.sh' }, + paneTransportsRef: sharedTransportsRef + }) + ) ) const remountDeps = createDeps() const remountPane = createPane(1) - connectPanePty(remountPane as never, createManager(2) as never, remountDeps as never) + connectPanePty(...mockPanePtyArgs(remountPane, createManager(2), remountDeps)) setupSpawn.resolve('pty-setup') mainSpawn.resolve('pty-main') @@ -467,7 +465,7 @@ export function registerPtyConnectionSplitSpawnTests(): void { const pane = createPane(1) const deps = createDeps() - connectPanePty(pane as never, createManager(1) as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks() expect(pane.container.dataset.ptyId).toBe('pty-daemon-reattach') @@ -509,7 +507,7 @@ export function registerPtyConnectionSplitSpawnTests(): void { startup: { command: 'codex resume provider-session' } }) - connectPanePty(pane as never, createManager(1) as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(4) if (!onDataHandler || parseCallbacks.length === 0) { throw new Error('expected replay and terminal input handlers') @@ -554,7 +552,7 @@ export function registerPtyConnectionSplitSpawnTests(): void { const deps = createDeps({ replayingPanesRef }) const { setFitOverride } = await import('@/lib/pane-manager/mobile-fit-overrides') - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) expect(onDataHandler).toBeDefined() if (!onDataHandler) { @@ -597,7 +595,7 @@ export function registerPtyConnectionSplitSpawnTests(): void { return { dispose: vi.fn() } }) const { setFitOverride } = await import('@/lib/pane-manager/mobile-fit-overrides') - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) const send = registered.onData if (!send) { throw new Error('expected onData handler to be registered') diff --git a/src/renderer/src/components/terminal-pane/pty-connection-startup-draft-delivery.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-startup-draft-delivery.spec.ts index 74b265e89c7..71500cf45ba 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-startup-draft-delivery.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-startup-draft-delivery.spec.ts @@ -19,6 +19,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionStartupDraftDeliveryTests(): void { let mockStoreState: StoreState @@ -89,8 +90,7 @@ export function registerPtyConnectionStartupDraftDeliveryTests(): void { }) vi.mocked(window.api.pty.getForegroundProcess).mockResolvedValue('codex') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the connection fields exercised here without a real terminal or DOM. - const binding = connectPanePty(pane as never, manager as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await vi.advanceTimersByTimeAsync(VISIBLE_PTY_SETTLE_MS) await flushAsyncTicks() expect(capturedDataCallback.current).not.toBeNull() @@ -181,7 +181,7 @@ export function registerPtyConnectionStartupDraftDeliveryTests(): void { } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) const sidecarClaimed = claimStartupDelivery({ worktreeId: 'wt-1', @@ -226,17 +226,19 @@ export function registerPtyConnectionStartupDraftDeliveryTests(): void { } const binding = connectPanePty( - createPane(1) as never, - createManager(1) as never, - createDeps({ - startup: { - command: 'codex', - launchAgent: 'codex', - launchConfig: { agentArgs: '', agentEnv: {} }, - launchToken: 'launch-token-1', - draftPrompt: 'https://github.com/stablyai/orca/issues/42' - } - }) as never + ...mockPanePtyArgs( + createPane(1), + createManager(1), + createDeps({ + startup: { + command: 'codex', + launchAgent: 'codex', + launchConfig: { agentArgs: '', agentEnv: {} }, + launchToken: 'launch-token-1', + draftPrompt: 'https://github.com/stablyai/orca/issues/42' + } + }) + ) ) binding.dispose() diff --git a/src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.spec.ts index 8a68c1d2187..7a52c65bf72 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.spec.ts @@ -23,6 +23,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionTaskCompleteDispatchTests(): void { let mockStoreState: StoreState @@ -85,7 +86,7 @@ export function registerPtyConnectionTaskCompleteDispatchTests(): void { const manager = createManager(1) const deps = createDeps() - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) const titleHandler = createdTransportOptions[0]?.onTitleChange as | ((title: string, rawTitle: string) => void) @@ -125,7 +126,7 @@ export function registerPtyConnectionTaskCompleteDispatchTests(): void { const manager = createManager(1) const deps = createDeps() - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) const titleHandler = createdTransportOptions[0]?.onTitleChange as | ((title: string, rawTitle: string) => void) @@ -158,7 +159,7 @@ export function registerPtyConnectionTaskCompleteDispatchTests(): void { const manager = createManager(1) const deps = createDeps() - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) const statusHandler = createdTransportOptions[0]?.onAgentStatus as | ((payload: { @@ -217,7 +218,7 @@ export function registerPtyConnectionTaskCompleteDispatchTests(): void { } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) const titleHandler = createdTransportOptions[0]?.onTitleChange as | ((title: string, rawTitle: string) => void) @@ -247,7 +248,7 @@ export function registerPtyConnectionTaskCompleteDispatchTests(): void { manager.getActivePane.mockReturnValue({ id: 1 }) const deps = createDeps() - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) const titleHandler = createdTransportOptions[0]?.onTitleChange as | ((title: string, rawTitle: string) => void) @@ -325,7 +326,7 @@ export function registerPtyConnectionTaskCompleteDispatchTests(): void { manager.getActivePane.mockReturnValue({ id: 1 }) const deps = createDeps() - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) const titleHandler = createdTransportOptions[0]?.onTitleChange as | ((title: string, rawTitle: string) => void) @@ -353,7 +354,7 @@ export function registerPtyConnectionTaskCompleteDispatchTests(): void { const manager = createManager(1) const deps = createDeps() - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) expect(createdTransportOptions[0]?.onAgentStatus).toBeUndefined() }) @@ -376,7 +377,7 @@ export function registerPtyConnectionTaskCompleteDispatchTests(): void { isVisibleRef: { current: false } }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks(6) vi.useFakeTimers() @@ -410,7 +411,7 @@ export function registerPtyConnectionTaskCompleteDispatchTests(): void { const manager = createManager(1) const deps = createDeps({ dispatchNotification }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) const idleHandler = createdTransportOptions[0]?.onAgentBecameIdle as | ((title: string) => void) @@ -464,7 +465,7 @@ export function registerPtyConnectionTaskCompleteDispatchTests(): void { const manager = createManager(1) const deps = createDeps({ dispatchNotification }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) const idleHandler = createdTransportOptions[0]?.onAgentBecameIdle as | ((title: string) => void) @@ -535,7 +536,7 @@ export function registerPtyConnectionTaskCompleteDispatchTests(): void { const manager = createManager(1) const deps = createDeps({ dispatchNotification }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) const idleHandler = createdTransportOptions[0]?.onAgentBecameIdle as | ((title: string) => void) @@ -587,7 +588,7 @@ export function registerPtyConnectionTaskCompleteDispatchTests(): void { const manager = createManager(1) const deps = createDeps({ dispatchNotification }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) const bellHandler = createdTransportOptions[0]?.onBell as (() => void) | undefined if (!bellHandler) { @@ -623,7 +624,7 @@ export function registerPtyConnectionTaskCompleteDispatchTests(): void { const manager = createManager(1) const deps = createDeps() - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Captured transport options expose onAgentExited as an optional zero-argument callback. const agentExitedHandler = createdTransportOptions[0]?.onAgentExited as diff --git a/src/renderer/src/components/terminal-pane/pty-connection-test-connect-mock-pane.ts b/src/renderer/src/components/terminal-pane/pty-connection-test-connect-mock-pane.ts index 89d7a8c008e..dc64408bcf5 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-test-connect-mock-pane.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-test-connect-mock-pane.ts @@ -4,6 +4,22 @@ import type { PanePtyBinding } from './pty-connection/pane-pty-binding' import type { PaneConnectionDeps } from './pty-connection-test-deps' import type { MockPane, MockPaneManager } from './pty-connection-test-pane-fixtures' +/** The mock pane, manager and deps as the argument tuple connectPanePty declares. */ +export function mockPanePtyArgs( + pane: MockPane, + manager: MockPaneManager, + deps: PaneConnectionDeps +): [ManagedPane, PaneManager, PtyConnectionDeps] { + return [ + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures stub every pane member the connection path reads. + pane as unknown as ManagedPane, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock manager stubs every manager member the connection path calls. + manager as unknown as PaneManager, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: buildPaneConnectionDeps mirrors PtyConnectionDeps with mock refs and callbacks. + deps as unknown as PtyConnectionDeps + ] +} + /** Connects a mock pane through the real connectPanePty. Imported lazily so a * test's vi.resetModules() hands back the freshly mocked module graph. */ export async function connectMockPanePty( @@ -12,11 +28,5 @@ export async function connectMockPanePty( deps: PaneConnectionDeps ): Promise { const { connectPanePty } = await import('./pty-connection') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures stub every pane member the connection path reads. - const managedPane = pane as unknown as ManagedPane - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mock manager stubs every manager member the connection path calls. - const paneManager = manager as unknown as PaneManager - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: buildPaneConnectionDeps mirrors PtyConnectionDeps with mock refs and callbacks. - const connectionDeps = deps as unknown as PtyConnectionDeps - return connectPanePty(managedPane, paneManager, connectionDeps) + return connectPanePty(...mockPanePtyArgs(pane, manager, deps)) } diff --git a/src/renderer/src/components/terminal-pane/pty-connection-unserved-ssh-pane.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-unserved-ssh-pane.test.ts index bf5b45777d6..32126e8b96d 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-unserved-ssh-pane.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-unserved-ssh-pane.test.ts @@ -9,6 +9,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' const { resetAndRefreshAllTerminalWebglAtlases, @@ -160,8 +161,7 @@ describe('a pane on an unconverted SSH row', () => { await installTerminalTestGlobals() const { connectPanePty } = await import('./pty-connection') connectPane = (paneId, deps) => - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the connection fields exercised here without a real terminal or DOM. - connectPanePty(createPane(paneId) as never, createManager(paneId) as never, deps as never) + connectPanePty(...mockPanePtyArgs(createPane(paneId), createManager(paneId), deps)) }) afterEach(async () => { @@ -244,8 +244,7 @@ describe('a pane on an unconverted SSH row', () => { it('leaves recovery to the overlay when typed input cannot be delivered', async () => { const pane = createPane(1) const { connectPanePty } = await import('./pty-connection') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the connection fields exercised here without a real terminal or DOM. - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + connectPanePty(...mockPanePtyArgs(pane, createManager(1), createDeps())) await flushAsyncTicks(10) vi.mocked(pane.terminal.onData).mock.calls[0]?.[0]('ls\r') diff --git a/src/renderer/src/components/terminal-pane/pty-connection-visibility-resume-size.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-visibility-resume-size.spec.ts index 6cb66ef9988..1ed5cb040e2 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-visibility-resume-size.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-visibility-resume-size.spec.ts @@ -16,6 +16,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' export function registerPtyConnectionVisibilityResumeSizeTests(): void { let mockStoreState: StoreState @@ -69,14 +70,7 @@ export function registerPtyConnectionVisibilityResumeSizeTests(): void { ...depsOverrides }) const pane = createPane(2) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The real PanePtyBinding exposes noteVisibilityResume; this fixture narrows it to that method. - const binding = connectPanePty( - pane as never, - manager as never, - deps as never - ) as unknown as { - noteVisibilityResume: () => void - } + const binding = connectPanePty(...mockPanePtyArgs(pane, manager, deps)) return { binding, transport, deps, pane } } @@ -185,7 +179,7 @@ export function registerPtyConnectionVisibilityResumeSizeTests(): void { pane.terminal.cols = 82 pane.terminal.rows = 40 - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() transport.resize.mockClear() vi.mocked(window.api.pty.getSize).mockClear() @@ -230,7 +224,7 @@ export function registerPtyConnectionVisibilityResumeSizeTests(): void { } as never vi.mocked(window.api.pty.getSize).mockResolvedValue({ cols: 62, rows: 63 }) - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() proposedGrid = { cols: 65, rows: 63 } vi.mocked(pane.fitAddon.fit).mockClear() @@ -277,7 +271,7 @@ export function registerPtyConnectionVisibilityResumeSizeTests(): void { } as never try { - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() proposedGrid = { cols: 65, rows: 63 } setDriverForPty('pty-pane-2', { kind: 'mobile', clientId: 'phone-1' }) @@ -315,7 +309,7 @@ export function registerPtyConnectionVisibilityResumeSizeTests(): void { proposeDimensions: vi.fn(() => ({ cols: 101, rows: 33 })) } as never - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() setFitOverride('pty-pane-2', 'mobile-fit', 40, 30) transport.resize.mockClear() @@ -362,7 +356,7 @@ export function registerPtyConnectionVisibilityResumeSizeTests(): void { proposeDimensions: vi.fn(() => proposedGrid) } as never - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() observer.trigger() await flushAsyncTicks() @@ -413,7 +407,7 @@ export function registerPtyConnectionVisibilityResumeSizeTests(): void { ...pane.fitAddon, proposeDimensions: vi.fn(() => proposedGrid) } as never - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() observer.trigger() await flushAsyncTicks() @@ -459,7 +453,7 @@ export function registerPtyConnectionVisibilityResumeSizeTests(): void { proposeDimensions: vi.fn(() => ({ cols: 130, rows: 50 })) } as never - connectPanePty(pane as never, manager as never, deps as never) + connectPanePty(...mockPanePtyArgs(pane, manager, deps)) await flushAsyncTicks() setDriverForPty('pty-pane-2', { kind: 'mobile', clientId: 'phone-1' }) transport.resize.mockClear() @@ -559,14 +553,7 @@ export function registerPtyConnectionVisibilityResumeSizeTests(): void { paneTransportsRef: { current: new Map([[1, createMockTransport('pty-pane-1')]]) } }) const pane = createPane(2) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The real PanePtyBinding exposes noteVisibilityResume; this fixture narrows it to that method. - const binding = connectPanePty( - pane as never, - manager as never, - deps as never - ) as unknown as { - noteVisibilityResume: () => void - } + const binding = connectPanePty(...mockPanePtyArgs(pane, manager, deps)) transport.resize.mockClear() deps.isVisibleRef.current = visible @@ -602,7 +589,7 @@ export function registerPtyConnectionVisibilityResumeSizeTests(): void { ...pane.fitAddon, proposeDimensions: vi.fn(() => ({ cols: 132, rows: 42 })) } as never - const binding = connectPanePty(pane as never, createManager(2) as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(pane, createManager(2), deps)) await flushAsyncTicks() transport.claimViewport.mockClear() diff --git a/src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts b/src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts index 7c5711870df..8445de57ef1 100644 --- a/src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts @@ -8,11 +8,10 @@ import { createPane, captureCallbackTerminalWrites, createManager, - type MockPane, - type MockPaneManager, type MockTransport } from './pty-connection-test-pane-fixtures' -import { buildPaneConnectionDeps, type PaneConnectionDeps } from './pty-connection-test-deps' +import { buildPaneConnectionDeps } from './pty-connection-test-deps' +import { connectMockPanePty } from './pty-connection-test-connect-mock-pane' import { createInitialStoreState } from './pty-connection-test-store-fixtures' import type { StoreState } from './pty-connection-test-store-state' import { @@ -142,18 +141,9 @@ function pairedParkingHostStatus(): StoreState['runtimeStatusByEnvironmentId'] { ]) } -type ConnectMockPane = ( - pane: MockPane, - manager: MockPaneManager, - deps: PaneConnectionDeps -) => { dispose: () => void } - async function revealParkedRemotePane( serializeBufferOutcome: ReturnType ): Promise<{ transport: MockTransport; writes: string[]; dispose: () => void }> { - const { connectPanePty } = await import('./pty-connection') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the shared mock pane, manager, and deps stand in for the real xterm-backed objects, exactly as every other connectPanePty suite drives them. - const connectMockPane = connectPanePty as unknown as ConnectMockPane const transport = createMockTransport() // The remote transport's park-reveal result: a bare reattach with no relay tail. transport.connect.mockImplementation(async () => { @@ -180,7 +170,7 @@ async function revealParkedRemotePane( const pane = createPane(1) const { writes } = captureCallbackTerminalWrites(pane) // Why the active pane: an inactive split defers its restore to the frame scheduler. - const binding = connectMockPane( + const binding = await connectMockPanePty( pane, createManager(1, 1), createDeps({ diff --git a/src/renderer/src/components/terminal-pane/remote-reconnect-unimaged-pane-redraw.test.ts b/src/renderer/src/components/terminal-pane/remote-reconnect-unimaged-pane-redraw.test.ts index d6c5b512c97..ea714a0f28d 100644 --- a/src/renderer/src/components/terminal-pane/remote-reconnect-unimaged-pane-redraw.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-reconnect-unimaged-pane-redraw.test.ts @@ -15,6 +15,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' // #26887: after an SSH reconnect the subscribe replays no screen image, so the pane asks the host // for one (onStreamRecovered). When the host cannot give one, an idle TUI that only writes diffs @@ -102,8 +103,7 @@ async function connectVisibleRemotePane( const deps = buildPaneConnectionDeps(() => mockStoreState, { isVisibleRef: { current: opts.visible ?? true } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocks stub only the members connectPanePty reaches; a missing one fails the test. - const disposable = connectPanePty(pane as never, createManager(1) as never, deps as never) + const disposable = connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(6) expect(onStreamRecovered).not.toBeNull() transport.resize.mockClear() diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-test-harness.ts b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-test-harness.ts index 5d1aa44854e..6ee270b1376 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-test-harness.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-test-harness.ts @@ -1,6 +1,3 @@ -/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 18 remote-runtime PTY transport specs, not shipped code, and it - falls outside the *.test / *.spec / tests glob set. refreshWebRuntimeSessionTabsSnapshot is imported directly by several - renderer runtime modules, so an injected seam would have to be threaded through all of them. */ import { vi } from 'vitest' import type { Mock } from 'vitest' import { diff --git a/src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-host-chain.test.ts b/src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-host-chain.test.ts index b887243f0aa..c312a946843 100644 --- a/src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-host-chain.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-host-chain.test.ts @@ -23,6 +23,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' // Why this suite: remote-snapshot-alt-screen-replay.test.ts hand-writes the replay meta, and // once fed a shape the host never sends. Here every input is produced by the real code: @@ -375,8 +376,7 @@ describe('host-published recovery snapshot onto a live alt screen', () => { events.push({ cols, rows }) }) const deps = buildPaneConnectionDeps(() => mockStoreState) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. - const binding = connectPanePty(pane as never, createManager(1) as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(8) pane.terminal.cols = COLS pane.terminal.rows = ROWS diff --git a/src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-replay.test.ts b/src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-replay.test.ts index 44e108e4a78..4b04144c17c 100644 --- a/src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-replay.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-replay.test.ts @@ -18,6 +18,7 @@ import { installTerminalTestGlobals, restoreTerminalTestGlobals } from './pty-connection-test-environment' +import { mockPanePtyArgs } from './pty-connection-test-connect-mock-pane' const { scheduleRuntimeGraphSync, shouldSeedCacheTimerOnInitialTitle, toastInfo } = vi.hoisted( () => ({ @@ -174,8 +175,7 @@ describe('remote snapshot replay onto a live alt screen', () => { events.push({ cols, rows }) }) const deps = buildPaneConnectionDeps(() => mockStoreState) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. - const binding = connectPanePty(pane as never, createManager(1) as never, deps as never) + const binding = connectPanePty(...mockPanePtyArgs(pane, createManager(1), deps)) await flushAsyncTicks(8) pane.terminal.cols = COLS pane.terminal.rows = ROWS diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-layout-snapshot-merge.ts b/src/renderer/src/components/terminal-pane/terminal-pane-layout-snapshot-merge.ts new file mode 100644 index 00000000000..7ed2ce0c414 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/terminal-pane-layout-snapshot-merge.ts @@ -0,0 +1,47 @@ +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' +import { mergeCapturedLeafState } from './merge-captured-leaf-state' + +/** Carries the saved scrollback of `leafIds` into a freshly serialized layout. */ +export function retainLayoutScrollback( + layout: TerminalLayoutSnapshot, + saved: TerminalLayoutSnapshot | undefined, + leafIds: ReadonlySet +): void { + for (const key of ['buffersByLeafId', 'scrollbackRefsByLeafId'] as const) { + const merged = mergeCapturedLeafState({ + prior: saved?.[key], + fresh: {}, + currentLeafIds: leafIds + }) + if (Object.keys(merged).length > 0) { + layout[key] = merged + } + } +} + +/** + * Saved titles of still-mounted leaves, overlaid by live pane titles. A live title un-removes its + * leaf, so `removedTitleLeafIds` is updated in place. + */ +export function mergeLayoutTitlesByLeafId( + panes: readonly { id: number; leafId: string }[], + savedTitlesByLeafId: Readonly> | undefined, + paneTitles: Readonly>, + removedTitleLeafIds: Set +): Record { + const titlesByLeafId: Record = {} + for (const pane of panes) { + const savedTitle = savedTitlesByLeafId?.[pane.leafId] + if (savedTitle && !removedTitleLeafIds.has(pane.leafId)) { + titlesByLeafId[pane.leafId] = savedTitle + } + } + for (const pane of panes) { + const title = paneTitles[pane.id] + if (title) { + titlesByLeafId[pane.leafId] = title + removedTitleLeafIds.delete(pane.leafId) + } + } + return titlesByLeafId +} diff --git a/src/renderer/src/components/terminal-pane/use-layout-owner-pane-tree.ts b/src/renderer/src/components/terminal-pane/use-layout-owner-pane-tree.ts index 8b563db341b..b547f362129 100644 --- a/src/renderer/src/components/terminal-pane/use-layout-owner-pane-tree.ts +++ b/src/renderer/src/components/terminal-pane/use-layout-owner-pane-tree.ts @@ -77,6 +77,6 @@ export function useLayoutOwnerPaneTree(controller: TerminalPaneCloseController): if (changed) { persistLayoutSnapshot() } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Refs and stable callbacks. - }, [restoredLayout, redraws, zoomed, tabId, worktreeId]) + // oxlint-disable-next-line react-hooks/exhaustive-deps -- closePaneLocally/persistLayoutSnapshot change with tab callbacks; re-reconciling on that would replay layout writes. + }, [restoredLayout, redraws, zoomed, tabId, worktreeId, managerRef, expandedPaneIdRef]) } diff --git a/src/renderer/src/components/terminal-pane/use-terminal-container-fit-sync.ts b/src/renderer/src/components/terminal-pane/use-terminal-container-fit-sync.ts index 0703202768b..5d1fb56e66a 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-container-fit-sync.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-container-fit-sync.ts @@ -74,6 +74,5 @@ export function useTerminalContainerFitSync({ clearTimeout(timerId) } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isVisible]) + }, [isVisible, containerRef, managerRef]) } diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts index 6aad9ba0595..2b4ac00cfa2 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts @@ -72,8 +72,7 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController const getNativeChatLeafIds = useCallback((): string[] => { const mountedLeafIds = managerRef.current?.getPanes().map((pane) => pane.leafId) ?? [] return [...new Set([...expectedLayoutLeafIds, ...mountedLeafIds])] - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [expectedLayoutLeafIds]) + }, [expectedLayoutLeafIds, managerRef]) const getTabWideAgentHintLeafId = useCallback((): string | null => { if (tabWideAgentHintLeafId !== undefined) { return tabWideAgentHintLeafId @@ -94,8 +93,7 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController return } setTabWideAgentHintLeafId(leafIds.length === 1 ? leafIds[0] : null) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [getNativeChatLeafIds, paneCount, tabWideAgentHintLeafId]) + }, [getNativeChatLeafIds, paneCount, tabWideAgentHintLeafId, setTabWideAgentHintLeafId]) const resolveTitleAgentForLeaf = useCallback( (leafId: string | null) => { const hasSingleKnownLeaf = @@ -108,13 +106,13 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController terminalTitle: hasSingleKnownLeaf ? terminalTab?.title : null }) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. [ getNativeChatLeafIds, getTabWideAgentHintLeafId, runtimePaneTitlesByPaneId, terminalTab?.title, - unifiedTabLabel + unifiedTabLabel, + managerRef ] ) const isChatEligibleForLeaf = useCallback( @@ -172,8 +170,16 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController setTabViewMode(unifiedTabId, 'terminal') } }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [chatLeafId, isChatViewMode, setTabLayout, setTabViewMode, tabId, unifiedTabId, worktreeId] + [ + chatLeafId, + isChatViewMode, + setTabLayout, + setTabViewMode, + tabId, + unifiedTabId, + worktreeId, + setChatLeafId + ] ) const handleConfirmedAgentExit = useCallback( (leafId: string): void => { @@ -193,13 +199,11 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController }) ) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [applyNativeChatLeafRoute, chatLeafId, isChatEligibleForLeaf, isChatViewMode] + [applyNativeChatLeafRoute, chatLeafId, isChatEligibleForLeaf, isChatViewMode, managerRef] ) useEffect(() => { onAgentExitedRef.current = handleConfirmedAgentExit - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [handleConfirmedAgentExit]) + }, [handleConfirmedAgentExit, onAgentExitedRef]) const canToggleChatForLeaf = useCallback( (leafId: string | null): boolean => { // Scope the "always allow toggling back" rule to the leaf showing chat; must not make an unsupported sibling look eligible. @@ -231,8 +235,7 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController return } toggleNativeChatForLeaf(activeLeafId) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- managerRef is a stable ref container. - }, [toggleNativeChatForLeaf]) + }, [toggleNativeChatForLeaf, managerRef]) const switchNativeChatToTerminal = useCallback(() => { if (chatLeafId && unifiedTabId) { setChatLeafId(null) @@ -245,8 +248,7 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController } const pane = managerRef.current?.getPanes().find((candidate) => candidate.leafId === chatLeafId) return pane?.serializeAddon.serialize({ scrollback: 0 }) ?? null - // oxlint-disable-next-line react-hooks/exhaustive-deps -- managerRef is a stable ref container. - }, [chatLeafId]) + }, [chatLeafId, managerRef]) return { setTabPaneExpanded, diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts index 95ec53b9abc..83b4778a014 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts @@ -93,14 +93,16 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr } manager.closePane(paneId) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. [ clearSessionRestoredBannerForPane, onCloseTab, syncPanePtyLayoutBinding, syncPanePtyLayoutBindingForLeaf, tabId, - worktreeId + worktreeId, + managerRef, + paneTransportsRef, + setTerminalErrorsByPaneId ] ) const executeClosePane = useCallback( @@ -127,13 +129,11 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr } closePaneLocally(paneId) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [closePaneLocally, onCloseTab, tabId, worktreeId] + [closePaneLocally, onCloseTab, tabId, worktreeId, managerRef] ) const getCloseDialogCopyKind = useCallback( (paneId: number) => resolveLeafCloseCopyKind(tabId, managerRef.current?.getLeafId(paneId)), - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [tabId] + [tabId, managerRef] ) const handleRequestClosePane = useCallback( (paneId: number) => { @@ -212,8 +212,7 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr if (pane) { handleRequestClosePane(pane.id) } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [handleRequestClosePane]) + }, [handleRequestClosePane, managerRef]) useEffect( () => onActiveTerminalPaneCloseRequest(tabId, closeActivePane), [closeActivePane, tabId] @@ -236,14 +235,12 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr } confirmedClose() }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [executeClosePane, pendingCloseConfirmation, updateSettings] + [pendingCloseConfirmation, updateSettings, setPendingCloseConfirmation] ) const handleCancelClose = useCallback(() => { confirmedCloseRef.current = null setPendingCloseConfirmation(null) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, []) + }, [setPendingCloseConfirmation]) const resolveExternalPaneDropTarget = useCallback( ({ sourcePaneId, @@ -265,8 +262,7 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr worktreeId }) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [worktreeId] + [worktreeId, managerRef] ) const handleExternalPaneDrop = useCallback( (sourcePaneId: number, target: PaneExternalDropTarget): boolean => { @@ -291,8 +287,7 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr }).catch((error) => console.warn('[terminal-pane-detach] move failed', error)) return true }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [persistLayoutSnapshot, tabId, worktreeId] + [persistLayoutSnapshot, tabId, worktreeId, managerRef, paneCwdRef, paneTransportsRef] ) return { diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-context-actions.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-context-actions.ts index b70e0f0ec75..64c7a1fd4ae 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-context-actions.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-context-actions.ts @@ -65,14 +65,21 @@ export function useTerminalPaneContextActions(controller: TerminalPaneCloseContr closeRenameSession() setRenamingPaneId(null) changePaneTitle(renamingPaneId, trimmed) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [closeRenameSession, renamingPaneId, renameValue, removePaneTitle, changePaneTitle]) + }, [ + closeRenameSession, + renamingPaneId, + renameValue, + removePaneTitle, + changePaneTitle, + paneTitlesRef, + renameSubmittedRef, + setRenamingPaneId + ]) const handleRenameCancel = useCallback(() => { renameSubmittedRef.current = true closeRenameSession() setRenamingPaneId(null) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [closeRenameSession]) + }, [closeRenameSession, renameSubmittedRef, setRenamingPaneId]) const handleRenameBlur = useCallback(() => { if (renameSubmittedRef.current) { return @@ -100,8 +107,16 @@ export function useTerminalPaneContextActions(controller: TerminalPaneCloseContr input.select() renameBlurCommitEnabledRef.current = true }) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [handleRenameSubmit, renamingPaneId]) + }, [ + handleRenameSubmit, + renamingPaneId, + renameBlurCommitEnabledRef, + renameInputRef, + renameRefocusFrameRef, + renameSessionIdRef, + renameSubmittedRef, + renameUserRequestedBlurCommitRef + ]) const handleRemoveTitle = useCallback( (paneId: number) => removePaneTitle(paneId), [removePaneTitle] @@ -138,8 +153,16 @@ export function useTerminalPaneContextActions(controller: TerminalPaneCloseContr }) }) return () => cancelPendingRenameFrames() - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [cancelPendingRenameFrames, renamingPaneId]) + }, [ + cancelPendingRenameFrames, + renamingPaneId, + renameBlurCommitEnabledRef, + renameEnableBlurFrameRef, + renameFocusFrameRef, + renameInputRef, + renameSessionIdRef, + renameSubmittedRef + ]) const contextMenu = useTerminalPaneContextMenu({ tabId, @@ -214,8 +237,7 @@ export function useTerminalPaneContextActions(controller: TerminalPaneCloseContr return manager.getPanes().find((pane) => pane.id === paneId)?.leafId ?? null } return manager.getActivePane()?.leafId ?? null - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [contextMenu.menuPaneId]) + }, [contextMenu.menuPaneId, managerRef]) const contextMenuLeafId = getContextMenuLeafId() return { diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-controller.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-controller.ts index fa4602b6088..d785b2b1e06 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-controller.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-controller.ts @@ -19,6 +19,11 @@ import { useTerminalPaneProjection } from './use-terminal-pane-projection' import { useLayoutOwnerPaneTree } from './use-layout-owner-pane-tree' import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch' +// Switch point: the mounted tab follows main's pane tree. Chosen once per build, so hook order is fixed. +const useLayoutOwnerPaneTreeWhenMain = ORCA_LAYOUT_OWNER_IS_MAIN + ? useLayoutOwnerPaneTree + : (): void => {} + export function useTerminalPaneController(props: TerminalPaneProps) { const foundation = useTerminalPaneFoundation(props) const title = Object.assign(foundation, useTerminalPaneTitleState(foundation)) @@ -31,11 +36,7 @@ export function useTerminalPaneController(props: TerminalPaneProps) { const close = Object.assign(bindings, useTerminalPaneCloseActions(bindings)) useTerminalPaneLifecycleStage(close) const reconciliation = Object.assign(close, useTerminalPaneReconciliation(close)) - // Switch point: the mounted tab follows main's pane tree. A build constant, so the hook order is fixed. - if (ORCA_LAYOUT_OWNER_IS_MAIN) { - // oxlint-disable-next-line react-hooks/rules-of-hooks -- SAFETY: the condition is a compile-time constant. - useLayoutOwnerPaneTree(close) - } + useLayoutOwnerPaneTreeWhenMain(close) useTerminalPaneGlobalListeners(reconciliation) useTerminalPanePasteListeners(reconciliation) useTerminalPaneTitleEffects(reconciliation) diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts index 4f4057f9b31..27d52277d81 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts @@ -182,8 +182,18 @@ export function useTerminalPaneGlobalEffects({ wasVisibleRef.current = false wasWorktreeActiveRef.current = isWorktreeActive - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isActive, isChatViewMode, isWorktreeActive, rendererVisible]) + }, [ + isActive, + isChatViewMode, + isWorktreeActive, + rendererVisible, + applyPendingFollowOutputRequests, + captureViewportPositions, + isActiveRef, + isVisibleRef, + managerRef, + withSuppressedScrollTracking + ]) useEffect(() => { const ptyId = isActive && isVisible && isWorktreeActive ? activeLeafPtyId : null diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-listeners.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-listeners.ts index a46027b67b9..994e0cfbb0f 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-listeners.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-listeners.ts @@ -155,8 +155,7 @@ export function useTerminalPaneGlobalListeners(controller: TerminalPaneCloseCont cleanup() } } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [isActive, isVisible]) + }, [isActive, isVisible, managerRef, paneTransportsRef]) useEffect(() => { const container = containerRef.current @@ -245,6 +244,5 @@ export function useTerminalPaneGlobalListeners(controller: TerminalPaneCloseCont syncFocused(false) } } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, []) + }, [containerRef]) } diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-layout-bindings.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-layout-bindings.ts index ebbe137cec4..d0ef9053441 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-layout-bindings.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-layout-bindings.ts @@ -76,8 +76,7 @@ export function useTerminalPaneLayoutBindings(controller: TerminalPaneLayoutCont } setTabLayout(tabId, nextLayout) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [setTabLayout, tabId] + [setTabLayout, tabId, paneTransportsRef] ) const writePanePtyLayoutBinding = useCallback( (paneId: number, ptyId: string | null, repairActiveLeafOnClear: boolean): void => { diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-layout-persistence.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-layout-persistence.ts index ba8f193f107..1f4f20f23da 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-layout-persistence.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-layout-persistence.ts @@ -15,6 +15,10 @@ import type { TerminalPaneStartupController } from './use-terminal-pane-startup- import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch' import { sendLayoutCommandToOwner } from '@/runtime/layout-owner-command' import { notePendingPaneTitleRename } from '@/runtime/pending-pane-title-renames' +import { + mergeLayoutTitlesByLeafId, + retainLayoutScrollback +} from './terminal-pane-layout-snapshot-merge' import { retireLeavesFromTerminalLayout } from '../../../../shared/terminal-layout-leaf-retirement' export function useTerminalPaneLayoutPersistence(controller: TerminalPaneStartupController) { @@ -45,117 +49,100 @@ export function useTerminalPaneLayoutPersistence(controller: TerminalPaneStartup useLayoutEffect(() => { chatOwnerRef.current = { chatLeafId, isChatViewMode } }, [chatLeafId, isChatViewMode]) - const persistLayoutSnapshot = useCallback( - (): void => { - const manager = managerRef.current - const container = containerRef.current - if (!manager || !container) { - return - } - const activePaneId = manager.getActivePane()?.id ?? manager.getPanes()[0]?.id ?? null - const leafIdByPaneId = manager.getLeafIdMap() - let layout = serializeTerminalLayout( - container, - activePaneId, - expandedPaneIdRef.current, - leafIdByPaneId + const persistLayoutSnapshot = useCallback((): void => { + const manager = managerRef.current + const container = containerRef.current + if (!manager || !container) { + return + } + const activePaneId = manager.getActivePane()?.id ?? manager.getPanes()[0]?.id ?? null + const leafIdByPaneId = manager.getLeafIdMap() + let layout = serializeTerminalLayout( + container, + activePaneId, + expandedPaneIdRef.current, + leafIdByPaneId + ) + const existing = useAppStore.getState().terminalLayoutsByTabId[tabId] + const currentPanes = manager.getPanes() + const currentLeafIds = new Set(currentPanes.map((pane) => pane.leafId)) + // PaneManager retains this callback for its entire mount. + const { chatLeafId, isChatViewMode } = chatOwnerRef.current + if (isChatViewMode && chatLeafId && currentPanes.some((pane) => pane.leafId === chatLeafId)) { + layout.chatLeafId = chatLeafId + } + const clearedScrollbackLeafIds = clearedScrollbackLeafIdsRef.current + const scrollbackPreserveLeafIds = new Set( + [...currentLeafIds].filter((leafId) => !clearedScrollbackLeafIds.has(leafId)) + ) + retainLayoutScrollback(layout, existing, scrollbackPreserveLeafIds) + const livePtyEntries = currentPanes + .map( + (pane) => [pane.leafId, paneTransportsRef.current.get(pane.id)?.getPtyId() ?? null] as const ) - const existing = useAppStore.getState().terminalLayoutsByTabId[tabId] - const currentPanes = manager.getPanes() - const currentLeafIds = new Set(currentPanes.map((pane) => pane.leafId)) - // PaneManager retains this callback for its entire mount. - const { chatLeafId, isChatViewMode } = chatOwnerRef.current - if (isChatViewMode && chatLeafId && currentPanes.some((pane) => pane.leafId === chatLeafId)) { - layout.chatLeafId = chatLeafId - } - const clearedScrollbackLeafIds = clearedScrollbackLeafIdsRef.current - const scrollbackPreserveLeafIds = new Set( - [...currentLeafIds].filter((leafId) => !clearedScrollbackLeafIds.has(leafId)) + .filter( + (entry): entry is readonly [(typeof currentPanes)[number]['leafId'], string] => + entry[1] !== null ) - const mergedBuffers = mergeCapturedLeafState({ - prior: existing?.buffersByLeafId, - fresh: {}, - currentLeafIds: scrollbackPreserveLeafIds + const mergedPtyIds = mergeCapturedLeafState({ + prior: existing?.ptyIdsByLeafId, + fresh: Object.fromEntries(livePtyEntries), + currentLeafIds + }) + if (Object.keys(mergedPtyIds).length > 0) { + layout.ptyIdsByLeafId = mergedPtyIds + } + layout.activeLeafId = resolveTerminalLayoutActiveLeafId({ + root: layout.root, + activeLeafId: layout.activeLeafId, + ptyIdsByLeafId: mergedPtyIds + }) + const removedTitleLeafIds = removedTitleLeafIdsRef.current + const titlesByLeafId = mergeLayoutTitlesByLeafId( + currentPanes, + existing?.titlesByLeafId, + paneTitlesRef.current, + removedTitleLeafIds + ) + if (Object.keys(titlesByLeafId).length > 0) { + layout.titlesByLeafId = titlesByLeafId + } + const hostRetiredLeafIds = hostRetiredLeafIdsRef.current + if (hostRetiredLeafIds.size > 0) { + // Why: never retire the last pane here; the host drops a whole tab by itself. + layout = retireLeavesFromTerminalLayout(layout, hostRetiredLeafIds) ?? layout + } + setTabLayout(tabId, layout) + const hasRemotePane = Object.values(mergedPtyIds).some( + (ptyId) => typeof ptyId === 'string' && isRemoteRuntimePtyId(ptyId) + ) + if (hasRemotePane) { + // Why: a host keeps its titles when the field is absent, so clearing the last one sends {}. + const clearedEveryTitle = + !layout.titlesByLeafId && + [...currentLeafIds].some((leafId) => removedTitleLeafIds.has(leafId)) + remotePaneLayoutPusherRef.current?.push({ + worktreeId, + tabId, + layout: clearedEveryTitle ? { ...layout, titlesByLeafId: {} } : layout }) - if (Object.keys(mergedBuffers).length > 0) { - layout.buffersByLeafId = mergedBuffers - } - const mergedScrollbackRefs = mergeCapturedLeafState({ - prior: existing?.scrollbackRefsByLeafId, - fresh: {}, - currentLeafIds: scrollbackPreserveLeafIds - }) - if (Object.keys(mergedScrollbackRefs).length > 0) { - layout.scrollbackRefsByLeafId = mergedScrollbackRefs - } - const livePtyEntries = currentPanes - .map( - (pane) => - [pane.leafId, paneTransportsRef.current.get(pane.id)?.getPtyId() ?? null] as const - ) - .filter( - (entry): entry is readonly [(typeof currentPanes)[number]['leafId'], string] => - entry[1] !== null - ) - const mergedPtyIds = mergeCapturedLeafState({ - prior: existing?.ptyIdsByLeafId, - fresh: Object.fromEntries(livePtyEntries), - currentLeafIds - }) - if (Object.keys(mergedPtyIds).length > 0) { - layout.ptyIdsByLeafId = mergedPtyIds - } - layout.activeLeafId = resolveTerminalLayoutActiveLeafId({ - root: layout.root, - activeLeafId: layout.activeLeafId, - ptyIdsByLeafId: mergedPtyIds - }) - const titlesByLeafId: Record = {} - const removedTitleLeafIds = removedTitleLeafIdsRef.current - for (const pane of currentPanes) { - const existingTitle = existing?.titlesByLeafId?.[pane.leafId] - if (existingTitle && !removedTitleLeafIds.has(pane.leafId)) { - titlesByLeafId[pane.leafId] = existingTitle - } - } - const titles = paneTitlesRef.current - for (const pane of currentPanes) { - const title = titles[pane.id] - if (title) { - titlesByLeafId[pane.leafId] = title - removedTitleLeafIds.delete(pane.leafId) - } - } - if (Object.keys(titlesByLeafId).length > 0) { - layout.titlesByLeafId = titlesByLeafId - } - const hostRetiredLeafIds = hostRetiredLeafIdsRef.current - if (hostRetiredLeafIds.size > 0) { - // Why: never retire the last pane here; the host drops a whole tab by itself. - layout = retireLeavesFromTerminalLayout(layout, hostRetiredLeafIds) ?? layout - } - setTabLayout(tabId, layout) - const hasRemotePane = Object.values(mergedPtyIds).some( - (ptyId) => typeof ptyId === 'string' && isRemoteRuntimePtyId(ptyId) - ) - if (hasRemotePane) { - // Why: a host keeps its titles when the field is absent, so clearing the last one sends {}. - const clearedEveryTitle = - !layout.titlesByLeafId && - [...currentLeafIds].some((leafId) => removedTitleLeafIds.has(leafId)) - remotePaneLayoutPusherRef.current?.push({ - worktreeId, - tabId, - layout: clearedEveryTitle ? { ...layout, titlesByLeafId: {} } : layout - }) - } - for (const leafId of currentLeafIds) { - clearedScrollbackLeafIds.delete(leafId) - } - }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [tabId, setTabLayout, worktreeId] - ) + } + for (const leafId of currentLeafIds) { + clearedScrollbackLeafIds.delete(leafId) + } + }, [ + tabId, + setTabLayout, + worktreeId, + clearedScrollbackLeafIdsRef, + containerRef, + expandedPaneIdRef, + managerRef, + paneTitlesRef, + paneTransportsRef, + remotePaneLayoutPusherRef, + removedTitleLeafIdsRef + ]) useEffect(() => { persistLayoutSnapshot() @@ -172,8 +159,7 @@ export function useTerminalPaneLayoutPersistence(controller: TerminalPaneStartup } persistLayoutSnapshot() }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [paneTransportsRef, persistLayoutSnapshot] + [paneTransportsRef, persistLayoutSnapshot, clearedScrollbackLeafIdsRef] ) const removePaneTitleLocally = useCallback( @@ -198,8 +184,7 @@ export function useTerminalPaneLayoutPersistence(controller: TerminalPaneStartup // Why: every caller is a user clearing a pane title. persistLayoutSnapshot() }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [persistLayoutSnapshot] + [persistLayoutSnapshot, managerRef, paneTitlesRef, removedTitleLeafIdsRef, setPaneTitles] ) const setPaneTitleLocally = useCallback( (paneId: number, title: string) => { @@ -211,8 +196,7 @@ export function useTerminalPaneLayoutPersistence(controller: TerminalPaneStartup } persistLayoutSnapshot() }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Refs are stable. - [persistLayoutSnapshot] + [persistLayoutSnapshot, managerRef, paneTitlesRef, removedTitleLeafIdsRef, setPaneTitles] ) /** Shows a title main already holds, without asking main again. */ const showPaneTitle = useCallback( @@ -245,8 +229,7 @@ export function useTerminalPaneLayoutPersistence(controller: TerminalPaneStartup } showTitle() }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Refs are stable. - [showPaneTitle, tabId, worktreeId] + [showPaneTitle, tabId, worktreeId, managerRef] ) const removePaneTitle = useCallback( (paneId: number) => changePaneTitle(paneId, null), @@ -259,8 +242,7 @@ export function useTerminalPaneLayoutPersistence(controller: TerminalPaneStartup } removePaneTitle(paneId) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [removePaneTitle] + [removePaneTitle, paneTitlesRef] ) useEffect(() => { @@ -302,8 +284,15 @@ export function useTerminalPaneLayoutPersistence(controller: TerminalPaneStartup return next }) persistLayoutSnapshot() - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [paneCount, paneTitles, persistLayoutSnapshot, terminalTab]) + }, [ + paneCount, + paneTitles, + persistLayoutSnapshot, + terminalTab, + managerRef, + paneTitlesRef, + setPaneTitles + ]) return { hostRetiredLeafIdsRef, diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-mobile-actions.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-mobile-actions.ts index 69639fac50b..47f19afd056 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-mobile-actions.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-mobile-actions.ts @@ -63,8 +63,7 @@ export function useTerminalPaneMobileActions(controller: TerminalPaneContextCont pane.terminal.focus() } }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [refreshMobileOverlays, scheduleRestoredTerminalRefit] + [refreshMobileOverlays, scheduleRestoredTerminalRefit, paneTransportsRef, settingsRef] ) const restoreAllTerminalFits = useCallback( async (focusPane: ManagedPane): Promise => { @@ -77,8 +76,7 @@ export function useTerminalPaneMobileActions(controller: TerminalPaneContextCont focusPane.terminal.focus() } }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [getMobileOwnedTerminalPtyIds, scheduleRestoredTerminalRefit] + [getMobileOwnedTerminalPtyIds, scheduleRestoredTerminalRefit, settingsRef] ) const terminalShouldHandleMiddleClick = useCallback( (target: EventTarget | null): target is Node => { @@ -115,8 +113,7 @@ export function useTerminalPaneMobileActions(controller: TerminalPaneContextCont null ) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [terminalShouldHandleMiddleClick] + [terminalShouldHandleMiddleClick, managerRef] ) const handlePrimarySelectionMiddleMouseDown = useCallback( (event: React.MouseEvent): void => { @@ -182,8 +179,7 @@ export function useTerminalPaneMobileActions(controller: TerminalPaneContextCont recordTerminalUserInputForLeaf(tabId, clickedPane.leafId) }) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [findTerminalPaneForMiddleClick, tabId] + [findTerminalPaneForMiddleClick, tabId, managerRef, paneTransportsRef, setTerminalError] ) const handlePrimarySelectionAuxClick = useCallback( (event: React.MouseEvent): void => { @@ -205,10 +201,12 @@ export function useTerminalPaneMobileActions(controller: TerminalPaneContextCont }, [findTerminalPaneForMiddleClick] ) - const activatePaneTitleInteraction = useCallback((paneId: number): void => { - managerRef.current?.setActivePane(paneId, { focus: false }) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, []) + const activatePaneTitleInteraction = useCallback( + (paneId: number): void => { + managerRef.current?.setActivePane(paneId, { focus: false }) + }, + [managerRef] + ) const splitTerminalPaneFromHeader = useCallback( (pane: ManagedPane, direction: 'vertical' | 'horizontal') => { const manager = managerRef.current @@ -228,15 +226,13 @@ export function useTerminalPaneMobileActions(controller: TerminalPaneContextCont source: 'context_menu' }) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [cwd] + [cwd, managerRef, paneCwdRef, paneTransportsRef, tabId, worktreeId] ) const beginPaneDragFromHeader = useCallback( (paneId: number, handle: HTMLElement, event: PointerEvent) => { managerRef.current?.beginPaneDragFromPointerDown(paneId, handle, event) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [] + [managerRef] ) return { diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-process-exit-actions.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-process-exit-actions.ts index 423e9b6a1b2..793b1b8ecce 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-process-exit-actions.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-process-exit-actions.ts @@ -133,7 +133,6 @@ export function useTerminalPaneProcessExitActions(controller: TerminalPaneCloseC manager.setActivePane(paneId, { focus: true }) } }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. [ clearCodexRestartNotice, clearExitedPanePtyLayoutBinding, @@ -166,7 +165,8 @@ export function useTerminalPaneProcessExitActions(controller: TerminalPaneCloseC tabId, updateTabPtyId, updateTabTitle, - worktreeId + worktreeId, + setTerminalErrorsByPaneId ] ) @@ -255,13 +255,15 @@ export function useTerminalPaneProcessExitActions(controller: TerminalPaneCloseC handleRestartPane(pane.id, startup, { focus: agent === 'codex' }) } } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. }, [ buildAccountRestartStartup, consumePendingPaneAccountRestart, handleRestartPane, panePtyLayoutBindings, - readyRestartPtyIds + readyRestartPtyIds, + managerRef, + paneTransportsRef, + tabId ]) return { handleRestartExitedPane, handleCloseExitedPane } diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-projection.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-projection.ts index 36617cc3fb6..ab3376d7361 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-projection.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-projection.ts @@ -100,8 +100,7 @@ export function useTerminalPaneProjection(controller: TerminalPaneMobileControll setTerminalErrorsByPaneId((current) => mapPaneTerminalErrors(current, stripSshReconnectOwnedErrorLines) ) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [sshReconnectOwnsTerminalErrors]) + }, [sshReconnectOwnsTerminalErrors, setTerminalError, setTerminalErrorsByPaneId]) const visibleTerminalError = terminalErrorForPane( terminalError, terminalErrorsByPaneId, diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-reconciliation.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-reconciliation.ts index 1dc48d66cf9..d94f627083f 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-reconciliation.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-reconciliation.ts @@ -136,14 +136,16 @@ export function useTerminalPaneReconciliation(controller: TerminalPaneCloseContr // Why ptyRecoveryStatesByPaneId: a host-retired pane whose PTY has not yet // finished ending is kept until this re-run, when its transport reports a new // recovery state and its PTY has cleared. - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. }, [ executeClosePane, isActive, paneCount, persistLayoutSnapshot, ptyRecoveryStatesByPaneId, - restoredLayout + restoredLayout, + hostRetiredLeafIdsRef, + managerRef, + paneTransportsRef ]) useLayoutEffect(() => { @@ -186,8 +188,7 @@ export function useTerminalPaneReconciliation(controller: TerminalPaneCloseContr } const frame = scheduleRefit() return () => cancelAnimationFrame(frame) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [isolatedPaneKey, paneCount, tabId]) + }, [isolatedPaneKey, paneCount, tabId, activityIsolationSnapshotRef, containerRef, managerRef]) useEffect(() => { const snapshots = activityIsolationSnapshotRef.current @@ -195,8 +196,7 @@ export function useTerminalPaneReconciliation(controller: TerminalPaneCloseContr restoreExpandedLayoutFrom(snapshots) cancelPendingPaneSizeRefreshFrames({ pendingPaneSizeRefreshFrameIdsRef }) } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, []) + }, [activityIsolationSnapshotRef, pendingPaneSizeRefreshFrameIdsRef]) const processExitActions = useTerminalPaneProcessExitActions(controller) diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-startup-actions.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-startup-actions.ts index 2eea4fcb752..f3641da899f 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-startup-actions.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-startup-actions.ts @@ -76,16 +76,23 @@ export function useTerminalPaneStartupActions(controller: TerminalPaneStoreContr ) ) } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [isVisible, shouldMeasureHiddenStartup]) + }, [ + isVisible, + shouldMeasureHiddenStartup, + setShouldMeasureHiddenStartup, + setTerminalError, + setTerminalErrorsByPaneId + ]) - const clearSessionRestoredBannerForPane = useCallback((paneId: number): void => { - setSessionRestoredBannerPaneIds((previous) => { - const next = removeSessionRestoredBannerPaneId(previous, paneId) - return next === previous ? previous : next - }) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, []) + const clearSessionRestoredBannerForPane = useCallback( + (paneId: number): void => { + setSessionRestoredBannerPaneIds((previous) => { + const next = removeSessionRestoredBannerPaneId(previous, paneId) + return next === previous ? previous : next + }) + }, + [setSessionRestoredBannerPaneIds] + ) const showRestoredSessionBanner = useCallback( (paneId: number, reason: SessionRestoredBannerReason = 'restored'): void => { setSessionRestoredBannerPaneIds((previous) => { @@ -93,8 +100,7 @@ export function useTerminalPaneStartupActions(controller: TerminalPaneStoreContr return next === previous ? previous : next }) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [] + [setSessionRestoredBannerPaneIds] ) const dismissSessionRestoredBanner = useCallback( (event: SessionRestoredBannerDismissEvent): void => { @@ -102,8 +108,7 @@ export function useTerminalPaneStartupActions(controller: TerminalPaneStoreContr dismissSessionRestoredBannerPaneIds(previous, event, managerRef.current?.getPanes() ?? []) ) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [] + [managerRef, setSessionRestoredBannerPaneIds] ) useSessionRestoredBannerDismiss( sessionRestoredBannerPaneIds.size > 0, @@ -117,8 +122,7 @@ export function useTerminalPaneStartupActions(controller: TerminalPaneStoreContr void refreshWorkspaceSpace().catch((error: unknown) => { console.warn('Failed to refresh Space Analyzer after terminal session save failure:', error) }) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [openSpacePage, refreshWorkspaceSpace]) + }, [openSpacePage, refreshWorkspaceSpace, setSessionStateSaveFailureMessage]) const quickCommandRepoId = worktreeId === FLOATING_TERMINAL_WORKTREE_ID ? null : getRepoIdFromWorktreeId(worktreeId) @@ -143,8 +147,7 @@ export function useTerminalPaneStartupActions(controller: TerminalPaneStoreContr setQuickCommandEditorHostId(hostId) setQuickCommandEditorOpen(true) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [] + [setQuickCommandDraft, setQuickCommandEditorHostId, setQuickCommandEditorOpen] ) const saveQuickCommand = useCallback( (command: TerminalQuickCommand): Promise => diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-title-effects.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-title-effects.ts index 9ac1bdf29c5..263c8303745 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-title-effects.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-title-effects.ts @@ -53,8 +53,7 @@ export function useTerminalPaneTitleEffects(controller: TerminalPaneCloseControl if (manager) { applyTerminalPaneAttentionToManager(manager, tabId) } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [tabId]) + }, [tabId, managerRef]) useLayoutEffect(() => { applyTerminalPaneAttention() return subscribeTerminalPaneAttention(tabId, applyTerminalPaneAttention) @@ -74,7 +73,6 @@ export function useTerminalPaneTitleEffects(controller: TerminalPaneCloseControl if (needsFit && (isVisible || shouldMeasureHiddenStartup)) { fitPanes(manager) } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. }, [ paneCount, paneLayoutRevision, @@ -82,7 +80,8 @@ export function useTerminalPaneTitleEffects(controller: TerminalPaneCloseControl renamingPaneId, sessionRestoredBannerPaneIds, isVisible, - shouldMeasureHiddenStartup + shouldMeasureHiddenStartup, + managerRef ]) const syncPaneTitleOverlayRects = useCallback((): void => { @@ -108,8 +107,7 @@ export function useTerminalPaneTitleEffects(controller: TerminalPaneCloseControl setPaneTitleOverlayRects((previous) => arePaneTitleOverlayRectsEqual(previous, nextRects) ? previous : nextRects ) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, []) + }, [containerRef, managerRef, setPaneTitleOverlayRects]) useLayoutEffect(() => { const manager = managerRef.current @@ -140,7 +138,6 @@ export function useTerminalPaneTitleEffects(controller: TerminalPaneCloseControl cancelAnimationFrame(frame) } } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. }, [ expandedPaneId, isolatedPaneKey, @@ -150,7 +147,10 @@ export function useTerminalPaneTitleEffects(controller: TerminalPaneCloseControl paneTitles, renamingPaneId, sessionRestoredBannerPaneIds, - syncPaneTitleOverlayRects + syncPaneTitleOverlayRects, + containerRef, + managerRef, + setPaneTitleOverlayRects ]) useEffect(() => { @@ -162,8 +162,7 @@ export function useTerminalPaneTitleEffects(controller: TerminalPaneCloseControl const next = pruneSessionRestoredBannerPaneIds(previous, manager.getPanes()) return next === previous ? previous : next }) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [paneCount]) + }, [paneCount, managerRef, setSessionRestoredBannerPaneIds]) useEffect(() => { const captureBuffers = (options?: ShutdownBufferCaptureOptions): void => { @@ -217,8 +216,15 @@ export function useTerminalPaneTitleEffects(controller: TerminalPaneCloseControl shutdownBufferCaptures.delete(tabId) } } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [tabId, worktreeId, setTabLayout, setTabLocalOnlyScrollback]) + }, [ + tabId, + worktreeId, + setTabLayout, + setTabLocalOnlyScrollback, + clearedScrollbackLeafIdsRef, + managerRef, + paneTransportsRef + ]) useEffect(() => { if (renamingPaneId === null) { @@ -243,6 +249,5 @@ export function useTerminalPaneTitleEffects(controller: TerminalPaneCloseControl document.removeEventListener('pointerdown', markPointerBlurIntent, true) document.removeEventListener('keydown', markKeyboardBlurIntent, true) } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [renamingPaneId]) + }, [renamingPaneId, renameInputRef, renameUserRequestedBlurCommitRef]) } diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-title-state.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-title-state.ts index 71c1bb1bede..7b6cce84e49 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-title-state.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-title-state.ts @@ -70,8 +70,7 @@ export function useTerminalPaneTitleState(controller: TerminalPaneFoundation) { } closeRenameSession() }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - [closeRenameSession] + [closeRenameSession, containerRef] ) const handleStartRename = useCallback( @@ -116,8 +115,7 @@ export function useTerminalPaneTitleState(controller: TerminalPaneFoundation) { setTerminalErrorsByPaneId((current) => clearPaneTerminalError(current, paneId)) paneTransportsRef.current.get(paneId)?.notifyErrorSurfaceDismissed?.() } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, []) + }, [managerRef, paneTransportsRef, setTerminalError, setTerminalErrorsByPaneId]) const onPtyRecoveryStateRef = useRef( (paneId: number, state: PtyTransportRecoveryState | null) => { setPtyRecoveryStatesByPaneId((previous) => diff --git a/src/renderer/src/components/terminal/use-activation-deferred-tab-admission.ts b/src/renderer/src/components/terminal/use-activation-deferred-tab-admission.ts index 71390ebbe2e..e4eaa34eba2 100644 --- a/src/renderer/src/components/terminal/use-activation-deferred-tab-admission.ts +++ b/src/renderer/src/components/terminal/use-activation-deferred-tab-admission.ts @@ -83,6 +83,12 @@ export function useActivationDeferredTabAdmission( // install a plan for the already-active worktree by mutating only refs — // neither other dep changes, and without this revision the tabs stay // unmounted until the user switches workspaces and back. - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. - }, [activationDeferralPlanRevision, backgroundMountRevision, renderedActiveWorktreeId]) + }, [ + activationDeferralPlanRevision, + activationDeferredMountTabIdsByWorktreeRef, + backgroundMountRevision, + backgroundMountTabIdsByWorktreeRef, + renderedActiveWorktreeId, + setBackgroundMountRevision + ]) } diff --git a/src/renderer/src/components/terminal/use-active-terminal-repair.ts b/src/renderer/src/components/terminal/use-active-terminal-repair.ts index 81197f29fd2..e14a53df196 100644 --- a/src/renderer/src/components/terminal/use-active-terminal-repair.ts +++ b/src/renderer/src/components/terminal/use-active-terminal-repair.ts @@ -48,9 +48,15 @@ export function useActiveTerminalRepair(input: ActiveTerminalRepairInput): void tabs } = input useEffect(() => { - repairActiveTerminalTab(input) + repairActiveTerminalTab({ + activeTabId, + activeTabIdByWorktree, + activeTabType, + renderedActiveWorktreeId, + setActiveTab, + tabs + }) // Why: `tabs` is the dependency so repair reacts to order/content changes, not just scalar ids. - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ activeTabId, activeTabType, diff --git a/src/renderer/src/components/use-task-page-jira-list-effects.ts b/src/renderer/src/components/use-task-page-jira-list-effects.ts index 56ba02ef81c..2d11869e32f 100644 --- a/src/renderer/src/components/use-task-page-jira-list-effects.ts +++ b/src/renderer/src/components/use-task-page-jira-list-effects.ts @@ -15,7 +15,8 @@ import { } from '@/components/task-page-jira-status-order' import { createTaskPageJiraLoadFailureState } from '@/components/task-page-jira-load-state' import { searchTaskPageJiraIssues } from '@/components/task-page-jira-search' -import { JIRA_ITEM_LIMIT, TASK_SEARCH_DEBOUNCE_MS } from './task-page-source-context' +import { JIRA_ITEM_LIMIT } from './task-page-source-context' +import { useTaskSearchDebouncedApply } from './task-page/use-task-search-debounced-apply' import { readLinkedJiraIssues } from './task-page-jira-linked-issues' import { ipcOrRuntimeTargetForHostId } from '@/runtime/runtime-client-target' import { buildJiraPresetJql } from '../../../shared/jira/jira-preset-jql' @@ -71,15 +72,7 @@ export function useTaskPageJiraListEffects(model: TaskPageLinearCollectionEffect jiraTaskSourceScopeKey, statusOrderOwner ) - useEffect(() => { - if (!taskResumeApplied) { - return - } - const timeout = window.setTimeout(() => { - setAppliedJiraSearch(jiraSearchInput) - }, TASK_SEARCH_DEBOUNCE_MS) - return () => window.clearTimeout(timeout) - }, [jiraSearchInput, taskResumeApplied, setAppliedJiraSearch]) + useTaskSearchDebouncedApply(jiraSearchInput, taskResumeApplied, setAppliedJiraSearch) useEffect(() => { if (!taskResumeApplied) { return @@ -242,7 +235,7 @@ export function useTaskPageJiraListEffects(model: TaskPageLinearCollectionEffect return () => { cancelled = true } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps -- linkedJiraIssueRefsSignature stands in for the refs array, which re-derives on unrelated workspace churn; store actions and setters are stable. }, [ taskSource, jiraConnected, diff --git a/src/renderer/src/components/use-task-page-provider-metadata.ts b/src/renderer/src/components/use-task-page-provider-metadata.ts index e5ceef429b1..16cfca31a54 100644 --- a/src/renderer/src/components/use-task-page-provider-metadata.ts +++ b/src/renderer/src/components/use-task-page-provider-metadata.ts @@ -51,7 +51,6 @@ export function useTaskPageProviderMetadata(model: TaskPageResumeRestorationMode return () => { cancelled = true } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ taskSource, linearConnected, diff --git a/src/renderer/src/components/use-terminal-browser-retention.ts b/src/renderer/src/components/use-terminal-browser-retention.ts index 1cc2e8d6740..1b6e5549479 100644 --- a/src/renderer/src/components/use-terminal-browser-retention.ts +++ b/src/renderer/src/components/use-terminal-browser-retention.ts @@ -34,8 +34,7 @@ export function useTerminalBrowserRetention(controller: TerminalParkingFoundatio removeDownloadTracking() removePaintRetentionTracking() } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- the controller setter preserves its original stable identity. - }, []) + }, [setBrowserGuestRetentionRevision]) useEffect(() => { if (!renderedActiveWorktreeId) { @@ -79,10 +78,12 @@ export function useTerminalBrowserRetention(controller: TerminalParkingFoundatio worktreeId ) } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs preserve their original stable identities. }, [ + browserGuestWorktreeRecencyRef, + mountedWorktreeIdsRef, renderedActiveWorktreeId, workspaceSurfaceIds, + workspaceSurfaceIdSet, browserGuestRetentionBudgetEnabled, browserGuestRetentionRevision ]) diff --git a/src/renderer/src/components/use-terminal-editor-close-dialog-actions.ts b/src/renderer/src/components/use-terminal-editor-close-dialog-actions.ts index c9a40f3ea5a..c682a368a1e 100644 --- a/src/renderer/src/components/use-terminal-editor-close-dialog-actions.ts +++ b/src/renderer/src/components/use-terminal-editor-close-dialog-actions.ts @@ -111,11 +111,14 @@ export function useTerminalEditorCloseDialogActions( settleQueuedClose(fileId) advanceEditorCloseQueue() releaseCloseDialogGuardAfterDebounce() - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. }, [ advanceEditorCloseQueue, + inFlightSaveFileIdRef, + isClosingRef, releaseCloseDialogGuardAfterDebounce, saveDialogFileId, + setSaveDialogFileId, + settleQueuedClose, waitForFileClosed ]) @@ -130,8 +133,14 @@ export function useTerminalEditorCloseDialogActions( settleQueuedClose(fileId) advanceEditorCloseQueue() releaseCloseDialogGuardAfterDebounce() - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. - }, [advanceEditorCloseQueue, releaseCloseDialogGuardAfterDebounce, saveDialogFileId]) + }, [ + advanceEditorCloseQueue, + isClosingRef, + releaseCloseDialogGuardAfterDebounce, + saveDialogFileId, + setSaveDialogFileId, + settleQueuedClose + ]) const handleSaveDialogCancel = useCallback(() => { if (isClosingRef.current) { @@ -143,8 +152,14 @@ export function useTerminalEditorCloseDialogActions( windowCloseAfterDirtyRef.current = null setSaveDialogFileId(null) releaseCloseDialogGuardAfterDebounce() - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. - }, [releaseCloseDialogGuardAfterDebounce]) + }, [ + closedReactionsRef, + isClosingRef, + pendingEditorCloseQueueRef, + releaseCloseDialogGuardAfterDebounce, + setSaveDialogFileId, + windowCloseAfterDirtyRef + ]) useEffect(() => { const onRequestEditorClose = (event: Event): void => { diff --git a/src/renderer/src/components/use-terminal-editor-close-queue.ts b/src/renderer/src/components/use-terminal-editor-close-queue.ts index 81ec1c243f4..140eb54413c 100644 --- a/src/renderer/src/components/use-terminal-editor-close-queue.ts +++ b/src/renderer/src/components/use-terminal-editor-close-queue.ts @@ -44,15 +44,17 @@ export function useTerminalEditorCloseQueue(controller: TerminalEditorCloseFound }) }, []) - const settleQueuedClose = useCallback((fileId: string) => { - pendingEditorCloseQueueRef.current = pendingEditorCloseQueueRef.current.filter( - (queuedId) => queuedId !== fileId - ) - const onClosed = closedReactionsRef.current.get(fileId) - closedReactionsRef.current.delete(fileId) - onClosed?.() - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs preserve their original stable identities. - }, []) + const settleQueuedClose = useCallback( + (fileId: string) => { + pendingEditorCloseQueueRef.current = pendingEditorCloseQueueRef.current.filter( + (queuedId) => queuedId !== fileId + ) + const onClosed = closedReactionsRef.current.get(fileId) + closedReactionsRef.current.delete(fileId) + onClosed?.() + }, + [closedReactionsRef, pendingEditorCloseQueueRef] + ) const getNextQueuedEditorClose = useCallback((): string | null => { while (pendingEditorCloseQueueRef.current.length > 0) { @@ -73,8 +75,7 @@ export function useTerminalEditorCloseQueue(controller: TerminalEditorCloseFound return fileId } return null - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs preserve their original stable identities. - }, [closeFile, settleQueuedClose]) + }, [closeFile, inFlightSaveFileIdRef, pendingEditorCloseQueueRef, settleQueuedClose]) const advanceEditorCloseQueue = useCallback(() => { const nextFileId = getNextQueuedEditorClose() @@ -95,8 +96,14 @@ export function useTerminalEditorCloseQueue(controller: TerminalEditorCloseFound windowCloseAfterDirtyRef.current = null proceedToNativeWindowClose(pendingWindowClose.isQuitting) } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. - }, [getNextQueuedEditorClose, proceedToNativeWindowClose, setActiveFile, setActiveTabType]) + }, [ + getNextQueuedEditorClose, + proceedToNativeWindowClose, + setActiveFile, + setActiveTabType, + setSaveDialogFileId, + windowCloseAfterDirtyRef + ]) const queueEditorCloseRequests = useCallback( (fileIds: string[], pendingWindowClose?: { isQuitting: boolean }) => { @@ -110,8 +117,7 @@ export function useTerminalEditorCloseQueue(controller: TerminalEditorCloseFound ) advanceEditorCloseQueue() }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs preserve their original stable identities. - [advanceEditorCloseQueue] + [advanceEditorCloseQueue, pendingEditorCloseQueueRef, windowCloseAfterDirtyRef] ) const handleCloseFile = useCallback( diff --git a/src/renderer/src/components/use-terminal-parking-foundation.ts b/src/renderer/src/components/use-terminal-parking-foundation.ts index 3123a881d20..aba2019d042 100644 --- a/src/renderer/src/components/use-terminal-parking-foundation.ts +++ b/src/renderer/src/components/use-terminal-parking-foundation.ts @@ -135,8 +135,7 @@ export function useTerminalParkingFoundation(controller: TerminalEditorCloseCont } closeDialogDebounceTimers.clear() } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. - }, []) + }, [closeDialogDebounceTimersRef, measurableBackgroundWorktreeIdsRef, mountedWorktreeIdsRef]) useEffect(() => { const timers = terminalWorktreeParkingTimersRef.current @@ -146,8 +145,7 @@ export function useTerminalParkingFoundation(controller: TerminalEditorCloseCont } timers.clear() } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- the controller ref preserves its original stable identity. - }, []) + }, [terminalWorktreeParkingTimersRef]) return { measurableBackgroundWorktreeTimersRef, diff --git a/src/renderer/src/components/use-terminal-parking-pass.ts b/src/renderer/src/components/use-terminal-parking-pass.ts index 3bb09d67d54..d19ca527cb0 100644 --- a/src/renderer/src/components/use-terminal-parking-pass.ts +++ b/src/renderer/src/components/use-terminal-parking-pass.ts @@ -206,14 +206,19 @@ export function useTerminalParkingPass(controller: TerminalParkingFoundation): v pass.parkingTimers.set(worktreeId, timer) } } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. + // oxlint-disable-next-line react-hooks/exhaustive-deps -- `controller` is rebuilt every render; the pass reads only the listed values and stable refs. }, [ activeView, activityTerminalPortals, backgroundMountRevision, + parkedCaptureDoneRef, pendingStartupByTabId, pairedRuntimeParkingEnvironmentIds, renderedActiveWorktreeId, + setEvictionExemptTerminalTabIds, + setForceParkedTerminalWorktreeIds, + setParkedTerminalWorktreeIds, + setTerminalParkingRevision, tabsByWorktree, terminalParkingEnabled, terminalParkingRevision, diff --git a/src/renderer/src/components/use-terminal-watcher-effects.ts b/src/renderer/src/components/use-terminal-watcher-effects.ts index d645860eac3..be311b79e03 100644 --- a/src/renderer/src/components/use-terminal-watcher-effects.ts +++ b/src/renderer/src/components/use-terminal-watcher-effects.ts @@ -167,8 +167,8 @@ export function useTerminalWatcherEffects(controller: TerminalWatcherController) }) } syncParkedTerminalTabWatchersForWorkspaces(syncEntriesByWorktreeId) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs preserve their original stable identities. }, [ + activationDeferredMountTabIdsByWorktreeRef, activeTabId, activeView, activityTerminalPortals, @@ -179,6 +179,8 @@ export function useTerminalWatcherEffects(controller: TerminalWatcherController) getEffectiveLayoutForWorktree, groupsByWorktree, effectiveParkedTerminalWorktreeIds, + measurableBackgroundWorktreeIdsRef, + mountedWorktreeIdsRef, pairedRuntimeParkingEnvironmentIds, pendingStartupByTabId, renderedActiveWorktreeId, diff --git a/src/renderer/src/components/use-terminal-window-lifecycle.ts b/src/renderer/src/components/use-terminal-window-lifecycle.ts index 762596f4df0..a2bacd89f9c 100644 --- a/src/renderer/src/components/use-terminal-window-lifecycle.ts +++ b/src/renderer/src/components/use-terminal-window-lifecycle.ts @@ -55,8 +55,7 @@ export function useTerminalWindowLifecycle(controller: TerminalActivationControl proceedToNativeWindowClose(isQuitting) }) return () => setWindowCloseRequestHandler(null) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs preserve their original stable identities. - }, [proceedToNativeWindowClose, queueEditorCloseRequests]) + }, [proceedToNativeWindowClose, queueEditorCloseRequests, windowCloseAfterDirtyRef]) // Why lazy: a `useRef(expr)` argument re-runs on every render and is thrown away, and this // walks every browser page and tab across all worktrees on a component that renders constantly. diff --git a/src/renderer/src/components/use-worktree-jump-palette-list-entries.ts b/src/renderer/src/components/use-worktree-jump-palette-list-entries.ts index 5127e4b94c1..1ca0617a865 100644 --- a/src/renderer/src/components/use-worktree-jump-palette-list-entries.ts +++ b/src/renderer/src/components/use-worktree-jump-palette-list-entries.ts @@ -289,8 +289,7 @@ export function useWorktreeJumpPaletteListEntries({ ) useEffect(() => { autoSelectedItemIdRef.current = selectionItemIds[0] ?? null - // oxlint-disable-next-line react-hooks/exhaustive-deps -- the controller ref preserves its original stable identity. - }, [selectionItemIds]) + }, [autoSelectedItemIdRef, selectionItemIds]) return { listEntries, listEntryRenderKeys, selectableItems, selectionItemIds } } diff --git a/src/renderer/src/components/use-worktree-jump-palette-quick-actions.ts b/src/renderer/src/components/use-worktree-jump-palette-quick-actions.ts index 05aef3e7cb5..d28e78a69fb 100644 --- a/src/renderer/src/components/use-worktree-jump-palette-quick-actions.ts +++ b/src/renderer/src/components/use-worktree-jump-palette-quick-actions.ts @@ -128,8 +128,8 @@ export function useWorktreeJumpPaletteQuickActions({ deleteActiveWorkspace: deleteActiveWorkspaceAction, openAddQuickCommand: openAddQuickCommandAction }), - // oxlint-disable-next-line react-hooks/exhaustive-deps -- the controller ref preserves its original stable identity. [ + activeGroupSnapshotRef, deleteActiveWorkspaceAction, openAddQuickCommandAction, openBrowserUrlAction, diff --git a/src/renderer/src/components/use-worktree-jump-palette-recent-tabs.ts b/src/renderer/src/components/use-worktree-jump-palette-recent-tabs.ts index 5faad0aa000..a5c99953d09 100644 --- a/src/renderer/src/components/use-worktree-jump-palette-recent-tabs.ts +++ b/src/renderer/src/components/use-worktree-jump-palette-recent-tabs.ts @@ -156,26 +156,6 @@ export function useWorktreeJumpPaletteRecentTabs({ () => new Map(openTabRecentRows.map(({ item, row }) => [item, row])), [openTabRecentRows] ) - const recentTabRows = useMemo(() => { - const now = Date.now() - const rows: RecentWorkspaceTabRow[] = [] - for (const { item, worktree, row } of openTabRecentRows) { - if ( - shouldIncludeOpenTabInRecentSection({ - item, - worktree, - row, - paneSources: recentTabPaneSources, - unreadTerminalTabs, - unreadAgentCompletionPanes, - now - }) - ) { - rows.push(row) - } - } - return rows - }, [openTabRecentRows, recentTabPaneSources, unreadAgentCompletionPanes, unreadTerminalTabs]) const [recentTabSnapshot, setRecentTabSnapshot] = useState(EMPTY_RECENT_TAB_SNAPSHOT) // Why: recent rows are already narrowed by the filter, so a filter change mid-open must // re-capture — a frozen order would otherwise hide rows a cleared chip brought back. @@ -214,9 +194,25 @@ export function useWorktreeJumpPaletteRecentTabs({ ) { return } - const order = orderRecentWorkspaceTabs({ - rows: recentTabRows - }) + // Why here: inclusion reads wall time, so it is sampled when the snapshot is captured. + const now = Date.now() + const rows: RecentWorkspaceTabRow[] = [] + for (const { item, worktree, row } of openTabRecentRows) { + if ( + shouldIncludeOpenTabInRecentSection({ + item, + worktree, + row, + paneSources: recentTabPaneSources, + unreadTerminalTabs, + unreadAgentCompletionPanes, + now + }) + ) { + rows.push(row) + } + } + const order = orderRecentWorkspaceTabs({ rows }) if (order.length === 0) { setRecentTabSnapshot(EMPTY_RECENT_TAB_SNAPSHOT) return @@ -225,15 +221,18 @@ export function useWorktreeJumpPaletteRecentTabs({ setSelectedItemId((current) => current === '' || current === autoSelectedItemIdRef.current ? '' : current ) - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. }, [ + autoSelectedItemIdRef, filter, hasQuery, + openTabRecentRows, query.length, recentOrderAttentionIncomplete, recentTabSnapshot, recentTabPaneSources, - recentTabRows, + setSelectedItemId, + unreadAgentCompletionPanes, + unreadTerminalTabs, visible ]) const recentTabItems = useMemo(() => { diff --git a/src/renderer/src/components/use-worktree-jump-palette-selection-actions.ts b/src/renderer/src/components/use-worktree-jump-palette-selection-actions.ts index 24911dea130..27b3baed81a 100644 --- a/src/renderer/src/components/use-worktree-jump-palette-selection-actions.ts +++ b/src/renderer/src/components/use-worktree-jump-palette-selection-actions.ts @@ -62,6 +62,12 @@ export function useWorktreeJumpPaletteSelectionActions({ previousWorktreeIdRef, previousFocusElementRef }: WorktreeJumpPaletteSelectionActionsInput) { + // Why: the selection moves focus itself, so closing must not restore the prior target. + const dismissForSelection = useCallback(() => { + skipRestoreFocusRef.current = true + closeModal() + setSelectedItemId('') + }, [closeModal, setSelectedItemId, skipRestoreFocusRef]) const handleSelectWorktree = useCallback( (worktree: Worktree) => { const executionHostId = getPaletteWorktreeExecutionHostId(worktree) @@ -80,15 +86,12 @@ export function useWorktreeJumpPaletteSelectionActions({ return } recordFeatureInteraction('cmd-j-workspace-open') - skipRestoreFocusRef.current = true - closeModal() - setSelectedItemId('') + dismissForSelection() if (!queueWorkspaceActivationTerminalFocus(worktree.id, activation)) { focusFallbackSurface() } }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. - [closeModal, focusFallbackSurface, recordFeatureInteraction] + [dismissForSelection, focusFallbackSurface, recordFeatureInteraction] ) const handleSelectBrowserPage = useCallback( (result: BrowserPaletteSearchResult) => { @@ -111,13 +114,10 @@ export function useWorktreeJumpPaletteSelectionActions({ return } recordFeatureInteraction('cmd-j-browser-page-open') - skipRestoreFocusRef.current = true - closeModal() - setSelectedItemId('') + dismissForSelection() requestBrowserFocus({ pageId: activation.pageId, target: activation.focusTarget }) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. - [closeModal, recordFeatureInteraction, requestBrowserFocus] + [dismissForSelection, recordFeatureInteraction, requestBrowserFocus] ) const handleSelectSimulatorTab = useCallback( (result: SimulatorPaletteSearchResult) => { @@ -139,12 +139,9 @@ export function useWorktreeJumpPaletteSelectionActions({ ) return } - skipRestoreFocusRef.current = true - closeModal() - setSelectedItemId('') + dismissForSelection() }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. - [closeModal] + [dismissForSelection] ) const handleSelectWorkspaceTab = useCallback( (result: WorkspaceTabPaletteSearchResult) => { @@ -166,12 +163,9 @@ export function useWorktreeJumpPaletteSelectionActions({ ) return } - skipRestoreFocusRef.current = true - closeModal() - setSelectedItemId('') + dismissForSelection() }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. - [closeModal] + [dismissForSelection] ) const handleSelectSettings = useCallback( (result: CmdJSettingsResult) => { @@ -179,24 +173,19 @@ export function useWorktreeJumpPaletteSelectionActions({ if (result.targetSectionId) { target.sectionId = result.targetSectionId } - skipRestoreFocusRef.current = true - closeModal() - setSelectedItemId('') + dismissForSelection() openSettingsTarget(target) openSettingsPage() recordFeatureInteraction('cmd-j-settings-open') }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. - [closeModal, openSettingsPage, openSettingsTarget, recordFeatureInteraction] + [dismissForSelection, openSettingsPage, openSettingsTarget, recordFeatureInteraction] ) const handleSelectQuickAction = useCallback( (action: CmdJActionResult) => { if (action.sourceQuery !== undefined && liveQueryRef.current.trim() !== action.sourceQuery) { return } - skipRestoreFocusRef.current = true - closeModal() - setSelectedItemId('') + dismissForSelection() const context = buildQuickActionContext() void action .run(context) @@ -224,8 +213,7 @@ export function useWorktreeJumpPaletteSelectionActions({ ) }) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. - [buildQuickActionContext, closeModal, recordFeatureInteraction] + [buildQuickActionContext, dismissForSelection, liveQueryRef, recordFeatureInteraction] ) const handleSelectProjectTarget = useCallback( (result: CmdJProjectSearchResult) => { @@ -247,13 +235,19 @@ export function useWorktreeJumpPaletteSelectionActions({ focusFallbackSurface(previousFocusElementRef.current) } }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. [ closeModal, focusFallbackSurface, + previousActiveTabTypeRef, + previousBrowserFocusTargetRef, + previousBrowserPageIdRef, + previousFocusElementRef, + previousWorktreeIdRef, recordFeatureInteraction, requestBrowserFocus, - revealSidebarRow + revealSidebarRow, + setSelectedItemId, + skipRestoreFocusRef ] ) const handleSelectTask = useWorktreeJumpPaletteTaskSelection({ diff --git a/src/renderer/src/components/use-worktree-jump-palette-selection-lifecycle.ts b/src/renderer/src/components/use-worktree-jump-palette-selection-lifecycle.ts index 10492e9d10a..1b8cedeb384 100644 --- a/src/renderer/src/components/use-worktree-jump-palette-selection-lifecycle.ts +++ b/src/renderer/src/components/use-worktree-jump-palette-selection-lifecycle.ts @@ -121,15 +121,29 @@ export function useWorktreeJumpPaletteSelectionLifecycle({ activeGroupSnapshotRef.current = null } wasVisibleRef.current = visible - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and state setters are stable. }, [ activeBrowserTabId, + activeGroupSnapshotRef, activeTabType, activeWorktreeId, browserTabsByWorktree, createLookupGuard, + latestQueryRef, + listRef, + preserveCreateLookupOnCloseRef, + previousActiveTabTypeRef, + previousBrowserFocusTargetRef, + previousBrowserPageIdRef, + previousFocusElementRef, + previousWorktreeIdRef, recordFeatureInteraction, - visible + selectionMovedByUserRef, + setExpandedSectionCaps, + setQuery, + setSelectedItemId, + skipRestoreFocusRef, + visible, + wasVisibleRef ]) const commandSelectedItemId = selectedItemId === CREATE_WORKTREE_ITEM_ID && @@ -180,8 +194,7 @@ export function useWorktreeJumpPaletteSelectionLifecycle({ setExpandedSectionCaps({}) listRef.current?.scrollTo(0, 0) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities. - [] + [latestQueryRef, listRef, setExpandedSectionCaps, setQuery, setSelectedItemId] ) const cancelFallbackFocusFrames = useCallback((): void => { if (fallbackFocusOuterFrameRef.current !== null) { @@ -192,8 +205,7 @@ export function useWorktreeJumpPaletteSelectionLifecycle({ cancelAnimationFrame(fallbackFocusInnerFrameRef.current) fallbackFocusInnerFrameRef.current = null } - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs preserve their original stable identities. - }, []) + }, [fallbackFocusInnerFrameRef, fallbackFocusOuterFrameRef]) useEffect(() => cancelFallbackFocusFrames, [cancelFallbackFocusFrames]) const focusFallbackSurface = useCallback( (preferredTarget?: HTMLElement | null) => { @@ -206,8 +218,7 @@ export function useWorktreeJumpPaletteSelectionLifecycle({ }) }) }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs preserve their original stable identities. - [cancelFallbackFocusFrames] + [cancelFallbackFocusFrames, fallbackFocusInnerFrameRef, fallbackFocusOuterFrameRef] ) const requestBrowserFocus = useCallback( (detail: { pageId: string; target: 'webview' | 'address-bar' }) => { @@ -236,8 +247,17 @@ export function useWorktreeJumpPaletteSelectionLifecycle({ focusFallbackSurface(previousFocusElementRef.current) } }, - // oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs preserve their original stable identities. - [closeModal, focusFallbackSurface, requestBrowserFocus] + [ + closeModal, + focusFallbackSurface, + previousActiveTabTypeRef, + previousBrowserFocusTargetRef, + previousBrowserPageIdRef, + previousFocusElementRef, + previousWorktreeIdRef, + requestBrowserFocus, + skipRestoreFocusRef + ] ) return { hasAnyWorktrees, diff --git a/src/renderer/src/hooks/ipc-events-agent-status-window-test-fixtures.ts b/src/renderer/src/hooks/ipc-events-agent-status-window-test-fixtures.ts index ecedfe0759f..7c8fc5c3f2d 100644 --- a/src/renderer/src/hooks/ipc-events-agent-status-window-test-fixtures.ts +++ b/src/renderer/src/hooks/ipc-events-agent-status-window-test-fixtures.ts @@ -1,6 +1,3 @@ -/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 11 agent-status ipc-events specs, not shipped code, and it falls - outside the *.test / *.spec / tests glob set. Inlining stubReactSyncEffect and stubAuxiliaryModules would duplicate them - into all 11 specs and push several past the max-lines ratchet. */ import type * as ReactModule from 'react' import { vi } from 'vitest' import type { diff --git a/src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts b/src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts index 13fb929835d..b830c5bf093 100644 --- a/src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts +++ b/src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts @@ -1,6 +1,3 @@ -/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 4 close-routing ipc-events specs, not shipped code, and it falls - outside the *.test / *.spec / tests glob set. The stubs and the hook invocation are one unit; splitting the 11 doMock - calls back out would duplicate them into all 4 specs. */ import type * as ReactModule from 'react' import { vi } from 'vitest' import type { TerminalSurfaceCloseTarget } from '../../../shared/terminal-surface-close-target' diff --git a/src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts b/src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts index 50e7115bfa6..389f743f1e3 100644 --- a/src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts +++ b/src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts @@ -1,6 +1,3 @@ -/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for useIpcEvents-terminal-create-surfacing.test.ts, not shipped code, and it - falls outside the *.test / *.spec / tests glob set. That spec already sits at 799 of its 800 max-lines budget, so these 13 - stubs cannot move back into it without a max-lines disable. */ import type * as ReactModule from 'react' import { vi } from 'vitest' import { buildTerminalCreateWindow } from './ipc-events-terminal-create-window-test-fixtures' diff --git a/src/renderer/src/hooks/ipc-events-test-harness.ts b/src/renderer/src/hooks/ipc-events-test-harness.ts index 7112d7715c3..915055d3ad5 100644 --- a/src/renderer/src/hooks/ipc-events-test-harness.ts +++ b/src/renderer/src/hooks/ipc-events-test-harness.ts @@ -1,6 +1,3 @@ -/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 8 useIpcEvents specs, not shipped code, and it falls outside - the *.test / *.spec / tests glob set. Inlining these 10 stubs would duplicate them into all 8 specs and push the largest - past the max-lines ratchet. */ import { vi } from 'vitest' import type * as ReactModule from 'react' import type { HarnessStoreState } from './ipc-events-harness-store-state' diff --git a/src/renderer/src/hooks/useSkillFreshness.test.tsx b/src/renderer/src/hooks/useSkillFreshness.test.tsx index d74a716a22a..e3182a55e22 100644 --- a/src/renderer/src/hooks/useSkillFreshness.test.tsx +++ b/src/renderer/src/hooks/useSkillFreshness.test.tsx @@ -61,6 +61,11 @@ function Probe({ return null } +function installBundledPlan(bundledPlan: Window['api']['skills']['bundledPlan']): void { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook calls only skills.bundledPlan on the preload API. + window.api = { skills: { bundledPlan } } as unknown as Window['api'] +} + describe('useSkillFreshness', () => { beforeEach(() => { _skillFreshnessCacheForTests.reset() @@ -90,8 +95,7 @@ describe('useSkillFreshness', () => { .fn() .mockReturnValueOnce(first.promise) .mockReturnValueOnce(second.promise) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a test double implements only the preload methods this code path calls. - window.api = { skills: { bundledPlan } } as never + installBundledPlan(bundledPlan) await act(async () => root?.render()) expect(bundledPlan).toHaveBeenCalledTimes(1) @@ -107,8 +111,7 @@ describe('useSkillFreshness', () => { it('does not scan, subscribe, or refresh while local freshness is disabled', async () => { const bundledPlan = vi.fn().mockResolvedValue(plan(1)) const addEventListener = vi.spyOn(window, 'addEventListener') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a test double implements only the preload methods this code path calls. - window.api = { skills: { bundledPlan } } as never + installBundledPlan(bundledPlan) await act(async () => root?.render()) await act(async () => window.dispatchEvent(new Event('orca:installed-agent-skills-changed'))) @@ -129,8 +132,7 @@ describe('useSkillFreshness', () => { .fn() .mockResolvedValueOnce(plan(1, ['orca-cli'])) .mockReturnValueOnce(second.promise) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a test double implements only the preload methods this code path calls. - window.api = { skills: { bundledPlan } } as never + installBundledPlan(bundledPlan) const renderProbes = (enabled: boolean): void => { root?.render( @@ -170,8 +172,7 @@ describe('useSkillFreshness', () => { .fn() .mockReturnValueOnce(first.promise) .mockReturnValueOnce(second.promise) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a test double implements only the preload methods this code path calls. - window.api = { skills: { bundledPlan } } as never + installBundledPlan(bundledPlan) await act(async () => root?.render()) await act(async () => first.resolve(plan(1))) @@ -194,8 +195,7 @@ describe('useSkillFreshness', () => { .fn() .mockResolvedValueOnce(plan(1, ['orca-cli'])) .mockReturnValueOnce(second.promise) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a test double implements only the preload methods this code path calls. - window.api = { skills: { bundledPlan } } as never + installBundledPlan(bundledPlan) await act(async () => root?.render()) expect(state?.plan?.skills.map((skill) => skill.name)).toEqual(['orca-cli']) @@ -215,8 +215,7 @@ describe('useSkillFreshness', () => { const first = deferred() const second = deferred() const bundledPlan = vi.fn().mockReturnValueOnce(first.promise).mockReturnValue(second.promise) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a test double implements only the preload methods this code path calls. - window.api = { skills: { bundledPlan } } as never + installBundledPlan(bundledPlan) await act(async () => root?.render( @@ -237,8 +236,7 @@ describe('useSkillFreshness', () => { it('publishes a manual refresh to every consumer', async () => { const second = deferred() const bundledPlan = vi.fn().mockResolvedValueOnce(plan(1)).mockReturnValueOnce(second.promise) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a test double implements only the preload methods this code path calls. - window.api = { skills: { bundledPlan } } as never + installBundledPlan(bundledPlan) await act(async () => root?.render( @@ -269,8 +267,7 @@ describe('useSkillFreshness', () => { it('fails closed when an invalidation scan rejects', async () => { const second = deferred() const bundledPlan = vi.fn().mockResolvedValueOnce(plan(1)).mockReturnValueOnce(second.promise) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a test double implements only the preload methods this code path calls. - window.api = { skills: { bundledPlan } } as never + installBundledPlan(bundledPlan) await act(async () => root?.render()) expect(state?.plan?.planDigest).toBe(digest(1)) @@ -288,10 +285,7 @@ describe('useSkillFreshness', () => { it('installs one event-listener pair for multiple consumers and cleans it up', async () => { const addEventListener = vi.spyOn(window, 'addEventListener') const removeEventListener = vi.spyOn(window, 'removeEventListener') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a test double implements only the preload methods this code path calls. - window.api = { - skills: { bundledPlan: vi.fn().mockResolvedValue(plan(1)) } - } as never + installBundledPlan(vi.fn().mockResolvedValue(plan(1))) await act(async () => root?.render( @@ -319,8 +313,7 @@ describe('useSkillFreshness', () => { it('re-plans a new host without ever showing the previous host’s plan', async () => { const bundledPlan = vi.fn().mockResolvedValueOnce(plan(1, ['orca-cli'])) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a test double implements only the preload methods this code path calls. - window.api = { skills: { bundledPlan } } as never + installBundledPlan(bundledPlan) await act(async () => root?.render()) expect(state?.plan?.planDigest).toBe(digest(1)) diff --git a/src/renderer/src/hooks/useSkillFreshness.ts b/src/renderer/src/hooks/useSkillFreshness.ts index 99034530b7d..48746ca6284 100644 --- a/src/renderer/src/hooks/useSkillFreshness.ts +++ b/src/renderer/src/hooks/useSkillFreshness.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useSyncExternalStore } from 'react' +import { useEffect, useEffectEvent, useRef, useSyncExternalStore } from 'react' import type { BundledSkillPlan } from '../../../shared/bundled-skill-plan' import { summarizeBundledSkillPlan } from '../../../shared/bundled-skill-summary' import { bundledSkillTargetKey, type BundledSkillTarget } from '@/lib/bundled-skill-target' @@ -244,9 +244,8 @@ export function useSkillFreshness(target: BundledSkillTarget | null): SkillFresh target ? getSnapshot : getDisabledSnapshot ) - useEffect(() => { - const previousKey = previousKeyRef.current - previousKeyRef.current = targetKey + // Why an event: `targetKey` is the target's identity, so only a key change re-plans. + const planTarget = useEffectEvent((previousKey: string | null) => { if (!target) { return } @@ -258,7 +257,11 @@ export function useSkillFreshness(target: BundledSkillTarget | null): SkillFresh return } ensurePlanLoaded() - // oxlint-disable-next-line react-hooks/exhaustive-deps -- `targetKey` is the target's identity. + }) + useEffect(() => { + const previousKey = previousKeyRef.current + previousKeyRef.current = targetKey + planTarget(previousKey) }, [targetKey]) return { diff --git a/src/renderer/src/hooks/useUnreadDockBadge.ts b/src/renderer/src/hooks/useUnreadDockBadge.ts index 4d66d42d3d4..b4f3f24f091 100644 --- a/src/renderer/src/hooks/useUnreadDockBadge.ts +++ b/src/renderer/src/hooks/useUnreadDockBadge.ts @@ -32,7 +32,6 @@ export function useUnreadDockBadge( const unreadCount = unreadWorkspaceCount + (floatingTerminalHasUnread && !floatingTerminalOpen ? 1 : 0) - // oxlint-disable-next-line react-doctor/no-derived-state-effect -- Why: this syncs an external OS dock badge, not React render state. useEffect(() => { setUnreadDockBadgeCountBestEffort(unreadCount) }, [unreadCount]) diff --git a/src/renderer/src/hooks/useVirtualizedScrollAnchor.listener-deps.test.ts b/src/renderer/src/hooks/useVirtualizedScrollAnchor.listener-deps.test.ts index 1c48d6456ff..e071e5651e6 100644 --- a/src/renderer/src/hooks/useVirtualizedScrollAnchor.listener-deps.test.ts +++ b/src/renderer/src/hooks/useVirtualizedScrollAnchor.listener-deps.test.ts @@ -101,7 +101,6 @@ describe('useVirtualizedScrollAnchor listener effect dependencies', () => { } harness.beginRender() - // oxlint-disable-next-line react-hooks/rules-of-hooks -- test harness mocks React's hook dispatcher directly. useVirtualizedScrollAnchor({ anchorRef, getRowKey: (row) => row, @@ -152,7 +151,6 @@ describe('useVirtualizedScrollAnchor listener effect dependencies', () => { } harness.beginRender() - // oxlint-disable-next-line react-hooks/rules-of-hooks -- test harness mocks React's hook dispatcher directly. useVirtualizedScrollAnchor({ anchorRef, getRowKey: (row) => row, diff --git a/src/renderer/src/hooks/useVirtualizedScrollAnchor.marks-restore-signal.test.ts b/src/renderer/src/hooks/useVirtualizedScrollAnchor.marks-restore-signal.test.ts index 6c9875ee11a..c425d62a16a 100644 --- a/src/renderer/src/hooks/useVirtualizedScrollAnchor.marks-restore-signal.test.ts +++ b/src/renderer/src/hooks/useVirtualizedScrollAnchor.marks-restore-signal.test.ts @@ -253,7 +253,6 @@ describe('useVirtualizedScrollAnchor with marks + restoreSignal', () => { const virtualizer = virtualizerWithRow1() harness.beginRender() - // oxlint-disable-next-line react-hooks/rules-of-hooks -- test harness mocks React's hook dispatcher directly. useVirtualizedScrollAnchor({ anchorRef, getItemElementKey: (element: FakeRowElement) => element.key, @@ -288,7 +287,6 @@ describe('useVirtualizedScrollAnchor with marks + restoreSignal', () => { const virtualizer = virtualizerWithRow1() harness.beginRender() - // oxlint-disable-next-line react-hooks/rules-of-hooks -- test harness mocks React's hook dispatcher directly. useVirtualizedScrollAnchor({ anchorRef, getItemElementKey: (element: FakeRowElement) => element.key, @@ -314,7 +312,6 @@ describe('useVirtualizedScrollAnchor with marks + restoreSignal', () => { const anchorRef = { current: null } harness.beginRender() - // oxlint-disable-next-line react-hooks/rules-of-hooks -- test harness mocks React's hook dispatcher directly. useVirtualizedScrollAnchor({ anchorRef, getRowKey: (row: string) => row, @@ -476,7 +473,6 @@ describe('useVirtualizedScrollAnchor with marks + restoreSignal', () => { const scrollOffsetRef = { current: 4_000 } harness.beginRender() - // oxlint-disable-next-line react-hooks/rules-of-hooks -- test harness mocks React's hook dispatcher directly. useVirtualizedScrollAnchor({ anchorRef, getRowKey: (row: string) => row, @@ -515,7 +511,6 @@ describe('useVirtualizedScrollAnchor with marks + restoreSignal', () => { const anchorRef = { current: null } harness.beginRender() - // oxlint-disable-next-line react-hooks/rules-of-hooks -- test harness mocks React's hook dispatcher directly. useVirtualizedScrollAnchor({ anchorRef, getRowKey: (row: string) => row, diff --git a/src/renderer/src/lib/execution-host-facts.test.ts b/src/renderer/src/lib/execution-host-facts.test.ts index 69e703e18a5..46824937b48 100644 --- a/src/renderer/src/lib/execution-host-facts.test.ts +++ b/src/renderer/src/lib/execution-host-facts.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import type { RuntimeEnvironmentStatus } from '../../../shared/runtime-host-status' import { resolveRepoWorktreeLaunchPlatform } from './execution-host-facts' +import { getDefaultSettings } from '../../../shared/constants' vi.mock('./new-workspace', async (importOriginal) => ({ ...(await importOriginal>()), @@ -39,8 +40,7 @@ function state(): State { projects: [], activeRepoId: null, activeWorktreeId: null, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads no other setting. - settings: {} as State['settings'], + settings: getDefaultSettings('/tmp'), sshConnectionStates: new Map(), sshStateByEnvironment: new Map(), runtimeStatusByEnvironmentId: linuxServer() diff --git a/src/renderer/src/lib/host-mirror-handle-gap-drain.test.ts b/src/renderer/src/lib/host-mirror-handle-gap-drain.test.ts index ab67ff19931..ed27f5c988d 100644 --- a/src/renderer/src/lib/host-mirror-handle-gap-drain.test.ts +++ b/src/renderer/src/lib/host-mirror-handle-gap-drain.test.ts @@ -8,6 +8,7 @@ import { parkUntilHostMirrorHandleLands, resetHostMirrorHandleGapWaitsForTests } from './host-mirror-handle-gap-wait' +import { makeTerminalTab } from '../store/slices/worktrees-slice-test-fixtures' // What this file pins, and why it is separate from host-mirror-handle-gap-resume.test.ts: that file // drives the waiter through the real resume sweep, so it cannot choose what a replay DOES. These @@ -33,13 +34,12 @@ function seedRows(): void { // Layout bindings are seeded because a verdict names the PANE by the environment-minted PTY it // held at park time. A pane with no binding never reaches the park path in production, and its // verdict deliberately refuses to answer, so a fixture without one models nothing real. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ ptyIdsByTabId: {}, tabsByWorktree: { [WORKTREE_ID]: [ - { id: FIRST_TAB_ID, title: 'one' }, - { id: SECOND_TAB_ID, title: 'two' } + makeTerminalTab({ id: FIRST_TAB_ID, worktreeId: WORKTREE_ID, title: 'one' }), + makeTerminalTab({ id: SECOND_TAB_ID, worktreeId: WORKTREE_ID, title: 'two' }) ] }, terminalLayoutsByTabId: { @@ -56,18 +56,17 @@ function seedRows(): void { ptyIdsByLeafId: { 'leaf-2': `remote:${ENVIRONMENT_ID}@@term_2` } } } - } as never) + }) } /** The host publishes both panes' PTY handles on one frame: both waiters come due together. */ function publishBothHandles(): void { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ ptyIdsByTabId: { [FIRST_TAB_ID]: [`remote:${ENVIRONMENT_ID}@@term_1`], [SECOND_TAB_ID]: [`remote:${ENVIRONMENT_ID}@@term_2`] } - } as never) + }) } describe('host-mirror handle-gap drain', () => { @@ -158,15 +157,14 @@ describe('host-mirror handle-gap drain', () => { it('does not release on retraction evidence a mid-drain adoption has already made stale', () => { const adoptedReplay = vi.fn() const ADOPTING_WORKTREE_ID = 'repo-1::/workspace/adopted' - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ tabsByWorktree: { [ADOPTING_WORKTREE_ID]: [ - { id: FIRST_TAB_ID, title: 'one' }, - { id: SECOND_TAB_ID, title: 'two' } + makeTerminalTab({ id: FIRST_TAB_ID, worktreeId: ADOPTING_WORKTREE_ID, title: 'one' }), + makeTerminalTab({ id: SECOND_TAB_ID, worktreeId: ADOPTING_WORKTREE_ID, title: 'two' }) ] } - } as never) + }) // Parked first so the drain reaches it first: `Map` preserves insertion order, and this pane's // replay is what makes the next entry's snapshot verdict stale. If that order ever inverted the // test would fail rather than pass quietly — the second pane would release before the adoption. @@ -185,10 +183,9 @@ describe('host-mirror handle-gap drain', () => { // The frame that starts the drain. The second tab is absent from the worktree its waiter is // filed under, so the snapshot reads retraction — evidence the adoption above makes obsolete // before the release loop reaches it. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ ptyIdsByTabId: { [FIRST_TAB_ID]: [`remote:${ENVIRONMENT_ID}@@term_1`] } - } as never) + }) expect(adoptedReplay).not.toHaveBeenCalled() expect(countParkedHostMirrorHandleGapPanesForTests()).toBe(1) diff --git a/src/renderer/src/lib/host-mirror-handle-gap-landed-handle.test.ts b/src/renderer/src/lib/host-mirror-handle-gap-landed-handle.test.ts index c031ec1b052..7f1e85b1d07 100644 --- a/src/renderer/src/lib/host-mirror-handle-gap-landed-handle.test.ts +++ b/src/renderer/src/lib/host-mirror-handle-gap-landed-handle.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { useAppStore, type AppState } from '@/store' +import { useAppStore } from '@/store' import { clearRuntimeEnvironmentConnectionGenerationsForTests, setRuntimeEnvironmentConnectionGenerationForTests @@ -11,6 +11,7 @@ import { parkUntilHostMirrorHandleLands, resetHostMirrorHandleGapWaitsForTests } from './host-mirror-handle-gap-wait' +import { makeTerminalTab } from '../store/slices/worktrees-slice-test-fixtures' /** * The fourth eviction trigger: a PUBLISHED HANDLE ends the gap episode its verdict measured. @@ -39,9 +40,10 @@ const initialAppStoreState = useAppStore.getState() /** Publishes the row AND the layout binding that makes the pane unverifiable rather than dead. */ function publishRow(options: { handleLanded: boolean }): void { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ - tabsByWorktree: { [WORKTREE]: [{ id: TAB_ID, title: 't', ptyId: null }] }, + tabsByWorktree: { + [WORKTREE]: [makeTerminalTab({ id: TAB_ID, worktreeId: WORKTREE, title: 't' })] + }, terminalLayoutsByTabId: { [TAB_ID]: { root: { type: 'leaf', leafId: 'leaf-1' }, @@ -51,7 +53,7 @@ function publishRow(options: { handleLanded: boolean }): void { } }, ptyIdsByTabId: options.handleLanded ? { [TAB_ID]: [PANE_PTY_ID] } : {} - } as unknown as AppState) + }) } describe('handle-gap verdict, landed-handle eviction', () => { @@ -102,27 +104,26 @@ describe('handle-gap verdict, landed-handle eviction', () => { // Adopting an orphaned terminal re-keys `tabsByWorktree` without re-keying the record, so the // re-park hands the live wait a new worktree. Retraction evidence about the OLD one says // nothing about the wait that is actually running. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ tabsByWorktree: { - 'wt-old': [{ id: TAB_ID, title: 't', ptyId: null }], - 'wt-new': [{ id: TAB_ID, title: 't', ptyId: null }] + 'wt-old': [makeTerminalTab({ id: TAB_ID, worktreeId: 'wt-old', title: 't' })], + 'wt-new': [makeTerminalTab({ id: TAB_ID, worktreeId: 'wt-new', title: 't' })] }, ptyIdsByTabId: {} - } as unknown as AppState) + }) parkUntilHostMirrorHandleLands(ENV_ID, 'wt-old', TAB_ID, vi.fn()) const replayAfterAdoption = vi.fn() parkUntilHostMirrorHandleLands(ENV_ID, 'wt-new', TAB_ID, replayAfterAdoption) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ - tabsByWorktree: { 'wt-new': [{ id: TAB_ID, title: 't', ptyId: null }] } - } as unknown as AppState) + tabsByWorktree: { + 'wt-new': [makeTerminalTab({ id: TAB_ID, worktreeId: 'wt-new', title: 't' })] + } + }) expect(replayAfterAdoption).not.toHaveBeenCalled() expect(countParkedHostMirrorHandleGapPanesForTests()).toBe(1) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. - useAppStore.setState({ tabsByWorktree: {} } as unknown as AppState) + useAppStore.setState({ tabsByWorktree: {} }) expect(replayAfterAdoption).toHaveBeenCalledTimes(1) }) }) diff --git a/src/renderer/src/lib/host-mirror-handle-gap-resume.test.ts b/src/renderer/src/lib/host-mirror-handle-gap-resume.test.ts index 908ebc5d525..cfc0054ddd6 100644 --- a/src/renderer/src/lib/host-mirror-handle-gap-resume.test.ts +++ b/src/renderer/src/lib/host-mirror-handle-gap-resume.test.ts @@ -24,6 +24,7 @@ import { hasHostMirrorHandleWaitExpired, resetHostMirrorHandleGapWaitsForTests } from './host-mirror-handle-gap-wait' +import { makeTerminalTab } from '../store/slices/worktrees-slice-test-fixtures' // The window this pins: a paired runtime publishes a workspace's tab rows and its PTY handles on // separate frames, so there is a frame where the row exists and `ptyIdsByTabId` is still empty. @@ -72,17 +73,15 @@ function seedMirroredWorkspace(worktree: ReturnType ): void { seedMirroredWorkspace(worktree) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ terminalLayoutsByTabId: { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture carries the fields this suite drives; the cast only supplies the rest of the declared shape. [WEB_TAB_ID]: { root: { type: 'split', - direction: 'row', + direction: 'vertical', first: { type: 'leaf', leafId: LEAF_ID }, second: { type: 'leaf', leafId: SIBLING_LEAF_ID } }, @@ -115,35 +111,32 @@ function seedSplitTabWithOnlySiblingReady( expandedLeafId: null, // Only the ready sibling is bound; the record's leaf has never held a handle. ptyIdsByLeafId: { [SIBLING_LEAF_ID]: 'remote:env-handle-gap@@term_sibling' } - } as never + } }, ptyIdsByTabId: { [WEB_TAB_ID]: ['remote:env-handle-gap@@term_sibling'] } - } as never) + }) } /** A second published mirrored row in the same environment, with its own leaf binding. */ function seedSecondMirroredPane(worktreeId: string): void { const before = useAppStore.getState() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ tabsByWorktree: { [worktreeId]: [ ...(before.tabsByWorktree[worktreeId] ?? []), - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the literal names every field this suite reads; the cast only supplies the rest of the declared shape. - { id: SECOND_TAB_ID, title: 'Claude 2', ptyId: null } as never + makeTerminalTab({ id: SECOND_TAB_ID, worktreeId, title: 'Claude 2' }) ] }, terminalLayoutsByTabId: { ...before.terminalLayoutsByTabId, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture carries the fields this suite drives; the cast only supplies the rest of the declared shape. [SECOND_TAB_ID]: { root: { type: 'leaf', leafId: SECOND_LEAF_ID }, activeLeafId: SECOND_LEAF_ID, expandedLeafId: null, ptyIdsByLeafId: { [SECOND_LEAF_ID]: 'remote:env-handle-gap@@term_2' } - } as never + } } - } as never) + }) } /** The capture the reported flow produces: recorded mid-turn, so it is active work, not history. */ @@ -154,7 +147,6 @@ function seedActiveSleepingRecordFor( sessionId: string ): string { const paneKey = makePaneKey(tabId, leafId) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ sleepingAgentSessionsByPaneKey: { ...useAppStore.getState().sleepingAgentSessionsByPaneKey, @@ -173,7 +165,7 @@ function seedActiveSleepingRecordFor( origin: 'live' } } - } as never) + }) return paneKey } @@ -324,18 +316,16 @@ describe('resume across the mirror handle gap', () => { const worktree = makeRuntimeOwnedWorktree() seedMirroredWorkspace(worktree) seedActiveSleepingRecord(worktree.id) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ terminalLayoutsByTabId: { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture carries the fields this suite drives; the cast only supplies the rest of the declared shape. [WEB_TAB_ID]: { root: { type: 'leaf', leafId: LEAF_ID }, activeLeafId: LEAF_ID, expandedLeafId: null, ptyIdsByLeafId: {} - } as never + } } - } as never) + }) markHostSessionMirrorHydrated(RUNTIME_ENV_ID) expect(resumeSleepingAgentSessionsForWorktree(worktree.id)).toBe(0) @@ -633,8 +623,7 @@ describe('resume across the mirror handle gap', () => { expect(resumeSleepingAgentSessionsForWorktree(worktree.id)).toBe(0) // Teardown drops every row the environment owned. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. - useAppStore.setState({ tabsByWorktree: {}, terminalLayoutsByTabId: {} } as never) + useAppStore.setState({ tabsByWorktree: {}, terminalLayoutsByTabId: {} }) expect(countParkedHostMirrorHandleGapPanesForTests()).toBe(0) // Nothing may still be scheduled against the torn-down environment. diff --git a/src/renderer/src/lib/host-mirror-handle-gap-subscription-lifetime.test.ts b/src/renderer/src/lib/host-mirror-handle-gap-subscription-lifetime.test.ts index 9882b9ed979..a00c9d49027 100644 --- a/src/renderer/src/lib/host-mirror-handle-gap-subscription-lifetime.test.ts +++ b/src/renderer/src/lib/host-mirror-handle-gap-subscription-lifetime.test.ts @@ -9,6 +9,7 @@ import { parkUntilHostMirrorHandleLands, resetHostMirrorHandleGapWaitsForTests } from './host-mirror-handle-gap-wait' +import { makeTerminalTab } from '../store/slices/worktrees-slice-test-fixtures' // The retention suite that the reconciled verdict loop replaced carried one assertion the split // suites did not: the store subscription is held for exactly as long as something needs it. @@ -43,11 +44,13 @@ let subscribeCalls: number function publishPaneAndPark(environmentId: string, tabId: string): void { const state = useAppStore.getState() const published = state.tabsByWorktree[WORKTREE_ID] ?? [] - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ ptyIdsByTabId: {}, tabsByWorktree: { - [WORKTREE_ID]: [...published.filter((tab) => tab.id !== tabId), { id: tabId, title: tabId }] + [WORKTREE_ID]: [ + ...published.filter((tab) => tab.id !== tabId), + makeTerminalTab({ id: tabId, worktreeId: WORKTREE_ID, title: tabId }) + ] }, terminalLayoutsByTabId: { ...state.terminalLayoutsByTabId, @@ -58,16 +61,15 @@ function publishPaneAndPark(environmentId: string, tabId: string): void { ptyIdsByLeafId: { [`leaf-${tabId}`]: `remote:${environmentId}@@term_${tabId}` } } } - } as never) + }) parkUntilHostMirrorHandleLands(environmentId, WORKTREE_ID, tabId, () => {}) } /** Lands the pane's handle, which is what both releases a waiter and retires a verdict. */ function landHandle(tabId: string): void { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ ptyIdsByTabId: { ...useAppStore.getState().ptyIdsByTabId, [tabId]: [`pty-${tabId}`] } - } as never) + }) } describe('host-mirror handle-gap store subscription lifetime', () => { @@ -78,15 +80,14 @@ describe('host-mirror handle-gap store subscription lifetime', () => { unsubscribeCalls = 0 subscribeCalls = 0 const realSubscribe = useAppStore.subscribe.bind(useAppStore) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the subscriber is invoked with the store state pair; the narrowed listener type is what this suite asserts on. - vi.spyOn(useAppStore, 'subscribe').mockImplementation(((listener: never) => { + vi.spyOn(useAppStore, 'subscribe').mockImplementation((listener) => { subscribeCalls += 1 const unsubscribe = realSubscribe(listener) return () => { unsubscribeCalls += 1 unsubscribe() } - }) as never) + }) }) afterEach(() => { diff --git a/src/renderer/src/lib/host-mirror-handle-gap-teardown.test.ts b/src/renderer/src/lib/host-mirror-handle-gap-teardown.test.ts index 1e769707fa1..9eeabcd34bf 100644 --- a/src/renderer/src/lib/host-mirror-handle-gap-teardown.test.ts +++ b/src/renderer/src/lib/host-mirror-handle-gap-teardown.test.ts @@ -14,6 +14,7 @@ import { parkUntilHostMirrorHandleLands, resetHostMirrorHandleGapWaitsForTests } from './host-mirror-handle-gap-wait' +import { makeTerminalTab } from '../store/slices/worktrees-slice-test-fixtures' // The orphan class no recording-driven prune can reach. Both existing rules — stale generation and // tab death — run only when a verdict is RECORDED, so an environment that is removed and never @@ -31,11 +32,13 @@ function parkAndExpire(environmentId: string, tabId: string): void { // suite is about a class no recording-driven prune can reach, so every pane here stays live. const state = useAppStore.getState() const published = state.tabsByWorktree[WORKTREE_ID] ?? [] - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ ptyIdsByTabId: {}, tabsByWorktree: { - [WORKTREE_ID]: [...published.filter((tab) => tab.id !== tabId), { id: tabId, title: tabId }] + [WORKTREE_ID]: [ + ...published.filter((tab) => tab.id !== tabId), + makeTerminalTab({ id: tabId, worktreeId: WORKTREE_ID, title: tabId }) + ] }, // A verdict names its PANE by the environment-minted PTY held at park time, so a fixture with // no layout binding records '' and the verdict refuses to answer. Bind per environment: one @@ -49,7 +52,7 @@ function parkAndExpire(environmentId: string, tabId: string): void { ptyIdsByLeafId: { [`leaf-${tabId}`]: `remote:${environmentId}@@term_${tabId}` } } } - } as never) + }) parkUntilHostMirrorHandleLands(environmentId, WORKTREE_ID, tabId, () => {}) vi.advanceTimersByTime(HOST_MIRROR_HANDLE_GAP_DEADLINE_MS) } @@ -87,11 +90,14 @@ describe('host-mirror handle-gap verdicts across environment teardown', () => { // silently drop a parked resume sweep that nothing else will replay. it('leaves a parked waiter alone, cancelling only the verdicts', () => { const replay = vi.fn() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ ptyIdsByTabId: {}, - tabsByWorktree: { [WORKTREE_ID]: [{ id: 'web-terminal-host-tab-9', title: 'nine' }] } - } as never) + tabsByWorktree: { + [WORKTREE_ID]: [ + makeTerminalTab({ id: 'web-terminal-host-tab-9', worktreeId: WORKTREE_ID, title: 'nine' }) + ] + } + }) parkUntilHostMirrorHandleLands(ENVIRONMENT_ID, WORKTREE_ID, 'web-terminal-host-tab-9', replay) clearHostMirrorHandleGapVerdictsForEnvironment(ENVIRONMENT_ID) diff --git a/src/renderer/src/lib/host-mirror-handle-gap-verdict-union.test.ts b/src/renderer/src/lib/host-mirror-handle-gap-verdict-union.test.ts index d1cb35be2fa..9338c72f3d4 100644 --- a/src/renderer/src/lib/host-mirror-handle-gap-verdict-union.test.ts +++ b/src/renderer/src/lib/host-mirror-handle-gap-verdict-union.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { useAppStore, type AppState } from '@/store' +import { useAppStore } from '@/store' import { clearRuntimeEnvironmentConnectionGenerationsForTests, setRuntimeEnvironmentConnectionGenerationForTests @@ -12,6 +12,8 @@ import { parkUntilHostMirrorHandleLands, resetHostMirrorHandleGapWaitsForTests } from './host-mirror-handle-gap-wait' +import { makeTerminalTab } from '../store/slices/worktrees-slice-test-fixtures' +import type { TerminalLayoutSnapshot } from '../../../shared/terminal-tab-types' /** * The UNION suite for `expiredGenerationByPane`. @@ -52,7 +54,7 @@ const ENV_OF_TAB: Record = { /** Publishes rows AND the layout PTY binding each pane holds — the binding is the pane's identity. */ function setLiveTabs(tabIds: string[], ptyByTabId: Record = {}): void { - const layouts: Record = {} + const layouts: Record = {} for (const id of tabIds) { const ptyId = ptyByTabId[id] ?? `remote:${ENV_OF_TAB[id] ?? ENV_A}@@term_${id}` layouts[id] = { @@ -62,12 +64,13 @@ function setLiveTabs(tabIds: string[], ptyByTabId: Record = {}): ptyIdsByLeafId: { [`leaf-${id}`]: ptyId } } } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ - tabsByWorktree: { [WORKTREE]: tabIds.map((id) => ({ id, title: id, ptyId: null })) }, + tabsByWorktree: { + [WORKTREE]: tabIds.map((id) => makeTerminalTab({ id, worktreeId: WORKTREE, title: id })) + }, terminalLayoutsByTabId: layouts, ptyIdsByTabId: {} - } as unknown as AppState) + }) } function parkAndExpire(environmentId: string, tabId: string): void { diff --git a/src/renderer/src/lib/keyboard-layout/option-as-alt-probe.test.ts b/src/renderer/src/lib/keyboard-layout/option-as-alt-probe.test.ts index 05b777b2254..68311b71c27 100644 --- a/src/renderer/src/lib/keyboard-layout/option-as-alt-probe.test.ts +++ b/src/renderer/src/lib/keyboard-layout/option-as-alt-probe.test.ts @@ -75,6 +75,11 @@ function makeMockWindow(initial: LayoutMapLike | null, userAgent = 'Linux'): Moc } } +function asWindow(mock: MockWindow): Window { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the probe reads only navigator and the focus listeners, which MockWindow supplies. + return mock as unknown as Window +} + describe('createOptionAsAltProbe', () => { beforeEach(() => { vi.restoreAllMocks() @@ -96,8 +101,7 @@ describe('createOptionAsAltProbe', () => { vi.stubGlobal('window', { api: { app: { getKeyboardLayoutSnapshot, getKeyboardInputSourceId } } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mock supplies every Window member the probe reads. - const probe = createOptionAsAltProbe(makeMockWindow(US_MAP, 'Macintosh') as unknown as Window) + const probe = createOptionAsAltProbe(asWindow(makeMockWindow(US_MAP, 'Macintosh'))) await probe.refresh() @@ -110,7 +114,7 @@ describe('createOptionAsAltProbe', () => { it('starts as unknown, upgrades after first probe resolves', async () => { const win = makeMockWindow(US_MAP) - const probe = createOptionAsAltProbe(win as unknown as Window) + const probe = createOptionAsAltProbe(asWindow(win)) expect(probe.getCurrent()).toBe('unknown') await probe.refresh() expect(probe.getCurrent()).toBe('us') @@ -119,7 +123,7 @@ describe('createOptionAsAltProbe', () => { it('detects non-US layout (Turkish)', async () => { const win = makeMockWindow(TURKISH_MAP) - const probe = createOptionAsAltProbe(win as unknown as Window) + const probe = createOptionAsAltProbe(asWindow(win)) await probe.refresh() expect(probe.getCurrent()).toBe('non-us') probe.dispose() @@ -127,7 +131,7 @@ describe('createOptionAsAltProbe', () => { it('notifies subscribers when category changes', async () => { const win = makeMockWindow(US_MAP) - const probe = createOptionAsAltProbe(win as unknown as Window) + const probe = createOptionAsAltProbe(asWindow(win)) const listener = vi.fn() probe.subscribe(listener) await probe.refresh() @@ -137,7 +141,7 @@ describe('createOptionAsAltProbe', () => { it('does not notify when category is unchanged', async () => { const win = makeMockWindow(US_MAP) - const probe = createOptionAsAltProbe(win as unknown as Window) + const probe = createOptionAsAltProbe(asWindow(win)) await probe.refresh() const listener = vi.fn() probe.subscribe(listener) @@ -152,7 +156,7 @@ describe('createOptionAsAltProbe', () => { const win = makeMockWindow(US_MAP) win.navigator.keyboard = { getLayoutMap: async () => active } - const probe = createOptionAsAltProbe(win as unknown as Window) + const probe = createOptionAsAltProbe(asWindow(win)) await probe.refresh() expect(probe.getCurrent()).toBe('us') @@ -169,7 +173,7 @@ describe('createOptionAsAltProbe', () => { let activeInputSourceId = 'com.apple.keylayout.ABC' let notifyLayoutChanged: (() => void) | undefined const unsubscribe = vi.fn() - const probe = createOptionAsAltProbe(makeMockWindow(US_MAP) as unknown as Window, { + const probe = createOptionAsAltProbe(asWindow(makeMockWindow(US_MAP)), { readInputSourceId: async () => activeInputSourceId, subscribeKeyboardLayoutChanged: (callback) => { notifyLayoutChanged = callback @@ -214,7 +218,7 @@ describe('createOptionAsAltProbe', () => { .fn<() => Promise>() .mockReturnValueOnce(oldRead) .mockResolvedValue('com.apple.keylayout.PolishPro') - const probe = createOptionAsAltProbe(makeMockWindow(US_MAP) as unknown as Window, { + const probe = createOptionAsAltProbe(asWindow(makeMockWindow(US_MAP)), { readInputSourceId, subscribeKeyboardLayoutChanged: (callback) => { notifyLayoutChanged = callback @@ -239,7 +243,7 @@ describe('createOptionAsAltProbe', () => { let notifyLayoutChanged: ((event: KeyboardLayoutChangeEvent) => void) | undefined const readInputSourceId = vi.fn(async () => 'com.apple.keylayout.US') const win = makeMockWindow(US_MAP) - const probe = createOptionAsAltProbe(win as unknown as Window, { + const probe = createOptionAsAltProbe(asWindow(win), { readInputSourceId, subscribeKeyboardLayoutChanged: (callback) => { notifyLayoutChanged = callback @@ -266,7 +270,7 @@ describe('createOptionAsAltProbe', () => { it('stays unknown if navigator.keyboard is unavailable', async () => { const win = makeMockWindow(null) - const probe = createOptionAsAltProbe(win as unknown as Window) + const probe = createOptionAsAltProbe(asWindow(win)) await probe.refresh() expect(probe.getCurrent()).toBe('unknown') probe.dispose() @@ -274,7 +278,7 @@ describe('createOptionAsAltProbe', () => { it('survives a rejected getLayoutMap without clobbering last-known value', async () => { const win = makeMockWindow(US_MAP) - const probe = createOptionAsAltProbe(win as unknown as Window) + const probe = createOptionAsAltProbe(asWindow(win)) await probe.refresh() expect(probe.getCurrent()).toBe('us') @@ -291,7 +295,7 @@ describe('createOptionAsAltProbe', () => { it('dispose removes focus listener', async () => { const win = makeMockWindow(US_MAP) - const probe = createOptionAsAltProbe(win as unknown as Window) + const probe = createOptionAsAltProbe(asWindow(win)) await probe.refresh() const listener = vi.fn() probe.subscribe(listener) @@ -310,7 +314,7 @@ describe('createOptionAsAltProbe', () => { 'com.apple.keylayout.PolishPro' ]) { const win = makeMockWindow(US_MAP) - const probe = createOptionAsAltProbe(win as unknown as Window, { + const probe = createOptionAsAltProbe(asWindow(win), { readInputSourceId: async () => id }) await probe.refresh() @@ -323,8 +327,7 @@ describe('createOptionAsAltProbe', () => { 'resolves to us for standard %s without a browser layout map', async (id) => { const win = makeMockWindow(null, 'Macintosh') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mock supplies every Window member the probe reads. - const probe = createOptionAsAltProbe(win as unknown as Window, { + const probe = createOptionAsAltProbe(asWindow(win), { readInputSourceId: async () => `com.apple.keylayout.${id}` }) await probe.refresh() @@ -337,8 +340,7 @@ describe('createOptionAsAltProbe', () => { 'uses the fingerprint without native identity on %s', async (userAgent) => { const win = makeMockWindow(US_MAP, userAgent) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mock supplies every Window member the probe reads. - const probe = createOptionAsAltProbe(win as unknown as Window, { + const probe = createOptionAsAltProbe(asWindow(win), { readInputSourceId: async () => null }) await probe.refresh() @@ -357,8 +359,7 @@ describe('createOptionAsAltProbe', () => { } return null }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mock supplies every Window member the probe reads. - const probe = createOptionAsAltProbe(win as unknown as Window, { readInputSourceId }) + const probe = createOptionAsAltProbe(asWindow(win), { readInputSourceId }) await probe.refresh() expect(probe.getCurrent()).toBe('unknown') expect(effectiveMacOptionAsAlt('auto', probe.getCurrent())).toBe('false') @@ -373,8 +374,7 @@ describe('createOptionAsAltProbe', () => { it('stays conservative on macOS without either native identity API', async () => { vi.stubGlobal('window', { api: { app: {} } }) const win = makeMockWindow(US_MAP, 'Macintosh') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mock supplies every Window member the probe reads. - const probe = createOptionAsAltProbe(win as unknown as Window) + const probe = createOptionAsAltProbe(asWindow(win)) await probe.refresh() expect(probe.getCurrent()).toBe('unknown') expect(win.navigator.keyboard?.getLayoutMap).not.toHaveBeenCalled() @@ -384,8 +384,7 @@ describe('createOptionAsAltProbe', () => { it('recovers macOS source identity after losing it without trusting the backing map', async () => { let activeInputSourceId: string | null = 'com.apple.keylayout.ABC' const win = makeMockWindow(US_MAP, 'Macintosh') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mock supplies every Window member the probe reads. - const probe = createOptionAsAltProbe(win as unknown as Window, { + const probe = createOptionAsAltProbe(asWindow(win), { readInputSourceId: async () => activeInputSourceId }) const listener = vi.fn() @@ -412,7 +411,7 @@ describe('createOptionAsAltProbe', () => { it('falls back to the fingerprint off macOS when the input-source reader throws', async () => { const win = makeMockWindow(TURKISH_MAP) - const probe = createOptionAsAltProbe(win as unknown as Window, { + const probe = createOptionAsAltProbe(asWindow(win), { readInputSourceId: async () => { throw new Error('ipc unavailable') } @@ -426,7 +425,7 @@ describe('createOptionAsAltProbe', () => { // The browser fingerprint stays US while the native identity changes. let activeInputSourceId: string | null = 'com.apple.keylayout.ABC' const win = makeMockWindow(US_MAP) - const probe = createOptionAsAltProbe(win as unknown as Window, { + const probe = createOptionAsAltProbe(asWindow(win), { readInputSourceId: async () => activeInputSourceId }) await probe.refresh() @@ -455,7 +454,7 @@ describe('createOptionAsAltProbe', () => { .fn<() => Promise>() .mockReturnValueOnce(oldRead) .mockReturnValueOnce(newRead) - const probe = createOptionAsAltProbe(makeMockWindow(US_MAP) as unknown as Window, { + const probe = createOptionAsAltProbe(asWindow(makeMockWindow(US_MAP)), { readInputSourceId }) const newestProbe = probe.refresh() @@ -476,8 +475,7 @@ describe('createOptionAsAltProbe', () => { let activeRead: Promise = Promise.resolve('com.apple.keylayout.ABC') let notifyLayoutChanged: ((event: KeyboardLayoutChangeEvent) => void) | undefined const readInputSourceId = vi.fn(() => activeRead) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The mock supplies every Window member the probe reads. - const probe = createOptionAsAltProbe(makeMockWindow(US_MAP) as unknown as Window, { + const probe = createOptionAsAltProbe(asWindow(makeMockWindow(US_MAP)), { readInputSourceId, subscribeKeyboardLayoutChanged: (callback) => { notifyLayoutChanged = callback diff --git a/src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-throughput.bench.test.ts b/src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-throughput.bench.test.ts index 77ca783af6c..22cd5dbd7b9 100644 --- a/src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-throughput.bench.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-throughput.bench.test.ts @@ -88,7 +88,6 @@ describe.skipIf(!benchEnabled)('scheduler drain ceiling', () => { it('measures foreground (visible pane flood) and background ceilings', async () => { const foreground = await measure({ foreground: true }) const background = await measure({ foreground: false }) - // eslint-disable-next-line no-console -- bench harness output console.log( `\n[scheduler-ceiling] foreground flood: ${foreground.toFixed(1)} MB/s, background: ${background.toFixed(1)} MB/s (simulated time, instant parse)` ) diff --git a/src/renderer/src/lib/resume-sleeping-agent-session-provider-claim.test.ts b/src/renderer/src/lib/resume-sleeping-agent-session-provider-claim.test.ts index 898f174f04a..31a39556124 100644 --- a/src/renderer/src/lib/resume-sleeping-agent-session-provider-claim.test.ts +++ b/src/renderer/src/lib/resume-sleeping-agent-session-provider-claim.test.ts @@ -1,8 +1,10 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume' +import type { AgentStatusEntry } from '../../../shared/agent-status-types' import { makePaneKey } from '../../../shared/stable-pane-id' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import { getProviderSessionClaimKey } from './sleeping-agent-pane-ownership' -import { useAppStore } from '@/store' +import { useAppStore, type AppState } from '@/store' import { resumeSleepingAgentSessionsForWorktree } from './resume-sleeping-agent-session' // Main's answer to the window's layout commands, as a switch-on build sends them. @@ -43,7 +45,7 @@ function makeRecord( } } -function makeTerminalTab(id: string): Record { +function makeTerminalTab(id: string): TerminalTab { return { id, ptyId: null, @@ -60,7 +62,7 @@ function makeWorkingStatus( paneKey: string, tabId: string, record: SleepingAgentSessionRecord -): Record { +): AgentStatusEntry { return { paneKey, tabId, @@ -75,7 +77,7 @@ function makeWorkingStatus( } } -function makePreservedPaneState(record: SleepingAgentSessionRecord): Record { +function makePreservedPaneState(record: SleepingAgentSessionRecord): Partial { return { activeWorktreeId: 'wt-1', activeTabType: 'terminal', @@ -101,7 +103,7 @@ describe('resume sleeping agent provider claims', () => { ...makePreservedPaneState(record), tabsByWorktree: { 'wt-1': [makeTerminalTab('tab-1')] }, agentStatusByPaneKey: { [paneKey]: makeWorkingStatus(paneKey, 'tab-1', record) } - } as never) + }) expect(resumeSleepingAgentSessionsForWorktree('wt-1')).toBe(0) @@ -122,7 +124,7 @@ describe('resume sleeping agent provider claims', () => { agentStatusByPaneKey: { [otherPaneKey]: makeWorkingStatus(otherPaneKey, 'tab-2', record) } - } as never) + }) expect(resumeSleepingAgentSessionsForWorktree('wt-1')).toBe(0) @@ -141,7 +143,7 @@ describe('resume sleeping agent provider claims', () => { activeTabIdByWorktree: {}, tabsByWorktree: { 'wt-1': [makeTerminalTab('tab-1')] }, agentStatusByPaneKey: { [paneKey]: makeWorkingStatus(paneKey, 'tab-1', record) } - } as never) + }) expect(resumeSleepingAgentSessionsForWorktree('wt-1')).toBe(0) @@ -161,7 +163,6 @@ describe('resume sleeping agent provider claims', () => { const paneKey = makePaneKey('tab-1', LEAF_ID) const peerPaneKey = makePaneKey('tab-peer', OTHER_LEAF_ID) const record = makeRecord(paneKey) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ activeWorktreeId: 'wt-1', activeTabType: 'terminal', @@ -179,7 +180,7 @@ describe('resume sleeping agent provider claims', () => { agentStatusByPaneKey: { [peerPaneKey]: { ...makeWorkingStatus(peerPaneKey, 'tab-peer', record), state: 'done' } } - } as never) + }) expect(resumeSleepingAgentSessionsForWorktree('wt-1')).toBe(0) expect(useAppStore.getState().sleepingAgentSessionsByPaneKey[record.paneKey]).toBeUndefined() @@ -192,7 +193,6 @@ describe('resume sleeping agent provider claims', () => { const paneKey = makePaneKey('tab-1', LEAF_ID) const peerPaneKey = makePaneKey('tab-peer', OTHER_LEAF_ID) const record = makeRecord(paneKey) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ activeWorktreeId: 'wt-1', activeTabType: 'terminal', @@ -215,7 +215,7 @@ describe('resume sleeping agent provider claims', () => { state: 'done' } } - } as never) + }) expect(resumeSleepingAgentSessionsForWorktree('wt-1')).toBe(0) @@ -229,7 +229,6 @@ describe('resume sleeping agent provider claims', () => { const paneKey = makePaneKey('tab-1', LEAF_ID) const peerPaneKey = makePaneKey('tab-peer', OTHER_LEAF_ID) const record = makeRecord(paneKey) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the seeded slice names only the store fields this suite drives; the rest of AppState keeps its defaults. useAppStore.setState({ activeWorktreeId: 'wt-1', activeTabType: 'terminal', @@ -251,7 +250,7 @@ describe('resume sleeping agent provider claims', () => { state: 'done' } } - } as never) + }) expect(resumeSleepingAgentSessionsForWorktree('wt-1')).toBe(1) }) diff --git a/src/renderer/src/lib/worktree-activation-reveal.test.ts b/src/renderer/src/lib/worktree-activation-reveal.test.ts index ea0afbc0a05..1c4c3ff7d1e 100644 --- a/src/renderer/src/lib/worktree-activation-reveal.test.ts +++ b/src/renderer/src/lib/worktree-activation-reveal.test.ts @@ -135,7 +135,6 @@ describe('activateAndRevealWorktree', () => { queueTabInitialCwd: vi.fn(), queueTabIssueCommandSplit: vi.fn(), revealWorktreeInSidebar: vi.fn() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture supplies only the activation slice used by the test. } as never) const result = activateAndRevealWorktree('wt-1') diff --git a/src/renderer/src/runtime/host-session-mirror-frame-fixtures.ts b/src/renderer/src/runtime/host-session-mirror-frame-fixtures.ts index fd3d6cf1a17..9944e1815d6 100644 --- a/src/renderer/src/runtime/host-session-mirror-frame-fixtures.ts +++ b/src/renderer/src/runtime/host-session-mirror-frame-fixtures.ts @@ -2,6 +2,8 @@ import { vi, type Mock } from 'vitest' import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types' import { toWebTerminalSurfaceTabId } from '../../../shared/terminal-surface-id' import type { AppState } from '@/store' +import { makeTerminalTab } from '@/store/slices/worktrees-slice-test-fixtures' +import type { TerminalTab } from '../../../shared/terminal-tab-types' /** Shared with the frame-ordering suites through their `vi.mock` factories, so * this module must stay free of runtime imports that would form a cycle. */ @@ -130,6 +132,6 @@ export function makePtylessHostSnapshot( return snapshot as never } -export function mirrorTabRow(tabId: string, worktreeId: string): unknown { - return { id: tabId, title: 'Codex', ptyId: null, worktreeId } +export function mirrorTabRow(tabId: string, worktreeId: string): TerminalTab { + return makeTerminalTab({ id: tabId, worktreeId, title: 'Codex' }) } diff --git a/src/renderer/src/runtime/host-session-mirror-frame-ordering-harness.ts b/src/renderer/src/runtime/host-session-mirror-frame-ordering-test-harness.ts similarity index 92% rename from src/renderer/src/runtime/host-session-mirror-frame-ordering-harness.ts rename to src/renderer/src/runtime/host-session-mirror-frame-ordering-test-harness.ts index 47af0c7b37e..424de9fcf33 100644 --- a/src/renderer/src/runtime/host-session-mirror-frame-ordering-harness.ts +++ b/src/renderer/src/runtime/host-session-mirror-frame-ordering-test-harness.ts @@ -1,9 +1,9 @@ import { act, cleanup } from '@testing-library/react' import { afterEach, beforeEach, expect, vi } from 'vitest' -import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments' +import { getDefaultSettings } from '../../../shared/constants' import { makePaneKey } from '../../../shared/stable-pane-id' import { resetStaleDocumentVisibilityForTesting } from '@/components/terminal-pane/stale-document-visibility' -import { useAppStore, type AppState } from '@/store' +import { useAppStore } from '@/store' import { clearRuntimeEnvironmentConnectionGenerationsForTests, setRuntimeEnvironmentConnectionGenerationForTests @@ -28,6 +28,10 @@ import { } from './host-session-mirror-hydration' import { replaceRuntimeEnvironmentRevisions } from './runtime-environment-revision' import { resetWebSessionTabsSnapshotFreshnessForTests } from './web-session-tabs-sync/tracking-lifecycle' +import { + connectedRuntimeStatus, + publicRuntimeEnvironment +} from './paired-runtime-environment-test-fixture' const initialState = useAppStore.getInitialState() @@ -53,8 +57,8 @@ export async function settle(): Promise { export function seedState(): void { const runtimeEnvironments = [ - { id: ENV, createdAt: 100, pairingRevision: REVISION } - ] as PublicKnownRuntimeEnvironment[] + publicRuntimeEnvironment({ id: ENV, createdAt: 100, pairingRevision: REVISION }) + ] replaceRuntimeEnvironmentRevisions(runtimeEnvironments) const worktrees = [ makeWorktree(WT, '/workspace/feature'), @@ -78,9 +82,7 @@ export function seedState(): void { activeView: 'terminal', workspaceSessionReady: true, runtimeEnvironments, - runtimeStatusByEnvironmentId: new Map([ - [ENV, { status: { runtimeId: 'runtime-a' }, connectionGeneration: 1 }] - ]) as AppState['runtimeStatusByEnvironmentId'], + runtimeStatusByEnvironmentId: new Map([[ENV, connectedRuntimeStatus('runtime-a', 1)]]), // Mirror rows the host published before the app restarted; no live PTY // handles yet, which is precisely why liveness is unknowable right now. tabsByWorktree: { @@ -92,16 +94,18 @@ export function seedState(): void { [MIRROR_TAB_ID]: { root: { type: 'leaf', leafId: LEAF_ID }, activeLeafId: LEAF_ID, + expandedLeafId: null, ptyIdsByLeafId: { [LEAF_ID]: 'pty-host-old-1' } }, [BG_MIRROR_TAB_ID]: { root: { type: 'leaf', leafId: LEAF_ID }, activeLeafId: LEAF_ID, + expandedLeafId: null, ptyIdsByLeafId: { [LEAF_ID]: 'pty-host-old-2' } } }, - settings: { agentCmdOverrides: {} } - } as never, + settings: { ...getDefaultSettings('/tmp'), agentCmdOverrides: {} } + }, true ) } diff --git a/src/renderer/src/runtime/host-session-mirror-hydration-frame-ordering.test.tsx b/src/renderer/src/runtime/host-session-mirror-hydration-frame-ordering.test.tsx index 4e2379811aa..2efa6874267 100644 --- a/src/renderer/src/runtime/host-session-mirror-hydration-frame-ordering.test.tsx +++ b/src/renderer/src/runtime/host-session-mirror-hydration-frame-ordering.test.tsx @@ -69,7 +69,7 @@ import { settle, setDocumentVisibility, tabIds -} from './host-session-mirror-frame-ordering-harness' +} from './host-session-mirror-frame-ordering-test-harness' import { clearWebSessionTabsTrackingForEnvironment } from './web-session-tabs-sync/tracking-lifecycle' import { useWebSessionTabsSync } from './web-session-tabs-sync/use-web-session-tabs-sync' import { WEB_SESSION_TABS_VISIBILITY_RESUME_STAGGER_MS } from './web-session-tabs-sync/state' diff --git a/src/renderer/src/runtime/host-session-mirror-settle-receipt-frames.test.tsx b/src/renderer/src/runtime/host-session-mirror-settle-receipt-frames.test.tsx index 16223f05ab5..5d085ecccc1 100644 --- a/src/renderer/src/runtime/host-session-mirror-settle-receipt-frames.test.tsx +++ b/src/renderer/src/runtime/host-session-mirror-settle-receipt-frames.test.tsx @@ -65,7 +65,7 @@ import { settle, setDocumentVisibility, tabIds -} from './host-session-mirror-frame-ordering-harness' +} from './host-session-mirror-frame-ordering-test-harness' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' import { clearHostSessionMirrorHydration, diff --git a/src/renderer/src/runtime/paired-runtime-environment-test-fixture.ts b/src/renderer/src/runtime/paired-runtime-environment-test-fixture.ts new file mode 100644 index 00000000000..7bce73d2284 --- /dev/null +++ b/src/renderer/src/runtime/paired-runtime-environment-test-fixture.ts @@ -0,0 +1,37 @@ +import type { RuntimeEnvironmentStatus } from '../../../shared/runtime-host-status' +import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments' + +/** A paired environment catalog row; the fields a test names override these defaults. */ +export function publicRuntimeEnvironment( + fields: Pick & Partial +): PublicKnownRuntimeEnvironment { + return { + name: fields.id, + createdAt: 1, + updatedAt: 1, + lastUsedAt: null, + runtimeId: null, + endpoints: [{ id: 'ws-1', kind: 'websocket', label: 'WebSocket', endpoint: 'ws://server' }], + preferredEndpointId: 'ws-1', + ...fields + } +} + +/** The status of a host that answered on the given connection. */ +export function connectedRuntimeStatus( + runtimeId: string, + connectionGeneration: number +): RuntimeEnvironmentStatus { + return { + status: { + runtimeId, + rendererGraphEpoch: 0, + graphStatus: 'ready', + authoritativeWindowId: null, + liveTabCount: 0, + liveLeafCount: 0 + }, + checkedAt: 1, + connectionGeneration + } +} diff --git a/src/renderer/src/runtime/server-layout-frames.test-fixture.ts b/src/renderer/src/runtime/server-layout-frames.test-fixture.ts index 7497196e14a..924d23a4bbc 100644 --- a/src/renderer/src/runtime/server-layout-frames.test-fixture.ts +++ b/src/renderer/src/runtime/server-layout-frames.test-fixture.ts @@ -16,6 +16,7 @@ import type { RuntimeClientTarget } from '../../../shared/runtime-client-target' import type { AppState } from '@/store/types' import type { RuntimeLayoutStreamStatus } from './runtime-layout-client' import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments' +import { publicRuntimeEnvironment } from './paired-runtime-environment-test-fixture' import { createTestStore } from '../store/slices/store-test-helpers' import { installServerLayoutFrames } from './server-layout-frames' import { applyWebSessionTabsSnapshotWithContext } from './web-session-tabs-sync/apply-snapshot' @@ -142,17 +143,7 @@ export function runtimeEnvironment( runtimeId: string, pairingRevision: number ): PublicKnownRuntimeEnvironment { - return { - id, - name: id, - createdAt: 1, - updatedAt: 1, - pairingRevision, - lastUsedAt: null, - runtimeId, - endpoints: [{ id: 'ws-1', kind: 'websocket', label: 'WebSocket', endpoint: 'ws://server' }], - preferredEndpointId: 'ws-1' - } + return publicRuntimeEnvironment({ id, runtimeId, pairingRevision }) } type MirrorStore = { diff --git a/src/renderer/src/runtime/use-runtime-session-mirror-environment-key.test.ts b/src/renderer/src/runtime/use-runtime-session-mirror-environment-key.test.ts index 4e777f746d2..f521b06730b 100644 --- a/src/renderer/src/runtime/use-runtime-session-mirror-environment-key.test.ts +++ b/src/renderer/src/runtime/use-runtime-session-mirror-environment-key.test.ts @@ -18,13 +18,16 @@ vi.mock('@/lib/runtime-session-mirror-targets', async (importOriginal) => { }) import { useAppStore } from '@/store' -import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments' import type { AppState } from '@/store/types' import { selectRuntimeSessionMirrorTargetInputs, useRuntimeSessionMirrorEnvironmentKeys } from './use-runtime-session-mirror-environment-key' import { useWebSessionTabsSync } from './web-session-tabs-sync/use-web-session-tabs-sync' +import { + connectedRuntimeStatus, + publicRuntimeEnvironment +} from './paired-runtime-environment-test-fixture' const initialState = useAppStore.getInitialState() @@ -71,25 +74,13 @@ function seedMirrorState(): void { projectGroups: [], restoredRuntimeHostIdByWorkspaceSessionKey: {}, runtimeEnvironments: [ - { id: 'env-a', createdAt: 100, pairingRevision: 101 }, - { id: 'env-b', createdAt: 200, pairingRevision: 201 } - ] as PublicKnownRuntimeEnvironment[], + publicRuntimeEnvironment({ id: 'env-a', createdAt: 100, pairingRevision: 101 }), + publicRuntimeEnvironment({ id: 'env-b', createdAt: 200, pairingRevision: 201 }) + ], runtimeStatusByEnvironmentId: new Map([ - [ - 'env-a', - { - status: { runtimeId: 'runtime-a' }, - connectionGeneration: 1 - } - ], - [ - 'env-b', - { - status: { runtimeId: 'runtime-b' }, - connectionGeneration: 2 - } - ] - ]) as AppState['runtimeStatusByEnvironmentId'] + ['env-a', connectedRuntimeStatus('runtime-a', 1)], + ['env-b', connectedRuntimeStatus('runtime-b', 2)] + ]) }, true ) @@ -245,15 +236,7 @@ describe('useRuntimeSessionMirrorEnvironmentKeys', () => { act(() => { useAppStore.setState({ - runtimeStatusByEnvironmentId: new Map([ - [ - 'env-a', - { - status: { runtimeId: 'runtime-a' }, - connectionGeneration: 2 - } - ] - ]) as AppState['runtimeStatusByEnvironmentId'] + runtimeStatusByEnvironmentId: new Map([['env-a', connectedRuntimeStatus('runtime-a', 2)]]) }) }) expect(hook.result.current.environmentKey).toBe('env-a\u0001runtime-a\u00012\u0001101') @@ -261,8 +244,8 @@ describe('useRuntimeSessionMirrorEnvironmentKeys', () => { act(() => { useAppStore.setState({ runtimeEnvironments: [ - { id: 'env-a', createdAt: 100, pairingRevision: 102 } - ] as PublicKnownRuntimeEnvironment[] + publicRuntimeEnvironment({ id: 'env-a', createdAt: 100, pairingRevision: 102 }) + ] }) }) expect(hook.result.current.environmentKey).toBe('env-a\u0001runtime-a\u00012\u0001102') diff --git a/src/renderer/src/runtime/web-session-tabs-retired-epoch-census-repair.test.tsx b/src/renderer/src/runtime/web-session-tabs-retired-epoch-census-repair.test.tsx index efb9941b9b9..62a7d02f54c 100644 --- a/src/renderer/src/runtime/web-session-tabs-retired-epoch-census-repair.test.tsx +++ b/src/renderer/src/runtime/web-session-tabs-retired-epoch-census-repair.test.tsx @@ -4,8 +4,6 @@ import { act, cleanup, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { toWebTerminalSurfaceTabId } from '../../../shared/terminal-surface-id' import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types' -import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments' -import type { AppState } from '@/store/types' const mocks = vi.hoisted(() => ({ ownerEnvironmentIdForWorkspace: vi.fn() @@ -30,6 +28,10 @@ import { useAppStore } from '@/store' import { replaceRuntimeEnvironmentRevisions } from './runtime-environment-revision' import { resetWebSessionTabsSnapshotFreshnessForTests } from './web-session-tabs-sync/tracking-lifecycle' import { useWebSessionTabsSync } from './web-session-tabs-sync/use-web-session-tabs-sync' +import { + connectedRuntimeStatus, + publicRuntimeEnvironment +} from './paired-runtime-environment-test-fixture' /** * #21928: a paired host mints `headless:` epochs for one worktree between renderer frames, so the @@ -134,10 +136,9 @@ describe('a renderer publisher returning after a headless epoch', () => { value: { runtimeEnvironments: { call: runtimeCall, subscribe: runtimeSubscribe } } }) resetWebSessionTabsSnapshotFreshnessForTests() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mirror reads only id, createdAt and pairingRevision. const runtimeEnvironments = [ - { id: ENV_A, createdAt: 100, pairingRevision: 101 } - ] as PublicKnownRuntimeEnvironment[] + publicRuntimeEnvironment({ id: ENV_A, createdAt: 100, pairingRevision: 101 }) + ] replaceRuntimeEnvironmentRevisions(runtimeEnvironments) useAppStore.setState( { @@ -145,10 +146,7 @@ describe('a renderer publisher returning after a headless epoch', () => { activeWorktreeId: WORKTREE, workspaceSessionReady: true, runtimeEnvironments, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mirror reads only runtimeId and connectionGeneration. - runtimeStatusByEnvironmentId: new Map([ - [ENV_A, { status: { runtimeId: 'runtime-a' }, connectionGeneration: 1 }] - ]) as AppState['runtimeStatusByEnvironmentId'] + runtimeStatusByEnvironmentId: new Map([[ENV_A, connectedRuntimeStatus('runtime-a', 1)]]) }, true ) diff --git a/src/renderer/src/runtime/web-session-tabs-stream-end-resubscribe.test.tsx b/src/renderer/src/runtime/web-session-tabs-stream-end-resubscribe.test.tsx index 7b74edfaf4e..8274c0b5470 100644 --- a/src/renderer/src/runtime/web-session-tabs-stream-end-resubscribe.test.tsx +++ b/src/renderer/src/runtime/web-session-tabs-stream-end-resubscribe.test.tsx @@ -4,8 +4,6 @@ import { act, cleanup, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { toWebTerminalSurfaceTabId } from '../../../shared/terminal-surface-id' import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types' -import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments' -import type { AppState } from '@/store/types' const mocks = vi.hoisted(() => ({ ownerEnvironmentIdForWorkspace: vi.fn() @@ -31,6 +29,10 @@ import { replaceRuntimeEnvironmentRevisions } from './runtime-environment-revisi import { resetWebSessionTabsSnapshotFreshnessForTests } from './web-session-tabs-sync/tracking-lifecycle' import { useWebSessionTabsSync } from './web-session-tabs-sync/use-web-session-tabs-sync' import { WINDOW_VISIBILITY_SUBSCRIPTION_RETRY_INITIAL_MS } from './window-visibility-subscription-parking' +import { + connectedRuntimeStatus, + publicRuntimeEnvironment +} from './paired-runtime-environment-test-fixture' /** * #21052/#19092: the host can end a tab stream while the connection stays up (an id-less @@ -120,10 +122,9 @@ describe('session-tabs streams the host ends over a live connection', () => { value: { runtimeEnvironments: { call: runtimeCall, subscribe: runtimeSubscribe } } }) resetWebSessionTabsSnapshotFreshnessForTests() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mirror reads only id, createdAt and pairingRevision. const runtimeEnvironments = [ - { id: ENV_A, createdAt: 100, pairingRevision: 101 } - ] as PublicKnownRuntimeEnvironment[] + publicRuntimeEnvironment({ id: ENV_A, createdAt: 100, pairingRevision: 101 }) + ] replaceRuntimeEnvironmentRevisions(runtimeEnvironments) useAppStore.setState( { @@ -131,10 +132,7 @@ describe('session-tabs streams the host ends over a live connection', () => { activeWorktreeId: WORKTREE, workspaceSessionReady: true, runtimeEnvironments, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mirror reads only runtimeId and connectionGeneration. - runtimeStatusByEnvironmentId: new Map([ - [ENV_A, { status: { runtimeId: 'runtime-a' }, connectionGeneration: 1 }] - ]) as AppState['runtimeStatusByEnvironmentId'] + runtimeStatusByEnvironmentId: new Map([[ENV_A, connectedRuntimeStatus('runtime-a', 1)]]) }, true ) diff --git a/src/renderer/src/runtime/web-session-tabs-sync-initial-terminal-relaunch.test.tsx b/src/renderer/src/runtime/web-session-tabs-sync-initial-terminal-relaunch.test.tsx index f292d5ebf5d..9de14aabc93 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync-initial-terminal-relaunch.test.tsx +++ b/src/renderer/src/runtime/web-session-tabs-sync-initial-terminal-relaunch.test.tsx @@ -48,7 +48,6 @@ vi.mock('./web-runtime-terminal-creation', async (importOriginal) => { }) import { useAppStore } from '@/store' -import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments' import type { AppState } from '@/store/types' import { replaceRuntimeEnvironmentRevisions } from './runtime-environment-revision' import { resetWebSessionTabsSnapshotFreshnessForTests } from './web-session-tabs-sync/tracking-lifecycle' @@ -57,6 +56,10 @@ import { clearRuntimeEnvironmentConnectionGenerationsForTests } from '@/store/sl import { resetWebRuntimeInitialTerminalBootstrapForTests } from './web-runtime-initial-terminal-bootstrap' import { resetWebRuntimeWakeTerminalRespawnForTests } from './web-runtime-wake-terminal-respawn' import { ensureWebRuntimeWorktreeTerminalAfterWake } from '@/lib/web-runtime-worktree-terminal-after-wake' +import { + connectedRuntimeStatus, + publicRuntimeEnvironment +} from './paired-runtime-environment-test-fixture' const ENV = 'env-a' const WORKTREE = 'repo-a::worktree-a' @@ -177,15 +180,13 @@ function hostTerminalSnapshot(snapshotVersion: number): RuntimeMobileSessionTabs } function runtimeStatusMap(connectionGeneration: number): AppState['runtimeStatusByEnvironmentId'] { - return new Map([ - [ENV, { status: { runtimeId: 'runtime-a' }, connectionGeneration }] - ]) as AppState['runtimeStatusByEnvironmentId'] + return new Map([[ENV, connectedRuntimeStatus('runtime-a', connectionGeneration)]]) } function seedRemoteMirrorState(connectionGeneration: number): void { const runtimeEnvironments = [ - { id: ENV, createdAt: 100, pairingRevision: REVISION } - ] as PublicKnownRuntimeEnvironment[] + publicRuntimeEnvironment({ id: ENV, createdAt: 100, pairingRevision: REVISION }) + ] replaceRuntimeEnvironmentRevisions(runtimeEnvironments) useAppStore.setState( { diff --git a/src/renderer/src/runtime/web-session-tabs-sync-reconnect-resubscribe.test.tsx b/src/renderer/src/runtime/web-session-tabs-sync-reconnect-resubscribe.test.tsx index 325b41c47b0..95cfdd08ce4 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync-reconnect-resubscribe.test.tsx +++ b/src/renderer/src/runtime/web-session-tabs-sync-reconnect-resubscribe.test.tsx @@ -4,7 +4,6 @@ import { act, cleanup, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../shared/constants' import type { RuntimeHostStatusSnapshot } from '../../../shared/runtime-host-status' -import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments' import type { RuntimeStatus } from '../../../shared/runtime-types' vi.mock('sonner', () => ({ toast: { warning: vi.fn(), dismiss: vi.fn() } })) @@ -30,6 +29,7 @@ import { resetWebSessionTabsSnapshotFreshnessForTests } from './web-session-tabs import { useWebSessionTabsSync } from './web-session-tabs-sync/use-web-session-tabs-sync' import { clearRuntimeEnvironmentConnectionGenerationsForTests } from '@/store/slices/runtime-status' import { buildRuntimeSessionMirrorEnvironmentKeys } from './use-runtime-session-mirror-environment-key' +import { publicRuntimeEnvironment } from './paired-runtime-environment-test-fixture' const ENV_A = 'env-a' const WORKTREE = 'repo-a::worktree-a' @@ -153,10 +153,9 @@ describe('session-tabs mirror across an outage and its recovery', () => { resetWebSessionTabsSnapshotFreshnessForTests() clearHostLiveTerminalProbesForTests() clearRuntimeEnvironmentConnectionGenerationsForTests() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mirror scan and revision ledger read only id, createdAt and pairingRevision. const runtimeEnvironments = [ - { id: ENV_A, createdAt: 100, pairingRevision: REVISION_A } - ] as PublicKnownRuntimeEnvironment[] + publicRuntimeEnvironment({ id: ENV_A, createdAt: 100, pairingRevision: REVISION_A }) + ] replaceRuntimeEnvironmentRevisions(runtimeEnvironments) useAppStore.setState( { diff --git a/src/renderer/src/runtime/web-session-tabs-sync-unverifiable-host.test.tsx b/src/renderer/src/runtime/web-session-tabs-sync-unverifiable-host.test.tsx index 7bfe374c21c..e9b002bb5fe 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync-unverifiable-host.test.tsx +++ b/src/renderer/src/runtime/web-session-tabs-sync-unverifiable-host.test.tsx @@ -4,7 +4,6 @@ import { act, cleanup, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { RuntimeHostStatusSnapshot } from '../../../shared/runtime-host-status' import type { RuntimeStatus } from '../../../shared/runtime-types' -import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments' const mocks = vi.hoisted(() => ({ ownerEnvironmentIdForWorkspace: vi.fn(), @@ -30,6 +29,7 @@ import { clearHostLiveTerminalProbesForTests } from './host-live-terminal-probe' import { resetWebSessionTabsSnapshotFreshnessForTests } from './web-session-tabs-sync/tracking-lifecycle' import { useWebSessionTabsSync } from './web-session-tabs-sync/use-web-session-tabs-sync' import { clearRuntimeEnvironmentConnectionGenerationsForTests } from '@/store/slices/runtime-status' +import { publicRuntimeEnvironment } from './paired-runtime-environment-test-fixture' const ENV_A = 'env-a' const WORKTREE = 'repo-a::worktree-a' @@ -106,10 +106,9 @@ describe('useWebSessionTabsSync under an unverifiable host probe', () => { }) resetWebSessionTabsSnapshotFreshnessForTests() clearHostLiveTerminalProbesForTests() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mirror scan and revision ledger read only id, createdAt and pairingRevision. const runtimeEnvironments = [ - { id: ENV_A, createdAt: 100, pairingRevision: REVISION_A } - ] as PublicKnownRuntimeEnvironment[] + publicRuntimeEnvironment({ id: ENV_A, createdAt: 100, pairingRevision: REVISION_A }) + ] replaceRuntimeEnvironmentRevisions(runtimeEnvironments) useAppStore.setState( { diff --git a/src/renderer/src/runtime/web-session-tabs-sync-visibility-collision.test.tsx b/src/renderer/src/runtime/web-session-tabs-sync-visibility-collision.test.tsx index 79be5c15a77..f34ed95c769 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync-visibility-collision.test.tsx +++ b/src/renderer/src/runtime/web-session-tabs-sync-visibility-collision.test.tsx @@ -38,8 +38,6 @@ vi.mock('./web-runtime-terminal-creation', async (importOriginal) => { import { useAppStore } from '@/store' import { buildOwnedEditorFileId } from '@/store/slices/editor/file-ids/editor-file-ids' -import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments' -import type { AppState } from '@/store/types' import { replaceRuntimeEnvironmentRevisions } from './runtime-environment-revision' import { toRemoteRuntimePtyId } from '../../../shared/remote-runtime-pty-id' import { subscribeAcceptedWebSessionTerminalHandle } from './web-session-terminal-handle-events' @@ -51,6 +49,10 @@ import { import { useWebSessionTabsSync } from './web-session-tabs-sync/use-web-session-tabs-sync' import { WEB_SESSION_TABS_VISIBILITY_RESUME_STAGGER_MS } from './web-session-tabs-sync/state' import { WINDOW_VISIBILITY_SUBSCRIPTION_PARK_DELAY_MS } from './window-visibility-subscription-parking' +import { + connectedRuntimeStatus, + publicRuntimeEnvironment +} from './paired-runtime-environment-test-fixture' const ENV_A = 'env-a' const ENV_B = 'env-b' @@ -220,9 +222,9 @@ async function publish(subscription: RuntimeSubscription, result: unknown): Prom function seedRemoteMirrorState(): void { const runtimeEnvironments = [ - { id: ENV_A, createdAt: 100, pairingRevision: REVISION_A }, - { id: ENV_B, createdAt: 200, pairingRevision: REVISION_B } - ] as PublicKnownRuntimeEnvironment[] + publicRuntimeEnvironment({ id: ENV_A, createdAt: 100, pairingRevision: REVISION_A }), + publicRuntimeEnvironment({ id: ENV_B, createdAt: 200, pairingRevision: REVISION_B }) + ] replaceRuntimeEnvironmentRevisions(runtimeEnvironments) useAppStore.setState( { @@ -231,9 +233,9 @@ function seedRemoteMirrorState(): void { workspaceSessionReady: true, runtimeEnvironments, runtimeStatusByEnvironmentId: new Map([ - [ENV_A, { status: { runtimeId: 'runtime-a' }, connectionGeneration: 1 }], - [ENV_B, { status: { runtimeId: 'runtime-b' }, connectionGeneration: 2 }] - ]) as AppState['runtimeStatusByEnvironmentId'] + [ENV_A, connectedRuntimeStatus('runtime-a', 1)], + [ENV_B, connectedRuntimeStatus('runtime-b', 2)] + ]) }, true ) diff --git a/src/renderer/src/runtime/web-session-tabs-sync-window-visibility.test.tsx b/src/renderer/src/runtime/web-session-tabs-sync-window-visibility.test.tsx index 6b8b94c66a4..31cb821d791 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync-window-visibility.test.tsx +++ b/src/renderer/src/runtime/web-session-tabs-sync-window-visibility.test.tsx @@ -36,8 +36,6 @@ vi.mock('./web-runtime-terminal-creation', async (importOriginal) => { }) import { useAppStore } from '@/store' -import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments' -import type { AppState } from '@/store/types' import { replaceRuntimeEnvironmentRevisions } from './runtime-environment-revision' import { clearHostLiveTerminalProbesForTests } from './host-live-terminal-probe' import { @@ -52,6 +50,10 @@ import { WINDOW_VISIBILITY_SUBSCRIPTION_PARK_DELAY_BACKOFF_LIMIT, WINDOW_VISIBILITY_SUBSCRIPTION_PARK_DELAY_MS } from './window-visibility-subscription-parking' +import { + connectedRuntimeStatus, + publicRuntimeEnvironment +} from './paired-runtime-environment-test-fixture' const ENV_A = 'env-a' const ENV_B = 'env-b' @@ -193,9 +195,9 @@ async function publish( function seedRemoteMirrorState(): void { const runtimeEnvironments = [ - { id: ENV_A, createdAt: 100, pairingRevision: REVISION_A }, - { id: ENV_B, createdAt: 200, pairingRevision: REVISION_B } - ] as PublicKnownRuntimeEnvironment[] + publicRuntimeEnvironment({ id: ENV_A, createdAt: 100, pairingRevision: REVISION_A }), + publicRuntimeEnvironment({ id: ENV_B, createdAt: 200, pairingRevision: REVISION_B }) + ] replaceRuntimeEnvironmentRevisions(runtimeEnvironments) useAppStore.setState( { @@ -204,9 +206,9 @@ function seedRemoteMirrorState(): void { workspaceSessionReady: true, runtimeEnvironments, runtimeStatusByEnvironmentId: new Map([ - [ENV_A, { status: { runtimeId: 'runtime-a' }, connectionGeneration: 1 }], - [ENV_B, { status: { runtimeId: 'runtime-b' }, connectionGeneration: 2 }] - ]) as AppState['runtimeStatusByEnvironmentId'] + [ENV_A, connectedRuntimeStatus('runtime-a', 1)], + [ENV_B, connectedRuntimeStatus('runtime-b', 2)] + ]) }, true ) diff --git a/src/renderer/src/runtime/web-session-tabs-sync-worktree-switch.test.tsx b/src/renderer/src/runtime/web-session-tabs-sync-worktree-switch.test.tsx index e55c86d39b9..aac3a0614e3 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync-worktree-switch.test.tsx +++ b/src/renderer/src/runtime/web-session-tabs-sync-worktree-switch.test.tsx @@ -4,7 +4,6 @@ import { act, cleanup, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../shared/constants' import type { RuntimeHostStatusSnapshot } from '../../../shared/runtime-host-status' -import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments' import type { RuntimeMobileSessionTabsResult, RuntimeStatus } from '../../../shared/runtime-types' import type * as WebRuntimeTerminalCreationModule from './web-runtime-terminal-creation' @@ -34,6 +33,7 @@ import { useWebSessionTabsSync } from './web-session-tabs-sync/use-web-session-t import { clearRuntimeEnvironmentConnectionGenerationsForTests } from '@/store/slices/runtime-status' import { resetWebRuntimeInitialTerminalBootstrapForTests } from './web-runtime-initial-terminal-bootstrap' import { TERMINAL_WORKTREE_HOT_RETAIN_LIMIT } from '@/components/terminal-pane/terminal-hidden-view-parking' +import { publicRuntimeEnvironment } from './paired-runtime-environment-test-fixture' const ENV = 'env-a' const WORKTREE_A = 'repo-a::worktree-a' @@ -184,10 +184,9 @@ describe('session-tabs streams across worktree switches on one host', () => { resetWebRuntimeInitialTerminalBootstrapForTests() clearHostLiveTerminalProbesForTests() clearRuntimeEnvironmentConnectionGenerationsForTests() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mirror scan and revision ledger read only id, createdAt and pairingRevision. const runtimeEnvironments = [ - { id: ENV, createdAt: 100, pairingRevision: REVISION } - ] as PublicKnownRuntimeEnvironment[] + publicRuntimeEnvironment({ id: ENV, createdAt: 100, pairingRevision: REVISION }) + ] replaceRuntimeEnvironmentRevisions(runtimeEnvironments) useAppStore.setState( { @@ -384,11 +383,10 @@ describe('session-tabs streams across worktree switches on one host', () => { mocks.ownerEnvironmentIdForWorkspace.mockImplementation((_state, worktreeId) => worktreeId === WORKTREE_B ? 'env-b' : ENV ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mirror scan and revision ledger read only id, createdAt and pairingRevision. const withHostB = [ ...useAppStore.getState().runtimeEnvironments, - { id: 'env-b', createdAt: 200, pairingRevision: 201 } - ] as PublicKnownRuntimeEnvironment[] + publicRuntimeEnvironment({ id: 'env-b', createdAt: 200, pairingRevision: 201 }) + ] replaceRuntimeEnvironmentRevisions(withHostB) await act(async () => { useAppStore.setState({ runtimeEnvironments: withHostB }) diff --git a/src/renderer/src/store/github/cache-identity.ts b/src/renderer/src/store/github/cache-identity.ts index b874366988b..90dd36486b1 100644 --- a/src/renderer/src/store/github/cache-identity.ts +++ b/src/renderer/src/store/github/cache-identity.ts @@ -16,10 +16,10 @@ import { composeRepoCacheKey, getGitHubPRCacheKey, getGitHubRepoCacheKey, - repoCacheKeyHostId + repoCacheKeyHostId, + type RuntimeFocusSettings } from '../../../../shared/github/github-cache-key' import type { Repo } from '../../../../shared/repo-types' -import type { AppState } from '../types' import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target' import type { CacheEntry } from './cache-model' @@ -115,7 +115,7 @@ export function runtimeScopedRepoCacheKey( repoPath: string, repoId: string | undefined, suffix: string, - settings?: AppState['settings'], + settings?: RuntimeFocusSettings, connectionId?: string | null, executionHostId?: string | null, hasRepoOwner = false @@ -135,7 +135,7 @@ export function sourceScopedRepoCacheKey( repoPath: string, repoId: string | undefined, suffix: string, - settings?: AppState['settings'], + settings?: RuntimeFocusSettings, connectionId?: string | null, executionHostId?: string | null, sourceContext?: TaskSourceContext | null, @@ -159,7 +159,7 @@ export function prCacheKey( repoPath: string, repoId: string | undefined, branch: string, - settings?: AppState['settings'], + settings?: RuntimeFocusSettings, connectionId?: string | null, executionHostId?: string | null, hasRepoOwner = false diff --git a/src/renderer/src/store/github/pr-result-cache.ts b/src/renderer/src/store/github/pr-result-cache.ts index 35b397e7df7..3b1e58f3a27 100644 --- a/src/renderer/src/store/github/pr-result-cache.ts +++ b/src/renderer/src/store/github/pr-result-cache.ts @@ -1,4 +1,5 @@ import type { AppState } from '../types' +import type { RuntimeFocusSettings } from '../../../../shared/github/github-cache-key' import type { PRInfo } from '../../../../shared/github/pull-request-types' import { getHostedReviewCacheKey } from '../slices/hosted-review-cache-identity' import type { GitHubPRFallbackSource } from './cache-model' @@ -16,7 +17,7 @@ type GitHubPRResultCacheArgs = { prCacheKey: string repoPath: string branch: string - settings: AppState['settings'] + settings: RuntimeFocusSettings repoId?: string connectionId?: string | null executionHostId?: string | null diff --git a/src/renderer/src/store/github/pr-result-routing.ts b/src/renderer/src/store/github/pr-result-routing.ts index 80f4a76bd20..52fde15f77e 100644 --- a/src/renderer/src/store/github/pr-result-routing.ts +++ b/src/renderer/src/store/github/pr-result-routing.ts @@ -1,4 +1,5 @@ import type { AppState } from '../types' +import type { RuntimeFocusSettings } from '../../../../shared/github/github-cache-key' import type { PRInfo } from '../../../../shared/github/pull-request-types' import { hostedReviewInfoFromGitHubPRInfo } from '../../../../shared/hosted-review-github' import { @@ -75,7 +76,7 @@ export function syncHostedReviewCacheFromGitHubPRResult(args: { cache: AppState['hostedReviewCache'] repoPath: string branch: string - settings: AppState['settings'] + settings: RuntimeFocusSettings repoId?: string connectionId?: string | null executionHostId?: string | null diff --git a/src/renderer/src/store/github/pull-request-execution.ts b/src/renderer/src/store/github/pull-request-execution.ts index 710b58f979b..289c6365f09 100644 --- a/src/renderer/src/store/github/pull-request-execution.ts +++ b/src/renderer/src/store/github/pull-request-execution.ts @@ -2,6 +2,7 @@ import { CLEARED_HOSTED_REVIEW_SELECTION_UPDATES } from '../../../../shared/work import type { StateCreator } from 'zustand' import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target' import type { AppState } from '../types' +import type { RuntimeFocusSettings } from '../../../../shared/github/github-cache-key' import type { GitHubSlice } from './slice-types' import type { Repo } from '../../../../shared/repo-types' import type { PRInfo } from '../../../../shared/github/pull-request-types' @@ -36,7 +37,7 @@ export function startPullRequestLookup(args: { options: Parameters[2] repo: Repo | undefined repoId: string | undefined - requestSettings: AppState['settings'] + requestSettings: RuntimeFocusSettings requestTarget: RuntimeClientTarget cacheKey: string cached: CacheEntry | undefined diff --git a/src/renderer/src/store/github/work-item-routing.ts b/src/renderer/src/store/github/work-item-routing.ts index ed2ffcac826..d447ccd02e8 100644 --- a/src/renderer/src/store/github/work-item-routing.ts +++ b/src/renderer/src/store/github/work-item-routing.ts @@ -1,4 +1,5 @@ import type { AppState } from '../types' +import type { RuntimeFocusSettings } from '../../../../shared/github/github-cache-key' import type { Repo } from '../../../../shared/repo-types' import type { GitHubWorkItem, ListWorkItemsResult } from '../../../../shared/github/work-item-types' import { @@ -44,7 +45,7 @@ export type GitHubWorkItemsListArgs = { export function settingsForGitHubRepoOwner( settings: AppState['settings'], repo: Pick | undefined -): AppState['settings'] { +): RuntimeFocusSettings { if (!repo) { return settings } @@ -59,18 +60,15 @@ export function settingsForGitHubRepoOwner( function withActiveRuntimeEnvironmentId( settings: AppState['settings'], activeRuntimeEnvironmentId: string | null -): AppState['settings'] { - return settings - ? { ...settings, activeRuntimeEnvironmentId } - : // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: routing readers only consume activeRuntimeEnvironmentId before settings hydrate. - ({ activeRuntimeEnvironmentId } as AppState['settings']) +): RuntimeFocusSettings { + return settings ? { ...settings, activeRuntimeEnvironmentId } : { activeRuntimeEnvironmentId } } function settingsForGitHubTaskSource( state: GitHubSourceRoutingState, repo: Pick | undefined, sourceContext: TaskSourceContext -): AppState['settings'] { +): RuntimeFocusSettings { const sourceHost = parseExecutionHostId(sourceContext.hostId) return withActiveRuntimeEnvironmentId( state.settings, @@ -133,7 +131,7 @@ export function getGitHubRepoSourceSettings( state: GitHubSourceRoutingState, repo: Pick | undefined, sourceContext?: TaskSourceContext | null -): AppState['settings'] { +): RuntimeFocusSettings { if (sourceContext?.provider === 'github') { return settingsForGitHubTaskSource(state, repo, sourceContext) } diff --git a/src/renderer/src/store/slices/agent-status.ts b/src/renderer/src/store/slices/agent-status.ts index b08ed80b5ea..7720a4643b9 100644 --- a/src/renderer/src/store/slices/agent-status.ts +++ b/src/renderer/src/store/slices/agent-status.ts @@ -79,7 +79,6 @@ export const createAgentStatusSlice: StateCreator { Object.assign(mockApi.browser, { sessionEnsureManagedProfile: ensureMock }) const { store, main } = await createTestStoreDrawnFromMain({ repos: [], - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only the browser keys are read. - settings: { browserProfileScope: 'repo' } as AppState['settings'], + settings: { ...getDefaultSettings('/tmp'), browserProfileScope: 'repo' }, defaultBrowserSessionProfileId: 'user-default' }) diff --git a/src/renderer/src/store/slices/editor-close-file-cleanup.test.ts b/src/renderer/src/store/slices/editor-close-file-cleanup.test.ts index bbd92f2c591..2fa522f2c71 100644 --- a/src/renderer/src/store/slices/editor-close-file-cleanup.test.ts +++ b/src/renderer/src/store/slices/editor-close-file-cleanup.test.ts @@ -1,5 +1,6 @@ import type { StoreApi } from 'zustand/vanilla' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' +import { getDefaultSettings } from '../../../../shared/constants' import { createEditorStore, createEditorTabsStore, @@ -97,9 +98,8 @@ describe('createEditorSlice untitled cleanup routing', () => { }) function seedRemoteWorktree(store: StoreApi): void { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the store under test reads only these fields of the partial state. store.setState({ - settings: { activeRuntimeEnvironmentId: 'env-1' } as never, + settings: { ...getDefaultSettings('/tmp'), activeRuntimeEnvironmentId: 'env-1' }, repos: [ { id: 'repo1', @@ -136,7 +136,7 @@ describe('createEditorSlice untitled cleanup routing', () => { } ] } - } as Partial) + }) } it('closeFile deletes untouched remote untitled files through runtime file RPC', async () => { @@ -232,10 +232,10 @@ describe('createEditorSlice untitled cleanup routing', () => { it('closeFile does not delete when worktree ownership metadata is missing', async () => { const store = createEditorStore() store.setState({ - settings: { activeRuntimeEnvironmentId: 'env-1' } as never, + settings: { ...getDefaultSettings('/tmp'), activeRuntimeEnvironmentId: 'env-1' }, repos: [], worktreesByRepo: {} - } as Partial) + }) const fileId = store.getState().openFile({ filePath: '/remote/wt/untitled.md', relativePath: 'untitled.md', diff --git a/src/renderer/src/store/slices/editor-markdown-link-activation.test.ts b/src/renderer/src/store/slices/editor-markdown-link-activation.test.ts index c4cd8388cd0..c8fbaeb6460 100644 --- a/src/renderer/src/store/slices/editor-markdown-link-activation.test.ts +++ b/src/renderer/src/store/slices/editor-markdown-link-activation.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' +import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultSettings } from '../../../../shared/constants' import { createEditorStore, ownedEditorFileId } from './editor-slice-test-harness' import type { AppState } from '../types' import { makeWorktree } from './worktrees-slice-test-fixtures' @@ -7,7 +8,6 @@ import { type RuntimeEnvironmentCallRequest } from '../../runtime/runtime-compatibility-test-fixture' import { clearRuntimeCompatibilityCacheForTests } from '../../runtime/runtime-rpc-client' -import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' const { toastErrorMock } = vi.hoisted(() => ({ toastErrorMock: vi.fn() @@ -377,9 +377,8 @@ describe('createEditorSlice activateMarkdownLink', () => { it('reveals active-runtime markdown line anchors on the owner-qualified tab id', async () => { const store = createEditorStore() pathExistsMock.mockResolvedValue(true) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the store under test reads only these fields of the partial state. store.setState({ - settings: { activeRuntimeEnvironmentId: 'env-active' } as AppState['settings'], + settings: { ...getDefaultSettings('/tmp'), activeRuntimeEnvironmentId: 'env-active' }, // The server's rows are stamped as they land; focus alone never names a file's owner. worktreesByRepo: { 'wt-1': [ @@ -404,7 +403,7 @@ describe('createEditorSlice activateMarkdownLink', () => { mode: 'edit' } ] - } as Partial) + }) const activeRuntimeFileId = ownedEditorFileId('/repo/docs/guide.md', 'wt-1', 'env-active') await store.getState().activateMarkdownLink('./guide.md#L10', { diff --git a/src/renderer/src/store/slices/github-checks.ts b/src/renderer/src/store/slices/github-checks.ts index 9668d159eee..94c56b83534 100644 --- a/src/renderer/src/store/slices/github-checks.ts +++ b/src/renderer/src/store/slices/github-checks.ts @@ -1,7 +1,10 @@ import type { AppState } from '../types' import type { PRCheckDetail } from '../../../../shared/github/check-types' import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-types' -import { getGitHubPRCacheKey } from '../../../../shared/github/github-cache-key' +import { + getGitHubPRCacheKey, + type RuntimeFocusSettings +} from '../../../../shared/github/github-cache-key' import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key' import { derivePRCheckStatus } from '../../../../shared/pr-check-status' @@ -19,7 +22,7 @@ export function syncPRChecksStatus( checks: PRCheckDetail[], headSha?: string, prRepo?: GitHubOwnerRepo | null, - settings?: AppState['settings'], + settings?: RuntimeFocusSettings, connectionId?: string | null, executionHostId?: string | null, hasRepoOwner = false diff --git a/src/renderer/src/store/slices/hosted-review.test.ts b/src/renderer/src/store/slices/hosted-review.test.ts index d88be4fadcd..eca3b16be70 100644 --- a/src/renderer/src/store/slices/hosted-review.test.ts +++ b/src/renderer/src/store/slices/hosted-review.test.ts @@ -6,6 +6,7 @@ import { refreshHostedReviewCard } from './hosted-review-card-refresh' import { getHostedReviewCacheKey } from './hosted-review-cache-identity' import { HostedReviewCreationEligibilityTimeoutError } from './hosted-review-cache-state' import type { HostedReviewInfo } from '../../../../shared/hosted-review' +import { getDefaultSettings } from '../../../../shared/constants' const runtimeRpc = vi.hoisted(() => ({ callRuntimeRpc: vi.fn() @@ -541,9 +542,10 @@ describe('hosted review slice', () => { it('creates a review for an unstamped local repo on this computer, not the focused server', async () => { mockApi.hostedReview.create.mockResolvedValueOnce({ ok: true, number: 1, url: 'u' }) - const focused = { activeRuntimeEnvironmentId: 'env-win' } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the slice reads only the focus field of settings. - const store = makeStore(focused as AppState['settings']) + const store = makeStore({ + ...getDefaultSettings('/tmp'), + activeRuntimeEnvironmentId: 'env-win' + }) await store.getState().createHostedReview('/repo', { provider: 'github', diff --git a/src/renderer/src/store/slices/jira-connection-identity.test.ts b/src/renderer/src/store/slices/jira-connection-identity.test.ts index 6f52e5ffd64..e5ef04a26a6 100644 --- a/src/renderer/src/store/slices/jira-connection-identity.test.ts +++ b/src/renderer/src/store/slices/jira-connection-identity.test.ts @@ -1,19 +1,10 @@ -import { create } from 'zustand' import { beforeEach, expect, it, vi } from 'vitest' -import type { AppState } from '../types' import type { JiraConnectionStatus } from '../../../../shared/jira/jira-types' -import { createJiraSlice } from './jira' +import { createTestStore } from './store-test-helpers' const jiraStatus = vi.hoisted(() => vi.fn()) vi.mock('@/runtime/runtime-jira-client', () => ({ jiraStatus })) -function createTestStore() { - return create()((...args) => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Connection reads use only settings and the fully initialized Jira slice. - return { settings: null, ...createJiraSlice(...args) } as AppState - }) -} - function status(accountId: string): JiraConnectionStatus { return { connected: true, diff --git a/src/renderer/src/store/slices/jira-sprint-filter-cache.test.ts b/src/renderer/src/store/slices/jira-sprint-filter-cache.test.ts index 3f55ce89cfd..61df54a7f99 100644 --- a/src/renderer/src/store/slices/jira-sprint-filter-cache.test.ts +++ b/src/renderer/src/store/slices/jira-sprint-filter-cache.test.ts @@ -1,8 +1,6 @@ import { expect, it, vi } from 'vitest' -import { create } from 'zustand' -import type { AppState } from '../types' import type { JiraIssue, JiraIssueListFilters } from '../../../../shared/jira/jira-types' -import { createJiraSlice } from './jira' +import { createTestStore } from './store-test-helpers' const { list } = vi.hoisted(() => ({ list: vi.fn() })) vi.mock('@/runtime/runtime-jira-client', () => ({ @@ -12,10 +10,7 @@ vi.mock('@/runtime/runtime-jira-client', () => ({ })) it('separates combined filters while treating an unchecked sprint like the default view', async () => { - const store = create()( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: These collection reads only access the supplied settings and Jira slice. - (...args) => ({ settings: null, ...createJiraSlice(...args) }) as AppState - ) + const store = createTestStore() list.mockImplementation( async ( _source, diff --git a/src/renderer/src/store/slices/jira-status-filter-cache.test.ts b/src/renderer/src/store/slices/jira-status-filter-cache.test.ts index 298e197f8cb..87f65e1b4a8 100644 --- a/src/renderer/src/store/slices/jira-status-filter-cache.test.ts +++ b/src/renderer/src/store/slices/jira-status-filter-cache.test.ts @@ -1,8 +1,6 @@ import { beforeEach, expect, it, vi } from 'vitest' -import { create } from 'zustand' -import type { AppState } from '../types' import type { JiraIssue, JiraIssueListFilters } from '../../../../shared/jira/jira-types' -import { createJiraSlice } from './jira' +import { createTestStore } from './store-test-helpers' const { list } = vi.hoisted(() => ({ list: vi.fn() })) vi.mock('@/runtime/runtime-jira-client', () => ({ jiraListIssues: list, @@ -27,10 +25,7 @@ function issue(key: string): JiraIssue { } } it('isolates status results from preset caches and reuses an equivalent selection order', async () => { - const store = create()( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Only settings and Jira slice fields are accessed by these list reads. - (...args) => ({ settings: null, ...createJiraSlice(...args) }) as AppState - ) + const store = createTestStore() list.mockImplementation( async (_source, _preset, _limit, _site, filters?: JiraIssueListFilters) => [ issue( diff --git a/src/renderer/src/store/slices/jira-web-client-status.test.ts b/src/renderer/src/store/slices/jira-web-client-status.test.ts index 0415238f274..325191d70cb 100644 --- a/src/renderer/src/store/slices/jira-web-client-status.test.ts +++ b/src/renderer/src/store/slices/jira-web-client-status.test.ts @@ -1,15 +1,6 @@ -import { create } from 'zustand' import { afterEach, expect, it, vi } from 'vitest' -import type { AppState } from '../types' import { withFallback } from '@/web/preload-api/web-fallback-api' -import { createJiraSlice } from './jira' - -function createTestStore() { - return create()((...args) => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Connection reads use only settings and the fully initialized Jira slice. - return { settings: null, ...createJiraSlice(...args) } as AppState - }) -} +import { createTestStore } from './store-test-helpers' afterEach(() => { vi.unstubAllGlobals() diff --git a/src/renderer/src/store/slices/worktrees-fetch-stale-catalog-version.test.ts b/src/renderer/src/store/slices/worktrees-fetch-stale-catalog-version.test.ts index a585def1496..bd28dbc6c7c 100644 --- a/src/renderer/src/store/slices/worktrees-fetch-stale-catalog-version.test.ts +++ b/src/renderer/src/store/slices/worktrees-fetch-stale-catalog-version.test.ts @@ -1,12 +1,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { AppState } from '../types' import type { WorktreeCatalogVersion } from '../../../../shared/worktree/catalog-version' +import { getDefaultSettings } from '../../../../shared/constants' import { TEST_SSH_AUTHORITY, makeDetectedResult, qualifyDetectedResult } from './worktrees-detected-listing-fixtures' -import { makeWorktree } from './worktrees-slice-test-fixtures' +import { makeTerminalTab, makeWorktree } from './worktrees-slice-test-fixtures' import { worktreeCatalogVersionKey } from './worktrees/listing/worktree-catalog-version-state' import { completeSameIdHostScopedRemoval } from './worktrees/teardown/host-qualified-worktree-removal' import { applyCreatedWorktree } from './worktrees/create/created-worktree-state-merge' @@ -41,19 +41,20 @@ function seed(store: ReturnType) { repoId: 'repo1', path: '/path/surviving' }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture tabs carry only the fields the purge and teardown paths read. store.setState({ repos: [ { id: 'repo1', path: '/path/repo1', displayName: 'Repo 1', badgeColor: '#000', addedAt: 0 } ], worktreesByRepo: { repo1: [created, surviving] }, detectedWorktreesByRepo: { repo1: makeDetectedResult('repo1', [created, surviving]) }, - tabsByWorktree: { [created.id]: [{ id: 'tab-created', worktreeId: created.id }] }, + tabsByWorktree: { + [created.id]: [makeTerminalTab({ id: 'tab-created', worktreeId: created.id })] + }, // The create reply that produced `created` has been applied at sequence 7. worktreeCatalogVersionByRepoHost: { [worktreeCatalogVersionKey('repo1', 'local')]: APPLIED_BY_CREATE } - } as unknown as Partial) + }) return { created, surviving } } @@ -168,9 +169,8 @@ describe('fetchWorktrees with a listing versioned before an applied create', () repoId: 'repo1', hostId: runtimeHost }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture settings carry only the field runtime routing reads. store.setState({ - settings: { activeRuntimeEnvironmentId: 'env-1' }, + settings: { ...getDefaultSettings('/tmp'), activeRuntimeEnvironmentId: 'env-1' }, repos: [ { id: 'repo1', @@ -185,7 +185,7 @@ describe('fetchWorktrees with a listing versioned before an applied create', () worktreeCatalogVersionByRepoHost: { [worktreeCatalogVersionKey('repo1', runtimeHost)]: APPLIED_BY_CREATE } - } as unknown as Partial) + }) runtimeEnvironmentCall.mockImplementation(async ({ method }: { method: string }) => ({ id: 'rpc', ok: true, @@ -365,14 +365,13 @@ describe('the startup hydration purge behind a listing that is no longer current const created = makeWorktree({ id: 'repo1::/path/created', repoId: 'repo1' }) const other = makeWorktree({ id: 'repo2::/path/other', repoId: 'repo2' }) const zombieId = 'repo1::/path/deleted-last-session' - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture tabs carry only the fields the purge reads. store.setState({ repos: [ { id: 'repo1', path: '/path/repo1', displayName: 'Repo 1', badgeColor: '#000', addedAt: 0 }, { id: 'repo2', path: '/path/repo2', displayName: 'Repo 2', badgeColor: '#000', addedAt: 0 } ], - tabsByWorktree: { [zombieId]: [{ id: 'tab-zombie', worktreeId: zombieId }] } - } as unknown as Partial) + tabsByWorktree: { [zombieId]: [makeTerminalTab({ id: 'tab-zombie', worktreeId: zombieId })] } + }) let releaseRepo2: () => void = () => {} const repo2Held = new Promise((resolve) => { releaseRepo2 = resolve @@ -401,13 +400,12 @@ describe('the startup hydration purge behind a listing that is no longer current worktree: created, catalogVersion: APPLIED_BY_CREATE }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture tabs carry only the fields the purge reads. store.setState({ tabsByWorktree: { ...store.getState().tabsByWorktree, - [created.id]: [{ id: 'tab-created', worktreeId: created.id }] + [created.id]: [makeTerminalTab({ id: 'tab-created', worktreeId: created.id })] } - } as unknown as Partial) + }) releaseRepo2() await startup @@ -440,11 +438,10 @@ describe('the startup hydration purge behind a listing that is no longer current badgeColor: '#000', addedAt: 0 } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture tabs carry only the fields the purge reads. store.setState({ repos: [repo1], - tabsByWorktree: { [zombieId]: [{ id: 'tab-zombie', worktreeId: zombieId }] } - } as unknown as Partial) + tabsByWorktree: { [zombieId]: [makeTerminalTab({ id: 'tab-zombie', worktreeId: zombieId })] } + }) mockApi.worktrees.listDetected.mockImplementation(async (args) => { // A second owner on the same host makes the listing's owner ambiguous by the time it lands. store.setState({ repos: [repo1, { ...repo1 }] }) diff --git a/src/renderer/src/web/WebConnect.tsx b/src/renderer/src/web/WebConnect.tsx index e95f2bb1210..a4b33f53f9d 100644 --- a/src/renderer/src/web/WebConnect.tsx +++ b/src/renderer/src/web/WebConnect.tsx @@ -1,5 +1,5 @@ import { pairedRuntimeConnectionFromOffer } from '../../../shared/paired-runtime-connection' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useEffectEvent, useMemo, useRef, useState } from 'react' import { Cable, Loader2, Server, Trash2 } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -96,14 +96,13 @@ export default function WebConnect({ // Why: a deep-linked offer that reaches this screen either has mobile scope // or unknown legacy scope; run the same connect path to reject/probe it. + const autoConnect = useEffectEvent(() => void connect()) useEffect(() => { if (autoConnectAttempted.current || !initialPairingInput || !parsedOffer) { return } autoConnectAttempted.current = true - void connect() - // Why: run once for the deep-linked offer; connect() reads current refs/state. - // eslint-disable-next-line react-hooks/exhaustive-deps + autoConnect() }, [initialPairingInput, parsedOffer]) const clear = (): void => { diff --git a/src/shared/agent-title-decoration.ts b/src/shared/agent-title-decoration.ts index b75cc5a138a..d0afe551972 100644 --- a/src/shared/agent-title-decoration.ts +++ b/src/shared/agent-title-decoration.ts @@ -4,7 +4,6 @@ // provider icon, this leading glyph reads as a redundant second icon, so strip // it from the displayed title. Scoped to titles we already know belong to an agent. const LEADING_AGENT_TITLE_DECORATION_RE = - // eslint-disable-next-line no-control-regex -- intentional unicode status-glyph ranges /^(?:[\u2733\u2726\u23F2\u25C7\u270B\u25B2\u2800-\u28FF\u25D0-\u25D3]+|[.*]\s)\s*/ export function stripLeadingAgentTitleDecorationOrEmpty(title: string): string { diff --git a/src/shared/agent-title-glyphs.ts b/src/shared/agent-title-glyphs.ts index a30f2d9a5be..fffd0c158b7 100644 --- a/src/shared/agent-title-glyphs.ts +++ b/src/shared/agent-title-glyphs.ts @@ -4,7 +4,6 @@ export const GEMINI_SILENT_WORKING = '\u23f2' // ⏲ export const GEMINI_IDLE = '\u25c7' // ◇ export const GEMINI_PERMISSION = '\u270b' // ✋ -// eslint-disable-next-line no-control-regex -- intentional unicode range export const BRAILLE_SPINNER_RE = /[\u2800-\u28ff]/g // Why: Claude Code 2.1.228 swapped its busy title spinner from braille to diff --git a/src/shared/ansi-escape-sequences.ts b/src/shared/ansi-escape-sequences.ts index 6220e99c3b8..4dd073d7538 100644 --- a/src/shared/ansi-escape-sequences.ts +++ b/src/shared/ansi-escape-sequences.ts @@ -13,7 +13,8 @@ type TerminatorSearch = { cursor: number; start: number; end: number } const OSC_INTRODUCER_PATTERN = /\u001b\]|\u009d/ const STRING_INTRODUCER_PATTERN = /\u001b[P_^X]|[\u0090\u0098\u009e\u009f]/ const CONTROL_STRING_INTRODUCER_PATTERN = /\u001b[\]P_^X]|[\u0090\u0098\u009d-\u009f]/ -const ANSI_INTRODUCER_PATTERN = /[\u001b\u0090\u0098\u009b\u009d-\u009f]/ +/** Any byte that can introduce an ANSI escape or control string. */ +export const ANSI_INTRODUCER_PATTERN = /[\u001b\u0090\u0098\u009b\u009d-\u009f]/ function nextSt(value: string, payloadStart: number, search: TerminatorSearch): void { if (search.start >= payloadStart || search.cursor === value.length) { diff --git a/src/shared/github/github-cache-key.ts b/src/shared/github/github-cache-key.ts index 939057b7911..12a4e5ce2a3 100644 --- a/src/shared/github/github-cache-key.ts +++ b/src/shared/github/github-cache-key.ts @@ -7,7 +7,10 @@ import { } from '../execution-host' import { taskSourceCacheScopeHostId } from '../task-source-context' -type RuntimeFocusSettings = Pick | null | undefined +export type RuntimeFocusSettings = + | Pick + | null + | undefined export function getGitHubRepoCacheKey( repoPath: string, diff --git a/src/shared/jcode-session-files.test.ts b/src/shared/jcode-session-files.test.ts index c3ffbb3c02d..4f7603270a8 100644 --- a/src/shared/jcode-session-files.test.ts +++ b/src/shared/jcode-session-files.test.ts @@ -7,9 +7,8 @@ const { homedirMock } = vi.hoisted(() => ({ homedirMock: vi.fn<() => string>() })) -vi.mock('os', async () => { - // oxlint-disable-next-line typescript/consistent-type-imports -- vi.importActual needs the inline import() to type the real module - const actual = await vi.importActual('os') +vi.mock(import('node:os'), async (importOriginal) => { + const actual = await importOriginal() return { ...actual, homedir: homedirMock } }) diff --git a/src/shared/rpc-contract/agent-launch-params.ts b/src/shared/rpc-contract/agent-launch-params.ts index be990829518..bc715bc74ee 100644 --- a/src/shared/rpc-contract/agent-launch-params.ts +++ b/src/shared/rpc-contract/agent-launch-params.ts @@ -27,15 +27,7 @@ import { SessionId } from './structured-agent-session-params' import { isStructuredAgentSessionIdFor } from '../structured-agent-session-create' import { AgentLaunchAccountEnv } from '../agent-launch-account-env' -const LaunchAgent = z - .unknown() - .superRefine((value, ctx) => { - if (!isTuiAgent(value)) { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Unknown TUI agent' }) - } - }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the superRefine above rejects anything isTuiAgent refuses, so the transform only ever runs on a TuiAgent. - .transform((value): TuiAgent => value as TuiAgent) +const LaunchAgent = z.custom(isTuiAgent, { message: 'Unknown TUI agent' }) /** The launch's fields without the cross-field check, for building an older host's shape in tests; * every receiver parses `AgentLaunch` or `AgentLaunchReplay`. */ diff --git a/src/shared/source-control-commit-failure.ts b/src/shared/source-control-commit-failure.ts index 29dbecd4dda..e8ac2cdfea0 100644 --- a/src/shared/source-control-commit-failure.ts +++ b/src/shared/source-control-commit-failure.ts @@ -1,3 +1,7 @@ +import { + stripAnsiEscapeSequences, + TERMINAL_CONTROL_CHARACTER_PATTERN +} from './ansi-escape-sequences' import type { GitStatusEntry } from './git-status-types' const FALLBACK_COMMIT_FAILURE_SUMMARY = 'Commit failed.' @@ -9,23 +13,15 @@ const COMMIT_FAILURE_PROMPT_OUTPUT_LIMIT = 12_000 const COMMIT_FAILURE_REPLY_INSTRUCTION = 'Reply with the root cause, files changed, validation run, final git status, and anything left for the user.' -const ANSI_PATTERN = - // eslint-disable-next-line no-control-regex - /[\u001b\u009b][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g -const CONTROL_PATTERN = - // eslint-disable-next-line no-control-regex - /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g const LOW_SIGNAL_LINE_PATTERN = /^(?:npm\s+(?:warn|warning)\b.*(?:env|config)|npm\s+notice\b|husky\s+-\s+deprecated\b)/i const HOOK_PATTERN = /\b(?:pre-commit|precommit|husky|lint-staged)\b/i const LINT_PATTERN = /\b(?:eslint|oxlint|lint-staged|lint)\b/i function normalizeCommitFailure(raw: string): string { - return raw - .slice(0, COMMIT_FAILURE_SUMMARY_SCAN_CODE_UNITS) - .replace(ANSI_PATTERN, '') + return stripAnsiEscapeSequences(raw.slice(0, COMMIT_FAILURE_SUMMARY_SCAN_CODE_UNITS)) .replace(/\r\n?/g, '\n') - .replace(CONTROL_PATTERN, '') + .replace(TERMINAL_CONTROL_CHARACTER_PATTERN, '') .trim() } diff --git a/src/shared/source-control-push-failure.ts b/src/shared/source-control-push-failure.ts index 41c5549f9b9..49db6401065 100644 --- a/src/shared/source-control-push-failure.ts +++ b/src/shared/source-control-push-failure.ts @@ -1,3 +1,7 @@ +import { + stripAnsiEscapeSequences, + TERMINAL_CONTROL_CHARACTER_PATTERN +} from './ansi-escape-sequences' import type { GitStatusEntry } from './git-status-types' const FALLBACK_PUSH_FAILURE_SUMMARY = 'Push failed.' @@ -10,12 +14,6 @@ export const PUSH_FAILURE_PROMPT_FILE_LIMIT = 40 const PUSH_FAILURE_REPLY_INSTRUCTION = 'Reply with the root cause, files changed, validation run, final git status, and anything left for the user.' -const ANSI_PATTERN = - // eslint-disable-next-line no-control-regex - /[\u001b\u009b][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g -const CONTROL_PATTERN = - // eslint-disable-next-line no-control-regex - /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g const LOW_SIGNAL_LINE_PATTERN = /^(?:npm\s+(?:warn|warning)\b.*(?:env|config)|npm\s+notice\b|husky\s+-\s+deprecated\b)/i const PUSH_HOOK_PATTERN = /\b(?:pre-push|prepush)\b/i @@ -26,11 +24,9 @@ const REMOTE_PUSH_EXCLUSION_PATTERN = /authentication failed|repository not found|not a git repository|does not appear to be a git repository|permission denied|protected branch|pre-receive hook declined|non-fast-forward|fetch first|updates were rejected|stale info|submodule|failed to push all needed submodules|unable to push submodule|unable to access|could not resolve host|network is unreachable|connection timed out|failed to connect|rpc failed|remote end hung up/i function normalizePushFailure(raw: string): string { - return raw - .slice(0, PUSH_FAILURE_SUMMARY_SCAN_CODE_UNITS) - .replace(ANSI_PATTERN, '') + return stripAnsiEscapeSequences(raw.slice(0, PUSH_FAILURE_SUMMARY_SCAN_CODE_UNITS)) .replace(/\r\n?/g, '\n') - .replace(CONTROL_PATTERN, '') + .replace(TERMINAL_CONTROL_CHARACTER_PATTERN, '') .trim() } diff --git a/src/shared/zod-salvage.ts b/src/shared/zod-salvage.ts index 2c64c33b770..71323884bda 100644 --- a/src/shared/zod-salvage.ts +++ b/src/shared/zod-salvage.ts @@ -197,9 +197,6 @@ export function salvagedField( export function salvagedOptional( name: string, schema: T -): z.ZodType | undefined, unknown> { - return salvaged(name, schema, () => undefined).optional() as z.ZodType< - z.output | undefined, - unknown - > +): z.ZodOptional | undefined, unknown>> { + return salvagedField(name, schema.optional(), () => undefined).optional() } diff --git a/src/wsl-guest/protocol.ts b/src/wsl-guest/protocol.ts index 383a15e222e..76f5fda32a7 100644 --- a/src/wsl-guest/protocol.ts +++ b/src/wsl-guest/protocol.ts @@ -78,6 +78,10 @@ const HANDSHAKE_OPTIONAL_STRING_FIELDS: Readonly< 'orca-relay-handshake-credential-mismatch': [] } +function isHandshakeType(value: unknown): value is HandshakeMessage['type'] { + return typeof value === 'string' && Object.hasOwn(HANDSHAKE_STRING_FIELDS, value) +} + export function parseHandshakeMessage(payload: Buffer): HandshakeMessage { const parsed: unknown = JSON.parse(payload.toString('utf-8')) if (typeof parsed !== 'object' || parsed === null) { @@ -86,24 +90,18 @@ export function parseHandshakeMessage(payload: Buffer): HandshakeMessage { // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the typeof/null guard directly above is exactly what makes this an index-able object; every read below still proves its own field. const msg = parsed as Record const t = msg.type - const required = - typeof t === 'string' && Object.hasOwn(HANDSHAKE_STRING_FIELDS, t) - ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: reached only when Object.hasOwn proved t is a key of this record, on the same line. - HANDSHAKE_STRING_FIELDS[t as HandshakeMessage['type']] - : null - if (required === null) { + if (!isHandshakeType(t)) { // Why typeof and not String(t): a peer-supplied `{ "type": { "toString": 1 } }` makes String() // itself throw "Cannot convert object to primitive value", replacing the one diagnostic this // line exists to produce. throw new Error(`Unknown handshake type: ${typeof t === 'string' ? t : typeof t}`) } - for (const field of required) { + for (const field of HANDSHAKE_STRING_FIELDS[t]) { if (typeof msg[field] !== 'string') { throw new Error(`Handshake field ${field} is not a string`) } } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the required === null bail above already refused every t that is not one of the four keys. - for (const field of HANDSHAKE_OPTIONAL_STRING_FIELDS[t as HandshakeMessage['type']]) { + for (const field of HANDSHAKE_OPTIONAL_STRING_FIELDS[t]) { if (msg[field] !== undefined && typeof msg[field] !== 'string') { throw new Error(`Handshake field ${field} is not a string`) } diff --git a/src/wsl-guest/wsl-install-plugins-handler.test.ts b/src/wsl-guest/wsl-install-plugins-handler.test.ts index 3b0116260a0..5e243033725 100644 --- a/src/wsl-guest/wsl-install-plugins-handler.test.ts +++ b/src/wsl-guest/wsl-install-plugins-handler.test.ts @@ -24,6 +24,13 @@ describe.skipIf(process.platform === 'win32')('createInstallPluginsHandler (gues } } + function requireOverlayDir(dir: string | undefined): string { + if (typeof dir !== 'string') { + throw new Error('expected the handler to return an overlay dir') + } + return dir + } + it('writes orca-opencode-status.js into the overlay and returns that dir', () => { withHome((home) => { const install = createInstallPluginsHandler(new PluginOverlayManager({ homeDir: home }), { @@ -36,8 +43,7 @@ describe.skipIf(process.platform === 'win32')('createInstallPluginsHandler (gues expect(res.installed.opencode).toBe(true) const dir = res.overlayDirs.opencode expect(typeof dir).toBe('string') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - const pluginPath = join(dir as string, 'plugins', 'orca-opencode-status.js') + const pluginPath = join(requireOverlayDir(dir), 'plugins', 'orca-opencode-status.js') expect(existsSync(pluginPath)).toBe(true) expect(readFileSync(pluginPath, 'utf8')).toBe(source) }) @@ -68,10 +74,9 @@ describe.skipIf(process.platform === 'win32')('createInstallPluginsHandler (gues const dir = res.overlayDirs.opencode2 expect(res.installed.opencode2).toBe(true) expect(typeof dir).toBe('string') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - expect(readFileSync(join(dir as string, 'plugins', 'orca-opencode2-status.js'), 'utf8')).toBe( - source - ) + expect( + readFileSync(join(requireOverlayDir(dir), 'plugins', 'orca-opencode2-status.js'), 'utf8') + ).toBe(source) expect(res.overlayDirs.opencode).toBeUndefined() }) }) @@ -83,8 +88,7 @@ describe.skipIf(process.platform === 'win32')('createInstallPluginsHandler (gues ORCA_WSL_HOOK_INSTANCE: 'inst1' }) const source = '// v1\n' - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - const dir = install({ opencodePluginSource: source }).overlayDirs.opencode as string + const dir = requireOverlayDir(install({ opencodePluginSource: source }).overlayDirs.opencode) // Why: a wipe-and-rebuild would delete this alongside the rest of the tree, // pulling the config root out from under an agent already running against it. @@ -113,8 +117,7 @@ describe.skipIf(process.platform === 'win32')('createInstallPluginsHandler (gues writeFileSync(join(userConfig, 'opencode.json'), '{"model":"late"}') env.ORCA_OPENCODE_SOURCE_CONFIG_DIR = userConfig - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - const dir = install({ opencodePluginSource: source }).overlayDirs.opencode as string + const dir = requireOverlayDir(install({ opencodePluginSource: source }).overlayDirs.opencode) expect(readFileSync(join(dir, 'opencode.json'), 'utf8')).toBe('{"model":"late"}') }) }) @@ -126,8 +129,7 @@ describe.skipIf(process.platform === 'win32')('createInstallPluginsHandler (gues ORCA_WSL_HOOK_INSTANCE: 'inst1' }) const source = '// v1\n' - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - const dir = install({ opencodePluginSource: source }).overlayDirs.opencode as string + const dir = requireOverlayDir(install({ opencodePluginSource: source }).overlayDirs.opencode) // Why: a rebuild that failed after the wipe leaves the dir but not the plugin; // an existsSync on the dir alone would call that a cache hit forever. rmSync(join(dir, 'plugins', 'orca-opencode-status.js')) @@ -145,8 +147,9 @@ describe.skipIf(process.platform === 'win32')('createInstallPluginsHandler (gues }) install({ opencodePluginSource: '// v1\n' }) // Why: a mid-session Orca upgrade ships new plugin source; future spawns must see it. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - const dir = install({ opencodePluginSource: '// v2\n' }).overlayDirs.opencode as string + const dir = requireOverlayDir( + install({ opencodePluginSource: '// v2\n' }).overlayDirs.opencode + ) expect(readFileSync(join(dir, 'plugins', 'orca-opencode-status.js'), 'utf8')).toBe('// v2\n') }) }) @@ -158,8 +161,7 @@ describe.skipIf(process.platform === 'win32')('createInstallPluginsHandler (gues ORCA_WSL_HOOK_INSTANCE: 'inst1' }) const source = '// v1\n' - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - const dir = install({ opencodePluginSource: source }).overlayDirs.opencode as string + const dir = requireOverlayDir(install({ opencodePluginSource: source }).overlayDirs.opencode) rmSync(dir, { recursive: true, force: true }) expect(install({ opencodePluginSource: source }).overlayDirs.opencode).toBe(dir) @@ -179,8 +181,9 @@ describe.skipIf(process.platform === 'win32')('createInstallPluginsHandler (gues ORCA_OPENCODE_SOURCE_CONFIG_DIR: userConfig, ORCA_WSL_HOOK_INSTANCE: 'inst1' }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - const dir = install({ opencodePluginSource: '// v1\n' }).overlayDirs.opencode as string + const dir = requireOverlayDir( + install({ opencodePluginSource: '// v1\n' }).overlayDirs.opencode + ) expect(readFileSync(join(dir, 'opencode.json'), 'utf8')).toBe('{"model":"user-set"}') expect(existsSync(join(dir, 'plugins', 'orca-opencode-status.js'))).toBe(true) @@ -198,8 +201,9 @@ describe.skipIf(process.platform === 'win32')('createInstallPluginsHandler (gues HOME: home, ORCA_WSL_HOOK_INSTANCE: 'inst1' }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Runtime validation or the local test fixture establishes the asserted shape. - const dir = install({ opencodePluginSource: '// v1\n' }).overlayDirs.opencode as string + const dir = requireOverlayDir( + install({ opencodePluginSource: '// v1\n' }).overlayDirs.opencode + ) expect(existsSync(join(dir, 'opencode.json'))).toBe(true) expect(existsSync(join(dir, 'plugins', 'orca-opencode-status.js'))).toBe(true) diff --git a/tests/e2e/__mocks__/native-chat-ssh-delivery.ts b/tests/e2e/__mocks__/native-chat-ssh-delivery.ts index cb091c59b61..f0da1937efa 100644 --- a/tests/e2e/__mocks__/native-chat-ssh-delivery.ts +++ b/tests/e2e/__mocks__/native-chat-ssh-delivery.ts @@ -10,6 +10,7 @@ import { writeUnverifiable, type WriteSettlement } from '../../../src/shared/pty-write-settlement' +import { toSshExecutionHostId } from '../../../src/shared/execution-host' import { createPtyWriteInput } from '../../../src/main/ipc/pty/ipc/write-input' import type { IPtyProvider } from '../../../src/main/providers/pty-provider-contract' import type * as PtyProviderRegistry from '../../../src/main/ipc/pty/provider/registry' @@ -45,11 +46,17 @@ vi.mock('@/runtime/runtime-terminal-stream', () => ({ : null })) +type SettlingPtyProvider = Omit & { + write: (id: string, data: string) => boolean + hasPty: (id: string) => boolean + dispose: () => void +} + /** A remote PTY owner whose write acknowledgments settle per `mode`, like a host transport. */ function createSettlingProvider( bytes: string[], mode: 'accepted' | 'lost' | 'slow' -): IPtyProvider & { hasPty: (id: string) => boolean; dispose: () => void } { +): SettlingPtyProvider { const timers = new Set>() const settleAfter = (ms: number, settlement: WriteSettlement): Promise => new Promise((resolve) => { @@ -82,7 +89,7 @@ function createSettlingProvider( } timers.clear() } - } as unknown as IPtyProvider & { hasPty: (id: string) => boolean; dispose: () => void } + } as unknown as SettlingPtyProvider } export function createSshDelivery(mode: 'accepted' | 'lost' | 'slow' = 'accepted') { @@ -93,7 +100,7 @@ export function createSshDelivery(mode: 'accepted' | 'lost' | 'slow' = 'accepted io.getProvider = () => provider const input = createPtyWriteInput({}) const id = 'ssh:connection@@pty-1' - ptyOwnership.set(id, 'connection') + ptyOwnership.set(id, toSshExecutionHostId('connection')) vi.stubGlobal('window', { api: { pty: { diff --git a/tests/e2e/__mocks__/structured-attention-read-surfaces.ts b/tests/e2e/__mocks__/structured-attention-read-surfaces.ts index c3465179103..6785ca0fa4e 100644 --- a/tests/e2e/__mocks__/structured-attention-read-surfaces.ts +++ b/tests/e2e/__mocks__/structured-attention-read-surfaces.ts @@ -1,5 +1,6 @@ import { vi } from 'vitest' import { RUNTIME_CAPABILITIES } from '../../../src/shared/protocol-version' +import type { NotificationsApi } from '../../../src/preload/api/os-permission-api' const transport = vi.hoisted(() => ({ call: vi.fn(), @@ -7,7 +8,7 @@ const transport = vi.hoisted(() => ({ supports: vi.fn(), dismiss: vi.fn(), settle: vi.fn(), - dispatch: vi.fn() + dispatch: vi.fn() })) vi.mock('@/runtime/runtime-rpc-client', async (original) => ({ ...(await original()), diff --git a/tests/e2e/agent-dashboard-status-burst.spec.ts b/tests/e2e/agent-dashboard-status-burst.spec.ts index 2b2d76fc280..2d913f727a7 100644 --- a/tests/e2e/agent-dashboard-status-burst.spec.ts +++ b/tests/e2e/agent-dashboard-status-burst.spec.ts @@ -37,12 +37,16 @@ test('keeps the visible Agent Dashboard interactive during a 100-pane status rep activate: false, id: 'agent-dashboard-burst-tab' }) + const settings = store.getState().settings + if (!settings) { + throw new Error('Settings are not loaded') + } store.setState({ agentDashboardDrawerOpen: false, settings: { - ...store.getState().settings, + ...settings, experimentalAgentDashboardPopout: true, - experimentalAgentDashboardMode: 'drawer', + experimentalAgentDashboardMode: 'in-window', experimentalAgentDashboardShowIdle: true, tabAutoGenerateTitle: false } diff --git a/tests/e2e/agent-session-live-force-exit-resume.spec.ts b/tests/e2e/agent-session-live-force-exit-resume.spec.ts index 2491fef72c6..891610099ed 100644 --- a/tests/e2e/agent-session-live-force-exit-resume.spec.ts +++ b/tests/e2e/agent-session-live-force-exit-resume.spec.ts @@ -20,6 +20,7 @@ import { import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store' import { attachRepoAndOpenTerminal, createRestartSession } from './helpers/orca-restart' import { PROTOCOL_VERSION } from '../../src/main/daemon/types' +import type { AgentProviderSessionKey } from '../../src/shared/agent-session-resume' const PROVIDER_SESSION_ID = 'e2e-live-force-exit-session' @@ -191,7 +192,8 @@ async function captureOutputRow(page: Page, expected: string, proofPath: string) test.describe.configure({ mode: 'serial' }) for (const agent of ['codex', 'cursor'] as const) { - const providerSessionKey = agent === 'cursor' ? 'conversation_id' : 'session_id' + const providerSessionKey: AgentProviderSessionKey = + agent === 'cursor' ? 'conversation_id' : 'session_id' test(`resumes a live ${agent} record after force-exit restart when pane PTY ownership is gone`, async (// oxlint-disable-next-line no-empty-pattern -- Playwright's second fixture arg is testInfo; the first must be an object destructure to opt out of the default fixture set. {}, testInfo) => { const repoPath = readFileSync(TEST_REPO_PATH_FILE, 'utf-8').trim() diff --git a/tests/e2e/agent-session-log-tail-stability.spec.ts b/tests/e2e/agent-session-log-tail-stability.spec.ts index e631a77d197..ec1b860173e 100644 --- a/tests/e2e/agent-session-log-tail-stability.spec.ts +++ b/tests/e2e/agent-session-log-tail-stability.spec.ts @@ -21,7 +21,6 @@ type MemorySample = { } declare global { - // eslint-disable-next-line no-var -- main-process global probe for this spec var __liveLogCrashProbe: CrashProbe | undefined } diff --git a/tests/e2e/ai-vault-typing-bench-renderer-probe.ts b/tests/e2e/ai-vault-typing-bench-renderer-probe.ts index c233aaaae50..782d746bd0a 100644 --- a/tests/e2e/ai-vault-typing-bench-renderer-probe.ts +++ b/tests/e2e/ai-vault-typing-bench-renderer-probe.ts @@ -76,7 +76,10 @@ export async function triggerVaultRefresh(page: Page): Promise { const target = window as Window & { __vaultBenchRefresh?: { startedAt: number; durationMs: number | null } } - const refresh = { startedAt: performance.now(), durationMs: null } + const refresh: { startedAt: number; durationMs: number | null } = { + startedAt: performance.now(), + durationMs: null + } target.__vaultBenchRefresh = refresh let sawBusy = false const observer = new MutationObserver(() => { diff --git a/tests/e2e/app-menu-paste-ownership.spec.ts b/tests/e2e/app-menu-paste-ownership.spec.ts index 81391c8bf1a..e233d022fda 100644 --- a/tests/e2e/app-menu-paste-ownership.spec.ts +++ b/tests/e2e/app-menu-paste-ownership.spec.ts @@ -72,7 +72,7 @@ async function getActiveTabTitle(page: Page, worktreeId: string): Promise entry.id === activeId) expect(tab).toBeDefined() - return tab!.customTitle ?? tab!.title ?? '' + return tab!.title ?? '' } function tabLocatorByTitle(page: Page, title: string): ReturnType { diff --git a/tests/e2e/artificial-opencode-main-pressure-scenario.ts b/tests/e2e/artificial-opencode-main-pressure-scenario.ts index 1c13a5d26a0..1a621e7f10d 100644 --- a/tests/e2e/artificial-opencode-main-pressure-scenario.ts +++ b/tests/e2e/artificial-opencode-main-pressure-scenario.ts @@ -32,6 +32,8 @@ type MainPressureSnapshot = { type MainPressureAckGate = { heldAckChars: number + heldAckCount: number + gatedPtyCount: number } type MainPressureSchedulerSnapshot = { @@ -214,8 +216,8 @@ async function measureAndAnnotateScroll< TMeasurement, TDebug, TScheduler extends MainPressureSchedulerSnapshot, - TMainPressure, - TAckGate + TMainPressure extends MainPressureSnapshot, + TAckGate extends MainPressureAckGate >({ annotationSuffix, deps, diff --git a/tests/e2e/automation-hidden-terminal-first-mount.spec.ts b/tests/e2e/automation-hidden-terminal-first-mount.spec.ts index 248fc3008a6..ae444b8dad5 100644 --- a/tests/e2e/automation-hidden-terminal-first-mount.spec.ts +++ b/tests/e2e/automation-hidden-terminal-first-mount.spec.ts @@ -96,7 +96,8 @@ test.describe('Automation hidden terminal first mount', () => { state.queueTabStartupCommand(tab.id, { command: `node -e "console.log('${marker}')"`, telemetry: { - launch_source: 'automation_hidden_first_mount_e2e', + agent_kind: 'other', + launch_source: 'unknown', request_kind: 'new' } }) diff --git a/tests/e2e/browser-floating-shortcut-scope.spec.ts b/tests/e2e/browser-floating-shortcut-scope.spec.ts index a7df8992b44..3e86d70258f 100644 --- a/tests/e2e/browser-floating-shortcut-scope.spec.ts +++ b/tests/e2e/browser-floating-shortcut-scope.spec.ts @@ -38,7 +38,11 @@ async function openFloatingBrowser( if (!store) { throw new Error('Store unavailable') } - store.setState({ settings: { ...store.getState().settings, floatingTerminalEnabled: true } }) + const settings = store.getState().settings + if (!settings) { + throw new Error('Settings are not loaded') + } + store.setState({ settings: { ...settings, floatingTerminalEnabled: true } }) const state = store.getState() const tab = state.createBrowserTab(worktreeId, initialUrl, { activate: true, diff --git a/tests/e2e/browser-tab.spec.ts b/tests/e2e/browser-tab.spec.ts index 48da3235a40..e258278743c 100644 --- a/tests/e2e/browser-tab.spec.ts +++ b/tests/e2e/browser-tab.spec.ts @@ -414,7 +414,11 @@ test.describe('Browser Tab', () => { webview.setZoomLevel(0.5) for (let reload = 0; reload < 3; reload += 1) { await new Promise((resolve) => { - webview.addEventListener('dom-ready', () => resolve(), { once: true }) + const onReady = (): void => { + webview.removeEventListener('dom-ready', onReady) + resolve() + } + webview.addEventListener('dom-ready', onReady) if (reload === 1) { webview.reloadIgnoringCache() } else { @@ -447,6 +451,7 @@ test.describe('Browser Tab', () => { .poll(async () => readBrowserInputValue(orcaPage, browserTab!.id), { timeout: 5_000 }) .not.toBeNull() + const zoomModifier: 'meta' | 'control' = process.platform === 'darwin' ? 'meta' : 'control' await orcaPage.evaluate( async ({ browserTabId, browserPageId, modifier }) => { const slot = document.querySelector(`[data-browser-overlay-tab-id="${browserTabId}"]`) @@ -465,7 +470,7 @@ test.describe('Browser Tab', () => { { browserTabId: browserTab!.id, browserPageId: browserTab!.pageId ?? browserTab!.id, - modifier: process.platform === 'darwin' ? 'meta' : 'control' + modifier: zoomModifier } ) await expect @@ -524,7 +529,11 @@ test.describe('Browser Tab', () => { const untouchedA = webviewA.getZoomLevel() await new Promise((resolve) => { - webviewA.addEventListener('dom-ready', () => resolve(), { once: true }) + const onReady = (): void => { + webviewA.removeEventListener('dom-ready', onReady) + resolve() + } + webviewA.addEventListener('dom-ready', onReady) webviewA.reload() }) diff --git a/tests/e2e/chinese-ime-chat-input-repro.spec.ts b/tests/e2e/chinese-ime-chat-input-repro.spec.ts index bb1eadd6698..13f6a15997b 100644 --- a/tests/e2e/chinese-ime-chat-input-repro.spec.ts +++ b/tests/e2e/chinese-ime-chat-input-repro.spec.ts @@ -648,7 +648,8 @@ test.describe('Chinese IME terminal chat input repro', () => { // Why: create the session/harness inside the try so a mid-setup throw // still hits finally and removes the harness script. writeFileSync(scriptPath, terminalImeHarnessScript(runId)) - session = await orcaPage.context().newCDPSession(orcaPage) + const cdpSession = await orcaPage.context().newCDPSession(orcaPage) + session = cdpSession await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`) harnessStarted = true await waitForTerminalOutput(orcaPage, `IME_HARNESS_READY_${runId}`, 10_000, 20_000) @@ -664,7 +665,7 @@ test.describe('Chinese IME terminal chat input repro', () => { await orcaPage.waitForTimeout(80) await dispatchSogouEmptyCompositionUpdate(orcaPage) await dispatchCandidateSelectionKey(session, { key: ' ', code: 'Space', keyCode: 32 }, () => - commitImeText(session, '你') + commitImeText(cdpSession, '你') ) await waitForLivePrompt(orcaPage, '你') await attachImeEvidence(orcaPage, testInfo, 'sogou-after-space-commit') @@ -682,7 +683,7 @@ test.describe('Chinese IME terminal chat input repro', () => { await orcaPage.waitForTimeout(80) await dispatchSogouEmptyCompositionUpdate(orcaPage) await dispatchCandidateSelectionKey(session, { key: '2', code: 'Digit2', keyCode: 50 }, () => - commitImeText(session, '你好') + commitImeText(cdpSession, '你好') ) await waitForLivePrompt(orcaPage, '你好') await attachImeEvidence(orcaPage, testInfo, 'sogou-after-digit-commit') @@ -702,7 +703,7 @@ test.describe('Chinese IME terminal chat input repro', () => { await dispatchSogouEmptyCompositionUpdate(orcaPage) await dispatchSogouPostCompositionEnd(orcaPage, '再见') await dispatchCandidateSelectionKey(session, { key: '3', code: 'Digit3', keyCode: 51 }, () => - commitImeText(session, '再见') + commitImeText(cdpSession, '再见') ) await waitForLivePrompt(orcaPage, '再见') const postCompositionLog = await readImeEventLog(orcaPage) diff --git a/tests/e2e/codex-account-switch-dismiss.spec.ts b/tests/e2e/codex-account-switch-dismiss.spec.ts index f85a2c22f5a..582ed163ea8 100644 --- a/tests/e2e/codex-account-switch-dismiss.spec.ts +++ b/tests/e2e/codex-account-switch-dismiss.spec.ts @@ -22,7 +22,11 @@ test('account switch actions return keyboard input to the terminal', async ({ const ptyId = await waitForActivePanePtyId(orcaPage) await orcaPage.evaluate( ({ ptyId, action }) => { - window.__store.getState().markCodexRestartNotices([ + const store = window.__store + if (!store) { + throw new Error('window.__store is unavailable') + } + store.getState().markCodexRestartNotices([ { ptyId, previousAccountLabel: 'Previous account', diff --git a/tests/e2e/codex-child-approval-activity-row.unit.test.ts b/tests/e2e/codex-child-approval-activity-row.unit.test.ts index c2b638ff7d4..c79b2c6029a 100644 --- a/tests/e2e/codex-child-approval-activity-row.unit.test.ts +++ b/tests/e2e/codex-child-approval-activity-row.unit.test.ts @@ -16,7 +16,10 @@ import type { AgentSessionStatusEvent, AgentSessionStatusSummary } from '../../src/shared/agent-session-wire' -import type { AgentJournalItemBody } from '../../src/shared/agent-session-journal-types' +import { + AGENT_JOURNAL_THREAD_SCOPE, + type AgentJournalItemBody +} from '../../src/shared/agent-session-journal-types' import { parseAgentJournalItemKey } from '../../src/shared/agent-session-journal-item-key' import type { Tab } from '../../src/shared/tab-types' import type { AppState } from '../../src/renderer/src/store/types' @@ -144,7 +147,13 @@ async function openHost() { const feed = new StructuredAgentSessionStatusFeed({ logger: createStructuredAgentSessionLogger(), sessions: new Map([ - [SESSION, indexedStatusFeedSession({ journal, child: { phase: 'ready' } })] + [ + SESSION, + indexedStatusFeedSession({ + journal, + child: { phase: 'ready', generation: null, fence: 1 } + }) + ] ]), getRecord: () => null, now: () => 1 @@ -194,7 +203,10 @@ async function openHost() { resolvedAt: tick() } } - await journal.appendItem(identity, resolved, { fence: 1 }) + await journal.appendItem(identity, resolved, { + fence: 1, + turnScope: AGENT_JOURNAL_THREAD_SCOPE + }) publish() translator.resolvePrompt(itemId) } @@ -227,7 +239,7 @@ describe("a Codex subagent's answered approval on the settled parent's Activity await host.journal.appendItem( { provider: 'orca', clientMessageId: 'prompt-1' }, { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'fan out' }] }, - { fence: 1 } + { fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE } ) host.on(CODEX_THREAD, 'turn/started', { turn: { id: 'parent-turn' } }) const spawn = { diff --git a/tests/e2e/codex-ctrl-c-status.spec.ts b/tests/e2e/codex-ctrl-c-status.spec.ts index 4ad240f1ad4..40ed4542d3c 100644 --- a/tests/e2e/codex-ctrl-c-status.spec.ts +++ b/tests/e2e/codex-ctrl-c-status.spec.ts @@ -62,7 +62,7 @@ process.stdout.write('ORCA_CODEX_INPUT_READY\\n') ` ) const terminalWindowsShell = await orcaPage.evaluate( - () => window.__store?.getState().settings.terminalWindowsShell + () => window.__store?.getState().settings?.terminalWindowsShell ) const shell = resolveStartupShell( process.platform, diff --git a/tests/e2e/combined-diff-scroll-restore.spec.ts b/tests/e2e/combined-diff-scroll-restore.spec.ts index da02f8d0bdd..fcc3e9c101f 100644 --- a/tests/e2e/combined-diff-scroll-restore.spec.ts +++ b/tests/e2e/combined-diff-scroll-restore.spec.ts @@ -360,7 +360,7 @@ async function clickVisibleDiffLine(page: Page): Promise { // asynchronously, so the visible .view-line set is briefly empty on a loaded // CI runner. Poll until a line is painted in the viewport instead of reading // it once and throwing on the first miss. - let linePoint: { x: number; y: number } | null = null + let linePoint: { x: number; y: number } | null | undefined await expect .poll( async () => { diff --git a/tests/e2e/completed-worker-retirement-resume.spec.ts b/tests/e2e/completed-worker-retirement-resume.spec.ts index f626b978118..ff055c2cd24 100644 --- a/tests/e2e/completed-worker-retirement-resume.spec.ts +++ b/tests/e2e/completed-worker-retirement-resume.spec.ts @@ -26,7 +26,7 @@ import { splitWorktreeIdForFilesystem } from '../../src/shared/worktree/id' const PROVIDER_SESSION_ID = '019feb51-2269-71c2-89c6-faa8dc65c8dc' const test = base.extend({ - launchEnv: [completedWorkerLaunchEnv, { option: true }] + launchEnv: completedWorkerLaunchEnv }) test.describe.configure({ mode: 'serial' }) @@ -79,7 +79,7 @@ for (const closeMode of ['terminal-close-cli', 'worker-release'] as const) { } const coordinatorBefore = terminalIdentity(coordinator) - let targetWorktreeId: string | null = null + let polledWorktreeId: string | null | undefined await expect .poll( async () => { @@ -89,19 +89,20 @@ for (const closeMode of ['terminal-close-cli', 'worker-release'] as const) { .flat() .map((worktree) => worktree.id) ) - targetWorktreeId = + polledWorktreeId = listed.result.worktrees.find( (worktree) => worktree.id !== coordinatorWorktreeId && rendererWorktreeIds.includes(worktree.id) )?.id ?? null - return targetWorktreeId + return polledWorktreeId }, { timeout: 60_000, message: 'runtime never registered the secondary worktree' } ) .not.toBeNull() - if (!targetWorktreeId) { + if (!polledWorktreeId) { throw new Error('The seeded repository did not expose its secondary worktree') } + const targetWorktreeId = polledWorktreeId const targetWorktreePath = splitWorktreeIdForFilesystem(targetWorktreeId)?.worktreePath if (!targetWorktreePath) { throw new Error('The secondary worktree did not expose a filesystem path') diff --git a/tests/e2e/completed-worker-retirement-resume.unit.test.ts b/tests/e2e/completed-worker-retirement-resume.unit.test.ts index 2478db8292b..b2629f2053b 100644 --- a/tests/e2e/completed-worker-retirement-resume.unit.test.ts +++ b/tests/e2e/completed-worker-retirement-resume.unit.test.ts @@ -1,6 +1,7 @@ import { getDefaultSettings } from '../../src/shared/constants' import path from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { z } from 'zod' import type { SleepingAgentSessionRecord } from '../../src/shared/agent-session-resume' import { makePaneKey } from '../../src/shared/stable-pane-id' import { parseWorkspaceSession } from '../../src/shared/workspace-session-schema' @@ -227,8 +228,16 @@ function expectCanaryUnchanged(): void { ) } -function orchestrationMethod(name: string) { - const method = ORCHESTRATION_METHODS.find((candidate) => candidate.name === name) +type OrchestrationMethod = (typeof ORCHESTRATION_METHODS)[number] + +const workerStartResult = z.object({ dispatchId: z.string(), state: z.string() }) + +function orchestrationMethod( + name: N +): Extract { + const method = ORCHESTRATION_METHODS.find( + (candidate): candidate is Extract => candidate.name === name + ) if (!method) { throw new Error(`Missing orchestration method: ${name}`) } @@ -246,11 +255,6 @@ async function releaseCompletedWorker(terminalState: 'running' | 'exited'): Prom coordinatorPaneKey }) const ctx: RpcContext = { runtime } - const call = async (name: string, params: Record) => { - const method = orchestrationMethod(name) - const parsed = method.params ? method.params.parse(params) : undefined - return method.handler(parsed, ctx) - } vi.spyOn(runtime, 'getTerminalPaneKey').mockImplementation((handle) => handle === 'terminal-coordinator' ? coordinatorPaneKey : ORIGINAL_PANE_KEY @@ -314,11 +318,13 @@ async function releaseCompletedWorker(terminalState: 'running' | 'exited'): Prom try { const task = db.createTask({ spec: 'release completed worker', runId: run.id }) - const started = (await call('orchestration.workerStart', { - task: task.id, - from: 'terminal-coordinator', - agent: 'codex' - })) as { dispatchId: string; state: string } + const workerStart = orchestrationMethod('orchestration.workerStart') + const started = workerStartResult.parse( + await workerStart.handler( + workerStart.params.parse({ task: task.id, from: 'terminal-coordinator', agent: 'codex' }), + ctx + ) + ) expect(started.state).toBe('ready') expect(db.getWorkerDispatch(started.dispatchId)?.state).toBe('ready') expect( @@ -331,8 +337,9 @@ async function releaseCompletedWorker(terminalState: 'running' | 'exited'): Prom ).toBe('settled') expect(db.getWorkerDispatch(started.dispatchId)?.state).toBe('succeeded') + const workerRelease = orchestrationMethod('orchestration.workerRelease') await expect( - call('orchestration.workerRelease', { dispatch: started.dispatchId }) + workerRelease.handler(workerRelease.params.parse({ dispatch: started.dispatchId }), ctx) ).resolves.toMatchObject({ dispatchId: started.dispatchId, state: 'released', diff --git a/tests/e2e/cross-version-wire/agent-session-handle-replacement-downgrade.unit.test.ts b/tests/e2e/cross-version-wire/agent-session-handle-replacement-downgrade.unit.test.ts index 23b47fd3c5c..64681ec236e 100644 --- a/tests/e2e/cross-version-wire/agent-session-handle-replacement-downgrade.unit.test.ts +++ b/tests/e2e/cross-version-wire/agent-session-handle-replacement-downgrade.unit.test.ts @@ -78,15 +78,24 @@ const REPLACED = [ }) ] -function storeMaps(value: unknown) { - if ( - typeof value !== 'object' || - value === null || - !('records' in value) || - !(value.records instanceof Map) || - !('unreadableRecords' in value) || - !(value.unreadableRecords instanceof Map) - ) { +type OldStoreMaps = { + records: Map + unreadableRecords: Map +} + +function hasStoreMaps(value: unknown): value is OldStoreMaps { + return ( + typeof value === 'object' && + value !== null && + 'records' in value && + value.records instanceof Map && + 'unreadableRecords' in value && + value.unreadableRecords instanceof Map + ) +} + +function storeMaps(value: unknown): OldStoreMaps { + if (!hasStoreMaps(value)) { throw new Error('old store must expose its readable and unreadable rows') } return value diff --git a/tests/e2e/cross-version-wire/agent-session-interrupted-tool-downgrade.unit.test.ts b/tests/e2e/cross-version-wire/agent-session-interrupted-tool-downgrade.unit.test.ts index 2138e3abb7a..9797ddd8c3c 100644 --- a/tests/e2e/cross-version-wire/agent-session-interrupted-tool-downgrade.unit.test.ts +++ b/tests/e2e/cross-version-wire/agent-session-interrupted-tool-downgrade.unit.test.ts @@ -2,6 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { expect, test } from 'vitest' +import { codexProviderHandle } from '../../../src/shared/agent-session-provider-handle-encoding' import { endedRunningAgentJournalToolCall, interruptedAgentJournalToolCall @@ -9,7 +10,11 @@ import { import type { AgentJournalItemIdentity } from '../../../src/shared/agent-session-journal-types' import { createTrackedJournalOpener } from '../../../src/main/native-chat/agent-session-journal/journal-host-database-test-support' import type { JournalRow } from '../../../src/main/native-chat/agent-session-journal/journal-row-schema' -import { importReleaseCheckoutModule, materializeReleaseCheckout } from './release-checkout' +import { + importReleaseCheckoutModule, + materializeReleaseCheckout, + releaseModuleExport +} from './release-checkout' // A release that predates a tool call's `endedAs`. Pinned, not the newest tag: what it must do is // read the cut-short call as failed, which no later release will contradict. @@ -21,15 +26,6 @@ function call(ordinal: number): AgentJournalItemIdentity { return { provider: 'codex', threadId: 'thread-1', turnId: 'turn-1', ordinal } } -function releaseExport(module: Record, name: string): T { - const value = module[name] - if (value === undefined) { - throw new Error(`the pinned release exports no ${name}`) - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: an export the pinned release has; each caller names the shape it uses, and a changed one fails the test. - return value as T -} - type OldRenderItem = { itemId: string; body: { kind: string; [field: string]: unknown } } type OldMessage = { blocks: unknown[] } | null type OldRunOutcome = ( @@ -49,7 +45,7 @@ test('an older host and client read a call a stop cut short as the failure they workspaceId: 'ws-1', hostId: 'host-1', agent: 'codex', - providerHandle: { kind: 'codex', threadId: 'thread-1' } + providerHandle: codexProviderHandle('thread-1') }, stateDirectory: directory, now: () => 1_000 @@ -88,27 +84,27 @@ test('an older host and client read a call a stop cut short as the failure they 'src/shared/native-chat-tool-run-outcome.ts' ].map((path) => importReleaseCheckoutModule(checkout, path)) ) - const createState = releaseExport<(sessionId: string, epoch: string) => unknown>( + const createState = releaseModuleExport<(sessionId: string, epoch: string) => unknown>( reducer, 'createJournalReducerState' ) - const applyRow = releaseExport<(state: unknown, row: JournalRow) => void>( + const applyRow = releaseModuleExport<(state: unknown, row: JournalRow) => void>( reducer, 'applyJournalRow' ) - const render = releaseExport<(state: unknown) => { items: OldRenderItem[] }>( + const render = releaseModuleExport<(state: unknown) => { items: OldRenderItem[] }>( reducer, 'renderJournalState' ) - const renderItemSchema = releaseExport<{ parse: (value: unknown) => OldRenderItem }>( + const renderItemSchema = releaseModuleExport<{ parse: (value: unknown) => OldRenderItem }>( schemas, 'AgentJournalRenderItemSchema' ) - const project = releaseExport<(item: unknown) => OldMessage>( + const project = releaseModuleExport<(item: unknown) => OldMessage>( projection, 'projectStructuredItemToNativeChat' ) - const runOutcome = releaseExport(outcome, 'nativeChatToolRunOutcome') + const runOutcome = releaseModuleExport(outcome, 'nativeChatToolRunOutcome') const settled = { activeTurnIsWorking: false } const blocksOf = (items: OldRenderItem[]) => diff --git a/tests/e2e/cross-version-wire/agent-session-newer-chat-refusal.unit.test.ts b/tests/e2e/cross-version-wire/agent-session-newer-chat-refusal.unit.test.ts index 237fde78161..da947337cd3 100644 --- a/tests/e2e/cross-version-wire/agent-session-newer-chat-refusal.unit.test.ts +++ b/tests/e2e/cross-version-wire/agent-session-newer-chat-refusal.unit.test.ts @@ -22,7 +22,8 @@ import { journalOpenRefusal } from '../../../src/main/native-chat/agent-session- import { importReleaseCheckoutModule, materializeReleaseCheckout, - resolveBaselineReleaseRef + resolveBaselineReleaseRef, + releaseModuleExport } from './release-checkout' /** @@ -74,16 +75,6 @@ afterAll(async () => { rmSync(directory, { recursive: true, force: true }) }) -/** A function the checked-out build exports, typed as the caller calls it. */ -function releaseExport(module: Record, name: string): T { - const value = module[name] - if (value === undefined) { - throw new Error(`the checked-out build exports no ${name}`) - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: an export the checked-out build defines; each caller names the shape it uses, and a changed one fails the test. - return value as T -} - const NEWER_BODY_KIND = { kind: 'plan-card', steps: [{ text: 'by a newer build' }] } const NEWER_NESTED_LITERAL = { kind: 'approval', @@ -163,11 +154,11 @@ async function releasedReader(ref: string) { checkout, 'src/shared/structured-agent-session-read-refusal.ts' ) - const parts = releaseExport(notice, 'agentSessionReadHistoryRefusalParts') - const english = releaseExport(notice, 'agentSessionWriteNoticeEnglish') - const isFinal = releaseExport(readRefusal, 'isFinalAgentSessionReadRefusal') + const parts = releaseModuleExport(notice, 'agentSessionReadHistoryRefusalParts') + const english = releaseModuleExport(notice, 'agentSessionWriteNoticeEnglish') + const isFinal = releaseModuleExport(readRefusal, 'isFinalAgentSessionReadRefusal') // The pane's own Retry stands beside the words, as on desktop. - const words = (refusal: Pick) => + const words = (refusal: { code: string; details?: unknown }) => english(parts(refusal.code, refusal.details, { retryControl: true })) return { words, isFinal } } @@ -228,7 +219,7 @@ describe('a chat a newer Orca saved, across versions', () => { checkout, `${JOURNAL}/journal-host-database-test-support.ts` ) - const older = releaseExport< + const older = releaseModuleExport< () => { open: (options: { identity: OlderJournalIdentity diff --git a/tests/e2e/cross-version-wire/agent-session-resume-marker-downgrade.unit.test.ts b/tests/e2e/cross-version-wire/agent-session-resume-marker-downgrade.unit.test.ts index 9d1f1a601e0..b8f862e6f2c 100644 --- a/tests/e2e/cross-version-wire/agent-session-resume-marker-downgrade.unit.test.ts +++ b/tests/e2e/cross-version-wire/agent-session-resume-marker-downgrade.unit.test.ts @@ -23,9 +23,13 @@ test('an older build reads the restart offer this build records at quit', async // This build: the offer teardown takes for a chat whose turn was running. const marker = structuredAgentSessionWorkingAtStop({ sessionId: SESSION, - session: { journal: journal([turnItem('turn-1', 'running')]), child: { fence: 1 } }, + session: { + journal: journal([turnItem('turn-1', 'running')]), + child: { fence: 1, phase: 'ready' } + }, getRecord: () => record(), childWork: () => undefined, + childWorkOpen: () => false, trigger: 'quit', teardownId: TEARDOWN_CURRENT, now: NOW diff --git a/tests/e2e/cross-version-wire/agent-session-stop-event-downgrade.unit.test.ts b/tests/e2e/cross-version-wire/agent-session-stop-event-downgrade.unit.test.ts index f2412b3c762..062428951ad 100644 --- a/tests/e2e/cross-version-wire/agent-session-stop-event-downgrade.unit.test.ts +++ b/tests/e2e/cross-version-wire/agent-session-stop-event-downgrade.unit.test.ts @@ -21,7 +21,11 @@ import { SAVED_BY_NEWER_ORCA } from '../../../src/main/native-chat/agent-session-journal/journal-host-database-test-support' import type { JournalRow } from '../../../src/main/native-chat/agent-session-journal/journal-row-schema' -import { importReleaseCheckoutModule, materializeReleaseCheckout } from './release-checkout' +import { + importReleaseCheckoutModule, + materializeReleaseCheckout, + releaseModuleExport +} from './release-checkout' import { agentJournalItemKey } from '../../../src/shared/agent-session-journal-item-key' import { agentSessionFailureFact } from '../../../src/shared/agent-session-failure' import { agentSessionFailureWords } from '../../../src/shared/agent-session-failure-words' @@ -92,11 +96,11 @@ test.each(['v1.4.219', 'v1.4.220', WRITABLE_BASELINE_REF])( const rawJson = JSON.stringify(since.rows) const checkout = await materializeReleaseCheckout(ref) const reducer = await importReleaseCheckoutModule(checkout, `${JOURNAL}/journal-reducer.ts`) - const create = releaseExport<(sessionId: string, epoch: string) => OldReplay['state']>( + const create = releaseModuleExport<(sessionId: string, epoch: string) => OldReplay['state']>( reducer, 'createJournalReducerState' ) - const fold = releaseExport<(state: OldReplay['state'], row: JournalRow) => void>( + const fold = releaseModuleExport<(state: OldReplay['state'], row: JournalRow) => void>( reducer, 'applyJournalRow' ) @@ -124,16 +128,6 @@ function item(ordinal: number): AgentJournalItemIdentity { return { provider: 'codex', threadId: 'thread-1', turnId: 'turn-1', ordinal } } -/** A function the pinned release exports, typed as the caller calls it. */ -function releaseExport(module: Record, name: string): T { - const value = module[name] - if (typeof value !== 'function') { - throw new Error(`the pinned release exports no ${name}`) - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a function the pinned release exports; each caller names the signature it calls, and a changed one fails the test. - return value as T -} - type OldReplay = { state: { items: Map } readOnly: boolean @@ -178,26 +172,24 @@ test("an older build keeps every row around a Stop's event, a Resume and a reope 'src/main/native-chat/agent-session-wire/agent-session-journal-batch.ts' ].map((path) => importReleaseCheckoutModule(checkout, path)) ) - const openJournalDatabase = releaseExport<(path: string) => { db: { close: () => void } }>( - database, - 'openJournalDatabase' - ) - const upsertJournalSessionRow = releaseExport< + const openJournalDatabase = releaseModuleExport< + (path: string) => { db: { close: () => void } } + >(database, 'openJournalDatabase') + const upsertJournalSessionRow = releaseModuleExport< (...args: [unknown, string, string, number]) => void >(table, 'upsertJournalSessionRow') - const insertJournalRow = releaseExport<(...args: [unknown, string, JournalRow]) => void>( + const insertJournalRow = releaseModuleExport<(...args: [unknown, string, JournalRow]) => void>( table, 'insertJournalRow' ) - const replayJournal = releaseExport<(...args: [unknown, boolean, string]) => OldReplay | null>( - open, - 'replayJournal' - ) - const renderJournalState = releaseExport<(state: unknown) => unknown>( + const replayJournal = releaseModuleExport< + (...args: [unknown, boolean, string]) => OldReplay | null + >(open, 'replayJournal') + const renderJournalState = releaseModuleExport<(state: unknown) => unknown>( reducer, 'renderJournalState' ) - const projectJournalBatch = releaseExport< + const projectJournalBatch = releaseModuleExport< (input: { rows: readonly JournalRow[]; snapshot: unknown; afterSequence: number }) => { ok: boolean batch?: { items: unknown[]; removedItemIds: string[] } @@ -282,7 +274,7 @@ test("an older build opens this build's journal writable and appends to it; the checkout, `${JOURNAL}/journal-host-database-test-support.ts` ) - const older = releaseExport<() => OlderOpener>(support, 'createTrackedJournalOpener')() + const older = releaseModuleExport<() => OlderOpener>(support, 'createTrackedJournalOpener')() try { const downgraded = await older.open({ identity: OLDER_IDENTITY, stateDirectory: directory }) expect(downgraded.isReadOnly).toBe(false) @@ -354,7 +346,7 @@ test("this build keeps a newer build's row kind and refuses the load; a build be checkout, `${JOURNAL}/journal-host-database-test-support.ts` ) - const older = releaseExport<() => OlderOpener>(support, 'createTrackedJournalOpener')() + const older = releaseModuleExport<() => OlderOpener>(support, 'createTrackedJournalOpener')() try { await older.open({ identity: OLDER_IDENTITY, stateDirectory: directory }) } finally { diff --git a/tests/e2e/cross-version-wire/cross-version-registered-agents.unit.test.ts b/tests/e2e/cross-version-wire/cross-version-registered-agents.unit.test.ts index 1c59cef5a5b..b10d1837931 100644 --- a/tests/e2e/cross-version-wire/cross-version-registered-agents.unit.test.ts +++ b/tests/e2e/cross-version-wire/cross-version-registered-agents.unit.test.ts @@ -38,7 +38,8 @@ import { importReleaseCheckoutModule, materializeReleaseCheckout, resolveBaselineReleaseRef, - type ReleaseCheckout + type ReleaseCheckout, + releaseModuleExport } from './release-checkout' import { loadAgentSessionWireBuild, @@ -73,15 +74,6 @@ type Baseline = { let baseline: Baseline let current: AgentSessionWireBuild -function member(module: Record, name: string): T { - const value = module[name] - if (value === undefined) { - throw new Error(`the baseline release exports no ${name}`) - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a release module is typed unknown; the name is the same export this build calls, and a missing one throws above. - return value as T -} - // v1.4.228 moved `isStructuredTab` to shared code; keeping both locations lets the rolling baseline // cross that move and an older pinned ref still load. const BASELINE_STRUCTURED_TAB_SOURCES = [ @@ -121,11 +113,14 @@ beforeAll(async () => { baseline = { ref, wire, - desktopClientCapabilities: member(capabilities, 'DESKTOP_RENDERER_RUNTIME_CLIENT_CAPABILITIES'), - projectSessionTabAgentStatus: member(projection, 'projectSessionTabAgentStatus'), - parseWorkspaceSession: member(schema, 'parseWorkspaceSession'), - isStructuredTab: member(tabs, 'isStructuredTab'), - isPersistedAgentSessionRecord: member(record, 'isPersistedAgentSessionRecord') + desktopClientCapabilities: releaseModuleExport( + capabilities, + 'DESKTOP_RENDERER_RUNTIME_CLIENT_CAPABILITIES' + ), + projectSessionTabAgentStatus: releaseModuleExport(projection, 'projectSessionTabAgentStatus'), + parseWorkspaceSession: releaseModuleExport(schema, 'parseWorkspaceSession'), + isStructuredTab: releaseModuleExport(tabs, 'isStructuredTab'), + isPersistedAgentSessionRecord: releaseModuleExport(record, 'isPersistedAgentSessionRecord') } }, SUITE_TIMEOUT_MS) diff --git a/tests/e2e/cross-version-wire/cross-version-stop-note-test-rig.ts b/tests/e2e/cross-version-wire/cross-version-stop-note-test-rig.ts index e7c790a4adf..12913e8147c 100644 --- a/tests/e2e/cross-version-wire/cross-version-stop-note-test-rig.ts +++ b/tests/e2e/cross-version-wire/cross-version-stop-note-test-rig.ts @@ -17,8 +17,10 @@ import { AGENT_JOURNAL_THREAD_SCOPE, type AgentJournalCursor, type AgentJournalItemBody, + type AgentJournalRenderItem, type AgentJournalTurnLifecycle } from '../../../src/shared/agent-session-journal-types' +import { isAdmissibleAgentJournalRenderItem } from '../../../src/shared/agent-session-journal-schemas' import type { AgentSessionHistoryPage, AgentSessionHistoryResult, @@ -145,6 +147,14 @@ export function createReleasedStopNoteRig(port: ScenarioPort, ref: string) { port.runtimeStub() ) } + /** The released client's schema decides admission; this build's guard types what it kept. */ + function admit(item: unknown): AgentJournalRenderItem { + const admitted = client.AgentJournalRenderItemSchema.parse(item) + if (!isAdmissibleAgentJournalRenderItem(admitted)) { + throw new Error('released client admitted an item this build cannot read') + } + return admitted + } async function page(params: Record): Promise { const reply = (await call('agentSession.history', { sessionId: SESSION, ...params }))[0] expect(reply?.ok).toBe(true) @@ -155,7 +165,7 @@ export function createReleasedStopNoteRig(port: ScenarioPort, ref: string) { } return { ...result.page, - items: result.page.items.map((item) => client.AgentJournalRenderItemSchema.parse(item)) + items: result.page.items.map(admit) } } async function subscribe( @@ -173,9 +183,7 @@ export function createReleasedStopNoteRig(port: ScenarioPort, ref: string) { ...event, batch: { ...event.batch, - items: event.batch.items.map((item) => - client.AgentJournalRenderItemSchema.parse(item) - ) + items: event.batch.items.map(admit) } } : event diff --git a/tests/e2e/cross-version-wire/cross-version-web-device-key.unit.test.ts b/tests/e2e/cross-version-wire/cross-version-web-device-key.unit.test.ts index 54528c28771..cd2201bc1fc 100644 --- a/tests/e2e/cross-version-wire/cross-version-web-device-key.unit.test.ts +++ b/tests/e2e/cross-version-wire/cross-version-web-device-key.unit.test.ts @@ -18,7 +18,7 @@ import { type AuthenticatedMobileSocket, type MobileSocketTransport } from '../../../src/main/runtime/rpc/mobile-socket-wiring' -import type { WebDeviceKeySupport } from '../../../src/renderer/src/web/web-device-key' +import type { KeyedWebDeviceKeySupport } from '../../../src/renderer/src/web/web-device-key' import { createMemoryWebDeviceKeyStore } from '../../../src/renderer/src/web/web-device-key-store' import { WebRuntimeConnectionTransport } from '../../../src/renderer/src/web/web-runtime-connection-transport' import { createWebRuntimeCredentialSource } from '../../../src/renderer/src/web/web-runtime-credential-source' @@ -168,7 +168,7 @@ const dialedUrls: string[] = [] function browserFor( token: string, - support: WebDeviceKeySupport, + support: KeyedWebDeviceKeySupport, storage: 'persisted' | 'session', store = createMemoryWebDeviceKeyStore(), pairingRevision = 1 @@ -284,7 +284,7 @@ describe('a browser moving from its pairing code to a device key', () => { currentHost = host([DEVICE_KEY_RUNTIME_CAPABILITY, DEVICE_KEY_EPHEMERAL_RUNTIME_CAPABILITY]) const paired = currentHost.registry.addDevice('Browser', 'runtime') currentHost.registry.updateLastSeen(paired.deviceId) - const browser = browserFor(paired.token, 'session-insecure-origin', 'persisted') + const browser = browserFor(paired.token, 'session-no-ed25519', 'persisted') await browser.transport.waitForConnected(5_000) expect(currentHost.authenticated.at(-1)?.device.deviceToken).toBe(paired.token) @@ -296,7 +296,7 @@ describe('a browser moving from its pairing code to a device key', () => { currentHost = host([DEVICE_KEY_RUNTIME_CAPABILITY, DEVICE_KEY_EPHEMERAL_RUNTIME_CAPABILITY]) const paired = currentHost.registry.addDevice('Browser', 'runtime') currentHost.registry.updateLastSeen(paired.deviceId) - const browser = browserFor(paired.token, 'session-insecure-origin', 'session') + const browser = browserFor(paired.token, 'session-no-ed25519', 'session') await browser.transport.waitForConnected(5_000) const keyed = currentHost.authenticated.at(-1)! diff --git a/tests/e2e/cross-version-wire/preflight-workspace-scoped-downgrade.unit.test.ts b/tests/e2e/cross-version-wire/preflight-workspace-scoped-downgrade.unit.test.ts index 6a25280a798..8b6d9c727c9 100644 --- a/tests/e2e/cross-version-wire/preflight-workspace-scoped-downgrade.unit.test.ts +++ b/tests/e2e/cross-version-wire/preflight-workspace-scoped-downgrade.unit.test.ts @@ -23,7 +23,8 @@ import { getRuntimeAgentInventoryKey } from '../../../src/renderer/src/store/sli import { importReleaseCheckoutModule, materializeReleaseCheckout, - resolveBaselineReleaseRef + resolveBaselineReleaseRef, + releaseModuleExport } from './release-checkout' // Why: a cold CI run extracts the baseline checkout before the first pairing. @@ -76,15 +77,6 @@ globalThis.window = { api: {} } const { createTestStore } = await import('../../../src/renderer/src/store/slices/store-test-helpers') -function member(module: Record, name: string): T { - const value = module[name] - if (value === undefined) { - throw new Error(`the baseline release exports no ${name}`) - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a release module is typed unknown; the name is the same export this build calls, and a missing one throws above. - return value as T -} - function hostBuild( capabilities: readonly string[], catalog: Record, @@ -117,14 +109,14 @@ beforeAll(async () => { ), importReleaseCheckoutModule(checkout, 'src/main/runtime/rpc/dispatcher-request-parsing.ts') ]) - const oldCatalog = member>(catalog, 'RPC_PARAMS_BY_METHOD') + const oldCatalog = releaseModuleExport>(catalog, 'RPC_PARAMS_BY_METHOD') oldHost = hostBuild( - member(protocol, 'RUNTIME_CAPABILITIES').filter( + releaseModuleExport(protocol, 'RUNTIME_CAPABILITIES').filter( (capability) => capability !== PREFLIGHT_WORKSPACE_SCOPED_RUNTIME_CAPABILITY ), // Why null: once a release ships the schema, the old side must still be the pre-schema host. { ...oldCatalog, 'preflight.detectAgents': null, 'preflight.refreshAgents': null }, - member(parsing, 'parseRpcRequestParams') + releaseModuleExport(parsing, 'parseRpcRequestParams') ) newHost = hostBuild(RUNTIME_CAPABILITIES, RPC_PARAMS_BY_METHOD, (request, method, meta) => // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the parser only reads `params` from the declaration. diff --git a/tests/e2e/cross-version-wire/release-checkout.ts b/tests/e2e/cross-version-wire/release-checkout.ts index 78b167bcf0d..9efefb46b1e 100644 --- a/tests/e2e/cross-version-wire/release-checkout.ts +++ b/tests/e2e/cross-version-wire/release-checkout.ts @@ -128,6 +128,16 @@ export function importReleaseCheckoutModule( return importModule(checkoutModulePath(checkout.root, rootRelativePath)) } +/** An export a release module must define; callers name the shape they use, so a changed one fails the test. */ +export function releaseModuleExport(module: Record, name: string): T { + const value = module[name] + if (value === undefined) { + throw new Error(`the release module exports no ${name}`) + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: release modules are typed unknown; a missing export throws above and a reshaped one fails its caller. + return value as T +} + /** * Import a copy of a working-tree module placed under the cache root. * diff --git a/tests/e2e/cross-version-wire/runtime-source-fence-downgrade.unit.test.ts b/tests/e2e/cross-version-wire/runtime-source-fence-downgrade.unit.test.ts index 55356acc0be..dd10ebd545f 100644 --- a/tests/e2e/cross-version-wire/runtime-source-fence-downgrade.unit.test.ts +++ b/tests/e2e/cross-version-wire/runtime-source-fence-downgrade.unit.test.ts @@ -25,7 +25,11 @@ import { publishHostDescriptor } from '../../../src/main/runtime/host-descriptor import type { RuntimeMetadata } from '../../../src/shared/runtime-bootstrap' import type { ExpectedRuntimeSource } from '../../../src/shared/runtime-rpc-envelope' import { sendRequest } from '../../../src/cli/runtime/transport' -import { importReleaseCheckoutModule, materializeReleaseCheckout } from './release-checkout' +import { + importReleaseCheckoutModule, + materializeReleaseCheckout, + releaseModuleExport +} from './release-checkout' // Why: a cold CI run extracts the baseline checkout before the first pairing. const SUITE_TIMEOUT_MS = 180_000 @@ -38,23 +42,14 @@ const SOURCE_B = '22222222-2222-4222-8222-222222222222' type Dispatcher = { dispatch(request: unknown): Promise } type HostBuild = (effect: () => unknown) => Dispatcher -function member(module: Record, name: string): T { - const value = module[name] - if (value === undefined) { - throw new Error(`the module exports no ${name}`) - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a release module is typed unknown; the name is the same export this build calls, and a missing one throws above. - return value as T -} - function hostBuildOf( dispatcher: Record, core: Record ): HostBuild { - const RpcDispatcher = member< + const RpcDispatcher = releaseModuleExport< new (options: { runtime: unknown; methods: unknown[] }) => Dispatcher >(dispatcher, 'RpcDispatcher') - const defineMethod = member<(declaration: Record) => unknown>( + const defineMethod = releaseModuleExport<(declaration: Record) => unknown>( core, 'defineMethod' ) diff --git a/tests/e2e/cross-version-wire/session-search-agent-set-downgrade.unit.test.ts b/tests/e2e/cross-version-wire/session-search-agent-set-downgrade.unit.test.ts index 495ec400947..e092771d9f4 100644 --- a/tests/e2e/cross-version-wire/session-search-agent-set-downgrade.unit.test.ts +++ b/tests/e2e/cross-version-wire/session-search-agent-set-downgrade.unit.test.ts @@ -139,7 +139,15 @@ test.each([{ supportedAgents: [] }, { supportedAgents: ['future-agent'] }])( const retrieve = vi.spyOn(harness.engine, 'search') const service = createSessionSearchService({ engine: harness.engine, - indexer: { status: unavailableSessionSearchStatus, reconcile: async () => {} } + indexer: { + status: () => ({ + ...unavailableSessionSearchStatus(), + messagesIndexed: 0, + degradedRoots: [], + sessionsByAgent: {} + }), + reconcile: async () => {} + } }) setSessionSearchService(service) const reply = await searchSessionService({ query: 'needle', supportedAgents }, 'runtime') @@ -168,19 +176,16 @@ test.each(['v1.4.211', LEGACY_REF])( 'src/shared/ai-vault-search-contract.ts' ) const parser = baseline.AiVaultSearchRequestSchema - if ( - !parser || - typeof parser !== 'object' || - !('parse' in parser) || - typeof parser.parse !== 'function' - ) { + const parse = + parser && typeof parser === 'object' && 'parse' in parser ? parser.parse : undefined + if (typeof parse !== 'function') { throw new Error('Pinned release has no request parser') } const call = vi.fn(async (method: string, request: Record) => { if (method === 'aiVault.searchStatus') { throw { code: 'method_not_found' } } - parser.parse(request) + parse.call(parser, request) expect(request).toMatchObject({ filters: { agents: AI_VAULT_AGENTS.filter( diff --git a/tests/e2e/cross-version-wire/structured-agent-session-host-fixture.ts b/tests/e2e/cross-version-wire/structured-agent-session-host-fixture.ts index 41cb2dde301..ec213d4001f 100644 --- a/tests/e2e/cross-version-wire/structured-agent-session-host-fixture.ts +++ b/tests/e2e/cross-version-wire/structured-agent-session-host-fixture.ts @@ -184,6 +184,7 @@ export function readyCodexProviderAdapter(input: { host: () => StructuredAgentSessionHost }): StructuredAgentSessionAdapter { const { store, now } = input + let dispatched = 0 return startsWhenPublished( { acquire: async ({ fence }) => ({ @@ -202,7 +203,18 @@ export function readyCodexProviderAdapter(input: { observedAt: now } }), - dispatch: async () => ({ state: 'accepted' }), + dispatch: async () => { + dispatched += 1 + return { + state: 'accepted', + providerIdentity: { + provider: 'codex', + threadId: input.thread, + turnId: `turn-${dispatched}`, + ordinal: dispatched + } + } + }, cancelTurn: async () => ({ cancelled: true }), answerPrompt: async () => undefined, setOption: async () => undefined diff --git a/tests/e2e/cross-version-wire/structured-agent-session-surface-manifest.ts b/tests/e2e/cross-version-wire/structured-agent-session-surface-manifest.ts index 5cfaefd5605..f4cb845e232 100644 --- a/tests/e2e/cross-version-wire/structured-agent-session-surface-manifest.ts +++ b/tests/e2e/cross-version-wire/structured-agent-session-surface-manifest.ts @@ -71,9 +71,14 @@ export function envelope(args: { } } -export function attachParams(fence: number | null): Record { - const params = { - envelope: { sessionId: SESSION, clientOperationId: operationId(), expectedRuntimeFence: fence }, +export function attachParams(fence: number | null): AgentSessionAttachParams { + const params: AgentSessionAttachParams = { + envelope: { + sessionId: SESSION, + clientOperationId: operationId(), + expectedRuntimeFence: fence, + payloadFingerprint: '' + }, location: { executionHostId: 'local', wslDistro: null, @@ -93,7 +98,7 @@ export function attachParams(fence: number | null): Record { payloadFingerprint: computeAgentSessionPayloadFingerprint({ method: 'agentSession.attach', sessionId: SESSION, - fields: attachFingerprintFields(params as unknown as AgentSessionAttachParams) + fields: attachFingerprintFields(params) }) } } diff --git a/tests/e2e/cross-version-wire/submission-positions-downgrade.unit.test.ts b/tests/e2e/cross-version-wire/submission-positions-downgrade.unit.test.ts index 0945d6a7728..90e25daea62 100644 --- a/tests/e2e/cross-version-wire/submission-positions-downgrade.unit.test.ts +++ b/tests/e2e/cross-version-wire/submission-positions-downgrade.unit.test.ts @@ -2,6 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { expect, test } from 'vitest' +import { codexProviderHandle } from '../../../src/shared/agent-session-provider-handle-encoding' import { agentSessionFailureFact } from '../../../src/shared/agent-session-failure' import { agentSessionFailureWords } from '../../../src/shared/agent-session-failure-words' import type { @@ -15,7 +16,11 @@ import { } from '../../../src/main/native-chat/agent-session-journal/journal-host-database-test-support' import { projectJournalBatch } from '../../../src/main/native-chat/agent-session-wire/agent-session-journal-batch' import { readAgentSessionHydrationPage } from '../../../src/main/native-chat/agent-session-wire/agent-session-history-page' -import { importReleaseCheckoutModule, materializeReleaseCheckout } from './release-checkout' +import { + importReleaseCheckoutModule, + materializeReleaseCheckout, + releaseModuleExport +} from './release-checkout' // Released clients that predate the published submission position and answered turn: each must // fold and draw a history page and an incremental batch carrying them exactly as it draws the same @@ -27,17 +32,7 @@ const IDENTITY: AgentSessionJournalIdentity = { workspaceId: 'ws-1', hostId: 'host-1', agent: 'codex', - providerHandle: { kind: 'codex', threadId: 'thread-1' } -} - -/** A function the pinned release exports, typed as the caller calls it. */ -function releaseExport(module: Record, name: string): T { - const value = module[name] - if (typeof value !== 'function' && (typeof value !== 'object' || value === null)) { - throw new Error(`the pinned release exports no ${name}`) - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a value the pinned release exports; each caller names the shape it uses, and a changed one fails the test. - return value as T + providerHandle: codexProviderHandle('thread-1') } type OldState = { submissions: Record[]; items: unknown[] } @@ -155,12 +150,12 @@ test.each(BASELINE_REFS)( checkout, 'src/shared/structured-agent-session-message-projection.ts' ) - const reduce = releaseExport<(state: unknown, action: unknown) => OldState>( + const reduce = releaseModuleExport<(state: unknown, action: unknown) => OldState>( reducer, 'reduceStructuredAgentSession' ) - const empty = releaseExport(reducer, 'EMPTY_STRUCTURED_AGENT_SESSION') - const project = releaseExport< + const empty = releaseModuleExport(reducer, 'EMPTY_STRUCTURED_AGENT_SESSION') + const project = releaseModuleExport< (items: unknown[], outbox: unknown[], submissions: unknown[]) => unknown[] >(projection, 'projectStructuredAgentSessionMessages') const draw = (state: OldState) => ({ diff --git a/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts b/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts index a0d4c4674c9..d244c13d0d6 100644 --- a/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts +++ b/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts @@ -90,12 +90,16 @@ async function loadClientProjection( load('/src/shared/structured-agent-session-projection.ts'), load('/src/shared/agent-session-journal-schemas.ts') ]) - const client = Object.assign({}, ...modules) + const client: Record = Object.assign({}, ...modules) + const itemSchema = client.AgentJournalRenderItemSchema if ( typeof client.reduceStructuredAgentSession !== 'function' || typeof client.projectStructuredItemsToNativeChat !== 'function' || !client.EMPTY_STRUCTURED_AGENT_SESSION || - typeof client.AgentJournalRenderItemSchema?.parse !== 'function' + typeof itemSchema !== 'object' || + itemSchema === null || + !('parse' in itemSchema) || + typeof itemSchema.parse !== 'function' ) { throw new Error('Release does not export the structured transcript reader') } diff --git a/tests/e2e/cross-version-wire/workspace-ports-host-scoped-downgrade.unit.test.ts b/tests/e2e/cross-version-wire/workspace-ports-host-scoped-downgrade.unit.test.ts index 28eb505c020..e9d37939f7e 100644 --- a/tests/e2e/cross-version-wire/workspace-ports-host-scoped-downgrade.unit.test.ts +++ b/tests/e2e/cross-version-wire/workspace-ports-host-scoped-downgrade.unit.test.ts @@ -22,7 +22,8 @@ import type { RpcAnyMethodDeclaration, RpcRequest } from '../../../src/main/runt import { importReleaseCheckoutModule, materializeReleaseCheckout, - resolveBaselineReleaseRef + resolveBaselineReleaseRef, + releaseModuleExport } from './release-checkout' // Why: a cold CI run extracts the baseline checkout before the first pairing. @@ -41,15 +42,6 @@ type HostBuild = { parse: (method: string, params: unknown) => unknown } -function member(module: Record, name: string): T { - const value = module[name] - if (value === undefined) { - throw new Error(`the baseline release exports no ${name}`) - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a release module is typed unknown; the name is the same export this build calls, and a missing one throws above. - return value as T -} - function hostBuild( capabilities: readonly string[], catalog: Record, @@ -84,16 +76,16 @@ beforeAll(async () => { importReleaseCheckoutModule(checkout, 'src/main/runtime/rpc/dispatcher-request-parsing.ts') ]) const oldCatalog = Object.fromEntries( - Object.entries(member>(catalog, 'RPC_PARAMS_BY_METHOD')).filter( - ([method]) => !HOST_SCOPED_METHODS.includes(method) - ) + Object.entries( + releaseModuleExport>(catalog, 'RPC_PARAMS_BY_METHOD') + ).filter(([method]) => !HOST_SCOPED_METHODS.includes(method)) ) oldHost = hostBuild( - member(protocol, 'RUNTIME_CAPABILITIES').filter( + releaseModuleExport(protocol, 'RUNTIME_CAPABILITIES').filter( (capability) => capability !== WORKSPACE_PORTS_HOST_SCOPED_RUNTIME_CAPABILITY ), oldCatalog, - member(parsing, 'parseRpcRequestParams') + releaseModuleExport(parsing, 'parseRpcRequestParams') ) newHost = hostBuild(RUNTIME_CAPABILITIES, RPC_PARAMS_BY_METHOD, (request, method, meta) => // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the parser only reads `params` from the declaration. diff --git a/tests/e2e/editable-context-paste-ownership.spec.ts b/tests/e2e/editable-context-paste-ownership.spec.ts index e6c3a7f37c9..8ff3be6a7d6 100644 --- a/tests/e2e/editable-context-paste-ownership.spec.ts +++ b/tests/e2e/editable-context-paste-ownership.spec.ts @@ -51,7 +51,7 @@ async function getActiveTabTitle(page: Page, worktreeId: string): Promise entry.id === activeId) expect(tab).toBeDefined() - return tab!.customTitle ?? tab!.title ?? '' + return tab!.title ?? '' } function tabLocatorByTitle(page: Page, title: string): ReturnType { diff --git a/tests/e2e/file-explorer-linked-watch-refresh.spec.ts b/tests/e2e/file-explorer-linked-watch-refresh.spec.ts index 602b85cf11a..0a5e2a3779d 100644 --- a/tests/e2e/file-explorer-linked-watch-refresh.spec.ts +++ b/tests/e2e/file-explorer-linked-watch-refresh.spec.ts @@ -36,7 +36,11 @@ test('refreshes followed directory links after target changes and retargeting', await waitForSessionReady(orcaPage) await waitForActiveWorktree(orcaPage) await orcaPage.evaluate(async () => { - await window.__store.getState().updateSettings({ followSymlinkedDirectories: true }) + const store = window.__store + if (!store) { + throw new Error('window.__store is unavailable') + } + await store.getState().updateSettings({ followSymlinkedDirectories: true }) }) const row = (name: string) => orcaPage.locator('[data-file-explorer-row]').filter({ diff --git a/tests/e2e/fixtures/daemon-generation-legacy-close-client.ts b/tests/e2e/fixtures/daemon-generation-legacy-close-client.ts index 473071c030b..10b5a09b688 100644 --- a/tests/e2e/fixtures/daemon-generation-legacy-close-client.ts +++ b/tests/e2e/fixtures/daemon-generation-legacy-close-client.ts @@ -162,7 +162,7 @@ async function main(): Promise { return true }, kill: () => false, - listProcesses: (options) => router.listProcesses(options), + listProcesses: (_hostId, options) => router.listProcesses(options), hasPty: (ptyId) => router.hasPty(ptyId), getForegroundProcess: (ptyId) => router.getForegroundProcess(ptyId) }) @@ -187,45 +187,45 @@ async function main(): Promise { } as never) runtime.attachWindow(1) - const snapshots: RuntimeMobileSessionTabsSnapshot[] = config.sessions.map((session, index) => { + const viewers = config.sessions.map((session, index) => { const leafId = `00000000-0000-4000-8000-${String(index + 1).padStart(12, '0')}` - return { + const tab = { + type: 'terminal' as const, + id: `${session.tabId}::${leafId}`, + parentTabId: session.tabId, + leafId, + ptyId: session.sessionId, + title: session.tabId, + isActive: true + } + const snapshot: RuntimeMobileSessionTabsSnapshot = { worktree: session.worktreeId, publicationEpoch: `legacy-viewer-${index + 1}`, snapshotVersion: 1, activeGroupId: null, - activeTabId: `${session.tabId}::${leafId}`, + activeTabId: tab.id, activeTabType: 'terminal', - tabs: [ - { - type: 'terminal', - id: `${session.tabId}::${leafId}`, - parentTabId: session.tabId, - leafId, - ptyId: session.sessionId, - title: session.tabId, - isActive: true - } - ] + tabs: [tab] } + return { session, tab, snapshot } }) runtime.syncWindowGraph(1, { - tabs: snapshots.map((snapshot) => ({ - tabId: snapshot.tabs[0]!.parentTabId, - worktreeId: snapshot.worktree, - title: snapshot.tabs[0]!.title, - activeLeafId: snapshot.tabs[0]!.leafId, + tabs: viewers.map(({ session, tab }) => ({ + tabId: tab.parentTabId, + worktreeId: session.worktreeId, + title: tab.title, + activeLeafId: tab.leafId, layout: null })), - leaves: snapshots.map((snapshot, index) => ({ - tabId: snapshot.tabs[0]!.parentTabId, - worktreeId: snapshot.worktree, - leafId: snapshot.tabs[0]!.leafId, + leaves: viewers.map(({ session, tab }, index) => ({ + tabId: tab.parentTabId, + worktreeId: session.worktreeId, + leafId: tab.leafId, paneRuntimeId: index + 1, - ptyId: config.sessions[index]!.sessionId, - paneTitle: snapshot.tabs[0]!.title + ptyId: session.sessionId, + paneTitle: tab.title })), - mobileSessionTabs: snapshots + mobileSessionTabs: viewers.map(({ snapshot }) => snapshot) }) const dispatcher = new RpcDispatcher({ runtime, methods: SESSION_TAB_METHODS }) diff --git a/tests/e2e/fixtures/daemon-generation-reconnect-client.ts b/tests/e2e/fixtures/daemon-generation-reconnect-client.ts index 6bd163cc474..ebe47682ac5 100644 --- a/tests/e2e/fixtures/daemon-generation-reconnect-client.ts +++ b/tests/e2e/fixtures/daemon-generation-reconnect-client.ts @@ -159,7 +159,7 @@ function createRuntimeClosePath( pendingShutdowns.push(shutdown) return true }, - listProcesses: (options) => router.listProcesses(options), + listProcesses: (_hostId, options) => router.listProcesses(options), hasPty: (ptyId) => router.hasPty(ptyId), getForegroundProcess: (ptyId) => router.getForegroundProcess(ptyId) }) @@ -271,6 +271,9 @@ async function main(): Promise { } router = await connectThroughDesktopDiscovery(config, burst) } + if (!router) { + throw new Error('Reconnect fixture needs at least one reconnect burst') + } await connectParallelRuntimeClients(config) const { dispatcher, targets, pendingShutdowns } = await createRuntimeClosePath(config, router) await dispatchFixtureCloseBursts({ dispatcher, worktreeId: WORKTREE_ID, targets }) diff --git a/tests/e2e/fixtures/daemon-lifecycle-entry.ts b/tests/e2e/fixtures/daemon-lifecycle-entry.ts index 0e09897b0c0..6cec9fec97d 100644 --- a/tests/e2e/fixtures/daemon-lifecycle-entry.ts +++ b/tests/e2e/fixtures/daemon-lifecycle-entry.ts @@ -2,7 +2,7 @@ import process from 'node:process' import { writeFileSync } from 'node:fs' import { startDaemon, type DaemonHandle } from '../../../src/main/daemon/daemon-main' import { serializeDaemonPidFile } from '../../../src/main/daemon/daemon-spawner' -import type { SubprocessHandle } from '../../../src/main/daemon/session' +import type { SubprocessHandle } from '../../../src/main/daemon/session-subprocess-handle' type FixtureArgs = { protocolVersion: number @@ -62,6 +62,7 @@ function createFixtureSubprocess(): SubprocessHandle { resize: () => {}, kill: () => exit(0), forceKill: () => exit(137), + terminateOwnedTree: () => 'unavailable', signal: () => {}, onData: (callback) => { onData = callback diff --git a/tests/e2e/github-cli-stall-repro.spec.ts b/tests/e2e/github-cli-stall-repro.spec.ts index 31a58f63b71..012dd404b13 100644 --- a/tests/e2e/github-cli-stall-repro.spec.ts +++ b/tests/e2e/github-cli-stall-repro.spec.ts @@ -4,6 +4,7 @@ import os from 'node:os' import path from 'node:path' import { test as base, expect } from './helpers/orca-app' import { waitForSessionReady } from './helpers/store' +import type { GitHubWorkItem } from '../../src/shared/github/work-item-types' const fakeGhDir = mkdtempSync(path.join(os.tmpdir(), 'orca-e2e-fake-gh-')) const fakeGhBody = `#!/usr/bin/env node @@ -51,13 +52,10 @@ if (process.platform === 'win32') { } const test = base.extend({ - launchEnv: [ - { - PATH: `${fakeGhDir}${path.delimiter}${process.env.PATH ?? ''}`, - ORCA_GH_EXEC_TIMEOUT_MS: '1000' - }, - { option: true } - ] + launchEnv: { + PATH: `${fakeGhDir}${path.delimiter}${process.env.PATH ?? ''}`, + ORCA_GH_EXEC_TIMEOUT_MS: '1000' + } }) test.afterAll(() => { @@ -101,7 +99,7 @@ test('GitHub Tasks drawer recovers when gh stalls on issue details', async ({ if (!repo) { throw new Error(`Expected repo to be loaded: ${repoPath}`) } - const item = { + const item: GitHubWorkItem = { id: 'issue-5388', type: 'issue', number: 5388, diff --git a/tests/e2e/github-created-issue-start-prefill.spec.ts b/tests/e2e/github-created-issue-start-prefill.spec.ts index 8934349e863..cd3202a861e 100644 --- a/tests/e2e/github-created-issue-start-prefill.spec.ts +++ b/tests/e2e/github-created-issue-start-prefill.spec.ts @@ -95,12 +95,9 @@ installFakeCli('gh', fakeGhSource) installFakeCli('claude', fakeClaudeSource) const test = base.extend({ - launchEnv: [ - { - PATH: `${fakeCliDir}${path.delimiter}${process.env.PATH ?? ''}` - }, - { option: true } - ] + launchEnv: { + PATH: `${fakeCliDir}${path.delimiter}${process.env.PATH ?? ''}` + } }) test.afterAll(() => { diff --git a/tests/e2e/headless-paired-remote-terminal-reveal-grid.spec.ts b/tests/e2e/headless-paired-remote-terminal-reveal-grid.spec.ts index 41728bc8d7e..b4dcccb918f 100644 --- a/tests/e2e/headless-paired-remote-terminal-reveal-grid.spec.ts +++ b/tests/e2e/headless-paired-remote-terminal-reveal-grid.spec.ts @@ -128,7 +128,7 @@ test('reveals a parked paired terminal once, at the pane grid rather than the ho } } // Why the extra worktrees: the most recent TERMINAL_WORKTREE_HOT_RETAIN_LIMIT worktrees stay warm. - const worktrees = [] + const worktrees: Awaited>[] = [] for (let index = 0; index <= WARM_WORKTREE_COUNT; index += 1) { worktrees.push(await createTerminalWorktree(index)) } diff --git a/tests/e2e/helpers/electron-home-isolation.ts b/tests/e2e/helpers/electron-home-isolation.ts index dfb2ee3d12b..7d2258ccb66 100644 --- a/tests/e2e/helpers/electron-home-isolation.ts +++ b/tests/e2e/helpers/electron-home-isolation.ts @@ -31,7 +31,8 @@ type ElectronHomeIsolationOptions = { } export type ElectronHomeIsolation = { - env: NodeJS.ProcessEnv + // Playwright's electron.launch rejects unset (undefined) env entries. + env: Record isolatedHome: string realHome: string } @@ -57,9 +58,13 @@ function assertOverlayDoesNotReplaceIsolation( } } -function stripAmbientHomeAndCodexEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { +function definedEnvEntries(env: NodeJS.ProcessEnv): [string, string][] { + return Object.entries(env).flatMap(([key, value]) => (value === undefined ? [] : [[key, value]])) +} + +function stripAmbientHomeAndCodexEnv(env: NodeJS.ProcessEnv): Record { return Object.fromEntries( - Object.entries(env).filter(([key]) => !RESTRICTED_ENV_KEYS.has(key.toUpperCase())) + definedEnvEntries(env).filter(([key]) => !RESTRICTED_ENV_KEYS.has(key.toUpperCase())) ) } @@ -96,7 +101,7 @@ export function createElectronHomeIsolation({ realHome, env: { ...stripAmbientHomeAndCodexEnv(inheritedEnv), - ...launchEnv, + ...Object.fromEntries(definedEnvEntries(launchEnv)), ...extraEnv, HOME: isolatedHome, USERPROFILE: isolatedHome, diff --git a/tests/e2e/helpers/headless-paired-runtime-host.ts b/tests/e2e/helpers/headless-paired-runtime-host.ts index acf7190b1f9..b6798a61917 100644 --- a/tests/e2e/helpers/headless-paired-runtime-host.ts +++ b/tests/e2e/helpers/headless-paired-runtime-host.ts @@ -157,14 +157,15 @@ export async function launchHeadlessPairedRuntimeHost( ], env: isolation.env }) - app = await launchServeProcess() + const launchedApp = await launchServeProcess() + app = launchedApp const [offer] = await Promise.all([ - readPairingOffer(app), + readPairingOffer(launchedApp), retryTransientMainEvaluate(() => - app.evaluate(({ app: electronApp }) => electronApp.getPath('home')) + launchedApp.evaluate(({ app: electronApp }) => electronApp.getPath('home')) ).then((home) => assertElectronResolvedIsolatedHome(home, isolation)) ]) - let serveProcess = app + let serveProcess = launchedApp // Why: a failed relaunch leaves only the already-closed app, which dispose must not close again // (that throw would replace the launch error). let serveProcessOpen = true @@ -192,6 +193,7 @@ export async function launchHeadlessPairedRuntimeHost( await readServeReadiness(relaunched, { requirePairingOffer: false }) }, dispose: async () => { + const socketDir = agentBrowserSocketDir await cleanupHeadlessHostResources([ ...(serveProcessOpen ? [ @@ -204,10 +206,10 @@ export async function launchHeadlessPairedRuntimeHost( () => cleanupE2EDaemons(userDataDir), () => preserveProfileLogs(userDataDir), () => rmSync(userDataDir, PROFILE_REMOVAL), - ...(agentBrowserSocketDir + ...(socketDir ? [ () => - rmSync(agentBrowserSocketDir, { + rmSync(socketDir, { recursive: true, force: true }) @@ -217,17 +219,20 @@ export async function launchHeadlessPairedRuntimeHost( } } } catch (error) { + const launchedApp = app + const socketDir = agentBrowserSocketDir try { await cleanupHeadlessHostResources([ - ...(app - ? [() => closeElectronAppForE2E(app, { cleanupDaemonsForUserDataDir: userDataDir })] + ...(launchedApp + ? [ + () => + closeElectronAppForE2E(launchedApp, { cleanupDaemonsForUserDataDir: userDataDir }) + ] : []), () => cleanupE2EDaemons(userDataDir), () => preserveProfileLogs(userDataDir), () => rmSync(userDataDir, PROFILE_REMOVAL), - ...(agentBrowserSocketDir - ? [() => rmSync(agentBrowserSocketDir, { recursive: true, force: true })] - : []) + ...(socketDir ? [() => rmSync(socketDir, { recursive: true, force: true })] : []) ]) } catch (cleanupError) { throw new AggregateError([error, cleanupError], 'Headless runtime startup and cleanup failed') diff --git a/tests/e2e/helpers/native-chat-view.ts b/tests/e2e/helpers/native-chat-view.ts index c2eae5633fb..60987280cbb 100644 --- a/tests/e2e/helpers/native-chat-view.ts +++ b/tests/e2e/helpers/native-chat-view.ts @@ -17,7 +17,7 @@ export async function showActivePaneAsNativeChat( } store.setState({ settings }) const state = store.getState() - state.setAgentStatus(paneKey, { state: 'idle', agentType, prompt: '' }, title, undefined, { + state.setAgentStatus(paneKey, { state: 'done', agentType, prompt: '' }, title, undefined, { worktreeId }) const [tabId] = paneKey.split(':') diff --git a/tests/e2e/helpers/nested-runtime-ssh-client-route.ts b/tests/e2e/helpers/nested-runtime-ssh-client-route.ts index bdef927c40a..6d0ac4fbffe 100644 --- a/tests/e2e/helpers/nested-runtime-ssh-client-route.ts +++ b/tests/e2e/helpers/nested-runtime-ssh-client-route.ts @@ -71,7 +71,7 @@ async function captureNestedTerminalRouteDiagnostic( }) : null return { - activeRuntimeEnvironmentId: state?.settings.activeRuntimeEnvironmentId ?? null, + activeRuntimeEnvironmentId: state?.settings?.activeRuntimeEnvironmentId ?? null, environmentId, environments: await window.api.runtimeEnvironments.list(), leafId, @@ -114,7 +114,7 @@ async function captureNestedTerminalRouteDiagnostic( ([owner, bucket]) => ({ owner, statuses: [...bucket.connectionStates.entries()], - targets: bucket.targets?.map((target) => target.id) ?? [], + targets: [...bucket.targetLabels.keys()], targetsHydrated: bucket.targetsHydrated }) ), diff --git a/tests/e2e/helpers/orcad-managed-workspace.ts b/tests/e2e/helpers/orcad-managed-workspace.ts index e1f111e3e7a..b1a089295cc 100644 --- a/tests/e2e/helpers/orcad-managed-workspace.ts +++ b/tests/e2e/helpers/orcad-managed-workspace.ts @@ -14,6 +14,7 @@ import { waitForSessionReady, waitForStartupWorktreeRefresh } from './store' import { dismissTransientAnnouncement } from './ssh-config-host-picker' import { focusActiveTerminalInput } from './terminal' import { toRuntimeExecutionHostId } from '../../../src/shared/execution-host' +import { getManagedOrcadSshTargetId } from '../../../src/shared/managed-orcad-execution-host' export const ORCAD_TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE @@ -136,7 +137,7 @@ export async function relaunchManagedApp( export async function managedEnvironmentId(page: Page, targetId: string): Promise { const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find( - (entry) => entry.orcadDeployment?.sshTargetId === targetId + (entry) => getManagedOrcadSshTargetId(entry) === targetId ) if (!environment) { throw new Error(`No managed server registered for ${targetId}`) diff --git a/tests/e2e/helpers/paired-terminal-cold-activation-oracle.ts b/tests/e2e/helpers/paired-terminal-cold-activation-oracle.ts index 19128fbc131..f2a1d0c8c65 100644 --- a/tests/e2e/helpers/paired-terminal-cold-activation-oracle.ts +++ b/tests/e2e/helpers/paired-terminal-cold-activation-oracle.ts @@ -95,8 +95,9 @@ export async function runPairedTerminalColdActivationOracle( noParent: true, startupCommand: fixture.command(firstMarker) }) - worktreeId = created.worktree.id - createdWorktreeIds.push(worktreeId) + const createdWorktreeId = created.worktree.id + worktreeId = createdWorktreeId + createdWorktreeIds.push(createdWorktreeId) if (!created.startupTerminal?.handle || !created.startupTerminal.tabId) { throw new Error('Paired cold-activation startup terminal was not created') } @@ -131,11 +132,11 @@ export async function runPairedTerminalColdActivationOracle( }) } - let originalPtyIds: string[] | null = null + const captured: { ptyIds: string[] | null } = { ptyIds: null } await expect .poll( async () => { - originalPtyIds = await page.evaluate( + captured.ptyIds = await page.evaluate( ({ tabIds, targetWorktreeId }) => { const tabs = window.__store?.getState().tabsByWorktree[targetWorktreeId] ?? [] const byId = new Map(tabs.map((tab) => [tab.id, tab.ptyId])) @@ -144,14 +145,15 @@ export async function runPairedTerminalColdActivationOracle( }, { tabIds: pendingTabs.map((tab) => tab.tabId), - targetWorktreeId: worktreeId + targetWorktreeId: createdWorktreeId } ) - return originalPtyIds + return captured.ptyIds }, { timeout: 30_000 } ) .not.toBeNull() + const originalPtyIds = captured.ptyIds if (originalPtyIds === null) { throw new Error('Paired cold-activation PTY ids were not captured') } diff --git a/tests/e2e/helpers/runtime-types.ts b/tests/e2e/helpers/runtime-types.ts index 7a2e6bdd2da..0b3876ac6a9 100644 --- a/tests/e2e/helpers/runtime-types.ts +++ b/tests/e2e/helpers/runtime-types.ts @@ -43,6 +43,8 @@ export type PaneManagerLike = { hasWebglRenderer(paneId: number): boolean getNumericIdForLeaf(leafId: string): number | null getLeafId(paneId: number): string | null + scheduleRevealRepaint(): void + scheduleRevealPresent(): void } export type ExplorerFileSummary = Pick diff --git a/tests/e2e/helpers/shortcuts.ts b/tests/e2e/helpers/shortcuts.ts index ca32b55f288..8b36a7b7ac0 100644 --- a/tests/e2e/helpers/shortcuts.ts +++ b/tests/e2e/helpers/shortcuts.ts @@ -30,7 +30,7 @@ export async function pressShortcut( key: string, options: ShortcutOptions = {} ): Promise { - const parts = [await getModifierKey(page)] + const parts: string[] = [await getModifierKey(page)] if (options.shift) { parts.push('Shift') } diff --git a/tests/e2e/helpers/xterm-render-dimensions.ts b/tests/e2e/helpers/xterm-render-dimensions.ts new file mode 100644 index 00000000000..3634cb12c14 --- /dev/null +++ b/tests/e2e/helpers/xterm-render-dimensions.ts @@ -0,0 +1,9 @@ +// Why: xterm exposes no public cell-size API, so rendering specs read the private render +// service's CSS cell dimensions; every level is optional so each read stays guarded. +export type XtermWithRenderDimensions = { + _core?: { + _renderService?: { + dimensions?: { css?: { cell?: { width: number; height: number } } } + } + } +} diff --git a/tests/e2e/host-agent-launch-settings-hook-bookkeeping.unit.test.ts b/tests/e2e/host-agent-launch-settings-hook-bookkeeping.unit.test.ts index 939bbcbd499..c150596fe20 100644 --- a/tests/e2e/host-agent-launch-settings-hook-bookkeeping.unit.test.ts +++ b/tests/e2e/host-agent-launch-settings-hook-bookkeeping.unit.test.ts @@ -62,7 +62,7 @@ describe('host form and committed-settings contract', () => { render( createElement( TooltipProvider, - {}, + null, createElement(HostAgentLaunchSettings, { environmentId: 'ssh-host', pairingRevision: 17, diff --git a/tests/e2e/issue-12656-terminal-link-tooltip.spec.ts b/tests/e2e/issue-12656-terminal-link-tooltip.spec.ts index 879c8305266..da6867d785c 100644 --- a/tests/e2e/issue-12656-terminal-link-tooltip.spec.ts +++ b/tests/e2e/issue-12656-terminal-link-tooltip.spec.ts @@ -130,19 +130,20 @@ test.describe('Issue #12656 terminal link tooltip', () => { ) await waitForTerminalOutput(orcaPage, url) - let probe: LinkProbe | null = null + const located: { probe: LinkProbe | null } = { probe: null } await expect .poll( async () => { - probe = await locateUrl(orcaPage, url) - return probe + located.probe = await locateUrl(orcaPage, url) + return located.probe }, { timeout: 5_000, message: 'URL did not become visible in the terminal viewport' } ) .not.toBeNull() - if (!probe) { + if (!located.probe) { throw new Error('URL probe disappeared before hover') } + let probe: LinkProbe = located.probe const idle = await readTooltipState(orcaPage, probe.tabId) expect(Math.abs(idle.paneBottom - idle.terminalBottom)).toBeLessThanOrEqual(1) await expect diff --git a/tests/e2e/korean-ime-terminal-shift-enter-commit.spec.ts b/tests/e2e/korean-ime-terminal-shift-enter-commit.spec.ts index 1ddfd56adb4..9a70fe18eb4 100644 --- a/tests/e2e/korean-ime-terminal-shift-enter-commit.spec.ts +++ b/tests/e2e/korean-ime-terminal-shift-enter-commit.spec.ts @@ -480,7 +480,10 @@ test.describe('Korean IME terminal committing Enter chords', () => { orcaPage, testRepoPath }, testInfo) => { - test.skip(chord.windowsOnly && process.platform !== 'win32', 'Windows IME ownership') + test.skip( + chord.windowsOnly === true && process.platform !== 'win32', + 'Windows IME ownership' + ) await waitForSessionReady(orcaPage) await waitForActiveWorktree(orcaPage) await ensureTerminalVisible(orcaPage) diff --git a/tests/e2e/large-diff-freeze-repro.spec.ts b/tests/e2e/large-diff-freeze-repro.spec.ts index 45ba0c8ec6c..3647f8628b0 100644 --- a/tests/e2e/large-diff-freeze-repro.spec.ts +++ b/tests/e2e/large-diff-freeze-repro.spec.ts @@ -242,7 +242,10 @@ test.describe('Large diff freeze repro', () => { // Why: reproduce stale snapshot behavior by opening combined diffs // as "unstaged" using entries captured from the staged status snapshot. - const staleUnstagedEntries = entries.map((entry) => ({ ...entry, area: 'unstaged' })) + const staleUnstagedEntries = entries.map((entry) => ({ + ...entry, + area: 'unstaged' as const + })) const intervalMs = 50 const samples: number[] = [] let last = performance.now() diff --git a/tests/e2e/linear-issue-view-persistence.spec.ts b/tests/e2e/linear-issue-view-persistence.spec.ts index 2993895ade4..c202131454b 100644 --- a/tests/e2e/linear-issue-view-persistence.spec.ts +++ b/tests/e2e/linear-issue-view-persistence.spec.ts @@ -199,7 +199,10 @@ async function installLinearPersistenceBackend( if (!teamId) { return [] } - return (payload.fixture.statesByTeamId as Record)[teamId] ?? [] + const statesByTeamId: Partial< + Record + > = payload.fixture.statesByTeamId + return statesByTeamId[teamId] ?? [] }) ipcMain.removeHandler('linear:teamLabels') diff --git a/tests/e2e/live-background-terminal-mount-authority.spec.ts b/tests/e2e/live-background-terminal-mount-authority.spec.ts index 9fade447e10..46396bf1d16 100644 --- a/tests/e2e/live-background-terminal-mount-authority.spec.ts +++ b/tests/e2e/live-background-terminal-mount-authority.spec.ts @@ -24,6 +24,7 @@ import type { } from '../../src/shared/runtime-types' import { PROTOCOL_VERSION } from '../../src/main/daemon/types' import { makePaneKey } from '../../src/shared/stable-pane-id' +import { getDefaultRepoHookSettings } from '../../src/shared/constants' import { buildFakeAgentCommandOverride, FAKE_AGENT_WINDOWS_SHELL @@ -79,16 +80,13 @@ const fakeCodexCommand = buildFakeAgentCommandOverride( ) const test = base.extend({ - launchEnv: [ - { - PATH: `${fakeCliDir}${path.delimiter}${process.env.PATH ?? ''}`, - ORCA_E2E_CODEX_SPAWN_LEDGER: spawnLedgerPath, - ORCA_E2E_SETUP_LEDGER: setupLedgerPath, - ORCA_E2E_CANARY_LEDGER: canaryLedgerPath, - ORCA_E2E_SIGNAL_LEDGER: signalLedgerPath - }, - { option: true } - ] + launchEnv: { + PATH: `${fakeCliDir}${path.delimiter}${process.env.PATH ?? ''}`, + ORCA_E2E_CODEX_SPAWN_LEDGER: spawnLedgerPath, + ORCA_E2E_SETUP_LEDGER: setupLedgerPath, + ORCA_E2E_CANARY_LEDGER: canaryLedgerPath, + ORCA_E2E_SIGNAL_LEDGER: signalLedgerPath + } }) function readSpawnLedger(): SpawnEvent[] { @@ -542,18 +540,20 @@ test('adopts runtime-owned agent and Setup PTYs on first mount', async ({ kind: 'git' }) const repoId = added.result.repo.id + const hookDefaults = getDefaultRepoHookSettings() await expect .poll(() => orcaPage.evaluate( - async ({ repoId, command, windowsShell }) => { + async ({ repoId, command, windowsShell, hookDefaults }) => { const state = window.__store?.getState() await state?.fetchRepos({ runtimeEnvironmentId: null, throwOnError: true }) const repo = window.__store?.getState().repos.find((candidate) => candidate.id === repoId) if (!repo) { return false } + const hookSettings = repo.hookSettings ?? hookDefaults await window.__store?.getState().updateRepo(repoId, { - hookSettings: { ...repo.hookSettings, setupAgentStartupPolicy: 'start-immediately' } + hookSettings: { ...hookSettings, setupAgentStartupPolicy: 'start-immediately' } }) await window.__store?.getState().updateSettings({ agentCmdOverrides: { codex: command }, @@ -563,7 +563,7 @@ test('adopts runtime-owned agent and Setup PTYs on first mount', async ({ }) return true }, - { repoId, command: fakeCodexCommand, windowsShell: FAKE_AGENT_WINDOWS_SHELL } + { repoId, command: fakeCodexCommand, windowsShell: FAKE_AGENT_WINDOWS_SHELL, hookDefaults } ) ) .toBe(true) diff --git a/tests/e2e/local-worktree-visibility-runtime-active.spec.ts b/tests/e2e/local-worktree-visibility-runtime-active.spec.ts index 79ba2f8ae16..b4d4309563d 100644 --- a/tests/e2e/local-worktree-visibility-runtime-active.spec.ts +++ b/tests/e2e/local-worktree-visibility-runtime-active.spec.ts @@ -64,9 +64,14 @@ test.describe('worktree visibility with a remote runtime active', () => { // Stage a remote runtime as active — the condition that triggered the drop. await orcaPage.evaluate(() => { - window.__store?.setState((current) => ({ - settings: { ...current.settings, activeRuntimeEnvironmentId: 'e2e-fake-runtime' } - })) + window.__store?.setState((current) => { + if (!current.settings) { + throw new Error('Settings are not hydrated') + } + return { + settings: { ...current.settings, activeRuntimeEnvironmentId: 'e2e-fake-runtime' } + } + }) }) // The fix: a CLI-created worktree must still appear, with no app restart. diff --git a/tests/e2e/markdown-tab-scroll-restore.spec.ts b/tests/e2e/markdown-tab-scroll-restore.spec.ts index 1b84c425270..c5100124b4c 100644 --- a/tests/e2e/markdown-tab-scroll-restore.spec.ts +++ b/tests/e2e/markdown-tab-scroll-restore.spec.ts @@ -83,7 +83,7 @@ test('restores the Markdown viewport when an image gains height after a tab swit .filter({ hasText: path.basename(filePath) }) .click() await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(4000) - await pendingImage.evaluate((element) => element.remove()) + await pendingImage.evaluate((element) => element.parentNode?.removeChild(element)) await expect .poll(() => image.evaluate((element) => element.getBoundingClientRect().height)) .toBeGreaterThan(500) diff --git a/tests/e2e/multi-client-navigation-isolation.spec.ts b/tests/e2e/multi-client-navigation-isolation.spec.ts index 1f281a9c264..2ac3be6ac46 100644 --- a/tests/e2e/multi-client-navigation-isolation.spec.ts +++ b/tests/e2e/multi-client-navigation-isolation.spec.ts @@ -294,14 +294,17 @@ test('shows only provider-backed creation actions in paired web', async ({ .poll(() => client.evaluate(() => { const state = window.__store?.getState() + if (!state) { + return false + } const worktree = state - ?.allWorktrees() + .allWorktrees() .find((candidate) => candidate.id === state.activeWorktreeId) const environmentId = worktree?.runtimeOwnerEnvironmentId return environmentId ? state.runtimeStatusByEnvironmentId .get(environmentId) - ?.status.capabilities?.includes('browser.screencast.v1') === true + ?.status?.capabilities?.includes('browser.screencast.v1') === true : false }) ) diff --git a/tests/e2e/native-chat-history-prepend-anchor.spec.ts b/tests/e2e/native-chat-history-prepend-anchor.spec.ts index cd0428c50bf..904096cfc98 100644 --- a/tests/e2e/native-chat-history-prepend-anchor.spec.ts +++ b/tests/e2e/native-chat-history-prepend-anchor.spec.ts @@ -162,7 +162,9 @@ test.describe('Native chat transcript anchoring', () => { .poll(() => transcriptWindow.locator(':scope > [data-index]').count()) .toBeGreaterThan(3) - const initialTotalSize = await transcriptWindow.evaluate((element) => element.offsetHeight) + const initialTotalSize = await transcriptWindow.evaluate( + (element: HTMLElement) => element.offsetHeight + ) const anchor = await scroll.evaluate(async (element) => { element.scrollTop = element.scrollHeight * 0.55 element.dispatchEvent(new Event('scroll', { bubbles: true })) @@ -207,7 +209,7 @@ test.describe('Native chat transcript anchoring', () => { // handler without Playwright first scrolling the reader to the transcript head. await loadEarlier.evaluate((button: HTMLButtonElement) => button.click()) await expect - .poll(() => transcriptWindow.evaluate((element) => element.offsetHeight)) + .poll(() => transcriptWindow.evaluate((element: HTMLElement) => element.offsetHeight)) .toBeGreaterThan(initialTotalSize) await expect(loadEarlier).toBeAttached({ timeout: 30_000 }) diff --git a/tests/e2e/native-chat-startup-option-pickers.unit.test.ts b/tests/e2e/native-chat-startup-option-pickers.unit.test.ts index d63d5612edc..321e5eaa412 100644 --- a/tests/e2e/native-chat-startup-option-pickers.unit.test.ts +++ b/tests/e2e/native-chat-startup-option-pickers.unit.test.ts @@ -128,6 +128,7 @@ it('picks model then effort through the menus during startup and uses both in th models: ['opus', 'sonnet'].map((id) => ({ id, label: id, + isDefault: id === 'opus', efforts: [ { value: 'low', label: 'Low' }, { value: 'high', label: 'High' } @@ -139,7 +140,7 @@ it('picks model then effort through the menus during startup and uses both in th render( createElement( TooltipProvider, - {}, + null, createElement(NativeChatSessionOptionPickers, { surface: { getSnapshot: () => snapshot, diff --git a/tests/e2e/nested-agent-idle-session-owner.spec.ts b/tests/e2e/nested-agent-idle-session-owner.spec.ts index 6dcd3b5dca0..f8e5af203b0 100644 --- a/tests/e2e/nested-agent-idle-session-owner.spec.ts +++ b/tests/e2e/nested-agent-idle-session-owner.spec.ts @@ -106,7 +106,7 @@ test('idle Claude keeps chat, mobile transcript, and return identity through nes await orcaPage.screenshot({ path: testInfo.outputPath('01-claude-before-nested.png') }) expect( await orcaPage.evaluate( - (key) => window.api.agentStatus.hasVerifiableAgentProcess(key), + async (key) => (await window.api.agentStatus.hasVerifiableAgentProcess?.(key)) ?? false, descriptor.paneKey ) ).toBe(true) diff --git a/tests/e2e/omp-model-picker.spec.ts b/tests/e2e/omp-model-picker.spec.ts index 0302d007299..832dc5b61bf 100644 --- a/tests/e2e/omp-model-picker.spec.ts +++ b/tests/e2e/omp-model-picker.spec.ts @@ -53,7 +53,7 @@ test('OMP model picker dispatches its advertised command and adopts the host rep state.setAgentStatus( paneKey, { - state: 'idle', + state: 'done', prompt: '', agentType: 'omp', model: 'anthropic/claude-sonnet-4-5', @@ -88,7 +88,7 @@ test('OMP model picker dispatches its advertised command and adopts the host rep window.__store?.getState().setAgentStatus( paneKey, { - state: 'idle', + state: 'done', prompt: '', agentType: 'omp', model: 'anthropic/claude-sonnet-4-6', diff --git a/tests/e2e/orchestration-legacy-worker-missing-terminal-recovery.spec.ts b/tests/e2e/orchestration-legacy-worker-missing-terminal-recovery.spec.ts index 9b5db8f59ec..3dd077a9423 100644 --- a/tests/e2e/orchestration-legacy-worker-missing-terminal-recovery.spec.ts +++ b/tests/e2e/orchestration-legacy-worker-missing-terminal-recovery.spec.ts @@ -269,6 +269,10 @@ test('a missing legacy worker cannot spawn a replacement during restart recovery return worker?.ptyId ?? null }) .toBeTruthy() + const workerPtyId = worker?.ptyId + if (!workerPtyId) { + throw new Error('Codex worker PTY was not listed') + } const workerPaneKey = `${worker!.tabId}:${worker!.leafId}` await expect .poll(async () => { @@ -327,9 +331,9 @@ test('a missing legacy worker cannot spawn a replacement during restart recovery await session.close(firstApp) firstApp = null - await removeDetachedDaemonSession(session.userDataDir, worker!.ptyId) + await removeDetachedDaemonSession(session.userDataDir, workerPtyId) await expect - .poll(() => detachedDaemonSessionExists(session.userDataDir, worker!.ptyId)) + .poll(() => detachedDaemonSessionExists(session.userDataDir, workerPtyId)) .toBe(false) await expect.poll(() => isProcessAlive(initialSpawn.pid)).toBe(false) rmSync(interruptionLedgerPath, { force: true }) @@ -343,7 +347,7 @@ test('a missing legacy worker cannot spawn a replacement during restart recovery .poll(async () => { const listed = await secondClient.call('terminal.list') return listed.result.terminals.filter( - (terminal) => terminal.ptyId === worker!.ptyId || terminal.title === 'Codex Ready' + (terminal) => terminal.ptyId === workerPtyId || terminal.title === 'Codex Ready' ) }) .toEqual([]) diff --git a/tests/e2e/orchestration-worker-settlement-release-cli.spec.ts b/tests/e2e/orchestration-worker-settlement-release-cli.spec.ts index 7e978049459..e8f77628f5f 100644 --- a/tests/e2e/orchestration-worker-settlement-release-cli.spec.ts +++ b/tests/e2e/orchestration-worker-settlement-release-cli.spec.ts @@ -85,14 +85,11 @@ if (process.platform === 'win32') { } const test = base.extend({ - launchEnv: [ - { - PATH: `${fakeCliDir}${path.delimiter}${process.env.PATH ?? ''}`, - ORCA_E2E_CLI_ENTRY: cliEntry, - ORCA_E2E_CLI_LEDGER: cliLedgerPath - }, - { option: true } - ] + launchEnv: { + PATH: `${fakeCliDir}${path.delimiter}${process.env.PATH ?? ''}`, + ORCA_E2E_CLI_ENTRY: cliEntry, + ORCA_E2E_CLI_LEDGER: cliLedgerPath + } }) type CliLedgerEntry = { diff --git a/tests/e2e/orchestration-worker-terminal-visibility.spec.ts b/tests/e2e/orchestration-worker-terminal-visibility.spec.ts index 9b3aa2bc2aa..3bdd709e72c 100644 --- a/tests/e2e/orchestration-worker-terminal-visibility.spec.ts +++ b/tests/e2e/orchestration-worker-terminal-visibility.spec.ts @@ -70,14 +70,11 @@ if (process.platform === 'win32') { } const test = base.extend({ - launchEnv: [ - { - PATH: `${fakeCliDir}${path.delimiter}${process.env.PATH ?? ''}`, - ORCA_E2E_SPAWN_LEDGER: spawnLedgerPath, - ORCA_E2E_INTERRUPTION_LEDGER: interruptionLedgerPath - }, - { option: true } - ] + launchEnv: { + PATH: `${fakeCliDir}${path.delimiter}${process.env.PATH ?? ''}`, + ORCA_E2E_SPAWN_LEDGER: spawnLedgerPath, + ORCA_E2E_INTERRUPTION_LEDGER: interruptionLedgerPath + } }) test.afterAll(() => { diff --git a/tests/e2e/orchestration-worker-transcript-providers.spec.ts b/tests/e2e/orchestration-worker-transcript-providers.spec.ts index 903c55ca24a..59d0c3fe43a 100644 --- a/tests/e2e/orchestration-worker-transcript-providers.spec.ts +++ b/tests/e2e/orchestration-worker-transcript-providers.spec.ts @@ -176,14 +176,11 @@ const agentCommands = Object.fromEntries( ) as Partial> const test = base.extend({ - launchEnv: [ - { - PATH: `${fakeCliDir}${path.delimiter}${process.env.PATH ?? ''}`, - GROK_HOME: fakeGrokHome, - OMP_CODING_AGENT_DIR: fakeOmpHome - }, - { option: true } - ] + launchEnv: { + PATH: `${fakeCliDir}${path.delimiter}${process.env.PATH ?? ''}`, + GROK_HOME: fakeGrokHome, + OMP_CODING_AGENT_DIR: fakeOmpHome + } }) test.afterAll(() => { diff --git a/tests/e2e/paired-quick-open-large-tree.spec.ts b/tests/e2e/paired-quick-open-large-tree.spec.ts index 683b7b435c3..e7868b963ba 100644 --- a/tests/e2e/paired-quick-open-large-tree.spec.ts +++ b/tests/e2e/paired-quick-open-large-tree.spec.ts @@ -7,8 +7,11 @@ import { type PairedElectronClient } from './helpers/paired-electron-client' import { createPairedQuickOpenLargeTreeFixture } from './helpers/paired-quick-open-large-tree-fixture' +import type { RuntimeFileListResult } from '../../src/shared/runtime-file-contracts' +import type { RuntimeWorktreeListResult } from '../../src/shared/runtime-worktree-contracts' import type { PairedQuickOpenLargeTreeFixture } from './helpers/paired-quick-open-large-tree-fixture' import { waitForSessionReady } from './helpers/store' +import { callPairedRuntime } from './helpers/paired-client-host-session' const QUICK_OPEN_SEARCH_DEBOUNCE_MS = 120 @@ -73,10 +76,12 @@ async function expectQuickOpenAndRuntimeHealthy( if (!response.ok) { throw new Error(`files.searchPaths oracle failed: ${JSON.stringify(response)}`) } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: runtimeEnvironments.call is untyped at the IPC boundary; files.searchPaths answers RuntimeFileListResult. + const result = response.result as RuntimeFileListResult const oracle = { - files: response.result.files.map((file) => file.relativePath), - totalCount: response.result.totalCount, - truncated: response.result.truncated + files: result.files.map((file) => file.relativePath), + totalCount: result.totalCount, + truncated: result.truncated } const encodedOracle = new TextEncoder().encode(JSON.stringify(oracle)) const digest = await crypto.subtle.digest('SHA-256', encodedOracle) @@ -93,19 +98,11 @@ async function expectQuickOpenAndRuntimeHealthy( ) for (const targetPath of [fixture.gitIgnoredTargetPath, fixture.orcaIgnoredTargetPath]) { const filename = targetPath.split('/').at(-1)! - const stat = await client.page.evaluate( - async ({ environmentId, worktreeId, targetPath }) => { - const response = await window.api.runtimeEnvironments.call({ - selector: environmentId, - method: 'files.stat', - params: { worktree: `id:${worktreeId}`, relativePath: targetPath } - }) - if (!response.ok) { - throw new Error(`files.stat oracle failed: ${JSON.stringify(response)}`) - } - return response.result - }, - { environmentId: client.environmentId, worktreeId, targetPath } + const stat = await callPairedRuntime<{ size: number; isDirectory: boolean }>( + client.page, + client.environmentId, + 'files.stat', + { worktree: `id:${worktreeId}`, relativePath: targetPath } ) expect(stat.isDirectory).toBe(false) expect(stat.size).toBeGreaterThanOrEqual(0) @@ -149,19 +146,13 @@ async function expectQuickOpenAndRuntimeHealthy( await expect(dialog).not.toContainText('Remote Orca runtime closed the connection') } - const response = await client.page.evaluate( - async ({ environmentId }) => - window.api.runtimeEnvironments.call({ - selector: environmentId, - method: 'worktree.list', - params: { limit: 10_000 } - }), - { environmentId: client.environmentId } + const { worktrees } = await callPairedRuntime( + client.page, + client.environmentId, + 'worktree.list', + { limit: 10_000 } ) - expect(response.ok).toBe(true) - if (response.ok) { - expect(response.result.worktrees.some((worktree) => worktree.id === worktreeId)).toBe(true) - } + expect(worktrees.some((worktree) => worktree.id === worktreeId)).toBe(true) } test('finds paths beyond the old prefix on a headed paired runtime @headful', async ({ diff --git a/tests/e2e/paired-remote-pane-layout-retry.spec.ts b/tests/e2e/paired-remote-pane-layout-retry.spec.ts index 778b6d0c93a..6672eb91cfe 100644 --- a/tests/e2e/paired-remote-pane-layout-retry.spec.ts +++ b/tests/e2e/paired-remote-pane-layout-retry.spec.ts @@ -1,5 +1,8 @@ import type { Page } from '@stablyai/playwright-test' -import type { RuntimeMobileSessionTabsResult } from '../../src/shared/runtime-types' +import type { + RuntimeMobileSessionTabsResult, + RuntimeMobileSessionTerminalClientTab +} from '../../src/shared/runtime-types' import { toWebTerminalSurfaceTabId } from '../../src/shared/terminal-surface-id' import type { TerminalLayoutSnapshot } from '../../src/shared/terminal-tab-types' import { @@ -77,8 +80,10 @@ async function readHostLayout( }) ).result return ( - snapshot.tabs.find((tab) => tab.type === 'terminal' && tab.parentTabId === hostTabId) - ?.parentLayout ?? null + snapshot.tabs.find( + (tab): tab is RuntimeMobileSessionTerminalClientTab => + tab.type === 'terminal' && tab.parentTabId === hostTabId + )?.parentLayout ?? null ) } @@ -117,7 +122,12 @@ test('retries an identical remote pane layout after reconnect', async ({ try { await host.client.call('repo.add', { path: testRepoPath, kind: 'git' }) - client = await launchPairedElectronClient(host.offer, testInfo, 'Pane layout retry client') + const pairedClient = await launchPairedElectronClient( + host.offer, + testInfo, + 'Pane layout retry client' + ) + client = pairedClient await expect .poll( () => @@ -173,10 +183,10 @@ test('retries an identical remote pane layout after reconnect', async ({ await expect .poll( () => - client.page.evaluate(async (selector) => { + pairedClient.page.evaluate(async (selector) => { const response = await window.api.runtimeEnvironments.connect({ selector }) return response.ok - }, client.environmentId), + }, pairedClient.environmentId), { timeout: 60_000, message: 'paired client never reconnected to the host runtime' } ) .toBe(true) diff --git a/tests/e2e/paired-remote-terminal-browser-link.spec.ts b/tests/e2e/paired-remote-terminal-browser-link.spec.ts index 3b2020e134d..cf2bdd59e05 100644 --- a/tests/e2e/paired-remote-terminal-browser-link.spec.ts +++ b/tests/e2e/paired-remote-terminal-browser-link.spec.ts @@ -9,6 +9,8 @@ import { sendClientGuestPointerInput } from './helpers/client-hosted-browser-observer' import { expect, test } from './helpers/orca-app' +import type { BrowserTabListResult } from '../../src/shared/runtime-browser-contracts' +import type { RuntimeMobileSessionTabsResult } from '../../src/shared/runtime-session-contracts' import { createRuntimeDesktopPairingOffer, launchPairedElectronClient, @@ -72,18 +74,19 @@ async function readTabInventory( if (!browserResponse.ok) { throw new Error('host browser page inventory unavailable') } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: runtimeEnvironments.call is untyped at the IPC boundary; session.tabs.list answers RuntimeMobileSessionTabsResult. + const sessionTabs = (sessionResponse.result as RuntimeMobileSessionTabsResult).tabs + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: runtimeEnvironments.call is untyped at the IPC boundary; browser.tabList answers BrowserTabListResult. + const browserPages = (browserResponse.result as BrowserTabListResult).tabs return { clientBrowserWorkspaces: (state?.browserTabsByWorktree[worktreeId] ?? []).length, clientBrowserTabs: (state?.unifiedTabsByWorktree[worktreeId] ?? []).filter( (tab) => tab.contentType === 'browser' ).length, clientTerminalTabs: (state?.tabsByWorktree[worktreeId] ?? []).length, - hostAuthoritativeBrowserTabs: sessionResponse.result.tabs.filter( - (tab) => tab.type === 'browser' - ).length, - hostRegisteredBrowserPages: browserResponse.result.tabs.length, - hostTerminalTabs: sessionResponse.result.tabs.filter((tab) => tab.type === 'terminal') - .length + hostAuthoritativeBrowserTabs: sessionTabs.filter((tab) => tab.type === 'browser').length, + hostRegisteredBrowserPages: browserPages.length, + hostTerminalTabs: sessionTabs.filter((tab) => tab.type === 'terminal').length } }, { environmentId, worktreeId } diff --git a/tests/e2e/paired-remote-terminal-host-restart-background-sync.spec.ts b/tests/e2e/paired-remote-terminal-host-restart-background-sync.spec.ts index 0d78b0c39fa..297cdb7fe50 100644 --- a/tests/e2e/paired-remote-terminal-host-restart-background-sync.spec.ts +++ b/tests/e2e/paired-remote-terminal-host-restart-background-sync.spec.ts @@ -304,7 +304,7 @@ async function moveHostAwayFromWorktree(page: Page, targetWorktreeId: string): P if (!state || !alternate) { return null } - state.setActiveView('editor') + state.setActiveView('terminal') state.setActiveWorktree(alternate.id) return alternate.id }, targetWorktreeId) diff --git a/tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts b/tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts index 237deec0399..7028cf399b2 100644 --- a/tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts +++ b/tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts @@ -147,7 +147,7 @@ async function createHostTerminal( hostTabId, sinkPath, terminal: result.tab.terminal, - webTabId: toWebTerminalSurfaceTabId(hostTabId, { environmentId, worktreeId }) + webTabId: toWebTerminalSurfaceTabId(hostTabId) } } diff --git a/tests/e2e/paired-remote-terminal-parked-scrollback-survives.spec.ts b/tests/e2e/paired-remote-terminal-parked-scrollback-survives.spec.ts index 9b4f3a1face..e49ec5cdbc0 100644 --- a/tests/e2e/paired-remote-terminal-parked-scrollback-survives.spec.ts +++ b/tests/e2e/paired-remote-terminal-parked-scrollback-survives.spec.ts @@ -34,7 +34,7 @@ import { randomUUID } from 'node:crypto' import os from 'node:os' import path from 'node:path' -import type { Page } from '@stablyai/playwright-test' +import type { Page, TestInfo } from '@stablyai/playwright-test' import { expect, test } from './helpers/orca-app' import { createRuntimeDesktopPairingOffer, @@ -311,7 +311,7 @@ async function runParkRevealScenario(args: { async function runScenario( orcaPage: Page, - testInfo: Parameters[1]>[1], + testInfo: TestInfo, clientName: string, parkKind: ParkKind ): Promise { diff --git a/tests/e2e/paired-remote-terminal-stall-recovery.spec.ts b/tests/e2e/paired-remote-terminal-stall-recovery.spec.ts index bf4aeb2a0ac..f8f15db4290 100644 --- a/tests/e2e/paired-remote-terminal-stall-recovery.spec.ts +++ b/tests/e2e/paired-remote-terminal-stall-recovery.spec.ts @@ -281,6 +281,7 @@ test('restarts one ACK-starved paired terminal stream without replacing its PTY .toBe(true) const observerOffer = await createRuntimeDesktopPairingOffer(orcaPage) observer = await launchPairedWebClient(electronApp, observerOffer) + const observerPage = observer.page await showHeadedClient(electronApp, client.page) await showHeadedClient(electronApp, observer.page) await expect @@ -388,7 +389,7 @@ test('restarts one ACK-starved paired terminal stream without replacing its PTY const observerOriginalPtyId = await waitForActivePanePtyId(observer.page, 30_000) expect(observerOriginalPtyId.split('@@').at(-1)).toBe(originalPtyId.split('@@').at(-1)) await expect - .poll(() => getTerminalContent(observer.page), { timeout: 30_000 }) + .poll(() => getTerminalContent(observerPage), { timeout: 30_000 }) .toContain('PAIRED_STALL_READY') await client.page.evaluate((target) => { @@ -485,7 +486,7 @@ test('restarts one ACK-starved paired terminal stream without replacing its PTY expect(await waitForActivePanePtyId(client.page, 30_000)).toBe(originalPtyId) await expect(tab).toHaveAttribute('data-active', 'true') await expect - .poll(() => getTerminalContent(observer.page), { timeout: 30_000 }) + .poll(() => getTerminalContent(observerPage), { timeout: 30_000 }) .toContain(`LIVE:${liveMarker}`) expect(await waitForActivePanePtyId(observer.page, 30_000)).toBe(observerOriginalPtyId) expect( @@ -589,7 +590,7 @@ test('restarts one ACK-starved paired terminal stream without replacing its PTY ), { tabId: webTabId, worktreeId: worktree.id } ), - observer.page.evaluate( + observerPage.evaluate( ({ tabId, worktreeId }) => (window.__store?.getState().tabsByWorktree[worktreeId] ?? []).some( (candidate) => candidate.id === tabId diff --git a/tests/e2e/paired-startup-exec-readiness.spec.ts b/tests/e2e/paired-startup-exec-readiness.spec.ts index cd96de038ce..0a93b618829 100644 --- a/tests/e2e/paired-startup-exec-readiness.spec.ts +++ b/tests/e2e/paired-startup-exec-readiness.spec.ts @@ -137,6 +137,7 @@ test('recovers the same startup exec through an isolated headless orca serve', a releasePath = path.join(homePath, `.sta4067-${runId}.release`) removeProfile = installZshExecProfile(homePath, runId, { releasePath, startedPath }) client = await launchPairedWebClient(host.app, host.offer, { waitForWorkspace: false }) + const clientPage = client.page await host.client.call('repo.add', { path: testRepoPath, kind: 'git' }) await client.page.locator('[data-worktree-sidebar]').waitFor({ state: 'visible', @@ -144,7 +145,7 @@ test('recovers the same startup exec through an isolated headless orca serve', a }) await expect .poll( - () => client.page.evaluate(() => window.__store?.getState().allWorktrees()[0]?.id ?? null), + () => clientPage.evaluate(() => window.__store?.getState().allWorktrees()[0]?.id ?? null), { timeout: 30_000 } ) .not.toBeNull() diff --git a/tests/e2e/plugin-marketplace-content.spec.ts b/tests/e2e/plugin-marketplace-content.spec.ts index a218339a617..de8d062905e 100644 --- a/tests/e2e/plugin-marketplace-content.spec.ts +++ b/tests/e2e/plugin-marketplace-content.spec.ts @@ -19,10 +19,10 @@ const execFileAsync = promisify(execFile) type MarketplaceFixture = { root: string home: string - gitEnvironment: NodeJS.ProcessEnv + gitEnvironment: Record } -function isolatedGitProcessEnv(gitEnvironment: NodeJS.ProcessEnv): NodeJS.ProcessEnv { +function isolatedGitProcessEnv(gitEnvironment: Record): NodeJS.ProcessEnv { return { ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_'))), ...gitEnvironment @@ -32,14 +32,14 @@ function isolatedGitProcessEnv(gitEnvironment: NodeJS.ProcessEnv): NodeJS.Proces async function runGit( cwd: string, args: string[], - gitEnvironment: NodeJS.ProcessEnv + gitEnvironment: Record ): Promise { await execFileAsync('git', args, { cwd, env: isolatedGitProcessEnv(gitEnvironment) }) } async function commitRepository( repository: string, - gitEnvironment: NodeJS.ProcessEnv + gitEnvironment: Record ): Promise { await runGit(repository, ['init', '--quiet'], gitEnvironment) await runGit(repository, ['checkout', '--quiet', '-b', 'main'], gitEnvironment) @@ -65,7 +65,7 @@ async function copyLaunchPlugin( repositories: string, repositoryName: string, launchDirectory: string, - gitEnvironment: NodeJS.ProcessEnv + gitEnvironment: Record ): Promise { const repository = join(repositories, `${repositoryName}.git`) await cp(join(process.cwd(), 'resources', 'plugins', 'launch', launchDirectory), repository, { @@ -74,11 +74,14 @@ async function copyLaunchPlugin( await commitRepository(repository, gitEnvironment) } -async function configureFixtureGit(home: string, repositories: string): Promise { +async function configureFixtureGit( + home: string, + repositories: string +): Promise> { const hooksDirectory = join(home, 'hooks') const configPath = join(home, '.gitconfig') await mkdir(hooksDirectory, { recursive: true }) - const gitEnvironment: NodeJS.ProcessEnv = { + const gitEnvironment: Record = { GIT_CONFIG_GLOBAL: configPath, GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0' diff --git a/tests/e2e/pr11346-selected-runtime-identity-oracle.ts b/tests/e2e/pr11346-selected-runtime-identity-oracle.ts index 1d1e4588b35..17505127c7a 100644 --- a/tests/e2e/pr11346-selected-runtime-identity-oracle.ts +++ b/tests/e2e/pr11346-selected-runtime-identity-oracle.ts @@ -5,6 +5,11 @@ import os from 'node:os' import path from 'node:path' import type { Page } from '@stablyai/playwright-test' import type { AppState } from '../../src/renderer/src/store/types' +import type { + DirectSshWorktreeFetchOptions, + WorktreeFetchOptions +} from '../../src/renderer/src/store/slices/worktree-helpers' +import type { HostQualifiedDetectedWorktreeResult } from '../../src/shared/detected-worktree-provider-contract' import { expect } from './helpers/orca-app' export function configureIsolatedGitIdentity(homePath: string): void { @@ -116,22 +121,27 @@ export async function installFinalActivationGate(page: Page, targetPath: string) release, waiting: false } - store.setState({ - fetchWorktrees: async (...args: Parameters) => { - const result = await originalFetchWorktrees(...args) - const targetRepo = store - .getState() - .repos.find( - (repo) => - repo.path === pathToGate && repo.executionHostId?.startsWith('runtime:') === true - ) - if (targetRepo?.id === args[0]) { - gateWindow.__pr11346ActivationGate!.waiting = true - await released - } - return result + const getStoreState = store.getState + function gatedFetchWorktrees( + repoId: string, + options: DirectSshWorktreeFetchOptions + ): Promise + function gatedFetchWorktrees(repoId: string, options?: WorktreeFetchOptions): Promise + async function gatedFetchWorktrees( + repoId: string, + options?: WorktreeFetchOptions + ): Promise { + const result = await originalFetchWorktrees(repoId, options) + const targetRepo = getStoreState().repos.find( + (repo) => repo.path === pathToGate && repo.executionHostId?.startsWith('runtime:') === true + ) + if (targetRepo?.id === repoId) { + gateWindow.__pr11346ActivationGate!.waiting = true + await released } - }) + return result + } + store.setState({ fetchWorktrees: gatedFetchWorktrees }) }, targetPath) } @@ -189,7 +199,7 @@ export async function injectSameIdLocalActivationCollision( repoId: localRepoId, path: localCollisionPath, hostId: 'local' as const, - runtimeOwnerEnvironmentId: null + runtimeOwnerEnvironmentId: undefined } store.setState({ repos: [ diff --git a/tests/e2e/project-group-manual-sort.spec.ts b/tests/e2e/project-group-manual-sort.spec.ts index 081cbc045bb..3f5ded42a70 100644 --- a/tests/e2e/project-group-manual-sort.spec.ts +++ b/tests/e2e/project-group-manual-sort.spec.ts @@ -134,7 +134,7 @@ async function seedDuplicateTabOrderProjectGroups( state.setGroupBy('repo') state.setProjectOrderBy('manual') - const groups = [] + const groupIds: string[] = [] for (const name of groupNames) { const created = await state.createProjectGroup(name, 'local') if (!created) { @@ -143,14 +143,14 @@ async function seedDuplicateTabOrderProjectGroups( // Why: existing profiles can have several groups with the same legacy rank; // the drag must still be able to insert one between its siblings. await state.updateProjectGroup(created.id, { tabOrder: 0 }) - groups.push(created) + groupIds.push(created.id) } return { - alphaId: groups[0]!.id, - bravoId: groups[1]!.id, - charlieId: groups[2]!.id, - deltaId: groups[3]!.id + alphaId: groupIds[0]!, + bravoId: groupIds[1]!, + charlieId: groupIds[2]!, + deltaId: groupIds[3]! } }, GROUP_NAMES) } diff --git a/tests/e2e/relay-region-correction.unit.test.ts b/tests/e2e/relay-region-correction.unit.test.ts index 7e37963a654..d37b03e31f4 100644 --- a/tests/e2e/relay-region-correction.unit.test.ts +++ b/tests/e2e/relay-region-correction.unit.test.ts @@ -12,7 +12,6 @@ import { createRelayServer } from '../../cloud/apps/relay/src/relay-server' import { IDLE_REHOME_MIN_CONTROL_AGE_MS } from '../../cloud/apps/relay/src/host-session-registry' import { CONTROL_RENEWAL_BATCH_INTERVAL_MS } from '../../cloud/apps/relay/src/control-renewal-batch' import { RELAY_PROTOCOL_LIMITS } from '../../cloud/packages/relay-contract/src/protocol-limits' -import type { RelayConfig } from '../../cloud/apps/relay/src/config' import type * as AdminTokenVerifier from '../../cloud/apps/relay/src/admin-token-verifier' import { RelayOriginPool } from '../../src/main/runtime/mobile-cloud-relay/relay-origin-pool' import { RELAY_HOST_CAPABILITY_HEADERS } from '../../src/main/runtime/mobile-cloud-relay/relay-control-protocol' @@ -100,6 +99,7 @@ async function topology() { assignmentSigningKey: new Uint8Array(32), adminAudience: 'https://director.example.test/v1/admin/drain', deployServiceAccount: 'deploy@example.test', + runtimeServiceAccount: 'relay-cell@example.test', rehomeAudience: 'https://director.example.test/v1/admin/host-drain', rehomeDirectorServiceAccount: 'director@example.test', databasePoolMax: 1, @@ -111,7 +111,7 @@ async function topology() { publicResolveWaitMs: 5_000, publicAssignmentRetryAfterSeconds: 5, regionCorrectionCohortPercent: 100 - } as RelayConfig, + }, database, { now: () => clock, random: () => 0.5, cellIncarnation: incarnations[index] } ) diff --git a/tests/e2e/remote-session-bulk-open-freeze-repro.spec.ts b/tests/e2e/remote-session-bulk-open-freeze-repro.spec.ts index ca23249df3e..ee6d6fac57b 100644 --- a/tests/e2e/remote-session-bulk-open-freeze-repro.spec.ts +++ b/tests/e2e/remote-session-bulk-open-freeze-repro.spec.ts @@ -114,8 +114,8 @@ test('R1 paired remote bulk-open freeze oracle @freeze-repro', async ({ }, testInfo) => { test.setTimeout(420_000) const host = await launchHeadlessPairedRuntimeHost() - let webClient: PairedWebClient | null = null - let desktopClient: PairedElectronClient | null = null + let webClient: PairedWebClient | undefined + let desktopClient: PairedElectronClient | undefined let disposeSessions: (() => Promise) | null = null try { const added = await host.client.call<{ repo: { id: string } }>('repo.add', { diff --git a/tests/e2e/remote-terminal-input-model-link.ts b/tests/e2e/remote-terminal-input-model-link.ts index b6d4c80d334..7d6d04d2eec 100644 --- a/tests/e2e/remote-terminal-input-model-link.ts +++ b/tests/e2e/remote-terminal-input-model-link.ts @@ -49,10 +49,11 @@ export function createModelLink(writeInput: (text: string) => Promise) let live: Connection | null = null const zombies: Connection[] = [] let counter = 0 - const initialMode: ModelLinkMode = 'pass' + // Why a call: a typed const still narrows to 'pass', which would type both fields as 'pass'. + const initialMode = (): ModelLinkMode => 'pass' const link = { - toHost: initialMode, - toClient: initialMode, + toHost: initialMode(), + toClient: initialMode(), /** Drops individual acks on an otherwise healthy connection. */ ackLoss: 0, unreachable: false, diff --git a/tests/e2e/remote-terminal-tab-retirement.unit.test.ts b/tests/e2e/remote-terminal-tab-retirement.unit.test.ts index dbe265a50d6..01f6a8e6bb0 100644 --- a/tests/e2e/remote-terminal-tab-retirement.unit.test.ts +++ b/tests/e2e/remote-terminal-tab-retirement.unit.test.ts @@ -131,15 +131,16 @@ describe('remote terminal tab retirement publication', () => { it('removes a permanent host exit from simultaneous viewers without stale resurrection', async () => { let session = makePersistedSession() const flushOrThrow = vi.fn() + const store = withDurableRuntimeStore({ + getWorkspaceSession: () => session, + setWorkspaceSession: (next: WorkspaceSessionState) => { + session = next + }, + flushOrThrow + }) const runtime = new OrcaRuntimeService( // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This retirement fixture implements the Store session and durability operations used by the runtime. - withDurableRuntimeStore({ - getWorkspaceSession: () => session, - setWorkspaceSession: (next) => { - session = next - }, - flushOrThrow - }) as never + store as never ) runtime.attachWindow(1) const staleLiveSnapshot = makeHostSnapshot() diff --git a/tests/e2e/renderer-crash-recovery-terminal-input.spec.ts b/tests/e2e/renderer-crash-recovery-terminal-input.spec.ts index 2b857402211..753af5d37bb 100644 --- a/tests/e2e/renderer-crash-recovery-terminal-input.spec.ts +++ b/tests/e2e/renderer-crash-recovery-terminal-input.spec.ts @@ -50,7 +50,6 @@ type CrashProbe = { } declare global { - // eslint-disable-next-line no-var -- main-process global probe for this spec var __crashProbe: CrashProbe | undefined } diff --git a/tests/e2e/repo-registration-async.spec.ts b/tests/e2e/repo-registration-async.spec.ts index 8e0c8a65e49..da7f7718b51 100644 --- a/tests/e2e/repo-registration-async.spec.ts +++ b/tests/e2e/repo-registration-async.spec.ts @@ -48,7 +48,7 @@ test('registration resolves nested and linked paths to one existing project', as await waitForSessionReady(orcaPage) const result = await orcaPage.evaluate( async ({ repoPath, nestedPath }) => { - const results = [] + const results: Awaited>[] = [] for (const path of [repoPath, nestedPath]) { results.push(await window.api.repos.add({ path })) } diff --git a/tests/e2e/restored-terminal-input-readiness.unit.test.ts b/tests/e2e/restored-terminal-input-readiness.unit.test.ts index 8eef3321f40..2bf759b46f4 100644 --- a/tests/e2e/restored-terminal-input-readiness.unit.test.ts +++ b/tests/e2e/restored-terminal-input-readiness.unit.test.ts @@ -57,7 +57,7 @@ describe('restored terminal input readiness', () => { it('retries after replay drops the first full input payload', async () => { let content = '' let attempts = 0 - const input = vi.fn((data: string) => { + const input = vi.fn((data: string, _wasUserInput?: boolean) => { attempts += 1 if (attempts >= 2) { content = data diff --git a/tests/e2e/setup-guide-sidebar.spec.ts b/tests/e2e/setup-guide-sidebar.spec.ts index eeaa105e80f..eb7d41ea1af 100644 --- a/tests/e2e/setup-guide-sidebar.spec.ts +++ b/tests/e2e/setup-guide-sidebar.spec.ts @@ -1,5 +1,6 @@ import type { ElectronApplication, Page } from '@stablyai/playwright-test' import type { SkillDiscoveryResult } from '../../src/shared/skills' +import type { Repo } from '../../src/shared/repo-types' import { test, expect } from './helpers/orca-app' import { getStoreState, waitForActiveWorktree, waitForSessionReady } from './helpers/store' @@ -191,7 +192,10 @@ async function seedCompletedSetupExceptCapabilityReadiness(page: Page): Promise< throw new Error('window.__store is not available') } const state = store.getState() - const existingRepo = state.repos[0] ?? { + if (!state.settings) { + throw new Error('settings are not loaded') + } + const existingRepo: Repo = state.repos[0] ?? { id: 'setup-guide-repo-a', path: '/tmp/setup-guide-repo-a', displayName: 'setup-guide-repo-a', @@ -199,7 +203,7 @@ async function seedCompletedSetupExceptCapabilityReadiness(page: Page): Promise< addedAt: Date.now(), kind: 'git' } - const primaryRepo = { + const primaryRepo: Repo = { ...existingRepo, kind: 'git', hookSettings: { @@ -260,7 +264,7 @@ async function seedCompletedSetupExceptCapabilityReadiness(page: Page): Promise< activeRuntimeEnvironmentId: null, defaultTuiAgent: 'codex', notifications: { - ...state.settings?.notifications, + ...state.settings.notifications, enabled: true, agentTaskComplete: true } @@ -304,7 +308,18 @@ async function seedCompletedSetupExceptCapabilityReadiness(page: Page): Promise< ] }, tabsByWorktree: { - [secondaryWorktree.id]: [{ id: 'setup-guide-terminal-tab', title: 'Terminal' }] + [secondaryWorktree.id]: [ + { + id: 'setup-guide-terminal-tab', + ptyId: null, + worktreeId: secondaryWorktree.id, + title: 'Terminal', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: Date.now() + } + ] }, terminalLayoutsByTabId: { 'setup-guide-terminal-tab': { @@ -313,7 +328,9 @@ async function seedCompletedSetupExceptCapabilityReadiness(page: Page): Promise< direction: 'horizontal', first: { type: 'leaf', leafId: 'setup-guide-left' }, second: { type: 'leaf', leafId: 'setup-guide-right' } - } + }, + activeLeafId: null, + expandedLeafId: null } }, setupGuideSidebarDismissed: false, diff --git a/tests/e2e/source-control-create-pr-intent-switch.spec.ts b/tests/e2e/source-control-create-pr-intent-switch.spec.ts index 2b6ad8bb9c4..60824469938 100644 --- a/tests/e2e/source-control-create-pr-intent-switch.spec.ts +++ b/tests/e2e/source-control-create-pr-intent-switch.spec.ts @@ -109,6 +109,7 @@ test.describe('Source Control Create PR intent worktree switching', () => { canCreate: false, blockedReason: 'needs_push' as const, nextAction: 'push' as const, + reviewLookupOutcome: 'not_found' as const, defaultBaseRef: primaryBranch, head: branch } @@ -119,6 +120,7 @@ test.describe('Source Control Create PR intent worktree switching', () => { canCreate: true, blockedReason: null, nextAction: null, + reviewLookupOutcome: 'not_found' as const, defaultBaseRef: primaryBranch, title: 'Create PR intent after switching worktrees', body: 'The intent flow should continue after navigation.', diff --git a/tests/e2e/source-control-create-pr.spec.ts b/tests/e2e/source-control-create-pr.spec.ts index f3c371f198e..d1d71f33324 100644 --- a/tests/e2e/source-control-create-pr.spec.ts +++ b/tests/e2e/source-control-create-pr.spec.ts @@ -28,16 +28,17 @@ async function openSourceControl(page: Page, expectedWorktreeId: string): Promis .poll( async () => page.evaluate((expectedWorktreeId) => { - const state = window.__store?.getState() - if (!state) { + const store = window.__store + if (!store) { return false } + const state = store.getState() if (state.activeWorktreeId !== expectedWorktreeId) { state.setActiveWorktree(expectedWorktreeId) } state.setRightSidebarOpen(true) state.setRightSidebarTab('source-control') - const current = window.__store.getState() + const current = store.getState() const activeWorktree = Object.values(current.worktreesByRepo) .flat() .some((entry) => entry.id === expectedWorktreeId) @@ -138,6 +139,7 @@ async function seedCreatePREligibleBranch( canCreate: true, blockedReason: null, nextAction: null, + reviewLookupOutcome: 'not_found' as const, defaultBaseRef: 'origin/main', head: branch } @@ -163,7 +165,7 @@ async function seedCreatePREligibleBranch( getHostedReviewCreationEligibility: async () => eligibility, fetchHostedReviewForBranch: async () => null, setUpstreamStatus: () => undefined, - fetchUpstreamStatus: async () => undefined, + fetchUpstreamStatus: async () => null, fetchPRForBranch: async (repoPath: string, targetBranch: string) => { store.setState((next) => ({ prCache: { diff --git a/tests/e2e/ssh-orcad-auto-convert.spec.ts b/tests/e2e/ssh-orcad-auto-convert.spec.ts index 60050531465..fc0e34f640a 100644 --- a/tests/e2e/ssh-orcad-auto-convert.spec.ts +++ b/tests/e2e/ssh-orcad-auto-convert.spec.ts @@ -46,6 +46,7 @@ import { openPairedClientTab } from './helpers/paired-host-terminal' import { expectTerminalAccessibilityText } from './helpers/terminal-accessibility-tree' +import { getOrcadSshDeployment } from '../../src/shared/runtime-environments' const HOST = process.env[ORCAD_CONVERT_HOST_ENV] const TEMPLATE_SOURCE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE @@ -157,7 +158,7 @@ test('a relay-era profile converts its SSH host on the first connect after upgra ) const environment = ( await upgraded.page.evaluate(() => window.api.runtimeEnvironments.list()) - ).find((entry) => entry.orcadDeployment?.sshTargetId === seeded.targetId) + ).find((entry) => getOrcadSshDeployment(entry)?.sshTargetId === seeded.targetId) if (!environment) { throw new Error('Converted host is missing from the environment catalog') } @@ -265,7 +266,7 @@ test('a host whose sshd refuses TCP forwarding runs a managed server over the st expect(target?.orcadFence).toBeTruthy() expect(target).not.toHaveProperty('managedServerUnavailable') const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find( - (entry) => entry.orcadDeployment?.sshTargetId === targetId + (entry) => getOrcadSshDeployment(entry)?.sshTargetId === targetId ) expect(environment, 'a managed server registered for the host').toBeTruthy() // sshd refuses every forward, so this call can only have ridden a stdio bridge. @@ -304,7 +305,7 @@ test('a host whose port 6768 another runtime holds still runs a managed server', .poll(() => managedServer(page, targetId), { timeout: 8 * 60_000 }) .toMatchObject({ kind: 'managed' }) const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find( - (entry) => entry.orcadDeployment?.sshTargetId === targetId + (entry) => getOrcadSshDeployment(entry)?.sshTargetId === targetId ) expect(environment, 'a managed server registered for the host').toBeTruthy() // The other listener drops every connection, so this call can only have reached orcad. @@ -469,7 +470,7 @@ test('reconnect restores the managed host name after its conversion catalog fail state.healCatalogFault?.() }) const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find( - (entry) => entry.orcadDeployment?.sshTargetId === seeded.targetId + (entry) => getOrcadSshDeployment(entry)?.sshTargetId === seeded.targetId ) if (!environment) { throw new Error('Managed host missing from main catalog') diff --git a/tests/e2e/ssh-orcad-browser-capabilities.spec.ts b/tests/e2e/ssh-orcad-browser-capabilities.spec.ts index 9451e7d9158..35c9dfffa03 100644 --- a/tests/e2e/ssh-orcad-browser-capabilities.spec.ts +++ b/tests/e2e/ssh-orcad-browser-capabilities.spec.ts @@ -5,9 +5,10 @@ import { startOrcadConvertHost } from './helpers/orcad-convert-host' import { reconnect, serverCall } from './helpers/orcad-convert-flow' import { runtimeAdvertisesBrowserClientHosting } from '../../src/shared/browser-client-hosting-eligibility' import { ORCAD_TEMPLATE, skipUnlessManagedDockerHost } from './helpers/orcad-managed-workspace' +import { getOrcadSshDeployment } from '../../src/shared/runtime-environments' skipUnlessManagedDockerHost() -test.use({ orcaAppExtraEnv: { ORCA_ORCAD_TEMPLATE_PATH: ORCAD_TEMPLATE } }) +test.use({ orcaAppExtraEnv: { ORCA_ORCAD_TEMPLATE_PATH: ORCAD_TEMPLATE ?? '' } }) test('a backendless managed host offers desktop-hosted pages and refuses its own engine', async ({ orcaPage: page @@ -24,7 +25,7 @@ test('a backendless managed host offers desktop-hosted pages and refuses its own return target.id }, target.input) const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find( - (entry) => entry.orcadDeployment?.sshTargetId === targetId + (entry) => getOrcadSshDeployment(entry)?.sshTargetId === targetId ) if (!environment) { throw new Error('Missing managed environment') diff --git a/tests/e2e/ssh-orcad-browser-routing.spec.ts b/tests/e2e/ssh-orcad-browser-routing.spec.ts index 5549570460e..0fa3d6bc3fe 100644 --- a/tests/e2e/ssh-orcad-browser-routing.spec.ts +++ b/tests/e2e/ssh-orcad-browser-routing.spec.ts @@ -25,6 +25,7 @@ import { sshExecutionHostStorageIdentity } from '../../src/main/browser/browser- import { reconnect } from './helpers/orcad-convert-flow' import { navigateGuest } from './helpers/browser-split-guest-probes' import { ORCAD_TEMPLATE, skipUnlessManagedDockerHost } from './helpers/orcad-managed-workspace' +import { getOrcadSshDeployment } from '../../src/shared/runtime-environments' skipUnlessManagedDockerHost() @@ -153,7 +154,7 @@ test('retained and new browser tabs keep SSH routing and login cookies after man expect(JSON.parse(server)).toMatchObject({ kind: 'managed' }) const environments = await page.evaluate(() => window.api.runtimeEnvironments.list()) const environment = environments.find( - (entry) => entry.orcadDeployment?.sshTargetId === remote.targetId + (entry) => getOrcadSshDeployment(entry)?.sshTargetId === remote.targetId ) if (!environment) { throw new Error('Missing managed environment') @@ -262,7 +263,7 @@ test('retained and new browser tabs keep SSH routing and login cookies after man path: '/echo/control', cookie: `${SSH_REMOTE_ONLY_COOKIE_NAME}=${SSH_REMOTE_ONLY_COOKIE_VALUE}` }) - const generation = environment.orcadDeployment?.sshTargetGeneration + const generation = getOrcadSshDeployment(environment)?.sshTargetGeneration if (generation === undefined) { throw new Error('Missing deployment registration') } diff --git a/tests/e2e/ssh-orcad-browser-service-status.spec.ts b/tests/e2e/ssh-orcad-browser-service-status.spec.ts index d3f95b58929..25ec7ab5b36 100644 --- a/tests/e2e/ssh-orcad-browser-service-status.spec.ts +++ b/tests/e2e/ssh-orcad-browser-service-status.spec.ts @@ -17,6 +17,7 @@ import { import { createRetentionFixtureDirectory } from './helpers/host-created-terminal-retention-oracle' import { reconnect, serverCall } from './helpers/orcad-convert-flow' import { ORCAD_TEMPLATE, skipUnlessManagedDockerHost } from './helpers/orcad-managed-workspace' +import { getOrcadSshDeployment } from '../../src/shared/runtime-environments' skipUnlessManagedDockerHost() @@ -64,7 +65,7 @@ test('an unavailable browser on a responding managed host does not report a serv expect(JSON.parse(server)).toMatchObject({ kind: 'managed' }) const environments = await page.evaluate(() => window.api.runtimeEnvironments.list()) const environment = environments.find( - (entry) => entry.orcadDeployment?.sshTargetId === remote.targetId + (entry) => getOrcadSshDeployment(entry)?.sshTargetId === remote.targetId ) if (!environment) { throw new Error('Missing managed environment') diff --git a/tests/e2e/ssh-orcad-explorer-selected-host.spec.ts b/tests/e2e/ssh-orcad-explorer-selected-host.spec.ts index 4d15f299efd..8362ce0a5b6 100644 --- a/tests/e2e/ssh-orcad-explorer-selected-host.spec.ts +++ b/tests/e2e/ssh-orcad-explorer-selected-host.spec.ts @@ -10,6 +10,7 @@ import { dismissTransientAnnouncement } from './helpers/ssh-config-host-picker' import { toRuntimeExecutionHostId } from '../../src/shared/execution-host' import { folderWorkspaceKey } from '../../src/shared/workspace-scope' import { ORCAD_TEMPLATE, skipUnlessManagedDockerHost } from './helpers/orcad-managed-workspace' +import { getOrcadSshDeployment } from '../../src/shared/runtime-environments' skipUnlessManagedDockerHost() @@ -105,10 +106,10 @@ for (const selectedHost of ['A', 'B'] as const) { console.log('[adopt-existing-managed-host]', await reconnect(page, adoptedTargetId)) const environments = await page.evaluate(() => window.api.runtimeEnvironments.list()) const environmentA = environments.find( - (entry) => entry.orcadDeployment?.sshTargetId === seededA.targetId + (entry) => getOrcadSshDeployment(entry)?.sshTargetId === seededA.targetId ) const environmentB = environments.find( - (entry) => entry.orcadDeployment?.sshTargetId === adoptedTargetId + (entry) => getOrcadSshDeployment(entry)?.sshTargetId === adoptedTargetId ) if (!environmentA || !environmentB) { throw new Error('Missing converted host') diff --git a/tests/e2e/ssh-orcad-terminal-network-outage.spec.ts b/tests/e2e/ssh-orcad-terminal-network-outage.spec.ts index 120a5cbbb13..421d734003f 100644 --- a/tests/e2e/ssh-orcad-terminal-network-outage.spec.ts +++ b/tests/e2e/ssh-orcad-terminal-network-outage.spec.ts @@ -37,7 +37,7 @@ for (const { mode, ms, label } of OUTAGES) { test(`a managed pane answers typing again after a ${label} ${mode} network outage`, async (// oxlint-disable-next-line no-empty-pattern -- Owns its app launch through a restart session. {}, testInfo) => { test.setTimeout(ms + 10 * 60_000) - let proxy: FreezableTcpProxy | null = null + let proxy: FreezableTcpProxy | undefined try { await withManagedHost(testInfo, async (fixture) => { const { host } = fixture diff --git a/tests/e2e/staging-skill-sharing.spec.ts b/tests/e2e/staging-skill-sharing.spec.ts index a02c7fa3faa..1e8cd12dc63 100644 --- a/tests/e2e/staging-skill-sharing.spec.ts +++ b/tests/e2e/staging-skill-sharing.spec.ts @@ -58,7 +58,7 @@ test('publishes, updates, revokes, and deletes without losing local state', asyn const source = join(sourceRoot, '.agents', 'skills', SKILL_NAME) const home = await electronApp.evaluate(({ app }) => app.getPath('home')) const globalSkill = join(home, '.agents', 'skills', SKILL_NAME) - let packageId: string | null = null + let packageId: string | undefined let physicalEnvironmentId: string | null = null let pairedHost: HeadlessPairedRuntimeHost | null = null let sshTargetId: string | null = null @@ -205,12 +205,12 @@ test('publishes, updates, revokes, and deletes without losing local state', asyn expect(removed).toMatchObject({ status: 'ok', value: { status: 'removed' } }) expect(existsSync(globalSkill)).toBe(false) expect( - await orcaPage.evaluate((id) => window.api.skills.getPackage(id), packageId) + await orcaPage.evaluate((id) => window.api.skills.getPackage(id), first.preview.packageId) ).toMatchObject({ status: 'ok' }) expect( - await orcaPage.evaluate((id) => window.api.skills.deletePackage(id), packageId) + await orcaPage.evaluate((id) => window.api.skills.deletePackage(id), first.preview.packageId) ).toMatchObject({ status: 'ok' }) - packageId = null + packageId = undefined } finally { for (const target of externalTargets(physicalEnvironmentId, sshTargetId)) { await orcaPage @@ -420,10 +420,10 @@ function expectPhysicalInstall( expect(skill?.canonicalPath).toContain(`.agents\\skills\\${SKILL_NAME}`) } else if (target === 'wsl') { expect(skill?.canonicalPath).toMatch(/^\/home\/[^/]+\/\.agents\/skills\//) - expect(skill?.canonicalPath.endsWith(`/${SKILL_NAME}`)).toBe(true) + expect(skill?.canonicalPath?.endsWith(`/${SKILL_NAME}`)).toBe(true) } else { expect(skill?.canonicalPath).toMatch(/^\//) - expect(skill?.canonicalPath.endsWith(`/.agents/skills/${SKILL_NAME}`)).toBe(true) + expect(skill?.canonicalPath?.endsWith(`/.agents/skills/${SKILL_NAME}`)).toBe(true) } } @@ -439,6 +439,10 @@ async function expectManagedRemoteVersion( if (installs.status !== 'ok') { throw new Error(`physical host managed-install listing failed: ${installs.status}`) } + const sshConnectionId = + target.destination.scope === 'global' && target.destination.executionTarget?.kind === 'ssh' + ? target.destination.executionTarget.connectionId + : undefined const install = installs.value.find((candidate) => { if (target.kind === 'windows' || target.kind === 'paired-posix') { return candidate.destination.scope === 'global' && !candidate.destination.executionTarget @@ -453,8 +457,7 @@ async function expectManagedRemoteVersion( return ( candidate.destination.scope === 'global' && candidate.destination.executionTarget?.kind === 'ssh' && - candidate.destination.executionTarget.connectionId === - target.destination.executionTarget?.connectionId + candidate.destination.executionTarget.connectionId === sshConnectionId ) }) expect(install).toMatchObject({ diff --git a/tests/e2e/structured-agent-session-read-owner.unit.test.ts b/tests/e2e/structured-agent-session-read-owner.unit.test.ts index 26b75cd6560..83f059ea821 100644 --- a/tests/e2e/structured-agent-session-read-owner.unit.test.ts +++ b/tests/e2e/structured-agent-session-read-owner.unit.test.ts @@ -8,7 +8,10 @@ import type { AgentSessionStatusSummary, AgentSessionSubscribeEvent } from '../../src/shared/agent-session-wire' -import type { AgentJournalCursor } from '../../src/shared/agent-session-journal-types' +import { + AGENT_JOURNAL_THREAD_SCOPE, + type AgentJournalCursor +} from '../../src/shared/agent-session-journal-types' import { EMPTY_STRUCTURED_AGENT_SESSION, reduceStructuredAgentSession @@ -65,7 +68,7 @@ async function fixture() { await journal.appendItem( { provider: 'orca', clientMessageId: `output-${index}` }, { kind: 'status', text: `Tool output ${index}` }, - { fence: 1 } + { fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE } ) } for (let index = 1; index < 99; index += 1) { @@ -133,8 +136,15 @@ describe('structured session cursor/body regression', () => { SESSION, { journal, - fence: 1, - params: { location: { workspaceId: 'folder-workspace' }, provider: 'codex' } + params: { + location: { + executionHostId: 'local', + wslDistro: null, + workspaceId: 'folder-workspace', + workspaceKind: 'folder' + }, + provider: 'codex' + } } ] ]), @@ -225,7 +235,7 @@ describe('structured session cursor/body regression', () => { await journal.appendItem( { provider: 'orca', clientMessageId: 'completed-turn' }, { kind: 'turn', turnId: 'turn-1', state: 'completed' }, - { fence: 1 } + { fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE } ) subscribers.publish(SESSION, journal) await vi.waitFor(() => expect(owner.getSnapshot().state.cursor).toEqual(journal.cursor())) diff --git a/tests/e2e/structured-attention-read-retirement.test-fixture.tsx b/tests/e2e/structured-attention-read-retirement.test-fixture.tsx index 8e2eb4ef811..b186f49878f 100644 --- a/tests/e2e/structured-attention-read-retirement.test-fixture.tsx +++ b/tests/e2e/structured-attention-read-retirement.test-fixture.tsx @@ -6,7 +6,7 @@ import { beforeEach, afterEach, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import type { RuntimeClientTarget } from '@/runtime/runtime-client-target' +import type { RuntimeClientTarget } from '../../src/shared/runtime-client-target' import type { NotificationSettings } from '../../src/shared/notification-settings-types' import type { AgentJournalRenderItem } from '../../src/shared/agent-session-journal-types' import type { AgentSessionHistoryPage } from '../../src/shared/agent-session-wire' diff --git a/tests/e2e/structured-attention-read-retirement.unit.test.ts b/tests/e2e/structured-attention-read-retirement.unit.test.ts index 3eda9bab51c..7daa9b006ff 100644 --- a/tests/e2e/structured-attention-read-retirement.unit.test.ts +++ b/tests/e2e/structured-attention-read-retirement.unit.test.ts @@ -34,7 +34,7 @@ import { makeUnifiedTab } from '@/store/slices/store-test-helpers' import { useAppStore } from '@/store' import { StructuredAgentSessionAttentionBridge } from '@/components/native-chat/StructuredAgentSessionAttentionBridge' import { useStructuredAgentSessionRead } from '@/components/native-chat/use-structured-agent-session-read' -import type { RuntimeClientTarget } from '@/runtime/runtime-client-target' +import type { RuntimeClientTarget } from '../../src/shared/runtime-client-target' import { findStructuredAgentSessionReadOwner } from '@/components/native-chat/structured-agent-session-read-owner' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' diff --git a/tests/e2e/structured-chat-owner-status-activation.unit.test.ts b/tests/e2e/structured-chat-owner-status-activation.unit.test.ts index de0ce6d5d98..b3b880704a8 100644 --- a/tests/e2e/structured-chat-owner-status-activation.unit.test.ts +++ b/tests/e2e/structured-chat-owner-status-activation.unit.test.ts @@ -64,7 +64,9 @@ function openHost(): void { }), closeSession, releaseAcquisition: async () => true, - dispatch: async () => ({ state: 'rejected', reason: 'unused' }), + dispatch: async () => { + throw new Error('dispatch is not exercised by this test') + }, cancelTurn: async () => ({ cancelled: false }), answerPrompt: async () => undefined, setOption: async () => undefined diff --git a/tests/e2e/tab-rename-paste-ownership.spec.ts b/tests/e2e/tab-rename-paste-ownership.spec.ts index ed38e3fc818..0556dc374e6 100644 --- a/tests/e2e/tab-rename-paste-ownership.spec.ts +++ b/tests/e2e/tab-rename-paste-ownership.spec.ts @@ -36,7 +36,7 @@ async function getActiveTabTitle(page: Page, worktreeId: string): Promise entry.id === activeId) expect(tab).toBeDefined() - return tab!.customTitle ?? tab!.title ?? '' + return tab!.title ?? '' } function tabLocatorByTitle(page: Page, title: string): ReturnType { diff --git a/tests/e2e/tab-rename.spec.ts b/tests/e2e/tab-rename.spec.ts index 30cdb9175a9..62b5d72b4dc 100644 --- a/tests/e2e/tab-rename.spec.ts +++ b/tests/e2e/tab-rename.spec.ts @@ -48,9 +48,8 @@ test.describe('Tab Rename (Inline)', () => { const tabs = await getWorktreeTabs(page, worktreeId) const tab = tabs.find((entry) => entry.id === activeId) expect(tab).toBeDefined() - // Why: mirror what the UI renders (customTitle ?? title) so locators that - // key off the tab's visible text match what's actually on screen. - return tab!.customTitle ?? tab!.title ?? '' + // Why: getWorktreeTabs already folds customTitle into title, matching what the UI renders. + return tab!.title ?? '' } function tabLocatorByTitle( diff --git a/tests/e2e/tasks-page.spec.ts b/tests/e2e/tasks-page.spec.ts index 962f86de0c5..33d6f09775e 100644 --- a/tests/e2e/tasks-page.spec.ts +++ b/tests/e2e/tasks-page.spec.ts @@ -324,7 +324,9 @@ test.describe('Tasks page', () => { ) const restoredList = orcaPage.locator('[data-task-list-scroll="github"]') await expect.poll(() => restoredList.evaluate((element) => element.scrollTop)).toBe(0) - await clampedRowsStyle.evaluate((element) => element.remove()) + await clampedRowsStyle.evaluate((element) => { + element.parentNode?.removeChild(element) + }) await expect(orcaPage.getByText('Issue page 28 item 1', { exact: true })).toBeVisible() await expect .poll(() => restoredList.evaluate((element) => element.scrollTop)) @@ -355,7 +357,9 @@ test.describe('Tasks page', () => { 'page' ) await orcaPage.getByRole('button', { name: 'Page 1', exact: true }).click() - await pendingRestoreStyle.evaluate((element) => element.remove()) + await pendingRestoreStyle.evaluate((element) => { + element.parentNode?.removeChild(element) + }) await expect(orcaPage.getByRole('button', { name: 'Page 1', exact: true })).toHaveAttribute( 'aria-current', 'page' @@ -399,7 +403,9 @@ test.describe('Tasks page', () => { return position.scrollTop }) .toBeGreaterThan(300) - await permanentlyClampedRowsStyle.evaluate((element) => element.remove()) + await permanentlyClampedRowsStyle.evaluate((element) => { + element.parentNode?.removeChild(element) + }) }) test('GitHub search waits for idle, keeps rows visible, and Enter does not double-fetch', async ({ diff --git a/tests/e2e/terminal-codex-cursor-jitter-repro.spec.ts b/tests/e2e/terminal-codex-cursor-jitter-repro.spec.ts index ad8de6ecd8e..4b75a04700f 100644 --- a/tests/e2e/terminal-codex-cursor-jitter-repro.spec.ts +++ b/tests/e2e/terminal-codex-cursor-jitter-repro.spec.ts @@ -94,7 +94,7 @@ type RasterFrameInput = { } type CursorReproWindow = Window & { - __cursorReproOriginalPtyWrite?: (ptyId: string, data: string) => void + __cursorReproOriginalPtyWrite?: Window['api']['pty']['write'] __cursorReproConptyDa1ReplyCount?: number __cursorReproRawChunks?: { id: string; data: string; at: number }[] __cursorReproRawUnsubscribe?: () => void @@ -295,12 +295,12 @@ async function installPtyWriteDiagnostics(page: Page): Promise { const reproWindow = window as CursorReproWindow if (!reproWindow.__cursorReproOriginalPtyWrite) { reproWindow.__cursorReproOriginalPtyWrite = window.api.pty.write.bind(window.api.pty) - window.api.pty.write = (ptyId, data) => { + window.api.pty.write = (ptyId, data, inputKind) => { if (data === da1Response) { reproWindow.__cursorReproConptyDa1ReplyCount = (reproWindow.__cursorReproConptyDa1ReplyCount ?? 0) + 1 } - reproWindow.__cursorReproOriginalPtyWrite!(ptyId, data) + reproWindow.__cursorReproOriginalPtyWrite!(ptyId, data, inputKind) } } reproWindow.__cursorReproConptyDa1ReplyCount = 0 diff --git a/tests/e2e/terminal-codex-hidden-startup-background.spec.ts b/tests/e2e/terminal-codex-hidden-startup-background.spec.ts index 158f3578314..87be612e411 100644 --- a/tests/e2e/terminal-codex-hidden-startup-background.spec.ts +++ b/tests/e2e/terminal-codex-hidden-startup-background.spec.ts @@ -1,3 +1,4 @@ +import type { XtermWithRenderDimensions } from './helpers/xterm-render-dimensions' import { Buffer } from 'node:buffer' import { PNG } from 'pngjs' import type { Page } from '@stablyai/playwright-test' @@ -138,7 +139,9 @@ async function readCodexStartupBackgroundTarget( throw new Error('Active Codex terminal pane is unavailable') } const screen = pane.container.querySelector('.xterm-screen') - const dimensions = pane.terminal._core?._renderService?.dimensions?.css?.cell + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: xterm has no public cell-size API; the private render service is read through all-optional fields. + const dimensions = (pane.terminal as XtermWithRenderDimensions)._core?._renderService + ?.dimensions?.css?.cell if (!screen || !dimensions) { throw new Error('Active Codex terminal has no measurable xterm screen') } @@ -319,16 +322,17 @@ test.describe('Codex hidden startup composer background', () => { }) .toContain(marker) - let target: CodexStartupBackgroundTarget | null = null + // Why a holder: TS can't see the poll callback's writes and would narrow a `let` to null. + const captured: { target: CodexStartupBackgroundTarget | null } = { target: null } await expect .poll( async () => { try { const nextTarget = await readCodexStartupBackgroundTarget(orcaPage, marker) - target = nextTarget + captured.target = nextTarget return nextTarget.modelBackgroundCells >= Math.min(40, nextTarget.cols) } catch { - target = null + captured.target = null return false } }, @@ -338,6 +342,7 @@ test.describe('Codex hidden startup composer background', () => { } ) .toBe(true) + const target = captured.target if (!target) { throw new Error('Codex startup background target was not captured') } diff --git a/tests/e2e/terminal-codex-local-typing-latency.spec.ts b/tests/e2e/terminal-codex-local-typing-latency.spec.ts index e738e60700a..27b2c3585f6 100644 --- a/tests/e2e/terminal-codex-local-typing-latency.spec.ts +++ b/tests/e2e/terminal-codex-local-typing-latency.spec.ts @@ -1,3 +1,4 @@ +import type { XtermWithRenderDimensions } from './helpers/xterm-render-dimensions' import type { Page } from '@stablyai/playwright-test' import { existsSync } from 'node:fs' import path from 'node:path' @@ -118,7 +119,9 @@ async function readActiveTerminalRasterTarget(page: Page): Promise('.xterm-screen') - const dimensions = pane?.terminal._core?._renderService?.dimensions?.css?.cell + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: xterm has no public cell-size API; the private render service is read through all-optional fields. + const dimensions = (pane?.terminal as XtermWithRenderDimensions | undefined)?._core + ?._renderService?.dimensions?.css?.cell if (!pane || !screen || !dimensions) { throw new Error('Active terminal screen is unavailable') } diff --git a/tests/e2e/terminal-codex-skill-preview-artifact-repro.spec.ts b/tests/e2e/terminal-codex-skill-preview-artifact-repro.spec.ts index 105fd3f5280..3c3c80a7d25 100644 --- a/tests/e2e/terminal-codex-skill-preview-artifact-repro.spec.ts +++ b/tests/e2e/terminal-codex-skill-preview-artifact-repro.spec.ts @@ -17,7 +17,8 @@ import { waitForPaneCount, waitForPaneIdentitySnapshot } from './helpers/terminal' -import { compareTerminalScreenshots } from './terminal-screenshot-diff' +import { compareTerminalScreenshots, type ScreenshotDiffSummary } from './terminal-screenshot-diff' +import { getDefaultRepoHookSettings } from '../../src/shared/constants' const RUN_REPRO = process.env.ORCA_E2E_CODEX_SKILL_PREVIEW_REPRO === '1' const EXPECT_NO_ARTIFACTS = process.env.ORCA_E2E_EXPECT_NO_CODEX_SKILL_PREVIEW_ARTIFACTS === '1' @@ -54,10 +55,7 @@ type PaneDescriptor = { hasWebgl: boolean } -type ArtifactCapture = { - diffRatio: number - changedPixels: number - totalPixels: number +type ArtifactCapture = ScreenshotDiffSummary & { leftPane: PaneDescriptor beforeContent: string afterContent: string @@ -108,55 +106,58 @@ async function setStableFullscreenWindow( } async function addRealOrcaRepo(page: Page, repoPath: string): Promise { - return page.evaluate(async (repoPath) => { - await window.api.repos.add({ path: repoPath }).catch((error: unknown) => { - if (!/already|exists|duplicate/i.test(String(error))) { - throw error + return page.evaluate( + async ({ repoPath, defaultHookSettings }) => { + await window.api.repos.add({ path: repoPath }).catch((error: unknown) => { + if (!/already|exists|duplicate/i.test(String(error))) { + throw error + } + }) + + const store = window.__store + if (!store) { + throw new Error('window.__store unavailable') } - }) - - const store = window.__store - if (!store) { - throw new Error('window.__store unavailable') - } - const state = store.getState() - await state.fetchRepos({ runtimeEnvironmentId: null, throwOnError: true }) - const repo = store.getState().repos.find((candidate) => candidate.path === repoPath) - if (!repo) { - throw new Error(`Real Orca repo did not load: ${repoPath}`) - } - - await store.getState().updateRepo(repo.id, { - externalWorktreeVisibility: 'show', - hookSettings: { - ...repo.hookSettings, - setupRunPolicy: 'run-by-default', - setupAgentStartupPolicy: 'start-immediately' + const state = store.getState() + await state.fetchRepos({ runtimeEnvironmentId: null, throwOnError: true }) + const repo = store.getState().repos.find((candidate) => candidate.path === repoPath) + if (!repo) { + throw new Error(`Real Orca repo did not load: ${repoPath}`) } - }) - await store.getState().fetchWorktrees(repo.id) - const nextState = store.getState() - const worktree = (nextState.worktreesByRepo[repo.id] ?? []).find( - (candidate) => candidate.path === repoPath - ) - if (!worktree) { - throw new Error(`Real Orca worktree did not load: ${repoPath}`) - } + await store.getState().updateRepo(repo.id, { + externalWorktreeVisibility: 'show', + hookSettings: { + ...(repo.hookSettings ?? defaultHookSettings), + setupRunPolicy: 'run-by-default', + setupAgentStartupPolicy: 'start-immediately' + } + }) + await store.getState().fetchWorktrees(repo.id) - nextState.updateSettings({ - defaultTuiAgent: 'codex', - disabledTuiAgents: [], - terminalGpuAcceleration: 'on', - theme: 'dark' - }) - nextState.setActiveRepo(repo.id) - nextState.setActiveView('terminal') - nextState.setActiveWorktree(worktree.id) - nextState.setRightSidebarOpen(false) - nextState.setSidebarOpen(true) - return repo.id - }, repoPath) + const nextState = store.getState() + const worktree = (nextState.worktreesByRepo[repo.id] ?? []).find( + (candidate) => candidate.path === repoPath + ) + if (!worktree) { + throw new Error(`Real Orca worktree did not load: ${repoPath}`) + } + + nextState.updateSettings({ + defaultTuiAgent: 'codex', + disabledTuiAgents: [], + terminalGpuAcceleration: 'on', + theme: 'dark' + }) + nextState.setActiveRepo(repo.id) + nextState.setActiveView('terminal') + nextState.setActiveWorktree(worktree.id) + nextState.setRightSidebarOpen(false) + nextState.setSidebarOpen(true) + return repo.id + }, + { repoPath, defaultHookSettings: getDefaultRepoHookSettings() } + ) } async function createWorkspaceThroughComposer(page: Page, workspaceName: string): Promise { @@ -656,8 +657,8 @@ test.describe('Codex skill preview terminal artifact repro @headful', () => { type: 'codex-skill-preview-click-diff', description: JSON.stringify({ diffRatio: evidence.diffRatio, - changedPixels: evidence.changedPixels, - totalPixels: evidence.totalPixels, + changedPixels: evidence.diffPixels, + totalPixels: evidence.width * evidence.height, leftPane: evidence.leftPane, setupChangesAfterPreview, overpaintedSkillRows, diff --git a/tests/e2e/terminal-foreground-confirmation.unit.test.ts b/tests/e2e/terminal-foreground-confirmation.unit.test.ts index 90124121f36..562bfda1546 100644 --- a/tests/e2e/terminal-foreground-confirmation.unit.test.ts +++ b/tests/e2e/terminal-foreground-confirmation.unit.test.ts @@ -26,6 +26,7 @@ vi.mock('../../src/main/providers/agent-foreground-process', () => ({ import { createPtySubprocess } from '../../src/main/daemon/pty-subprocess' import { createPaneForegroundAgentTracker } from '../../src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker' +import type { PaneForegroundAgentEntry } from '../../src/renderer/src/store/slices/pane-foreground-agent' function mockWindowsPty() { const exitListeners: ((event: { exitCode: number }) => void)[] = [] @@ -61,7 +62,7 @@ describe('daemon foreground confirmation composes with pane tracking', () => { } }) - async function createComposedTracker(publish: ReturnType) { + async function createComposedTracker(publish: (entry: PaneForegroundAgentEntry) => void) { const handle = await createPtySubprocess({ sessionId: 'test', cols: 80, rows: 24 }) const tracker = createPaneForegroundAgentTracker({ getPtyId: () => 'pty-1', diff --git a/tests/e2e/terminal-foreground-redraw-freeze.spec.ts b/tests/e2e/terminal-foreground-redraw-freeze.spec.ts index 995abb3fcea..8527e77ded1 100644 --- a/tests/e2e/terminal-foreground-redraw-freeze.spec.ts +++ b/tests/e2e/terminal-foreground-redraw-freeze.spec.ts @@ -184,7 +184,7 @@ async function installActivePaneRefreshProbe(page: Page): Promise { : null const manager = tabId ? window.__paneManagers?.get(tabId) : null const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null - if (!pane) { + if (!manager || !pane) { throw new Error('Active terminal pane is unavailable') } const terminal = pane.terminal as unknown as { diff --git a/tests/e2e/terminal-ime-preedit-cell-grid.spec.ts b/tests/e2e/terminal-ime-preedit-cell-grid.spec.ts index 2042813ba29..037022d840b 100644 --- a/tests/e2e/terminal-ime-preedit-cell-grid.spec.ts +++ b/tests/e2e/terminal-ime-preedit-cell-grid.spec.ts @@ -1,3 +1,4 @@ +import type { XtermWithRenderDimensions } from './helpers/xterm-render-dimensions' import { expect, test } from './helpers/orca-app' import { closeTerminalImePaneArena, openTerminalImePaneArena } from './terminal-ime-pane-arena' import { setImeComposition } from './terminal-ime-cdp-composition' @@ -55,7 +56,12 @@ for (const dpr of [1, 1.25, 2]) { const screen = terminal.element!.querySelector('.xterm-screen')! const preedit = screen.querySelector('.xterm-composition-preedit')! // The canvas width rounds independently of fractional WebGL cell widths. - const cellWidth = terminal._core._renderService.dimensions.css.cell.width + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: xterm has no public cell-size API; the private render service is read through all-optional fields. + const cellWidth = (terminal as XtermWithRenderDimensions)._core?._renderService + ?.dimensions?.css?.cell?.width + if (cellWidth === undefined) { + throw new Error('xterm render service has no cell dimensions') + } const line = terminal.buffer.active.getLine(terminal.buffer.active.baseY)! const committed: { text: string; column: number; width: number }[] = [] const end = line.translateToString(true).length @@ -164,7 +170,12 @@ for (const dpr of [1, 1.25, 2]) { const terminal = window .__paneManagers!.get(state.activeTabId!)! .getActivePane()!.terminal - const cellWidth = terminal._core._renderService.dimensions.css.cell.width + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: xterm has no public cell-size API; the private render service is read through all-optional fields. + const cellWidth = (terminal as XtermWithRenderDimensions)._core?._renderService + ?.dimensions?.css?.cell?.width + if (cellWidth === undefined) { + throw new Error('xterm render service has no cell dimensions') + } const preedit = terminal.element!.querySelector('.xterm-composition-preedit')! return Math.abs(preedit.getBoundingClientRect().width - 16 * cellWidth) }) diff --git a/tests/e2e/terminal-long-table-scroll-restore.spec.ts b/tests/e2e/terminal-long-table-scroll-restore.spec.ts index 5aeaabb8ba5..28b859f657d 100644 --- a/tests/e2e/terminal-long-table-scroll-restore.spec.ts +++ b/tests/e2e/terminal-long-table-scroll-restore.spec.ts @@ -1,3 +1,4 @@ +import type { XtermWithRenderDimensions } from './helpers/xterm-render-dimensions' import { randomUUID } from 'node:crypto' import { rmSync, writeFileSync } from 'node:fs' import path from 'node:path' @@ -195,7 +196,9 @@ async function readTerminalRightEdgeOverpaint(page: Page): Promise<{ } } - const cellWidth = pane.terminal._core?._renderService?.dimensions?.css?.cell?.width ?? 0 + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: xterm has no public cell-size API; the private render service is read through all-optional fields. + const xterm = pane.terminal as XtermWithRenderDimensions + const cellWidth = xterm._core?._renderService?.dimensions?.css?.cell?.width ?? 0 const maxRight = screenRect.right + Math.max(1, cellWidth * 0.5) const offenders = Array.from(rows.querySelectorAll('span')) .map((span) => { diff --git a/tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts b/tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts index 7bb19138067..ac33c60aaf7 100644 --- a/tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts +++ b/tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts @@ -1,3 +1,4 @@ +import type { XtermWithRenderDimensions } from './helpers/xterm-render-dimensions' import { randomUUID } from 'node:crypto' import { rmSync, writeFileSync } from 'node:fs' import path from 'node:path' @@ -193,7 +194,9 @@ async function readActiveTerminalRasterTarget(page: Page): Promise('.xterm-screen') - const dimensions = pane.terminal._core?._renderService?.dimensions?.css?.cell + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: xterm has no public cell-size API; the private render service is read through all-optional fields. + const dimensions = (pane.terminal as XtermWithRenderDimensions)._core?._renderService + ?.dimensions?.css?.cell if (!screen || !dimensions) { throw new Error('Active terminal has no measurable xterm screen') } diff --git a/tests/e2e/terminal-osc8-cold-park-restore.spec.ts b/tests/e2e/terminal-osc8-cold-park-restore.spec.ts index 1f0924cf517..6214505618a 100644 --- a/tests/e2e/terminal-osc8-cold-park-restore.spec.ts +++ b/tests/e2e/terminal-osc8-cold-park-restore.spec.ts @@ -132,6 +132,9 @@ test('restores and opens an OSC 8 link after its terminal is cold-parked', async await ensureTerminalVisible(orcaPage) await waitForActiveTerminalManager(orcaPage, 30_000) const tabId = await getActiveTabId(orcaPage) + if (!tabId) { + throw new Error('No active terminal tab') + } const ptyId = await waitForActivePanePtyId(orcaPage) await waitForPtyShellEcho(orcaPage, ptyId, 15_000) diff --git a/tests/e2e/terminal-pane-divider-capture-loss.spec.ts b/tests/e2e/terminal-pane-divider-capture-loss.spec.ts index 1b96b775b4d..4ea2f35d39e 100644 --- a/tests/e2e/terminal-pane-divider-capture-loss.spec.ts +++ b/tests/e2e/terminal-pane-divider-capture-loss.spec.ts @@ -70,7 +70,7 @@ async function readDividerGeometry(page: Page): Promise { .flatMap((manager) => manager.getPanes()) .find((candidate) => candidate.container.dataset.ptyId === ptyId) : null - let proposed = null + let proposed: PaneGeometry['proposed'] = null try { proposed = pane?.fitAddon.proposeDimensions() ?? null } catch { @@ -120,7 +120,9 @@ test('@headful keeps resizing after the divider loses pointer capture', async ({ await divider.evaluate((element) => { element.dataset.captureLossCount = '0' element.addEventListener('pointerdown', (event) => { - element.dataset.captureLossPointerId = String(event.pointerId) + if (event instanceof PointerEvent) { + element.dataset.captureLossPointerId = String(event.pointerId) + } }) element.addEventListener('lostpointercapture', () => { element.dataset.captureLossCount = String(Number(element.dataset.captureLossCount ?? '0') + 1) diff --git a/tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts b/tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts index 9835de0566f..baf04d5ec95 100644 --- a/tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts +++ b/tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts @@ -21,6 +21,7 @@ import { scrollActiveTerminalToText } from './artificial-opencode-active-termina import { nodeTerminalCommand } from './terminal-node-command' type BrowserTerminalPane = { + id: number terminal: { cols: number rows: number @@ -203,7 +204,7 @@ async function waitForActiveTerminalColumns( .poll( () => page.evaluate(() => { - let pane: ReturnType> = null + let pane: BrowserTerminalPane | null = null try { pane = (window as RawTableDebugWindow).getActiveTestPane?.() ?? null } catch { diff --git a/tests/e2e/terminal-reveal-paused-render-repro.spec.ts b/tests/e2e/terminal-reveal-paused-render-repro.spec.ts index 5fc0e233693..155367e8ea1 100644 --- a/tests/e2e/terminal-reveal-paused-render-repro.spec.ts +++ b/tests/e2e/terminal-reveal-paused-render-repro.spec.ts @@ -466,7 +466,7 @@ test.describe('terminal reveal paused-render recovery', () => { expect(held.screen).toContain('REVEALED_FRAME') await orcaPage.evaluate((tabId) => { - window.__paneManagers?.get(tabId)?.scheduleRevealRepaint?.() + window.__paneManagers?.get(tabId)?.scheduleRevealRepaint() }, tabId) await expect .poll(async () => (await readSynchronizedRevealProbe(orcaPage)).atlasClears) @@ -595,7 +595,7 @@ test.describe('terminal reveal paused-render recovery', () => { }) const beforeWatchdog = await readSynchronizedRevealProbe(orcaPage) await orcaPage.evaluate((tabId) => { - window.__paneManagers?.get(tabId)?.scheduleRevealPresent?.() + window.__paneManagers?.get(tabId)?.scheduleRevealPresent() }, tabId) await expect .poll(async () => (await readSynchronizedRevealProbe(orcaPage)).synchronizedOutput, { diff --git a/tests/e2e/terminal-search-results.spec.ts b/tests/e2e/terminal-search-results.spec.ts index 5ef4290ed49..f9451a39ee2 100644 --- a/tests/e2e/terminal-search-results.spec.ts +++ b/tests/e2e/terminal-search-results.spec.ts @@ -46,7 +46,11 @@ test('terminal search counts real matches and repeat find selects the query', as await expect(input).toBeFocused() await expect .poll(() => - input.evaluate((element) => ({ start: element.selectionStart, end: element.selectionEnd })) + input.evaluate((element) => + element instanceof HTMLInputElement + ? { start: element.selectionStart, end: element.selectionEnd } + : null + ) ) .toEqual({ start: 0, end: query.length }) await orcaPage.screenshot({ path: testInfo.outputPath('search-query-selected.png') }) diff --git a/tests/e2e/terminal-shortcuts.spec.ts b/tests/e2e/terminal-shortcuts.spec.ts index 0ea9f19b354..bcf7fb6401c 100644 --- a/tests/e2e/terminal-shortcuts.spec.ts +++ b/tests/e2e/terminal-shortcuts.spec.ts @@ -200,7 +200,7 @@ async function pressShiftedRussianLayoutKey(page: Page): Promise<{ const textarea = pane?.container.querySelector( '.xterm-helper-textarea' ) as HTMLTextAreaElement | null - if (!textarea) { + if (!pane || !textarea) { throw new Error('No xterm helper textarea to receive keyboard input') } textarea.focus() @@ -650,8 +650,10 @@ test.describe('Terminal Shortcuts', () => { : null const manager = tabId ? window.__paneManagers?.get(tabId) : null const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null - const terminalText = pane?.terminal.buffer.active - .translateBufferLineToString(pane.terminal.buffer.active.cursorY, true) + const activeBuffer = pane?.terminal.buffer.active + const terminalText = activeBuffer + ?.getLine(activeBuffer.baseY + activeBuffer.cursorY) + ?.translateToString(true) .trim() const visibleText = pane?.container.textContent ?? '' return { diff --git a/tests/e2e/terminal-sleep-wake-restore.spec.ts b/tests/e2e/terminal-sleep-wake-restore.spec.ts index 0dedcedcc00..ceaab420dc6 100644 --- a/tests/e2e/terminal-sleep-wake-restore.spec.ts +++ b/tests/e2e/terminal-sleep-wake-restore.spec.ts @@ -23,7 +23,7 @@ type SleepWakeTerminalDebug = { activeWorktreeId: string | null tabs: { id: string - ptyId?: string + ptyId: string | null generation?: number pendingActivationSpawn?: boolean | number }[] diff --git a/tests/e2e/terminal-tab-switch-visual-restore.spec.ts b/tests/e2e/terminal-tab-switch-visual-restore.spec.ts index e32627140d5..f293fb04dba 100644 --- a/tests/e2e/terminal-tab-switch-visual-restore.spec.ts +++ b/tests/e2e/terminal-tab-switch-visual-restore.spec.ts @@ -1,3 +1,4 @@ +import type { XtermWithRenderDimensions } from './helpers/xterm-render-dimensions' import type { Page, TestInfo } from '@stablyai/playwright-test' import { test, expect } from './helpers/orca-app' import { @@ -455,7 +456,9 @@ async function readTabTerminalGeometry( throw new Error(`No xterm screen for tab ${tabId}`) } const screenRect = screen.getBoundingClientRect() - const cellWidth = pane.terminal._core?._renderService?.dimensions?.css?.cell?.width ?? 0 + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: xterm has no public cell-size API; the private render service is read through all-optional fields. + const xterm = pane.terminal as XtermWithRenderDimensions + const cellWidth = xterm._core?._renderService?.dimensions?.css?.cell?.width ?? 0 const renderedContentWidth = pane.terminal.cols * cellWidth const rowRight = screenRect.left + renderedContentWidth const contentWidthRatio = screenRect.width > 0 ? renderedContentWidth / screenRect.width : 0 diff --git a/tests/e2e/terminal-tui-wheel-drain.spec.ts b/tests/e2e/terminal-tui-wheel-drain.spec.ts index cee81a02ec1..a2315ada45b 100644 --- a/tests/e2e/terminal-tui-wheel-drain.spec.ts +++ b/tests/e2e/terminal-tui-wheel-drain.spec.ts @@ -1,3 +1,4 @@ +import type { XtermWithRenderDimensions } from './helpers/xterm-render-dimensions' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' @@ -96,8 +97,9 @@ async function terminalWheelTarget( x: rect.left + rect.width / 2, y: rect.top + Math.min(rect.height - 1, 40), cellHeight: - pane.terminal._core?._renderService?.dimensions?.css?.cell?.height ?? - rect.height / pane.terminal.rows + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: xterm has no public cell-size API; the private render service is read through all-optional fields. + (pane.terminal as XtermWithRenderDimensions)._core?._renderService?.dimensions?.css?.cell + ?.height ?? rect.height / pane.terminal.rows } }) } diff --git a/tests/e2e/terminal-tui-wheel-reports.spec.ts b/tests/e2e/terminal-tui-wheel-reports.spec.ts index f3259409cdb..89644f9123b 100644 --- a/tests/e2e/terminal-tui-wheel-reports.spec.ts +++ b/tests/e2e/terminal-tui-wheel-reports.spec.ts @@ -1,3 +1,4 @@ +import type { XtermWithRenderDimensions } from './helpers/xterm-render-dimensions' import type { Page } from '@stablyai/playwright-test' import path from 'node:path' import { test, expect } from './helpers/orca-app' @@ -58,8 +59,9 @@ async function probeSmallMouseWheelReports( const rect = screen.getBoundingClientRect() const cellHeight = - pane.terminal._core?._renderService?.dimensions?.css?.cell?.height ?? - rect.height / pane.terminal.rows + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: xterm has no public cell-size API; the private render service is read through all-optional fields. + (pane.terminal as XtermWithRenderDimensions)._core?._renderService?.dimensions?.css?.cell + ?.height ?? rect.height / pane.terminal.rows const scrollSensitivity = Number(pane.terminal.options.scrollSensitivity ?? 1) // Why: this is a notched mouse wheel event that Chromium can surface as a // small pixel delta; xterm's <50px damping accumulates it for four ticks. @@ -148,8 +150,9 @@ async function probeTimedSmallMouseWheelReports( const rect = screen.getBoundingClientRect() const cellHeight = - pane.terminal._core?._renderService?.dimensions?.css?.cell?.height ?? - rect.height / pane.terminal.rows + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: xterm has no public cell-size API; the private render service is read through all-optional fields. + (pane.terminal as XtermWithRenderDimensions)._core?._renderService?.dimensions?.css?.cell + ?.height ?? rect.height / pane.terminal.rows const scrollSensitivity = Number(pane.terminal.options.scrollSensitivity ?? 1) // Why: this is a notched mouse wheel event that Chromium can surface as a // small pixel delta; xterm's <50px damping accumulates it for four ticks. diff --git a/tests/e2e/terminal-warm-reattach-split-live-output.spec.ts b/tests/e2e/terminal-warm-reattach-split-live-output.spec.ts index 10372c6a126..289ba8b3293 100644 --- a/tests/e2e/terminal-warm-reattach-split-live-output.spec.ts +++ b/tests/e2e/terminal-warm-reattach-split-live-output.spec.ts @@ -181,21 +181,20 @@ test('restored hidden split drains live alternate-screen output without a click await waitForPaneCount(first.page, 2, 30_000) const split = await waitForPaneIdentitySnapshot(first.page, 2) const streamingPane = split.panes.find((pane) => pane.leafId === split.activeLeafId) - if (!streamingPane?.ptyId) { + const streamingPtyId = streamingPane?.ptyId + if (!streamingPtyId) { throw new Error('Split did not expose its active PTY') } - expect(streamingPane.ptyId).toContain(PTY_SESSION_ID_SEPARATOR) + expect(streamingPtyId).toContain(PTY_SESSION_ID_SEPARATOR) await execInTerminal( first.page, - streamingPane.ptyId, + streamingPtyId, `node ${JSON.stringify(scriptPath)} ${JSON.stringify(heartbeatPath)}` ) await expect .poll( async () => - frameNumber( - (await probePane(first.page, split.tabId, streamingPane.ptyId))?.content ?? '' - ), + frameNumber((await probePane(first.page, split.tabId, streamingPtyId))?.content ?? ''), { timeout: 20_000, message: 'Streaming TUI did not start in the split pane' @@ -222,7 +221,7 @@ test('restored hidden split drains live alternate-screen output without a click await expect .poll( async () => { - restoredTabId = await findRestoredTabForPty(second.page, streamingPane.ptyId!) + restoredTabId = await findRestoredTabForPty(second.page, streamingPtyId) return restoredTabId }, { timeout: 20_000, message: 'Persisted split PTY was not restored into a tab layout' } @@ -232,12 +231,12 @@ test('restored hidden split drains live alternate-screen output without a click await waitForActiveTerminalManager(second.page, 30_000) await waitForPaneCount(second.page, 2, 30_000) - const initial = await probePane(second.page, restoredTabId!, streamingPane.ptyId) + const initial = await probePane(second.page, restoredTabId!, streamingPtyId) expect(initial, 'Streaming pane did not remount after tab activation').not.toBeNull() const initialFrame = frameNumber(initial!.content) const initialHeartbeat = heartbeatNumber(heartbeatPath) await second.page.waitForTimeout(2_000) - const later = await probePane(second.page, restoredTabId!, streamingPane.ptyId) + const later = await probePane(second.page, restoredTabId!, streamingPtyId) const laterHeartbeat = heartbeatNumber(heartbeatPath) expect( diff --git a/tests/e2e/workspace-space-git-status.spec.ts b/tests/e2e/workspace-space-git-status.spec.ts index 53985be0fa3..498934da78d 100644 --- a/tests/e2e/workspace-space-git-status.spec.ts +++ b/tests/e2e/workspace-space-git-status.spec.ts @@ -89,7 +89,7 @@ test.describe('Workspace Space git status checks', () => { branch: worktree.branch, isMainWorktree: false, isRemote: false, - isSparse: worktree.isSparse, + isSparse: worktree.isSparse === true, canDelete: true, lastActivityAt: worktree.lastActivityAt, status: 'ok' as const, diff --git a/tests/tools/daemon-relocation-spike/ndjson-client.mjs b/tests/tools/daemon-relocation-spike/ndjson-client.mjs index 8919823e54a..e783ca4f3e0 100644 --- a/tests/tools/daemon-relocation-spike/ndjson-client.mjs +++ b/tests/tools/daemon-relocation-spike/ndjson-client.mjs @@ -73,7 +73,6 @@ function delay(ms) { // Strip CSI / OSC / two-char VT escapes so the marker regex can match executed // PTY output that ConPTY interleaves with cursor-move and SGR color codes. -// eslint-disable-next-line no-control-regex -- ANSI escapes are control chars by definition const ANSI_ESCAPE = /\[[0-9;?]*[ -/]*[@-~]|\][^]*?|[@-Z\\-_]/g function stripAnsi(text) { diff --git a/tests/tools/omp-child-history-rendered/fixture.tsx b/tests/tools/omp-child-history-rendered/fixture.tsx index b8fb1b54707..c7f27ab8756 100644 --- a/tests/tools/omp-child-history-rendered/fixture.tsx +++ b/tests/tools/omp-child-history-rendered/fixture.tsx @@ -122,6 +122,7 @@ function App() { buildResumeStartup={(session) => ({ command: session.resumeCommand })} getOriginalPaneTarget={() => null} getSessionLiveState={() => null} + isStructuredSessionOpen={() => false} getWorktreeInfo={() => null} getSessionResumeState={() => ({ blocked: false, @@ -139,6 +140,7 @@ function App() { onResume={(session, target) => setResult(`Resume ${session.sessionId} in ${target}`)} onContinueInNewSession={ignore} onResumeInNewChat={ignore} + onResumeInNewCli={ignore} onCopyResume={ignore} onCopyId={ignore} onCopyPath={ignore} diff --git a/tests/tools/omp-fresh-launch-windows-smoke.ts b/tests/tools/omp-fresh-launch-windows-smoke.ts index d8ebd4315a7..6aecf98b8a9 100644 --- a/tests/tools/omp-fresh-launch-windows-smoke.ts +++ b/tests/tools/omp-fresh-launch-windows-smoke.ts @@ -10,7 +10,7 @@ if (process.platform !== 'win32') { const root = mkdtempSync(join(tmpdir(), 'orca-fresh-windows-')) const config = join(root, 'fresh settings.yml') const capture = join(root, 'calls') -const results = [] +const results: { shell: 'cmd' | 'powershell'; state: string; code: number; passed: boolean }[] = [] try { writeFileSync(join(root, 'omp.cmd'), '@echo off\r\necho %*>>"%CAPTURE%"\r\nexit /b 17\r\n') for (const shell of ['cmd', 'powershell'] as const) { diff --git a/tests/tools/omp-remote-sidebar-rendered/fixture.tsx b/tests/tools/omp-remote-sidebar-rendered/fixture.tsx index 8f3247055c9..ee41a295362 100644 --- a/tests/tools/omp-remote-sidebar-rendered/fixture.tsx +++ b/tests/tools/omp-remote-sidebar-rendered/fixture.tsx @@ -3,7 +3,6 @@ import { createRoot } from 'react-dom/client' import { CompactAgentRow } from '../../../src/renderer/src/components/sidebar/worktree-card-compact-agent-row' import { buildWorktreeAgentRows } from '../../../src/renderer/src/components/sidebar/worktree-agent-rows' import { selectLiveAgentStatusEntriesForWorktree } from '../../../src/renderer/src/components/sidebar/worktree-agent-row-selectors' -import type { WorktreeAgentRowsState } from '../../../src/renderer/src/components/sidebar/worktree-agent-row-selectors' import { makePaneKey } from '../../../src/shared/stable-pane-id' import { toWebTerminalSurfaceTabId } from '../../../src/shared/terminal-surface-id' import { TooltipProvider } from '../../../src/renderer/src/components/ui/tooltip' @@ -11,7 +10,7 @@ import './fixture.css' const now = Date.now() const worktreeId = 'folder:remote-project' -const state: WorktreeAgentRowsState = { +const state: Parameters[0] = { agentStatusByPaneKey: {}, tabsByWorktree: {}, unifiedTabsByWorktree: {}, diff --git a/tests/tools/omp-transcript-rendered/fixture.tsx b/tests/tools/omp-transcript-rendered/fixture.tsx index f3fa771fd6b..2f4ed169058 100644 --- a/tests/tools/omp-transcript-rendered/fixture.tsx +++ b/tests/tools/omp-transcript-rendered/fixture.tsx @@ -28,12 +28,12 @@ createRoot(root).render( messages: transcript.messages, hasMore: false, loadingEarlier: false, + olderHistoryGeneration: 0, loadEarlier: () => Promise.resolve('exhausted' as const), readPhase: 'ready' }} isWorking={false} expandSignal={false} - fontScale={1} />