mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
* fix(orchestration): typed error codes for dispatch and worker-start refusals orchestration dispatch (and worker-start, which composes it) surfaced task not found, task not ready, and inject rejected as the same bare runtime_error, so an agent reading the receipt could not choose between creating the task, waiting on dependencies, or picking another terminal. Add task_not_found (data.taskId), task_not_ready (data.status, data.unmetDependencies), and inject_rejected (data.terminal, data.reason), each carrying data.nextSteps so every shipped CLI already prints the recovery. worker-start's not-ready refusal moves from task_not_startable to task_not_ready with the same detail. runtime_error stays for genuinely unexpected failures. Proven red-first from RpcDispatcher through the CLI's own failure formatting, plus an SSH bridge test that the host CLI's typed refusal relays unchanged. * test(orchestration): load CLI formatter at runtime in the dispatch-code test The composite node typecheck (config/tsconfig.node.json without --composite false, as CI runs it) rejects a static import of src/cli from a main test with TS6307. Load the formatter and error class dynamically behind narrow structural types, as the CLI/runtime boundary test does. * fix(orchestration): keep task_not_startable and split the CLI-format proof Review on #18902: - Drop task_not_ready. worker-start already published task_not_startable for a not-ready Task, so renaming it would change an existing receipt value under old clients. dispatch now emits task_not_startable too (it was a bare runtime_error before, so this is purely additive), with the new data.status / data.unmetDependencies / data.nextSteps. - Move the refusal receipts (code, message, data) into src/shared/orchestration-dispatch-refusal-contract.ts so the runtime emits them and the CLI test formats the identical envelope. The RPC test under src/main asserts toEqual against the contract; the new src/cli/orchestration-dispatch-refusal-format.test.ts feeds those same receipts to formatCliError / reportCliError. Neither tsconfig widens and the composite typecheck CI runs is clean. * fix(orchestration): keep published refusal messages and type the DB claim guards Codex review of #18902: - Every call site keeps the exact message it published on main ("Task not found: <id>", "only a ready Task can start.", "cannot retry from Dispatch"); the shared contract now takes the message per site and only owns the code and data. Baseline strings are pinned as literals. - createDispatchContext's own missing/non-ready guards, including the atomic-claim loser, now emit the same typed receipt instead of a bare Error, so a dispatch that races a status change no longer flattens to runtime_error. Covered by a dispatcher-level race test. - Invalid --retry-of keeps task_not_startable but now carries status, unmetDependencies, retryOf, and a retry-specific next step. - Dependency recovery text distinguishes waiting on running deps from retrying/unblocking failed ones. - CLI test adds an unknown-code case so the old-client claim rests on an assertion, not a comment; SSH test asserts exact stdout. - Guide table narrowed to the covered preflight cases; occupancy stays runtime_error and is named as such.
78 lines
3.2 KiB
TypeScript
78 lines
3.2 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
injectRejectedRefusal,
|
|
taskNotFoundRefusal,
|
|
taskNotStartableRefusal,
|
|
type DispatchRefusalReceipt
|
|
} from '../shared/orchestration-dispatch-refusal-contract'
|
|
import { formatCliError, reportCliError } from './format'
|
|
import { RuntimeRpcFailureError, type RuntimeRpcFailure } from './runtime/types'
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
// Why: these are the exact envelopes the RPC dispatcher test proved the runtime emits. This
|
|
// checkout's formatter never enumerates codes (verified below with a code no build has defined),
|
|
// which is what lets a client that predates a new code still print its message and nextSteps.
|
|
describe('orchestration dispatch refusals through the CLI error boundary', () => {
|
|
it.each([
|
|
{
|
|
receipt: taskNotFoundRefusal('Task not found: task_missing', { taskId: 'task_missing' }),
|
|
recovery: /task-create|task-list/
|
|
},
|
|
{
|
|
receipt: taskNotStartableRefusal(
|
|
'Task task_child is pending; only ready tasks can be dispatched',
|
|
{ taskId: 'task_child', status: 'pending', unmetDependencies: ['task_parent'] }
|
|
),
|
|
recovery: /task_parent/
|
|
},
|
|
{
|
|
receipt: injectRejectedRefusal('term_worker', 'no_agent_detected'),
|
|
recovery: /without --inject/
|
|
}
|
|
])('prints $receipt.code with its recovery in human and JSON output', ({ receipt, recovery }) => {
|
|
const failure = envelope(receipt)
|
|
const error = new RuntimeRpcFailureError(failure)
|
|
expect(error.code).toBe(receipt.code)
|
|
|
|
const human = formatCliError(error, { commandPath: ['orchestration', 'dispatch'] })
|
|
expect(human).toContain(receipt.message)
|
|
expect(human).toMatch(recovery)
|
|
|
|
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
reportCliError(error, true, { commandPath: ['orchestration', 'dispatch'] })
|
|
const printed = JSON.parse(log.mock.calls[0]?.[0] as string) as RuntimeRpcFailure
|
|
expect(printed.ok).toBe(false)
|
|
expect(printed.error).toEqual(receipt)
|
|
})
|
|
})
|
|
|
|
// Why: a code this build has never defined stands in for a future host's new code; if the
|
|
// formatter ever starts gating on known codes, this is the assertion that catches it.
|
|
it('prints an unknown code with its message and nextSteps unchanged', () => {
|
|
const failure: RuntimeRpcFailure = {
|
|
id: 'rpc_1',
|
|
ok: false,
|
|
error: {
|
|
code: 'code_from_a_newer_host',
|
|
message: 'Refused for a reason this CLI has never heard of.',
|
|
data: { nextSteps: ['Do the thing the newer host suggested.'] }
|
|
},
|
|
_meta: { runtimeId: 'runtime_1' }
|
|
}
|
|
const error = new RuntimeRpcFailureError(failure)
|
|
|
|
expect(formatCliError(error, { commandPath: ['orchestration', 'dispatch'] })).toBe(
|
|
'Refused for a reason this CLI has never heard of.\nNext step: Do the thing the newer host suggested.'
|
|
)
|
|
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
reportCliError(error, true, { commandPath: ['orchestration', 'dispatch'] })
|
|
expect(JSON.parse(log.mock.calls[0]?.[0] as string)).toEqual(failure)
|
|
})
|
|
|
|
function envelope(receipt: DispatchRefusalReceipt): RuntimeRpcFailure {
|
|
return { id: 'rpc_1', ok: false, error: receipt, _meta: { runtimeId: 'runtime_1' } }
|
|
}
|