mirror of
https://github.com/stablyai/orca.git
synced 2026-10-12 16:03:03 +00:00
Addresses four items from ORCHESTRATOR_FEEDBACK: - #5 preamble visibility: `dispatch-show --preamble` regenerates the preamble text for a task; `dispatch --inject --dry-run` previews without mutating state; `dispatch --return-preamble` echoes the injected preamble in the JSON response so coordinators can audit what a worker received. - #6 status enum validation: CLI rejects unknown `task-update --status` values with `invalid status '<x>', expected one of: pending, ready, dispatched, completed, failed, blocked` before the RPC's generic Zod message. Valid statuses are listed under Notes in `task-update --help`. - #13 task-list dispatch cross-ref: `task-list --json` now includes `assignee_handle` and `dispatch_id` for tasks in status=dispatched via a read-only LEFT JOIN on dispatch_contexts. Non-dispatched rows keep their legacy shape so existing consumers are unaffected. - #14 inbox body visibility: `inbox --full` prints body + payload verbatim; default output is unchanged (id/from/to/subject only). No DB migrations; join-only change on dispatch_contexts so the sibling preamble PR's `last_heartbeat_at` column addition will not conflict. Co-authored-by: Orca <help@stably.ai>
270 lines
8.0 KiB
TypeScript
270 lines
8.0 KiB
TypeScript
import path from 'path'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const callMock = vi.fn()
|
|
|
|
vi.mock('./runtime-client', () => {
|
|
class RuntimeClient {
|
|
call = callMock
|
|
getCliStatus = vi.fn()
|
|
openOrca = vi.fn()
|
|
}
|
|
|
|
class RuntimeClientError extends Error {
|
|
readonly code: string
|
|
|
|
constructor(code: string, message: string) {
|
|
super(message)
|
|
this.code = code
|
|
}
|
|
}
|
|
|
|
class RuntimeRpcFailureError extends RuntimeClientError {
|
|
readonly response: unknown
|
|
|
|
constructor(response: unknown) {
|
|
super('runtime_error', 'runtime_error')
|
|
this.response = response
|
|
}
|
|
}
|
|
|
|
return {
|
|
RuntimeClient,
|
|
RuntimeClientError,
|
|
RuntimeRpcFailureError
|
|
}
|
|
})
|
|
|
|
import {
|
|
buildCurrentWorktreeSelector,
|
|
COMMAND_SPECS,
|
|
main,
|
|
normalizeWorktreeSelector
|
|
} from './index'
|
|
import { buildWorktree, okFixture, queueFixtures, worktreeListFixture } from './test-fixtures'
|
|
|
|
describe('COMMAND_SPECS collision check', () => {
|
|
it('has no duplicate command paths', () => {
|
|
const seen = new Set<string>()
|
|
for (const spec of COMMAND_SPECS) {
|
|
const key = spec.path.join(' ')
|
|
expect(seen.has(key), `Duplicate COMMAND_SPECS path: "${key}"`).toBe(false)
|
|
seen.add(key)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('orca cli worktree awareness', () => {
|
|
const originalTerminalHandle = process.env.ORCA_TERMINAL_HANDLE
|
|
const originalUserDataPath = process.env.ORCA_USER_DATA_PATH
|
|
|
|
beforeEach(() => {
|
|
callMock.mockReset()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
if (originalTerminalHandle === undefined) {
|
|
delete process.env.ORCA_TERMINAL_HANDLE
|
|
} else {
|
|
process.env.ORCA_TERMINAL_HANDLE = originalTerminalHandle
|
|
}
|
|
if (originalUserDataPath === undefined) {
|
|
delete process.env.ORCA_USER_DATA_PATH
|
|
} else {
|
|
process.env.ORCA_USER_DATA_PATH = originalUserDataPath
|
|
}
|
|
})
|
|
|
|
it('builds the current worktree selector from cwd', () => {
|
|
expect(buildCurrentWorktreeSelector('/tmp/repo/feature')).toBe(
|
|
`path:${path.resolve('/tmp/repo/feature')}`
|
|
)
|
|
})
|
|
|
|
it('normalizes active/current worktree selectors to cwd', () => {
|
|
const resolved = path.resolve('/tmp/repo/feature')
|
|
expect(normalizeWorktreeSelector('active', '/tmp/repo/feature')).toBe(`path:${resolved}`)
|
|
expect(normalizeWorktreeSelector('current', '/tmp/repo/feature')).toBe(`path:${resolved}`)
|
|
expect(normalizeWorktreeSelector('branch:feature/foo', '/tmp/repo/feature')).toBe(
|
|
'branch:feature/foo'
|
|
)
|
|
})
|
|
|
|
it('shows the enclosing worktree for `worktree current`', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo')]),
|
|
okFixture('req_1', {
|
|
worktree: {
|
|
id: 'repo::/tmp/repo/feature',
|
|
branch: 'feature/foo',
|
|
path: '/tmp/repo/feature'
|
|
}
|
|
})
|
|
)
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(['worktree', 'current', '--json'], '/tmp/repo/feature/src')
|
|
|
|
expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', { limit: 10_000 })
|
|
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.show', {
|
|
worktree: `path:${path.resolve('/tmp/repo/feature')}`
|
|
})
|
|
expect(logSpy).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
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: `path:${path.resolve('/tmp/repo/feature')}`,
|
|
displayName: undefined,
|
|
linkedIssue: undefined,
|
|
comment: 'hello'
|
|
})
|
|
})
|
|
|
|
it('uses the resolved enclosing worktree for other worktree consumers', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo')]),
|
|
okFixture('req_show', {
|
|
worktree: {
|
|
id: 'repo::/tmp/repo/feature',
|
|
branch: 'feature/foo',
|
|
path: '/tmp/repo/feature'
|
|
}
|
|
})
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(['worktree', 'show', '--worktree', 'current', '--json'], '/tmp/repo/feature/src')
|
|
|
|
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.show', {
|
|
worktree: `path:${path.resolve('/tmp/repo/feature')}`
|
|
})
|
|
})
|
|
|
|
it('formats group orchestration sends in text mode', async () => {
|
|
process.env.ORCA_TERMINAL_HANDLE = 'term_sender'
|
|
callMock.mockResolvedValueOnce({
|
|
id: 'req_send',
|
|
ok: true,
|
|
result: {
|
|
messages: [{ id: 'msg_1' }, { id: 'msg_2' }],
|
|
recipients: 2
|
|
},
|
|
_meta: {
|
|
runtimeId: 'runtime-1'
|
|
}
|
|
})
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(['orchestration', 'send', '--to', '@all', '--subject', 'hello'], '/tmp/repo')
|
|
|
|
expect(callMock).toHaveBeenCalledWith('orchestration.send', {
|
|
from: 'term_sender',
|
|
to: '@all',
|
|
subject: 'hello',
|
|
body: undefined,
|
|
type: undefined,
|
|
priority: undefined,
|
|
threadId: undefined,
|
|
payload: undefined,
|
|
devMode: false
|
|
})
|
|
expect(logSpy).toHaveBeenCalledWith('Sent 2 messages to 2 recipients')
|
|
})
|
|
|
|
it('rejects unknown task-update status with an enum-aware error', async () => {
|
|
process.env.ORCA_TERMINAL_HANDLE = 'term_coord'
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const priorExitCode = process.exitCode
|
|
|
|
await main(
|
|
['orchestration', 'task-update', '--id', 'task_x', '--status', 'complete'],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
const output = [...errSpy.mock.calls, ...logSpy.mock.calls]
|
|
.flat()
|
|
.map((v) => (typeof v === 'string' ? v : JSON.stringify(v)))
|
|
.join('\n')
|
|
expect(output).toContain("invalid status 'complete'")
|
|
expect(output).toContain('pending, ready, dispatched, completed, failed, blocked')
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect(process.exitCode).toBe(1)
|
|
|
|
// Reset exitCode so subsequent tests don't inherit the failure.
|
|
process.exitCode = priorExitCode
|
|
errSpy.mockRestore()
|
|
})
|
|
|
|
it('passes dev mode to injected orchestration dispatches', async () => {
|
|
process.env.ORCA_TERMINAL_HANDLE = 'term_sender'
|
|
process.env.ORCA_USER_DATA_PATH = '/tmp/orca-dev'
|
|
callMock.mockResolvedValueOnce({
|
|
id: 'req_dispatch',
|
|
ok: true,
|
|
result: {
|
|
dispatch: { id: 'ctx_1', task_id: 'task_1', status: 'dispatched' }
|
|
},
|
|
_meta: {
|
|
runtimeId: 'runtime-1'
|
|
}
|
|
})
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(
|
|
['orchestration', 'dispatch', '--task', 'task_1', '--to', 'term_worker', '--inject'],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith('orchestration.dispatch', {
|
|
task: 'task_1',
|
|
to: 'term_worker',
|
|
from: 'term_sender',
|
|
inject: true,
|
|
devMode: true
|
|
})
|
|
})
|
|
|
|
it('uses the resolved enclosing worktree for terminal consumers', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo')]),
|
|
okFixture('req_term', { terminals: [], totalCount: 0, truncated: false })
|
|
)
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(['terminal', 'list', '--worktree', 'active', '--json'], '/tmp/repo/feature/src')
|
|
|
|
expect(callMock).toHaveBeenNthCalledWith(2, 'terminal.list', {
|
|
worktree: `path:${path.resolve('/tmp/repo/feature')}`,
|
|
limit: undefined
|
|
})
|
|
})
|
|
})
|