Files
octoturge 818f28ea5d Pin the discovery frame against a real capture
The gateway and backend halves of `discovered_printers` are hand-synced, so
every hand-written example in this file agrees with whichever side wrote it -
which is exactly how the two ends of /ws/edge/v1 drifted into different
protocols before. A capture is the only thing that catches that.

This one came off a live LAN with a Bambu P1P and H2C on it, through the
gateway's own serde enum, with its fleet file pointed at an empty file so
nothing was filtered out. The addresses and the shape are as captured.

The two serials are placeholders of the captured shape. They identify
someone's physical hardware, no test depends on their value, and no other
fixture here carries one - so the real ones are not something to commit on
the owner's behalf.
2026-10-19 08:20:58 +00:00

403 lines
16 KiB
TypeScript

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<typeof Value.Check>[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<string, unknown> = { ...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);
});
});