docs(slice1): filbert row 39 S4 round 1 review record (changes)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sequential gate; $1 = tree, $2 = out prefix
|
||||
T="$1"; P="$2"; O=~/filbert-scratch/r39/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-r39.sock timeout 900 bash "$s" > "$O/$P-suite-$n.txt" 2>&1; echo "$P suite-$n exit $?"
|
||||
done
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 39 mutants: one perl substitution each, restored after its run.
|
||||
# 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 node --test 'packages/cli/tests/*.test.mjs' 'packages/discord/tests/*.test.mjs' > "$O/mut-$id.txt" 2>&1
|
||||
local f; f=$(grep -E '^ℹ fail ' "$O/mut-$id.txt" | awk '{print $3}')
|
||||
if [ "${f:-?}" = 0 ]; then echo "$id SURVIVED"; else echo "$id killed ($f)"; 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)/'
|
||||
@@ -0,0 +1,125 @@
|
||||
// Row 39 S4 round 1 probes (Filbert). Run from the candidate tree:
|
||||
// PROBE_REPO=<tree> node probe.mjs
|
||||
import { appendFileSync, chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const R = process.env.PROBE_REPO;
|
||||
const N = await import(pathToFileURL(join(R, "packages/cli/src/notifier.mjs")));
|
||||
const base = mkdtempSync(join(homedir(), "filbert-scratch/r39/probe-"));
|
||||
let pass = 0;
|
||||
let fail = 0;
|
||||
const check = (id, ok, what) => {
|
||||
if (ok) pass++;
|
||||
else fail++;
|
||||
console.log(`${ok ? "PASS" : "FAIL"} ${id} ${what}`);
|
||||
};
|
||||
const refuses = (fn) => {
|
||||
try {
|
||||
fn();
|
||||
return null;
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
// J1: torn final line, then one confirmed send, then a restart.
|
||||
{
|
||||
const f = join(base, "j1", "sent.jsonl");
|
||||
mkdirSync(join(base, "j1"), { mode: 0o700 });
|
||||
writeFileSync(f, `${JSON.stringify({ at: "x", kind: "dm", decision: "d1", outcome: "confirmed", messageId: "m1" })}\n{"at":"x","kind":"dm","dec`, { mode: 0o600 });
|
||||
const logs = [];
|
||||
const j = N.openJournal(f, { log: (l) => logs.push(l) });
|
||||
check("J1a", logs.length === 1 && j.sent.has("d1"), "first open skips the torn line and logs it");
|
||||
j.append({ at: "y", kind: "dm", decision: "d2", outcome: "confirmed", messageId: "m2" });
|
||||
const e = refuses(() => N.openJournal(f));
|
||||
console.log(` J1 file after append: ${JSON.stringify(readFileSync(f, "utf8").split("\n")[1].slice(0, 80))}`);
|
||||
check("J1b", e === null, `restart after one more send opens the journal (got ${e ? `exit ${e.exitCode}: ${e.message}` : "ok"})`);
|
||||
}
|
||||
|
||||
// J2: journal directory with a loose mode.
|
||||
{
|
||||
const d = join(base, "j2");
|
||||
mkdirSync(d, { mode: 0o755 });
|
||||
chmodSync(d, 0o755);
|
||||
const e = refuses(() => N.openJournal(join(d, "sent.jsonl")));
|
||||
console.log(` J2 dir mode ${(statSync(d).mode & 0o777).toString(8)}; open ${e ? `refused: ${e.message}` : "accepted"}`);
|
||||
}
|
||||
|
||||
// J3: sent.jsonl is a symlink to a 0600 file elsewhere.
|
||||
{
|
||||
const d = join(base, "j3");
|
||||
mkdirSync(d, { mode: 0o700 });
|
||||
const target = join(base, "elsewhere.jsonl");
|
||||
writeFileSync(target, "", { mode: 0o600 });
|
||||
symlinkSync(target, join(d, "sent.jsonl"));
|
||||
const e = refuses(() => N.openJournal(join(d, "sent.jsonl")));
|
||||
console.log(` J3 symlinked journal: ${e ? `refused: ${e.message}` : "accepted"}`);
|
||||
}
|
||||
|
||||
// J4: a confirmed line with a non-string decision or a missing day loads.
|
||||
{
|
||||
const d = join(base, "j4");
|
||||
mkdirSync(d, { mode: 0o700 });
|
||||
const f = join(d, "sent.jsonl");
|
||||
writeFileSync(f, `${JSON.stringify({ kind: "digest", outcome: "confirmed" })}\n${JSON.stringify({ kind: "dm", outcome: "confirmed", decision: 7 })}\n`, { mode: 0o600 });
|
||||
const e = refuses(() => N.openJournal(f));
|
||||
console.log(` J4 loose-typed confirmed lines: ${e ? `refused: ${e.message}` : "accepted"}`);
|
||||
}
|
||||
|
||||
// D1: the digest nonce ignores the business.
|
||||
check("D1", N.digestNonce("2026-10-08") === "dg2026-10-08", `digest nonce is ${N.digestNonce("2026-10-08")} for every business`);
|
||||
|
||||
// D2: digest and DM content stay within 1..2000 chars over a sweep.
|
||||
{
|
||||
let worst = 0;
|
||||
let bad = 0;
|
||||
const mk = (i, qlen, blocking) => ({ id: `${String(i).padStart(8, "0")}-aaaa-bbbb`, action: "release.push", question: "q".repeat(qlen), blocking, options: [{ key: "yes", text: "y".repeat(200) }, { key: "no", text: "n" }], recommendation: "no", raised_by_role: "coder", task_ref: "T-1" });
|
||||
for (let n = 0; n <= 60; n++) {
|
||||
for (const qlen of [1, 50, 159, 160, 161, 400, 5000]) {
|
||||
const inbox = Array.from({ length: n }, (_, i) => mk(i, qlen, i % 2 === 0));
|
||||
const c = N.digestContent("acme", "2026-10-08", inbox, (id) => id.startsWith("0"));
|
||||
worst = Math.max(worst, c.length);
|
||||
if (c.length < 1 || c.length > 2000) bad++;
|
||||
if (n > 0 && !c.includes("mosaic inbox")) bad++;
|
||||
for (const d of inbox.slice(0, 2)) {
|
||||
const m = N.dmContent("acme", d);
|
||||
worst = Math.max(worst, m.length);
|
||||
if (m.length < 1 || m.length > 2000) bad++;
|
||||
}
|
||||
}
|
||||
}
|
||||
check("D2", bad === 0, `digest/DM length sweep (427 inboxes), longest ${worst}`);
|
||||
}
|
||||
|
||||
// D3: DST: 08:00 local is 13:00Z in summer and 14:00Z in winter.
|
||||
check("D3", N.zoned(new Date("2026-07-01T13:00:00Z")).hour === 8 && N.zoned(new Date("2026-12-01T14:00:00Z")).hour === 8 && N.zoned(new Date("2026-11-01T13:30:00Z")).hour === 7, "zoned 08:00 in CDT and CST; 2026-11-01 13:30Z is 07 CST");
|
||||
|
||||
// D4: digest catch-up: a tick at 07:59 local sends nothing; at 08:00 sends one; second tick none.
|
||||
{
|
||||
const d = join(base, "d4");
|
||||
let t = new Date("2026-12-01T13:59:00Z");
|
||||
const sent = [];
|
||||
const n = N.createNotifier({ business: "acme", dataRoot: d, inbox: async () => [], direct: { send: async (m) => (sent.push(m), { messageId: `m${sent.length}` }) }, now: () => t });
|
||||
await n.tick();
|
||||
const a = sent.length;
|
||||
t = new Date("2026-12-01T14:00:00Z");
|
||||
await n.tick();
|
||||
await n.tick();
|
||||
check("D4", a === 0 && sent.length === 1 && sent[0].nonce === "dg2026-12-01", `digest edge: before ${a}, after ${sent.length}`);
|
||||
}
|
||||
|
||||
// D5: a send that throws a non-RestOutcome (a bug) is journaled unknown and retried, not fatal.
|
||||
{
|
||||
const d = join(base, "d5");
|
||||
let t = new Date("2026-12-01T10:00:00Z");
|
||||
let calls = 0;
|
||||
const n = N.createNotifier({ business: "acme", dataRoot: d, inbox: async () => [{ id: "dec-1", blocking: true, action: "a", question: "q", options: [{ key: "y", text: "y" }], recommendation: "y", raised_by_role: "coder" }], direct: { send: async () => { calls++; throw new TypeError("boom"); } }, now: () => t });
|
||||
const r = await n.tick();
|
||||
const lines = readFileSync(join(d, "notify", "acme", "sent.jsonl"), "utf8").trim().split("\n").map((l) => JSON.parse(l));
|
||||
check("D5", r.failed === 2 && lines.every((l) => l.outcome === "unknown"), `TypeError from send: failed ${r.failed}, outcomes ${lines.map((l) => l.outcome).join(",")}`);
|
||||
}
|
||||
|
||||
console.log(`\n${pass} PASS, ${fail} FAIL`);
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
@@ -0,0 +1,66 @@
|
||||
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (345.522871ms)
|
||||
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (427.048359ms)
|
||||
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (441.566977ms)
|
||||
✔ launch events require a human CLI capability; generic emit cannot forge authority events (283.44889ms)
|
||||
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (446.692328ms)
|
||||
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (170.66971ms)
|
||||
✔ task action subjects and linked decision trail are complete and ordered (368.979812ms)
|
||||
✔ launch binding is durable and reconnecting requires the identical trusted record (185.070975ms)
|
||||
✔ business isolation includes inherited object names and cross-business message references (171.821867ms)
|
||||
✔ authority never transfers between action, run, target, unresolved or replaced role holder (225.672529ms)
|
||||
✔ task projection uses schema current view, skipping earlier and equal-start polls (139.363239ms)
|
||||
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (261.35952ms)
|
||||
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (213.211117ms)
|
||||
✔ both arbiters require human resolution when their cross-role route is themselves (336.739209ms)
|
||||
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (3.073206ms)
|
||||
✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.972891ms)
|
||||
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (5.252125ms)
|
||||
✔ expiry refuses use and env references never become client data (1.207147ms)
|
||||
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.715701ms)
|
||||
✔ opaque tokens shorter than 16 characters refuse before use (0.472508ms)
|
||||
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (3.165414ms)
|
||||
✔ process reader gets own kernel identity without exposing environment values (1.862049ms)
|
||||
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (3.701848ms)
|
||||
✔ real pid 1 remains inspectable when its environment is protected (0.969262ms)
|
||||
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (370.136447ms)
|
||||
✔ missing and foreign-business citations refuse and roll back message and grant (330.928876ms)
|
||||
✔ cross-role sends still need a matching resolved decision and consume it once (326.636028ms)
|
||||
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (367.828924ms)
|
||||
✔ startup token refusal returns safe code without value or partial listening broker (43.387676ms)
|
||||
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (328.064225ms)
|
||||
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (227.709519ms)
|
||||
✔ trusted host registers later launches; socket clients never have a registration verb (312.078161ms)
|
||||
✔ runtime excludes declared project roots even when host supplies no repoRoots (46.464111ms)
|
||||
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (246.870912ms)
|
||||
✔ v3b prototype refusals, views and append-only mutations (1947.449264ms)
|
||||
✔ gated approval authorizes once, survives store reopen, and fresh approval works (428.208356ms)
|
||||
✔ another run cannot consume an approval; a failed check leaves it usable (375.265364ms)
|
||||
✔ two scheduled callers have exactly one grant and one consumed refusal (237.250553ms)
|
||||
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (661.669101ms)
|
||||
✔ class drift gated to cross-role refuses before consumption (358.584232ms)
|
||||
✔ class drift cross-role to gated refuses before consumption (410.520227ms)
|
||||
✔ class drift gated to within-role refuses before consumption (287.157909ms)
|
||||
✔ class drift cross-role to within-role refuses before consumption (205.354648ms)
|
||||
✔ class drift within-role to gated refuses before consumption (169.87182ms)
|
||||
✔ class drift within-role to cross-role refuses before consumption (228.552142ms)
|
||||
✔ message.send consumes approval and prevents a later send or authorize (281.866321ms)
|
||||
✔ role.revoke consumes approval and prevents a later revoke or authorize (371.574913ms)
|
||||
✔ creates private WAL store and excludes a second writer until explicit close (293.50566ms)
|
||||
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (223.16126ms)
|
||||
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (305.591335ms)
|
||||
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (319.186738ms)
|
||||
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (160.264015ms)
|
||||
✔ async transactions refuse before invoking their function (146.885032ms)
|
||||
✔ socket capability stamps launch identity; shared views use wire, no SQL client (322.665011ms)
|
||||
✔ two wire claims serialize; a lost reply never automatically retries (230.919199ms)
|
||||
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (227.869134ms)
|
||||
✔ client preserves UTF-8 when a response divides a multibyte character (12.163385ms)
|
||||
✔ committed mutation followed by dropped reply reports unknown and is never retried (192.340835ms)
|
||||
ℹ tests 58
|
||||
ℹ suites 0
|
||||
ℹ pass 58
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 4141.695422
|
||||
@@ -0,0 +1,68 @@
|
||||
✔ config directory and file path follow MOSAIC_CONFIG (2.121194ms)
|
||||
✔ the fixture business validates and comes back frozen (6.452165ms)
|
||||
✔ two instances may share a definition (2.134752ms)
|
||||
✔ top-level refusals (5.199262ms)
|
||||
✔ arbiters and projects (7.524685ms)
|
||||
✔ role instances (3.757541ms)
|
||||
✔ Vikunja bots (8.766374ms)
|
||||
✔ a role without Vikunja takes no tracker block (2.142773ms)
|
||||
✔ credential references match the definition's services (2.576937ms)
|
||||
✔ launch (7.988747ms)
|
||||
✔ loadBusiness: file checks (1.878581ms)
|
||||
✔ loadBusiness: not a regular file (47.109419ms)
|
||||
✔ loading writes nothing (1.228293ms)
|
||||
✔ names that are Object.prototype properties don't count as declared (3.5501ms)
|
||||
✔ the shipped example refuses as written and validates once filled in (0.969864ms)
|
||||
✔ usage errors exit 4 (350.481125ms)
|
||||
✔ validate: a good business exits 0 and prints instance digests (81.30678ms)
|
||||
✔ validate: project files (502.453739ms)
|
||||
✔ validate: missing files and a broken system config (314.663149ms)
|
||||
✔ validate: credential reference problems exit 2 and name each one (105.671515ms)
|
||||
✔ validate: a token file inside the repository is refused (91.773913ms)
|
||||
✔ validate: role definitions come from MOSAIC_ROLES_DIR (319.588051ms)
|
||||
✔ resolve: prints one instance's record (271.287856ms)
|
||||
✔ resolve: refusals (576.865688ms)
|
||||
✔ parse: exactly one of file or env, plus the service's date (3.132765ms)
|
||||
✔ check: a good file has no problems (0.906848ms)
|
||||
✔ check never opens the file: a write-only token passes (0.453124ms)
|
||||
✔ check: file problems (1.182827ms)
|
||||
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (1.153042ms)
|
||||
✔ check: dates and environment references (0.567197ms)
|
||||
✔ path and load (3.146583ms)
|
||||
✔ refusals (1.775462ms)
|
||||
✔ systemVars flattens the validated config (2.734069ms)
|
||||
✔ precedence: system, business, project, project role, agent (6.835112ms)
|
||||
✔ limits narrow the definition and never widen it (3.133181ms)
|
||||
✔ role.launch stays within-role only for the instance the launch block names (5.873857ms)
|
||||
✔ limits.authority without role.launch leaves the launcher with no launch block (2.286532ms)
|
||||
✔ limits.authority narrows cross-role actions too (1.172703ms)
|
||||
✔ classify (1.436853ms)
|
||||
✔ the record carries what the broker and launcher need (1.392019ms)
|
||||
✔ digest: key order doesn't matter, any value change does (9.675767ms)
|
||||
✔ refusals (2.094174ms)
|
||||
✔ the four shipped version 2 roles load (4.860176ms)
|
||||
✔ shipped role scopes match addendum B section 2 and the SR runbook (1.472529ms)
|
||||
✔ shipped authority follows the note's table (0.794529ms)
|
||||
✔ version 1 files keep loading with no authority (1.340498ms)
|
||||
✔ the conductor policy isn't a role (0.308921ms)
|
||||
✔ a missing role file is exit 4, a symbolic link too (0.480449ms)
|
||||
✔ version 2 refusals (1.524426ms)
|
||||
✔ authority: closed vocabulary, no gated-only action, no overlap (2.786759ms)
|
||||
✔ credentials: Gitea scopes (0.933318ms)
|
||||
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (1.156786ms)
|
||||
✔ credentials: services (1.317671ms)
|
||||
✔ contract: a non-empty regular Markdown file beside the role file (1.001296ms)
|
||||
✔ every key names known layers and a merge rule (1.355683ms)
|
||||
✔ unknown keys and wrong layers refuse (1.060639ms)
|
||||
✔ types (2.698005ms)
|
||||
✔ merge: defaults, then the most specific layer wins (0.462315ms)
|
||||
✔ merge: limits only narrow, and provenance lists each source (0.53096ms)
|
||||
✔ merge doesn't change its inputs (0.222347ms)
|
||||
ℹ tests 60
|
||||
ℹ suites 0
|
||||
ℹ pass 60
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2694.425501
|
||||
@@ -0,0 +1,181 @@
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.991866ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.853409ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.768509ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.585816ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (25.708904ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (7.727245ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (10.456405ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (3.946781ms)
|
||||
✔ authorize: open channel, listed user (2.26399ms)
|
||||
✔ authorize: wrong guild (0.211867ms)
|
||||
✔ authorize: no guild (DM) (0.347613ms)
|
||||
✔ authorize: unlisted channel (0.209452ms)
|
||||
✔ authorize: unknown channel, no info (0.175509ms)
|
||||
✔ authorize: thread of listed parent (0.205757ms)
|
||||
✔ authorize: thread of unlisted parent (0.169478ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.162147ms)
|
||||
✔ authorize: unlisted user (1.282016ms)
|
||||
✔ authorize: no author (0.305478ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.137552ms)
|
||||
✔ authorize: system author (0.124114ms)
|
||||
✔ authorize: the bot itself (0.096306ms)
|
||||
✔ authorize: webhook (0.156563ms)
|
||||
✔ authorize: mention channel without mention (2.493763ms)
|
||||
✔ authorize: mention channel with bot mention (5.400877ms)
|
||||
✔ authorize: mention channel with @everyone only (0.357569ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.097817ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.086567ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.107775ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.072784ms)
|
||||
✔ authorize: thread in another guild per channel info (0.083018ms)
|
||||
✔ authorize: not an object (0.068265ms)
|
||||
✔ authorize: no id (0.06516ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.085947ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.066532ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.543754ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.178068ms)
|
||||
✔ binding: a complete binding validates and is frozen (3.351194ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.721196ms)
|
||||
✔ binding: empty allowlists refuse (0.430278ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.908215ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.958372ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (3.291355ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.874695ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (114.845914ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (1.922663ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (436.525941ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (307.43185ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (197.763683ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.098613ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.707678ms)
|
||||
✔ 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.146472ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (22.161727ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.922165ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (2.360403ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.5168ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.393632ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (32.764761ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (2.530436ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.106317ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.162747ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (41.659505ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (31.900167ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (5.824945ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (3.346ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (1.483835ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.303309ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (1.940904ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.710878ms)
|
||||
✔ 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 (3.075694ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (5.40085ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (3.977243ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.820204ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (5.046271ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (4.716101ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (1.020369ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.643927ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (2.288011ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (3.169459ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.462494ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (1.366629ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (3.027103ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.542906ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (69.047219ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (46.05589ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (39.372748ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (366.344826ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (249.142072ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (106.123841ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (243.439262ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (148.537966ms)
|
||||
✔ 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.694936ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (617.856714ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.801167ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.643121ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (435.916681ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (38.724015ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (61.524282ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (3.259735ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.838789ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.599133ms)
|
||||
✔ gateway: op 9 resumable resumes (0.487412ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.09363ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.881424ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.595796ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (71.066053ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (48.306073ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (103.092487ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.504824ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (109.074982ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (115.846377ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (153.614912ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (310.727025ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (108.667033ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (124.328012ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (314.682173ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (1166.014042ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (6.20988ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (6.640335ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.121802ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (3.627903ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (56.459965ms)
|
||||
✔ 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 (450.955141ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.565327ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (0.667884ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.405106ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (54.089469ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (166.687895ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (99.520019ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.804254ms)
|
||||
✔ 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.038549ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (47.112719ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (37.629128ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (105.740322ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (939.945936ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (4.101332ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (4.704323ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.824992ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.999776ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (7.995106ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.76169ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.858973ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.738808ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (25.907916ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (10.790342ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.833831ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.834458ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.67999ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.042848ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1013.235865ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.580633ms)
|
||||
✔ 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 (9.002066ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (2.868128ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.361119ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (4.954567ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.206487ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.772655ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (3.889209ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (2.541703ms)
|
||||
✔ tools: listing and search caps hold (11.651533ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.902246ms)
|
||||
✔ 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.101179ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.292173ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.202218ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (6.193899ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (4.36968ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.856538ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.141386ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.277415ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1034.014731ms)
|
||||
✔ 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.187079ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.364894ms)
|
||||
✔ 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.636648ms)
|
||||
✔ 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.196885ms)
|
||||
ℹ tests 173
|
||||
ℹ suites 0
|
||||
ℹ pass 173
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2867.645667
|
||||
@@ -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 'd68aa20f278816f53583fb73bd3cb1e8cdc0fdb2'.
|
||||
|
||||
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 'd68aa20f278816f53583fb73bd3cb1e8cdc0fdb2'.
|
||||
|
||||
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: 2271363 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/r39/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 (157.589774ms)
|
||||
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (214.882734ms)
|
||||
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (234.370396ms)
|
||||
✔ launch events require a human CLI capability; generic emit cannot forge authority events (154.43658ms)
|
||||
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (147.744863ms)
|
||||
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (128.923406ms)
|
||||
✔ task action subjects and linked decision trail are complete and ordered (135.580688ms)
|
||||
✔ launch binding is durable and reconnecting requires the identical trusted record (70.784072ms)
|
||||
✔ business isolation includes inherited object names and cross-business message references (128.216659ms)
|
||||
✔ authority never transfers between action, run, target, unresolved or replaced role holder (214.293626ms)
|
||||
✔ task projection uses schema current view, skipping earlier and equal-start polls (104.369741ms)
|
||||
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (202.700031ms)
|
||||
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (113.034346ms)
|
||||
✔ both arbiters require human resolution when their cross-role route is themselves (186.11149ms)
|
||||
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (4.901157ms)
|
||||
✔ only validated broker references load; returned data and exceptions cannot expose a known token (7.674124ms)
|
||||
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (13.179581ms)
|
||||
✔ expiry refuses use and env references never become client data (1.129997ms)
|
||||
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (2.104835ms)
|
||||
✔ opaque tokens shorter than 16 characters refuse before use (0.516553ms)
|
||||
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (3.290937ms)
|
||||
✔ process reader gets own kernel identity without exposing environment values (2.69631ms)
|
||||
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.407953ms)
|
||||
✔ real pid 1 remains inspectable when its environment is protected (0.446615ms)
|
||||
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (179.466727ms)
|
||||
✔ missing and foreign-business citations refuse and roll back message and grant (157.029434ms)
|
||||
✔ cross-role sends still need a matching resolved decision and consume it once (216.213587ms)
|
||||
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (191.273445ms)
|
||||
✔ startup token refusal returns safe code without value or partial listening broker (44.184184ms)
|
||||
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (163.394401ms)
|
||||
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (172.087705ms)
|
||||
✔ trusted host registers later launches; socket clients never have a registration verb (162.33158ms)
|
||||
✔ runtime excludes declared project roots even when host supplies no repoRoots (43.79939ms)
|
||||
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (151.635091ms)
|
||||
✔ v3b prototype refusals, views and append-only mutations (921.886122ms)
|
||||
✔ gated approval authorizes once, survives store reopen, and fresh approval works (235.197428ms)
|
||||
✔ another run cannot consume an approval; a failed check leaves it usable (188.114042ms)
|
||||
✔ two scheduled callers have exactly one grant and one consumed refusal (145.100506ms)
|
||||
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (262.999514ms)
|
||||
✔ class drift gated to cross-role refuses before consumption (174.183825ms)
|
||||
✔ class drift cross-role to gated refuses before consumption (161.959479ms)
|
||||
✔ class drift gated to within-role refuses before consumption (140.823855ms)
|
||||
✔ class drift cross-role to within-role refuses before consumption (165.128575ms)
|
||||
✔ class drift within-role to gated refuses before consumption (149.257083ms)
|
||||
✔ class drift within-role to cross-role refuses before consumption (164.919308ms)
|
||||
✔ message.send consumes approval and prevents a later send or authorize (195.343532ms)
|
||||
✔ role.revoke consumes approval and prevents a later revoke or authorize (207.352231ms)
|
||||
✔ creates private WAL store and excludes a second writer until explicit close (115.38464ms)
|
||||
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (145.673579ms)
|
||||
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (161.739595ms)
|
||||
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (158.645686ms)
|
||||
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (88.416456ms)
|
||||
✔ async transactions refuse before invoking their function (81.367723ms)
|
||||
✔ socket capability stamps launch identity; shared views use wire, no SQL client (135.569067ms)
|
||||
✔ two wire claims serialize; a lost reply never automatically retries (149.058021ms)
|
||||
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (101.144057ms)
|
||||
✔ client preserves UTF-8 when a response divides a multibyte character (13.072058ms)
|
||||
✔ committed mutation followed by dropped reply reports unknown and is never retried (120.466141ms)
|
||||
ℹ tests 58
|
||||
ℹ suites 0
|
||||
ℹ pass 58
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2334.386318
|
||||
@@ -0,0 +1,68 @@
|
||||
✔ config directory and file path follow MOSAIC_CONFIG (2.247029ms)
|
||||
✔ the fixture business validates and comes back frozen (6.633818ms)
|
||||
✔ two instances may share a definition (2.079058ms)
|
||||
✔ top-level refusals (6.494276ms)
|
||||
✔ arbiters and projects (9.593184ms)
|
||||
✔ role instances (4.099484ms)
|
||||
✔ Vikunja bots (9.407995ms)
|
||||
✔ a role without Vikunja takes no tracker block (2.980678ms)
|
||||
✔ credential references match the definition's services (4.080229ms)
|
||||
✔ launch (13.062449ms)
|
||||
✔ loadBusiness: file checks (2.443093ms)
|
||||
✔ loadBusiness: not a regular file (60.332879ms)
|
||||
✔ loading writes nothing (1.620853ms)
|
||||
✔ names that are Object.prototype properties don't count as declared (5.165091ms)
|
||||
✔ the shipped example refuses as written and validates once filled in (0.861003ms)
|
||||
✔ usage errors exit 4 (422.043713ms)
|
||||
✔ validate: a good business exits 0 and prints instance digests (105.035447ms)
|
||||
✔ validate: project files (427.125782ms)
|
||||
✔ validate: missing files and a broken system config (307.241615ms)
|
||||
✔ validate: credential reference problems exit 2 and name each one (95.518816ms)
|
||||
✔ validate: a token file inside the repository is refused (87.525855ms)
|
||||
✔ validate: role definitions come from MOSAIC_ROLES_DIR (285.815308ms)
|
||||
✔ resolve: prints one instance's record (278.379994ms)
|
||||
✔ resolve: refusals (502.16823ms)
|
||||
✔ parse: exactly one of file or env, plus the service's date (3.909755ms)
|
||||
✔ check: a good file has no problems (0.982554ms)
|
||||
✔ check never opens the file: a write-only token passes (0.391532ms)
|
||||
✔ check: file problems (1.499342ms)
|
||||
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (1.002701ms)
|
||||
✔ check: dates and environment references (0.599406ms)
|
||||
✔ path and load (2.380308ms)
|
||||
✔ refusals (1.438365ms)
|
||||
✔ systemVars flattens the validated config (2.454743ms)
|
||||
✔ precedence: system, business, project, project role, agent (6.249979ms)
|
||||
✔ limits narrow the definition and never widen it (3.213516ms)
|
||||
✔ role.launch stays within-role only for the instance the launch block names (9.064504ms)
|
||||
✔ limits.authority without role.launch leaves the launcher with no launch block (4.036328ms)
|
||||
✔ limits.authority narrows cross-role actions too (1.761735ms)
|
||||
✔ classify (1.653353ms)
|
||||
✔ the record carries what the broker and launcher need (1.368244ms)
|
||||
✔ digest: key order doesn't matter, any value change does (10.405106ms)
|
||||
✔ refusals (3.017749ms)
|
||||
✔ the four shipped version 2 roles load (4.981049ms)
|
||||
✔ shipped role scopes match addendum B section 2 and the SR runbook (1.460327ms)
|
||||
✔ shipped authority follows the note's table (0.791107ms)
|
||||
✔ version 1 files keep loading with no authority (1.248782ms)
|
||||
✔ the conductor policy isn't a role (0.328608ms)
|
||||
✔ a missing role file is exit 4, a symbolic link too (0.536317ms)
|
||||
✔ version 2 refusals (1.944397ms)
|
||||
✔ authority: closed vocabulary, no gated-only action, no overlap (2.922779ms)
|
||||
✔ credentials: Gitea scopes (1.349151ms)
|
||||
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (1.633512ms)
|
||||
✔ credentials: services (0.882165ms)
|
||||
✔ contract: a non-empty regular Markdown file beside the role file (1.18187ms)
|
||||
✔ every key names known layers and a merge rule (0.886623ms)
|
||||
✔ unknown keys and wrong layers refuse (1.149448ms)
|
||||
✔ types (2.318627ms)
|
||||
✔ merge: defaults, then the most specific layer wins (0.405798ms)
|
||||
✔ merge: limits only narrow, and provenance lists each source (0.514157ms)
|
||||
✔ merge doesn't change its inputs (0.234247ms)
|
||||
ℹ tests 60
|
||||
ℹ suites 0
|
||||
ℹ pass 60
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2597.243526
|
||||
@@ -0,0 +1,45 @@
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (98.278528ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (125.163588ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (92.707937ms)
|
||||
✔ decide prints a declining choice as declining (95.741332ms)
|
||||
✔ an unknown outcome is reported once and never resent (81.962323ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (45.953551ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (46.449623ms)
|
||||
✔ agents and tasks print through the broker (83.023299ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (7.967045ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (64.626568ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (50.171832ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (42.368165ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (42.066317ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (78.66042ms)
|
||||
✔ empty views say so (1.355696ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.687364ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.278509ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (979.495158ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (224.208015ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (228.13232ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (272.223452ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (120.733662ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (771.564129ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (50.72064ms)
|
||||
✔ zoned uses the IANA zone across DST (24.109218ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (103.574053ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (91.244207ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (95.320613ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (80.705267ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.857406ms)
|
||||
✔ no Discord id reaches the journal or the log (93.864798ms)
|
||||
✔ the journal: a torn last line is skipped, a malformed line or a loose mode refuses (1.377894ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.386801ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.566277ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (45.086525ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2187.431879ms)
|
||||
✔ busExit and refuseInsideAgent (0.520981ms)
|
||||
ℹ tests 37
|
||||
ℹ suites 0
|
||||
ℹ pass 37
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2803.844436
|
||||
@@ -0,0 +1,186 @@
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (12.706939ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (5.764051ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (7.566468ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.995741ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (38.336621ms)
|
||||
✔ 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.395638ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (11.589773ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (6.470232ms)
|
||||
✔ authorize: open channel, listed user (2.025499ms)
|
||||
✔ authorize: wrong guild (0.32383ms)
|
||||
✔ authorize: no guild (DM) (1.290476ms)
|
||||
✔ authorize: unlisted channel (0.25834ms)
|
||||
✔ authorize: unknown channel, no info (0.190158ms)
|
||||
✔ authorize: thread of listed parent (0.569506ms)
|
||||
✔ authorize: thread of unlisted parent (1.333381ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (4.451006ms)
|
||||
✔ authorize: unlisted user (0.3146ms)
|
||||
✔ authorize: no author (0.358961ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.292437ms)
|
||||
✔ authorize: system author (0.158314ms)
|
||||
✔ authorize: the bot itself (0.092842ms)
|
||||
✔ authorize: webhook (0.148654ms)
|
||||
✔ authorize: mention channel without mention (0.286772ms)
|
||||
✔ authorize: mention channel with bot mention (0.772901ms)
|
||||
✔ authorize: mention channel with @everyone only (0.419414ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.159232ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.12284ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.112841ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.086922ms)
|
||||
✔ authorize: thread in another guild per channel info (0.092072ms)
|
||||
✔ authorize: not an object (0.729575ms)
|
||||
✔ authorize: no id (0.130448ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.093947ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.078506ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (1.364029ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.232417ms)
|
||||
✔ binding: a complete binding validates and is frozen (3.254847ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (2.041286ms)
|
||||
✔ binding: empty allowlists refuse (0.716762ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.749186ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (4.484196ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (4.102947ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (2.929102ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.836278ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (150.934705ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.045947ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (409.016799ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (322.732436ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (184.423251ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.560055ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.461387ms)
|
||||
✔ 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 (25.146486ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (24.71943ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.744631ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (4.017534ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (3.374441ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (4.887526ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (32.529615ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.743256ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.0251ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (1.075254ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (41.617425ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.581695ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (5.252804ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (3.706118ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (4.125556ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (4.789809ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.730712ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.526137ms)
|
||||
✔ 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.952413ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.378462ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.212779ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.426159ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (3.09644ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (12.891189ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (3.647852ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.904691ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.677825ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (4.853621ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (4.947595ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.849847ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (5.368302ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (1.226371ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (107.033502ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (63.818299ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (42.255924ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (344.755134ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (251.990584ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (108.724674ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (255.988873ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (136.893442ms)
|
||||
✔ 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.99112ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (615.851085ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.513739ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.560912ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (437.363964ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (30.347024ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (51.504732ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (3.193741ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (2.285417ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.739426ms)
|
||||
✔ gateway: op 9 resumable resumes (0.589898ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.443946ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.655803ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.451018ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (86.785692ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (47.586388ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (91.552876ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.463205ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (105.806954ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (123.419333ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (147.237328ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (343.702406ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (94.888588ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (116.824952ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (259.187461ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (942.967811ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (8.778041ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (11.200806ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (2.3718ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (7.75423ms)
|
||||
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (81.257582ms)
|
||||
✔ 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 (397.113082ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.404544ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (0.430983ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.86426ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (53.576304ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (150.380081ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (110.971911ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (1.076422ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (6.658024ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (4.299482ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.824233ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (63.925791ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (43.099931ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (38.201778ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (88.834611ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (953.281324ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (5.076501ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (9.560275ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.908581ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.95532ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.953824ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (8.777398ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.703766ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (2.325637ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (3.231525ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (33.277354ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (13.626319ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (8.598593ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.570522ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.54395ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.444602ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1014.124706ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.893219ms)
|
||||
✔ 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 (9.777572ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.665942ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.275876ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (3.165245ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.637984ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (5.022056ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (6.469087ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (5.531425ms)
|
||||
✔ tools: listing and search caps hold (13.958999ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.942619ms)
|
||||
✔ 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 (6.371998ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.383966ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.125258ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (7.337062ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.499635ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.996341ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.790844ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.953583ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1040.353738ms)
|
||||
✔ 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.394779ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.501611ms)
|
||||
✔ 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.758476ms)
|
||||
✔ 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.847279ms)
|
||||
ℹ tests 178
|
||||
ℹ suites 0
|
||||
ℹ pass 178
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2819.110201
|
||||
@@ -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 'd68aa20f278816f53583fb73bd3cb1e8cdc0fdb2'.
|
||||
|
||||
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 'd68aa20f278816f53583fb73bd3cb1e8cdc0fdb2'.
|
||||
|
||||
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: 2204219 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/r39/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
|
||||
cand suite-conductor exit 0
|
||||
cand suite-config exit 0
|
||||
cand suite-discord exit 0
|
||||
cand suite-extension-package exit 0
|
||||
cand suite-foundation exit 0
|
||||
cand suite-queue exit 0
|
||||
cand suite-release exit 0
|
||||
cand suite-task exit 1
|
||||
base node-bus exit 0
|
||||
base node-business exit 0
|
||||
base node-discord exit 0
|
||||
base suite-auth exit 0
|
||||
base suite-conductor exit 0
|
||||
base suite-config exit 0
|
||||
base suite-discord exit 0
|
||||
base suite-extension-package exit 0
|
||||
base suite-foundation exit 0
|
||||
base suite-queue exit 0
|
||||
base suite-release exit 0
|
||||
base suite-task exit 1
|
||||
@@ -0,0 +1,34 @@
|
||||
M1 killed (1)
|
||||
M2 killed (1)
|
||||
M3 killed (2)
|
||||
M4 killed (1)
|
||||
M5 killed (1)
|
||||
M6 killed (9)
|
||||
M7 killed (4)
|
||||
M8 SURVIVED
|
||||
M9 killed (1)
|
||||
M10 killed (1)
|
||||
M11 killed (1)
|
||||
M12 killed (1)
|
||||
M13 killed (2)
|
||||
M14 killed (1)
|
||||
M15 killed (2)
|
||||
M16 killed (2)
|
||||
M17 killed (1)
|
||||
M18 SURVIVED
|
||||
M19 killed (1)
|
||||
M20 killed (1)
|
||||
M21 killed (1)
|
||||
M22 SURVIVED
|
||||
M23 SURVIVED
|
||||
M24 SURVIVED
|
||||
M25 killed (1)
|
||||
M26 SURVIVED
|
||||
M27 SURVIVED
|
||||
M28 SURVIVED
|
||||
M29 SURVIVED
|
||||
M30 killed (1)
|
||||
M31 killed (1)
|
||||
M32 killed (2)
|
||||
M33 SURVIVED
|
||||
M34 killed (1)
|
||||
@@ -0,0 +1,224 @@
|
||||
v24.21.0
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (150.435882ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (200.178803ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (118.123019ms)
|
||||
✔ decide prints a declining choice as declining (134.534ms)
|
||||
✔ an unknown outcome is reported once and never resent (128.982594ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (123.245098ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (76.722535ms)
|
||||
✔ agents and tasks print through the broker (70.33817ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.89813ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (110.715533ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (64.50296ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (64.507858ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (47.644944ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (116.847144ms)
|
||||
✔ empty views say so (3.14604ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (4.254618ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.681733ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1142.966359ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (238.34223ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (269.794327ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (211.442714ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (88.527484ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (767.121333ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (121.357435ms)
|
||||
✔ zoned uses the IANA zone across DST (58.838911ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (152.651058ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (196.852785ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (126.049679ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (118.867316ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.372407ms)
|
||||
✔ no Discord id reaches the journal or the log (134.687969ms)
|
||||
✔ the journal: a torn last line is skipped, a malformed line or a loose mode refuses (1.840228ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.364783ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (112.057763ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (60.499405ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2287.730105ms)
|
||||
✔ busExit and refuseInsideAgent (0.344819ms)
|
||||
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.904371ms)
|
||||
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.869811ms)
|
||||
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.668165ms)
|
||||
✔ approvals: a button approves only on its own request message with the matching custom id (0.568448ms)
|
||||
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (29.744837ms)
|
||||
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (10.792007ms)
|
||||
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (12.091111ms)
|
||||
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (4.453013ms)
|
||||
✔ authorize: open channel, listed user (2.631297ms)
|
||||
✔ authorize: wrong guild (0.215547ms)
|
||||
✔ authorize: no guild (DM) (0.203643ms)
|
||||
✔ authorize: unlisted channel (0.203563ms)
|
||||
✔ authorize: unknown channel, no info (0.198187ms)
|
||||
✔ authorize: thread of listed parent (0.228421ms)
|
||||
✔ authorize: thread of unlisted parent (0.158194ms)
|
||||
✔ authorize: text channel that is not a thread and not listed (0.144199ms)
|
||||
✔ authorize: unlisted user (1.375392ms)
|
||||
✔ authorize: no author (0.429395ms)
|
||||
✔ authorize: bot author (listed id, bot flag) (0.354272ms)
|
||||
✔ authorize: system author (0.382177ms)
|
||||
✔ authorize: the bot itself (0.105437ms)
|
||||
✔ authorize: webhook (0.119066ms)
|
||||
✔ authorize: mention channel without mention (0.761486ms)
|
||||
✔ authorize: mention channel with bot mention (0.154281ms)
|
||||
✔ authorize: mention channel with @everyone only (0.255599ms)
|
||||
✔ authorize: mention channel mentioning someone else (0.105281ms)
|
||||
✔ authorize: mention channel, content says @bot but mentions empty (0.095571ms)
|
||||
✔ authorize: private thread under mention channel, mentioned (0.095101ms)
|
||||
✔ authorize: private thread under mention channel, not mentioned (0.086938ms)
|
||||
✔ authorize: thread in another guild per channel info (0.084159ms)
|
||||
✔ authorize: not an object (0.074414ms)
|
||||
✔ authorize: no id (0.072953ms)
|
||||
✔ authorize: oversize content is accepted and flagged (0.081435ms)
|
||||
✔ authorize: exactly the limit is not oversize (0.083942ms)
|
||||
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.542027ms)
|
||||
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.169126ms)
|
||||
✔ binding: a complete binding validates and is frozen (3.336891ms)
|
||||
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (2.35773ms)
|
||||
✔ binding: empty allowlists refuse (3.591544ms)
|
||||
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (3.436071ms)
|
||||
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (5.557911ms)
|
||||
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.793604ms)
|
||||
✔ binding: file must be 0600, regular, not a symlink (2.449567ms)
|
||||
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (3.969886ms)
|
||||
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (156.289692ms)
|
||||
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.362516ms)
|
||||
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (449.667173ms)
|
||||
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (219.503697ms)
|
||||
✔ cli: run refuses when STOP is present, before any network use (129.18445ms)
|
||||
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (3.395845ms)
|
||||
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.395129ms)
|
||||
✔ 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.669603ms)
|
||||
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (38.455002ms)
|
||||
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (3.515252ms)
|
||||
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.892221ms)
|
||||
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.942892ms)
|
||||
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (6.720968ms)
|
||||
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (34.793536ms)
|
||||
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (5.684398ms)
|
||||
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (3.760396ms)
|
||||
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (4.591196ms)
|
||||
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (43.702178ms)
|
||||
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (33.167424ms)
|
||||
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (7.065196ms)
|
||||
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (5.348704ms)
|
||||
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (3.188081ms)
|
||||
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (5.924764ms)
|
||||
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (3.004252ms)
|
||||
✔ journal: no token-shaped string and no model output on the drop path reaches disk (1.342169ms)
|
||||
✔ 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 (3.204885ms)
|
||||
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (5.018006ms)
|
||||
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (8.783793ms)
|
||||
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.410026ms)
|
||||
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (6.741756ms)
|
||||
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (3.597169ms)
|
||||
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.977235ms)
|
||||
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (3.142584ms)
|
||||
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.884853ms)
|
||||
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (2.199674ms)
|
||||
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.445207ms)
|
||||
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.928253ms)
|
||||
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.806951ms)
|
||||
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.643047ms)
|
||||
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (67.856538ms)
|
||||
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (53.284192ms)
|
||||
✔ engine: one prompt, one turn, text and usage come back (53.025172ms)
|
||||
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (346.769288ms)
|
||||
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (241.93361ms)
|
||||
✔ engine: timeout sends abort and fails only that turn; the process stays (106.049225ms)
|
||||
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (240.039179ms)
|
||||
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (132.438308ms)
|
||||
✔ 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.508216ms)
|
||||
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (617.115448ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.456684ms)
|
||||
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.665191ms)
|
||||
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (426.354012ms)
|
||||
✔ engine: a malformed JSONL line fails the turn, not the process (23.667112ms)
|
||||
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (45.734903ms)
|
||||
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (5.207451ms)
|
||||
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.91995ms)
|
||||
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.679031ms)
|
||||
✔ gateway: op 9 resumable resumes (0.482662ms)
|
||||
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (1.235122ms)
|
||||
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.765713ms)
|
||||
✔ gateway: close() is final and unparseable frames are ignored (0.578199ms)
|
||||
✔ git: config validation is strict, needs write: true, a work tree and a private token file (172.457051ms)
|
||||
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (98.62783ms)
|
||||
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (133.981392ms)
|
||||
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.309823ms)
|
||||
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (115.185496ms)
|
||||
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (112.794483ms)
|
||||
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (112.277117ms)
|
||||
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (203.637551ms)
|
||||
✔ git: push pushes the named branch only and reports up to date (66.93841ms)
|
||||
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (104.082841ms)
|
||||
✔ git: the credential helper answers get over https from a private file and nothing else (204.297929ms)
|
||||
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (512.581464ms)
|
||||
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.592493ms)
|
||||
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (7.19046ms)
|
||||
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (2.568321ms)
|
||||
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (20.07439ms)
|
||||
✔ 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.344777ms)
|
||||
✔ 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 (422.903726ms)
|
||||
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.457342ms)
|
||||
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (0.517485ms)
|
||||
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.975483ms)
|
||||
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (49.395811ms)
|
||||
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (130.119921ms)
|
||||
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (70.542802ms)
|
||||
✔ notices: a kind is recorded per UTC day and found again (0.556458ms)
|
||||
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (7.224783ms)
|
||||
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.877674ms)
|
||||
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.789212ms)
|
||||
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (47.696558ms)
|
||||
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (58.960783ms)
|
||||
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (77.434184ms)
|
||||
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (82.63822ms)
|
||||
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (739.571771ms)
|
||||
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (3.8089ms)
|
||||
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.466246ms)
|
||||
✔ rest: content and nonce limits are enforced locally; typing never throws (0.862413ms)
|
||||
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.916757ms)
|
||||
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.312148ms)
|
||||
✔ setspark config: a bare https or loopback origin, a private key file, a principal (7.18167ms)
|
||||
✔ setspark config: reaches the tools config and the binding as a fixed key (3.947701ms)
|
||||
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (2.145815ms)
|
||||
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (3.191331ms)
|
||||
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (52.161187ms)
|
||||
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (24.261416ms)
|
||||
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (9.563071ms)
|
||||
✔ setspark keys: read per call, one printable token per file, rotation without a restart (4.746867ms)
|
||||
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.72222ms)
|
||||
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (3.763035ms)
|
||||
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1026.486885ms)
|
||||
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.558047ms)
|
||||
✔ 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.889988ms)
|
||||
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (2.382002ms)
|
||||
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.237737ms)
|
||||
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (3.045548ms)
|
||||
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (3.900058ms)
|
||||
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.356126ms)
|
||||
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.103007ms)
|
||||
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.264669ms)
|
||||
✔ tools: listing and search caps hold (17.80897ms)
|
||||
✔ tools: credential shapes are caught; ordinary prose and ids are not (1.040674ms)
|
||||
✔ 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 (30.477334ms)
|
||||
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.583548ms)
|
||||
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.419993ms)
|
||||
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (10.106202ms)
|
||||
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.714621ms)
|
||||
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (2.740889ms)
|
||||
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (4.166067ms)
|
||||
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (3.305128ms)
|
||||
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1042.751174ms)
|
||||
✔ 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.436221ms)
|
||||
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.303589ms)
|
||||
✔ 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.9654ms)
|
||||
✔ 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.465095ms)
|
||||
ℹ tests 215
|
||||
ℹ suites 0
|
||||
ℹ pass 215
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3008.126265
|
||||
@@ -0,0 +1,13 @@
|
||||
PASS J1a first open skips the torn line and logs it
|
||||
J1 file after append: "{\"at\":\"x\",\"kind\":\"dm\",\"dec{\"at\":\"y\",\"kind\":\"dm\",\"decision\":\"d2\",\"outcome\":\"confi"
|
||||
FAIL J1b restart after one more send opens the journal (got exit 3: notify journal line 2 is malformed: /home/jwoltje/filbert-scratch/r39/probe-jkXow7/j1/sent.jsonl)
|
||||
J2 dir mode 755; open accepted
|
||||
J3 symlinked journal: accepted
|
||||
J4 loose-typed confirmed lines: accepted
|
||||
PASS D1 digest nonce is dg2026-10-08 for every business
|
||||
PASS D2 digest/DM length sweep (427 inboxes), longest 1997
|
||||
PASS D3 zoned 08:00 in CDT and CST; 2026-11-01 13:30Z is 07 CST
|
||||
PASS D4 digest edge: before 0, after 1
|
||||
FAIL D5 TypeError from send: failed 1, outcomes unknown
|
||||
|
||||
5 PASS, 2 FAIL
|
||||
@@ -0,0 +1,169 @@
|
||||
# Slice 1 S4, row 39, round 1 review (Filbert)
|
||||
|
||||
Issue #1521, request comment 26841, queue rev 176. Packet:
|
||||
`agents/rocko/work/slice1-s4/BUILD.md`. Candidate: manifest
|
||||
`candidate-manifest.sha256`, sha256
|
||||
`895a7021805ad3e8d3c47df88a8ceb03d6345ac3bc95059558691c981ea837be`, 29
|
||||
files, over `d68aa20f` with `build.patch` (sha256
|
||||
`b725998c92c3dae3ceea93059d854778999e27f4ccf908d82e61b47d0c0b872a`).
|
||||
Brief: `docs/plans/2026-10-04_slice-1.md`, S4. Ruling: lead decision 70.
|
||||
Second reviewer: Darkwing, comment 26843 (approve, F1 to F4).
|
||||
|
||||
Verdict: **changes.** There is one blocker, B1. A torn journal line turns
|
||||
into a permanent exit-3 refusal after the next send, which is the case
|
||||
choice 6 exists to handle. The fix is small. I also ask for Darkwing's F1
|
||||
and one test (R2) in round 2. Everything else is a note.
|
||||
|
||||
## Method
|
||||
|
||||
- Detached worktrees at `d68aa20f`, one for the base and one for the
|
||||
candidate, under `~/filbert-scratch/r39/`. I ran `git apply build.patch`
|
||||
in the candidate and then `sha256sum -c`: 29 OK. After the mutant run
|
||||
the tree checks clean against the manifest again.
|
||||
- `gate.sh` runs the suites one at a time in both trees and tees each
|
||||
output. `DOCKER_HOST` points at a socket that doesn't exist, so the
|
||||
Docker cases skip the same way in both.
|
||||
- `probe.mjs` exercises the notifier journal and digest/DM code directly
|
||||
(J1 to J4, D1 to D5).
|
||||
- `mutants.sh` applies 34 single perl substitutions to `packages/cli/src`
|
||||
and `packages/discord/src`. For each one it runs the cli and discord
|
||||
node tests, then restores the file.
|
||||
- Node 24.21.0 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) | 37/37 | n/a |
|
||||
| node cli + discord (Node 24.21.0) | 215/215 | 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". That matches Rocko's and Sage's
|
||||
numbers.
|
||||
|
||||
## B1 (blocking): a torn line breaks the journal on the next append
|
||||
|
||||
`openJournal` pops the torn final line, logs it and skips it. It leaves
|
||||
the file as it is. The next `append` then writes onto the end of the
|
||||
fragment:
|
||||
|
||||
```
|
||||
{"at":"x","kind":"dm","dec{"at":"y","kind":"dm","decision":"d2","outcome":"confi...
|
||||
```
|
||||
|
||||
The next start reads that as a malformed middle line and refuses with exit
|
||||
3 ("notify journal line 2 is malformed"). Probe J1b shows this. Three
|
||||
things follow:
|
||||
|
||||
- The confirmed record of the d2 send is lost inside the bad line.
|
||||
- `RestartPreventExitStatus=2 3 4` keeps the unit down, so blocking DMs
|
||||
and the digest stop until someone edits the journal by hand.
|
||||
- Choice 6 and the README say a torn final line "is skipped". In practice
|
||||
it is skipped once, then breaks the journal on the next append.
|
||||
|
||||
The existing test, "the journal: a torn last line is skipped...", only
|
||||
checks the first open.
|
||||
|
||||
Fix: when the tail is torn, truncate the file to the last newline (or
|
||||
write a `"\n"`) before the first append, and log either way. Add a test
|
||||
that opens a journal with a torn tail, appends once and reopens: the
|
||||
reopen must succeed and must load the appended record.
|
||||
|
||||
## Asked for in round 2 (not blocking on their own)
|
||||
|
||||
- **F1 (Darkwing):** the journal directory is created at 0700, but an
|
||||
existing 0755 directory is accepted (probe J2). Check the directory mode
|
||||
as the file mode is checked, or document the exception.
|
||||
- **R2, a test that DM nonces differ per decision.** Mutant M8 makes
|
||||
`dmNonce` a constant and survives. The fake Discord doesn't model nonce
|
||||
dedupe. Under M8, Discord would return the first message for a second
|
||||
decision's DM inside its dedupe window, and the notifier would journal
|
||||
it as confirmed. Every DM after the first would then go missing with no
|
||||
error. Asserting that two decisions get different nonces is one line.
|
||||
|
||||
## Notes (not blocking)
|
||||
|
||||
1. **D1, the digest nonce has no business in it.** The nonce is
|
||||
`dg<day>`. Two businesses with the same bot and recipient would send
|
||||
the same nonce on the same day. Discord dedupes the second digest and
|
||||
the notifier journals it as confirmed. Slice 1 runs one business, so
|
||||
this doesn't bite yet. `"dg" + sha256(business + day)` truncated would
|
||||
avoid it.
|
||||
2. **Host startup race.** `broker.on("exit")` and `notify.on("exit")` are
|
||||
attached only after the notifier's start reply. If the broker dies
|
||||
while the notifier starts (up to `START_TIMEOUT_MS`), no listener sees
|
||||
it, and the host runs with a dead broker. Checking
|
||||
`broker.exitCode !== null` after attaching the listeners closes the
|
||||
window.
|
||||
3. **J3:** a symlinked `sent.jsonl` is accepted, because `openSync(file,
|
||||
"a")` follows links. The mode and uid checks run on the target. The
|
||||
directory is the user's own, so this records the boundary only.
|
||||
4. **J4:** confirmed lines with loose types load, for example a
|
||||
`decision` that is a number or a digest line with no `day`. They do no
|
||||
harm, since lookups are by string.
|
||||
5. **Reader-cap exposure test.** host.test checks that the launch cap is
|
||||
absent from `/proc/<pid>/cmdline` and `environ`. It doesn't check the
|
||||
reader cap sent to the notifier. By inspection it travels only over
|
||||
IPC.
|
||||
6. **`dmRecipient` is a FIXED_KEY.** After the binding edit for the live
|
||||
run, a connector reload refuses the change and keeps the old binding.
|
||||
The notifier reads the binding fresh at its own start, so it is
|
||||
unaffected. The running connector keeps its old binding, including any
|
||||
other change made in the same edit, until it restarts. Sage should know
|
||||
this before the live run.
|
||||
7. **F2 and F3 (Darkwing):** I agree they don't block. A refused DM retries
|
||||
at most every 30 min, forever. `network-online.target` does nothing in
|
||||
the user manager.
|
||||
8. **human-cli timeout.** The `spawnSync` timeout kills the direct child.
|
||||
An inner re-exec child that keeps the pipes open could hold the call
|
||||
past it. This is untested and lives in the bus shim, not in this row.
|
||||
|
||||
## Mutants
|
||||
|
||||
20 of 34 killed. M19 hung on a stdin prompt under the mutant. I counted
|
||||
it as killed when I stopped it.
|
||||
|
||||
| Mutant | Result |
|
||||
|---|---|
|
||||
| M1 to M7, M9 to M17, M20, M21, M25, M30 to M32, M34 | killed |
|
||||
| M8 constant DM nonce | **survived** (R2) |
|
||||
| M18 transport drops `status === null` | survived; equivalent, a signalled child still ends as invalid-response, exit 1 |
|
||||
| M19 decide drops the no-TTY check | killed (hang) |
|
||||
| M22 cli drops the `decision-closed` message | survived; test gap, the generic error still exits nonzero |
|
||||
| M23 cli drops the ambiguous-prefix message | survived; test gap, it falls through to "no open decision", exit 2 |
|
||||
| M24 `businessFor` uses a stale host.json | survived; test gap, no test for a dead host's state file |
|
||||
| M26 `trackerFor` counts projects without `tracker.project` | survived; test gap, no fixture has a project without one |
|
||||
| M27 host exits 0 on child death | survived; near-equivalent, `close()` already maps a nonzero child end to 1 |
|
||||
| M28 `startHost` skips the live-host check | survived; near-equivalent, the broker's `writer.lock` refuses a second boot |
|
||||
| M29 `stopHost` skips the cmdline check | survived; test gap, the start-time check already guards pid reuse |
|
||||
| M33 notifier drops its SIGTERM handler | survived; near-equivalent, the default action still ends it, and KillMode=mixed signals the host only |
|
||||
|
||||
Tests for M22 to M24, M26 and M29 would each be short. They are optional.
|
||||
|
||||
## Darkwing's F4
|
||||
|
||||
The S1 `baseUrl` validator accepts a path. Darkwing is documenting that in
|
||||
`packages/tasks`, so it stays out of this row.
|
||||
|
||||
## Files
|
||||
|
||||
- `probe.mjs`, `mutants.sh`, `gate.sh`
|
||||
- Output:
|
||||
- `r1-probe.txt`
|
||||
- `r1-mut-summary.txt`
|
||||
- `r1-node24.txt`
|
||||
- `r1-gate-summary.txt`
|
||||
- `r1-cand-*.txt` and `r1-base-*.txt` (each suite, teed)
|
||||
Reference in New Issue
Block a user