/** * @file Tests server/lib/opencode-usage.js and the OpenCode branch of the * transcript routes — OpenCode sessions from its SQLite store: a root session * created as `tool = 'opencode'` with its title, cwd, model and per-model * tokens (its subagent session's included, reasoning counted as output), a * subagent row per child session, an unchanged store never re-read, a * session the user deleted staying deleted until it is updated, and the * conversation (text, reasoning, tool call + result) in the Claude * transcript shape. The store is a temp file with the tables and columns * opencode 1.19 writes. * @author Nguyễn Ngọc Trí Vĩ */ const os = require("node:os"); const fs = require("node:fs"); const path = require("node:path"); const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-opencode-usage-")); process.env.DASHBOARD_DB_PATH = path.join(ROOT, "dashboard.db"); process.env.CLAUDE_HOME = path.join(ROOT, "claude"); process.env.AGENTHUB_OPENCODE_DIR = path.join(ROOT, "opencode"); const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const { db, Database } = require("../db"); const oc = require("../lib/opencode-usage"); const { createApp, startServer } = require("../index"); const ID = "ses_eac8393b4ffeEz26VGbeyq8sir"; const CHILD = "ses_eac8393b4fffChildSession01"; const CWD = path.join(fs.realpathSync(ROOT), "work"); const STORE = path.join(process.env.AGENTHUB_OPENCODE_DIR, "opencode.db"); const T0 = Date.parse("2026-10-19T09:27:05.000Z"); let server; let BASE; after(() => { server?.close(); fs.rmSync(ROOT, { recursive: true, force: true }); }); function withStore(fn) { const conn = new Database(STORE); try { fn(conn); } finally { conn.close(); } } const assistant = (model, tokens) => JSON.stringify({ role: "assistant", modelID: model, tokens }); const tokens = (input, output, reasoning = 0, read = 0, write = 0) => ({ input, output, reasoning, cache: { read, write }, }); before(async () => { fs.mkdirSync(path.dirname(STORE), { recursive: true }); withStore((c) => { c.exec(` CREATE TABLE session (id TEXT PRIMARY KEY, project_id TEXT NOT NULL, parent_id TEXT, slug TEXT NOT NULL, directory TEXT NOT NULL, title TEXT NOT NULL, version TEXT NOT NULL, model TEXT, time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL, time_archived INTEGER); CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT NOT NULL, time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL, data TEXT NOT NULL); CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT NOT NULL, session_id TEXT NOT NULL, time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL, data TEXT NOT NULL); `); const s = c.prepare( "INSERT INTO session (id, project_id, parent_id, slug, directory, title, version, model, time_created, time_updated) VALUES (?, 'global', ?, 'x', ?, ?, '1.19.3', ?, ?, ?)" ); s.run(ID, null, CWD, "Fix the login bug", '{"id":"claude-sonnet-4-5","providerID":"anthropic"}', T0, T0 + 5000); s.run(CHILD, ID, CWD, "Explore auth (@explore subagent)", null, T0 + 1000, T0 + 3000); const m = c.prepare("INSERT INTO message VALUES (?, ?, ?, ?, ?)"); m.run("msg_1", ID, T0 + 10, T0 + 10, JSON.stringify({ role: "user" })); m.run("msg_2", ID, T0 + 20, T0 + 20, assistant("claude-sonnet-4-5", tokens(100, 20, 5, 1000, 50))); m.run("msg_3", CHILD, T0 + 1100, T0 + 1100, JSON.stringify({ role: "user" })); m.run("msg_4", CHILD, T0 + 1200, T0 + 1200, assistant("claude-haiku-4-5", tokens(10, 4))); const p = c.prepare("INSERT INTO part VALUES (?, ?, ?, ?, ?, ?)"); p.run("prt_1", "msg_1", ID, T0 + 11, T0 + 11, JSON.stringify({ type: "text", text: "Fix the login bug" })); p.run("prt_2", "msg_2", ID, T0 + 21, T0 + 21, JSON.stringify({ type: "reasoning", text: "Look first" })); p.run( "prt_3", "msg_2", ID, T0 + 22, T0 + 22, JSON.stringify({ type: "tool", tool: "read", callID: "call_1", state: { status: "completed", input: { filePath: "auth.ts" }, output: "export {}" }, }) ); p.run("prt_4", "msg_2", ID, T0 + 23, T0 + 23, JSON.stringify({ type: "step-start" })); p.run("prt_5", "msg_3", CHILD, T0 + 1101, T0 + 1101, JSON.stringify({ type: "text", text: "Explore auth" })); p.run("prt_6", "msg_4", CHILD, T0 + 1201, T0 + 1201, JSON.stringify({ type: "text", text: "Found it" })); }); server = await startServer(createApp(), 0); BASE = `http://127.0.0.1:${server.address().port}`; }); const sessionRow = () => db.prepare("SELECT * FROM sessions WHERE id = ?").get(ID); describe("opencode usage ingest", () => { it("creates the root session with its title, cwd, model and its tree's tokens", async () => { const r = await oc.ingestOpencodeUsage({ now: T0 + 600_000 }); assert.equal(r.files, 1); const s = sessionRow(); assert.equal(s.tool, "opencode"); assert.equal(s.name, "Fix the login bug"); assert.equal(s.cwd, CWD); assert.equal(s.model, "claude-sonnet-4-5"); assert.equal(s.status, "completed"); assert.equal(db.prepare("SELECT 1 FROM sessions WHERE id = ?").get(CHILD), undefined); const usage = db .prepare( "SELECT model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens FROM token_usage WHERE session_id = ? ORDER BY model" ) .all(ID); assert.deepEqual(usage, [ { model: "claude-haiku-4-5", input_tokens: 10, output_tokens: 4, cache_read_tokens: 0, cache_write_tokens: 0 }, { model: "claude-sonnet-4-5", input_tokens: 100, output_tokens: 25, cache_read_tokens: 1000, cache_write_tokens: 50 }, ]); const agent = db.prepare("SELECT * FROM agents WHERE id = ?").get(`opencode-${CHILD}`); assert.equal(agent.session_id, ID); assert.equal(agent.type, "subagent"); assert.equal(agent.name, "Explore auth (@explore subagent)"); assert.equal(agent.status, "completed"); }); it("leaves an unchanged store unread", async () => { const r = await oc.ingestOpencodeUsage({ now: T0 + 600_000 }); assert.equal(r.files, 0); }); it("maps the store to the transcript shape, and lists the subagent's", async () => { const list = await (await fetch(`${BASE}/api/sessions/${ID}/transcripts`)).json(); assert.deepEqual( list.transcripts.map((t) => [t.id, t.type, t.db_agent_id]), [ ["main", "main", null], [CHILD, "subagent", `opencode-${CHILD}`], ] ); const body = await (await fetch(`${BASE}/api/sessions/${ID}/transcript`)).json(); assert.deepEqual( body.messages.map((m) => [m.type, m.sender, m.content.map((c) => c.type).join("+")]), [ ["user", "user", "text"], ["assistant", "assistant", "thinking+tool_use"], ["user", "tool", "tool_result"], ] ); assert.equal(body.messages[2].content[0].output, "export {}"); assert.equal(body.last_line, 3); const tail = await (await fetch(`${BASE}/api/sessions/${ID}/transcript?after=2`)).json(); assert.equal(tail.messages.length, 1); const sub = await ( await fetch(`${BASE}/api/sessions/${ID}/transcript?agent_id=${CHILD}`) ).json(); assert.deepEqual( sub.messages.map((m) => [m.sender, m.content[0].text]), [ ["orchestrator", "Explore auth"], ["assistant", "Found it"], ] ); }); it("keeps a deleted session deleted until OpenCode updates it", async () => { db.prepare("DELETE FROM sessions WHERE id = ?").run(ID); const bump = (t) => { withStore((c) => c.prepare("UPDATE session SET title = title WHERE id = ?").run(ID)); const at = new Date(t); fs.utimesSync(STORE, at, at); }; bump(Date.now() + 5000); await oc.ingestOpencodeUsage({ now: T0 + 600_000 }); assert.equal(sessionRow(), undefined); withStore((c) => c.prepare("UPDATE session SET time_updated = ? WHERE id = ?").run(T0 + 9000, ID)); fs.utimesSync(STORE, new Date(Date.now() + 10_000), new Date(Date.now() + 10_000)); await oc.ingestOpencodeUsage({ now: T0 + 600_000 }); assert.equal(sessionRow()?.tool, "opencode"); }); });