import { describe, expect, it } from "bun:test"; import { Value } from "@sinclair/typebox/value"; import { EdgeClientMessageSchema, EdgeServerMessageSchema } from "@continuum/ts-types"; // Registers `uuid` and `uri`. Not optional: TypeBox treats an unregistered // format as a failure, so without this every `start_print` in this file is // rejected for its job_id and url. This file used to rely on another test // file importing Elysia first, which made it pass in a full run and fail on // its own. import "../src/validation/formats"; import currentCapture from "./fixtures/gateway_frames.json"; import legacyCapture from "./fixtures/gateway_frames_legacy_nulls.json"; import discoveryCapture from "./fixtures/gateway_discovery_frame.json"; /** * A contract test, not a unit test: it checks that what continuum-proxy * actually puts on the wire is what this backend's validator accepts. * * The fixtures are real captures from a live gateway talking to a real P1P and * H2C, not hand-written examples - which is the whole point. The two ends of * /ws/edge/v1 had silently drifted into completely different protocols * (different handshake, different telemetry shape, printer ids the gateway * could not know), and every hand-written example agreed with whichever side * wrote it. Re-capture with scripts/capture-gateway-frames.js when the protocol * changes, rather than editing these by hand. */ function check(schema: Parameters[0], value: unknown) { const ok = Value.Check(schema, value); if (!ok) { // Surface *why*, or a failure here is just "false !== true". const errors = [...Value.Errors(schema, value)].map( (e) => `${e.path || "/"}: ${e.message} (got ${JSON.stringify(e.value)})`, ); throw new Error(`validation failed:\n ${errors.join("\n ")}`); } } describe("gateway -> backend frames", () => { for (const frame of currentCapture.frames) { it(`accepts a real '${frame.type}' frame`, () => { check(EdgeClientMessageSchema, frame); }); } it("accepts the pre-fix capture, where empty fields were explicit nulls", () => { // Liberal reader: the gateway now omits these, but a serde-derived // producer emits null by default and one such field must not cost us a // whole farm's telemetry batch. for (const frame of legacyCapture.frames) { check(EdgeClientMessageSchema, frame); } }); it("carries the live telemetry this was captured from", () => { const telemetry = currentCapture.frames.find((f) => f.type === "telemetry"); expect(telemetry).toBeDefined(); const printers = (telemetry as { printers: Array<{ printer_id: string }> }).printers; expect(printers.length).toBeGreaterThan(0); // Guards against a fixture that validates because it is empty. expect(printers.every((p) => typeof p.printer_id === "string" && p.printer_id.length > 0)).toBe(true); }); it("accepts a stage, including stage 0", () => { // `stage` is Bambu's `stg_cur`: what the printer is doing when it is not // laying down plastic. 0 means printing, and a schema or serialiser that // treats 0 as absent loses the distinction that makes the field worth // having - a printer at 14% and layer 0 is mid-bed-levelling at stage 54 // and genuinely stuck at stage 0. for (const stage of [0, 13, 54]) { check(EdgeClientMessageSchema, { type: "telemetry", printers: [{ printer_id: "p1p", name: "P1P", percent: 14, layer: 0, stage }], }); } // The assertion that makes the loop above mean anything. This schema // permits unknown properties, so `{ stage: 54 }` validates whether or // not `stage` is declared - verified, it does. Rejecting a *wrongly // typed* stage is what proves the field is really in the schema, so this // is what fails if someone removes it. expect( Value.Check(EdgeClientMessageSchema, { type: "telemetry", printers: [{ printer_id: "p1p", name: "P1P", stage: "54" }], }), ).toBe(false); }); it("still accepts telemetry with no stage at all", () => { // The field is additive: the committed live capture predates it, and a // gateway that has not been updated yet simply omits it. check(EdgeClientMessageSchema, { type: "telemetry", printers: [{ printer_id: "p1p", name: "P1P", percent: 0 }], }); }); it("rejects a telemetry batch whose printer has no id", () => { // printer_id and name are the only required fields; everything else is // genuinely unknowable for a printer that has only just connected. check(EdgeClientMessageSchema, { type: "telemetry", printers: [{ printer_id: "p1p", name: "P1P" }] }); expect(Value.Check(EdgeClientMessageSchema, { type: "telemetry", printers: [{ name: "P1P" }] })).toBe(false); }); it("rejects an unknown message type", () => { // The old protocol's handshake, which no gateway ever sent. expect(Value.Check(EdgeClientMessageSchema, { type: "auth", token: "x" })).toBe(false); }); /** * The hello carries no credential any more - the edge JWT goes in an * `Authorization: Bearer` header on the upgrade, verified before the * socket exists (see tests/edge_upgrade.test.ts). This asserts the schema * does not require the `token` field it used to. * * Note the committed captures predate that change and still contain * `token: "dev-token"`, which validates because this schema is a liberal * reader. That is deliberate and harmless: the field is now ignored, and * an old gateway sending it is refused at the upgrade for having no * header, so there is no path by which a token in a frame authenticates * anything. */ it("accepts a hello with no token at all", () => { check(EdgeClientMessageSchema, { type: "hello", gateway_id: "dev-gw", farm_id: "dev-farm", }); check(EdgeClientMessageSchema, { type: "hello", gateway_id: "dev-gw", farm_id: "dev-farm", stream_base_url: "http://gateway.local:1984", }); }); it("still requires the fields a hello cannot work without", () => { // `farm_id` is checked against the token's own claim rather than // trusted, which is only possible if it is actually sent. expect(Value.Check(EdgeClientMessageSchema, { type: "hello", gateway_id: "g" })).toBe(false); expect(Value.Check(EdgeClientMessageSchema, { type: "hello", farm_id: "f" })).toBe(false); }); describe("discovered_printers", () => { const candidate = { vendor: "bambu", address: "192.168.0.20", serial: "00M09A382100000", model: "C11", name: "P1P", source: "certificate", driver_ready: true, }; it("accepts the real scan captured from a live LAN", () => { // The one case a hand-written example cannot cover. Everything below // agrees with how this test file imagines the gateway behaves; this // agrees with what the gateway actually emitted, through its own serde // enum, about two real printers. The two halves of this message are // hand-synced, so a capture is the only thing that catches a drift. for (const frame of discoveryCapture.frames) { check(EdgeClientMessageSchema, frame); } // Guards against a fixture that validates because it is empty. const printers = discoveryCapture.frames[0].printers; expect(printers.length).toBeGreaterThan(0); // And against one that validates because every field is absent: the // address is the entire reason this message exists. expect(printers.every((p) => p.address.length > 0 && p.vendor.length > 0)).toBe(true); }); it("accepts a scan result", () => { check(EdgeClientMessageSchema, { type: "discovered_printers", printers: [candidate], scanned_at: "2026-10-19T12:00:00Z", }); }); it("accepts a gateway that looked and found nothing", () => { // Distinct from never having reported, which is the off-by-default // case. An empty list is a real answer and must validate. check(EdgeClientMessageSchema, { type: "discovered_printers", printers: [] }); }); it("accepts a candidate with nothing but where and what it is", () => { // The honest minimum for a Klipper host: Moonraker answers, so the // address and the vendor are known, and nothing else is. check(EdgeClientMessageSchema, { type: "discovered_printers", printers: [{ vendor: "klipper", address: "192.168.0.31", source: "http_api", driver_ready: false }], }); }); it("accepts the prusa_link / octoprint ambiguity", () => { check(EdgeClientMessageSchema, { type: "discovered_printers", printers: [ { vendor: "octoprint", address: "192.168.0.42", ambiguous_with: "prusa_link", source: "http_api", driver_ready: false, }, ], }); }); it("rejects a name longer than the cap", () => { // The point of the cap, and the reason this variant is closed where // its siblings are open: every string in a candidate was chosen by a // device on the customer's network. Anything that answers an SSDP // query names itself, so an unbounded `name` is LAN-supplied text on // its way to a database column and a React tree. expect( Value.Check(EdgeClientMessageSchema, { type: "discovered_printers", printers: [{ ...candidate, name: "x".repeat(129) }], }), ).toBe(false); }); it("rejects a vendor nothing can drive and a source nothing can produce", () => { for (const bad of [{ vendor: "makerbot" }, { source: "telepathy" }]) { expect( Value.Check(EdgeClientMessageSchema, { type: "discovered_printers", printers: [{ ...candidate, ...bad }], }), ).toBe(false); } }); it("rejects a candidate carrying a credential", () => { // Not a hypothetical tidiness check. A probe cannot learn an access // code, so a candidate that has one got it from somewhere else, and // the closed object is what stops this message quietly becoming a // channel for secrets nobody meant to send to the cloud. expect( Value.Check(EdgeClientMessageSchema, { type: "discovered_printers", printers: [{ ...candidate, access_code: "12345678" }], }), ).toBe(false); }); it("requires an address, because a candidate without one is unusable", () => { const { address: _address, ...addressless } = candidate; expect( Value.Check(EdgeClientMessageSchema, { type: "discovered_printers", printers: [addressless], }), ).toBe(false); // And an empty one is no better than a missing one. expect( Value.Check(EdgeClientMessageSchema, { type: "discovered_printers", printers: [{ ...candidate, address: "" }], }), ).toBe(false); }); }); }); describe("backend -> gateway frames", () => { // Exactly what src/routes/ws/edge.ts sends. If one of these stops // validating, the gateway's serde enum will have stopped accepting it too - // see the mirror of this test in continuum-proxy/tests/uplink_protocol.rs. const outbound = [ { type: "hello_ack", session_id: "3f2b1c5e-0000-4000-8000-000000000000" }, { type: "hello_error", message: "invalid edge token" }, { type: "heartbeat_ack", seq: 7 }, { type: "command", command_id: "9a7d2f10-0000-4000-8000-000000000000", printer_id: "p1p", command: { action: "set_fan_speed", percent: 40 }, }, { type: "command", command_id: "9a7d2f11-0000-4000-8000-000000000000", printer_id: "p1p", command: { action: "send_gcode_line", line: "M106 S0" }, }, { type: "command", command_id: "9a7d2f12-0000-4000-8000-000000000000", printer_id: "p1p", command: { action: "control_print", control: "pause" }, }, { type: "command", command_id: "9a7d2f13-0000-4000-8000-000000000000", printer_id: "p1p", command: { action: "control_print", control: "resume" }, }, { type: "command", command_id: "9a7d2f14-0000-4000-8000-000000000000", printer_id: "p1p", command: { action: "control_print", control: "stop" }, }, { type: "command", command_id: "9a7d2f15-0000-4000-8000-000000000000", printer_id: "p1p", command: { action: "start_print", job_id: "7c9e6679-0000-4000-8000-000000000000", file_name: "cube.gcode.3mf", // Exactly what src/dispatch sends: a presigned GET, minted at // dispatch time rather than at job creation. url: "https://example.r2.cloudflarestorage.com/farms/f/jobs/j/cube.gcode.3mf?X-Amz-Signature=x", }, }, ]; for (const frame of outbound) { it(`emits a valid '${frame.type}' (${(frame as { command?: { action: string } }).command?.action ?? "no command"})`, () => { check(EdgeServerMessageSchema, frame); }); } it("rejects a print control verb the gateway does not implement", () => { // "cancel" is the trap: it is what Moonraker calls this, and the Klipper // implementation behind `control_print` really does send a cancel - but // the wire word is "stop". A verb that only one vendor recognises must // not validate here, or it reaches a gateway that cannot map it. for (const control of ["cancel", "abort", "Pause", ""]) { expect( Value.Check(EdgeServerMessageSchema, { type: "command", command_id: "x", printer_id: "p1p", command: { action: "control_print", control }, }), ).toBe(false); } }); it("rejects a start_print missing any of its three required fields", () => { // The variant exists now - the job scheduler in src/dispatch sends it. // What this guards is that none of its fields is optional. Each one is // load-bearing in a different way: without `url` the gateway has nothing // to fetch, without `file_name` it has nowhere to put it, and without // `job_id` nothing can line a gateway's logs up against a job. An // optional field here would fail at the gateway, minutes later, on a // machine nobody is watching. const complete = { action: "start_print", job_id: "7c9e6679-0000-4000-8000-000000000000", file_name: "plate_1.3mf", url: "https://example.test/signed", }; check(EdgeServerMessageSchema, { type: "command", command_id: "x", printer_id: "p1p", command: complete, }); for (const omit of ["job_id", "file_name", "url"] as const) { const partial: Record = { ...complete }; delete partial[omit]; expect( Value.Check(EdgeServerMessageSchema, { type: "command", command_id: "x", printer_id: "p1p", command: partial, }), ).toBe(false); } }); it("will not carry a filament or AMS mapping on a start_print", () => { // Deliberately absent from the contract, and the reason is expensive: // hardcoding the filament choice instead of reading it from the project // archive made a real P1P heat to 250C and pause itself at 1%. The // gateway derives it from the `.3mf`'s own slice_info.config, which is // the only thing that knows. A field here would invite a caller to guess // again - and because this schema is a closed union, an extra property // makes the whole command fail to validate rather than being ignored. expect( Value.Check(EdgeServerMessageSchema, { type: "command", command_id: "x", printer_id: "p1p", command: { action: "start_print", job_id: "7c9e6679-0000-4000-8000-000000000000", file_name: "plate_1.3mf", url: "https://example.test/signed", ams_mapping: [3, -1, -1, -1, -1], }, }), ).toBe(false); }); });