mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
fix(cli): gracefully handle --resume when no sessions exist (#21429)
This commit is contained in:
@@ -747,6 +747,60 @@ describe('gemini.tsx main function kitty protocol', () => {
|
|||||||
emitFeedbackSpy.mockRestore();
|
emitFeedbackSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should start normally with a warning when no sessions found for resume', async () => {
|
||||||
|
const { SessionSelector, SessionError } = await import(
|
||||||
|
'./utils/sessionUtils.js'
|
||||||
|
);
|
||||||
|
vi.mocked(SessionSelector).mockImplementation(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
resolveSession: vi
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValue(SessionError.noSessionsFound()),
|
||||||
|
}) as unknown as InstanceType<typeof SessionSelector>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const processExitSpy = vi
|
||||||
|
.spyOn(process, 'exit')
|
||||||
|
.mockImplementation((code) => {
|
||||||
|
throw new MockProcessExitError(code);
|
||||||
|
});
|
||||||
|
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
|
||||||
|
|
||||||
|
vi.mocked(loadSettings).mockReturnValue(
|
||||||
|
createMockSettings({
|
||||||
|
merged: { advanced: {}, security: { auth: {} }, ui: { theme: 'test' } },
|
||||||
|
workspace: { settings: {} },
|
||||||
|
setValue: vi.fn(),
|
||||||
|
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.mocked(parseArguments).mockResolvedValue({
|
||||||
|
promptInteractive: false,
|
||||||
|
resume: 'latest',
|
||||||
|
} as unknown as CliArgs);
|
||||||
|
vi.mocked(loadCliConfig).mockResolvedValue(
|
||||||
|
createMockConfig({
|
||||||
|
isInteractive: () => true,
|
||||||
|
getQuestion: () => '',
|
||||||
|
getSandbox: () => undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await main();
|
||||||
|
|
||||||
|
// Should NOT have crashed
|
||||||
|
expect(processExitSpy).not.toHaveBeenCalled();
|
||||||
|
// Should NOT have emitted a feedback error
|
||||||
|
expect(emitFeedbackSpy).not.toHaveBeenCalledWith(
|
||||||
|
'error',
|
||||||
|
expect.stringContaining('Error resuming session'),
|
||||||
|
);
|
||||||
|
processExitSpy.mockRestore();
|
||||||
|
emitFeedbackSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
it.skip('should log error when cleanupExpiredSessions fails', async () => {
|
it.skip('should log error when cleanupExpiredSessions fails', async () => {
|
||||||
const { cleanupExpiredSessions } = await import(
|
const { cleanupExpiredSessions } = await import(
|
||||||
'./utils/sessionCleanup.js'
|
'./utils/sessionCleanup.js'
|
||||||
@@ -959,13 +1013,18 @@ describe('gemini.tsx main function exit codes', () => {
|
|||||||
resume: 'invalid-session',
|
resume: 'invalid-session',
|
||||||
} as unknown as CliArgs);
|
} as unknown as CliArgs);
|
||||||
|
|
||||||
vi.mock('./utils/sessionUtils.js', () => ({
|
vi.mock('./utils/sessionUtils.js', async (importOriginal) => {
|
||||||
|
const original =
|
||||||
|
await importOriginal<typeof import('./utils/sessionUtils.js')>();
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
SessionSelector: vi.fn().mockImplementation(() => ({
|
SessionSelector: vi.fn().mockImplementation(() => ({
|
||||||
resolveSession: vi
|
resolveSession: vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockRejectedValue(new Error('Session not found')),
|
.mockRejectedValue(new Error('Session not found')),
|
||||||
})),
|
})),
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|
||||||
process.env['GEMINI_API_KEY'] = 'test-key';
|
process.env['GEMINI_API_KEY'] = 'test-key';
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
|
|||||||
import { checkForUpdates } from './ui/utils/updateCheck.js';
|
import { checkForUpdates } from './ui/utils/updateCheck.js';
|
||||||
import { handleAutoUpdate } from './utils/handleAutoUpdate.js';
|
import { handleAutoUpdate } from './utils/handleAutoUpdate.js';
|
||||||
import { appEvents, AppEvent } from './utils/events.js';
|
import { appEvents, AppEvent } from './utils/events.js';
|
||||||
import { SessionSelector } from './utils/sessionUtils.js';
|
import { SessionError, SessionSelector } from './utils/sessionUtils.js';
|
||||||
import { SettingsContext } from './ui/contexts/SettingsContext.js';
|
import { SettingsContext } from './ui/contexts/SettingsContext.js';
|
||||||
import { MouseProvider } from './ui/contexts/MouseContext.js';
|
import { MouseProvider } from './ui/contexts/MouseContext.js';
|
||||||
import { StreamingState } from './ui/types.js';
|
import { StreamingState } from './ui/types.js';
|
||||||
@@ -706,6 +706,17 @@ export async function main() {
|
|||||||
// Use the existing session ID to continue recording to the same session
|
// Use the existing session ID to continue recording to the same session
|
||||||
config.setSessionId(resumedSessionData.conversation.sessionId);
|
config.setSessionId(resumedSessionData.conversation.sessionId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error instanceof SessionError &&
|
||||||
|
error.code === 'NO_SESSIONS_FOUND'
|
||||||
|
) {
|
||||||
|
// No sessions to resume — start a fresh session with a warning
|
||||||
|
startupWarnings.push({
|
||||||
|
id: 'resume-no-sessions',
|
||||||
|
message: error.message,
|
||||||
|
priority: WarningPriority.High,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
coreEvents.emitFeedback(
|
coreEvents.emitFeedback(
|
||||||
'error',
|
'error',
|
||||||
`Error resuming session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
`Error resuming session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
@@ -714,6 +725,7 @@ export async function main() {
|
|||||||
process.exit(ExitCodes.FATAL_INPUT_ERROR);
|
process.exit(ExitCodes.FATAL_INPUT_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cliStartupHandle?.end();
|
cliStartupHandle?.end();
|
||||||
// Render UI, passing necessary config values. Check that there is no command line question.
|
// Render UI, passing necessary config values. Check that there is no command line question.
|
||||||
|
|||||||
@@ -341,6 +341,29 @@ describe('SessionSelector', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw SessionError with NO_SESSIONS_FOUND when resolving latest with no sessions', async () => {
|
||||||
|
// Empty chats directory — no session files
|
||||||
|
const chatsDir = path.join(tmpDir, 'chats');
|
||||||
|
await fs.mkdir(chatsDir, { recursive: true });
|
||||||
|
|
||||||
|
const emptyConfig = {
|
||||||
|
storage: {
|
||||||
|
getProjectTempDir: () => tmpDir,
|
||||||
|
},
|
||||||
|
getSessionId: () => 'current-session-id',
|
||||||
|
} as Partial<Config> as Config;
|
||||||
|
|
||||||
|
const sessionSelector = new SessionSelector(emptyConfig);
|
||||||
|
|
||||||
|
await expect(sessionSelector.resolveSession('latest')).rejects.toSatisfy(
|
||||||
|
(error) => {
|
||||||
|
expect(error).toBeInstanceOf(SessionError);
|
||||||
|
expect((error as SessionError).code).toBe('NO_SESSIONS_FOUND');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should not list sessions with only system messages', async () => {
|
it('should not list sessions with only system messages', async () => {
|
||||||
const sessionIdWithUser = randomUUID();
|
const sessionIdWithUser = randomUUID();
|
||||||
const sessionIdSystemOnly = randomUUID();
|
const sessionIdSystemOnly = randomUUID();
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ export class SessionSelector {
|
|||||||
const sessions = await this.listSessions();
|
const sessions = await this.listSessions();
|
||||||
|
|
||||||
if (sessions.length === 0) {
|
if (sessions.length === 0) {
|
||||||
throw new Error('No previous sessions found for this project.');
|
throw SessionError.noSessionsFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by startTime (oldest first, so newest sessions get highest numbers)
|
// Sort by startTime (oldest first, so newest sessions get highest numbers)
|
||||||
|
|||||||
Reference in New Issue
Block a user