Sync upstream Gemini-CLI v0.8.2 (#838)

This commit is contained in:
tanzhenxin
2025-10-23 09:27:04 +08:00
committed by GitHub
parent 096fabb5d6
commit eb95c131be
644 changed files with 70389 additions and 23709 deletions

View File

@@ -0,0 +1,301 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { expect, describe, it } from 'vitest';
import type { SessionMetrics } from '../telemetry/uiTelemetry.js';
import { JsonFormatter } from './json-formatter.js';
import type { JsonError } from './types.js';
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 expected = {
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 parsed = JSON.parse(formatted);
expect(parsed.response).toBe('Red text and Green text');
});
it('should strip control characters from response text', () => {
const formatter = new JsonFormatter();
const responseWithControlChars =
'Text with\x07 bell\x08 and\x0B vertical tab';
const formatted = formatter.format(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');
});
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 parsed = JSON.parse(formatted);
expect(parsed.response).toBe('Line 1\nLine 2\r\nLine 3\twith tab');
});
it('should format the response as JSON with stats', () => {
const formatter = new JsonFormatter();
const response = 'This is a test response.';
const stats: SessionMetrics = {
models: {
'gemini-2.5-pro': {
api: {
totalRequests: 2,
totalErrors: 0,
totalLatencyMs: 5672,
},
tokens: {
prompt: 24401,
candidates: 215,
total: 24719,
cached: 10656,
thoughts: 103,
tool: 0,
},
},
'gemini-2.5-flash': {
api: {
totalRequests: 2,
totalErrors: 0,
totalLatencyMs: 5914,
},
tokens: {
prompt: 20803,
candidates: 716,
total: 21657,
cached: 0,
thoughts: 138,
tool: 0,
},
},
},
tools: {
totalCalls: 1,
totalSuccess: 1,
totalFail: 0,
totalDurationMs: 4582,
totalDecisions: {
accept: 0,
reject: 0,
modify: 0,
auto_accept: 1,
},
byName: {
google_web_search: {
count: 1,
success: 1,
fail: 0,
durationMs: 4582,
decisions: {
accept: 0,
reject: 0,
modify: 0,
auto_accept: 1,
},
},
},
},
files: {
totalLinesAdded: 0,
totalLinesRemoved: 0,
},
};
const formatted = formatter.format(response, stats);
const expected = {
response,
stats,
};
expect(JSON.parse(formatted)).toEqual(expected);
});
it('should format error as JSON', () => {
const formatter = new JsonFormatter();
const error: JsonError = {
type: 'ValidationError',
message: 'Invalid input provided',
code: 400,
};
const formatted = formatter.format(undefined, undefined, error);
const expected = {
error,
};
expect(JSON.parse(formatted)).toEqual(expected);
});
it('should format response with error as JSON', () => {
const formatter = new JsonFormatter();
const response = 'Partial response';
const error: JsonError = {
type: 'TimeoutError',
message: 'Request timed out',
code: 'TIMEOUT',
};
const formatted = formatter.format(response, undefined, error);
const expected = {
response,
error,
};
expect(JSON.parse(formatted)).toEqual(expected);
});
it('should format error using formatError method', () => {
const formatter = new JsonFormatter();
const error = new Error('Something went wrong');
const formatted = formatter.formatError(error, 500);
const parsed = JSON.parse(formatted);
expect(parsed).toEqual({
error: {
type: 'Error',
message: 'Something went wrong',
code: 500,
},
});
});
it('should format custom error using formatError method', () => {
class CustomError extends Error {
constructor(message: string) {
super(message);
this.name = 'CustomError';
}
}
const formatter = new JsonFormatter();
const error = new CustomError('Custom error occurred');
const formatted = formatter.formatError(error);
const parsed = JSON.parse(formatted);
expect(parsed).toEqual({
error: {
type: 'CustomError',
message: 'Custom error occurred',
},
});
});
it('should format complete JSON output with response, stats, and error', () => {
const formatter = new JsonFormatter();
const response = 'Partial response before error';
const stats: SessionMetrics = {
models: {},
tools: {
totalCalls: 0,
totalSuccess: 0,
totalFail: 1,
totalDurationMs: 0,
totalDecisions: {
accept: 0,
reject: 0,
modify: 0,
auto_accept: 0,
},
byName: {},
},
files: {
totalLinesAdded: 0,
totalLinesRemoved: 0,
},
};
const error: JsonError = {
type: 'ApiError',
message: 'Rate limit exceeded',
code: 429,
};
const formatted = formatter.format(response, stats, error);
const expected = {
response,
stats,
error,
};
expect(JSON.parse(formatted)).toEqual(expected);
});
it('should handle error messages containing JSON content', () => {
const formatter = new JsonFormatter();
const errorWithJson = new Error(
'API returned: {"error": "Invalid request", "code": 400}',
);
const formatted = formatter.formatError(errorWithJson, 'API_ERROR');
const parsed = JSON.parse(formatted);
expect(parsed).toEqual({
error: {
type: 'Error',
message: 'API returned: {"error": "Invalid request", "code": 400}',
code: 'API_ERROR',
},
});
// Verify the entire output is valid JSON
expect(() => JSON.parse(formatted)).not.toThrow();
});
it('should handle error messages with quotes and special characters', () => {
const formatter = new JsonFormatter();
const errorWithQuotes = new Error('Error: "quoted text" and \\backslash');
const formatted = formatter.formatError(errorWithQuotes);
const parsed = JSON.parse(formatted);
expect(parsed).toEqual({
error: {
type: 'Error',
message: 'Error: "quoted text" and \\backslash',
},
});
// Verify the entire output is valid JSON
expect(() => JSON.parse(formatted)).not.toThrow();
});
it('should handle error messages with control characters', () => {
const formatter = new JsonFormatter();
const errorWithControlChars = new Error('Error with\n newline and\t tab');
const formatted = formatter.formatError(errorWithControlChars);
const parsed = JSON.parse(formatted);
// Should preserve newlines and tabs as they are common whitespace characters
expect(parsed.error.message).toBe('Error with\n newline and\t tab');
// Verify the entire output is valid JSON
expect(() => JSON.parse(formatted)).not.toThrow();
});
it('should strip ANSI escape sequences from error messages', () => {
const formatter = new JsonFormatter();
const errorWithAnsi = new Error('\x1B[31mRed error\x1B[0m message');
const formatted = formatter.formatError(errorWithAnsi);
const parsed = JSON.parse(formatted);
expect(parsed.error.message).toBe('Red error message');
expect(() => JSON.parse(formatted)).not.toThrow();
});
it('should strip unsafe control characters from error messages', () => {
const formatter = new JsonFormatter();
const errorWithControlChars = new Error(
'Error\x07 with\x08 control\x0B chars',
);
const formatted = formatter.formatError(errorWithControlChars);
const parsed = JSON.parse(formatted);
// Only ANSI codes are stripped, other control chars are preserved
expect(parsed.error.message).toBe('Error\x07 with\x08 control\x0B chars');
expect(() => JSON.parse(formatted)).not.toThrow();
});
});

View File

@@ -0,0 +1,39 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import stripAnsi from 'strip-ansi';
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 {
const output: JsonOutput = {};
if (response !== undefined) {
output.response = stripAnsi(response);
}
if (stats) {
output.stats = stats;
}
if (error) {
output.error = error;
}
return JSON.stringify(output, null, 2);
}
formatError(error: Error, code?: string | number): string {
const jsonError: JsonError = {
type: error.constructor.name,
message: stripAnsi(error.message),
...(code && { code }),
};
return this.format(undefined, undefined, jsonError);
}
}

View File

@@ -0,0 +1,24 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { SessionMetrics } from '../telemetry/uiTelemetry.js';
export enum OutputFormat {
TEXT = 'text',
JSON = 'json',
}
export interface JsonError {
type: string;
message: string;
code?: string | number;
}
export interface JsonOutput {
response?: string;
stats?: SessionMetrics;
error?: JsonError;
}