mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-08 01:43:07 +00:00
fix(codex,web): preserve late command result identity and timing (#2239)
This commit is contained in:
@@ -5,6 +5,24 @@ import { codexPlanProposalId } from './plan';
|
||||
import { extractNotifySummary } from '@hapi/protocol/messages';
|
||||
|
||||
describe('shared history projection', () => {
|
||||
it.each([undefined, 'root'])('projects command outcomes for live output and replay (parent: %s)', async parentThreadId => {
|
||||
const send = vi.fn();
|
||||
const session = { getMetadata: () => ({}), sendAgentMessage: send } as unknown as ApiSessionClient;
|
||||
const projection = new SharedCodexProjection(session, 'thread', async () => {}, parentThreadId);
|
||||
const items = [
|
||||
{ id: 'failed', type: 'commandExecution', command: 'sleep 100', status: 'failed', exitCode: -1, durationMs: 2000 },
|
||||
{ id: 'cancelled', type: 'commandExecution', command: 'sleep 100', status: 'cancelled' },
|
||||
{ id: 'exit', type: 'commandExecution', command: 'exit 2', status: 'completed', exitCode: 2 },
|
||||
{ id: 'ok', type: 'commandExecution', command: 'echo ok', status: 'completed', exitCode: 0 }
|
||||
];
|
||||
for (const item of items) await projection.notification('item/completed', { threadId: 'thread', turnId: 'turn', item });
|
||||
const results = () => send.mock.calls.map(([body]) => parentThreadId ? body.message : body).filter(body => body.type === 'tool-call-result');
|
||||
expect(results().map(body => body.is_error)).toEqual([true, true, true, false]);
|
||||
expect(results()[0].output).toMatchObject({ command: 'sleep 100', exit_code: -1, duration_ms: 2000 });
|
||||
projection.reset(); send.mockClear();
|
||||
await projection.history({ turns: [{ id: 'turn', status: 'completed', items }] });
|
||||
expect(results().map(body => body.is_error)).toEqual([true, true, true, false]);
|
||||
});
|
||||
it.each([undefined, 'root'])('publishes only final summaries in live output and history replay (parent: %s)', async parentThreadId => {
|
||||
const send = vi.fn();
|
||||
const session = { getMetadata: () => ({}), sendAgentMessage: send } as unknown as ApiSessionClient;
|
||||
|
||||
@@ -145,7 +145,10 @@ export class SharedCodexProjection {
|
||||
else if (event.type === 'exec_command_begin' && callId) {
|
||||
send({ type: 'tool-call', name: 'CodexBash', callId, input: event }, key);
|
||||
} else if (event.type === 'exec_command_end' && callId) {
|
||||
send({ type: 'tool-call-result', callId, output: { ...event, stdout: event.output } }, key);
|
||||
const failed = ['failed', 'cancelled', 'canceled', 'declined', 'interrupted'].includes(String(event.status))
|
||||
|| (typeof event.exit_code === 'number' && event.exit_code !== 0)
|
||||
|| Boolean(event.error);
|
||||
send({ type: 'tool-call-result', callId, output: { ...event, stdout: event.output }, is_error: failed }, key);
|
||||
} else if (event.type === 'patch_apply_begin' && callId) {
|
||||
send({ type: 'tool-call', name: 'CodexPatch', callId, input: { changes: event.changes, auto_approved: event.auto_approved } }, key);
|
||||
} else if (event.type === 'patch_apply_end' && callId) {
|
||||
|
||||
@@ -215,6 +215,16 @@ describe('AppServerEventConverter', () => {
|
||||
}]);
|
||||
});
|
||||
|
||||
it('preserves command identity when completion arrives without a start', () => {
|
||||
const converter = new AppServerEventConverter();
|
||||
const events = converter.handleNotification('item/completed', {
|
||||
item: { id: 'late-command', type: 'commandExecution', command: 'node server.js', cwd: '/repo',
|
||||
status: 'failed', exitCode: -1, durationMs: 16_636_720 }
|
||||
});
|
||||
expect(events).toEqual([expect.objectContaining({ type: 'exec_command_end', call_id: 'late-command',
|
||||
command: 'node server.js', cwd: '/repo', status: 'failed', exit_code: -1, duration_ms: 16_636_720 })]);
|
||||
});
|
||||
|
||||
it('maps MCP tool call items', () => {
|
||||
const converter = new AppServerEventConverter();
|
||||
|
||||
|
||||
@@ -1067,6 +1067,9 @@ export class AppServerEventConverter {
|
||||
|
||||
if (method === 'item/completed') {
|
||||
const meta = this.commandMeta.get(itemId) ?? {};
|
||||
// Completion snapshots can arrive without a start (reconnect or late shutdown).
|
||||
const command = extractCommand(item.command ?? item.cmd ?? item.args);
|
||||
const cwd = asString(item.cwd ?? item.workingDirectory ?? item.working_directory);
|
||||
const output = asString(item.aggregatedOutput ?? item.aggregated_output ?? item.output ?? item.result ?? item.stdout)
|
||||
?? this.commandOutputBuffers.get(itemId);
|
||||
const stderr = asString(item.stderr);
|
||||
@@ -1079,6 +1082,8 @@ export class AppServerEventConverter {
|
||||
type: 'exec_command_end',
|
||||
call_id: itemId,
|
||||
...meta,
|
||||
...(command ? { command } : {}),
|
||||
...(cwd ? { cwd } : {}),
|
||||
...(output ? { output } : {}),
|
||||
...(stderr ? { stderr } : {}),
|
||||
...(error ? { error } : {}),
|
||||
|
||||
Reference in New Issue
Block a user