import { afterEach, beforeEach, expect, test } from "bun:test"; import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; let directory: string; const project = new URL("../", import.meta.url); beforeEach(() => { directory = mkdtempSync(join(tmpdir(), "minabot-env-test-")); // Run the real package commands with disposable entrypoints that report only fixture values. writeFileSync(join(directory, "package.json"), readFileSync(new URL("package.json", project))); writeFileSync(join(directory, "bunfig.toml"), readFileSync(new URL("bunfig.toml", project))); mkdirSync(join(directory, "scripts")); mkdirSync(join(directory, "src")); for (const [file, stage] of [["scripts/migrate.ts", "migration"], ["scripts/start.ts", "server"], ["src/index.ts", "server"]]) { writeFileSync(join(directory, file!), `console.log(JSON.stringify({ stage: ${JSON.stringify(stage)}, mode: process.env.NODE_ENV, origin: process.env.APP_ORIGIN, database: process.env.DATABASE_PATH, shared: process.env.MINABOT_ENV_TEST_SHARED })); process.exit(0);`); } writeFileSync(join(directory, ".env"), "APP_ORIGIN=https://shared.example\nDATABASE_PATH=shared.sqlite\nMINABOT_ENV_TEST_SHARED=shared\n"); writeFileSync(join(directory, ".env.development"), "APP_ORIGIN=http://127.0.0.1:4000\nDATABASE_PATH=development.sqlite\n"); writeFileSync(join(directory, ".env.production"), "APP_ORIGIN=https://production.example\nDATABASE_PATH=production.sqlite\n"); }); afterEach(() => rmSync(directory, { recursive: true, force: true })); function run(command: string, overrides: Record = {}) { const env = { ...process.env }; for (const key of ["NODE_ENV", "APP_ORIGIN", "DATABASE_PATH", "MINABOT_ENV_TEST_SHARED", "BUN_OPTIONS"]) delete env[key]; const result = Bun.spawnSync([process.execPath, "run", command], { cwd: directory, env: { ...env, ...overrides }, timeout: 10_000, stdout: "pipe", stderr: "pipe", }); expect(result.exitCode).toBe(0); return result.stdout.toString().trim().split("\n").map(line => JSON.parse(line)); } test.each(["development", "production"])("%s loads the same environment before migration and server startup", mode => { const records = run(mode === "production" ? "start" : "dev"); expect(records).toEqual(["migration", "server"].map(stage => ({ stage, mode, origin: mode === "production" ? "https://production.example" : "http://127.0.0.1:4000", database: `${mode}.sqlite`, shared: "shared", }))); }); test("database commands select their corresponding environment", () => { expect(run("db:migrate")[0].database).toBe("development.sqlite"); expect(run("db:migrate:production")[0].database).toBe("production.sqlite"); }); test("exported deployment variables override environment files", () => { const records = run("start", { APP_ORIGIN: "https://deployed.example", DATABASE_PATH: "deployed.sqlite" }); expect(records.every(record => record.origin === "https://deployed.example" && record.database === "deployed.sqlite")).toBe(true); }); test("production never preloads development values when its file is absent", () => { rmSync(join(directory, ".env.production")); const records = run("start"); expect(records.every(record => record.origin === "https://shared.example" && record.database === "shared.sqlite")).toBe(true); });