Files
orca/src
Lesley Murfin 946dacc65d fix(worktrees): route unstamped local worktrees local in the two states #16841 still fails closed (#16829)
* test(worktrees): cover local worktree owner routing with saved runtimes (#16733)

A local git worktree whose rows carry no host stamp fails every owner-routed
operation closed as soon as any runtime environment is saved, however unrelated.
resolveWorktreeOperationRouteResult establishes positive identity from the
worktree/repo catalogs, then discards it: with no runtime active the only exit is
the legacy-local gate, which demands an empty saved-runtime list. One saved
environment makes that false and the call returns { kind: 'missing' }.

These tests state the contract before the fix, so the claim that the fix is
purely additive can be checked rather than asserted. Committed red on purpose.

Observed at 5631aa00dd (vitest run, both files):

  Tests  7 failed | 52 passed (59)

The 7 failing are exactly the states that must become local, plus their two
consumers:

  - an unrelated runtime is saved (the reported bug)
  - several unrelated runtimes are saved
  - the repo is known before its worktree row is listed
  - the saved-runtime catalog has not hydrated
  - an unrelated runtime was removed
  - resolveTerminalWorktreeRoute on such a worktree (the gate in front of the
    "Terminal creation is unavailable" reply)
  - the folder/worktree parity state: identical store, folder local, worktree
    missing

The other 52 pass now and must keep passing: connection-owned and
runtime-stamped repos never route local, a stamped worktree row still outranks
its repo, contradictory repo rows stay ambiguous, an ambiguous or hydrating
runtime focus still fails closed, and a genuinely unknown id still fails closed.
That set is the additive-only guarantee.

* test(worktrees): re-aim two fail-closed cases at genuinely missing owners (#16733)

Two cases in src/renderer/src/lib/worktree-operation-route.test.ts assert the
behaviour #16733 reports as the bug, so they have to move:

  - :158 'fails a paired-client ownerless stale publication closed instead of
    routing it locally'
  - :171 'fails ownerless rows closed until the saved-runtime catalog is hydrated'

Both arrived with #9994 (41751dd90d, route HUB-owned SSH worktrees through their
owning runtime), whose stated goal was to fail closed for missing or stale
owners. That goal is right and is kept. The premise being rebutted is narrower:
neither fixture describes a missing or stale owner. Each carries a present repo
row that is merely unstamped -- repos: [{ id: 'repo-1' }] -- and three places in
this codebase already read exactly that row as locally owned:

  - shared/execution-host.ts getRepoExecutionHostId returns LOCAL_EXECUTION_HOST_ID
  - main/ipc/worktrees/listing/worktree-host-ownership.ts resolveRepoOwnershipEvidence
    falls back to LOCAL_EXECUTION_HOST_ID, and the listing and removal paths trust it
  - shared/repo-types.ts documents executionHostId as the field runtime-host repos
    need precisely because they otherwise look identical to local repos

attribute. It swept in the legacy-local case because at the time nothing in this
resolver consulted the repo index for an unstamped row.

So each case is re-aimed at the state it was actually defending, and neither is
deleted -- the fail-closed coverage is not reduced, it is pointed at a real
missing owner:

  - the first becomes 'fails a paired-client publication closed when no repo row
    can own it': same runtime state, repos: []. A worktree row alone is not host
    evidence, so this still returns missing, before and after the fix.
  - the second becomes 'fails ownerless rows closed mid-hydration while a saved
    runtime could own them': an active runtime with an ambiguous saved catalog
    during hydration. This returns missing from the active-runtime branch and is
    untouched by the fix. It is worded to stay distinct from the neighbouring
    case at :185, which already covers focus-is-not-ownership with an empty
    catalog, rather than duplicating it.

The states these two cases vacate are re-asserted with their corrected expected
result in the #16733 block added by the previous commit. Suite unchanged at
7 failed | 52 passed (59): the rewrites pass, the 7 reds are still the 7 states
the fix must convert.

* fix(worktrees): keep unstamped local worktrees routable when runtimes are saved (#16733)

resolveWorktreeOperationRouteResult establishes positive identity from the
worktree and repo catalogs, then discards it. With no runtime active the only
exit is the legacy-local gate, which requires an empty saved-runtime list, so one
saved runtime environment -- connected or not, related or not -- made it false
and the call returned { kind: 'missing' }. Every owner-routed operation on a
genuinely local git worktree then failed closed, and because
resolveTerminalWorktreeRoute is the sole gate in front of
terminal-request-ipc-bridge.ts, the user saw "Terminal creation is unavailable
because the worktree owner could not be resolved".

Folder workspaces hit the same gate and were carved out in #10251/#10269, whose
comment in this file states the principle and names this exact failure mode: a
found record is positive identity evidence, and the worktree legacy hydration
gates "would fail local folders closed whenever unrelated runtimes exist". Git
worktrees never got the equivalent. This adds it, in the same shape and the same
function.

The rule is not new. An unstamped repo row is read as locally owned by
getRepoExecutionHostId, by main's resolveRepoOwnershipEvidence, and by
Repo.executionHostId's own documentation; and the repo write path
(repoWithFetchedOwner) stamps runtime: and ssh: owners at fetch time, so an
unstamped row is a legacy row that predates owner projection -- local by
construction. The router now consults that evidence instead of contradicting it.
The sidebar already rendered these worktrees as Local; this removes the
disagreement rather than adding a heuristic.

Four properties this change holds to:

1. The branch sits after the active-runtime block, so an unambiguous active
   runtime still wins (routes runtime:<id>, not local) and an ambiguous or
   mid-hydration focus still returns missing. That ordering is structural, not
   incidental.
2. mayBeLegacyLocal is left byte-identical (verified: both 7-line hunks hash to
   f0ed1b4287c646cb). The new branch does take over the two states where a local
   repo row exists and no runtime is saved, but returns the identical local
   route, so no input changes its answer -- only which branch produced it.
3. The helper returns null on anything but unanimous local, so the branch can
   only ever convert missing into local. It never returns ambiguous: a
   contradiction between repo rows is already decided upstream by
   resolveExplicitWorktreeOperationRouteResult, and answering it here would be a
   second, divergent authority.
4. It reads neither runtimeEnvironmentCatalogHydrated nor
   removedRuntimeEnvironmentIds. That is sound rather than merely convenient,
   because it consults host evidence rather than runtime-environment inference: a
   runtime-owned repo row is stamped runtime:<id> at fetch time, so neither an
   unhydrated runtime catalog nor a removed environment can turn an unstamped row
   into a remote one.

Control only reaches this point after the explicit catalog resolver returned
missing, which means every worktree row and every repo row for this id is
unstamped -- any stamped row routes ssh: or runtime: earlier, and two disagreeing
rows return ambiguous earlier. There is no remote-owned state left here to leak.

Out of scope, deliberately: who wins when a runtime is focused (#11512), and
back-filling Worktree.hostId at creation time, which is a persistence migration
over worktreeMeta and does nothing for the users already carrying unstamped rows.

The 7 cases red in the two preceding commits now pass; the 52 that guard the
fail-closed contract are unchanged.

  Test Files  2 passed (2)
       Tests  59 passed (59)

* test(worktrees): defer repo-row-only routing to #16841's fail-closed rule (#16733)

Upstream #16841 (merged d3475957f3) landed its own fix for #16733 and drew the
positive-identity line one notch tighter than this branch did: its
'does not treat a repo row alone as positive local identity' case asserts that a
worktree id no row has ever listed stays `missing`, even when the repo row for
its repoId is local.

This branch's 'routes a known local repo before its worktree row has been listed'
asserted the opposite result for that identical state, so the two cannot both
hold. Main's rule is the safer reading — a repo row is repo identity, not
worktree identity — so the reconciled code gates
resolveUnstampedLocalWorktreeRoute on hasKnownWorktree and this case is dropped
rather than re-pinned. Every state that actually reproduces #16733 keeps a
worktree row (listed or detected), so the reported bug and both extra
fail-closed edge cases this branch fixes are unaffected.

* refactor(worktrees): drop the unreachable disagreement loop in resolveUnstampedLocalWorktreeRoute

resolveWorktreeOperationRouteResult only calls resolveUnstampedLocalWorktreeRoute after
resolveExplicitWorktreeOperationRouteResult has already returned 'missing' for this repoId.
That function (worktree-operation-catalog-route.ts) indexes every repo row carrying a
non-empty executionHostId or connectionId and resolves/ambiguous-es on any of them, so by
construction every row resolveUnstampedLocalWorktreeRoute ever sees is unstamped -- and
getRepoExecutionHostId's own fallback (shared/execution-host.ts) always resolves an unstamped
row to local. The per-row disagreement check could never actually return null; it was dead
defensive code describing a state the caller's short-circuit already rules out. Reduced to
an existence check with identical behavior (verified: same 66/66 tests, same mutation-proof
property -- reverting only this file still fails exactly the same 6 tests it did before).
Also harmonized a same-function 'local' string literal to the LOCAL_EXECUTION_HOST_ID constant
already in use one branch above it, and dropped a dangling getWorktreeExecutionHostId doc
reference the shipped code never actually calls.

* docs(routing): document the undocumented worktree operation route helpers
2026-09-15 16:01:29 -07:00
..