|
| 1 | +// test/integrity/baseline-rollback.test.ts |
| 2 | +import { describe, test, expect, afterAll } from "bun:test"; |
| 3 | +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; |
| 4 | +import { tmpdir } from "node:os"; |
| 5 | +import { join } from "node:path"; |
| 6 | +import { Kysely, sql } from "kysely"; |
| 7 | +import { LibsqlDialect } from "@libsql/kysely-libsql"; |
| 8 | +import { applyPending, rollbackTo } from "../../src/apply/apply.js"; |
| 9 | +import { recordBaseline, baselineRecord } from "../../src/apply/ledger.js"; |
| 10 | + |
| 11 | +const tmps: string[] = []; |
| 12 | +function db(file: string) { |
| 13 | + return new Kysely<Record<string, unknown>>({ |
| 14 | + dialect: new LibsqlDialect({ url: `file:${file}` }), |
| 15 | + }); |
| 16 | +} |
| 17 | +async function root() { |
| 18 | + const d = await mkdtemp(join(tmpdir(), "baseline-rollback-")); |
| 19 | + tmps.push(d); |
| 20 | + return d; |
| 21 | +} |
| 22 | +async function writeMigration( |
| 23 | + dir: string, |
| 24 | + name: string, |
| 25 | + up: string, |
| 26 | + down: string, |
| 27 | +): Promise<void> { |
| 28 | + const mdir = join(dir, name); |
| 29 | + await mkdir(mdir, { recursive: true }); |
| 30 | + await writeFile(join(mdir, "up.sql"), up, "utf8"); |
| 31 | + await writeFile(join(mdir, "down.sql"), down, "utf8"); |
| 32 | +} |
| 33 | +async function tableExists( |
| 34 | + k: Kysely<Record<string, unknown>>, |
| 35 | + name: string, |
| 36 | +): Promise<boolean> { |
| 37 | + const r = await sql<{ name: string }>` |
| 38 | + SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${name} |
| 39 | + `.execute(k); |
| 40 | + return r.rows.length > 0; |
| 41 | +} |
| 42 | + |
| 43 | +afterAll(async () => { |
| 44 | + for (const d of tmps) await rm(d, { recursive: true, force: true }); |
| 45 | +}); |
| 46 | + |
| 47 | +describe("rollback-all with a baseline marker present", () => { |
| 48 | + test("rollback-all ignores the baseline marker and runs the real down", async () => { |
| 49 | + const r = await root(); |
| 50 | + const migrationsDir = join(r, "migrations"); |
| 51 | + await writeMigration( |
| 52 | + migrationsDir, |
| 53 | + "20260101000000-init", |
| 54 | + `CREATE TABLE t ( id INTEGER NOT NULL PRIMARY KEY );`, |
| 55 | + `DROP TABLE t;`, |
| 56 | + ); |
| 57 | + const k = db(join(r, "rb.db")); |
| 58 | + try { |
| 59 | + // Apply the one real migration. |
| 60 | + await applyPending(k, migrationsDir, { dryRun: false, dialect: "sqlite" }); |
| 61 | + expect(await tableExists(k, "t")).toBe(true); |
| 62 | + |
| 63 | + // Record a baseline marker row in the ledger (NOT a migration). |
| 64 | + await recordBaseline(k, "sqlite", "somehash"); |
| 65 | + |
| 66 | + // Rollback ALL — must NOT throw on the 0000-baseline sentinel. |
| 67 | + const result = await rollbackTo(k, migrationsDir, null, { |
| 68 | + dialect: "sqlite", |
| 69 | + }); |
| 70 | + |
| 71 | + // The real migration's down ran (table gone), baseline was not treated |
| 72 | + // as a migration. |
| 73 | + expect(result.rolledBack).toEqual(["20260101000000-init"]); |
| 74 | + expect(await tableExists(k, "t")).toBe(false); |
| 75 | + |
| 76 | + // Baseline is still readable (the filter only hides it from listings). |
| 77 | + expect((await baselineRecord(k, "sqlite"))?.checksum).toBe("somehash"); |
| 78 | + } finally { |
| 79 | + await k.destroy(); |
| 80 | + } |
| 81 | + }); |
| 82 | +}); |
0 commit comments