import { describe, expect, it } from "bun:test"; import { mkdir, mkdtemp, rm, utimes, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { getOpenCodeSessionsStreaming, parseOpenCodeSessionFile, } from "./opencode"; describe("opencode ingestion", () => { it("parses opencode storage session/message/part structure", async () => { const dir = await mkdtemp(join(tmpdir(), "scribe-opencode-test-")); const storageRoot = join(dir, "storage"); const sessionId = "session-123"; const projectId = "project-1"; const sessionDir = join(storageRoot, "session", projectId); const messageDir = join(storageRoot, "message", sessionId); const partDirUser = join(storageRoot, "part", "message-user"); const partDirAssistant = join(storageRoot, "part", "message-assistant"); const partDirSummary = join(storageRoot, "part", "message-summary"); const sessionFile = join(sessionDir, "s1.json"); try { await mkdir(sessionDir, { recursive: true }); await mkdir(messageDir, { recursive: true }); await mkdir(partDirUser, { recursive: true }); await mkdir(partDirAssistant, { recursive: true }); await mkdir(partDirSummary, { recursive: true }); await writeFile( sessionFile, JSON.stringify({ id: sessionId, projectID: projectId, directory: "/Users/satchmo/code/scribe", title: "OpenCode Session", slug: "open-code-session", version: "0.1.0", time: { created: Date.UTC(2026, 1, 9, 12, 0, 0), updated: Date.UTC(2026, 1, 9, 12, 10, 0), }, }), "utf8", ); await writeFile( join(messageDir, "m-user.json"), JSON.stringify({ id: "message-user", sessionID: sessionId, role: "user", time: { created: Date.UTC(2026, 1, 9, 12, 0, 0), }, }), "utf8", ); await writeFile( join(messageDir, "m-assistant.json"), JSON.stringify({ id: "message-assistant", sessionID: sessionId, role: "assistant", time: { created: Date.UTC(2026, 1, 9, 12, 1, 0), }, }), "utf8", ); await writeFile( join(messageDir, "m-summary.json"), JSON.stringify({ id: "message-summary", sessionID: sessionId, role: "assistant", summary: { title: "Summarized fallback content", }, time: { created: Date.UTC(2026, 1, 9, 12, 2, 0), }, }), "utf8", ); await writeFile( join(partDirUser, "p1.json"), JSON.stringify({ id: "part-user-1", type: "text", text: "User asks for parser fixtures", time: { start: Date.UTC(2026, 1, 9, 12, 0, 0), }, }), "utf8", ); await writeFile( join(partDirAssistant, "p1.json"), JSON.stringify({ id: "part-assistant-1", type: "reasoning", text: "Thinking about deterministic fixture coverage.", time: { start: Date.UTC(2026, 1, 9, 12, 1, 0), }, }), "utf8", ); await writeFile( join(partDirAssistant, "p2.json"), JSON.stringify({ id: "part-tool-1", type: "tool", tool: "list_files", state: { status: "completed", input: { path: "." }, output: { ok: true, count: 2 }, }, tokens: { input: 11, output: 22, }, time: { start: Date.UTC(2026, 1, 9, 12, 1, 30), }, }), "utf8", ); const session = await parseOpenCodeSessionFile(storageRoot, sessionFile); expect(session).not.toBeNull(); expect(session?.providerId).toBe("opencode"); expect(session?.externalThreadId).toBe(sessionId); expect(session?.projectPath).toBe("/Users/satchmo/code/scribe"); expect(session?.title).toBe("OpenCode Session"); expect(session?.messages).toHaveLength(4); expect(session?.messages[0]?.role).toBe("user"); expect(session?.messages[0]?.text).toContain("parser fixtures"); expect(session?.messages[1]?.role).toBe("assistant"); expect(session?.messages[1]?.text).toContain("deterministic fixture"); expect(session?.messages[2]?.role).toBe("tool"); expect(session?.messages[2]?.text).toContain("Tool: list_files"); expect(session?.messages[2]?.tokenIn).toBe(11); expect(session?.messages[2]?.tokenOut).toBe(22); expect(session?.messages[3]?.role).toBe("assistant"); expect(session?.messages[3]?.text).toContain("Summarized fallback"); } finally { await rm(dir, { recursive: true, force: true }); } }); it("streams opencode sessions and filters messages by since timestamp", async () => { const dir = await mkdtemp(join(tmpdir(), "scribe-opencode-stream-test-")); const storageRoot = join(dir, "storage"); const sessionId = "session-since"; const projectId = "project-stream"; const sessionDir = join(storageRoot, "session", projectId); const messageDir = join(storageRoot, "message", sessionId); const partDirOld = join(storageRoot, "part", "message-old"); const partDirNew = join(storageRoot, "part", "message-new"); const sessionFile = join(sessionDir, "s2.json"); try { await mkdir(sessionDir, { recursive: true }); await mkdir(messageDir, { recursive: true }); await mkdir(partDirOld, { recursive: true }); await mkdir(partDirNew, { recursive: true }); await writeFile( sessionFile, JSON.stringify({ id: sessionId, projectID: projectId, directory: "/Users/satchmo/code/scribe", time: { created: Date.UTC(2026, 1, 9, 12, 0, 0), updated: Date.UTC(2026, 1, 9, 12, 10, 0), }, }), "utf8", ); await writeFile( join(messageDir, "old.json"), JSON.stringify({ id: "message-old", role: "user", time: { created: Date.UTC(2026, 1, 9, 12, 0, 0), }, }), "utf8", ); await writeFile( join(messageDir, "new.json"), JSON.stringify({ id: "message-new", role: "assistant", time: { created: Date.UTC(2026, 1, 9, 12, 5, 0), }, }), "utf8", ); await writeFile( join(partDirOld, "p1.json"), JSON.stringify({ id: "part-old-1", type: "text", text: "before window", time: { start: Date.UTC(2026, 1, 9, 12, 0, 0), }, }), "utf8", ); await writeFile( join(partDirNew, "p1.json"), JSON.stringify({ id: "part-new-1", type: "text", text: "inside window", time: { start: Date.UTC(2026, 1, 9, 12, 5, 0), }, }), "utf8", ); const now = new Date(); await utimes(sessionFile, now, now); const since = new Date("2026-02-09T12:01:00.000Z"); const sessions = []; for await (const session of getOpenCodeSessionsStreaming( ["/Users/satchmo/code"], since, { dataRoots: [storageRoot] }, )) { sessions.push(session); } expect(sessions).toHaveLength(1); expect(sessions[0]?.messages).toHaveLength(1); expect(sessions[0]?.messages[0]?.text).toContain("inside window"); } finally { await rm(dir, { recursive: true, force: true }); } }); });