fix(core): harden env loading, OpenAI compatibility, and runtime recovery

This commit is contained in:
William Valentin
2026-02-22 15:56:21 -08:00
parent 387906ce4d
commit dafe9b4d3d
11 changed files with 450 additions and 21 deletions
+76
View File
@@ -49,6 +49,7 @@ function minimalTuiPrivates(value: MinimalTui): {
};
activePromptCancel: (() => void) | null;
activeOperationCancel: (() => void) | null;
commandInFlight: boolean;
running: boolean;
} {
return value as unknown as {
@@ -70,6 +71,7 @@ function minimalTuiPrivates(value: MinimalTui): {
};
activePromptCancel: (() => void) | null;
activeOperationCancel: (() => void) | null;
commandInFlight: boolean;
running: boolean;
};
}
@@ -469,6 +471,38 @@ describe('MinimalTui prompt cancellation', () => {
expect(minimalTuiPrivates(tui).activePromptCancel).toBeNull();
});
it('returns empty string when readline is already closed during question', async () => {
const mockSession = {
id: 'test',
getHistory: () => [],
addMessage: vi.fn(),
clear: vi.fn(),
replaceHistory: vi.fn(),
};
const tui = new MinimalTui({
session: asSession(mockSession),
modelClient: asRouter({}),
systemPrompt: 'test',
});
const questionError = new Error('readline was closed');
(questionError as Error & { code?: string }).code = 'ERR_USE_AFTER_CLOSE';
minimalTuiPrivates(tui).rl = {
once: vi.fn(),
removeListener: vi.fn(),
question: vi.fn(() => {
throw questionError;
}),
write: vi.fn(),
prompt: vi.fn(),
};
await expect(minimalTuiPrivates(tui).prompt('Confirm? ')).resolves.toBe('');
expect(minimalTuiPrivates(tui).activePromptCancel).toBeNull();
});
it('uses Esc to cancel active running operation', () => {
const mockSession = {
id: 'test',
@@ -530,4 +564,46 @@ describe('MinimalTui prompt cancellation', () => {
logSpy.mockRestore();
}
});
it('exits immediately on Ctrl+C when a command is in flight', () => {
const mockSession = {
id: 'test',
getHistory: () => [],
addMessage: vi.fn(),
clear: vi.fn(),
replaceHistory: vi.fn(),
};
const write = vi.fn();
const prompt = vi.fn();
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
try {
const tui = new MinimalTui({
session: asSession(mockSession),
modelClient: asRouter({}),
systemPrompt: 'test',
});
minimalTuiPrivates(tui).rl = {
once: vi.fn(),
removeListener: vi.fn(),
question: vi.fn(),
write,
prompt,
};
minimalTuiPrivates(tui).running = true;
minimalTuiPrivates(tui).commandInFlight = true;
const cancel = vi.fn();
minimalTuiPrivates(tui).activeOperationCancel = cancel;
const shouldExit = minimalTuiPrivates(tui).handleCtrlCPress(1000);
expect(shouldExit).toBe(true);
expect(cancel).toHaveBeenCalledOnce();
expect(write).not.toHaveBeenCalled();
expect(prompt).not.toHaveBeenCalled();
expect(logSpy).not.toHaveBeenCalledWith(expect.stringContaining('Press Ctrl+C again to quit'));
} finally {
logSpy.mockRestore();
}
});
});
+23 -5
View File
@@ -95,6 +95,7 @@ export class MinimalTui {
private busyActive = false;
private verbose = false;
private lastCtrlCAtMs = 0;
private commandInFlight = false;
constructor(private config: MinimalTuiConfig) {}
@@ -325,6 +326,13 @@ export class MinimalTui {
return true;
}
// If a command is currently running (e.g. /council), exit immediately.
// Some command paths are not cancellable mid-flight, so double-press UX can trap users.
if (this.commandInFlight) {
this.activeOperationCancel?.();
return true;
}
const shouldExit = this.lastCtrlCAtMs > 0
&& (nowMs - this.lastCtrlCAtMs) <= MinimalTui.CTRL_C_EXIT_WINDOW_MS;
this.lastCtrlCAtMs = nowMs;
@@ -358,7 +366,12 @@ export class MinimalTui {
continue;
}
await this.handleCommand(command);
this.commandInFlight = true;
try {
await this.handleCommand(command);
} finally {
this.commandInFlight = false;
}
}
}
@@ -421,9 +434,14 @@ export class MinimalTui {
}
};
this.rl.question(promptText, (answer) => {
finish(answer);
});
try {
this.rl.question(promptText, (answer) => {
finish(answer);
});
} catch {
// readline can throw synchronously if it was closed between our guards.
finish('');
}
});
}
@@ -559,7 +577,7 @@ export class MinimalTui {
private async handleCouncilCommand(task: string): Promise<void> {
if (!task.trim()) {
console.log(`${colors.gray}Usage: /council <question or task>${colors.reset}\n`);
console.log(`${colors.gray}Usage: /council <question or task> | /council preflight${colors.reset}\n`);
return;
}
if (!this.config.onCouncil) {