mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-05 01:01:42 +00:00
fix(codex): surface thread system errors (#519)
Co-authored-by: Liu-KM <Liu-KM@users.noreply.github.com>
This commit is contained in:
@@ -5,7 +5,11 @@ import type { EnhancedMode } from './loop';
|
||||
const harness = vi.hoisted(() => ({
|
||||
notifications: [] as Array<{ method: string; params: unknown }>,
|
||||
registerRequestCalls: [] as string[],
|
||||
initializeCalls: [] as unknown[]
|
||||
initializeCalls: [] as unknown[],
|
||||
startThreadIds: [] as string[],
|
||||
resumeThreadIds: [] as string[],
|
||||
startTurnThreadIds: [] as string[],
|
||||
remainingThreadSystemErrors: 0
|
||||
}));
|
||||
|
||||
vi.mock('./codexAppServerClient', () => {
|
||||
@@ -28,23 +32,41 @@ vi.mock('./codexAppServerClient', () => {
|
||||
}
|
||||
|
||||
async startThread(): Promise<{ thread: { id: string }; model: string }> {
|
||||
return { thread: { id: 'thread-anonymous' }, model: 'gpt-5.4' };
|
||||
const id = `thread-${harness.startThreadIds.length + 1}`;
|
||||
harness.startThreadIds.push(id);
|
||||
return { thread: { id }, model: 'gpt-5.4' };
|
||||
}
|
||||
|
||||
async resumeThread(): Promise<{ thread: { id: string }; model: string }> {
|
||||
return { thread: { id: 'thread-anonymous' }, model: 'gpt-5.4' };
|
||||
async resumeThread(params?: { threadId?: string }): Promise<{ thread: { id: string }; model: string }> {
|
||||
const id = params?.threadId ?? 'thread-resumed';
|
||||
harness.resumeThreadIds.push(id);
|
||||
return { thread: { id }, model: 'gpt-5.4' };
|
||||
}
|
||||
|
||||
async startTurn(): Promise<{ turn: Record<string, never> }> {
|
||||
const started = { turn: {} };
|
||||
async startTurn(params?: { threadId?: string }): Promise<{ turn: { id?: string } }> {
|
||||
const threadId = params?.threadId ?? 'thread-unknown';
|
||||
harness.startTurnThreadIds.push(threadId);
|
||||
const turnId = `turn-${harness.startTurnThreadIds.length}`;
|
||||
const started = { turn: { id: turnId } };
|
||||
harness.notifications.push({ method: 'turn/started', params: started });
|
||||
this.notificationHandler?.('turn/started', started);
|
||||
|
||||
const completed = { status: 'Completed', turn: {} };
|
||||
if (harness.remainingThreadSystemErrors > 0) {
|
||||
harness.remainingThreadSystemErrors -= 1;
|
||||
const failed = {
|
||||
thread: { id: threadId },
|
||||
status: { type: 'systemError' }
|
||||
};
|
||||
harness.notifications.push({ method: 'thread/status/changed', params: failed });
|
||||
this.notificationHandler?.('thread/status/changed', failed);
|
||||
return { turn: { id: turnId } };
|
||||
}
|
||||
|
||||
const completed = { status: 'Completed', turn: { id: turnId } };
|
||||
harness.notifications.push({ method: 'turn/completed', params: completed });
|
||||
this.notificationHandler?.('turn/completed', completed);
|
||||
|
||||
return { turn: {} };
|
||||
return { turn: { id: turnId } };
|
||||
}
|
||||
|
||||
async interruptTurn(): Promise<Record<string, never>> {
|
||||
@@ -80,9 +102,15 @@ function createMode(): EnhancedMode {
|
||||
};
|
||||
}
|
||||
|
||||
function createSessionStub() {
|
||||
function createSessionStub(messages = ['hello from launcher test']) {
|
||||
const queue = new MessageQueue2<EnhancedMode>((mode) => JSON.stringify(mode));
|
||||
queue.push('hello from launcher test', createMode());
|
||||
messages.forEach((message, index) => {
|
||||
if (index === 0 && messages.length > 1) {
|
||||
queue.pushIsolateAndClear(message, createMode());
|
||||
} else {
|
||||
queue.push(message, createMode());
|
||||
}
|
||||
});
|
||||
queue.close();
|
||||
|
||||
const sessionEvents: Array<{ type: string; [key: string]: unknown }> = [];
|
||||
@@ -168,9 +196,13 @@ describe('codexRemoteLauncher', () => {
|
||||
harness.notifications = [];
|
||||
harness.registerRequestCalls = [];
|
||||
harness.initializeCalls = [];
|
||||
harness.startThreadIds = [];
|
||||
harness.resumeThreadIds = [];
|
||||
harness.startTurnThreadIds = [];
|
||||
harness.remainingThreadSystemErrors = 0;
|
||||
});
|
||||
|
||||
it('finishes a turn and emits ready when task lifecycle events omit turn_id', async () => {
|
||||
it('finishes a turn and emits ready when task lifecycle events include turn_id', async () => {
|
||||
const {
|
||||
session,
|
||||
sessionEvents,
|
||||
@@ -182,7 +214,7 @@ describe('codexRemoteLauncher', () => {
|
||||
const exitReason = await codexRemoteLauncher(session as never);
|
||||
|
||||
expect(exitReason).toBe('exit');
|
||||
expect(foundSessionIds).toContain('thread-anonymous');
|
||||
expect(foundSessionIds).toContain('thread-1');
|
||||
expect(getModel()).toBe('gpt-5.4');
|
||||
expect(harness.initializeCalls).toEqual([{
|
||||
clientInfo: {
|
||||
@@ -198,4 +230,34 @@ describe('codexRemoteLauncher', () => {
|
||||
expect(thinkingChanges).toContain(true);
|
||||
expect(session.thinking).toBe(false);
|
||||
});
|
||||
|
||||
it('surfaces thread-level systemError as a visible failure and emits ready', async () => {
|
||||
harness.remainingThreadSystemErrors = 1;
|
||||
const { session, sessionEvents } = createSessionStub();
|
||||
|
||||
const exitReason = await codexRemoteLauncher(session as never);
|
||||
|
||||
expect(exitReason).toBe('exit');
|
||||
expect(harness.notifications.map((entry) => entry.method)).toEqual(['turn/started', 'thread/status/changed']);
|
||||
expect(sessionEvents).toContainEqual({
|
||||
type: 'message',
|
||||
message: 'Task failed: Codex thread entered systemError'
|
||||
});
|
||||
expect(sessionEvents.filter((event) => event.type === 'ready').length).toBeGreaterThanOrEqual(1);
|
||||
expect(session.thinking).toBe(false);
|
||||
});
|
||||
|
||||
it('starts a fresh thread for the next queued message after thread-level systemError', async () => {
|
||||
harness.remainingThreadSystemErrors = 1;
|
||||
const { session } = createSessionStub(['first message', 'second message']);
|
||||
|
||||
const exitReason = await codexRemoteLauncher(session as never);
|
||||
|
||||
expect(exitReason).toBe('exit');
|
||||
expect(harness.startThreadIds).toEqual(['thread-1', 'thread-2']);
|
||||
expect(harness.resumeThreadIds).toEqual([]);
|
||||
expect(harness.startTurnThreadIds).toEqual(['thread-1', 'thread-2']);
|
||||
expect(session.sessionId).toBe('thread-2');
|
||||
expect(session.thinking).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user