Files
AgentHub/server/__tests__/lanes-cwd-unique-rebuild.test.js
T
nntrivi2001 40fe41bb77 fix(db): keep router_profile through the legacy lanes rebuild
The (cwd, tool) rebuild lists its columns by hand and omitted
router_profile, which its probe adds earlier in the same start — a legacy
DB lost the column (and every lane's router) until the next restart.
2026-09-25 11:30:10 +07:00

139 lines
4.8 KiB
JavaScript

/**
* @file Verifies the legacy `lanes_new` rebuild in db.js (triggered when the
* lanes table's stored SQL still contains the old single-column
* `cwd TEXT NOT NULL UNIQUE`) drops that constraint in favor of the composite
* `idx_lanes_cwd_tool` unique index, preserves existing rows, and recreates
* idx_lanes_session / idx_lanes_slot.
*
* Regression coverage for the codex+claude coexistence feature: a cwd used to
* back at most one lane; now one cwd can back a `claude` lane and a `codex`
* lane at once, which requires dropping the old column-level UNIQUE.
*
* @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-lanes-legacy-${Date.now()}-${process.pid}.db`);
process.env.DASHBOARD_DB_PATH = TEST_DB;
// Hand-build a legacy lanes table using the original single-column UNIQUE
// cwd, none of the columns added by later migrations (kind, source_repo,
// base_branch, slug, tool, detected_*, slot, ports, active_feature_id).
// This is the exact shape the "cwd TEXT NOT NULL UNIQUE" detector is
// looking for.
const raw = new Database(TEST_DB);
raw.pragma("foreign_keys = OFF");
raw.exec(`
CREATE TABLE lanes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL DEFAULT '',
cwd TEXT NOT NULL UNIQUE,
branch TEXT,
pipeline TEXT NOT NULL DEFAULT 'default',
session_id TEXT,
run_id TEXT,
stage TEXT NOT NULL DEFAULT 'idle',
stage_since TEXT,
status TEXT NOT NULL DEFAULT 'idle',
gate_decision TEXT,
ci_status TEXT,
needs_action TEXT,
links TEXT NOT NULL DEFAULT '{}',
stages TEXT NOT NULL DEFAULT '{}',
notes TEXT,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
`);
raw
.prepare("INSERT INTO lanes (title, cwd) VALUES (?, ?)")
.run("legacy lane", "/tmp/legacy-lane-cwd");
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 lanes_new rebuild (pre-tool, single-column cwd UNIQUE DB)", () => {
it("loads db.js against the legacy DB without throwing", () => {
delete require.cache[require.resolve("../db")];
assert.doesNotThrow(() => require("../db"));
});
it("preserves the pre-existing lane row and defaults tool to claude", () => {
const { db } = require("../db");
const row = db.prepare("SELECT * FROM lanes WHERE cwd = ?").get("/tmp/legacy-lane-cwd");
assert.ok(row, "expected the legacy lane row to survive the rebuild");
assert.equal(row.title, "legacy lane");
assert.equal(row.tool, "claude");
});
it("keeps every column added before the rebuild, router_profile included", () => {
const { db } = require("../db");
const cols = db
.prepare("PRAGMA table_info(lanes)")
.all()
.map((c) => c.name);
for (const name of ["router_profile", "needs_action_session"]) {
assert.ok(cols.includes(name), `expected ${name} to survive the rebuild`);
}
});
it("recreates idx_lanes_session and idx_lanes_slot", () => {
const { db } = require("../db");
for (const name of ["idx_lanes_session", "idx_lanes_slot"]) {
const idx = db
.prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?")
.get(name);
assert.ok(idx, `expected ${name} to exist after the rebuild`);
}
});
it("drops the single-column cwd UNIQUE, allowing a second lane at the same cwd on a different tool", () => {
const { db } = require("../db");
const legacy = db.prepare("SELECT id FROM lanes WHERE cwd = ?").get("/tmp/legacy-lane-cwd");
assert.doesNotThrow(() => {
db.prepare("INSERT INTO lanes (title, cwd, tool) VALUES (?, ?, ?)").run(
"codex sibling",
"/tmp/legacy-lane-cwd",
"codex"
);
});
const both = db
.prepare("SELECT id, tool FROM lanes WHERE cwd = ? ORDER BY id")
.all("/tmp/legacy-lane-cwd");
assert.equal(both.length, 2);
assert.equal(both[0].id, legacy.id);
assert.equal(both[0].tool, "claude");
assert.equal(both[1].tool, "codex");
});
it("still enforces uniqueness within the same (cwd, tool) pair", () => {
const { db } = require("../db");
assert.throws(() => {
db.prepare("INSERT INTO lanes (title, cwd, tool) VALUES (?, ?, ?)").run(
"duplicate claude",
"/tmp/legacy-lane-cwd",
"claude"
);
}, /UNIQUE/);
});
});