fix(cli): gracefully handle --resume when no sessions exist (#21429)

This commit is contained in:
Sandy Tao
2026-03-06 11:02:33 -08:00
committed by GitHub
parent 6f579934db
commit 42d367d72f
4 changed files with 109 additions and 15 deletions
+61 -2
View File
@@ -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 {
+13 -1
View File
@@ -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();
+1 -1
View File
@@ -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)