/** * Integration: `SessionManager` driven by `SqlSessionStorage` (SQLite * backend) instead of the file-backed store. Verifies the SQL substrate is * genuinely pluggable — append → flush → reload via `open()` works against * a real `Bun.SQL` connection, and `SessionManager.list()` enumerates rows * out of the table. */ import { describe, expect, it } from "bun:test"; import type { Usage } from "@oh-my-pi/pi-ai"; import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager"; import { SqlSessionStorage } from "@oh-my-pi/pi-coding-agent/session/sql-session-storage"; import { SQL } from "bun"; function fakeUsage(input: number, output: number): Usage { return { input, output, cacheRead: 0, cacheWrite: 0, totalTokens: input + output, cost: { total: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, }; } describe("SessionManager + SqlSessionStorage (SQLite)", () => { it("persists appended assistant messages into SQL and reloads via open()", async () => { const client = new SQL("sqlite::memory:"); const storage = await SqlSessionStorage.create({ client }); const sessionDir = "/sessions/proj"; const manager = SessionManager.create("/cwd", sessionDir, storage); manager.appendMessage({ role: "assistant", provider: "anthropic", model: "claude-3-7-sonnet", content: [{ type: "text", text: "hi" }], usage: fakeUsage(10, 5), api: "anthropic-messages", stopReason: "stop", timestamp: Date.now(), }); const sessionFile = manager.getSessionFile(); expect(sessionFile).toBeDefined(); const sessionFilePath = sessionFile as string; expect(sessionFilePath.startsWith(sessionDir)).toBe(true); // `appendMessage` queues the cold-path rewrite onto SessionManager's // internal persist chain via a fire-and-forget call. `flush()` awaits // that chain; `drain()` mops up the storage-level pending tail. await manager.flush(); await storage.drain(); await manager.close(); const rows = (await client.unsafe(`SELECT content FROM omp_session_files WHERE path = ?`, [ sessionFilePath, ])) as Array<{ content: string }>; expect(rows).toHaveLength(1); const lines = rows[0].content.trim().split("\n"); expect(lines.length).toBeGreaterThanOrEqual(3); // The fixed-width title slot is always the first physical line; the session header follows. const slot = JSON.parse(lines[0]); expect(slot.type).toBe("title"); const header = JSON.parse(lines[1]); expect(header.type).toBe("session"); const msg = JSON.parse(lines[lines.length - 1]); expect(msg.type).toBe("message"); expect(msg.message.role).toBe("assistant"); expect(msg.message.content[0].text).toBe("hi"); const reopened = await SessionManager.open(sessionFilePath, sessionDir, storage); const leaf = reopened.getLeafEntry(); expect(leaf).toBeDefined(); expect(leaf?.type).toBe("message"); await reopened.close(); await client.end(); }); it("SessionManager.list returns SQL-backed sessions for the cwd", async () => { const client = new SQL("sqlite::memory:"); const storage = await SqlSessionStorage.create({ client }); const sessionDir = "/sessions/list-proj"; const a = SessionManager.create("/cwd", sessionDir, storage); a.appendMessage({ role: "assistant", provider: "anthropic", model: "claude-3-7-sonnet", content: [{ type: "text", text: "alpha" }], usage: fakeUsage(1, 1), api: "anthropic-messages", stopReason: "stop", timestamp: Date.now(), }); await a.flush(); await storage.drain(); await a.close(); const b = SessionManager.create("/cwd", sessionDir, storage); b.appendMessage({ role: "assistant", provider: "anthropic", model: "claude-3-7-sonnet", content: [{ type: "text", text: "beta" }], usage: fakeUsage(1, 1), api: "anthropic-messages", stopReason: "stop", timestamp: Date.now(), }); await b.flush(); await storage.drain(); await b.close(); const aFile = a.getSessionFile(); const bFile = b.getSessionFile(); expect(aFile).toBeDefined(); expect(bFile).toBeDefined(); const sessions = await SessionManager.list("/cwd", sessionDir, storage); const sessionFiles = sessions.map(s => s.path).sort(); expect(sessionFiles).toContain(aFile as string); expect(sessionFiles).toContain(bFile as string); await client.end(); }); });