fix(persistence): worktree meta and lineage writes take the row's host, not the id (R4-2b) (#30811)

* fix(persistence): worktree meta and lineage writes take the row's host, not the id (R4-2b)

* fix(persistence): review fixes for host-targeted worktree writes

* test: SAFETY notes on partial runtime store mocks

* test: drop imports duplicated by the rebase onto #30797
This commit is contained in:
Neil
2026-10-19 11:39:54 -07:00
committed by GitHub
parent 86ef00ad55
commit eedca5ee1c
142 changed files with 1625 additions and 772 deletions
-7
View File
@@ -1,10 +1,3 @@
# Store writes whose first parameter is a bare repo or worktree id (R4-2), as Class.method.
# This is a RATCHET: entries may only be REMOVED. Take RepoHostTarget or HostWorktreeTarget.
# Prune after a fix: pnpm check:id-only-store-write-ratchet --prune
MetadataLineageOperations.migrateWorktreeIdentity
MetadataLineageOperations.removeWorkspaceLineage
MetadataLineageOperations.removeWorktreeLineage
MetadataLineageOperations.removeWorktreeMeta
MetadataLineageOperations.setWorktreeLineage
MetadataLineageOperations.setWorktreeMeta
MetadataLineageOperations.setWorktreeMetaForHost
@@ -42,6 +42,10 @@ export const NOT_HOST_TWINNED = new Map([
['AutomationPersistence.advanceAutomationNextRun', 'automation ids are minted UUIDs'],
['AutomationPersistence.deleteAutomation', 'automation ids are minted UUIDs'],
['AutomationPersistence.updateAutomation', 'automation ids are minted UUIDs'],
[
'MetadataLineageOperations.removeUnhostedWorktreeMeta',
'acts only on a row no host stamped; it refuses a stamped one'
],
[
'OrcadMigrationBlobInstallPersistence.installDeferredOrcadMigrationShellHistory',
'a retry trigger: the id only gates a store-wide reinstall; nothing is written under it'
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { GlobalSettings } from '../../shared/global-settings-types'
import type { Repo } from '../../shared/repo-types'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
import {
renameWorktreeFolderOnFirstWork,
type FirstWorkFolderRenameDeps
@@ -42,7 +43,7 @@ describe('renameWorktreeFolderOnFirstWork', () => {
'/ws/worktree-creation-spinner'
)
expect(deps.migrateWorktreeIdentity).toHaveBeenCalledWith(
OLD_ID,
hostWorktreeTarget({ id: OLD_ID, hostId: 'local' }),
'repo1::/ws/worktree-creation-spinner'
)
expect(deps.notifyWorktreeRenamed).toHaveBeenCalledWith(
@@ -66,7 +67,7 @@ describe('renameWorktreeFolderOnFirstWork', () => {
)
expect(result).toBe(true)
expect(deps.migrateWorktreeIdentity).toHaveBeenCalledWith(
FOLDER_WORKSPACE_ID,
hostWorktreeTarget({ id: FOLDER_WORKSPACE_ID, hostId: 'local' }),
'repo1::/ws/worktree-creation-spinner::workspace:12345678-1234-1234-1234-123456789abc'
)
expect(deps.notifyWorktreeRenamed).toHaveBeenCalledWith(
@@ -13,12 +13,14 @@ import {
} from '../../shared/worktree/id'
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
import { planWorktreeFolderRename } from '../ipc/worktree-folder-rename-target'
import { hostWorktreeTargetForRepo } from '../persistence/host-qualified-worktree-meta'
import type { HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
export type FirstWorkFolderRenameDeps = {
getRepo: (repoId: string) => Repo | undefined
getSettings: () => Parameters<typeof planWorktreeFolderRename>[0]['settings']
/** Re-key all worktreeId-keyed state from the old (path-derived) id to the new. */
migrateWorktreeIdentity: (oldWorktreeId: string, newWorktreeId: string) => void
migrateWorktreeIdentity: (target: HostWorktreeTarget, newWorktreeId: string) => void
/** Invalidate caches + tell the renderer the worktree's id changed (old->new) so
* it re-keys its state instead of treating the rename as a deletion. */
notifyWorktreeRenamed: (repoId: string, oldWorktreeId: string, newWorktreeId: string) => void
@@ -67,7 +69,7 @@ export async function renameWorktreeFolderOnFirstWork(
await deps.moveWorktree(repo.path, plan.oldPath, plan.newPath)
// Order: move first (point of no return), then re-key identity synchronously so
// nothing interleaves before the worktree's state is re-bound to the new id.
deps.migrateWorktreeIdentity(worktreeId, plan.newWorktreeId)
deps.migrateWorktreeIdentity(hostWorktreeTargetForRepo(worktreeId, repo), plan.newWorktreeId)
deps.notifyWorktreeRenamed(repo.id, worktreeId, plan.newWorktreeId)
return true
}
@@ -9,6 +9,7 @@ import { isWslMoveFolderWorkspaceBlocked } from '../persistence/moving-wsl-proje
import type { Store } from '../persistence'
import type { RuntimeStore } from '../runtime/runtime-store-contract'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import { resolveWorktreeMetaOwner } from '../persistence/host-qualified-worktree-meta'
const ENABLE_FIRST_WORK_FOLDER_RENAME = false
@@ -17,6 +18,7 @@ export function firstWorkRenameDeps(
RuntimeStore,
| 'getSettings'
| 'getRepo'
| 'getRepos'
| 'getWorktreeMeta'
| 'setWorktreeMeta'
| 'getFolderWorkspace'
@@ -77,7 +79,11 @@ export function firstWorkRenameDeps(
runtime.notifyFolderWorkspaceChanged()
return
}
store.setWorktreeMeta(worktreeId, {
const target = resolveWorktreeMetaOwner(store, worktreeId)
if (!target) {
return
}
store.setWorktreeMeta(target, {
displayName,
// The first-agent title is an intentional user-facing label; keep it stable after the
// generated branch is renamed and across subsequent catalog refreshes.
@@ -92,7 +98,8 @@ export function firstWorkRenameDeps(
renameWorktreeFolderOnFirstWork(worktreeId, newLeaf, {
getRepo: (repoId) => store.getRepo(repoId),
getSettings: () => store.getSettings(),
migrateWorktreeIdentity: (oldId, newId) => store.migrateWorktreeIdentity!(oldId, newId),
migrateWorktreeIdentity: (target, newId) =>
store.migrateWorktreeIdentity!(target, newId),
notifyWorktreeRenamed: (repoId, oldId, newId) =>
runtime.notifyWorktreeFolderRenamed(repoId, oldId, newId),
pathExists: async (candidate) => existsSync(candidate),
@@ -121,7 +128,11 @@ export function firstWorkRenameDeps(
if ((current ?? null) === (error ?? null)) {
return
}
store.setWorktreeMeta(worktreeId, { firstAgentMessageRenameError: error })
const target = resolveWorktreeMetaOwner(store, worktreeId)
if (!target) {
return
}
store.setWorktreeMeta(target, { firstAgentMessageRenameError: error })
// Why: the hook only knows the worktreeId, so derive the repoId notifyBranchRenamed expects.
runtime.notifyBranchRenamed(getRepoIdFromWorktreeId(worktreeId))
},
+18 -11
View File
@@ -8,6 +8,10 @@ import {
assertAttachableParentWorkspace,
recordWorkspaceLineageForCreatedWorktree
} from './worktree-remote'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../shared/worktree/host-qualified-identity'
const CHILD_ID = 'repo-1::/repos/child'
const PARENT_ID = 'repo-1::/repos/parent'
@@ -72,7 +76,7 @@ function createStore(options: {
getFolderWorkspace: vi.fn((id: string) =>
folderWorkspaceIds.has(id) ? { id, path: `/folders/${id}` } : undefined
),
setWorktreeLineage: vi.fn((_id: string, lineage: unknown) => lineage),
setWorktreeLineage: vi.fn((_target: HostWorktreeTarget, lineage: unknown) => lineage),
setWorkspaceLineage: vi.fn((lineage: unknown) => lineage)
}
}
@@ -104,15 +108,18 @@ describe('recordWorkspaceLineageForCreatedWorktree', () => {
const result = record(store, { parentWorkspace: worktreeWorkspaceKey(PARENT_ID) })
expect(store.setWorktreeLineage).toHaveBeenCalledWith(CHILD_ID, {
worktreeId: CHILD_ID,
worktreeInstanceId: 'child-instance',
parentWorktreeId: PARENT_ID,
parentWorktreeInstanceId: 'parent-instance',
origin: 'manual',
capture: { source: 'manual-action', confidence: 'explicit' },
createdAt: CREATED_AT
})
expect(store.setWorktreeLineage).toHaveBeenCalledWith(
hostWorktreeTarget({ id: CHILD_ID, hostId: 'local' }),
{
worktreeId: CHILD_ID,
worktreeInstanceId: 'child-instance',
parentWorktreeId: PARENT_ID,
parentWorktreeInstanceId: 'parent-instance',
origin: 'manual',
capture: { source: 'manual-action', confidence: 'explicit' },
createdAt: CREATED_AT
}
)
expect(result.lineage).toMatchObject({
parentWorktreeId: PARENT_ID,
parentWorktreeInstanceId: 'parent-instance',
@@ -140,7 +147,7 @@ describe('recordWorkspaceLineageForCreatedWorktree', () => {
const result = record(store, { parentWorkspace: worktreeWorkspaceKey(foreignParentId) })
expect(store.setWorktreeLineage).toHaveBeenCalledWith(
CHILD_ID,
hostWorktreeTarget({ id: CHILD_ID, hostId: 'local' }),
expect.objectContaining({ parentWorktreeId: foreignParentId })
)
expect(result.lineage).toMatchObject({ parentWorktreeId: foreignParentId })
+1 -1
View File
@@ -20,7 +20,7 @@ export type GitRemoteExec = (
// `getAllWorktreeMeta`) keep compiling; callers that want materialize-time
// provenance persistence (worktree-remote.ts) pass a store that has it.
export type WorktreePushTargetStore = Pick<Store, 'getAllWorktreeMeta'> &
Partial<Pick<Store, 'setWorktreeMeta'>>
Partial<Pick<Store, 'setWorktreeMeta' | 'getRepos'>>
export function sameGitHubRemoteUrl(left: string, right: string): boolean {
if (left === right) {
@@ -6,6 +6,7 @@ const { gitExecFileAsyncMock } = vi.hoisted(() => ({ gitExecFileAsyncMock: vi.fn
vi.mock('../git/runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock }))
import { materializeWorktreePushTargetRemote } from './worktree-remote'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
const REPO_PATH = '/repo-root'
const FORK_URL = 'git@github.com:contributor/orca.git'
@@ -342,10 +343,14 @@ describe('materializeWorktreePushTargetRemote', () => {
})
const target = forkTarget()
const setWorktreeMeta = vi.fn()
// The write lands on the id's only repo row.
const store: WorktreePushTargetStore = {
getAllWorktreeMeta: () => ({}),
getRepos: () => [
{ id: 'worktree-1', path: REPO_PATH, displayName: 'r', badgeColor: '#000', addedAt: 1 }
],
setWorktreeMeta
} as unknown as WorktreePushTargetStore
}
const result = await materializeWorktreePushTargetRemote(
REPO_PATH,
@@ -357,9 +362,12 @@ describe('materializeWorktreePushTargetRemote', () => {
)
expect(result).toEqual({ ...target, remoteCreated: true })
expect(setWorktreeMeta).toHaveBeenCalledWith('worktree-1', {
pushTarget: { ...target, remoteCreated: true }
})
expect(setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'worktree-1', hostId: 'local' }),
{
pushTarget: { ...target, remoteCreated: true }
}
)
})
it('does not touch the store when no worktreeId is provided', async () => {
+33 -14
View File
@@ -81,7 +81,6 @@ import { getRepoIdFromWorktreeId } from '../../shared/worktree/id'
import type { BranchPrefixSettings } from '../../shared/branch-prefix'
import { parseWorkspaceKey, worktreeWorkspaceKey } from '../../shared/workspace-scope'
import { sharesWorktreeLineageBoundary } from '../../shared/resolved-worktree-lineage'
import { readWorktreeMetaForHost } from '../persistence/host-qualified-worktree-meta'
import {
cleanupUnusedWorktreePushTargetRemoteWithExec,
sameGitHubRemoteUrl,
@@ -145,6 +144,12 @@ import {
import { createRetiredNameLookup } from '../../shared/worktree/retired-name-registry'
import { findPendingWorktreeRemovalConflict } from '../worktree-removal-table'
import { isWslMoveWorktreeBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff'
import {
hostWorktreeTargetForRepo,
readWorktreeMetaForHost,
resolveWorktreeMetaOwner,
worktreeRowTarget
} from '../persistence/host-qualified-worktree-meta'
// Why: bound the fallback `git fetch origin` so a Windows credential-manager GUI hang (STA-1292) can't wedge worktree creation forever.
const CREATE_BASE_FALLBACK_FETCH_TIMEOUT_MS = 60_000
// Why (#17828 CodeRabbit follow-up): the deferred materialize fetch runs off the main
@@ -253,6 +258,7 @@ export function recordWorkspaceLineageForCreatedWorktree(
// Why: only a worktree parent produces sidebar nesting; a folder parent has no WorktreeLineage row.
let lineage: CreateWorktreeResult['lineage'] = null
let parentOutsideLineageBoundary = false
let lineageOwnedByTwin = false
if (parentScope.type === 'worktree' && parentWorktreeMeta) {
if (!parentWorktreeMeta.instanceId) {
console.warn(
@@ -264,20 +270,26 @@ export function recordWorkspaceLineageForCreatedWorktree(
`[worktree-create] parent ${parentScope.worktreeId} is outside ${worktree.id}'s execution host; skipping lineage`
)
} else {
lineage = store.setWorktreeLineage(worktree.id, {
worktreeId: worktree.id,
worktreeInstanceId: worktree.instanceId,
parentWorktreeId: parentScope.worktreeId,
parentWorktreeInstanceId: parentWorktreeMeta.instanceId,
origin: 'manual',
capture: { source: 'manual-action', confidence: 'explicit' },
createdAt
})
const target = worktreeRowTarget(store, worktree)
lineage =
(target &&
store.setWorktreeLineage(target, {
worktreeId: worktree.id,
worktreeInstanceId: worktree.instanceId,
parentWorktreeId: parentScope.worktreeId,
parentWorktreeInstanceId: parentWorktreeMeta.instanceId,
origin: 'manual',
capture: { source: 'manual-action', confidence: 'explicit' },
createdAt
})) ??
null
lineageOwnedByTwin = lineage === null
}
}
// Why: a workspace row for an out-of-boundary worktree parent is a cross-host link that survives restarts.
if (parentOutsideLineageBoundary) {
// A twin-owned bucket keeps the twin's workspace row too.
if (parentOutsideLineageBoundary || lineageOwnedByTwin) {
return NO_CREATED_WORKTREE_LINEAGE
}
@@ -807,11 +819,18 @@ function persistMaterializedPushTargetIfCreated(
if (!target.remoteCreated || !worktreeId || !store?.setWorktreeMeta) {
return
}
const owner = resolveWorktreeMetaOwner(
{
getWorktreeMeta: (id) => store.getAllWorktreeMeta()[id],
getRepos: () => store.getRepos?.() ?? []
},
worktreeId
)
// Bookkeeping incidental to the push: a project a WSL move is taking skips it, not the push.
if (isWslMoveWorktreeBlocked(worktreeId, store.getAllWorktreeMeta()[worktreeId]?.hostId)) {
if (!owner || isWslMoveWorktreeBlocked(worktreeId, owner.hostId)) {
return
}
store.setWorktreeMeta(worktreeId, { pushTarget: target })
store.setWorktreeMeta(owner, { pushTarget: target })
}
function isPushTargetRemoteCreatedByKnownWorktree(
@@ -1575,7 +1594,7 @@ async function performLocalWorktreeCreate(
...(args.workspaceStatus !== undefined ? { workspaceStatus: args.workspaceStatus } : {})
}
const { worktree } = timing.timeSync('persist_metadata', () => {
const meta = store.setWorktreeMeta(worktreeId, metaUpdates)
const meta = store.setWorktreeMeta(hostWorktreeTargetForRepo(worktreeId, repo), metaUpdates)
return { worktree: mergeWorktree(repo.id, created, meta) }
})
onWorkspaceCreated?.(worktreeId)
@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { GitWorktreeInfo } from '../../shared/worktree/types'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
import type { NativeLocalWorktreeMetadataScanExpectation } from '../persistence/tracking-repos/missing-local-worktree-metadata-pruning'
import { notifyWorktreesChanged } from './worktree-remote'
import {
@@ -367,7 +368,10 @@ describe('authoritative local worktree metadata pruning integration', () => {
resolveScan([worktree(REPO_PATH)])
queueMicrotask(() => {
registerWorktreeRootsForRepo(store as never, REPO_ID, [REPO_PATH, newPath])
store.setWorktreeMeta(newId, { hostId: LOCAL_HOST_ID, instanceId: 'new-instance' })
store.setWorktreeMeta(hostWorktreeTarget({ id: newId, hostId: LOCAL_HOST_ID }), {
hostId: LOCAL_HOST_ID,
instanceId: 'new-instance'
})
store.getAllWorktreeLineage.mockReturnValue({
[newId]: {
worktreeId: newId,
@@ -465,7 +469,7 @@ describe('authoritative local worktree metadata pruning integration', () => {
registerWorktreeRootsForRepo(store as never, REPO_ID, [REPO_PATH, newPath])
const newMetadata = { hostId: LOCAL_HOST_ID, instanceId: 'new-instance' }
metadata.set(newId, newMetadata)
store.setWorktreeMeta(newId, newMetadata)
store.setWorktreeMeta(hostWorktreeTarget({ id: newId, hostId: LOCAL_HOST_ID }), newMetadata)
store.getAllWorktreeLineage.mockReturnValue({
[newId]: {
worktreeId: newId,
@@ -646,8 +650,6 @@ describe('authoritative local worktree metadata pruning integration', () => {
await listDetected()
expect(store.pruneSessionlessMissingLocalWorktreeMetadataForRepo).toHaveBeenCalledTimes(1)
expect(store.setWorktreeMeta.mock.calls.some(([worktreeId]) => worktreeId === staleId)).toBe(
false
)
expect(store.setWorktreeMeta.mock.calls.some(([target]) => target.id === staleId)).toBe(false)
})
})
@@ -13,6 +13,7 @@ import {
_resetPendingWorktreeRemovalsForTests,
startBackgroundWorktreeRemoval
} from '../worktree-background-removal'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -154,7 +155,9 @@ describe('worktrees:remove in the background', () => {
const result = await reply
expect(result).toMatchObject({ preservedBranch: { branchName: 'feature', head: 'feature' } })
expect(result.removing).toBeUndefined()
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(featureId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: featureId, hostId: 'local' })
)
const after = await listRepoRows()
expect(after.map((row) => row.id)).not.toContain(featureId)
})
@@ -43,6 +43,10 @@ import {
listDetectedGitWorktrees,
rememberLocalWorktreeRoots
} from './worktrees/listing/detected-worktree-scan-cache'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../shared/worktree/host-qualified-identity'
const path = '/remote/repository'
const rows = [{ path, head: 'abc', branch: 'main', isBare: false, isMainWorktree: true }]
@@ -180,11 +184,13 @@ it.each(['git', 'folder'] as const)(
if (kind === 'folder') {
metaById[parentId].instanceId = undefined
}
store.setWorktreeMeta.mockImplementation((id: string, updates: Partial<WorktreeMeta>) => {
const meta = mergeWorktreeMetaForWrite(metaById[id], updates)
metaById[id] = meta
return meta
})
store.setWorktreeMeta.mockImplementation(
({ id }: HostWorktreeTarget, updates: Partial<WorktreeMeta>) => {
const meta = mergeWorktreeMetaForWrite(metaById[id], updates)
metaById[id] = meta
return meta
}
)
Object.assign(store, {
getRepos: () => [repo, parentRepo],
getAllWorktreeMeta: () => metaById,
@@ -207,7 +213,10 @@ it.each(['git', 'folder'] as const)(
status: 'complete',
result: { worktrees: [expect.objectContaining({ id: childId, parentWorktreeId: parentId })] }
})
expect(store.setWorktreeMeta).not.toHaveBeenCalledWith(parentId, expect.anything())
expect(store.setWorktreeMeta).not.toHaveBeenCalledWith(
hostWorktreeTarget({ id: parentId, hostId: 'local' }),
expect.anything()
)
const parent = mocks.handlers.get('worktrees:listKnownForExecutionHost')!(null, {
repoId: parentRepo.id,
executionHostId: 'ssh:host-a'
@@ -118,7 +118,7 @@ function makeRepo(fields: Record<string, unknown>) {
function useRepo(repo: ReturnType<typeof makeRepo>): void {
store.getRepos.mockReturnValue([repo])
store.getRepo.mockReturnValue(repo)
store.setWorktreeMeta.mockImplementation((_worktreeId: string, meta: unknown) => meta)
store.setWorktreeMeta.mockImplementation((_target: unknown, meta: unknown) => meta)
}
function useLocalListing(): void {
@@ -16,6 +16,10 @@ import {
makeWorktreeMeta
} from './worktrees-test-fixtures'
import type { WorktreeRuntimeStub } from './worktrees-test-runtime-stub'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../shared/worktree/host-qualified-identity'
const WORKTREE_HANDLER_CHANNELS = [
'worktrees:listAll',
@@ -168,7 +172,7 @@ describe('registerWorktreeHandlers', () => {
handlers['worktrees:persistSortOrder'](null, { orderedIds: [liveId, staleId] })
const orderedTargets = store.setWorktreeMeta.mock.calls.map((call) => call[0])
const orderedTargets = store.setWorktreeMeta.mock.calls.map((call) => call[0].id)
expect(orderedTargets).toContain(liveId)
expect(orderedTargets).not.toContain(staleId)
})
@@ -186,7 +190,7 @@ describe('registerWorktreeHandlers', () => {
})
it('strips Orca provenance fields from renderer metadata updates', () => {
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
const result = handlers['worktrees:updateMeta'](null, {
worktreeId: 'repo-1::/workspace/feature-wt',
@@ -199,14 +203,14 @@ describe('registerWorktreeHandlers', () => {
}
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith('repo-1::/workspace/feature-wt', {
comment: 'keep me',
isPinned: true
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'repo-1::/workspace/feature-wt', hostId: 'local' }),
{ comment: 'keep me', isPinned: true }
)
expect(result).toMatchObject({ comment: 'keep me', isPinned: true })
})
it('routes metadata updates through the explicitly selected host', () => {
store.setWorktreeMetaForHost.mockImplementation((_worktreeId, _executionHostId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
const result = handlers['worktrees:updateMeta'](null, {
worktreeId: 'repo-1::/workspace/feature-wt',
@@ -214,12 +218,10 @@ describe('registerWorktreeHandlers', () => {
updates: { comment: 'remote note' }
})
expect(store.setWorktreeMetaForHost).toHaveBeenCalledWith(
'repo-1::/workspace/feature-wt',
'ssh:build-box',
expect(store.setWorktreeMeta).toHaveBeenCalledExactlyOnceWith(
hostWorktreeTarget({ id: 'repo-1::/workspace/feature-wt', hostId: 'ssh:build-box' }),
{ comment: 'remote note' }
)
expect(store.setWorktreeMeta).not.toHaveBeenCalled()
expect(result).toMatchObject({ comment: 'remote note' })
})
@@ -232,12 +234,11 @@ describe('registerWorktreeHandlers', () => {
})
).toThrow('Invalid execution host identity.')
expect(store.setWorktreeMetaForHost).not.toHaveBeenCalled()
expect(store.setWorktreeMeta).not.toHaveBeenCalled()
})
it('pushes a remote-client invalidation for renames but not read-state updates', () => {
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
handlers['worktrees:updateMeta'](null, {
worktreeId: 'repo-1::/workspace/feature-wt',
@@ -264,11 +265,13 @@ describe('registerWorktreeHandlers', () => {
store.getRepos.mockReturnValue([folderRepo])
store.getWorktreeMeta.mockImplementation((id: string) => table[id])
store.getAllWorktreeMeta.mockImplementation(() => table)
store.setWorktreeMeta.mockImplementation((id: string, meta: Partial<WorktreeMeta>) => {
table[id] = mergeWorktreeMetaForWrite(table[id], meta)
return table[id]
})
store.removeWorktreeMeta.mockImplementation((id: string) => {
store.setWorktreeMeta.mockImplementation(
({ id }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
table[id] = mergeWorktreeMetaForWrite(table[id], meta)
return table[id]
}
)
store.removeWorktreeMeta.mockImplementation(({ id }: HostWorktreeTarget) => {
delete table[id]
})
table[workspaceId] = mergeWorktreeMetaForWrite(undefined, {
@@ -329,7 +332,7 @@ describe('registerWorktreeHandlers', () => {
})
it('persists display-name provenance at the host boundary', () => {
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
handlers['worktrees:updateMeta'](null, {
worktreeId: 'repo-1::/workspace/feature-wt',
@@ -337,13 +340,13 @@ describe('registerWorktreeHandlers', () => {
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/feature-wt',
hostWorktreeTarget({ id: 'repo-1::/workspace/feature-wt', hostId: 'local' }),
expect.objectContaining({ displayName: 'Agent label', displayNameIsPinned: true })
)
})
it('does not trust renderer-authored automation provenance during local create', async () => {
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/improve-dashboard',
@@ -371,7 +374,7 @@ describe('registerWorktreeHandlers', () => {
})
const persistedMeta = store.setWorktreeMeta.mock.calls.find(
([worktreeId]) => worktreeId === 'repo-1::/workspace/improve-dashboard'
([target]) => target.id === 'repo-1::/workspace/improve-dashboard'
)?.[1]
expect(persistedMeta).toBeDefined()
expect(persistedMeta).not.toHaveProperty('automationProvenance')
@@ -387,7 +390,7 @@ describe('registerWorktreeHandlers', () => {
isMainWorktree: false
}
])
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
const result = await handlers['worktrees:create'](null, {
repoId: 'repo-1',
@@ -396,7 +399,7 @@ describe('registerWorktreeHandlers', () => {
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/improve-dashboard',
hostWorktreeTarget({ id: 'repo-1::/workspace/improve-dashboard', hostId: 'local' }),
expect.objectContaining({
displayName: 'Fix: dashboards for PRs'
})
@@ -418,7 +421,7 @@ describe('registerWorktreeHandlers', () => {
isMainWorktree: false
}
])
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
@@ -426,7 +429,7 @@ describe('registerWorktreeHandlers', () => {
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/feature',
hostWorktreeTarget({ id: 'repo-1::/workspace/feature', hostId: 'local' }),
expect.objectContaining({ displayName: 'feature', displayNameIsPinned: true })
)
})
@@ -441,7 +444,7 @@ describe('registerWorktreeHandlers', () => {
isMainWorktree: false
}
])
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
const result = await handlers['worktrees:create'](null, {
repoId: 'repo-1',
@@ -453,7 +456,7 @@ describe('registerWorktreeHandlers', () => {
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/improve-dashboard',
hostWorktreeTarget({ id: 'repo-1::/workspace/improve-dashboard', hostId: 'local' }),
expect.objectContaining({
linkedIssue: 123,
linkedPR: 456,
@@ -481,7 +484,7 @@ describe('registerWorktreeHandlers', () => {
isMainWorktree: false
}
])
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
const result = await handlers['worktrees:create'](null, {
repoId: 'repo-1',
@@ -490,7 +493,7 @@ describe('registerWorktreeHandlers', () => {
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/improve-dashboard',
hostWorktreeTarget({ id: 'repo-1::/workspace/improve-dashboard', hostId: 'local' }),
expect.objectContaining({
createdWithAgent: 'codex'
})
@@ -515,7 +518,7 @@ describe('registerWorktreeHandlers', () => {
isMainWorktree: false
}
])
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
@@ -560,7 +563,7 @@ describe('registerWorktreeHandlers', () => {
// Exact object, not objectContaining: `remoteCreated` must stay absent until
// something actually mints the remote.
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/improve-dashboard',
hostWorktreeTarget({ id: 'repo-1::/workspace/improve-dashboard', hostId: 'local' }),
expect.objectContaining({
pushTarget: {
remoteName: 'pr-prateek-orca',
@@ -594,7 +597,7 @@ describe('registerWorktreeHandlers', () => {
store.getAllWorktreeMeta.mockReturnValue({
'repo-1::/workspace/previous-fix': makeWorktreeMeta({ pushTarget: existingPushTarget })
})
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
gitExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (args[0] === 'remote' && args.length === 1) {
return { stdout: 'pr-contributor-orca\n', stderr: '' }
@@ -620,7 +623,7 @@ describe('registerWorktreeHandlers', () => {
expect.any(Object)
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/improve-dashboard',
hostWorktreeTarget({ id: 'repo-1::/workspace/improve-dashboard', hostId: 'local' }),
expect.objectContaining({
pushTarget: {
remoteName: 'pr-contributor-orca',
@@ -12,6 +12,7 @@ import {
import { handlers, mainWindow, setupWorktreeHandlers, store } from './worktrees-test-harness'
import { mockKnownFeatureWorktree } from './worktrees-test-fixtures'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -446,7 +447,9 @@ describe('registerWorktreeHandlers', () => {
)
expect(killAllProcessesForWorktreeMock).toHaveBeenCalledTimes(3)
for (const item of approved) {
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(`repo-1::${item.path}`, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: `repo-1::${item.path}`, hostId: 'local' })
)
}
})
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { listWorktreesMock } from './worktrees-test-module-mocks'
import { handlers, setupWorktreeHandlers, store } from './worktrees-test-harness'
import { makeWorktreeMeta } from './worktrees-test-fixtures'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -114,7 +115,7 @@ describe('registerWorktreeHandlers', () => {
}[]
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/discovered-wt',
hostWorktreeTarget({ id: 'repo-1::/workspace/discovered-wt', hostId: 'local' }),
expect.objectContaining({
lastActivityAt: expect.any(Number),
projectId: 'repo:repo-1',
@@ -167,11 +168,14 @@ describe('registerWorktreeHandlers', () => {
projectHostSetupId?: string
}[]
expect(store.setWorktreeMeta).toHaveBeenCalledWith('repo-1::/workspace/existing-wt', {
projectId: 'repo:repo-1',
hostId: 'local',
projectHostSetupId: 'repo-1'
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'repo-1::/workspace/existing-wt', hostId: 'local' }),
{
projectId: 'repo:repo-1',
hostId: 'local',
projectHostSetupId: 'repo-1'
}
)
expect(listed[0].lastActivityAt).toBe(42)
expect(listed[0]).toMatchObject({
projectId: 'repo:repo-1',
@@ -236,9 +240,12 @@ describe('registerWorktreeHandlers', () => {
projectHostSetupId?: string
}[]
expect(store.setWorktreeMeta).toHaveBeenCalledWith('repo-1::/workspace/existing-wt', {
projectId: 'github:stablyai/orca'
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'repo-1::/workspace/existing-wt', hostId: 'local' }),
{
projectId: 'github:stablyai/orca'
}
)
expect(listed[0]).toMatchObject({
id: 'repo-1::/workspace/existing-wt',
projectId: 'github:stablyai/orca',
@@ -345,9 +352,12 @@ describe('registerWorktreeHandlers', () => {
}[]
expect(listWorktreesMock).not.toHaveBeenCalled()
expect(store.setWorktreeMeta).toHaveBeenCalledWith('repo-ssh::/remote/orca', {
projectId: 'github:stablyai/orca'
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'repo-ssh::/remote/orca', hostId: 'ssh:ssh-target-1' }),
{
projectId: 'github:stablyai/orca'
}
)
expect(listed).toEqual([
expect.objectContaining({
id: 'repo-ssh::/remote/orca',
@@ -425,7 +435,7 @@ describe('registerWorktreeHandlers', () => {
}[]
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/existing-wt',
hostWorktreeTarget({ id: 'repo-1::/workspace/existing-wt', hostId: 'local' }),
expect.objectContaining({
instanceId: expect.any(String),
projectId: 'repo:repo-1',
@@ -472,7 +482,7 @@ describe('registerWorktreeHandlers', () => {
await handlers['worktrees:list'](null, { repoId: 'repo-1' })
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/folder',
hostWorktreeTarget({ id: 'repo-1::/workspace/folder', hostId: 'local' }),
expect.objectContaining({
lastActivityAt: expect.any(Number),
projectId: 'repo:repo-1',
@@ -502,7 +512,7 @@ describe('registerWorktreeHandlers', () => {
}[]
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/discovered-wt',
hostWorktreeTarget({ id: 'repo-1::/workspace/discovered-wt', hostId: 'local' }),
expect.objectContaining({ lastActivityAt: expect.any(Number) })
)
expect(listed[0]).toMatchObject({
@@ -10,6 +10,10 @@ import {
} from './worktrees-test-module-mocks'
import { handlers, setupWorktreeHandlers, store } from './worktrees-test-harness'
import type { WorktreeRuntimeStub } from './worktrees-test-runtime-stub'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -144,7 +148,7 @@ describe('registerWorktreeHandlers', () => {
{ checkoutExistingBranch: true }
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/fix-bug-0',
hostWorktreeTarget({ id: 'repo-1::/workspace/fix-bug-0', hostId: 'local' }),
expect.objectContaining({ preserveBranchOnDelete: true })
)
expect(result).toMatchObject({
@@ -202,7 +206,7 @@ describe('registerWorktreeHandlers', () => {
{ checkoutExistingBranch: true }
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/my-folder',
hostWorktreeTarget({ id: 'repo-1::/workspace/my-folder', hostId: 'local' }),
expect.objectContaining({ preserveBranchOnDelete: true })
)
expect(result).toMatchObject({
@@ -312,7 +316,7 @@ describe('registerWorktreeHandlers', () => {
isMainWorktree: false
}
])
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
getPRForBranchMock.mockResolvedValueOnce({
number: 42,
title: 'Selected PR',
@@ -347,7 +351,7 @@ describe('registerWorktreeHandlers', () => {
{ cwd: '/workspace/fix-title' }
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/fix-title',
hostWorktreeTarget({ id: 'repo-1::/workspace/fix-title', hostId: 'local' }),
expect.objectContaining({
baseRef: 'refs/remotes/origin/main',
linkedPR: 42
@@ -373,7 +377,7 @@ describe('registerWorktreeHandlers', () => {
isMainWorktree: false
}
])
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
@@ -386,7 +390,7 @@ describe('registerWorktreeHandlers', () => {
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/fix-title',
hostWorktreeTarget({ id: 'repo-1::/workspace/fix-title', hostId: 'local' }),
expect.objectContaining({
baseRef: 'refs/remotes/origin/main',
linkedGitLabMR: 7
@@ -407,7 +411,7 @@ describe('registerWorktreeHandlers', () => {
isMainWorktree: false
}
])
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
getHostedReviewForBranchMock.mockResolvedValueOnce({
provider: 'bitbucket',
number: 11,
@@ -438,7 +442,7 @@ describe('registerWorktreeHandlers', () => {
{}
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/bitbucket-title',
hostWorktreeTarget({ id: 'repo-1::/workspace/bitbucket-title', hostId: 'local' }),
expect.objectContaining({ linkedBitbucketPR: 11 })
)
expect(getHostedReviewForBranchMock).toHaveBeenCalledWith(
@@ -494,7 +498,7 @@ describe('registerWorktreeHandlers', () => {
{}
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/bitbucket-title-2',
hostWorktreeTarget({ id: 'repo-1::/workspace/bitbucket-title-2', hostId: 'local' }),
expect.objectContaining({ linkedBitbucketPR: 11 })
)
})
@@ -23,6 +23,7 @@ import {
retryFailedWorktreeRemoval,
startBackgroundWorktreeRemoval
} from '../worktree-background-removal'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -242,7 +243,9 @@ describe('a failed delete Git no longer registers, over desktop IPC', () => {
featureId,
expect.objectContaining({ requirePhysicalStop: true })
)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(featureId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: featureId, hostId: 'local' })
)
expect(await listFeature()).toBeUndefined()
})
+25 -8
View File
@@ -18,6 +18,7 @@ import {
import { handlers, mainWindow, setupWorktreeHandlers, store } from './worktrees-test-harness'
import type { WorktreeRuntimeStub } from './worktrees-test-runtime-stub'
import { SshHostNotServedError } from '../providers/ssh-host-not-served-error'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -139,7 +140,9 @@ describe('registerWorktreeHandlers', () => {
})
expect(runtimeStub.clearOptimisticReconcileToken).toHaveBeenCalledWith(worktreeId)
// The purge must be scoped to the same owner the sweep used, or the ssh:* partition keeps this worktree's session state.
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'ssh:ssh-dead')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'ssh:ssh-dead' })
)
expect(advertisedUrlWatcherForgetWorktreeMock).toHaveBeenCalledWith(worktreeId)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId)
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
@@ -157,7 +160,9 @@ describe('registerWorktreeHandlers', () => {
await expect(handlers['worktrees:forgetLocal'](null, { worktreeId })).resolves.toEqual({})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
repoId: 'repo-1'
})
@@ -198,7 +203,9 @@ describe('registerWorktreeHandlers', () => {
await expect(resolveRegisteredWorktreePath(worktreePath, store as never)).rejects.toThrow(
'Access denied: unknown repository or worktree path'
)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'runtime:env-1')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'runtime:env-1' })
)
expect(runtimeStub.clearOptimisticReconcileToken).toHaveBeenCalledWith(worktreeId)
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
repoId: 'repo-gone'
@@ -227,7 +234,9 @@ describe('registerWorktreeHandlers', () => {
await expect(handlers['worktrees:forgetLocal'](null, { worktreeId })).resolves.toEqual({})
// Without the resolved owner the purge resolves to [local] only and ssh:ssh-live keeps tabsByWorktree et al. forever.
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'ssh:ssh-live')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'ssh:ssh-live' })
)
})
it('rejects an unqualified forget when the repo id has owners on two hosts', async () => {
@@ -273,7 +282,9 @@ describe('registerWorktreeHandlers', () => {
await expect(handlers['worktrees:forgetLocal'](null, { worktreeId })).resolves.toEqual({})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
})
it('rejects forgetting a folder project root', async () => {
@@ -316,7 +327,9 @@ describe('registerWorktreeHandlers', () => {
handlers['worktrees:forgetLocal'](null, { worktreeId, hostId: 'ssh:removed' })
).resolves.toEqual({})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'ssh:removed')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'ssh:removed' })
)
})
})
})
@@ -629,7 +642,11 @@ describe('worktree execution-host disambiguation', () => {
finishFirst()
await expect(first).resolves.toEqual({})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'ssh:conn-1')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'ssh:conn-1' })
)
})
})
@@ -109,7 +109,7 @@ function makeRepo(fields: Record<string, unknown>) {
function useRepo(repo: ReturnType<typeof makeRepo>): void {
store.getRepos.mockReturnValue([repo])
store.getRepo.mockReturnValue(repo)
store.setWorktreeMeta.mockImplementation((_worktreeId: string, meta: unknown) => meta)
store.setWorktreeMeta.mockImplementation((_target: unknown, meta: unknown) => meta)
}
describe('registerWorktreeHandlers', () => {
@@ -4,6 +4,10 @@ import type { Worktree } from '../../shared/worktree/types'
import { listWorktreesMock } from './worktrees-test-module-mocks'
import { handlers, setupWorktreeHandlers, store } from './worktrees-test-harness'
import { makeWorktreeMeta } from './worktrees-test-fixtures'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -100,10 +104,12 @@ describe('registerWorktreeHandlers', () => {
[childId]: { instanceId: 'child-instance' }
}
store.getWorktreeMeta.mockImplementation((id: string) => metaById[id])
store.setWorktreeMeta.mockImplementation((id: string, updates: Partial<WorktreeMeta>) => ({
...metaById[id],
...updates
}))
store.setWorktreeMeta.mockImplementation(
({ id }: HostWorktreeTarget, updates: Partial<WorktreeMeta>) => ({
...metaById[id],
...updates
})
)
store.getAllWorktreeLineage.mockReturnValue({
[childId]: {
worktreeId: childId,
@@ -279,7 +285,7 @@ describe('registerWorktreeHandlers', () => {
await handlers['worktrees:list'](null, { repoId: 'repo-ssh' })
expect(store.setWorktreeMeta).not.toHaveBeenCalledWith(
'repo-ssh::/remote/missing-parent',
hostWorktreeTarget({ id: 'repo-ssh::/remote/missing-parent', hostId: 'local' }),
expect.objectContaining({ instanceId: expect.any(String) })
)
})
@@ -4,6 +4,7 @@ import { handlers, setupWorktreeHandlers, store } from './worktrees-test-harness
import { makeWorktreeMeta } from './worktrees-test-fixtures'
import { buildDetectedGitWorktrees } from './worktrees/listing/ssh-worktree-fallback'
import type { Store } from '../persistence/loading-store/store'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -224,11 +225,14 @@ describe('registerWorktreeHandlers', () => {
})
])
expect(store.getWorktreeMeta).not.toHaveBeenCalled()
expect(store.setWorktreeMeta).toHaveBeenCalledWith('repo-ssh::/remote/feature-wt', {
projectId: 'repo:repo-ssh',
hostId: 'ssh:conn-1',
projectHostSetupId: 'repo-ssh'
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'repo-ssh::/remote/feature-wt', hostId: 'ssh:conn-1' }),
{
projectId: 'repo:repo-ssh',
hostId: 'ssh:conn-1',
projectHostSetupId: 'repo-ssh'
}
)
})
it('lists a canonical-only SSH row without leaking the same-id local row', async () => {
@@ -10,6 +10,7 @@ import {
} from './worktrees-test-module-mocks'
import { handlers, setupWorktreeHandlers, store } from './worktrees-test-harness'
import type { WorktreeRuntimeStub } from './worktrees-test-runtime-stub'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -157,7 +158,7 @@ describe('registerWorktreeHandlers', () => {
expect(addWorktreeMock).toHaveBeenCalled()
expect(result.worktree.id).toBe('repo-1::/workspace/improve-dashboard')
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/improve-dashboard',
hostWorktreeTarget({ id: 'repo-1::/workspace/improve-dashboard', hostId: 'local' }),
expect.objectContaining({ baseRef: 'refs/remotes/origin/main' })
)
})
@@ -517,7 +518,7 @@ describe('registerWorktreeHandlers', () => {
}
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/improve-dashboard',
hostWorktreeTarget({ id: 'repo-1::/workspace/improve-dashboard', hostId: 'local' }),
expect.objectContaining({ baseRef: 'refs/remotes/origin/main' })
)
expect(result.localBaseRefUpdateSuggestion).toEqual({
@@ -22,6 +22,10 @@ import {
} from './worktrees-test-module-mocks'
import { handlers, mainWindow, setupWorktreeHandlers, store } from './worktrees-test-harness'
import type { WorktreeRuntimeStub } from './worktrees-test-runtime-stub'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -394,7 +398,7 @@ describe('registerWorktreeHandlers', () => {
{}
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/rocket',
hostWorktreeTarget({ id: 'repo-1::/workspace/rocket', hostId: 'local' }),
expect.objectContaining({ displayName: '🚀' })
)
})
@@ -446,7 +450,7 @@ describe('registerWorktreeHandlers', () => {
{}
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::../worktrees/feature',
hostWorktreeTarget({ id: 'repo-1::../worktrees/feature', hostId: 'local' }),
expect.objectContaining({
orcaCreationWorkspaceLayout: { path: '../worktrees', nestWorkspaces: false }
})
@@ -584,7 +588,7 @@ describe('registerWorktreeHandlers', () => {
kind: 'folder' as const
}
store.getRepo.mockReturnValue(repo)
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => ({
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => ({
displayName: '',
comment: '',
linkedIssue: null,
@@ -13,6 +13,7 @@ import {
import { handlers, mainWindow, setupWorktreeHandlers, store } from './worktrees-test-harness'
import { makeWorktreeMeta, mockKnownFeatureWorktree } from './worktrees-test-fixtures'
import type { WorktreeRuntimeStub } from './worktrees-test-runtime-stub'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -141,8 +142,7 @@ describe('registerWorktreeHandlers', () => {
'repo-1::/workspace/already-deleted-wt'
)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/already-deleted-wt',
'local'
hostWorktreeTarget({ id: 'repo-1::/workspace/already-deleted-wt', hostId: 'local' })
)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(
'repo-1::/workspace/already-deleted-wt'
@@ -166,7 +166,9 @@ describe('registerWorktreeHandlers', () => {
expect(runHookMock).not.toHaveBeenCalled()
expect(removeWorktreeMock).not.toHaveBeenCalled()
expect(runtimeStub.clearOptimisticReconcileToken).toHaveBeenCalledWith(worktreeId)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId)
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
repoId: 'repo-1'
@@ -188,8 +190,7 @@ describe('registerWorktreeHandlers', () => {
'repo-1::/workspace/already-deleted-wt'
)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/already-deleted-wt',
'local'
hostWorktreeTarget({ id: 'repo-1::/workspace/already-deleted-wt', hostId: 'local' })
)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(
'repo-1::/workspace/already-deleted-wt'
@@ -236,7 +237,9 @@ describe('registerWorktreeHandlers', () => {
expect(runHookMock).not.toHaveBeenCalled()
expect(removeWorktreeMock).not.toHaveBeenCalled()
expect(runtimeStub.clearOptimisticReconcileToken).toHaveBeenCalledWith(worktreeId)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId)
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
repoId: 'repo-1'
@@ -327,7 +330,9 @@ describe('registerWorktreeHandlers', () => {
catalogVersion: anyCatalogVersion
})
expect(removeWorktreeMock).not.toHaveBeenCalled()
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
} finally {
await rm(parentDir, { recursive: true, force: true })
}
+31 -10
View File
@@ -37,6 +37,7 @@ import {
} from './worktrees-test-harness'
import { makeWorktreeMeta, mockKnownFeatureWorktree } from './worktrees-test-fixtures'
import type { WorktreeRuntimeStub } from './worktrees-test-runtime-stub'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -233,7 +234,9 @@ describe('registerWorktreeHandlers', () => {
try {
await handlers['worktrees:remove'](null, { worktreeId, hostId: 'local' })
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(advertisedUrlWatcherForgetWorktreeMock).not.toHaveBeenCalled()
expect(deleteWorktreeHistoryDirMock).not.toHaveBeenCalled()
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
@@ -355,7 +358,9 @@ describe('registerWorktreeHandlers', () => {
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], {
cwd: '/workspace/repo'
})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith('repo-1::/workspace/feature-wt', 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'repo-1::/workspace/feature-wt', hostId: 'local' })
)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith('repo-1::/workspace/feature-wt')
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
repoId: 'repo-1'
@@ -407,7 +412,9 @@ describe('registerWorktreeHandlers', () => {
await expect(handlers['worktrees:remove'](null, { worktreeId })).resolves.toEqual({
catalogVersion: anyCatalogVersion
})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(finish).toHaveBeenLastCalledWith(true)
} finally {
removePath.mockRestore()
@@ -451,7 +458,9 @@ describe('registerWorktreeHandlers', () => {
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], {
cwd: '/workspace/repo'
})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
repoId: 'repo-1'
})
@@ -571,7 +580,9 @@ describe('registerWorktreeHandlers', () => {
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], {
cwd: '/workspace/repo'
})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
}
)
@@ -596,7 +607,9 @@ describe('registerWorktreeHandlers', () => {
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], {
cwd: '/workspace/repo'
})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect((await lstat(markerPath)).isFile()).toBe(true)
} finally {
await rm(root, { recursive: true, force: true })
@@ -683,7 +696,9 @@ describe('registerWorktreeHandlers', () => {
expect(killAllProcessesForWorktreeMock.mock.invocationCallOrder[0]).toBeLessThan(
store.removeWorktreeMeta.mock.invocationCallOrder[0]
)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(advertisedUrlWatcherForgetWorktreeMock).toHaveBeenCalledWith(worktreeId)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId)
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
@@ -705,7 +720,9 @@ describe('registerWorktreeHandlers', () => {
await handlers['worktrees:remove'](null, { worktreeId })
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
repoId: 'repo-folder'
})
@@ -745,7 +762,9 @@ describe('registerWorktreeHandlers', () => {
includeLocalRegistry: false,
closeStructuredSessions: true
})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'ssh:conn-1')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'ssh:conn-1' })
)
expect(advertisedUrlWatcherForgetWorktreeMock).not.toHaveBeenCalled()
expect(deleteWorktreeHistoryDirMock).not.toHaveBeenCalled()
})
@@ -817,7 +836,9 @@ describe('registerWorktreeHandlers', () => {
worktreeIds: [worktreeId]
})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'ssh:target-a')
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'ssh:target-a' })
)
expect(agentHookServer.getStatusSnapshot().map((row) => row.paneKey)).toEqual([otherHostPane])
} finally {
agentHookServer.dropStatusEntriesByTabPrefix('tab-scan')
@@ -10,6 +10,7 @@ import {
} from './worktrees-test-module-mocks'
import { handlers, mainWindow, setupWorktreeHandlers, store } from './worktrees-test-harness'
import { createdWorktreeList } from './worktrees-test-fixtures'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -225,7 +226,7 @@ describe('registerWorktreeHandlers', () => {
{}
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/improve-dashboard',
hostWorktreeTarget({ id: 'repo-1::/workspace/improve-dashboard', hostId: 'local' }),
expect.objectContaining({
sparseDirectories: ['packages/web', 'apps/api'],
sparseBaseRef: 'origin/main',
@@ -293,7 +294,7 @@ describe('registerWorktreeHandlers', () => {
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/improve-dashboard',
hostWorktreeTarget({ id: 'repo-1::/workspace/improve-dashboard', hostId: 'local' }),
expect.objectContaining({
sparseDirectories: ['packages/web'],
sparseBaseRef: 'origin/main',
@@ -330,7 +331,7 @@ describe('registerWorktreeHandlers', () => {
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/improve-dashboard',
hostWorktreeTarget({ id: 'repo-1::/workspace/improve-dashboard', hostId: 'local' }),
expect.objectContaining({
sparseDirectories: ['packages/web'],
sparseBaseRef: 'origin/main',
@@ -14,6 +14,10 @@ import {
} from './worktrees-test-module-mocks'
import { handlers, ipcEvent, setupWorktreeHandlers, store } from './worktrees-test-harness'
import { makeWorktreeMeta } from './worktrees-test-fixtures'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -369,7 +373,7 @@ describe('registerWorktreeHandlers', () => {
store.getProjectHostSetups.mockReturnValue([])
store.getAllWorktreeMeta.mockReturnValue(metaById)
store.getWorktreeMeta.mockImplementation((worktreeId: string) => metaById[worktreeId])
store.removeWorktreeMeta.mockImplementation((worktreeId: string) => {
store.removeWorktreeMeta.mockImplementation(({ id: worktreeId }: HostWorktreeTarget) => {
delete metaById[worktreeId]
})
@@ -390,7 +394,9 @@ describe('registerWorktreeHandlers', () => {
forgottenWorktreeIds: ['repo-1::/remote/deleted', 'repo-1::/remote/deleted-too']
})
expect(store.removeWorktreeMeta).toHaveBeenCalledTimes(2)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith('repo-1::/remote/deleted', sshHostId)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'repo-1::/remote/deleted', hostId: sshHostId })
)
const pruneTargets = [
{ worktreeId: 'repo-1::/remote/deleted', executionHostId: sshHostId },
{ worktreeId: 'repo-1::/remote/deleted-too', executionHostId: sshHostId }
@@ -461,7 +467,7 @@ describe('registerWorktreeHandlers', () => {
store.getRepos.mockReturnValue([runtimeRepo])
store.getProjectHostSetups.mockReturnValue([])
store.getAllWorktreeMeta.mockReturnValue(metaById)
store.removeWorktreeMeta.mockImplementation((worktreeId: string) => {
store.removeWorktreeMeta.mockImplementation(({ id: worktreeId }: HostWorktreeTarget) => {
delete metaById[worktreeId]
})
@@ -474,8 +480,7 @@ describe('registerWorktreeHandlers', () => {
// The row stamped to another host needs that host's own scan, not this one's.
expect(forgotten).toEqual({ forgottenWorktreeIds: ['repo-1::/home/orca/deleted'] })
expect(store.removeWorktreeMeta).toHaveBeenCalledExactlyOnceWith(
'repo-1::/home/orca/deleted',
runtimeHostId
hostWorktreeTarget({ id: 'repo-1::/home/orca/deleted', hostId: runtimeHostId })
)
})
-4
View File
@@ -139,7 +139,6 @@ export function setupWorktreeHandlers(): WorktreeRuntimeStub {
store.getAllWorktreeMeta,
store.captureNativeLocalWorktreeMetadataScanExpectation,
store.setWorktreeMeta,
store.setWorktreeMetaForHost,
store.getProjectHostSetups,
store.removeWorktreeMeta,
store.pruneSessionlessMissingLocalWorktreeMetadataForRepo,
@@ -207,9 +206,6 @@ export function setupWorktreeHandlers(): WorktreeRuntimeStub {
store.getRetiredWorktreeNameRegistry.mockReturnValue({ exhaustedTiers: 0, names: [] })
resetRetirementCollisionKeyCacheForTests()
store.setWorktreeMeta.mockReturnValue({})
store.setWorktreeMetaForHost.mockImplementation((...args: unknown[]) =>
store.setWorktreeMeta(args[0] as string, args[2] as object)
)
store.getProjectHostSetups.mockReturnValue([
{
id: 'repo-1',
+8 -5
View File
@@ -1,6 +1,7 @@
import { EventEmitter } from 'node:events'
import { type Mock, vi } from 'vitest'
import type { WorktreeMeta } from '../../shared/worktree/meta-types'
import type { HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
export type HandlerMap = Record<string, (_event: unknown, args: unknown) => unknown>
@@ -9,7 +10,11 @@ type StoreMock = Mock<(...args: unknown[]) => unknown>
/** Store lookups tests re-implement per id, so the first arg stays narrowed. */
type KeyedStoreMock = Mock<(id: string, ...rest: unknown[]) => unknown>
/** Store writers tests re-implement by merging the patch they receive. */
type KeyedStoreWriteMock = Mock<(id: string, patch: Partial<WorktreeMeta>) => unknown>
type KeyedStoreWriteMock = Mock<
(target: HostWorktreeTarget, patch: Partial<WorktreeMeta>) => unknown
>
/** Host-qualified store removals. */
type TargetStoreMock = Mock<(target: HostWorktreeTarget) => unknown>
export type TestMainWindow = {
isDestroyed: () => boolean
@@ -28,13 +33,12 @@ export type TestStore = {
getAllWorktreeMeta: StoreMock
captureNativeLocalWorktreeMetadataScanExpectation: StoreMock
setWorktreeMeta: KeyedStoreWriteMock
setWorktreeMetaForHost: StoreMock
getProjectHostSetups: StoreMock
removeWorktreeMeta: KeyedStoreMock
removeWorktreeMeta: TargetStoreMock
pruneSessionlessMissingLocalWorktreeMetadataForRepo: StoreMock
removeWorkspaceSessionStateForWorktree: KeyedStoreMock
getAllWorktreeLineage: StoreMock
removeWorktreeLineage: KeyedStoreMock
removeWorktreeLineage: TargetStoreMock
getAllWorkspaceLineage: StoreMock
getFolderWorkspaces: StoreMock
getProjectGroups: StoreMock
@@ -64,7 +68,6 @@ export const store: TestStore = {
getAllWorktreeMeta: vi.fn(),
captureNativeLocalWorktreeMetadataScanExpectation: vi.fn(),
setWorktreeMeta: vi.fn(),
setWorktreeMetaForHost: vi.fn(),
getProjectHostSetups: vi.fn(),
removeWorktreeMeta: vi.fn(),
pruneSessionlessMissingLocalWorktreeMetadataForRepo: vi.fn(),
+3 -3
View File
@@ -149,6 +149,7 @@ vi.mock('./worktree-logic', async (importOriginal) => {
import { registerWorktreeHandlers } from './worktrees'
import { resetRetirementCollisionKeyCacheForTests } from '../worktree-name-retirement'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
type HandlerMap = Record<string, (_event: unknown, args: unknown) => unknown>
@@ -349,7 +350,7 @@ describe('registerWorktreeHandlers – Windows path handling', () => {
// A name the user typed is never retired — the pool holds ordinary words people choose.
expect(store.addRetiredWorktreeName).not.toHaveBeenCalled()
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::C:/workspaces/improve-dashboard',
hostWorktreeTarget({ id: 'repo-1::C:/workspaces/improve-dashboard', hostId: 'local' }),
expect.objectContaining({
lastActivityAt: expect.any(Number)
})
@@ -598,8 +599,7 @@ describe('registerWorktreeHandlers – Windows path handling', () => {
})
)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
'repo-1::C:/workspaces/improve-dashboard',
'local'
hostWorktreeTarget({ id: 'repo-1::C:/workspaces/improve-dashboard', hostId: 'local' })
)
// Windows history lives under a path-derived hash, so scheduling must not regress on this path only.
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(
+14 -5
View File
@@ -19,9 +19,10 @@ import {
listWorktreesMock,
removeWorktreeMock
} from './worktrees-test-module-mocks'
import { handlers, setupWorktreeHandlers, store } from './worktrees-test-harness'
import { handlers, harnessRepo, setupWorktreeHandlers, store } from './worktrees-test-harness'
import { rememberPreservedBranchCleanupTarget } from './worktrees/removal/preserved-branch-cleanup'
import * as issueCommandFile from '../issue-command-file'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -51,6 +52,8 @@ const STAYING = 'repo-2::/workspace/other-wt'
let userDataPath: string
beforeEach(() => {
setupWorktreeHandlers()
// The staying worktree's repo must be registered: an unstamped write resolves its host from it.
store.getRepos.mockReturnValue([harnessRepo, { ...harnessRepo, id: 'repo-2' }])
userDataPath = mkdtempSync(join(tmpdir(), 'wsl-move-worktree-ipc-'))
loadWslMoveFence(userDataPath)
seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced'))
@@ -126,7 +129,10 @@ describe('worktree IPC under a WSL move', () => {
expect(() => updateMeta(MOVING, { comment: 'x' })).toThrow(WslMoveProjectFencedError)
expect(store.setWorktreeMeta).not.toHaveBeenCalled()
updateMeta(STAYING, { comment: 'x' })
expect(store.setWorktreeMeta).toHaveBeenCalledWith(STAYING, { comment: 'x' })
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: STAYING, hostId: 'local' }),
{ comment: 'x' }
)
})
it('drops the moving worktree from a sort-order batch', async () => {
@@ -135,8 +141,11 @@ describe('worktree IPC under a WSL move', () => {
)
await handlers['worktrees:persistSortOrder'](null, { orderedIds: [MOVING, STAYING] })
expect(store.setWorktreeMeta).toHaveBeenCalledTimes(1)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(STAYING, {
sortOrder: expect.any(Number)
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: STAYING, hostId: 'local' }),
{
sortOrder: expect.any(Number)
}
)
})
})
@@ -24,6 +24,7 @@ import {
mockKnownFeatureWorktree,
mockSelectedWslProjectRuntime
} from './worktrees-test-fixtures'
import type { HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -260,7 +261,7 @@ describe('registerWorktreeHandlers', () => {
isMainWorktree: false
}
])
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
@@ -277,7 +278,7 @@ describe('registerWorktreeHandlers', () => {
expect(calls.some((args) => args[0] === 'remote' && args[1] === 'add')).toBe(false)
expect(calls.some((args) => args[0] === 'fetch')).toBe(false)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ id: expect.any(String) }),
expect.objectContaining({
pushTarget: {
remoteName: 'pr-contributor-orca',
@@ -6,6 +6,7 @@ import type { Store } from '../../../persistence/loading-store/store'
import type { CreateWorktreeArgsWithSystemProvenance } from '../ipc-context-schemas'
import { getFolderWorkspaceInstanceId, mergeFolderWorkspace } from '../folder-workspace-model'
import { resolveWorktreeCreateDisplayNameRequest } from '../../worktree-display-name'
import { hostWorktreeTargetForRepo } from '../../../persistence/host-qualified-worktree-meta'
export function createFolderWorkspace(
args: CreateWorktreeArgsWithSystemProvenance,
@@ -22,7 +23,7 @@ export function createFolderWorkspace(
args.cliProvenance?.kind === 'created-by-cli',
args.nameWasGenerated === true
)
const meta = store.setWorktreeMeta(worktreeId, {
const meta = store.setWorktreeMeta(hostWorktreeTargetForRepo(worktreeId, repo), {
instanceId,
...(store.getProjectHostSetups
? getProjectHostSetupWorktreeMeta(store.getProjectHostSetups(), repo)
@@ -9,6 +9,7 @@ import type { WorktreeMeta } from '../../../../shared/worktree/meta-types'
import type { GitWorktreeInfo } from '../../../../shared/worktree/types'
import type * as NodeCryptoModule from 'node:crypto'
import type * as OwnershipModule from '../../../../shared/worktree/ownership'
import type { HostWorktreeTarget } from '../../../../shared/worktree/host-qualified-identity'
const { toDetectedWorktreeSpy } = vi.hoisted(() => ({ toDetectedWorktreeSpy: vi.fn() }))
@@ -79,6 +80,7 @@ function settledMeta(overrides: Partial<WorktreeMeta> = {}): WorktreeMeta {
function createStore(meta: Record<string, WorktreeMeta>, repos: Repo[] = [repo]) {
const rows = { ...meta }
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial store mock; the code under test reads only the members it defines.
return {
getRepos: () => repos,
getSettings: () => ({ workspaceDir: '/workspace', nestWorkspaces: true }),
@@ -89,11 +91,7 @@ function createStore(meta: Record<string, WorktreeMeta>, repos: Repo[] = [repo])
getWorktreeMetaForHost: (id: string, hostId: string) =>
rows[id]?.hostId === hostId ? rows[id] : undefined,
getAllWorktreeMetaForHost: () => rows,
setWorktreeMeta: (id: string, patch: Partial<WorktreeMeta>) => {
rows[id] = { ...rows[id], ...patch } as WorktreeMeta
return rows[id]
},
setWorktreeMetaForHost: (id: string, hostId: string, patch: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id, hostId }: HostWorktreeTarget, patch: Partial<WorktreeMeta>) => {
rows[id] = { ...rows[id], ...patch, hostId } as WorktreeMeta
return rows[id]
}
@@ -7,6 +7,7 @@ import { mergeWorktreeMetaForWrite } from '../../../persistence/loading-store/wo
import { isWslMoveRepoBlocked } from '../../../persistence/moving-wsl-projects/wsl-move-background-backoff'
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
import {
hostWorktreeTargetForRepo,
readAllWorktreeMetaForHost,
readWorktreeMetaForHost
} from '../../../persistence/host-qualified-worktree-meta'
@@ -34,7 +35,8 @@ function folderMetaWriter(
repo: Repo
): (worktreeId: string, updates: Partial<WorktreeMeta>) => WorktreeMeta {
if (!isWslMoveRepoBlocked(repo)) {
return (worktreeId, updates) => store.setWorktreeMeta(worktreeId, updates)
return (worktreeId, updates) =>
store.setWorktreeMeta(hostWorktreeTargetForRepo(worktreeId, repo), updates)
}
return (worktreeId, updates) => {
const existing = store.getWorktreeMeta(worktreeId)
@@ -29,6 +29,7 @@ import {
} from './ssh-worktree-fallback'
import type { WorktreeIpcContext } from '../worktree-ipc-context'
import { readAllWorktreeMetaForHost } from '../../../persistence/host-qualified-worktree-meta'
import { hostWorktreeTarget } from '../../../../shared/worktree/host-qualified-identity'
export function registerHostCatalogHandlers(context: WorktreeIpcContext): void {
const { store } = context
@@ -142,7 +143,9 @@ export function registerHostCatalogHandlers(context: WorktreeIpcContext): void {
if (meta.hostId && meta.hostId !== requestedExecutionHostId) {
continue
}
store.removeWorktreeMeta(worktreeId, requestedExecutionHostId)
store.removeWorktreeMeta(
hostWorktreeTarget({ id: worktreeId, hostId: requestedExecutionHostId })
)
// Why here too: a scan the host answered is positive evidence of removal, and this is the only
// path that ever retires an off-host row — so it owes the status store the same drop the
// in-Orca delete does, or the SSH rows stay stranded in `last-status.json`.
@@ -10,10 +10,8 @@ import type { Repo } from '../../../../shared/repo-types'
import type { GitWorktreeInfo, DetectedWorktree, Worktree } from '../../../../shared/worktree/types'
import type { Store } from '../../../persistence/loading-store/store'
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
import {
readWorktreeMetaForRepo,
writeWorktreeMetaForHost
} from '../../../persistence/host-qualified-worktree-meta'
import { readWorktreeMetaForRepo } from '../../../persistence/host-qualified-worktree-meta'
import { hostWorktreeTarget } from '../../../../shared/worktree/host-qualified-identity'
import { getRepoOwnedWorktreeMeta } from '../../../worktree-metadata-ownership'
import {
buildKnownOrcaWorkspaceLayouts,
@@ -117,7 +115,10 @@ export function listDisconnectedSshWorktrees(
? { ...candidate.meta, ...ownershipUpdates }
: candidate.meta
if (Object.keys(ownershipUpdates).length > 0) {
writeWorktreeMetaForHost(store, candidate.id, expectedHostId, ownershipUpdates)
store.setWorktreeMeta(
hostWorktreeTarget({ id: candidate.id, hostId: expectedHostId }),
ownershipUpdates
)
}
// Why: synthesized rows carry no branch, so the title would fall through to the DESKTOP's basename()
// applied to a REMOTE path — a Windows remote then renders its whole C:\... path as the name. Rows must
@@ -2,10 +2,9 @@ import type { Store } from '../../../persistence/loading-store/store'
import type { Repo } from '../../../../shared/repo-types'
import type { WorktreeMeta } from '../../../../shared/worktree/meta-types'
import { getProjectHostSetupWorktreeMeta } from '../../../../shared/project-host-setup-lookup'
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
import {
readWorktreeMetaForRepo,
writeWorktreeMetaForHost
hostWorktreeTargetForRepo,
readWorktreeMetaForRepo
} from '../../../persistence/host-qualified-worktree-meta'
import { getRepoOwnedWorktreeMeta } from '../../../worktree-metadata-ownership'
import { randomUUID } from 'node:crypto'
@@ -39,7 +38,7 @@ export function resolveWorktreeMetaWithDiscoveryBackfill(
allMetaOverride?: Record<string, WorktreeMeta>,
repoOwnerCount = store.getRepos().filter((candidate) => candidate.id === repo.id).length
): WorktreeMeta {
const executionHostId = getRepoExecutionHostId(repo)
const target = hostWorktreeTargetForRepo(worktreeId, repo)
const allMeta = allMetaOverride ?? store.getAllWorktreeMeta?.()
// Why: the locator-keyed row is only a stand-in for a missing snapshot, so don't read it when we have one.
const legacyMeta = allMeta === undefined ? store.getWorktreeMeta?.(worktreeId) : undefined
@@ -59,11 +58,11 @@ export function resolveWorktreeMetaWithDiscoveryBackfill(
}
if (Object.keys(updates).length > 0) {
// Why: pre-lineage profiles already have WorktreeMeta rows; backfill on discovery so upgraded workspaces get lineage and host routing.
return writeWorktreeMetaForHost(store, worktreeId, executionHostId, updates)
return store.setWorktreeMeta(target, updates)
}
return existing
}
return writeWorktreeMetaForHost(store, worktreeId, executionHostId, {
return store.setWorktreeMeta(target, {
lastActivityAt: Date.now(),
...ownershipUpdates
})
@@ -10,6 +10,8 @@ import { planWorktreeSortOrderUpdates } from '../../../../shared/worktree/sort-o
import { readBranchRenameFailureOutputForDisplay } from '../../../agent-hooks/branch-rename-failure-output'
import { normalizeLinkedWorkItemFields } from '../ipc-context-schemas'
import { isRemovedFolderWorkspaceInstance } from '../../../folder-workspace-meta-presence'
import { resolveWorktreeMetaOwner } from '../../../persistence/host-qualified-worktree-meta'
import { hostWorktreeTarget } from '../../../../shared/worktree/host-qualified-identity'
import type { WorktreeIpcContext } from '../worktree-ipc-context'
import {
isWslMoveSourceHost,
@@ -63,9 +65,13 @@ export function registerWorktreeMetadataHandlers(context: WorktreeIpcContext): v
if (isRemovedFolderWorkspaceInstance(store, args.worktreeId, executionHostId)) {
return null
}
const meta = executionHostId
? store.setWorktreeMetaForHost(args.worktreeId, executionHostId, sanitizedUpdates)
: store.setWorktreeMeta(args.worktreeId, sanitizedUpdates)
const target = executionHostId
? hostWorktreeTarget({ id: args.worktreeId, hostId: executionHostId })
: resolveWorktreeMetaOwner(store, args.worktreeId)
if (!target) {
return null
}
const meta = store.setWorktreeMeta(target, sanitizedUpdates)
// Do NOT notify here: renderer already applied this optimistically; a notification would re-sort the sidebar (bug PR #209).
if (args.updates.displayName !== undefined) {
// Why: remote clients have no optimistic rename and stopped polling titles, so push a remote-only invalidation; gate on displayName so per-click isUnread updates stay event-free.
@@ -109,7 +115,10 @@ export function registerWorktreeMetadataHandlers(context: WorktreeIpcContext): v
Date.now()
).filter((update) => !isWslMoveWorkspaceBlocked(store, update.worktreeId))
for (const update of updates) {
store.setWorktreeMeta(update.worktreeId, { sortOrder: update.sortOrder })
const target = resolveWorktreeMetaOwner(store, update.worktreeId)
if (target) {
store.setWorktreeMeta(target, { sortOrder: update.sortOrder })
}
}
})
@@ -16,6 +16,7 @@ import { pruneWorktreePRRefreshAliases } from '../../../github/pr-refresh-coordi
import { recordWorkspaceCleanupRemovalSnapshotPrune } from '../../../workspace-cleanup-removal-snapshot-prune'
import { pruneWorkspaceCleanupScanSnapshot } from '../../../workspace-cleanup-scan-snapshot'
import { pruneWorkspaceSpaceAnalysisSnapshot } from '../../../workspace-space-analysis-snapshot'
import { removeWorktreeMetaRow } from '../../../persistence/host-qualified-worktree-meta'
export async function stopPtysForDestructiveWorktreeRemoval(
runtime: OrcaRuntimeService,
@@ -91,11 +92,7 @@ export function removeWorktreeMetadataAndTransientState(
hasWorktreeRemovalRepoOwnerOnOtherHost(store, repoId, hostId))
)
// Why: worktree IDs are path-derived and reusable; drop process-local caches before the same ID can map to a new workspace.
if (hostId) {
store.removeWorktreeMeta(worktreeId, hostId)
} else {
store.removeWorktreeMeta(worktreeId)
}
removeWorktreeMetaRow(store, worktreeId, hostId)
// Why outside the same-id gate: retirement is per host and per pane, so a surviving owner keeps its own.
agentHookServer.dropStatusEntriesForRemovedWorktree(worktreeId, hostId ?? persistedHostId)
if (!preservesSameIdOwner) {
+5 -2
View File
@@ -13,6 +13,7 @@ import { tmpdir } from 'node:os'
import type { PersistedState } from '../shared/persisted-state-types'
import { toRuntimeExecutionHostId, toSshExecutionHostId } from '../shared/execution-host'
import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity'
import { removeWorktreeMetaRow } from './persistence/host-qualified-worktree-meta'
// Stub the ~/.ssh/config parser so the SSH-import test drives the real Store with deterministic hosts, not the operator's actual ~/.ssh/config.
const { loadUserSshConfigMock, sshConfigHostsToTargetsMock } = vi.hoisted(() => ({
@@ -707,7 +708,9 @@ describe('Store', () => {
it('snapshots automation run workspace names for deleted-workspace history', async () => {
const store = await createStore()
store.addRepo(makeRepo())
store.setWorktreeMeta('wt1', { displayName: 'Nightly workspace' })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'wt1', hostId: 'local' }), {
displayName: 'Nightly workspace'
})
const automation = store.createAutomation({
name: 'Nightly',
prompt: 'Run checks',
@@ -721,7 +724,7 @@ describe('Store', () => {
})
const run = store.createAutomationRun(automation, new Date('2026-05-13T09:00:00Z').getTime())
store.removeWorktreeMeta('wt1')
removeWorktreeMetaRow(store, 'wt1', undefined)
expect(run.workspaceDisplayName).toBe('Nightly workspace')
expect(store.listAutomationRuns(automation.id)[0].workspaceDisplayName).toBe(
@@ -17,6 +17,7 @@ import type { WorkspaceSessionState } from '../shared/workspace-session-state-ty
import { getDefaultWorkspaceSession } from '../shared/constants'
import { folderWorkspaceKey, worktreeWorkspaceKey } from '../shared/workspace-scope'
import { createProfileStateStore } from './persistence/profile-state/profile-state-store-factory'
import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity'
// Stub the ~/.ssh/config parser so the SSH-import test drives the real Store with deterministic hosts, not the operator's actual ~/.ssh/config.
const { loadUserSshConfigMock, sshConfigHostsToTargetsMock } = vi.hoisted(() => ({
@@ -216,8 +217,14 @@ describe('Store.migrateWorktreeIdentity', () => {
it('moves meta, lineage, tabs, active pointers, and records the prior id', async () => {
const store = await createStore()
store.setWorktreeMeta(OLD, { displayName: 'Cunner', linkedIssue: 42 })
store.setWorktreeLineage(OLD, makeWorktreeLineage({ worktreeId: OLD }))
store.setWorktreeMeta(hostWorktreeTarget({ id: OLD, hostId: 'local' }), {
displayName: 'Cunner',
linkedIssue: 42
})
store.setWorktreeLineage(
hostWorktreeTarget({ id: OLD, hostId: 'local' }),
makeWorktreeLineage({ worktreeId: OLD })
)
store.setWorkspaceLineage(
makeWorkspaceLineage({
childWorkspaceKey: OLD_WORKSPACE_KEY,
@@ -322,7 +329,7 @@ describe('Store.migrateWorktreeIdentity', () => {
'runtime:env-a'
)
store.migrateWorktreeIdentity(OLD, NEW)
store.migrateWorktreeIdentity(hostWorktreeTarget({ id: OLD, hostId: 'local' }), NEW)
expect(store.getWorktreeMeta(OLD)).toBeUndefined()
const meta = store.getWorktreeMeta(NEW)
@@ -384,14 +391,16 @@ describe('Store.migrateWorktreeIdentity', () => {
it('rewrites parentWorktreeId back-references in other lineage entries', async () => {
const store = await createStore()
store.setWorktreeMeta(OLD, { displayName: 'Cunner' })
store.setWorktreeMeta(hostWorktreeTarget({ id: OLD, hostId: 'local' }), {
displayName: 'Cunner'
})
const CHILD = 'repo1::/ws/child'
store.setWorktreeLineage(
CHILD,
hostWorktreeTarget({ id: CHILD, hostId: 'local' }),
makeWorktreeLineage({ worktreeId: CHILD, parentWorktreeId: OLD })
)
store.migrateWorktreeIdentity(OLD, NEW)
store.migrateWorktreeIdentity(hostWorktreeTarget({ id: OLD, hostId: 'local' }), NEW)
expect(store.getWorktreeLineage(CHILD)?.parentWorktreeId).toBe(NEW)
})
@@ -406,7 +415,7 @@ describe('Store.migrateWorktreeIdentity', () => {
}
})
store.migrateWorktreeIdentity(OLD, NEW)
store.migrateWorktreeIdentity(hostWorktreeTarget({ id: OLD, hostId: 'local' }), NEW)
store.flush()
expect(store.getMobileClientTabSelections()['device-a']?.[OLD]).toBeUndefined()
@@ -416,10 +425,12 @@ describe('Store.migrateWorktreeIdentity', () => {
it('accumulates prior ids across chained renames', async () => {
const store = await createStore()
store.setWorktreeMeta(OLD, { displayName: 'Cunner' })
store.migrateWorktreeIdentity(OLD, NEW)
store.setWorktreeMeta(hostWorktreeTarget({ id: OLD, hostId: 'local' }), {
displayName: 'Cunner'
})
store.migrateWorktreeIdentity(hostWorktreeTarget({ id: OLD, hostId: 'local' }), NEW)
const NEWER = 'repo1::/ws/final-name'
store.migrateWorktreeIdentity(NEW, NEWER)
store.migrateWorktreeIdentity(hostWorktreeTarget({ id: NEW, hostId: 'local' }), NEWER)
expect(store.getWorktreeMeta(NEWER)?.priorWorktreeIds).toEqual([OLD, NEW])
})
@@ -430,7 +441,7 @@ describe('Store.migrateWorktreeIdentity', () => {
lastVisitedAtByWorktreeId: { [OLD]: 100, [NEW]: 900 }
})
store.migrateWorktreeIdentity(OLD, NEW)
store.migrateWorktreeIdentity(hostWorktreeTarget({ id: OLD, hostId: 'local' }), NEW)
expect(store.getWorkspaceSession().lastVisitedAtByWorktreeId?.[OLD]).toBeUndefined()
expect(store.getWorkspaceSession().lastVisitedAtByWorktreeId?.[NEW]).toBe(900)
@@ -438,8 +449,10 @@ describe('Store.migrateWorktreeIdentity', () => {
it('is a no-op when the ids match', async () => {
const store = await createStore()
store.setWorktreeMeta(OLD, { displayName: 'Cunner' })
store.migrateWorktreeIdentity(OLD, OLD)
store.setWorktreeMeta(hostWorktreeTarget({ id: OLD, hostId: 'local' }), {
displayName: 'Cunner'
})
store.migrateWorktreeIdentity(hostWorktreeTarget({ id: OLD, hostId: 'local' }), OLD)
expect(store.getWorktreeMeta(OLD)?.priorWorktreeIds).toBeUndefined()
})
})
@@ -14,7 +14,10 @@ import { rmSync, mkdtempSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { getDefaultWorkspaceSession } from '../shared/constants'
import { composeWorktreeHostIdentity } from '../shared/worktree/host-qualified-identity'
import {
composeWorktreeHostIdentity,
hostWorktreeTarget
} from '../shared/worktree/host-qualified-identity'
import { folderWorkspaceKey, worktreeWorkspaceKey } from '../shared/workspace-scope'
import type { PersistedState } from '../shared/persisted-state-types'
@@ -77,8 +80,12 @@ describe('deregistered repo residue', () => {
const seed = await createStore()
seed.addRepo(makeRepo({ id: LIVE_REPO, path: '/workspace/live' }))
seed.addRepo(makeRepo({ id: GONE_REPO, path: '/workspace/orphan' }))
seed.setWorktreeMetaForHost(LIVE_WORKTREE, 'local', { displayName: 'Live' })
seed.setWorktreeMetaForHost(GONE_WORKTREE, 'local', { displayName: 'Orphan' })
seed.setWorktreeMeta(hostWorktreeTarget({ id: LIVE_WORKTREE, hostId: 'local' }), {
displayName: 'Live'
})
seed.setWorktreeMeta(hostWorktreeTarget({ id: GONE_WORKTREE, hostId: 'local' }), {
displayName: 'Orphan'
})
seed.setWorkspaceSession(sessionFor(GONE_WORKTREE), 'local')
seed.flush()
@@ -249,7 +256,9 @@ describe('deregistered repo residue', () => {
it('leaves a profile with no orphans byte-identical across reloads', async () => {
const seed = await createStore()
seed.addRepo(makeRepo({ id: LIVE_REPO, path: '/workspace/live' }))
seed.setWorktreeMetaForHost(LIVE_WORKTREE, 'local', { displayName: 'Live' })
seed.setWorktreeMeta(hostWorktreeTarget({ id: LIVE_WORKTREE, hostId: 'local' }), {
displayName: 'Live'
})
seed.setWorkspaceSession(sessionFor(LIVE_WORKTREE), 'local')
seed.flush()
@@ -20,6 +20,7 @@ import { getDefaultPersistedState } from '../shared/constants'
import { toRuntimeExecutionHostId } from '../shared/execution-host'
import { installFakeAppEnvironment } from '../../config/scripts/vitest-host-ports-setup'
import { resetRetirementCollisionKeyCacheForTests } from './worktree-name-retirement'
import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity'
let hasCreatedStoreInCase = false
@@ -130,7 +131,9 @@ describe('deleting one host copy of a repo id shared by two hosts', () => {
const store = await createStoreFromState({
repos: duplicateIdRepos().map((repo) => ({ ...repo, kind }))
})
store.setWorktreeMetaForHost('dup::/remote/dup', 'ssh:ssh-1', { displayName: 'SSH wt' })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'dup::/remote/dup', hostId: 'ssh:ssh-1' }), {
displayName: 'SSH wt'
})
store.removeProjectForHost({ id: 'dup', executionHostId: 'local' })
expect(store.getRepos().map((repo) => repo.path)).toEqual(['/remote/dup'])
@@ -21,6 +21,7 @@ import { getDefaultWorkspaceSession } from '../shared/constants'
import { isTerminalLeafId } from '../shared/stable-pane-id'
import { worktreeWorkspaceKey } from '../shared/workspace-scope'
import { removeWorktreeMetaRow } from './persistence/host-qualified-worktree-meta'
// Stub the ~/.ssh/config parser so the SSH-import test drives the real Store with deterministic hosts, not the operator's actual ~/.ssh/config.
const { loadUserSshConfigMock, sshConfigHostsToTargetsMock } = vi.hoisted(() => ({
@@ -463,9 +464,11 @@ describe('Store host-partitioned workspace sessions', () => {
store.setWorkspaceSession(session, 'local')
store.setWorkspaceSession(session, 'runtime:env-a')
store.setWorkspaceSession(session, 'runtime:env-b')
store.setWorktreeMeta(worktreeId, { hostId: 'runtime:env-a' })
store.setWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: 'runtime:env-a' }), {
hostId: 'runtime:env-a'
})
store.removeWorktreeMeta(worktreeId)
removeWorktreeMetaRow(store, worktreeId, undefined)
expect(store.getWorkspaceSession('runtime:env-a').tabsByWorktree[worktreeId]).toBeUndefined()
// Only the owning host's partition and the local blob it may spill into are cleaned; a same-id
@@ -489,9 +492,11 @@ describe('Store host-partitioned workspace sessions', () => {
// terminal state to the host's own `ssh:*` partition. Removal must clear both.
store.setWorkspaceSession(makeSession(), 'local')
store.setWorkspaceSession(makeSession(), 'ssh:conn-1')
store.setWorktreeMeta(worktreeId, { hostId: 'ssh:conn-1' })
store.setWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: 'ssh:conn-1' }), {
hostId: 'ssh:conn-1'
})
store.removeWorktreeMeta(worktreeId, 'ssh:conn-1')
store.removeWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: 'ssh:conn-1' }))
for (const hostId of ['local', 'ssh:conn-1'] as const) {
expect(store.getWorkspaceSession(hostId)).toMatchObject({
@@ -527,9 +532,11 @@ describe('Store host-partitioned workspace sessions', () => {
},
'runtime:env-a'
)
store.setWorktreeMeta(worktreeId, { hostId: 'runtime:env-a' })
store.setWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: 'runtime:env-a' }), {
hostId: 'runtime:env-a'
})
store.removeWorktreeMeta(worktreeId, 'local')
store.removeWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: 'local' }))
expect(
store.getWorkspaceSession('runtime:env-a').terminalTopologyRevisionByRepoId?.['repo-split']
@@ -552,17 +559,22 @@ describe('Store host-partitioned workspace sessions', () => {
}
store.setWorkspaceSession(session, 'local')
store.setWorkspaceSession(session, 'runtime:env-b')
store.setWorktreeMeta(worktreeId, { hostId: 'local' })
store.setWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: 'local' }), {
hostId: 'local'
})
const worktreeLineage = makeWorktreeLineage({ worktreeId })
const workspaceLineage = makeWorkspaceLineage({
childWorkspaceKey: worktreeWorkspaceKey(worktreeId)
})
store.setWorktreeLineage(worktreeId, worktreeLineage)
store.setWorktreeLineage(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' }),
worktreeLineage
)
store.setWorkspaceLineage(workspaceLineage)
// The confirmed removal target is env-b. Bare metadata belongs to the same-id
// local owner and must not redirect the post-delete purge back to local.
store.removeWorktreeMeta(worktreeId, 'runtime:env-b')
store.removeWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: 'runtime:env-b' }))
expect(store.getWorktreeMeta(worktreeId)?.hostId).toBe('local')
expect(store.getWorktreeLineage(worktreeId)).toEqual(worktreeLineage)
@@ -597,9 +609,11 @@ describe('Store host-partitioned workspace sessions', () => {
}
store.setWorkspaceSession(localSession, 'local')
store.setWorkspaceSession(remoteSession, 'ssh:ssh-b')
store.setWorktreeMeta(worktreeId, { hostId: 'ssh:ssh-b' })
store.setWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: 'ssh:ssh-b' }), {
hostId: 'ssh:ssh-b'
})
store.removeWorktreeMeta(worktreeId, 'ssh:ssh-b')
store.removeWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: 'ssh:ssh-b' }))
expect(store.getWorkspaceSession('local').tabsByWorktree[worktreeId]).toEqual([
expect.objectContaining({ id: 'same-id-local-tab' })
@@ -620,7 +634,7 @@ describe('Store host-partitioned workspace sessions', () => {
store.setWorkspaceSession(session, 'runtime:env-a')
store.setWorkspaceSession(session, 'local')
store.removeWorktreeMeta(worktreeId, 'runtime:env-a')
store.removeWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: 'runtime:env-a' }))
expect(store.getWorkspaceSession('runtime:env-a').tabsByWorktree[worktreeId]).toBeUndefined()
expect(store.getWorkspaceSession('local').tabsByWorktree[worktreeId]).toBeUndefined()
@@ -24,6 +24,7 @@ import {
testState,
writeDataFile
} from './persistence-test-harness'
import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity'
const { trackMock, getCohortAtEmitMock } = vi.hoisted(() => ({
trackMock: vi.fn(),
@@ -267,7 +268,7 @@ describe('orcad migration catalog persistence', () => {
it('rejects dormant-state conflicts before publishing any catalog row', () => {
const store = createStore()
store.setWorktreeMeta(DORMANT_WORKTREE_ID, {
store.setWorktreeMeta(hostWorktreeTarget({ id: DORMANT_WORKTREE_ID, hostId: 'local' }), {
...dormantState().worktreeMeta[0].meta,
comment: 'Different owner'
})
@@ -5,6 +5,7 @@ import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { getDefaultWorkspaceSession } from '../shared/constants'
import type { BrowserPage, BrowserWorkspace } from '../shared/browser-workspace-types'
import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity'
vi.mock('./ssh/config/ssh-config-parser', () => ({
loadUserSshConfig: vi.fn(),
@@ -88,7 +89,9 @@ describe('remote session startup ownership', () => {
it('retains remote metadata when no session or local catalog row names its repo', () => {
const seed = createStore()
seed.setWorktreeMetaForHost(WORKTREE, HOST, { displayName: 'Remote work' })
seed.setWorktreeMeta(hostWorktreeTarget({ id: WORKTREE, hostId: HOST }), {
displayName: 'Remote work'
})
seed.flush()
const reloaded = createStore()
expect(reloaded.getWorktreeMeta(WORKTREE)).toMatchObject({ displayName: 'Remote work' })
@@ -114,7 +117,9 @@ describe('remote session startup ownership', () => {
const seed = createStore()
seed.setWorkspaceSession(browserSession(), HOST)
seed.setWorkspaceSession(browserSession(), OTHER_HOST)
seed.setWorktreeMetaForHost(WORKTREE, HOST, { displayName: 'Removed host' })
seed.setWorktreeMeta(hostWorktreeTarget({ id: WORKTREE, hostId: HOST }), {
displayName: 'Removed host'
})
seed.flush()
const reloaded = createStore()
+24 -23
View File
@@ -29,7 +29,10 @@ import {
getLocalWorktreeScanGeneration,
isLocalWorktreeScanGenerationCurrent
} from './local-worktree-scan-generation'
import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity'
import type { ExecutionHostId } from '../shared/execution-host'
const at = (id: string, hostId: ExecutionHostId) => hostWorktreeTarget({ id, hostId })
// Stub the ~/.ssh/config parser so the SSH-import test drives the real Store with deterministic hosts, not the operator's actual ~/.ssh/config.
const { loadUserSshConfigMock, sshConfigHostsToTargetsMock } = vi.hoisted(() => ({
loadUserSshConfigMock: vi.fn(),
@@ -289,9 +292,9 @@ describe('Store', () => {
store.addRepo(makeRepo({ id: 'r1' }))
store.addRepo(makeRepo({ id: 'r2', path: '/repo2' }))
store.setWorktreeMeta('r1::/path/wt1', { displayName: 'wt1' })
store.setWorktreeMeta('r1::/path/wt2', { displayName: 'wt2' })
store.setWorktreeMeta('r2::/other', { displayName: 'other' })
store.setWorktreeMeta(at('r1::/path/wt1', 'local'), { displayName: 'wt1' })
store.setWorktreeMeta(at('r1::/path/wt2', 'local'), { displayName: 'wt2' })
store.setWorktreeMeta(at('r2::/other', 'local'), { displayName: 'other' })
store.removeProjectForHost({ id: 'r1', executionHostId: 'local' })
@@ -323,8 +326,8 @@ describe('Store', () => {
store.addRepo(makeRepo({ id: 'r1' }))
store.addRepo(makeRepo({ id: 'r2', path: '/repo2' }))
store.setWorktreeMeta('r1::/path/wt1', { displayName: 'wt1' })
store.setWorktreeMeta('r2::/other', { displayName: 'other' })
store.setWorktreeMeta(at('r1::/path/wt1', 'local'), { displayName: 'wt1' })
store.setWorktreeMeta(at('r2::/other', 'local'), { displayName: 'other' })
store.setWorkspaceSession({
...getDefaultWorkspaceSession(),
@@ -342,7 +345,7 @@ describe('Store', () => {
const store = await createStore()
store.addRepo(makeRepo({ id: 'r1' }))
store.setWorktreeMeta('r1::/path/wt1', { displayName: 'wt1' })
store.setWorktreeMeta(at('r1::/path/wt1', 'local'), { displayName: 'wt1' })
const hostId = 'ssh:host-a'
store.setWorkspaceSession(
@@ -391,21 +394,21 @@ describe('Store', () => {
store.addRepo(makeRepo({ id: 'r2', path: '/repo2' }))
store.setWorktreeLineage(
'r1::/path/child',
at('r1::/path/child', 'local'),
makeWorktreeLineage({
worktreeId: 'r1::/path/child',
parentWorktreeId: 'r1::/path/parent'
})
)
store.setWorktreeLineage(
'r2::/other-child',
at('r2::/other-child', 'local'),
makeWorktreeLineage({
worktreeId: 'r2::/other-child',
parentWorktreeId: 'r1::/path/parent'
})
)
store.setWorktreeLineage(
'r2::/other',
at('r2::/other', 'local'),
makeWorktreeLineage({
worktreeId: 'r2::/other',
parentWorktreeId: 'r2::/parent'
@@ -451,10 +454,9 @@ describe('Store', () => {
executionHostId: 'ssh:ssh-old'
})
)
store.setWorktreeMeta('shared::/local/repo/wt', { displayName: 'local-wt', hostId: 'local' })
store.setWorktreeMeta('shared::/remote/repo/wt', {
displayName: 'remote-wt',
hostId: 'ssh:ssh-old'
store.setWorktreeMeta(at('shared::/local/repo/wt', 'local'), { displayName: 'local-wt' })
store.setWorktreeMeta(at('shared::/remote/repo/wt', 'ssh:ssh-old'), {
displayName: 'remote-wt'
})
store.removeProjectForHost({ id: 'shared', executionHostId: 'ssh:ssh-old' })
@@ -487,7 +489,7 @@ describe('Store', () => {
executionHostId: 'ssh:ssh-a'
})
)
store.setWorktreeMeta('shared::/repo', { displayName: 'local', hostId: 'local' })
store.setWorktreeMeta(at('shared::/repo', 'local'), { displayName: 'local' })
store.setWorkspaceSession({
...getDefaultWorkspaceSession(),
@@ -521,7 +523,7 @@ describe('Store', () => {
executionHostId: 'ssh:ssh-a'
})
)
store.setWorktreeMeta('shared::/repo', { displayName: 'local', hostId: 'local' })
store.setWorktreeMeta(at('shared::/repo', 'local'), { displayName: 'local' })
store.setWorkspaceSession({
...getDefaultWorkspaceSession(),
@@ -592,7 +594,7 @@ describe('Store', () => {
executionHostId: 'ssh:ssh-b'
})
)
store.setWorktreeMeta('shared::/repo', { displayName: 'local', hostId: 'local' })
store.setWorktreeMeta(at('shared::/repo', 'local'), { displayName: 'local' })
store.setWorkspaceSession({
...getDefaultWorkspaceSession(),
@@ -693,12 +695,11 @@ describe('Store', () => {
})
)
// SSH worktree meta correctly carries hostId (the normal, stamped case).
store.setWorktreeMeta('shared::/remote/repo/wt', {
displayName: 'remote-wt',
hostId: 'ssh:ssh-old'
store.setWorktreeMeta(at('shared::/remote/repo/wt', 'ssh:ssh-old'), {
displayName: 'remote-wt'
})
// A hostId-less meta is treated as local and left behind (never delete the wrong host's meta).
store.setWorktreeMeta('shared::/local/repo/wt', { displayName: 'local-wt' })
store.setWorktreeMeta(at('shared::/local/repo/wt', 'local'), { displayName: 'local-wt' })
store.removeProjectForHost({ id: 'shared', executionHostId: 'ssh:ssh-old' })
@@ -709,7 +710,7 @@ describe('Store', () => {
it('removeProjectForHost prunes everything when the id exists on no other host', async () => {
const store = await createStore()
store.addRepo(makeRepo({ id: 'only', connectionId: 'ssh-x', executionHostId: 'ssh:ssh-x' }))
store.setWorktreeMeta('only::/repo/wt', { displayName: 'wt', hostId: 'ssh:ssh-x' })
store.setWorktreeMeta(at('only::/repo/wt', 'ssh:ssh-x'), { displayName: 'wt' })
store.removeProjectForHost({ id: 'only', executionHostId: 'ssh:ssh-x' })
@@ -755,7 +756,7 @@ describe('Store', () => {
it('reassignSshTargetId re-points repos and worktree metas onto the new id', async () => {
const store = await createStore()
store.addRepo(makeRepo({ id: 'r1', connectionId: 'ssh-old', executionHostId: 'ssh:ssh-old' }))
store.setWorktreeMeta('r1::/repo/wt', { displayName: 'wt', hostId: 'ssh:ssh-old' })
store.setWorktreeMeta(at('r1::/repo/wt', 'ssh:ssh-old'), { displayName: 'wt' })
const repoIds = store.reassignSshTargetId('ssh-old', 'ssh-new')
@@ -805,7 +806,7 @@ describe('Store', () => {
// persisted, not memory-only. The repo id stays registered so the load-time orphan sweep,
// which only reads repo ids, leaves the row alone.
store.addRepo(makeRepo({ id: 'r1', path: '/r1' }))
store.setWorktreeMeta('r1::/remote/wt', { displayName: 'wt', hostId: 'ssh:ssh-old' })
store.setWorktreeMeta(at('r1::/remote/wt', 'ssh:ssh-old'), { displayName: 'wt' })
const repoIds = store.reassignSshTargetId('ssh-old', 'ssh-new')
expect(repoIds).toEqual([]) // no repo matched
+9 -3
View File
@@ -23,6 +23,8 @@ import {
getLocalWorktreeScanGeneration,
isLocalWorktreeScanGenerationCurrent
} from './local-worktree-scan-generation'
import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity'
import { removeWorktreeMetaRow } from './persistence/host-qualified-worktree-meta'
// Stub the ~/.ssh/config parser so the SSH-import test drives the real Store with deterministic hosts, not the operator's actual ~/.ssh/config.
const { loadUserSshConfigMock, sshConfigHostsToTargetsMock } = vi.hoisted(() => ({
@@ -365,7 +367,9 @@ describe('Store', () => {
const store = await createStore()
store.addRepo(makeRepo({ id: 'r1', path: '/repo', kind: 'folder', displayName: 'Repo' }))
const workspaceId = 'r1::/repo::workspace:11111111-1111-4111-8111-111111111111'
store.setWorktreeMeta(workspaceId, { displayName: 'Second workspace' })
store.setWorktreeMeta(hostWorktreeTarget({ id: workspaceId, hostId: 'local' }), {
displayName: 'Second workspace'
})
expect(() =>
store.updateProjectHostSetup({
@@ -375,7 +379,7 @@ describe('Store', () => {
).toThrow("This folder project has 1 other workspace that a Git project can't show")
expect(store.getRepo('r1')).toMatchObject({ kind: 'folder', displayName: 'Repo' })
store.removeWorktreeMeta(workspaceId)
removeWorktreeMetaRow(store, workspaceId, undefined)
expect(
store.updateProjectHostSetup({ setupId: 'r1', updates: { kind: 'git' } })?.repo
).toMatchObject({ kind: 'git' })
@@ -460,7 +464,9 @@ describe('Store', () => {
it('deletes repo-backed project host setups by removing the compatibility repo', async () => {
const store = await createStore()
store.addRepo(makeRepo({ id: 'r1', path: '/repo' }))
store.setWorktreeMeta('r1::/path/wt1', { displayName: 'wt1' })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'r1::/path/wt1', hostId: 'local' }), {
displayName: 'wt1'
})
const result = store.deleteProjectHostSetup({ setupId: 'r1' })
@@ -12,6 +12,7 @@ import {
import { getWorkspaceAttachments } from '../shared/workspace-attachments'
import type { WorkspaceAttachment } from '../shared/worktree/types'
import { mergeWorktree } from './ipc/worktree-metadata-merge'
import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity'
vi.mock('electron', () => ({
app: { getPath: () => testState.dir },
@@ -35,8 +36,12 @@ describe('workspace attachment persistence', () => {
it('round-trips collections and active selection through host-qualified metadata and git projections', async () => {
writeDataFile({ repos: [makeRepo({ id: 'repo', path: '/work', connectionId: 'test' })] })
const store = await createStore()
store.setWorktreeMetaForHost('repo::/work', 'ssh:test', { linkedPR: 7 })
store.setWorktreeMetaForHost('repo::/work', 'ssh:test', { linkedPR: 8 })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'repo::/work', hostId: 'ssh:test' }), {
linkedPR: 7
})
store.setWorktreeMeta(hostWorktreeTarget({ id: 'repo::/work', hostId: 'ssh:test' }), {
linkedPR: 8
})
store.flush()
const restored = await createStore()
const meta = restored.getWorktreeMetaForHost('repo::/work', 'ssh:test')
@@ -49,7 +54,9 @@ describe('workspace attachment persistence', () => {
)
expect(projected.linkedItems).toEqual([pr(7), pr(8)])
expect(
restored.setWorktreeMetaForHost('repo::/work', 'ssh:test', { linkedItems: [pr(7)] }).linkedPR
restored.setWorktreeMeta(hostWorktreeTarget({ id: 'repo::/work', hostId: 'ssh:test' }), {
linkedItems: [pr(7)]
}).linkedPR
).toBe(7)
restored.flush()
expect(
@@ -67,7 +74,7 @@ describe('workspace attachment persistence', () => {
const store = await createStore()
expect(getWorkspaceAttachments(store.getWorktreeMeta('legacy'))).toEqual([pr(7)])
expect(store.getWorktreeMeta('repaired')?.linkedItems).toEqual([pr(7), pr(8)])
store.setWorktreeMeta('legacy', { linkedPR: 8 })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'legacy', hostId: 'local' }), { linkedPR: 8 })
expect(store.getWorktreeMeta('legacy')?.linkedItems).toEqual([pr(7), pr(8)])
})
@@ -97,11 +104,13 @@ describe('workspace attachment persistence', () => {
})
it('applies stale client add/remove deltas atomically for worktrees and folders', async () => {
const store = await createStore()
store.setWorktreeMeta('wt', { linkedItems: [pr(1)] })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'wt', hostId: 'local' }), {
linkedItems: [pr(1)]
})
const firstClient = { linkedItemsBase: [pr(1)], linkedItems: [pr(1), pr(2)] }
const secondClient = { linkedItemsBase: [pr(1)], linkedItems: [pr(3)] }
store.setWorktreeMeta('wt', firstClient)
store.setWorktreeMeta('wt', secondClient)
store.setWorktreeMeta(hostWorktreeTarget({ id: 'wt', hostId: 'local' }), firstClient)
store.setWorktreeMeta(hostWorktreeTarget({ id: 'wt', hostId: 'local' }), secondClient)
expect(store.getWorktreeMeta('wt')?.linkedItems).toEqual([pr(2), pr(3)])
expect(store.getWorktreeMeta('wt')).not.toHaveProperty('linkedItemsBase')
const group = store.createProjectGroup({
@@ -123,8 +132,12 @@ describe('workspace attachment persistence', () => {
it('keeps the current review target when a stale collection edit resolved another review', async () => {
const store = await createStore()
const currentTarget = { remoteName: 'origin', branchName: 'pr-2' }
store.setWorktreeMeta('wt', { linkedItems: [pr(2)], linkedPR: 2, pushTarget: currentTarget })
store.setWorktreeMeta('wt', {
store.setWorktreeMeta(hostWorktreeTarget({ id: 'wt', hostId: 'local' }), {
linkedItems: [pr(2)],
linkedPR: 2,
pushTarget: currentTarget
})
store.setWorktreeMeta(hostWorktreeTarget({ id: 'wt', hostId: 'local' }), {
linkedItemsBase: [],
linkedItems: [pr(1)],
linkedPR: 1,
@@ -136,7 +149,7 @@ describe('workspace attachment persistence', () => {
linkedItems: [pr(2), pr(1)],
pushTarget: currentTarget
})
store.setWorktreeMeta('wt', {
store.setWorktreeMeta(hostWorktreeTarget({ id: 'wt', hostId: 'local' }), {
linkedItemsBase: [pr(2), pr(1)],
linkedItems: [pr(2), pr(1)],
linkedPR: 1,
@@ -11,6 +11,8 @@ import { tmpdir } from 'node:os'
import { getDefaultWorkspaceSession } from '../shared/constants'
import { TEST_LEAF_1, TEST_LEAF_2 } from './persistence-session-fixtures'
import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity'
import { removeWorktreeMetaRow } from './persistence/host-qualified-worktree-meta'
// Stub the ~/.ssh/config parser so the SSH-import test drives the real Store with deterministic hosts, not the operator's actual ~/.ssh/config.
const { loadUserSshConfigMock, sshConfigHostsToTargetsMock } = vi.hoisted(() => ({
@@ -156,7 +158,9 @@ describe('Store', () => {
it('keeps worktree deletion authoritative against stale writes and later same-path reuse', async () => {
const store = await createStore()
store.setWorktreeMeta('wt1', { displayName: 'Worktree' })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'wt1', hostId: 'local' }), {
displayName: 'Worktree'
})
store.setWorkspaceSession({
...getDefaultWorkspaceSession(),
tabsByWorktree: {
@@ -177,7 +181,7 @@ describe('Store', () => {
})
const stale = structuredClone(store.getWorkspaceSession())
store.removeWorktreeMeta('wt1')
removeWorktreeMetaRow(store, 'wt1', undefined)
expect(store.getWorkspaceSession().tabsByWorktree.wt1).toBeUndefined()
expect(store.getWorkspaceSession().terminalLayoutsByTabId['old-tab']).toBeUndefined()
@@ -202,7 +206,9 @@ describe('Store', () => {
it('fences a delayed terminal snapshot after an empty worktree is deleted', async () => {
const store = await createStore()
const worktreeId = 'repo::/empty-worktree'
store.setWorktreeMeta(worktreeId, { displayName: 'Empty worktree' })
store.setWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: 'local' }), {
displayName: 'Empty worktree'
})
const stale = {
...getDefaultWorkspaceSession(),
tabsByWorktree: {
@@ -210,7 +216,7 @@ describe('Store', () => {
}
}
store.removeWorktreeMeta(worktreeId)
removeWorktreeMetaRow(store, worktreeId, undefined)
expect(store.getWorkspaceSession().terminalTopologyRevisionByRepoId?.repo).toBe(1)
store.setWorkspaceSession(stale)
@@ -221,7 +227,9 @@ describe('Store', () => {
it('advances deletion authority when only a legacy retirement fence existed', async () => {
const store = await createStore()
const worktreeId = 'repo::/legacy-tombstone'
store.setWorktreeMeta(worktreeId, { displayName: 'Legacy worktree' })
store.setWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: 'local' }), {
displayName: 'Legacy worktree'
})
store.setWorkspaceSession({
...getDefaultWorkspaceSession(),
terminalSurfaceTombstonesByPaneKey: {
@@ -238,7 +246,7 @@ describe('Store', () => {
const revisionBeforeDelete =
store.getWorkspaceSession().terminalTopologyRevisionByRepoId?.repo ?? 0
store.removeWorktreeMeta(worktreeId)
removeWorktreeMetaRow(store, worktreeId, undefined)
expect(store.getWorkspaceSession().terminalTopologyRevisionByRepoId?.repo).toBe(
revisionBeforeDelete + 1
@@ -287,14 +295,16 @@ describe('Store', () => {
for (let index = 0; index < 25; index += 1) {
const worktreeId = `repo::/worktree-${index}`
store.setWorktreeMeta(worktreeId, { displayName: `Worktree ${index}` })
store.setWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: 'local' }), {
displayName: `Worktree ${index}`
})
await store.persistPtyBinding({
worktreeId,
tabId: `tab-${index}`,
leafId: TEST_LEAF_1,
ptyId: `pty-${index}`
})
store.removeWorktreeMeta(worktreeId)
removeWorktreeMetaRow(store, worktreeId, undefined)
}
const session = store.getWorkspaceSession()
@@ -305,7 +315,9 @@ describe('Store', () => {
it('does not remove colliding worktree ids from other execution-host partitions', async () => {
const store = await createStore()
const worktreeId = 'repo::/same-path'
store.setWorktreeMeta(worktreeId, { displayName: 'Local worktree' })
store.setWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: 'local' }), {
displayName: 'Local worktree'
})
store.setWorkspaceSession({
...getDefaultWorkspaceSession(),
tabsByWorktree: {
@@ -331,7 +343,7 @@ describe('Store', () => {
'runtime:env-b'
)
store.removeWorktreeMeta(worktreeId)
removeWorktreeMetaRow(store, worktreeId, undefined)
expect(store.getWorkspaceSession().tabsByWorktree[worktreeId]).toBeUndefined()
expect(store.getWorkspaceSession('runtime:env-a').tabsByWorktree[worktreeId]?.[0]?.id).toBe(
@@ -20,6 +20,8 @@ import { restoreProfileStateJsonExport } from './persistence/profile-state/legac
import { acquireProfileStateMaintenance } from './persistence/profile-state/profile-state-access'
import type { Repo } from '../shared/repo-types'
import { folderWorkspaceKey, worktreeWorkspaceKey } from '../shared/workspace-scope'
import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity'
import { removeWorktreeMetaRow } from './persistence/host-qualified-worktree-meta'
// Stub the ~/.ssh/config parser so the SSH-import test drives the real Store with deterministic hosts, not the operator's actual ~/.ssh/config.
const { loadUserSshConfigMock, sshConfigHostsToTargetsMock } = vi.hoisted(() => ({
@@ -77,8 +79,8 @@ describe('Store', () => {
it('getAllWorktreeMeta returns all entries', async () => {
const store = await createStore()
store.setWorktreeMeta('a', { displayName: 'A' })
store.setWorktreeMeta('b', { displayName: 'B' })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'a', hostId: 'local' }), { displayName: 'A' })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'b', hostId: 'local' }), { displayName: 'B' })
const all = store.getAllWorktreeMeta()
expect(Object.keys(all)).toHaveLength(2)
expect(all['a'].displayName).toBe('A')
@@ -89,9 +91,9 @@ describe('Store', () => {
it('removeWorktreeMeta deletes a single entry', async () => {
const store = await createStore()
store.setWorktreeMeta('a', { displayName: 'A' })
store.setWorktreeMeta('b', { displayName: 'B' })
store.removeWorktreeMeta('a')
store.setWorktreeMeta(hostWorktreeTarget({ id: 'a', hostId: 'local' }), { displayName: 'A' })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'b', hostId: 'local' }), { displayName: 'B' })
removeWorktreeMetaRow(store, 'a', undefined)
expect(store.getWorktreeMeta('a')).toBeUndefined()
expect(store.getWorktreeMeta('b')).toBeDefined()
})
@@ -100,13 +102,18 @@ describe('Store', () => {
const store = await createStore()
const lineage = makeWorktreeLineage()
store.setWorktreeMeta(lineage.worktreeId, { displayName: 'child' })
store.setWorktreeLineage(lineage.worktreeId, lineage)
store.setWorktreeMeta(hostWorktreeTarget({ id: lineage.worktreeId, hostId: 'local' }), {
displayName: 'child'
})
store.setWorktreeLineage(
hostWorktreeTarget({ id: lineage.worktreeId, hostId: 'local' }),
lineage
)
expect(store.getWorktreeLineage(lineage.worktreeId)).toEqual(lineage)
expect(store.getAllWorktreeLineage()).toEqual({ [lineage.worktreeId]: lineage })
store.removeWorktreeLineage(lineage.worktreeId)
store.removeWorktreeLineage(hostWorktreeTarget({ id: lineage.worktreeId, hostId: 'local' }))
expect(store.getWorktreeLineage(lineage.worktreeId)).toBeUndefined()
expect(store.getWorktreeMeta(lineage.worktreeId)).toBeDefined()
@@ -116,10 +123,15 @@ describe('Store', () => {
const store = await createStore()
const lineage = makeWorktreeLineage()
store.setWorktreeMeta(lineage.worktreeId, { displayName: 'child' })
store.setWorktreeLineage(lineage.worktreeId, lineage)
store.setWorktreeMeta(hostWorktreeTarget({ id: lineage.worktreeId, hostId: 'local' }), {
displayName: 'child'
})
store.setWorktreeLineage(
hostWorktreeTarget({ id: lineage.worktreeId, hostId: 'local' }),
lineage
)
store.removeWorktreeMeta(lineage.worktreeId)
removeWorktreeMetaRow(store, lineage.worktreeId, undefined)
expect(store.getWorktreeMeta(lineage.worktreeId)).toBeUndefined()
expect(store.getWorktreeLineage(lineage.worktreeId)).toBeUndefined()
@@ -129,13 +141,15 @@ describe('Store', () => {
const store = await createStore()
const lineage = makeWorkspaceLineage()
store.setWorktreeMeta('r1::/path/child', { displayName: 'child' })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'r1::/path/child', hostId: 'local' }), {
displayName: 'child'
})
store.setWorkspaceLineage(lineage)
expect(store.getWorkspaceLineage(lineage.childWorkspaceKey)).toEqual(lineage)
expect(store.getAllWorkspaceLineage()).toEqual({ [lineage.childWorkspaceKey]: lineage })
store.removeWorktreeMeta('r1::/path/child')
removeWorktreeMetaRow(store, 'r1::/path/child', undefined)
expect(store.getWorkspaceLineage(lineage.childWorkspaceKey)).toBeUndefined()
})
@@ -14,6 +14,7 @@ import { tmpdir } from 'node:os'
import type { PersistedState } from '../shared/persisted-state-types'
import { getDefaultWorkspaceSession } from '../shared/constants'
import { folderWorkspaceKey } from '../shared/workspace-scope'
import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity'
// Stub the ~/.ssh/config parser so the SSH-import test drives the real Store with deterministic hosts, not the operator's actual ~/.ssh/config.
const { loadUserSshConfigMock, sshConfigHostsToTargetsMock } = vi.hoisted(() => ({
@@ -71,7 +72,9 @@ describe('Store', () => {
it('setWorktreeMeta creates meta with defaults for missing fields', async () => {
const store = await createStore()
const meta = store.setWorktreeMeta('wt1', { displayName: 'my-wt' })
const meta = store.setWorktreeMeta(hostWorktreeTarget({ id: 'wt1', hostId: 'local' }), {
displayName: 'my-wt'
})
expect(meta.displayName).toBe('my-wt')
expect(meta.comment).toBe('')
@@ -82,8 +85,13 @@ describe('Store', () => {
it('setWorktreeMeta merges with existing meta', async () => {
const store = await createStore()
store.setWorktreeMeta('wt1', { displayName: 'first', comment: 'hello' })
const updated = store.setWorktreeMeta('wt1', { comment: 'updated' })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'wt1', hostId: 'local' }), {
displayName: 'first',
comment: 'hello'
})
const updated = store.setWorktreeMeta(hostWorktreeTarget({ id: 'wt1', hostId: 'local' }), {
comment: 'updated'
})
expect(updated.displayName).toBe('first')
expect(updated.comment).toBe('updated')
@@ -113,7 +121,10 @@ describe('Store', () => {
accountLabel: 'ada@example.com'
}
store.setWorktreeMeta('wt-jira', { linkedWorkItem, linkedTaskSourceContext })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'wt-jira', hostId: 'local' }), {
linkedWorkItem,
linkedTaskSourceContext
})
store.flush()
const restored = await createStore()
@@ -122,12 +133,12 @@ describe('Store', () => {
linkedTaskSourceContext
})
expect(
restored.setWorktreeMeta('wt-jira', {
restored.setWorktreeMeta(hostWorktreeTarget({ id: 'wt-jira', hostId: 'local' }), {
linkedTaskSourceContext: { ...linkedTaskSourceContext, provider: 'linear' }
}).linkedTaskSourceContext
).toBeNull()
expect(
restored.setWorktreeMeta('wt-jira', {
restored.setWorktreeMeta(hostWorktreeTarget({ id: 'wt-jira', hostId: 'local' }), {
linkedTaskSourceContext: {
...linkedTaskSourceContext,
providerIdentity: {
@@ -0,0 +1,185 @@
import {
closeTestStores,
createStore as createFreshStore,
testState
} from './persistence-test-harness'
/**
* A local row and an SSH row can share a repo id, so they share every worktree id under it.
* Worktree meta and lineage writes take the row's host (R4-2 family b), and a write for one
* twin must never land on the other.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import type { Repo } from '../shared/repo-types'
import type { WorktreeLineage } from '../shared/worktree/lineage-types'
import { getDefaultPersistedState } from '../shared/constants'
import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity'
import { worktreeWorkspaceKey } from '../shared/workspace-scope'
import {
removeWorktreeMetaRow,
resolveWorktreeMetaOwner
} from './persistence/host-qualified-worktree-meta'
import { persistRuntimeManagedWorktreeSortOrder } from './runtime/runtime-managed-worktree-metadata'
vi.mock('electron', () => ({
app: { getPath: () => testState.dir },
safeStorage: { isEncryptionAvailable: () => false }
}))
vi.mock('./telemetry/client', () => ({ track: vi.fn() }))
vi.mock('./telemetry/cohort-classifier', () => ({ getCohortAtEmit: vi.fn() }))
const WORKTREE_ID = 'dup::/wt'
const LOCAL = hostWorktreeTarget({ id: WORKTREE_ID, hostId: 'local' })
const SSH = hostWorktreeTarget({ id: WORKTREE_ID, hostId: 'ssh:ssh-1' })
const twinRepos: Repo[] = [
{ id: 'dup', path: '/wt', displayName: 'Local', badgeColor: '#000', addedAt: 1 },
{
id: 'dup',
path: '/wt',
displayName: 'Remote',
badgeColor: '#000',
addedAt: 2,
connectionId: 'ssh-1'
}
]
function lineage(instanceId: string): WorktreeLineage {
return {
worktreeId: WORKTREE_ID,
worktreeInstanceId: instanceId,
parentWorktreeId: 'dup::/parent',
parentWorktreeInstanceId: 'parent',
origin: 'manual',
capture: { source: 'manual-action', confidence: 'explicit' },
createdAt: 1
}
}
function createTwinStore(state: Record<string, unknown> = {}) {
mkdirSync(testState.dir, { recursive: true })
writeFileSync(
join(testState.dir, 'orca-data.json'),
JSON.stringify({ ...getDefaultPersistedState(testState.dir), repos: twinRepos, ...state }),
'utf-8'
)
return createFreshStore()
}
beforeEach(() => {
testState.dir = mkdtempSync(join(tmpdir(), 'orca-worktree-meta-twins-'))
})
afterEach(async () => {
await closeTestStores()
rmSync(testState.dir, { recursive: true, force: true })
})
describe('worktree meta and lineage writes for same-id twins', () => {
it('writes each twin its own meta', async () => {
const store = await createTwinStore()
store.setWorktreeMeta(LOCAL, { displayName: 'local copy' })
store.setWorktreeMeta(SSH, { displayName: 'ssh copy' })
expect(store.getWorktreeMetaForHost(WORKTREE_ID, 'local')?.displayName).toBe('local copy')
expect(store.getWorktreeMetaForHost(WORKTREE_ID, 'ssh:ssh-1')?.displayName).toBe('ssh copy')
expect(store.getWorktreeMeta(WORKTREE_ID)?.displayName).toBe('local copy')
})
it('removes one twin without touching the other', async () => {
const store = await createTwinStore()
store.setWorktreeMeta(LOCAL, { displayName: 'local copy' })
store.setWorktreeMeta(SSH, { displayName: 'ssh copy' })
store.setWorktreeLineage(LOCAL, lineage('local'))
store.removeWorktreeMeta(SSH)
expect(store.getWorktreeMetaForHost(WORKTREE_ID, 'ssh:ssh-1')).toBeUndefined()
expect(store.getWorktreeMetaForHost(WORKTREE_ID, 'local')?.displayName).toBe('local copy')
expect(store.getWorktreeLineage(WORKTREE_ID)?.worktreeInstanceId).toBe('local')
})
it('keeps the owning twin lineage when the other twin writes or removes it', async () => {
const store = await createTwinStore()
store.setWorktreeMeta(LOCAL, { displayName: 'local copy' })
store.setWorktreeLineage(LOCAL, lineage('local'))
store.setWorkspaceLineage({
childWorkspaceKey: worktreeWorkspaceKey(WORKTREE_ID),
childInstanceId: 'local',
parentWorkspaceKey: worktreeWorkspaceKey('dup::/parent'),
parentInstanceId: 'parent',
origin: 'manual',
capture: { source: 'manual-action', confidence: 'explicit' },
createdAt: 1
})
expect(store.setWorktreeLineage(SSH, lineage('ssh'))).toBeUndefined()
store.removeWorktreeLineage(SSH)
expect(store.getWorktreeLineage(WORKTREE_ID)?.worktreeInstanceId).toBe('local')
expect(store.getWorkspaceLineage(worktreeWorkspaceKey(WORKTREE_ID))).toBeDefined()
store.removeWorktreeLineage(LOCAL)
expect(store.getWorktreeLineage(WORKTREE_ID)).toBeUndefined()
expect(store.getWorkspaceLineage(worktreeWorkspaceKey(WORKTREE_ID))).toBeUndefined()
})
it('renames only the named twin', async () => {
const store = await createTwinStore()
store.setWorktreeMeta(LOCAL, { displayName: 'local copy' })
store.setWorktreeMeta(SSH, { displayName: 'ssh copy' })
store.migrateWorktreeIdentity(SSH, 'dup::/renamed')
expect(store.getWorktreeMetaForHost('dup::/renamed', 'ssh:ssh-1')?.displayName).toBe('ssh copy')
expect(store.getWorktreeMeta(WORKTREE_ID)?.displayName).toBe('local copy')
expect(store.getWorktreeMetaForHost('dup::/renamed', 'local')).toBeUndefined()
})
it('removes an unhosted row only when nothing stamped it', async () => {
const store = await createTwinStore()
store.setWorktreeMeta(LOCAL, { displayName: 'local copy' })
expect(store.removeUnhostedWorktreeMeta(WORKTREE_ID)).toBe(false)
expect(store.getWorktreeMeta(WORKTREE_ID)?.displayName).toBe('local copy')
})
it('a removal with no host takes the stamped owner, not every twin', async () => {
const store = await createTwinStore()
store.setWorktreeMeta(LOCAL, { displayName: 'local copy' })
store.setWorktreeMeta(SSH, { displayName: 'ssh copy' })
removeWorktreeMetaRow(store, WORKTREE_ID, undefined)
expect(store.getWorktreeMetaForHost(WORKTREE_ID, 'local')).toBeUndefined()
expect(store.getWorktreeMetaForHost(WORKTREE_ID, 'ssh:ssh-1')?.displayName).toBe('ssh copy')
})
it('neither saves nor reports a sort order for an unstamped twin row', async () => {
const store = await createTwinStore({
worktreeMeta: { [WORKTREE_ID]: { displayName: 'legacy', sortOrder: 1 } }
})
const notifyChanged = vi.fn()
const result = persistRuntimeManagedWorktreeSortOrder({
orderedIds: [WORKTREE_ID],
store,
invalidateResolved: vi.fn(),
notifyChanged
})
expect(result).toEqual({ updated: 0 })
expect(notifyChanged).not.toHaveBeenCalled()
expect(store.getWorktreeMeta(WORKTREE_ID)?.sortOrder).toBe(1)
})
it('resolves an id-only caller to the stamp, and refuses an unstamped twin', async () => {
const store = await createTwinStore()
expect(resolveWorktreeMetaOwner(store, WORKTREE_ID)).toBeUndefined()
store.setWorktreeMeta(SSH, { displayName: 'ssh copy' })
expect(resolveWorktreeMetaOwner(store, WORKTREE_ID)).toEqual(SSH)
})
})
@@ -1,6 +1,11 @@
import { getRepoExecutionHostId, type ExecutionHostId } from '../../shared/execution-host'
import type { Repo } from '../../shared/repo-types'
import type { WorktreeMeta } from '../../shared/worktree/meta-types'
import { getRepoIdFromWorktreeId } from '../../shared/worktree/id'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../shared/worktree/host-qualified-identity'
/**
* The listing paths take partial store shapes (`Pick<Store, ...>`), so the
@@ -14,12 +19,6 @@ export type HostQualifiedWorktreeMetaStore = {
executionHostId: ExecutionHostId
) => WorktreeMeta | undefined
getAllWorktreeMetaForHost?: (executionHostId: ExecutionHostId) => Record<string, WorktreeMeta>
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => WorktreeMeta
setWorktreeMetaForHost?: (
worktreeId: string,
executionHostId: ExecutionHostId,
meta: Partial<WorktreeMeta>
) => WorktreeMeta
}
/** Canonical host snapshot, with a filtered legacy fallback for partial test/compatibility stores. */
@@ -73,14 +72,59 @@ export function readWorktreeMetaForRepo(
return readWorktreeMetaForHost(store, worktreeId, getRepoExecutionHostId(repo))
}
export function writeWorktreeMetaForHost(
store: Pick<HostQualifiedWorktreeMetaStore, 'setWorktreeMeta' | 'setWorktreeMetaForHost'>,
/**
* The row an id-only caller (an agent hook, a sort list) means: the id's own host stamp, else its
* repo's only row. Undefined when same-id twins leave it ambiguous: callers skip, never guess.
*/
export function resolveWorktreeMetaOwner(
store: {
getWorktreeMeta: (worktreeId: string) => { hostId?: ExecutionHostId } | undefined
getRepos: () => readonly Repo[]
},
worktreeId: string
): HostWorktreeTarget | undefined {
const stamped = store.getWorktreeMeta(worktreeId)?.hostId
if (stamped) {
return hostWorktreeTarget({ id: worktreeId, hostId: stamped })
}
const repoId = getRepoIdFromWorktreeId(worktreeId)
const owners = store.getRepos().filter((repo) => repo.id === repoId)
return owners.length === 1 && owners[0]
? hostWorktreeTargetForRepo(worktreeId, owners[0])
: undefined
}
/** A worktree row's own host when it carries one; otherwise as `resolveWorktreeMetaOwner`. */
export function worktreeRowTarget(
store: Parameters<typeof resolveWorktreeMetaOwner>[0],
worktree: { id: string; hostId?: ExecutionHostId }
): HostWorktreeTarget | undefined {
return worktree.hostId
? hostWorktreeTarget({ id: worktree.id, hostId: worktree.hostId })
: resolveWorktreeMetaOwner(store, worktree.id)
}
/** Removes by the caller's host, else the row's own stamp; only an unstamped row goes on every host. */
export function removeWorktreeMetaRow(
store: {
getWorktreeMeta: (worktreeId: string) => WorktreeMeta | undefined
removeWorktreeMeta: (target: HostWorktreeTarget) => void
removeUnhostedWorktreeMeta?: (worktreeId: string) => boolean
},
worktreeId: string,
executionHostId: ExecutionHostId,
meta: Partial<WorktreeMeta>
): WorktreeMeta {
return (
store.setWorktreeMetaForHost?.(worktreeId, executionHostId, meta) ??
store.setWorktreeMeta(worktreeId, { ...meta, hostId: executionHostId })
)
hostId: ExecutionHostId | undefined
): void {
const owner = hostId ?? store.getWorktreeMeta(worktreeId)?.hostId
if (owner) {
store.removeWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: owner }))
} else {
store.removeUnhostedWorktreeMeta?.(worktreeId)
}
}
export function hostWorktreeTargetForRepo(
worktreeId: string,
repo: Pick<Repo, 'connectionId' | 'executionHostId'>
): HostWorktreeTarget {
return hostWorktreeTarget({ id: worktreeId, hostId: getRepoExecutionHostId(repo) })
}
@@ -3,7 +3,8 @@ import type { WorkspaceKey } from '../../../shared/folder-workspace-types'
import type { WorkspaceLineage, WorktreeLineage } from '../../../shared/worktree/lineage-types'
import type { WorktreeMeta } from '../../../shared/worktree/meta-types'
import type { Repo } from '../../../shared/repo-types'
import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../shared/execution-host'
import type { ExecutionHostId } from '../../../shared/execution-host'
import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id'
import { hasWorktreeRemovalRepoOwnerOnOtherHost } from '../../worktree-removal-repo-owner'
import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../shared/workspace-scope'
@@ -31,7 +32,6 @@ import {
setWorktreeMetaForHost as setWorktreeMetaForHostOperation,
WORKTREE_METADATA_DOMAINS
} from './worktree-identity-metadata'
import { mergeWorktreeMetaForWrite } from './worktree-meta-write-normalization'
import {
captureNativeLocalWorktreeMetadataScanExpectation as captureNativeLocalWorktreeMetadataScanExpectationOperation,
pruneSessionlessMissingLocalWorktreeMetadataForRepo as pruneSessionlessMissingLocalWorktreeMetadataForRepoOperation,
@@ -75,20 +75,6 @@ export class MetadataLineageOperations {
)
}
setWorktreeMetaForHost(
worktreeId: string,
executionHostId: ExecutionHostId,
meta: Partial<WorktreeMeta> & WorkspaceAttachmentMutation
): WorktreeMeta {
return setWorktreeMetaForHostOperation(
this[metadataLineageOperationsContext].runtime,
this[metadataLineageOperationsContext].scheduling,
worktreeId,
executionHostId,
meta
)
}
getAllWorktreeMeta(): Record<string, WorktreeMeta> {
return this[metadataLineageOperationsContext].runtime.state.worktreeMeta
}
@@ -110,28 +96,32 @@ export class MetadataLineageOperations {
}
setWorktreeMeta(
worktreeId: string,
target: HostWorktreeTarget,
meta: Partial<WorktreeMeta> & WorkspaceAttachmentMutation
): WorktreeMeta {
const state = this[metadataLineageOperationsContext].runtime.state
const stored = state.worktreeMeta[worktreeId]
const executionHostId = meta.hostId ?? stored?.hostId
if (executionHostId) {
return setWorktreeMetaForHostOperation(
this[metadataLineageOperationsContext].runtime,
this[metadataLineageOperationsContext].scheduling,
worktreeId,
executionHostId,
meta
)
}
const updated = mergeWorktreeMetaForWrite(stored, meta)
state.worktreeMeta[worktreeId] = updated
scheduleSave(this[metadataLineageOperationsContext].scheduling, ['worktreeMeta'])
return updated
return setWorktreeMetaForHostOperation(
this[metadataLineageOperationsContext].runtime,
this[metadataLineageOperationsContext].scheduling,
target.id,
target.hostId,
meta
)
}
removeWorktreeMeta(worktreeId: string, hostId?: ExecutionHostId | null): void {
removeWorktreeMeta(target: HostWorktreeTarget): void {
this.removeWorktreeMetaRows(target.id, target.hostId)
}
/** For a row no host stamped: removes every host's copy, and refuses a stamped row. */
removeUnhostedWorktreeMeta(worktreeId: string): boolean {
if (this[metadataLineageOperationsContext].runtime.state.worktreeMeta[worktreeId]?.hostId) {
return false
}
this.removeWorktreeMetaRows(worktreeId, undefined)
return true
}
private removeWorktreeMetaRows(worktreeId: string, hostId: ExecutionHostId | undefined): void {
// A host-qualified removal names the owner; the persisted host is the fallback.
const persistedOwner =
this[metadataLineageOperationsContext].runtime.state.worktreeMeta[worktreeId]?.hostId
@@ -154,7 +144,7 @@ export class MetadataLineageOperations {
removeWorktreeMetadataForHost(
this[metadataLineageOperationsContext].runtime.state,
worktreeId,
hostId === undefined ? undefined : (hostId ?? undefined)
hostId
)
// Skip partitions main never wrote: materializing one fences every sibling worktree of the repo.
const partitions = new Set<ExecutionHostId>(
@@ -242,36 +232,41 @@ export class MetadataLineageOperations {
return this[metadataLineageOperationsContext].runtime.state.worktreeLineageById
}
setWorktreeLineage(worktreeId: string, lineage: WorktreeLineage): WorktreeLineage {
this[metadataLineageOperationsContext].runtime.state.worktreeLineageById[worktreeId] = lineage
/** Undefined when the id's lineage bucket belongs to a same-id twin on another host. */
setWorktreeLineage(
target: HostWorktreeTarget,
lineage: WorktreeLineage
): WorktreeLineage | undefined {
const state = this[metadataLineageOperationsContext].runtime.state
if (!ownsRawIdBucket(state, target)) {
return undefined
}
state.worktreeLineageById[target.id] = lineage
scheduleSave(this[metadataLineageOperationsContext].scheduling)
return lineage
}
removeWorktreeLineage(worktreeId: string): void {
delete this[metadataLineageOperationsContext].runtime.state.worktreeLineageById[worktreeId]
/** Drops both lineage records of a worktree, unless a same-id twin on another host owns them. */
removeWorktreeLineage(target: HostWorktreeTarget): void {
const state = this[metadataLineageOperationsContext].runtime.state
if (!ownsRawIdBucket(state, target)) {
return
}
delete state.worktreeLineageById[target.id]
delete state.workspaceLineageByChildKey[worktreeWorkspaceKey(target.id)]
scheduleSave(this[metadataLineageOperationsContext].scheduling)
}
migrateWorktreeIdentity(
oldWorktreeId: string,
newWorktreeId: string,
executionHostId?: ExecutionHostId
): void {
migrateWorktreeIdentity(target: HostWorktreeTarget, newWorktreeId: string): void {
const state = this[metadataLineageOperationsContext].runtime.state
const persistedOwner = state.worktreeMeta[oldWorktreeId]?.hostId
const mover = executionHostId ?? persistedOwner ?? LOCAL_EXECUTION_HOST_ID
const legacyChanged =
executionHostId !== undefined &&
persistedOwner !== undefined &&
persistedOwner !== executionHostId
? false
: migrateWorktreeIdentityOperation(state, oldWorktreeId, newWorktreeId)
ownsRawIdBucket(state, target) &&
migrateWorktreeIdentityOperation(state, target.id, newWorktreeId)
const canonicalChanged = migrateWorktreeMetadataLocator(
state,
oldWorktreeId,
target.id,
newWorktreeId,
mover
target.hostId
)
if (legacyChanged || canonicalChanged) {
// Legacy identity moves also re-key sessions, lineage, mobile selections, and UI state.
@@ -299,13 +294,15 @@ export class MetadataLineageOperations {
scheduleSave(this[metadataLineageOperationsContext].scheduling)
return lineage
}
}
removeWorkspaceLineage(childWorkspaceKey: WorkspaceKey): void {
delete this[metadataLineageOperationsContext].runtime.state.workspaceLineageByChildKey[
childWorkspaceKey
]
scheduleSave(this[metadataLineageOperationsContext].scheduling)
}
// Raw-id buckets (legacy meta, lineage) belong to the first host that stamped the id (D7).
function ownsRawIdBucket(
state: MetadataLineageOperationsRuntime['state'],
target: HostWorktreeTarget
): boolean {
const owner = state.worktreeMeta[target.id]?.hostId
return !owner || owner === target.hostId
}
export function removeWorkspaceLineageForFolderParent(
@@ -7,6 +7,7 @@ import { ProfileStateSqliteAuthority } from '../profile-state/profile-state-sqli
import { readProfileStateDomain } from '../profile-state/profile-state-domain-reader'
import { buildProfileStateCutoverFixture } from '../profile-state-cutover-fixture'
import { Store } from './store'
import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
vi.mock('electron', () => ({
app: {
@@ -112,7 +113,7 @@ describe('SQLite durability barriers with another selective write pending', () =
seed.workspaceSession.activeWorktreeId = oldId
seed.ui.showDotfilesByWorktree = { [oldId]: true }
const state = fixture(seed)
state.store.migrateWorktreeIdentity(oldId, newId)
state.store.migrateWorktreeIdentity(hostWorktreeTarget({ id: oldId, hostId: 'local' }), newId)
state.store.flushOrThrow()
expect(state.read('workspaceSession')).toMatchObject({
kind: 'value',
@@ -25,6 +25,7 @@ import {
} from '../profile-state/profile-state-database'
import { profileStateJsonExportPath } from '../profile-state/legacy-json/profile-state-export-path'
import { buildProfileStateCutoverFixture } from '../profile-state-cutover-fixture'
import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
vi.mock('electron', () => ({
app: {
@@ -656,9 +657,12 @@ describe('Store with an injected SQLite profile-state authority', () => {
const writeDomains = vi.spyOn(authority, 'writeSerializedDomains')
const store = new Store({ dataFile, profileStateAuthority: authority })
store.flushOrThrow()
store.setWorktreeMetaForHost('repo-local::/fixture/local', 'local', {
displayName: 'Updated fixture local'
})
store.setWorktreeMeta(
hostWorktreeTarget({ id: 'repo-local::/fixture/local', hostId: 'local' }),
{
displayName: 'Updated fixture local'
}
)
store.flushOrThrow()
expect(writeDomains).toHaveBeenCalledTimes(1)
@@ -1,6 +1,7 @@
import { expect, it } from 'vitest'
import { fixture } from './profile-state-delayed-authority-fixture'
import { WORKTREE_METADATA_DOMAINS } from './worktree-identity-metadata'
import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
export function registerStateWriteListenersTests(): void {
it('tells each write the domains it changed, undefined for a full save', async () => {
@@ -8,10 +9,14 @@ export function registerStateWriteListenersTests(): void {
const seen: (readonly string[] | undefined)[] = []
const detach = store.onStateWritten((domains) => seen.push(domains))
store.setWorktreeMeta('repo-1::/r/a', { isPinned: true })
store.setWorktreeMetaForHost('repo-1::/r/b', 'local', { isUnread: true })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'repo-1::/r/a', hostId: 'local' }), {
isPinned: true
})
store.setWorktreeMeta(hostWorktreeTarget({ id: 'repo-1::/r/b', hostId: 'local' }), {
isUnread: true
})
store.patchWorkspaceSession({ activeTabId: 'tab-1' })
store.setWorktreeLineage('repo-1::/r/b', {
store.setWorktreeLineage(hostWorktreeTarget({ id: 'repo-1::/r/b', hostId: 'local' }), {
worktreeId: 'repo-1::/r/b',
worktreeInstanceId: 'b-1',
parentWorktreeId: 'repo-1::/r/a',
@@ -21,10 +26,13 @@ export function registerStateWriteListenersTests(): void {
createdAt: 1
})
detach()
store.setWorktreeMeta('repo-1::/r/a', { isPinned: false })
store.setWorktreeMeta(hostWorktreeTarget({ id: 'repo-1::/r/a', hostId: 'local' }), {
isPinned: false
})
// Every metadata write also writes the host-qualified identity row.
expect(seen).toEqual([
['worktreeMeta'],
WORKTREE_METADATA_DOMAINS,
WORKTREE_METADATA_DOMAINS,
['workspaceSession'],
undefined
@@ -10,6 +10,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
import { removeWorktreeMetaRow } from '../host-qualified-worktree-meta'
vi.mock('electron', () => ({
app: {
@@ -88,10 +89,13 @@ describe('store signals to the worktree metadata prune gate', () => {
it('re-arms the gate when a metadata row is removed', () => {
const store = createStore()
store.setWorktreeMeta(WORKTREE_ID, { displayName: 'a', hostId: 'local' })
store.setWorktreeMeta(hostWorktreeTarget({ id: WORKTREE_ID, hostId: 'local' }), {
displayName: 'a',
hostId: 'local'
})
parkGate()
store.removeWorktreeMeta(WORKTREE_ID)
removeWorktreeMetaRow(store, WORKTREE_ID, undefined)
expect(isLocalWorktreeMetadataPruneDue(REPO_ID)).toBe(true)
})
@@ -100,8 +104,14 @@ describe('store signals to the worktree metadata prune gate', () => {
const store = createStore()
parkGate()
store.setWorktreeMeta(WORKTREE_ID, { displayName: 'a', hostId: 'local' })
store.setWorktreeMeta(WORKTREE_ID, { displayName: 'b', hostId: 'local' })
store.setWorktreeMeta(hostWorktreeTarget({ id: WORKTREE_ID, hostId: 'local' }), {
displayName: 'a',
hostId: 'local'
})
store.setWorktreeMeta(hostWorktreeTarget({ id: WORKTREE_ID, hostId: 'local' }), {
displayName: 'b',
hostId: 'local'
})
// Why this matters: the worktree listing itself stamps metadata on every scan, so a gate that
// re-armed on ordinary writes would restore the storm it exists to stop (#17775).
@@ -17,6 +17,7 @@ import {
type ProfileStateStoreFactoryResult
} from './profile-state/profile-state-store-factory'
import { profileStateDatabaseFile } from './profile-state/profile-state-database'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', () => ({
app: {
@@ -185,10 +186,13 @@ describe('profile-state candidate cutover soak', () => {
}
]
})
reopened.store.setWorktreeMeta('repo-local::/fixture/local', {
comment: `soak-${round}`,
linkedPR: 100 + round
})
reopened.store.setWorktreeMeta(
hostWorktreeTarget({ id: 'repo-local::/fixture/local', hostId: 'local' }),
{
comment: `soak-${round}`,
linkedPR: 100 + round
}
)
reopened.store.createAutomationRun(currentAutomation, 100 + round, 'manual')
reopened.store.flushOrThrow()
reopened.store.freezeWrites()
@@ -9,6 +9,7 @@ import {
} from './acknowledged-terminal-tab-retirement-fixture'
import { advanceTerminalTopologyRevision } from '../persistence/terminal-topology/terminal-topology-membership'
import { delegatedMobileSessionTabClose } from './mobile-session-tab-close-outcome'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
const fixtures: ReturnType<typeof createAcknowledgedTabRetirementFixture>[] = []
afterEach(async () => {
@@ -140,7 +141,9 @@ it('rechecks current pins after renderer acknowledgement', async () => {
it('keeps persistence writable when worktree teardown finds remaining terminal rows', async () => {
const f = fixture()
f.store.updateRepo({ id: 'repo1' }, { executionHostId: 'ssh:target' })
f.store.setWorktreeMeta(ACK_WORKTREE, { hostId: 'ssh:target' })
f.store.setWorktreeMeta(hostWorktreeTarget({ id: ACK_WORKTREE, hostId: 'ssh:target' }), {
hostId: 'ssh:target'
})
f.store.setWorkspaceSession(f.store.getWorkspaceSession(), 'ssh:target')
vi.spyOn(f.runtime, 'closeMobileSessionTab').mockResolvedValue(delegatedMobileSessionTabClose())
await expect(f.runtime.closeTerminalsForWorktree(`id:${ACK_WORKTREE}`)).rejects.toThrow(
@@ -31,6 +31,7 @@ import type { RuntimeStore } from '../runtime-store-contract'
import type { ResolvedWorktree } from '../runtime-worktree-path-identity'
import type { Repo } from '../../../shared/repo-types'
import { isWslMoveWorktreeBlocked } from '../../persistence/moving-wsl-projects/wsl-move-background-backoff'
import { worktreeRowTarget } from '../../persistence/host-qualified-worktree-meta'
export abstract class OrcaRuntimeWithFileCommands extends OrcaRuntimeWithPreservedBranchCleanup {
abstract resolveRuntimeFileTarget(worktreeSelector: string): Promise<{
@@ -202,16 +203,14 @@ export abstract class OrcaRuntimeWithFileCommands extends OrcaRuntimeWithPreserv
// Why (#17828 review follow-up): RuntimeGitSyncCommands materializes with no store to
// avoid unrelated side effects; this is its only way back into the persisted
// `pushTarget.remoteCreated` flag that #17842's orphan sweep relies on.
persistMaterializedPushTarget: (worktreeId, pushTarget) => {
persistMaterializedPushTarget: (worktree, pushTarget) => {
const store = this.store
if (!store?.setWorktreeMeta) {
return
}
const target = store ? worktreeRowTarget(store, worktree) : undefined
// Bookkeeping incidental to the push: a project a WSL move is taking skips it, not the push.
if (isWslMoveWorktreeBlocked(worktreeId, store.getWorktreeMeta?.(worktreeId)?.hostId)) {
if (!store || !target || isWslMoveWorktreeBlocked(target.id, target.hostId)) {
return
}
store.setWorktreeMeta(worktreeId, { pushTarget })
store.setWorktreeMeta(target, { pushTarget })
}
})
@@ -26,6 +26,7 @@ vi.mock('./repo-worktree-admin-fingerprint', () => ({
}))
import { OrcaRuntimeService } from './orca-runtime'
import type { HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
const REPO_ID = 'repo-upgraded'
const FOLDER_PATH = 'D:\\project\\a'
@@ -52,11 +53,11 @@ function makeRuntime(): OrcaRuntimeService {
getRepos: () => repos,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (id: string) => metaById[id],
setWorktreeMeta: (id: string, meta: Record<string, unknown>) => {
setWorktreeMeta: ({ id }: HostWorktreeTarget, meta: Record<string, unknown>) => {
metaById[id] = { ...metaById[id], ...meta }
return metaById[id]
},
removeWorktreeMeta: (id: string) => {
removeWorktreeMeta: ({ id }: HostWorktreeTarget) => {
delete metaById[id]
},
getAllWorktreeLineage: () => ({}),
@@ -77,7 +77,7 @@ function createRuntimeInternals(
getRepo: (id: string) => (id === REPO_ID ? REPO : undefined),
getAllWorktreeMeta: () => meta,
getWorktreeMeta: (worktreeId: string) => meta[worktreeId],
setWorktreeMeta: (worktreeId: string, patch: Record<string, unknown>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, patch: Record<string, unknown>) => {
meta[worktreeId] = { ...meta[worktreeId], ...patch }
return meta[worktreeId]
},
@@ -2,6 +2,7 @@ import type { RuntimeTerminalShow } from '../../../../shared/runtime-terminal-co
import type { ResolvedWorktree } from '../../runtime-worktree-path-identity'
import { OrcaRuntimeWithFileCommands } from '../../construction/runtime-file-command-state'
import { RuntimeLinearCommands } from '../../runtime-linear-connection-commands'
import { worktreeRowTarget } from '../../../persistence/host-qualified-worktree-meta'
export abstract class OrcaRuntimeWithLinearCommands extends OrcaRuntimeWithFileCommands {
abstract showTerminal(handle: string): Promise<RuntimeTerminalShow>
@@ -15,7 +16,12 @@ export abstract class OrcaRuntimeWithLinearCommands extends OrcaRuntimeWithFileC
showTerminal: (handle) => this.showTerminal(handle),
resolveWorktreeSelector: (selector) => this.resolveWorktreeSelector(selector),
listResolvedWorktrees: () => this.listResolvedWorktrees(),
setWorktreeMeta: (worktreeId, meta) => this.store!.setWorktreeMeta(worktreeId, meta),
setWorktreeMeta: (worktree, meta) => {
const target = worktreeRowTarget(this.store!, worktree)
if (target) {
this.store!.setWorktreeMeta(target, meta)
}
},
emitClientEvent: (event) => this.emitClientEvent(event)
})
}
@@ -7,6 +7,7 @@ import type * as GitStatusModule from '../git/status'
import type * as CommitMessageTextGenerationModule from '../text-generation/commit-message-text-generation'
import type * as WorktreeModule from '../git/worktree'
import { OrcaRuntimeService } from './orca-runtime'
import type { HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
const mocks = vi.hoisted(() => ({
listWorktrees: vi.fn(),
@@ -79,7 +80,7 @@ function makeStore(worktreePath: string) {
: undefined,
getAllWorktreeMeta: () => worktreeMeta,
getWorktreeMeta: (id: string) => worktreeMeta[id],
setWorktreeMeta: (id: string, updates: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id }: HostWorktreeTarget, updates: Partial<WorktreeMeta>) => {
worktreeMeta[id] = { ...worktreeMeta[id], ...updates }
return worktreeMeta[id]
},
@@ -11,6 +11,7 @@ import type { WorktreeMeta } from './orca-runtime-test-mocks.spec'
import { makeWorktreeMeta, store } from './orca-runtime-test-fixtures.spec'
import { createWorktreeWithStartupAgent } from '../agent-launch/startup-agent-worktree-create'
import type { RuntimeManagedWorktreeCreateArgs } from './runtime-managed-worktree-create-types'
import type { HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
type DraftSettings = {
defaultTuiAgent: 'claude' | null
@@ -37,9 +38,9 @@ async function runCreate(
}),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
setWorktreeMeta: ({ id }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[id] = { ...(metaById[id] ?? makeWorktreeMeta()), ...meta }
return metaById[id]
}
})
const spawn = vi.fn().mockResolvedValue({ id: `pty-${route}` })
@@ -28,6 +28,7 @@ import { InMemoryOrchestrationMessages } from './orca-runtime-test-orchestration
import type { OrchestrationDb } from './orchestration/db'
import type { PtyProcessInspection } from '../providers/pty-process-inspection'
import type { Store } from '../persistence'
import type { HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
type RuntimeService = InstanceType<typeof OrcaRuntimeService>
type HeadlessTerminal = InstanceType<typeof HeadlessEmulator>
@@ -375,14 +376,14 @@ function createStaleRuntimeWorktreeStore(
const metaById: Record<string, WorktreeMeta> = {
[worktreeId]: makeWorktreeMeta(metaOverrides)
}
const removeWorktreeMeta = vi.fn((id: string) => {
const removeWorktreeMeta = vi.fn(({ id }: HostWorktreeTarget) => {
delete metaById[id]
})
const runtimeStore = {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (id: string) => metaById[id],
setWorktreeMeta: (id: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[id] = { ...(metaById[id] ?? makeWorktreeMeta()), ...meta }
return metaById[id]
},
@@ -428,7 +429,7 @@ const store = {
}
}),
getWorktreeMeta: (worktreeId: string) => store.getAllWorktreeMeta()[worktreeId],
setWorktreeMeta: (_worktreeId: string, meta: Record<string, unknown>) =>
setWorktreeMeta: (_target: HostWorktreeTarget, meta: Record<string, unknown>) =>
({
...store.getAllWorktreeMeta()[TEST_WORKTREE_ID],
...meta
@@ -31,6 +31,7 @@ import {
resetBrowserIdentityModeStoreForTests
} from '../../browser/browser-identity-mode-store'
import { runtimeAdvertisesBrowserClientHosting } from '../../../shared/browser-client-hosting-eligibility'
import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
describe('OrcaRuntimeService', () => {
it('advertises client hosting when its factory implements client page creation', () => {
@@ -688,7 +689,9 @@ describe('OrcaRuntimeService', () => {
'runtime:env-b'
)
expect(removeWorktreeMeta).toHaveBeenCalledWith(TEST_WORKTREE_ID, 'runtime:env-b')
expect(removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: TEST_WORKTREE_ID, hostId: 'runtime:env-b' })
)
expect(internals.mobileSessionTabsByWorktree.get(TEST_WORKTREE_ID)).toBe(localSession)
})
@@ -721,7 +724,9 @@ describe('OrcaRuntimeService', () => {
'ssh:ssh-1'
)
expect(removeWorktreeMeta).toHaveBeenCalledWith(TEST_WORKTREE_ID, 'ssh:ssh-1')
expect(removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: TEST_WORKTREE_ID, hostId: 'ssh:ssh-1' })
)
expect(internals.mobileSessionTabsByWorktree.get(TEST_WORKTREE_ID)).toBe(survivingSession)
expect(deleteWorktreeHistoryDirMock).not.toHaveBeenCalled()
})
@@ -8,6 +8,10 @@ import {
makeWorktreeMeta,
store
} from '../orca-runtime-test-fixtures.spec'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../../shared/worktree/host-qualified-identity'
const anyCatalogVersion = { epoch: expect.any(String), sequence: expect.any(Number) }
@@ -16,10 +20,12 @@ describe('OrcaRuntimeService', () => {
const metaById: Record<string, WorktreeMeta> = {
[TEST_WORKTREE_ID]: makeWorktreeMeta({ instanceId: 'child-instance' })
}
const setWorktreeMeta = vi.fn((worktreeId: string, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
})
const setWorktreeMeta = vi.fn(
({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
}
)
const runtimeStore = {
...store,
getAllWorktreeMeta: () => metaById,
@@ -35,7 +41,10 @@ describe('OrcaRuntimeService', () => {
orcaCreationWorkspaceLayout: { path: '/tmp', nestWorkspaces: false }
})
expect(setWorktreeMeta).toHaveBeenCalledWith(TEST_WORKTREE_ID, { comment: 'keep me' })
expect(setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: TEST_WORKTREE_ID, hostId: 'local' }),
{ comment: 'keep me' }
)
})
it('ignores stale instance-mismatched lineage when validating manual cycle repairs', async () => {
@@ -47,12 +56,12 @@ describe('OrcaRuntimeService', () => {
[parentId]: makeWorktreeMeta({ instanceId: 'new-parent-instance' }),
[childId]: makeWorktreeMeta({ instanceId: 'child-instance' })
}
const setWorktreeLineage = vi.fn((_worktreeId, lineage) => lineage)
const setWorktreeLineage = vi.fn((_target: HostWorktreeTarget, lineage) => lineage)
const runtimeStore = {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
},
@@ -93,7 +102,7 @@ describe('OrcaRuntimeService', () => {
})
expect(setWorktreeLineage).toHaveBeenCalledWith(
childId,
hostWorktreeTarget({ id: childId, hostId: 'local' }),
expect.objectContaining({
worktreeId: childId,
worktreeInstanceId: 'child-instance',
@@ -116,12 +125,12 @@ describe('OrcaRuntimeService', () => {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
},
getWorktreeLineage: () => undefined,
setWorktreeLineage: vi.fn((_worktreeId: string, lineage) => lineage)
setWorktreeLineage: vi.fn((_target: HostWorktreeTarget, lineage) => lineage)
}
vi.mocked(listWorktrees).mockResolvedValue([
{
@@ -170,18 +179,18 @@ describe('OrcaRuntimeService', () => {
createdAt: 1
}
}
const setWorktreeLineage = vi.fn((_worktreeId: string, lineage) => lineage)
const setWorktreeLineage = vi.fn((_target: HostWorktreeTarget, lineage) => lineage)
const runtimeStore = {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
},
getWorktreeLineage: (worktreeId: string) => lineageById[worktreeId],
getAllWorktreeLineage: () => lineageById,
removeWorktreeLineage: vi.fn((worktreeId: string) => {
removeWorktreeLineage: vi.fn(({ id: worktreeId }: HostWorktreeTarget) => {
delete lineageById[worktreeId]
}),
setWorktreeLineage
@@ -227,7 +236,7 @@ describe('OrcaRuntimeService', () => {
})
expect(setWorktreeLineage).toHaveBeenCalledWith(
childId,
hostWorktreeTarget({ id: childId, hostId: 'local' }),
expect.objectContaining({
worktreeInstanceId: 'child-instance',
parentWorktreeInstanceId: rotatedParentInstance
@@ -255,17 +264,19 @@ describe('OrcaRuntimeService', () => {
createdAt: 1
}
}
const removeWorktreeLineage = vi.fn((worktreeId: string) => {
const removeWorktreeLineage = vi.fn(({ id: worktreeId }: HostWorktreeTarget) => {
delete lineageById[worktreeId]
})
const runtimeStore = {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: vi.fn((worktreeId: string, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
}),
setWorktreeMeta: vi.fn(
({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
}
),
getAllWorktreeLineage: () => lineageById,
removeWorktreeLineage
}
@@ -549,13 +560,14 @@ 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],
getRepo: (id: string) => (id === folderRepo.id ? folderRepo : undefined),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
},
@@ -13,6 +13,7 @@ import {
store,
withPlatform
} from '../orca-runtime-test-fixtures.spec'
import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
describe('OrcaRuntimeService', () => {
it('hides agent scratch created inside a linked checkout from runtime listings', async () => {
@@ -235,7 +236,7 @@ describe('OrcaRuntimeService', () => {
[firstId]: { sortOrder: 200 },
[secondId]: { sortOrder: 100 }
}
const setWorktreeMeta = vi.fn((id: string, updates: Partial<WorktreeMeta>) => {
const setWorktreeMeta = vi.fn(({ id }: HostWorktreeTarget, updates: Partial<WorktreeMeta>) => {
metaById[id] = { ...metaById[id], ...updates }
})
const runtime = new OrcaRuntimeService({
@@ -453,7 +454,7 @@ describe('OrcaRuntimeService', () => {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
},
@@ -499,7 +500,7 @@ describe('OrcaRuntimeService', () => {
createdAt: 1
}
}
const removeWorktreeLineage = vi.fn((worktreeId: string) => {
const removeWorktreeLineage = vi.fn(({ id: worktreeId }: HostWorktreeTarget) => {
delete lineageById[worktreeId]
})
const runtimeStore = {
@@ -508,10 +509,12 @@ describe('OrcaRuntimeService', () => {
getRepos: () => [remoteRepo],
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: vi.fn((worktreeId: string, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
}),
setWorktreeMeta: vi.fn(
({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
}
),
getAllWorktreeLineage: () => lineageById,
removeWorktreeLineage
}
@@ -14,6 +14,10 @@ import {
makeWorktreeMeta,
store
} from '../orca-runtime-test-fixtures.spec'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../../shared/worktree/host-qualified-identity'
describe('OrcaRuntimeService', () => {
it('exposes valid parent and child lineage in CLI worktree records', async () => {
@@ -46,7 +50,7 @@ describe('OrcaRuntimeService', () => {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
},
@@ -103,7 +107,7 @@ describe('OrcaRuntimeService', () => {
displayName: 'coordinator'
})
}
const setWorktreeLineage = vi.fn((worktreeId: string, lineage) => {
const setWorktreeLineage = vi.fn(({ id: worktreeId }: HostWorktreeTarget, lineage) => {
metaById[worktreeId] = metaById[worktreeId] ?? makeWorktreeMeta()
return lineage
})
@@ -111,7 +115,7 @@ describe('OrcaRuntimeService', () => {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
const existing = metaById[worktreeId] ?? makeWorktreeMeta({ instanceId: 'child-instance' })
metaById[worktreeId] = { ...existing, ...meta }
return metaById[worktreeId]
@@ -167,7 +171,10 @@ describe('OrcaRuntimeService', () => {
})
expect(result.lineage).not.toHaveProperty('createdByTerminalHandle')
expect(result.warnings).toEqual([])
expect(setWorktreeLineage).toHaveBeenCalledWith(childId, expect.any(Object))
expect(setWorktreeLineage).toHaveBeenCalledWith(
hostWorktreeTarget({ id: childId, hostId: 'local' }),
expect.any(Object)
)
})
it('enriches caller-terminal lineage with active orchestration dispatch context', async () => {
@@ -185,12 +192,12 @@ describe('OrcaRuntimeService', () => {
displayName: 'worker'
})
}
const setWorktreeLineage = vi.fn((_worktreeId: string, lineage) => lineage)
const setWorktreeLineage = vi.fn((_target: HostWorktreeTarget, lineage) => lineage)
const runtimeStore = {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
const existing = metaById[worktreeId] ?? makeWorktreeMeta()
metaById[worktreeId] = { ...existing, ...meta }
return metaById[worktreeId]
@@ -287,7 +294,7 @@ describe('OrcaRuntimeService', () => {
createdByTerminalHandle: workerHandle
})
expect(setWorktreeLineage).toHaveBeenCalledWith(
childId,
hostWorktreeTarget({ id: childId, hostId: 'local' }),
expect.objectContaining({
worktreeInstanceId: expect.not.stringMatching(/^old-/),
parentWorktreeInstanceId: 'worker-instance'
@@ -15,6 +15,10 @@ import {
makeWorktreeMeta,
store
} from '../orca-runtime-test-fixtures.spec'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../../shared/worktree/host-qualified-identity'
describe('OrcaRuntimeService', () => {
it.each(['failed', 'circuit_broken'] as const)(
@@ -125,12 +129,12 @@ describe('OrcaRuntimeService', () => {
const metaById: Record<string, WorktreeMeta> = {
[parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' })
}
const setWorktreeLineage = vi.fn((_worktreeId: string, lineage) => lineage)
const setWorktreeLineage = vi.fn((_target: HostWorktreeTarget, lineage) => lineage)
const runtimeStore = {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
const existing = metaById[worktreeId] ?? makeWorktreeMeta()
metaById[worktreeId] = { ...existing, ...meta }
return metaById[worktreeId]
@@ -181,7 +185,10 @@ describe('OrcaRuntimeService', () => {
childWorktreeIds: [],
lineage: result.lineage
})
expect(setWorktreeLineage).toHaveBeenCalledWith(childId, expect.any(Object))
expect(setWorktreeLineage).toHaveBeenCalledWith(
hostWorktreeTarget({ id: childId, hostId: 'local' }),
expect.any(Object)
)
})
it('keeps cwd-inferred lineage best-effort when the cwd parent cannot be resolved', async () => {
@@ -235,12 +242,12 @@ describe('OrcaRuntimeService', () => {
displayName: 'worker'
})
}
const setWorktreeLineage = vi.fn((_worktreeId: string, lineage) => lineage)
const setWorktreeLineage = vi.fn((_target: HostWorktreeTarget, lineage) => lineage)
const runtimeStore = {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
const existing = metaById[worktreeId] ?? makeWorktreeMeta()
metaById[worktreeId] = { ...existing, ...meta }
return metaById[worktreeId]
@@ -315,7 +322,7 @@ describe('OrcaRuntimeService', () => {
taskId: 'task_abc123'
})
expect(setWorktreeLineage).toHaveBeenCalledWith(
childId,
hostWorktreeTarget({ id: childId, hostId: 'local' }),
expect.objectContaining({
parentWorktreeInstanceId: 'worker-instance'
})
@@ -333,12 +340,12 @@ describe('OrcaRuntimeService', () => {
displayName: 'parent'
})
}
const setWorktreeLineage = vi.fn((_worktreeId: string, lineage) => lineage)
const setWorktreeLineage = vi.fn((_target: HostWorktreeTarget, lineage) => lineage)
const runtimeStore = {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
const existing = metaById[worktreeId] ?? makeWorktreeMeta()
metaById[worktreeId] = { ...existing, ...meta }
return metaById[worktreeId]
@@ -413,7 +420,7 @@ describe('OrcaRuntimeService', () => {
taskId: 'task_creator123'
})
expect(setWorktreeLineage).toHaveBeenCalledWith(
childId,
hostWorktreeTarget({ id: childId, hostId: 'local' }),
expect.objectContaining({
parentWorktreeInstanceId: 'parent-instance'
})
@@ -21,6 +21,10 @@ import {
store
} from '../orca-runtime-test-fixtures.spec'
import { REPO_SEARCH_REFS_MAX_LIMIT } from '../../../shared/repo-search-limits'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../../shared/worktree/host-qualified-identity'
describe('OrcaRuntimeService', () => {
it('rejects invalid positive limits for bounded list commands', async () => {
@@ -75,14 +79,16 @@ describe('OrcaRuntimeService', () => {
[childId]: makeWorktreeMeta({ instanceId: 'child-instance' }),
[parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' })
}
const setWorktreeLineage = vi.fn((_worktreeId: string, lineage: WorktreeLineage) => lineage)
const setWorktreeLineage = vi.fn(
(_target: HostWorktreeTarget, lineage: WorktreeLineage) => lineage
)
const runtimeStore = {
...store,
getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined),
getRepos: () => [remoteRepo],
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
},
@@ -96,7 +102,7 @@ describe('OrcaRuntimeService', () => {
})
expect(setWorktreeLineage).toHaveBeenCalledWith(
childId,
hostWorktreeTarget({ id: childId, hostId: 'ssh:ssh-1' }),
expect.objectContaining({
worktreeId: childId,
worktreeInstanceId: 'child-instance',
@@ -127,12 +133,12 @@ describe('OrcaRuntimeService', () => {
getRepos: () => [remoteRepo],
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
},
getWorktreeLineage: () => undefined,
setWorktreeLineage: vi.fn((_worktreeId: string, lineage: WorktreeLineage) => lineage)
setWorktreeLineage: vi.fn((_target: HostWorktreeTarget, lineage: WorktreeLineage) => lineage)
}
const runtime = new OrcaRuntimeService(runtimeStore as never)
@@ -152,7 +158,9 @@ describe('OrcaRuntimeService', () => {
const childId = `${repoId}::${childPath}`
const parentId = `${repoId}::${parentPath}`
const metaById: Record<string, WorktreeMeta> = {}
const setWorktreeLineage = vi.fn((_worktreeId: string, lineage: WorktreeLineage) => lineage)
const setWorktreeLineage = vi.fn(
(_target: HostWorktreeTarget, lineage: WorktreeLineage) => lineage
)
const runtimeStore = {
...store,
getRepo: (id: string) =>
@@ -176,7 +184,7 @@ describe('OrcaRuntimeService', () => {
],
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
},
@@ -227,13 +235,15 @@ describe('OrcaRuntimeService', () => {
[parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' }),
[childId]: makeWorktreeMeta({ instanceId: 'child-instance' })
}
const setWorktreeLineage = vi.fn((_worktreeId: string, lineage: WorktreeLineage) => lineage)
const setWorktreeLineage = vi.fn(
(_target: HostWorktreeTarget, lineage: WorktreeLineage) => lineage
)
const setWorkspaceLineage = vi.fn((lineage: WorkspaceLineage) => lineage)
const runtimeStore = {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
},
@@ -264,7 +274,7 @@ describe('OrcaRuntimeService', () => {
})
expect(setWorktreeLineage).toHaveBeenCalledWith(
childId,
hostWorktreeTarget({ id: childId, hostId: 'local' }),
expect.objectContaining({
parentWorktreeId: parentId,
parentWorktreeInstanceId: 'parent-instance',
@@ -325,7 +335,9 @@ describe('OrcaRuntimeService', () => {
[childId]: makeWorktreeMeta({ instanceId: 'child-instance', ...scenario.childMeta }),
[parentId]: makeWorktreeMeta({ instanceId: 'parent-instance', ...scenario.parentMeta })
}
const setWorktreeLineage = vi.fn()
const setWorktreeLineage = vi.fn(
(_target: HostWorktreeTarget, lineage: WorktreeLineage) => lineage
)
const setWorkspaceLineage = vi.fn()
const runtimeStore = {
...store,
@@ -333,7 +345,7 @@ describe('OrcaRuntimeService', () => {
getRepo: (id: string) => repos.find((repo) => repo.id === id),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
},
@@ -353,7 +365,7 @@ describe('OrcaRuntimeService', () => {
if (scenario.accepted) {
await update
expect(setWorktreeLineage).toHaveBeenCalledWith(
childId,
hostWorktreeTarget({ id: childId, hostId: 'local' }),
expect.objectContaining({ parentWorktreeId: parentId })
)
expect(setWorkspaceLineage).toHaveBeenCalled()
@@ -371,17 +383,15 @@ describe('OrcaRuntimeService', () => {
[childId]: makeWorktreeMeta({ instanceId: 'child-instance' })
}
const removeWorktreeLineage = vi.fn()
const removeWorkspaceLineage = vi.fn()
const runtimeStore = {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
},
removeWorktreeLineage,
removeWorkspaceLineage
removeWorktreeLineage
}
vi.mocked(listWorktrees).mockResolvedValue([
{
@@ -398,7 +408,8 @@ describe('OrcaRuntimeService', () => {
lineage: { noParent: true }
})
expect(removeWorktreeLineage).toHaveBeenCalledWith(childId)
expect(removeWorkspaceLineage).toHaveBeenCalledWith(`worktree:${childId}`)
expect(removeWorktreeLineage).toHaveBeenCalledWith(
hostWorktreeTarget({ id: childId, hostId: 'local' })
)
})
})
@@ -20,6 +20,7 @@ import {
makeWorktreeMeta,
store
} from '../orca-runtime-test-fixtures.spec'
import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
describe('OrcaRuntimeService', () => {
it('creates additional workspace metadata for folder-mode repos through runtime create', async () => {
@@ -47,11 +48,11 @@ describe('OrcaRuntimeService', () => {
getRepo: (id: string) => (id === folderRepo.id ? folderRepo : undefined),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
},
removeWorktreeMeta: (worktreeId: string) => {
removeWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget) => {
delete metaById[worktreeId]
}
}
@@ -17,6 +17,7 @@ import {
store,
withPlatform
} from '../orca-runtime-test-fixtures.spec'
import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
describe('OrcaRuntimeService', () => {
it('tui-idle times out when PTY data has no agent OSC title transitions', async () => {
@@ -268,7 +269,7 @@ describe('OrcaRuntimeService', () => {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
},
@@ -17,6 +17,10 @@ import {
makeWorktreeMeta,
store
} from '../orca-runtime-test-fixtures.spec'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../../shared/worktree/host-qualified-identity'
describe('OrcaRuntimeService', () => {
it('reports the wake as unsupported when a phone activates a worktree on headless serve', async () => {
@@ -138,15 +142,19 @@ describe('OrcaRuntimeService', () => {
const metaById: Record<string, WorktreeMeta> = {
[TEST_WORKTREE_ID]: makeWorktreeMeta({ isUnread: false, instanceId: 'wt-instance' })
}
const setWorktreeMeta = vi.fn((worktreeId: string, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
})
const setWorktreeMeta = vi.fn(
({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
)
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({
@@ -180,7 +188,10 @@ describe('OrcaRuntimeService', () => {
await runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`, { notifyClients: false })
expect(setWorktreeMeta).toHaveBeenCalledTimes(1)
expect(setWorktreeMeta).toHaveBeenCalledWith(TEST_WORKTREE_ID, { isUnread: false })
expect(setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: TEST_WORKTREE_ID, hostId: 'local' }),
{ isUnread: false }
)
expect(worktreesChanged).toHaveBeenCalledTimes(1)
expect(worktreesChanged).toHaveBeenCalledWith(TEST_REPO_ID)
})
@@ -189,14 +200,18 @@ describe('OrcaRuntimeService', () => {
const metaById: Record<string, WorktreeMeta> = {
[TEST_WORKTREE_ID]: makeWorktreeMeta({ isUnread: true })
}
const setWorktreeMeta = vi.fn((worktreeId: string, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
})
const setWorktreeMeta = vi.fn(
({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
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({
@@ -12,6 +12,10 @@ import {
makeWorktreeMeta,
store
} from '../orca-runtime-test-fixtures.spec'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../../shared/worktree/host-qualified-identity'
describe('OrcaRuntimeService', () => {
it('retires exited saved PTY bindings instead of publishing a pending ghost', async () => {
@@ -528,16 +532,20 @@ describe('OrcaRuntimeService', () => {
const metaById: Record<string, WorktreeMeta> = {
[TEST_WORKTREE_ID]: makeWorktreeMeta({ isUnread: true })
}
const setWorktreeMeta = vi.fn((worktreeId: string, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
})
const setWorktreeMeta = vi.fn(
({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
)
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({
@@ -560,7 +568,10 @@ describe('OrcaRuntimeService', () => {
await runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`, { notifyClients: false })
expect(setWorktreeMeta).toHaveBeenCalledWith(TEST_WORKTREE_ID, { isUnread: false })
expect(setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: TEST_WORKTREE_ID, hostId: 'local' }),
{ isUnread: false }
)
expect(metaById[TEST_WORKTREE_ID]?.isUnread).toBe(false)
expect(worktreesChanged).toHaveBeenCalledWith(TEST_REPO_ID)
expect(activateWorktree).not.toHaveBeenCalled()
@@ -573,10 +584,12 @@ 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,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
getWorktreeMetaForHost: (worktreeId: string) => metaById[worktreeId],
getWorkspaceSession: () => ({
...getDefaultWorkspaceSession(),
sleepingAgentSessionsByPaneKey: {
@@ -15,6 +15,7 @@ import {
makeWorktreeMeta,
store
} from '../orca-runtime-test-fixtures.spec'
import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
// Why: this process serves no SSH git, so an SSH repo lists its stored rows.
function storedWorktreeMeta(
@@ -254,7 +255,7 @@ describe('OrcaRuntimeService', () => {
getRepo,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
@@ -18,6 +18,7 @@ import {
makeWorktreeMeta,
store
} from '../orca-runtime-test-fixtures.spec'
import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
describe('OrcaRuntimeService', () => {
it('rejects when every exact PR branch checkout path suffix is occupied', async () => {
@@ -93,7 +94,7 @@ describe('OrcaRuntimeService', () => {
...createFolderWorkspaceRuntimeStore(),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
},
@@ -138,7 +139,7 @@ describe('OrcaRuntimeService', () => {
...createFolderWorkspaceRuntimeStore(),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
},
@@ -42,6 +42,7 @@ import {
syncSinglePty
} from '../orca-runtime-test-fixtures.spec'
import { createWorktreeRemovalRuntime } from '../orca-runtime-test-scenario-builders.spec'
import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
// Why: these fixtures register an SSH provider, which models a connected relay session — and a
// connected session has always read the host's `$HOME`. The removal guards refuse without it.
@@ -394,7 +395,9 @@ describe('OrcaRuntimeService', () => {
})
// The repo resolved to the local host, so the metadata purge names it —
// an unqualified purge would evict a same-id row owned by another host.
expect(removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
} finally {
gitSpy.mockRestore()
}
@@ -30,6 +30,7 @@ import {
store
} from '../orca-runtime-test-fixtures.spec'
import { createWorktreeRemovalRuntime } from '../orca-runtime-test-scenario-builders.spec'
import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
// Why: these fixtures register an SSH provider, which models a connected relay session — and a
// connected session has always read the host's `$HOME`. The removal guards refuse without it.
@@ -226,7 +227,9 @@ describe('OrcaRuntimeService', () => {
expect(removeWorktree).not.toHaveBeenCalled()
// The repo resolved to the local host, so the metadata purge names it —
// an unqualified purge would evict a same-id row owned by another host.
expect(removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId)
expect(invalidateAuthorizedRootsCacheMock).toHaveBeenCalledWith(runtimeStore, TEST_REPO_ID)
expect(notifier.worktreesChanged).toHaveBeenCalledWith(TEST_REPO_ID)
@@ -252,7 +255,9 @@ describe('OrcaRuntimeService', () => {
expect(removeWorktree).not.toHaveBeenCalled()
// The repo resolved to the local host, so the metadata purge names it —
// an unqualified purge would evict a same-id row owned by another host.
expect(removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId)
expect(invalidateAuthorizedRootsCacheMock).toHaveBeenCalledWith(runtimeStore, TEST_REPO_ID)
expect(notifier.worktreesChanged).toHaveBeenCalledWith(TEST_REPO_ID)
@@ -313,7 +318,9 @@ describe('OrcaRuntimeService', () => {
expect(removeWorktree).not.toHaveBeenCalled()
// The repo resolved to the local host, so the metadata purge names it —
// an unqualified purge would evict a same-id row owned by another host.
expect(removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId)
expect(invalidateAuthorizedRootsCacheMock).toHaveBeenCalled()
expect(notifier.worktreesChanged).toHaveBeenCalledWith(TEST_REPO_ID)
@@ -386,7 +393,9 @@ describe('OrcaRuntimeService', () => {
expect(removeWorktree).not.toHaveBeenCalled()
// The repo resolved to the local host, so the metadata purge names it —
// an unqualified purge would evict a same-id row owned by another host.
expect(removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId)
expect(invalidateAuthorizedRootsCacheMock).toHaveBeenCalled()
expect(notifier.worktreesChanged).toHaveBeenCalledWith(TEST_REPO_ID)
@@ -26,6 +26,10 @@ import {
writeFile
} from '../orca-runtime-test-mocks.spec'
import type { WorktreeMeta } from '../orca-runtime-test-mocks.spec'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../../shared/worktree/host-qualified-identity'
import {
TEST_REPO_ID,
TEST_REPO_PATH,
@@ -62,7 +66,7 @@ describe('OrcaRuntimeService', () => {
orcaCreationSource: 'ssh'
})
}
const removeWorktreeMeta = vi.fn((id: string) => {
const removeWorktreeMeta = vi.fn(({ id }: HostWorktreeTarget) => {
delete metaById[id]
})
const runtimeStore = {
@@ -71,7 +75,7 @@ describe('OrcaRuntimeService', () => {
getRepo: (id: string) => (id === repo.id ? repo : undefined),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (id: string) => metaById[id],
setWorktreeMeta: (id: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[id] = { ...(metaById[id] ?? makeWorktreeMeta()), ...meta }
return metaById[id]
},
@@ -405,7 +409,9 @@ describe('OrcaRuntimeService', () => {
await rm(orphanPath, { recursive: true, force: true })
vi.mocked(listWorktrees).mockResolvedValue([])
await expect(runtime.removeManagedWorktree(worktreeId)).resolves.toEqual({})
expect(removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
expect(removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
} finally {
gitSpy.mockRestore()
await rm(parentDir, { recursive: true, force: true })
@@ -747,7 +753,7 @@ describe('OrcaRuntimeService', () => {
updateRepo: () => undefined as never,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
const existingMeta = metaById[worktreeId]
const nextMeta: WorktreeMeta = {
displayName: meta.displayName ?? existingMeta?.displayName ?? '',
@@ -37,6 +37,7 @@ import {
import { createWorktreeRemovalRuntime } from '../orca-runtime-test-scenario-builders.spec'
import { getLocalWorktreeScanGeneration } from '../../local-worktree-scan-generation'
import { makeAgentStatusStoreWiring } from '../agent-status-store-wiring.test-fixture'
import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
describe('OrcaRuntimeService', () => {
it('creates the first terminal by id when duplicate repo entries expose the same path', async () => {
@@ -573,7 +574,9 @@ describe('OrcaRuntimeService', () => {
warning: expect.stringContaining(TEST_WORKTREE_PATH)
})
expect(removeWorktreeMeta).toHaveBeenCalledWith(TEST_WORKTREE_ID, 'runtime:env-1')
expect(removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: TEST_WORKTREE_ID, hostId: 'runtime:env-1' })
)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(TEST_WORKTREE_ID)
expect(invalidateAuthorizedRootsCacheMock).toHaveBeenCalled()
expect(removeWorktree).not.toHaveBeenCalled()
@@ -25,6 +25,7 @@ import {
_settlePendingWorktreeRemovalsForTests
} from '../../worktree-background-removal'
import { RuntimeWorktreeRemovalInFlight } from '../runtime-worktree-removal-in-flight'
import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
const REMOTE_REPO_PATH = '/remote/repo'
@@ -42,8 +43,8 @@ function makeRemoteRepoStore(
const metaById: Record<string, WorktreeMeta> = {
[TEST_WORKTREE_ID]: makeWorktreeMeta({ hostId: executionHostId, ...metaOverrides })
}
const removeWorktreeMeta = vi.fn((worktreeId: string, hostId?: string) => {
if (!hostId || metaById[worktreeId]?.hostId === hostId) {
const removeWorktreeMeta = vi.fn(({ id: worktreeId, hostId }: HostWorktreeTarget) => {
if (metaById[worktreeId]?.hostId === hostId) {
delete metaById[worktreeId]
}
})
@@ -57,7 +58,7 @@ function makeRemoteRepoStore(
getRepo: (id: string) => (id === repo.id ? repo : undefined),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
},
@@ -30,6 +30,7 @@ import {
makeWorktreeMeta,
store
} from '../orca-runtime-test-fixtures.spec'
import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
const LOCAL_CHILD_REPO = {
id: 'child-repo',
@@ -191,7 +192,7 @@ describe('OrcaRuntimeService', () => {
getRepo: () => remoteRepo,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...metaById[worktreeId], ...meta }
return metaById[worktreeId]
}
@@ -21,6 +21,7 @@ import {
store,
makeRuntimeStoreWithWorkspaceSession
} from '../orca-runtime-test-fixtures.spec'
import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
describe('OrcaRuntimeService', () => {
it('does not surface the new workspace when a background create opens Setup', async () => {
@@ -193,7 +194,7 @@ describe('OrcaRuntimeService', () => {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
@@ -256,7 +257,7 @@ describe('OrcaRuntimeService', () => {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
@@ -331,7 +332,7 @@ describe('OrcaRuntimeService', () => {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
@@ -419,7 +420,7 @@ describe('OrcaRuntimeService', () => {
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
@@ -515,7 +516,7 @@ describe('OrcaRuntimeService', () => {
}),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
@@ -770,7 +771,7 @@ describe('OrcaRuntimeService', () => {
}),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
@@ -22,6 +22,7 @@ import {
store,
makeRuntimeStoreWithWorkspaceSession
} from '../orca-runtime-test-fixtures.spec'
import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
describe('OrcaRuntimeService', () => {
it('sends follow-up prompts for CLI-created stdin-after-start startup agents', async () => {
@@ -34,7 +35,7 @@ describe('OrcaRuntimeService', () => {
}),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
@@ -107,7 +108,7 @@ describe('OrcaRuntimeService', () => {
}),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
@@ -177,7 +178,7 @@ describe('OrcaRuntimeService', () => {
}),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
@@ -248,7 +249,7 @@ describe('OrcaRuntimeService', () => {
}),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
@@ -476,7 +477,7 @@ describe('OrcaRuntimeService', () => {
}),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
@@ -562,7 +563,7 @@ describe('OrcaRuntimeService', () => {
}),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
setWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, meta: Partial<WorktreeMeta>) => {
metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
return metaById[worktreeId]
}
@@ -27,6 +27,7 @@ import type { ProjectGroup } from '../../shared/project-group-types'
import type { Repo } from '../../shared/repo-types'
import type { WorktreeMeta } from '../../shared/worktree/meta-types'
import { OrcaRuntimeService } from './orca-runtime'
import type { HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
const REPO_ID = 'repo-1'
const REPO_PATH = '/repo'
@@ -74,7 +75,7 @@ function makeStore(
getRepos: () => repos,
getAllWorktreeMeta: () => meta,
getWorktreeMeta: (id: string) => meta[id],
setWorktreeMeta: (id: string, patch: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id }: HostWorktreeTarget, patch: Partial<WorktreeMeta>) => {
meta[id] = { ...(meta[id] ?? makeMeta('')), ...patch }
return meta[id]
},
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import { getDefaultWorkspaceSession } from '../../shared/constants'
import { makePaneKey } from '../../shared/stable-pane-id'
import { OrcaRuntimeService } from './orca-runtime'
import type { HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
export function registerPtyInventoryPartialRelayLivenessTests(): void {
// STA-517: the worktree.ps liveness refresh is the only thing that retires an exited PTY, and
@@ -58,7 +59,7 @@ export function registerPtyInventoryPartialRelayLivenessTests(): void {
getRepo: (id: string) => (id === REPO_ID ? REPO : undefined),
getAllWorktreeMeta: () => meta,
getWorktreeMeta: (worktreeId: string) => meta[worktreeId],
setWorktreeMeta: (worktreeId: string, patch: Record<string, unknown>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, patch: Record<string, unknown>) => {
meta[worktreeId] = { ...meta[worktreeId], ...patch }
return meta[worktreeId]
},
@@ -13,6 +13,7 @@ import {
resolveScopedWorktreeIdRow,
type RepoWorktreeRowDeps
} from './repo-worktree-row-resolution'
import type { HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
function repo(
id: string,
@@ -46,13 +47,14 @@ function createDeps(repos: Repo[]): RepoWorktreeRowDeps & {
listFolderWorkspaces: ReturnType<typeof vi.fn<RepoWorktreeRowDeps['listFolderWorkspaces']>>
} {
const metaById: Record<string, WorktreeMeta> = {}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial store mock; the code under test reads only the members it defines.
const store = {
getRepos: () => repos,
getAllWorktreeMeta: () => metaById,
getAllWorktreeLineage: () => ({}),
getProjects: () => [],
getSettings: () => ({}),
setWorktreeMeta: (worktreeId: string, updates: Partial<WorktreeMeta>) => {
setWorktreeMeta: ({ id: worktreeId }: HostWorktreeTarget, updates: Partial<WorktreeMeta>) => {
const next = { ...metaById[worktreeId], ...updates } as WorktreeMeta
metaById[worktreeId] = next
return next
@@ -7,9 +7,9 @@ import {
import { getRepoExecutionHostId, type ExecutionHostId } from '../../shared/execution-host'
import {
readAllWorktreeMetaForHost,
readWorktreeMetaForHost,
writeWorktreeMetaForHost
readWorktreeMetaForHost
} from '../persistence/host-qualified-worktree-meta'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
import { isFolderRepo } from '../../shared/repo-kind'
import {
getConnectedWorktreeLineageIds,
@@ -41,11 +41,9 @@ type RepoWorktreeRowStore = Pick<
| 'getWorktreeMeta'
| 'getWorktreeMetaForHost'
| 'setWorktreeMeta'
| 'setWorktreeMetaForHost'
| 'getAllWorktreeLineage'
| 'getAllWorkspaceLineage'
| 'removeWorktreeLineage'
| 'removeWorkspaceLineage'
>
/**
@@ -172,7 +170,7 @@ export async function resolveRepoWorktreeRows(
const meta = ownedExistingMeta?.instanceId
? ownedExistingMeta
: ownedExistingMeta || (!existingMeta && repoOwnerCount === 1)
? writeWorktreeMetaForHost(store, worktreeId, expectedHostId, {})
? store.setWorktreeMeta(hostWorktreeTarget({ id: worktreeId, hostId: expectedHostId }), {})
: undefined
const merged = {
...mergeWorktree(repo.id, gitWorktree, meta, repo.displayName),
@@ -41,6 +41,7 @@ import { OrchestrationDb } from '../../../../orchestration/db'
import { OrcaRuntimeService } from '../../../../orca-runtime'
import { ORCHESTRATION_WORKER_LIST_METHOD } from './worker-list-method'
import { projectFleetWorkerPage } from './worker-observation'
import type { HostWorktreeTarget } from '../../../../../../shared/worktree/host-qualified-identity'
const PANE_KEY = 'tab-census:leaf-census'
const TERMINAL_HANDLE = 'term_census'
@@ -119,7 +120,7 @@ function censusStore() {
],
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (id: string) => metaById[id],
setWorktreeMeta: (id: string, meta: Record<string, unknown>) => {
setWorktreeMeta: ({ id }: HostWorktreeTarget, meta: Record<string, unknown>) => {
metaById[id] = { ...(metaById[id] as object), ...meta }
return metaById[id]
},
@@ -15,6 +15,7 @@ import type { Repo } from '../../shared/repo-types'
import type { WorktreeMeta } from '../../shared/worktree/meta-types'
import type { Worktree } from '../../shared/worktree/types'
import type { WorktreeLineage } from '../../shared/worktree/lineage-types'
import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
import type { RuntimeStore } from './runtime-store-contract'
import type { ResolvedWorktree } from './runtime-worktree-path-identity'
import { mergeWorktree } from '../ipc/worktree-metadata-merge'
@@ -89,9 +90,10 @@ function makeFixture(repos: Repo[] = [owner()], hostId: ExecutionHostId = 'local
getAllWorktreeMeta: () => metadata,
getWorktreeMeta: (id) => metadata[id],
getAllWorktreeLineage: () => lineageById,
setWorktreeMeta: (id, patch) => (metadata[id] = { ...meta, ...metadata[id], ...patch }),
removeWorktreeMeta: (id) => {
delete metadata[id]
setWorktreeMeta: (target, patch) =>
(metadata[target.id] = { ...meta, ...metadata[target.id], ...patch }),
removeWorktreeMeta: (target) => {
delete metadata[target.id]
},
getGitHubCache: vi.fn<RuntimeStore['getGitHubCache']>(),
getSettings: () => ({
@@ -385,8 +387,11 @@ describe('workspace activation and metadata reentry', () => {
expect(store.getWorktreeMeta(runtime.selectedWorktree.id)?.comment).toBe('updated')
expect(notify.mock.calls).toEqual([['repo']])
const otherId = `repo::${join(directory, 'other')}`
store.setWorktreeMeta(runtime.selectedWorktree.id, { sortOrder: 0 })
store.setWorktreeMeta(otherId, { sortOrder: 0 })
store.setWorktreeMeta(
hostWorktreeTarget({ id: runtime.selectedWorktree.id, hostId: 'local' }),
{ sortOrder: 0 }
)
store.setWorktreeMeta(hostWorktreeTarget({ id: otherId, hostId: 'local' }), { sortOrder: 0 })
expect(runtime.persistManagedWorktreeSortOrder([runtime.selectedWorktree.id, otherId])).toEqual(
{
updated: 2
@@ -3,6 +3,7 @@
import { describe, expect, it, vi } from 'vitest'
import type { Repo } from '../../shared/repo-types'
import { createRuntimeFolderWorktree } from './runtime-folder-worktree-create'
import type { HostWorktreeTarget } from '../../shared/worktree/host-qualified-identity'
type CreateArgs = Parameters<typeof createRuntimeFolderWorktree>[0]
@@ -20,7 +21,7 @@ function createDeps() {
.mockResolvedValue({ handle: 'term-1', worktreeId: 'folder-1', title: null })
const store = {
getSettings: () => ({ workspaceDir: '/ws', nestWorkspaces: false }),
setWorktreeMeta: (_id: string, meta: Record<string, unknown>) => meta,
setWorktreeMeta: (_target: HostWorktreeTarget, meta: Record<string, unknown>) => meta,
getProjectHostSetups: () => []
}
const deps: CreateArgs['deps'] = {
@@ -20,6 +20,7 @@ import type {
WorktreeStartupDraftPaste,
WorktreeStartupFollowup
} from './runtime-worktree-agent-startup'
import { hostWorktreeTargetForRepo } from '../persistence/host-qualified-worktree-meta'
type RuntimeFolderWorktreeCreateDeps = {
store: RuntimeStore
@@ -74,7 +75,7 @@ export async function createRuntimeFolderWorktree(args: {
request.nameWasGenerated === true
)
const resolvedFolderDisplayName = displayNameRequest.value
const meta = deps.store.setWorktreeMeta(worktreeId, {
const meta = deps.store.setWorktreeMeta(hostWorktreeTargetForRepo(worktreeId, repo), {
instanceId,
...getProjectHostSetupWorktreeMeta(deps.store.getProjectHostSetups?.() ?? [], repo),
displayName: resolvedFolderDisplayName ?? request.name,
@@ -47,7 +47,10 @@ export type RuntimeGitCommandHost = {
* no store (avoids unrelated ownership-inheritance/refspec-migration side effects), so a
* lazily-minted remote still needs a way back into the store's `pushTarget.remoteCreated`
* for #17842's orphan sweep. Called only when materialize reports `remoteCreated: true`. */
persistMaterializedPushTarget?(worktreeId: string, pushTarget: GitPushTarget): void
persistMaterializedPushTarget?(
worktree: Pick<Worktree, 'id' | 'hostId'>,
pushTarget: GitPushTarget
): void
}
/**
@@ -36,7 +36,7 @@ export class RuntimeGitSyncCommands {
)
: undefined
if (materialized?.remoteCreated) {
this.host.persistMaterializedPushTarget?.(target.worktree.id, materialized)
this.host.persistMaterializedPushTarget?.(target.worktree, materialized)
}
return { provider, pushTarget: materialized }
}

Some files were not shown because too many files have changed in this diff Show More