/** * celilo#604: a table COUNT reported "up to date" for a DB missing a migrated * COLUMN, so the rollout's own stop-condition was uncheckable by the tools the * runbook named. These assert the column case specifically. */ import { Database } from 'bun:sqlite'; import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { getMigrationStatus, readMigrationJournal } from './migration-status'; /** A journal + a `__drizzle_migrations` table, wired the way drizzle wires them. */ function seed(dir: string, entries: Array<{ when: number; tag: string }>, appliedWhens: number[]) { mkdirSync(join(dir, 'meta'), { recursive: true }); writeFileSync( join(dir, 'meta', '_journal.json'), JSON.stringify({ version: '7', dialect: 'sqlite', entries }), ); const db = new Database(':memory:'); db.run( 'CREATE TABLE `__drizzle_migrations` (id INTEGER PRIMARY KEY, hash TEXT, created_at NUMERIC)', ); for (const when of appliedWhens) { db.run('INSERT INTO `__drizzle_migrations` (hash, created_at) VALUES (?, ?)', [ `h${when}`, when, ]); } return db; } describe('getMigrationStatus', () => { let dir: string; beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'celilo-migstatus-')); }); afterEach(() => rmSync(dir, { recursive: true, force: true })); it('names the latest applied migration and every pending one', () => { const db = seed( dir, [ { when: 100, tag: '0018_drop_alert_policy_snapshot' }, { when: 200, tag: '0019_backup_pid' }, { when: 300, tag: '0020_future' }, ], [100, 200], ); const status = getMigrationStatus(db, dir); expect(status.appliedCount).toBe(2); expect(status.latestApplied).toBe('0019_backup_pid'); expect(status.pending).toEqual(['0020_future']); }); it('reports nothing pending once every journal entry is applied', () => { const db = seed(dir, [{ when: 100, tag: '0000_init' }], [100]); const status = getMigrationStatus(db, dir); expect(status.pending).toEqual([]); expect(status.latestApplied).toBe('0000_init'); }); it('reports a DB that has never been migrated as all-pending', () => { mkdirSync(join(dir, 'meta'), { recursive: true }); writeFileSync( join(dir, 'meta', '_journal.json'), JSON.stringify({ entries: [{ when: 1, tag: '0000_init' }] }), ); const status = getMigrationStatus(new Database(':memory:'), dir); expect(status.appliedCount).toBe(0); expect(status.latestApplied).toBeNull(); expect(status.pending).toEqual(['0000_init']); }); it('counts columns, not just tables — the signal a column migration needs', () => { const db = seed(dir, [], []); const status = getMigrationStatus(db, dir); // Every schema table is missing here, so the counts are the code's totals. expect(status.tableCount).toBeGreaterThan(0); expect(status.columnCount).toBeGreaterThan(status.tableCount); }); it('reads journal entries oldest-first regardless of file order', () => { mkdirSync(join(dir, 'meta'), { recursive: true }); writeFileSync( join(dir, 'meta', '_journal.json'), JSON.stringify({ entries: [ { when: 300, tag: 'c' }, { when: 100, tag: 'a' }, ], }), ); expect(readMigrationJournal(dir).map((e) => e.tag)).toEqual(['a', 'c']); }); it('returns an empty journal when the folder has none', () => { expect(readMigrationJournal(join(dir, 'nope'))).toEqual([]); }); }); describe('getMigrationStatus against the real migrations folder', () => { it('sees backups.pid as missing when the column migration did not land', () => { // The exact celilo#604 shape: every table present, one migrated column not. const db = new Database(':memory:'); db.run('CREATE TABLE backups (id TEXT PRIMARY KEY)'); const status = getMigrationStatus(db, join(import.meta.dir, '../../drizzle')); expect(status.missingColumns).toContain('backups.pid'); expect(status.pending).toContain('0019_backup_pid'); }); });