Files
Claude-Code-Monitor/server/__tests__/agents-legacy-rebuild.test.js
nntrivi2001 57dc91585d feat: Claude Code Monitor — lanes, pipelines and a merged workspace
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.
2026-07-30 14:39:03 +07:00

128 lines
4.9 KiB
JavaScript

/**
* @file Verifies the legacy `agents_new` rebuild in db.js (triggered when the
* agents table's stored CHECK constraint still contains the old 'idle'
* status) preserves the workflow_run_id/workflow_phase columns added by the
* earlier workflow migration, plus awaiting_reason, and recreates
* idx_agents_workflow.
*
* Regression coverage for a bug flagged on PR #228: the rebuild's CREATE
* TABLE agents_new / INSERT INTO agents_new SELECT / index-recreate block was
* written before the workflow_run_id + workflow_phase columns existed, so it
* silently dropped them (and idx_agents_workflow) for any DB old enough to
* still carry the legacy CHECK. Because the workflow_run_id/workflow_phase
* prepared statements are compiled at module load time, right after the
* migrations run, this crashed startup for such legacy DBs.
*
* @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");
const Database = require("better-sqlite3");
let TEST_DB;
before(() => {
TEST_DB = path.join(os.tmpdir(), `dashboard-agents-legacy-${Date.now()}-${process.pid}.db`);
process.env.DASHBOARD_DB_PATH = TEST_DB;
// Hand-build a pre-workflow, pre-awaiting-reason legacy DB: a sessions table
// just rich enough to satisfy the agents FK, and an agents table using the
// old 3/5-status CHECK (still containing 'idle') with none of the columns
// added by later migrations (workflow_run_id, workflow_phase, updated_at,
// awaiting_input_since, awaiting_reason). This is the exact shape the
// 'idle' CHECK detector at db.js:~731 is looking for.
const raw = new Database(TEST_DB);
raw.pragma("foreign_keys = OFF");
raw.exec(`
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
name TEXT,
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','completed','error','abandoned')),
cwd TEXT,
model TEXT,
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
ended_at TEXT,
metadata TEXT
);
CREATE TABLE agents (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'main' CHECK(type IN ('main','subagent')),
subagent_type TEXT,
status TEXT NOT NULL DEFAULT 'idle' CHECK(status IN ('idle','connected','working','completed','error')),
task TEXT,
current_tool TEXT,
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
ended_at TEXT,
parent_agent_id TEXT,
metadata TEXT,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
FOREIGN KEY (parent_agent_id) REFERENCES agents(id) ON DELETE SET NULL
);
`);
raw
.prepare("INSERT INTO sessions (id, name, status, cwd, model) VALUES (?, ?, ?, ?, ?)")
.run("s-legacy-1", "legacy session", "active", "/tmp/legacy-proj", "claude");
raw
.prepare(
"INSERT INTO agents (id, session_id, name, type, status, task) VALUES (?, ?, ?, ?, ?, ?)"
)
.run("a-legacy-1", "s-legacy-1", "legacy main agent", "main", "idle", "legacy task");
raw.close();
});
after(() => {
try {
delete require.cache[require.resolve("../db")];
} catch {}
for (const suffix of ["", "-wal", "-shm"]) {
try {
fs.unlinkSync(TEST_DB + suffix);
} catch {}
}
});
describe("legacy agents_new rebuild (pre-workflow, pre-awaiting-reason DB)", () => {
it("loads db.js against the legacy DB without throwing", () => {
delete require.cache[require.resolve("../db")];
assert.doesNotThrow(() => require("../db"));
});
it("adds workflow_run_id, workflow_phase and awaiting_reason to agents", () => {
const { db } = require("../db");
const cols = db.prepare("PRAGMA table_info(agents)").all();
const names = cols.map((c) => c.name);
for (const col of ["workflow_run_id", "workflow_phase", "awaiting_reason"]) {
assert.ok(names.includes(col), `expected agents.${col}; got: ${names.join(",")}`);
}
});
it("recreates idx_agents_workflow", () => {
const { db } = require("../db");
const idx = db
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'idx_agents_workflow'"
)
.get();
assert.ok(idx, "expected idx_agents_workflow to exist after the rebuild");
});
it("preserves the pre-existing agent row and remaps its status idle -> waiting", () => {
const { db } = require("../db");
const row = db.prepare("SELECT * FROM agents WHERE id = ?").get("a-legacy-1");
assert.ok(row, "expected the legacy agent row to survive the rebuild");
assert.equal(row.status, "waiting");
assert.equal(row.session_id, "s-legacy-1");
assert.equal(row.name, "legacy main agent");
assert.equal(row.task, "legacy task");
assert.equal(row.workflow_run_id, null);
assert.equal(row.workflow_phase, null);
});
});