mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 13:22:35 -07:00
fix(cli): restore resume for legacy sessions (#26577)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
This commit is contained in:
@@ -1189,6 +1189,39 @@ describe('resolveSessionId', () => {
|
||||
expect(sessionId).toBe('new-id');
|
||||
expect(resumedSessionData).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should exit with FATAL_INPUT_ERROR when explicit resume session is missing', async () => {
|
||||
vi.mocked(SessionSelector).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
resolveSession: vi
|
||||
.fn()
|
||||
.mockRejectedValue(SessionError.noSessionsFound()),
|
||||
}) as unknown as InstanceType<typeof SessionSelector>,
|
||||
);
|
||||
|
||||
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
|
||||
const processExitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation((code) => {
|
||||
throw new MockProcessExitError(code);
|
||||
});
|
||||
|
||||
try {
|
||||
await resolveSessionId('explicit-session-id');
|
||||
} catch (e) {
|
||||
if (!(e instanceof MockProcessExitError)) throw e;
|
||||
}
|
||||
|
||||
expect(emitFeedbackSpy).toHaveBeenCalledWith(
|
||||
'error',
|
||||
expect.stringContaining('Error resuming session:'),
|
||||
);
|
||||
expect(processExitSpy).toHaveBeenCalledWith(ExitCodes.FATAL_INPUT_ERROR);
|
||||
|
||||
emitFeedbackSpy.mockRestore();
|
||||
processExitSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('gemini.tsx main function exit codes', () => {
|
||||
|
||||
@@ -85,7 +85,11 @@ import { validateAuthMethod } from './config/auth.js';
|
||||
import { runAcpClient } from './acp/acpStdioTransport.js';
|
||||
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
|
||||
import { appEvents, AppEvent } from './utils/events.js';
|
||||
import { SessionError, SessionSelector } from './utils/sessionUtils.js';
|
||||
import {
|
||||
RESUME_LATEST,
|
||||
SessionError,
|
||||
SessionSelector,
|
||||
} from './utils/sessionUtils.js';
|
||||
|
||||
import { relaunchOnExitCode } from './utils/relaunch.js';
|
||||
import { loadSandboxConfig } from './config/sandboxConfig.js';
|
||||
@@ -309,8 +313,10 @@ export async function resolveSessionId(
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof SessionError && error.code === 'NO_SESSIONS_FOUND') {
|
||||
coreEvents.emitFeedback('warning', error.message);
|
||||
return { sessionId: createSessionId() };
|
||||
if (resumeArg === RESUME_LATEST) {
|
||||
coreEvents.emitFeedback('warning', error.message);
|
||||
return { sessionId: createSessionId() };
|
||||
}
|
||||
}
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
|
||||
@@ -616,6 +616,120 @@ describe('SessionSelector', () => {
|
||||
expect(sessions.length).toBe(1);
|
||||
expect(sessions[0].id).toBe(mainSessionId);
|
||||
});
|
||||
|
||||
it('should list legacy session JSON without timestamps (regression #18593)', async () => {
|
||||
const sessionId = randomUUID();
|
||||
|
||||
const chatsDir = path.join(tmpDir, 'chats');
|
||||
await fs.mkdir(chatsDir, { recursive: true });
|
||||
|
||||
const session = {
|
||||
sessionId,
|
||||
projectHash: 'test-hash',
|
||||
messages: [
|
||||
{
|
||||
type: 'user',
|
||||
content: 'Legacy session message',
|
||||
id: 'msg1',
|
||||
timestamp: '2024-01-01T10:00:00.000Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const filePath = path.join(
|
||||
chatsDir,
|
||||
`${SESSION_FILE_PREFIX}2024-01-01T10-00-${sessionId.slice(0, 8)}.json`,
|
||||
);
|
||||
await fs.writeFile(filePath, JSON.stringify(session, null, 2));
|
||||
const fallbackTimestamp = new Date('2024-01-01T10:30:00.000Z');
|
||||
await fs.utimes(filePath, fallbackTimestamp, fallbackTimestamp);
|
||||
|
||||
const sessionSelector = new SessionSelector(storage);
|
||||
const sessions = await sessionSelector.listSessions();
|
||||
|
||||
expect(sessions.length).toBe(1);
|
||||
expect(sessions[0].id).toBe(sessionId);
|
||||
expect(sessions[0].startTime).toBe(fallbackTimestamp.toISOString());
|
||||
expect(sessions[0].lastUpdated).toBe(fallbackTimestamp.toISOString());
|
||||
});
|
||||
|
||||
it('should resolve legacy session JSON without timestamps by UUID (regression #18593)', async () => {
|
||||
const sessionId = randomUUID();
|
||||
|
||||
const chatsDir = path.join(tmpDir, 'chats');
|
||||
await fs.mkdir(chatsDir, { recursive: true });
|
||||
|
||||
const session = {
|
||||
sessionId,
|
||||
projectHash: 'test-hash',
|
||||
messages: [
|
||||
{
|
||||
type: 'user',
|
||||
content: 'Legacy session message',
|
||||
id: 'msg1',
|
||||
timestamp: '2024-01-01T10:00:00.000Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const filePath = path.join(
|
||||
chatsDir,
|
||||
`${SESSION_FILE_PREFIX}2024-01-01T10-00-${sessionId.slice(0, 8)}.json`,
|
||||
);
|
||||
await fs.writeFile(filePath, JSON.stringify(session, null, 2));
|
||||
const fallbackTimestamp = new Date('2024-01-01T10:30:00.000Z');
|
||||
await fs.utimes(filePath, fallbackTimestamp, fallbackTimestamp);
|
||||
|
||||
const sessionSelector = new SessionSelector(storage);
|
||||
const result = await sessionSelector.resolveSession(sessionId);
|
||||
|
||||
expect(result.sessionData.sessionId).toBe(sessionId);
|
||||
expect(result.sessionData.startTime).toBe(fallbackTimestamp.toISOString());
|
||||
expect(result.sessionData.lastUpdated).toBe(
|
||||
fallbackTimestamp.toISOString(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw INVALID_SESSION_IDENTIFIER for a UUID that does not exist on disk at all', async () => {
|
||||
const existingSessionId = randomUUID();
|
||||
const nonExistentId = randomUUID();
|
||||
|
||||
const chatsDir = path.join(tmpDir, 'chats');
|
||||
await fs.mkdir(chatsDir, { recursive: true });
|
||||
|
||||
const session = {
|
||||
sessionId: existingSessionId,
|
||||
projectHash: 'test-hash',
|
||||
startTime: '2024-01-01T10:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T10:30:00.000Z',
|
||||
messages: [
|
||||
{
|
||||
type: 'user',
|
||||
content: 'Hello',
|
||||
id: 'msg1',
|
||||
timestamp: '2024-01-01T10:00:00.000Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(
|
||||
chatsDir,
|
||||
`${SESSION_FILE_PREFIX}2024-01-01T10-00-${existingSessionId.slice(0, 8)}.json`,
|
||||
),
|
||||
JSON.stringify(session, null, 2),
|
||||
);
|
||||
|
||||
const sessionSelector = new SessionSelector(storage);
|
||||
|
||||
await expect(sessionSelector.findSession(nonExistentId)).rejects.toSatisfy(
|
||||
(error) => {
|
||||
expect(error).toBeInstanceOf(SessionError);
|
||||
expect((error as SessionError).code).toBe('INVALID_SESSION_IDENTIFIER');
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractFirstUserMessage', () => {
|
||||
|
||||
@@ -270,15 +270,23 @@ export const getAllSessionFiles = async (
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
!content.sessionId ||
|
||||
!content.startTime ||
|
||||
!content.lastUpdated
|
||||
) {
|
||||
if (!content.sessionId) {
|
||||
// Missing required fields - treat as corrupted
|
||||
return { fileName: file, sessionInfo: null };
|
||||
}
|
||||
|
||||
const fileTimestamp =
|
||||
!content.startTime || !content.lastUpdated
|
||||
? (
|
||||
await fs.stat(filePath).catch(() => undefined)
|
||||
)?.mtime.toISOString()
|
||||
: undefined;
|
||||
const fallbackTimestamp = fileTimestamp ?? new Date().toISOString();
|
||||
const startTime =
|
||||
content.startTime || content.lastUpdated || fallbackTimestamp;
|
||||
const lastUpdated =
|
||||
content.lastUpdated || content.startTime || fallbackTimestamp;
|
||||
|
||||
// Skip sessions that only contain system messages (info, error, warning)
|
||||
if (!content.hasUserOrAssistantMessage) {
|
||||
return { fileName: file, sessionInfo: null };
|
||||
@@ -319,8 +327,8 @@ export const getAllSessionFiles = async (
|
||||
id: content.sessionId,
|
||||
file: file.replace(/\.jsonl?$/, ''),
|
||||
fileName: file,
|
||||
startTime: content.startTime,
|
||||
lastUpdated: content.lastUpdated,
|
||||
startTime,
|
||||
lastUpdated,
|
||||
messageCount: content.messageCount ?? content.messages.length,
|
||||
displayName: content.summary
|
||||
? stripUnsafeCharacters(content.summary)
|
||||
@@ -546,12 +554,17 @@ export class SessionSelector {
|
||||
if (!sessionData) {
|
||||
throw new Error('Failed to load session data');
|
||||
}
|
||||
const normalizedSessionData = {
|
||||
...sessionData,
|
||||
startTime: sessionData.startTime || sessionInfo.startTime,
|
||||
lastUpdated: sessionData.lastUpdated || sessionInfo.lastUpdated,
|
||||
};
|
||||
|
||||
const displayInfo = `Session ${sessionInfo.index}: ${sessionInfo.firstUserMessage} (${sessionInfo.messageCount} messages, ${formatRelativeTime(sessionInfo.lastUpdated)})`;
|
||||
|
||||
return {
|
||||
sessionPath,
|
||||
sessionData,
|
||||
sessionData: normalizedSessionData,
|
||||
displayInfo,
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user