import { expect, test } from "bun:test"; import { Database } from "bun:sqlite"; import { drizzle } from "drizzle-orm/bun-sqlite"; import { migrate } from "drizzle-orm/bun-sqlite/migrator"; import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; test("development entrypoint upgrades an existing database before serving requests", async () => { const directory = mkdtempSync(join(tmpdir(), "minabot-dev-upgrade-")); const path = join(directory, "app.sqlite"); const sqlite = new Database(path); let server: ReturnType | undefined; try { const migrations = join(directory, "migrations"); mkdirSync(join(migrations, "meta"), { recursive: true }); const journal = JSON.parse(readFileSync("drizzle/meta/_journal.json", "utf8")); journal.entries = journal.entries.slice(0, 6); writeFileSync(join(migrations, "meta/_journal.json"), JSON.stringify(journal)); for (const entry of journal.entries) copyFileSync(`drizzle/${entry.tag}.sql`, join(migrations, `${entry.tag}.sql`)); migrate(drizzle(sqlite), { migrationsFolder: migrations }); sqlite.exec("INSERT INTO users (id, discord_id, username, created_at, updated_at) VALUES ('existing', 'existing', 'Existing user', 0, 0)"); expect(sqlite.query("SELECT name FROM sqlite_master WHERE name = 'reminder_settings'").get()).toBeNull(); // Launch directly, as Bun does when re-evaluating the entrypoint during HMR; // deliberately skip package.json's separate migration command. server = Bun.spawn([process.execPath, "src/index.ts"], { env: { ...process.env, NODE_ENV: "development", DATABASE_PATH: path, PORT: "0", APP_ORIGIN: "http://127.0.0.1", DISCORD_BOT_TOKEN: "", DISCORD_ALLOWED_USER_IDS: "" }, stdout: "pipe", stderr: "pipe", }); const reader = (server.stdout as ReadableStream).getReader(); let output = ""; while (!output.includes("Server running at")) { const chunk = await reader.read(); if (chunk.done) throw new Error("Development server exited before it was ready"); output += new TextDecoder().decode(chunk.value); } reader.releaseLock(); const origin = output.match(/Server running at (http:\/\/[^\s]+)/)?.[1]; expect(origin).toBeDefined(); expect((await fetch(new URL("/api/health", origin))).status).toBe(200); expect(sqlite.query("SELECT COUNT(*) AS count FROM __drizzle_migrations").get()).toEqual({ count: JSON.parse(readFileSync("drizzle/meta/_journal.json", "utf8")).entries.length }); expect(sqlite.query("SELECT enabled FROM reminder_settings").all()).toEqual([]); expect(sqlite.query("SELECT retry_at FROM reminder_deliveries").all()).toEqual([]); expect(sqlite.query("SELECT blocked_until FROM reminder_worker_state").all()).toEqual([]); expect(sqlite.query("SELECT username FROM users WHERE id = 'existing'").get()).toEqual({ username: "Existing user" }); } finally { server?.kill(); if (server) await server.exited; sqlite.close(); rmSync(directory, { recursive: true, force: true }); } }, 15000);