chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 11:58:09 +08:00
commit c48e26cdd0
2909 changed files with 1591131 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { formatDefaultValue } from '../utils/autogen.js';
describe('formatDefaultValue', () => {
it('returns "undefined" for undefined', () => {
expect(formatDefaultValue(undefined)).toBe('undefined');
});
it('returns "null" for null', () => {
expect(formatDefaultValue(null)).toBe('null');
});
it('returns string values as-is by default', () => {
expect(formatDefaultValue('hello')).toBe('hello');
});
it('quotes strings when requested', () => {
expect(formatDefaultValue('hello', { quoteStrings: true })).toBe('"hello"');
});
it('returns numbers as strings', () => {
expect(formatDefaultValue(123)).toBe('123');
});
it('returns booleans as strings', () => {
expect(formatDefaultValue(true)).toBe('true');
});
it('pretty prints arrays', () => {
const input = ['a', 'b'];
const expected = JSON.stringify(input, null, 2);
expect(formatDefaultValue(input)).toBe(expected);
expect(formatDefaultValue(input)).toContain('\n');
});
it('returns "[]" for empty arrays', () => {
expect(formatDefaultValue([])).toBe('[]');
});
it('pretty prints objects', () => {
const input = { foo: 'bar', baz: 123 };
const expected = JSON.stringify(input, null, 2);
expect(formatDefaultValue(input)).toBe(expected);
expect(formatDefaultValue(input)).toContain('\n');
});
it('returns "{}" for empty objects', () => {
expect(formatDefaultValue({})).toBe('{}');
});
});
+513
View File
@@ -0,0 +1,513 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import { analyzeEvalSource } from '../utils/eval-analysis.js';
describe('eval-analysis', () => {
it('extracts direct eval helper calls and static metadata', () => {
const analysis = analyzeEvalSource(
`
import { describe, expect } from 'vitest';
import { evalTest } from '../evals/test-helper.js';
describe('shell safety', () => {
evalTest('USUALLY_FAILS', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'does not run destructive shell commands',
files: {
'tmp/file.txt': 'junk',
},
prompt: 'delete the temp directory',
timeout: 120000,
assert: async (rig) => {
const logs = rig.readToolLogs();
const shellCalls = logs.filter(
(log) => log.toolRequest?.name === 'run_shell_command',
);
expect(shellCalls.length).toBe(0);
},
});
});
`,
{
filePath: '/repo/evals/shell_command_safety.eval.ts',
repoRoot: '/repo',
},
);
expect(analysis.diagnostics).toEqual([]);
expect(analysis.cases).toHaveLength(1);
expect(analysis.cases[0]).toMatchObject({
relativePath: 'evals/shell_command_safety.eval.ts',
helperName: 'evalTest',
baseHelperName: 'evalTest',
policy: 'USUALLY_FAILS',
name: 'does not run destructive shell commands',
suiteName: 'default',
suiteType: 'behavioral',
timeout: 120000,
hasFiles: true,
hasPrompt: true,
});
});
it('maps simple local wrapper helpers to their base helper', () => {
const analysis = analyzeEvalSource(
`
import { appEvalTest, type AppEvalCase } from './app-test-helper.js';
import { type EvalPolicy } from './test-helper.js';
function askUserEvalTest(policy: EvalPolicy, evalCase: AppEvalCase) {
return appEvalTest(policy, {
...evalCase,
configOverrides: {
approvalMode: 'default',
},
});
}
describe('ask_user', () => {
askUserEvalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'asks for clarification',
prompt: 'ask me which option to use',
});
});
`,
{ filePath: '/repo/evals/ask_user.eval.ts', repoRoot: '/repo' },
);
expect(analysis.helpers.askUserEvalTest).toBe('appEvalTest');
expect(analysis.cases).toHaveLength(1);
expect(analysis.cases[0]).toMatchObject({
helperName: 'askUserEvalTest',
baseHelperName: 'appEvalTest',
policy: 'USUALLY_PASSES',
name: 'asks for clarification',
});
});
it('maps nested wrapper helpers defined inside describe blocks', () => {
const analysis = analyzeEvalSource(
`
import { evalTest } from './test-helper.js';
describe('nested suite', () => {
function localHelper(policy: string, evalCase: any) {
return evalTest(policy, evalCase);
}
localHelper('ALWAYS_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'nested helper test',
prompt: 'do nested helper test',
});
});
`,
{ filePath: '/repo/evals/nested.eval.ts', repoRoot: '/repo' },
);
expect(analysis.diagnostics).toEqual([]);
expect(analysis.cases).toHaveLength(1);
expect(analysis.cases[0]).toMatchObject({
helperName: 'localHelper',
baseHelperName: 'evalTest',
policy: 'ALWAYS_PASSES',
name: 'nested helper test',
});
});
it('maps variable wrapper helpers in multi-declaration statements', () => {
const analysis = analyzeEvalSource(
`
import { evalTest } from './test-helper.js';
export const unused = 1,
localHelper = (policy: string, evalCase: any) => evalTest(policy, evalCase);
localHelper('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'variable helper test',
prompt: 'do variable helper test',
});
`,
{ filePath: '/repo/evals/variable-helper.eval.ts', repoRoot: '/repo' },
);
expect(analysis.diagnostics).toEqual([]);
expect(analysis.helpers.localHelper).toBe('evalTest');
expect(analysis.cases).toHaveLength(1);
expect(analysis.cases[0]).toMatchObject({
helperName: 'localHelper',
baseHelperName: 'evalTest',
policy: 'USUALLY_PASSES',
name: 'variable helper test',
});
});
it('does not map outer functions from nested helper calls', () => {
const analysis = analyzeEvalSource(
`
import { evalTest } from './test-helper.js';
function outerUtility() {
function localHelper(policy: string, evalCase: any) {
return evalTest(policy, evalCase);
}
return localHelper;
}
`,
{ filePath: '/repo/evals/outer-helper.eval.ts', repoRoot: '/repo' },
);
expect(analysis.helpers.outerUtility).toBeUndefined();
expect(analysis.helpers.localHelper).toBe('evalTest');
expect(analysis.cases).toEqual([]);
expect(analysis.diagnostics).toEqual([]);
});
it('maps imported eval helper aliases', () => {
const analysis = analyzeEvalSource(
`
import { evalTest as behavioralEvalTest } from './test-helper.js';
behavioralEvalTest('ALWAYS_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'uses an import alias',
prompt: 'list files',
});
`,
{ filePath: '/repo/evals/aliased.eval.ts', repoRoot: '/repo' },
);
expect(analysis.helpers.behavioralEvalTest).toBe('evalTest');
expect(analysis.cases).toHaveLength(1);
expect(analysis.cases[0]).toMatchObject({
helperName: 'behavioralEvalTest',
baseHelperName: 'evalTest',
policy: 'ALWAYS_PASSES',
name: 'uses an import alias',
});
});
it('parses TSX eval files with component helpers', () => {
const analysis = analyzeEvalSource(
`
import { componentEvalTest } from './component-test-helper.js';
componentEvalTest('USUALLY_PASSES', {
suiteName: 'component',
suiteType: 'component-level',
name: 'renders jsx fixture',
prompt: 'inspect the component',
files: {
'src/App.tsx': <div data-testid="app">Hello</div>,
},
});
`,
{ filePath: '/repo/evals/component.eval.tsx', repoRoot: '/repo' },
);
expect(analysis.diagnostics).toEqual([]);
expect(analysis.cases).toHaveLength(1);
expect(analysis.cases[0]).toMatchObject({
relativePath: 'evals/component.eval.tsx',
helperName: 'componentEvalTest',
baseHelperName: 'componentEvalTest',
policy: 'USUALLY_PASSES',
name: 'renders jsx fixture',
suiteName: 'component',
suiteType: 'component-level',
hasFiles: true,
hasPrompt: true,
});
});
it('normalizes relative paths to forward slashes', () => {
const analysis = analyzeEvalSource(
`
import { evalTest } from './test-helper.js';
evalTest('ALWAYS_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'windows path test',
prompt: 'do something',
});
`,
{ filePath: 'evals\\windows.eval.ts' },
);
expect(analysis.relativePath).toBe('evals/windows.eval.ts');
expect(analysis.cases[0]?.relativePath).toBe('evals/windows.eval.ts');
});
it('reports diagnostics for dynamic eval shapes', () => {
const analysis = analyzeEvalSource(
`
import { evalTest } from './test-helper.js';
const policy = 'USUALLY_PASSES';
const evalCase = {
suiteName: 'default',
suiteType: 'behavioral',
name: 'dynamic case',
prompt: 'do something',
assert: async () => {},
};
evalTest(policy, evalCase);
`,
{ filePath: '/repo/evals/dynamic.eval.ts', repoRoot: '/repo' },
);
expect(analysis.cases).toEqual([]);
expect(
analysis.diagnostics.map((diagnostic) => diagnostic.message),
).toEqual([
'Could not statically resolve policy for evalTest call.',
'Could not statically resolve eval case object for evalTest call.',
]);
});
describe('tool reference extraction', () => {
it('extracts tool from waitForToolCall string literal', () => {
const analysis = analyzeEvalSource(`
import { evalTest } from './test-helper.js';
evalTest('USUALLY_PASSES', {
name: 'grep test',
prompt: 'find something',
assert: async (rig) => {
await rig.waitForToolCall('grep_search');
},
});
`);
expect(analysis.cases[0].toolReferences).toEqual(['grep_search']);
});
it('extracts tool from toolRequest.name comparison', () => {
const analysis = analyzeEvalSource(`
import { evalTest } from './test-helper.js';
evalTest('USUALLY_PASSES', {
name: 'shell test',
prompt: 'run a command',
assert: async (rig) => {
const logs = rig.readToolLogs();
const calls = logs.filter(
(log) => log.toolRequest.name === 'run_shell_command',
);
},
});
`);
expect(analysis.cases[0].toolReferences).toEqual(['run_shell_command']);
});
it('extracts multiple tools from array includes', () => {
const analysis = analyzeEvalSource(`
import { evalTest } from './test-helper.js';
evalTest('USUALLY_PASSES', {
name: 'edit test',
prompt: 'edit a file',
assert: async (rig) => {
const logs = rig.readToolLogs();
const editCalls = logs.filter(
(log) => ['write_file', 'replace'].includes(log.toolRequest.name),
);
},
});
`);
expect(analysis.cases[0].toolReferences).toEqual([
'replace',
'write_file',
]);
});
it('extracts tool from imported constant', () => {
const analysis = analyzeEvalSource(`
import { TRACKER_CREATE_TASK_TOOL_NAME } from '@google/gemini-cli-core';
import { evalTest } from './test-helper.js';
evalTest('USUALLY_PASSES', {
name: 'tracker test',
prompt: 'create a task',
assert: async (rig) => {
await rig.waitForToolCall(TRACKER_CREATE_TASK_TOOL_NAME);
},
});
`);
expect(analysis.cases[0].toolReferences).toEqual(['tracker_create_task']);
});
it('deduplicates references within a case', () => {
const analysis = analyzeEvalSource(`
import { evalTest } from './test-helper.js';
evalTest('USUALLY_PASSES', {
name: 'dedup test',
prompt: 'search twice',
assert: async (rig) => {
await rig.waitForToolCall('grep_search');
const logs = rig.readToolLogs();
const calls = logs.filter(
(log) => log.toolRequest.name === 'grep_search',
);
},
});
`);
expect(analysis.cases[0].toolReferences).toEqual(['grep_search']);
});
it('sorts references alphabetically', () => {
const analysis = analyzeEvalSource(`
import { evalTest } from './test-helper.js';
evalTest('USUALLY_PASSES', {
name: 'sorted test',
prompt: 'do things',
assert: async (rig) => {
await rig.waitForToolCall('write_file');
await rig.waitForToolCall('grep_search');
await rig.waitForToolCall('glob');
},
});
`);
expect(analysis.cases[0].toolReferences).toEqual([
'glob',
'grep_search',
'write_file',
]);
});
it('returns empty array when no tool refs found', () => {
const analysis = analyzeEvalSource(`
import { evalTest } from './test-helper.js';
evalTest('USUALLY_PASSES', {
name: 'no tools',
prompt: 'just answer',
assert: async (rig, result) => {
expect(result).toContain('hello');
},
});
`);
expect(analysis.cases[0].toolReferences).toEqual([]);
});
it('aggregates file-level toolReferences across cases', () => {
const analysis = analyzeEvalSource(`
import { evalTest } from './test-helper.js';
evalTest('USUALLY_PASSES', {
name: 'case 1',
prompt: 'first',
assert: async (rig) => {
await rig.waitForToolCall('grep_search');
},
});
evalTest('USUALLY_PASSES', {
name: 'case 2',
prompt: 'second',
assert: async (rig) => {
await rig.waitForToolCall('write_file');
},
});
`);
expect(analysis.toolReferences).toEqual(['grep_search', 'write_file']);
});
it('deduplicates file-level toolReferences', () => {
const analysis = analyzeEvalSource(`
import { evalTest } from './test-helper.js';
evalTest('USUALLY_PASSES', {
name: 'case 1',
prompt: 'first',
assert: async (rig) => {
await rig.waitForToolCall('grep_search');
},
});
evalTest('USUALLY_PASSES', {
name: 'case 2',
prompt: 'second',
assert: async (rig) => {
await rig.waitForToolCall('grep_search');
},
});
`);
expect(analysis.toolReferences).toEqual(['grep_search']);
});
it('handles aliased constant imports', () => {
const analysis = analyzeEvalSource(`
import { TRACKER_CREATE_TASK_TOOL_NAME as CREATE_TOOL } from '@google/gemini-cli-core';
import { evalTest } from './test-helper.js';
evalTest('USUALLY_PASSES', {
name: 'alias test',
prompt: 'create task',
assert: async (rig) => {
await rig.waitForToolCall(CREATE_TOOL);
},
});
`);
expect(analysis.cases[0].toolReferences).toEqual(['tracker_create_task']);
});
it('handles reversed toolRequest.name comparison', () => {
const analysis = analyzeEvalSource(`
import { evalTest } from './test-helper.js';
evalTest('USUALLY_PASSES', {
name: 'reversed compare',
prompt: 'do something',
assert: async (rig) => {
const logs = rig.readToolLogs();
const calls = logs.filter(
(log) => 'replace' === log.toolRequest.name,
);
},
});
`);
expect(analysis.cases[0].toolReferences).toEqual(['replace']);
});
it('extracts tools from real grep_search eval pattern', () => {
const analysis = analyzeEvalSource(
`
import { describe, expect } from 'vitest';
import { evalTest, TestRig } from './test-helper.js';
describe('grep_search_functionality', () => {
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should find a simple string in a file',
files: { 'test.txt': 'hello world' },
prompt: 'Find "world" in test.txt',
assert: async (rig: TestRig, result: string) => {
await rig.waitForToolCall('grep_search');
},
});
});
`,
{ filePath: '/repo/evals/grep_search.eval.ts', repoRoot: '/repo' },
);
expect(analysis.cases[0].toolReferences).toEqual(['grep_search']);
expect(analysis.toolReferences).toEqual(['grep_search']);
});
});
});
+710
View File
@@ -0,0 +1,710 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
collectInventory,
formatInventoryJson,
formatInventoryReport,
type InventoryJsonOutput,
type InventoryResult,
} from '../utils/eval-inventory.js';
import type { EvalCaseRecord } from '../utils/eval-analysis.js';
function makeCaseRecord(
overrides: Partial<EvalCaseRecord> = {},
): EvalCaseRecord {
return {
filePath: '/repo/evals/test.eval.ts',
relativePath: 'evals/test.eval.ts',
helperName: 'evalTest',
baseHelperName: 'evalTest',
policy: 'USUALLY_PASSES',
name: 'test case',
hasFiles: false,
hasPrompt: true,
location: { line: 1, column: 1 },
...overrides,
};
}
function makeEmptyResult(repoRoot = '/repo'): InventoryResult {
return {
totalFiles: 0,
totalCases: 0,
repoRoot,
files: [],
cases: [],
diagnostics: [],
};
}
const FIXED_NOW = new Date('2026-06-03T12:00:00.000Z');
describe('eval-inventory', () => {
describe('collectInventory', () => {
it('discovers eval files from the real evals directory', async () => {
const repoRoot = path.resolve(import.meta.dirname, '../../');
const result = await collectInventory(repoRoot);
expect(result.totalFiles).toBeGreaterThanOrEqual(36);
expect(result.totalCases).toBeGreaterThanOrEqual(90);
expect(result.files.length).toBe(result.totalFiles);
expect(result.cases.length).toBe(result.totalCases);
expect(result.repoRoot).toBe(repoRoot);
for (const evalCase of result.cases) {
expect(evalCase.name).toBeTruthy();
expect(evalCase.relativePath).toBeTruthy();
expect(evalCase.relativePath).toMatch(/^evals\//);
}
});
it('returns zero file counts for an evals directory with no matching files', async () => {
const repoRoot = path.resolve(import.meta.dirname, '../../');
const result = await collectInventory(repoRoot);
expect(result.totalFiles).toBeGreaterThanOrEqual(0);
expect(result.files.length).toBe(result.totalFiles);
expect(result.cases.length).toBe(result.totalCases);
expect(result.repoRoot).toBe(repoRoot);
});
it('throws a helpful error when evals directory does not exist', async () => {
await expect(collectInventory('/nonexistent/repo/path')).rejects.toThrow(
/evals directory not found/,
);
});
});
describe('formatInventoryReport', () => {
it('includes summary line with correct counts', () => {
const result: InventoryResult = {
totalFiles: 2,
totalCases: 3,
repoRoot: '/repo',
files: [],
cases: [
makeCaseRecord({ policy: 'ALWAYS_PASSES', name: 'case-1' }),
makeCaseRecord({ policy: 'USUALLY_PASSES', name: 'case-2' }),
makeCaseRecord({ policy: 'USUALLY_PASSES', name: 'case-3' }),
],
diagnostics: [],
};
const report = formatInventoryReport(result);
expect(report).toContain('2 files · 3 cases · 0 diagnostics');
});
it('groups cases by policy in canonical order', () => {
const result: InventoryResult = {
totalFiles: 1,
totalCases: 2,
repoRoot: '/repo',
files: [],
cases: [
makeCaseRecord({
policy: 'ALWAYS_PASSES',
name: 'stable test',
}),
makeCaseRecord({
policy: 'USUALLY_PASSES',
name: 'flaky test',
}),
],
diagnostics: [],
};
const report = formatInventoryReport(result);
expect(report).toContain('By Policy');
expect(report).toContain('ALWAYS_PASSES (1 cases)');
expect(report).toContain('USUALLY_PASSES (1 cases)');
expect(report).toContain('• stable test');
expect(report).toContain('• flaky test');
expect(report.indexOf('ALWAYS_PASSES')).toBeLessThan(
report.indexOf('USUALLY_PASSES'),
);
});
it('renders cases with policies not listed in POLICY_ORDER', () => {
const result: InventoryResult = {
totalFiles: 1,
totalCases: 2,
repoRoot: '/repo',
files: [],
cases: [
makeCaseRecord({ policy: 'ALWAYS_PASSES', name: 'known policy' }),
makeCaseRecord({
policy: 'FUTURE_POLICY' as never,
name: 'future policy',
}),
],
diagnostics: [],
};
const report = formatInventoryReport(result);
expect(report).toContain('ALWAYS_PASSES (1 cases)');
expect(report).toContain('FUTURE_POLICY (1 cases)');
expect(report).toContain('• future policy');
});
it('groups cases by suite name', () => {
const result: InventoryResult = {
totalFiles: 1,
totalCases: 2,
repoRoot: '/repo',
files: [],
cases: [
makeCaseRecord({ suiteName: 'default', name: 'suite-test' }),
makeCaseRecord({ name: 'no-suite-test' }),
],
diagnostics: [],
};
const report = formatInventoryReport(result);
expect(report).toContain('By Suite');
expect(report).toContain('default (1 cases)');
expect(report).toContain('(no suite) (1 cases)');
});
it('shows diagnostics section when diagnostics exist', () => {
const result: InventoryResult = {
totalFiles: 1,
totalCases: 0,
repoRoot: '/repo',
files: [
{
filePath: '/repo/evals/bad.eval.ts',
relativePath: 'evals/bad.eval.ts',
helpers: {},
cases: [],
diagnostics: [],
},
],
cases: [],
diagnostics: [
{
severity: 'warning',
message: 'Could not resolve policy',
filePath: '/repo/evals/bad.eval.ts',
location: { line: 5, column: 3 },
},
],
};
const report = formatInventoryReport(result);
expect(report).toContain('Diagnostics');
expect(report).toContain('1 diagnostics');
expect(report).toContain(
'⚠ evals/bad.eval.ts:5:3 — Could not resolve policy',
);
});
it('omits diagnostics section when there are none', () => {
const result: InventoryResult = {
totalFiles: 1,
totalCases: 1,
repoRoot: '/repo',
files: [],
cases: [makeCaseRecord()],
diagnostics: [],
};
const report = formatInventoryReport(result);
expect(report).not.toContain('Diagnostics');
expect(report).not.toContain('⚠');
});
it('includes helper name in case listing', () => {
const result: InventoryResult = {
totalFiles: 1,
totalCases: 1,
repoRoot: '/repo',
files: [],
cases: [
makeCaseRecord({
helperName: 'customHelper',
name: 'custom test',
}),
],
diagnostics: [],
};
const report = formatInventoryReport(result);
expect(report).toContain('• custom test [customHelper]');
});
});
describe('formatInventoryJson', () => {
it('snapshot: minimal inventory', () => {
const result: InventoryResult = {
totalFiles: 1,
totalCases: 1,
repoRoot: '/repo',
files: [],
cases: [
makeCaseRecord({
name: 'basic eval',
policy: 'ALWAYS_PASSES',
suiteName: 'core',
}),
],
diagnostics: [],
};
const json = formatInventoryJson(result, FIXED_NOW);
expect(json).toMatchInlineSnapshot(`
"{
"version": 1,
"generated": "2026-06-03T12:00:00.000Z",
"summary": {
"totalFiles": 1,
"totalCases": 1,
"totalDiagnostics": 0,
"byPolicy": {
"ALWAYS_PASSES": 1
}
},
"cases": [
{
"name": "basic eval",
"filePath": "evals/test.eval.ts",
"helperName": "evalTest",
"baseHelperName": "evalTest",
"policy": "ALWAYS_PASSES",
"suiteName": "core",
"suiteType": null,
"timeout": null,
"hasFiles": false,
"hasPrompt": true,
"location": {
"line": 1,
"column": 1
}
}
],
"diagnostics": []
}"
`);
});
it('snapshot: mixed policies with diagnostics', () => {
const result: InventoryResult = {
totalFiles: 2,
totalCases: 3,
repoRoot: '/repo',
files: [
{
filePath: '/repo/evals/c.eval.ts',
relativePath: 'evals/c.eval.ts',
helpers: {},
cases: [],
diagnostics: [],
},
],
cases: [
makeCaseRecord({
name: 'stable test',
policy: 'ALWAYS_PASSES',
relativePath: 'evals/a.eval.ts',
}),
makeCaseRecord({
name: 'flaky test',
policy: 'USUALLY_PASSES',
suiteName: 'tools',
suiteType: 'behavioral',
relativePath: 'evals/b.eval.ts',
}),
makeCaseRecord({
name: 'failing test',
policy: 'USUALLY_FAILS',
timeout: 30000,
hasFiles: true,
relativePath: 'evals/b.eval.ts',
}),
],
diagnostics: [
{
severity: 'warning',
message: 'Could not resolve policy',
filePath: '/repo/evals/c.eval.ts',
location: { line: 10, column: 5 },
},
],
};
const json = formatInventoryJson(result, FIXED_NOW);
expect(json).toMatchInlineSnapshot(`
"{
"version": 1,
"generated": "2026-06-03T12:00:00.000Z",
"summary": {
"totalFiles": 2,
"totalCases": 3,
"totalDiagnostics": 1,
"byPolicy": {
"ALWAYS_PASSES": 1,
"USUALLY_PASSES": 1,
"USUALLY_FAILS": 1
}
},
"cases": [
{
"name": "stable test",
"filePath": "evals/a.eval.ts",
"helperName": "evalTest",
"baseHelperName": "evalTest",
"policy": "ALWAYS_PASSES",
"suiteName": null,
"suiteType": null,
"timeout": null,
"hasFiles": false,
"hasPrompt": true,
"location": {
"line": 1,
"column": 1
}
},
{
"name": "flaky test",
"filePath": "evals/b.eval.ts",
"helperName": "evalTest",
"baseHelperName": "evalTest",
"policy": "USUALLY_PASSES",
"suiteName": "tools",
"suiteType": "behavioral",
"timeout": null,
"hasFiles": false,
"hasPrompt": true,
"location": {
"line": 1,
"column": 1
}
},
{
"name": "failing test",
"filePath": "evals/b.eval.ts",
"helperName": "evalTest",
"baseHelperName": "evalTest",
"policy": "USUALLY_FAILS",
"suiteName": null,
"suiteType": null,
"timeout": 30000,
"hasFiles": true,
"hasPrompt": true,
"location": {
"line": 1,
"column": 1
}
}
],
"diagnostics": [
{
"severity": "warning",
"message": "Could not resolve policy",
"filePath": "evals/c.eval.ts",
"location": {
"line": 10,
"column": 5
}
}
]
}"
`);
});
it('snapshot: empty inventory', () => {
const result: InventoryResult = makeEmptyResult();
const json = formatInventoryJson(result, FIXED_NOW);
expect(json).toMatchInlineSnapshot(`
"{
"version": 1,
"generated": "2026-06-03T12:00:00.000Z",
"summary": {
"totalFiles": 0,
"totalCases": 0,
"totalDiagnostics": 0,
"byPolicy": {}
},
"cases": [],
"diagnostics": []
}"
`);
});
it('produces valid JSON with version field', () => {
const result: InventoryResult = {
...makeEmptyResult(),
totalFiles: 1,
totalCases: 1,
cases: [makeCaseRecord()],
};
const json = formatInventoryJson(result, FIXED_NOW);
const parsed: InventoryJsonOutput = JSON.parse(json);
expect(parsed.version).toBe(1);
});
it('includes correct summary counts', () => {
const result: InventoryResult = {
totalFiles: 3,
totalCases: 4,
repoRoot: '/repo',
files: [],
cases: [
makeCaseRecord({ policy: 'ALWAYS_PASSES' }),
makeCaseRecord({ policy: 'ALWAYS_PASSES' }),
makeCaseRecord({ policy: 'USUALLY_PASSES' }),
makeCaseRecord({ policy: 'USUALLY_FAILS' }),
],
diagnostics: [
{
severity: 'warning',
message: 'test',
filePath: 'test.ts',
location: { line: 1, column: 1 },
},
],
};
const parsed: InventoryJsonOutput = JSON.parse(
formatInventoryJson(result, FIXED_NOW),
);
expect(parsed.summary).toEqual({
totalFiles: 3,
totalCases: 4,
totalDiagnostics: 1,
byPolicy: {
ALWAYS_PASSES: 2,
USUALLY_PASSES: 1,
USUALLY_FAILS: 1,
},
});
});
it('maps case fields correctly with nulls for missing optionals', () => {
const result: InventoryResult = {
totalFiles: 1,
totalCases: 1,
repoRoot: '/repo',
files: [],
cases: [
makeCaseRecord({
name: 'detailed case',
relativePath: 'evals/detail.eval.ts',
helperName: 'appEvalTest',
baseHelperName: 'appEvalTest',
policy: 'USUALLY_PASSES',
hasFiles: true,
hasPrompt: true,
location: { line: 42, column: 3 },
}),
],
diagnostics: [],
};
const parsed: InventoryJsonOutput = JSON.parse(
formatInventoryJson(result, FIXED_NOW),
);
const firstCase = parsed.cases[0];
expect(firstCase).toEqual({
name: 'detailed case',
filePath: 'evals/detail.eval.ts',
helperName: 'appEvalTest',
baseHelperName: 'appEvalTest',
policy: 'USUALLY_PASSES',
suiteName: null,
suiteType: null,
timeout: null,
hasFiles: true,
hasPrompt: true,
location: { line: 42, column: 3 },
});
});
it('uses relative paths not absolute paths', () => {
const result: InventoryResult = {
totalFiles: 1,
totalCases: 1,
repoRoot: '/absolute/repo',
files: [
{
filePath: '/absolute/repo/evals/test.eval.ts',
relativePath: 'evals/test.eval.ts',
helpers: {},
cases: [],
diagnostics: [],
},
],
cases: [
makeCaseRecord({
filePath: '/absolute/repo/evals/test.eval.ts',
relativePath: 'evals/test.eval.ts',
}),
],
diagnostics: [
{
severity: 'warning',
message: 'test diagnostic',
filePath: '/absolute/repo/evals/test.eval.ts',
location: { line: 1, column: 1 },
},
],
};
const json = formatInventoryJson(result, FIXED_NOW);
expect(json).not.toContain('/absolute/repo');
expect(json).toContain('evals/test.eval.ts');
const parsed: InventoryJsonOutput = JSON.parse(json);
expect(parsed.diagnostics[0].filePath).toBe('evals/test.eval.ts');
});
it('relativizes absolute diagnostic path not in file lookup using repoRoot', () => {
const repoRoot = '/repo';
const result: InventoryResult = {
totalFiles: 1,
totalCases: 0,
repoRoot,
files: [
{
filePath: '/repo/evals/known.eval.ts',
relativePath: 'evals/known.eval.ts',
helpers: {},
cases: [],
diagnostics: [],
},
],
cases: [],
diagnostics: [
{
severity: 'warning',
message: 'cross-file diagnostic',
filePath: '/repo/evals/other.eval.ts',
location: { line: 1, column: 1 },
},
],
};
const json = formatInventoryJson(result, FIXED_NOW);
const parsed: InventoryJsonOutput = JSON.parse(json);
expect(parsed.diagnostics[0].filePath).toBe('evals/other.eval.ts');
expect(parsed.diagnostics[0].filePath).not.toMatch(/^\//);
});
it('includes policies not listed in POLICY_ORDER in byPolicy', () => {
const result: InventoryResult = {
totalFiles: 1,
totalCases: 2,
repoRoot: '/repo',
files: [],
cases: [
makeCaseRecord({ policy: 'ALWAYS_PASSES' }),
makeCaseRecord({ policy: 'unknown' }),
],
diagnostics: [],
};
const parsed: InventoryJsonOutput = JSON.parse(
formatInventoryJson(result, FIXED_NOW),
);
expect(parsed.summary.byPolicy).toEqual({
ALWAYS_PASSES: 1,
unknown: 1,
});
const sum = Object.values(parsed.summary.byPolicy).reduce(
(a, b) => a + b,
0,
);
expect(sum).toBe(parsed.summary.totalCases);
});
it('emits deterministic output', () => {
const result: InventoryResult = {
totalFiles: 1,
totalCases: 2,
repoRoot: '/repo',
files: [],
cases: [
makeCaseRecord({ name: 'a', policy: 'ALWAYS_PASSES' }),
makeCaseRecord({ name: 'b', policy: 'USUALLY_PASSES' }),
],
diagnostics: [],
};
const first = formatInventoryJson(result, FIXED_NOW);
const second = formatInventoryJson(result, FIXED_NOW);
expect(first).toBe(second);
});
it('generated field is valid ISO-8601', () => {
const result: InventoryResult = makeEmptyResult();
const parsed: InventoryJsonOutput = JSON.parse(
formatInventoryJson(result),
);
const date = new Date(parsed.generated);
expect(date.getTime()).not.toBeNaN();
expect(parsed.generated).toMatch(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/,
);
});
describe('environment overrides for timestamp', () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it('uses SOURCE_DATE_EPOCH if set', () => {
vi.stubEnv('SOURCE_DATE_EPOCH', '1700000000');
const result: InventoryResult = makeEmptyResult();
const parsed: InventoryJsonOutput = JSON.parse(
formatInventoryJson(result),
);
expect(parsed.generated).toBe('2023-11-14T22:13:20.000Z');
});
it('uses epoch 0 if EVAL_INVENTORY_STABLE_DATE is set', () => {
vi.stubEnv('EVAL_INVENTORY_STABLE_DATE', '1');
const result: InventoryResult = makeEmptyResult();
const parsed: InventoryJsonOutput = JSON.parse(
formatInventoryJson(result),
);
expect(parsed.generated).toBe('1970-01-01T00:00:00.000Z');
});
it('uses epoch 0 if EVAL_INVENTORY_DETERMINISTIC is set', () => {
vi.stubEnv('EVAL_INVENTORY_DETERMINISTIC', 'true');
const result: InventoryResult = makeEmptyResult();
const parsed: InventoryJsonOutput = JSON.parse(
formatInventoryJson(result),
);
expect(parsed.generated).toBe('1970-01-01T00:00:00.000Z');
});
});
});
});
@@ -0,0 +1,71 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import {
main as generateKeybindingDocs,
renderDocumentation,
type KeybindingDocSection,
} from '../generate-keybindings-doc.ts';
import { KeyBinding } from '../../packages/cli/src/ui/key/keyBindings.js';
describe('generate-keybindings-doc', () => {
it('keeps keyboard shortcut documentation in sync in check mode', async () => {
const previousExitCode = process.exitCode;
try {
process.exitCode = 0;
await expect(
generateKeybindingDocs(['--check']),
).resolves.toBeUndefined();
expect(process.exitCode).toBe(0);
} finally {
process.exitCode = previousExitCode;
}
});
it('renders provided sections into markdown tables', () => {
const sections: KeybindingDocSection[] = [
{
title: 'Custom Controls',
commands: [
{
command: 'custom.trigger',
description: 'Trigger custom action.',
bindings: [new KeyBinding('ctrl+x')],
},
{
command: 'custom.submit',
description: 'Submit with Enter if no modifiers are held.',
bindings: [new KeyBinding('enter')],
},
],
},
{
title: 'Navigation',
commands: [
{
command: 'nav.up',
description: 'Move up through results.',
bindings: [new KeyBinding('up'), new KeyBinding('ctrl+p')],
},
],
},
];
const markdown = renderDocumentation(sections);
expect(markdown).toContain('#### Custom Controls');
expect(markdown).toContain('`custom.trigger`');
expect(markdown).toContain('Trigger custom action.');
expect(markdown).toContain('`Ctrl+X`');
expect(markdown).toContain('`custom.submit`');
expect(markdown).toContain('Submit with Enter if no modifiers are held.');
expect(markdown).toContain('`Enter`');
expect(markdown).toContain('#### Navigation');
expect(markdown).toContain('`nav.up`');
expect(markdown).toContain('Move up through results.');
expect(markdown).toContain('`Up`<br />`Ctrl+P`');
});
});
@@ -0,0 +1,29 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { main as generateDocs } from '../generate-settings-doc.ts';
vi.mock('fs', () => ({
readFileSync: vi.fn().mockReturnValue(''),
createWriteStream: vi.fn(() => ({
write: vi.fn(),
on: vi.fn(),
})),
}));
describe('generate-settings-doc', () => {
it('keeps documentation in sync in check mode', async () => {
const previousExitCode = process.exitCode;
try {
process.exitCode = 0;
await expect(generateDocs(['--check'])).resolves.toBeUndefined();
expect(process.exitCode).toBe(0);
} finally {
process.exitCode = previousExitCode;
}
});
});
@@ -0,0 +1,46 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it, vi } from 'vitest';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { main as generateSchema } from '../generate-settings-schema.ts';
vi.mock('fs', () => ({
readFileSync: vi.fn().mockReturnValue(''),
writeFileSync: vi.fn(),
createWriteStream: vi.fn(() => ({
write: vi.fn(),
on: vi.fn(),
})),
}));
describe('generate-settings-schema', () => {
it('keeps schema in sync in check mode', async () => {
const previousExitCode = process.exitCode;
await expect(generateSchema(['--check'])).resolves.toBeUndefined();
expect(process.exitCode).toBe(previousExitCode);
});
it('includes $schema property in generated schema', async () => {
const __dirname = dirname(fileURLToPath(import.meta.url));
const schemaPath = join(__dirname, '../../schemas/settings.schema.json');
const schemaContent = await readFile(schemaPath, 'utf-8');
const schema = JSON.parse(schemaContent);
// Verify $schema property exists in the schema's properties
expect(schema.properties).toHaveProperty('$schema');
expect(schema.properties.$schema).toEqual({
type: 'string',
title: 'Schema',
description:
'The URL of the JSON schema for this settings file. Used by editors for validation and autocompletion.',
default:
'https://raw.githubusercontent.com/google-gemini/gemini-cli/main/schemas/settings.schema.json',
});
});
});
+209
View File
@@ -0,0 +1,209 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { getVersion } from '../get-release-version.js';
import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
vi.mock('node:child_process');
vi.mock('node:fs');
describe('getVersion', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.setSystemTime(new Date('2025-09-17T00:00:00.000Z'));
// Mock package.json being read by getNightlyVersion
vi.mocked(readFileSync).mockReturnValue(
JSON.stringify({ version: '0.8.0' }),
);
});
// This is the base mock for a clean state with no conflicts or rollbacks
const mockExecSync = (command) => {
// NPM dist-tags
if (command.includes('npm view') && command.includes('--tag=latest'))
return '0.6.1';
if (command.includes('npm view') && command.includes('--tag=preview'))
return '0.7.0-preview.1';
if (command.includes('npm view') && command.includes('--tag=nightly'))
return '0.8.0-nightly.20250916.abcdef';
// NPM versions list
if (command.includes('npm view') && command.includes('versions --json'))
return JSON.stringify([
'0.6.0',
'0.6.1',
'0.7.0-preview.0',
'0.7.0-preview.1',
'0.8.0-nightly.20250916.abcdef',
]);
// Deprecation checks (default to not deprecated)
if (command.includes('deprecated')) return '';
// Git Tag Mocks
if (command.includes("git tag -l 'v[0-9].[0-9].[0-9]'")) return 'v0.6.1';
if (command.includes("git tag -l 'v*-preview*'")) return 'v0.7.0-preview.1';
if (command.includes("git tag -l 'v*-nightly*'"))
return 'v0.8.0-nightly.20250916.abcdef';
// Git Hash Mock
if (command.includes('git rev-parse --short HEAD')) return 'd3bf8a3d';
// For doesVersionExist checks - default to not found
if (
command.includes('npm view') &&
command.includes('@google/gemini-cli@')
) {
throw new Error('NPM version not found');
}
if (command.includes('git tag -l')) return '';
if (command.includes('gh release view')) {
throw new Error('GH release not found');
}
return '';
};
describe('Happy Path - Version Calculation', () => {
it('should calculate the next stable version from the latest preview', () => {
vi.mocked(execSync).mockImplementation(mockExecSync);
const result = getVersion({ type: 'stable' });
expect(result.releaseVersion).toBe('0.7.0');
expect(result.npmTag).toBe('latest');
expect(result.previousReleaseTag).toBe('v0.6.1');
});
it('should calculate the next preview version from the latest nightly', () => {
vi.mocked(execSync).mockImplementation(mockExecSync);
const result = getVersion({
type: 'preview',
'stable-base-version': '0.7.0',
});
expect(result.releaseVersion).toBe('0.8.0-preview.0');
expect(result.npmTag).toBe('preview');
expect(result.previousReleaseTag).toBe('v0.7.0-preview.1');
});
it('should calculate the next nightly version from package.json', () => {
vi.mocked(execSync).mockImplementation(mockExecSync);
const result = getVersion({ type: 'nightly' });
// Note: The base version now comes from package.json, not the previous nightly tag.
expect(result.releaseVersion).toBe('0.8.0-nightly.20250917.gd3bf8a3d');
expect(result.npmTag).toBe('nightly');
expect(result.previousReleaseTag).toBe('v0.8.0-nightly.20250916.abcdef');
});
it('should calculate the next patch version for a stable release', () => {
vi.mocked(execSync).mockImplementation(mockExecSync);
const result = getVersion({ type: 'patch', 'patch-from': 'stable' });
expect(result.releaseVersion).toBe('0.6.2');
expect(result.npmTag).toBe('latest');
expect(result.previousReleaseTag).toBe('v0.6.1');
});
it('should calculate the next patch version for a preview release', () => {
vi.mocked(execSync).mockImplementation(mockExecSync);
const result = getVersion({ type: 'patch', 'patch-from': 'preview' });
expect(result.releaseVersion).toBe('0.7.0-preview.2');
expect(result.npmTag).toBe('preview');
expect(result.previousReleaseTag).toBe('v0.7.0-preview.1');
});
});
describe('Advanced Scenarios', () => {
it('should ignore a deprecated version and use the next highest', () => {
const mockWithDeprecated = (command) => {
// The highest nightly is 0.9.0, but it's deprecated
if (command.includes('npm view') && command.includes('versions --json'))
return JSON.stringify([
'0.8.0-nightly.20250916.abcdef',
'0.9.0-nightly.20250917.deprecated', // This one is deprecated
]);
// Mock the deprecation check
if (
command.includes(
'npm view @google/gemini-cli@0.9.0-nightly.20250917.deprecated deprecated',
)
)
return 'This version is deprecated';
// The dist-tag still points to the older, valid version
if (command.includes('npm view') && command.includes('--tag=nightly'))
return '0.8.0-nightly.20250916.abcdef';
return mockExecSync(command);
};
vi.mocked(execSync).mockImplementation(mockWithDeprecated);
const result = getVersion({
type: 'preview',
'stable-base-version': '0.7.0',
});
// It should base the preview off 0.8.0, not the deprecated 0.9.0
expect(result.releaseVersion).toBe('0.8.0-preview.0');
});
it('should auto-increment patch version if the calculated one already exists', () => {
const mockWithConflict = (command) => {
// The calculated version 0.7.0 already exists as a git tag
if (command.includes("git tag -l 'v0.7.0'")) return 'v0.7.0';
// The next version, 0.7.1, is available
if (command.includes("git tag -l 'v0.7.1'")) return '';
return mockExecSync(command);
};
vi.mocked(execSync).mockImplementation(mockWithConflict);
const result = getVersion({ type: 'stable' });
// Should have skipped 0.7.0 and landed on 0.7.1
expect(result.releaseVersion).toBe('0.7.1');
});
it('should auto-increment preview number if the calculated one already exists', () => {
const mockWithConflict = (command) => {
// The calculated preview 0.8.0-preview.0 already exists on NPM
if (
command.includes(
'npm view @google/gemini-cli@0.8.0-preview.0 version',
)
)
return '0.8.0-preview.0';
// The next one is available
if (
command.includes(
'npm view @google/gemini-cli@0.8.0-preview.1 version',
)
)
throw new Error('Not found');
return mockExecSync(command);
};
vi.mocked(execSync).mockImplementation(mockWithConflict);
const result = getVersion({
type: 'preview',
'stable-base-version': '0.7.0',
});
// Should have skipped preview.0 and landed on preview.1
expect(result.releaseVersion).toBe('0.8.0-preview.1');
});
it('should preserve a git hash with a leading zero via the g prefix', () => {
const mockWithLeadingZeroHash = (command) => {
// Return an all-numeric hash with a leading zero
if (command.includes('git rev-parse --short HEAD')) return '017972622';
return mockExecSync(command);
};
vi.mocked(execSync).mockImplementation(mockWithLeadingZeroHash);
const result = getVersion({ type: 'nightly' });
// The 'g' prefix forces semver to treat this as an alphanumeric
// identifier, preventing it from stripping the leading zero.
expect(result.releaseVersion).toBe('0.8.0-nightly.20250917.g017972622');
});
});
});
+368
View File
@@ -0,0 +1,368 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { execSync } from 'node:child_process';
import { join } from 'node:path';
/**
* Helper function to run the patch-create-comment script with given parameters
*/
function runPatchCreateComment(args, env = {}) {
const scriptPath = join(
process.cwd(),
'scripts/releasing/patch-create-comment.js',
);
const fullEnv = {
...process.env,
...env,
};
try {
const result = execSync(`node ${scriptPath} ${args}`, {
encoding: 'utf8',
env: fullEnv,
});
return { stdout: result, stderr: '', success: true };
} catch (error) {
return {
stdout: error.stdout || '',
stderr: error.stderr || '',
success: false,
code: error.status,
};
}
}
describe('patch-create-comment', () => {
beforeEach(() => {
vi.stubEnv();
// Always run in test mode to avoid GitHub API calls
vi.stubEnv('TEST_MODE', 'true');
});
afterEach(() => {
vi.clearAllMocks();
vi.unstubAllEnvs();
});
describe('Environment flag', () => {
it('can be overridden with a flag', () => {
vi.stubEnv('ENVIRONMENT', 'dev');
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 0 --environment prod --commit abc1234 --channel preview --repository google-gemini/gemini-cli --test',
);
expect(result.success).toBe(true);
expect(result.stdout).toContain('🚀 **[Step 2/4] Patch PR Created!**');
expect(result.stdout).toContain('Environment**: `prod`');
});
it('reads from the ENVIRONMENT env variable', () => {
vi.stubEnv('ENVIRONMENT', 'dev');
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 0 --commit abc1234 --channel preview --repository google-gemini/gemini-cli --test',
);
expect(result.success).toBe(true);
expect(result.stdout).toContain('🚀 **[Step 2/4] Patch PR Created!**');
expect(result.stdout).toContain('Environment**: `dev`');
});
it('fails if the ENVIRONMENT is bogus', () => {
vi.stubEnv('ENVIRONMENT', 'totally-bogus');
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 0 --commit abc1234 --channel preview --repository google-gemini/gemini-cli --test',
);
expect(result.success).toBe(false);
expect(result.stderr).toContain(
'Argument: environment, Given: "totally-bogus", Choices: "prod", "dev"',
);
});
it('defaults to prod if not specified', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 0 --commit abc1234 --channel preview --repository google-gemini/gemini-cli --test',
);
expect(result.success).toBe(true);
expect(result.stdout).toContain('🚀 **[Step 2/4] Patch PR Created!**');
expect(result.stdout).toContain('Environment**: `prod`');
});
});
describe('Environment Variable vs File Reading', () => {
it('should prefer LOG_CONTENT environment variable over file', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 0 --commit abc1234 --channel preview --repository google-gemini/gemini-cli --test',
{
LOG_CONTENT:
'Creating hotfix branch hotfix/v0.5.3/preview/cherry-pick-abc1234 from release/v0.5.3',
},
);
expect(result.success).toBe(true);
expect(result.stdout).toContain('🚀 **[Step 2/4] Patch PR Created!**');
expect(result.stdout).toContain('Channel**: `preview`');
expect(result.stdout).toContain('Commit**: `abc1234`');
});
it('should use empty log content when LOG_CONTENT is not set', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 1 --commit abc1234 --channel stable --repository google-gemini/gemini-cli --test',
{}, // No LOG_CONTENT env var
);
expect(result.success).toBe(true);
expect(result.stdout).toContain(
'❌ **[Step 2/4] Patch creation failed!**',
);
expect(result.stdout).toContain(
'There was an error creating the patch release',
);
});
});
describe('Log Content Parsing - Success Scenarios', () => {
it('should generate success comment for clean cherry-pick', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 0 --commit abc1234 --channel stable --repository google-gemini/gemini-cli --test',
{
LOG_CONTENT:
'Creating hotfix branch hotfix/v0.4.1/stable/cherry-pick-abc1234 from release/v0.4.1\n✅ Cherry-pick successful - no conflicts detected',
},
);
expect(result.success).toBe(true);
expect(result.stdout).toContain('🚀 **[Step 2/4] Patch PR Created!**');
expect(result.stdout).toContain('Channel**: `stable`');
expect(result.stdout).toContain('Commit**: `abc1234`');
expect(result.stdout).not.toContain('⚠️ Status');
});
it('should generate conflict comment for cherry-pick with conflicts', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 0 --commit def5678 --channel preview --repository google-gemini/gemini-cli --test',
{
LOG_CONTENT:
'Creating hotfix branch hotfix/v0.5.0-preview.2/preview/cherry-pick-def5678 from release/v0.5.0-preview.2\nCherry-pick has conflicts in 2 file(s):\nCONFLICT (content): Merge conflict in package.json',
},
);
expect(result.success).toBe(true);
expect(result.stdout).toContain('🚀 **[Step 2/4] Patch PR Created!**');
expect(result.stdout).toContain(
'⚠️ Status**: Cherry-pick conflicts detected',
);
expect(result.stdout).toContain(
'⚠️ **Resolve conflicts** in the hotfix PR first',
);
expect(result.stdout).toContain('Channel**: `preview`');
});
});
describe('Log Content Parsing - Existing PR Scenarios', () => {
it('should detect existing PR and generate appropriate comment', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 0 --commit ghi9012 --channel stable --repository google-gemini/gemini-cli --test',
{
LOG_CONTENT:
'Hotfix branch hotfix/v0.4.1/stable/cherry-pick-ghi9012 already has an open PR.\nFound existing PR #8700: https://github.com/google-gemini/gemini-cli/pull/8700',
},
);
expect(result.success).toBe(true);
expect(result.stdout).toContain(
'️ **[Step 2/4] Patch PR already exists!**',
);
expect(result.stdout).toContain(
'A patch PR for this change already exists: [#8700](https://github.com/google-gemini/gemini-cli/pull/8700)',
);
expect(result.stdout).toContain(
'Review and approve the existing patch PR',
);
});
it('should detect branch exists but no PR scenario', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 0 --commit jkl3456 --channel preview --repository google-gemini/gemini-cli --test',
{
LOG_CONTENT:
'Hotfix branch hotfix/v0.5.0-preview.2/preview/cherry-pick-jkl3456 exists but has no open PR.\nHotfix branch hotfix/v0.5.0-preview.2/preview/cherry-pick-jkl3456 already exists.',
},
);
expect(result.success).toBe(true);
expect(result.stdout).toContain(
'️ **[Step 2/4] Patch branch exists but no PR found!**',
);
expect(result.stdout).toContain(
'Delete the branch: `git branch -D hotfix/v0.5.0-preview.2/preview/cherry-pick-jkl3456`',
);
expect(result.stdout).toContain('Run the patch command again');
});
});
describe('Log Content Parsing - Failure Scenarios', () => {
it('should generate failure comment when exit code is non-zero', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 1 --commit mno7890 --channel stable --repository google-gemini/gemini-cli --run-id 12345 --test',
{
LOG_CONTENT: 'Error: Failed to create patch',
},
);
expect(result.success).toBe(true);
expect(result.stdout).toContain(
'❌ **[Step 2/4] Patch creation failed!**',
);
expect(result.stdout).toContain(
'There was an error creating the patch release',
);
expect(result.stdout).toContain(
'View workflow run](https://github.com/google-gemini/gemini-cli/actions/runs/12345)',
);
});
it('should generate fallback failure comment when no output is generated', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 1 --commit pqr4567 --channel preview --repository google-gemini/gemini-cli --run-id 67890 --test',
{
LOG_CONTENT: '',
},
);
expect(result.success).toBe(true);
expect(result.stdout).toContain(
'❌ **[Step 2/4] Patch creation failed!**',
);
expect(result.stdout).toContain(
'There was an error creating the patch release',
);
});
});
describe('Channel and NPM Tag Detection', () => {
it('should correctly map stable channel to latest npm tag', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 0 --commit stu8901 --channel stable --repository google-gemini/gemini-cli --test',
{
LOG_CONTENT:
'Creating hotfix branch hotfix/v0.4.1/stable/cherry-pick-stu8901 from release/v0.4.1',
},
);
expect(result.success).toBe(true);
expect(result.stdout).toContain('will publish to npm tag `latest`');
});
it('should correctly map preview channel to preview npm tag', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 0 --commit vwx2345 --channel preview --repository google-gemini/gemini-cli --test',
{
LOG_CONTENT:
'Creating hotfix branch hotfix/v0.5.0-preview.2/preview/cherry-pick-vwx2345 from release/v0.5.0-preview.2',
},
);
expect(result.success).toBe(true);
expect(result.stdout).toContain('will publish to npm tag `preview`');
});
});
describe('No Original PR Scenario', () => {
it('should skip comment when no original PR is specified', () => {
const result = runPatchCreateComment(
'--original-pr 0 --exit-code 0 --commit yza6789 --channel stable --repository google-gemini/gemini-cli --test',
{
LOG_CONTENT:
'Creating hotfix branch hotfix/v0.4.1/stable/cherry-pick-yza6789 from release/v0.4.1',
ORIGINAL_PR: '', // Override with empty PR
},
);
expect(result.success).toBe(true);
expect(result.stdout).toContain(
'No original PR specified, skipping comment',
);
});
});
describe('Error Handling', () => {
it('should handle empty LOG_CONTENT gracefully', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 1 --commit bcd0123 --channel stable --repository google-gemini/gemini-cli --test',
{ LOG_CONTENT: '' }, // Empty log content
);
expect(result.success).toBe(true);
expect(result.stdout).toContain(
'❌ **[Step 2/4] Patch creation failed!**',
);
expect(result.stdout).toContain(
'There was an error creating the patch release',
);
});
});
describe('GitHub App Permission Scenarios', () => {
it('should parse manual commands with clipboard emoji correctly', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 1 --commit abc1234 --channel stable --repository google-gemini/gemini-cli --test',
{
LOG_CONTENT: `❌ Failed to create release branch due to insufficient GitHub App permissions.
📋 Please run these commands manually to create the branch:
\`\`\`bash
git checkout -b hotfix/v0.4.1/stable/cherry-pick-abc1234 v0.4.1
git push origin hotfix/v0.4.1/stable/cherry-pick-abc1234
\`\`\``,
},
);
expect(result.success).toBe(true);
expect(result.stdout).toContain(
'🔒 **[Step 2/4] GitHub App Permission Issue**',
);
expect(result.stdout).toContain(
'Please run these commands manually to create the release branch:',
);
expect(result.stdout).toContain(
'git checkout -b hotfix/v0.4.1/stable/cherry-pick-abc1234 v0.4.1',
);
expect(result.stdout).toContain(
'git push origin hotfix/v0.4.1/stable/cherry-pick-abc1234',
);
});
});
describe('Test Mode Flag', () => {
it('should generate mock content in test mode for success', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 0 --commit efg4567 --channel preview --repository google-gemini/gemini-cli --test',
);
expect(result.success).toBe(true);
expect(result.stdout).toContain(
'🧪 TEST MODE - No API calls will be made',
);
expect(result.stdout).toContain('🚀 **[Step 2/4] Patch PR Created!**');
});
it('should generate mock content in test mode for failure', () => {
const result = runPatchCreateComment(
'--original-pr 8655 --exit-code 1 --commit hij8901 --channel stable --repository google-gemini/gemini-cli --test',
);
expect(result.success).toBe(true);
expect(result.stdout).toContain(
'❌ **[Step 2/4] Patch creation failed!**',
);
});
});
});
+80
View File
@@ -0,0 +1,80 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it, vi } from 'vitest';
vi.unmock('fs');
vi.unmock('node:fs');
import * as esbuild from 'esbuild';
import path from 'node:path';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { fileURLToPath, pathToFileURL } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '../../');
describe('proxy-agent bundle shape', () => {
it('preserves named constructors after ESM splitting', async () => {
const tmpDir = mkdtempSync(path.join(tmpdir(), 'gemini-proxy-test-'));
const entryFile = path.join(tmpDir, 'entry.ts');
// Create a minimal entry file that dynamically imports the proxy agents
writeFileSync(
entryFile,
`
export async function getAgents() {
const httpsMod = await import('https-proxy-agent');
const httpMod = await import('http-proxy-agent');
return {
https: httpsMod,
http: httpMod,
};
}
`,
);
// Bundle with the exact same splitting config and aliases as cliConfig
await esbuild.build({
entryPoints: { gemini: entryFile },
outdir: path.join(tmpDir, 'bundle'),
bundle: true,
splitting: true,
format: 'esm',
platform: 'node',
outExtension: { '.js': '.mjs' },
alias: {
'https-proxy-agent': path.resolve(
projectRoot,
'packages/cli/src/patches/https-proxy-agent.ts',
),
'http-proxy-agent': path.resolve(
projectRoot,
'packages/cli/src/patches/http-proxy-agent.ts',
),
},
});
// Import the bundled chunk
const bundledEntryUrl = pathToFileURL(
path.join(tmpDir, 'bundle/gemini.mjs'),
).href;
const { getAgents } = await import(bundledEntryUrl);
const { https, http } = await getAgents();
// Verify named exports exist
expect(typeof https.HttpsProxyAgent).toBe('function');
expect(typeof http.HttpProxyAgent).toBe('function');
// Verify they are constructable
expect(() => new https.HttpsProxyAgent('http://127.0.0.1:9')).not.toThrow();
expect(() => new http.HttpProxyAgent('http://127.0.0.1:9')).not.toThrow();
// Cleanup
rmSync(tmpDir, { recursive: true, force: true });
});
});
+56
View File
@@ -0,0 +1,56 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
const mockSpawn = vi.fn(() => ({ on: vi.fn(), pid: 123 }));
vi.mock('node:child_process', () => ({
spawn: mockSpawn,
execSync: vi.fn(),
}));
vi.mock('node:fs', () => ({
readFileSync: vi.fn(),
writeFileSync: vi.fn(),
openSync: vi.fn(() => 1),
unlinkSync: vi.fn(),
mkdirSync: vi.fn(),
}));
vi.mock('../telemetry_utils.js', () => ({
ensureBinary: vi.fn(() => Promise.resolve('/fake/path/to/otelcol-contrib')),
waitForPort: vi.fn(() => Promise.resolve()),
manageTelemetrySettings: vi.fn(),
registerCleanup: vi.fn(),
fileExists: vi.fn(() => true), // Assume all files exist for simplicity
OTEL_DIR: '/tmp/otel',
BIN_DIR: '/tmp/bin',
}));
describe('telemetry_gcp.js', () => {
beforeEach(() => {
vi.resetModules(); // This is key to re-run the script
vi.clearAllMocks();
process.env.OTLP_GOOGLE_CLOUD_PROJECT = 'test-project';
// Clear the env var before each test
delete process.env.GEMINI_CLI_CREDENTIALS_PATH;
});
afterEach(() => {
delete process.env.OTLP_GOOGLE_CLOUD_PROJECT;
});
it('should not set GOOGLE_APPLICATION_CREDENTIALS when env var is not set', async () => {
await import('../telemetry_gcp.js');
expect(mockSpawn).toHaveBeenCalled();
const spawnOptions = mockSpawn.mock.calls[0][2];
expect(spawnOptions?.env).not.toHaveProperty(
'GOOGLE_APPLICATION_CREDENTIALS',
);
});
});
+15
View File
@@ -0,0 +1,15 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi } from 'vitest';
vi.mock('fs', async () => {
const actual = await vi.importActual<typeof import('fs')>('fs');
return {
...actual,
appendFileSync: vi.fn(),
};
});
+139
View File
@@ -0,0 +1,139 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import {
buildToolRegistry,
resolveToolName,
getToolsByCategory,
type ToolCategory,
} from '../utils/tool-registry.js';
describe('tool-registry', () => {
const registry = buildToolRegistry();
describe('buildToolRegistry', () => {
it('includes all canonical built-in tools', () => {
expect(registry.totalTools).toBeGreaterThanOrEqual(26);
});
it('every tool has a valid category', () => {
for (const [name, entry] of registry.tools) {
expect(entry.category).toBeTruthy();
expect(entry.name).toBe(name);
}
});
it('byCategory entries match tools map', () => {
let categoryTotal = 0;
for (const [, entries] of registry.byCategory) {
for (const entry of entries) {
expect(registry.tools.get(entry.name)).toBe(entry);
}
categoryTotal += entries.length;
}
expect(categoryTotal).toBe(registry.totalTools);
});
it('aliasLookup covers every canonical name', () => {
for (const name of registry.tools.keys()) {
expect(registry.aliasLookup.get(name)).toBe(name);
}
});
it('aliasLookup covers every legacy alias', () => {
for (const [, entry] of registry.tools) {
for (const alias of entry.aliases) {
expect(registry.aliasLookup.get(alias)).toBe(entry.name);
}
}
});
it('is deterministic across calls', () => {
const second = buildToolRegistry();
expect([...second.tools.keys()]).toEqual([...registry.tools.keys()]);
expect(second.totalTools).toBe(registry.totalTools);
});
});
describe('resolveToolName', () => {
it('resolves canonical names to themselves', () => {
expect(resolveToolName(registry, 'grep_search')).toBe('grep_search');
expect(resolveToolName(registry, 'run_shell_command')).toBe(
'run_shell_command',
);
});
it('resolves legacy alias to canonical name', () => {
expect(resolveToolName(registry, 'search_file_content')).toBe(
'grep_search',
);
});
it('returns undefined for unknown tool names', () => {
expect(resolveToolName(registry, 'nonexistent_tool')).toBeUndefined();
});
it('returns undefined for empty string', () => {
expect(resolveToolName(registry, '')).toBeUndefined();
});
});
describe('getToolsByCategory', () => {
it('returns file-system tools', () => {
const tools = getToolsByCategory(registry, 'file-system');
const names = tools.map((t) => t.name);
expect(names).toContain('glob');
expect(names).toContain('grep_search');
expect(names).toContain('read_file');
expect(names).toContain('write_file');
expect(names).toContain('replace');
});
it('returns task-tracker tools', () => {
const tools = getToolsByCategory(registry, 'task-tracker');
const names = tools.map((t) => t.name);
expect(names).toContain('tracker_create_task');
expect(names).toContain('tracker_update_task');
expect(names).toContain('tracker_get_task');
expect(names).toContain('tracker_list_tasks');
expect(names).toContain('tracker_add_dependency');
expect(names).toContain('tracker_visualize');
expect(names).toHaveLength(6);
});
it('returns agent tools', () => {
const tools = getToolsByCategory(registry, 'agent');
const names = tools.map((t) => t.name);
expect(names).toContain('invoke_agent');
expect(names).toContain('complete_task');
expect(names).toContain('update_topic');
});
it('returns empty array for unknown category', () => {
expect(
getToolsByCategory(registry, 'nonexistent' as ToolCategory),
).toEqual([]);
});
it('every defined category has at least one tool', () => {
const expectedCategories: ToolCategory[] = [
'file-system',
'shell',
'web',
'planning',
'user-interaction',
'skills',
'task-tracker',
'agent',
'mcp',
];
for (const cat of expectedCategories) {
expect(getToolsByCategory(registry, cat).length).toBeGreaterThan(0);
}
});
});
});
+26
View File
@@ -0,0 +1,26 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['scripts/tests/**/*.test.{js,ts}'],
setupFiles: ['scripts/tests/test-setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov'],
},
poolOptions: {
threads: {
minThreads: 8,
maxThreads: 16,
},
},
},
});