/** * @file Route tests for /api/snapshots: capture freezes a session (events * oldest first, stats, cost), redaction replaces paths before anything is * stored, rows are immutable, writes are same-origin only, and snapshots * survive Clear Data. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const http = require("node:http"); const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-snapshots-")); process.env.DASHBOARD_DB_PATH = path.join(ROOT, "t.db"); process.env.CLAUDE_HOME = path.join(ROOT, ".claude"); process.env.DASHBOARD_REMOTE_SYNC_MS = "0"; process.env.DASHBOARD_LIVENESS_PROBE = "0"; const { createApp } = require("../index"); const { db } = require("../db"); const CWD = "/work/secret-project"; let server; let base; function request(method, p, body, headers = {}) { return fetch(`${base}${p}`, { method, headers: { "Content-Type": "application/json", ...headers }, body: body === undefined ? undefined : JSON.stringify(body), }).then(async (r) => ({ status: r.status, body: await r.json().catch(() => null) })); } before(async () => { server = http.createServer(createApp()); await new Promise((r) => server.listen(0, "127.0.0.1", r)); base = `http://127.0.0.1:${server.address().port}`; db.prepare( "INSERT INTO sessions (id, name, status, cwd, started_at, transcript_path) VALUES (?, ?, 'completed', ?, ?, ?)" ).run("s1", "Snap me", CWD, "2026-10-01T10:00:00.000Z", `${CWD}/t.jsonl`); db.prepare( "INSERT INTO agents (id, session_id, name, type) VALUES ('a1', 's1', 'main', 'main')" ).run(); const ev = db.prepare( "INSERT INTO events (session_id, agent_id, event_type, tool_name, summary, data, created_at) VALUES ('s1', 'a1', ?, ?, ?, ?, ?)" ); ev.run("SessionStart", null, "start", JSON.stringify({ cwd: CWD }), "2026-10-01T10:00:00.000Z"); ev.run( "PreToolUse", "Read", `Read ${CWD}/src/a.js`, JSON.stringify({ cwd: CWD, tool_input: { file_path: `${CWD}/src/a.js` }, command: `ls ${CWD}/src`, }), "2026-10-01T10:00:01.000Z" ); ev.run("Stop", null, "stop", "{}", "2026-10-01T10:00:02.000Z"); db.prepare( "INSERT INTO token_usage (session_id, model, input_tokens, output_tokens) VALUES ('s1', 'claude-sonnet-4-5', 1000000, 0)" ).run(); }); after(async () => { await new Promise((r) => server.close(r)); fs.rmSync(ROOT, { recursive: true, force: true }); }); describe("/api/snapshots", () => { let plain; let redacted; it("freezes a session under an unguessable token", async () => { const r = await request("POST", "/api/snapshots", { session_id: "s1" }); assert.equal(r.status, 201); plain = r.body.snapshot; assert.match(plain.token, /^[A-Za-z0-9_-]{32}$/); const got = await request("GET", `/api/snapshots/${plain.token}`); assert.equal(got.status, 200); const p = got.body.payload; assert.equal(p.session.id, "s1"); assert.equal(p.session.cwd, CWD); assert.deepEqual( p.events.map((e) => e.event_type), ["SessionStart", "PreToolUse", "Stop"] ); assert.equal(p.agents.length, 1); assert.equal(p.stats.total_events, 3); assert.ok(p.cost.total_cost > 0); assert.equal(p.redacted.file_paths, false); }); it("redacts paths at capture", async () => { const r = await request("POST", "/api/snapshots", { session_id: "s1", redact: { file_paths: true }, }); assert.equal(r.status, 201); redacted = r.body.snapshot; const stored = db .prepare("SELECT payload FROM session_snapshots WHERE id = ?") .get(redacted.id).payload; assert.ok(!stored.includes(CWD), "no raw cwd in the stored row"); const p = JSON.parse(stored); assert.equal(p.session.cwd, "[redacted]"); assert.equal(p.session.transcript_path, "[redacted]"); const read = JSON.parse(p.events[1].data); assert.equal(read.tool_input.file_path, "[redacted]"); assert.equal(read.command, "ls /src"); assert.equal(p.events[1].summary, "Read /src/a.js"); }); it("lists without payloads, newest first", async () => { const r = await request("GET", "/api/snapshots"); assert.equal(r.status, 200); assert.deepEqual( r.body.snapshots.map((s) => s.id), [redacted.id, plain.id] ); const [first] = r.body.snapshots; assert.equal(first.session_name, "Snap me"); assert.equal(first.redacted, true); assert.equal(first.event_count, 3); assert.equal(first.payload, undefined); }); it("refuses an update in the database", () => { assert.throws( () => db.prepare("UPDATE session_snapshots SET payload = '{}' WHERE id = ?").run(plain.id), /immutable/ ); }); it("answers 400 / 404 on bad input", async () => { assert.equal((await request("POST", "/api/snapshots", {})).status, 400); assert.equal((await request("POST", "/api/snapshots", { session_id: "nope" })).status, 404); assert.equal((await request("GET", "/api/snapshots/short")).status, 404); assert.equal((await request("DELETE", "/api/snapshots/99999")).status, 404); }); it("refuses a cross-origin write", async () => { const h = { Origin: "https://evil.example" }; assert.equal((await request("POST", "/api/snapshots", { session_id: "s1" }, h)).status, 403); assert.equal((await request("DELETE", `/api/snapshots/${plain.id}`, undefined, h)).status, 403); }); it("survives Clear Data, and deletes", async () => { assert.equal((await request("POST", "/api/settings/clear-data", {})).status, 200); const got = await request("GET", `/api/snapshots/${plain.token}`); assert.equal(got.status, 200); assert.equal(got.body.payload.session.id, "s1"); assert.equal((await request("DELETE", `/api/snapshots/${plain.id}`)).status, 200); assert.equal((await request("GET", `/api/snapshots/${plain.token}`)).status, 404); }); });