mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 00:02:56 +00:00
* fix(worktrees): preserve user workspace names across branch changes * test(worktrees): cover pinned rename metadata * fix(workspaces): address display-name review edge cases * fix(workspaces): keep automatic names fresh across refreshes * fix(workspaces): preserve legacy CLI labels * fix(workspaces): preserve display-name provenance across hosts * fix(workspaces): honor legacy display-name provenance * fix(workspaces): fence display-name refresh races * fix(workspaces): accept peer renames from provenance-less hosts The old-host preserve fence kept a pinned local label on every refresh, which also suppressed a legitimate rename another client persisted through the same host until app restart. Narrow it to labels the host re-derived itself (branch short name, or path basename when detached); any other changed label in a mode-less response is explicit meta a peer wrote there. Stale prior-label responses stay covered by the downstream staleness fence, in-flight writes by the pending fence. * refactor(workspaces): unify display-name pin derivation Three call sites (renderer optimistic update, local IPC updateMeta handler, remote worktree.set handler) each restated the same formula; a future edit to one would silently skew provenance between paths.
590 lines
17 KiB
TypeScript
590 lines
17 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
const {
|
|
callMock,
|
|
runtimeClientConstructorMock,
|
|
serveOrcaAppMock,
|
|
getDefaultUserDataPathMock,
|
|
addEnvironmentFromPairingCodeMock,
|
|
listEnvironmentsMock,
|
|
spawnMock
|
|
} = vi.hoisted(() => ({
|
|
callMock: vi.fn(),
|
|
runtimeClientConstructorMock: vi.fn(),
|
|
serveOrcaAppMock: vi.fn(),
|
|
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
|
|
addEnvironmentFromPairingCodeMock: vi.fn(),
|
|
listEnvironmentsMock: vi.fn(),
|
|
spawnMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('./runtime-client', async () => {
|
|
const { createRuntimeClientModuleMock } = await import('./index-test-harness.js')
|
|
return createRuntimeClientModuleMock({
|
|
callMock,
|
|
runtimeClientConstructorMock,
|
|
serveOrcaAppMock,
|
|
getDefaultUserDataPathMock
|
|
})
|
|
})
|
|
|
|
vi.mock('./runtime/environments', () => ({
|
|
addEnvironmentFromPairingCode: addEnvironmentFromPairingCodeMock,
|
|
listEnvironments: listEnvironmentsMock,
|
|
removeEnvironment: vi.fn(),
|
|
resolveEnvironment: vi.fn()
|
|
}))
|
|
|
|
vi.mock('child_process', async () => {
|
|
const { createChildProcessModuleMock } = await import('./index-test-harness.js')
|
|
return createChildProcessModuleMock(spawnMock)
|
|
})
|
|
|
|
import { main } from './index'
|
|
import { RuntimeRpcFailureError } from './runtime-client'
|
|
import { buildWorktree, okFixture, queueFixtures, worktreeListFixture } from './test-fixtures'
|
|
import { useWorktreeAwarenessEnvironment } from './index-test-harness'
|
|
|
|
describe('orca cli worktree awareness', () => {
|
|
useWorktreeAwarenessEnvironment({
|
|
callMock,
|
|
serveOrcaAppMock,
|
|
getDefaultUserDataPathMock,
|
|
addEnvironmentFromPairingCodeMock,
|
|
listEnvironmentsMock,
|
|
spawnMock
|
|
})
|
|
|
|
it('passes an explicit parent through worktree.create without cwd inference', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_create', {
|
|
worktree: {
|
|
...buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
|
|
parentWorktreeId: 'repo-1::/tmp/repo/parent',
|
|
lineage: {
|
|
worktreeId: 'repo-1::/tmp/repo/child',
|
|
worktreeInstanceId: 'child-instance',
|
|
parentWorktreeId: 'repo-1::/tmp/repo/parent',
|
|
parentWorktreeInstanceId: 'parent-instance',
|
|
origin: 'cli',
|
|
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
|
|
createdAt: 1
|
|
}
|
|
},
|
|
lineage: {
|
|
worktreeId: 'repo-1::/tmp/repo/child',
|
|
worktreeInstanceId: 'child-instance',
|
|
parentWorktreeId: 'repo-1::/tmp/repo/parent',
|
|
parentWorktreeInstanceId: 'parent-instance',
|
|
origin: 'cli',
|
|
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
|
|
createdAt: 1
|
|
},
|
|
warnings: []
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'create',
|
|
'--repo',
|
|
'id:repo-1',
|
|
'--name',
|
|
'child',
|
|
'--parent-worktree',
|
|
'id:repo-1::/tmp/repo/parent',
|
|
'--json'
|
|
],
|
|
'/tmp/repo/parent/src'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledTimes(1)
|
|
expect(callMock).toHaveBeenCalledWith('worktree.create', {
|
|
repo: 'id:repo-1',
|
|
name: 'child',
|
|
displayName: 'child',
|
|
displayNameKind: 'user',
|
|
baseBranch: undefined,
|
|
linkedIssue: undefined,
|
|
comment: undefined,
|
|
runHooks: false,
|
|
activate: false,
|
|
parentWorktree: 'id:repo-1::/tmp/repo/parent',
|
|
noParent: false,
|
|
callerTerminalHandle: undefined,
|
|
cliProvenanceRequest: {}
|
|
})
|
|
})
|
|
|
|
it('routes traditional parent-worktree selectors through parentWorktree', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_create', {
|
|
worktree: {
|
|
...buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
|
|
parentWorktreeId: 'repo-1::/tmp/repo/parent'
|
|
},
|
|
lineage: null,
|
|
warnings: []
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'create',
|
|
'--repo',
|
|
'id:repo-1',
|
|
'--name',
|
|
'child',
|
|
'--parent-worktree',
|
|
'branch:feature/parent',
|
|
'--json'
|
|
],
|
|
'/tmp/repo/parent/src'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledTimes(1)
|
|
expect(callMock).toHaveBeenCalledWith('worktree.create', {
|
|
repo: 'id:repo-1',
|
|
name: 'child',
|
|
displayName: 'child',
|
|
displayNameKind: 'user',
|
|
baseBranch: undefined,
|
|
linkedIssue: undefined,
|
|
comment: undefined,
|
|
runHooks: false,
|
|
activate: false,
|
|
parentWorktree: 'branch:feature/parent',
|
|
noParent: false,
|
|
callerTerminalHandle: undefined,
|
|
cliProvenanceRequest: {}
|
|
})
|
|
})
|
|
|
|
it('routes workspace-key parent-worktree selectors through parentWorkspace', async () => {
|
|
const cases = [
|
|
{ selector: 'folder:folder-1', parentWorkspace: 'folder:folder-1' },
|
|
{
|
|
selector: 'worktree:repo-1::/tmp/repo/parent',
|
|
parentWorkspace: 'worktree:repo-1::/tmp/repo/parent'
|
|
},
|
|
{ selector: 'id:folder:folder-1', parentWorkspace: 'folder:folder-1' },
|
|
{
|
|
selector: 'id:worktree:repo-1::/tmp/repo/parent',
|
|
parentWorkspace: 'worktree:repo-1::/tmp/repo/parent'
|
|
}
|
|
]
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
for (const testCase of cases) {
|
|
callMock.mockReset()
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_create', {
|
|
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
|
|
lineage: null,
|
|
workspaceLineage: {
|
|
childWorkspaceKey: 'worktree:repo-1::/tmp/repo/child',
|
|
childInstanceId: 'child-instance',
|
|
parentWorkspaceKey: testCase.parentWorkspace,
|
|
parentInstanceId: null,
|
|
origin: 'cli',
|
|
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
|
|
createdAt: 1
|
|
},
|
|
warnings: []
|
|
})
|
|
)
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'create',
|
|
'--repo',
|
|
'id:repo-1',
|
|
'--name',
|
|
'child',
|
|
'--parent-worktree',
|
|
testCase.selector,
|
|
'--json'
|
|
],
|
|
'/tmp/repo/parent/src'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledTimes(1)
|
|
expect(callMock).toHaveBeenCalledWith('worktree.create', {
|
|
repo: 'id:repo-1',
|
|
name: 'child',
|
|
displayName: 'child',
|
|
displayNameKind: 'user',
|
|
baseBranch: undefined,
|
|
linkedIssue: undefined,
|
|
comment: undefined,
|
|
runHooks: false,
|
|
activate: false,
|
|
parentWorktree: undefined,
|
|
parentWorkspace: testCase.parentWorkspace,
|
|
noParent: false,
|
|
callerTerminalHandle: undefined,
|
|
cliProvenanceRequest: {}
|
|
})
|
|
}
|
|
})
|
|
|
|
it('passes folder workspace environment lineage through worktree.create', async () => {
|
|
process.env.ORCA_WORKSPACE_ID = 'folder:folder-1'
|
|
queueFixtures(
|
|
callMock,
|
|
worktreeListFixture([buildWorktree('/tmp/repo', 'main', 'abc', 'repo-1')]),
|
|
okFixture('req_create', {
|
|
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
|
|
lineage: null,
|
|
workspaceLineage: {
|
|
childWorkspaceKey: 'worktree:repo-1::/tmp/repo/child',
|
|
childInstanceId: 'child-instance',
|
|
parentWorkspaceKey: 'folder:folder-1',
|
|
parentInstanceId: null,
|
|
origin: 'cli',
|
|
capture: { source: 'env-workspace', confidence: 'inferred' },
|
|
createdAt: 1
|
|
},
|
|
warnings: []
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
['worktree', 'create', '--repo', 'id:repo-1', '--name', 'child', '--json'],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
|
|
repo: 'id:repo-1',
|
|
name: 'child',
|
|
displayName: 'child',
|
|
displayNameKind: 'user',
|
|
baseBranch: undefined,
|
|
linkedIssue: undefined,
|
|
comment: undefined,
|
|
runHooks: false,
|
|
activate: false,
|
|
parentWorktree: undefined,
|
|
envParentWorkspace: 'folder:folder-1',
|
|
cwdParentWorktree: 'id:repo-1::/tmp/repo',
|
|
noParent: false,
|
|
callerTerminalHandle: undefined,
|
|
cliProvenanceRequest: {}
|
|
})
|
|
})
|
|
|
|
it('resolves current for explicit parent-worktree on create', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
worktreeListFixture([buildWorktree('/tmp/repo/parent', 'feature/parent', 'abc', 'repo-1')]),
|
|
okFixture('req_create', {
|
|
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
|
|
lineage: null,
|
|
warnings: []
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'create',
|
|
'--repo',
|
|
'id:repo-1',
|
|
'--name',
|
|
'child',
|
|
'--parent-worktree',
|
|
'current',
|
|
'--json'
|
|
],
|
|
'/tmp/repo/parent/src'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
|
|
repo: 'id:repo-1',
|
|
name: 'child',
|
|
displayName: 'child',
|
|
displayNameKind: 'user',
|
|
baseBranch: undefined,
|
|
linkedIssue: undefined,
|
|
comment: undefined,
|
|
runHooks: false,
|
|
activate: false,
|
|
parentWorktree: 'id:repo-1::/tmp/repo/parent',
|
|
noParent: false,
|
|
callerTerminalHandle: undefined,
|
|
cliProvenanceRequest: {}
|
|
})
|
|
})
|
|
|
|
it('routes active/current folder workspace parent selectors through parentWorkspace on create', async () => {
|
|
const folderWorkspace = {
|
|
...buildWorktree('/tmp/folder', '', '', 'folder-workspace:group-1'),
|
|
id: 'folder:folder-1',
|
|
repoId: 'folder-workspace:group-1',
|
|
displayName: 'Folder'
|
|
}
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
for (const parentSelector of ['current', 'active']) {
|
|
callMock.mockReset()
|
|
queueFixtures(
|
|
callMock,
|
|
worktreeListFixture([folderWorkspace]),
|
|
okFixture('req_create', {
|
|
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
|
|
lineage: null,
|
|
workspaceLineage: {
|
|
childWorkspaceKey: 'worktree:repo-1::/tmp/repo/child',
|
|
childInstanceId: 'child-instance',
|
|
parentWorkspaceKey: 'folder:folder-1',
|
|
parentInstanceId: null,
|
|
origin: 'cli',
|
|
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
|
|
createdAt: 1
|
|
},
|
|
warnings: []
|
|
})
|
|
)
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'create',
|
|
'--repo',
|
|
'id:repo-1',
|
|
'--name',
|
|
'child',
|
|
'--parent-worktree',
|
|
parentSelector,
|
|
'--json'
|
|
],
|
|
'/tmp/folder/src'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
|
|
repo: 'id:repo-1',
|
|
name: 'child',
|
|
displayName: 'child',
|
|
displayNameKind: 'user',
|
|
baseBranch: undefined,
|
|
linkedIssue: undefined,
|
|
comment: undefined,
|
|
runHooks: false,
|
|
activate: false,
|
|
parentWorktree: undefined,
|
|
parentWorkspace: 'folder:folder-1',
|
|
noParent: false,
|
|
callerTerminalHandle: undefined,
|
|
cliProvenanceRequest: {}
|
|
})
|
|
}
|
|
})
|
|
|
|
it('rejects contradictory parent flags on worktree.create before resolving selectors', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'create',
|
|
'--repo',
|
|
'id:repo-1',
|
|
'--name',
|
|
'child',
|
|
'--parent-worktree',
|
|
'current',
|
|
'--no-parent',
|
|
'--json'
|
|
],
|
|
'/tmp/not-managed'
|
|
)
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
|
|
'Choose either one parent selector or --no-parent.'
|
|
)
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
it('rejects removed parent-workspace on worktree.create', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
const outputModes = [[], ['--json']]
|
|
|
|
for (const outputArgs of outputModes) {
|
|
logSpy.mockClear()
|
|
errSpy.mockClear()
|
|
process.exitCode = priorExitCode
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'create',
|
|
'--repo',
|
|
'id:repo-1',
|
|
'--name',
|
|
'child',
|
|
'--parent-workspace',
|
|
'folder:folder-1',
|
|
...outputArgs
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
const output = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
|
|
expect(output).toContain('Unknown flag --parent-workspace for command: worktree create')
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect(process.exitCode).toBe(1)
|
|
}
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
it('rejects bare parent-worktree on worktree.create', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'create',
|
|
'--repo',
|
|
'id:repo-1',
|
|
'--name',
|
|
'child',
|
|
'--parent-worktree',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
|
|
'Missing required --parent-worktree'
|
|
)
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
it('reports runtime parent selector failures without hidden flag guidance', async () => {
|
|
callMock.mockRejectedValueOnce(
|
|
new RuntimeRpcFailureError({
|
|
id: 'req_create',
|
|
ok: false,
|
|
error: {
|
|
code: 'LINEAGE_PARENT_NOT_FOUND',
|
|
message: 'Parent selector was not found.',
|
|
data: {
|
|
nextSteps: [
|
|
'Pass a valid --parent-worktree selector such as folder:<id>, worktree:<worktreeId>, id:<repo-id>::<path>, branch:<branch>, issue:<number>, path:<absolute-path>, or active/current.',
|
|
'Retry with --no-parent to create without lineage.'
|
|
]
|
|
}
|
|
},
|
|
_meta: { runtimeId: 'runtime-1' }
|
|
})
|
|
)
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'create',
|
|
'--repo',
|
|
'id:repo-1',
|
|
'--name',
|
|
'child',
|
|
'--parent-worktree',
|
|
'folder:missing',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
const output = String(logSpy.mock.calls[0][0])
|
|
expect(callMock).toHaveBeenCalledWith('worktree.create', {
|
|
repo: 'id:repo-1',
|
|
name: 'child',
|
|
displayName: 'child',
|
|
displayNameKind: 'user',
|
|
baseBranch: undefined,
|
|
linkedIssue: undefined,
|
|
comment: undefined,
|
|
runHooks: false,
|
|
activate: false,
|
|
parentWorktree: undefined,
|
|
parentWorkspace: 'folder:missing',
|
|
noParent: false,
|
|
callerTerminalHandle: undefined,
|
|
cliProvenanceRequest: {}
|
|
})
|
|
expect(output).toContain('"ok": false')
|
|
expect(output).toContain('Parent selector was not found.')
|
|
expect(output).toContain('--parent-worktree selector')
|
|
expect(output).not.toContain('--parent-workspace')
|
|
expect(errSpy).not.toHaveBeenCalled()
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
it('passes no-parent through worktree.create and skips cwd inference', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_create', {
|
|
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
|
|
lineage: null,
|
|
warnings: []
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
await main(
|
|
['worktree', 'create', '--repo', 'id:repo-1', '--name', 'child', '--no-parent', '--json'],
|
|
'/tmp/repo/parent/src'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledTimes(1)
|
|
expect(callMock).toHaveBeenCalledWith('worktree.create', {
|
|
repo: 'id:repo-1',
|
|
name: 'child',
|
|
displayName: 'child',
|
|
displayNameKind: 'user',
|
|
baseBranch: undefined,
|
|
linkedIssue: undefined,
|
|
comment: undefined,
|
|
runHooks: false,
|
|
activate: false,
|
|
parentWorktree: undefined,
|
|
noParent: true,
|
|
callerTerminalHandle: undefined,
|
|
cliProvenanceRequest: {}
|
|
})
|
|
})
|
|
})
|