99 lines
3 KiB
TypeScript
99 lines
3 KiB
TypeScript
|
|
// 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());
|
||
|
|
});
|