Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions test/js/improv-frame.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
encodeApplyOpFrames,
APPLY_OP_CHUNK_MAX,
IMPROV_CMD_APPLY_OP,
IMPROV_CMD_SET_TX_POWER,
IMPROV_FRAME_TYPE_RPC,
IMPROV_MAGIC,
} from "../../docs/install/improv-frame.js";
Expand Down Expand Up @@ -80,3 +81,138 @@ test("APPLY_OP always emits at least one frame (so `last` always sends)", () =>
assert.equal(frames.length, 1);
assert.equal(frames[0][9 + 2], 1, "last=1 on the lone frame");
});

// ---------------------------------------------------------------------------
// PR regression: IMPROV_CMD_SET_DEVICE_MODEL (0xFE) removed from improv-frame.js
// ---------------------------------------------------------------------------

test("IMPROV_CMD_SET_DEVICE_MODEL is NOT exported (removed in this PR)", async () => {
// The SET_DEVICE_MODEL RPC (0xFE) was a dedicated vendor command; it has been
// removed — the device model is now just one `set` op in the APPLY_OP stream.
// Verify the export no longer exists so callers can't accidentally reference it.
const mod = await import("../../docs/install/improv-frame.js");
assert.equal(
mod.IMPROV_CMD_SET_DEVICE_MODEL,
undefined,
"IMPROV_CMD_SET_DEVICE_MODEL must not be exported",
);
// 0xFE must not appear as a value for any named export either
const values = Object.values(mod).filter(v => typeof v === "number");
assert.ok(!values.includes(0xFE), "0xFE must not be exported as any constant");
});

// ---------------------------------------------------------------------------
// IMPROV_CMD_SET_TX_POWER — still exported, value and wire format
// ---------------------------------------------------------------------------

test("IMPROV_CMD_SET_TX_POWER equals 0xFD", () => {
assert.equal(IMPROV_CMD_SET_TX_POWER, 0xFD);
});

test("SET_TX_POWER frame layout: [cmd][1][dBm] three-byte payload", () => {
// The orchestrator sends: buildImprovFrame(IMPROV_FRAME_TYPE_RPC,
// new Uint8Array([IMPROV_CMD_SET_TX_POWER, 1, dBm & 0xFF]))
const dBm = 8;
const payload = new Uint8Array([IMPROV_CMD_SET_TX_POWER, 0x01, dBm]);
const frame = buildImprovFrame(IMPROV_FRAME_TYPE_RPC, payload);
assert.equal(frame[7], IMPROV_FRAME_TYPE_RPC, "type byte");
assert.equal(frame[8], 3, "payload length = 3");
assert.equal(frame[9], IMPROV_CMD_SET_TX_POWER, "payload[0] = 0xFD");
assert.equal(frame[10], 0x01, "payload[1] = length-of-value = 1");
assert.equal(frame[11], dBm, "payload[2] = dBm value");
assert.equal(frame.length, 13, "total frame length");
});

test("golden vector: SET_TX_POWER(8 dBm) frame bytes", () => {
// Hand-verified: IMPROV_MAGIC + version(1) + type(3) + length(3) +
// [0xFD, 0x01, 0x08] + checksum(0xEA).
// Checksum = (0x49+0x4d+0x50+0x52+0x4f+0x56+0x01+0x03+0x03+0xFD+0x01+0x08) mod 256
// = 746 mod 256 = 234 = 0xEA.
const frame = buildImprovFrame(
IMPROV_FRAME_TYPE_RPC,
new Uint8Array([IMPROV_CMD_SET_TX_POWER, 0x01, 8]),
);
assert.equal(hex(frame), "49 4d 50 52 4f 56 01 03 03 fd 01 08 ea");
});

test("SET_TX_POWER frame: dBm=0 (lift cap) encodes correctly", () => {
const frame = buildImprovFrame(
IMPROV_FRAME_TYPE_RPC,
new Uint8Array([IMPROV_CMD_SET_TX_POWER, 0x01, 0]),
);
assert.equal(frame[11], 0, "dBm=0 lifts the cap");
assert.equal(frame.length, 13);
});

