Ghostlight/sidecar/claude-sdk/test/frames.test.ts

99 lines
3 KiB
TypeScript
Raw Normal View History

// Spec test 18: the frame bytes Rust writes decode here, and the bytes this
// side encodes are byte-identical to Rust's. The `.bin` fixtures are written by
// the Rust test `frame_fixtures_match_the_checked_in_sidecar_bytes`.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import test from "node:test";
import {
FrameReader,
MAX_FRAME_BYTES,
decodeFrameBody,
encodeFrame,
type SidecarFrame,
} from "../src/frames.ts";
function fixture(name: string): Uint8Array {
return new Uint8Array(
readFileSync(fileURLToPath(new URL(`./frames/${name}.bin`, import.meta.url))),
);
}
const NAMES = ["query", "tool_call", "tool_result", "output", "fault"];
test("every Rust frame fixture decodes and re-encodes to the same bytes", () => {
for (const name of NAMES) {
const bytes = fixture(name);
const frame = decodeFrameBody(bytes.subarray(4));
assert.equal(frame.kind, name, `${name} decoded to the wrong kind`);
assert.deepEqual(
Array.from(encodeFrame(frame)),
Array.from(bytes),
`${name} did not re-encode to Rust's bytes`,
);
}
});
test("the query fixture carries the fields the lowering reads", () => {
const frame = decodeFrameBody(fixture("query").subarray(4)) as Extract<
SidecarFrame,
{ kind: "query" }
>;
assert.equal(frame.query_id, 7);
assert.equal(frame.model, "claude-opus-5");
assert.equal(frame.turn_cap, 24);
assert.equal(frame.effort, "medium");
assert.equal(frame.max_output_tokens, 4000);
assert.deepEqual(frame.transcript, ["assistant: thinking", "tool result: ok"]);
assert.equal(frame.tools.length, 1);
assert.equal(frame.tools[0]!.name, "submit");
});
test("the output fixture reports dispatch honestly", () => {
const frame = decodeFrameBody(fixture("output").subarray(4)) as Extract<
SidecarFrame,
{ kind: "output" }
>;
assert.deepEqual(frame.events[0], { kind: "text", text: "Done." });
assert.deepEqual(frame.events[1], {
kind: "tool_call",
call_id: "call-0",
name: "submit",
arguments: "{}",
dispatched: true,
});
assert.equal(frame.receipt.session_id, "session-one");
assert.equal(frame.receipt.usage[0]!.output_tokens, 34);
});
test("a chunked stream reassembles into whole frames", () => {
const reader = new FrameReader();
const stream = new Uint8Array([
...fixture("tool_call"),
...fixture("tool_result"),
]);
const seen: SidecarFrame[] = [];
for (let at = 0; at < stream.length; at += 3) {
reader.push(stream.subarray(at, Math.min(at + 3, stream.length)));
for (;;) {
const frame = reader.next();
if (!frame) {
break;
}
seen.push(frame);
}
}
assert.deepEqual(
seen.map((frame) => frame.kind),
["tool_call", "tool_result"],
);
});
test("a length prefix outside the cap is refused", () => {
const reader = new FrameReader();
const prefix = new Uint8Array(4);
new DataView(prefix.buffer).setUint32(0, MAX_FRAME_BYTES + 1, false);
reader.push(prefix);
assert.throws(() => reader.next());
});