mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 00:02:38 +00:00
Pass validated read/unread flags through the existing workspace metadata update. Contributor attribution correction: the earlier Ctrl+M squash message placed its related link after the co-author footer, so GitHub did not recognize all contributors. Preserve that credit here for https://github.com/stablyai/orca/pull/24100 and its related contribution https://github.com/stablyai/orca/pull/24143 . Related CLI link validation retained from https://github.com/stablyai/orca/pull/20036 . Co-authored-by: Raz Shlomo <12373339+razshlomo@users.noreply.github.com> Co-authored-by: Neil <neil@stably.ai> Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>
462 lines
13 KiB
TypeScript
462 lines
13 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 { 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('uses cwd when active is passed to worktree.set', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
worktreeListFixture([
|
|
buildWorktree('/tmp/repo', 'main', 'aaa'),
|
|
buildWorktree('/tmp/repo/feature', 'feature/foo')
|
|
]),
|
|
okFixture('req_1', {
|
|
worktree: {
|
|
id: 'repo::/tmp/repo/feature',
|
|
branch: 'feature/foo',
|
|
path: '/tmp/repo/feature',
|
|
comment: 'hello'
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
['worktree', 'set', '--worktree', 'active', '--comment', 'hello', '--json'],
|
|
'/tmp/repo/feature/src'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.set', {
|
|
worktree: 'id:repo::/tmp/repo/feature',
|
|
displayName: undefined,
|
|
linkedIssue: undefined,
|
|
comment: 'hello',
|
|
parentWorktree: undefined,
|
|
noParent: false
|
|
})
|
|
})
|
|
|
|
it('passes parent lineage through worktree.set', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_set_parent', {
|
|
worktree: {
|
|
...buildWorktree('/tmp/repo/child', 'feature/child'),
|
|
parentWorktreeId: 'repo::/tmp/repo/parent',
|
|
childWorktreeIds: [],
|
|
lineage: {
|
|
worktreeId: 'repo::/tmp/repo/child',
|
|
worktreeInstanceId: 'child-instance',
|
|
parentWorktreeId: 'repo::/tmp/repo/parent',
|
|
parentWorktreeInstanceId: 'parent-instance',
|
|
origin: 'manual',
|
|
capture: { source: 'manual-action', confidence: 'explicit' },
|
|
createdAt: 1
|
|
}
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'set',
|
|
'--worktree',
|
|
'id:repo::/tmp/repo/child',
|
|
'--parent-worktree',
|
|
'id:repo::/tmp/repo/parent',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith('worktree.set', {
|
|
worktree: 'id:repo::/tmp/repo/child',
|
|
displayName: undefined,
|
|
linkedIssue: undefined,
|
|
comment: undefined,
|
|
parentWorktree: 'id:repo::/tmp/repo/parent',
|
|
noParent: false
|
|
})
|
|
})
|
|
|
|
it('resolves current for explicit parent-worktree on set', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
worktreeListFixture([buildWorktree('/tmp/repo/parent', 'feature/parent')]),
|
|
okFixture('req_set_parent', {
|
|
worktree: {
|
|
...buildWorktree('/tmp/repo/child', 'feature/child'),
|
|
parentWorktreeId: 'repo::/tmp/repo/parent',
|
|
childWorktreeIds: [],
|
|
lineage: null
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'set',
|
|
'--worktree',
|
|
'id:repo::/tmp/repo/child',
|
|
'--parent-worktree',
|
|
'current',
|
|
'--json'
|
|
],
|
|
'/tmp/repo/parent/src'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.set', {
|
|
worktree: 'id:repo::/tmp/repo/child',
|
|
displayName: undefined,
|
|
linkedIssue: undefined,
|
|
comment: undefined,
|
|
parentWorktree: 'id:repo::/tmp/repo/parent',
|
|
noParent: false
|
|
})
|
|
})
|
|
|
|
it('rejects contradictory parent flags on worktree.set 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',
|
|
'set',
|
|
'--worktree',
|
|
'id:repo::/tmp/repo/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 --parent-worktree or --no-parent, not both.'
|
|
)
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
it('rejects bare parent-worktree on worktree.set', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(
|
|
['worktree', 'set', '--worktree', 'id:repo::/tmp/repo/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('passes parent removal through worktree.set', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_clear_parent', {
|
|
worktree: {
|
|
...buildWorktree('/tmp/repo/child', 'feature/child'),
|
|
parentWorktreeId: null,
|
|
childWorktreeIds: [],
|
|
lineage: null
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
['worktree', 'set', '--worktree', 'id:repo::/tmp/repo/child', '--no-parent', '--json'],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith('worktree.set', {
|
|
worktree: 'id:repo::/tmp/repo/child',
|
|
displayName: undefined,
|
|
linkedIssue: undefined,
|
|
comment: undefined,
|
|
workspaceStatus: undefined,
|
|
parentWorktree: undefined,
|
|
noParent: true
|
|
})
|
|
})
|
|
|
|
it('passes Linear URL metadata through worktree.set', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_set_linear', {
|
|
worktree: {
|
|
...buildWorktree('/tmp/repo/child', 'feature/child'),
|
|
linkedLinearIssue: 'STA-335',
|
|
linkedLinearIssueWorkspaceId: null,
|
|
linkedLinearIssueOrganizationUrlKey: 'stably'
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'set',
|
|
'--worktree',
|
|
'id:repo::/tmp/repo/child',
|
|
'--linear-issue',
|
|
'https://linear.app/stably/issue/STA-335/test-issue',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith('worktree.set', {
|
|
worktree: 'id:repo::/tmp/repo/child',
|
|
displayName: undefined,
|
|
linkedIssue: undefined,
|
|
linkedLinearIssue: 'STA-335',
|
|
linkedLinearIssueWorkspaceId: null,
|
|
linkedLinearIssueOrganizationUrlKey: 'stably',
|
|
comment: undefined,
|
|
workspaceStatus: undefined,
|
|
parentWorktree: undefined,
|
|
noParent: false
|
|
})
|
|
})
|
|
|
|
it('clears all Linear metadata through worktree.set', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_clear_linear', {
|
|
worktree: {
|
|
...buildWorktree('/tmp/repo/child', 'feature/child'),
|
|
linkedLinearIssue: null,
|
|
linkedLinearIssueWorkspaceId: null,
|
|
linkedLinearIssueOrganizationUrlKey: null
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'set',
|
|
'--worktree',
|
|
'id:repo::/tmp/repo/child',
|
|
'--linear-issue',
|
|
'null',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith('worktree.set', {
|
|
worktree: 'id:repo::/tmp/repo/child',
|
|
displayName: undefined,
|
|
linkedIssue: undefined,
|
|
linkedLinearIssue: null,
|
|
linkedLinearIssueWorkspaceId: null,
|
|
linkedLinearIssueOrganizationUrlKey: null,
|
|
comment: undefined,
|
|
workspaceStatus: undefined,
|
|
parentWorktree: undefined,
|
|
noParent: false
|
|
})
|
|
})
|
|
|
|
it('rejects invalid Linear issue values on worktree.set before RPC', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'set',
|
|
'--worktree',
|
|
'id:repo::/tmp/repo/child',
|
|
'--linear-issue',
|
|
'not-a-linear-link',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
|
|
'Pass a Linear issue identifier like STA-335'
|
|
)
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
|
|
it('passes workspace status through worktree.set', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_set_status', {
|
|
worktree: {
|
|
...buildWorktree('/tmp/repo/child', 'feature/child'),
|
|
workspaceStatus: 'in-review'
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'set',
|
|
'--worktree',
|
|
'id:repo::/tmp/repo/child',
|
|
'--workspace-status',
|
|
'in-review',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith('worktree.set', {
|
|
worktree: 'id:repo::/tmp/repo/child',
|
|
displayName: undefined,
|
|
linkedIssue: undefined,
|
|
comment: undefined,
|
|
workspaceStatus: 'in-review',
|
|
parentWorktree: undefined,
|
|
noParent: false
|
|
})
|
|
})
|
|
|
|
it.each([
|
|
['id:repo::/tmp/repo/child', '--unread', true],
|
|
['id:repo::/tmp/repo/child', '--read', false],
|
|
['id:folder-repo::/tmp/notes', '--unread', true],
|
|
['id:folder-repo::/tmp/notes', '--read', false]
|
|
])('passes %s %s through worktree.set as isUnread', async (selector, flag, isUnread) => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_set_unread', {
|
|
worktree: { ...buildWorktree('/tmp/repo/child', 'feature/child'), isUnread }
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(['worktree', 'set', '--worktree', selector, flag, '--json'], '/tmp/repo')
|
|
|
|
expect(callMock).toHaveBeenCalledExactlyOnceWith('worktree.set', {
|
|
worktree: selector,
|
|
displayName: undefined,
|
|
linkedIssue: undefined,
|
|
comment: undefined,
|
|
workspaceStatus: undefined,
|
|
isUnread,
|
|
parentWorktree: undefined,
|
|
noParent: false
|
|
})
|
|
})
|
|
|
|
it('leaves isUnread unchanged when neither --unread nor --read is passed', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_set_comment', { worktree: buildWorktree('/tmp/repo/child', 'feature/child') })
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
['worktree', 'set', '--worktree', 'id:repo::/tmp/repo/child', '--comment', 'hi', '--json'],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock.mock.calls[0]?.[1]).toHaveProperty('isUnread', undefined)
|
|
expect(JSON.stringify(callMock.mock.calls[0]?.[1])).not.toContain('isUnread')
|
|
})
|
|
|
|
it.each([
|
|
[['--unread', '--read'], 'Choose either --unread or --read'],
|
|
[['--read', '--unread'], 'Choose either --unread or --read'],
|
|
[['--read', 'yes'], '--read takes no value'],
|
|
[['--read=false'], '--read takes no value'],
|
|
[['--read='], '--read takes no value'],
|
|
[['--unread=false'], '--unread takes no value'],
|
|
[['--unread='], '--unread takes no value']
|
|
])('rejects %j on worktree.set before resolving selectors', async (flags, message) => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
// `active` resolves through worktree.list, so any RPC here means validation ran too late.
|
|
await main(['worktree', 'set', '--worktree', 'active', ...flags, '--json'], '/tmp/repo')
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(message)
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
process.exitCode = priorExitCode
|
|
})
|
|
})
|