Files
orca/tests/tools/daemon-relocation-spike
OrcaWinandOrcaWin fdcf887bf0 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>
2026-10-24 19:37:19 -07:00
..
2026-08-28 00:59:21 -07:00

daemon-relocation-spike

A throwaway probe that answers one empirical question for Phase 1 of the Windows update-survival work:

What is the minimal set of files that must be copied out of a packaged win-unpacked build so that a copied Orca.exe — run with ELECTRON_RUN_AS_NODE=1 from a directory outside the install dir — can: (a) start the terminal daemon and signal ready over IPC, (b) spawn a real ConPTY node-pty session, write input, and read output back, (c) do all of that while holding no open file handles into the app/install dir, so an NSIS update could delete the original install.

Why this matters

The daemon is fork()ed from the app's own Electron binary (Orca.exe) with ELECTRON_RUN_AS_NODE=1, from the install directory. On a Windows update, electron-builder's NSIS installer (a) runs uninstallOldVersion (deletes the registered install's files) and (b) CHECK_APP_RUNNING force-closes every process whose image path is under $INSTDIR. So the daemon dies and its held file locks can break the update.

The Phase 1 fix copies the daemon's whole file closure out of the install dir (into %LOCALAPPDATA%/userData) and forks the daemon from the copy, so its image + all loaded modules live outside $INSTDIR. This spike measures how small that copy can be while still working.

We keep the Electron binary run as node (not a stock node.exe): a prior attempt (#7473, reverted) switched to stock node and caused Windows-console flashing (stock node lacks Electron's kHideConsoleWindows) plus asar breakage. This spike does not reintroduce stock node.

Usage

# Real run (Windows, needs a packaged win-unpacked build):
node tests/tools/daemon-relocation-spike/spike.mjs \
  --app-dir <path-to-win-unpacked> \
  --work-dir <scratch-dir> \
  [--tier full|no-gpu|minimal] \
  [--keep-work-dir]

# Offline logic validation (any OS, no build, no launch):
node tests/tools/daemon-relocation-spike/spike.mjs --selftest

Exit code is 0 only when the run PASSES: daemon ready, PTY echo round-trips the nonce, the daemon's main module is the copied Orca.exe, and no loaded module resolves under --app-dir.

Tiers (defined as data in tier-file-set.mjs)

Every tier includes the irreducible core: Orca.exe, icudtl.dat, both V8 snapshot blobs (snapshot_blob.bin, v8_context_snapshot.bin), the daemon bundle (out/main/daemon-entry.js + chunks/ + out/package.json), and the whole node-pty package (native conpty.node + the sibling conpty/ runtime dir holding conpty.dll + OpenConsole.exe).

Tiers differ only in which top-level *.dll files they carry:

Tier Top-level DLLs
full all top-level *.dll
no-gpu all except GPU/render DLLs (libEGL, libGLESv2, vk_swiftshader, vulkan-1, d3dcompiler_47); keeps ffmpeg.dll
minimal none (exe + data blobs + daemon bundle + node-pty only)

Trimming further is a config change (edit TIER_DEFINITIONS / GPU_DLLS), not a code change.

How node-pty's native + ConPTY runtime is handled

The whole node-pty package tree is copied, and the entire win-unpacked layout is mirrored verbatim (every copy destination is relative to the win-unpacked root, not the asar-unpacked root). This matters because node-pty is packaged at resources/node_modules/node-pty — a sibling of app.asar.unpacked, not under it (see config/packaged-runtime-node-modules.cjs). Mirroring the full layout preserves two resolutions from the relocated path:

  1. The daemon require-closure resolves require('node-pty') by walking parent dirs up from the mirrored resources/app.asar.unpacked/out/main/daemon-entry.js, which passes through resources/ and finds resources/node_modules/node-pty — exactly as in the packaged app.
  2. node-pty's own native loader resolves conpty.node from build/Release (or prebuilds/win32-<arch>) relative to node-pty's own __dirname, and node-pty's Windows addon loads conpty.dll from <dir-of-conpty.node>/conpty/ and spawns OpenConsole.exe from beside it. Copying the tree verbatim keeps all three side-by-side.

ORCA_NODE_PTY_NATIVE_DIR

The reverted #7421 added a node-pty patch that reads ORCA_NODE_PTY_NATIVE_DIR to override the native dir. The current branch's config/patches/node-pty@1.1.0.patch does NOT contain that override — it was reverted. The spike therefore relies on layout preservation (copying the node-pty tree at its default relative path) rather than the env override. The spike still sets ORCA_NODE_PTY_NATIVE_DIR to the relocated native dir so it keeps working if pointed at a build that carries the patch, but on this branch the var is inert.

Implication for the real Phase 1 implementation: if the production copy does NOT preserve node-pty at the path its loader resolves by default (e.g. if the daemon-entry is relocated without the sibling node_modules/node-pty), the impl will need to re-add the ORCA_NODE_PTY_NATIVE_DIR patch from #7421. If it mirrors the layout as this spike does, the patch is not strictly required — though re-adding it is the more robust choice.

The handshake / client

ndjson-client.mjs is a small standalone NDJSON client (no electron/src imports) that mirrors src/main/daemon/daemon-server.ts:

  1. Read the token the server writes to the token file after it begins listening.
  2. Open a control socket, send hello {role:'control'}, await {type:'hello', ok:true}.
  3. Open a stream socket with the same clientId, send hello {role:'stream'}.
  4. createOrAttach on control, then write echo SPIKE-OK-<nonce>\r\n, and read data events on the stream socket until the nonce appears alone at line start (executed output, distinct from the echoed input line).

PROTOCOL_VERSION is read at runtime from src/main/daemon/types.ts so the client never drifts from the daemon.

The handle probe

loaded-modules.ps1 (via loaded-module-probe.mjs) runs Get-Process -Id <pid> and enumerates .Modules[].FileName. Any module path under --app-dir is a lock risk (the installer cannot replace a file a live process maps), so a passing relocation must show zero. It also asserts the process's main module is the copied Orca.exe, not the install-dir one.

Loaded DLLs are the lock-critical set. Data files (icudtl.dat, asar) are not memory-mapped as modules, so this probe does not enumerate them — the copy plan handles those by construction (they are copied, so nothing opens the originals).

What remains unverified until CI runs it

This session has no build, so the launch path is unproven. Verified here: node --check on every .mjs, a green --selftest, and clean pnpm exec oxlint. Open questions the real CI run must answer:

  • Whether TIER_MINIMAL (no top-level DLLs) boots Orca.exe as node at all, or whether run-as-node still needs ffmpeg.dll / others — this is the core empirical result.
  • Whether the daemon bundle require-closure needs any other unpacked node_modules beyond node-pty (surfaces as a ready-timeout if so).
  • Whether any loaded module still resolves under --app-dir (the handle probe will name it).

Recommendation for the likely-minimal tier

no-gpu is the safe minimal target to ship: run-as-node Electron does not initialize the GPU/render stack, so libEGL / libGLESv2 / vk_swiftshader / vulkan-1 / d3dcompiler_47 are very unlikely to load, while ffmpeg.dll and the ICU/snapshot data are retained because the Electron bootstrap references them regardless of run-as-node. Run --tier minimal on CI first: if it PASSES, ship minimal; if Orca.exe fails to boot without the non-GPU DLLs, fall back to no-gpu. full is the always-works upper bound for comparison.