mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 01:47:24 +00:00
Guard concise orchestration list output against long text (#36625)
One table-driven test keeps every concise orchestration list row short when stored text is huge. Concise run-list cuts each Run's objective to one line of at most 160 characters; --full is unchanged.
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('../../selectors', () => ({ getTerminalHandle: vi.fn() }))
|
||||
|
||||
import { ORCHESTRATION_HANDLERS } from '../orchestration'
|
||||
import { orchestrationTestContext } from '../orchestration-handler-test-context'
|
||||
|
||||
const HUGE = 'x'.repeat(10_000)
|
||||
// Message bodies and gate questions are what check, inbox, and gate-list exist to show.
|
||||
const CONTENT = 'c'.repeat(10_000)
|
||||
const MAX_ROW_CHARS = 500
|
||||
|
||||
const message = {
|
||||
id: 'msg_1',
|
||||
from_handle: 'dispatch:ctx_1',
|
||||
to_handle: 'run:run_1',
|
||||
subject: 'Done',
|
||||
body: CONTENT,
|
||||
type: 'worker_done',
|
||||
priority: 'normal',
|
||||
read: 0,
|
||||
thread_id: 'msg_1',
|
||||
payload: JSON.stringify({ taskId: 'task_1', dispatchId: 'ctx_1' }),
|
||||
created_at: '2026-10-31T00:00:00Z'
|
||||
}
|
||||
|
||||
const LISTS: [string, [string, string][], string, unknown][] = [
|
||||
[
|
||||
'task-list',
|
||||
[['run', 'run_1']],
|
||||
'tasks',
|
||||
{
|
||||
tasks: [
|
||||
{ id: 'task_1', spec: HUGE, status: 'completed', result: HUGE, dispatch_id: 'ctx_1' }
|
||||
],
|
||||
count: 1
|
||||
}
|
||||
],
|
||||
[
|
||||
'worker-list',
|
||||
[['run', 'run_1']],
|
||||
'workers',
|
||||
{
|
||||
workers: [
|
||||
{
|
||||
dispatchId: 'ctx_1',
|
||||
taskId: 'task_1',
|
||||
runId: 'run_1',
|
||||
workerState: 'failed',
|
||||
dispatchStatus: 'failed',
|
||||
agentTerminalHandle: 'term_1',
|
||||
terminalState: 'released',
|
||||
lastError: HUGE,
|
||||
output: HUGE,
|
||||
projection: {
|
||||
host: { id: 'local' },
|
||||
stage: { activity: 'failed' },
|
||||
liveness: { verdict: 'exited' },
|
||||
nextAction: { argv: [] },
|
||||
description: HUGE
|
||||
}
|
||||
}
|
||||
],
|
||||
counts: { released: 1 },
|
||||
page: { hasMore: false, nextCursor: null, total: 1 }
|
||||
}
|
||||
],
|
||||
[
|
||||
'gate-list',
|
||||
[['run', 'run_1']],
|
||||
'gates',
|
||||
{
|
||||
gates: [
|
||||
{
|
||||
id: 'gate_1',
|
||||
task_id: 'task_1',
|
||||
question: CONTENT,
|
||||
options: '["yes","no"]',
|
||||
status: 'resolved',
|
||||
resolution: HUGE
|
||||
}
|
||||
],
|
||||
count: 1
|
||||
}
|
||||
],
|
||||
[
|
||||
'run-list',
|
||||
[],
|
||||
'runs',
|
||||
{ runs: [{ id: 'run_1', objective: HUGE, legacy: 0 }], nextCursor: null }
|
||||
],
|
||||
['inbox', [], 'messages', { messages: [message], count: 1 }],
|
||||
[
|
||||
'check',
|
||||
[['run', 'run_1']],
|
||||
'messages',
|
||||
{
|
||||
runId: 'run_1',
|
||||
deliveryId: 'delivery_1',
|
||||
deliveryAcknowledged: true,
|
||||
messages: [message],
|
||||
count: 1
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
async function printed(command: string, flags: [string, string][], result: unknown, json: boolean) {
|
||||
const callMock = vi.fn().mockResolvedValue({ id: 'req_1', ok: true, result, _meta: {} })
|
||||
const log = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockClear()
|
||||
.mockImplementation(() => {})
|
||||
await ORCHESTRATION_HANDLERS[`orchestration ${command}`](
|
||||
orchestrationTestContext(callMock, new Map(flags), json)
|
||||
)
|
||||
return log.mock.calls.map(([line]) => String(line).replaceAll(CONTENT, '')).join('\n')
|
||||
}
|
||||
|
||||
describe('concise orchestration list output', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('ORCA_TERMINAL_HANDLE', 'term_coord')
|
||||
vi.stubEnv('ORCA_AGENT_SESSION_ID', undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it.each(LISTS)(
|
||||
'keeps each %s row short when stored text is huge',
|
||||
async (command, flags, key, result) => {
|
||||
const text = await printed(command, flags, result, false)
|
||||
const json: { result: Record<string, unknown[]> } = JSON.parse(
|
||||
await printed(command, flags, result, true)
|
||||
)
|
||||
const rows = [...text.split('\n'), ...json.result[key].map((row) => JSON.stringify(row))]
|
||||
expect(rows.length).toBeGreaterThan(1)
|
||||
for (const row of rows) {
|
||||
expect(row.length).toBeLessThan(MAX_ROW_CHARS)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { abbreviateOrchestrationText } from '../../../shared/orchestration-task-summary'
|
||||
import type { CommandHandler } from '../../dispatch'
|
||||
import { printResult } from '../../format'
|
||||
import { printOrchestrationResult } from './agent-view'
|
||||
@@ -94,7 +95,8 @@ export const ORCHESTRATION_RUN_HANDLERS: Record<string, CommandHandler> = {
|
||||
fields: (r) => ({
|
||||
runs: r.runs.map((run) => {
|
||||
const { runId, ...rest } = runFields(run)
|
||||
return { id: runId, ...rest }
|
||||
// Objectives are caller-written and unbounded; --full prints them whole.
|
||||
return { id: runId, ...rest, objective: abbreviateOrchestrationText(run.objective) }
|
||||
}),
|
||||
nextCursor: r.nextCursor ?? undefined
|
||||
})
|
||||
|
||||
@@ -3,16 +3,18 @@ const TASK_SPEC_BRIEF_LENGTH = 160
|
||||
export function abbreviateOrchestrationTasks<T extends { spec: string }>(
|
||||
tasks: readonly T[]
|
||||
): (T & { spec_truncated: boolean })[] {
|
||||
return tasks.map((task) => {
|
||||
const spec = task.spec.replace(/\s+/g, ' ').trim()
|
||||
const truncated = spec.length > TASK_SPEC_BRIEF_LENGTH
|
||||
return {
|
||||
...task,
|
||||
spec: truncated ? `${truncateAtCodePoint(spec).trimEnd()}…` : spec,
|
||||
// Whitespace cleanup alone must not trigger agents to refetch an already complete brief.
|
||||
spec_truncated: truncated
|
||||
}
|
||||
})
|
||||
return tasks.map((task) => ({
|
||||
...task,
|
||||
spec: abbreviateOrchestrationText(task.spec),
|
||||
// Whitespace cleanup alone must not trigger agents to refetch an already complete brief.
|
||||
spec_truncated: task.spec.replace(/\s+/g, ' ').trim().length > TASK_SPEC_BRIEF_LENGTH
|
||||
}))
|
||||
}
|
||||
|
||||
/** One line of at most 160 characters, ending in … when cut. */
|
||||
export function abbreviateOrchestrationText(text: string): string {
|
||||
const flat = text.replace(/\s+/g, ' ').trim()
|
||||
return flat.length > TASK_SPEC_BRIEF_LENGTH ? `${truncateAtCodePoint(flat).trimEnd()}…` : flat
|
||||
}
|
||||
|
||||
function truncateAtCodePoint(spec: string): string {
|
||||
|
||||
Reference in New Issue
Block a user