docs(slice1): filbert row 39 S4 round 2 review record (approve)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-08 20:16:18 -05:00
co-authored by Claude Opus 5.5
parent 1608e3da45
commit 2de56a471c
40 changed files with 2613 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Sequential gate; $1 = tree, $2 = out prefix
T="$1"; P="$2"; O=~/filbert-scratch/r39b/out; cd "$T"
for p in cli bus business discord; do
[ -d packages/$p/tests ] || continue
env -u NODE_TEST_CONTEXT node --test "packages/$p/tests/*.test.mjs" > "$O/$P-node-$p.txt" 2>&1; echo "$P node-$p exit $?"
done
for s in scripts/test-*.sh; do
n=$(basename "$s" .sh); n=${n#test-}
env -u NODE_TEST_CONTEXT DOCKER_HOST=unix:///nonexistent-filbert-r39b.sock timeout 900 bash "$s" > "$O/$P-suite-$n.txt" 2>&1; echo "$P suite-$n exit $? load $(cut -d" " -f1 /proc/loadavg)"
done
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# Row 39 round 2 mutants: Filbert's round 1 set (M1-M34), Rocko's round 2
# set (R5-F3, from agents/rocko/work/slice1-s4/mutants-r2.sh) and N1-N6.
# One perl substitution each, restored after its run. A run counts as
# killed when any test fails or is cancelled; --test-timeout turns a hang
# (M19 waits on a readline prompt) into a cancellation.
# Usage: mutants.sh <tree> <outdir>
set -u
T="$1"; O="$2"; cd "$T"
run() {
local id="$1" file="$2" expr="$3"
cp "$file" "$file.orig"
perl -0pi -e "$expr" "$file"
if cmp -s "$file" "$file.orig"; then echo "$id NOT-APPLIED $file"; mv "$file.orig" "$file"; return; fi
env -u NODE_TEST_CONTEXT timeout 900 node --test --test-timeout=90000 'packages/cli/tests/*.test.mjs' 'packages/discord/tests/*.test.mjs' > "$O/mut-$id.txt" 2>&1
local rc=$? f c
f=$(grep -E '^ℹ fail ' "$O/mut-$id.txt" | awk '{print $3}')
c=$(grep -E '^ℹ cancelled ' "$O/mut-$id.txt" | awk '{print $3}')
if [ "$rc" = 124 ]; then echo "$id killed (hang, outer timeout)"
elif [ "${f:-?}" = 0 ] && [ "${c:-?}" = 0 ]; then echo "$id SURVIVED"
else echo "$id killed (fail ${f:-?}, cancelled ${c:-?})"; fi
mv "$file.orig" "$file"
}
NT=packages/cli/src/notifier.mjs
run M1 $NT 's/if \(!d\.blocking \|\| journal\.sent\.has\(d\.id\)\) continue;/if (journal.sent.has(d.id)) continue;/'
run M2 $NT 's/hour >= DIGEST_HOUR/hour > DIGEST_HOUR/'
run M3 $NT 's/!journal\.days\.has\(day\) &&/true \&\&/'
run M4 $NT 's/return b !== undefined && t < b\.next;/return false;/'
run M5 $NT 's/if \(st\.uid !== process\.getuid\(\) \|\| \(st\.mode & 0o777\) !== 0o600\)/if (false)/'
run M6 $NT 's/const torn = lines\.pop\(\);/const torn = "";/'
run M7 $NT 's/export const ZONE = "America\/Chicago";/export const ZONE = "UTC";/'
run M8 $NT 's/export const dmNonce = \(id\) => `dm\$\{[^;]*;/export const dmNonce = (id) => "dmfixed";/'
run M9 $NT 's/\(dmSent\(d\.id\) \? "\[blocking, DM sent\] " : "\[blocking, DM pending\] "\)/"[blocking] "/'
run M10 $NT 's/Math\.min\(BACKOFF_MS \* 2 \*\* n, BACKOFF_MAX_MS\) \}\);/BACKOFF_MS });/'
run M11 packages/discord/src/notify.mjs 's/if \(err\.kind === "refused"\) channel = null;//'
run M12 packages/discord/src/notify.mjs 's/throw new RestOutcome\(err\.kind, `dm: \$\{err\.kind\}`/throw new RestOutcome(err.kind, err.message/'
run M13 packages/discord/src/notify.mjs 's/if \(binding\.dmRecipient === null\)[^\n]*\n//'
run M14 packages/discord/src/binding.mjs 's/if \(!users\.some\(\(u\) => u\.id === dmRecipient\)\)[^\n]*\n//'
run M15 packages/discord/src/binding.mjs 's/"tokenFile", "dmRecipient", "engine", "context"/"tokenFile", "engine", "context"/'
TR=packages/cli/src/transport.mjs
run M16 $TR 's/if \(found\.length > 0\) \{/if (false) {/'
run M17 $TR 's/"outcome-unknown": 1,/"outcome-unknown": 2,/'
run M18 $TR 's/if \(proc\.error \|\| proc\.status === null\)/if (proc.error)/'
CL=packages/cli/src/cli.mjs
run M19 $CL 's/if \(!io\.stdin\.isTTY\) throw usage/if (false) throw usage/'
run M20 $CL 's/if \(ref\.length < 8\)/if (ref.length < 1)/'
run M21 $CL 's/\|\| \(st\.mode & 0o777\) !== 0o600\)/)/'
run M22 $CL 's/if \(e\.code === "decision-closed"\)[^\n]*\n//'
run M23 $CL 's/if \(hits\.length > 1\)[^\n]*\n//'
run M24 $CL 's/if \(state\?\.live\) return state\.business;/if (state) return state.business;/'
CF=packages/cli/src/config.mjs
run M25 $CF 's/if \(named\.length > 1\)/if (named.length > 2)/'
run M26 $CF 's/if \(vars\["tracker\.project"\] !== undefined\) named\.push/named.push/'
HO=packages/cli/src/host.mjs
run M27 $HO 's/close\(1\);\n \};/close(0);\n };/'
run M28 $HO 's/if \(prior\?\.live\) throw/if (false) throw/'
run M29 $HO 's/if \(i < 0 \|\| argv\[i \+ 1\] !== "bus" \|\| argv\[i \+ 2\] !== "start"\)/if (false)/'
run M30 $HO 's/return fields\[0\] === "Z" \? null : fields\[19\];/return fields[19];/'
run M31 $HO 's/if \(ready\?\.ok !== true\) \{/if (ready?.ok === "never") {/'
run M32 packages/cli/src/format.mjs 's/if \(decision\?\.task_ref\) out\.push[^\n]*\n//'
run M33 packages/cli/src/notifier-process.mjs 's/process\.on\("SIGTERM", \(\) => stop\(0\)\);//'
run M34 packages/discord/src/rest.mjs 's/if \(typeof recipientId !== "string" \|\| !\/\^\[0-9\]\{17,20\}\$\/\.test\(recipientId\)\)/if (false)/'
run R5 $NT 's/if \(!st\.isFile\(\) \|\| st\.uid !== process\.getuid\(\) \|\| \(st\.mode & 0o777\) !== 0o600\)/if (false)/'
run R6 $NT 's/if \(end < bytes\.length\) \{/if (false) {/'
run R6b $NT 's/ftruncateSync\(fd, end\);//'
run R6c $NT 's/const name = copyTorn\(dir, bytes\.subarray\(end\), now\(\)\);/const name = "none";/'
run F1 $NT 's/if \(!ds\.isDirectory\(\) \|\| ds\.uid !== process\.getuid\(\) \|\| \(ds\.mode & 0o077\) !== 0\)/if (false)/'
run J3 $NT 's/O_RDWR \| O_APPEND \| O_CREAT \| O_NOFOLLOW/O_RDWR | O_APPEND | O_CREAT/'
run D1 $NT 's/hash23\(`\$\{business\}\\n\$\{day\}`\)/hash23(day)/'
run RACE packages/cli/src/host.mjs 's/if \(child\.exitCode !== null \|\| child\.signalCode !== null\) onDeath\(name, child\.exitCode, child\.signalCode\);\n\s*else //'
run F3 packages/cli/systemd/mosaic-bus.service.in 's/\[Unit\]\n/[Unit]\nAfter=network-online.target\n/'
# Round 2, Filbert
run N1 $NT 's/O_WRONLY \| O_CREAT \| constants\.O_EXCL \| O_NOFOLLOW/O_WRONLY | O_CREAT | O_NOFOLLOW/'
run N2 $NT 's/lstatSync, mkdirSync/lstatSync, statSync, mkdirSync/; s/const ds = lstatSync\(dir\);/const ds = statSync(dir);/'
run N3 $NT 's/const dfd = openSync\(dir, constants\.O_RDONLY\);\n try \{\n fsyncSync\(dfd\);/const dfd = openSync(dir, constants.O_RDONLY);\n try {\n 0;/'
run N4 $NT 's/openSync\(file, O_WRONLY \| O_APPEND \| O_NOFOLLOW\)/openSync(file, O_WRONLY | O_APPEND)/'
run N5 packages/cli/src/host.mjs 's/if \(child\.exitCode !== null \|\| child\.signalCode !== null\) onDeath/if (child.exitCode !== null) onDeath/'
run N6 $NT 's/ftruncateSync\(fd, end\);/ftruncateSync(fd, end > 0 ? end - 1 : 0);/'
@@ -0,0 +1,41 @@
// Filbert, row 39 r2: notify journal edge cases against the candidate.
// Usage: node probe.mjs <candidate root>
import { appendFileSync, chmodSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
const { openJournal, journalPath } = await import(process.argv[2] + "/packages/cli/src/notifier.mjs");
const scratch = join(homedir(), "filbert-scratch", "r39b", "probe", "tmp");
mkdirSync(scratch, { recursive: true });
const fresh = () => journalPath(mkdtempSync(join(scratch, "j-")), "demo");
const torn = (f) => readdirSync(dirname(f)).filter((n) => n.startsWith("torn-")).sort();
const tryOpen = (f, o) => { try { return openJournal(f, o); } catch (e) { return { error: `${e.exitCode ?? e.code}: ${e.message.replace(scratch, "<s>")}` }; } };
const rec = (d) => JSON.stringify({ at: "x", kind: "dm", decision: d, outcome: "confirmed", messageId: "1" });
// T1: a confirmed record whose newline never landed is treated as torn: copied out, dropped, so the decision is DM'd again.
{ const f = fresh(); openJournal(f).append(JSON.parse(rec("a"))); appendFileSync(f, rec("b"));
const logs = []; const j = openJournal(f, { log: (l) => logs.push(l) });
console.log("T1 sent after open", JSON.stringify([...j.sent]), "torn copies", torn(f).length, "logs", logs.length); }
// T2: the round 1 B1 sequence: torn tail, append, reopen.
{ const f = fresh(); openJournal(f).append(JSON.parse(rec("a"))); appendFileSync(f, '{"at":"x","kind":"dm","dec');
openJournal(f).append(JSON.parse(rec("b"))); const r = tryOpen(f);
console.log("T2 reopen", r.error ?? JSON.stringify([...r.sent])); }
// T3: torn tail plus a malformed middle line: refuses, file and directory untouched.
{ const f = fresh(); openJournal(f); writeFileSync(f, `${rec("a")}\nnot json\n${rec("b").slice(0, 10)}`); const before = readFileSync(f, "utf8");
console.log("T3", tryOpen(f).error, "| unchanged", readFileSync(f, "utf8") === before, "| torn copies", torn(f).length); }
// T4: torn tail with a loose file mode: refuses before any repair.
{ const f = fresh(); openJournal(f); writeFileSync(f, "{\"a"); chmodSync(f, 0o644);
console.log("T4", tryOpen(f).error, "| torn copies", torn(f).length); }
// T5: the journal directory is a symlink to a 0700 directory.
{ const root = mkdtempSync(join(scratch, "j-")); const real = join(root, "real"); mkdirSync(real, { mode: 0o700 });
mkdirSync(join(root, "notify"), { mode: 0o700 }); symlinkSync(real, join(root, "notify", "demo"));
console.log("T5", tryOpen(journalPath(root, "demo")).error ?? "accepted"); }
// T6: three crashes between copy and truncate, then a clean open: one copy per attempt, none overwritten.
{ const f = fresh(); openJournal(f); writeFileSync(f, "{\"torn"); const now = () => new Date("2026-10-09T01:00:00Z");
for (let i = 0; i < 3; i++) tryOpen(f, { now, log: () => { throw new Error("crash"); } });
const j = openJournal(f, { now }); console.log("T6 copies", JSON.stringify(torn(f)), "journal bytes", readFileSync(f).length, "sent", j.sent.size); }
// T7: a torn tail in a directory the user cannot write (0500): what refuses, and how.
{ const f = fresh(); openJournal(f); writeFileSync(f, "{\"torn"); chmodSync(dirname(f), 0o500);
console.log("T7", tryOpen(f).error ?? "opened", "| journal", JSON.stringify(readFileSync(f, "utf8"))); chmodSync(dirname(f), 0o700); }
// T8: a blank complete line (e.g. "\n\n" from a hand edit).
{ const f = fresh(); openJournal(f); writeFileSync(f, `${rec("a")}\n\n`); console.log("T8", tryOpen(f).error ?? "opened"); }
rmSync(scratch, { recursive: true, force: true });
@@ -0,0 +1,66 @@
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (217.310414ms)
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (303.537957ms)
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (253.286695ms)
✔ launch events require a human CLI capability; generic emit cannot forge authority events (160.225541ms)
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (162.19786ms)
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (128.801264ms)
✔ task action subjects and linked decision trail are complete and ordered (149.430351ms)
✔ launch binding is durable and reconnecting requires the identical trusted record (86.051671ms)
✔ business isolation includes inherited object names and cross-business message references (157.309435ms)
✔ authority never transfers between action, run, target, unresolved or replaced role holder (223.035126ms)
✔ task projection uses schema current view, skipping earlier and equal-start polls (210.412718ms)
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (191.349274ms)
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (222.442927ms)
✔ both arbiters require human resolution when their cross-role route is themselves (195.17119ms)
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.246744ms)
✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.360273ms)
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (1.628386ms)
✔ expiry refuses use and env references never become client data (0.592553ms)
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.278882ms)
✔ opaque tokens shorter than 16 characters refuse before use (0.323611ms)
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.685349ms)
✔ process reader gets own kernel identity without exposing environment values (1.400191ms)
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.8581ms)
✔ real pid 1 remains inspectable when its environment is protected (0.305429ms)
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (260.83156ms)
✔ missing and foreign-business citations refuse and roll back message and grant (226.690882ms)
✔ cross-role sends still need a matching resolved decision and consume it once (233.511399ms)
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (248.645504ms)
✔ startup token refusal returns safe code without value or partial listening broker (41.71346ms)
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (198.391657ms)
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (157.488841ms)
✔ trusted host registers later launches; socket clients never have a registration verb (173.653461ms)
✔ runtime excludes declared project roots even when host supplies no repoRoots (37.15333ms)
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (165.733757ms)
✔ v3b prototype refusals, views and append-only mutations (1094.861222ms)
✔ gated approval authorizes once, survives store reopen, and fresh approval works (343.625879ms)
✔ another run cannot consume an approval; a failed check leaves it usable (233.997111ms)
✔ two scheduled callers have exactly one grant and one consumed refusal (150.27752ms)
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (294.068958ms)
✔ class drift gated to cross-role refuses before consumption (169.324257ms)
✔ class drift cross-role to gated refuses before consumption (181.47066ms)
✔ class drift gated to within-role refuses before consumption (174.542442ms)
✔ class drift cross-role to within-role refuses before consumption (182.622361ms)
✔ class drift within-role to gated refuses before consumption (167.596658ms)
✔ class drift within-role to cross-role refuses before consumption (237.783601ms)
✔ message.send consumes approval and prevents a later send or authorize (307.546305ms)
✔ role.revoke consumes approval and prevents a later revoke or authorize (211.423445ms)
✔ creates private WAL store and excludes a second writer until explicit close (171.415558ms)
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (234.689651ms)
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (188.953806ms)
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (164.016855ms)
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (86.356441ms)
✔ async transactions refuse before invoking their function (83.160309ms)
✔ socket capability stamps launch identity; shared views use wire, no SQL client (205.097157ms)
✔ two wire claims serialize; a lost reply never automatically retries (215.068339ms)
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (127.551648ms)
✔ client preserves UTF-8 when a response divides a multibyte character (11.827025ms)
✔ committed mutation followed by dropped reply reports unknown and is never retried (118.23409ms)
ℹ tests 58
ℹ suites 0
ℹ pass 58
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2739.556368
@@ -0,0 +1,68 @@
✔ config directory and file path follow MOSAIC_CONFIG (1.261412ms)
✔ the fixture business validates and comes back frozen (3.486499ms)
✔ two instances may share a definition (1.135312ms)
✔ top-level refusals (4.052468ms)
✔ arbiters and projects (5.50727ms)
✔ role instances (2.908438ms)
✔ Vikunja bots (5.357349ms)
✔ a role without Vikunja takes no tracker block (1.918244ms)
✔ credential references match the definition's services (2.41167ms)
✔ launch (7.652372ms)
✔ loadBusiness: file checks (1.760046ms)
✔ loadBusiness: not a regular file (43.811309ms)
✔ loading writes nothing (1.147463ms)
✔ names that are Object.prototype properties don't count as declared (3.088735ms)
✔ the shipped example refuses as written and validates once filled in (0.806055ms)
✔ usage errors exit 4 (281.842061ms)
✔ validate: a good business exits 0 and prints instance digests (65.70548ms)
✔ validate: project files (343.187247ms)
✔ validate: missing files and a broken system config (246.116384ms)
✔ validate: credential reference problems exit 2 and name each one (68.667591ms)
✔ validate: a token file inside the repository is refused (67.922015ms)
✔ validate: role definitions come from MOSAIC_ROLES_DIR (214.225914ms)
✔ resolve: prints one instance's record (208.183465ms)
✔ resolve: refusals (390.711309ms)
✔ parse: exactly one of file or env, plus the service's date (2.300609ms)
✔ check: a good file has no problems (0.624398ms)
✔ check never opens the file: a write-only token passes (0.309092ms)
✔ check: file problems (0.677698ms)
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (0.619586ms)
✔ check: dates and environment references (0.321745ms)
✔ path and load (1.70873ms)
✔ refusals (1.06147ms)
✔ systemVars flattens the validated config (1.602672ms)
✔ precedence: system, business, project, project role, agent (4.096363ms)
✔ limits narrow the definition and never widen it (1.855891ms)
✔ role.launch stays within-role only for the instance the launch block names (3.74717ms)
✔ limits.authority without role.launch leaves the launcher with no launch block (1.445106ms)
✔ limits.authority narrows cross-role actions too (0.934994ms)
✔ classify (1.225729ms)
✔ the record carries what the broker and launcher need (0.879546ms)
✔ digest: key order doesn't matter, any value change does (5.684839ms)
✔ refusals (1.786089ms)
✔ the four shipped version 2 roles load (2.972336ms)
✔ shipped role scopes match addendum B section 2 and the SR runbook (1.142959ms)
✔ shipped authority follows the note's table (0.654999ms)
✔ version 1 files keep loading with no authority (0.911567ms)
✔ the conductor policy isn't a role (0.193819ms)
✔ a missing role file is exit 4, a symbolic link too (0.337323ms)
✔ version 2 refusals (1.328539ms)
✔ authority: closed vocabulary, no gated-only action, no overlap (2.030509ms)
✔ credentials: Gitea scopes (0.828626ms)
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (1.097888ms)
✔ credentials: services (0.512348ms)
✔ contract: a non-empty regular Markdown file beside the role file (0.606346ms)
✔ every key names known layers and a merge rule (0.66841ms)
✔ unknown keys and wrong layers refuse (0.557532ms)
✔ types (1.552146ms)
✔ merge: defaults, then the most specific layer wins (0.269319ms)
✔ merge: limits only narrow, and provenance lists each source (0.340672ms)
✔ merge doesn't change its inputs (0.136302ms)
ℹ tests 60
ℹ suites 0
ℹ pass 60
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1946.375781
@@ -0,0 +1,181 @@
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.311357ms)
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.701935ms)
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.666129ms)
✔ approvals: a button approves only on its own request message with the matching custom id (0.466461ms)
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (16.530003ms)
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (8.97202ms)
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (9.876476ms)
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (3.614364ms)
✔ authorize: open channel, listed user (1.918309ms)
✔ authorize: wrong guild (0.209469ms)
✔ authorize: no guild (DM) (0.183666ms)
✔ authorize: unlisted channel (0.244643ms)
✔ authorize: unknown channel, no info (0.196908ms)
✔ authorize: thread of listed parent (0.202889ms)
✔ authorize: thread of unlisted parent (0.191015ms)
✔ authorize: text channel that is not a thread and not listed (0.153491ms)
✔ authorize: unlisted user (0.204601ms)
✔ authorize: no author (0.916465ms)
✔ authorize: bot author (listed id, bot flag) (0.173926ms)
✔ authorize: system author (0.120911ms)
✔ authorize: the bot itself (0.10735ms)
✔ authorize: webhook (0.100761ms)
✔ authorize: mention channel without mention (0.140699ms)
✔ authorize: mention channel with bot mention (0.151843ms)
✔ authorize: mention channel with @everyone only (0.167888ms)
✔ authorize: mention channel mentioning someone else (0.089486ms)
✔ authorize: mention channel, content says @bot but mentions empty (0.089341ms)
✔ authorize: private thread under mention channel, mentioned (0.084556ms)
✔ authorize: private thread under mention channel, not mentioned (0.05975ms)
✔ authorize: thread in another guild per channel info (0.069707ms)
✔ authorize: not an object (0.061778ms)
✔ authorize: no id (0.07487ms)
✔ authorize: oversize content is accepted and flagged (0.075356ms)
✔ authorize: exactly the limit is not oversize (0.081014ms)
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.486988ms)
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.204973ms)
✔ binding: a complete binding validates and is frozen (2.928165ms)
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.414856ms)
✔ binding: empty allowlists refuse (0.496834ms)
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.373232ms)
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.044495ms)
✔ binding: file must be 0600, regular, not a symlink (1.679612ms)
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (2.276414ms)
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (102.613686ms)
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.50081ms)
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (354.541628ms)
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (214.315306ms)
✔ cli: run refuses when STOP is present, before any network use (121.851384ms)
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.735489ms)
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.184618ms)
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (18.267254ms)
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (16.980222ms)
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.475552ms)
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.498543ms)
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.464203ms)
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.505835ms)
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.203656ms)
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.334043ms)
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (0.96087ms)
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.052453ms)
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (41.357704ms)
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.2268ms)
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (5.648472ms)
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.041924ms)
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.401109ms)
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.260933ms)
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.889649ms)
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.694598ms)
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (1.741159ms)
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.260447ms)
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.2361ms)
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.330372ms)
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (2.893545ms)
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.642891ms)
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.830227ms)
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (1.927868ms)
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.4971ms)
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.540519ms)
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.506593ms)
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.924662ms)
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.701325ms)
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.471719ms)
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (67.931376ms)
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (45.990579ms)
✔ engine: one prompt, one turn, text and usage come back (35.765858ms)
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (353.640991ms)
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (231.414967ms)
✔ engine: timeout sends abort and fails only that turn; the process stays (103.36315ms)
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (233.842944ms)
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (131.995471ms)
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (213.759436ms)
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (616.153458ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.412333ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.447298ms)
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (427.505852ms)
✔ engine: a malformed JSONL line fails the turn, not the process (25.976253ms)
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (48.518719ms)
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.727944ms)
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.902931ms)
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.711452ms)
✔ gateway: op 9 resumable resumes (0.386186ms)
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.977284ms)
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.593665ms)
✔ gateway: close() is final and unparseable frames are ignored (0.626696ms)
✔ git: config validation is strict, needs write: true, a work tree and a private token file (75.657153ms)
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (45.88357ms)
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (87.721754ms)
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.302868ms)
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (88.384999ms)
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (90.045981ms)
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (103.560019ms)
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (206.388678ms)
✔ git: push pushes the named branch only and reports up to date (65.616181ms)
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (92.830705ms)
✔ git: the credential helper answers get over https from a private file and nothing else (213.591577ms)
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (837.248736ms)
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.220185ms)
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (4.518775ms)
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.422894ms)
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (4.582692ms)
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (63.8715ms)
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (344.239416ms)
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.336098ms)
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (0.362061ms)
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.76214ms)
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (39.224352ms)
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (129.362888ms)
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (63.552354ms)
✔ notices: a kind is recorded per UTC day and found again (0.51231ms)
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (53.144558ms)
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (47.151395ms)
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (32.2825ms)
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (82.540159ms)
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (679.325926ms)
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.657912ms)
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (6.281283ms)
✔ rest: content and nonce limits are enforced locally; typing never throws (3.152613ms)
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.761836ms)
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.390986ms)
✔ setspark config: reaches the tools config and the binding as a fixed key (2.644296ms)
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.538722ms)
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.010351ms)
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (40.502527ms)
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (9.130128ms)
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (5.5993ms)
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.048084ms)
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.504142ms)
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.120153ms)
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1009.602815ms)
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.504453ms)
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.184188ms)
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.273923ms)
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.202957ms)
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.283008ms)
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.339522ms)
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.333603ms)
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (4.855722ms)
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.752295ms)
✔ tools: listing and search caps hold (21.069267ms)
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.870034ms)
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (8.328982ms)
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.063217ms)
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.163075ms)
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.921007ms)
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.552488ms)
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.46398ms)
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.339588ms)
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.329664ms)
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1033.562992ms)
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.6957ms)
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.268031ms)
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.797272ms)
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.368338ms)
ℹ tests 173
ℹ suites 0
ℹ pass 173
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2671.315407
@@ -0,0 +1,17 @@
OK status with missing harness credential exits 3 and still lists accounts
OK status reports harness credential (read-only) + mosaic accounts
OK api key material never reaches output
OK oauth token material never reaches output
OK unparseable credential file exits 2
OK symlinked credential file exits 4
OK env-side credential names reported
OK env var values never reach output
OK accounts without an accounts dir reports none and creates nothing
OK accounts lists files and marks the active one
OK loose account perms flagged in listing
OK agent --auth with missing account file refuses (exit 4)
OK agent --auth with non-0600 account file refuses
OK agent --auth with invalid account name refuses
OK auth.sh without valid config refuses
selftest: 15 passed, 0 failed
@@ -0,0 +1,55 @@
Note: switching to 'b9b6cf008a8d7083676fc4a2760ae50fd43ad9b0'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
Not currently on any branch.
nothing to commit, working tree clean
Note: switching to 'b9b6cf008a8d7083676fc4a2760ae50fd43ad9b0'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
OK dry-run: allowed change, exit 0, nothing committed (exit 0)
OK dry-run committed nothing
OK apply: allowed change exits 0 (exit 0)
OK apply: attribution in commit subject
OK apply: target tree clean after commit
OK disallowed path refused (exit 1)
OK disallowed path: target untouched
OK syntax gate refused broken .mjs (exit 1)
OK syntax gate: target untouched
OK suite failure refused (exit 1)
OK suite failure: target reverted to clean
OK disabled policy refused (exit 2)
OK disabled policy: target untouched
OK failed run refused (exit 1)
OK failed run: target untouched
OK missing run exits 4 (exit 4)
OK invalid policy exits 2 (exit 2)
selftest: 17 passed, 0 failed
@@ -0,0 +1,26 @@
OK absent adapter defaults to pi
OK adapter mock validates (exit 0)
OK unsupported adapter exits 2 (exit 2)
OK env exports adapter
OK bootstrap creates default when absent (exit 0)
OK bootstrap wrote config file
OK bootstrap is idempotent on existing config (exit 0)
OK bootstrap did not rewrite existing config
OK validate missing config exits 3 (exit 3)
OK malformed JSON exits 2 (exit 2)
OK unsupported configVersion exits 2 (exit 2)
OK unknown top-level key exits 2 (exit 2)
OK unknown execution key exits 2 (exit 2)
OK unsupported backend exits 2 (exit 2)
OK unsupported environment exits 2 (exit 2)
OK relative dataRoot exits 2 (exit 2)
OK non-canonical dataRoot exits 2 (exit 2)
OK filesystem root dataRoot exits 2 (exit 2)
OK home directory dataRoot exits 2 (exit 2)
OK dataRoot containing config dir exits 2 (exit 2)
OK control character in provider exits 2 (exit 2)
OK symlinked config file exits 2 (exit 2)
OK env exports resolve correctly
OK failed validation modified nothing
selftest: 24 passed, 0 failed
@@ -0,0 +1,68 @@
toolchain: node v26.8.1
OK syntax: packages/discord/src/approvals.mjs
OK syntax: packages/discord/src/authorize.mjs
OK syntax: packages/discord/src/binding.mjs
OK syntax: packages/discord/src/cli.mjs
OK syntax: packages/discord/src/connector.mjs
OK syntax: packages/discord/src/context.mjs
OK syntax: packages/discord/src/engine-pi.mjs
OK syntax: packages/discord/src/errors.mjs
OK syntax: packages/discord/src/gateway.mjs
OK syntax: packages/discord/src/git.mjs
OK syntax: packages/discord/src/journal.mjs
OK syntax: packages/discord/src/rest.mjs
OK syntax: packages/discord/src/setspark.mjs
OK syntax: packages/discord/src/tools.mjs
OK syntax: packages/discord/src/web.mjs
OK syntax: packages/discord/bin/git-credential.mjs
OK syntax: packages/discord/extension/tools.mjs
OK syntax: packages/discord/tests/approvals.test.mjs
OK syntax: packages/discord/tests/authorize.test.mjs
OK syntax: packages/discord/tests/binding.test.mjs
OK syntax: packages/discord/tests/connector.test.mjs
OK syntax: packages/discord/tests/context.test.mjs
OK syntax: packages/discord/tests/engine.test.mjs
OK syntax: packages/discord/tests/fake-pi.mjs
OK syntax: packages/discord/tests/gateway.test.mjs
OK syntax: packages/discord/tests/git.test.mjs
OK syntax: packages/discord/tests/helpers.mjs
OK syntax: packages/discord/tests/journal.test.mjs
OK syntax: packages/discord/tests/recover.test.mjs
OK syntax: packages/discord/tests/rest.test.mjs
OK syntax: packages/discord/tests/setspark.test.mjs
OK syntax: packages/discord/tests/tools.test.mjs
OK syntax: packages/discord/tests/web.test.mjs
OK syntax: packages/discord/fixtures/claim-worker.mjs
OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs
OK syntax: scripts/discord.sh
OK syntax: scripts/discord-service.sh
OK packages/discord declares no dependencies
OK no bot-token-shaped string in packages/discord
OK fixture binding uses placeholder ids only
OK fixture binding validates
OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool
OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start
OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root
OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token
OK real pi with protocol vault adds reserve_id to the git verbs
OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key
OK real pi refuses a git key on a read-only root (fail closed)
OK real pi with the pilot flags (--no-tools) exposes no tool at all
OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed)
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test packages/discord/tests/ (ℹ pass 173)
OK scripts/discord.sh --help exits 0
OK scripts/discord.sh check without a binding exits 4
OK scripts/discord.sh recover without a binding exits 4
OK scripts/discord.sh reload without a binding exits 4
OK scripts/discord-service.sh without a command exits 4
OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP
OK service install writes the rendered unit (0644) and leaves no temp file
OK service install a second time reports unchanged
OK systemd-analyze verify accepts the rendered unit
OK service uninstall removes the unit file
OK service install with an unknown flag exits 4
OK service install with USER unset finishes and names the account for lingering
discord suite: 64 passed, 0 failed
@@ -0,0 +1,21 @@
OK initial ordinary-file install
OK installed tree matches canonical source
OK installed tree has no symlinks
OK check detects installation drift
OK sync refuses to overwrite installation drift
OK check detects an extra destination file
OK check detects an extra destination directory
OK check rejects a destination symlink
OK sync accepts a canonical source update
OK updated installation matches canonical source
scripts/test-extension-package.sh: line 14: 192975 Killed "$@" > /dev/null 2>&1
OK forced interruption kills the replacing process
OK next invocation recovers old consistent installation
OK interrupted replacement rolled back
OK sync succeeds after interruption recovery
OK unlocked stale lock file does not block
OK active lock refuses a concurrent sync
OK source symlink fails closed
OK nested second entrypoint fails closed
extension package selftest: 18 passed, 0 failed
@@ -0,0 +1,53 @@
toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0
OK syntax: scripts/foundation-inspect.mjs
OK syntax: scripts/foundation/strict-json.mjs
OK syntax: scripts/foundation/canonical.mjs
OK syntax: scripts/foundation/resolve.mjs
OK syntax: scripts/foundation/validate-record.mjs
OK syntax: scripts/foundation/fixtures/build-fixtures.mjs
OK syntax: scripts/foundation/canonical.test.mjs
OK syntax: scripts/foundation/cli.test.mjs
OK syntax: scripts/foundation/fixtures.test.mjs
OK syntax: scripts/foundation/resolve.test.mjs
OK syntax: scripts/foundation/strict-json.test.mjs
OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written)
OK fixture generator runs
OK checked-in fixtures/bundles equal a fresh generation
OK checked-in fixtures/raw equal a fresh generation
OK checked-in fixtures/index.json equal a fresh generation
OK checked-in demo bundles equal a fresh generation
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test scripts/foundation/ (ℹ pass 80)
OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof)
platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999)
node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases)
schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10
profile column (schema-valid records only): profile-valid 510, profile-invalid 30
profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns
OK oracle: zero schema-column disagreements with the pinned checker
OK oracle: strict-only profile refusals are counted and asserted
OK demo: permitted read preview exits 0 (exit 0)
OK demo: permitted file.change preview exits 0 (exit 0)
OK demo: assignment.change proposal is unresolved (exit 3) (exit 3)
OK demo: revoked registration is refused (exit 3) (exit 3)
OK demo: message is not authority (exit 3) (exit 3)
OK usage: no arguments exits 2 (exit 2)
OK io: missing file exits 4 (exit 4)
OK io: directory exits 4 (exit 4)
OK io: symlink exits 4 (O_NOFOLLOW) (exit 4)
OK bound: oversize fixture exits 2 (exit 2)
OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2)
OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2)
OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0)
OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed
OK text output starts with the disclaimer
OK json output is valid JSON with result allowed and exactly the charter §7 fields
OK json golden matches byte-for-byte
OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal
OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs
OK canary never printed (bundle run and credential-file run)
OK a non-bundle JSON file is refused at the shape gate, not read into output
OK no field of the non-bundle file is echoed
selftest: 44 passed, 0 failed
@@ -0,0 +1,35 @@
toolchain: node v26.8.1, git version 2.55.0
OK syntax: packages/queue/src/cli.mjs
OK syntax: packages/queue/src/errors.mjs
OK syntax: packages/queue/src/io.mjs
OK syntax: packages/queue/src/lock.mjs
OK syntax: packages/queue/src/queue.mjs
OK syntax: packages/queue/src/review.mjs
OK syntax: packages/queue/src/store.mjs
OK syntax: packages/queue/tests/commit.test.mjs
OK syntax: packages/queue/tests/data.test.mjs
OK syntax: packages/queue/tests/dispatch.test.mjs
OK syntax: packages/queue/tests/helpers.mjs
OK syntax: packages/queue/tests/lock.test.mjs
OK syntax: packages/queue/tests/migration.test.mjs
OK syntax: packages/queue/tests/review.test.mjs
OK syntax: packages/queue/tests/store.test.mjs
OK syntax: packages/queue/tests/write.test.mjs
OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs
OK syntax: packages/queue/tests/fixtures/kill-at.mjs
OK syntax: packages/queue/tests/fixtures/lock-child.mjs
OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh
OK syntax: scripts/queue-commit.sh
OK syntax: scripts/git-hooks/pre-commit
OK syntax: scripts/mosaic
OK queue-commit.sh, the guard and scripts/mosaic are executable
OK packages/queue declares no dependencies
ℹ tests 148
ℹ pass 148
ℹ fail 0
OK node --test packages/queue/tests/
OK scripts/mosaic queue help
skip queue verify and render --check: this checkout (/home/jwoltje/filbert-scratch/r39b/base) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
queue suite: 27 passed, 0 failed
@@ -0,0 +1,7 @@
OK valid RELEASE resolves (exit 0)
OK invalid RELEASE exits 1 (exit 1)
OK missing RELEASE exits 1 (exit 1)
OK valid RELEASE leaves image tag consistent with version
skip state-machine cases (docker daemon unavailable)
selftest: 4 passed, 0 failed
@@ -0,0 +1,33 @@
OK valid task validates (exit 0)
OK unknown task key exits 2 (exit 2)
OK unsupported taskVersion exits 2 (exit 2)
OK invalid task id exits 2 (exit 2)
OK empty prompt exits 2 (exit 2)
OK NUL in expectExact exits 2 (exit 2)
OK out-of-range timeout exits 2 (exit 2)
OK missing mission file exits 4 (exit 4)
OK task with valid mission validates (exit 0)
OK invalid mission exits 2 (exit 2)
OK validate missing task exits 4 (exit 4)
OK validation does not modify the task file
OK prune dry-run exits 0 (exit 0)
OK dry-run deleted nothing
OK prune --keep=2 --yes removes oldest (exit 0)
OK kept exactly 2 newest runs
OK newest run kept, oldest pruned
OK append-only receipt written (3 entries)
OK sessions/workspaces untouched by prune
OK prune with invalid keep exits 4 (exit 4)
skip adapter seam cases (docker daemon unavailable)
skip workspace/capability cases (docker daemon unavailable)
skip live task cases (docker unavailable)
OK onboard without name exits 4 (non-interactive) (exit 4)
OK onboard --name renders profile (exit 0)
OK profile written
OK canon structure: required filled, optional placeholdered
OK canon sections present
FAIL user recall run succeeds (exit 1)
FAIL recalled user name (response: )
OK no agent identity on headless run
selftest: 26 passed, 2 failed
@@ -0,0 +1,66 @@
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (139.654375ms)
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (247.063501ms)
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (228.702372ms)
✔ launch events require a human CLI capability; generic emit cannot forge authority events (152.589158ms)
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (154.294352ms)
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (128.295573ms)
✔ task action subjects and linked decision trail are complete and ordered (137.15036ms)
✔ launch binding is durable and reconnecting requires the identical trusted record (64.285079ms)
✔ business isolation includes inherited object names and cross-business message references (146.396237ms)
✔ authority never transfers between action, run, target, unresolved or replaced role holder (243.938983ms)
✔ task projection uses schema current view, skipping earlier and equal-start polls (94.170395ms)
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (161.232705ms)
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (101.449267ms)
✔ both arbiters require human resolution when their cross-role route is themselves (201.868218ms)
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (1.792956ms)
✔ only validated broker references load; returned data and exceptions cannot expose a known token (3.145636ms)
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (1.852726ms)
✔ expiry refuses use and env references never become client data (1.151507ms)
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.274286ms)
✔ opaque tokens shorter than 16 characters refuse before use (0.249982ms)
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (1.518729ms)
✔ process reader gets own kernel identity without exposing environment values (1.029208ms)
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.603923ms)
✔ real pid 1 remains inspectable when its environment is protected (0.304814ms)
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (170.40665ms)
✔ missing and foreign-business citations refuse and roll back message and grant (167.569254ms)
✔ cross-role sends still need a matching resolved decision and consume it once (202.66731ms)
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (170.952763ms)
✔ startup token refusal returns safe code without value or partial listening broker (36.821787ms)
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (165.196727ms)
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (170.727787ms)
✔ trusted host registers later launches; socket clients never have a registration verb (164.592257ms)
✔ runtime excludes declared project roots even when host supplies no repoRoots (32.806422ms)
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (153.089197ms)
✔ v3b prototype refusals, views and append-only mutations (947.370703ms)
✔ gated approval authorizes once, survives store reopen, and fresh approval works (241.796595ms)
✔ another run cannot consume an approval; a failed check leaves it usable (198.279698ms)
✔ two scheduled callers have exactly one grant and one consumed refusal (141.759733ms)
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (271.282029ms)
✔ class drift gated to cross-role refuses before consumption (168.101142ms)
✔ class drift cross-role to gated refuses before consumption (161.738213ms)
✔ class drift gated to within-role refuses before consumption (157.073495ms)
✔ class drift cross-role to within-role refuses before consumption (206.009333ms)
✔ class drift within-role to gated refuses before consumption (144.380123ms)
✔ class drift within-role to cross-role refuses before consumption (121.658912ms)
✔ message.send consumes approval and prevents a later send or authorize (166.58839ms)
✔ role.revoke consumes approval and prevents a later revoke or authorize (221.729859ms)
✔ creates private WAL store and excludes a second writer until explicit close (115.826599ms)
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (156.703344ms)
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (170.059617ms)
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (152.580868ms)
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (92.676541ms)
✔ async transactions refuse before invoking their function (77.571318ms)
✔ socket capability stamps launch identity; shared views use wire, no SQL client (136.661151ms)
✔ two wire claims serialize; a lost reply never automatically retries (177.631582ms)
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (106.21344ms)
✔ client preserves UTF-8 when a response divides a multibyte character (12.320555ms)
✔ committed mutation followed by dropped reply reports unknown and is never retried (120.72807ms)
ℹ tests 58
ℹ suites 0
ℹ pass 58
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2281.14019
@@ -0,0 +1,68 @@
✔ config directory and file path follow MOSAIC_CONFIG (1.341014ms)
✔ the fixture business validates and comes back frozen (3.783753ms)
✔ two instances may share a definition (1.283933ms)
✔ top-level refusals (3.513758ms)
✔ arbiters and projects (6.980478ms)
✔ role instances (2.481886ms)
✔ Vikunja bots (5.977217ms)
✔ a role without Vikunja takes no tracker block (2.187817ms)
✔ credential references match the definition's services (2.491705ms)
✔ launch (7.223763ms)
✔ loadBusiness: file checks (1.762953ms)
✔ loadBusiness: not a regular file (41.927398ms)
✔ loading writes nothing (0.980735ms)
✔ names that are Object.prototype properties don't count as declared (2.715734ms)
✔ the shipped example refuses as written and validates once filled in (0.550139ms)
✔ usage errors exit 4 (284.740895ms)
✔ validate: a good business exits 0 and prints instance digests (63.480045ms)
✔ validate: project files (309.702262ms)
✔ validate: missing files and a broken system config (244.883943ms)
✔ validate: credential reference problems exit 2 and name each one (78.619778ms)
✔ validate: a token file inside the repository is refused (83.544263ms)
✔ validate: role definitions come from MOSAIC_ROLES_DIR (347.298703ms)
✔ resolve: prints one instance's record (220.944754ms)
✔ resolve: refusals (568.395168ms)
✔ parse: exactly one of file or env, plus the service's date (2.488417ms)
✔ check: a good file has no problems (0.727746ms)
✔ check never opens the file: a write-only token passes (0.321555ms)
✔ check: file problems (0.837081ms)
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (0.729554ms)
✔ check: dates and environment references (0.373624ms)
✔ path and load (2.09583ms)
✔ refusals (1.025402ms)
✔ systemVars flattens the validated config (1.734096ms)
✔ precedence: system, business, project, project role, agent (4.324111ms)
✔ limits narrow the definition and never widen it (2.066702ms)
✔ role.launch stays within-role only for the instance the launch block names (4.116684ms)
✔ limits.authority without role.launch leaves the launcher with no launch block (1.570418ms)
✔ limits.authority narrows cross-role actions too (1.247099ms)
✔ classify (1.04595ms)
✔ the record carries what the broker and launcher need (0.868636ms)
✔ digest: key order doesn't matter, any value change does (5.570897ms)
✔ refusals (1.953933ms)
✔ the four shipped version 2 roles load (3.020071ms)
✔ shipped role scopes match addendum B section 2 and the SR runbook (0.911031ms)
✔ shipped authority follows the note's table (0.503269ms)
✔ version 1 files keep loading with no authority (0.976922ms)
✔ the conductor policy isn't a role (0.20077ms)
✔ a missing role file is exit 4, a symbolic link too (0.346796ms)
✔ version 2 refusals (1.510228ms)
✔ authority: closed vocabulary, no gated-only action, no overlap (1.956643ms)
✔ credentials: Gitea scopes (0.842503ms)
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (0.965281ms)
✔ credentials: services (0.507288ms)
✔ contract: a non-empty regular Markdown file beside the role file (0.60658ms)
✔ every key names known layers and a merge rule (1.074622ms)
✔ unknown keys and wrong layers refuse (0.713932ms)
✔ types (1.651585ms)
✔ merge: defaults, then the most specific layer wins (0.235419ms)
✔ merge: limits only narrow, and provenance lists each source (0.392174ms)
✔ merge doesn't change its inputs (0.172929ms)
ℹ tests 60
ℹ suites 0
ℹ pass 60
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2270.726837
@@ -0,0 +1,57 @@
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (100.078936ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (107.319875ms)
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (99.730956ms)
✔ decide prints a declining choice as declining (90.510381ms)
✔ an unknown outcome is reported once and never resent (112.00473ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (108.740493ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (77.352582ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (45.635253ms)
✔ every human command refuses inside an agent run before it touches the bus (45.464533ms)
✔ usage errors exit 4; no business and no host is a usage error (45.159304ms)
✔ agents and tasks print through the broker (53.456446ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.27951ms)
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (37.49377ms)
✔ trackers come from the tracker.* variables of the one project that names a tracker project (36.732668ms)
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (36.58984ms)
✔ two projects that each name a tracker project refuse, since the boot shape holds one (28.492158ms)
✔ a business without tracker.baseUrl gets no trackers entry (29.24871ms)
✔ an unknown business and a broken system config refuse with exit 3 (55.884577ms)
✔ empty views say so (1.044953ms)
✔ the trail keeps the broker's order and names a decision's task without its rows (1.380369ms)
✔ tasks print the tracker fields the snapshot carries (0.224637ms)
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (859.449566ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (187.04901ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (173.229976ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (22.622042ms)
✔ bus stop refuses to signal a live pid that is not a bus host (202.886448ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (210.010048ms)
✔ bus start refuses with exit 3 without a notifier config (84.912019ms)
✔ bus start runs until bus stop; status reports it while it runs (650.965231ms)
✔ bus-service.sh renders the unit and installs it into a given directory (27.10127ms)
✔ zoned uses the IANA zone across DST (13.969441ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (94.732038ms)
✔ two blocking decisions get two DMs with different nonces (109.219033ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.214105ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (87.262675ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (91.765134ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (90.620084ms)
✔ an inbox read failure is logged and the next poll retries (0.714447ms)
✔ no Discord id reaches the journal or the log (97.129267ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (28.777778ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (29.548192ms)
✔ the journal: a whole file that is one torn line truncates to empty (14.661147ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.862905ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.5764ms)
✔ digest content stays within Discord's 2000 characters (0.315122ms)
✔ runLoop never overlaps ticks and stops after the one in flight (111.13999ms)
✔ the transport writes {business, verb, args} to the child and reads its JSON (33.566934ms)
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2140.027835ms)
✔ busExit and refuseInsideAgent (0.491405ms)
ℹ tests 49
ℹ suites 0
ℹ pass 49
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2517.736153
@@ -0,0 +1,186 @@
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.542746ms)
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.934868ms)
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (1.475863ms)
✔ approvals: a button approves only on its own request message with the matching custom id (0.538308ms)
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (25.448134ms)
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (11.685966ms)
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (10.841998ms)
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (5.170841ms)
✔ authorize: open channel, listed user (22.287105ms)
✔ authorize: wrong guild (0.267614ms)
✔ authorize: no guild (DM) (0.191727ms)
✔ authorize: unlisted channel (0.222855ms)
✔ authorize: unknown channel, no info (0.316141ms)
✔ authorize: thread of listed parent (0.250805ms)
✔ authorize: thread of unlisted parent (0.208692ms)
✔ authorize: text channel that is not a thread and not listed (0.149837ms)
✔ authorize: unlisted user (1.531999ms)
✔ authorize: no author (0.293545ms)
✔ authorize: bot author (listed id, bot flag) (0.132914ms)
✔ authorize: system author (2.617179ms)
✔ authorize: the bot itself (0.129547ms)
✔ authorize: webhook (0.690146ms)
✔ authorize: mention channel without mention (0.243579ms)
✔ authorize: mention channel with bot mention (0.238357ms)
✔ authorize: mention channel with @everyone only (0.236778ms)
✔ authorize: mention channel mentioning someone else (0.081781ms)
✔ authorize: mention channel, content says @bot but mentions empty (0.081841ms)
✔ authorize: private thread under mention channel, mentioned (0.076594ms)
✔ authorize: private thread under mention channel, not mentioned (0.081193ms)
✔ authorize: thread in another guild per channel info (0.089329ms)
✔ authorize: not an object (0.052593ms)
✔ authorize: no id (0.052998ms)
✔ authorize: oversize content is accepted and flagged (0.05717ms)
✔ authorize: exactly the limit is not oversize (0.049367ms)
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.411752ms)
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.121684ms)
✔ binding: a complete binding validates and is frozen (5.790531ms)
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.767833ms)
✔ binding: empty allowlists refuse (0.530678ms)
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.434008ms)
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.160588ms)
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.286517ms)
✔ binding: file must be 0600, regular, not a symlink (1.69439ms)
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.417494ms)
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (104.652492ms)
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.813887ms)
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (366.266935ms)
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (207.600874ms)
✔ cli: run refuses when STOP is present, before any network use (120.080515ms)
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.565436ms)
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.016626ms)
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (24.754313ms)
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (23.517154ms)
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (4.521681ms)
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (3.510393ms)
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.836254ms)
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (4.523989ms)
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.772732ms)
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (6.500024ms)
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.770107ms)
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (2.071734ms)
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.380346ms)
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.375624ms)
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (8.859983ms)
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (10.699491ms)
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.121998ms)
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.571623ms)
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.913785ms)
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.765934ms)
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (1.938226ms)
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (1.278103ms)
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (9.799634ms)
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.248465ms)
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (3.792951ms)
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.49622ms)
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.897878ms)
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.224316ms)
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.331116ms)
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.57512ms)
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.340266ms)
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.711663ms)
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.660918ms)
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.597104ms)
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (66.981725ms)
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (46.315019ms)
✔ engine: one prompt, one turn, text and usage come back (38.85986ms)
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (362.520383ms)
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (232.404059ms)
✔ engine: timeout sends abort and fails only that turn; the process stays (105.101303ms)
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (227.882056ms)
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (124.917471ms)
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (214.301179ms)
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.943458ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.407506ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.63402ms)
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (435.256752ms)
✔ engine: a malformed JSONL line fails the turn, not the process (28.489293ms)
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.291897ms)
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.731491ms)
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.78848ms)
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.694244ms)
✔ gateway: op 9 resumable resumes (0.387033ms)
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.046497ms)
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.620206ms)
✔ gateway: close() is final and unparseable frames are ignored (0.539526ms)
✔ git: config validation is strict, needs write: true, a work tree and a private token file (106.5672ms)
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (66.381881ms)
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (89.732409ms)
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.306919ms)
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (81.547628ms)
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (93.304949ms)
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (114.704235ms)
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (201.165005ms)
✔ git: push pushes the named branch only and reports up to date (60.465398ms)
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (83.600115ms)
✔ git: the credential helper answers get over https from a private file and nothing else (218.04961ms)
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (888.492779ms)
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.649702ms)
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (5.603631ms)
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.088287ms)
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (2.778693ms)
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (60.984081ms)
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (342.062302ms)
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.42599ms)
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (0.443703ms)
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.79832ms)
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (49.089317ms)
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (131.551284ms)
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (69.87592ms)
✔ notices: a kind is recorded per UTC day and found again (0.708582ms)
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.667109ms)
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.177148ms)
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.87754ms)
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (50.94603ms)
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (43.95032ms)
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (50.103263ms)
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (72.273143ms)
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (677.205774ms)
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.518193ms)
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.114429ms)
✔ rest: content and nonce limits are enforced locally; typing never throws (0.643462ms)
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (1.020396ms)
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.044026ms)
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.027816ms)
✔ setspark config: reaches the tools config and the binding as a fixed key (3.325777ms)
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.78746ms)
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (3.620258ms)
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (29.582174ms)
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.508143ms)
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.497698ms)
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.755965ms)
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (1.068823ms)
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.314254ms)
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1012.527762ms)
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.434897ms)
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (5.922125ms)
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.195623ms)
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.197563ms)
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.298838ms)
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.450242ms)
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.223885ms)
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.4028ms)
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.476365ms)
✔ tools: listing and search caps hold (17.774841ms)
✔ tools: credential shapes are caught; ordinary prose and ids are not (1.049228ms)
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (9.337057ms)
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.339143ms)
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.20303ms)
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.917216ms)
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.149135ms)
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.826738ms)
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.567972ms)
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.262031ms)
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1029.97115ms)
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (6.108577ms)
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.337669ms)
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.857954ms)
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.362929ms)
ℹ tests 178
ℹ suites 0
ℹ pass 178
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2744.162363
@@ -0,0 +1,17 @@
OK status with missing harness credential exits 3 and still lists accounts
OK status reports harness credential (read-only) + mosaic accounts
OK api key material never reaches output
OK oauth token material never reaches output
OK unparseable credential file exits 2
OK symlinked credential file exits 4
OK env-side credential names reported
OK env var values never reach output
OK accounts without an accounts dir reports none and creates nothing
OK accounts lists files and marks the active one
OK loose account perms flagged in listing
OK agent --auth with missing account file refuses (exit 4)
OK agent --auth with non-0600 account file refuses
OK agent --auth with invalid account name refuses
OK auth.sh without valid config refuses
selftest: 15 passed, 0 failed
@@ -0,0 +1,55 @@
Note: switching to 'b9b6cf008a8d7083676fc4a2760ae50fd43ad9b0'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
Not currently on any branch.
nothing to commit, working tree clean
Note: switching to 'b9b6cf008a8d7083676fc4a2760ae50fd43ad9b0'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
OK dry-run: allowed change, exit 0, nothing committed (exit 0)
OK dry-run committed nothing
OK apply: allowed change exits 0 (exit 0)
OK apply: attribution in commit subject
OK apply: target tree clean after commit
OK disallowed path refused (exit 1)
OK disallowed path: target untouched
OK syntax gate refused broken .mjs (exit 1)
OK syntax gate: target untouched
OK suite failure refused (exit 1)
OK suite failure: target reverted to clean
OK disabled policy refused (exit 2)
OK disabled policy: target untouched
OK failed run refused (exit 1)
OK failed run: target untouched
OK missing run exits 4 (exit 4)
OK invalid policy exits 2 (exit 2)
selftest: 17 passed, 0 failed
@@ -0,0 +1,26 @@
OK absent adapter defaults to pi
OK adapter mock validates (exit 0)
OK unsupported adapter exits 2 (exit 2)
OK env exports adapter
OK bootstrap creates default when absent (exit 0)
OK bootstrap wrote config file
OK bootstrap is idempotent on existing config (exit 0)
OK bootstrap did not rewrite existing config
OK validate missing config exits 3 (exit 3)
OK malformed JSON exits 2 (exit 2)
OK unsupported configVersion exits 2 (exit 2)
OK unknown top-level key exits 2 (exit 2)
OK unknown execution key exits 2 (exit 2)
OK unsupported backend exits 2 (exit 2)
OK unsupported environment exits 2 (exit 2)
OK relative dataRoot exits 2 (exit 2)
OK non-canonical dataRoot exits 2 (exit 2)
OK filesystem root dataRoot exits 2 (exit 2)
OK home directory dataRoot exits 2 (exit 2)
OK dataRoot containing config dir exits 2 (exit 2)
OK control character in provider exits 2 (exit 2)
OK symlinked config file exits 2 (exit 2)
OK env exports resolve correctly
OK failed validation modified nothing
selftest: 24 passed, 0 failed
@@ -0,0 +1,70 @@
toolchain: node v26.8.1
OK syntax: packages/discord/src/approvals.mjs
OK syntax: packages/discord/src/authorize.mjs
OK syntax: packages/discord/src/binding.mjs
OK syntax: packages/discord/src/cli.mjs
OK syntax: packages/discord/src/connector.mjs
OK syntax: packages/discord/src/context.mjs
OK syntax: packages/discord/src/engine-pi.mjs
OK syntax: packages/discord/src/errors.mjs
OK syntax: packages/discord/src/gateway.mjs
OK syntax: packages/discord/src/git.mjs
OK syntax: packages/discord/src/journal.mjs
OK syntax: packages/discord/src/notify.mjs
OK syntax: packages/discord/src/rest.mjs
OK syntax: packages/discord/src/setspark.mjs
OK syntax: packages/discord/src/tools.mjs
OK syntax: packages/discord/src/web.mjs
OK syntax: packages/discord/bin/git-credential.mjs
OK syntax: packages/discord/extension/tools.mjs
OK syntax: packages/discord/tests/approvals.test.mjs
OK syntax: packages/discord/tests/authorize.test.mjs
OK syntax: packages/discord/tests/binding.test.mjs
OK syntax: packages/discord/tests/connector.test.mjs
OK syntax: packages/discord/tests/context.test.mjs
OK syntax: packages/discord/tests/engine.test.mjs
OK syntax: packages/discord/tests/fake-pi.mjs
OK syntax: packages/discord/tests/gateway.test.mjs
OK syntax: packages/discord/tests/git.test.mjs
OK syntax: packages/discord/tests/helpers.mjs
OK syntax: packages/discord/tests/journal.test.mjs
OK syntax: packages/discord/tests/notify.test.mjs
OK syntax: packages/discord/tests/recover.test.mjs
OK syntax: packages/discord/tests/rest.test.mjs
OK syntax: packages/discord/tests/setspark.test.mjs
OK syntax: packages/discord/tests/tools.test.mjs
OK syntax: packages/discord/tests/web.test.mjs
OK syntax: packages/discord/fixtures/claim-worker.mjs
OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs
OK syntax: scripts/discord.sh
OK syntax: scripts/discord-service.sh
OK packages/discord declares no dependencies
OK no bot-token-shaped string in packages/discord
OK fixture binding uses placeholder ids only
OK fixture binding validates
OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool
OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start
OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root
OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token
OK real pi with protocol vault adds reserve_id to the git verbs
OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key
OK real pi refuses a git key on a read-only root (fail closed)
OK real pi with the pilot flags (--no-tools) exposes no tool at all
OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed)
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test packages/discord/tests/ (ℹ pass 178)
OK scripts/discord.sh --help exits 0
OK scripts/discord.sh check without a binding exits 4
OK scripts/discord.sh recover without a binding exits 4
OK scripts/discord.sh reload without a binding exits 4
OK scripts/discord-service.sh without a command exits 4
OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP
OK service install writes the rendered unit (0644) and leaves no temp file
OK service install a second time reports unchanged
OK systemd-analyze verify accepts the rendered unit
OK service uninstall removes the unit file
OK service install with an unknown flag exits 4
OK service install with USER unset finishes and names the account for lingering
discord suite: 66 passed, 0 failed
@@ -0,0 +1,21 @@
OK initial ordinary-file install
OK installed tree matches canonical source
OK installed tree has no symlinks
OK check detects installation drift
OK sync refuses to overwrite installation drift
OK check detects an extra destination file
OK check detects an extra destination directory
OK check rejects a destination symlink
OK sync accepts a canonical source update
OK updated installation matches canonical source
scripts/test-extension-package.sh: line 14: 141359 Killed "$@" > /dev/null 2>&1
OK forced interruption kills the replacing process
OK next invocation recovers old consistent installation
OK interrupted replacement rolled back
OK sync succeeds after interruption recovery
OK unlocked stale lock file does not block
OK active lock refuses a concurrent sync
OK source symlink fails closed
OK nested second entrypoint fails closed
extension package selftest: 18 passed, 0 failed
@@ -0,0 +1,53 @@
toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0
OK syntax: scripts/foundation-inspect.mjs
OK syntax: scripts/foundation/strict-json.mjs
OK syntax: scripts/foundation/canonical.mjs
OK syntax: scripts/foundation/resolve.mjs
OK syntax: scripts/foundation/validate-record.mjs
OK syntax: scripts/foundation/fixtures/build-fixtures.mjs
OK syntax: scripts/foundation/canonical.test.mjs
OK syntax: scripts/foundation/cli.test.mjs
OK syntax: scripts/foundation/fixtures.test.mjs
OK syntax: scripts/foundation/resolve.test.mjs
OK syntax: scripts/foundation/strict-json.test.mjs
OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written)
OK fixture generator runs
OK checked-in fixtures/bundles equal a fresh generation
OK checked-in fixtures/raw equal a fresh generation
OK checked-in fixtures/index.json equal a fresh generation
OK checked-in demo bundles equal a fresh generation
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test scripts/foundation/ (ℹ pass 80)
OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof)
platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999)
node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases)
schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10
profile column (schema-valid records only): profile-valid 510, profile-invalid 30
profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns
OK oracle: zero schema-column disagreements with the pinned checker
OK oracle: strict-only profile refusals are counted and asserted
OK demo: permitted read preview exits 0 (exit 0)
OK demo: permitted file.change preview exits 0 (exit 0)
OK demo: assignment.change proposal is unresolved (exit 3) (exit 3)
OK demo: revoked registration is refused (exit 3) (exit 3)
OK demo: message is not authority (exit 3) (exit 3)
OK usage: no arguments exits 2 (exit 2)
OK io: missing file exits 4 (exit 4)
OK io: directory exits 4 (exit 4)
OK io: symlink exits 4 (O_NOFOLLOW) (exit 4)
OK bound: oversize fixture exits 2 (exit 2)
OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2)
OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2)
OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0)
OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed
OK text output starts with the disclaimer
OK json output is valid JSON with result allowed and exactly the charter §7 fields
OK json golden matches byte-for-byte
OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal
OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs
OK canary never printed (bundle run and credential-file run)
OK a non-bundle JSON file is refused at the shape gate, not read into output
OK no field of the non-bundle file is echoed
selftest: 44 passed, 0 failed
@@ -0,0 +1,35 @@
toolchain: node v26.8.1, git version 2.55.0
OK syntax: packages/queue/src/cli.mjs
OK syntax: packages/queue/src/errors.mjs
OK syntax: packages/queue/src/io.mjs
OK syntax: packages/queue/src/lock.mjs
OK syntax: packages/queue/src/queue.mjs
OK syntax: packages/queue/src/review.mjs
OK syntax: packages/queue/src/store.mjs
OK syntax: packages/queue/tests/commit.test.mjs
OK syntax: packages/queue/tests/data.test.mjs
OK syntax: packages/queue/tests/dispatch.test.mjs
OK syntax: packages/queue/tests/helpers.mjs
OK syntax: packages/queue/tests/lock.test.mjs
OK syntax: packages/queue/tests/migration.test.mjs
OK syntax: packages/queue/tests/review.test.mjs
OK syntax: packages/queue/tests/store.test.mjs
OK syntax: packages/queue/tests/write.test.mjs
OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs
OK syntax: packages/queue/tests/fixtures/kill-at.mjs
OK syntax: packages/queue/tests/fixtures/lock-child.mjs
OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh
OK syntax: scripts/queue-commit.sh
OK syntax: scripts/git-hooks/pre-commit
OK syntax: scripts/mosaic
OK queue-commit.sh, the guard and scripts/mosaic are executable
OK packages/queue declares no dependencies
ℹ tests 148
ℹ pass 148
ℹ fail 0
OK node --test packages/queue/tests/
OK scripts/mosaic queue help
skip queue verify and render --check: this checkout (/home/jwoltje/filbert-scratch/r39b/cand) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
queue suite: 27 passed, 0 failed
@@ -0,0 +1,7 @@
OK valid RELEASE resolves (exit 0)
OK invalid RELEASE exits 1 (exit 1)
OK missing RELEASE exits 1 (exit 1)
OK valid RELEASE leaves image tag consistent with version
skip state-machine cases (docker daemon unavailable)
selftest: 4 passed, 0 failed
@@ -0,0 +1,33 @@
OK valid task validates (exit 0)
OK unknown task key exits 2 (exit 2)
OK unsupported taskVersion exits 2 (exit 2)
OK invalid task id exits 2 (exit 2)
OK empty prompt exits 2 (exit 2)
OK NUL in expectExact exits 2 (exit 2)
OK out-of-range timeout exits 2 (exit 2)
OK missing mission file exits 4 (exit 4)
OK task with valid mission validates (exit 0)
OK invalid mission exits 2 (exit 2)
OK validate missing task exits 4 (exit 4)
OK validation does not modify the task file
OK prune dry-run exits 0 (exit 0)
OK dry-run deleted nothing
OK prune --keep=2 --yes removes oldest (exit 0)
OK kept exactly 2 newest runs
OK newest run kept, oldest pruned
OK append-only receipt written (3 entries)
OK sessions/workspaces untouched by prune
OK prune with invalid keep exits 4 (exit 4)
skip adapter seam cases (docker daemon unavailable)
skip workspace/capability cases (docker daemon unavailable)
skip live task cases (docker unavailable)
OK onboard without name exits 4 (non-interactive) (exit 4)
OK onboard --name renders profile (exit 0)
OK profile written
OK canon structure: required filled, optional placeholdered
OK canon sections present
FAIL user recall run succeeds (exit 1)
FAIL recalled user name (response: )
OK no agent identity on headless run
selftest: 26 passed, 2 failed
@@ -0,0 +1,25 @@
cand node-cli exit 0
cand node-bus exit 0
cand node-business exit 0
cand node-discord exit 0
cand suite-auth exit 0 load 3.88
cand suite-conductor exit 0 load 4.13
cand suite-config exit 0 load 4.13
cand suite-discord exit 0 load 4.12
cand suite-extension-package exit 0 load 3.87
cand suite-foundation exit 0 load 3.46
cand suite-queue exit 0 load 3.97
cand suite-release exit 0 load 3.97
cand suite-task exit 1 load 3.97
base node-bus exit 0
base node-business exit 0
base node-discord exit 0
base suite-auth exit 0 load 4.29
base suite-conductor exit 0 load 5.31
base suite-config exit 0 load 5.31
base suite-discord exit 0 load 5.47
base suite-extension-package exit 0 load 5.47
base suite-foundation exit 0 load 4.24
base suite-queue exit 0 load 4.43
base suite-release exit 0 load 4.43
base suite-task exit 1 load 4.72
@@ -0,0 +1,34 @@
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (851.881094ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (171.408869ms)
mosaic-notify: notify: poll failed: ENOENT: no such file or directory, open '/tmp/mosaic-cli-RnYDTZ/data/notify/acme/sent.jsonl'
✖ a notifier that refuses stops the broker and the host refuses with exit 3 (163.293325ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (22.529171ms)
✔ bus stop refuses to signal a live pid that is not a bus host (202.458765ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (219.699321ms)
✔ bus start refuses with exit 3 without a notifier config (89.542554ms)
✔ bus start runs until bus stop; status reports it while it runs (685.795533ms)
✔ bus-service.sh renders the unit and installs it into a given directory (40.826807ms)
ℹ tests 9
ℹ suites 0
ℹ pass 8
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2532.392133
✖ failing tests:
test at packages/cli/tests/host.test.mjs:117:1
✖ a notifier that refuses stops the broker and the host refuses with exit 3 (163.293325ms)
AssertionError [ERR_ASSERTION]: Missing expected rejection.
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async TestContext.<anonymous> (file:///home/jwoltje/filbert-scratch/r39b/mut/packages/cli/tests/host.test.mjs:125:3)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: undefined,
operator: 'rejects',
diff: 'simple'
}
@@ -0,0 +1,26 @@
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (87.216667ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (72.518856ms)
✖ decide refuses without a terminal or --yes, on an unknown option and on a short reference (60001.70155ms)
✔ decide prints a declining choice as declining (71.997826ms)
✔ an unknown outcome is reported once and never resent (54.943141ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (50.941742ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (51.85766ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (45.937668ms)
✔ every human command refuses inside an agent run before it touches the bus (48.960227ms)
✔ usage errors exit 4; no business and no host is a usage error (51.712577ms)
✔ agents and tasks print through the broker (49.316379ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.177967ms)
ℹ tests 12
ℹ suites 0
ℹ pass 11
ℹ fail 0
ℹ cancelled 1
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 60677.507287
✖ failing tests:
test at packages/cli/tests/cli.test.mjs:66:1
✖ decide refuses without a terminal or --yes, on an unknown option and on a short reference (60001.70155ms)
'test timed out after 60000ms'
@@ -0,0 +1,235 @@
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (128.195384ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (173.03349ms)
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (101.725873ms)
✔ decide prints a declining choice as declining (211.031203ms)
✔ an unknown outcome is reported once and never resent (168.845654ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (197.827882ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (142.434566ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (105.022882ms)
✔ every human command refuses inside an agent run before it touches the bus (114.596595ms)
✔ usage errors exit 4; no business and no host is a usage error (66.603429ms)
✔ agents and tasks print through the broker (68.059725ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.285612ms)
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (92.375276ms)
✔ trackers come from the tracker.* variables of the one project that names a tracker project (61.819103ms)
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (60.559355ms)
✔ two projects that each name a tracker project refuse, since the boot shape holds one (49.117414ms)
✔ a business without tracker.baseUrl gets no trackers entry (53.852451ms)
✔ an unknown business and a broken system config refuse with exit 3 (87.694477ms)
✔ empty views say so (1.285418ms)
✔ the trail keeps the broker's order and names a decision's task without its rows (1.478037ms)
✔ tasks print the tracker fields the snapshot carries (0.28882ms)
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1033.632014ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (262.045607ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (203.472241ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (22.806465ms)
✔ bus stop refuses to signal a live pid that is not a bus host (203.474754ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (245.182997ms)
✔ bus start refuses with exit 3 without a notifier config (101.641085ms)
✔ bus start runs until bus stop; status reports it while it runs (687.392239ms)
✔ bus-service.sh renders the unit and installs it into a given directory (27.544558ms)
✔ zoned uses the IANA zone across DST (26.513988ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (103.232685ms)
✔ two blocking decisions get two DMs with different nonces (166.594618ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.542796ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (101.736767ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (176.495893ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (175.331443ms)
✔ an inbox read failure is logged and the next poll retries (1.349032ms)
✔ no Discord id reaches the journal or the log (176.712746ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (56.520073ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (49.840375ms)
✔ the journal: a whole file that is one torn line truncates to empty (22.119255ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.003867ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.994243ms)
✔ digest content stays within Discord's 2000 characters (0.486197ms)
✔ runLoop never overlaps ticks and stops after the one in flight (110.970108ms)
✔ the transport writes {business, verb, args} to the child and reads its JSON (54.522158ms)
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2267.743264ms)
✔ busExit and refuseInsideAgent (0.466679ms)
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.446995ms)
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.777098ms)
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.701757ms)
✔ approvals: a button approves only on its own request message with the matching custom id (0.514843ms)
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (19.376227ms)
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.783828ms)
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (11.817459ms)
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.623937ms)
✔ authorize: open channel, listed user (1.991359ms)
✔ authorize: wrong guild (0.234897ms)
✔ authorize: no guild (DM) (0.176042ms)
✔ authorize: unlisted channel (0.199873ms)
✔ authorize: unknown channel, no info (0.184158ms)
✔ authorize: thread of listed parent (0.238941ms)
✔ authorize: thread of unlisted parent (0.155529ms)
✔ authorize: text channel that is not a thread and not listed (0.183767ms)
✔ authorize: unlisted user (0.190022ms)
✔ authorize: no author (0.365207ms)
✔ authorize: bot author (listed id, bot flag) (0.173026ms)
✔ authorize: system author (0.118875ms)
✔ authorize: the bot itself (0.099871ms)
✔ authorize: webhook (0.095675ms)
✔ authorize: mention channel without mention (0.149411ms)
✔ authorize: mention channel with bot mention (0.17239ms)
✔ authorize: mention channel with @everyone only (0.113569ms)
✔ authorize: mention channel mentioning someone else (0.096703ms)
✔ authorize: mention channel, content says @bot but mentions empty (0.094882ms)
✔ authorize: private thread under mention channel, mentioned (0.098952ms)
✔ authorize: private thread under mention channel, not mentioned (0.105221ms)
✔ authorize: thread in another guild per channel info (0.086125ms)
✔ authorize: not an object (0.080474ms)
✔ authorize: no id (0.08ms)
✔ authorize: oversize content is accepted and flagged (0.083916ms)
✔ authorize: exactly the limit is not oversize (0.072872ms)
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.48684ms)
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.164021ms)
✔ binding: a complete binding validates and is frozen (3.221593ms)
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.664308ms)
✔ binding: empty allowlists refuse (0.489484ms)
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.530283ms)
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.331597ms)
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.395098ms)
✔ binding: file must be 0600, regular, not a symlink (2.156108ms)
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (3.713437ms)
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (115.304298ms)
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (3.29538ms)
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (434.958653ms)
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (238.946386ms)
✔ cli: run refuses when STOP is present, before any network use (152.871089ms)
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (3.135857ms)
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (2.122184ms)
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (18.934291ms)
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (30.088217ms)
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (3.499336ms)
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (2.520585ms)
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.451321ms)
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.229017ms)
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (33.681902ms)
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.973696ms)
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.294851ms)
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.799863ms)
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (45.065449ms)
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.457106ms)
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (5.686495ms)
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.778305ms)
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.148577ms)
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.217326ms)
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (4.351708ms)
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.873163ms)
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (2.348064ms)
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (3.501793ms)
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.052151ms)
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.393233ms)
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.212187ms)
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.823545ms)
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.82393ms)
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.13179ms)
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.298018ms)
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.744131ms)
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.733223ms)
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.762688ms)
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.944535ms)
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.465626ms)
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (62.822814ms)
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (61.145295ms)
✔ engine: one prompt, one turn, text and usage come back (65.381089ms)
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (351.693041ms)
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (252.49447ms)
✔ engine: timeout sends abort and fails only that turn; the process stays (104.961156ms)
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (236.952688ms)
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (136.060149ms)
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (213.56674ms)
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.013047ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.50905ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.982612ms)
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (438.371393ms)
✔ engine: a malformed JSONL line fails the turn, not the process (25.024093ms)
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (44.214839ms)
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.80776ms)
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.836378ms)
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.562911ms)
✔ gateway: op 9 resumable resumes (0.397066ms)
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.091203ms)
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.585441ms)
✔ gateway: close() is final and unparseable frames are ignored (0.509514ms)
✔ git: config validation is strict, needs write: true, a work tree and a private token file (81.630624ms)
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (55.182558ms)
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (104.588538ms)
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.386886ms)
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (129.509564ms)
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (129.242538ms)
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (128.941211ms)
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (270.059409ms)
✔ git: push pushes the named branch only and reports up to date (108.38127ms)
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (120.832286ms)
✔ git: the credential helper answers get over https from a private file and nothing else (235.297232ms)
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (870.316574ms)
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.461445ms)
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (6.077463ms)
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.154073ms)
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (2.958004ms)
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (68.497107ms)
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (414.276972ms)
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.568485ms)
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (0.622695ms)
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.24818ms)
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (65.895538ms)
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (136.975315ms)
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (62.784536ms)
✔ notices: a kind is recorded per UTC day and found again (0.47959ms)
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (6.202273ms)
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.347778ms)
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (2.088161ms)
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (46.846093ms)
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (46.862321ms)
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (36.118273ms)
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (85.679261ms)
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (786.96409ms)
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.469546ms)
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (6.499215ms)
✔ rest: content and nonce limits are enforced locally; typing never throws (0.663236ms)
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (1.10301ms)
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.019735ms)
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.327066ms)
✔ setspark config: reaches the tools config and the binding as a fixed key (2.605257ms)
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.225081ms)
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.048754ms)
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (28.04122ms)
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (12.465965ms)
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.034413ms)
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.868421ms)
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.667357ms)
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (2.697976ms)
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1011.422397ms)
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.574031ms)
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (7.422913ms)
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.467746ms)
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.250436ms)
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.738467ms)
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.390017ms)
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.716188ms)
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.057023ms)
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.659219ms)
✔ tools: listing and search caps hold (12.247961ms)
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.942743ms)
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.486088ms)
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.284782ms)
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.243556ms)
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.044765ms)
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.66857ms)
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.687135ms)
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.829048ms)
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.407129ms)
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1029.680966ms)
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (5.749177ms)
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.345888ms)
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (3.21551ms)
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.689858ms)
ℹ tests 227
ℹ suites 0
ℹ pass 227
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2941.501687
@@ -0,0 +1,235 @@
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (127.753251ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (129.025855ms)
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (96.418066ms)
✔ decide prints a declining choice as declining (100.853056ms)
✔ an unknown outcome is reported once and never resent (96.778988ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (105.734474ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (93.012442ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (53.206865ms)
✔ every human command refuses inside an agent run before it touches the bus (62.921536ms)
✔ usage errors exit 4; no business and no host is a usage error (56.550824ms)
✔ agents and tasks print through the broker (57.582994ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.307361ms)
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (64.984295ms)
✔ trackers come from the tracker.* variables of the one project that names a tracker project (65.53152ms)
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (62.872752ms)
✔ two projects that each name a tracker project refuse, since the boot shape holds one (53.003211ms)
✔ a business without tracker.baseUrl gets no trackers entry (50.542782ms)
✔ an unknown business and a broken system config refuse with exit 3 (74.0262ms)
✔ empty views say so (2.944357ms)
✔ the trail keeps the broker's order and names a decision's task without its rows (3.005385ms)
✔ tasks print the tracker fields the snapshot carries (0.30078ms)
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (974.684957ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (214.521088ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (203.644138ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (25.221184ms)
✔ bus stop refuses to signal a live pid that is not a bus host (203.41222ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (215.480332ms)
✔ bus start refuses with exit 3 without a notifier config (93.644985ms)
✔ bus start runs until bus stop; status reports it while it runs (651.972183ms)
✔ bus-service.sh renders the unit and installs it into a given directory (26.333963ms)
✔ zoned uses the IANA zone across DST (30.582528ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (126.420982ms)
✔ two blocking decisions get two DMs with different nonces (123.200882ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.488688ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (100.613569ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (102.035032ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (86.894435ms)
✔ an inbox read failure is logged and the next poll retries (1.01765ms)
✔ no Discord id reaches the journal or the log (102.020379ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (25.701484ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (38.403667ms)
✔ the journal: a whole file that is one torn line truncates to empty (20.953522ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.164213ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.676866ms)
✔ digest content stays within Discord's 2000 characters (0.322469ms)
✔ runLoop never overlaps ticks and stops after the one in flight (111.200287ms)
✔ the transport writes {business, verb, args} to the child and reads its JSON (82.057379ms)
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2253.399727ms)
✔ busExit and refuseInsideAgent (0.42075ms)
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.628277ms)
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.75015ms)
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.789884ms)
✔ approvals: a button approves only on its own request message with the matching custom id (0.528425ms)
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (17.730053ms)
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (8.531053ms)
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (8.368222ms)
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.490163ms)
✔ authorize: open channel, listed user (2.163248ms)
✔ authorize: wrong guild (0.413078ms)
✔ authorize: no guild (DM) (0.246179ms)
✔ authorize: unlisted channel (0.226812ms)
✔ authorize: unknown channel, no info (0.216533ms)
✔ authorize: thread of listed parent (0.244115ms)
✔ authorize: thread of unlisted parent (0.18617ms)
✔ authorize: text channel that is not a thread and not listed (0.212158ms)
✔ authorize: unlisted user (0.216673ms)
✔ authorize: no author (1.07892ms)
✔ authorize: bot author (listed id, bot flag) (0.556837ms)
✔ authorize: system author (0.132411ms)
✔ authorize: the bot itself (0.124679ms)
✔ authorize: webhook (0.092331ms)
✔ authorize: mention channel without mention (0.367416ms)
✔ authorize: mention channel with bot mention (0.15319ms)
✔ authorize: mention channel with @everyone only (0.094163ms)
✔ authorize: mention channel mentioning someone else (0.106898ms)
✔ authorize: mention channel, content says @bot but mentions empty (0.07215ms)
✔ authorize: private thread under mention channel, mentioned (0.107247ms)
✔ authorize: private thread under mention channel, not mentioned (0.076093ms)
✔ authorize: thread in another guild per channel info (0.081567ms)
✔ authorize: not an object (0.078452ms)
✔ authorize: no id (0.087671ms)
✔ authorize: oversize content is accepted and flagged (0.088343ms)
✔ authorize: exactly the limit is not oversize (0.103414ms)
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.510951ms)
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.151798ms)
✔ binding: a complete binding validates and is frozen (2.811979ms)
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.534343ms)
✔ binding: empty allowlists refuse (0.489052ms)
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.332179ms)
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.730779ms)
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.479475ms)
✔ binding: file must be 0600, regular, not a symlink (1.561869ms)
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.360705ms)
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (118.459641ms)
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (5.138223ms)
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (390.500298ms)
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (225.842844ms)
✔ cli: run refuses when STOP is present, before any network use (127.056523ms)
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.86736ms)
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.263217ms)
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (19.600503ms)
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (22.072709ms)
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.633239ms)
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.375273ms)
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.379997ms)
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.479605ms)
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (35.877674ms)
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (6.498832ms)
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.019322ms)
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.114521ms)
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.243936ms)
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (31.749582ms)
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (6.897904ms)
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (4.729896ms)
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.503019ms)
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.841041ms)
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.776015ms)
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.802936ms)
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (6.148514ms)
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.668272ms)
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.084101ms)
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (3.3259ms)
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (6.035844ms)
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.65906ms)
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.859541ms)
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.287212ms)
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.651253ms)
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.766928ms)
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.413465ms)
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.742759ms)
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.827857ms)
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.491479ms)
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (70.86549ms)
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (69.620553ms)
✔ engine: one prompt, one turn, text and usage come back (51.782501ms)
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (359.437182ms)
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (238.14282ms)
✔ engine: timeout sends abort and fails only that turn; the process stays (104.388246ms)
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (227.259601ms)
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (126.831582ms)
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (213.412083ms)
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (615.408499ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.270054ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.489266ms)
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (427.920283ms)
✔ engine: a malformed JSONL line fails the turn, not the process (26.809844ms)
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (45.871482ms)
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (4.042801ms)
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.783841ms)
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.518696ms)
✔ gateway: op 9 resumable resumes (0.356799ms)
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.956958ms)
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.57353ms)
✔ gateway: close() is final and unparseable frames are ignored (0.404589ms)
✔ git: config validation is strict, needs write: true, a work tree and a private token file (89.033224ms)
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (63.115487ms)
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (104.076686ms)
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.349054ms)
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (92.960511ms)
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (110.702274ms)
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (126.24749ms)
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (201.410346ms)
✔ git: push pushes the named branch only and reports up to date (79.932738ms)
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (93.41013ms)
✔ git: the credential helper answers get over https from a private file and nothing else (240.864545ms)
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (836.648473ms)
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.93703ms)
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (5.34184ms)
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.049106ms)
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (2.501652ms)
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (61.604453ms)
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (388.69257ms)
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.431042ms)
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (0.413009ms)
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.859556ms)
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (40.072031ms)
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (131.561811ms)
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (69.87073ms)
✔ notices: a kind is recorded per UTC day and found again (0.616648ms)
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (6.579462ms)
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (4.713692ms)
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (4.738148ms)
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (48.62245ms)
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (47.091302ms)
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (33.514263ms)
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (92.247189ms)
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (725.566027ms)
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.80118ms)
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.155356ms)
✔ rest: content and nonce limits are enforced locally; typing never throws (0.665527ms)
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (1.148698ms)
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.866994ms)
✔ setspark config: a bare https or loopback origin, a private key file, a principal (4.907299ms)
✔ setspark config: reaches the tools config and the binding as a fixed key (2.885344ms)
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.415831ms)
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (1.676002ms)
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (26.652602ms)
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (11.608155ms)
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.134591ms)
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.890858ms)
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (2.733054ms)
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.599123ms)
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1011.42976ms)
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.570767ms)
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.650093ms)
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.427899ms)
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.226813ms)
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.807204ms)
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.286692ms)
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.667324ms)
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.102094ms)
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (4.099343ms)
✔ tools: listing and search caps hold (11.508786ms)
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.881486ms)
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.835094ms)
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.19701ms)
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.125946ms)
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.987562ms)
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.708844ms)
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.627306ms)
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.544969ms)
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.000021ms)
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1024.136539ms)
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (5.463547ms)
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.314305ms)
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (3.573597ms)
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (3.249626ms)
ℹ tests 227
ℹ suites 0
ℹ pass 227
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2765.910563
@@ -0,0 +1,50 @@
M1 killed (fail 1, cancelled 0)
M2 killed (fail 1, cancelled 0)
M3 killed (fail 2, cancelled 0)
M4 killed (fail 1, cancelled 0)
M5 NOT-APPLIED packages/cli/src/notifier.mjs
M6 NOT-APPLIED packages/cli/src/notifier.mjs
M7 killed (fail 5, cancelled 0)
M8 killed (fail 1, cancelled 0)
M9 killed (fail 1, cancelled 0)
M10 killed (fail 1, cancelled 0)
M11 killed (fail 1, cancelled 0)
M12 killed (fail 1, cancelled 0)
M13 killed (fail 2, cancelled 0)
M14 killed (fail 1, cancelled 0)
M15 killed (fail 2, cancelled 0)
M16 killed (fail 2, cancelled 0)
M17 killed (fail 1, cancelled 0)
M18 SURVIVED
M19 killed (fail 0, cancelled 1)
M20 killed (fail 1, cancelled 0)
M21 killed (fail 1, cancelled 0)
M22 killed (fail 1, cancelled 0)
M23 killed (fail 1, cancelled 0)
M24 killed (fail 1, cancelled 0)
M25 killed (fail 1, cancelled 0)
M26 killed (fail 1, cancelled 0)
M27 NOT-APPLIED packages/cli/src/host.mjs
M28 SURVIVED
M29 killed (fail 1, cancelled 0)
M30 killed (fail 1, cancelled 0)
M31 killed (fail 1, cancelled 0)
M32 killed (fail 2, cancelled 0)
M33 SURVIVED
M34 killed (fail 1, cancelled 0)
R5 killed (fail 1, cancelled 0)
R6 killed (fail 3, cancelled 0)
R6b killed (fail 3, cancelled 0)
R6c killed (fail 3, cancelled 0)
F1 killed (fail 1, cancelled 0)
J3 killed (fail 1, cancelled 0)
D1 killed (fail 1, cancelled 0)
RACE killed (fail 1, cancelled 0)
F3 killed (fail 1, cancelled 0)
N1 killed (fail 1, cancelled 0)
N2 SURVIVED
N3 SURVIVED
N4 SURVIVED
N5 SURVIVED
N6 killed (fail 2, cancelled 0)
manifest exit 0
@@ -0,0 +1,236 @@
v24.21.0
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (133.148875ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (144.284714ms)
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (105.577091ms)
✔ decide prints a declining choice as declining (149.14756ms)
✔ an unknown outcome is reported once and never resent (98.973182ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (116.155887ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (86.731111ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (60.683256ms)
✔ every human command refuses inside an agent run before it touches the bus (59.372134ms)
✔ usage errors exit 4; no business and no host is a usage error (59.391836ms)
✔ agents and tasks print through the broker (52.619851ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.306544ms)
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (70.290138ms)
✔ trackers come from the tracker.* variables of the one project that names a tracker project (45.218875ms)
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (73.190968ms)
✔ two projects that each name a tracker project refuse, since the boot shape holds one (50.949454ms)
✔ a business without tracker.baseUrl gets no trackers entry (49.646488ms)
✔ an unknown business and a broken system config refuse with exit 3 (73.975271ms)
✔ empty views say so (1.324689ms)
✔ the trail keeps the broker's order and names a decision's task without its rows (2.356319ms)
✔ tasks print the tracker fields the snapshot carries (0.221546ms)
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1034.927582ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (186.210396ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (174.291356ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (22.270706ms)
✔ bus stop refuses to signal a live pid that is not a bus host (202.662447ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (201.680155ms)
✔ bus start refuses with exit 3 without a notifier config (90.929576ms)
✔ bus start runs until bus stop; status reports it while it runs (654.588193ms)
✔ bus-service.sh renders the unit and installs it into a given directory (37.869352ms)
✔ zoned uses the IANA zone across DST (55.713246ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (135.192226ms)
✔ two blocking decisions get two DMs with different nonces (126.131514ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.357865ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (97.327268ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (145.621758ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (97.606994ms)
✔ an inbox read failure is logged and the next poll retries (1.289062ms)
✔ no Discord id reaches the journal or the log (108.828729ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (19.13901ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (39.211452ms)
✔ the journal: a whole file that is one torn line truncates to empty (24.705091ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.989135ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.748042ms)
✔ digest content stays within Discord's 2000 characters (0.320981ms)
✔ runLoop never overlaps ticks and stops after the one in flight (111.21029ms)
✔ the transport writes {business, verb, args} to the child and reads its JSON (58.297057ms)
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2219.381688ms)
✔ busExit and refuseInsideAgent (0.347476ms)
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.867876ms)
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.930998ms)
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.622318ms)
✔ approvals: a button approves only on its own request message with the matching custom id (0.745477ms)
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (22.317701ms)
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (8.212755ms)
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (6.007654ms)
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.234751ms)
✔ authorize: open channel, listed user (1.91511ms)
✔ authorize: wrong guild (0.197809ms)
✔ authorize: no guild (DM) (0.329677ms)
✔ authorize: unlisted channel (0.237793ms)
✔ authorize: unknown channel, no info (0.170099ms)
✔ authorize: thread of listed parent (0.207444ms)
✔ authorize: thread of unlisted parent (0.174588ms)
✔ authorize: text channel that is not a thread and not listed (0.146914ms)
✔ authorize: unlisted user (2.273145ms)
✔ authorize: no author (0.330478ms)
✔ authorize: bot author (listed id, bot flag) (0.15287ms)
✔ authorize: system author (0.134099ms)
✔ authorize: the bot itself (0.094939ms)
✔ authorize: webhook (0.101119ms)
✔ authorize: mention channel without mention (0.156474ms)
✔ authorize: mention channel with bot mention (0.153772ms)
✔ authorize: mention channel with @everyone only (0.108938ms)
✔ authorize: mention channel mentioning someone else (0.093841ms)
✔ authorize: mention channel, content says @bot but mentions empty (3.283104ms)
✔ authorize: private thread under mention channel, mentioned (0.142484ms)
✔ authorize: private thread under mention channel, not mentioned (0.081361ms)
✔ authorize: thread in another guild per channel info (0.072228ms)
✔ authorize: not an object (0.369775ms)
✔ authorize: no id (0.096019ms)
✔ authorize: oversize content is accepted and flagged (0.080047ms)
✔ authorize: exactly the limit is not oversize (0.064125ms)
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.537813ms)
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.166175ms)
✔ binding: a complete binding validates and is frozen (2.683269ms)
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.858271ms)
✔ binding: empty allowlists refuse (0.698964ms)
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (2.117376ms)
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.08131ms)
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.115265ms)
✔ binding: file must be 0600, regular, not a symlink (1.68097ms)
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (3.174229ms)
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (117.285167ms)
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (1.909175ms)
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (377.041478ms)
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (216.295324ms)
✔ cli: run refuses when STOP is present, before any network use (115.048351ms)
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.467178ms)
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.09963ms)
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (21.316467ms)
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (33.272973ms)
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.688757ms)
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.275857ms)
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.39992ms)
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.560595ms)
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (36.07126ms)
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (4.736815ms)
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (3.359329ms)
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (3.802886ms)
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (46.352161ms)
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.3766ms)
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.460856ms)
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (3.778733ms)
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.070541ms)
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (6.080508ms)
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.213655ms)
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.63942ms)
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (1.918608ms)
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.440224ms)
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (5.739841ms)
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (1.712085ms)
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.645544ms)
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (3.595148ms)
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.816121ms)
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (3.90803ms)
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.357139ms)
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (2.524516ms)
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.16571ms)
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.69284ms)
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.867992ms)
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.456893ms)
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (53.731313ms)
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (55.72278ms)
✔ engine: one prompt, one turn, text and usage come back (51.932374ms)
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (345.279587ms)
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (244.214588ms)
✔ engine: timeout sends abort and fails only that turn; the process stays (105.437654ms)
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (224.117287ms)
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (126.738318ms)
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (212.59562ms)
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (615.157126ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.351493ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.556148ms)
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (427.018428ms)
✔ engine: a malformed JSONL line fails the turn, not the process (25.43066ms)
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.951295ms)
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.69792ms)
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.869035ms)
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.533233ms)
✔ gateway: op 9 resumable resumes (0.383641ms)
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.340036ms)
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.636877ms)
✔ gateway: close() is final and unparseable frames are ignored (0.462875ms)
✔ git: config validation is strict, needs write: true, a work tree and a private token file (160.392307ms)
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (59.185612ms)
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (99.969922ms)
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.307647ms)
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (80.090709ms)
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (126.0413ms)
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (109.954892ms)
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (167.839313ms)
✔ git: push pushes the named branch only and reports up to date (53.632654ms)
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (67.062539ms)
✔ git: the credential helper answers get over https from a private file and nothing else (175.014557ms)
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (259.931632ms)
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.113238ms)
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (5.474769ms)
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.085455ms)
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (16.807399ms)
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (65.916029ms)
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (344.449758ms)
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.348967ms)
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (0.431166ms)
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.868604ms)
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (44.707965ms)
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (130.97353ms)
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (63.553437ms)
✔ notices: a kind is recorded per UTC day and found again (0.634182ms)
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.771808ms)
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.633108ms)
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.727567ms)
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (43.414226ms)
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (36.289292ms)
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (44.93181ms)
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (78.616995ms)
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (654.649528ms)
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (2.318177ms)
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (2.7292ms)
✔ rest: content and nonce limits are enforced locally; typing never throws (1.019023ms)
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (1.822671ms)
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (2.16405ms)
✔ setspark config: a bare https or loopback origin, a private key file, a principal (6.461151ms)
✔ setspark config: reaches the tools config and the binding as a fixed key (3.264288ms)
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.646026ms)
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.251922ms)
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (33.26389ms)
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (10.908174ms)
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.971595ms)
✔ setspark keys: read per call, one printable token per file, rotation without a restart (5.757372ms)
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (6.206062ms)
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (11.202128ms)
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1013.46248ms)
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.431636ms)
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (5.826749ms)
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (2.37378ms)
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.201864ms)
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.484639ms)
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.284492ms)
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.842823ms)
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (9.336853ms)
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (2.946598ms)
✔ tools: listing and search caps hold (15.272918ms)
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.876088ms)
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (14.495331ms)
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.811072ms)
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.434261ms)
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (4.796861ms)
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.933489ms)
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (3.509719ms)
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.647114ms)
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.47267ms)
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1039.582548ms)
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.443289ms)
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.308404ms)
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.850135ms)
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.433448ms)
ℹ tests 227
ℹ suites 0
ℹ pass 227
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2755.78809
@@ -0,0 +1,8 @@
T1 sent after open ["a"] torn copies 1 logs 2
T2 reopen ["a","b"]
T3 3: notify journal line 2 is malformed: <s>/j-OP2V6r/notify/demo/sent.jsonl | unchanged true | torn copies 0
T4 3: notify journal must be a regular file, mode 0600, owned by this user: <s>/j-ohiVsv/notify/demo/sent.jsonl | torn copies 0
T5 3: notify journal directory must be mode 0700 and owned by this user: <s>/j-h6fq3N/notify/demo
T6 copies ["torn-20261009T010000000Z-1.bin","torn-20261009T010000000Z-2.bin","torn-20261009T010000000Z-3.bin","torn-20261009T010000000Z.bin"] journal bytes 0 sent 0
T7 EACCES: EACCES: permission denied, open '<s>/j-famMXt/notify/demo/torn-20261009T010503790Z.bin' | journal "{\"torn"
T8 3: notify journal line 2 is malformed: <s>/j-Pv0zPl/notify/demo/sent.jsonl
@@ -0,0 +1,19 @@
connected false exitCode 0
send returned without throwing
process exit code 1
node:events:505
throw er; // Unhandled 'error' event
^
Error [ERR_IPC_CHANNEL_CLOSED]: Channel closed
at target.send (node:internal/child_process:778:16)
at file:///home/jwoltje/filbert-scratch/r39b/probe/sendclosed.mjs:10:9
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
Emitted 'error' event on ChildProcess instance at:
at node:internal/child_process:782:35
at process.processTicksAndRejections (node:internal/process/task_queues:85:11) {
code: 'ERR_IPC_CHANNEL_CLOSED'
}
Node.js v26.8.1
shell exit 1
@@ -0,0 +1,32 @@
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (873.048242ms)
✖ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (60005.00757ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (185.620796ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (20.748664ms)
✔ bus stop refuses to signal a live pid that is not a bus host (202.751381ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (257.772528ms)
✔ bus start refuses with exit 3 without a notifier config (105.611141ms)
✔ bus start runs until bus stop; status reports it while it runs (673.656974ms)
✔ bus-service.sh renders the unit and installs it into a given directory (27.783457ms)
Interrupted while running:
⚠ packages/cli/tests/host.test.mjs (packages/cli/tests/host.test.mjs:1:1)
✖ packages/cli/tests/host.test.mjs (239968.123988ms)
ℹ tests 10
ℹ suites 0
ℹ pass 8
ℹ fail 0
ℹ cancelled 2
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 239973.857677
✖ failing tests:
test at packages/cli/tests/host.test.mjs:102:1
✖ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (60005.00757ms)
'test timed out after 60000ms'
test at packages/cli/tests/host.test.mjs:1:1
✖ packages/cli/tests/host.test.mjs (239968.123988ms)
'Promise resolution is still pending but the event loop has already resolved'
@@ -0,0 +1,247 @@
# Slice 1 S4, row 39, round 2 review (Filbert)
Issue #1521, request comment 26854, queue rev 191, pushed at `26bd829c`.
Packet: `agents/rocko/work/slice1-s4/` (`packet-manifest.sha256`
`fef8a758d0c75c380294591a59c865c9ecc8c8e634b0e11b012a3dd481c86ec4`,
44 OK). Candidate: manifest sha256
`e858504e54d5b06581ec8b7168b82cd4895491c980aae1c62bd8a8be5e4f2f17`,
29 files, over `b9b6cf00` with `build.patch` (sha256
`b52f7d6800538cdaf9df29300ad37672d00552e7086f496606722df3967e88f8`).
Scope: lead decision 71. Round 1: `review-r1.md` here.
Verdict: **approve.** B1 is fixed the way decision 71 lays it out, and
F1, R2, D1, the startup race, J3 and F3 are fixed and tested. So are
the round 1 test gaps M22 to M24, M26 and M29. F2 and J4 stay
follow-ups, as decision 71 says. The notes below don't block.
## Method
- Detached worktrees at `b9b6cf00` under `~/filbert-scratch/r39b/`: base,
candidate, and a third candidate copy for mutants. In both candidate
trees I ran `git apply build.patch` and then `sha256sum -c`: 29 OK.
After the mutant run the mutant tree checks clean again.
- No code differs between `b9b6cf00` and `26bd829c` outside `agents/`
and `docs/`.
- `gate-r2.sh` runs the suites one at a time in both trees, tees each
output and logs the load average. As in round 1, `DOCKER_HOST` points
at a socket that doesn't exist, so no Docker case runs. I touched no
Docker network.
- `probe-r2.mjs` exercises `openJournal` directly (T1 to T8).
- `sendclosed.mjs` checks what `ChildProcess.send` does on a closed
channel.
- `mutants-r2.sh` runs my round 1 mutants M1 to M34, Rocko's round 2 set
(R5 to F3, from `mutants-r2.sh` in the packet), and N1 to N6, which
target the round 2 code. Each run is the cli and discord node tests,
227 tests, under `--test-timeout=90000` and an outer `timeout 900`. A
run counts as killed on any failure, any cancellation, or the outer
timeout. My round 1 harness counted only failures, which is why M19
showed SURVIVED in Rocko's run of it.
- Node 24 run: `node:24` with no network, the tree mounted read-only, and
the host uid.
## Suites
| Suite | Candidate | Base |
|---|---|---|
| node cli (Node 26.8.1) | 49/49 | n/a |
| node cli + discord (Node 24.21.0) | 227/227 | n/a |
| node bus | 58/58 | 58/58 |
| node business | 60/60 | 60/60 |
| node discord | 178/178 | 173/173 |
| test-auth | 15/15 | 15/15 |
| test-conductor | 17/17 | 17/17 |
| test-config | 24/24 | 24/24 |
| test-discord | 66/66 | 64/64 |
| test-extension-package | 18/18 | 18/18 |
| test-foundation | 44/44 | 44/44 |
| test-queue | 27/27 | 27/27 |
| test-release | 4/4, Docker cases skipped | 4/4, same |
| test-task | 26 pass, 2 fail | 26 pass, 2 fail |
Base and candidate fail the same two test-task cases, "user recall run
succeeds" and "recalled user name", as in round 1 and row 38. That
matches Rocko's 26/2. I saw no 429 and no address-pool error.
## Sage's three questions
### 1. M19: confirmed, it never passes
M19 removes decide's no-TTY check. The test's stdin is a PassThrough
with `isTTY` false that never ends, so readline waits forever. Run alone
with `--test-timeout=60000` (`r2-m19-timeout.txt`):
```
ℹ pass 11
ℹ fail 0
ℹ cancelled 1
✖ decide refuses without a terminal or --yes, on an unknown option and on a short reference (60001.70155ms)
'test timed out after 60000ms'
```
The test is cancelled, never passed. My round 1 harness logged
SURVIVED because it read only the `fail` count. In the round 2 harness
M19 is killed (fail 0, cancelled 1). Rocko's reading is right.
### 2. M13's host: closed on every path
M13 drops the `dmRecipient === null` refusal in `notify.mjs`, so the
refusal test's `startHost` resolves a running host. Rocko's hook:
```js
t.after(async () => (await started.catch(() => null))?.close(0));
```
- **The refusal holds** (the normal case). `started` rejects, the hook
gets null and does nothing. Nothing is left to close: `startHost`
stopped the notifier and the broker before it threw.
- **The refusal regresses** (M13). `started` resolves and the hook
closes the host.
- **A different rejection.** Same as the first path: every throw in
`startHost` after a fork ends the child first.
Under M13 the test fails (fail 1), host.test ends in about 3 s, and no
process is left over (`r2-m13-host.txt`).
One gap of the same kind, in a different test. Under a mutant that
never wires `watchChildren` (W0, `watchChildren({}, …)`), "a notifier
that dies takes the host down with exit 1" waits on `host.done`
forever. That test has no `t.after`, so the broker stays up and the file
ran until my 240 s outer timeout (`r2-w0-host.txt`). The test still
fails, so the mutant is caught, but slowly. `t.after(() => host.close(0))`
would fix it. This is not blocking.
### 3. Parse first, then repair: that's the right order
`openJournal` checks the directory and the file, parses every complete
line, and only then copies and truncates a torn tail. Decision 71 fixes
the two repair steps and the exit-3 refusal, but not their order. This
order is the better one:
- A refusal changes nothing on disk. The human who fixes the bad line
sees the file as it was, torn tail included.
- The next open after the fix repairs the tail as usual.
- Repairing first would write a torn copy and truncate a journal that is
about to be refused anyway. A human would then be looking at a changed
file.
Probe T3 (a bad middle line plus a torn tail): exit 3 "notify journal
line 2 is malformed", file unchanged, no torn copy. Probe T4 (mode 0644)
also refuses before any repair.
## Decision 71, item by item
- **B1, torn tail.** `copyTorn` writes the torn bytes to
`torn-<UTC stamp>.bin`: `O_EXCL`, 0600, the file fsynced, then the
directory. A name that exists gets `-1`, `-2`, and so on. Then
`ftruncateSync` to the last newline and `fsyncSync`, and each step is
logged. Probes:
- T2 is the round 1 B1 sequence: torn tail, append, reopen. It now
reopens with `["a","b"]`.
- T6 replays a crash between the steps three times. That gives four
copies, none overwritten, and an empty journal.
- Mutants R6, R6b, R6c, N1 (no `O_EXCL`) and N6 (truncate one byte
short) are killed.
- **F1.** An existing directory with any group or other bit refuses with
exit 3. Mutant F1 is killed.
- **R2.** A test asserts different DM nonces for two decisions. Round
1's M8 (constant nonce), which survived, is now killed.
- **D1.** `digestNonce(business, day)` is `"dg"` + 23 hex characters of
sha256 of business and day. It stays within Discord's 25-character
limit. Mutant D1 is killed.
- **Startup race.** `watchChildren` checks `exitCode` and `signalCode`
before it attaches `once("exit")`. It runs after `queue` exists.
Mutant RACE is killed.
- **J3.** The journal opens with `O_NOFOLLOW`, `ELOOP` refuses with exit
3, and `fstat` checks the opened file. `append` reopens with
`O_NOFOLLOW` and no `O_CREAT`. Mutant J3 is killed.
- **F3.** `network-online.target` is gone from the unit. Mutant F3 is
killed.
- **M22 to M24, M26, M29.** Each now has a test and is killed, one
failure each.
## Notes (not blocking)
1. **Unguarded `broker.send` in `startHost`'s notifier-failure path**
(`host.mjs:144` and `:150`). `close()` checks `broker.connected`, but
these two lines don't. `sendclosed.mjs` shows what happens if the
broker is already gone:
- `send` returns.
- An unhandled `ERR_IPC_CHANNEL_CLOSED` follows on the next tick.
- The process exits 1, even with `exitCode` set to 3.
So a broker that dies while the notifier starts turns a refusal (exit
3, the unit stays down) into exit 1 (the unit restarts). That is a
narrow window, and a restart is not wrong there. `if
(broker.connected)` on both lines would match `close()`.
`bindLaunch` has the same exposure, which is S6's to settle.
2. **The "notifier that dies" test has no `t.after`** (see question 2).
3. **M28 in BUILD.md.** Rocko's table lists M28 as killed. In my run it
survived three times out of three: the combined run plus two reruns,
227/227 pass each (`r2-mut-M28-rerun1.txt`, `r2-mut-M28-rerun2.txt`).
No test covers the "a bus host already runs" refusal. Rocko's single
failure was probably a flaky test. Round 1 rated M28 near-equivalent,
because `writer.lock` refuses a second broker, and I still do. Only
the packet's record is wrong.
4. **N2 survives.** It swaps the directory's `lstatSync` for `statSync`,
so a symlinked notify directory is accepted. Probe T5 shows the
candidate refuses one. No test does, though.
5. **N4 survives.** It drops `O_NOFOLLOW` from `append`, so a symlink
swapped in after the open would be followed. Like J3 in round 1, this
sits behind the 0700 directory and records the boundary only.
6. **N5 survives.** It makes `watchChildren`'s early check ignore
`signalCode`. The early-death test exits with code 7, and no test
kills a child with a signal before the watch.
7. **N3 survives.** It drops the directory fsync in `copyTorn`. That
can't be tested in-process. Equivalent for the suite.
8. **T1, a confirmed line that lost only its newline.** It is treated as
torn and dropped, so the decision is DM'd again with the same nonce.
Decision 71 accepts this: a duplicate costs less than a miss, and the
nonce folds a retry inside Discord's dedupe window.
9. **T5 wording.** A symlinked directory refuses with "must be mode 0700
and owned by this user". That is true, but it doesn't say the
directory is a link.
10. **T7, a read-only journal directory.** `copyTorn` throws a raw
`EACCES`, not a `CliError`. The notifier still refuses to start, the
host exits 3, and the journal is untouched. Fail-closed, with a less
clear message.
11. **Live run.** Rocko documents that an existing notify directory
looser than 0700 needs `chmod 700` before the first start. Round 1
note 6 (`dmRecipient` is a FIXED_KEY) still applies to the binding
edit.
12. F2 and J4 stay follow-ups (decision 71). Round 1 notes 5 and 8 still
apply.
## Mutants
| Mutant | Result |
|---|---|
| M1 to M4, M7 to M17, M20 to M26, M29 to M32, M34 | killed |
| M8 constant DM nonce | killed (R2 test; survived in round 1) |
| M19 decide drops the no-TTY check | killed (cancelled 1; see question 1) |
| M22, M23, M24, M26, M29 | killed (new tests; survived in round 1) |
| M5, M6, M27 | not applied; round 2 rewrote those lines |
| M18, M33 | survived; equivalent and near-equivalent, as in round 1 |
| M28 | survived three times; near-equivalent, as in round 1 (note 3) |
| R5, R6, R6b, R6c, F1, J3, D1, RACE, F3 (Rocko) | killed |
| N1 `copyTorn` without `O_EXCL` | killed |
| N2 directory `statSync`, not `lstatSync` | **survived** (note 4) |
| N3 no directory fsync in `copyTorn` | survived; not testable in-process (note 7) |
| N4 `append` without `O_NOFOLLOW` | **survived** (note 5) |
| N5 early check ignores `signalCode` | **survived** (note 6) |
| N6 truncate to `end - 1` | killed |
46 applied: 39 killed, 7 survived.
## Files
- `mutants-r2.sh`, `gate-r2.sh`, `probe-r2.mjs`, `sendclosed.mjs` (the
round 1 scripts stay as they were)
- Output:
- `r2-probe.txt`
- `r2-sendclosed.txt`
- `r2-mut-summary.txt`
- `r2-m19-timeout.txt`, `r2-m13-host.txt`, `r2-w0-host.txt`
- `r2-mut-M28-rerun1.txt`, `r2-mut-M28-rerun2.txt`
- `r2-node24.txt`
- `r2-gate-summary.txt`
- `r2-cand-*.txt` and `r2-base-*.txt` (each suite, teed)
@@ -0,0 +1,12 @@
// Filbert, row 39 r2: what ChildProcess.send() does on a child that has exited, as
// startHost's notifier-failure path would call broker.send({op: "close"}) after the broker died.
import { fork } from "node:child_process";
import { once } from "node:events";
import { fileURLToPath } from "node:url";
if (process.argv[2] === "child") process.exit(0);
const c = fork(fileURLToPath(import.meta.url), ["child"], { stdio: ["ignore", "inherit", "inherit", "ipc"] });
await once(c, "exit");
console.log("connected", c.connected, "exitCode", c.exitCode);
try { c.send({ op: "close" }); console.log("send returned without throwing"); } catch (e) { console.log("send threw", e.code); }
process.on("exit", (code) => console.log("process exit code", code));
process.exitCode = 3; // what the CLI sets for a refusal