import { test } from 'node:test'; import assert from 'node:assert/strict'; import { runMigrations } from '../src/migrate.js'; import { createManager } from '../src/plugin-manager.js'; // Dialog modules import supabase.js, which exits without these (loadPlugins for // the real dialog manifest in the last test touches them transitively). process.env.SUPABASE_URL ||= 'http://localhost'; process.env.SUPABASE_SERVICE_KEY ||= 'x'; process.env.JWT_SECRET ||= 'x'; process.env.SITE_DIR ||= '/tmp/nosite'; // A fake SQL executor over an in-memory schema_migrations set. Records every // non-bookkeeping statement so we can assert what actually ran. function fakeExec(seed = []) { const applied = new Set(seed); const ran = []; const exec = async (text, params) => { if (/^\s*SELECT 1 FROM public\.schema_migrations/i.test(text)) { return applied.has(params[0]) ? [{ '?column?': 1 }] : []; } if (/^\s*INSERT INTO public\.schema_migrations/i.test(text)) { applied.add(params[0]); return []; } if (/^\s*(CREATE TABLE IF NOT EXISTS public\.schema_migrations|BEGIN|COMMIT|ROLLBACK)/i.test(text)) return []; ran.push(text); // the actual migration file SQL return []; }; return { exec, ran, applied }; } test('no declared migrations → no-op, never needs a DB', async () => { const mgr = createManager([{ name: 'kgva-ish' }]); // no migrations const res = await runMigrations(mgr, {}); // no exec, no databaseUrl — must not throw assert.deepEqual(res, { applied: [], skipped: [], pending: false }); }); test('declared migrations but no DATABASE_URL → pending, nothing applied', async () => { const mgr = createManager([{ name: 'p', migrations: ['001.sql'], __dir: new URL('file:///x/') }]); const warns = []; const res = await runMigrations(mgr, { log: { warn: (m) => warns.push(m) } }); assert.equal(res.pending, true); assert.deepEqual(res.skipped, ['p/001.sql']); assert.equal(res.applied.length, 0); assert.match(warns[0], /DATABASE_URL fehlt/); }); test('fresh apply: real dialog migration runs once and is recorded', async () => { const { loadPlugins } = await import('../src/plugin-manager.js'); const mgr = await loadPlugins(['dialog']); // real manifest → real 001_dialog.sql url const fx = fakeExec(); const res = await runMigrations(mgr, { exec: fx.exec, log: {} }); assert.deepEqual(res.applied, ['dialog/001_dialog.sql']); assert.deepEqual(res.skipped, []); assert.equal(fx.ran.length, 1); assert.match(fx.ran[0], /create table if not exists public\.forums/i); // the captured DDL assert.equal(fx.applied.has('dialog/001_dialog.sql'), true); }); test('idempotent: already-recorded migration is skipped, file SQL not re-run', async () => { const { loadPlugins } = await import('../src/plugin-manager.js'); const mgr = await loadPlugins(['dialog']); const fx = fakeExec(['dialog/001_dialog.sql']); // pretend already applied const res = await runMigrations(mgr, { exec: fx.exec, log: {} }); assert.deepEqual(res.skipped, ['dialog/001_dialog.sql']); assert.deepEqual(res.applied, []); assert.equal(fx.ran.length, 0); // file SQL never executed again }); test('a failing migration rolls back and surfaces the id', async () => { const mgr = createManager([{ name: 'boom', migrations: ['x.sql'], __dir: new URL('file:///does-not-exist/') }]); const calls = []; const exec = async (text) => { calls.push(text.trim().split(/\s+/).slice(0, 2).join(' ')); return []; }; // readFile on the missing url throws before BEGIN; ensure the error names the id. await assert.rejects(() => runMigrations(mgr, { exec, log: {} }), /boom\/x\.sql/); });