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:
Jinwoo Hong
2026-10-31 03:52:18 -04:00
committed by GitHub
parent 2cc33b8d89
commit bf13e6cc76
3 changed files with 159 additions and 11 deletions
@@ -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
})
+12 -10
View File
@@ -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 {