import { describe, expect, it, vi } from "vitest"; import { orderForRole, printWithFailover, NoPrinterAvailableError, type PrinterInstance, } from "./printer-routing.js"; import type { PrinterDevice } from "./interfaces.js"; // Printer routing is pure selection over (config, health): which printer prints a job, // best-first, with failover. The key business rules: the booth printer is a FALLBACK for // entry tickets but a receipt NEVER prints on the outside dispenser; rank then id break // ties deterministically; printWithFailover walks the order and surfaces all failures. function inst(id: string, role: PrinterInstance["role"], failoverRank = 0, device?: PrinterDevice): PrinterInstance { return { id, role, failoverRank, device: device ?? ({} as PrinterDevice) }; } describe("orderForRole", () => { it("entry-dispenser job: dispensers first, booth-receipt as fallback", () => { const printers = [inst("booth", "booth-receipt"), inst("disp", "entry-dispenser")]; expect(orderForRole(printers, "entry-dispenser").map((p) => p.id)).toEqual(["disp", "booth"]); }); it("booth-receipt job: NEVER falls back to the outside dispenser", () => { const printers = [inst("disp", "entry-dispenser"), inst("booth", "booth-receipt")]; expect(orderForRole(printers, "booth-receipt").map((p) => p.id)).toEqual(["booth"]); }); it("wash-desk job: the desk printer first, the booth printer as fallback, never the dispenser", () => { const printers = [inst("disp", "entry-dispenser"), inst("booth", "booth-receipt"), inst("desk", "wash-desk")]; expect(orderForRole(printers, "wash-desk").map((p) => p.id)).toEqual(["desk", "booth"]); // A site without a desk printer keeps printing wash slips in the booth. expect(orderForRole(printers.slice(0, 2), "wash-desk").map((p) => p.id)).toEqual(["booth"]); }); it("nothing ever falls back TO the wash desk (booth receipts and entry tickets stay off it)", () => { const printers = [inst("desk", "wash-desk")]; expect(orderForRole(printers, "booth-receipt")).toEqual([]); expect(orderForRole(printers, "entry-dispenser")).toEqual([]); }); it("breaks ties by failoverRank (higher first), then id", () => { const printers = [ inst("b", "entry-dispenser", 1), inst("a", "entry-dispenser", 1), inst("c", "entry-dispenser", 5), ]; expect(orderForRole(printers, "entry-dispenser").map((p) => p.id)).toEqual(["c", "a", "b"]); }); it("excludes printers of no relevant role", () => { const printers = [inst("booth", "booth-receipt")]; expect(orderForRole(printers, "booth-receipt").map((p) => p.id)).toEqual(["booth"]); // For a receipt job, an entry dispenser is excluded entirely. expect(orderForRole([inst("disp", "entry-dispenser")], "booth-receipt")).toEqual([]); }); }); describe("printWithFailover", () => { function device(behavior: "ok" | "fail"): PrinterDevice { return { printTicket: vi.fn(behavior === "ok" ? async () => {} : async () => { throw new Error("offline"); }), } as unknown as PrinterDevice; } it("prints on the first healthy candidate and returns its id", async () => { const printers = [inst("disp", "entry-dispenser", 0, device("ok")), inst("booth", "booth-receipt", 0, device("ok"))]; const job = vi.fn(async (d: PrinterDevice) => d.printTicket({} as never)); const used = await printWithFailover(printers, "entry-dispenser", job); expect(used).toBe("disp"); expect(job).toHaveBeenCalledTimes(1); }); it("fails over to the booth printer when the dispenser throws", async () => { const printers = [inst("disp", "entry-dispenser", 0, device("fail")), inst("booth", "booth-receipt", 0, device("ok"))]; const used = await printWithFailover(printers, "entry-dispenser", (d) => d.printTicket({} as never)); expect(used).toBe("booth"); }); it("throws NoPrinterAvailableError listing every failed attempt", async () => { const printers = [inst("disp", "entry-dispenser", 0, device("fail")), inst("booth", "booth-receipt", 0, device("fail"))]; await expect(printWithFailover(printers, "entry-dispenser", (d) => d.printTicket({} as never))) .rejects.toBeInstanceOf(NoPrinterAvailableError); }); it("throws when no printer is configured for the role", async () => { await expect(printWithFailover([], "entry-dispenser", async () => {})) .rejects.toBeInstanceOf(NoPrinterAvailableError); }); });