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.
128 lines
4.9 KiB
JavaScript
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);
|
|
});
|
|
});
|