// 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/ 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), []); });