import { describe, expect, it } from "bun:test"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { getGeminiSessionsStreaming, parseGeminiSessionFile } from "./gemini"; describe("gemini ingestion", () => { it("parses gemini session JSON and normalizes rich message content", async () => { const dir = await mkdtemp(join(tmpdir(), "scribe-gemini-test-")); const projectHash = "abc123def456"; const chatsDir = join(dir, projectHash, "chats"); const sessionFile = join(chatsDir, "session-1.json"); try { await mkdir(chatsDir, { recursive: true }); await writeFile( sessionFile, JSON.stringify( { sessionId: "gemini-thread-1", projectHash, startTime: "2026-02-09T09:00:00.000Z", lastUpdated: "2026-02-09T09:10:00.000Z", messages: [ { id: "u-1", timestamp: "2026-02-09T09:00:00.000Z", type: "user", content: "Help me write fixture tests.", }, { id: "a-1", timestamp: "2026-02-09T09:01:00.000Z", type: "gemini", content: "Sure, here is a plan.", thoughts: [ { subject: "Approach", description: "Build deterministic fixtures by provider.", }, ], toolCalls: [{ name: "listFiles", args: { path: "." } }], tokens: { input: 101, output: 202 }, }, { id: "invalid-empty", timestamp: "2026-02-09T09:02:00.000Z", type: "gemini", }, { id: "invalid-time", timestamp: "not-a-date", type: "user", content: "should be ignored", }, ], }, null, 2, ), "utf8", ); const session = await parseGeminiSessionFile(sessionFile, projectHash); expect(session).not.toBeNull(); expect(session?.providerId).toBe("gemini"); expect(session?.externalThreadId).toBe("gemini-thread-1"); expect(session?.projectHash).toBe(projectHash); expect(session?.messages).toHaveLength(2); expect(session?.messages[0]?.role).toBe("user"); expect(session?.messages[0]?.text).toContain("fixture tests"); expect(session?.messages[1]?.role).toBe("assistant"); expect(session?.messages[1]?.text).toContain("Sure, here is a plan."); expect(session?.messages[1]?.text).toContain("Approach"); expect(session?.messages[1]?.text).toContain("Tool calls:"); expect(session?.messages[1]?.tokenIn).toBe(101); expect(session?.messages[1]?.tokenOut).toBe(202); } finally { await rm(dir, { recursive: true, force: true }); } }); it("streams gemini sessions and filters messages by since timestamp", async () => { const dir = await mkdtemp(join(tmpdir(), "scribe-gemini-stream-test-")); const projectHash = "streamhash123"; const chatsDir = join(dir, projectHash, "chats"); const sessionFile = join(chatsDir, "session-2.json"); try { await mkdir(chatsDir, { recursive: true }); await writeFile( sessionFile, JSON.stringify({ sessionId: "gemini-thread-2", messages: [ { id: "m-1", timestamp: "2026-02-09T10:00:00.000Z", type: "user", content: "old", }, { id: "m-2", timestamp: "2026-02-09T10:05:00.000Z", type: "gemini", content: "new", }, ], }), "utf8", ); const since = new Date("2026-02-09T10:01:00.000Z"); const sessions = []; for await (const session of getGeminiSessionsStreaming([dir], since, { dataRoots: [dir], })) { sessions.push(session); } expect(sessions).toHaveLength(1); expect(sessions[0]?.messages).toHaveLength(1); expect(sessions[0]?.messages[0]?.text).toBe("new"); } finally { await rm(dir, { recursive: true, force: true }); } }); });