This commit is contained in:
Amaan 2026-04-21 14:59:09 +05:30 committed by GitHub
commit ae5b34b0e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 167 additions and 10 deletions

View file

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

View file

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

View file

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

View file

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

View file

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