352c643009
Pins the device-layer bugs we kept hand-verifying, as pure byte-stream assertions (no sockets, no hardware): - printer-escpos.test.ts (12): CP852 codepage select; the ë→0x89 / Ë→0xD3 mapping and the em-dash/⚠ ASCII fallbacks (never a stray 0x3f "?"); and the Code128 MODULE WIDTH contract — a short ticket id at width 3, but the ~20-char out-of-window occurrence id at width 2 so it fits the 80mm head (width 3 overflows ~576 dots and the firmware silently aborts the barcode). Plus the QR-and-Code128 dual encoding and the Albanian stamp() format. - printer-routing.test.ts (6): the failover order (booth printer is a fallback for entry tickets; a receipt never prints on the outside dispenser), rank-then-id tiebreak, and printWithFailover walking the order + NoPrinterAvailableError. Wires Vitest into @parking/devices. devices 18/18 green. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
79 lines
3.6 KiB
TypeScript
79 lines
3.6 KiB
TypeScript
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("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);
|
|
});
|
|
});
|