57dc91585d
Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
115 lines
3.6 KiB
JavaScript
115 lines
3.6 KiB
JavaScript
/**
|
|
* @file Verifies the idempotent ALTER TABLE migration adds a transcript_path
|
|
* column to sessions and that a fresh db.js load on an existing DB does not
|
|
* throw or duplicate the column.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const { describe, it, before, after } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const path = require("path");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
|
|
let TEST_DB;
|
|
|
|
before(() => {
|
|
TEST_DB = path.join(os.tmpdir(), `dashboard-tp-migration-${Date.now()}-${process.pid}.db`);
|
|
process.env.DASHBOARD_DB_PATH = TEST_DB;
|
|
});
|
|
|
|
after(() => {
|
|
try {
|
|
fs.unlinkSync(TEST_DB);
|
|
} catch {}
|
|
try {
|
|
fs.unlinkSync(TEST_DB + "-wal");
|
|
} catch {}
|
|
try {
|
|
fs.unlinkSync(TEST_DB + "-shm");
|
|
} catch {}
|
|
});
|
|
|
|
describe("sessions.transcript_path migration", () => {
|
|
it("adds transcript_path column on first load", () => {
|
|
delete require.cache[require.resolve("../db")];
|
|
const { db } = require("../db");
|
|
const cols = db.prepare("PRAGMA table_info(sessions)").all();
|
|
const names = cols.map((c) => c.name);
|
|
assert.ok(
|
|
names.includes("transcript_path"),
|
|
`expected transcript_path; got: ${names.join(",")}`
|
|
);
|
|
});
|
|
|
|
it("is idempotent — loading db.js a second time does not throw", () => {
|
|
delete require.cache[require.resolve("../db")];
|
|
assert.doesNotThrow(() => require("../db"));
|
|
});
|
|
|
|
it("transcript_path is nullable and accepts an UPDATE", () => {
|
|
const { db, stmts } = require("../db");
|
|
stmts.insertSession.run("s-tp-1", "name", "active", "/tmp/proj", "claude", null);
|
|
db.prepare("UPDATE sessions SET transcript_path = ? WHERE id = ?").run(
|
|
"/tmp/foo.jsonl",
|
|
"s-tp-1"
|
|
);
|
|
const row = db.prepare("SELECT transcript_path FROM sessions WHERE id = ?").get("s-tp-1");
|
|
assert.equal(row.transcript_path, "/tmp/foo.jsonl");
|
|
});
|
|
});
|
|
|
|
describe("hooks ingestion populates sessions.transcript_path", () => {
|
|
it("sets transcript_path on first event that carries it", async () => {
|
|
delete require.cache[require.resolve("../db")];
|
|
const { db, stmts } = require("../db");
|
|
|
|
// Pre-create a session without transcript_path (simulate legacy state)
|
|
stmts.insertSession.run("s-hook-1", "n", "active", "/tmp/proj", "claude", null);
|
|
let row = db.prepare("SELECT transcript_path FROM sessions WHERE id = ?").get("s-hook-1");
|
|
assert.equal(row.transcript_path, null);
|
|
|
|
// Spin up app and POST a hook event with transcript_path
|
|
delete require.cache[require.resolve("../index")];
|
|
const { createApp, startServer } = require("../index");
|
|
const app = createApp();
|
|
const server = await startServer(app, 0);
|
|
const port = server.address().port;
|
|
|
|
const payload = JSON.stringify({
|
|
hook_type: "PostToolUse",
|
|
data: {
|
|
session_id: "s-hook-1",
|
|
transcript_path: "/tmp/somewhere/session.jsonl",
|
|
cwd: "/tmp/proj",
|
|
},
|
|
});
|
|
await new Promise((resolve, reject) => {
|
|
const http = require("http");
|
|
const req = http.request(
|
|
{
|
|
hostname: "127.0.0.1",
|
|
port,
|
|
path: "/api/hooks/event",
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Content-Length": Buffer.byteLength(payload),
|
|
},
|
|
},
|
|
(res) => {
|
|
res.resume();
|
|
res.once("end", resolve);
|
|
}
|
|
);
|
|
req.on("error", reject);
|
|
req.write(payload);
|
|
req.end();
|
|
});
|
|
server.close();
|
|
|
|
row = db.prepare("SELECT transcript_path FROM sessions WHERE id = ?").get("s-hook-1");
|
|
assert.equal(row.transcript_path, "/tmp/somewhere/session.jsonl");
|
|
});
|
|
});
|