mirror of
https://github.com/google-gemini/gemini-cli
synced 2026-04-21 13:37:17 +00:00
Merge 1ab411addd into a38e2f0048
This commit is contained in:
commit
ae5b34b0e8
5 changed files with 167 additions and 10 deletions
|
|
@ -60,7 +60,8 @@ These commands are available within the interactive REPL.
|
|||
| `--allowed-tools` | - | array | - | **Deprecated.** Use the [Policy Engine](../reference/policy-engine.md) instead. Tools that are allowed to run without confirmation (comma-separated or multiple flags) |
|
||||
| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) |
|
||||
| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit |
|
||||
| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (for example `--resume 5`) |
|
||||
| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) |
|
||||
| `--session-id` | - | string | - | Start a new session with a specific session ID for deterministic orchestration. Cannot be combined with `--resume`. |
|
||||
| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit |
|
||||
| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) |
|
||||
| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) |
|
||||
|
|
|
|||
|
|
@ -658,6 +658,76 @@ describe('parseArguments', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('should parse --session-id', async () => {
|
||||
process.argv = ['node', 'script.js', '--session-id', 'my-session-123'];
|
||||
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
expect(argv.sessionId).toBe('my-session-123');
|
||||
});
|
||||
|
||||
it('should trim --session-id value', async () => {
|
||||
process.argv = ['node', 'script.js', '--session-id', ' my-session-123 '];
|
||||
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
expect(argv.sessionId).toBe('my-session-123');
|
||||
});
|
||||
|
||||
it('should throw an error when using --session-id with --resume', async () => {
|
||||
process.argv = [
|
||||
'node',
|
||||
'script.js',
|
||||
'--session-id',
|
||||
'my-session-123',
|
||||
'--resume',
|
||||
'latest',
|
||||
];
|
||||
|
||||
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('process.exit called');
|
||||
});
|
||||
|
||||
const mockConsoleError = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(parseArguments(createTestMergedSettings())).rejects.toThrow(
|
||||
'process.exit called',
|
||||
);
|
||||
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Cannot use both --session-id and --resume together',
|
||||
),
|
||||
);
|
||||
|
||||
mockExit.mockRestore();
|
||||
mockConsoleError.mockRestore();
|
||||
});
|
||||
|
||||
it.each(['.', '..', ' '])(
|
||||
'should reject invalid --session-id value "%s"',
|
||||
async (badSessionId) => {
|
||||
process.argv = ['node', 'script.js', '--session-id', badSessionId];
|
||||
|
||||
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('process.exit called');
|
||||
});
|
||||
|
||||
const mockConsoleError = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(parseArguments(createTestMergedSettings())).rejects.toThrow(
|
||||
'process.exit called',
|
||||
);
|
||||
|
||||
expect(mockConsoleError).toHaveBeenCalled();
|
||||
|
||||
mockExit.mockRestore();
|
||||
mockConsoleError.mockRestore();
|
||||
},
|
||||
);
|
||||
|
||||
it('should support comma-separated values for --allowed-tools', async () => {
|
||||
process.argv = [
|
||||
'node',
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ export interface CliArgs {
|
|||
extensions: string[] | undefined;
|
||||
listExtensions: boolean | undefined;
|
||||
resume: string | typeof RESUME_LATEST | undefined;
|
||||
sessionId: string | undefined;
|
||||
listSessions: boolean | undefined;
|
||||
deleteSession: string | undefined;
|
||||
includeDirectories: string[] | undefined;
|
||||
|
|
@ -244,6 +245,23 @@ export async function parseArguments(
|
|||
if (argv['yolo'] && argv['approvalMode']) {
|
||||
return 'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.';
|
||||
}
|
||||
if (argv['sessionId'] && argv['resume']) {
|
||||
return 'Cannot use both --session-id and --resume together';
|
||||
}
|
||||
|
||||
const providedSessionId = argv['sessionId'];
|
||||
if (typeof providedSessionId === 'string') {
|
||||
const trimmedSessionId = providedSessionId.trim();
|
||||
|
||||
if (!trimmedSessionId) {
|
||||
return 'The --session-id flag requires a non-empty value';
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(trimmedSessionId)) {
|
||||
return 'Invalid --session-id value. Only alphanumeric characters, hyphens, and underscores are allowed.';
|
||||
}
|
||||
argv['sessionId'] = trimmedSessionId;
|
||||
}
|
||||
|
||||
const outputFormat = argv['outputFormat'];
|
||||
if (
|
||||
|
|
@ -399,6 +417,11 @@ export async function parseArguments(
|
|||
return trimmed;
|
||||
},
|
||||
})
|
||||
.option('session-id', {
|
||||
type: 'string',
|
||||
description:
|
||||
'Start a new session with a specific session ID (for deterministic orchestration).',
|
||||
})
|
||||
.option('list-sessions', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -547,6 +547,7 @@ describe('gemini.tsx main function kitty protocol', () => {
|
|||
screenReader: undefined,
|
||||
useWriteTodos: undefined,
|
||||
resume: undefined,
|
||||
sessionId: undefined,
|
||||
listSessions: undefined,
|
||||
deleteSession: undefined,
|
||||
outputFormat: undefined,
|
||||
|
|
@ -605,6 +606,7 @@ describe('gemini.tsx main function kitty protocol', () => {
|
|||
screenReader: undefined,
|
||||
useWriteTodos: undefined,
|
||||
resume: undefined,
|
||||
sessionId: undefined,
|
||||
listSessions: undefined,
|
||||
deleteSession: undefined,
|
||||
outputFormat: undefined,
|
||||
|
|
@ -623,6 +625,67 @@ describe('gemini.tsx main function kitty protocol', () => {
|
|||
resumeSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should pass provided --session-id to loadCliConfig', async () => {
|
||||
vi.mocked(loadCliConfig).mockResolvedValue(
|
||||
createMockConfig({
|
||||
isInteractive: () => true,
|
||||
getQuestion: () => '',
|
||||
getSandbox: () => undefined,
|
||||
}),
|
||||
);
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: {
|
||||
advanced: {},
|
||||
security: { auth: {} },
|
||||
ui: {},
|
||||
},
|
||||
}),
|
||||
);
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
model: undefined,
|
||||
sandbox: undefined,
|
||||
debug: undefined,
|
||||
prompt: undefined,
|
||||
promptInteractive: undefined,
|
||||
query: undefined,
|
||||
yolo: undefined,
|
||||
approvalMode: undefined,
|
||||
policy: undefined,
|
||||
adminPolicy: undefined,
|
||||
allowedMcpServerNames: undefined,
|
||||
allowedTools: undefined,
|
||||
experimentalAcp: undefined,
|
||||
extensions: undefined,
|
||||
listExtensions: undefined,
|
||||
includeDirectories: undefined,
|
||||
screenReader: undefined,
|
||||
useWriteTodos: undefined,
|
||||
resume: undefined,
|
||||
sessionId: 'fixed-session-id',
|
||||
listSessions: undefined,
|
||||
deleteSession: undefined,
|
||||
outputFormat: undefined,
|
||||
fakeResponses: undefined,
|
||||
recordResponses: undefined,
|
||||
rawOutput: undefined,
|
||||
acceptRawOutputRisk: undefined,
|
||||
isCommand: undefined,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await main();
|
||||
});
|
||||
|
||||
const sessionIdsPassedToConfig = vi
|
||||
.mocked(loadCliConfig)
|
||||
.mock.calls.map((call) => call[1]);
|
||||
expect(sessionIdsPassedToConfig.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
sessionIdsPassedToConfig.every((id) => id === 'fixed-session-id'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ flag: 'listExtensions' },
|
||||
{ flag: 'listSessions' },
|
||||
|
|
@ -876,9 +939,8 @@ describe('gemini.tsx main function kitty protocol', () => {
|
|||
});
|
||||
|
||||
it('should start normally with a warning when no sessions found for resume', async () => {
|
||||
const { SessionSelector, SessionError } = await import(
|
||||
'./utils/sessionUtils.js'
|
||||
);
|
||||
const { SessionSelector, SessionError } =
|
||||
await import('./utils/sessionUtils.js');
|
||||
vi.mocked(SessionSelector).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
|
|
@ -933,9 +995,8 @@ describe('gemini.tsx main function kitty protocol', () => {
|
|||
});
|
||||
|
||||
it.skip('should log error when cleanupExpiredSessions fails', async () => {
|
||||
const { cleanupExpiredSessions } = await import(
|
||||
'./utils/sessionCleanup.js'
|
||||
);
|
||||
const { cleanupExpiredSessions } =
|
||||
await import('./utils/sessionCleanup.js');
|
||||
vi.mocked(cleanupExpiredSessions).mockRejectedValue(
|
||||
new Error('Cleanup failed'),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -319,7 +319,9 @@ export async function main() {
|
|||
|
||||
const argv = await argvPromise;
|
||||
|
||||
const { sessionId, resumedSessionData } = await resolveSessionId(argv.resume);
|
||||
const { sessionId: resolvedSessionId, resumedSessionData } =
|
||||
await resolveSessionId(argv.resume);
|
||||
const sessionId = argv.sessionId ?? resolvedSessionId;
|
||||
|
||||
if (
|
||||
(argv.allowedTools && argv.allowedTools.length > 0) ||
|
||||
|
|
@ -726,7 +728,7 @@ export async function main() {
|
|||
process.exit(ExitCodes.FATAL_INPUT_ERROR);
|
||||
}
|
||||
|
||||
const prompt_id = sessionId;
|
||||
const prompt_id = config.getSessionId();
|
||||
logUserPrompt(
|
||||
config,
|
||||
new UserPromptEvent(
|
||||
|
|
@ -746,7 +748,7 @@ export async function main() {
|
|||
await config.refreshAuth(authType);
|
||||
|
||||
if (config.getDebugMode()) {
|
||||
debugLogger.log('Session ID: %s', sessionId);
|
||||
debugLogger.log('Session ID: %s', config.getSessionId());
|
||||
}
|
||||
|
||||
initializeOutputListenersAndFlush();
|
||||
|
|
|
|||
Loading…
Reference in a new issue