`sidecar/claude-sdk` is the repo's second TypeScript package, wired to the root only through `npm --prefix`. It reads length-prefixed MessagePack frames on stdin, runs one SDK query, forwards every tool call to Rust, and returns Rust's answer to the model verbatim. It computes no tool result, reads no credential, logs no prompt, argument, or result, and writes nothing outside its own stdout. The query is opened with a custom system prompt, `settingSources: []`, `tools: []`, `strictMcpConfig`, `persistSession: false`, and `alwaysLoad` on every tool, so no CLAUDE.md, settings file, output style, built-in tool, or persisted transcript reaches a caller-derived prompt. `schema.ts` converts exactly the grammar `tool_schema.rs` emits and throws on anything else, so an emitter that outgrows it fails a test rather than registering a looser tool. Two Rust tests publish the fixtures the sidecar tests read — the frame bytes and every real `parameters_json` — so the halves cannot drift apart silently. Dependencies are exact-pinned: @anthropic-ai/claude-agent-sdk 0.3.261, @msgpack/msgpack 3.1.3, zod 4.5.4, and dev typescript 5.9.3 with @types/node 24.13.3. `engines.node` is the installed major. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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());
|
|
});
|