test("SET_TX_POWER frame: dBm=21 (max) encodes correctly", () => {
const frame = buildImprovFrame(
IMPROV_FRAME_TYPE_RPC,
new Uint8Array([IMPROV_CMD_SET_TX_POWER, 0x01, 21]),
);
assert.equal(frame[11], 21);
});

// ---------------------------------------------------------------------------
// buildImprovFrame edge cases
// ---------------------------------------------------------------------------

test("buildImprovFrame throws if payload exceeds 255 bytes", () => {
const oversized = new Uint8Array(256);
assert.throws(
() => buildImprovFrame(IMPROV_FRAME_TYPE_RPC, oversized),
/255/,
"must mention the 255 limit in the error",
);
});

test("buildImprovFrame accepts payload of exactly 255 bytes", () => {
const maxPayload = new Uint8Array(255).fill(0x01);
const frame = buildImprovFrame(IMPROV_FRAME_TYPE_RPC, maxPayload);
assert.equal(frame[8], 255, "length byte = 255");
assert.equal(frame.length, 6 + 1 + 1 + 1 + 255 + 1, "total frame size");
});

// ---------------------------------------------------------------------------
// encodeApplyOpFrames boundary cases
// ---------------------------------------------------------------------------

test("exactly APPLY_OP_CHUNK_MAX bytes of JSON stays in one frame", () => {
// Craft an op whose JSON is exactly APPLY_OP_CHUNK_MAX bytes.
// {"op":"set","module":"X","control":"c","value":"<padding>"}
const prefix = '{"op":"set","module":"X","control":"c","value":"';
const suffix = '"}';
const pad = APPLY_OP_CHUNK_MAX - prefix.length - suffix.length;
// Guard: if pad < 0 the test premise is broken.
assert.ok(pad >= 0, "prefix+suffix fits within APPLY_OP_CHUNK_MAX");
const value = "A".repeat(pad);
const op = { op: "set", module: "X", control: "c", value };
const json = JSON.stringify(op);
assert.equal(new TextEncoder().encode(json).length, APPLY_OP_CHUNK_MAX, "JSON is exactly the chunk max");
const frames = encodeApplyOpFrames(op);
assert.equal(frames.length, 1, "fits in one frame");
assert.equal(frames[0][9 + 2], 1, "last=1");
});

test("APPLY_OP_CHUNK_MAX + 1 bytes of JSON produces exactly two frames", () => {
// Build an op whose JSON exceeds the chunk max by exactly one byte.
const prefix = '{"op":"set","module":"X","control":"c","value":"';
const suffix = '"}';
const pad = APPLY_OP_CHUNK_MAX - prefix.length - suffix.length + 1;
assert.ok(pad >= 0, "prefix+suffix fits within APPLY_OP_CHUNK_MAX+1");
const value = "A".repeat(pad);
const op = { op: "set", module: "X", control: "c", value };
const jsonLen = new TextEncoder().encode(JSON.stringify(op)).length;
assert.equal(jsonLen, APPLY_OP_CHUNK_MAX + 1);
const frames = encodeApplyOpFrames(op);
assert.equal(frames.length, 2);
assert.equal(frames[0][9 + 1], 0, "frame 0: seq=0");
assert.equal(frames[0][9 + 2], 0, "frame 0: not last");
assert.equal(frames[1][9 + 1], 1, "frame 1: seq=1");
assert.equal(frames[1][9 + 2], 1, "frame 1: last=1");
// The two chunks reassemble to the original JSON exactly.
const enc = new TextEncoder().encode(JSON.stringify(op));
const chunk0 = enc.subarray(0, APPLY_OP_CHUNK_MAX);
const chunk1 = enc.subarray(APPLY_OP_CHUNK_MAX);
assert.deepEqual(Array.from(frames[0].subarray(9 + 3, 9 + frames[0][8])), Array.from(chunk0));
assert.deepEqual(Array.from(frames[1].subarray(9 + 3, 9 + frames[1][8])), Array.from(chunk1));
});
Loading