mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 08:03:09 +00:00
Fix canonical worktree identity CI failures
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -3,6 +3,7 @@ single setup-free pure-logic module, and splitting them would make the related
|
||||
edge cases harder to audit together. */
|
||||
import { posix, resolve } from 'path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { makeWorktreeKey } from '../../shared/worktree-id'
|
||||
import {
|
||||
sanitizeWorktreeName,
|
||||
sanitizeWorktreeDisplayName,
|
||||
@@ -372,7 +373,11 @@ describe('mergeWorktree', () => {
|
||||
}
|
||||
const result = mergeWorktree('repo1', baseGit, meta)
|
||||
expect(result).toEqual({
|
||||
id: 'repo1::/workspaces/feature',
|
||||
id: makeWorktreeKey({
|
||||
hostId: 'ssh:openclaw-2',
|
||||
repoId: 'repo1',
|
||||
path: '/workspaces/feature'
|
||||
}),
|
||||
repoId: 'repo1',
|
||||
path: '/workspaces/feature',
|
||||
head: 'abc123',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { makeWorktreeKey } from '../../shared/worktree-id'
|
||||
|
||||
const {
|
||||
handleMock,
|
||||
@@ -147,7 +148,8 @@ describe('registerWorktreeHandlers – Windows path handling', () => {
|
||||
getSettings: vi.fn(),
|
||||
getWorktreeMeta: vi.fn(),
|
||||
setWorktreeMeta: vi.fn(),
|
||||
removeWorktreeMeta: vi.fn()
|
||||
removeWorktreeMeta: vi.fn(),
|
||||
migrateWorktreeIdentity: vi.fn()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -187,6 +189,7 @@ describe('registerWorktreeHandlers – Windows path handling', () => {
|
||||
store.getWorktreeMeta.mockReset()
|
||||
store.setWorktreeMeta.mockReset()
|
||||
store.removeWorktreeMeta.mockReset()
|
||||
store.migrateWorktreeIdentity.mockReset()
|
||||
|
||||
for (const key of Object.keys(handlers)) {
|
||||
delete handlers[key]
|
||||
@@ -260,6 +263,11 @@ describe('registerWorktreeHandlers – Windows path handling', () => {
|
||||
})
|
||||
|
||||
it('accepts a newly created Windows worktree when git lists the same path with different separators', async () => {
|
||||
const worktreeId = makeWorktreeKey({
|
||||
hostId: 'local',
|
||||
repoId: 'repo-1',
|
||||
path: 'C:/workspaces/improve-dashboard'
|
||||
})
|
||||
listWorktreesMock.mockResolvedValue([
|
||||
{
|
||||
path: 'C:/workspaces/improve-dashboard',
|
||||
@@ -283,14 +291,14 @@ describe('registerWorktreeHandlers – Windows path handling', () => {
|
||||
false
|
||||
)
|
||||
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
|
||||
'repo-1::C:/workspaces/improve-dashboard',
|
||||
worktreeId,
|
||||
expect.objectContaining({
|
||||
lastActivityAt: expect.any(Number)
|
||||
})
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
worktree: expect.objectContaining({
|
||||
id: 'repo-1::C:/workspaces/improve-dashboard',
|
||||
id: worktreeId,
|
||||
path: 'C:/workspaces/improve-dashboard',
|
||||
branch: 'refs/heads/improve-dashboard'
|
||||
})
|
||||
@@ -298,6 +306,11 @@ describe('registerWorktreeHandlers – Windows path handling', () => {
|
||||
})
|
||||
|
||||
it('preserves create-time metadata on the next list when Windows path formatting differs', async () => {
|
||||
const worktreeId = makeWorktreeKey({
|
||||
hostId: 'local',
|
||||
repoId: 'repo-1',
|
||||
path: 'C:/workspaces/improve-dashboard'
|
||||
})
|
||||
const worktreeEntry = {
|
||||
path: 'C:/workspaces/improve-dashboard',
|
||||
head: 'abc123',
|
||||
@@ -314,8 +327,8 @@ describe('registerWorktreeHandlers – Windows path handling', () => {
|
||||
linkedIssue: 123,
|
||||
linkedPR: 456
|
||||
})
|
||||
store.getWorktreeMeta.mockImplementation((worktreeId: string) =>
|
||||
worktreeId === 'repo-1::C:/workspaces/improve-dashboard'
|
||||
store.getWorktreeMeta.mockImplementation((id: string) =>
|
||||
id === worktreeId
|
||||
? {
|
||||
lastActivityAt: 123,
|
||||
displayName: 'Improve Dashboard',
|
||||
@@ -337,7 +350,7 @@ describe('registerWorktreeHandlers – Windows path handling', () => {
|
||||
|
||||
expect(listed).toMatchObject([
|
||||
{
|
||||
id: 'repo-1::C:/workspaces/improve-dashboard',
|
||||
id: worktreeId,
|
||||
displayName: 'Improve Dashboard',
|
||||
linkedIssue: 123,
|
||||
linkedPR: 456,
|
||||
|
||||
@@ -712,12 +712,16 @@ const TEST_WINDOW_ID = 1
|
||||
const TEST_REPO_ID = 'repo-1'
|
||||
const TEST_REPO_PATH = '/tmp/repo'
|
||||
const TEST_WORKTREE_PATH = '/tmp/worktree-a'
|
||||
const TEST_WORKTREE_ID = `${TEST_REPO_ID}::${TEST_WORKTREE_PATH}`
|
||||
const TEST_CANONICAL_WORKTREE_ID = makeWorktreeKey({
|
||||
hostId: 'local',
|
||||
repoId: TEST_REPO_ID,
|
||||
path: TEST_WORKTREE_PATH
|
||||
})
|
||||
const TEST_WORKTREE_ID = TEST_CANONICAL_WORKTREE_ID
|
||||
const makeLocalTestWorktreeId = (path: string): string =>
|
||||
makeWorktreeKey({ hostId: 'local', repoId: TEST_REPO_ID, path })
|
||||
const makeSshTestWorktreeId = (repoId: string, connectionId: string, path: string): string =>
|
||||
makeWorktreeKey({ hostId: `ssh:${connectionId}`, repoId, path })
|
||||
const TEST_FOLDER_PROJECT_GROUP_ID = 'folder-project-group-1'
|
||||
const TEST_FOLDER_WORKSPACE_ID = 'folder-workspace-1'
|
||||
const TEST_FOLDER_WORKSPACE_KEY = `folder:${TEST_FOLDER_WORKSPACE_ID}`
|
||||
@@ -1350,7 +1354,7 @@ describe('OrcaRuntimeService', () => {
|
||||
tabs: [
|
||||
{
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'repo-1::/tmp/worktree-a',
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
title: 'Claude',
|
||||
activeLeafId: 'pane:1',
|
||||
layout: null
|
||||
@@ -1359,7 +1363,7 @@ describe('OrcaRuntimeService', () => {
|
||||
leaves: [
|
||||
{
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'repo-1::/tmp/worktree-a',
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
leafId: 'pane:1',
|
||||
paneRuntimeId: 1,
|
||||
ptyId: 'pty-1'
|
||||
@@ -1371,7 +1375,7 @@ describe('OrcaRuntimeService', () => {
|
||||
const terminals = await runtime.listTerminals('branch:feature/foo')
|
||||
expect(terminals.terminals).toHaveLength(1)
|
||||
expect(terminals.terminals[0]).toMatchObject({
|
||||
worktreeId: 'repo-1::/tmp/worktree-a',
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
branch: 'feature/foo',
|
||||
ptyId: 'pty-1',
|
||||
title: 'Claude',
|
||||
@@ -1603,7 +1607,8 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
|
||||
it('matches explicit-id cwd PTYs for folder workspace instance IDs', async () => {
|
||||
const folderWorktreeId = `${TEST_REPO_ID}::${TEST_FOLDER_WORKSPACE_PATH}${FOLDER_WORKSPACE_INSTANCE_SEPARATOR}11111111-1111-4111-8111-111111111111`
|
||||
const folderWorktreeRootId = makeLocalTestWorktreeId(TEST_FOLDER_WORKSPACE_PATH)
|
||||
const folderWorktreeId = `${folderWorktreeRootId}${FOLDER_WORKSPACE_INSTANCE_SEPARATOR}11111111-1111-4111-8111-111111111111`
|
||||
vi.mocked(listWorktrees).mockClear()
|
||||
vi.mocked(listWorktrees).mockRejectedValue(
|
||||
new Error('folder explicit-id fallback should not rescan worktrees')
|
||||
@@ -1621,7 +1626,9 @@ describe('OrcaRuntimeService', () => {
|
||||
const terminals = await runtime.listTerminals(`id:${folderWorktreeId}`)
|
||||
|
||||
expect(listWorktrees).not.toHaveBeenCalled()
|
||||
expect(terminals.terminals.map((terminal) => terminal.worktreeId)).toEqual([folderWorktreeId])
|
||||
expect(terminals.terminals.map((terminal) => terminal.worktreeId)).toEqual([
|
||||
folderWorktreeRootId
|
||||
])
|
||||
expect(terminals.terminals[0]?.worktreePath).toBe(TEST_FOLDER_WORKSPACE_PATH)
|
||||
})
|
||||
|
||||
@@ -1916,8 +1923,18 @@ describe('OrcaRuntimeService', () => {
|
||||
addedAt: 1,
|
||||
kind: 'folder' as const
|
||||
}
|
||||
const rootWorktreeId = 'folder-repo::/workspace/folder'
|
||||
const rootPriorWorktreeIds = ['folder-repo::/workspace/old-folder']
|
||||
const rootWorktreeId = makeWorktreeKey({
|
||||
hostId: 'local',
|
||||
repoId: 'folder-repo',
|
||||
path: '/workspace/folder'
|
||||
})
|
||||
const rootPriorWorktreeIds = [
|
||||
makeWorktreeKey({
|
||||
hostId: 'local',
|
||||
repoId: 'folder-repo',
|
||||
path: '/workspace/old-folder'
|
||||
})
|
||||
]
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[rootWorktreeId]: makeWorktreeMeta({
|
||||
instanceId: 'root-instance',
|
||||
@@ -1958,7 +1975,9 @@ describe('OrcaRuntimeService', () => {
|
||||
expect(addWorktreeMock).not.toHaveBeenCalled()
|
||||
expect(result.worktree).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.stringMatching(/^folder-repo::\/workspace\/folder::workspace:[0-9a-f-]{36}$/),
|
||||
id: expect.stringMatching(
|
||||
/^orca-worktree:\/\/v1\?hostId=local&repoId=folder-repo&path=%2Fworkspace%2Ffolder::workspace:[0-9a-f-]{36}$/
|
||||
),
|
||||
repoId: 'folder-repo',
|
||||
path: '/workspace/folder',
|
||||
displayName: 'folder-session',
|
||||
@@ -1998,9 +2017,9 @@ describe('OrcaRuntimeService', () => {
|
||||
id: result.worktree.id,
|
||||
comment: 'note'
|
||||
})
|
||||
await expect(
|
||||
runtime.removeManagedWorktree('id:folder-repo::/workspace/folder')
|
||||
).rejects.toThrow('Cannot delete the project root workspace')
|
||||
await expect(runtime.removeManagedWorktree(`id:${rootWorktreeId}`)).rejects.toThrow(
|
||||
'Cannot delete the project root workspace'
|
||||
)
|
||||
deletedWorktreeId = result.worktree.id
|
||||
await expect(runtime.removeManagedWorktree(`id:${result.worktree.id}`)).resolves.toEqual({})
|
||||
expect(localProvider.shutdown).toHaveBeenCalledWith(`${result.worktree.id}@@pty-1`, {
|
||||
@@ -2949,7 +2968,7 @@ describe('OrcaRuntimeService', () => {
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
const childId = `${TEST_REPO_ID}::${created.path}`
|
||||
const childId = makeLocalTestWorktreeId(created.path)
|
||||
const metaById: Record<string, WorktreeMeta> = {}
|
||||
const workspaceLineageByChildKey: Record<string, WorkspaceLineage> = {}
|
||||
const runtimeStore = {
|
||||
@@ -5037,11 +5056,11 @@ describe('OrcaRuntimeService', () => {
|
||||
expect(terminals.terminals).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
worktreeId: `${TEST_REPO_ID}::C:\\Repo`,
|
||||
worktreeId: makeLocalTestWorktreeId('C:\\Repo'),
|
||||
worktreePath: 'C:\\Repo'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
worktreeId: `${TEST_REPO_ID}:://Server/Share/Repo`,
|
||||
worktreeId: makeLocalTestWorktreeId('//Server/Share/Repo'),
|
||||
worktreePath: '//Server/Share/Repo'
|
||||
})
|
||||
])
|
||||
@@ -11774,6 +11793,7 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
|
||||
it('spawns fresh headless SSH mobile session terminals instead of reattaching synthetic local ids', async () => {
|
||||
const sshWorktreeId = makeSshTestWorktreeId(TEST_REPO_ID, 'ssh-1', TEST_WORKTREE_PATH)
|
||||
const remoteRepo = { ...store.getRepo(TEST_REPO_ID)!, connectionId: 'ssh-1' }
|
||||
const remoteStore = {
|
||||
...store,
|
||||
@@ -11790,12 +11810,12 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
|
||||
|
||||
await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`)
|
||||
await runtime.createMobileSessionTerminal(`id:${sshWorktreeId}`)
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
connectionId: 'ssh-1',
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
worktreeId: sshWorktreeId,
|
||||
persistHostSessionBinding: true
|
||||
})
|
||||
)
|
||||
@@ -13192,14 +13212,15 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
|
||||
it('reattaches hydrated SSH headless terminals with the persisted relay identity', async () => {
|
||||
const sshWorktreeId = makeSshTestWorktreeId(TEST_REPO_ID, 'ssh-1', TEST_WORKTREE_PATH)
|
||||
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
|
||||
makeWorkspaceSessionWithHeadlessTerminal({
|
||||
tabsByWorktree: {
|
||||
[TEST_WORKTREE_ID]: [
|
||||
[sshWorktreeId]: [
|
||||
{
|
||||
id: 'host-tab',
|
||||
ptyId: 'ssh:ssh-1@@relay-pty',
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
worktreeId: sshWorktreeId,
|
||||
title: 'Remote Terminal',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
@@ -13232,7 +13253,7 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
|
||||
|
||||
await runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab')
|
||||
await runtime.activateMobileSessionTab(`id:${sshWorktreeId}`, 'host-tab')
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -13246,15 +13267,16 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
|
||||
it('spawns fresh after an expired hydrated SSH headless reattach clears persistence', async () => {
|
||||
const sshWorktreeId = makeSshTestWorktreeId(TEST_REPO_ID, 'ssh-1', TEST_WORKTREE_PATH)
|
||||
const stalePtyId = 'ssh:ssh-1@@relay-pty'
|
||||
const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(
|
||||
makeWorkspaceSessionWithHeadlessTerminal({
|
||||
tabsByWorktree: {
|
||||
[TEST_WORKTREE_ID]: [
|
||||
[sshWorktreeId]: [
|
||||
{
|
||||
id: 'host-tab',
|
||||
ptyId: stalePtyId,
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
worktreeId: sshWorktreeId,
|
||||
title: 'Remote Terminal',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
@@ -13282,7 +13304,7 @@ describe('OrcaRuntimeService', () => {
|
||||
...session,
|
||||
tabsByWorktree: {
|
||||
...session.tabsByWorktree,
|
||||
[TEST_WORKTREE_ID]: session.tabsByWorktree[TEST_WORKTREE_ID].map((tab) =>
|
||||
[sshWorktreeId]: session.tabsByWorktree[sshWorktreeId].map((tab) =>
|
||||
tab.id === 'host-tab' ? { ...tab, ptyId: null } : tab
|
||||
)
|
||||
},
|
||||
@@ -13308,9 +13330,9 @@ describe('OrcaRuntimeService', () => {
|
||||
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
|
||||
|
||||
await expect(
|
||||
runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab')
|
||||
runtime.activateMobileSessionTab(`id:${sshWorktreeId}`, 'host-tab')
|
||||
).rejects.toThrow('SSH session expired')
|
||||
await runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab')
|
||||
await runtime.activateMobileSessionTab(`id:${sshWorktreeId}`, 'host-tab')
|
||||
|
||||
expect(spawn).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
@@ -13476,14 +13498,15 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
|
||||
it('spawns fresh SSH terminals when hydrated persistence has no relay identity', async () => {
|
||||
const sshWorktreeId = makeSshTestWorktreeId(TEST_REPO_ID, 'ssh-1', TEST_WORKTREE_PATH)
|
||||
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
|
||||
makeWorkspaceSessionWithHeadlessTerminal({
|
||||
tabsByWorktree: {
|
||||
[TEST_WORKTREE_ID]: [
|
||||
[sshWorktreeId]: [
|
||||
{
|
||||
id: 'host-tab',
|
||||
ptyId: null,
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
worktreeId: sshWorktreeId,
|
||||
title: 'Remote Terminal',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
@@ -13514,7 +13537,7 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
|
||||
|
||||
await runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab')
|
||||
await runtime.activateMobileSessionTab(`id:${sshWorktreeId}`, 'host-tab')
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -15871,8 +15894,16 @@ describe('OrcaRuntimeService', () => {
|
||||
addedAt: 1,
|
||||
connectionId: 'ssh-1'
|
||||
}
|
||||
const childId = `${remoteRepo.id}::/home/user/repo-child`
|
||||
const parentId = `${remoteRepo.id}::/home/user/repo-parent`
|
||||
const childId = makeSshTestWorktreeId(
|
||||
remoteRepo.id,
|
||||
remoteRepo.connectionId,
|
||||
'/home/user/repo-child'
|
||||
)
|
||||
const parentId = makeSshTestWorktreeId(
|
||||
remoteRepo.id,
|
||||
remoteRepo.connectionId,
|
||||
'/home/user/repo-parent'
|
||||
)
|
||||
const canonicalChildId = makeWorktreeKey({
|
||||
hostId: 'ssh:ssh-1',
|
||||
repoId: remoteRepo.id,
|
||||
@@ -15946,8 +15977,16 @@ describe('OrcaRuntimeService', () => {
|
||||
addedAt: 1,
|
||||
connectionId: 'ssh-1'
|
||||
}
|
||||
const childId = `${remoteRepo.id}::/home/user/repo-child`
|
||||
const parentId = `${remoteRepo.id}::/home/user/repo-parent`
|
||||
const childId = makeSshTestWorktreeId(
|
||||
remoteRepo.id,
|
||||
remoteRepo.connectionId,
|
||||
'/home/user/repo-child'
|
||||
)
|
||||
const parentId = makeSshTestWorktreeId(
|
||||
remoteRepo.id,
|
||||
remoteRepo.connectionId,
|
||||
'/home/user/repo-parent'
|
||||
)
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[childId]: makeWorktreeMeta({ instanceId: 'child-instance' }),
|
||||
[parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' })
|
||||
@@ -16185,8 +16224,8 @@ describe('OrcaRuntimeService', () => {
|
||||
it('keeps workspace lineage in sync when manually reparenting a worktree', async () => {
|
||||
const parentPath = '/tmp/worktree-parent'
|
||||
const childPath = '/tmp/worktree-child'
|
||||
const parentId = `${TEST_REPO_ID}::${parentPath}`
|
||||
const childId = `${TEST_REPO_ID}::${childPath}`
|
||||
const parentId = makeLocalTestWorktreeId(parentPath)
|
||||
const childId = makeLocalTestWorktreeId(childPath)
|
||||
const canonicalParentId = makeWorktreeKey({
|
||||
hostId: 'local',
|
||||
repoId: TEST_REPO_ID,
|
||||
@@ -16258,7 +16297,7 @@ describe('OrcaRuntimeService', () => {
|
||||
|
||||
it('clears workspace lineage when manually removing a parent', async () => {
|
||||
const childPath = '/tmp/worktree-child'
|
||||
const childId = `${TEST_REPO_ID}::${childPath}`
|
||||
const childId = makeLocalTestWorktreeId(childPath)
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[childId]: makeWorktreeMeta({ instanceId: 'child-instance' })
|
||||
}
|
||||
@@ -16323,8 +16362,8 @@ describe('OrcaRuntimeService', () => {
|
||||
it('ignores stale instance-mismatched lineage when validating manual cycle repairs', async () => {
|
||||
const parentPath = '/tmp/worktree-a'
|
||||
const childPath = '/tmp/worktree-b'
|
||||
const parentId = `${TEST_REPO_ID}::${parentPath}`
|
||||
const childId = `${TEST_REPO_ID}::${childPath}`
|
||||
const parentId = makeLocalTestWorktreeId(parentPath)
|
||||
const childId = makeLocalTestWorktreeId(childPath)
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[parentId]: makeWorktreeMeta({ instanceId: 'new-parent-instance' }),
|
||||
[childId]: makeWorktreeMeta({ instanceId: 'child-instance' })
|
||||
@@ -16388,8 +16427,8 @@ describe('OrcaRuntimeService', () => {
|
||||
it('rejects lineage updates when upgraded metadata is missing a parent instance id', async () => {
|
||||
const parentPath = '/tmp/worktree-parent'
|
||||
const childPath = '/tmp/worktree-child'
|
||||
const parentId = `${TEST_REPO_ID}::${parentPath}`
|
||||
const childId = `${TEST_REPO_ID}::${childPath}`
|
||||
const parentId = makeLocalTestWorktreeId(parentPath)
|
||||
const childId = makeLocalTestWorktreeId(childPath)
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[parentId]: makeWorktreeMeta(),
|
||||
[childId]: makeWorktreeMeta({ instanceId: 'child-instance' })
|
||||
@@ -16435,8 +16474,8 @@ describe('OrcaRuntimeService', () => {
|
||||
it('rotates a missing parent instance during runtime selector scans before same-path reuse', async () => {
|
||||
const parentPath = '/tmp/worktree-parent'
|
||||
const childPath = '/tmp/worktree-child'
|
||||
const parentId = `${TEST_REPO_ID}::${parentPath}`
|
||||
const childId = `${TEST_REPO_ID}::${childPath}`
|
||||
const parentId = makeLocalTestWorktreeId(parentPath)
|
||||
const childId = makeLocalTestWorktreeId(childPath)
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[parentId]: makeWorktreeMeta({ instanceId: 'old-parent-instance' }),
|
||||
[childId]: makeWorktreeMeta({ instanceId: 'child-instance' })
|
||||
@@ -16520,8 +16559,8 @@ describe('OrcaRuntimeService', () => {
|
||||
it('does not prune lineage when a runtime local worktree scan fails', async () => {
|
||||
const parentPath = '/tmp/worktree-parent'
|
||||
const childPath = '/tmp/worktree-child'
|
||||
const parentId = `${TEST_REPO_ID}::${parentPath}`
|
||||
const childId = `${TEST_REPO_ID}::${childPath}`
|
||||
const parentId = makeLocalTestWorktreeId(parentPath)
|
||||
const childId = makeLocalTestWorktreeId(childPath)
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' }),
|
||||
[childId]: makeWorktreeMeta({ instanceId: 'child-instance' })
|
||||
@@ -16591,8 +16630,16 @@ describe('OrcaRuntimeService', () => {
|
||||
addedAt: 1,
|
||||
connectionId: 'ssh-1'
|
||||
}
|
||||
const parentId = `${remoteRepo.id}::/home/user/repo-parent`
|
||||
const childId = `${remoteRepo.id}::/home/user/repo-child`
|
||||
const parentId = makeSshTestWorktreeId(
|
||||
remoteRepo.id,
|
||||
remoteRepo.connectionId,
|
||||
'/home/user/repo-parent'
|
||||
)
|
||||
const childId = makeSshTestWorktreeId(
|
||||
remoteRepo.id,
|
||||
remoteRepo.connectionId,
|
||||
'/home/user/repo-child'
|
||||
)
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' }),
|
||||
[childId]: makeWorktreeMeta({ instanceId: 'child-instance' })
|
||||
@@ -16735,8 +16782,8 @@ describe('OrcaRuntimeService', () => {
|
||||
it('keeps valid orchestration lineage when caller terminal context is stale', async () => {
|
||||
const parentPath = '/tmp/worktree-parent'
|
||||
const childPath = '/tmp/workspaces/worker-child'
|
||||
const parentId = `${TEST_REPO_ID}::${parentPath}`
|
||||
const childId = `${TEST_REPO_ID}::${childPath}`
|
||||
const parentId = makeLocalTestWorktreeId(parentPath)
|
||||
const childId = makeLocalTestWorktreeId(childPath)
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[parentId]: makeWorktreeMeta({
|
||||
instanceId: 'parent-instance',
|
||||
@@ -16813,8 +16860,8 @@ describe('OrcaRuntimeService', () => {
|
||||
it('enriches caller-terminal lineage with active orchestration dispatch context', async () => {
|
||||
const workerPath = '/tmp/worktree-worker'
|
||||
const childPath = '/tmp/workspaces/worker-child'
|
||||
const childId = `${TEST_REPO_ID}::${childPath}`
|
||||
const workerId = `${TEST_REPO_ID}::${workerPath}`
|
||||
const childId = makeLocalTestWorktreeId(childPath)
|
||||
const workerId = makeLocalTestWorktreeId(workerPath)
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[TEST_WORKTREE_ID]: makeWorktreeMeta({
|
||||
instanceId: 'parent-instance',
|
||||
@@ -17205,8 +17252,8 @@ describe('OrcaRuntimeService', () => {
|
||||
it('falls back to cwd lineage when the caller terminal handle is stale', async () => {
|
||||
const parentPath = '/tmp/worktree-parent'
|
||||
const childPath = '/tmp/workspaces/cwd-child'
|
||||
const parentId = `${TEST_REPO_ID}::${parentPath}`
|
||||
const childId = `${TEST_REPO_ID}::${childPath}`
|
||||
const parentId = makeLocalTestWorktreeId(parentPath)
|
||||
const childId = makeLocalTestWorktreeId(childPath)
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[parentId]: makeWorktreeMeta({ instanceId: 'parent-instance' })
|
||||
}
|
||||
@@ -17312,8 +17359,8 @@ describe('OrcaRuntimeService', () => {
|
||||
it('infers orchestration lineage from task-id comments when dispatch is completed', async () => {
|
||||
const workerPath = '/tmp/worktree-worker'
|
||||
const childPath = '/tmp/workspaces/worker-child'
|
||||
const childId = `${TEST_REPO_ID}::${childPath}`
|
||||
const workerId = `${TEST_REPO_ID}::${workerPath}`
|
||||
const childId = makeLocalTestWorktreeId(childPath)
|
||||
const workerId = makeLocalTestWorktreeId(workerPath)
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[workerId]: makeWorktreeMeta({
|
||||
instanceId: 'worker-instance',
|
||||
@@ -17410,8 +17457,8 @@ describe('OrcaRuntimeService', () => {
|
||||
it('infers orchestration lineage from task creator when no dispatch context exists', async () => {
|
||||
const parentPath = '/tmp/worktree-parent'
|
||||
const childPath = '/tmp/workspaces/parent-child'
|
||||
const childId = `${TEST_REPO_ID}::${childPath}`
|
||||
const parentId = `${TEST_REPO_ID}::${parentPath}`
|
||||
const childId = makeLocalTestWorktreeId(childPath)
|
||||
const parentId = makeLocalTestWorktreeId(parentPath)
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[parentId]: makeWorktreeMeta({
|
||||
instanceId: 'parent-instance',
|
||||
@@ -19991,7 +20038,7 @@ describe('OrcaRuntimeService', () => {
|
||||
try {
|
||||
const worktree = await runtime.showManagedWorktree(`path:${duplicatePath}`)
|
||||
|
||||
expect(worktree.id).toBe(`${TEST_REPO_ID}::${duplicatePath}`)
|
||||
expect(worktree.id).toBe(makeLocalTestWorktreeId(duplicatePath))
|
||||
expect(worktree.path).toBe(duplicatePath)
|
||||
} finally {
|
||||
getRepos.mockRestore()
|
||||
@@ -20690,7 +20737,7 @@ describe('OrcaRuntimeService', () => {
|
||||
addedAt: 1,
|
||||
connectionId: 'ssh-missing-fs'
|
||||
}
|
||||
const worktreeId = `${repo.id}::${localPath}`
|
||||
const worktreeId = makeSshTestWorktreeId(repo.id, repo.connectionId, localPath)
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[worktreeId]: makeWorktreeMeta({
|
||||
orcaCreatedAt: Date.now(),
|
||||
@@ -21270,7 +21317,7 @@ describe('OrcaRuntimeService', () => {
|
||||
|
||||
expect(listed.worktrees).toMatchObject([
|
||||
{
|
||||
id: 'repo-1::C:/workspaces/improve-dashboard',
|
||||
id: makeLocalTestWorktreeId('C:/workspaces/improve-dashboard'),
|
||||
displayName: 'Improve Dashboard'
|
||||
}
|
||||
])
|
||||
|
||||
@@ -16685,10 +16685,13 @@ export class OrcaRuntimeService {
|
||||
store.removeWorktreeLineage(childId)
|
||||
store.removeWorkspaceLineage?.(worktreeWorkspaceKey(childId))
|
||||
}
|
||||
if (
|
||||
const parentMissingFromLegacyScan =
|
||||
lineage.parentWorktreeId.startsWith(repoPrefix) &&
|
||||
!legacyLiveIds.has(lineage.parentWorktreeId)
|
||||
) {
|
||||
const parentMissingFromCanonicalScan =
|
||||
isRuntimeCanonicalWorktreeIdForRepoHost(repo, lineage.parentWorktreeId) &&
|
||||
!liveIds.has(lineage.parentWorktreeId)
|
||||
if (parentMissingFromLegacyScan || parentMissingFromCanonicalScan) {
|
||||
const parentMeta = store.getWorktreeMeta(lineage.parentWorktreeId)
|
||||
if (!parentMeta || parentMeta.instanceId === lineage.parentWorktreeInstanceId) {
|
||||
// Why: preserving child lineage powers the repair UI, but a missing
|
||||
|
||||
@@ -15,6 +15,7 @@ import { createRuntimeTransportMetadata, OrcaRuntimeRpcServer } from './runtime-
|
||||
import { parsePairingCode } from '../../shared/pairing'
|
||||
import { decrypt, deriveSharedKey, encrypt, generateKeyPair } from './rpc/e2ee-crypto'
|
||||
import { DeviceRegistry } from './device-registry'
|
||||
import { makeWorktreeKey } from '../../shared/worktree-id'
|
||||
|
||||
vi.mock('../git/worktree', () => ({
|
||||
listWorktrees: vi.fn().mockResolvedValue([
|
||||
@@ -259,6 +260,25 @@ class FakeWebSocket extends EventEmitter {
|
||||
}
|
||||
|
||||
describe('OrcaRuntimeRpcServer', () => {
|
||||
const LEGACY_WORKTREE_A_ID = 'repo-1::/tmp/worktree-a'
|
||||
const WORKTREE_A_ID = makeWorktreeKey({
|
||||
hostId: 'local',
|
||||
repoId: 'repo-1',
|
||||
path: '/tmp/worktree-a'
|
||||
})
|
||||
const makeWorktreeMeta = (overrides?: { isUnread?: boolean }) => ({
|
||||
displayName: 'foo',
|
||||
comment: '',
|
||||
linkedIssue: 123,
|
||||
linkedPR: null,
|
||||
linkedLinearIssue: null,
|
||||
isArchived: false,
|
||||
isUnread: overrides?.isUnread ?? false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0
|
||||
})
|
||||
|
||||
const makeStore = (overrides?: { isUnread?: boolean }) => ({
|
||||
getRepo: (id: string) =>
|
||||
makeStore(overrides)
|
||||
@@ -279,27 +299,20 @@ describe('OrcaRuntimeRpcServer', () => {
|
||||
...makeStore(overrides).getRepo(id),
|
||||
...updates
|
||||
}) as never,
|
||||
getAllWorktreeMeta: () => ({
|
||||
'repo-1::/tmp/worktree-a': {
|
||||
displayName: 'foo',
|
||||
comment: '',
|
||||
linkedIssue: 123,
|
||||
linkedPR: null,
|
||||
linkedLinearIssue: null,
|
||||
isArchived: false,
|
||||
isUnread: overrides?.isUnread ?? false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0
|
||||
getAllWorktreeMeta: () => {
|
||||
const meta = makeWorktreeMeta(overrides)
|
||||
return {
|
||||
[LEGACY_WORKTREE_A_ID]: meta,
|
||||
[WORKTREE_A_ID]: meta
|
||||
}
|
||||
}),
|
||||
},
|
||||
getWorktreeMeta: (worktreeId: string) =>
|
||||
worktreeId === 'repo-1::/tmp/worktree-a'
|
||||
worktreeId === LEGACY_WORKTREE_A_ID || worktreeId === WORKTREE_A_ID
|
||||
? (makeStore(overrides).getAllWorktreeMeta()[worktreeId] as never)
|
||||
: undefined,
|
||||
setWorktreeMeta: (_worktreeId: string, meta: Record<string, unknown>) =>
|
||||
({
|
||||
...makeStore(overrides).getAllWorktreeMeta()['repo-1::/tmp/worktree-a'],
|
||||
...makeStore(overrides).getAllWorktreeMeta()[WORKTREE_A_ID],
|
||||
...meta
|
||||
}) as never,
|
||||
removeWorktreeMeta: () => {},
|
||||
@@ -2240,7 +2253,7 @@ describe('OrcaRuntimeRpcServer', () => {
|
||||
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-'))
|
||||
const runtime = new OrcaRuntimeService(makeStore() as never)
|
||||
const server = new OrcaRuntimeRpcServer({ runtime, userDataPath })
|
||||
const worktreeId = 'repo-1::/tmp/worktree-a'
|
||||
const worktreeId = WORKTREE_A_ID
|
||||
const leftLeaf = '11111111-1111-4111-8111-111111111111'
|
||||
const topLeaf = '22222222-2222-4222-8222-222222222222'
|
||||
const bottomLeaf = '33333333-3333-4333-8333-333333333333'
|
||||
@@ -2507,7 +2520,7 @@ describe('OrcaRuntimeRpcServer', () => {
|
||||
ok: true,
|
||||
result: {
|
||||
terminal: {
|
||||
worktreeId: 'repo-1::/tmp/worktree-a',
|
||||
worktreeId: WORKTREE_A_ID,
|
||||
surface: 'background'
|
||||
}
|
||||
}
|
||||
@@ -2520,7 +2533,7 @@ describe('OrcaRuntimeRpcServer', () => {
|
||||
authToken,
|
||||
method: 'session.tabs.list',
|
||||
params: {
|
||||
worktree: 'id:repo-1::/tmp/worktree-a'
|
||||
worktree: `id:${WORKTREE_A_ID}`
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2609,7 +2622,7 @@ describe('OrcaRuntimeRpcServer', () => {
|
||||
const metadata = readRuntimeMetadata(userDataPath)
|
||||
const laptopEndpoint = metadata!.transports[0]!.endpoint
|
||||
const laptopAuthToken = metadata!.authToken
|
||||
const worktree = 'id:repo-1::/tmp/worktree-a'
|
||||
const worktree = `id:${WORKTREE_A_ID}`
|
||||
const leafId = '11111111-1111-4111-8111-111111111111'
|
||||
|
||||
try {
|
||||
@@ -2757,7 +2770,7 @@ describe('OrcaRuntimeRpcServer', () => {
|
||||
result: {
|
||||
worktrees: [
|
||||
{
|
||||
worktreeId: 'repo-1::/tmp/worktree-a',
|
||||
worktreeId: WORKTREE_A_ID,
|
||||
repoId: 'repo-1',
|
||||
repo: 'repo',
|
||||
path: '/tmp/worktree-a',
|
||||
@@ -2903,7 +2916,7 @@ describe('OrcaRuntimeRpcServer', () => {
|
||||
tabs: [
|
||||
{
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'repo-1::/tmp/worktree-a',
|
||||
worktreeId: WORKTREE_A_ID,
|
||||
title: 'Terminal 1',
|
||||
activeLeafId: 'pane:1',
|
||||
layout: null
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { makeWorktreeKey } from '../shared/worktree-id'
|
||||
import { loadKnownUsageWorktreesByRepo } from './usage-worktree-metadata'
|
||||
|
||||
describe('loadKnownUsageWorktreesByRepo', () => {
|
||||
@@ -36,7 +37,11 @@ describe('loadKnownUsageWorktreesByRepo', () => {
|
||||
'repo-1',
|
||||
[
|
||||
{
|
||||
worktreeId: 'repo-1::/workspace/repo-a',
|
||||
worktreeId: makeWorktreeKey({
|
||||
hostId: 'local',
|
||||
repoId: 'repo-1',
|
||||
path: '/workspace/repo-a'
|
||||
}),
|
||||
path: '/workspace/repo-a',
|
||||
displayName: 'Repo A'
|
||||
},
|
||||
|
||||
@@ -28,13 +28,13 @@ describe('createWorktreeChangeRefreshQueue', () => {
|
||||
queue.enqueue({ repoId: 'repo-1' })
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
expect(handler).toHaveBeenCalledWith('repo-1', undefined)
|
||||
expect(handler).toHaveBeenCalledWith('repo-1', undefined, undefined)
|
||||
|
||||
firstRefresh.resolve()
|
||||
await flushPromises()
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(2)
|
||||
expect(handler).toHaveBeenNthCalledWith(2, 'repo-1', undefined)
|
||||
expect(handler).toHaveBeenNthCalledWith(2, 'repo-1', undefined, undefined)
|
||||
})
|
||||
|
||||
it('does not overlap refreshes for the same repo', async () => {
|
||||
@@ -73,8 +73,8 @@ describe('createWorktreeChangeRefreshQueue', () => {
|
||||
queue.enqueue({ repoId: 'repo-2' })
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(2)
|
||||
expect(handler).toHaveBeenNthCalledWith(1, 'repo-1', undefined)
|
||||
expect(handler).toHaveBeenNthCalledWith(2, 'repo-2', undefined)
|
||||
expect(handler).toHaveBeenNthCalledWith(1, 'repo-1', undefined, undefined)
|
||||
expect(handler).toHaveBeenNthCalledWith(2, 'repo-2', undefined, undefined)
|
||||
|
||||
repoOneRefresh.resolve()
|
||||
await flushPromises()
|
||||
@@ -100,8 +100,8 @@ describe('createWorktreeChangeRefreshQueue', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(3)
|
||||
expect(handler).toHaveBeenNthCalledWith(2, 'repo-1', renamed)
|
||||
expect(handler).toHaveBeenNthCalledWith(3, 'repo-1', undefined)
|
||||
expect(handler).toHaveBeenNthCalledWith(2, 'repo-1', undefined, renamed)
|
||||
expect(handler).toHaveBeenNthCalledWith(3, 'repo-1', undefined, undefined)
|
||||
} finally {
|
||||
consoleError.mockRestore()
|
||||
}
|
||||
@@ -116,8 +116,8 @@ describe('createWorktreeChangeRefreshQueue', () => {
|
||||
queue.enqueue({ repoId: 'repo-1', renamed })
|
||||
await flushPromises()
|
||||
|
||||
expect(handler).toHaveBeenNthCalledWith(1, 'repo-1', undefined)
|
||||
expect(handler).toHaveBeenNthCalledWith(2, 'repo-1', renamed)
|
||||
expect(handler).toHaveBeenNthCalledWith(1, 'repo-1', undefined, undefined)
|
||||
expect(handler).toHaveBeenNthCalledWith(2, 'repo-1', undefined, renamed)
|
||||
})
|
||||
|
||||
it('keeps a plain refresh queued after a rename', async () => {
|
||||
@@ -136,8 +136,8 @@ describe('createWorktreeChangeRefreshQueue', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(3)
|
||||
expect(handler).toHaveBeenNthCalledWith(2, 'repo-1', renamed)
|
||||
expect(handler).toHaveBeenNthCalledWith(3, 'repo-1', undefined)
|
||||
expect(handler).toHaveBeenNthCalledWith(2, 'repo-1', undefined, renamed)
|
||||
expect(handler).toHaveBeenNthCalledWith(3, 'repo-1', undefined, undefined)
|
||||
})
|
||||
|
||||
it('drops queued trailing refreshes after disposal', async () => {
|
||||
|
||||
@@ -5036,7 +5036,35 @@ describe('migrateWorktreeIdentity', () => {
|
||||
return [{ id: 'group1', worktreeId }]
|
||||
case 'recentlyClosedEditorTabsByWorktree':
|
||||
return [{ id: 'file1', worktreeId }]
|
||||
default:
|
||||
case 'deleteStateByWorktreeId':
|
||||
case 'baseStatusByWorktreeId':
|
||||
case 'remoteBranchConflictByWorktreeId':
|
||||
case 'fileSearchStateByWorktree':
|
||||
case 'activeBrowserTabIdByWorktree':
|
||||
case 'activeFileIdByWorktree':
|
||||
case 'activeTabTypeByWorktree':
|
||||
case 'activeTabIdByWorktree':
|
||||
case 'tabBarOrderByWorktree':
|
||||
case 'pendingReconnectTabByWorktree':
|
||||
case 'rightSidebarTabByWorktree':
|
||||
case 'rightSidebarExplorerViewByWorktree':
|
||||
case 'layoutByWorktree':
|
||||
case 'activeGroupIdByWorktree':
|
||||
case 'gitStatusByWorktree':
|
||||
case 'gitStatusHeadByWorktree':
|
||||
case 'gitStatusHugeByWorktree':
|
||||
case 'gitIgnoredPathsByWorktree':
|
||||
case 'gitConflictOperationByWorktree':
|
||||
case 'trackedConflictPathsByWorktree':
|
||||
case 'gitBranchChangesByWorktree':
|
||||
case 'gitBranchCompareSummaryByWorktree':
|
||||
case 'gitBranchCompareRequestKeyByWorktree':
|
||||
case 'gitBranchCompareRequestStatusHeadByWorktree':
|
||||
case 'remoteStatusesByWorktree':
|
||||
case 'showDotfilesByWorktree':
|
||||
case 'expandedDirs':
|
||||
case 'lastVisitedAtByWorktreeId':
|
||||
case 'defaultTerminalTabsAppliedByWorktreeId':
|
||||
return { marker: key }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user