Files
stack/packages/cli/tests/compactions-launch.test.mjs
T
jason.woltje 39eadf8ba8 Row 332 R2 (#1830): hermetic pi launches in cli tests, pi bundle default, review F2 and F3
The cli tests that launch pi deleted only the session login names, so pi's
preflight passed or refused by whatever provider keys the host exported. A
shared helper now deletes every credential pi's preflight reads and gives pi
launches one made-up key. The refusal itself is unchanged.

pi bundle() takes a default argument again, so a call with none returns the
non-proxy bundle, as before P3. pi.bundle leaves out a launch.models entry
whose provider has no proxy route instead of failing every pi launch (F2);
malformed proxy entries still refuse. The invalid session proxy context
BundleError gets the code invalid-proxy-context. New tests cover F2, the code,
and the three proxy account branches in the launcher (F3).
2026-11-02 15:22:17 -06:00

352 lines
19 KiB
JavaScript

// Compaction counts on a real run (row 270, 264c, #1775, T3's Pi half): the
// launcher collects a thread session's completed compactions into its
// store's ledger at run start and at run end, and session.list reads the
// ledger through the runtime's reader. The count holds across a resume and a
// host restart. Real runners in the bwrap sandbox; a stub Pi adapter writes
// the session stream (fixtures/compaction-adapter.mjs).
//
// Round 2 (duckworth-0223 B2, sage-3031, mos-1352): a failed collection, the
// read budget from the system config, a clear's retired session and its
// successor, and a legacy session read through the runtime's reader. Round 3
// (duckworth-0226 B4, sage-3038): the stream cap from the system config.
//
// The host is session-launch.test.mjs's: startBroker on bootConfig's config,
// with the ops the broker child forwards, and the human capability from
// bindHuman directly.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, rmSync, truncateSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { launchDoc } from "../../business/tests/helpers.mjs";
import { BusError } from "../../bus/src/broker.mjs";
import { startBroker } from "../../bus/src/runtime.mjs";
import { Store } from "../../bus/src/store.mjs";
import { collectCompactions, LEDGER } from "../../seat/src/compactions.mjs";
import { gitEnv } from "../../seat/src/checkout.mjs";
import { sandboxProblem } from "../../seat/src/sandbox.mjs";
import { launchLogFile, sessionStore } from "../../seat/src/session.mjs";
import { compactions as pi } from "../../plugins/harness/pi/index.mjs";
import { bootConfig, loadSystem } from "../src/config.mjs";
import { createLauncher } from "../src/launcher.mjs";
import { fakeHarnesses, fixture, tmp, withoutHostCredentials } from "./helpers.mjs";
const ADAPTER = fileURLToPath(new URL("./fixtures/compaction-adapter.mjs", import.meta.url));
const skip = sandboxProblem() ?? false;
async function until(fn, ms = 20000) {
const end = Date.now() + ms;
while (Date.now() < end) {
const v = await fn();
if (v) return v;
await new Promise((r) => setTimeout(r, 50));
}
throw new Error("timed out waiting");
}
// A mutant's kill is an assertion failure, not a timeout (sage-2707), so a wait a mutant could
// stall returns what it last saw and the assertions after it decide.
async function settle(fn, ms = 5000) {
const end = Date.now() + ms;
let v;
while (Date.now() < end) {
v = await fn();
if (v) return v;
await new Promise((r) => setTimeout(r, 50));
}
return v;
}
function gitIn(cwd, ...args) {
const r = spawnSync("git", ["-c", "user.name=test", "-c", "[email protected]", "-C", cwd, ...args], { env: gitEnv(), encoding: "utf8" });
assert.equal(r.status, 0, r.stderr);
}
// The assistant is the thread instance, on Pi.
function prepare(t) {
const root = tmp(t, "mosaic-compactions-");
const f = fixture(root, "acme", (doc) => {
doc.roles.chief.vars = { harness: "pi", model: "claude-opus-5-5", thinking: "high" };
doc.roles.assistant = { definition: "assistant", vars: { harness: "pi", model: "claude-sonnet-5-5", thinking: "high" } };
doc.launch = launchDoc("chief", ["coder", "assistant"], { opus: 1, sonnet: 3 });
doc.launch.threadIdleSeconds = 1;
return doc;
});
withoutHostCredentials(f.env, { piKey: true });
const project = f.doc.projects.stack.root;
gitIn(project, "init", "-q", "-b", "main");
writeFileSync(join(project, "file.txt"), "one\n");
gitIn(project, "add", "file.txt");
gitIn(project, "commit", "-q", "-m", "one");
const adapter = join(root, "adapter.sh");
writeFileSync(adapter, `exec '${process.execPath}' '${ADAPTER}'\n`, { mode: 0o644 });
return { root, f, adapter };
}
// A host and its launcher on the prepared root.
async function start(t, { f, adapter }) {
const system = loadSystem({ env: f.env });
const { trackers, ...boot } = bootConfig({ system, businessId: "acme", env: f.env });
assert.equal(trackers, undefined);
const runtime = await startBroker(boot);
const ops = {
identity: (m) => runtime.identity(m.cap),
authorizeLaunch: (m) => runtime.authorizeLaunch(m.cap, m.instance),
refuse: (m) => runtime.refuse(m.cap, m.code, m.subject),
endLaunch: (m) => runtime.endLaunch(m.record),
credentialStatus: (m) => runtime.credentialStatus(m.business, m.role),
threadsState: (m) => runtime.threadsState(m.business),
threadIdle: (m) => runtime.threadIdle(m.record),
mayLaunch: (m) => runtime.mayLaunch(m),
landHeld: (m) => runtime.landHeld(m.record),
landHolders: (m) => runtime.landHolders(m.business),
landRelease: (m) => runtime.landRelease(m.record),
sessionClears: (m) => runtime.sessionClears(m.business),
sessionClearExited: (m) => runtime.sessionClearExited(m.record),
sessionClearUncertain: (m) => runtime.sessionClearUncertain(m.record),
sessionClearRetire: (m) => runtime.sessionClearRetire(m.record),
sessionClearStart: (m) => runtime.sessionClearStart(m.record),
};
const refusing = (what, fn) => async (m) => {
try {
return fn(m);
} catch (e) {
const code = e instanceof BusError ? e.code : `${what}-refused`;
throw Object.assign(new Error(`${what} refused: ${code}`), { code });
}
};
const hooks = [];
let closing = null;
const host = {
business: "acme",
path: runtime.path,
dataRoot: f.dataRoot,
op: refusing("launch-op", (m) => {
if (!Object.hasOwn(ops, m.op)) throw new BusError("invalid-host-request");
return ops[m.op](m);
}),
bindLaunch: refusing("launch bind", (r) => runtime.bindLaunch(r)),
beforeClose: (fn) => void hooks.push(fn),
};
const logs = [];
const launcher = createLauncher({
host,
system,
env: f.env,
makeLoader: await fakeHarnesses(t, { pi: adapter, "claude-code": adapter }),
sessionDefaults: { pollInterval: 100 },
harnessVersion: "0.85.1",
readOnly: [ADAPTER],
threadTickMs: 100,
log: (l) => logs.push(l),
});
const close = () =>
(closing ??= (async () => {
for (const fn of hooks) await fn(0);
await runtime.close();
})());
t.after(close);
await launcher.listen();
const human = runtime.broker.bindHuman({ business: "acme", human: f.doc.human, via: "cli", outsideAgent: true });
const call = (verb, args = {}) => runtime.broker.request(human, { verb, args });
return { runtime, launcher, call, logs, close };
}
const launchLog = (f) =>
existsSync(launchLogFile(f.dataRoot, "acme"))
? readFileSync(launchLogFile(f.dataRoot, "acme"), "utf8").trim().split("\n").map((l) => JSON.parse(l))
: [];
const events = (f, event) => launchLog(f).filter((e) => e.event === event);
const counted = (f) => events(f, "compactions").map((e) => [e.session, e.count, e.known, e.reason]);
// Send to a thread's default session and wait for the assistant's RESULT reply.
async function ask(call, thread, body) {
const sent = call("thread.send", { thread, body });
const r = await until(() => call("thread.messages", { thread }).find((m) => m.in_reply_to === sent.id && m.class === "RESULT"));
assert.equal(r.from_role, "assistant");
return JSON.parse(r.body.match(/THREAD (\{.*\})/)[1]);
}
const line = (e) => `${JSON.stringify(e)}\n`;
test("REQ-THREAD-2 T3 a run collects compactions at its start and its end, and the count holds across a resume and a host restart", { skip, timeout: 90000 }, async (t) => {
const prepared = prepare(t);
const { f } = prepared;
let host = await start(t, prepared);
const project = host.call("project.create", { name: "stack work", repo: "stack" }).id;
const A = host.call("thread.create", { project, name: "alpha" }).id;
const [{ id: D }] = host.call("session.list", { thread: A });
// An earlier run left a compaction no run-end collection saw (a host that died mid-run).
// Only the run-start collection counts it before the agent's first turn.
const store = sessionStore(f.dataRoot, "acme", A, D);
writeFileSync(join(store.session, "pi.jsonl"), line({ type: "session", version: 3, id: "pi", timestamp: "2026-10-22T11:00:00.000Z", cwd: "/w" }) + line({ type: "compaction", id: "c0", parentId: null, timestamp: "2026-10-22T10:59:00.000Z", summary: "s", firstKeptEntryId: "k", tokensBefore: 100 }), { mode: 0o600 });
// Run 1: start counts c0 (M13), the turn compacts c1, the end counts it (M14).
assert.deepEqual(await ask(host.call, A, "compact c1"), { body: "compact c1", before: [] });
await until(() => events(f, "end").length === 1, 30000);
await settle(() => counted(f).length >= 2);
assert.deepEqual(counted(f), [[D, 1, true, null], [D, 2, true, null]]);
// A restart: a new host and launcher on the same data root. Run 2 resumes the session.
await host.close();
host = await start(t, prepared);
assert.deepEqual(await ask(host.call, A, "compact c2"), { body: "compact c2", before: ["compact c1"] });
await until(() => events(f, "end").length === 2, 30000);
await settle(() => counted(f).length >= 4);
assert.deepEqual(counted(f), [[D, 1, true, null], [D, 2, true, null], [D, 2, true, null], [D, 3, true, null]]);
// session.list reads the ledger through the runtime's reader (M15), after another restart.
await host.close();
host = await start(t, prepared);
const [row] = host.call("session.list", { thread: A });
assert.equal(row.id, D);
assert.deepEqual({ ...row.compactions, at: typeof row.compactions?.at }, { count: 3, known: true, reason: null, at: "string" });
});
test("REQ-THREAD-2 T3 a collection that fails is logged and recorded store-unreadable, and the run ends as one whose collections count", { skip, timeout: 90000 }, async (t) => {
const prepared = prepare(t);
const { f } = prepared;
const host = await start(t, prepared);
const project = host.call("project.create", { name: "stack work", repo: "stack" }).id;
const A = host.call("thread.create", { project, name: "alpha" }).id;
const [{ id: D }] = host.call("session.list", { thread: A });
// The ledger's path is a directory, so readLedger refuses it (ledger-unsafe).
const store = sessionStore(f.dataRoot, "acme", A, D);
mkdirSync(join(store.dir, LEDGER));
assert.deepEqual(await ask(host.call, A, "compact c1"), { body: "compact c1", before: [] });
await until(() => events(f, "end").length === 1, 30000);
await settle(() => counted(f).length >= 2);
assert.deepEqual(counted(f), [[D, null, false, "store-unreadable"], [D, null, false, "store-unreadable"]]);
const [{ run: first }] = events(f, "launch");
assert.equal(host.logs.filter((l) => l.startsWith(`run ${first}: compactions: `)).length, 2);
// With the ledger's path free again, the next run's collections count what the stream holds.
rmSync(join(store.dir, LEDGER), { recursive: true });
assert.deepEqual(await ask(host.call, A, "compact c2"), { body: "compact c2", before: ["compact c1"] });
await until(() => events(f, "end").length === 2, 30000);
await settle(() => counted(f).length >= 4);
assert.deepEqual(counted(f).slice(2), [[D, 1, true, null], [D, 2, true, null]]);
const ends = events(f, "end").map((e) => [e.reason, e.exitCode, e.signal]);
assert.deepEqual(ends[0], ends[1]);
});
test("REQ-THREAD-2 T3 the read budget is the system config's compactionReadBytes, and a bad value takes the default", { skip, timeout: 90000 }, async (t) => {
const prepared = prepare(t);
const { f } = prepared;
const setting = (v) => {
const doc = JSON.parse(readFileSync(f.config, "utf8"));
doc.compactionReadBytes = v;
writeFileSync(f.config, JSON.stringify(doc));
};
setting(1 << 20);
let host = await start(t, prepared);
const project = host.call("project.create", { name: "stack work", repo: "stack" }).id;
const A = host.call("thread.create", { project, name: "alpha" }).id;
const [{ id: D }] = host.call("session.list", { thread: A });
// c0, then a 2 MiB hole with no newline: a line past 1 MiB, inside the 64 MiB default.
const store = sessionStore(f.dataRoot, "acme", A, D);
const path = join(store.session, "pi.jsonl");
const whole = line({ type: "session", version: 3, id: "pi", timestamp: "2026-10-22T11:00:00.000Z", cwd: "/w" }) + line({ type: "compaction", id: "c0", parentId: null, timestamp: "2026-10-22T10:59:00.000Z", summary: "s", firstKeptEntryId: "k", tokensBefore: 100 });
writeFileSync(path, whole, { mode: 0o600 });
truncateSync(path, Buffer.byteLength(whole) + 2 * (1 << 20));
assert.deepEqual(await ask(host.call, A, "one"), { body: "one", before: [] });
await until(() => events(f, "end").length === 1, 30000);
await settle(() => counted(f).length >= 2);
assert.deepEqual(counted(f), [[D, 1, false, "not-collected"], [D, 1, false, "not-collected"]]);
assert.deepEqual(host.logs.filter((l) => l.startsWith("system config: ")), []);
setting("x");
await host.close();
host = await start(t, prepared);
assert.deepEqual(host.logs.filter((l) => l.startsWith("system config: ")), [
'system config: compactionReadBytes "x" isn\'t a whole number of bytes from 1048576 to 1073741824; compaction collections read up to the default 67108864',
]);
assert.deepEqual(await ask(host.call, A, "two"), { body: "two", before: ["one"] });
await until(() => events(f, "end").length === 2, 30000);
await settle(() => counted(f).length >= 4);
assert.deepEqual(counted(f).slice(2), [[D, 1, true, null], [D, 1, true, null]]);
});
test("REQ-THREAD-2 T3 the stream cap is the system config's compactionStreams, and a bad value takes the default", { skip, timeout: 90000 }, async (t) => {
const prepared = prepare(t);
const { f } = prepared;
const setting = (v) => {
const doc = JSON.parse(readFileSync(f.config, "utf8"));
doc.compactionStreams = v;
writeFileSync(f.config, JSON.stringify(doc));
};
setting(32);
let host = await start(t, prepared);
const project = host.call("project.create", { name: "stack work", repo: "stack" }).id;
const A = host.call("thread.create", { project, name: "alpha" }).id;
const [{ id: D }] = host.call("session.list", { thread: A });
// 33 empty streams that sort before pi.jsonl: 34 in all, past a cap of 32, so pi.jsonl and its
// c0 aren't opened. The default cap opens all 34.
const store = sessionStore(f.dataRoot, "acme", A, D);
for (let i = 0; i < 33; i++) writeFileSync(join(store.session, `e${String(i).padStart(2, "0")}.jsonl`), "", { mode: 0o600 });
writeFileSync(join(store.session, "pi.jsonl"), line({ type: "session", version: 3, id: "pi", timestamp: "2026-10-22T11:00:00.000Z", cwd: "/w" }) + line({ type: "compaction", id: "c0", parentId: null, timestamp: "2026-10-22T10:59:00.000Z", summary: "s", firstKeptEntryId: "k", tokensBefore: 100 }), { mode: 0o600 });
assert.deepEqual(await ask(host.call, A, "one"), { body: "one", before: [] });
await until(() => events(f, "end").length === 1, 30000);
await settle(() => counted(f).length >= 2);
assert.deepEqual(counted(f), [[D, null, false, "not-collected"], [D, null, false, "not-collected"]]);
assert.deepEqual(host.logs.filter((l) => l.startsWith("system config: ")), []);
setting("x");
await host.close();
host = await start(t, prepared);
assert.deepEqual(host.logs.filter((l) => l.startsWith("system config: ")), [
'system config: compactionStreams "x" isn\'t a whole number from 32 to 65536; compaction collections open up to the default 1024 streams',
]);
assert.deepEqual(await ask(host.call, A, "two"), { body: "two", before: ["one"] });
await until(() => events(f, "end").length === 2, 30000);
await settle(() => counted(f).length >= 4);
assert.deepEqual(counted(f).slice(2), [[D, 1, true, null], [D, 1, true, null]]);
});
test("REQ-THREAD-2 T3 T4 a clear leaves the retired session's count as it was, its successor counts from 0, and a legacy session reads the thread's ledger", { skip, timeout: 90000 }, async (t) => {
const prepared = prepare(t);
const { f } = prepared;
let host = await start(t, prepared);
const project = host.call("project.create", { name: "stack work", repo: "stack" }).id;
const A = host.call("thread.create", { project, name: "alpha" }).id;
const [{ id: D }] = host.call("session.list", { thread: A });
assert.deepEqual(await ask(host.call, A, "compact c1"), { body: "compact c1", before: [] });
await until(() => events(f, "end").length === 1, 30000);
await settle(() => counted(f).length >= 2);
assert.deepEqual(counted(f), [[D, 0, true, null], [D, 1, true, null]]);
// The run has ended, so the clear retires D at once and starts the successor, whose run-start
// collection is its first.
const S = host.call("session.clear", { thread: A, session: D, revision: 0, confirm: true, op: "clear-1" }).successor;
const done = await settle(() => {
const e = host.call("session.operation", { thread: A, op: "clear-1" });
return e.start === "started" && e;
}, 30000);
assert.deepEqual([done?.state, done?.start], ["retired", "started"]);
await settle(() => counted(f).some(([s]) => s === S));
assert.deepEqual(counted(f).filter(([s]) => s === S)[0], [S, 0, true, null]);
const list = () => Object.fromEntries(host.call("session.list", { thread: A }).map((r) => [r.id, r.compactions && { count: r.compactions.count, known: r.compactions.known, reason: r.compactions.reason }]));
assert.deepEqual(list(), { [D]: { count: 1, known: true, reason: null }, [S]: { count: 0, known: true, reason: null } });
// A legacy session (op legacy-3l, from the 3k-3l migration): its store is the thread's, made by
// hand as packages/bus/tests/session-compactions.test.mjs does, with a ledger the launcher's
// collect wrote.
await until(() => events(f, "end").length === 2, 30000);
await host.close();
const L = "4b1c3a52-9a51-4d7e-8f2e-6c0a1b2d3e4f";
const seeded = new Store(f.dataRoot);
const at = "2026-10-21T00:00:00.000Z";
seeded.run("INSERT INTO threads(id,at,business,project,instance,owner,name) VALUES(?,?,?,?,?,?,?)", L, at, "acme", project, "assistant", f.doc.human, "legacy");
seeded.run("INSERT INTO sessions(at,business,thread,id,instance,owner,name,op) SELECT at,business,id,id,instance,owner,'default','legacy-3l' FROM threads WHERE id=?", L);
seeded.run("INSERT INTO session_defaults(at,thread,session,by) SELECT at,id,id,'migration-3l' FROM threads WHERE id=?", L);
seeded.close();
const legacy = sessionStore(f.dataRoot, "acme", L, L, { legacy: true });
writeFileSync(join(legacy.session, "pi.jsonl"), line({ type: "session", version: 3, id: "pi", timestamp: "2026-10-22T11:00:00.000Z", cwd: "/w" }) + line({ type: "compaction", id: "c7", parentId: null, timestamp: "2026-10-22T11:07:00.000Z", summary: "s", firstKeptEntryId: "k", tokensBefore: 100 }), { mode: 0o600 });
assert.equal(collectCompactions(legacy, { name: "pi", compactions: pi }).count, 1);
host = await start(t, prepared);
const [row] = host.call("session.list", { thread: L });
assert.equal(row.id, L);
assert.deepEqual({ ...row.compactions, at: typeof row.compactions?.at }, { count: 1, known: true, reason: null, at: "string" });
assert.deepEqual(list(), { [D]: { count: 1, known: true, reason: null }, [S]: { count: 0, known: true, reason: null } });
});