Files
stack/packages/conversation/tests/smoke.test.mjs
T
jason.woltjeandClaude Opus 5.5 243e153c8b feat(conversation): CHAT-03 I1, mediated control of a sealed headless Pi (#1507)
Controller, claim store, live-session guard, engine link and seal,
turn tracker, cohort force stop and recovery, client library,
transcript and mediated terminal, with the fake engine and tests.
Fixtures only; no live cutover.

Dewey built it. Darkwing (comment 26690) and Filbert (comment 26694)
approved round 2. Manifest I1-r2-manifest.sha256 (2b48e333, 27 files).
Suites on an export: conversation 152/152, control-board 124, webui 14,
seat 19, chat-00/01/01c checks, and all nine scripts/test-*.sh green.
Follow-ups for I3 are in DEFERRED. Gate E stays with Jason.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-04 15:47:53 -05:00

180 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// CHAT-03 (#1507): the pinned Pi binary, started sealed in a scratch home with
// no credentials, answers the RPC commands the controller sends with the
// shapes the fake engine models. No prompt is sent, so no model is called.
//
// It also records pinned Pi's startup append (sdk.js 240–252): a session
// whose branch has no thinking_level_change entry gains one at every start,
// so the leaf moves after launch and the K8 load check fails closed on it.
//
// Set CHAT03_SMOKE_OUT=<file> to keep the exchange as evidence.
import { test, after } from "node:test";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { PassThrough } from "node:stream";
import { LineSplitter, encodeLine } from "../src/framing.mjs";
import { PI_BIN, PI_VERSION, buildPiArgs, checkEnginePin, checkSeal } from "../src/pi-pin.mjs";
import { FakePi } from "./fake-pi.mjs";
import { REPO, assistantEntry, header, thinkingEntry, userEntry } from "./harness.mjs";
const scratch = mkdtempSync(join(tmpdir(), "chat03-smoke-"));
after(() => rmSync(scratch, { recursive: true, force: true }));
const COMMANDS = ["get_state", "get_commands", "clear_queue", "abort", "get_tree"];
const exchanges = {};
function session(name, entries) {
const dir = join(scratch, "proj", ".pi", "state", "smoke", "sessions");
mkdirSync(dir, { recursive: true });
const file = join(dir, name);
writeFileSync(file, [header(join(scratch, "proj")), ...entries].map((v) => JSON.stringify(v) + "\n").join(""));
return file;
}
// A minimal environment: no inherited variables, so no provider key or token
// can reach Pi; HOME and the agent dir are empty scratch directories.
function scratchEnv(tag) {
const home = join(scratch, `home-${tag}`);
const agent = join(scratch, `agent-${tag}`);
for (const d of [home, agent, join(home, ".config"), join(home, ".cache"), join(home, ".local", "share")]) mkdirSync(d, { recursive: true });
return {
home, agent,
env: {
PATH: process.env.PATH, HOME: home, PI_CODING_AGENT_DIR: agent, PI_OFFLINE: "1", PI_SKIP_VERSION_CHECK: "1", PI_TELEMETRY: "0",
XDG_CONFIG_HOME: join(home, ".config"), XDG_CACHE_HOME: join(home, ".cache"), XDG_DATA_HOME: join(home, ".local", "share"),
},
};
}
// Sends each command after the previous response arrives and records every
// line in order.
async function converse(input, output, { onExit = null } = {}) {
const lines = [];
const waiters = new Set();
const split = new LineSplitter((l) => {
lines.push(JSON.parse(l));
for (const w of [...waiters]) w();
});
output.on("data", (c) => split.push(c));
const reply = (id) => new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error(`no response to ${id}; lines ${JSON.stringify(lines).slice(0, 2000)}`)), 20000);
const check = () => {
const hit = lines.find((v) => v.type === "response" && v.id === id);
if (hit) {
waiters.delete(check);
clearTimeout(t);
resolve(hit);
}
};
waiters.add(check);
onExit?.(() => reject(new Error(`pi exited; lines ${JSON.stringify(lines).slice(0, 2000)}`)));
check();
});
const responses = {};
for (const type of COMMANDS) {
const id = `smoke-${type}`;
input.write(encodeLine({ id, type }));
responses[type] = await reply(id);
}
return { lines, responses };
}
async function realPi(tag, sessionFile) {
const { home, agent, env } = scratchEnv(tag);
assert.equal(existsSync(join(home, ".pi", "agent", "auth.json")), false);
assert.deepEqual(readdirSync(agent), [], "the agent dir starts empty");
const args = buildPiArgs({ sessionFile });
checkSeal(args);
const pi = spawn(process.execPath, [join(REPO, PI_BIN), ...args], { cwd: join(scratch, "proj"), env, stdio: ["pipe", "pipe", "pipe"] });
let stderr = "";
pi.stderr.on("data", (c) => (stderr += c));
const exited = new Promise((r) => pi.on("exit", (code, signal) => r({ code, signal })));
try {
const out = await converse(pi.stdin, pi.stdout, { onExit: (fn) => exited.then(fn) });
return { ...out, stderr, agentFiles: readdirSync(agent).sort() };
} finally {
pi.stdin.end();
pi.kill("SIGTERM");
const t = setTimeout(() => pi.kill("SIGKILL"), 3000);
await exited;
clearTimeout(t);
}
}
function fakePi(sessionFile) {
const input = new PassThrough();
const output = new PassThrough();
new FakePi({ input, output, argv: buildPiArgs({ sessionFile }) });
return converse(input, output);
}
const lastId = (file) => JSON.parse(readFileSync(file, "utf8").trim().split("\n").at(-1)).id;
const typesOf = (data) => Object.fromEntries(Object.entries(data ?? {}).map(([k, v]) => [k, v === null ? "null" : Array.isArray(v) ? "array" : typeof v]));
test("the engine pin holds for the installed package", () => {
assert.deepEqual(checkEnginePin(REPO).version, PI_VERSION);
});
test("pinned Pi, sealed and without credentials, answers the controller's commands with the shapes the fake models", async () => {
const entries = [thinkingEntry(), userEntry("a1b2c3d4", "f0e1d2c3", "hello"), assistantEntry("b2c3d4e5", "a1b2c3d4", "hi")];
const realFile = session("real.jsonl", entries);
const fakeFile = session("fake.jsonl", entries);
const before = readFileSync(realFile, "utf8");
const real = await realPi("real", realFile);
const fake = await fakePi(fakeFile);
exchanges.sealed = real;
assert.equal(readFileSync(realFile, "utf8"), before, "a session that carries a thinking entry is not appended to at start");
assert.deepEqual(real.agentFiles.filter((f) => f !== "auth.json" && f !== "models-store.json"), [], `agent dir: ${real.agentFiles}`);
if (real.agentFiles.includes("auth.json")) assert.deepEqual(JSON.parse(readFileSync(join(scratch, "agent-real", "auth.json"), "utf8")), {}, "no credential was written");
const st = real.responses.get_state;
assert.equal(st.success, true);
assert.equal(st.data.sessionFile, realFile);
assert.equal(st.data.isStreaming, false);
assert.equal(st.data.sessionId, fake.responses.get_state.data.sessionId);
// K8 reads get_tree's leafId; for this session it is the file's last entry.
assert.equal(real.responses.get_tree.data.leafId, lastId(realFile));
assert.equal(real.responses.get_tree.data.leafId, fake.responses.get_tree.data.leafId);
// Sealed, Pi still registers one bundled inline extension command, /llama
// (llama.cpp router). No file extension, template or skill loads. Any slash
// text is refused at admission (S1, S2), so it can't be invoked; this pins
// the set so a change shows here.
const offered = (real.responses.get_commands.data?.commands ?? []).map((c) => `${c.name}:${c.source}:${c.sourceInfo?.source}`);
assert.deepEqual(offered, ["llama:extension:inline"]);
for (const type of ["get_state", "clear_queue", "abort", "get_tree"]) {
const r = real.responses[type], f = fake.responses[type];
assert.equal(r.command, f.command, type);
assert.equal(r.success, f.success, type);
const rt = typesOf(r.data), ft = typesOf(f.data);
for (const [k, t] of Object.entries(ft)) assert.equal(rt[k], t, `${type}.${k}: the fake models a field pinned Pi doesn't send that way`);
}
// clear_queue: one queue_update with both queues empty precedes the response, in both.
for (const side of [real, fake]) {
const i = side.lines.findIndex((v) => v.type === "response" && v.id === "smoke-clear_queue");
const updates = side.lines.slice(0, i).filter((v) => v.type === "queue_update");
assert.deepEqual(updates.at(-1), { type: "queue_update", steering: [], followUp: [] });
}
});
test("pinned Pi appends thinking_level_change at start when the branch lacks one, so the leaf moves (K8 then fails closed)", async () => {
const entries = [userEntry("a1b2c3d4", null, "hello"), assistantEntry("b2c3d4e5", "a1b2c3d4", "hi")];
const realFile = session("bare-real.jsonl", entries);
const fakeFile = session("bare-fake.jsonl", entries);
const real = await realPi("bare", realFile);
const fake = await fakePi(fakeFile);
exchanges.startupAppend = real;
for (const [file, side] of [[realFile, real], [fakeFile, fake]]) {
const added = readFileSync(file, "utf8").trim().split("\n").map((l) => JSON.parse(l)).slice(1 + entries.length);
assert.deepEqual(added.map((e) => e.type), ["thinking_level_change"], file);
assert.equal(added[0].parentId, "b2c3d4e5");
assert.notEqual(side.responses.get_tree.data.leafId, "b2c3d4e5", "the leaf moved off the loaded leaf");
assert.equal(side.responses.get_tree.data.leafId, added[0].id);
}
if (process.env.CHAT03_SMOKE_OUT) writeFileSync(process.env.CHAT03_SMOKE_OUT, JSON.stringify({ pi: PI_VERSION, commands: COMMANDS, exchanges }, null, 2) + "\n");
});