/** * @file Regression test for the alert_rules rebuild that drops its 4-type * `rule_type` CHECK: an existing DB keeps every rule and every fired alert * (alert_events cascades on rule delete, so the rebuild must run with foreign * keys off), and the new types can be stored afterwards. * @author Nguyễn Ngọc Trí Vĩ */ "use strict"; const { it } = 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 ROOT = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "alert-mig-"))); const DB_PATH = path.join(ROOT, "t.db"); process.env.DASHBOARD_DB_PATH = DB_PATH; process.env.CLAUDE_HOME = path.join(ROOT, "claude"); process.env.DASHBOARD_DATA_DIR = path.join(ROOT, "data"); // The schema an older build created. { const Database = require("better-sqlite3"); const old = new Database(DB_PATH); old.pragma("foreign_keys = ON"); old.exec(` CREATE TABLE alert_rules ( id TEXT PRIMARY KEY, name TEXT NOT NULL, rule_type TEXT NOT NULL CHECK(rule_type IN ('event_pattern','inactivity','status_duration','token_threshold')), config TEXT NOT NULL DEFAULT '{}', enabled INTEGER NOT NULL DEFAULT 1, cooldown_seconds INTEGER NOT NULL DEFAULT 300, 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')) ); CREATE TABLE alert_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, rule_id TEXT NOT NULL, rule_name TEXT NOT NULL, rule_type TEXT NOT NULL, session_id TEXT, agent_id TEXT, message TEXT NOT NULL, details TEXT, triggered_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), acknowledged_at TEXT, FOREIGN KEY (rule_id) REFERENCES alert_rules(id) ON DELETE CASCADE ); INSERT INTO alert_rules (id, name, rule_type, config) VALUES ('r1', 'Old', 'inactivity', '{"minutes":5}'); INSERT INTO alert_events (rule_id, rule_name, rule_type, message) VALUES ('r1', 'Old', 'inactivity', 'fired'); `); old.close(); } const { db, stmts } = require("../db"); it("keeps rules and fired alerts, and accepts the new types", () => { assert.equal(db.prepare("SELECT name FROM alert_rules WHERE id = 'r1'").get().name, "Old"); assert.equal(db.prepare("SELECT COUNT(*) c FROM alert_events WHERE rule_id = 'r1'").get().c, 1); stmts.insertAlertRule.run("r2", "Budget", "budget", '{"period":"day","limit_usd":1}', 1, 0); assert.equal(stmts.getAlertRule.get("r2").rule_type, "budget"); // The cascade still works after the rename. stmts.deleteAlertRule.run("r1"); assert.equal(db.prepare("SELECT COUNT(*) c FROM alert_events").get().c, 0); assert.equal(db.pragma("foreign_keys", { simple: true }), 1); fs.rmSync(ROOT, { recursive: true, force: true }); });