docs(build): row 41 slice 1 S6 round 3 candidate packet (filbert)

R4: the gate checks a read under every spelling Pi's read would open
(AM/PM U+202F, NFD, U+2019, NFD with U+2019), from the workspace and its
real path, for Pi read and Claude Read. Mr test, launcher test timeouts.
Candidate manifest 9f0593af (42 files), gate at 66b9e082 green but
test-task Docker recall, same on base. Candidate source not committed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-09 21:50:18 -05:00
co-authored by Claude Opus 5.5
parent 66b9e082e9
commit 0f38236fa6
21 changed files with 1524 additions and 1228 deletions
+149 -19
View File
@@ -1,14 +1,141 @@
# Row 41 (#1523): slice 1 S6, candidate packet, round 2
# Row 41 (#1523): slice 1 S6, candidate packet, round 3
Author: Filbert. Reviewer: Darkwing. Brief: `docs/plans/2026-10-04_slice-1.md`,
section "Slice 1 S6", blob `72d11de2`. Plan: `PLAN.md` here (b6d2fe2b).
Rulings: lead decisions 77 and 78, and Sage's round 2 ruling (fix R1, R2
and R3). Base: `915e00e5` (`base.txt`). None of the 42 files changed between
the base and `8dd5ff00`, where the round 2 gate ran, or `54cb7ab8`. The
Rulings: lead decisions 77 and 78, Sage's round 2 ruling (fix R1, R2
and R3) and round 3 ruling (fix R4). Base: `915e00e5` (`base.txt`). None of
the 42 files changed between the base and `66b9e082`, where the round 3 gate
ran. The
candidate source is uncommitted. There are no pushes. No token, private
binding or tracker host was read or written, and nothing touched the live
`mosaic-bus@mosaic-stack` unit or `~/.mosaic-dev/bus/`.
## Round 3
Darkwing's round 2 asked for changes (#1523 comment 27010, rev 259, packet
`agents/darkwing/work/s6-review/review-r2.md` and `r2/`). Sage ruled: fix
R4 in this round; R1 to R3 hold and stay closed. Six files changed since
round 2, and none is new:
- `packages/harness/src/gate.mjs`, `README.md`
- `packages/harness/tests/gate.test.mjs`, `pi-session.test.mjs`
- `packages/cli/tests/host.test.mjs`, `launcher.test.mjs`
No source outside the harness gate changed.
### R4: a read is checked under every spelling Pi's read would open
When the name it is given doesn't exist, Pi 0.85.1's `read` opens another
spelling. `resolveReadPathAsync` (`dist/core/tools/path-utils.js`) tries
these in order, and opens the first that exists:
1. ` AM.` or ` PM.` with U+202F before the `AM`/`PM`;
2. the NFD form;
3. `'` changed to U+2019;
4. both 2 and 3.
The `pi` binary runs the same code from `dist/bundle/`. In 0.85.1 only
`read` and the CLI's file arguments call it. `write`, `edit`, `grep`,
`find` and `ls` take the name as given. The gate checked only the given
name, so asking for `notes's.txt` could read a link `notes’s.txt` that
points outside.
The fix is `otherSpelling` in `gate.mjs`. It covers Pi's `read` and
Claude Code's `Read` (Darkwing's note 3):
- It builds every spelling from the workspace path and from its real
path. The adapter starts Pi with `cd "$MOSAIC_WORKSPACE"`, so Pi resolves
a relative name against the real path.
- A spelling that exists and resolves outside the workspace refuses the
read. So does one that goes through a dangling link.
- A spelling that doesn't exist is skipped, and so is ENOTDIR. Any other
`lstat` error refuses the read.
It doesn't depend on which spelling Pi would open. So a read is refused
even when the exact name exists inside and some spelling points outside.
The README's "Limits" says so, with the example of a sibling `jo’s/ws`
beside a workspace in `jo's/ws`. Its pin note lists what to recheck on a
Pi upgrade.
Tests:
- `gate.test.mjs`, "read is checked under every spelling pi's read would
open, in both harnesses". It asks for six names, relative and absolute,
in both harnesses, covering each family and their mixes:
- straight quote
- AM/PM
- NFD
- NFD with a quote
- NFD with a straight quote kept
- a quote with composed accents
Each is refused when its other spelling links outside. It is allowed
when that spelling links to a file inside. A `write` of the asked name is
allowed.
- `gate.test.mjs`, "other spellings cover directories, dangling links and
pi's cwd". These are refused:
- a directory spelling;
- a dangling spelling;
- a workspace given through a link, whose real path has a sibling
`jo’s/ws`.
A name with no other spelling is allowed, and so is ENOTDIR.
- `pi-session.test.mjs`, "pi: a read is refused when pi would open another
spelling outside". A real Pi 0.85.1 session runs through the adapter and
the mock API. It reads the six names, plus one whose other spelling is
inside:
- each of the six comes back as an error naming the other spelling;
- the outside file's text never reaches stdout;
- the inside one returns its content.
Darkwing's `r2/probe/pi-variant.sh` against this round refuses all four
modes: plain, quote, ampm and nfd. Plain is refused by the existing check.
Mutants: each was made on `gate.mjs`, restored from a copy and checked
with `cmp`. The base passes `gate.test.mjs` and `pi-session.test.mjs`,
16/16.
| Mutant | Change | Result |
|---|---|---|
| none | no other-spelling check | 13/3 |
| pibase | spellings from the given workspace path only | 15/1: Pi's cwd case |
| ampm | no AM/PM spelling | 14/2 |
| nfd | no NFD spelling | 14/2 |
| curly | no U+2019 spelling | 14/2 |
| nfdcurly | no NFD-with-U+2019 spelling | 14/2 |
| pionly | Pi's `read` only, not Claude's `Read` | 15/1 |
| danglingok | a dangling spelling is skipped | 15/1 |
| enotdir | ENOTDIR refuses | 15/1 |
| existsonly | a spelling that is a link is skipped | 13/3 |
The first draft of the tests let `nfd` and `curly` survive. Their names
had both features, so the combined spelling alone caught them. The two
single-feature names were added for that.
### Darkwing's other round 2 notes
1. **Mr.** `host.test.mjs` has a new test, "a broker reply with no
request waiting breaks the channel and the host exits 1". It binds a
coder, then has the broker child send an `identity` reply with an id
that no request waits for. The host logs "broker channel broken (reply
with no request waiting)" and exits 1, and a later op refuses with
`broker-channel-broken`. Under Mr (such a reply is ignored), the test
fails at its 5 s limit. No code changed.
2. **Mv.** Every `launcher.test.mjs` test now has a 30 s timeout. The
longest real run is 3.7 s. Under Mv the file ends in 62 s with 6
failed, 2 cancelled and 1 passed; round 2's run hung.
3. **Claude's `Read`.** It gets the same check (R4 above).
4. **The leftover after Ma.** Not traced this round. It goes to
follow-ups.
5. **Mq** still fails only at the 30 s test timeout. That is a failure,
not a hang, so I left it.
A related leftover, not from this candidate: a `conversation/src/shim.mjs`
from my round 2 gate's conversation suite was still running about an hour
later. It belonged to a removed worktree, under my scratch `TMPDIR`. It
ignored SIGTERM, and I killed it by PID with SIGKILL. The conversation
package is row 40's, so it goes to follow-ups for its owner.
## Round 2
Darkwing's round 1 asked for changes (#1523 comment 26995, rev 247,
@@ -158,7 +285,7 @@ How the new tests separate the mutants:
## Files (`files.txt`, 42)
`build.patch` is `git diff --cached --binary 915e00e5` over those files,
+4679/−63. It applies cleanly to `8dd5ff00` with `git apply --index`, and
+4873/−63. It applies cleanly to `66b9e082` with `git apply --index`, and
`sha256sum -c candidate-manifest.sha256` passes in that tree.
| Area | Files | What |
@@ -273,31 +400,29 @@ project extensions) and is untouched.
## Gate
Run at `8dd5ff00` with this round's patch applied, in a detached worktree,
sequentially, each output in `out/` (`out/summary.txt`). `TMPDIR` on the
scratch disk; `DOCKER_HOST=unix:///nonexistent.sock`, so nothing reaches
Docker. Round 1's outputs stay in this directory at `9b670e27`.
Run at `66b9e082` with this round's patch applied, in a detached worktree.
`sha256sum -c candidate-manifest.sha256` passed there (42 OK). Suites ran
sequentially, each output in `out/` (`out/summary.txt`). `TMPDIR` was on
the scratch disk and `DOCKER_HOST=unix:///nonexistent.sock`, so nothing
reached Docker. Round 1's outputs stay in this directory at `9b670e27`,
and round 2's at `779e9780`.
| Suite | Pass | Fail |
|---|---|---|
| node: bus, business, cli, control-board | 74, 60, 82, 124 | 0 |
| node: conversation, discord, harness, ledger | 152, 178, 50, 78 | 0 |
| node: bus, business, cli, control-board | 74, 60, 83, 124 | 0 |
| node: conversation, discord, harness, ledger | 152, 178, 53, 78 | 0 |
| node: mosaic, queue, seat, tasks, webui | 69, 148, 27, 51, 14 | 0 |
| test-auth, conductor, config, discord | 15, 17, 24, 66 | 0 |
| test-extension-package, foundation, queue, release | 18, 44, 27, 4 | 0 |
| test-task | 26 | 2 |
Against round 1, cli gains 5 tests and harness 5.
Against round 2, cli gains 1 test (Mr) and harness 3 (R4).
The two `test-task` failures are "user recall run succeeds (exit 1)" and
"recalled user name". That check runs a live worker and needs Docker. The
unpatched base fails the same two (`out/base-test-task.txt`, identical
PASS/FAIL lines), so they are the environment, not this candidate.
One edit came after the gate started: the R2 sentence in
`packages/cli/README.md` ("drops every open `launch.sock` connection …").
No suite reads that file, and it is the only file where the manifest and
the gated tree differ.
unpatched base at `66b9e082` fails the same two (`out/base-test-task.txt`,
identical PASS/FAIL lines), so they come from the environment, not this
candidate. No test process was left running after the gate.
## Acceptance run: waiting
@@ -316,3 +441,8 @@ names one, and fixtures use 127.0.0.1 and example.test.
`test-queue` F1 matches it.
- `scripts/test-task.sh` (:514-518): the live user-recall check needs
Docker and isn't skipped when Docker is unavailable.
- Darkwing's round 2 note 4: after the Ma mutant's harness run, a
`fake-adapter.mjs` was left running. Which test spawned it, and whether
that test cleans up when it fails, isn't traced.
- `packages/conversation` (row 40): a `shim.mjs` from a gate's
conversation suite outlived the run, and it ignored SIGTERM.
+214 -20
View File
@@ -1602,10 +1602,10 @@ index 00000000..0acc8424
+await launcher.listen();
+process.stdout.write(`${JSON.stringify(await launcher.launchPm())}\n`);
diff --git a/packages/cli/tests/host.test.mjs b/packages/cli/tests/host.test.mjs
index f8f11ec9..f8914479 100644
index f8f11ec9..50db2473 100644
--- a/packages/cli/tests/host.test.mjs
+++ b/packages/cli/tests/host.test.mjs
@@ -203,6 +203,65 @@ test("a second host for the same data root refuses with exit 3 while the first r
@@ -203,6 +203,88 @@ test("a second host for the same data root refuses with exit 3 while the first r
assert.equal(await host.close(0), 0);
});
@@ -1667,13 +1667,36 @@ index f8f11ec9..f8914479 100644
+ assert.ok(logs.includes("broker channel broken (reply for another request); stopping the host"), `${what}: ${logs.join("\n")}`);
+ }
+});
+
+test("a broker reply with no request waiting breaks the channel and the host exits 1", { timeout: 30000 }, async (t) => {
+ // A request sent to the real broker past the host's queue: its reply
+ // arrives when nothing is waiting (Darkwing round 2, Mr).
+ const children = [];
+ const onChild = ({ process: child }) => children.push(child);
+ subscribe("child_process", onChild);
+ t.after(() => unsubscribe("child_process", onChild));
+ const root = tmp(t);
+ const f = fixture(root);
+ makeDeployment(root);
+ const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
+ const logs = [];
+ const host = await startHost({ boot, business: "acme", log: (l) => logs.push(l) });
+ t.after(() => host.close(0));
+ const record = { business: "acme", role: "coder", run: "coder-run", harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) };
+ const coder = await host.bindLaunch(record);
+ children.find((c) => c.pid === host.pids.broker).send({ op: "identity", cap: coder.cap, id: 1_000_000 });
+ const late = new Promise((r) => setTimeout(r, 5000, "still running").unref());
+ assert.equal(await Promise.race([host.done, late]), 1);
+ assert.ok(logs.includes("broker channel broken (reply with no request waiting); stopping the host"), logs.join("\n"));
+ await assert.rejects(host.op({ op: "identity", cap: coder.cap }), (e) => e.code === "broker-channel-broken");
+});
+
test("a notifier that refuses stops the broker and the host refuses with exit 3", async (t) => {
const root = tmp(t);
const f = fixture(root);
diff --git a/packages/cli/tests/launcher.test.mjs b/packages/cli/tests/launcher.test.mjs
new file mode 100644
index 00000000..75ee91b8
index 00000000..c9b589e3
--- /dev/null
+++ b/packages/cli/tests/launcher.test.mjs
@@ -0,0 +1,394 @@
@@ -1782,7 +1805,7 @@ index 00000000..75ee91b8
+
+const log = (f) => readFileSync(launchLogFile(f.dataRoot, "acme"), "utf8").trim().split("\n").map((l) => JSON.parse(l));
+
+test("bus start --pm: the PM runs under its own PID namespace, registered, with a manifest", { skip }, async (t) => {
+test("bus start --pm: the PM runs under its own PID namespace, registered, with a manifest", { skip, timeout: 30000 }, async (t) => {
+ const { f, launcher, host, cto, close } = await setup(t);
+ const sock = launchSocketPath(f.dataRoot);
+ assert.equal(statSync(sock).mode & 0o777, 0o600);
@@ -1852,7 +1875,7 @@ index 00000000..75ee91b8
+ assert.equal(end.exitCode, 0);
+});
+
+test("the PM launches a coder through its launch tool; the coder answers; refusals name their code", { skip }, async (t) => {
+test("the PM launches a coder through its launch tool; the coder answers; refusals name their code", { skip, timeout: 30000 }, async (t) => {
+ const { f, launcher, client, cto } = await setup(t);
+ const sock = launchSocketPath(f.dataRoot);
+ const pm = await launcher.launchPm();
@@ -1913,7 +1936,7 @@ index 00000000..75ee91b8
+ assert.ok(readSessions(f.dataRoot).some((s) => s.run === pm.run));
+});
+
+test("a runner that stops at once ends its launch with the runner's reason", { skip }, async (t) => {
+test("a runner that stops at once ends its launch with the runner's reason", { skip, timeout: 30000 }, async (t) => {
+ // pm's tracker token has expired: the runner's founder-credential stop
+ // exits 20 before role.claim, and the host records founder-credentials.
+ const { f, launcher } = await setup(t, (doc) => {
@@ -1929,7 +1952,7 @@ index 00000000..75ee91b8
+});
+
+for (const exited of [false, true]) {
+ test(`a host that died hard leaves its sessions to the next host, which ${exited ? "finds one already exited (23)" : "kills them"} and ends their runs so the role can be launched again`, { skip }, async (t) => {
+ test(`a host that died hard leaves its sessions to the next host, which ${exited ? "finds one already exited (23)" : "kills them"} and ends their runs so the role can be launched again`, { skip, timeout: 30000 }, async (t) => {
+ const given = prepare(t);
+ const { f } = given;
+ const old = spawn(process.execPath, [HOST, given.adapter], { env: f.env, stdio: ["ignore", "pipe", "inherit"] });
@@ -1990,7 +2013,7 @@ index 00000000..75ee91b8
+ });
+}
+
+test("a malformed sessions.json refuses the host start with exit 3 and stays as it was", async (t) => {
+test("a malformed sessions.json refuses the host start with exit 3 and stays as it was", { timeout: 30000 }, async (t) => {
+ const given = prepare(t);
+ const file = sessionsFile(given.f.dataRoot);
+ mkdirSync(join(given.f.dataRoot, "bus-host"), { mode: 0o700 });
@@ -2002,7 +2025,7 @@ index 00000000..75ee91b8
+
+const within = (p, ms, what) => Promise.race([p, new Promise((_, reject) => setTimeout(() => reject(new Error(`${what} took over ${ms} ms`)), ms).unref())]);
+
+test("an over-long launch request is refused at once, not at the 10 s idle timeout", async (t) => {
+test("an over-long launch request is refused at once, not at the 10 s idle timeout", { timeout: 30000 }, async (t) => {
+ const { f } = await setup(t);
+ const s = connect(launchSocketPath(f.dataRoot));
+ t.after(() => s.destroy());
@@ -2211,10 +2234,10 @@ index 00000000..41432793
+});
diff --git a/packages/harness/README.md b/packages/harness/README.md
new file mode 100644
index 00000000..79ff829a
index 00000000..aa705b77
--- /dev/null
+++ b/packages/harness/README.md
@@ -0,0 +1,167 @@
@@ -0,0 +1,193 @@
+# harness
+
+What a managed role session runs from and runs as (slice 1 S6, issue #1523):
@@ -2300,6 +2323,27 @@ index 00000000..79ff829a
+directories first. Pi calls it from the extension, Claude Code from
+`claude-gate.mjs`.
+
+Pi's `read` doesn't always open the name it is given. When that name
+doesn't exist, it tries other spellings of the whole resolved path: a
+narrow no-break space (U+202F) before ` AM.` or ` PM.`, the NFD form, a
+curly apostrophe (U+2019) for `'`, and NFD with the curly apostrophe. So
+for `read`, the gate also checks every one of those spellings that exists
+as a name, built from both the workspace as given and its real path (Pi's
+working directory). If any of them resolves outside the workspace or goes
+through a dangling symlink, the read is refused, whichever one Pi would
+pick. A spelling that doesn't exist is ignored. Claude Code's `Read` gets
+the same check, though in Darkwing's round 2 probe Claude Code's own
+symlink check already stopped the one variant it tries (AM/PM). `write`,
+`edit`, `grep`, `find` and `ls` take the name as given in Pi 0.85.1, so
+they get no such check.
+
+The gate copies Pi 0.85.1 code, so recheck these files on a Pi upgrade:
+`dist/utils/paths.js` (`normalizePath`), `dist/core/tools/path-utils.js`
+(`resolveToCwd`, `resolveReadPathAsync`) and which tools in
+`dist/core/tools/` call `resolveReadPath*`. The `pi` binary runs the
+bundled copy under `dist/bundle/`; in 0.85.1 it holds the same code, and
+only `read` and the CLI's own file arguments call `resolveReadPath*`.
+
+## The runner
+
+`node runner.mjs <run dir>`, with `{"cap": "..."}` and a newline on stdin.
@@ -2373,6 +2417,11 @@ index 00000000..79ff829a
+ manifest but not applied to either harness.
+- **`--restricted`** (Claude Code) is not one of the S0 lines. It is what
+ keeps `CLAUDE.md` files and auto-memory out of the prompt (above).
+- **Other spellings.** A read is refused when a spelling Pi might open
+ points outside the workspace, even if the name as given exists and is
+ inside, and Pi would open that one. The same holds when a directory
+ next to the workspace has another spelling of its path, for example
+ `jo’s/ws` beside a workspace in `jo's/ws`.
+- **The gate checks a path when the call is made.** A link created or
+ changed between the check and the tool's own open (by `bash`, or by
+ another process of the same user) isn't seen. That is the same reach as
@@ -2597,10 +2646,10 @@ index 00000000..ecada305
+}
diff --git a/packages/harness/src/gate.mjs b/packages/harness/src/gate.mjs
new file mode 100644
index 00000000..47cc029e
index 00000000..eaddd5dd
--- /dev/null
+++ b/packages/harness/src/gate.mjs
@@ -0,0 +1,102 @@
@@ -0,0 +1,158 @@
+// The tool gate both harnesses share. decide(policy, tool, input) answers
+// one tool call: the policy's built-in tools and its typed tools pass,
+// everything else is blocked, and every path argument of a file tool must
@@ -2665,13 +2714,60 @@ index 00000000..47cc029e
+ }
+}
+
+export function insideWorkspace(workspace, p) {
+function within(workspace, path) {
+ const root = realpathSync(workspace);
+ const s = normalise(p);
+ const target = real(isAbsolute(s) ? resolve(s) : resolve(workspace, s));
+ const target = real(path);
+ return target === root || target.startsWith(root + sep);
+}
+
+export function insideWorkspace(workspace, p) {
+ const s = normalise(p);
+ return within(workspace, isAbsolute(s) ? resolve(s) : resolve(workspace, s));
+}
+
+// Pi's read (dist/core/tools/path-utils.js resolveReadPathAsync) opens
+// another spelling when the resolved path doesn't exist: U+202F before
+// AM/PM, then NFD, then U+2019 for ', then both. Each applies to the whole
+// resolved path, directory names included, and Pi resolves against its cwd,
+// the workspace's real path. So every spelling that exists as a name is
+// checked, not only the one Pi would pick, and the check doesn't depend on
+// which of them exists when Pi opens it. Claude Code's Read gets the same
+// check; its own retries aren't documented.
+function spellings(workspace, p) {
+ const s = normalise(p);
+ const bases = isAbsolute(s) ? [resolve(s)] : [resolve(workspace, s), resolve(realpathSync(workspace), s)];
+ const curly = (v) => v.replace(/'/g, "\u2019");
+ const out = new Set();
+ for (const r of bases) {
+ const nfd = r.normalize("NFD");
+ for (const v of [r.replace(/ (AM|PM)\./gi, "\u202F$1."), nfd, curly(r), curly(nfd)]) if (v !== r) out.add(v);
+ }
+ return out;
+}
+
+// The first spelling that exists and resolves outside, or through a
+// dangling link, as a refusal reason; null if there is none.
+function otherSpelling(workspace, tool, p) {
+ for (const v of spellings(workspace, p)) {
+ let found;
+ try {
+ found = lstatSync(v, { throwIfNoEntry: false });
+ } catch (error) {
+ // A file used as a directory: Pi's access() fails too.
+ if (error.code === "ENOTDIR") continue;
+ return `${tool} path can't be checked under another spelling: ${error.code ?? error.message}`;
+ }
+ if (!found) continue;
+ try {
+ if (!within(workspace, v)) return `${tool} path is outside the workspace under another spelling: ${p} -> ${JSON.stringify(v)}`;
+ } catch (error) {
+ if (error.code === "dangling-symlink") return `${tool} path goes through a dangling symlink under another spelling: ${p} -> ${JSON.stringify(v)}`;
+ return `${tool} path can't be checked under another spelling: ${error.code ?? error.message}`;
+ }
+ }
+ return null;
+}
+
+// decide(policy, tool, input) -> { allow: true } | { allow: false, reason }
+// policy { harness: "pi" | "claude-code", workspace, tools: [pi names], typed: [typed tool names] }
+export function decide(policy, tool, input) {
@@ -2701,6 +2797,15 @@ index 00000000..47cc029e
+ if (error.code === "dangling-symlink") return no(`${tool} path goes through a dangling symlink: ${value}`);
+ return no(`${tool} path can't be checked: ${error.code ?? error.message}`);
+ }
+ if (tool === (claude ? "Read" : "read")) {
+ let other;
+ try {
+ other = otherSpelling(policy.workspace, tool, value);
+ } catch (error) {
+ return no(`${tool} path can't be checked: ${error.code ?? error.message}`);
+ }
+ if (other) return no(other);
+ }
+ return { allow: true };
+}
diff --git a/packages/harness/src/mcp-server.mjs b/packages/harness/src/mcp-server.mjs
@@ -3808,10 +3913,10 @@ index 00000000..4024172d
+}
diff --git a/packages/harness/tests/gate.test.mjs b/packages/harness/tests/gate.test.mjs
new file mode 100644
index 00000000..c32394c2
index 00000000..116364a0
--- /dev/null
+++ b/packages/harness/tests/gate.test.mjs
@@ -0,0 +1,130 @@
@@ -0,0 +1,186 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { mkdirSync, symlinkSync, writeFileSync } from "node:fs";
@@ -3916,6 +4021,62 @@ index 00000000..c32394c2
+ }
+});
+
+// Pi's read opens another spelling when the name it's given doesn't exist
+// (path-utils.js resolveReadPathAsync). Asked name -> name on disk.
+const SPELLINGS = [
+ ["quote", "notes's.txt", "notes\u2019s.txt"],
+ ["ampm", "shot 9.41 AM.png", "shot 9.41\u202FAM.png"],
+ ["nfd", "r\u00E9sum\u00E9.txt", "r\u00E9sum\u00E9.txt".normalize("NFD")],
+ ["nfd and quote", "d'\u00E9cran.png", "d\u2019\u00E9cran.png".normalize("NFD")],
+ // Each family alone, on a name with both an apostrophe and an accent.
+ ["nfd, straight quote", "l'\u00E9t\u00E9.txt", "l'\u00E9t\u00E9.txt".normalize("NFD")],
+ ["quote, composed", "l'\u00E9t\u00E9.md", "l\u2019\u00E9t\u00E9.md"],
+];
+
+test("read is checked under every spelling pi's read would open, in both harnesses", (t) => {
+ for (const [harness, read, write, field] of [["pi", "read", "write", "path"], ["claude-code", "Read", "Write", "file_path"]]) {
+ for (const [, asked, disk] of SPELLINGS) {
+ const { dir, workspace, policy } = setup(t, harness, ["read", "write"]);
+ mkdirSync(join(dir, "outside"));
+ writeFileSync(join(dir, "outside", "secret.txt"), "s");
+ symlinkSync(join(dir, "outside", "secret.txt"), join(workspace, disk));
+ const why = new RegExp(`outside the workspace under another spelling: (.*/)?${asked.replace(/[.']/g, "\\$&")}`);
+ blocked(decide(policy, read, { [field]: asked }), why);
+ blocked(decide(policy, read, { [field]: join(workspace, asked) }), why);
+ // Write takes the name as given, so writing the asked name stays inside.
+ allowed(decide(policy, write, { [field]: asked }));
+ // The same spelling pointing inside is fine.
+ const inner = setup(t, harness, ["read"]);
+ symlinkSync(join(inner.workspace, "a.txt"), join(inner.workspace, disk));
+ allowed(decide(inner.policy, read, { [field]: asked }));
+ }
+ }
+});
+
+test("other spellings cover directories, dangling links and pi's cwd", (t) => {
+ const { dir, workspace, policy } = setup(t, "pi", ["read"]);
+ mkdirSync(join(dir, "outside"));
+ writeFileSync(join(dir, "outside", "s.txt"), "s");
+ // A directory name is respelled too.
+ symlinkSync(join(dir, "outside"), join(workspace, "it\u2019s"));
+ blocked(decide(policy, "read", { path: "it's/s.txt" }), /outside the workspace under another spelling/);
+ // A dangling link under another spelling is refused, like the name itself.
+ symlinkSync(join(dir, "nowhere"), join(workspace, "gone\u2019s"));
+ blocked(decide(policy, "read", { path: "gone's" }), /dangling symlink under another spelling/);
+ // A spelling that doesn't exist, or uses a file as a directory, isn't opened.
+ allowed(decide(policy, "read", { path: "nobody's.txt" }));
+ writeFileSync(join(workspace, "f\u2019s"), "f");
+ allowed(decide(policy, "read", { path: "f's/x" }));
+ // Pi resolves against its cwd, the workspace's real path, and respells
+ // that too: here the real path has an apostrophe the given path lacks.
+ mkdirSync(join(dir, "jo's", "ws"), { recursive: true });
+ mkdirSync(join(dir, "jo\u2019s", "ws"), { recursive: true });
+ writeFileSync(join(dir, "jo\u2019s", "ws", "x.txt"), "x");
+ symlinkSync(join(dir, "jo's"), join(dir, "jo"));
+ const viaLink = { ...policy, workspace: join(dir, "jo", "ws") };
+ blocked(decide(viaLink, "read", { path: "x.txt" }), /outside the workspace under another spelling/);
+});
+
+test("claude path fields per tool", (t) => {
+ const { workspace, policy } = setup(t, "claude-code", ["read", "write", "edit", "grep", "find"]);
+ allowed(decide(policy, "Read", { file_path: join(workspace, "a.txt") }));
@@ -4151,10 +4312,10 @@ index 00000000..72992dde
+});
diff --git a/packages/harness/tests/pi-session.test.mjs b/packages/harness/tests/pi-session.test.mjs
new file mode 100644
index 00000000..0486857f
index 00000000..d2a89aef
--- /dev/null
+++ b/packages/harness/tests/pi-session.test.mjs
@@ -0,0 +1,139 @@
@@ -0,0 +1,172 @@
+// Real pi (the pinned CLI) through adapters/pi with the extension, against
+// the scripted Messages API. No real model, no network beyond 127.0.0.1.
+
@@ -4277,6 +4438,39 @@ index 00000000..0486857f
+ assert.ok(existsSync(s.turnMarker));
+});
+
+test("pi: a read is refused when pi would open another spelling outside", async (t) => {
+ // Asked name -> link on disk; pi's read opens the link when the asked
+ // name doesn't exist (path-utils.js resolveReadPathAsync).
+ const spellings = [
+ ["notes's.txt", "notes\u2019s.txt"],
+ ["shot 9.41 AM.png", "shot 9.41\u202FAM.png"],
+ ["r\u00E9sum\u00E9.txt", "r\u00E9sum\u00E9.txt".normalize("NFD")],
+ ["d'\u00E9cran.png", "d\u2019\u00E9cran.png".normalize("NFD")],
+ ["l'\u00E9t\u00E9.txt", "l'\u00E9t\u00E9.txt".normalize("NFD")],
+ ["l'\u00E9t\u00E9.md", "l\u2019\u00E9t\u00E9.md"],
+ ];
+ const { dir, workspace, s, env } = await session(t, [
+ ...spellings.map(([asked]) => ({ name: "read", input: { path: asked } })),
+ { name: "read", input: { path: "inside's.txt" } },
+ ]);
+ mkdirSync(join(dir, "outside"));
+ writeFileSync(join(dir, "outside", "secret.txt"), "SECRET-OUTSIDE\n");
+ for (const [, disk] of spellings) symlinkSync(join(dir, "outside", "secret.txt"), join(workspace, disk));
+ // The same respelling pointing inside is still read.
+ symlinkSync(join(workspace, "notes.txt"), join(workspace, "inside\u2019s.txt"));
+ const r = await turn(env, "Message 1 from jason, class REQUEST:\n\nread the files", workspace);
+ assert.equal(r.code, 0, r.stderr);
+ assert.doesNotMatch(r.stdout, /SECRET/);
+ const results = JSON.parse(r.stdout.trim().slice("ANSWER ".length));
+ assert.equal(results.length, spellings.length + 1);
+ spellings.forEach(([asked], i) => {
+ assert.equal(results[i][0], true, asked);
+ assert.match(results[i][1], /outside the workspace under another spelling/, asked);
+ });
+ assert.deepEqual(results.at(-1), [false, "inside\n"]);
+ assert.ok(existsSync(s.turnMarker));
+});
+
+test("pi: a missing extension refuses before any model call", async (t) => {
+ const { dir, api, s, env } = await session(t, []);
+ const r = await turn({ ...env, MOSAIC_EXTENSIONS: join(dir, "missing.mjs") }, "x", s.workspace);
@@ -12,14 +12,14 @@ bd207b81a2b9965b9e46da66ab31ed9a587b87e8669e8293184d4b842e45bff5 packages/cli/s
e9eeec4bef3384b5b15d1e7a2c9d913d2f3e329228c3e3e2cacb8f741aa21379 packages/cli/src/host.mjs
2d0b075982f295a88161607d2795aadc86f286994ab6cc31873b83b2daebf7fa packages/cli/src/launcher.mjs
9b30a3bfe96a5d7c6956b6c75196c08bc35ceec302859337915ca1cfe2a76b76 packages/cli/tests/fixtures/launch-host.mjs
436c15af25c48a8135d6b4bbb7df26d318b87da32fc6432455f23043a07e791c packages/cli/tests/host.test.mjs
670e3a8dd3e57b96b96f6932693853e1149e53da577299931cbe3d428ddb89da packages/cli/tests/launcher.test.mjs
0a2a452fdb3a4ea68478e54b3ca6694c51d074fa29a0ad8ea3740eb1b44ab780 packages/cli/tests/host.test.mjs
1dbdb8166e8c47290bb4499b330ea32bec9a6f07179eef816edaa35603b1c408 packages/cli/tests/launcher.test.mjs
709bd331bb0d76f28b464e518f4e1fc3bb0b303614e79d7e82479dcd679043cc packages/cli/tests/verbs.test.mjs
07f8033da769b18c194520356f900dd10a12d146d4d10f1a80e3c40a2a60655e packages/harness/README.md
570817222c5cc2f195963569e295db11f09f5dd56f19e3c9bdc220d5ee54435f packages/harness/README.md
34ef8212e27f052223443c66830839517f7a54ecb6ef7d85795b3e89c65b4d4c packages/harness/package.json
22efd0bed7991561920ad29f6bf9a1ab2d7384185fab4267684a037e026d8e85 packages/harness/src/bundle.mjs
32b5090760c95eea0e1232ff36ec13a1d9b58335b2eade621196dec1ebdbe784 packages/harness/src/claude-gate.mjs
570a1e64db8624b57eb972fd7599c7543e3c41324d0e9666cb5701d5491ec39b packages/harness/src/gate.mjs
be416a0c70cd84c2c53e3259296077c28c5d3006e0a151efc950b2ce73354604 packages/harness/src/gate.mjs
71e00113b8e5b55b410c7aae6232f76e972fc77aa68afa893da45c6b78d297e2 packages/harness/src/mcp-server.mjs
d19753fa72f0b57e751749359644093713bcb650bfac566f55d2bc05cb817121 packages/harness/src/pi-extension.mjs
1047aa092080e92efde2044dddaf82bacf428ed4b143c3846693471481f3612e packages/harness/src/runner.mjs
@@ -28,10 +28,10 @@ d19753fa72f0b57e751749359644093713bcb650bfac566f55d2bc05cb817121 packages/harne
96524da43b212e84d78108cbbf05eef324c934f3ad9e0d4cb2edcb0f3df256d4 packages/harness/tests/claude-gate.test.mjs
197ec0f6c842552bea65fb2ab4c8cb8615fd6e691a2d0a592e1465d18a45d75f packages/harness/tests/claude-session.test.mjs
8392172b243356932c974bdca9b4c449a312ae7a042ee7a22e0f22fbb98dbec9 packages/harness/tests/fixtures/fake-adapter.mjs
8576749388b3f962ed6cbd694d9af814665f3f491f278a3cdf9b9c5435388460 packages/harness/tests/gate.test.mjs
38111dc604d32486941f9422654cc392fa2c7767e7f02d13ebbb0038b71669ce packages/harness/tests/gate.test.mjs
6e7509cfe6ad909440c25b750528360c0ba617c6c68b05d400bcd94b481c1f76 packages/harness/tests/helpers.mjs
793eb11f970e4a8eecddec4dc48c24331e4de795bc04bfadf806a2fc3a15a8b4 packages/harness/tests/mcp-server.test.mjs
ba907e3bee4547c97c8700b4b19febc46c19074a259dd113b727522d68115273 packages/harness/tests/pi-session.test.mjs
c4798bb5681172e707d41446388e0490ca8a5398e801766988c8daba2646beaf packages/harness/tests/pi-session.test.mjs
c8f23144316501c3e163cf5a5566ddee552b3c93dee13594ae8c1eac66aedc64 packages/harness/tests/runner.test.mjs
ef9130a3cb1ca3e278a8ed370060afd1145d1ceb211a915e83d75c8346b04931 packages/harness/tests/tools.test.mjs
4e34456187387b02fa6d996c270dea4076b5d57952cddaa01f7a04843b351a02 packages/seat/README.md
+75 -75
View File
@@ -1,77 +1,77 @@
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (158.294055ms)
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (259.614212ms)
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (242.506362ms)
✔ launch events require a human CLI capability; generic emit cannot forge authority events (161.976041ms)
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (148.695884ms)
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (137.583297ms)
✔ task action subjects and linked decision trail are complete and ordered (186.436959ms)
✔ launch binding is durable and reconnecting requires the identical trusted record (104.399977ms)
✔ business isolation includes inherited object names and cross-business message references (188.02737ms)
✔ authority never transfers between action, run, target, unresolved or replaced role holder (350.15037ms)
✔ task projection uses schema current view, skipping earlier and equal-start polls (159.818248ms)
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (189.67006ms)
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (124.230049ms)
✔ both arbiters require human resolution when their cross-role route is themselves (199.180591ms)
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.264919ms)
✔ only validated broker references load; returned data and exceptions cannot expose a known token (5.481319ms)
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (4.18746ms)
✔ expiry refuses use and env references never become client data (1.105304ms)
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (7.468986ms)
✔ opaque tokens shorter than 16 characters refuse before use (0.513997ms)
✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (135.481002ms)
✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (134.248144ms)
✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (153.895653ms)
✔ endLaunch leaves a claim another run took alone (161.970249ms)
✔ refuse records action.refused against the caller with the code only (111.340973ms)
✔ launches off refuses role.launch with launch-revoked until launches on (160.281981ms)
✔ broker process: launch ops authorize role.launch, record refusals and end runs (221.362748ms)
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (3.096307ms)
✔ process reader gets own kernel identity without exposing environment values (2.657909ms)
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.273874ms)
✔ real pid 1 remains inspectable when its environment is protected (0.505849ms)
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (188.712257ms)
✔ missing and foreign-business citations refuse and roll back message and grant (177.133146ms)
✔ cross-role sends still need a matching resolved decision and consume it once (230.758224ms)
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (193.74033ms)
✔ startup token refusal returns safe code without value or partial listening broker (39.198239ms)
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (184.106087ms)
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (157.880698ms)
✔ trusted host registers later launches; socket clients never have a registration verb (173.751442ms)
✔ runtime excludes declared project roots even when host supplies no repoRoots (40.167143ms)
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (154.175551ms)
✔ v3b prototype refusals, views and append-only mutations (974.316595ms)
✔ gated approval authorizes once, survives store reopen, and fresh approval works (249.273742ms)
✔ another run cannot consume an approval; a failed check leaves it usable (219.736933ms)
✔ two scheduled callers have exactly one grant and one consumed refusal (156.511323ms)
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (273.588058ms)
✔ class drift gated to cross-role refuses before consumption (196.834803ms)
✔ class drift cross-role to gated refuses before consumption (220.857243ms)
✔ class drift gated to within-role refuses before consumption (200.437464ms)
✔ class drift cross-role to within-role refuses before consumption (298.862655ms)
✔ class drift within-role to gated refuses before consumption (232.176051ms)
✔ class drift within-role to cross-role refuses before consumption (171.660401ms)
✔ message.send consumes approval and prevents a later send or authorize (191.809221ms)
✔ role.revoke consumes approval and prevents a later revoke or authorize (196.68699ms)
✔ creates private WAL store and excludes a second writer until explicit close (116.781089ms)
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (172.437705ms)
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (169.581936ms)
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (158.747085ms)
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (99.516826ms)
✔ async transactions refuse before invoking their function (86.659243ms)
✔ recordTask keeps sync reads and a role write apart (149.917777ms)
✔ read_at must be one canonical UTC format, so the projection compares strings safely (117.042328ms)
✔ a bad entry refuses the whole record (99.401189ms)
✔ taskView reads the projection for one business (121.318568ms)
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (221.737809ms)
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (392.357601ms)
✔ without an adapter the server refuses every task verb (200.843487ms)
✔ the runtime refuses an invalid adapter and closes a valid one (213.365501ms)
✔ the process loads the S3 adapter from plain-data trackers (327.129025ms)
✔ socket capability stamps launch identity; shared views use wire, no SQL client (140.998311ms)
✔ two wire claims serialize; a lost reply never automatically retries (179.486351ms)
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (118.361562ms)
✔ client preserves UTF-8 when a response divides a multibyte character (12.083191ms)
✔ committed mutation followed by dropped reply reports unknown and is never retried (127.825313ms)
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (239.740458ms)
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (434.495891ms)
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (318.977003ms)
✔ launch events require a human CLI capability; generic emit cannot forge authority events (182.276491ms)
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (193.552882ms)
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (170.799289ms)
✔ task action subjects and linked decision trail are complete and ordered (203.941682ms)
✔ launch binding is durable and reconnecting requires the identical trusted record (95.56779ms)
✔ business isolation includes inherited object names and cross-business message references (163.750481ms)
✔ authority never transfers between action, run, target, unresolved or replaced role holder (320.028764ms)
✔ task projection uses schema current view, skipping earlier and equal-start polls (133.747714ms)
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (191.711174ms)
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (134.762457ms)
✔ both arbiters require human resolution when their cross-role route is themselves (255.780592ms)
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.307073ms)
✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.946341ms)
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (6.01105ms)
✔ expiry refuses use and env references never become client data (0.903117ms)
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.796059ms)
✔ opaque tokens shorter than 16 characters refuse before use (0.398635ms)
✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (206.587941ms)
✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (134.400371ms)
✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (323.818329ms)
✔ endLaunch leaves a claim another run took alone (183.703048ms)
✔ refuse records action.refused against the caller with the code only (186.044736ms)
✔ launches off refuses role.launch with launch-revoked until launches on (189.472852ms)
✔ broker process: launch ops authorize role.launch, record refusals and end runs (274.249507ms)
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.232835ms)
✔ process reader gets own kernel identity without exposing environment values (1.347663ms)
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.961234ms)
✔ real pid 1 remains inspectable when its environment is protected (0.369189ms)
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (253.116133ms)
✔ missing and foreign-business citations refuse and roll back message and grant (200.662761ms)
✔ cross-role sends still need a matching resolved decision and consume it once (427.569214ms)
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (253.423734ms)
✔ startup token refusal returns safe code without value or partial listening broker (35.293578ms)
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (291.67146ms)
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (192.094911ms)
✔ trusted host registers later launches; socket clients never have a registration verb (239.798505ms)
✔ runtime excludes declared project roots even when host supplies no repoRoots (37.521057ms)
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (167.72013ms)
✔ v3b prototype refusals, views and append-only mutations (1374.206323ms)
✔ gated approval authorizes once, survives store reopen, and fresh approval works (315.775301ms)
✔ another run cannot consume an approval; a failed check leaves it usable (391.815636ms)
✔ two scheduled callers have exactly one grant and one consumed refusal (203.799579ms)
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (362.938983ms)
✔ class drift gated to cross-role refuses before consumption (228.029982ms)
✔ class drift cross-role to gated refuses before consumption (244.576932ms)
✔ class drift gated to within-role refuses before consumption (177.896571ms)
✔ class drift cross-role to within-role refuses before consumption (195.452584ms)
✔ class drift within-role to gated refuses before consumption (243.556487ms)
✔ class drift within-role to cross-role refuses before consumption (179.418758ms)
✔ message.send consumes approval and prevents a later send or authorize (199.875778ms)
✔ role.revoke consumes approval and prevents a later revoke or authorize (269.090073ms)
✔ creates private WAL store and excludes a second writer until explicit close (184.52587ms)
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (177.881186ms)
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (357.948758ms)
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (218.819903ms)
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (101.258819ms)
✔ async transactions refuse before invoking their function (96.216937ms)
✔ recordTask keeps sync reads and a role write apart (262.686766ms)
✔ read_at must be one canonical UTC format, so the projection compares strings safely (116.864183ms)
✔ a bad entry refuses the whole record (273.912315ms)
✔ taskView reads the projection for one business (139.393069ms)
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (298.358617ms)
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (434.226779ms)
✔ without an adapter the server refuses every task verb (214.917219ms)
✔ the runtime refuses an invalid adapter and closes a valid one (193.702117ms)
✔ the process loads the S3 adapter from plain-data trackers (305.830649ms)
✔ socket capability stamps launch identity; shared views use wire, no SQL client (218.013406ms)
✔ two wire claims serialize; a lost reply never automatically retries (188.544206ms)
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (268.564851ms)
✔ client preserves UTF-8 when a response divides a multibyte character (11.877466ms)
✔ committed mutation followed by dropped reply reports unknown and is never retried (149.111642ms)
ℹ tests 74
ℹ suites 0
ℹ pass 74
@@ -79,4 +79,4 @@
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2747.495478
ℹ duration_ms 3108.809343
+61 -61
View File
@@ -1,63 +1,63 @@
✔ config directory and file path follow MOSAIC_CONFIG (1.91162ms)
✔ the fixture business validates and comes back frozen (5.712812ms)
✔ two instances may share a definition (1.68694ms)
✔ top-level refusals (4.925076ms)
✔ arbiters and projects (6.586166ms)
✔ role instances (3.076648ms)
✔ Vikunja bots (6.665056ms)
✔ a role without Vikunja takes no tracker block (2.871312ms)
✔ credential references match the definition's services (2.642532ms)
✔ launch (7.613634ms)
✔ loadBusiness: file checks (2.052319ms)
✔ loadBusiness: not a regular file (47.666198ms)
✔ loading writes nothing (1.520534ms)
✔ names that are Object.prototype properties don't count as declared (2.954491ms)
✔ the shipped example refuses as written and validates once filled in (0.63865ms)
✔ usage errors exit 4 (322.723133ms)
✔ validate: a good business exits 0 and prints instance digests (71.239074ms)
✔ validate: project files (341.689159ms)
✔ validate: missing files and a broken system config (291.858814ms)
✔ validate: credential reference problems exit 2 and name each one (75.189953ms)
✔ validate: a token file inside the repository is refused (74.77167ms)
✔ validate: role definitions come from MOSAIC_ROLES_DIR (222.784057ms)
✔ resolve: prints one instance's record (227.709604ms)
✔ resolve: refusals (443.985563ms)
✔ parse: exactly one of file or env, plus the service's date (2.456548ms)
✔ check: a good file has no problems (0.775399ms)
✔ check never opens the file: a write-only token passes (0.407183ms)
✔ check: file problems (0.843938ms)
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (0.861362ms)
✔ check: dates and environment references (0.465771ms)
✔ path and load (2.010503ms)
✔ refusals (1.064579ms)
✔ systemVars flattens the validated config (2.461642ms)
✔ precedence: system, business, project, project role, agent (8.241305ms)
✔ limits narrow the definition and never widen it (2.836179ms)
✔ role.launch stays within-role only for the instance the launch block names (5.636504ms)
✔ limits.authority without role.launch leaves the launcher with no launch block (2.024129ms)
✔ limits.authority narrows cross-role actions too (1.025935ms)
✔ classify (1.912616ms)
✔ the record carries what the broker and launcher need (0.981644ms)
✔ digest: key order doesn't matter, any value change does (6.433678ms)
✔ refusals (2.701053ms)
✔ the four shipped version 2 roles load (3.348744ms)
✔ shipped role scopes match addendum B section 2 and the SR runbook (1.219132ms)
✔ shipped authority follows the note's table (0.64651ms)
✔ version 1 files keep loading with no authority (1.080549ms)
✔ the conductor policy isn't a role (0.255061ms)
✔ a missing role file is exit 4, a symbolic link too (0.438644ms)
✔ version 2 refusals (1.487183ms)
✔ authority: closed vocabulary, no gated-only action, no overlap (2.444188ms)
✔ credentials: Gitea scopes (1.019814ms)
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (1.419399ms)
✔ credentials: services (0.596546ms)
✔ contract: a non-empty regular Markdown file beside the role file (0.836011ms)
✔ every key names known layers and a merge rule (0.986663ms)
✔ unknown keys and wrong layers refuse (0.879891ms)
✔ types (2.321956ms)
✔ merge: defaults, then the most specific layer wins (0.418729ms)
✔ merge: limits only narrow, and provenance lists each source (0.426782ms)
✔ merge doesn't change its inputs (0.189432ms)
✔ config directory and file path follow MOSAIC_CONFIG (1.94594ms)
✔ the fixture business validates and comes back frozen (5.707571ms)
✔ two instances may share a definition (1.972691ms)
✔ top-level refusals (5.720077ms)
✔ arbiters and projects (7.599597ms)
✔ role instances (3.911216ms)
✔ Vikunja bots (8.160669ms)
✔ a role without Vikunja takes no tracker block (3.078813ms)
✔ credential references match the definition's services (3.806947ms)
✔ launch (10.102968ms)
✔ loadBusiness: file checks (2.022471ms)
✔ loadBusiness: not a regular file (45.202904ms)
✔ loading writes nothing (1.253617ms)
✔ names that are Object.prototype properties don't count as declared (2.657347ms)
✔ the shipped example refuses as written and validates once filled in (0.542513ms)
✔ usage errors exit 4 (285.619026ms)
✔ validate: a good business exits 0 and prints instance digests (61.986118ms)
✔ validate: project files (315.802361ms)
✔ validate: missing files and a broken system config (235.381833ms)
✔ validate: credential reference problems exit 2 and name each one (62.229075ms)
✔ validate: a token file inside the repository is refused (64.115882ms)
✔ validate: role definitions come from MOSAIC_ROLES_DIR (200.324478ms)
✔ resolve: prints one instance's record (193.814279ms)
✔ resolve: refusals (386.379682ms)
✔ parse: exactly one of file or env, plus the service's date (3.254409ms)
✔ check: a good file has no problems (1.798437ms)
✔ check never opens the file: a write-only token passes (0.467731ms)
✔ check: file problems (1.279571ms)
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (1.400708ms)
✔ check: dates and environment references (0.575693ms)
✔ path and load (2.919347ms)
✔ refusals (1.710575ms)
✔ systemVars flattens the validated config (2.516987ms)
✔ precedence: system, business, project, project role, agent (6.390188ms)
✔ limits narrow the definition and never widen it (3.070619ms)
✔ role.launch stays within-role only for the instance the launch block names (6.400121ms)
✔ limits.authority without role.launch leaves the launcher with no launch block (1.722667ms)
✔ limits.authority narrows cross-role actions too (1.035999ms)
✔ classify (1.279788ms)
✔ the record carries what the broker and launcher need (1.144397ms)
✔ digest: key order doesn't matter, any value change does (8.9708ms)
✔ refusals (3.013121ms)
✔ the four shipped version 2 roles load (5.279908ms)
✔ shipped role scopes match addendum B section 2 and the SR runbook (1.490848ms)
✔ shipped authority follows the note's table (0.764831ms)
✔ version 1 files keep loading with no authority (1.274805ms)
✔ the conductor policy isn't a role (0.306126ms)
✔ a missing role file is exit 4, a symbolic link too (0.509379ms)
✔ version 2 refusals (1.439143ms)
✔ authority: closed vocabulary, no gated-only action, no overlap (2.33389ms)
✔ credentials: Gitea scopes (0.923002ms)
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (1.222158ms)
✔ credentials: services (0.632125ms)
✔ contract: a non-empty regular Markdown file beside the role file (1.111439ms)
✔ every key names known layers and a merge rule (1.289197ms)
✔ unknown keys and wrong layers refuse (0.896979ms)
✔ types (1.783348ms)
✔ merge: defaults, then the most specific layer wins (0.30552ms)
✔ merge: limits only narrow, and provenance lists each source (0.393139ms)
✔ merge doesn't change its inputs (0.177042ms)
ℹ tests 60
ℹ suites 0
ℹ pass 60
@@ -65,4 +65,4 @@
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2141.835239
ℹ duration_ms 1887.219031
+86 -85
View File
@@ -1,92 +1,93 @@
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (146.776928ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (136.801344ms)
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (103.086176ms)
✔ decide prints a declining choice as declining (134.723723ms)
✔ an unknown outcome is reported once and never resent (118.430821ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (96.582082ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (93.910913ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (100.191586ms)
✔ every human command refuses inside an agent run before it touches the bus (89.223907ms)
✔ usage errors exit 4; no business and no host is a usage error (83.466371ms)
✔ agents and tasks print through the broker (99.638395ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.56895ms)
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (53.987196ms)
✔ trackers come from the tracker.* variables of the one project that names a tracker project (50.54766ms)
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (52.575457ms)
✔ two projects that each name a tracker project refuse, since the boot shape holds one (41.939509ms)
✔ a business without tracker.baseUrl gets no trackers entry (44.220059ms)
✔ an unknown business and a broken system config refuse with exit 3 (77.545035ms)
✔ empty views say so (1.213263ms)
✔ the trail keeps the broker's order and names a decision's task without its rows (1.465349ms)
✔ tasks print the tracker fields the snapshot carries (0.259502ms)
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (968.120835ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (244.370802ms)
✔ a second host for the same data root refuses with exit 3 while the first runs (195.501934ms)
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1168.798507ms)
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (252.111853ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (192.327382ms)
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (149.865957ms)
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (109.163695ms)
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (152.634134ms)
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (116.928022ms)
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (185.169842ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (26.156512ms)
✔ bus stop refuses to signal a live pid that is not a bus host (203.06222ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (218.54117ms)
✔ bus start refuses with exit 3 without a notifier config (87.417832ms)
✔ bus start runs until bus stop; status reports it while it runs (665.256061ms)
✔ bus-service.sh renders the unit and installs it into a given directory (27.206795ms)
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1344.798577ms)
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (619.298083ms)
✔ a runner that stops at once ends its launch with the runner's reason (199.689244ms)
✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (651.083095ms)
✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3599.486558ms)
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (100.745764ms)
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (115.983641ms)
✔ a launch client that never closes its side doesn't hold the host's close (138.95718ms)
✔ a runner that ignores SIGTERM is killed when the host closes (1228.324814ms)
✔ zoned uses the IANA zone across DST (19.841055ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (152.572961ms)
✔ two blocking decisions get two DMs with different nonces (127.82059ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.211354ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (112.394683ms)
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (138.735105ms)
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (129.023835ms)
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (102.098533ms)
✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (172.839344ms)
✔ a restart after the second refusal does not send before that refusal's 30 min are up (125.552318ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (112.654074ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (75.674926ms)
✔ an inbox read failure is logged and the next poll retries (0.621548ms)
✔ no Discord id reaches the journal or the log (86.296393ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (41.163659ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (55.929482ms)
✔ the journal: a whole file that is one torn line truncates to empty (24.260055ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (3.363193ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.952902ms)
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (3.077859ms)
✔ the journal: a symlinked directory refuses and says it is a link (0.589086ms)
✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.754225ms)
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.464759ms)
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.527358ms)
✔ digest content stays within Discord's 2000 characters (0.352942ms)
✔ runLoop never overlaps ticks and stops after the one in flight (111.100812ms)
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (116.16035ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (132.767677ms)
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (112.711559ms)
✔ decide prints a declining choice as declining (109.36668ms)
✔ an unknown outcome is reported once and never resent (99.216529ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (99.275753ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (79.160142ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (81.978588ms)
✔ every human command refuses inside an agent run before it touches the bus (106.067287ms)
✔ usage errors exit 4; no business and no host is a usage error (102.352812ms)
✔ agents and tasks print through the broker (103.812899ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.186116ms)
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (42.538119ms)
✔ trackers come from the tracker.* variables of the one project that names a tracker project (38.865355ms)
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (40.992134ms)
✔ two projects that each name a tracker project refuse, since the boot shape holds one (37.564192ms)
✔ a business without tracker.baseUrl gets no trackers entry (31.426747ms)
✔ an unknown business and a broken system config refuse with exit 3 (54.512934ms)
✔ empty views say so (0.965635ms)
✔ the trail keeps the broker's order and names a decision's task without its rows (1.169355ms)
✔ tasks print the tracker fields the snapshot carries (0.231141ms)
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (921.823079ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (234.249151ms)
✔ a second host for the same data root refuses with exit 3 while the first runs (145.474385ms)
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1163.334044ms)
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (234.382551ms)
✔ a broker reply with no request waiting breaks the channel and the host exits 1 (143.162698ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (161.984306ms)
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (156.804707ms)
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (101.744364ms)
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (148.28775ms)
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (103.666899ms)
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (163.923058ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (22.293656ms)
✔ bus stop refuses to signal a live pid that is not a bus host (202.92148ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (204.29267ms)
✔ bus start refuses with exit 3 without a notifier config (88.991059ms)
✔ bus start runs until bus stop; status reports it while it runs (656.946975ms)
✔ bus-service.sh renders the unit and installs it into a given directory (26.216533ms)
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (285.286099ms)
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (677.971127ms)
✔ a runner that stops at once ends its launch with the runner's reason (266.797745ms)
✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (698.435904ms)
✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3619.790341ms)
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (127.464512ms)
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (138.102925ms)
✔ a launch client that never closes its side doesn't hold the host's close (123.236149ms)
✔ a runner that ignores SIGTERM is killed when the host closes (1222.473883ms)
✔ zoned uses the IANA zone across DST (15.721138ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (121.040523ms)
✔ two blocking decisions get two DMs with different nonces (132.938253ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.184982ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (103.264969ms)
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (111.979075ms)
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (96.261206ms)
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (109.787479ms)
✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (141.731781ms)
✔ a restart after the second refusal does not send before that refusal's 30 min are up (131.77121ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (135.646325ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (121.708992ms)
✔ an inbox read failure is logged and the next poll retries (0.643351ms)
✔ no Discord id reaches the journal or the log (97.433045ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (13.455834ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (26.085454ms)
✔ the journal: a whole file that is one torn line truncates to empty (22.000323ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.339076ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.593157ms)
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (4.170535ms)
✔ the journal: a symlinked directory refuses and says it is a link (0.308408ms)
✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.463907ms)
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.378159ms)
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.576535ms)
✔ digest content stays within Discord's 2000 characters (0.254664ms)
✔ runLoop never overlaps ticks and stops after the one in flight (110.674806ms)
task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200
task.close on a missing task answered: task-not-found; it made GET /tasks/999 404
✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (450.705988ms)
✔ the transport writes {business, verb, args} to the child and reads its JSON (46.868792ms)
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2232.458816ms)
✔ busExit and refuseInsideAgent (0.506966ms)
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (213.349559ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1113.591923ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (116.819293ms)
✔ launches off and on go to the broker and change the business's launch state (109.402112ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (64.2915ms)
ℹ tests 82
✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (388.552078ms)
✔ the transport writes {business, verb, args} to the child and reads its JSON (33.653582ms)
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2175.341854ms)
✔ busExit and refuseInsideAgent (0.649603ms)
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (222.190237ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1115.846632ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (95.891317ms)
✔ launches off and on go to the broker and change the business's launch state (103.309056ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (64.963523ms)
ℹ tests 83
ℹ suites 0
ℹ pass 82
ℹ pass 83
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 8105.490386
ℹ duration_ms 7229.556012
+125 -125
View File
@@ -1,127 +1,127 @@
✔ explicit request, Seen, ordinary completion and a new request have distinct attention states (3.691168ms)
✔ attention convention ignores reasoning/quoted examples and permits leading blank lines (0.238072ms)
✔ completed smoke replies and ordinary questions are idle, not human blockers (0.714916ms)
✔ only an explicit first-line input request makes a finished reply waiting (0.167776ms)
✔ tool activity, user text, errors and unfinished turns override attention text (0.129623ms)
✔ STOP access failure is unknown, not absence, under a non-root identity (33.013191ms)
✔ connector Task never inherits Discord routing envelopes; ordinary Task still uses user text (3.180599ms)
✔ connector discovery keeps only safe identity; rejects modes, mismatches, links and traversal (1.38538ms)
✔ canonical owner identity and STOP are independent; no tmux fallback or forged registration (6.11372ms)
✔ connector reply refusal precedes forged live tmux registration; ordinary agent still sends (0.622748ms)
✔ server rescans connector discovery and refuses HTTP reply without transport (33.252474ms)
✔ connector session links and linked directories are not read (0.975739ms)
✔ newer live matching launch marks old activity, preserves history/attention/attribution, then clears on new activity (2.6993ms)
✔ CLI print uses the relaunch notice instead of old current preview (62.64595ms)
✔ connector owner and fixed task never inherit a native relaunch notice (2.453627ms)
✔ equality, stale/unknown/offline, mismatched registration and unknown activity do not assert relaunch (1.69238ms)
✔ loadConfig: missing file throws ConfigError (1.813512ms)
✔ loadConfig: invalid JSON throws ConfigError (0.320534ms)
✔ loadConfig: missing dataRoot throws ConfigError (0.241769ms)
✔ loadConfig: relative dataRoot throws ConfigError (0.243223ms)
✔ loadConfig: valid config returns dataRoot (0.694324ms)
✔ findNewestSession: picks the newest by mtime among two files (0.606778ms)
✔ findNewestSession: finds files in nested subdirectories (0.449554ms)
✔ findNewestSession: returns null for a missing dir (0.190029ms)
✔ readSession: extracts fields, collapses/truncates text, counts a truncated final line (0.953037ms)
✔ readSession: model and provider follow the latest model_change entry or assistant turn; null when the log names neither; scanAgent carries them (1.571058ms)
✔ readSession: lastError carries the assistant errorMessage only when the last assistant turn errored (0.503874ms)
✔ findNewestSession/scan: never read sibling auth or secrets next to a sessions dir (1.243797ms)
✔ deriveState: full state table (0.285701ms)
✔ rule: newest entry is an assistant message with a tool call, after a question-looking text, is working (0.375156ms)
✔ rule: newest entry is a tool result with no assistant text after it is working (0.333717ms)
✔ rule: a finished ordinary turn is idle, even if it says your move (0.278815ms)
✔ task: the first user message of the session, from text blocks (0.299581ms)
✔ task: a plain-string user content is accepted, whitespace collapsed and long text capped (0.272129ms)
✔ task: no user message in the log means null (shown as unknown), never a guess (0.302093ms)
✔ workspace: the live tmux pane path wins; the session cwd is the fallback; neither means null (0.433808ms)
✔ activeProject: basename of the nearest .git directory or .git file above the workspace; none means null (0.574491ms)
✔ scan: the written record carries task, workspace and activeProject (0.567433ms)
✔ registration: overrides task, project and workspace; every source says registration; registered carries the launch fields; the grouping column is untouched (0.655101ms)
✔ registration: empty task and null project/workspace leave the derived values in place; registered is still non-null (0.43647ms)
✔ registration: a record whose pid is gone is stale; derived values win, sources say derived, registered stays with alive false; a pid the probe cannot decide is not stale; pidAlive itself (1.087099ms)
✔ registration: no registration leaves the Gate A fields exactly as before, and registered is null (0.468892ms)
✔ loadRegistrations: a missing seatsDir gives empty lists (0.171266ms)
✔ loadRegistrations: one good record, one malformed JSON, one with an unknown field; a stray file under seatsDir is ignored (0.848083ms)
✔ matchRegistration: matches by sessionsDir, and by realpath through a symlink; sessionsDir null never matches; same seat name with a different sessionsDir does not match (fleet vs repo darkwing) (0.378216ms)
✔ scan: writes the registration override to disk; index.json carries registered and registrationErrors (1.248013ms)
✔ scan: a relative seatsDir throws ConfigError; an omitted seatsDir behaves as before (0.252124ms)
✔ scanAgent: waitingOnYou is true for waiting/error and false otherwise (0.603413ms)
✔ scanAgent: ageSeconds is computed from the injected now (0.2699ms)
✔ scanAgent: sessionFile null and state idle when sessions dir is empty but alive (0.170176ms)
✔ discoverRepoAgents: finds agents with a sessions dir, skips those without, sorted by name (0.37218ms)
✔ discoverFleetAgents: finds agents with a sessions dir, sorted by name, fleet tmux fields (0.378987ms)
✔ scan: writes per-agent files and index.json, rerun overwrites, no leftover tmp files (0.967412ms)
✔ scan: relative boardDir throws ConfigError (0.093777ms)
✔ CLI: scan with assume-alive liveness exits 0, prints board summary, writes board files (58.802027ms)
✔ CLI: missing config exits 2 with a refused: message (51.021156ms)
✔ CLI: unknown command exits 2 (50.285259ms)
✔ CLI: unknown --liveness value exits 2 (55.257373ms)
✔ panesRunPi: true when any trimmed line equals 'pi' (0.247684ms)
✔ panesRunPi: false for bash-only, claude, empty, or node-pi-style lines (0.078529ms)
✔ tmuxIsAlive: a pane running pi is alive (0.211692ms)
✔ tmuxIsAlive: session exists but pi has exited is not alive (0.076486ms)
✔ tmuxIsAlive: no such tmux session is not alive (0.061456ms)
✔ tmuxIsAlive: tmux could not be run at all is unknown (null), never assumed alive (0.062541ms)
✔ tmuxIsAlive: passes -L <socket> only when a socket is given (0.096803ms)
✔ parsePanes: one pane per line, command and optional tab-separated path (0.07962ms)
✔ tmuxInspect: reports the path of the pane running pi, not of a shell pane (0.074285ms)
✔ tmuxInspect: no pi pane, no session, or no tmux gives no workspace and the matching liveness (0.097511ms)
✔ loadSeen: missing file returns {} (0.155968ms)
✔ loadSeen: invalid JSON throws ConfigError (0.219738ms)
✔ loadSeen: a JSON array throws ConfigError (0.168137ms)
✔ loadSeen: a non-string value throws ConfigError (0.168774ms)
✔ markSeen: seen true adds the key and writes seen.json mode 0600, no leftover tmp files (0.309188ms)
✔ markSeen: seen false deletes the key (0.24856ms)
✔ markSeen: missing, empty, or non-string fields throw ConfigError (0.218582ms)
✔ markSeen: project containing '/' throws ConfigError (0.111987ms)
✔ markSeen: non-boolean seen throws ConfigError (0.108609ms)
✔ scanAgent: a seen mark matching the waiting session's lastTimestamp clears waitingOnYou (0.282144ms)
✔ scanAgent: a stale mark (agent wrote something newer) is not seen and waitingOnYou is true (0.247828ms)
✔ scanAgent: a working session with a matching mark is not seen (marks only apply to waiting/error) (0.227325ms)
✔ scanAgent: an error-state session with a matching mark is seen (0.227839ms)
✔ scan: index.seen and waitingOnYou reflect seen.json, which scan never rewrites or deletes (0.496047ms)
✔ scan: a corrupt seen.json makes scan throw ConfigError (fail closed) (0.186552ms)
✔ taskSetBy: a registered task carries the record's setter; a record without the field (pre-#1511) reads unknown; the value is not copied into registered (0.619757ms)
✔ taskSetBy: null whenever the task shown is not the registered one: no registration, an empty registered task, a stale registration; the field is always present (0.563664ms)
✔ taskSetBy: scan() reads the field from disk through the seat package (bounded there), writes it to the per-agent record and index, and an invalid on-disk value is a registrationError, never a row value (0.939906ms)
✔ isLoopbackHost: recognizes loopback hosts (1.277363ms)
✔ isLoopbackHost: rejects non-loopback hosts (4.523177ms)
✔ startServer: refuses a non-loopback host with ConfigError, never opens a socket (3.59353ms)
✔ startServer: serves page, healthz, and a rescanning /api/board (35.659466ms)
✔ startServer: a seatsDir registration overrides the row and index.registered reflects it (6.146856ms)
✔ startServer: /api/board returns 500 JSON with an error field when scan throws (2.400245ms)
✔ CLI: serve refuses a non-loopback host with exit 2 and a refused: message (53.880169ms)
✔ CLI: serve rejects a non-numeric --port with exit 2 (47.763257ms)
✔ CLI: scan still works after the async cli refactor (53.502565ms)
✔ CLI: live serve prints its URL and answers /healthz (59.412096ms)
✔ page.html: esc() escapes every HTML-significant character (0.648508ms)
✔ POST /api/seen marks a row; GET /api/board still shows it seen; seen:false clears it (7.799646ms)
✔ POST /api/seen without a JSON content-type returns 400 and does not write a mark (2.508134ms)
✔ POST /api/seen with invalid JSON returns 400 (1.903236ms)
✔ POST /api/seen with a body over 4096 bytes returns 400 (or resets the connection) and writes no mark (1.975783ms)
✔ POST /api/seen with a missing agent returns 400 (1.259383ms)
✔ POST /api/board returns 405; PUT /api/seen returns 405 (1.573435ms)
✔ CLI: scan --print marks a seen row with 's' and the summary line ends with 'N seen)' (50.607954ms)
✔ page.html: seenControl() escapes rec.project/agent/lastActivity, and the POST uses a JSON content-type (0.317323ms)
✔ page.html: has a collapsed Seen section that lists seen rows with the shared row builder (0.260037ms)
✔ page.html: each project has a Hide seen checkbox (default on) beside Hide offline, with a hidden-count note (0.154397ms)
✔ page.html: a project header reads "N of N" only while a checkbox hides rows (0.16282ms)
✔ page.html: every row shows Task and Active project, derived or the word unknown, with the workspace in the detail (0.283516ms)
✔ page.html: task and active project cells show their source via sourceTag(); the detail has a Registered row via registeredText(); SOURCE_LABEL maps registration to registered; every dynamic value in sourceTag/fromSource/registeredText is escaped (0.548275ms)
✔ POST /api/reply: runs agent-send.sh with -s from the registration, -S <host>:control-board, -m text plus the fixed trailer, no -L on the default socket, MOSAIC_TMUX_SOCKET stripped; answers delivered with the exit code and both streams (28.520776ms)
✔ POST /api/reply: a registration with a tmux socket adds -L <socket> (27.006224ms)
✔ POST /api/reply: a non-zero tool exit is a 200 with delivered false, the exit code and the stderr verbatim (27.012188ms)
✔ POST /api/reply: refusals before the tool runs: empty or blank or long text 400, unknown row 404, no registration 409, stale registration 409, no tmux session 409, bad JSON 400; the tool is never called (13.925126ms)
✔ POST /api/reply: a missing agent-send.sh is a 500 with the path in the error, not a crash (4.242121ms)
✔ replyToRow: DEFAULT_AGENT_SEND is the repository's tools/tmux/agent-send.sh and it is executable (0.14168ms)
✔ page.html: the reply box appears only where canReply() holds (live registration with a tmux session), the detail has a Reply row, the submit posts JSON to /api/reply, receipts and drafts survive a refresh, and every receipt value is escaped (0.571395ms)
✔ startServer: /api/board carries taskSetBy from a live registration and null for the derived rows (2.507482ms)
✔ page.html: the task cell and detail show who set a registered task via setByTag()/setByText(), both escaped, only from rec.taskSetBy; the reply gate does not read it (0.311159ms)
✔ Host/Origin guard: GET /api/board and POST /api/reply refuse a foreign Host, a wrong port and a cross-origin Origin with 403 JSON, before any scan or send, and never send CORS headers (7.759398ms)
✔ Host/Origin guard: loopback names on this port are accepted, with or without a same-origin Origin (31.693895ms)
✔ conversation routes (F16): a foreign Host, a wrong port and a cross-origin Origin get 403 before the reader runs, with no CORS headers (5.606208ms)
✔ every refusal code the reader can raise has an HTTP status (0.955939ms)
✔ conversation routes: catalogue, first page, next page and follow over HTTP; refusals map to 4xx with their code; nothing is written (45.78585ms)
✔ explicit request, Seen, ordinary completion and a new request have distinct attention states (4.096287ms)
✔ attention convention ignores reasoning/quoted examples and permits leading blank lines (0.242785ms)
✔ completed smoke replies and ordinary questions are idle, not human blockers (0.846915ms)
✔ only an explicit first-line input request makes a finished reply waiting (0.171746ms)
✔ tool activity, user text, errors and unfinished turns override attention text (0.136452ms)
✔ STOP access failure is unknown, not absence, under a non-root identity (38.327363ms)
✔ connector Task never inherits Discord routing envelopes; ordinary Task still uses user text (3.000707ms)
✔ connector discovery keeps only safe identity; rejects modes, mismatches, links and traversal (1.497489ms)
✔ canonical owner identity and STOP are independent; no tmux fallback or forged registration (5.879171ms)
✔ connector reply refusal precedes forged live tmux registration; ordinary agent still sends (0.584339ms)
✔ server rescans connector discovery and refuses HTTP reply without transport (35.559009ms)
✔ connector session links and linked directories are not read (1.077521ms)
✔ newer live matching launch marks old activity, preserves history/attention/attribution, then clears on new activity (3.749232ms)
✔ CLI print uses the relaunch notice instead of old current preview (61.194863ms)
✔ connector owner and fixed task never inherit a native relaunch notice (2.122352ms)
✔ equality, stale/unknown/offline, mismatched registration and unknown activity do not assert relaunch (1.149868ms)
✔ loadConfig: missing file throws ConfigError (2.276188ms)
✔ loadConfig: invalid JSON throws ConfigError (0.410099ms)
✔ loadConfig: missing dataRoot throws ConfigError (0.325667ms)
✔ loadConfig: relative dataRoot throws ConfigError (0.324818ms)
✔ loadConfig: valid config returns dataRoot (0.983333ms)
✔ findNewestSession: picks the newest by mtime among two files (0.644738ms)
✔ findNewestSession: finds files in nested subdirectories (0.3859ms)
✔ findNewestSession: returns null for a missing dir (0.126045ms)
✔ readSession: extracts fields, collapses/truncates text, counts a truncated final line (0.665906ms)
✔ readSession: model and provider follow the latest model_change entry or assistant turn; null when the log names neither; scanAgent carries them (1.644308ms)
✔ readSession: lastError carries the assistant errorMessage only when the last assistant turn errored (0.50558ms)
✔ findNewestSession/scan: never read sibling auth or secrets next to a sessions dir (1.156336ms)
✔ deriveState: full state table (0.18949ms)
✔ rule: newest entry is an assistant message with a tool call, after a question-looking text, is working (0.317703ms)
✔ rule: newest entry is a tool result with no assistant text after it is working (0.275116ms)
✔ rule: a finished ordinary turn is idle, even if it says your move (0.335356ms)
✔ task: the first user message of the session, from text blocks (0.252296ms)
✔ task: a plain-string user content is accepted, whitespace collapsed and long text capped (0.247832ms)
✔ task: no user message in the log means null (shown as unknown), never a guess (0.302295ms)
✔ workspace: the live tmux pane path wins; the session cwd is the fallback; neither means null (0.384643ms)
✔ activeProject: basename of the nearest .git directory or .git file above the workspace; none means null (0.569899ms)
✔ scan: the written record carries task, workspace and activeProject (0.505496ms)
✔ registration: overrides task, project and workspace; every source says registration; registered carries the launch fields; the grouping column is untouched (0.613266ms)
✔ registration: empty task and null project/workspace leave the derived values in place; registered is still non-null (0.40108ms)
✔ registration: a record whose pid is gone is stale; derived values win, sources say derived, registered stays with alive false; a pid the probe cannot decide is not stale; pidAlive itself (1.061634ms)
✔ registration: no registration leaves the Gate A fields exactly as before, and registered is null (0.447677ms)
✔ loadRegistrations: a missing seatsDir gives empty lists (0.156698ms)
✔ loadRegistrations: one good record, one malformed JSON, one with an unknown field; a stray file under seatsDir is ignored (0.751044ms)
✔ matchRegistration: matches by sessionsDir, and by realpath through a symlink; sessionsDir null never matches; same seat name with a different sessionsDir does not match (fleet vs repo darkwing) (0.33041ms)
✔ scan: writes the registration override to disk; index.json carries registered and registrationErrors (0.730321ms)
✔ scan: a relative seatsDir throws ConfigError; an omitted seatsDir behaves as before (0.688744ms)
✔ scanAgent: waitingOnYou is true for waiting/error and false otherwise (0.569466ms)
✔ scanAgent: ageSeconds is computed from the injected now (0.263488ms)
✔ scanAgent: sessionFile null and state idle when sessions dir is empty but alive (0.185838ms)
✔ discoverRepoAgents: finds agents with a sessions dir, skips those without, sorted by name (0.323154ms)
✔ discoverFleetAgents: finds agents with a sessions dir, sorted by name, fleet tmux fields (0.329147ms)
✔ scan: writes per-agent files and index.json, rerun overwrites, no leftover tmp files (0.846915ms)
✔ scan: relative boardDir throws ConfigError (0.116027ms)
✔ CLI: scan with assume-alive liveness exits 0, prints board summary, writes board files (60.587692ms)
✔ CLI: missing config exits 2 with a refused: message (60.171409ms)
✔ CLI: unknown command exits 2 (52.257478ms)
✔ CLI: unknown --liveness value exits 2 (49.224582ms)
✔ panesRunPi: true when any trimmed line equals 'pi' (0.178207ms)
✔ panesRunPi: false for bash-only, claude, empty, or node-pi-style lines (0.064143ms)
✔ tmuxIsAlive: a pane running pi is alive (0.164539ms)
✔ tmuxIsAlive: session exists but pi has exited is not alive (0.078587ms)
✔ tmuxIsAlive: no such tmux session is not alive (0.046507ms)
✔ tmuxIsAlive: tmux could not be run at all is unknown (null), never assumed alive (0.057703ms)
✔ tmuxIsAlive: passes -L <socket> only when a socket is given (0.084943ms)
✔ parsePanes: one pane per line, command and optional tab-separated path (0.083186ms)
✔ tmuxInspect: reports the path of the pane running pi, not of a shell pane (0.068346ms)
✔ tmuxInspect: no pi pane, no session, or no tmux gives no workspace and the matching liveness (0.085678ms)
✔ loadSeen: missing file returns {} (0.143595ms)
✔ loadSeen: invalid JSON throws ConfigError (0.176736ms)
✔ loadSeen: a JSON array throws ConfigError (0.146391ms)
✔ loadSeen: a non-string value throws ConfigError (0.1506ms)
✔ markSeen: seen true adds the key and writes seen.json mode 0600, no leftover tmp files (0.317689ms)
✔ markSeen: seen false deletes the key (0.245251ms)
✔ markSeen: missing, empty, or non-string fields throw ConfigError (0.190901ms)
✔ markSeen: project containing '/' throws ConfigError (0.114614ms)
✔ markSeen: non-boolean seen throws ConfigError (0.097122ms)
✔ scanAgent: a seen mark matching the waiting session's lastTimestamp clears waitingOnYou (0.270157ms)
✔ scanAgent: a stale mark (agent wrote something newer) is not seen and waitingOnYou is true (0.207344ms)
✔ scanAgent: a working session with a matching mark is not seen (marks only apply to waiting/error) (0.221339ms)
✔ scanAgent: an error-state session with a matching mark is seen (0.22026ms)
✔ scan: index.seen and waitingOnYou reflect seen.json, which scan never rewrites or deletes (0.461494ms)
✔ scan: a corrupt seen.json makes scan throw ConfigError (fail closed) (0.168406ms)
✔ taskSetBy: a registered task carries the record's setter; a record without the field (pre-#1511) reads unknown; the value is not copied into registered (0.584854ms)
✔ taskSetBy: null whenever the task shown is not the registered one: no registration, an empty registered task, a stale registration; the field is always present (0.559492ms)
✔ taskSetBy: scan() reads the field from disk through the seat package (bounded there), writes it to the per-agent record and index, and an invalid on-disk value is a registrationError, never a row value (0.887582ms)
✔ isLoopbackHost: recognizes loopback hosts (1.257641ms)
✔ isLoopbackHost: rejects non-loopback hosts (4.586746ms)
✔ startServer: refuses a non-loopback host with ConfigError, never opens a socket (3.074877ms)
✔ startServer: serves page, healthz, and a rescanning /api/board (34.39639ms)
✔ startServer: a seatsDir registration overrides the row and index.registered reflects it (6.300202ms)
✔ startServer: /api/board returns 500 JSON with an error field when scan throws (2.380365ms)
✔ CLI: serve refuses a non-loopback host with exit 2 and a refused: message (55.462546ms)
✔ CLI: serve rejects a non-numeric --port with exit 2 (54.15464ms)
✔ CLI: scan still works after the async cli refactor (48.467479ms)
✔ CLI: live serve prints its URL and answers /healthz (60.472536ms)
✔ page.html: esc() escapes every HTML-significant character (0.615927ms)
✔ POST /api/seen marks a row; GET /api/board still shows it seen; seen:false clears it (8.194124ms)
✔ POST /api/seen without a JSON content-type returns 400 and does not write a mark (2.489995ms)
✔ POST /api/seen with invalid JSON returns 400 (2.794772ms)
✔ POST /api/seen with a body over 4096 bytes returns 400 (or resets the connection) and writes no mark (2.07639ms)
✔ POST /api/seen with a missing agent returns 400 (1.345416ms)
✔ POST /api/board returns 405; PUT /api/seen returns 405 (1.686153ms)
✔ CLI: scan --print marks a seen row with 's' and the summary line ends with 'N seen)' (54.281161ms)
✔ page.html: seenControl() escapes rec.project/agent/lastActivity, and the POST uses a JSON content-type (0.320625ms)
✔ page.html: has a collapsed Seen section that lists seen rows with the shared row builder (0.264823ms)
✔ page.html: each project has a Hide seen checkbox (default on) beside Hide offline, with a hidden-count note (0.169369ms)
✔ page.html: a project header reads "N of N" only while a checkbox hides rows (0.128358ms)
✔ page.html: every row shows Task and Active project, derived or the word unknown, with the workspace in the detail (0.308994ms)
✔ page.html: task and active project cells show their source via sourceTag(); the detail has a Registered row via registeredText(); SOURCE_LABEL maps registration to registered; every dynamic value in sourceTag/fromSource/registeredText is escaped (0.560554ms)
✔ POST /api/reply: runs agent-send.sh with -s from the registration, -S <host>:control-board, -m text plus the fixed trailer, no -L on the default socket, MOSAIC_TMUX_SOCKET stripped; answers delivered with the exit code and both streams (27.75916ms)
✔ POST /api/reply: a registration with a tmux socket adds -L <socket> (25.018106ms)
✔ POST /api/reply: a non-zero tool exit is a 200 with delivered false, the exit code and the stderr verbatim (26.681206ms)
✔ POST /api/reply: refusals before the tool runs: empty or blank or long text 400, unknown row 404, no registration 409, stale registration 409, no tmux session 409, bad JSON 400; the tool is never called (14.406945ms)
✔ POST /api/reply: a missing agent-send.sh is a 500 with the path in the error, not a crash (4.315379ms)
✔ replyToRow: DEFAULT_AGENT_SEND is the repository's tools/tmux/agent-send.sh and it is executable (0.152417ms)
✔ page.html: the reply box appears only where canReply() holds (live registration with a tmux session), the detail has a Reply row, the submit posts JSON to /api/reply, receipts and drafts survive a refresh, and every receipt value is escaped (0.568856ms)
✔ startServer: /api/board carries taskSetBy from a live registration and null for the derived rows (2.500947ms)
✔ page.html: the task cell and detail show who set a registered task via setByTag()/setByText(), both escaped, only from rec.taskSetBy; the reply gate does not read it (0.300847ms)
✔ Host/Origin guard: GET /api/board and POST /api/reply refuse a foreign Host, a wrong port and a cross-origin Origin with 403 JSON, before any scan or send, and never send CORS headers (7.930828ms)
✔ Host/Origin guard: loopback names on this port are accepted, with or without a same-origin Origin (29.300207ms)
✔ conversation routes (F16): a foreign Host, a wrong port and a cross-origin Origin get 403 before the reader runs, with no CORS headers (5.619576ms)
✔ every refusal code the reader can raise has an HTTP status (0.918207ms)
✔ conversation routes: catalogue, first page, next page and follow over HTTP; refusals map to 4xx with their code; nothing is written (45.444967ms)
ℹ tests 124
ℹ suites 0
ℹ pass 124
@@ -129,4 +129,4 @@
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 625.036405
ℹ duration_ms 621.068041
+153 -153
View File
@@ -1,155 +1,155 @@
✔ W1: two processes acquire the same pair at once; exactly one claim (500.172302ms)
✔ W1: two writers publish the same revision at once: one wins, the other gets null, the winner's record stays (34.447301ms)
✔ W1: a revision name appears only after its bytes are synced; before that, only a temp file exists (21.245207ms)
✔ W2: acquire while a claim is reserved or active refuses already-active (380.782492ms)
✔ W3: acquire while stopping, uncertain, or stopped without proof refuses unsafe-replacement (415.536314ms)
✔ W4: same session with another seat tuple, and the reverse, both refuse; a loser on the seat key closes it no-unit (258.443009ms)
✔ W4: a hard link of one session under another seat is the same session: the second controller refuses already-active and launches nothing (208.321924ms)
✔ W4: a copy of one session under another seat is the same session: the second controller refuses already-active and launches nothing (209.096493ms)
✔ W4: a session header ID that changes after construction refuses target; nothing is claimed or launched (2.343681ms)
✔ W5: SIGKILL between every publication barrier of acquire and transition; restart never finds two holders or a lost claim (10639.643125ms)
✔ W5: SIGKILL between every publication barrier of release; restart finishes or holds the release (33722.598575ms)
✔ W6: controller killed mid-turn while the engine lives; restart is uncertain, no launch, prompts refuse (297.831509ms)
✔ W12: a live owner paused with SIGSTOP; a second controller refuses already-active and changes nothing (167.189405ms)
✔ W13: crash after the engine spawns, before active; restart finds the live unit: uncertain, no second spawn, force stop only (484.422997ms)
✔ W14: crash after reservation, before the spawn marker: stopped with a no-unit observation; the pair is free (317.977402ms)
✔ W20: crash after the spawn marker, scope collected; uncertain in both runs, the marker is copied, no launch until a boot proof (434.213183ms)
✔ W15: crash between the two keys during release; restart finishes it under the same claim ID (138.169018ms)
✔ W7: recorded boot ID differs on the same machine: stopped with a boot proof; open tool calls become uncertain (202.378983ms)
✔ W8: resume after a proven stop with the same pins: new claim ID, generation +1, same conversation, branch and leaf (397.75689ms)
✔ W9: resume with a changed binary, argv digest, branch or leaf is refused and the claim is unchanged (946.3831ms)
✔ W11: the controller writes no session file; only the fake engine's own appends appear (225.315133ms)
✔ W16: a highest revision that won't parse holds the pair uncertain; the older stopped revision is not reused (93.363585ms)
✔ W17: a claim root copied from another host refuses foreign-host and promotes nothing (72.203607ms)
✔ G1: a session path or claim root under .pi/state, ~/.claude, the data root or a registration refuses at construction (3.569722ms)
✔ G2: a symlink inside the fixture root to a live session file is refused by the real-path check (1.325889ms)
✔ G3: a fixture path swapped for a live path after construction is refused at bind (1.738078ms)
✔ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2794.484922ms)
✔ K2: K1 on the process-group fallback ends uncertain, never stopped (469.89623ms)
✔ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2644.635443ms)
✔ K4: two engines; force stop one; the other survives by independent observation (4852.089312ms)
✔ K5: a stop during a tool call leaves the effect uncertain, and it is shown (2563.661152ms)
✔ K12: a member forking in a loop: the freeze stops it, enumeration is complete, populated 0 after cgroup.kill (2494.802302ms)
✔ K13: a member writing its pid into another cgroup is refused by the namespace; the kill is complete (2746.623948ms)
✔ K15: the shim gone, engine/cgroup.events unreadable, or the engine cgroup missing: evidence unavailable, not empty; uncertain (5278.616647ms)
✔ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (920.216881ms)
✔ K11: controller killed after the confirmation is recorded, before TERM: restart checks the invocation ID and re-runs from TERM for the same stop (750.409927ms)
✔ K14: a unit with the recorded name but another invocation ID: evidence unavailable, no signals, uncertain (590.614603ms)
✔ K6: recover without proof, without confirmation, or with changed pins is refused (984.081218ms)
✔ K7: recover after proof, then launch: new claim and execution, generation +1, same leaf; the cancelled prompt is not replayed (486.910482ms)
✔ K8: an engine that loads another leaf on resume is refused before admission; it stays claimed until a proven stop (679.003936ms)
✔ K9: an interrupt that never settles stays uncertain; force stop stays available; takeover is refused while fenced (3412.938836ms)
✔ K16: a claim from another machine ID refuses foreign-host; no boot proof is issued (36.405575ms)
✔ K17: two launcher calls with one eligibility record: one launch, the other refuses, no second engine (486.630344ms)
✔ K18: the leaf changes after eligibility: launch refused; the reservation stays until released with proof (531.704758ms)
✔ S1: `/goal x`, with leading spaces or a tab, refuses text-policy at admission; zero engine bytes (504.380237ms)
✔ S2: every prefix pinned Pi interprets is refused, from the list the code uses; the rest reach the engine exactly (217.876841ms)
✔ S3: `/goal` on the second line is pinned from the source: Pi checks only index 0, so it is admitted and sent exactly (212.438677ms)
✔ S4: a `/` left in the composer is cleared when control transfers and returns; the next submit sends only the new text (308.695737ms)
✔ S5: an observer terminal gets a paste then Enter, as send-message.sh does: not admitted: controller, nothing sent (205.567788ms)
✔ S6: a mediated-shaped registration (no tmux) passed to the board's replyToRow: 409 no tmux session; exec never runs (0.474109ms)
✔ S7: ESC, bracketed-paste markers and U+2028/U+2029 travel as one JSON string; the engine receives the exact text in one record (213.418843ms)
✔ P3: a Pi confirm, select, input or editor dialog is shown disabled with a reason and never answered (304.548788ms)
✔ E1: send, ack, user, toolCall, toolResult, final answer: shown once, no refresh, draft and reading position kept (230.67903ms)
✔ E2: U+2028, U+2029 inside JSON strings and CRLF line ends each parse as one record, on the splitter and through the controller (232.279238ms)
✔ E3: a multipart final, two blocks, null request correlation and duplicate delivery (225.911236ms)
✔ E4: a page read after message_end but before its entry is persisted: marker at the seam, re-read after run-settled, each message once (226.928276ms)
✔ E4: a gap or a new epoch also reconciles; nothing is concatenated across a gap (11.157214ms)
✔ E5: an unknown native event gives no client event; evidence records its type and bytes; the terminal count goes up (240.351129ms)
✔ E6: a tool result delayed across a pause and a reconnect is reconciled without a manual refresh (230.11528ms)
✔ E7: the terminal renders the same stream as the library client, as observer and then as controller, and submits only as controller (317.639113ms)
✔ terminal: engine control characters are made visible; a lost connection refuses submit (374.398052ms)
✔ terminal: outcome unknown is shown as such, with no resend offer, and nothing is resent (0.539984ms)
✔ terminal: text after Enter in the same input chunk starts the next message; it never joins the one submitted (0.282556ms)
✔ terminal: a paste-start marker split right after its ESC still opens the paste; the Enter inside it never submits (0.30821ms)
✔ terminal: invisible and bidi characters are made visible; head, status and notice lines stay one line (0.123271ms)
✔ every record these fixtures produced is a valid CHAT-01 record (E5: no record fails the schema) (245.322895ms)
✔ H1: two takeovers with the same expected generation: one wins, +1; the other refuses generation (559.299151ms)
✔ H2: the old controller's prompt after a takeover commits is refused with zero engine bytes (306.639949ms)
✔ H3: a takeover while a prompt holds the dispatch lock: written under the old actor, or refused; never both (864.367817ms)
✔ H4: self-takeover is refused (207.977962ms)
✔ H9: Interrupt racing a prompt's dispatch: before the write, dispatch-refused and no-turn; after, §3 rules (489.967376ms)
✔ H10: Interrupt and force stop together: one stop chain, force stop supersedes (1162.284778ms)
✔ H10: an overlap during the pause before the abort: no abort, the stop ends uncertain (400.286574ms)
✔ H10: a no-turn Interrupt lifts only its own fence; admission stays closed under force stop, overlap or revocation (854.487718ms)
✔ H11: the controller disconnects mid-turn: work continues, the claim is unchanged, control stays put (474.571479ms)
✔ H12: an exact retry after reconnecting to the same incarnation returns the same receipt; one dispatch (235.94039ms)
✔ H13: a retry with the same request ID and different text is refused (212.137643ms)
✔ H14: late stdout from the old engine after a replacement is dropped by incarnation, counted, never rendered (722.250079ms)
✔ H15: a revoked connection's command is refused; the revocation fence holds (301.681102ms)
✔ H16: a second controller for the same session refuses already-active; the first is untouched (230.452513ms)
✔ H10: a second force stop while the first escalation runs refuses fenced; one escalation, and the claim records only the first stop's phases (939.093746ms)
✔ H17: a confirmation reused, answered from another connection, or used after the stop changed is refused (959.856778ms)
✔ H18: two prompts before any native output: the second refuses busy; one engine write (363.959262ms)
✔ H19: the pipe fails mid-line under a large prompt: delivery-unknown transport-unknown, poisoned, no later write (409.106014ms)
✔ H19: the link itself never writes again after an unknown outcome, whoever calls it (0.655539ms)
✔ H19: the controller dies mid-write of a large line: after restart the outcome is unknown and nothing is resent (703.309217ms)
✔ H20: the line is written but the ack is lost when the controller dies: orphan, outcome unknown, nothing resent (603.731272ms)
✔ H21: a retry of the exact request with the old token after a crash is stale-incarnation; no second write (790.922524ms)
✔ H22: after H21 and a valid recovery, a new request with the new token is admitted (2967.111484ms)
✔ H23: requests pending at a restart are not resent; each shows outcome unknown (1706.924017ms)
✔ a plain conversation: catalogue row, one page, CHAT-01 records (7.943027ms)
✔ native entries map to blocks: tools, thinking, bash, notices, ids that do not fit (1.772943ms)
✔ F1: a malformed line is an unavailable part at its position, and reading continues (1.752425ms)
✔ F1: a missing parent stops the history with a notice that names the unreadable lines (2.556892ms)
✔ F1: an unreadable fork is never merged into another branch's history (1.541133ms)
✔ F1: a follow stays on its branch when the next entry's parent is unreadable (1.990715ms)
✔ F1: a file whose entries are all unreadable shows a notice per line (1.130976ms)
✔ F2: a truncated trailing line marks the view incomplete, not an error (1.328783ms)
✔ pagination: 100 parts, then the rest; parts concatenate to the whole branch (4.148949ms)
✔ F3: a replaced file (new inode) refuses old cursors with reconcile (7.152193ms)
✔ F4: a same-inode rewrite of the prefix refuses old cursors with reconcile (6.687027ms)
✔ F5: growth between pages keeps the epoch and the page stops at the pinned length (5.6262ms)
✔ F6: unknown, foreign and expired cursors refuse and leave the cursor usable (7.512257ms)
✔ F7: a symlinked file and a symlinked directory component are refused, never opened (8.779647ms)
✔ F8: a file swapped for a symlink after the catalogue is refused (2.496262ms)
✔ F9: registrations never add or redirect a root (1.588804ms)
✔ F10: a header cwd naming another project is refused (3.361116ms)
✔ F11: parentSession renders with a marker and the parent is never opened (0.776124ms)
✔ F12: two leaves: the default leaf is shown and the other branch reads alone (5.470116ms)
✔ F12: a follow refuses when an appended duplicate id changes the branch's earlier parts (2.272352ms)
✔ F12: a second root (Pi's resetLeaf) starts its own branch (1.350252ms)
✔ F13: compaction is a marker in place, then the retained content (0.727293ms)
✔ F14: long strings split into fragments and parts, reassemble exactly, and pages respect the byte cap (523.566064ms)
✔ fragments never cut a surrogate pair and keep an empty string (5.179993ms)
✔ F15: a Claude seat is an unsupported-harness placeholder whose directory is never read (1.677094ms)
✔ unknown conversations, empty files and non-Pi files refuse (2.369772ms)
✔ an unreadable file or root inside the roots is refused per row, not a failed catalogue (1.093831ms)
✔ a seat directory without search permission refuses that root, not the catalogue (2.100106ms)
✔ every page and cursor is a valid CHAT-01 record (804.092641ms)
✔ the engine pin holds for the installed package (1.919918ms)
✔ pinned Pi, sealed and without credentials, answers the controller's commands with the shapes the fake models (331.021752ms)
✔ pinned Pi appends thinking_level_change at start when the branch lacks one, so the leaf moves (K8 then fails closed) (275.832025ms)
✔ N25: ordinary Interrupt reconciles; a non-empty queue_update in the window is O5 (806.068343ms)
✔ N1: an extension's follow-up queued after the fence is cleared before any abort; O5, Unknown (469.189878ms)
✔ N1: a follow-up queued before the fence is O5 at once; the Interrupt refuses fenced (254.60826ms)
✔ N2: with abort first, the fake runs the external item (the ordering guard has teeth) (22.156872ms)
✔ N3: the fence lands in preflight, preflight errors, no run: failed, No run, uncertain (368.884282ms)
✔ N4: the ack arrives after the first abort and a run starts: clear and abort again; Interrupted (213.556408ms)
✔ N5: an input handler takes the prompt: ack, no run, delivery-unknown handled-without-run (331.026384ms)
✔ N6: an extension queues between clear_queue and abort: O5 and O6, Unknown (537.207506ms)
✔ N7: clear_queue times out: no abort, nativeQueue unknown, force stop still ends it (1925.400277ms)
✔ N7: clear_queue answers an error: no abort, nativeQueue unknown, the link not poisoned (258.018644ms)
✔ N8: an extension prompt starts a run during Mosaic preflight; the losing settle is O3 (378.641567ms)
✔ N9: a run that started before the fence and ends aborted: failed interrupted, Interrupted (210.25368ms)
✔ N9: decision 34: a run that ends aborted with no stop in progress: aborted-without-stop, uncertain, outcome unknown (309.741586ms)
✔ N9: an aborted that lands after the fence but before any abort is written: aborted-without-stop, Unknown (446.67773ms)
✔ N10: fake conformance (31.971148ms)
✔ N11: the run fails before any user message_start: delivery-unknown ack-without-start, never failed (474.454081ms)
✔ N12: input that starts a run after the final empty clear is O1 and not part of the stop's proof (300.784606ms)
✔ N13: agent_start with no slot held is O1; a later prompt refuses with zero engine bytes (324.956671ms)
✔ N14: the run completes while clear_queue is in flight: finished, Completed first, uncertain (414.997271ms)
✔ N14: the run completes after the abort is written, before Pi applies it: finished, never relabelled (450.246043ms)
✔ N15: the fence lands in preflight, then an input handler takes it: handled-without-run, No run (562.546304ms)
✔ N16: Interrupt with no slot and no run refuses no-turn: no stop, no bytes, admission open (274.511468ms)
✔ N17: the run fails on its own during the exchange: failed, Failed on its own (398.931267ms)
✔ N18: no final assistant message_end, or a lost line: working stays working; before working, transport-unknown (919.910026ms)
✔ N19: a losing extension prompt settles inside the Mosaic run before its user message: O3, run-overlap (662.641784ms)
✔ N20: an extension triggerTurn during Mosaic preflight starts first; while streaming it queues with no signal (582.515503ms)
✔ N21: a losing settle after the receipt settled finished is O2; the receipt stays finished (203.649783ms)
✔ N22: an agent-level custom message is dropped by the clear with no signal; evidence names the seal (225.882597ms)
✔ N23: a nextTurn message survives clear and abort and attaches to the next prompt, with no signal (213.819475ms)
✔ N24: the seal is an allow-list: --extension, a missing --no-* flag, a second --mode or --session, a session or output flag, or a stray word refuses unsealed-engine; no engine starts (229.585286ms)
✔ W1: two processes acquire the same pair at once; exactly one claim (477.086964ms)
✔ W1: two writers publish the same revision at once: one wins, the other gets null, the winner's record stays (37.843206ms)
✔ W1: a revision name appears only after its bytes are synced; before that, only a temp file exists (19.247751ms)
✔ W2: acquire while a claim is reserved or active refuses already-active (368.84834ms)
✔ W3: acquire while stopping, uncertain, or stopped without proof refuses unsafe-replacement (410.911345ms)
✔ W4: same session with another seat tuple, and the reverse, both refuse; a loser on the seat key closes it no-unit (280.706297ms)
✔ W4: a hard link of one session under another seat is the same session: the second controller refuses already-active and launches nothing (217.122101ms)
✔ W4: a copy of one session under another seat is the same session: the second controller refuses already-active and launches nothing (199.76267ms)
✔ W4: a session header ID that changes after construction refuses target; nothing is claimed or launched (2.223489ms)
✔ W5: SIGKILL between every publication barrier of acquire and transition; restart never finds two holders or a lost claim (9978.92205ms)
✔ W5: SIGKILL between every publication barrier of release; restart finishes or holds the release (32447.817836ms)
✔ W6: controller killed mid-turn while the engine lives; restart is uncertain, no launch, prompts refuse (328.138189ms)
✔ W12: a live owner paused with SIGSTOP; a second controller refuses already-active and changes nothing (193.375637ms)
✔ W13: crash after the engine spawns, before active; restart finds the live unit: uncertain, no second spawn, force stop only (647.531117ms)
✔ W14: crash after reservation, before the spawn marker: stopped with a no-unit observation; the pair is free (307.626437ms)
✔ W20: crash after the spawn marker, scope collected; uncertain in both runs, the marker is copied, no launch until a boot proof (442.883523ms)
✔ W15: crash between the two keys during release; restart finishes it under the same claim ID (120.786161ms)
✔ W7: recorded boot ID differs on the same machine: stopped with a boot proof; open tool calls become uncertain (199.28518ms)
✔ W8: resume after a proven stop with the same pins: new claim ID, generation +1, same conversation, branch and leaf (510.669432ms)
✔ W9: resume with a changed binary, argv digest, branch or leaf is refused and the claim is unchanged (978.317356ms)
✔ W11: the controller writes no session file; only the fake engine's own appends appear (384.697568ms)
✔ W16: a highest revision that won't parse holds the pair uncertain; the older stopped revision is not reused (93.004349ms)
✔ W17: a claim root copied from another host refuses foreign-host and promotes nothing (93.796846ms)
✔ G1: a session path or claim root under .pi/state, ~/.claude, the data root or a registration refuses at construction (3.873228ms)
✔ G2: a symlink inside the fixture root to a live session file is refused by the real-path check (1.11013ms)
✔ G3: a fixture path swapped for a live path after construction is refused at bind (1.892311ms)
✔ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2760.86431ms)
✔ K2: K1 on the process-group fallback ends uncertain, never stopped (630.859964ms)
✔ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2695.393731ms)
✔ K4: two engines; force stop one; the other survives by independent observation (4759.634246ms)
✔ K5: a stop during a tool call leaves the effect uncertain, and it is shown (2495.83911ms)
✔ K12: a member forking in a loop: the freeze stops it, enumeration is complete, populated 0 after cgroup.kill (2572.283036ms)
✔ K13: a member writing its pid into another cgroup is refused by the namespace; the kill is complete (2503.7825ms)
✔ K15: the shim gone, engine/cgroup.events unreadable, or the engine cgroup missing: evidence unavailable, not empty; uncertain (5381.751999ms)
✔ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (817.464529ms)
✔ K11: controller killed after the confirmation is recorded, before TERM: restart checks the invocation ID and re-runs from TERM for the same stop (622.709407ms)
✔ K14: a unit with the recorded name but another invocation ID: evidence unavailable, no signals, uncertain (529.469371ms)
✔ K6: recover without proof, without confirmation, or with changed pins is refused (1132.171151ms)
✔ K7: recover after proof, then launch: new claim and execution, generation +1, same leaf; the cancelled prompt is not replayed (411.918412ms)
✔ K8: an engine that loads another leaf on resume is refused before admission; it stays claimed until a proven stop (746.116766ms)
✔ K9: an interrupt that never settles stays uncertain; force stop stays available; takeover is refused while fenced (3445.858035ms)
✔ K16: a claim from another machine ID refuses foreign-host; no boot proof is issued (42.281682ms)
✔ K17: two launcher calls with one eligibility record: one launch, the other refuses, no second engine (507.081862ms)
✔ K18: the leaf changes after eligibility: launch refused; the reservation stays until released with proof (390.990071ms)
✔ S1: `/goal x`, with leading spaces or a tab, refuses text-policy at admission; zero engine bytes (474.385289ms)
✔ S2: every prefix pinned Pi interprets is refused, from the list the code uses; the rest reach the engine exactly (210.910934ms)
✔ S3: `/goal` on the second line is pinned from the source: Pi checks only index 0, so it is admitted and sent exactly (205.02606ms)
✔ S4: a `/` left in the composer is cleared when control transfers and returns; the next submit sends only the new text (292.731024ms)
✔ S5: an observer terminal gets a paste then Enter, as send-message.sh does: not admitted: controller, nothing sent (215.429587ms)
✔ S6: a mediated-shaped registration (no tmux) passed to the board's replyToRow: 409 no tmux session; exec never runs (0.556421ms)
✔ S7: ESC, bracketed-paste markers and U+2028/U+2029 travel as one JSON string; the engine receives the exact text in one record (215.469925ms)
✔ P3: a Pi confirm, select, input or editor dialog is shown disabled with a reason and never answered (319.283723ms)
✔ E1: send, ack, user, toolCall, toolResult, final answer: shown once, no refresh, draft and reading position kept (200.663647ms)
✔ E2: U+2028, U+2029 inside JSON strings and CRLF line ends each parse as one record, on the splitter and through the controller (204.301153ms)
✔ E3: a multipart final, two blocks, null request correlation and duplicate delivery (221.684003ms)
✔ E4: a page read after message_end but before its entry is persisted: marker at the seam, re-read after run-settled, each message once (228.565966ms)
✔ E4: a gap or a new epoch also reconciles; nothing is concatenated across a gap (10.969327ms)
✔ E5: an unknown native event gives no client event; evidence records its type and bytes; the terminal count goes up (408.146813ms)
✔ E6: a tool result delayed across a pause and a reconnect is reconciled without a manual refresh (219.412379ms)
✔ E7: the terminal renders the same stream as the library client, as observer and then as controller, and submits only as controller (242.362495ms)
✔ terminal: engine control characters are made visible; a lost connection refuses submit (191.926822ms)
✔ terminal: outcome unknown is shown as such, with no resend offer, and nothing is resent (0.410826ms)
✔ terminal: text after Enter in the same input chunk starts the next message; it never joins the one submitted (0.247826ms)
✔ terminal: a paste-start marker split right after its ESC still opens the paste; the Enter inside it never submits (0.29839ms)
✔ terminal: invisible and bidi characters are made visible; head, status and notice lines stay one line (0.105483ms)
✔ every record these fixtures produced is a valid CHAT-01 record (E5: no record fails the schema) (217.236622ms)
✔ H1: two takeovers with the same expected generation: one wins, +1; the other refuses generation (520.226617ms)
✔ H2: the old controller's prompt after a takeover commits is refused with zero engine bytes (311.119802ms)
✔ H3: a takeover while a prompt holds the dispatch lock: written under the old actor, or refused; never both (866.507043ms)
✔ H4: self-takeover is refused (204.501347ms)
✔ H9: Interrupt racing a prompt's dispatch: before the write, dispatch-refused and no-turn; after, §3 rules (447.543165ms)
✔ H10: Interrupt and force stop together: one stop chain, force stop supersedes (1285.535233ms)
✔ H10: an overlap during the pause before the abort: no abort, the stop ends uncertain (218.518959ms)
✔ H10: a no-turn Interrupt lifts only its own fence; admission stays closed under force stop, overlap or revocation (810.936365ms)
✔ H11: the controller disconnects mid-turn: work continues, the claim is unchanged, control stays put (330.683336ms)
✔ H12: an exact retry after reconnecting to the same incarnation returns the same receipt; one dispatch (179.099307ms)
✔ H13: a retry with the same request ID and different text is refused (175.414126ms)
✔ H14: late stdout from the old engine after a replacement is dropped by incarnation, counted, never rendered (598.452038ms)
✔ H15: a revoked connection's command is refused; the revocation fence holds (276.572706ms)
✔ H16: a second controller for the same session refuses already-active; the first is untouched (194.198551ms)
✔ H10: a second force stop while the first escalation runs refuses fenced; one escalation, and the claim records only the first stop's phases (824.830099ms)
✔ H17: a confirmation reused, answered from another connection, or used after the stop changed is refused (899.906199ms)
✔ H18: two prompts before any native output: the second refuses busy; one engine write (207.591122ms)
✔ H19: the pipe fails mid-line under a large prompt: delivery-unknown transport-unknown, poisoned, no later write (353.488159ms)
✔ H19: the link itself never writes again after an unknown outcome, whoever calls it (0.622545ms)
✔ H19: the controller dies mid-write of a large line: after restart the outcome is unknown and nothing is resent (651.247301ms)
✔ H20: the line is written but the ack is lost when the controller dies: orphan, outcome unknown, nothing resent (547.326231ms)
✔ H21: a retry of the exact request with the old token after a crash is stale-incarnation; no second write (725.273442ms)
✔ H22: after H21 and a valid recovery, a new request with the new token is admitted (3026.548403ms)
✔ H23: requests pending at a restart are not resent; each shows outcome unknown (669.354531ms)
✔ a plain conversation: catalogue row, one page, CHAT-01 records (7.522907ms)
✔ native entries map to blocks: tools, thinking, bash, notices, ids that do not fit (1.848223ms)
✔ F1: a malformed line is an unavailable part at its position, and reading continues (2.044985ms)
✔ F1: a missing parent stops the history with a notice that names the unreadable lines (2.975236ms)
✔ F1: an unreadable fork is never merged into another branch's history (1.92965ms)
✔ F1: a follow stays on its branch when the next entry's parent is unreadable (3.307045ms)
✔ F1: a file whose entries are all unreadable shows a notice per line (1.344609ms)
✔ F2: a truncated trailing line marks the view incomplete, not an error (2.117891ms)
✔ pagination: 100 parts, then the rest; parts concatenate to the whole branch (6.007126ms)
✔ F3: a replaced file (new inode) refuses old cursors with reconcile (5.955883ms)
✔ F4: a same-inode rewrite of the prefix refuses old cursors with reconcile (5.84029ms)
✔ F5: growth between pages keeps the epoch and the page stops at the pinned length (5.009923ms)
✔ F6: unknown, foreign and expired cursors refuse and leave the cursor usable (9.34471ms)
✔ F7: a symlinked file and a symlinked directory component are refused, never opened (7.62926ms)
✔ F8: a file swapped for a symlink after the catalogue is refused (1.867437ms)
✔ F9: registrations never add or redirect a root (1.373913ms)
✔ F10: a header cwd naming another project is refused (3.03181ms)
✔ F11: parentSession renders with a marker and the parent is never opened (0.704383ms)
✔ F12: two leaves: the default leaf is shown and the other branch reads alone (4.582929ms)
✔ F12: a follow refuses when an appended duplicate id changes the branch's earlier parts (2.130198ms)
✔ F12: a second root (Pi's resetLeaf) starts its own branch (1.109862ms)
✔ F13: compaction is a marker in place, then the retained content (0.617581ms)
✔ F14: long strings split into fragments and parts, reassemble exactly, and pages respect the byte cap (509.870195ms)
✔ fragments never cut a surrogate pair and keep an empty string (4.365936ms)
✔ F15: a Claude seat is an unsupported-harness placeholder whose directory is never read (1.369863ms)
✔ unknown conversations, empty files and non-Pi files refuse (2.036043ms)
✔ an unreadable file or root inside the roots is refused per row, not a failed catalogue (0.950981ms)
✔ a seat directory without search permission refuses that root, not the catalogue (1.775213ms)
✔ every page and cursor is a valid CHAT-01 record (809.072625ms)
✔ the engine pin holds for the installed package (2.202385ms)
✔ pinned Pi, sealed and without credentials, answers the controller's commands with the shapes the fake models (267.632344ms)
✔ pinned Pi appends thinking_level_change at start when the branch lacks one, so the leaf moves (K8 then fails closed) (262.448955ms)
✔ N25: ordinary Interrupt reconciles; a non-empty queue_update in the window is O5 (766.717715ms)
✔ N1: an extension's follow-up queued after the fence is cleared before any abort; O5, Unknown (446.523598ms)
✔ N1: a follow-up queued before the fence is O5 at once; the Interrupt refuses fenced (256.450936ms)
✔ N2: with abort first, the fake runs the external item (the ordering guard has teeth) (22.687863ms)
✔ N3: the fence lands in preflight, preflight errors, no run: failed, No run, uncertain (386.692522ms)
✔ N4: the ack arrives after the first abort and a run starts: clear and abort again; Interrupted (193.528679ms)
✔ N5: an input handler takes the prompt: ack, no run, delivery-unknown handled-without-run (301.720364ms)
✔ N6: an extension queues between clear_queue and abort: O5 and O6, Unknown (718.122152ms)
✔ N7: clear_queue times out: no abort, nativeQueue unknown, force stop still ends it (1878.204406ms)
✔ N7: clear_queue answers an error: no abort, nativeQueue unknown, the link not poisoned (183.386995ms)
✔ N8: an extension prompt starts a run during Mosaic preflight; the losing settle is O3 (259.106542ms)
✔ N9: a run that started before the fence and ends aborted: failed interrupted, Interrupted (187.803775ms)
✔ N9: decision 34: a run that ends aborted with no stop in progress: aborted-without-stop, uncertain, outcome unknown (240.606876ms)
✔ N9: an aborted that lands after the fence but before any abort is written: aborted-without-stop, Unknown (364.999405ms)
✔ N10: fake conformance (33.333721ms)
✔ N11: the run fails before any user message_start: delivery-unknown ack-without-start, never failed (422.040983ms)
✔ N12: input that starts a run after the final empty clear is O1 and not part of the stop's proof (268.737656ms)
✔ N13: agent_start with no slot held is O1; a later prompt refuses with zero engine bytes (279.646287ms)
✔ N14: the run completes while clear_queue is in flight: finished, Completed first, uncertain (362.146811ms)
✔ N14: the run completes after the abort is written, before Pi applies it: finished, never relabelled (496.549872ms)
✔ N15: the fence lands in preflight, then an input handler takes it: handled-without-run, No run (363.792931ms)
✔ N16: Interrupt with no slot and no run refuses no-turn: no stop, no bytes, admission open (214.329359ms)
✔ N17: the run fails on its own during the exchange: failed, Failed on its own (373.398495ms)
✔ N18: no final assistant message_end, or a lost line: working stays working; before working, transport-unknown (800.518546ms)
✔ N19: a losing extension prompt settles inside the Mosaic run before its user message: O3, run-overlap (623.374789ms)
✔ N20: an extension triggerTurn during Mosaic preflight starts first; while streaming it queues with no signal (588.079734ms)
✔ N21: a losing settle after the receipt settled finished is O2; the receipt stays finished (243.209329ms)
✔ N22: an agent-level custom message is dropped by the clear with no signal; evidence names the seal (245.190658ms)
✔ N23: a nextTurn message survives clear and abort and attaches to the next prompt, with no signal (236.863088ms)
✔ N24: the seal is an allow-list: --extension, a missing --no-* flag, a second --mode or --session, a session or output flag, or a stray word refuses unsealed-engine; no engine starts (260.673316ms)
ℹ tests 152
ℹ suites 0
ℹ pass 152
@@ -157,4 +157,4 @@
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 52102.666518
ℹ duration_ms 50646.61675
+179 -179
View File
@@ -1,181 +1,181 @@
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.513067ms)
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.622039ms)
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.657851ms)
✔ approvals: a button approves only on its own request message with the matching custom id (0.506829ms)
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (20.372481ms)
✔ 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.942082ms)
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (5.686265ms)
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.451802ms)
✔ authorize: open channel, listed user (2.089988ms)
✔ authorize: wrong guild (0.204754ms)
✔ authorize: no guild (DM) (0.17918ms)
✔ authorize: unlisted channel (2.484597ms)
✔ authorize: unknown channel, no info (0.252987ms)
✔ authorize: thread of listed parent (0.213716ms)
✔ authorize: thread of unlisted parent (0.155577ms)
✔ authorize: text channel that is not a thread and not listed (0.331012ms)
✔ authorize: unlisted user (0.195206ms)
✔ authorize: no author (0.352216ms)
✔ authorize: bot author (listed id, bot flag) (0.171847ms)
✔ authorize: system author (0.11868ms)
✔ authorize: the bot itself (0.100113ms)
✔ authorize: webhook (0.091549ms)
✔ authorize: mention channel without mention (0.139131ms)
✔ authorize: mention channel with bot mention (0.269928ms)
✔ authorize: mention channel with @everyone only (0.110887ms)
✔ authorize: mention channel mentioning someone else (0.074523ms)
✔ authorize: mention channel, content says @bot but mentions empty (0.089763ms)
✔ authorize: private thread under mention channel, mentioned (0.092374ms)
✔ authorize: private thread under mention channel, not mentioned (0.064862ms)
✔ authorize: thread in another guild per channel info (0.076909ms)
✔ authorize: not an object (0.063716ms)
✔ authorize: no id (0.066689ms)
✔ authorize: oversize content is accepted and flagged (0.077499ms)
✔ authorize: exactly the limit is not oversize (0.058051ms)
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.495786ms)
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (1.552985ms)
✔ binding: a complete binding validates and is frozen (2.984441ms)
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.82498ms)
✔ binding: empty allowlists refuse (0.367254ms)
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.571886ms)
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (2.058133ms)
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (0.492391ms)
✔ binding: file must be 0600, regular, not a symlink (3.49713ms)
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (2.149593ms)
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (110.567998ms)
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.40013ms)
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (349.708865ms)
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (216.498377ms)
✔ cli: run refuses when STOP is present, before any network use (137.183231ms)
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (2.049555ms)
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.221218ms)
✔ 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 (17.53611ms)
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (17.196582ms)
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.487369ms)
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.174302ms)
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.223402ms)
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.155626ms)
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (31.706556ms)
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (2.981659ms)
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (0.826213ms)
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (0.655156ms)
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (41.645133ms)
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.315913ms)
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (5.881895ms)
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (3.712336ms)
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (1.233407ms)
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (3.998497ms)
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.241541ms)
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.496718ms)
✔ 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 (5.570856ms)
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.101271ms)
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.133321ms)
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (2.142423ms)
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.321246ms)
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.391083ms)
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.782881ms)
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.023176ms)
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.253659ms)
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.584064ms)
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.262211ms)
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.94795ms)
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (4.050497ms)
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.467297ms)
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (65.861693ms)
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (42.322062ms)
✔ engine: one prompt, one turn, text and usage come back (35.874086ms)
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (350.609136ms)
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (240.28076ms)
✔ engine: timeout sends abort and fails only that turn; the process stays (106.320108ms)
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (232.398024ms)
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (125.558786ms)
✔ 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.642182ms)
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.22918ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (2.158478ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.938839ms)
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (431.707736ms)
✔ engine: a malformed JSONL line fails the turn, not the process (27.92475ms)
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (46.568294ms)
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (3.962931ms)
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.820945ms)
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.505877ms)
✔ gateway: op 9 resumable resumes (0.388086ms)
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.880231ms)
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.577018ms)
✔ gateway: close() is final and unparseable frames are ignored (0.433901ms)
✔ git: config validation is strict, needs write: true, a work tree and a private token file (79.813399ms)
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (47.008635ms)
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (75.188574ms)
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.289055ms)
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (101.080883ms)
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (105.441377ms)
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (94.002264ms)
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (234.268712ms)
✔ git: push pushes the named branch only and reports up to date (74.775987ms)
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (98.386955ms)
✔ git: the credential helper answers get over https from a private file and nothing else (211.796826ms)
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (832.162327ms)
✔ lock: the claim is exclusive; a second start against a live owner refuses (4.805541ms)
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (91.35218ms)
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (1.026631ms)
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (5.010642ms)
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (39.081716ms)
✔ 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 (335.097304ms)
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.478148ms)
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (26.306592ms)
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.148782ms)
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (44.742055ms)
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (151.909677ms)
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (91.508358ms)
✔ notices: a kind is recorded per UTC day and found again (0.663793ms)
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.350295ms)
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.184953ms)
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.79695ms)
✔ 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.744741ms)
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (48.327362ms)
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (35.166632ms)
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (77.466581ms)
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (736.615945ms)
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (4.774151ms)
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.44002ms)
✔ rest: content and nonce limits are enforced locally; typing never throws (1.936608ms)
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (1.290834ms)
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (0.845933ms)
✔ setspark config: a bare https or loopback origin, a private key file, a principal (4.723533ms)
✔ setspark config: reaches the tools config and the binding as a fixed key (3.019049ms)
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.727043ms)
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.039368ms)
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (23.379469ms)
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (11.751887ms)
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (6.042864ms)
✔ setspark keys: read per call, one printable token per file, rotation without a restart (2.676422ms)
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.46837ms)
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (2.002481ms)
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1011.153004ms)
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.47554ms)
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (7.378494ms)
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.8349ms)
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.306603ms)
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.854819ms)
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (4.013354ms)
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.014583ms)
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.436845ms)
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.615438ms)
✔ tools: listing and search caps hold (9.65926ms)
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.903078ms)
✔ 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.27101ms)
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.332638ms)
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.474367ms)
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (5.680721ms)
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (3.018418ms)
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.491755ms)
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.852318ms)
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.335601ms)
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1027.404183ms)
✔ 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.231804ms)
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.2477ms)
✔ 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.610441ms)
✔ 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.220307ms)
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.061117ms)
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (1.767093ms)
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.615603ms)
✔ approvals: a button approves only on its own request message with the matching custom id (0.478036ms)
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (16.487303ms)
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (6.236326ms)
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.83515ms)
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.795146ms)
✔ authorize: open channel, listed user (4.438211ms)
✔ authorize: wrong guild (0.234954ms)
✔ authorize: no guild (DM) (0.297955ms)
✔ authorize: unlisted channel (0.180274ms)
✔ authorize: unknown channel, no info (0.281968ms)
✔ authorize: thread of listed parent (0.188408ms)
✔ authorize: thread of unlisted parent (0.277177ms)
✔ authorize: text channel that is not a thread and not listed (0.135711ms)
✔ authorize: unlisted user (1.317345ms)
✔ authorize: no author (0.494804ms)
✔ authorize: bot author (listed id, bot flag) (0.229594ms)
✔ authorize: system author (0.324529ms)
✔ authorize: the bot itself (0.080666ms)
✔ authorize: webhook (0.567864ms)
✔ authorize: mention channel without mention (0.132158ms)
✔ authorize: mention channel with bot mention (0.119555ms)
✔ authorize: mention channel with @everyone only (0.264226ms)
✔ authorize: mention channel mentioning someone else (0.073763ms)
✔ authorize: mention channel, content says @bot but mentions empty (0.07773ms)
✔ authorize: private thread under mention channel, mentioned (0.077728ms)
✔ authorize: private thread under mention channel, not mentioned (0.063982ms)
✔ authorize: thread in another guild per channel info (0.068838ms)
✔ authorize: not an object (0.064676ms)
✔ authorize: no id (0.050024ms)
✔ authorize: oversize content is accepted and flagged (0.060955ms)
✔ authorize: exactly the limit is not oversize (0.038377ms)
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.433661ms)
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.211684ms)
✔ binding: a complete binding validates and is frozen (2.713898ms)
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (3.959445ms)
✔ binding: empty allowlists refuse (0.42288ms)
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.510414ms)
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.56028ms)
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.942119ms)
✔ binding: file must be 0600, regular, not a symlink (1.904793ms)
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (1.618572ms)
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (95.112607ms)
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (2.882373ms)
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (324.375472ms)
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (215.962601ms)
✔ cli: run refuses when STOP is present, before any network use (130.511988ms)
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.732355ms)
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.188652ms)
✔ 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.059867ms)
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (16.533069ms)
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.394054ms)
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.059545ms)
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.278418ms)
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (2.785008ms)
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (32.205924ms)
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (2.018175ms)
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.07793ms)
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (0.911887ms)
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (42.432734ms)
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (32.443071ms)
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (5.276205ms)
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (3.124916ms)
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.308167ms)
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (4.251125ms)
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (1.25404ms)
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.496794ms)
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (2.467632ms)
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (2.027341ms)
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (4.119314ms)
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (4.968528ms)
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.093585ms)
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.581971ms)
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (0.869654ms)
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.108748ms)
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.341709ms)
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.476551ms)
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.314316ms)
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.832442ms)
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.866483ms)
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.41256ms)
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (56.029123ms)
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (38.421229ms)
✔ engine: one prompt, one turn, text and usage come back (32.150553ms)
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (332.641169ms)
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (235.751447ms)
✔ engine: timeout sends abort and fails only that turn; the process stays (105.139637ms)
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (228.086886ms)
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (135.450867ms)
✔ 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.23065ms)
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.382317ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.267878ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.480882ms)
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (425.419954ms)
✔ engine: a malformed JSONL line fails the turn, not the process (23.979553ms)
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (47.337302ms)
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (2.488183ms)
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.739254ms)
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.751079ms)
✔ gateway: op 9 resumable resumes (0.355449ms)
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.814585ms)
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.53415ms)
✔ gateway: close() is final and unparseable frames are ignored (0.397126ms)
✔ git: config validation is strict, needs write: true, a work tree and a private token file (68.353129ms)
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (47.851387ms)
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (94.180702ms)
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.281882ms)
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (87.170171ms)
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (97.112654ms)
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (94.925433ms)
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (210.685657ms)
✔ git: push pushes the named branch only and reports up to date (75.691059ms)
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (87.339624ms)
✔ git: the credential helper answers get over https from a private file and nothing else (197.323202ms)
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (751.124043ms)
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.01841ms)
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (79.953023ms)
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (0.800902ms)
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (2.668839ms)
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (39.552971ms)
✔ 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 (301.652949ms)
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.599138ms)
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (28.147864ms)
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (1.525339ms)
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (48.503637ms)
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (146.638909ms)
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (96.227093ms)
✔ notices: a kind is recorded per UTC day and found again (0.413961ms)
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.040159ms)
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.517272ms)
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.894822ms)
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (52.534669ms)
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (32.739742ms)
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (32.258484ms)
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (74.709691ms)
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (674.149506ms)
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (8.893063ms)
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.289935ms)
✔ rest: content and nonce limits are enforced locally; typing never throws (0.578211ms)
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (0.766243ms)
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.383503ms)
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.518356ms)
✔ setspark config: reaches the tools config and the binding as a fixed key (3.994465ms)
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.523408ms)
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (1.479859ms)
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (18.113285ms)
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (8.297505ms)
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (6.297547ms)
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.103093ms)
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.455146ms)
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (2.041441ms)
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1009.053659ms)
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.981369ms)
✔ 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.59881ms)
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.329166ms)
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.217806ms)
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (2.626764ms)
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (3.419099ms)
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (3.60831ms)
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.026763ms)
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.707762ms)
✔ tools: listing and search caps hold (10.282751ms)
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.943132ms)
✔ 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 (4.419338ms)
✔ tools: an unreadable file under the root is skipped by search and refused by read (0.868225ms)
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (0.995424ms)
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (4.213116ms)
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.597178ms)
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.282156ms)
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (2.530552ms)
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.209727ms)
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1022.074646ms)
✔ 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.468695ms)
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.248987ms)
✔ 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.702411ms)
✔ 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.555878ms)
ℹ tests 178
ℹ suites 0
ℹ pass 178
@@ -183,4 +183,4 @@
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2687.487226
ℹ duration_ms 2645.723969
+56 -53
View File
@@ -1,58 +1,61 @@
✔ sessionModel: agent vars win, then the system's execution settings (13.13889ms)
✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.754143ms)
✔ a claude-code bundle adds the wrapped gate hook and the MCP config (2.793955ms)
✔ a bundle is written once: an existing file refuses (2.706625ms)
✔ a path with a single quote can't go into the hook command (1.81122ms)
✔ allow exits 0, a deny exits 2 with the reason on stderr (124.816491ms)
✔ a missing or wrong policy, or a bad event, exits 2 (95.807838ms)
✔ the bundle's wrapped command: a missing gate or node still blocks (1096.785958ms)
✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (851.292573ms)
✔ claude: the hook alone blocks a path outside the workspace (519.27064ms)
✔ claude: a second turn resumes the first turn's session (776.321586ms)
✔ claude adapter: --restricted is always passed (5.499034ms)
✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (867.702037ms)
✔ claude: a missing hook or MCP file refuses before claude starts (10.298703ms)
✔ pi: policy tools and typed tools pass, anything else is blocked (4.220059ms)
✔ claude: builtins map from pi names, typed tools need the mcp prefix (1.062326ms)
✔ file tool paths must resolve inside the workspace (1.522023ms)
✔ pi's own path normalisation can't be used to step out (1.483331ms)
✔ a symlink inside the workspace that points out is outside (1.269628ms)
✔ a dangling symlink is refused at any depth, in both harnesses (4.263112ms)
✔ claude path fields per tool (1.118926ms)
✔ glob patterns stay inside the workspace (1.022964ms)
✔ a path that can't be checked is blocked (0.454162ms)
✔ initialize, ping and tools/list (48.504759ms)
✔ tools/call goes through the tool socket; a refusal is an isError result (38.124036ms)
✔ unknown tools and methods are JSON-RPC errors and never reach the socket (33.297546ms)
✔ a missing argument is a usage error (30.763266ms)
✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (405.108517ms)
✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (381.475986ms)
✔ pi: a missing extension refuses before any model call (7.159081ms)
✔ pi: an extension without its configuration fails pi's start (340.423518ms)
✔ founderCheck: founder variables, then a needed service without a usable token (1.358533ms)
✔ turnRequest names the sender, class, reply and decision (0.199896ms)
✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (249.9341ms)
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (116.889992ms)
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (423.548366ms)
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1349.631237ms)
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (177.09152ms)
✔ founder credentials stop before the claim (20) (194.175258ms)
✔ a refused claim exits 21; an ended run's capability exits 22 (236.143025ms)
✔ the launch ending under a running session exits 22 (118.270606ms)
✔ a broker that stays unreachable exits 23 after brokerRetries polls (250.227078ms)
✔ a broker that is down at the claim exits 23, not 21 (93.191317ms)
✔ no capability, or a malformed one, on stdin exits 2 (189.733487ms)
✔ a missing or malformed policy exits 2 before the claim (128.184915ms)
✔ the PM gets launch, its task verbs and the reads (6.94313ms)
✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.622772ms)
✔ launch only when the business's launch block names the instance as launcher (2.472817ms)
✔ an action outside the instance's authority has no tool (2.525087ms)
✔ callTool: one JSON line out, the result back, a refusal rejects (10.34312ms)
ℹ tests 50
✔ sessionModel: agent vars win, then the system's execution settings (9.866803ms)
✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.436208ms)
✔ a claude-code bundle adds the wrapped gate hook and the MCP config (2.729439ms)
✔ a bundle is written once: an existing file refuses (1.77934ms)
✔ a path with a single quote can't go into the hook command (1.80468ms)
✔ allow exits 0, a deny exits 2 with the reason on stderr (126.770406ms)
✔ a missing or wrong policy, or a bad event, exits 2 (76.681201ms)
✔ the bundle's wrapped command: a missing gate or node still blocks (1093.817326ms)
✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (754.278903ms)
✔ claude: the hook alone blocks a path outside the workspace (481.812812ms)
✔ claude: a second turn resumes the first turn's session (713.762483ms)
✔ claude adapter: --restricted is always passed (4.950058ms)
✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (717.308696ms)
✔ claude: a missing hook or MCP file refuses before claude starts (7.466398ms)
✔ pi: policy tools and typed tools pass, anything else is blocked (3.399466ms)
✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.784748ms)
✔ file tool paths must resolve inside the workspace (0.998403ms)
✔ pi's own path normalisation can't be used to step out (0.906289ms)
✔ a symlink inside the workspace that points out is outside (0.787366ms)
✔ a dangling symlink is refused at any depth, in both harnesses (2.581633ms)
✔ read is checked under every spelling pi's read would open, in both harnesses (15.173931ms)
✔ other spellings cover directories, dangling links and pi's cwd (1.755587ms)
✔ claude path fields per tool (0.614355ms)
✔ glob patterns stay inside the workspace (0.669965ms)
✔ a path that can't be checked is blocked (0.330426ms)
✔ initialize, ping and tools/list (43.946107ms)
✔ tools/call goes through the tool socket; a refusal is an isError result (39.418977ms)
✔ unknown tools and methods are JSON-RPC errors and never reach the socket (28.163039ms)
✔ a missing argument is a usage error (33.807275ms)
✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (357.982252ms)
✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (329.534992ms)
✔ pi: a read is refused when pi would open another spelling outside (330.331356ms)
✔ pi: a missing extension refuses before any model call (7.207519ms)
✔ pi: an extension without its configuration fails pi's start (262.623748ms)
✔ founderCheck: founder variables, then a needed service without a usable token (1.361445ms)
✔ turnRequest names the sender, class, reply and decision (0.286463ms)
✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (280.080623ms)
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (131.852678ms)
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (555.206373ms)
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1350.970037ms)
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (163.705924ms)
✔ founder credentials stop before the claim (20) (181.182154ms)
✔ a refused claim exits 21; an ended run's capability exits 22 (216.20297ms)
✔ the launch ending under a running session exits 22 (150.079251ms)
✔ a broker that stays unreachable exits 23 after brokerRetries polls (246.177626ms)
✔ a broker that is down at the claim exits 23, not 21 (143.792117ms)
✔ no capability, or a malformed one, on stdin exits 2 (260.79667ms)
✔ a missing or malformed policy exits 2 before the claim (260.249644ms)
✔ the PM gets launch, its task verbs and the reads (9.127299ms)
✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (3.041734ms)
✔ launch only when the business's launch block names the instance as launcher (1.525074ms)
✔ an action outside the instance's authority has no tool (1.464781ms)
✔ callTool: one JSON line out, the result back, a refusal rejects (9.215168ms)
ℹ tests 53
ℹ suites 0
ℹ pass 50
ℹ pass 53
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 10459.373798
ℹ duration_ms 10592.472255
+79 -79
View File
@@ -1,81 +1,81 @@
✔ a raw token file, with or without one trailing newline, reaches curl only through the config stream (115.944145ms)
✔ the raw path accepts nothing else, and refuses before curl runs (435.681779ms)
✔ the file checks still apply on the raw path: mode, symlink, missing, directory (292.596759ms)
✔ the raw path base URL has no override (57.417954ms)
✔ the JSON path is unchanged, and JSON never falls through to the raw path (241.09635ms)
✔ a file that changes between the two reads refuses before curl runs, with or without a body (716.736353ms)
✔ the token reaches no child environment, even with an inherited CFG or SHELLOPTS=allexport (785.126238ms)
✔ real helper GET HTTP 200 preserves exit 0 without credentials (13.102534ms)
✔ real helper POST HTTP 201 preserves exit 0 without credentials (9.478963ms)
✔ real helper GET HTTP 403 preserves exit 1 without credentials (8.609799ms)
✔ fixture git subjects only, follow-ups and three session kinds (161.732019ms)
✔ text and JSON carry same numbers, open and truncated title (283.864137ms)
✔ missing credentials exit 2, no-issues never calls API and shows unknown (203.728119ms)
✔ empty range gives no rows and zero totals (173.519667ms)
✔ inclusive UTC dates, first-line preamble only, role and seat boundaries (160.627786ms)
✔ close-only issue included, even median, missing metadata stays unknown (146.587403ms)
✔ unique commits but per-issue links count multiple tags once each (149.63893ms)
✔ page cap refuses rather than silently undercounting (155.481838ms)
✔ bad API payload not JSON refuses (133.857006ms)
✔ bad API payload {} refuses (135.827145ms)
✔ bad API payload [{"number":1}] refuses (138.744231ms)
✔ partial or malformed session log refuses with location, not content (160.62317ms)
✔ a U+2028 or U+2029 inside a session string is one line, not a malformed record (151.198757ms)
✔ no sessions is an empty table; symlink source refuses (242.064585ms)
✔ reads only refactor even when another branch is checked out (174.678374ms)
✔ invalid dates, reverse dates and duplicate options refuse (115.749728ms)
✔ T3 agent assignments do not count as human in Table 2 (151.662023ms)
✔ preamble parsing and issue number boundaries (0.467437ms)
✔ T3 header: agent, or board from control-board; anything short of the full header is human (0.152354ms)
✔ no closed issues with human messages means undefined ratio, not invented zero (0.198078ms)
✔ T3: seat, archived, unmapped and Researcher threads count; imported, deleted and other-project threads do not (557.126543ms)
✔ T3: the default path is read from HOME and prints no path line; --no-t3 says so (718.686387ms)
✔ T3: a HOME with no database exits 1 and names --no-t3 (163.535756ms)
✔ T3: a file that is not a database exits 1 and names --no-t3 (142.692882ms)
✔ T3: a seat thread renamed to another seat exits 1 naming thread, title and roles (418.551026ms)
✔ T3: an unmapped thread addressed as a seat exits 1 (460.458843ms)
✔ T3: a header to another thread id is not cross-checked (446.050289ms)
✔ T3: no project, or two, for this root exits 1 (1228.151017ms)
✔ T3: a removed column exits 1 and names it (263.155213ms)
✔ T3: a missing table exits 1 and names it (250.866899ms)
✔ T3: a counted row with an unknown role exits 1 without its text (378.791672ms)
✔ T3: a counted row with non-text content exits 1 without its text (487.567915ms)
✔ T3: a counted row with an unparseable created_at exits 1 without its text (402.431871ms)
✔ T3: a missing orchestration_events makes the diagnostic unknown and keeps the counts (698.410145ms)
✔ T3: a human message with no event counts in humanWithoutEvent (434.282761ms)
✔ T3: an unparseable event makes the diagnostic unknown and keeps the counts (725.856377ms)
✔ T3: an event with no string messageId makes the diagnostic unknown and keeps the counts (709.744542ms)
✔ T3: an error that is not from SQLite is rethrown, not reported as a database failure (349.43309ms)
✔ T3: a symlink at ~/.t3 exits 1 (148.50403ms)
✔ T3: a symlink at ~/.t3/userdata exits 1 (172.909616ms)
✔ T3: a symlink at ~/.t3/userdata/state.sqlite exits 1 (178.900103ms)
✔ T3: with --t3-db, a symlinked file or directory exits 1 (586.925199ms)
✔ T3 WAL: the newest message only in -wal, writer attached, is counted (473.441354ms)
✔ T3 WAL: stopped cleanly, counts are correct and the main file is unchanged (481.222531ms)
✔ T3 WAL: -wal without -shm in a writable directory is read (522.716681ms)
✔ T3 WAL: -wal without -shm in a read-only directory exits 1 (489.764562ms)
✔ T3 WAL: stopped cleanly in a read-only directory exits 1 (389.568508ms)
✔ T3: a lock held past the 5 s busy timeout exits 1 and names --no-t3 (5415.405848ms)
✔ a done row whose closing issue is open is a violation; a row that is not done is not (1.763922ms)
✔ an issue several rows close is expected closed only once all of them are done (0.477384ms)
✔ closure needs positive evidence: unknown is undecided, and so is a skipped or short issue check (0.353983ms)
✔ each owner of an in-progress or in-review row gets one liveness class (7.516547ms)
✔ a required row not done after 14 days is a violation; a legacy row uses genesis as its lower bound (0.386933ms)
✔ an ISO requiredSince, as `set required` writes it, ages from its UTC day; one that does not parse is a violation (0.229823ms)
✔ the text section always ends in a count and a result, and never prints a full pass (0.335678ms)
✔ pidAlive: a running pid is present, an exited one is gone, and EPERM still means present (22.228761ms)
✔ issue states: open list first, then the metric page, then at most 10 lookups (314.974468ms)
✔ a full open list: lookups settle what it leaves out, and only an unsettled issue keeps it undecided (413.703487ms)
✔ the open list refuses on a failed call or a bad record, and never echoes the helper (171.870124ms)
✔ a helper call past the deadline is killed with its child, and the call reports it (2008.175883ms)
✔ readQueue loads queue.json through the queue validator and refuses anything else (153.594153ms)
✔ protected changes list every in-range entry that changes a required or parked row (407.661757ms)
✔ the CLI prints the queue section above the weekly table and under a queue key in --json (555.885026ms)
✔ a queue with nothing wrong prints 0 violations and a reduced pass, never a full pass (261.24945ms)
✔ --no-issues makes no call and leaves the issue checks undecided; --no-queue skips the section (308.534593ms)
✔ the CLI refuses a bad queue before any call, and a failed open list with exit 2 (294.675823ms)
✔ --unsupported-runtime repeats once per seat and takes a seat name (395.674304ms)
✔ an unreadable config makes every owner invalid instead of passing them (195.182174ms)
✔ a raw token file, with or without one trailing newline, reaches curl only through the config stream (114.71017ms)
✔ the raw path accepts nothing else, and refuses before curl runs (363.007926ms)
✔ the file checks still apply on the raw path: mode, symlink, missing, directory (304.628111ms)
✔ the raw path base URL has no override (64.735925ms)
✔ the JSON path is unchanged, and JSON never falls through to the raw path (228.133678ms)
✔ a file that changes between the two reads refuses before curl runs, with or without a body (715.743095ms)
✔ the token reaches no child environment, even with an inherited CFG or SHELLOPTS=allexport (689.540339ms)
✔ real helper GET HTTP 200 preserves exit 0 without credentials (14.872591ms)
✔ real helper POST HTTP 201 preserves exit 0 without credentials (10.438959ms)
✔ real helper GET HTTP 403 preserves exit 1 without credentials (9.544019ms)
✔ fixture git subjects only, follow-ups and three session kinds (241.298844ms)
✔ text and JSON carry same numbers, open and truncated title (308.52844ms)
✔ missing credentials exit 2, no-issues never calls API and shows unknown (280.299865ms)
✔ empty range gives no rows and zero totals (213.054503ms)
✔ inclusive UTC dates, first-line preamble only, role and seat boundaries (215.603367ms)
✔ close-only issue included, even median, missing metadata stays unknown (197.614777ms)
✔ unique commits but per-issue links count multiple tags once each (215.527408ms)
✔ page cap refuses rather than silently undercounting (201.211581ms)
✔ bad API payload not JSON refuses (140.742625ms)
✔ bad API payload {} refuses (196.87563ms)
✔ bad API payload [{"number":1}] refuses (182.932775ms)
✔ partial or malformed session log refuses with location, not content (186.997309ms)
✔ a U+2028 or U+2029 inside a session string is one line, not a malformed record (213.14167ms)
✔ no sessions is an empty table; symlink source refuses (291.100853ms)
✔ reads only refactor even when another branch is checked out (226.626428ms)
✔ invalid dates, reverse dates and duplicate options refuse (176.688993ms)
✔ T3 agent assignments do not count as human in Table 2 (219.617626ms)
✔ preamble parsing and issue number boundaries (0.434196ms)
✔ T3 header: agent, or board from control-board; anything short of the full header is human (0.143448ms)
✔ no closed issues with human messages means undefined ratio, not invented zero (0.19361ms)
✔ T3: seat, archived, unmapped and Researcher threads count; imported, deleted and other-project threads do not (824.563293ms)
✔ T3: the default path is read from HOME and prints no path line; --no-t3 says so (1000.905234ms)
✔ T3: a HOME with no database exits 1 and names --no-t3 (203.609674ms)
✔ T3: a file that is not a database exits 1 and names --no-t3 (194.71429ms)
✔ T3: a seat thread renamed to another seat exits 1 naming thread, title and roles (518.142029ms)
✔ T3: an unmapped thread addressed as a seat exits 1 (569.946942ms)
✔ T3: a header to another thread id is not cross-checked (787.859966ms)
✔ T3: no project, or two, for this root exits 1 (1458.185096ms)
✔ T3: a removed column exits 1 and names it (374.658545ms)
✔ T3: a missing table exits 1 and names it (333.135189ms)
✔ T3: a counted row with an unknown role exits 1 without its text (518.1692ms)
✔ T3: a counted row with non-text content exits 1 without its text (470.069153ms)
✔ T3: a counted row with an unparseable created_at exits 1 without its text (594.355264ms)
✔ T3: a missing orchestration_events makes the diagnostic unknown and keeps the counts (1101.049509ms)
✔ T3: a human message with no event counts in humanWithoutEvent (559.271689ms)
✔ T3: an unparseable event makes the diagnostic unknown and keeps the counts (909.949273ms)
✔ T3: an event with no string messageId makes the diagnostic unknown and keeps the counts (971.023265ms)
✔ T3: an error that is not from SQLite is rethrown, not reported as a database failure (424.377883ms)
✔ T3: a symlink at ~/.t3 exits 1 (178.873006ms)
✔ T3: a symlink at ~/.t3/userdata exits 1 (194.873019ms)
✔ T3: a symlink at ~/.t3/userdata/state.sqlite exits 1 (138.790741ms)
✔ T3: with --t3-db, a symlinked file or directory exits 1 (598.260244ms)
✔ T3 WAL: the newest message only in -wal, writer attached, is counted (616.999343ms)
✔ T3 WAL: stopped cleanly, counts are correct and the main file is unchanged (747.300765ms)
✔ T3 WAL: -wal without -shm in a writable directory is read (669.972464ms)
✔ T3 WAL: -wal without -shm in a read-only directory exits 1 (859.988644ms)
✔ T3 WAL: stopped cleanly in a read-only directory exits 1 (780.564496ms)
✔ T3: a lock held past the 5 s busy timeout exits 1 and names --no-t3 (5802.477411ms)
✔ a done row whose closing issue is open is a violation; a row that is not done is not (1.814873ms)
✔ an issue several rows close is expected closed only once all of them are done (0.493759ms)
✔ closure needs positive evidence: unknown is undecided, and so is a skipped or short issue check (0.366812ms)
✔ each owner of an in-progress or in-review row gets one liveness class (7.558511ms)
✔ a required row not done after 14 days is a violation; a legacy row uses genesis as its lower bound (0.382616ms)
✔ an ISO requiredSince, as `set required` writes it, ages from its UTC day; one that does not parse is a violation (0.239616ms)
✔ the text section always ends in a count and a result, and never prints a full pass (0.322972ms)
✔ pidAlive: a running pid is present, an exited one is gone, and EPERM still means present (24.300433ms)
✔ issue states: open list first, then the metric page, then at most 10 lookups (299.016341ms)
✔ a full open list: lookups settle what it leaves out, and only an unsettled issue keeps it undecided (379.027884ms)
✔ the open list refuses on a failed call or a bad record, and never echoes the helper (187.181352ms)
✔ a helper call past the deadline is killed with its child, and the call reports it (2008.003463ms)
✔ readQueue loads queue.json through the queue validator and refuses anything else (242.189312ms)
✔ protected changes list every in-range entry that changes a required or parked row (525.618188ms)
✔ the CLI prints the queue section above the weekly table and under a queue key in --json (563.216329ms)
✔ a queue with nothing wrong prints 0 violations and a reduced pass, never a full pass (284.538284ms)
✔ --no-issues makes no call and leaves the issue checks undecided; --no-queue skips the section (343.338245ms)
✔ the CLI refuses a bad queue before any call, and a failed open list with exit 2 (308.846971ms)
✔ --unsupported-runtime repeats once per seat and takes a seat name (342.898875ms)
✔ an unreadable config makes every owner invalid instead of passing them (197.221174ms)
ℹ tests 78
ℹ suites 0
ℹ pass 78
@@ -83,4 +83,4 @@
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 20587.431008
ℹ duration_ms 26163.120468
+70 -70
View File
@@ -1,72 +1,72 @@
✔ pure resolution selects current default or explicit enrolled account (1.878586ms)
✔ scope is explicit, bounded and never inferred (1.581008ms)
✔ fork pin is preserved against default change, override, missing account and revocation (0.716452ms)
✔ unenrolled account/provider, missing harness, model expansion and native model ceiling refuse (0.776018ms)
✔ only explicit synthetic credential forms and internal fixture stores admitted (5.536742ms)
✔ two concurrent workspaces of the same agent publish distinct complete private generations (39.166081ms)
✔ same execution ID is exclusively claimed and cannot overwrite a published generation (32.875243ms)
✔ failed generation after-auth preserves prior files, records failure and refuses blind same-ID retry (45.958981ms)
✔ failed generation before-publish preserves prior files, records failure and refuses blind same-ID retry (56.130026ms)
✔ credential lock contention refuses without duplicate side effects (11.675666ms)
✔ symlinked pre-existing final target is refused and never followed (30.62088ms)
✔ invalid registry cannot resolve; no fallback to supplied partial entries (0.276916ms)
✔ post-publication failure records uncertainty, preserves complete generation and prevents replay (26.435512ms)
✔ expired credentials refresh under transaction and subsequent generation reuses rotation (84.153218ms)
✔ refresh failure retains prior generation and store state (64.937175ms)
✔ refresh timeout retains prior generation and store state (144.305597ms)
✔ refresh malformed retains prior generation and store state (65.747822ms)
✔ concurrent refresh on same account refuses contention while unrelated account proceeds (215.395134ms)
✔ invalid refresh options refuse before burning claim (37.333871ms)
✔ fixed fake process rotates both OAuth fields without mutating caller input (30.930125ms)
✔ concurrent isolated processes preserve separate provider credentials (31.4959ms)
✔ fake failure is refused with fixed diagnostics (24.940459ms)
✔ fake malformed is refused with fixed diagnostics (25.340709ms)
✔ fake timeout is refused with fixed diagnostics (104.107324ms)
✔ fake unchanged is refused with fixed diagnostics (23.534776ms)
✔ caller executable/environment injection is rejected before spawning (0.284482ms)
✔ valid fixture tree validates and lists without secrets (66.88632ms)
✔ unknown-field refuses (0.382128ms)
✔ invalid-id refuses uppercase and traversal shapes (0.275639ms)
✔ plain-http baseUrl requires allowInsecureTransport (0.296326ms)
✔ native provider rejects allowInsecureTransport (0.125085ms)
✔ unsupported credential type and kind refuse (0.153165ms)
✔ account provider-path mismatch refuses (0.100346ms)
✔ profile account refs must be provider/account shaped (0.214059ms)
✔ seat selection accepts fork pin field, validates account refs (0.645005ms)
✔ harness manifest id must equal executable (gate 1) (0.201294ms)
✔ CLI validate: duplicate provider id across files refuses (29.663789ms)
✔ CLI validate: missing referenced provider/account refuse (32.571833ms)
✔ CLI validate: broken JSON refuses without secret echo (30.087738ms)
✔ CLI usage errors exit 2 (50.630519ms)
✔ credential.json sibling presence does not break validation and is never read (67.345203ms)
✔ D1 missing, empty and structurally empty roots refuse, no list projection (178.078068ms)
✔ D1 required directory auth cannot be absent (55.124406ms)
✔ D1 required directory auth/providers cannot be absent (55.174051ms)
✔ D1 required directory auth/accounts cannot be absent (65.607705ms)
✔ D1 required directory auth/settings cannot be absent (68.194942ms)
✔ D1 required directory harnesses cannot be absent (68.430456ms)
✔ D1 root file and unreadable metadata refuse (123.016733ms)
✔ D2 no symlink traversal at auth/providers/openai-codex.json (61.956083ms)
✔ D2 no symlink traversal at auth/accounts/openai-codex/homelab-openai (70.11802ms)
✔ D2 no symlink traversal at auth/providers (75.574364ms)
✔ D2 no symlink traversal at auth (59.487548ms)
✔ D2 root and ancestor symlinks and lexical traversal refuse (170.024215ms)
✔ private filesystem modes enforced for root (63.592633ms)
✔ private filesystem modes enforced for auth (53.667674ms)
✔ private filesystem modes enforced for auth/providers/openai-codex.json (56.37671ms)
✔ private filesystem modes enforced for auth/accounts/openai-codex/homelab-openai/account.json (58.259061ms)
✔ D3 numeric version 1 only across all record kinds (1.367263ms)
✔ D4 nested unknown keys and missing per-kind required fields refuse (63.141006ms)
✔ D5 unenrolled default refuses even when account exists (63.538674ms)
✔ D6 provider/account credential type must match (65.440288ms)
✔ D7 every harness endpoint enforces HTTP opt-in and shape (0.464132ms)
✔ D8 URLs reject embedded credentials and unsupported protocols without echo (174.497296ms)
✔ D9 malformed JSON diagnostics contain no content excerpt (75.363136ms)
✔ D10 missing metadata is missing-path, not invalid-json (71.448136ms)
✔ D10 library returns no partial entries on any invalid record (77.188608ms)
✔ null/scalar/array metadata refuses without stack or echo (238.266046ms)
✔ credential sibling is never opened, even when an unreadable symlink (36.426891ms)
✔ oversized metadata refuses before parsing (56.665304ms)
✔ pure resolution selects current default or explicit enrolled account (2.054528ms)
✔ scope is explicit, bounded and never inferred (1.596542ms)
✔ fork pin is preserved against default change, override, missing account and revocation (0.79176ms)
✔ unenrolled account/provider, missing harness, model expansion and native model ceiling refuse (1.159723ms)
✔ only explicit synthetic credential forms and internal fixture stores admitted (6.518592ms)
✔ two concurrent workspaces of the same agent publish distinct complete private generations (68.351516ms)
✔ same execution ID is exclusively claimed and cannot overwrite a published generation (33.972978ms)
✔ failed generation after-auth preserves prior files, records failure and refuses blind same-ID retry (52.451468ms)
✔ failed generation before-publish preserves prior files, records failure and refuses blind same-ID retry (58.496772ms)
✔ credential lock contention refuses without duplicate side effects (18.327397ms)
✔ symlinked pre-existing final target is refused and never followed (44.660615ms)
✔ invalid registry cannot resolve; no fallback to supplied partial entries (0.259178ms)
✔ post-publication failure records uncertainty, preserves complete generation and prevents replay (35.197219ms)
✔ expired credentials refresh under transaction and subsequent generation reuses rotation (120.496699ms)
✔ refresh failure retains prior generation and store state (98.926492ms)
✔ refresh timeout retains prior generation and store state (173.954281ms)
✔ refresh malformed retains prior generation and store state (94.802167ms)
✔ concurrent refresh on same account refuses contention while unrelated account proceeds (216.827049ms)
✔ invalid refresh options refuse before burning claim (50.160412ms)
✔ fixed fake process rotates both OAuth fields without mutating caller input (31.053588ms)
✔ concurrent isolated processes preserve separate provider credentials (27.535508ms)
✔ fake failure is refused with fixed diagnostics (22.739666ms)
✔ fake malformed is refused with fixed diagnostics (27.208676ms)
✔ fake timeout is refused with fixed diagnostics (103.025381ms)
✔ fake unchanged is refused with fixed diagnostics (25.003792ms)
✔ caller executable/environment injection is rejected before spawning (0.310957ms)
✔ valid fixture tree validates and lists without secrets (72.979155ms)
✔ unknown-field refuses (0.36807ms)
✔ invalid-id refuses uppercase and traversal shapes (0.267451ms)
✔ plain-http baseUrl requires allowInsecureTransport (0.341539ms)
✔ native provider rejects allowInsecureTransport (0.128697ms)
✔ unsupported credential type and kind refuse (0.153047ms)
✔ account provider-path mismatch refuses (0.108869ms)
✔ profile account refs must be provider/account shaped (0.223655ms)
✔ seat selection accepts fork pin field, validates account refs (0.67099ms)
✔ harness manifest id must equal executable (gate 1) (0.208191ms)
✔ CLI validate: duplicate provider id across files refuses (30.331947ms)
✔ CLI validate: missing referenced provider/account refuse (35.752038ms)
✔ CLI validate: broken JSON refuses without secret echo (26.482716ms)
✔ CLI usage errors exit 2 (52.783092ms)
✔ credential.json sibling presence does not break validation and is never read (67.16895ms)
✔ D1 missing, empty and structurally empty roots refuse, no list projection (177.535483ms)
✔ D1 required directory auth cannot be absent (53.462767ms)
✔ D1 required directory auth/providers cannot be absent (58.208941ms)
✔ D1 required directory auth/accounts cannot be absent (60.123059ms)
✔ D1 required directory auth/settings cannot be absent (64.777835ms)
✔ D1 required directory harnesses cannot be absent (63.810214ms)
✔ D1 root file and unreadable metadata refuse (117.813395ms)
✔ D2 no symlink traversal at auth/providers/openai-codex.json (54.008571ms)
✔ D2 no symlink traversal at auth/accounts/openai-codex/homelab-openai (65.397095ms)
✔ D2 no symlink traversal at auth/providers (54.760645ms)
✔ D2 no symlink traversal at auth (56.803918ms)
✔ D2 root and ancestor symlinks and lexical traversal refuse (165.243458ms)
✔ private filesystem modes enforced for root (57.733162ms)
✔ private filesystem modes enforced for auth (53.800829ms)
✔ private filesystem modes enforced for auth/providers/openai-codex.json (64.945974ms)
✔ private filesystem modes enforced for auth/accounts/openai-codex/homelab-openai/account.json (63.277721ms)
✔ D3 numeric version 1 only across all record kinds (1.246662ms)
✔ D4 nested unknown keys and missing per-kind required fields refuse (64.557054ms)
✔ D5 unenrolled default refuses even when account exists (65.244555ms)
✔ D6 provider/account credential type must match (69.207623ms)
✔ D7 every harness endpoint enforces HTTP opt-in and shape (0.51095ms)
✔ D8 URLs reject embedded credentials and unsupported protocols without echo (166.65518ms)
✔ D9 malformed JSON diagnostics contain no content excerpt (62.800794ms)
✔ D10 missing metadata is missing-path, not invalid-json (56.760167ms)
✔ D10 library returns no partial entries on any invalid record (74.177067ms)
✔ null/scalar/array metadata refuses without stack or echo (229.571906ms)
✔ credential sibling is never opened, even when an unreadable symlink (32.269336ms)
✔ oversized metadata refuses before parsing (54.703633ms)
ℹ tests 69
ℹ suites 0
ℹ pass 69
@@ -74,4 +74,4 @@
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2258.430424
ℹ duration_ms 2162.523966
+149 -149
View File
@@ -1,153 +1,153 @@
✔ F1: an ordinary commit after update-ref is refused until step 8; then it commits and the queue stays at C (1324.965524ms)
✔ F1: a plain `commit -e` whose guard ran before update-ref fails at its own HEAD update (1069.545926ms)
✔ F1: an ordinary commit after update-ref is refused until step 8; then it commits and the queue stays at C (1260.532537ms)
✔ F1: a plain `commit -e` whose guard ran before update-ref fails at its own HEAD update (1076.47454ms)
ℹ git commit -e: index.lock free during the editor
✔ F1: a `commit -e -- path` whose guard ran before update-ref fails at its own HEAD update (1091.836977ms)
✔ F1: a `commit -e -- path` whose guard ran before update-ref fails at its own HEAD update (1085.134721ms)
ℹ git commit -e -- src.txt: index.lock held during the editor
✔ F1: step 8 with index.lock held exits 3, and ordinary commits stay refused until the printed command runs (1178.432914ms)
✔ F1: HEAD moving after the step-7 guard check and before update-ref: refused, nothing published (1226.51663ms)
✔ F1: H is recorded before the canary, so HEAD moving during the step-1 canary is refused at step 7 (1214.38844ms)
✔ F1: a queue commit landing after H is recorded: step 1 says HEAD moved, not the guard (1436.845813ms)
✔ F1: a queue commit landing between the HEAD check and the canary: the failed clean run is reported as HEAD moved (995.087339ms)
✔ F1: a shared-index change during the procedure is not committed (1152.559542ms)
✔ F1: a queue path staged after update-ref: step 8 stops and touches nothing (1066.061784ms)
✔ F1: a missing or a different hook refuses (752.89826ms)
✔ F1: same bytes without the exec bit, a symlinked hook, and core.hooksPath in the local or global scope each refuse before update-ref (1254.926539ms)
✔ F1: the canary refuses a hook that git would not run (711.558543ms)
✔ F1: the guard deactivated after step 1 is refused at the step-7 recheck (996.255958ms)
✔ bootstrap: implementation-only HEAD, the guard, genesis, the --genesis commit, then an extending commit (940.929157ms)
✔ bootstrap: --genesis with a base present, no base without --genesis, an op before the first commit, a changed map, another branch (1127.235024ms)
✔ bootstrap: the archived tests and validator run outside any repository (933.119951ms)
✔ general: an unrelated staged file stays staged, and the committed blobs are the snapshot bytes (892.477365ms)
✔ general: a queue write after the snapshot is not committed (1503.673101ms)
✔ general: a snapshot whose log does not extend the base refuses (1079.409414ms)
✔ general: install-hook privilege, repair of a same-bytes hook, and its refusals (159.738017ms)
✔ general: environment overrides, a linked worktree and usage (629.320722ms)
✔ general: a queue path staged before the run refuses at step 1 (672.626209ms)
✔ general: HEAD's queue tests failing in the archive refuse (856.104785ms)
✔ genesis document serializes deterministically and replays (6.025791ms)
✔ a hand edit that stays valid JSON fails replay; a formatting-only edit fails re-serialization (3.307413ms)
✔ a tampered result, receipt or viewSha fails replay (3.307094ms)
✔ op ids: 8 to 72 characters for callers, 80 in the log for .outcome entries (0.180195ms)
✔ add: defaults for an ordinary seat, privileged extras, refusals (4.430774ms)
✔ matrix: queued→briefed privileged; briefed→in-progress owner with after satisfied (5.558526ms)
✔ matrix: release, review round, changes requested and waiting-on-jason (12.150619ms)
✔ matrix J5: in-review→done by the gate owner with evidence naming the current round (8.739873ms)
✔ review issue, lead decision 23: none refuses, one is used, several need --issue, later rounds keep it (19.356517ms)
✔ the row schema refuses a round with a null issue, and the A1 review shape (P2) (2.343705ms)
✔ matrix R1: every state × target × actor class matches 8.7, gate owner jason or not, required or not (1434.478155ms)
✔ matrix: blocked keeps the claim and returns only to previousState (3.724545ms)
✔ matrix J4: parking is Jason's, refused while required; unpark returns to queued (2.737006ms)
✔ field edits: who may change what (5.257737ms)
✔ set issues keeps a logged narrowing of closes (N10) (3.005939ms)
✔ text the table shows refuses \ and <, everywhere it enters (N8) (1.967734ms)
✔ every accepted text renders to nine cells on every row (N8) (27.287644ms)
✔ genesis: the map refuses an owner among its row's reviewers; replay doesn't (2026-10-04) (0.513896ms)
✔ times and dates must be calendar values, not just the shape (2026-10-04) (1.900355ms)
✔ replay holds every op id to the caller's rule (N11) (5.131008ms)
✔ note: owner, listed reviewer or privileged; empty clears (1.192835ms)
✔ assign moves the claim with the owner; done clears it (2.62756ms)
✔ render is byte-stable and escapes pipes (0.498811ms)
✔ view classification: current, genuine stale, edited stale marker, changed current body, markers (0.542756ms)
✔ next: resume, then review, then start, then wait, then nothing; lowest id first (17.804243ms)
✔ canonical args make a retry's identity independent of list order (0.283935ms)
✔ manifests, headings and blob ids (0.411391ms)
✔ the migration map: one queue-map block, exact keys (0.52762ms)
✔ every call but `queue` reaches the seat CLI exactly as before A2 (674.544225ms)
✔ `queue` reaches the queue CLI with the rest of the arguments (195.352557ms)
✔ the pre-A2 fixture is the script A2 changed (0.290873ms)
✔ acquire publishes the record by link; release removes only its own lock (9.563753ms)
✔ a kill between the temp write and the link leaves no lock (51.394846ms)
✔ a short or failed temp write refuses and leaves no lock and no temp (6.803209ms)
✔ a link error other than EEXIST refuses (6.005551ms)
✔ an error after the link releases the lock: unreadable gate, failing temp stat (19.514826ms)
✔ a release that fails on a gate path is reported, never a stack trace (P1) (32.128036ms)
✔ a paused holder: another writer waits 10 s, then refuses naming it live (10096.79375ms)
✔ two concurrent unlockers: the second refuses on the gate (40.573245ms)
✔ a writer publishing during an unlock, lock first: unlock sees it live and refuses (24.490257ms)
✔ a writer publishing during an unlock, gate first: the writer releases and refuses (21.506575ms)
✔ a gate swapped while held is left in place and reported, on success and on refusal (N1) (20.288019ms)
✔ a reused pid within one boot is mismatch; unlock removes the lock and never signals the process (21.734301ms)
✔ the same pid and start on a different boot is mismatch (0.911222ms)
✔ a foreign host is unknown whatever the local pid says; unlock refuses (84.192907ms)
✔ unreadable /proc: classification is unknown and acquire refuses (0.761046ms)
✔ invalid records: empty, unparsable, wrong keys, bad start or boot (0.231473ms)
✔ a stale gate blocks writers; --check-gate says mismatch for a reused pid (21.32465ms)
✔ a delayed release by a dead owner, after unlock and a new owner: the inode check keeps the new lock (26.912867ms)
✔ release checks the inode too: a byte-identical lock file with a new inode is left in place (5.802826ms)
✔ unlock refuses a live, unknown or invalid lock, and does nothing without one (41.223634ms)
✔ the migration map validates and renders the golden genesis table (4.79073ms)
✔ the marked QUEUE.md holds every row and parked item between its markers (1.258254ms)
✔ map-check reports each kind of drift (4.415784ms)
✔ a request posts once as the requester; a retry sends nothing (905.088098ms)
✔ each transport answer maps to posted, failed or uncertain (8.9 step 3) (4705.943451ms)
✔ the pre-send checks: GET user must name the requester, under the deadline (1827.694497ms)
✔ the lead's request refuses a token for login sage (941.467669ms)
✔ the credential file: the seat's own, 0600, no symlink, never the shared default (866.334216ms)
✔ an unresolved request blocks a new request, a new round, waiting-on-jason and done (2369.942983ms)
✔ a same-op retry after a kill sends nothing, even with a stale view (3210.314414ms)
✔ a held lock at the outcome exits 3 and names what the transport said (695.500575ms)
✔ late outcomes: after an abandon, and after a resolve with the same or another id (2548.722514ms)
✔ resolve checks the comment: issue, markers, round, candidate and author (1716.454565ms)
✔ the lead resolves a seat's request: the comment's author is the requester, fetched with the lead's token (847.067684ms)
✔ validateRow checks a request round's shape, which every replayed entry must keep (544.335506ms)
✔ request, changes, a new candidate, approval: every round pinned; no review files (1799.442121ms)
✔ a row with no reviewers opens a round that posts nothing (1183.071596ms)
✔ verify-commit: a prospective tree must hold exactly the candidate's paths (1371.550499ms)
✔ semantics: v1 entries replay as before; review entries need v2 (561.678604ms)
✔ set reviewers refuses the row's owner (2026-09-28) (416.725441ms)
✔ the owner records no verdict, even as a listed reviewer (669.107016ms)
✔ a request comment over the length limit is not sent (551.594405ms)
✔ a late POST on a closed row leaves a conflict nothing can resolve, and resolve asks nothing (664.607ms)
✔ a Jason-gated row reaches waiting-on-jason only on every reviewer's approval (2517.080568ms)
✔ genesis: refusals before anything is written (427.885336ms)
✔ genesis: the map must be committed, well formed, with committed briefs and seats (680.25551ms)
✔ genesis: markers, a stray witness, once only; a retry returns the receipt (554.26491ms)
✔ genesis: a file holding genesis alone with no witness is confirmed by sync or a retry (433.591615ms)
✔ canonical checks: worktree, second clone, detached HEAD, wrong branch, GIT_DIR, foreign code; a symlink works (668.163054ms)
✔ --by that differs from MOSAIC_AGENT_NAME warns on stderr and logs nothing more (N12) (940.316189ms)
✔ op ids: missing, too long, reserved; a retry answers; another payload refuses (779.602848ms)
✔ a retried add returns the id it first allocated, after reassignment and after done (992.408974ms)
✔ Rocko's S4 schedule: a lost result, another writer, then the retry opens no second round (895.763614ms)
✔ the review issue and the evidence round through the CLI (lead decision 23, 8.7) (1324.679043ms)
✔ claims and add defaults through the CLI; candidates are manifests or reachable commits (838.423346ms)
✔ add, set reviewers and assign refuse the row's owner as a reviewer (520.931597ms)
✔ the working-brief check: a changed working copy refuses the start and flags next (694.243984ms)
✔ next: resume first, then nothing for an idle seat; needs a seat (323.350984ms)
✔ view stale: new ops and verify refuse naming the unshown op; retries answer; reads warn; render fixes (749.885267ms)
✔ view unknown: a hand edit, an old marker over an edited body, missing or duplicate markers (1166.173374ms)
✔ a hand edit to queue.json refuses every verb, reads included (579.327776ms)
✔ verify and render --check leave bytes and mtimes unchanged (470.413342ms)
✔ render is byte-stable across runs and repositories (345.591299ms)
✔ snapshot and verify --snapshot (844.369863ms)
✔ usage errors exit 4 (590.929916ms)
✔ a short write, ENOSPC or a file fsync failure: nothing visible, temp removed (294.991623ms)
✔ a rename failure: nothing visible, temp removed (214.12502ms)
✔ a directory fsync failure: uncertain, exit 3, no receipt; the tail refuses new ops; a retry confirms (285.208379ms)
✔ a directory fsync failure, then sync names the op (395.573749ms)
✔ a witness write failure: uncertain, durable, exit 3; the view is untouched (216.389577ms)
✔ the .git fsync after the witness rename fails: uncertain, exit 3, the witness says so (199.305752ms)
✔ confirming a tail fsyncs queue.json and docs/plans before the witness; either failure changes nothing (220.389696ms)
✔ the docs/plans fsync after the view rename fails: the op stands, the view is written, a warning says so (213.091354ms)
✔ a lock swapped while held is left in place and reported, on a receipt and on a refusal (254.333892ms)
✔ a lock that cannot be released after an op is reported, on a receipt and on a refusal (255.382903ms)
✔ unlock prints a swapped gate's warning on stderr, the result on stdout (180.863444ms)
✔ a view write that fails keeps the op and reports a stale view (198.772798ms)
✔ SIGKILL before the rename: nothing recorded; the retry removes the leftover temp (773.517989ms)
✔ SIGKILL after the rename, before the witness: the tail refuses new ops and sync names the op (711.603704ms)
✔ SIGKILL after the witness, before the view: the stale refusal names the op (643.250736ms)
✔ SIGKILL after the view, before the receipt: the retry returns the receipt (710.760378ms)
✔ git checkout between steps 1 and 7: step 7 refuses and nothing is written (322.711236ms)
✔ git stash restoring an older valid pair: history lost; accept-history needs privilege, a reason and --yes (1134.208445ms)
✔ a deleted witness: refused after the locked recheck; accept-history records it absent (408.04457ms)
✔ a header edit during a write: the op stands, the view write is skipped with a warning (230.851588ms)
✔ a reader paused between the witness and the file while a writer finishes: no lost-history report (220.343628ms)
✔ file-then-witness order forced by a hook: the locked recheck prevents a false report (224.916508ms)
✔ a writer paused before and after the witness rename: readers see a tail, then a match (228.249859ms)
✔ a true rollback is reported only after the locked recheck; a held lock names its holder instead (601.437386ms)
✔ an accept-history in progress: an unlocked reader waits on the lock and never reports lost history (603.09745ms)
✔ the platform check refuses other filesystems (172.544266ms)
✔ tmpfs passes only a test layer that allows it (N5) (225.79821ms)
✔ unlock keeps a multi-line lock record on stdout (P3) (232.916337ms)
✔ F1: step 8 with index.lock held exits 3, and ordinary commits stay refused until the printed command runs (1061.824905ms)
✔ F1: HEAD moving after the step-7 guard check and before update-ref: refused, nothing published (1066.708895ms)
✔ F1: H is recorded before the canary, so HEAD moving during the step-1 canary is refused at step 7 (1017.127371ms)
✔ F1: a queue commit landing after H is recorded: step 1 says HEAD moved, not the guard (1355.445314ms)
✔ F1: a queue commit landing between the HEAD check and the canary: the failed clean run is reported as HEAD moved (1001.736236ms)
✔ F1: a shared-index change during the procedure is not committed (1123.875559ms)
✔ F1: a queue path staged after update-ref: step 8 stops and touches nothing (1133.958432ms)
✔ F1: a missing or a different hook refuses (787.253511ms)
✔ F1: same bytes without the exec bit, a symlinked hook, and core.hooksPath in the local or global scope each refuse before update-ref (1268.622115ms)
✔ F1: the canary refuses a hook that git would not run (767.910797ms)
✔ F1: the guard deactivated after step 1 is refused at the step-7 recheck (1000.642982ms)
✔ bootstrap: implementation-only HEAD, the guard, genesis, the --genesis commit, then an extending commit (1216.284271ms)
✔ bootstrap: --genesis with a base present, no base without --genesis, an op before the first commit, a changed map, another branch (1072.93868ms)
✔ bootstrap: the archived tests and validator run outside any repository (1114.228462ms)
✔ general: an unrelated staged file stays staged, and the committed blobs are the snapshot bytes (1133.802007ms)
✔ general: a queue write after the snapshot is not committed (1588.451551ms)
✔ general: a snapshot whose log does not extend the base refuses (1345.572094ms)
✔ general: install-hook privilege, repair of a same-bytes hook, and its refusals (158.499452ms)
✔ general: environment overrides, a linked worktree and usage (809.991692ms)
✔ general: a queue path staged before the run refuses at step 1 (752.034412ms)
✔ general: HEAD's queue tests failing in the archive refuse (974.440619ms)
✔ genesis document serializes deterministically and replays (4.243965ms)
✔ a hand edit that stays valid JSON fails replay; a formatting-only edit fails re-serialization (2.363785ms)
✔ a tampered result, receipt or viewSha fails replay (2.23881ms)
✔ op ids: 8 to 72 characters for callers, 80 in the log for .outcome entries (0.180499ms)
✔ add: defaults for an ordinary seat, privileged extras, refusals (4.59724ms)
✔ matrix: queued→briefed privileged; briefed→in-progress owner with after satisfied (5.711521ms)
✔ matrix: release, review round, changes requested and waiting-on-jason (12.703351ms)
✔ matrix J5: in-review→done by the gate owner with evidence naming the current round (7.854129ms)
✔ review issue, lead decision 23: none refuses, one is used, several need --issue, later rounds keep it (18.361508ms)
✔ the row schema refuses a round with a null issue, and the A1 review shape (P2) (2.443285ms)
✔ matrix R1: every state × target × actor class matches 8.7, gate owner jason or not, required or not (1321.934974ms)
✔ matrix: blocked keeps the claim and returns only to previousState (3.498242ms)
✔ matrix J4: parking is Jason's, refused while required; unpark returns to queued (2.258188ms)
✔ field edits: who may change what (4.444263ms)
✔ set issues keeps a logged narrowing of closes (N10) (2.396823ms)
✔ text the table shows refuses \ and <, everywhere it enters (N8) (1.627096ms)
✔ every accepted text renders to nine cells on every row (N8) (20.232327ms)
✔ genesis: the map refuses an owner among its row's reviewers; replay doesn't (2026-10-04) (0.463181ms)
✔ times and dates must be calendar values, not just the shape (2026-10-04) (1.396766ms)
✔ replay holds every op id to the caller's rule (N11) (4.32996ms)
✔ note: owner, listed reviewer or privileged; empty clears (1.31496ms)
✔ assign moves the claim with the owner; done clears it (2.206523ms)
✔ render is byte-stable and escapes pipes (0.421495ms)
✔ view classification: current, genuine stale, edited stale marker, changed current body, markers (0.547949ms)
✔ next: resume, then review, then start, then wait, then nothing; lowest id first (15.614361ms)
✔ canonical args make a retry's identity independent of list order (0.261014ms)
✔ manifests, headings and blob ids (0.41674ms)
✔ the migration map: one queue-map block, exact keys (0.712422ms)
✔ every call but `queue` reaches the seat CLI exactly as before A2 (593.551394ms)
✔ `queue` reaches the queue CLI with the rest of the arguments (214.598714ms)
✔ the pre-A2 fixture is the script A2 changed (0.270252ms)
✔ acquire publishes the record by link; release removes only its own lock (13.699806ms)
✔ a kill between the temp write and the link leaves no lock (54.469267ms)
✔ a short or failed temp write refuses and leaves no lock and no temp (9.326024ms)
✔ a link error other than EEXIST refuses (6.479184ms)
✔ an error after the link releases the lock: unreadable gate, failing temp stat (17.151141ms)
✔ a release that fails on a gate path is reported, never a stack trace (P1) (42.844766ms)
✔ a paused holder: another writer waits 10 s, then refuses naming it live (10092.670947ms)
✔ two concurrent unlockers: the second refuses on the gate (44.779225ms)
✔ a writer publishing during an unlock, lock first: unlock sees it live and refuses (22.456092ms)
✔ a writer publishing during an unlock, gate first: the writer releases and refuses (23.746529ms)
✔ a gate swapped while held is left in place and reported, on success and on refusal (N1) (26.303537ms)
✔ a reused pid within one boot is mismatch; unlock removes the lock and never signals the process (18.32773ms)
✔ the same pid and start on a different boot is mismatch (0.773792ms)
✔ a foreign host is unknown whatever the local pid says; unlock refuses (82.512363ms)
✔ unreadable /proc: classification is unknown and acquire refuses (0.801342ms)
✔ invalid records: empty, unparsable, wrong keys, bad start or boot (0.247735ms)
✔ a stale gate blocks writers; --check-gate says mismatch for a reused pid (24.859261ms)
✔ a delayed release by a dead owner, after unlock and a new owner: the inode check keeps the new lock (28.793256ms)
✔ release checks the inode too: a byte-identical lock file with a new inode is left in place (12.386533ms)
✔ unlock refuses a live, unknown or invalid lock, and does nothing without one (53.426691ms)
✔ the migration map validates and renders the golden genesis table (3.605974ms)
✔ the marked QUEUE.md holds every row and parked item between its markers (1.374021ms)
✔ map-check reports each kind of drift (3.769497ms)
✔ a request posts once as the requester; a retry sends nothing (925.490028ms)
✔ each transport answer maps to posted, failed or uncertain (8.9 step 3) (4633.824139ms)
✔ the pre-send checks: GET user must name the requester, under the deadline (1649.705611ms)
✔ the lead's request refuses a token for login sage (1002.199905ms)
✔ the credential file: the seat's own, 0600, no symlink, never the shared default (936.047983ms)
✔ an unresolved request blocks a new request, a new round, waiting-on-jason and done (2754.343636ms)
✔ a same-op retry after a kill sends nothing, even with a stale view (3581.147531ms)
✔ a held lock at the outcome exits 3 and names what the transport said (972.520861ms)
✔ late outcomes: after an abandon, and after a resolve with the same or another id (3471.630995ms)
✔ resolve checks the comment: issue, markers, round, candidate and author (2199.757148ms)
✔ the lead resolves a seat's request: the comment's author is the requester, fetched with the lead's token (963.207915ms)
✔ validateRow checks a request round's shape, which every replayed entry must keep (871.059553ms)
✔ request, changes, a new candidate, approval: every round pinned; no review files (2129.60435ms)
✔ a row with no reviewers opens a round that posts nothing (1824.373429ms)
✔ verify-commit: a prospective tree must hold exactly the candidate's paths (1550.689384ms)
✔ semantics: v1 entries replay as before; review entries need v2 (619.323764ms)
✔ set reviewers refuses the row's owner (2026-09-28) (427.726772ms)
✔ the owner records no verdict, even as a listed reviewer (760.230018ms)
✔ a request comment over the length limit is not sent (640.357309ms)
✔ a late POST on a closed row leaves a conflict nothing can resolve, and resolve asks nothing (775.036707ms)
✔ a Jason-gated row reaches waiting-on-jason only on every reviewer's approval (2832.128294ms)
✔ genesis: refusals before anything is written (403.047086ms)
✔ genesis: the map must be committed, well formed, with committed briefs and seats (623.222448ms)
✔ genesis: markers, a stray witness, once only; a retry returns the receipt (535.769096ms)
✔ genesis: a file holding genesis alone with no witness is confirmed by sync or a retry (411.50267ms)
✔ canonical checks: worktree, second clone, detached HEAD, wrong branch, GIT_DIR, foreign code; a symlink works (654.999389ms)
✔ --by that differs from MOSAIC_AGENT_NAME warns on stderr and logs nothing more (N12) (863.898734ms)
✔ op ids: missing, too long, reserved; a retry answers; another payload refuses (732.001521ms)
✔ a retried add returns the id it first allocated, after reassignment and after done (961.703142ms)
✔ Rocko's S4 schedule: a lost result, another writer, then the retry opens no second round (767.524552ms)
✔ the review issue and the evidence round through the CLI (lead decision 23, 8.7) (1288.336113ms)
✔ claims and add defaults through the CLI; candidates are manifests or reachable commits (851.663689ms)
✔ add, set reviewers and assign refuse the row's owner as a reviewer (568.745548ms)
✔ the working-brief check: a changed working copy refuses the start and flags next (688.532933ms)
✔ next: resume first, then nothing for an idle seat; needs a seat (343.272758ms)
✔ view stale: new ops and verify refuse naming the unshown op; retries answer; reads warn; render fixes (736.097429ms)
✔ view unknown: a hand edit, an old marker over an edited body, missing or duplicate markers (1284.828802ms)
✔ a hand edit to queue.json refuses every verb, reads included (686.215095ms)
✔ verify and render --check leave bytes and mtimes unchanged (452.027098ms)
✔ render is byte-stable across runs and repositories (323.191895ms)
✔ snapshot and verify --snapshot (831.73524ms)
✔ usage errors exit 4 (533.103717ms)
✔ a short write, ENOSPC or a file fsync failure: nothing visible, temp removed (343.935558ms)
✔ a rename failure: nothing visible, temp removed (209.136069ms)
✔ a directory fsync failure: uncertain, exit 3, no receipt; the tail refuses new ops; a retry confirms (259.837306ms)
✔ a directory fsync failure, then sync names the op (406.195753ms)
✔ a witness write failure: uncertain, durable, exit 3; the view is untouched (220.710753ms)
✔ the .git fsync after the witness rename fails: uncertain, exit 3, the witness says so (205.853729ms)
✔ confirming a tail fsyncs queue.json and docs/plans before the witness; either failure changes nothing (252.052248ms)
✔ the docs/plans fsync after the view rename fails: the op stands, the view is written, a warning says so (201.571019ms)
✔ a lock swapped while held is left in place and reported, on a receipt and on a refusal (228.011398ms)
✔ a lock that cannot be released after an op is reported, on a receipt and on a refusal (234.160899ms)
✔ unlock prints a swapped gate's warning on stderr, the result on stdout (145.80488ms)
✔ a view write that fails keeps the op and reports a stale view (211.930761ms)
✔ SIGKILL before the rename: nothing recorded; the retry removes the leftover temp (673.688149ms)
✔ SIGKILL after the rename, before the witness: the tail refuses new ops and sync names the op (686.847329ms)
✔ SIGKILL after the witness, before the view: the stale refusal names the op (650.580533ms)
✔ SIGKILL after the view, before the receipt: the retry returns the receipt (704.871034ms)
✔ git checkout between steps 1 and 7: step 7 refuses and nothing is written (262.774005ms)
✔ git stash restoring an older valid pair: history lost; accept-history needs privilege, a reason and --yes (1074.729772ms)
✔ a deleted witness: refused after the locked recheck; accept-history records it absent (386.684099ms)
✔ a header edit during a write: the op stands, the view write is skipped with a warning (227.042343ms)
✔ a reader paused between the witness and the file while a writer finishes: no lost-history report (227.289466ms)
✔ file-then-witness order forced by a hook: the locked recheck prevents a false report (230.099313ms)
✔ a writer paused before and after the witness rename: readers see a tail, then a match (226.156708ms)
✔ a true rollback is reported only after the locked recheck; a held lock names its holder instead (621.321999ms)
✔ an accept-history in progress: an unlocked reader waits on the lock and never reports lost history (649.492684ms)
✔ the platform check refuses other filesystems (156.699881ms)
✔ tmpfs passes only a test layer that allows it (N5) (259.564709ms)
✔ unlock keeps a multi-line lock record on stdout (P3) (228.264417ms)
ℹ tests 148
ℹ suites 0
ℹ pass 148
@@ -155,4 +155,4 @@
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 30990.382954
ℹ duration_ms 35595.254131
+28 -28
View File
@@ -1,30 +1,30 @@
✔ resolveSeat: by name under --repo resolves the repo layout (1.319343ms)
✔ resolveSeat: by path resolves the fleet layout (0.336467ms)
✔ resolveSeat: refusals for missing dir, missing launch.sh, non-executable launch.sh, invalid name, and unknown layout (0.765941ms)
✔ tmuxContext: outside tmux, default socket, custom socket, and exec failure (0.678008ms)
✔ makeRegistration produces a record that validates; each shape violation throws SeatError (0.83333ms)
✔ writeRegistration/readRegistration: round trip, permissions, absence, and malformed records (1.31461ms)
✔ updateTask: changes task, taskSetBy and updatedAt only, and refuses appropriately (1.206706ms)
✔ CLI launch: registers, execs the fake launch script, and passes args through (31.07694ms)
✔ CLI launch: --harness lands in the record (34.419142ms)
✔ CLI launch: the launch script's own exit code passes through (31.343789ms)
✔ CLI launch: relaunching a seat rewrites the one registration record (61.170784ms)
✔ CLI launch: omitting --task records an empty string, not null (34.172856ms)
✔ CLI seat task: updates only the task after a launch, and refuses on an unlaunched seat (83.178003ms)
✔ CLI refusals: no args, unknown flag, missing config, already-registered env, and exec failure (140.712262ms)
✔ samePath: equal paths, symlinked dirs, distinct dirs, and non-strings (0.469104ms)
✔ resolveSetBy: explicit --by wins over the environment; absent or empty environment gives unknown; invalid explicit or environment values refuse with exit 4 (1.461526ms)
✔ validateRegistration/readRegistration: taskSetBy is optional; a record without it (written before #1511) still loads unchanged; an invalid one is refused; the version does not change (0.754913ms)
✔ updateTask: records setBy, preserves startedAt and every unrelated field, upgrades an old record in place only when the task is set, and replaces a previous attribution (0.88094ms)
✔ CLI seat task: --by beats MOSAIC_AGENT_NAME, the environment beats nothing, empty environment is unknown, invalid --by or environment refuses with exit 4 and leaves the record byte for byte (305.051589ms)
✔ family: exactly one launch.max key in the model name, else null (0.882429ms)
✔ sessionEnv passes only the allowlist, the repo's bin on PATH, and the run id (0.899512ms)
✔ newRun: short ids, 0700 directories, and a refusal when the socket path won't fit (1.695646ms)
✔ session file and launch log: 0600, the session file written once (1.399717ms)
✔ endReason maps the runner's exit codes; a signal is killed (0.136655ms)
✔ Registry mirrors to sessions.json; readSessions marks live entries; bad files refuse (1.737279ms)
✔ stopSession refuses an unknown run, reports a stale one, and won't signal a pid that isn't a runner (1.649146ms)
✔ a session runs under unshare as pid 1 of its namespace, claims, answers, and stops on mosaic stop (252.111361ms)
✔ resolveSeat: by name under --repo resolves the repo layout (2.38375ms)
✔ resolveSeat: by path resolves the fleet layout (0.307ms)
✔ resolveSeat: refusals for missing dir, missing launch.sh, non-executable launch.sh, invalid name, and unknown layout (0.687428ms)
✔ tmuxContext: outside tmux, default socket, custom socket, and exec failure (0.671855ms)
✔ makeRegistration produces a record that validates; each shape violation throws SeatError (0.816835ms)
✔ writeRegistration/readRegistration: round trip, permissions, absence, and malformed records (1.280225ms)
✔ updateTask: changes task, taskSetBy and updatedAt only, and refuses appropriately (1.155249ms)
✔ CLI launch: registers, execs the fake launch script, and passes args through (27.419109ms)
✔ CLI launch: --harness lands in the record (28.863116ms)
✔ CLI launch: the launch script's own exit code passes through (27.969508ms)
✔ CLI launch: relaunching a seat rewrites the one registration record (56.792485ms)
✔ CLI launch: omitting --task records an empty string, not null (28.510595ms)
✔ CLI seat task: updates only the task after a launch, and refuses on an unlaunched seat (82.272699ms)
✔ CLI refusals: no args, unknown flag, missing config, already-registered env, and exec failure (136.848395ms)
✔ samePath: equal paths, symlinked dirs, distinct dirs, and non-strings (0.370135ms)
✔ resolveSetBy: explicit --by wins over the environment; absent or empty environment gives unknown; invalid explicit or environment values refuse with exit 4 (0.574248ms)
✔ validateRegistration/readRegistration: taskSetBy is optional; a record without it (written before #1511) still loads unchanged; an invalid one is refused; the version does not change (0.756508ms)
✔ updateTask: records setBy, preserves startedAt and every unrelated field, upgrades an old record in place only when the task is set, and replaces a previous attribution (0.859621ms)
✔ CLI seat task: --by beats MOSAIC_AGENT_NAME, the environment beats nothing, empty environment is unknown, invalid --by or environment refuses with exit 4 and leaves the record byte for byte (265.483659ms)
✔ family: exactly one launch.max key in the model name, else null (0.688496ms)
✔ sessionEnv passes only the allowlist, the repo's bin on PATH, and the run id (0.645819ms)
✔ newRun: short ids, 0700 directories, and a refusal when the socket path won't fit (1.455045ms)
✔ session file and launch log: 0600, the session file written once (0.94776ms)
✔ endReason maps the runner's exit codes; a signal is killed (0.112724ms)
✔ Registry mirrors to sessions.json; readSessions marks live entries; bad files refuse (1.128494ms)
✔ stopSession refuses an unknown run, reports a stale one, and won't signal a pid that isn't a runner (0.992954ms)
✔ a session runs under unshare as pid 1 of its namespace, claims, answers, and stops on mosaic stop (266.652798ms)
ℹ tests 27
ℹ suites 0
ℹ pass 27
@@ -32,4 +32,4 @@
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 786.592101
ℹ duration_ms 715.303268
+52 -52
View File
@@ -1,54 +1,54 @@
✔ the boot config is checked before anything starts (86.53616ms)
✔ a business with no tracker entry refuses task verbs (121.033728ms)
✔ credential.expiring and .expired are recorded once per instance (238.662195ms)
✔ a token file that changes on disk records credential.changed (110.167027ms)
✔ autostart polls, reconciles and retries a startup the tracker was down for (137.516851ms)
✔ a refusal a restart must clear is not retried by the poll (83.605374ms)
✔ a poll that fires while two are queued is dropped (102.558062ms)
✔ close waits for a running verb and refuses one that has not started (218.733326ms)
✔ the bundled Vikunja is the pinned upstream image the runbook names (0.943209ms)
✔ every published port is on 127.0.0.1, and no secret is in the file (0.356824ms)
✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (375.792226ms)
✔ the recorded task bodies pass the checks S3 applies to every read (0.594935ms)
✔ the client works against the fake over real HTTP with the platform fetch (247.020492ms)
✔ a correct install starts, and the first reconcile records tasks that already exist (107.080549ms)
✔ verbs refuse while a business is starting and after startup refused it (156.327278ms)
✔ startup refuses a token that can do more than its role needs (390.24299ms)
✔ startup refuses an unsupported version and flags an untested one (257.301253ms)
✔ startup refuses a board that the runbook did not install (377.532789ms)
✔ startup refuses a project the sync bot cannot read (87.091341ms)
✔ startup refuses a configured label the pm bot cannot see (98.739148ms)
✔ startup refuses an expired credential and a missing sync credential (176.990677ms)
✔ an unreachable tracker refuses with tracker-unavailable (72.813225ms)
✔ an edit in the UI is recorded once, with the fields that changed (198.316094ms)
✔ a move between open buckets is seen on the board, though updated does not change (195.702643ms)
✔ a person's comment is counted and a bot's is not (282.562191ms)
✔ the hourly reconcile catches a comment through comment_count (230.197458ms)
✔ a task closed in the UI leaves the open view with its done bucket (392.842554ms)
✔ a task that leaves the board is recorded as deleted, moved or out of reach (272.858491ms)
✔ a poll that read before a verb wrote does not overwrite the verb (166.093849ms)
✔ a tracker fault during a tick is reported and the next tick catches up (163.758114ms)
✔ a malformed answer refuses the tick with tracker-shape (127.398439ms)
✔ no token value reaches the database, the log or a refusal (270.094556ms)
✔ the first look at a task counts only comments inside the window (137.891107ms)
✔ task.create needs a recorded human request and a requirement id (188.444054ms)
✔ only labels named in the business file can be written (244.533409ms)
✔ task.schedule sets and clears a due date and relations (289.26533ms)
✔ assign and reassign move the role bots and record task.assigned (294.57936ms)
✔ task.update.assigned is for the assignee and records task.state (299.706629ms)
✔ a wrong expected digest records task.conflict and writes nothing (196.565364ms)
✔ a cross-role verb needs a resolved decision, used once (254.681696ms)
✔ task.close needs a verdict; after it every verb refuses with task-done (217.486778ms)
✔ a lost answer is settled by a re-read and never retried (252.812073ms)
✔ a create whose answer is lost is reported uncertain, and the poll finds the task (174.684319ms)
✔ a task the sync bot cannot read refuses and records nothing (105.904241ms)
✔ verbs and polls for one business run one at a time (171.006531ms)
✔ a due date with milliseconds is written to the second (168.703594ms)
✔ every write landed and the final read failed: the verb succeeds and records what it wrote (107.939507ms)
✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (88.213901ms)
✔ a create whose final read fails succeeds and records task.created (94.169486ms)
✔ an edit between the last write and the final read shows as external on the next poll (115.615544ms)
✔ task.created is recorded when a later label write fails (79.373231ms)
✔ the boot config is checked before anything starts (197.479775ms)
✔ a business with no tracker entry refuses task verbs (139.742344ms)
✔ credential.expiring and .expired are recorded once per instance (219.539472ms)
✔ a token file that changes on disk records credential.changed (110.804248ms)
✔ autostart polls, reconciles and retries a startup the tracker was down for (137.469261ms)
✔ a refusal a restart must clear is not retried by the poll (106.393381ms)
✔ a poll that fires while two are queued is dropped (102.177258ms)
✔ close waits for a running verb and refuses one that has not started (228.556608ms)
✔ the bundled Vikunja is the pinned upstream image the runbook names (0.795107ms)
✔ every published port is on 127.0.0.1, and no secret is in the file (0.32377ms)
✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (494.146398ms)
✔ the recorded task bodies pass the checks S3 applies to every read (0.538607ms)
✔ the client works against the fake over real HTTP with the platform fetch (248.265171ms)
✔ a correct install starts, and the first reconcile records tasks that already exist (214.536073ms)
✔ verbs refuse while a business is starting and after startup refused it (148.912633ms)
✔ startup refuses a token that can do more than its role needs (410.384994ms)
✔ startup refuses an unsupported version and flags an untested one (320.020087ms)
✔ startup refuses a board that the runbook did not install (381.770113ms)
✔ startup refuses a project the sync bot cannot read (97.801388ms)
✔ startup refuses a configured label the pm bot cannot see (81.711336ms)
✔ startup refuses an expired credential and a missing sync credential (221.987872ms)
✔ an unreachable tracker refuses with tracker-unavailable (89.937615ms)
✔ an edit in the UI is recorded once, with the fields that changed (330.854999ms)
✔ a move between open buckets is seen on the board, though updated does not change (190.310234ms)
✔ a person's comment is counted and a bot's is not (288.545396ms)
✔ the hourly reconcile catches a comment through comment_count (228.898375ms)
✔ a task closed in the UI leaves the open view with its done bucket (429.734775ms)
✔ a task that leaves the board is recorded as deleted, moved or out of reach (287.164684ms)
✔ a poll that read before a verb wrote does not overwrite the verb (176.826276ms)
✔ a tracker fault during a tick is reported and the next tick catches up (155.192537ms)
✔ a malformed answer refuses the tick with tracker-shape (125.233379ms)
✔ no token value reaches the database, the log or a refusal (283.776287ms)
✔ the first look at a task counts only comments inside the window (146.045513ms)
✔ task.create needs a recorded human request and a requirement id (319.485088ms)
✔ only labels named in the business file can be written (228.589076ms)
✔ task.schedule sets and clears a due date and relations (308.501904ms)
✔ assign and reassign move the role bots and record task.assigned (329.197503ms)
✔ task.update.assigned is for the assignee and records task.state (317.345199ms)
✔ a wrong expected digest records task.conflict and writes nothing (235.892284ms)
✔ a cross-role verb needs a resolved decision, used once (253.179147ms)
✔ task.close needs a verdict; after it every verb refuses with task-done (234.270236ms)
✔ a lost answer is settled by a re-read and never retried (232.558998ms)
✔ a create whose answer is lost is reported uncertain, and the poll finds the task (194.4704ms)
✔ a task the sync bot cannot read refuses and records nothing (132.208983ms)
✔ verbs and polls for one business run one at a time (185.004946ms)
✔ a due date with milliseconds is written to the second (247.995644ms)
✔ every write landed and the final read failed: the verb succeeds and records what it wrote (146.862751ms)
✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (110.616531ms)
✔ a create whose final read fails succeeds and records task.created (161.566382ms)
✔ an edit between the last write and the final read shows as external on the next poll (155.139858ms)
✔ task.created is recorded when a later label write fails (124.486898ms)
ℹ tests 51
ℹ suites 0
ℹ pass 51
@@ -56,4 +56,4 @@
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3446.444304
ℹ duration_ms 4013.267787
+15 -15
View File
@@ -1,18 +1,18 @@
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (3469.489458ms)
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (3622.121735ms)
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (3364.830245ms)
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (3544.973289ms)
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (1654.619907ms)
✔ conversation view: a newer session with no readable history keeps the marker (1085.988414ms)
✔ conversation view: seats without history say so and offer no reply (793.672153ms)
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (3334.88261ms)
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (54055.861502ms)
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (3478.193516ms)
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (21953.046074ms)
✔ loopback host and board origin fail closed (6.453466ms)
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (90.944633ms)
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (125.009085ms)
✔ unreachable board reports URL; CLI rejects unsupported options (954.243888ms)
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (3565.038836ms)
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (3611.776637ms)
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (1686.895048ms)
✔ conversation view: a newer session with no readable history keeps the marker (1116.328238ms)
✔ conversation view: seats without history say so and offer no reply (851.317089ms)
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (3608.726306ms)
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (54301.56092ms)
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (3605.029743ms)
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (21936.08607ms)
✔ loopback host and board origin fail closed (6.73058ms)
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (84.22649ms)
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (95.864801ms)
✔ unreachable board reports URL; CLI rejects unsupported options (1030.704712ms)
ℹ tests 14
ℹ suites 0
ℹ pass 14
@@ -20,4 +20,4 @@ Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 54438.577036
ℹ duration_ms 54764.629902
+3 -3
View File
@@ -1,10 +1,10 @@
node-bus exit=0 ℹ pass 74 ℹ fail 0
node-business exit=0 ℹ pass 60 ℹ fail 0
node-cli exit=0 ℹ pass 82 ℹ fail 0
node-bus exit=0 ℹ pass 74 ℹ fail 0
node-cli exit=0 ℹ pass 83 ℹ fail 0
node-control-board exit=0 ℹ pass 124 ℹ fail 0
node-conversation exit=0 ℹ pass 152 ℹ fail 0
node-discord exit=0 ℹ pass 178 ℹ fail 0
node-harness exit=0 ℹ pass 50 ℹ fail 0
node-harness exit=0 ℹ pass 53 ℹ fail 0
node-ledger exit=0 ℹ pass 78 ℹ fail 0
node-mosaic exit=0 ℹ pass 69 ℹ fail 0
node-queue exit=0 ℹ pass 148 ℹ fail 0
+2 -34
View File
@@ -1,39 +1,7 @@
Note: switching to '8dd5ff004b0ce6157446ef78523a1036ed86f42c'.
On branch refactor
Your branch is up to date with 'origin/refactor'.
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 '8dd5ff004b0ce6157446ef78523a1036ed86f42c'.
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)
@@ -8,7 +8,7 @@ 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: 945540 Killed "$@" > /dev/null 2>&1
scripts/test-extension-package.sh: line 14: 2015159 Killed "$@" > /dev/null 2>&1
OK forced interruption kills the replacing process
OK next invocation recovers old consistent installation
OK interrupted replacement rolled back
+1 -1
View File
@@ -30,6 +30,6 @@ OK packages/queue declares no dependencies
ℹ 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/s6-gate-r2) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
skip queue verify and render --check: this checkout (/home/jwoltje/filbert-scratch/s6-gate-r3) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
queue suite: 27 passed, 0 failed
+20 -20
View File
@@ -1,29 +1,29 @@
5dc9e67c9ce42b86c40d83c8b1aa8d5ba93d6e8b51bb1f4d2964f4b270ee52aa base.txt
610db30b90e3ce17614bf8d806f35102947deb8396f0045eec0c58b22aca549d BUILD.md
f8cbf7bde355312253265d2071e5756f4b04d116bac06cfcf0a4a2c0474da942 build.patch
fd21bdc27573f080d965da3121a390b148a3370acd8d9dfa5d34fcd59460eb02 candidate-manifest.sha256
d9ae770c156020b3b536f06f0ecfa6e550423343f85c8b4566fa90fd9a650319 BUILD.md
7b63583de7a701cfb835ffabe4118b2624df49cfcaedf7c7691b097695b8907e build.patch
9f0593af17902e6ec084987175a1e48affd9c71568cb2eeacc7b4cd1ef165cbe candidate-manifest.sha256
c6309079b471a4b304ba55679bfa2ce7f1e145137ec784f2e35ee7330df844ea files.txt
8df77c49bf82833efa56d42401a4a06f20931b7a76dd5eed3a73fdec60e92e79 out/base-test-task.txt
d72ae9083340920f68e4f549d110810d8790450cb09920745e969af2b2568145 out/node-business.txt
7eaf5878f8e31e7eb01c51c8daf52a8be2e983b3fd9c4d3b98fb2243476ccbb4 out/node-bus.txt
a83e51052bdb03b412b1451002bafad57ad89ceffc73baf4be3caa61fb8f4200 out/node-cli.txt
fe7f83107c5a25fe617f8e58413a9ff704e140c1884dc6f241c58d55bd9912d7 out/node-control-board.txt
b60466aba670a9b30a8537e24b9f291d5cbe49befe13c31c6781e3c46b8889e0 out/node-conversation.txt
e58a0e4907739b6c79ca291b428bbd5160f2444fca78656bde0283f92032891f out/node-discord.txt
1c6c21518e709a43acbf3ca4a4a7eb89cda52cf9ecc3a2a55d3c9160c0129ba8 out/node-harness.txt
7e47533785ff363fe2f7e9a3ff6fdd308f6f161578c15b836fce96a0730e4466 out/node-ledger.txt
493b230afa7be6bdbc8baad8430fb32f8e0450340c609d91648d2b2c2344db14 out/node-mosaic.txt
634086ba61d96f9bd421fdda0d1c20e08760980fbce6f70056a31586f2e0fb21 out/node-queue.txt
75f62e174199cae604672b2cc883dc0d8588816db63048090d03290f2b67bc93 out/node-seat.txt
70d50a65bf6e55adf11a9ed594f3ad3f37a75ba54ca9febf4746df82f42c6a35 out/node-tasks.txt
43f03acf96397b1658e7b0eae7136feae19f052ae323a69aef28a77c6c035a46 out/node-webui.txt
5790ec180458b55445a450625a583e09b6a5de5d820f062be4ff500074c8a9e4 out/summary.txt
08ff9447c1cdff32245cb957876aff62406b5d0343bd50bf0924666523a92de0 out/node-business.txt
454d05e1e09f87988d90d302deb0a38790c195973009e14073149e0df3ca26ee out/node-bus.txt
155a45072ac3e2068891db10ca9dc7de4da0d58a4e05945f89e61e354444069a out/node-cli.txt
1da902b53efce461be93f6841ea257ec08ce345150e3d9a1d76e98f1c65a3ff4 out/node-control-board.txt
60ad9ef705fe72365e7bb87470045a10ffb604322dcf7f376b6b11946f453657 out/node-conversation.txt
54bc9eda871683552daffc452d6255789210261dee93dd7250da0d10ce8408c5 out/node-discord.txt
0518f1253d7ae01547e580a7072d67f93846f438840582e577cd65b746d5c59f out/node-harness.txt
5d7953f6c6ec388351712adccb5ce97b74921ec53d0dcb122bc218e3625ca9eb out/node-ledger.txt
a39c3ebe4da7a99cf89d71b98cb26f5c9cb90dd7c44df15485debd2b9b79d781 out/node-mosaic.txt
cff341d0a8e89e5e316deb91eea7ea13813b85385178fc5e3059c26866494a61 out/node-queue.txt
c2290fe1e3d10953c590af952dc22591d7402cdabc64f718cec0fb49f95ecb5b out/node-seat.txt
24ce87d27bd52f5554a795da5dbfe409351ec0e265faa68b455fd7863e6b743c out/node-tasks.txt
fa73b82e2d48e88d23191f8c7d7a7b5aba283e6675b1d0db63425d764fd43227 out/node-webui.txt
195c9716b01d1b25b2de36c99dd1cf970e579fdf9c108b40fc83d910db121769 out/summary.txt
f91fc95a173339137f981e24fbf630acfde957e32c3cd71e28304532083c6a77 out/test-auth.txt
b13547b025c824fae58dcb2d15bdac9d331a7965218e1c7491814020572984da out/test-conductor.txt
e1206366f07f3d9029530dcdcb55c0282d3f052adc60fc7fd51642d5d4ec3600 out/test-conductor.txt
52d211444489ea729dafc3c9745d4474d9dfd50f41055cbad792424197f04073 out/test-config.txt
7d5016ded1994048642f8b9ae845e52e8af4a069ba05c056c8f580a4589bacbe out/test-discord.txt
836c9cbc92e8c384b80a17e8e31a7ece39aca33efcad61b86898293e8d2f2cc3 out/test-extension-package.txt
4726725a020747117f43feaffffd04935633fd809e33306b97146e688233ec25 out/test-extension-package.txt
83dfaa21c4e4ad941088efab3f69e87ebd2f6bd3cc72ede938e21dc06da8a34f out/test-foundation.txt
45f31e21b7685482530fc5bae64e910462e4c6f2c1554e56465d098d0faf3da0 out/test-queue.txt
5bdf623f67f231a226bdc707bfaec9796c3c227c9ffedd95953693714e6182dd out/test-queue.txt
6183e6b9b05edb497e92ffd09a19bba49c6f656c750ecb2acc283021113d1a98 out/test-release.txt
8df77c49bf82833efa56d42401a4a06f20931b7a76dd5eed3a73fdec60e92e79 out/test-task.txt