fix(codex): keep local session timeout non-fatal (#304)

This commit is contained in:
ROOOO
2026-03-18 07:27:23 +08:00
committed by GitHub
parent f24ea4fea8
commit a30bb398e1
2 changed files with 75 additions and 25 deletions
+71 -23
View File
@@ -1,7 +1,9 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
const harness = vi.hoisted(() => ({
launches: [] as Array<Record<string, unknown>>
launches: [] as Array<Record<string, unknown>>,
sessionScannerCalls: [] as Array<Record<string, unknown>>,
scannerFailureMessage: 'No Codex session found within 120000ms for cwd c:\\workspace\\project; refusing fallback.'
}));
vi.mock('./codexLocal', () => ({
@@ -21,10 +23,18 @@ vi.mock('./utils/buildHapiMcpBridge', () => ({
}));
vi.mock('./utils/codexSessionScanner', () => ({
createCodexSessionScanner: async () => ({
cleanup: async () => {},
onNewSession: () => {}
})
createCodexSessionScanner: async (opts: {
onSessionMatchFailed?: (message: string) => void;
}) => {
harness.sessionScannerCalls.push(opts as Record<string, unknown>);
return {
cleanup: async () => {},
onNewSession: () => {},
triggerFailure: () => {
opts.onSessionMatchFailed?.(harness.scannerFailureMessage);
}
};
}
}));
vi.mock('@/modules/common/launcher/BaseLocalLauncher', () => ({
@@ -44,33 +54,55 @@ vi.mock('@/modules/common/launcher/BaseLocalLauncher', () => ({
import { codexLocalLauncher } from './codexLocalLauncher';
function createSessionStub(permissionMode: 'default' | 'read-only' | 'safe-yolo' | 'yolo', codexArgs?: string[]) {
function createQueueStub() {
return {
sessionId: null,
path: '/tmp/worktree',
startedBy: 'terminal' as const,
startingMode: 'local' as const,
codexArgs,
client: {
rpcHandlerManager: {}
size: () => 0,
reset: () => {},
setOnMessage: () => {}
};
}
function createSessionStub(permissionMode: 'default' | 'read-only' | 'safe-yolo' | 'yolo', codexArgs?: string[], path = '/tmp/worktree') {
const sessionEvents: Array<{ type: string; message?: string }> = [];
let localLaunchFailure: { message: string; exitReason: 'switch' | 'exit' } | null = null;
return {
session: {
sessionId: null,
path,
startedBy: 'terminal' as const,
startingMode: 'local' as const,
codexArgs,
client: {
rpcHandlerManager: {
registerHandler: () => {}
}
},
getPermissionMode: () => permissionMode,
onSessionFound: () => {},
sendSessionEvent: (event: { type: string; message?: string }) => {
sessionEvents.push(event);
},
recordLocalLaunchFailure: (message: string, exitReason: 'switch' | 'exit') => {
localLaunchFailure = { message, exitReason };
},
sendUserMessage: () => {},
sendCodexMessage: () => {},
queue: createQueueStub()
},
getPermissionMode: () => permissionMode,
onSessionFound: () => {},
sendSessionEvent: () => {},
recordLocalLaunchFailure: () => {},
sendUserMessage: () => {},
sendCodexMessage: () => {},
queue: {}
sessionEvents,
getLocalLaunchFailure: () => localLaunchFailure
};
}
describe('codexLocalLauncher', () => {
afterEach(() => {
harness.launches = [];
harness.sessionScannerCalls = [];
});
it('rebuilds approval and sandbox args from yolo mode', async () => {
const session = createSessionStub('yolo', [
const { session } = createSessionStub('yolo', [
'--sandbox',
'read-only',
'--ask-for-approval',
@@ -94,7 +126,7 @@ describe('codexLocalLauncher', () => {
});
it('preserves raw Codex approval flags in default mode', async () => {
const session = createSessionStub('default', [
const { session } = createSessionStub('default', [
'--ask-for-approval',
'on-request',
'--sandbox',
@@ -117,7 +149,7 @@ describe('codexLocalLauncher', () => {
});
it('keeps sandbox escalation available in safe-yolo mode', async () => {
const session = createSessionStub('safe-yolo', [
const { session } = createSessionStub('safe-yolo', [
'--ask-for-approval',
'never',
'--sandbox',
@@ -138,4 +170,20 @@ describe('codexLocalLauncher', () => {
'o3'
]);
});
it('warns on session match failure without aborting local Codex launch', async () => {
const { session, sessionEvents, getLocalLaunchFailure } = createSessionStub('default', undefined, 'c:\\workspace\\project');
await codexLocalLauncher(session as never);
const scannerCall = harness.sessionScannerCalls[0] as { onSessionMatchFailed?: (message: string) => void } | undefined;
scannerCall?.onSessionMatchFailed?.(harness.scannerFailureMessage);
expect(harness.launches).toHaveLength(1);
expect(getLocalLaunchFailure()).toBeNull();
expect(sessionEvents).toContainEqual({
type: 'message',
message: `${harness.scannerFailureMessage} Keeping local Codex running; remote transcript sync may be unavailable for this launch.`
});
});
});
+4 -2
View File
@@ -60,8 +60,10 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
const handleSessionMatchFailed = (message: string) => {
logger.warn(`[codex-local]: ${message}`);
session.sendSessionEvent({ type: 'message', message });
launcher.control.requestExit();
session.sendSessionEvent({
type: 'message',
message: `${message} Keeping local Codex running; remote transcript sync may be unavailable for this launch.`
});
};
scanner = await createCodexSessionScanner({