// Backfill coverage for the acknowledge-to-dismiss migrations (issue #641). The HIGHEST-RISK item:
// repointing the four "Active …" grids at the derived `list_bucket` — which folds an UNACKNOWLEDGED
// terminal row into `active` — would flood every historical terminal PR / delivery-graph run into
// Active on the next boot. Migrations 093 (PRs) and 095 (delivery graphs) prevent that by stamping
// `acknowledged_at` on every CURRENTLY-terminal row, so they load in History from day one, while rows
// that reach terminal AFTER the migration stay in Active until an operator dismisses them.
//
// This test reproduces the real upgrade path: apply the migration chain UP TO (but not including) the
// `acknowledged_at` additions, seed pre-existing rows the way a live DB carries them (terminal + live,
// NO acknowledged_at column yet), then apply the remaining migrations (093/094/095/096/097 …) and read
// the derived read-model VIEWs to prove the resulting Active/History partition.
import { DatabaseSync } from "node:sqlite";
import { test } from "node:test";
import { assert, assertEquals } from "#test-assert";
import { applyMigrationSet, readMigrationSetFromDisk } from "../test/migrations.ts";
// The pre-`acknowledged_at` schema slice (everything numbered below 093, lexically) — the base
// `pull_requests` (001) and `delivery_graph_runs` (058) tables exist here, WITHOUT the dismissal stamp.
function migrationsBefore093() {
return readMigrationSetFromDisk().filter((f) => f.name < "093");
}
function backfillDb(): DatabaseSync {
const db = new DatabaseSync(":memory:");
const files = readMigrationSetFromDisk();
// Phase 1: schema as it stood before the dismissal stamp.
applyMigrationSet(db, migrationsBefore093());
// Seed pre-existing rows exactly as a live DB carries them — no acknowledged_at column yet.
const insPr = (pr_key: string, status: string) =>
db
.prepare(
"INSERT INTO pull_requests (pr_key, repo, number, url, status, created_at, updated_at, merged_at) VALUES (?, 'o/r', 1, 'https://x', ?, '2025-01-01T00:00:00Z', '2025-06-01T00:00:00Z', ?)",
)
.run(pr_key, status, status === "merged" ? "2025-06-01T00:00:00Z" : null);
// Terminal (must backfill → History) + live (must stay Active).
for (const s of ["merged", "converged", "abandoned", "closed", "failed"]) insPr(`pre-${s}`, s);
insPr("pre-live", "converging");
const insDg = (run_key: string, status: string) =>
db
.prepare(
"INSERT INTO delivery_graph_runs (run_key, digest, status, created_at, updated_at) VALUES (?, 'dig', ?, '2025-01-01T00:00:00Z', '2025-06-01T00:00:00Z')",
)
.run(run_key, status);
for (const s of ["done", "failed", "abandoned"]) insDg(`pre-${s}`, s);
insDg("pre-live", "running");
// Phase 2: apply the remaining migrations — 093/095 add the column + backfill the pre-existing
// terminal rows, 094/096 (re)create the read-model VIEWs. applyMigrationSet skips the already-applied.
applyMigrationSet(db, files);
// Stand-ins for the managed `
__tracking` derived VIEWs urban provisions at mount (pass-through
// `derived_status := base.status`, modelling settled rows) — the read-model VIEWs read these.
db.exec(
`CREATE VIEW pull_requests__tracking AS SELECT p.*, p.status AS derived_status FROM pull_requests p;
CREATE VIEW delivery_graph_runs__tracking AS SELECT d.*, d.status AS derived_status FROM delivery_graph_runs d;`,
);
// A row that reaches terminal AFTER the migration — acknowledged_at stays NULL, so it must stay Active.
db.prepare(
"INSERT INTO pull_requests (pr_key, repo, number, url, status, created_at, updated_at) VALUES ('post-merged', 'o/r', 2, 'https://y', 'merged', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')",
).run();
db.prepare(
"INSERT INTO delivery_graph_runs (run_key, digest, status, created_at, updated_at) VALUES ('post-done', 'dig', 'done', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')",
).run();
return db;
}
function prBucket(db: DatabaseSync, pr_key: string) {
return db.prepare("SELECT list_bucket, ack_open, acknowledged_at FROM pull_requests_read_model WHERE pr_key = ?").get(pr_key) as {
list_bucket: string;
ack_open: number;
acknowledged_at: string | null;
};
}
function dgBucket(db: DatabaseSync, run_key: string) {
return db.prepare("SELECT list_bucket, ack_open, acknowledged_at FROM delivery_graph_read_model WHERE run_key = ?").get(run_key) as {
list_bucket: string;
ack_open: number;
acknowledged_at: string | null;
};
}
test("migration 093 backfill: every pre-existing terminal PR loads in History (acknowledged_at stamped); a live PR stays Active; a post-migration terminal PR stays Active until dismissed", () => {
const db = backfillDb();
for (const s of ["merged", "converged", "abandoned", "closed", "failed"]) {
const b = prBucket(db, `pre-${s}`);
assert(b.acknowledged_at !== null, `pre-existing terminal PR (${s}) must be backfilled with acknowledged_at`);
assertEquals(b.list_bucket, "history", `pre-existing terminal PR (${s}) must load in History`);
assertEquals(b.ack_open, 0);
}
// A live PR was never terminal → not backfilled → Active, no Dismiss.
const live = prBucket(db, "pre-live");
assertEquals(live.acknowledged_at, null);
assertEquals(live.list_bucket, "active");
// A PR that settled AFTER the migration is NOT auto-dismissed — stays Active with the Dismiss flag.
const post = prBucket(db, "post-merged");
assertEquals(post.acknowledged_at, null);
assertEquals(post.list_bucket, "active");
assertEquals(post.ack_open, 1);
db.close();
});
test("migration 095 backfill: every pre-existing terminal delivery-graph run loads in History; a live run stays Active; a post-migration terminal run stays Active until dismissed", () => {
const db = backfillDb();
for (const s of ["done", "failed", "abandoned"]) {
const b = dgBucket(db, `pre-${s}`);
assert(b.acknowledged_at !== null, `pre-existing terminal run (${s}) must be backfilled with acknowledged_at`);
assertEquals(b.list_bucket, "history", `pre-existing terminal run (${s}) must load in History`);
assertEquals(b.ack_open, 0);
}
const live = dgBucket(db, "pre-live");
assertEquals(live.acknowledged_at, null);
assertEquals(live.list_bucket, "active");
const post = dgBucket(db, "post-done");
assertEquals(post.acknowledged_at, null);
assertEquals(post.list_bucket, "active");
assertEquals(post.ack_open, 1);
db.close();
});