import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; import type { Browser } from "playwright-core"; import { chromium } from "playwright-core"; import { detectRuntime, launchBrowser, PdfBrowserLaunchError } from "./runtime"; const KEYS = [ "SCRIBE_PDF_RUNTIME", "AWS_LAMBDA_FUNCTION_NAME", "VERCEL", "SCRIBE_CHROMIUM_EXECUTABLE_PATH", ] as const; const saved: Record = {}; beforeEach(() => { for (const k of KEYS) { saved[k] = process.env[k]; delete process.env[k]; } }); afterEach(() => { for (const k of KEYS) { if (saved[k] === undefined) { delete process.env[k]; } else { process.env[k] = saved[k]; } } }); describe("detectRuntime", () => { test("defaults to local with no serverless signals", () => { expect(detectRuntime()).toBe("local"); }); test("explicit override forces local", () => { process.env.SCRIBE_PDF_RUNTIME = "local"; process.env.VERCEL = "1"; // override wins over autodetection expect(detectRuntime()).toBe("local"); }); test("explicit override forces serverless", () => { process.env.SCRIBE_PDF_RUNTIME = "serverless"; expect(detectRuntime()).toBe("serverless"); }); test("invalid override is a hard error (no silent fallback)", () => { process.env.SCRIBE_PDF_RUNTIME = "lambda"; expect(() => detectRuntime()).toThrow(/SCRIBE_PDF_RUNTIME/); }); test("does not trim override values", () => { process.env.SCRIBE_PDF_RUNTIME = " local"; expect(() => detectRuntime()).toThrow(); }); test("detects serverless from AWS_LAMBDA_FUNCTION_NAME", () => { process.env.AWS_LAMBDA_FUNCTION_NAME = "scribe-hub-fn"; expect(detectRuntime()).toBe("serverless"); }); test("detects serverless from VERCEL=1", () => { process.env.VERCEL = "1"; expect(detectRuntime()).toBe("serverless"); }); test("VERCEL set to a non-1 value does not trigger serverless", () => { process.env.VERCEL = "0"; expect(detectRuntime()).toBe("local"); }); }); describe("launchBrowser", () => { test("rejects an unknown runtime instead of falling back to local", async () => { // A mistyped caller string (bypassing the type) must fail loudly, not // silently launch the local runtime. await expect(launchBrowser("lambda" as unknown as "local")).rejects.toThrow( /Unknown PDF runtime/, ); }); test("an unlaunchable explicit executablePath fails with PdfBrowserLaunchError", async () => { // The override branch wins, and a bogus binary can never launch — so this // is deterministic without any browser install. We also assert the typed // sentinel so callers (report route) can degrade gracefully on it. process.env.SCRIBE_CHROMIUM_EXECUTABLE_PATH = "/nonexistent/scribe-no-such-chrome"; await expect(launchBrowser("local")).rejects.toBeInstanceOf( PdfBrowserLaunchError, ); }); test("does not trim the executablePath env (verbatim in the error)", async () => { // Per project rules env vars are never trimmed; a padded value is used as-is // and surfaced verbatim in the actionable error. const padded = " /nonexistent/scribe-no-such-chrome "; process.env.SCRIBE_CHROMIUM_EXECUTABLE_PATH = padded; await expect(launchBrowser("local")).rejects.toThrow( JSON.stringify(padded), ); }); }); describe("launchLocal auto-discovery (no executablePath)", () => { // No SCRIBE_CHROMIUM_EXECUTABLE_PATH is set (beforeEach clears it), so the // local runtime takes the 3-step auto-discovery chain. chromium.launch is // mocked so the decision logic is deterministic without a real browser. const fakeBrowser = { close: async () => {}, } as unknown as Browser; test("a channel:chrome failure falls through to plain chromium", async () => { const spy = spyOn(chromium, "launch").mockImplementation( async (opts?: { channel?: string }) => { if (opts?.channel === "chrome") throw new Error("no system chrome"); return fakeBrowser; }, ); try { const browser = await launchBrowser("local"); expect(browser).toBe(fakeBrowser); // Exactly two attempts, in order: system chrome, then plain chromium. expect(spy).toHaveBeenCalledTimes(2); expect(spy.mock.calls[0]?.[0]).toMatchObject({ channel: "chrome" }); expect( (spy.mock.calls[1]?.[0] as { channel?: string } | undefined)?.channel, ).toBeUndefined(); } finally { spy.mockRestore(); } }); test("two consecutive launch failures produce exactly one PdfBrowserLaunchError", async () => { const spy = spyOn(chromium, "launch").mockImplementation(async () => { throw new Error("boom"); }); try { let caught: unknown; try { await launchBrowser("local"); } catch (err) { caught = err; } expect(caught).toBeInstanceOf(PdfBrowserLaunchError); // Both auto-discovery steps were attempted before the single final throw. expect(spy).toHaveBeenCalledTimes(2); } finally { spy.mockRestore(); } }); }); describe("PdfBrowserLaunchError", () => { test("is an Error subclass carrying its name", () => { const err = new PdfBrowserLaunchError("nope"); expect(err).toBeInstanceOf(Error); expect(err.name).toBe("PdfBrowserLaunchError"); }); });