mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-19 14:30:52 -07:00
refactor(sdk): introduce session-based architecture (#19180)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -180,6 +180,86 @@ describe('Storage – additional helpers', () => {
|
||||
expect(storageWithSession.getProjectTempPlansDir()).toBe(expected);
|
||||
});
|
||||
|
||||
describe('Session and JSON Loading', () => {
|
||||
beforeEach(async () => {
|
||||
await storage.initialize();
|
||||
});
|
||||
|
||||
it('listProjectChatFiles returns sorted sessions from chats directory', async () => {
|
||||
const readdirSpy = vi
|
||||
.spyOn(fs.promises, 'readdir')
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
.mockResolvedValue([
|
||||
'session-1.json',
|
||||
'session-2.json',
|
||||
'not-a-session.txt',
|
||||
] as any);
|
||||
|
||||
const statSpy = vi
|
||||
.spyOn(fs.promises, 'stat')
|
||||
.mockImplementation(async (p: any) => {
|
||||
if (p.toString().endsWith('session-1.json')) {
|
||||
return {
|
||||
mtime: new Date('2026-02-01'),
|
||||
mtimeMs: 1000,
|
||||
} as any;
|
||||
}
|
||||
return {
|
||||
mtime: new Date('2026-02-02'),
|
||||
mtimeMs: 2000,
|
||||
} as any;
|
||||
});
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
const sessions = await storage.listProjectChatFiles();
|
||||
|
||||
expect(readdirSpy).toHaveBeenCalledWith(expect.stringContaining('chats'));
|
||||
expect(sessions).toHaveLength(2);
|
||||
// Sorted by mtime desc
|
||||
expect(sessions[0].filePath).toBe(path.join('chats', 'session-2.json'));
|
||||
expect(sessions[1].filePath).toBe(path.join('chats', 'session-1.json'));
|
||||
expect(sessions[0].lastUpdated).toBe(
|
||||
new Date('2026-02-02').toISOString(),
|
||||
);
|
||||
|
||||
readdirSpy.mockRestore();
|
||||
statSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('loadProjectTempFile loads and parses JSON from relative path', async () => {
|
||||
const readFileSpy = vi
|
||||
.spyOn(fs.promises, 'readFile')
|
||||
.mockResolvedValue(JSON.stringify({ hello: 'world' }));
|
||||
|
||||
const result = await storage.loadProjectTempFile<{ hello: string }>(
|
||||
'some/file.json',
|
||||
);
|
||||
|
||||
expect(readFileSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(path.join(PROJECT_SLUG, 'some/file.json')),
|
||||
'utf8',
|
||||
);
|
||||
expect(result).toEqual({ hello: 'world' });
|
||||
|
||||
readFileSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('loadProjectTempFile returns null if file does not exist', async () => {
|
||||
const error = new Error('File not found');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(error as any).code = 'ENOENT';
|
||||
const readFileSpy = vi
|
||||
.spyOn(fs.promises, 'readFile')
|
||||
.mockRejectedValue(error);
|
||||
|
||||
const result = await storage.loadProjectTempFile('missing.json');
|
||||
|
||||
expect(result).toBeNull();
|
||||
|
||||
readFileSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlansDir', () => {
|
||||
interface TestCase {
|
||||
name: string;
|
||||
|
||||
@@ -295,6 +295,63 @@ export class Storage {
|
||||
return path.join(this.getProjectTempDir(), 'tasks');
|
||||
}
|
||||
|
||||
async listProjectChatFiles(): Promise<
|
||||
Array<{ filePath: string; lastUpdated: string }>
|
||||
> {
|
||||
const chatsDir = path.join(this.getProjectTempDir(), 'chats');
|
||||
try {
|
||||
const files = await fs.promises.readdir(chatsDir);
|
||||
const jsonFiles = files.filter((f) => f.endsWith('.json'));
|
||||
|
||||
const sessions = await Promise.all(
|
||||
jsonFiles.map(async (file) => {
|
||||
const absolutePath = path.join(chatsDir, file);
|
||||
const stats = await fs.promises.stat(absolutePath);
|
||||
return {
|
||||
filePath: path.join('chats', file),
|
||||
lastUpdated: stats.mtime.toISOString(),
|
||||
mtimeMs: stats.mtimeMs,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return sessions
|
||||
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
||||
.map(({ filePath, lastUpdated }) => ({ filePath, lastUpdated }));
|
||||
} catch (e) {
|
||||
// If directory doesn't exist, return empty
|
||||
if (
|
||||
e instanceof Error &&
|
||||
'code' in e &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(e as NodeJS.ErrnoException).code === 'ENOENT'
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async loadProjectTempFile<T>(filePath: string): Promise<T | null> {
|
||||
const absolutePath = path.join(this.getProjectTempDir(), filePath);
|
||||
try {
|
||||
const content = await fs.promises.readFile(absolutePath, 'utf8');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return JSON.parse(content) as T;
|
||||
} catch (e) {
|
||||
// If file doesn't exist, return null
|
||||
if (
|
||||
e instanceof Error &&
|
||||
'code' in e &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(e as NodeJS.ErrnoException).code === 'ENOENT'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
getExtensionsDir(): string {
|
||||
return path.join(this.getGeminiDir(), 'extensions');
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ export { OAuthUtils } from './mcp/oauth-utils.js';
|
||||
|
||||
// Export telemetry functions
|
||||
export * from './telemetry/index.js';
|
||||
export { sessionId } from './utils/session.js';
|
||||
export { sessionId, createSessionId } from './utils/session.js';
|
||||
export * from './utils/compatibility.js';
|
||||
export * from './utils/browser.js';
|
||||
export { Storage } from './config/storage.js';
|
||||
|
||||
@@ -7,3 +7,7 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
export const sessionId = randomUUID();
|
||||
|
||||
export function createSessionId(): string {
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user