fix(cli): preserve max-turns non-interactive parity

This commit is contained in:
Adam Weidman
2026-03-23 18:47:11 -04:00
committed by Adam Weidman
parent 6795567d28
commit 1088e1febf
3 changed files with 31 additions and 63 deletions
@@ -8,6 +8,14 @@ exports[`runNonInteractive > should emit appropriate error event in streaming JS
"
`;
exports[`runNonInteractive > should emit appropriate error event in streaming JSON mode: 'max session turns' 1`] = `
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Max turns test"}
{"type":"error","timestamp":"<TIMESTAMP>","severity":"error","message":"Maximum session turns exceeded"}
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"models":{}}}
"
`;
exports[`runNonInteractive > should emit appropriate events for streaming JSON output 1`] = `
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Stream test"}
+9 -62
View File
@@ -679,21 +679,6 @@ describe('runNonInteractive', () => {
).rejects.toThrow('process.exit(53) called');
});
it('should exit when the session reports max turns through agent_end', async () => {
mockGeminiClient.sendMessageStream.mockReturnValue(
createStreamFromEvents([{ type: GeminiEventType.MaxSessionTurns }]),
);
await expect(
runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Trigger max turns event',
prompt_id: 'prompt-id-max-turns-event',
}),
).rejects.toThrow('process.exit(53) called');
});
it('should preprocess @include commands before sending to the model', async () => {
// 1. Mock the imported atCommandProcessor
const { handleAtCommand } = await import(
@@ -1855,9 +1840,17 @@ describe('runNonInteractive', () => {
input: 'Loop test',
promptId: 'prompt-id-loop',
},
{
name: 'max session turns',
events: [
{ type: GeminiEventType.MaxSessionTurns },
] as ServerGeminiStreamEvent[],
input: 'Max turns test',
promptId: 'prompt-id-max-turns',
},
])(
'should emit appropriate error event in streaming JSON mode: $name',
async ({ name, events, input, promptId }) => {
async ({ events, input, promptId }) => {
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(
OutputFormat.STREAM_JSON,
);
@@ -1895,52 +1888,6 @@ describe('runNonInteractive', () => {
},
);
it('should emit a terminal max-turns error event in streaming JSON mode', async () => {
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(
OutputFormat.STREAM_JSON,
);
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(
MOCK_SESSION_METRICS,
);
mockGeminiClient.sendMessageStream.mockReturnValue(
createStreamFromEvents([
{ type: GeminiEventType.MaxSessionTurns },
{
type: GeminiEventType.Finished,
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
},
]),
);
try {
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Max turns test',
prompt_id: 'prompt-id-max-turns',
});
} catch (_error) {
// Expected exit
}
const streamEvents = getWrittenOutput()
.trim()
.split('\n')
.map((line) => JSON.parse(line) as Record<string, unknown>);
expect(streamEvents).toHaveLength(3);
expect(streamEvents[2]).toMatchObject({
type: 'result',
status: 'error',
error: {
type: 'FatalTurnLimitedError',
message:
'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.',
},
});
});
it('should log error when tool recording fails', async () => {
const toolCallEvent: ServerGeminiStreamEvent = {
type: GeminiEventType.ToolCallRequest,
+14 -1
View File
@@ -523,7 +523,20 @@ export async function runNonInteractive({
if (event.reason === 'aborted') {
runTerminalExitHandler(() => handleCancellationError(config));
} else if (event.reason === 'max_turns') {
runTerminalExitHandler(() => handleMaxTurnsExceededError(config));
const isConfiguredTurnLimit =
typeof event.data?.['maxTurns'] === 'number' ||
typeof event.data?.['turnCount'] === 'number';
if (isConfiguredTurnLimit) {
runTerminalExitHandler(() => handleMaxTurnsExceededError(config));
} else if (streamFormatter) {
streamFormatter.emitEvent({
type: JsonStreamEventType.ERROR,
timestamp: new Date().toISOString(),
severity: 'error',
message: 'Maximum session turns exceeded',
});
}
}
const stopMessage =