mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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:
@@ -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))
|
||||
},
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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]
|
||||
},
|
||||
|
||||
+4
-1
@@ -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()
|
||||
}
|
||||
|
||||
+13
-4
@@ -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)
|
||||
|
||||
+10
-4
@@ -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),
|
||||
|
||||
+2
-1
@@ -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
Reference in New Issue
Block a user