mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
Return optional warnings from the owning host when a requested workspace status does not match its configured columns. Preserve the requested write and older-peer behavior; show warnings separately from normal CLI output. Co-authored-by: Kalvin Mizzi <kalvin@mizzi.com>
198 lines
7.6 KiB
TypeScript
198 lines
7.6 KiB
TypeScript
import '../../src/main/runtime/rpc/unused-default-rpc-methods.test-fixture'
|
|
import type * as childProcess from 'node:child_process'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import type { OrcaRuntimeService } from '../../src/main/runtime/orca-runtime'
|
|
import { RpcDispatcher } from '../../src/main/runtime/rpc/dispatcher'
|
|
import { WORKTREE_METHODS } from '../../src/main/runtime/rpc/methods/worktree'
|
|
import type { WorkspaceStatusDefinition } from '../../src/shared/worktree/types'
|
|
import { DEFAULT_WORKSPACE_STATUSES, getWorkspaceStatus } from '../../src/shared/workspace-statuses'
|
|
|
|
const mocks = 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('../../src/cli/runtime-client', async () => {
|
|
const { createRuntimeClientModuleMock } = await import('../../src/cli/index-test-harness.js')
|
|
return createRuntimeClientModuleMock(mocks)
|
|
})
|
|
vi.mock('../../src/cli/runtime/environments', () => ({
|
|
addEnvironmentFromPairingCode: mocks.addEnvironmentFromPairingCodeMock,
|
|
listEnvironments: mocks.listEnvironmentsMock,
|
|
removeEnvironment: vi.fn(),
|
|
resolveEnvironment: vi.fn()
|
|
}))
|
|
vi.mock('child_process', async (importOriginal) => {
|
|
const original = await importOriginal<typeof childProcess>()
|
|
const { createChildProcessModuleMock } = await import('../../src/cli/index-test-harness.js')
|
|
return { ...original, ...(await createChildProcessModuleMock(mocks.spawnMock)) }
|
|
})
|
|
|
|
import { main } from '../../src/cli/index'
|
|
import { buildWorktree, okFixture } from '../../src/cli/test-fixtures'
|
|
import { useWorktreeAwarenessEnvironment } from '../../src/cli/index-test-harness'
|
|
|
|
const CUSTOM_STATUSES: WorkspaceStatusDefinition[] = [
|
|
{ id: 'ready', label: 'Ready', color: 'neutral', icon: 'circle' },
|
|
{ id: 'shipped', label: 'Shipped', color: 'emerald', icon: 'circle-check' }
|
|
]
|
|
|
|
function fixture(
|
|
options: {
|
|
statuses?: readonly WorkspaceStatusDefinition[]
|
|
failRead?: boolean
|
|
hostId?: string
|
|
} = {}
|
|
) {
|
|
const stored = {
|
|
...buildWorktree('/tmp/repo/child', 'feature/child'),
|
|
hostId: options.hostId ?? 'local',
|
|
workspaceStatus: 'in-progress'
|
|
}
|
|
const getUIState = vi.fn(() => {
|
|
if (options.failRead) {
|
|
throw new Error('runtime_unavailable')
|
|
}
|
|
return { workspaceStatuses: options.statuses }
|
|
})
|
|
const updateManagedWorktreeMeta = vi.fn(async (_selector, updates) => {
|
|
if (updates.workspaceStatus !== undefined) {
|
|
stored.workspaceStatus = updates.workspaceStatus
|
|
}
|
|
return stored
|
|
})
|
|
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the explicit registry only calls these runtime methods; all data is in memory.
|
|
const runtime = {
|
|
getRuntimeId: () => 'owner-runtime',
|
|
getUIState,
|
|
updateManagedWorktreeMeta
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
mocks.callMock.mockImplementation(async (method, params) => {
|
|
expect(method).toBe('worktree.set')
|
|
return dispatcher.dispatch({ id: 'fixture', authToken: 'tok', method, params })
|
|
})
|
|
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
return { stored, getUIState, updateManagedWorktreeMeta, log, error }
|
|
}
|
|
|
|
async function setStatus(status: string | undefined, json: boolean) {
|
|
await main(
|
|
[
|
|
'worktree',
|
|
'set',
|
|
'--worktree',
|
|
'id:repo::/tmp/repo/child',
|
|
...(status === undefined
|
|
? ['--comment', 'unchanged status']
|
|
: ['--workspace-status', status]),
|
|
...(json ? ['--json'] : [])
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
}
|
|
|
|
describe('workspace status warnings through CLI and owning RPC handler', () => {
|
|
useWorktreeAwarenessEnvironment(mocks)
|
|
|
|
it.each([false, true])('warns about the default fallback with json=%s', async (json) => {
|
|
const { stored, log, error } = fixture()
|
|
await setStatus('done', json)
|
|
expect(stored.workspaceStatus).toBe('done')
|
|
expect(getWorkspaceStatus(stored, DEFAULT_WORKSPACE_STATUSES)).toBe('in-progress')
|
|
const warning = {
|
|
code: 'UNKNOWN_WORKSPACE_STATUS',
|
|
message: expect.stringContaining('Unknown workspace status "done"'),
|
|
details: {
|
|
requestedId: 'done',
|
|
effectiveId: 'in-progress',
|
|
validIds: ['todo', 'in-progress', 'in-review', 'completed']
|
|
}
|
|
}
|
|
const output = log.mock.calls.flat().join('\n')
|
|
if (json) {
|
|
expect(JSON.parse(output)).toMatchObject({
|
|
ok: true,
|
|
result: { worktree: { workspaceStatus: 'done' }, warnings: [warning] }
|
|
})
|
|
expect(output).toContain('In progress')
|
|
expect(error).not.toHaveBeenCalled()
|
|
} else {
|
|
expect(error).toHaveBeenCalledWith(expect.stringContaining('Unknown workspace status "done"'))
|
|
const text = error.mock.calls.flat().join('\n')
|
|
expect(text).toContain('In progress')
|
|
expect(text).toContain('todo, in-progress, in-review, completed')
|
|
}
|
|
})
|
|
|
|
it.each(['ssh:build-box', 'runtime:paired-owner'])(
|
|
'uses %s owner columns for unknown ids',
|
|
async (hostId) => {
|
|
const { stored, log } = fixture({ statuses: CUSTOM_STATUSES, hostId })
|
|
await setStatus('done', true)
|
|
expect(getWorkspaceStatus(stored, CUSTOM_STATUSES)).toBe('ready')
|
|
expect(JSON.parse(log.mock.calls.flat().join('\n'))).toMatchObject({
|
|
ok: true,
|
|
result: {
|
|
warnings: [
|
|
{
|
|
details: { requestedId: 'done', effectiveId: 'ready', validIds: ['ready', 'shipped'] }
|
|
}
|
|
]
|
|
}
|
|
})
|
|
expect(log.mock.calls.flat().join('\n')).toContain('Ready')
|
|
expect(mocks.callMock).toHaveBeenCalledTimes(1)
|
|
}
|
|
)
|
|
|
|
it.each(['completed', 'shipped'])('accepts valid column %s without warnings', async (status) => {
|
|
const { stored, log, error } = fixture({
|
|
statuses: status === 'shipped' ? CUSTOM_STATUSES : undefined
|
|
})
|
|
await setStatus(status, true)
|
|
expect(stored.workspaceStatus).toBe(status)
|
|
const response = JSON.parse(log.mock.calls.flat().join('\n'))
|
|
expect(response).toMatchObject({ ok: true, result: { worktree: { workspaceStatus: status } } })
|
|
expect(response.result).not.toHaveProperty('warnings')
|
|
expect(error).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('preserves the completed write when owner settings cannot be read', async () => {
|
|
const { stored, log, error } = fixture({ failRead: true })
|
|
await setStatus('done', true)
|
|
expect(stored.workspaceStatus).toBe('done')
|
|
const response = JSON.parse(log.mock.calls.flat().join('\n'))
|
|
expect(response).toMatchObject({ ok: true })
|
|
expect(response.result).not.toHaveProperty('warnings')
|
|
expect(error).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('skips the settings read when the status is unchanged', async () => {
|
|
const { getUIState, stored } = fixture({ failRead: true })
|
|
await setStatus(undefined, true)
|
|
expect(getUIState).not.toHaveBeenCalled()
|
|
expect(stored.workspaceStatus).toBe('in-progress')
|
|
})
|
|
|
|
it('keeps an older host response without warnings working', async () => {
|
|
const { stored, log, error } = fixture()
|
|
mocks.callMock.mockResolvedValue(
|
|
okFixture('old-host', { worktree: { ...stored, workspaceStatus: 'done' } })
|
|
)
|
|
await setStatus('done', true)
|
|
expect(JSON.parse(log.mock.calls.flat().join('\n'))).toMatchObject({
|
|
ok: true,
|
|
result: { worktree: { workspaceStatus: 'done' } }
|
|
})
|
|
expect(error).not.toHaveBeenCalled()
|
|
expect(mocks.callMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|