Adding session id as part of json o/p (#14504)

This commit is contained in:
Jainam M
2025-12-04 22:36:20 +05:30
committed by GitHub
parent 84f521b1c6
commit 8b0a8f47c1
8 changed files with 126 additions and 27 deletions

View File

@@ -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('');
});
});

View File

@@ -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',

View File

@@ -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
}

View File

@@ -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:

View File

@@ -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);

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -19,6 +19,7 @@ export interface JsonError {
}
export interface JsonOutput {
session_id?: string;
response?: string;
stats?: SessionMetrics;
error?: JsonError;