import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { PassThrough } from 'node:stream'; import Database from 'better-sqlite3'; import { runMigrateCli } from '../src/cli.ts'; function captureStreams() { const stdout = new PassThrough(); const stderr = new PassThrough(); const stdoutBuf: Buffer[] = []; const stderrBuf: Buffer[] = []; stdout.on('data', (c) => stdoutBuf.push(Buffer.isBuffer(c) ? c : Buffer.from(c))); stderr.on('data', (c) => stderrBuf.push(Buffer.isBuffer(c) ? c : Buffer.from(c))); return { stdout, stderr, get stdoutText() { return Buffer.concat(stdoutBuf).toString('utf8'); }, get stderrText() { return Buffer.concat(stderrBuf).toString('utf8'); }, }; } describe('runMigrateCli', () => { let tmpDir: string; beforeEach(() => { tmpDir = mkdtempSync(join(tmpdir(), 'sqlite-migrate-cli-test-')); }); afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); }); it("command='migrate' calls applyMigrations and writes expected lines", () => { writeFileSync(join(tmpDir, '0001_init.sql'), 'CREATE TABLE x (a INTEGER);'); const caps = captureStreams(); const code = runMigrateCli({ openDb: () => new Database(':memory:'), migrationsDir: tmpDir, command: 'migrate', stdout: caps.stdout, stderr: caps.stderr, }); expect(code).toBe(0); expect(caps.stdoutText).toContain('applied:1'); expect(caps.stdoutText).toContain('migrations: 1 applied, 0 pending'); }); it("command='status' emits pending then applied then pending again", () => { writeFileSync(join(tmpDir, '0001_init.sql'), 'CREATE TABLE x (a INTEGER);'); const sharedDb = new Database(':memory:'); const openDb = () => { const proxy = new Proxy(sharedDb, { get(target, prop) { if (prop === 'close') return () => {}; const v = (target as any)[prop]; return typeof v === 'function' ? v.bind(target) : v; }, }); return proxy as unknown as Database.Database; }; const migrateCaps = captureStreams(); const migrateCode = runMigrateCli({ openDb, migrationsDir: tmpDir, command: 'migrate', stdout: migrateCaps.stdout, stderr: migrateCaps.stderr, }); expect(migrateCode).toBe(0); const statusCaps = captureStreams(); const statusCode = runMigrateCli({ openDb, migrationsDir: tmpDir, command: 'status', stdout: statusCaps.stdout, stderr: statusCaps.stderr, }); expect(statusCode).toBe(0); expect(statusCaps.stdoutText).toContain('applied:0001'); writeFileSync(join(tmpDir, '0002_more.sql'), 'CREATE TABLE y (b INTEGER);'); const statusCaps2 = captureStreams(); const statusCode2 = runMigrateCli({ openDb, migrationsDir: tmpDir, command: 'status', stdout: statusCaps2.stdout, stderr: statusCaps2.stderr, }); expect(statusCode2).toBe(0); expect(statusCaps2.stdoutText).toContain('pending:0002'); sharedDb.close(); }); it("command='stamp' marks applied without running body", () => { writeFileSync( join(tmpDir, '0001_init.sql'), 'CREATE TABLE only_if_applied (x INTEGER);', ); const sharedDb = new Database(':memory:'); const openDb = () => { const proxy = new Proxy(sharedDb, { get(target, prop) { if (prop === 'close') return () => {}; const v = (target as any)[prop]; return typeof v === 'function' ? v.bind(target) : v; }, }); return proxy as unknown as Database.Database; }; const stampCaps = captureStreams(); const code = runMigrateCli({ openDb, migrationsDir: tmpDir, command: 'stamp', version: '0001', stdout: stampCaps.stdout, stderr: stampCaps.stderr, }); expect(code).toBe(0); expect(stampCaps.stdoutText).toContain('stamped:0001'); const tableMissing = sharedDb .prepare( `SELECT name FROM sqlite_master WHERE type='table' AND name='only_if_applied'`, ) .get(); expect(tableMissing).toBeUndefined(); const noVersionCaps = captureStreams(); const noVersionCode = runMigrateCli({ openDb, migrationsDir: tmpDir, command: 'stamp', stdout: noVersionCaps.stdout, stderr: noVersionCaps.stderr, }); expect(noVersionCode).toBe(1); expect(noVersionCaps.stderrText).toContain('error:'); sharedDb.close(); }); });