Files
stack/packages/conversation/tests/flows.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

619 lines
29 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 §4, §7 and §9 (#1507): the slash path S1–S7, Pi dialogs P3 and the
// return flow E1–E7, on the in-process fake engine, the library client, the
// Transcript view and the mediated terminal. Every record the controllers
// produce here is checked against the CHAT-01 schema at the end.
import { test, after } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { join } from "node:path";
import { ConversationClient, OUTCOME_UNKNOWN } from "../src/client.mjs";
import { Transcript, RECONCILE_MARKER } from "../src/transcript.mjs";
import { Terminal, NOT_CONTROLLER, visible } from "../src/terminal.mjs";
import { LATER_LINE_SLASH_INTERPRETED, PREFIXES, TEXT_POLICY } from "../src/text-policy.mjs";
import { LineSplitter, encodeLine } from "../src/framing.mjs";
import { replyToRow } from "../../control-board/src/serve.mjs";
import { REPO, started, receiptState, cleanupAll, tick } from "./harness.mjs";
after(() => cleanupAll());
const PASTE_START = "\x1b[200~";
const PASTE_END = "\x1b[201~";
const PAGE = ["user: hello", "assistant: hi"];
const records = [];
async function until(pred, ms = 4000, what = "condition") {
const end = Date.now() + ms;
while (!pred()) {
if (Date.now() > end) throw new Error(`timed out waiting for ${what}`);
await tick(5);
}
}
// A view reads the page once when it attaches to a connected client.
const loaded = (view) => until(() => view.loads >= 1 && !view.loading, 4000, "the first page read");
const prompts = (engine) => engine.commands.filter((c) => c.type === "prompt");
// Every CHAT-01 record a fixture produced, for the schema check.
function collect(h, clients = h.clients) {
records.push(...h.ctrl.events, ...h.ctrl.requests.values());
for (const c of clients) {
records.push(...c.receipts.values());
for (const p of c.pushes) {
for (const key of ["binding", "connection", "confirmation", "stop"]) if (p[key]?.version === 2) records.push(p[key]);
}
}
}
// A client that follows the conversation through a Transcript from its first
// welcome, as a library user or the terminal would.
async function follower(h) {
const client = new ConversationClient({ socketPath: h.ctrl.socketPath });
const view = new Transcript({ client });
await client.connect();
await loaded(view);
return { client, view };
}
async function admitted(client, text) {
const r = await client.prompt(text);
assert.equal(r.outcome, "admitted", JSON.stringify(r));
return r.receipt.id;
}
// Each message exactly once: no line repeats.
function once(lines) {
assert.equal(new Set(lines).size, lines.length, `a message repeats: ${JSON.stringify(lines)}`);
}
// ---- S: the slash path ------------------------------------------------------
test("S1: `/goal x`, with leading spaces or a tab, refuses text-policy at admission; zero engine bytes", async () => {
const h = await started();
try {
for (const text of ["/goal x", " /goal x", "\t/goal x", "\n/goal x"]) {
const before = h.engine.bytes().length;
const r = await h.client.prompt(text);
assert.equal(r.refusal, TEXT_POLICY, JSON.stringify(text));
assert.equal(h.engine.bytes().length, before, `${JSON.stringify(text)} reached the engine`);
}
assert.equal(prompts(h.engine).length, 0);
collect(h);
} finally {
await h.close();
}
});
test("S2: every prefix pinned Pi interprets is refused, from the list the code uses; the rest reach the engine exactly", async () => {
const h = await started();
try {
assert.ok(PREFIXES.some((p) => p.example === "/skill:ms-unslop" && p.interpreted));
for (const p of PREFIXES) {
const before = h.engine.bytes().length;
const r = await h.client.prompt(p.example);
if (p.interpreted) {
assert.equal(r.refusal, TEXT_POLICY, p.prefix);
assert.equal(h.engine.bytes().length, before, `${p.prefix} reached the engine`);
} else {
assert.equal(r.outcome, "admitted", `${p.prefix}: ${JSON.stringify(r)}`);
await receiptState(h.client, r.receipt.id, "finished");
assert.equal(prompts(h.engine).at(-1).message, p.example);
}
}
assert.equal(prompts(h.engine).length, PREFIXES.filter((p) => !p.interpreted).length);
collect(h);
} finally {
await h.close();
}
});
test("S3: `/goal` on the second line is pinned from the source: Pi checks only index 0, so it is admitted and sent exactly", async () => {
const h = await started();
try {
const text = "please note\n/goal x";
const r = await h.client.prompt(text);
if (LATER_LINE_SLASH_INTERPRETED) {
assert.equal(r.refusal, TEXT_POLICY);
} else {
assert.equal(r.outcome, "admitted", JSON.stringify(r));
await receiptState(h.client, r.receipt.id, "finished");
assert.equal(prompts(h.engine).at(-1).message, text);
}
collect(h);
} finally {
await h.close();
}
});
test("S4: a `/` left in the composer is cleared when control transfers and returns; the next submit sends only the new text", async () => {
const h = await started({ clients: 2 });
try {
const [mine, other] = h.clients;
const term = new Terminal({ client: mine });
await term.key("/");
assert.equal(term.composer, "/");
await until(() => other.binding?.controllerGeneration === h.ctrl.binding.controllerGeneration, 2000, "the binding push");
assert.equal((await other.takeover()).outcome, "transferred");
await until(() => !mine.isController, 2000, "the transfer to reach the terminal");
assert.equal(term.composer, "", "the composer clears on transfer");
await term.key("\x14");
await until(() => mine.isController, 2000, "control back");
await term.key("hello");
await term.key("\r");
await until(() => prompts(h.engine).length === 1, 2000, "the prompt");
assert.equal(prompts(h.engine)[0].message, "hello");
const line = h.engine.bytes().toString("utf8").split("\n").find((l) => l.includes('"type":"prompt"'));
assert.equal(JSON.parse(line).message, "hello", "the engine's exact bytes");
assert.equal(term.composer, "");
await receiptState(mine, term.lastReceipt.id, "finished");
collect(h);
} finally {
await h.close();
}
});
test("S5: an observer terminal gets a paste then Enter, as send-message.sh does: not admitted: controller, nothing sent", async () => {
const h = await started({ clients: 2 });
try {
const observer = h.clients[1];
let sends = 0;
const send = observer.send.bind(observer);
observer.send = (...a) => {
if (a[0].command.operation !== "observe") sends += 1;
return send(...a);
};
const term = new Terminal({ client: observer });
const before = h.engine.bytes().length;
await term.key(`${PASTE_START}run the deploy\n/goal x${PASTE_END}`);
assert.equal(term.composer, "run the deploy\n/goal x", "a paste is literal and never submits by itself");
const r = await term.submit();
await term.key("\r");
assert.deepEqual(r, { sent: false, refusal: "controller" });
assert.equal(term.status, NOT_CONTROLLER);
assert.ok(term.frame.includes(NOT_CONTROLLER));
assert.equal(sends, 0, "the client sent nothing but its page read");
assert.equal(h.engine.bytes().length, before, "zero engine bytes");
assert.equal(h.ctrl.requests.size, 0);
collect(h);
} finally {
await h.close();
}
});
test("S6: a mediated-shaped registration (no tmux) passed to the board's replyToRow: 409 no tmux session; exec never runs", () => {
const calls = [];
const exec = (...a) => {
calls.push(a);
return { status: 0, stdout: "", stderr: "" };
};
const index = { sessions: [{ project: "proj", agent: "mediated-seat", registered: { pid: process.pid, alive: true, tmux: null, mediated: { socket: "/nonexistent/control.sock" } } }] };
const r = replyToRow({ index, key: "proj/mediated-seat", text: "hello", exec });
assert.equal(r.status, 409);
assert.match(r.body.error, /no tmux session/);
assert.equal(calls.length, 0);
});
test("S7: ESC, bracketed-paste markers and U+2028/U+2029 travel as one JSON string; the engine receives the exact text in one record", async () => {
const h = await started();
try {
const text = `a\x1b[31mred\x1b[0m ${PASTE_START}pasted${PASTE_END} b
c
d\r\ne`;
const id = await admitted(h.client, text);
await receiptState(h.client, id, "finished");
assert.equal(prompts(h.engine).length, 1);
assert.equal(prompts(h.engine)[0].message, text);
const lines = [];
const split = new LineSplitter((l) => lines.push(l));
split.push(h.engine.bytes());
const records = lines.filter((l) => l.includes('"type":"prompt"'));
assert.equal(records.length, 1, "no record split");
assert.equal(JSON.parse(records[0]).message, text);
collect(h);
} finally {
await h.close();
}
});
// ---- P3: Pi dialogs ---------------------------------------------------------
test("P3: a Pi confirm, select, input or editor dialog is shown disabled with a reason and never answered", async () => {
const h = await started();
try {
const term = new Terminal({ client: h.client });
for (const method of ["confirm", "select", "input", "editor"]) h.engine.dialog(method);
await until(() => term.dialogs.length === 4, 2000, "four dialogs");
for (const d of term.dialogs) {
assert.equal(d.disabled, true);
assert.match(d.reason, /not answered/);
assert.ok(term.frame.some((l) => l.startsWith(`[dialog ${d.method} disabled: `)), d.method);
}
await tick(100);
assert.equal(h.engine.commands.filter((c) => c.type === "extension_ui_response").length, 0, "no response is sent");
assert.equal(h.ctrl.evidence.dialogs.length, 4);
collect(h);
} finally {
await h.close();
}
});
// ---- E: the return flow -----------------------------------------------------
test("E1: send, ack, user, toolCall, toolResult, final answer: shown once, no refresh, draft and reading position kept", async () => {
const h = await started({ clients: 2 });
try {
const lib = new Transcript({ client: h.client });
await loaded(lib);
const term = new Terminal({ client: h.clients[1], rows: 8 });
await loaded(term.transcript);
assert.equal(term.transcript.reconcile, false, "an idle engine gives a quiet cut");
await term.key("half-typed draft");
term.scroll = 1;
h.engine.script([{ tool: { id: "call-e1", name: "bash", args: { cmd: "ls" }, result: "file.txt" } }, { text: "final answer", stop: "stop" }]);
const id = await admitted(h.client, "list the files");
await receiptState(h.client, id, "finished");
await until(() => term.transcript.events.some((e) => e.type === "run-settled"), 2000, "the settle");
const want = [...PAGE, "user: list the files", 'assistant: [tool-call bash {"cmd":"ls"}]', "tool: [tool-result] file.txt", "assistant: final answer"];
assert.deepEqual(term.transcript.lines(), want);
assert.deepEqual(lib.lines(), want);
assert.equal(term.transcript.loads, 1, "no refresh");
assert.equal(lib.loads, 1, "no refresh");
assert.equal(term.composer, "half-typed draft", "the draft is kept");
assert.equal(term.scroll, 1, "the reading position is kept");
collect(h);
} finally {
await h.close();
}
});
test("E2: U+2028, U+2029 inside JSON strings and CRLF line ends each parse as one record, on the splitter and through the controller", async () => {
const lines = [];
const split = new LineSplitter((l) => lines.push(l));
const a = JSON.stringify({ type: "x", text: "one
two
three" });
const b = JSON.stringify({ type: "y", text: "crlf" });
const bytes = Buffer.from(`${a}\r\n${b}\n`);
for (let i = 0; i < bytes.length; i += 7) split.push(bytes.subarray(i, i + 7));
assert.deepEqual(lines.map((l) => JSON.parse(l).type), ["x", "y"]);
assert.equal(JSON.parse(lines[0]).text, "one
two
three");
const h = await started();
try {
const text = "line
sep
para";
h.engine.script([
{ raw: JSON.stringify({ type: "message_start", message: { role: "assistant", content: [] } }) + "\r\n" },
{ raw: JSON.stringify({ type: "message_end", message: { role: "assistant", content: [{ type: "text", text }], stopReason: "stop" } }) + "\r\n" },
]);
const id = await admitted(h.client, "go");
await receiptState(h.client, id, "finished");
const end = h.ctrl.events.find((e) => e.type === "message-end" && e.role === "assistant");
assert.equal(end.content[0].text, text);
assert.equal(Object.keys(h.ctrl.evidence.unknownEvents).length, 0);
assert.equal(h.ctrl.evidence.dropped.lines, 0);
collect(h);
} finally {
await h.close();
}
});
// Synthetic events for the Transcript alone.
const target = { conversation: "pi-c", branch: "main", execution: "exec-1", controllerGeneration: 1 };
const ev = (sequence, type, fields = {}, streamEpoch = "E1") => ({
version: 2, kind: "event", id: `${streamEpoch}.${sequence}`, target, sequence, streamEpoch, request: null, entry: null, type, contentIndex: null,
updateMode: "none", content: [], visibility: "permitted-visible", createdAt: "2026-10-04T12:00:00.000Z", stop: null, message: null, part: null, lastPart: null, role: null, ...fields,
});
const text = (t, block = 0) => ({ type: "text", text: t, block, fragment: 0, lastFragment: true });
const end = (sequence, message, part, lastPart, content, epoch) => ev(sequence, "message-end", { message, role: "assistant", entry: `${message}.e`, part, lastPart, updateMode: "replace", content }, epoch);
test("E3: a multipart final, two blocks, null request correlation and duplicate delivery", async () => {
const v = new Transcript();
v.load([], { streamEpoch: "E1", fromSequence: 1, quiet: true });
const stream = [
ev(1, "message-start", { message: "m1", role: "assistant" }),
end(2, "m1", 0, false, [text("part zero")]),
end(3, "m1", 1, true, [text("part one", 1)]),
ev(4, "message-start", { message: "m2", role: "assistant" }),
end(5, "m2", 0, true, [text("block A", 0), text("block B", 1)]),
];
for (const e of stream) {
assert.equal(v.event(e), null);
assert.equal(v.event(structuredClone(e)), null, "an identical repeat is dropped");
}
assert.ok(stream.every((e) => e.request === null));
assert.deepEqual(v.lines(), ["assistant: part zero\npart one", "assistant: block A\nblock B"]);
assert.equal(v.reconcile, false);
assert.equal(v.event({ ...stream[4], content: [text("changed")] }), "conflict", "a repeated ID with other bytes reconciles");
assert.equal(v.reconcile, true);
// Live: a final message of 70 blocks crosses the 64-block part limit.
const h = await started();
try {
const blocks = Array.from({ length: 70 }, (_, i) => ({ type: "text", text: `b${i}` }));
h.engine.script([{ emit: { type: "message_start", message: { role: "assistant", content: [] } } }, { emit: { type: "message_end", message: { role: "assistant", content: blocks, stopReason: "stop" } } }]);
const { view } = await follower(h);
const id = await admitted(h.client, "many blocks");
await receiptState(h.client, id, "finished");
const ends = h.ctrl.events.filter((e) => e.type === "message-end" && e.role === "assistant");
assert.deepEqual(ends.map((e) => [e.part, e.lastPart]), [[0, false], [1, true]]);
await until(() => view.events.some((e) => e.type === "run-settled"), 2000, "the settle");
assert.equal(view.lines().at(-1), "assistant: " + blocks.map((b) => b.text).join("\n"));
collect(h);
} finally {
await h.close();
}
});
test("E4: a page read after message_end but before its entry is persisted: marker at the seam, re-read after run-settled, each message once", async () => {
const h = await started();
try {
h.engine.holdPersist();
h.engine.script([{ text: "one", stop: "stop" }, { pause: "p1" }, { text: "two", stop: "stop" }]);
h.engine.arm("p1");
const id = await admitted(h.client, "count");
await h.engine.waitPaused("p1");
const { client, view } = await follower(h);
assert.equal(view.seam.replay, "unavailable");
assert.equal(view.seam.quiet, false);
assert.equal(view.reconcile, true);
assert.deepEqual(view.lines(), [...PAGE, RECONCILE_MARKER], "the unpersisted messages are missing, and the seam says so");
h.engine.flushPersist();
h.engine.resume("p1");
await receiptState(h.client, id, "finished");
await until(() => view.loads >= 2 && !view.loading && !view.reconcile, 4000, "the re-read after run-settled");
assert.equal(view.lastReload, "settled");
const lines = view.lines();
assert.deepEqual(lines, [...PAGE, "user: count", "assistant: one", "assistant: two"]);
once(lines);
client.close();
collect(h);
} finally {
await h.close();
}
});
test("E4: a gap or a new epoch also reconciles; nothing is concatenated across a gap", async () => {
const entry = (id, role, t) => ({ version: 2, kind: "entry", id: `n.${id}:0`, message: id, role, content: [text(t)], part: 0, lastPart: true });
let page = { entries: [], seam: { replay: "unavailable", streamEpoch: "E1", fromSequence: 1, quiet: true } };
const listeners = new Set();
const stub = {
observes: 0,
on: (fn) => listeners.add(fn),
observe: async () => {
stub.observes += 1;
return { outcome: "observing", data: { page: { entries: page.entries, hasMore: false, nextCursor: null }, seam: page.seam } };
},
};
const push = (event) => listeners.forEach((fn) => fn({ type: "push", kind: "event", event }));
const v = new Transcript({ client: stub });
await v.reload("initial");
push(ev(1, "message-start", { message: "m1", role: "assistant" }));
push(ev(2, "text-delta", { message: "m1", role: "assistant", content: [text("hel")] }));
page = { entries: [entry("x1", "assistant", "hello world")], seam: { replay: "unavailable", streamEpoch: "E1", fromSequence: 5, quiet: true } };
push(ev(4, "text-delta", { message: "m1", role: "assistant", content: [text("WORLD")] }));
assert.equal(v.reason, "gap");
assert.ok(!v.lines().some((l) => l.includes("WORLD")), "no concatenation across the gap");
await until(() => !v.loading && !v.reconcile, 2000, "the re-read after the gap");
assert.deepEqual(v.lines(), ["assistant: hello world"]);
page = { entries: page.entries, seam: { replay: "unavailable", streamEpoch: "E2", fromSequence: 2, quiet: true } };
push(ev(1, "message-start", { message: "n1", role: "assistant" }, "E2"));
assert.equal(v.reason, "epoch");
push(ev(2, "message-start", { message: "n2", role: "assistant" }, "E2"));
push(end(3, "n2", 0, true, [text("after the epoch")], "E2"));
await until(() => !v.loading && !v.reconcile, 2000, "the re-read after the epoch");
const lines = v.lines();
assert.deepEqual(lines, ["assistant: hello world", "assistant: after the epoch"]);
once(lines);
assert.equal(stub.observes, 3, "one read per reconcile, none repeated");
});
test("E5: an unknown native event gives no client event; evidence records its type and bytes; the terminal count goes up", async () => {
const h = await started();
try {
const term = new Terminal({ client: h.client });
const events = h.client.events.length;
const line = { type: "brand_new_event", payload: "x".repeat(40) };
h.engine.emit(line);
await until(() => term.unknownCount === 1, 2000, "the count");
h.engine.emit({ type: "another_new_one" });
await until(() => term.unknownCount === 2, 2000, "the second count");
assert.equal(h.client.events.length, events, "no client event");
assert.equal(h.ctrl.evidence.unknownEvents.brand_new_event.count, 1);
assert.equal(h.ctrl.evidence.unknownEvents.brand_new_event.bytes, Buffer.byteLength(JSON.stringify(line)));
assert.match(term.frame[0], /unknown events: 2/);
collect(h);
} finally {
await h.close();
}
});
test("E6: a tool result delayed across a pause and a reconnect is reconciled without a manual refresh", async () => {
const h = await started();
try {
h.engine.script([{ tool: { id: "call-e6", name: "bash", args: {}, result: "late result", hold: true } }, { pause: "p2" }, { text: "after", stop: "stop" }]);
h.engine.arm("tool:call-e6");
h.engine.arm("p2");
const { client, view } = await follower(h);
const id = await admitted(h.client, "slow tool");
await h.engine.waitPaused("tool:call-e6");
await until(() => view.lines().some((l) => l.includes("[tool bash running]")), 2000, "the running call");
client.close();
await until(() => client.closed, 2000, "the disconnect");
h.engine.resume("tool:call-e6");
await h.engine.waitPaused("p2");
await client.connect();
await until(() => view.loads >= 2 && !view.loading, 2000, "the read on reconnect");
h.engine.resume("p2");
await receiptState(h.client, id, "finished");
await until(() => view.lastReload === "settled" && !view.loading && !view.reconcile, 4000, "the settled re-read");
const lines = view.lines();
assert.deepEqual(lines, [...PAGE, "user: slow tool", "assistant: [tool-call bash {}]", "tool: [tool-result] late result", "assistant: after"]);
once(lines);
assert.notEqual(view.lastReload, "manual");
client.close();
collect(h);
} finally {
await h.close();
}
});
test("E7: the terminal renders the same stream as the library client, as observer and then as controller, and submits only as controller", async () => {
const h = await started({ clients: 2 });
try {
const lib = new Transcript({ client: h.client });
await loaded(lib);
const term = new Terminal({ client: h.clients[1], rows: 40 });
await loaded(term.transcript);
const body = () => term.frame.slice(1, 1 + term.transcript.lines().join("\n").split("\n").length);
const id = await admitted(h.client, "first");
await receiptState(h.client, id, "finished");
await until(() => term.transcript.events.some((e) => e.type === "run-settled"), 2000, "settle at the terminal");
assert.deepEqual(term.transcript.lines(), lib.lines());
term.render();
assert.deepEqual(body(), lib.lines().join("\n").split("\n").map(visible));
await term.key("blocked\r");
assert.equal(term.status, NOT_CONTROLLER);
assert.equal(prompts(h.engine).length, 1);
term.composer = "";
await term.key("\x14");
await until(() => h.clients[1].isController, 2000, "terminal control");
await term.key("second\r");
await until(() => prompts(h.engine).length === 2, 2000, "the terminal's prompt");
assert.equal(prompts(h.engine)[1].message, "second");
await receiptState(h.client, term.lastReceipt.id, "finished");
await until(() => term.transcript.events.filter((e) => e.type === "run-settled").length === 2 && lib.events.filter((e) => e.type === "run-settled").length === 2, 2000, "both settles");
assert.deepEqual(term.transcript.lines(), lib.lines());
assert.deepEqual(term.transcript.lines().slice(-2), ["user: second", "assistant: ok"]);
collect(h);
} finally {
await h.close();
}
});
test("terminal: engine control characters are made visible; a lost connection refuses submit", async () => {
const h = await started();
try {
const term = new Terminal({ client: h.client });
h.engine.script([{ text: "\x1b]52;c;ZXZpbA==\x07 bell‮", stop: "stop" }]);
await term.key("go\r");
await receiptState(h.client, term.lastReceipt.id, "finished");
await until(() => term.transcript.lines().some((l) => l.includes("bell")), 2000, "the reply");
const shown = term.frame.find((l) => l.includes("bell"));
assert.equal(shown, "assistant: ^[]52;c;ZXZpbA==^G bell<U+202E>");
assert.ok(!term.frame.join("").includes("\x1b"), "no raw ESC in the frame");
h.client.close();
await until(() => h.client.closed, 2000, "the disconnect");
assert.match(term.status, /disconnected/);
term.composer = "x";
const r = await term.submit();
assert.equal(r.reply.refusal, "channel");
assert.equal(prompts(h.engine).length, 1);
} finally {
await h.close();
}
});
test("terminal: outcome unknown is shown as such, with no resend offer, and nothing is resent", async () => {
const listeners = new Set();
let calls = 0;
const stub = {
closed: false, connection: { id: "conn-1" }, binding: { state: "active", controllerConnection: "conn-1" }, isController: true,
on: (fn) => listeners.add(fn),
observe: async () => ({ outcome: "refused:channel", refusal: "channel" }),
prompt: async () => {
calls += 1;
return { outcome: "outcome-unknown", refusal: null, display: OUTCOME_UNKNOWN, reason: "disconnected", request: null, receipt: null };
},
};
const term = new Terminal({ client: stub });
await term.key("held\r");
assert.equal(term.status, `prompt: ${OUTCOME_UNKNOWN}`);
for (const fn of listeners) fn({ type: "outcome-unknown", request: "req-1", operation: "prompt", reason: "disconnected", display: OUTCOME_UNKNOWN });
for (const fn of listeners) fn({ type: "push", kind: "receipt", receipt: { id: "receipt-1", state: "admitted" }, outcomeUnknown: true });
assert.ok(term.notices.includes(`prompt: ${OUTCOME_UNKNOWN}`));
assert.ok(term.notices.includes(`prompt receipt-1: ${OUTCOME_UNKNOWN}`));
await term.key("\r");
assert.equal(calls, 1, "an empty Enter sends nothing; nothing is resent");
assert.equal(term.composer, "");
assert.ok(!term.frame.some((l) => /resend|retry/i.test(l)));
});
// A controller-side stub that records every prompt.
function promptStub() {
const sent = [];
const stub = {
closed: false, connection: { id: "conn-1" }, binding: { state: "active", controllerConnection: "conn-1" }, isController: true,
on: () => {},
observe: async () => ({ outcome: "refused:channel", refusal: "channel" }),
prompt: async (text) => (sent.push(text), { outcome: "admitted", refusal: null, receipt: { id: `receipt-${sent.length}`, state: "admitted" } }),
};
return { stub, sent };
}
test("terminal: text after Enter in the same input chunk starts the next message; it never joins the one submitted", async () => {
{
const { stub, sent } = promptStub();
const term = new Terminal({ client: stub });
await term.key("first\rsecond\r");
assert.deepEqual(sent, ["first", "second"]);
assert.equal(term.composer, "");
}
{
const { stub, sent } = promptStub();
const term = new Terminal({ client: stub });
await term.key("abc\rdef");
assert.deepEqual(sent, ["abc"]);
assert.equal(term.composer, "def", "the rest waits in the composer");
}
});
test("terminal: a paste-start marker split right after its ESC still opens the paste; the Enter inside it never submits", async () => {
for (const cut of [1, 2, 3, 4, 5]) {
const { stub, sent } = promptStub();
const term = new Terminal({ client: stub });
const input = `${PASTE_START}a\rb${PASTE_END}`;
await term.key(input.slice(0, cut));
await term.key(input.slice(cut));
assert.deepEqual(sent, [], `cut ${cut}: nothing sent`);
assert.equal(term.composer, "a\rb", `cut ${cut}: the paste is literal`);
}
// A lone Escape has no action: it waits, then is consumed with what follows.
const { stub, sent } = promptStub();
const term = new Terminal({ client: stub });
await term.key("x\x1b");
await term.key("\r");
assert.deepEqual(sent, [], "ESC then CR is Alt-Enter, a newline");
assert.equal(term.composer, "x\n");
});
test("terminal: invisible and bidi characters are made visible; head, status and notice lines stay one line", () => {
assert.equal(visible("a\u061cb\u200bc\u2060d\ufeffe\u{e0041}f"), "a<U+061C>b<U+200B>c<U+2060>d<U+FEFF>e<U+E0041>f");
assert.equal(visible("\u{1F469}\u200d\u{1F4BB}"), "\u{1F469}\u200d\u{1F4BB}", "ZWJ sequences pass");
const { stub } = promptStub();
const term = new Terminal({ client: stub });
term.status = "one\ntwo";
term.notices.push("three\nfour");
term.render();
assert.ok(term.frame.includes("one^Jtwo"), "status");
assert.ok(term.frame.includes("three^Jfour"), "notice");
});
test("every record these fixtures produced is a valid CHAT-01 record (E5: no record fails the schema)", () => {
assert.ok(records.length > 100, `only ${records.length} records`);
const kinds = [...new Set(records.map((r) => r.kind))].sort();
for (const k of ["binding", "connection", "event", "receipt", "request"]) assert.ok(kinds.includes(k), `no ${k} records`);
const script = `
import json, sys
from jsonschema import Draft202012Validator, FormatChecker
s = json.load(open(sys.argv[1]))
bad = []
for i, r in enumerate(json.load(sys.stdin)):
v = Draft202012Validator({"$defs": s["$defs"], "$ref": "#/$defs/" + r["kind"]}, format_checker=FormatChecker())
for e in v.iter_errors(r):
bad.append(f"{i} {r['kind']}: {e.message[:200]}")
break
print(json.dumps(bad))
`;
const run = spawnSync("python3", ["-c", script, join(REPO, "docs", "plans", "chat-01", "contracts.schema.json")], { input: JSON.stringify(records), encoding: "utf8", maxBuffer: 1 << 30 });
assert.equal(run.status, 0, run.stderr);
assert.deepEqual(JSON.parse(run.stdout), [], `kinds checked: ${kinds.join(", ")}`);
});