mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
Adding session id as part of json o/p (#14504)
This commit is contained in:
@@ -37,6 +37,15 @@ describe('JSON output', () => {
|
||||
expect(typeof parsed.stats).toBe('object');
|
||||
});
|
||||
|
||||
it('should return a valid JSON with a session ID', async () => {
|
||||
const result = await rig.run('Hello', '--output-format', 'json');
|
||||
const parsed = JSON.parse(result);
|
||||
|
||||
expect(parsed).toHaveProperty('session_id');
|
||||
expect(typeof parsed.session_id).toBe('string');
|
||||
expect(parsed.session_id).not.toBe('');
|
||||
});
|
||||
|
||||
it('should return a JSON error for sd auth mismatch before running', async () => {
|
||||
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
|
||||
await rig.setup('json-output-auth-mismatch', {
|
||||
@@ -87,6 +96,9 @@ describe('JSON output', () => {
|
||||
"enforced authentication type is 'gemini-api-key'",
|
||||
);
|
||||
expect(payload.error.message).toContain("current type is 'oauth-personal'");
|
||||
expect(payload).toHaveProperty('session_id');
|
||||
expect(typeof payload.session_id).toBe('string');
|
||||
expect(payload.session_id).not.toBe('');
|
||||
});
|
||||
|
||||
it('should not exit on tool errors and allow model to self-correct in JSON mode', async () => {
|
||||
@@ -129,5 +141,9 @@ describe('JSON output', () => {
|
||||
|
||||
// Should NOT have an error field at the top level
|
||||
expect(parsed.error).toBeUndefined();
|
||||
|
||||
expect(parsed).toHaveProperty('session_id');
|
||||
expect(typeof parsed.session_id).toBe('string');
|
||||
expect(parsed.session_id).not.toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -637,7 +637,11 @@ describe('runNonInteractive', () => {
|
||||
);
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify(
|
||||
{ response: 'Hello World', stats: MOCK_SESSION_METRICS },
|
||||
{
|
||||
session_id: 'test-session-id',
|
||||
response: 'Hello World',
|
||||
stats: MOCK_SESSION_METRICS,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
@@ -720,7 +724,15 @@ describe('runNonInteractive', () => {
|
||||
|
||||
// This should output JSON with empty response but include stats
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify({ response: '', stats: MOCK_SESSION_METRICS }, null, 2),
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: 'test-session-id',
|
||||
response: '',
|
||||
stats: MOCK_SESSION_METRICS,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -755,7 +767,15 @@ describe('runNonInteractive', () => {
|
||||
|
||||
// This should output JSON with empty response but include stats
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify({ response: '', stats: MOCK_SESSION_METRICS }, null, 2),
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: 'test-session-id',
|
||||
response: '',
|
||||
stats: MOCK_SESSION_METRICS,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -792,6 +812,7 @@ describe('runNonInteractive', () => {
|
||||
expect(consoleErrorJsonSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: 'test-session-id',
|
||||
error: {
|
||||
type: 'Error',
|
||||
message: 'Invalid input provided',
|
||||
@@ -837,6 +858,7 @@ describe('runNonInteractive', () => {
|
||||
expect(consoleErrorJsonSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: 'test-session-id',
|
||||
error: {
|
||||
type: 'FatalInputError',
|
||||
message: 'Invalid command syntax provided',
|
||||
|
||||
@@ -428,7 +428,9 @@ export async function runNonInteractive({
|
||||
} else if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
const formatter = new JsonFormatter();
|
||||
const stats = uiTelemetryService.getMetrics();
|
||||
textOutput.write(formatter.format(responseText, stats));
|
||||
textOutput.write(
|
||||
formatter.format(config.getSessionId(), responseText, stats),
|
||||
);
|
||||
} else {
|
||||
textOutput.ensureTrailingNewline(); // Ensure a final newline
|
||||
}
|
||||
|
||||
@@ -29,18 +29,20 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
return `API Error: ${String(error)}`;
|
||||
}),
|
||||
JsonFormatter: vi.fn().mockImplementation(() => ({
|
||||
formatError: vi.fn((error: Error, code?: string | number) =>
|
||||
JSON.stringify(
|
||||
{
|
||||
error: {
|
||||
type: error.constructor.name,
|
||||
message: error.message,
|
||||
...(code && { code }),
|
||||
formatError: vi.fn(
|
||||
(error: Error, code?: string | number, sessionId?: string) =>
|
||||
JSON.stringify(
|
||||
{
|
||||
...(sessionId && { session_id: sessionId }),
|
||||
error: {
|
||||
type: error.constructor.name,
|
||||
message: error.message,
|
||||
...(code && { code }),
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
),
|
||||
})),
|
||||
StreamJsonFormatter: vi.fn().mockImplementation(() => ({
|
||||
@@ -77,6 +79,8 @@ describe('errors', () => {
|
||||
let processExitSpy: MockInstance;
|
||||
let consoleErrorSpy: MockInstance;
|
||||
|
||||
const TEST_SESSION_ID = 'test-session-123';
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
vi.clearAllMocks();
|
||||
@@ -93,6 +97,7 @@ describe('errors', () => {
|
||||
mockConfig = {
|
||||
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'test' }),
|
||||
getSessionId: vi.fn().mockReturnValue(TEST_SESSION_ID),
|
||||
} as unknown as Config;
|
||||
});
|
||||
|
||||
@@ -166,6 +171,7 @@ describe('errors', () => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: TEST_SESSION_ID,
|
||||
error: {
|
||||
type: 'Error',
|
||||
message: 'Test error',
|
||||
@@ -188,6 +194,7 @@ describe('errors', () => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: TEST_SESSION_ID,
|
||||
error: {
|
||||
type: 'Error',
|
||||
message: 'Test error',
|
||||
@@ -210,6 +217,7 @@ describe('errors', () => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: TEST_SESSION_ID,
|
||||
error: {
|
||||
type: 'FatalInputError',
|
||||
message: 'Fatal error',
|
||||
@@ -246,6 +254,7 @@ describe('errors', () => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: TEST_SESSION_ID,
|
||||
error: {
|
||||
type: 'Error',
|
||||
message: 'Error with status',
|
||||
@@ -398,6 +407,7 @@ describe('errors', () => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: TEST_SESSION_ID,
|
||||
error: {
|
||||
type: 'FatalToolExecutionError',
|
||||
message: 'Error executing tool test-tool: Tool failed',
|
||||
@@ -467,6 +477,7 @@ describe('errors', () => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: TEST_SESSION_ID,
|
||||
error: {
|
||||
type: 'FatalCancellationError',
|
||||
message: 'Operation cancelled.',
|
||||
@@ -529,6 +540,7 @@ describe('errors', () => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify(
|
||||
{
|
||||
session_id: TEST_SESSION_ID,
|
||||
error: {
|
||||
type: 'FatalTurnLimitedError',
|
||||
message:
|
||||
|
||||
@@ -100,6 +100,7 @@ export function handleError(
|
||||
const formattedError = formatter.formatError(
|
||||
error instanceof Error ? error : new Error(getErrorMessage(error)),
|
||||
errorCode,
|
||||
config.getSessionId(),
|
||||
);
|
||||
|
||||
console.error(formattedError);
|
||||
@@ -152,6 +153,7 @@ export function handleToolError(
|
||||
const formattedError = formatter.formatError(
|
||||
toolExecutionError,
|
||||
errorType ?? toolExecutionError.exitCode,
|
||||
config.getSessionId(),
|
||||
);
|
||||
console.error(formattedError);
|
||||
} else {
|
||||
@@ -191,6 +193,7 @@ export function handleCancellationError(config: Config): never {
|
||||
const formattedError = formatter.formatError(
|
||||
cancellationError,
|
||||
cancellationError.exitCode,
|
||||
config.getSessionId(),
|
||||
);
|
||||
|
||||
console.error(formattedError);
|
||||
@@ -231,6 +234,7 @@ export function handleMaxTurnsExceededError(config: Config): never {
|
||||
const formattedError = formatter.formatError(
|
||||
maxTurnsError,
|
||||
maxTurnsError.exitCode,
|
||||
config.getSessionId(),
|
||||
);
|
||||
|
||||
console.error(formattedError);
|
||||
|
||||
@@ -13,18 +13,30 @@ describe('JsonFormatter', () => {
|
||||
it('should format the response as JSON', () => {
|
||||
const formatter = new JsonFormatter();
|
||||
const response = 'This is a test response.';
|
||||
const formatted = formatter.format(response);
|
||||
const formatted = formatter.format(undefined, response);
|
||||
const expected = {
|
||||
response,
|
||||
};
|
||||
expect(JSON.parse(formatted)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should format the response as JSON with a session ID', () => {
|
||||
const formatter = new JsonFormatter();
|
||||
const response = 'This is a test response.';
|
||||
const sessionId = 'test-session-id';
|
||||
const formatted = formatter.format(sessionId, response);
|
||||
const expected = {
|
||||
session_id: sessionId,
|
||||
response,
|
||||
};
|
||||
expect(JSON.parse(formatted)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should strip ANSI escape sequences from response text', () => {
|
||||
const formatter = new JsonFormatter();
|
||||
const responseWithAnsi =
|
||||
'\x1B[31mRed text\x1B[0m and \x1B[32mGreen text\x1B[0m';
|
||||
const formatted = formatter.format(responseWithAnsi);
|
||||
const formatted = formatter.format(undefined, responseWithAnsi);
|
||||
const parsed = JSON.parse(formatted);
|
||||
expect(parsed.response).toBe('Red text and Green text');
|
||||
});
|
||||
@@ -33,7 +45,7 @@ describe('JsonFormatter', () => {
|
||||
const formatter = new JsonFormatter();
|
||||
const responseWithControlChars =
|
||||
'Text with\x07 bell\x08 and\x0B vertical tab';
|
||||
const formatted = formatter.format(responseWithControlChars);
|
||||
const formatted = formatter.format(undefined, responseWithControlChars);
|
||||
const parsed = JSON.parse(formatted);
|
||||
// Only ANSI codes are stripped, other control chars are preserved
|
||||
expect(parsed.response).toBe('Text with\x07 bell\x08 and\x0B vertical tab');
|
||||
@@ -42,7 +54,7 @@ describe('JsonFormatter', () => {
|
||||
it('should preserve newlines and tabs in response text', () => {
|
||||
const formatter = new JsonFormatter();
|
||||
const responseWithWhitespace = 'Line 1\nLine 2\r\nLine 3\twith tab';
|
||||
const formatted = formatter.format(responseWithWhitespace);
|
||||
const formatted = formatter.format(undefined, responseWithWhitespace);
|
||||
const parsed = JSON.parse(formatted);
|
||||
expect(parsed.response).toBe('Line 1\nLine 2\r\nLine 3\twith tab');
|
||||
});
|
||||
@@ -114,7 +126,7 @@ describe('JsonFormatter', () => {
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
};
|
||||
const formatted = formatter.format(response, stats);
|
||||
const formatted = formatter.format(undefined, response, stats);
|
||||
const expected = {
|
||||
response,
|
||||
stats,
|
||||
@@ -129,7 +141,7 @@ describe('JsonFormatter', () => {
|
||||
message: 'Invalid input provided',
|
||||
code: 400,
|
||||
};
|
||||
const formatted = formatter.format(undefined, undefined, error);
|
||||
const formatted = formatter.format(undefined, undefined, undefined, error);
|
||||
const expected = {
|
||||
error,
|
||||
};
|
||||
@@ -144,7 +156,7 @@ describe('JsonFormatter', () => {
|
||||
message: 'Request timed out',
|
||||
code: 'TIMEOUT',
|
||||
};
|
||||
const formatted = formatter.format(response, undefined, error);
|
||||
const formatted = formatter.format(undefined, response, undefined, error);
|
||||
const expected = {
|
||||
response,
|
||||
error,
|
||||
@@ -167,6 +179,23 @@ describe('JsonFormatter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should format error using formatError method with a session ID', () => {
|
||||
const formatter = new JsonFormatter();
|
||||
const error = new Error('Something went wrong');
|
||||
const sessionId = 'test-session-id';
|
||||
const formatted = formatter.formatError(error, 500, sessionId);
|
||||
const parsed = JSON.parse(formatted);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
session_id: sessionId,
|
||||
error: {
|
||||
type: 'Error',
|
||||
message: 'Something went wrong',
|
||||
code: 500,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should format custom error using formatError method', () => {
|
||||
class CustomError extends Error {
|
||||
constructor(message: string) {
|
||||
@@ -177,7 +206,7 @@ describe('JsonFormatter', () => {
|
||||
|
||||
const formatter = new JsonFormatter();
|
||||
const error = new CustomError('Custom error occurred');
|
||||
const formatted = formatter.formatError(error);
|
||||
const formatted = formatter.formatError(error, undefined);
|
||||
const parsed = JSON.parse(formatted);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
@@ -217,7 +246,7 @@ describe('JsonFormatter', () => {
|
||||
code: 429,
|
||||
};
|
||||
|
||||
const formatted = formatter.format(response, stats, error);
|
||||
const formatted = formatter.format(undefined, response, stats, error);
|
||||
const expected = {
|
||||
response,
|
||||
stats,
|
||||
|
||||
@@ -9,9 +9,18 @@ import type { SessionMetrics } from '../telemetry/uiTelemetry.js';
|
||||
import type { JsonError, JsonOutput } from './types.js';
|
||||
|
||||
export class JsonFormatter {
|
||||
format(response?: string, stats?: SessionMetrics, error?: JsonError): string {
|
||||
format(
|
||||
sessionId?: string,
|
||||
response?: string,
|
||||
stats?: SessionMetrics,
|
||||
error?: JsonError,
|
||||
): string {
|
||||
const output: JsonOutput = {};
|
||||
|
||||
if (sessionId) {
|
||||
output.session_id = sessionId;
|
||||
}
|
||||
|
||||
if (response !== undefined) {
|
||||
output.response = stripAnsi(response);
|
||||
}
|
||||
@@ -27,13 +36,17 @@ export class JsonFormatter {
|
||||
return JSON.stringify(output, null, 2);
|
||||
}
|
||||
|
||||
formatError(error: Error, code?: string | number): string {
|
||||
formatError(
|
||||
error: Error,
|
||||
code?: string | number,
|
||||
sessionId?: string,
|
||||
): string {
|
||||
const jsonError: JsonError = {
|
||||
type: error.constructor.name,
|
||||
message: stripAnsi(error.message),
|
||||
...(code && { code }),
|
||||
};
|
||||
|
||||
return this.format(undefined, undefined, jsonError);
|
||||
return this.format(sessionId, undefined, undefined, jsonError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface JsonError {
|
||||
}
|
||||
|
||||
export interface JsonOutput {
|
||||
session_id?: string;
|
||||
response?: string;
|
||||
stats?: SessionMetrics;
|
||||
error?: JsonError;
|
||||
|
||||
Reference in New Issue
Block a user