Files
stack/packages/conversation/tests/reader.test.mjs
T
jason.woltjeandClaude Opus 5.5 a5beb6d97d feat(conversation): CHAT-02 read-only Pi history reader and two board routes (#1507)
packages/conversation is a library with no server: safe-fs, the Pi session
parser, CHAT-01 pages, pinned snapshots, cursors and follow. The control
board adds GET /api/conversations and /api/conversation behind the Host
and Origin guard. Both are read-only, their queries are validated, and
each refusal code maps to a status.

Dewey authored it (packet 0cf177b1, revision 2). Filbert reviewed the code:
R1 revise (branch ids moving on append, the assumed-link bridge merging
branches, one unreadable seat directory turning the catalogue into a 500),
then R2 approve (3b14d66c). Darkwing reviewed the routes: R1 approve
(07b10ad1), R2 approve (b9d92003). The package lands with the routes,
because serve.mjs imports the reader at load.

On an index export: the eight suites 24/90/43/17/14/15/63/18,
conversation and control-board 153/153, webui 9/9.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 16:36:20 -05:00

774 lines
42 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.
// packages/conversation (#1507, CHAT-02): fixtures F1–F15 and F17 from the
// brief (agents/dewey/work/chat-02/BRIEF.md §2.1). F16 is in the control-board
// suite, which serves these routes. Every reader call runs inside noWrites(),
// which fingerprints the fixture tree before and after (F17). Every page and
// cursor is checked against the CHAT-01 schema at the end.
import { test, after } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, readdirSync, rmSync, chmodSync, symlinkSync, lstatSync, renameSync, openSync, writeSync, closeSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve, relative } from "node:path";
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { rootsFromSpecs, createReader, conversationId, cwdInProject, UNSUPPORTED_HARNESS } from "../src/reader.mjs";
import { fragments, safeId, LIMITS } from "../src/parts.mjs";
const repoRoot = resolve(import.meta.dirname, "..", "..", "..");
const schemaPath = join(repoRoot, "docs", "plans", "chat-01", "contracts.schema.json");
const tmp = mkdtempSync(join(tmpdir(), "conversation-"));
const records = [];
after(() => {
// chmod 000 fixtures would block removal
spawnSync("chmod", ["-R", "u+rwx", tmp]);
rmSync(tmp, { recursive: true, force: true });
});
let fixtureCount = 0;
const time = (i) => new Date(Date.UTC(2026, 8, 26, 12, 0, 0) + i * 1000).toISOString();
// A project with one seat's sessions directory.
function project(seat = "pi-seat") {
const base = join(tmp, `f${++fixtureCount}`);
const proj = join(base, "proj");
const dir = join(proj, ".pi", "state", seat, "sessions");
mkdirSync(dir, { recursive: true });
return { base, proj, dir, seat, spec: { agent: seat, project: "proj", sessionsDir: dir } };
}
const header = (proj, extra = {}) => ({ type: "session", version: 3, id: "0f5e1c2a-1111-4222-8333-944455556666", timestamp: time(0), cwd: proj, ...extra });
const msg = (id, parentId, role, content, extra = {}, i = 1) => ({ type: "message", id, parentId, timestamp: time(i), message: { role, content, ...extra } });
const user = (id, parentId, text, i) => msg(id, parentId, "user", [{ type: "text", text }], {}, i);
const assistant = (id, parentId, text, i) => msg(id, parentId, "assistant", [{ type: "text", text }], { stopReason: "stop" }, i);
const lines = (...values) => values.map((v) => (typeof v === "string" ? v : JSON.stringify(v)) + "\n").join("");
function writeSession(dir, name, head, entries, trailing = "") {
const path = join(dir, name);
writeFileSync(path, lines(head, ...entries) + trailing);
return path;
}
// Fingerprint of every file and directory under `root`, symlinks not followed.
function fingerprint(root) {
const out = {};
const walk = (path) => {
const st = lstatSync(path, { bigint: true });
const rec = { dev: String(st.dev), ino: String(st.ino), mtime: String(st.mtimeNs), size: String(st.size), mode: String(st.mode) };
if (st.isDirectory()) {
rec.list = st.mode & 0o400n ? readdirSync(path).sort() : null;
for (const n of rec.list ?? []) walk(join(path, n));
} else if (st.isFile() && st.mode & 0o400n) {
rec.sha256 = createHash("sha256").update(readFileSync(path)).digest("hex");
}
out[path] = rec;
};
walk(root);
return out;
}
// F17: a reader operation leaves every file and listing in `root` unchanged.
function noWrites(root, fn) {
const before = fingerprint(root);
const result = fn();
assert.deepEqual(fingerprint(root), before, "a reader operation changed the fixture tree");
for (const key of ["page", "cursor", "follow"]) if (result?.[key]) records.push(result[key]);
return result;
}
function readerFor(fx, opts = {}) {
return createReader({ roots: rootsFromSpecs([fx.spec], opts.registrations ?? []), ...opts });
}
function openOne(fx, reader, name = "s.jsonl", args = {}) {
return noWrites(fx.base, () => reader.open({ conversation: conversationId(rootsFromSpecs([fx.spec])[0], name), ...args }));
}
// Every page from an open, following next cursors.
function readAll(fx, reader, first) {
const pages = [first];
let r = first;
while (r.ok && r.page.hasMore) {
const page = r.page;
r = noWrites(fx.base, () => reader.next({ cursor: page.nextCursor, conversation: page.conversation, branch: page.branch }));
assert.equal(r.ok, true, JSON.stringify(r.refusal));
pages.push(r);
}
return pages;
}
const texts = (page) => page.entries.map((e) => e.content.map((b) => b.text ?? b.summary ?? b.argumentsText ?? "").join(""));
const roles = (page) => page.entries.map((e) => e.role);
test("a plain conversation: catalogue row, one page, CHAT-01 records", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("a1", null, "hello", 1),
{ type: "session_info", id: "a2", parentId: "a1", timestamp: time(2), name: "Greeting" },
assistant("a3", "a2", "hi there", 3),
{ type: "model_change", id: "a4", parentId: "a3", timestamp: time(4), provider: "x", modelId: "y" },
]);
const reader = readerFor(fx);
const cat = noWrites(fx.base, () => reader.catalogue());
assert.equal(cat.conversations.length, 1);
const row = cat.conversations[0];
assert.equal(row.title, "Greeting");
assert.equal(row.availability, "available");
assert.equal(row.readOnly, true);
assert.equal(row.conversationCreatedAt, time(0));
assert.equal(row.lastActivityAt, time(4));
assert.equal(row.engineStartedAt, null);
const r = openOne(fx, reader);
assert.equal(r.ok, true);
assert.deepEqual(roles(r.page), ["user", "assistant"]);
assert.deepEqual(texts(r.page), ["hello", "hi there"]);
assert.equal(r.page.hasMore, false);
assert.equal(r.page.nextCursor, null);
assert.equal(r.cursor, null);
assert.ok(r.follow, "the last page carries a follow cursor");
assert.equal(r.page.branch, "main");
assert.equal(r.view.defaultBranch, "main");
assert.equal(r.page.entries[1].parent, "a2");
assert.equal(r.page.readOnly, true);
});
test("native entries map to blocks: tools, thinking, bash, notices, ids that do not fit", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("b1", null, "run it", 1),
msg("b2", "b1", "assistant", [
{ type: "thinking", thinking: "plan" },
{ type: "thinking", thinking: "" },
{ type: "toolCall", id: "call_x|fc_y", name: "bash", arguments: { command: "ls" } },
{ type: "thinking", thinking: "[Reasoning redacted]", redacted: true, thinkingSignature: "opaque" },
], { stopReason: "toolUse" }, 2),
msg("b3", "b2", "toolResult", [{ type: "text", text: "a" }, { type: "image", data: "AAAA", mimeType: "image/png" }, { type: "text", text: "b" }], { toolCallId: "call_x|fc_y", toolName: "bash", isError: false }, 3),
{ type: "message", id: "b4", parentId: "b3", timestamp: time(4), message: { role: "bashExecution", command: "pwd", output: "/p", exitCode: 1, cancelled: false } },
{ type: "custom_message", id: "b5", parentId: "b4", timestamp: time(5), customType: "x", content: "shown", display: true },
{ type: "custom_message", id: "b6", parentId: "b5", timestamp: time(6), customType: "x", content: "hidden", display: false },
msg("b7", "b6", "assistant", [{ type: "text", text: "partial" }], { stopReason: "error", errorMessage: "overloaded" }, 7),
{ type: "future_kind", id: "b8", parentId: "b7", timestamp: time(8) },
]);
const r = openOne(fx, readerFor(fx));
assert.equal(r.ok, true);
assert.deepEqual(roles(r.page), ["user", "assistant", "tool", "tool", "notice", "assistant", "notice", "notice"]);
const [, a, tool, bash, shown, , end, unknown] = r.page.entries;
assert.deepEqual(a.content.map((b) => [b.type, b.visibility ?? null]), [["thinking", "permitted-visible"], ["thinking", "unavailable"], ["tool-call", null], ["thinking", "unavailable"]]);
assert.equal(a.content[3].text, "", "redacted reasoning shows no placeholder");
assert.equal(a.content[2].call, safeId("call_x|fc_y"));
assert.match(a.content[2].call, /^h-[0-9a-f]{40}$/);
assert.equal(a.content[2].argumentsText, '{"command":"ls"}');
assert.deepEqual(tool.content.map((b) => b.type), ["tool-result", "attachment"]);
assert.equal(tool.content[0].text, "a\nb");
assert.equal(tool.content[0].call, a.content[2].call);
assert.deepEqual(bash.content.map((b) => b.type), ["tool-call", "tool-result"]);
assert.equal(bash.content[1].isError, true);
assert.equal(shown.content[0].text, "shown");
assert.match(end.content[0].text, /overloaded/);
assert.match(unknown.content[0].text, /future_kind/);
assert.equal(new Set(r.page.entries.map((e) => e.id)).size, r.page.entries.length, "entry ids are unique");
});
test("F1: a malformed line is an unavailable part at its position, and reading continues", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("c1", null, "one", 1),
assistant("c2", "c1", "two", 2),
'{"type":"message","id":"c3", broken',
user("c4", "c2", "four", 4),
"[1,2,3]",
assistant("x.line-6", "c4", "five", 5), // a native id shaped like a notice id
]);
const r = openOne(fx, readerFor(fx));
assert.equal(r.ok, true);
assert.deepEqual(roles(r.page), ["user", "assistant", "notice", "user", "notice", "assistant"]);
assert.equal(new Set(r.page.entries.map((e) => e.id)).size, 6, "native ids never collide with notice ids");
assert.match(texts(r.page)[2], /^Line 4 could not be read/);
assert.match(texts(r.page)[4], /^Line 6 could not be read/);
assert.equal(r.view.unreadableLines, 2);
const cat = noWrites(fx.base, () => readerFor(fx).catalogue());
assert.equal(cat.conversations[0].availability, "available");
});
test("F1: a missing parent stops the history with a notice that names the unreadable lines", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("d1", null, "one", 1),
"garbage",
assistant("d3", "d2", "three", 3),
]);
const reader = readerFor(fx);
const r = openOne(fx, reader);
assert.deepEqual([roles(r.page), r.page.branch], [["notice", "assistant"], "b.d3"]);
assert.match(texts(r.page)[0], /missing from the file\. Line 3 could not be read and may have held it\.$/);
// The history before the gap is its own branch, and the line shows there too.
const main = openOne(fx, reader, "s.jsonl", { branch: "main" });
assert.deepEqual([roles(main.page), texts(main.page)[0]], [["user", "notice"], "one"]);
assert.match(texts(main.page)[1], /^Line 3 could not be read\. It may belong to this branch or another one\.$/);
const fx2 = project();
writeSession(fx2.dir, "s.jsonl", header(fx2.proj), [user("e1", null, "one", 1), assistant("e3", "e2", "three", 3)]);
const r2 = openOne(fx2, readerFor(fx2));
assert.deepEqual(roles(r2.page), ["notice", "assistant"], "no unreadable line: history stops with a notice");
assert.equal(texts(r2.page)[0], "History before this point is unavailable: an earlier entry is missing from the file.");
});
test("F1: an unreadable fork is never merged into another branch's history", () => {
// Line 4 held X, a fork from a; y is X's child.
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("a", null, "root", 1),
assistant("b", "a", "ON THE OTHER BRANCH", 2),
'{"type":"message","id":"X","parentId":"a",',
user("y", "X", "leaf", 4),
]);
const reader = readerFor(fx);
const r = openOne(fx, reader);
assert.deepEqual(r.view.branches.map((b) => [b.branch, b.isDefault]), [["main", false], ["b.y", true]]);
assert.deepEqual(roles(r.page), ["notice", "user"]);
assert.match(texts(r.page)[0], /Line 4 could not be read and may have held it/);
assert.ok(!texts(r.page).some((t) => /ON THE OTHER BRANCH|^root$/.test(t)), "no text from the other branch");
});
test("F1: a follow stays on its branch when the next entry's parent is unreadable", () => {
const fx = project();
const path = writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("t1", null, "one", 1), "garbage"]);
const reader = readerFor(fx);
const r = openOne(fx, reader);
assert.deepEqual([roles(r.page), r.page.branch], [["user", "notice"], "main"]);
// t3's parent was on the unreadable line, so t3 starts its own branch. The
// view keeps its branch and parts, and reports the new default.
appendFileSync(path, lines(assistant("t3", "t2", "three", 3)));
const f = noWrites(fx.base, () => reader.next({ cursor: r.follow.id, conversation: r.page.conversation, branch: r.page.branch }));
assert.deepEqual([f.ok, f.page.entries.length, f.page.branch, f.view.defaultBranch], [true, 0, "main", "b.t3"]);
const again = openOne(fx, reader);
assert.deepEqual([roles(again.page), again.page.branch], [["notice", "assistant"], "b.t3"]);
assert.match(texts(again.page)[0], /Line 3 could not be read and may have held it/);
});
test("F1: a file whose entries are all unreadable shows a notice per line", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), ["garbage", "[1]", '{"type":"message"}']);
const r = openOne(fx, readerFor(fx));
assert.deepEqual([r.ok, r.page.branch, r.view.unreadableLines], [true, "main", 3]);
assert.deepEqual(texts(r.page).map((t) => t.match(/^Line (\d+)/)[1]), ["2", "3", "4"]);
});
test("F2: a truncated trailing line marks the view incomplete, not an error", () => {
const fx = project();
const path = writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("f1", null, "one", 1)], '{"type":"message","id":"f2","parentId":"f1"');
const reader = readerFor(fx);
const r = openOne(fx, reader);
assert.equal(r.ok, true);
assert.equal(r.view.incomplete, true);
assert.deepEqual(texts(r.page), ["one"]);
assert.equal(r.view.unreadableLines, 0);
appendFileSync(path, ',"timestamp":"' + time(2) + '","message":{"role":"assistant","content":[{"type":"text","text":"two"}]}}\n');
const f = noWrites(fx.base, () => reader.next({ cursor: r.follow.id, conversation: r.page.conversation, branch: r.page.branch }));
assert.equal(f.ok, true);
assert.deepEqual(texts(f.page), ["two"]);
assert.equal(f.view.incomplete, false);
assert.equal(f.page.sourceEpoch, r.page.sourceEpoch);
});
// 150 one-part entries: two pages.
function longSession(fx) {
const entries = [];
for (let i = 0; i < 150; i++) entries.push(i % 2 ? assistant(`g${i}`, i ? `g${i - 1}` : null, `m${i}`, i + 1) : user(`g${i}`, i ? `g${i - 1}` : null, `m${i}`, i + 1));
return writeSession(fx.dir, "s.jsonl", header(fx.proj), entries);
}
test("pagination: 100 parts, then the rest; parts concatenate to the whole branch", () => {
const fx = project();
longSession(fx);
const reader = readerFor(fx);
const pages = readAll(fx, reader, openOne(fx, reader));
assert.deepEqual(pages.map((p) => p.page.entries.length), [100, 50]);
assert.deepEqual(pages.flatMap((p) => texts(p.page)), Array.from({ length: 150 }, (_, i) => `m${i}`));
assert.equal(pages[0].cursor.lastEntry, pages[0].page.entries[99].id);
assert.equal(pages[0].follow, null);
assert.ok(pages[1].follow);
});
test("F3: a replaced file (new inode) refuses old cursors with reconcile", () => {
const fx = project();
const path = longSession(fx);
const reader = readerFor(fx);
const r = openOne(fx, reader);
const content = readFileSync(path);
writeFileSync(path + ".new", content);
renameSync(path + ".new", path);
for (const cursor of [r.cursor.id]) {
const x = noWrites(fx.base, () => reader.next({ cursor, conversation: r.page.conversation, branch: r.page.branch }));
assert.deepEqual([x.ok, x.refusal.code, x.refusal.reconcile], [false, "source-replaced", true]);
}
const short = project();
writeSession(short.dir, "s.jsonl", header(short.proj), [user("h1", null, "one", 1)]);
const reader2 = readerFor(short);
const r2 = openOne(short, reader2);
const p2 = join(short.dir, "s.jsonl");
writeFileSync(p2 + ".new", readFileSync(p2));
renameSync(p2 + ".new", p2);
const f = noWrites(short.base, () => reader2.next({ cursor: r2.follow.id, conversation: r2.page.conversation, branch: r2.page.branch }));
assert.deepEqual([f.ok, f.refusal.code, f.refusal.reconcile], [false, "source-replaced", true], "follow cursor too");
const reopened = openOne(short, reader2);
assert.notEqual(reopened.page.sourceEpoch, r2.page.sourceEpoch, "a reopen is a new epoch");
});
test("F4: a same-inode rewrite of the prefix refuses old cursors with reconcile", () => {
const fx = project();
const path = longSession(fx);
const reader = readerFor(fx);
const r = openOne(fx, reader);
const ino = lstatSync(path).ino;
const text = readFileSync(path, "utf8").replace('"m3"', '"M3"');
const fd = openSync(path, "r+");
writeSync(fd, text, 0);
closeSync(fd);
assert.equal(lstatSync(path).ino, ino, "same inode");
const x = noWrites(fx.base, () => reader.next({ cursor: r.cursor.id, conversation: r.page.conversation, branch: r.page.branch }));
assert.deepEqual([x.ok, x.refusal.code, x.refusal.reconcile], [false, "source-replaced", true]);
// Shorter than the snapshot.
const r2 = openOne(fx, reader);
writeFileSync(path, lines(header(fx.proj)));
const y = noWrites(fx.base, () => reader.next({ cursor: r2.cursor.id, conversation: r2.page.conversation, branch: r2.page.branch }));
assert.deepEqual([y.ok, y.refusal.code, y.refusal.reconcile], [false, "source-replaced", true]);
});
test("F5: growth between pages keeps the epoch and the page stops at the pinned length", () => {
const fx = project();
const path = longSession(fx);
const reader = readerFor(fx);
const r = openOne(fx, reader);
appendFileSync(path, lines(user("g150", "g149", "later", 200)) + '{"type":"mess');
const r2 = noWrites(fx.base, () => reader.next({ cursor: r.cursor.id, conversation: r.page.conversation, branch: r.page.branch }));
assert.equal(r2.ok, true);
assert.equal(r2.page.entries.length, 50);
assert.equal(r2.page.snapshotDigest, r.page.snapshotDigest);
assert.equal(r2.page.sourceEpoch, r.page.sourceEpoch);
assert.equal(r2.view.incomplete, false, "the pinned snapshot ended on a complete line");
assert.ok(!texts(r2.page).includes("later"));
// Following picks up the growth, up to the last complete line.
const f = noWrites(fx.base, () => reader.next({ cursor: r2.follow.id, conversation: r2.page.conversation, branch: r2.page.branch }));
assert.equal(f.ok, true);
assert.deepEqual(texts(f.page), ["later"]);
assert.equal(f.page.sourceEpoch, r.page.sourceEpoch);
assert.notEqual(f.page.snapshotDigest, r.page.snapshotDigest);
assert.equal(f.page.branch, "main");
assert.equal(f.view.incomplete, true);
// A read of a file that grows by a partial line cuts at the last newline.
const again = openOne(fx, reader);
assert.equal(again.view.incomplete, true);
const all = readAll(fx, reader, again).flatMap((p) => texts(p.page));
assert.equal(all.length, 151);
});
test("F6: unknown, foreign and expired cursors refuse and leave the cursor usable", () => {
const fx = project();
longSession(fx);
let clock = Date.parse("2026-09-26T12:00:00Z");
const reader = readerFor(fx, { now: () => clock, ttlMs: 60_000 });
const r = openOne(fx, reader);
const good = { cursor: r.cursor.id, conversation: r.page.conversation, branch: r.page.branch };
const cases = [
[{ ...good, cursor: "c-unknown" }, "cursor-unknown"],
[{ ...good, actor: "someone-else" }, "cursor-foreign"],
[{ ...good, purpose: "drafts" }, "cursor-foreign"],
[{ ...good, conversation: "pi-" + "0".repeat(32) }, "cursor-foreign"],
[{ ...good, branch: "g1" }, "cursor-foreign"],
];
for (const [args, code] of cases) {
const x = noWrites(fx.base, () => reader.next(args));
assert.deepEqual([x.ok, x.refusal.code, x.refusal.reconcile], [false, code, true], code);
}
const ok = noWrites(fx.base, () => reader.next(good));
assert.equal(ok.ok, true, "the old view's cursor still works after the refusals");
clock += 60_000;
const expired = noWrites(fx.base, () => reader.next(good));
assert.deepEqual([expired.ok, expired.refusal.code, expired.refusal.reconcile], [false, "cursor-expired", true]);
const gone = noWrites(fx.base, () => reader.next(good));
assert.equal(gone.refusal.code, "cursor-unknown");
// Evicted cursors are unknown.
const small = readerFor(fx, { maxCursors: 2 });
const first = openOne(fx, small);
openOne(fx, small);
openOne(fx, small);
const evicted = noWrites(fx.base, () => small.next({ cursor: first.cursor.id, conversation: first.page.conversation, branch: first.page.branch }));
assert.equal(evicted.refusal.code, "cursor-unknown");
assert.ok(small.cursorCount() <= 2);
// open refuses other actors and purposes outright.
assert.equal(openOne(fx, reader, "s.jsonl", { actor: "x" }).refusal.code, "unknown-actor");
assert.equal(openOne(fx, reader, "s.jsonl", { purpose: "drafts" }).refusal.code, "unsupported-purpose");
});
// A file outside every root that no read may touch: unreadable, so an
// attempted open would throw EACCES rather than refuse.
function forbidden(base, name = "outside.jsonl") {
const dir = join(base, "outside");
mkdirSync(dir, { recursive: true });
const path = join(dir, name);
writeFileSync(path, lines(header("/elsewhere"), user("z1", null, "secret", 1)));
chmodSync(path, 0o000);
return path;
}
test("F7: a symlinked file and a symlinked directory component are refused, never opened", () => {
const fx = project();
const outside = forbidden(fx.base);
symlinkSync(outside, join(fx.dir, "link.jsonl"));
writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("i1", null, "ok", 1)]);
const reader = readerFor(fx);
const cat = noWrites(fx.base, () => reader.catalogue());
const link = cat.conversations.find((c) => c.availability === "denied");
assert.deepEqual([link.refusal, link.title], ["unsafe-path", null]);
const r = openOne(fx, reader, "link.jsonl");
assert.deepEqual([r.ok, r.refusal.code], [false, "unsafe-path"]);
assert.equal(cat.conversations.filter((c) => c.availability === "available").length, 1);
// .pi/state/<seat> is a symlink to a directory holding a sessions dir.
const fy = project("other");
const realSeat = join(fy.base, "real-seat");
mkdirSync(join(realSeat, "sessions"), { recursive: true });
writeFileSync(join(realSeat, "sessions", "s.jsonl"), lines(header(fy.proj), user("j1", null, "hidden", 1)));
chmodSync(join(realSeat, "sessions", "s.jsonl"), 0o000);
mkdirSync(join(fy.proj, ".pi", "state"), { recursive: true });
symlinkSync(realSeat, join(fy.proj, ".pi", "state", "linked"));
const spec = { agent: "linked", project: "proj", sessionsDir: join(fy.proj, ".pi", "state", "linked", "sessions") };
const reader2 = createReader({ roots: rootsFromSpecs([spec]) });
const cat2 = noWrites(fy.base, () => reader2.catalogue());
assert.deepEqual(cat2.conversations, []);
assert.deepEqual(cat2.refusedRoots, [{ seat: "linked", project: "proj", refusal: "unsafe-path" }]);
const root = rootsFromSpecs([spec])[0];
const r2 = noWrites(fy.base, () => reader2.open({ conversation: conversationId(root, "s.jsonl") }));
assert.deepEqual([r2.ok, r2.refusal.code], [false, "unknown-conversation"]);
});
test("F8: a file swapped for a symlink after the catalogue is refused", () => {
const fx = project();
const outside = forbidden(fx.base);
const path = writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("k1", null, "ok", 1)]);
const reader = readerFor(fx);
const cat = noWrites(fx.base, () => reader.catalogue());
const r = openOne(fx, reader);
assert.equal(r.ok, true);
rmSync(path);
symlinkSync(outside, path);
const x = noWrites(fx.base, () => reader.open({ conversation: cat.conversations[0].conversation }));
assert.deepEqual([x.ok, x.refusal.code], [false, "unsafe-path"]);
const f = noWrites(fx.base, () => reader.next({ cursor: r.follow.id, conversation: r.page.conversation, branch: r.page.branch }));
assert.deepEqual([f.ok, f.refusal.code], [false, "unsafe-path"]);
});
test("F9: registrations never add or redirect a root", () => {
const fx = project("seat-a");
writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("l1", null, "ok", 1)]);
const outside = forbidden(fx.base);
const other = project("seat-a");
writeSession(other.dir, "o.jsonl", header(other.proj), [user("l2", null, "other project", 1)]);
const linkDir = join(fx.base, "link-to-other");
symlinkSync(other.dir, linkDir);
const reg = (extra) => ({ version: 1, seat: "seat-a", project: "proj", layout: "repo", harness: "claude-code", startedAt: time(0), ...extra });
const registrations = [
reg({ sessionsDir: join(fx.base, "outside") }), // outside every root
reg({ sessionsDir: linkDir }), // a symlink to another project's root
reg({ sessionsDir: other.dir }), // another project's directory
reg({ sessionsDir: fx.dir, project: "someone-else" }), // this directory, another project
reg({ seat: "seat-b", sessionsDir: join(fx.base, "outside") }), // a non-Pi placeholder outside the project
];
const roots = rootsFromSpecs([fx.spec], registrations);
assert.deepEqual(roots.map((r) => [r.seat, r.dir, r.harness, r.unsupportedReason]), [["seat-a", fx.dir, "pi", null]]);
const reader = createReader({ roots });
const cat = noWrites(fx.base, () => reader.catalogue());
assert.deepEqual(cat.conversations.map((c) => [c.title, c.availability]), [["ok", "available"]]);
assert.ok(outside);
// A registration that matches the root narrows it and supplies launch time.
const matched = rootsFromSpecs([fx.spec], [reg({ harness: "pi", sessionsDir: fx.dir, startedAt: time(-5) })]);
assert.equal(matched[0].engineStartedAt, time(-5));
const cat2 = noWrites(fx.base, () => createReader({ roots: matched }).catalogue());
assert.equal(cat2.conversations[0].engineStartedAt, time(-5));
// Only repository specs are roots.
assert.deepEqual(rootsFromSpecs([
{ agent: "x", project: "fleet", sessionsDir: join(fx.base, "fleet", "x", ".pi", "agent", "sessions") },
{ agent: "seat-a", project: "wrong", sessionsDir: fx.dir },
{ agent: "seat-b", project: "proj", sessionsDir: fx.dir },
{ ...fx.spec, connector: "discord" },
]), []);
});
test("F10: a header cwd naming another project is refused", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(join(fx.base, "another-project")), [user("m1", null, "x", 1)]);
writeSession(fx.dir, "sub.jsonl", header(join(fx.proj, "packages", "x")), [user("m2", null, "sub", 1)]);
writeSession(fx.dir, "prefix.jsonl", header(fx.proj + "-evil"), [user("m3", null, "x", 1)]);
// Relative to the reader's own directory this names the project; it is still refused.
writeSession(fx.dir, "relative.jsonl", header(relative(process.cwd(), join(fx.proj, "packages")) || "."), [user("m5", null, "x", 1)]);
const linkRoot = join(fx.base, "compat");
symlinkSync(fx.proj, linkRoot);
writeSession(fx.dir, "link.jsonl", header(linkRoot), [user("m4", null, "via compat link", 1)]);
const reader = readerFor(fx);
const cat = noWrites(fx.base, () => reader.catalogue());
const by = Object.fromEntries(cat.conversations.map((c) => [c.conversation, c]));
const root = rootsFromSpecs([fx.spec])[0];
for (const name of ["s.jsonl", "prefix.jsonl", "relative.jsonl"]) {
assert.deepEqual([by[conversationId(root, name)].availability, by[conversationId(root, name)].refusal], ["denied", "foreign-project"], name);
assert.deepEqual(openOne(fx, reader, name).refusal.code, "foreign-project", name);
}
assert.equal(openOne(fx, reader, "sub.jsonl").ok, true);
assert.equal(openOne(fx, reader, "link.jsonl").ok, true);
assert.equal(cwdInProject(undefined, fx.proj), false);
});
test("F11: parentSession renders with a marker and the parent is never opened", () => {
const fx = project();
const parent = forbidden(fx.base, "parent.jsonl");
writeSession(fx.dir, "s.jsonl", header(fx.proj, { parentSession: parent }), [user("n1", null, "forked", 1)]);
const r = openOne(fx, readerFor(fx));
assert.equal(r.ok, true);
assert.deepEqual(roles(r.page), ["notice", "user"]);
assert.match(texts(r.page)[0], /forked from an earlier session/);
assert.equal(r.view.forkedFromEarlierSession, true);
});
test("F12: two leaves: the default leaf is shown and the other branch reads alone", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("o1", null, "root", 1),
assistant("o2", "o1", "first answer", 2),
user("o3", "o2", "on branch one", 3),
assistant("o4", "o1", "second answer", 4),
]);
const reader = readerFor(fx);
const r = openOne(fx, reader);
// o1's earliest child continues "main"; the later child o4 starts "b.o4".
assert.deepEqual(texts(r.page), ["root", "second answer"]);
assert.equal(r.page.branch, "b.o4");
assert.deepEqual(r.view.branches.map((b) => [b.branch, b.isDefault]), [["main", false], ["b.o4", true]]);
const other = openOne(fx, reader, "s.jsonl", { branch: "main" });
assert.deepEqual(texts(other.page), ["root", "first answer", "on branch one"]);
assert.equal(other.page.branch, "main");
assert.ok(other.page.entries.every((e) => e.branch === "main"));
for (const inner of ["o2", "b.o2", "o3", "b.o3", "o4"]) assert.equal(openOne(fx, reader, "s.jsonl", { branch: inner }).refusal.code, "unknown-branch", `${inner} is not a branch name`);
// Following a non-default branch after the default moves on adds nothing.
const path = join(fx.dir, "s.jsonl");
appendFileSync(path, lines(user("o5", "o4", "more on default", 5)));
const f = noWrites(fx.base, () => reader.next({ cursor: other.follow.id, conversation: other.page.conversation, branch: "main" }));
assert.deepEqual([f.ok, f.page.entries.length, f.page.branch, f.view.defaultBranch], [true, 0, "main", "b.o4"]);
// Following the default branch continues on it under the same name.
const g = noWrites(fx.base, () => reader.next({ cursor: r.follow.id, conversation: r.page.conversation, branch: "b.o4" }));
assert.deepEqual([texts(g.page), g.page.branch, g.view.defaultBranch], [["more on default"], "b.o4", "b.o4"]);
assert.ok([...r.page.entries, ...g.page.entries].every((e) => e.branch === "b.o4"), "one branch value across follows");
// The pre-growth name still opens, now with the growth.
assert.deepEqual(texts(openOne(fx, reader, "s.jsonl", { branch: "b.o4" }).page), ["root", "second answer", "more on default"]);
// A new default elsewhere: the old default view stays put.
appendFileSync(path, lines(assistant("o6", "o3", "back on branch one", 6)));
const h = noWrites(fx.base, () => reader.next({ cursor: g.follow.id, conversation: g.page.conversation, branch: "b.o4" }));
assert.deepEqual([h.page.entries.length, h.page.branch, h.view.defaultBranch], [0, "b.o4", "main"]);
// And the "main" view picks its growth up.
const k = noWrites(fx.base, () => reader.next({ cursor: f.follow.id, conversation: f.page.conversation, branch: "main" }));
assert.deepEqual([texts(k.page), k.page.branch], [["back on branch one"], "main"]);
});
test("F12: a follow refuses when an appended duplicate id changes the branch's earlier parts", () => {
// Where an id repeats the later entry wins, so an append can move an entry.
for (const [extra, why] of [
[[{ ...assistant("r2", "x1", "moved", 4) }], "r2 moves to the other root: main is shorter"],
[[{ ...assistant("r2", "x1", "moved", 4) }, assistant("r5", "r1", "in its place", 5)], "same length, another entry in r2's place"],
]) {
const fx = project();
const path = writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("r1", null, "one", 1), user("x1", null, "other root", 2), assistant("r2", "r1", "two", 3)]);
const reader = readerFor(fx);
const r = openOne(fx, reader, "s.jsonl", { branch: "main" });
assert.deepEqual(texts(r.page), ["one", "two"]);
appendFileSync(path, lines(...extra));
const f = noWrites(fx.base, () => reader.next({ cursor: r.follow.id, conversation: r.page.conversation, branch: "main" }));
assert.deepEqual([f.ok, f.refusal?.code, f.refusal?.reconcile], [false, "source-replaced", true], why);
}
});
test("F12: a second root (Pi's resetLeaf) starts its own branch", () => {
const fx = project();
const path = writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("r1", null, "first root", 1),
assistant("r2", "r1", "first reply", 2),
user("r3", null, "second root", 3),
]);
const reader = readerFor(fx);
const r = openOne(fx, reader);
assert.deepEqual([texts(r.page), r.page.branch], [["second root"], "b.r3"]);
assert.deepEqual(r.view.branches.map((b) => [b.branch, b.isDefault]), [["main", false], ["b.r3", true]]);
assert.deepEqual(texts(openOne(fx, reader, "s.jsonl", { branch: "main" }).page), ["first root", "first reply"]);
appendFileSync(path, lines(assistant("r4", "r3", "second reply", 4)));
const f = noWrites(fx.base, () => reader.next({ cursor: r.follow.id, conversation: r.page.conversation, branch: "b.r3" }));
assert.deepEqual([texts(f.page), f.page.branch], [["second reply"], "b.r3"]);
});
test("F13: compaction is a marker in place, then the retained content", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("p1", null, "early", 1),
assistant("p2", "p1", "early answer", 2),
{ type: "compaction", id: "p3", parentId: "p2", timestamp: time(3), summary: "they talked", firstKeptEntryId: "p2", tokensBefore: 1000 },
user("p4", "p3", "after", 4),
]);
const r = openOne(fx, readerFor(fx));
assert.deepEqual(roles(r.page), ["user", "assistant", "compaction", "user"]);
const marker = r.page.entries[2].content[0];
assert.deepEqual([marker.type, marker.summary, marker.nativeEntry], ["compaction", "they talked", "p3"]);
});
test("F14: long strings split into fragments and parts, reassemble exactly, and pages respect the byte cap", () => {
const fx = project();
const long = "x".repeat(LIMITS.chars + 1);
const emoji = "😀".repeat(LIMITS.chars + 3); // 4 bytes each: splits on bytes before characters
const blocks = Array.from({ length: 70 }, (_, i) => ({ type: "text", text: `block ${i}` }));
const wide = "€".repeat(200_000); // 3 bytes each, ~600 KB per part
const entries = [
user("q1", null, long, 1),
assistant("q2", "q1", emoji, 2),
msg("q3", "q2", "assistant", blocks, { stopReason: "stop" }, 3),
];
for (let i = 4; i < 44; i++) entries.push(user(`q${i}`, `q${i - 1}`, wide, i));
writeSession(fx.dir, "s.jsonl", header(fx.proj), entries);
const reader = readerFor(fx);
const pages = readAll(fx, reader, openOne(fx, reader));
for (const p of pages) {
assert.ok(p.page.entries.length <= LIMITS.parts);
assert.ok(Buffer.byteLength(JSON.stringify(p.page), "utf8") <= LIMITS.pageBytes, "page within 8 MiB");
for (const e of p.page.entries) {
assert.ok(e.content.length <= LIMITS.blocks);
for (const b of e.content) assert.ok([...(b.text ?? "")].length <= LIMITS.chars);
}
}
assert.ok(pages.length >= 3, "the multibyte parts fill pages on bytes before 100 parts");
assert.ok(pages[1].page.entries.length < LIMITS.parts);
// Reassemble per native entry: parts in order, fragments in order.
const all = pages.flatMap((p) => p.page.entries);
const rebuilt = new Map();
for (const e of all) {
const blocksOf = rebuilt.get(e.nativeEntry) ?? [];
for (const b of e.content) blocksOf[b.block] = (blocksOf[b.block] ?? "") + b.text;
rebuilt.set(e.nativeEntry, blocksOf);
}
assert.equal(rebuilt.get("q1")[0], long);
assert.equal(rebuilt.get("q2")[0], emoji);
assert.deepEqual(rebuilt.get("q3"), blocks.map((b) => b.text));
assert.equal(rebuilt.get("q20")[0], wide);
const q1 = all.filter((e) => e.nativeEntry === "q1");
assert.deepEqual(q1.flatMap((e) => e.content.map((b) => [b.fragment, b.lastFragment])), [[0, false], [1, true]]);
const q3 = all.filter((e) => e.nativeEntry === "q3");
assert.deepEqual(q3.map((e) => [e.part, e.lastPart, e.content.length]), [[0, false, 64], [1, true, 6]]);
assert.equal(new Set(all.map((e) => e.id)).size, all.length);
});
test("fragments never cut a surrogate pair and keep an empty string", () => {
assert.deepEqual(fragments(""), [""]);
const s = "a" + "😀".repeat(LIMITS.chars);
const parts = fragments(s);
assert.equal(parts.join(""), s);
for (const p of parts) {
assert.ok(!/^[\udc00-\udfff]/.test(p) && !/[\ud800-\udbff]$/.test(p));
assert.ok(Buffer.byteLength(JSON.stringify(p)) <= 1024 * 1024 + 2);
}
});
test("F15: a Claude seat is an unsupported-harness placeholder whose directory is never read", () => {
const fx = project("pi-seat");
writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("r1", null, "ok", 1)]);
// Rocko's case: a claude-code registration and no Pi sessions directory.
const claudeDir = join(fx.proj, ".pi", "state", "claude-seat", "sessions");
const regs = [{ version: 1, seat: "claude-seat", project: "proj", layout: "repo", harness: "claude-code", sessionsDir: claudeDir, startedAt: time(-1) }];
// And a spec'd seat with sessions whose registration says claude-code.
const both = join(fx.proj, ".pi", "state", "mixed", "sessions");
mkdirSync(both, { recursive: true });
writeFileSync(join(both, "s.jsonl"), lines(header(fx.proj), user("r2", null, "never read", 1)));
chmodSync(join(both, "s.jsonl"), 0o000);
regs.push({ version: 1, seat: "mixed", project: "proj", layout: "repo", harness: "claude-code", sessionsDir: both, startedAt: time(-1) });
const roots = rootsFromSpecs([fx.spec, { agent: "mixed", project: "proj", sessionsDir: both }], regs);
const reader = createReader({ roots });
const cat = noWrites(fx.base, () => reader.catalogue());
const unsupported = cat.conversations.filter((c) => c.availability === "unsupported");
assert.deepEqual(unsupported.map((c) => [c.seat, c.harness, c.unsupportedReason, c.history]).sort(), [["claude-seat", "claude-code", UNSUPPORTED_HARNESS, null], ["mixed", "claude-code", UNSUPPORTED_HARNESS, null]]);
for (const c of unsupported) {
const r = noWrites(fx.base, () => reader.open({ conversation: c.conversation }));
assert.deepEqual([r.ok, r.refusal.code, r.refusal.reconcile], [false, UNSUPPORTED_HARNESS, false]);
}
});
test("unknown conversations, empty files and non-Pi files refuse", () => {
const fx = project();
writeFileSync(join(fx.dir, "empty.jsonl"), "");
writeFileSync(join(fx.dir, "notpi.jsonl"), lines({ hello: 1 }));
writeFileSync(join(fx.dir, "header-only.jsonl"), lines(header(fx.proj)));
const reader = readerFor(fx);
const cat = noWrites(fx.base, () => reader.catalogue());
assert.deepEqual(cat.conversations.map((c) => [c.availability, c.refusal]).sort(), [["available", null], ["unavailable", "incomplete-header"], ["unavailable", "not-a-pi-session"]]);
assert.equal(openOne(fx, reader, "empty.jsonl").refusal.code, "incomplete-header");
assert.equal(openOne(fx, reader, "notpi.jsonl").refusal.code, "not-a-pi-session");
const empty = openOne(fx, reader, "header-only.jsonl");
assert.deepEqual([empty.ok, empty.page.entries.length, empty.page.branch, empty.view.branches], [true, 0, "main", [{ branch: "main", isDefault: true, lastActivityAt: null }]]);
appendFileSync(join(fx.dir, "header-only.jsonl"), lines(user("s1", null, "first", 1)));
const f = noWrites(fx.base, () => reader.next({ cursor: empty.follow.id, conversation: empty.page.conversation, branch: "main" }));
assert.deepEqual([texts(f.page), f.page.branch], [["first"], "main"]);
const u = noWrites(fx.base, () => reader.open({ conversation: "pi-" + "f".repeat(32) }));
assert.deepEqual([u.refusal.code, u.refusal.reconcile], ["unknown-conversation", true]);
});
test("an unreadable file or root inside the roots is refused per row, not a failed catalogue", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("u1", null, "ok", 1)]);
writeSession(fx.dir, "locked.jsonl", header(fx.proj), [user("u2", null, "locked", 1)]);
chmodSync(join(fx.dir, "locked.jsonl"), 0o000);
const reader = readerFor(fx);
const cat = noWrites(fx.base, () => reader.catalogue());
assert.deepEqual(cat.conversations.map((c) => [c.availability, c.refusal]).sort(), [["available", null], ["denied", "unreadable"]]);
assert.equal(openOne(fx, reader, "locked.jsonl").refusal.code, "unreadable");
chmodSync(fx.dir, 0o300);
try {
const cat2 = noWrites(fx.base, () => reader.catalogue());
assert.deepEqual([cat2.conversations, cat2.refusedRoots.map((r) => r.refusal)], [[], ["unreadable"]]);
} finally {
chmodSync(fx.dir, 0o700);
}
});
test("a seat directory without search permission refuses that root, not the catalogue", () => {
const fx = project("good");
writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("p1", null, "readable", 1)]);
const badDir = join(fx.proj, ".pi", "state", "bad", "sessions");
mkdirSync(badDir, { recursive: true });
writeSession(badDir, "s.jsonl", header(fx.proj), [user("q1", null, "hidden", 1)]);
const bad = { agent: "bad", project: "proj", sessionsDir: badDir };
const goodId = conversationId(rootsFromSpecs([fx.spec])[0], "s.jsonl");
chmodSync(join(fx.proj, ".pi", "state", "bad"), 0o000);
try {
for (const specs of [[bad, fx.spec], [fx.spec, bad]]) {
const reader = createReader({ roots: rootsFromSpecs(specs) });
const cat = noWrites(fx.base, () => reader.catalogue());
assert.deepEqual([cat.conversations.map((c) => [c.seat, c.availability]), cat.refusedRoots], [[["good", "available"]], [{ seat: "bad", project: "proj", refusal: "unreadable" }]]);
const r = noWrites(fx.base, () => reader.open({ conversation: goodId }));
assert.deepEqual(texts(r.page), ["readable"]);
const u = noWrites(fx.base, () => reader.open({ conversation: "pi-" + "e".repeat(32) }));
assert.equal(u.refusal.code, "unknown-conversation");
}
} finally {
chmodSync(join(fx.proj, ".pi", "state", "bad"), 0o700);
}
});
test("every page and cursor is a valid CHAT-01 record", () => {
assert.ok(records.length > 50);
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, schemaPath], { input: JSON.stringify(records), encoding: "utf8", maxBuffer: 1 << 30 });
assert.equal(run.status, 0, run.stderr);
assert.deepEqual(JSON.parse(run.stdout), []);
});