agents: darkwing row 130 round 1 packet (#1625)
Build patch against c6570f142, candidate manifest (4 files), repro,
mutant (killed, 2 of 191), targeted gate (cli 191/0, webui 23/0) and
build.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f8932fa176
commit
21e8f64ea7
15 files changed
+795
No files matched your search
@@ -0,0 +1,64 @@
|
||||
# Row 130 (#1625) round 1: human and tester commands name the tree the bus runs from
|
||||
|
||||
Brief: docs/plans/2026-10-14_clone-cli-copy-path.md (blob 8120438a6, re-pinned at 8089b98c4). Sage assigned the row in sage-0242 and granted the slot in sage-0246.
|
||||
|
||||
BASE: c6570f142. The candidate is build.patch against BASE, and candidate-manifest.sha256 lists its 4 files. `repro.sh` exports BASE with `git archive`, applies build.patch and checks every manifest digest: DIGESTS_OK (repro-run.log).
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| packages/cli/src/cli.mjs | the entry block becomes an exported `run(argv, io, deps)`, plus the `OTHER_TREE` text and a set of the two codes. Nothing above the entry changes |
|
||||
| packages/cli/tests/other-tree.test.mjs | new, 4 tests |
|
||||
| packages/cli/README.md | one sentence in the `--tester` bullet, one paragraph at the end of "One pinned copy" |
|
||||
| docs/TOOLS.md | one sentence at the end of "Pinned stack update", linking the README section |
|
||||
|
||||
## The code
|
||||
|
||||
`run()` calls `main()` and catches a CliError, as the entry did. It writes `mosaic: <message>` as before. When the error's `code` is `human-required` or `tester-required`, it writes one more line:
|
||||
|
||||
```
|
||||
mosaic: the bus accepts these commands only from the tree it runs from; on a pinned host run `mosaic` on PATH
|
||||
```
|
||||
|
||||
It returns the error's exit code, which `busExit` sets to 3 for both codes. The entry sets `process.exitCode` from it only when it isn't 0, so a success leaves the exit code unset, as before. `run()`'s default io is the same as `main()`'s.
|
||||
|
||||
The hint goes at the print site, not into the error. app.mjs (lines 347-379) compares `e.message` to `e.code`, so the message has to stay the bare code. The code comes from the transport's own stderr parse (`error.code`), so the agent-check refusal from `refuseInsideAgent`, which has no code, never gets the line. The line names no path. `run()` is exported so the tests drive the same print path the command line uses.
|
||||
|
||||
## Tests
|
||||
|
||||
other-tree.test.mjs, 4 of 4 passing, and 191 of 191 in the cli suite (gate/pkg-cli.log).
|
||||
|
||||
1. "a human command refused with human-required ...": `inbox --business demo` through the real `humanTransport`, whose spawn is a stand-in that exits 1 with `human-required` on stderr, so the real code parse runs. It asserts exit 3, stderr exactly `mosaic: human-required` and then the hint line, and empty stdout.
|
||||
2. "a --tester command refused with tester-required ...": `agents --tester --business demo` from an agent environment, through `deps.testerSpawn`. It asserts exit 3 and stderr exactly the code line plus the hint.
|
||||
3. "other exit-3 refusals print no other-tree line": `tester-off` the same way gives exit 3 and stderr exactly `mosaic: tester-off`. The plain human `inbox` from an agent environment gives the agent-check refusal, exit 3, with no hint.
|
||||
4. "the line names no path": `OTHER_TREE` has no `/`.
|
||||
|
||||
No existing test changes. stack-copy.test.mjs:144 reads `human-required` from human-cli.mjs run on its own, not through cli.mjs, so it is unaffected.
|
||||
|
||||
## Mutant
|
||||
|
||||
mutants.py drops the `if (OTHER_TREE_CODES.has(...))` line from `run()`, runs the cli suite, restores the file from `.orig` with a sha256 check and then kills any process whose cwd is under this row's TMPDIR. mutants-run.log has the run.
|
||||
|
||||
| Mutant | Change | Result |
|
||||
|---|---|---|
|
||||
| m1-drop-other-tree-line | drop the extra stderr line | killed, 2 of 191 fail: tests 1 and 2 |
|
||||
|
||||
The run ended `restored: True`, and the sweep killed nothing.
|
||||
|
||||
## Gate
|
||||
|
||||
A targeted builder gate under #271 and decision 136 (gate.sh, logs in gate/). Sage's landing gate stays full.
|
||||
|
||||
| Step | Result |
|
||||
|---|---|
|
||||
| packages/cli `node --test tests/*.test.mjs` | 191 pass, 0 fail |
|
||||
| packages/webui `node --test tests/reads.test.mjs tests/views.test.mjs tests/pinned-copy.test.mjs` | 23 pass, 0 fail |
|
||||
|
||||
GATE_PASSED 00:27:55Z (gate-run.log). webui/src/reads.mjs imports `readNotifyConfig` from cli.mjs, and those three webui tests import reads.mjs directly. serve.mjs and webui's cli.mjs also import reads.mjs, and Sage's full gate covers the rest of webui. No scripts/test-*.sh reads a changed path. packages/queue/tests/dispatch.test.mjs names `/packages/cli/src/cli.mjs` only as a string. host.mjs:375 matches a process argv ending in that path, and the path doesn't change. No step failed, so the flake rule had nothing to count.
|
||||
|
||||
## Leftovers (sweep.log)
|
||||
|
||||
No process was left under the TMPDIR. The cli suite left 6 `mosaic-launch-*` directories, 3 from the mutant run and 3 from the gate. They come from the cli tests' own launches (#1624) and are unrelated to this change. sweep.log lists them with their mtimes and records that I removed them.
|
||||
|
||||
## Going live
|
||||
|
||||
This row has no live step. The refusal the line explains happens on the checkout's `scripts/mosaic`, which runs the checkout's own cli.mjs, so the line shows there once Sage lands the row. `mosaic` on PATH runs the pinned copy and gets the change only at the next re-pin, on Mos's go (decision 109). Per the brief, that re-pin is when the row counts as live.
|
||||
@@ -0,0 +1,133 @@
|
||||
diff --git a/docs/TOOLS.md b/docs/TOOLS.md
|
||||
index fc38cefab..13366b63a 100644
|
||||
--- a/docs/TOOLS.md
|
||||
+++ b/docs/TOOLS.md
|
||||
@@ -351,6 +351,11 @@ check is expected to refuse this restored mixed setup; #225 remains open.
|
||||
Later rollback repeats steps 2-6 with the prior shared copy's full id, each
|
||||
live step again approved by Mos. Nothing prunes copies or removes mosaic-old.
|
||||
|
||||
+After a switch, the human commands and `--tester` work only through `mosaic`
|
||||
+on PATH; the checkout's `scripts/mosaic` exits 3 with `human-required` or
|
||||
+`tester-required`, and queue writes stay on it ([packages/cli/README.md, One
|
||||
+pinned copy](../packages/cli/README.md#one-pinned-copy-row-85-1579)).
|
||||
+
|
||||
## Console service (`scripts/webui-service.sh`)
|
||||
|
||||
```bash
|
||||
diff --git a/packages/cli/README.md b/packages/cli/README.md
|
||||
index fb4532c84..1757ef1b6 100644
|
||||
--- a/packages/cli/README.md
|
||||
+++ b/packages/cli/README.md
|
||||
@@ -86,7 +86,9 @@ mosaic launches list [--json]
|
||||
exception to the agent check above: Mos or a T3 seat may run it for a PM
|
||||
test, and the broker still refuses it under a managed run
|
||||
(`tester-required`) or while the business var `bus.tester` isn't `true`
|
||||
- (`tester-off`), each exit 3. `talk --tester` text must start with
|
||||
+ (`tester-off`), each exit 3. Like the human commands, `--tester` works
|
||||
+ only from the tree the bus runs from ("One pinned copy" below).
|
||||
+ `talk --tester` text must start with
|
||||
`[mos test]`, else exit 2 before any call. The role's reply comes back to
|
||||
the tester, not to the human's inbox. `decide`, `stop` and `launches`
|
||||
take no `--tester` (exit 4).
|
||||
@@ -494,6 +496,15 @@ Each live switch/unit change/stop/start requires Mos's go; agents build
|
||||
and test fixtures only. Restarting the bus ends its sessions, so no PM
|
||||
turn may be in flight. Queue writes stay on the canonical scripts/mosaic.
|
||||
|
||||
+The human commands and `--tester` run only from the tree the bus runs from
|
||||
+(row 130, #1625). The broker checks the path of the calling human-cli.mjs or
|
||||
+tester-cli.mjs against its own. On a host with a pinned copy that tree is
|
||||
+the copy, and `mosaic` on PATH is the wrapper that runs it. The checkout's
|
||||
+`scripts/mosaic` exits 3 with `human-required` or `tester-required` and
|
||||
+prints one more line: "the bus accepts these commands only from the tree it
|
||||
+runs from; on a pinned host run `mosaic` on PATH". Queue writes still go
|
||||
+through the checkout's `scripts/mosaic`.
|
||||
+
|
||||
## Notifier
|
||||
|
||||
The notifier is a child of the host. Each poll (every 30 s) it reads the
|
||||
diff --git a/packages/cli/src/cli.mjs b/packages/cli/src/cli.mjs
|
||||
index a547e36c7..a95eabaab 100644
|
||||
--- a/packages/cli/src/cli.mjs
|
||||
+++ b/packages/cli/src/cli.mjs
|
||||
@@ -388,12 +388,27 @@ export async function main(argv, io = { env: process.env, stdin: process.stdin,
|
||||
throw usage();
|
||||
}
|
||||
|
||||
-if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
+// The broker's human and tester proofs accept only the human-cli.mjs and
|
||||
+// tester-cli.mjs of the tree the bus runs from. Since row 85 that is a
|
||||
+// pinned copy, so the checkout's own scripts/mosaic is refused with these
|
||||
+// codes (row 130). The message stays the bare code: app.mjs reads it.
|
||||
+export const OTHER_TREE = "the bus accepts these commands only from the tree it runs from; on a pinned host run `mosaic` on PATH";
|
||||
+const OTHER_TREE_CODES = new Set(["human-required", "tester-required"]);
|
||||
+
|
||||
+// main, with a CliError printed to stderr. Returns the exit code.
|
||||
+export async function run(argv, io = { env: process.env, stdin: process.stdin, stdout: process.stdout, stderr: process.stderr }, deps = {}) {
|
||||
try {
|
||||
- await main(process.argv.slice(2));
|
||||
+ await main(argv, io, deps);
|
||||
+ return 0;
|
||||
} catch (error) {
|
||||
if (!(error instanceof CliError)) throw error;
|
||||
- process.stderr.write(`mosaic: ${error.message}\n`);
|
||||
- process.exitCode = error.exitCode;
|
||||
+ io.stderr.write(`mosaic: ${error.message}\n`);
|
||||
+ if (OTHER_TREE_CODES.has(error.code)) io.stderr.write(`mosaic: ${OTHER_TREE}\n`);
|
||||
+ return error.exitCode;
|
||||
}
|
||||
}
|
||||
+
|
||||
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
+ const code = await run(process.argv.slice(2));
|
||||
+ if (code !== 0) process.exitCode = code;
|
||||
+}
|
||||
diff --git a/packages/cli/tests/other-tree.test.mjs b/packages/cli/tests/other-tree.test.mjs
|
||||
new file mode 100644
|
||||
index 000000000..d00d0a9c1
|
||||
--- /dev/null
|
||||
+++ b/packages/cli/tests/other-tree.test.mjs
|
||||
@@ -0,0 +1,44 @@
|
||||
+import { test } from "node:test";
|
||||
+import assert from "node:assert/strict";
|
||||
+import { OTHER_TREE, run } from "../src/cli.mjs";
|
||||
+import { humanTransport } from "../src/transport.mjs";
|
||||
+import { humanEnv, io, tmp } from "./helpers.mjs";
|
||||
+
|
||||
+// Row 130 (#1625): the broker accepts the human and tester proofs only from
|
||||
+// the tree the bus runs from, so a refusal with human-required or
|
||||
+// tester-required gets one more stderr line naming the command that works.
|
||||
+// The spawn stands in for human-cli.mjs and tester-cli.mjs: it exits 1 with
|
||||
+// the code on stderr, and the real transport parses it.
|
||||
+const refusing = (code) => () => ({ status: 1, stdout: "", stderr: `${code}\n` });
|
||||
+const HINT = `mosaic: ${OTHER_TREE}\n`;
|
||||
+
|
||||
+test("a human command refused with human-required prints the other-tree line and exits 3", async (t) => {
|
||||
+ const x = io();
|
||||
+ const transport = (business) => humanTransport({ socket: "/nonexistent/bus.sock", business, env: {}, spawn: refusing("human-required") });
|
||||
+ const code = await run(["inbox", "--business", "demo"], x, { system: { dataRoot: tmp(t) }, transport });
|
||||
+ assert.equal(code, 3);
|
||||
+ assert.equal(x.err.text, `mosaic: human-required\n${HINT}`);
|
||||
+ assert.equal(x.out.text, "");
|
||||
+});
|
||||
+
|
||||
+test("a --tester command refused with tester-required prints the other-tree line and exits 3", async (t) => {
|
||||
+ const x = io({ ...humanEnv(), CLAUDECODE: "1" });
|
||||
+ const code = await run(["agents", "--tester", "--business", "demo"], x, { system: { dataRoot: tmp(t) }, testerSpawn: refusing("tester-required") });
|
||||
+ assert.equal(code, 3);
|
||||
+ assert.equal(x.err.text, `mosaic: tester-required\n${HINT}`);
|
||||
+});
|
||||
+
|
||||
+test("other exit-3 refusals print no other-tree line", async (t) => {
|
||||
+ const off = io({ ...humanEnv(), CLAUDECODE: "1" });
|
||||
+ assert.equal(await run(["agents", "--tester", "--business", "demo"], off, { system: { dataRoot: tmp(t) }, testerSpawn: refusing("tester-off") }), 3);
|
||||
+ assert.equal(off.err.text, "mosaic: tester-off\n");
|
||||
+
|
||||
+ const agent = io({ ...humanEnv(), CLAUDECODE: "1" });
|
||||
+ assert.equal(await run(["inbox", "--business", "demo"], agent, { system: { dataRoot: tmp(t) } }), 3);
|
||||
+ assert.match(agent.err.text, /^mosaic: refused: this runs only from a human shell/);
|
||||
+ assert.doesNotMatch(agent.err.text, /tree it runs from/);
|
||||
+});
|
||||
+
|
||||
+test("the line names no path", () => {
|
||||
+ assert.doesNotMatch(OTHER_TREE, /\//);
|
||||
+});
|
||||
@@ -0,0 +1,4 @@
|
||||
34fcfe0efd864c3025144035c824209d7df76e43b5ddac3f60edfdc621cb2194 docs/TOOLS.md
|
||||
5fd51fa683a546ce5fd44aa186224901ccb0e3ef12c385b601e6d8969af7fe29 packages/cli/README.md
|
||||
907723f48c07bda055a4752bd3c0a518fa9b919e311fd2f698837f230c05193d packages/cli/src/cli.mjs
|
||||
f7d75045de7c018d0a3f15f9e0395cf05b0b4510b6cd32549e591b7b87d16b82 packages/cli/tests/other-tree.test.mjs
|
||||
@@ -0,0 +1,4 @@
|
||||
base c6570f142 start 00:27:26
|
||||
pkg-cli rc=0 ℹ pass 191 ℹ fail 0
|
||||
webui-reads rc=0 ℹ pass 23 ℹ fail 0
|
||||
GATE_PASSED 00:27:55
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# Row 130 targeted builder gate (#271, decision 136): the cli suite, and the
|
||||
# webui tests that import reads.mjs, which imports cli.mjs. No
|
||||
# scripts/test-*.sh reads a changed path.
|
||||
set -o pipefail
|
||||
G=~/darkwing-scratch/r130/wt
|
||||
OUT=~/darkwing-scratch/r130/pkt/gate
|
||||
mkdir -p "$OUT"
|
||||
cd "$G" || exit 9
|
||||
[ -d "$G/node_modules" ] || { echo "STOP no node_modules in $G"; exit 8; }
|
||||
echo "base $(git rev-parse --short HEAD) start $(date -u +%H:%M:%S)"
|
||||
run() { local name="$1" dir="$2" cmd="$3"
|
||||
(cd "$G/$dir" && TMPDIR=~/darkwing-scratch/r130/tmp bash -c "$cmd") > "$OUT/$name.log" 2>&1; local rc=$?
|
||||
echo "$name rc=$rc $(grep -E '^ℹ (pass|fail) [0-9]+' "$OUT/$name.log" | tr '\n' ' ')"
|
||||
[ $rc -eq 0 ] || { echo "STOP $name"; exit $rc; }; }
|
||||
run pkg-cli packages/cli 'node --test tests/*.test.mjs'
|
||||
run webui-reads packages/webui 'node --test tests/reads.test.mjs tests/views.test.mjs tests/pinned-copy.test.mjs'
|
||||
echo "GATE_PASSED $(date -u +%H:%M:%S)"
|
||||
@@ -0,0 +1,201 @@
|
||||
✔ every screen and overlay fits 120x36, 80x24 and the narrow layout, row for row (1857.559017ms)
|
||||
✔ width counts terminal cells: wide CJK and emoji 2, U+FE0F after a one-cell base 1, marks 0 (90.923241ms)
|
||||
✔ broker text is shown, never interpreted: no raw control character or bidi override reaches the terminal (218.950627ms)
|
||||
✔ NO_COLOR: no escape codes, except reverse video on the selected row (278.499044ms)
|
||||
✔ 1-9 shows the decide flow's own lines and question; only y resolves (170.221213ms)
|
||||
✔ the app shows the same decide text as mosaic decide (126.209749ms)
|
||||
✔ any key but y or Y at the question answers No; nothing is sent (1427.613405ms)
|
||||
✔ below 40x10 the question is hidden, so every key answers it No, y included (Filbert R3) (845.953577ms)
|
||||
✔ under 40 columns a tall terminal shows the too-small screen even where the decide box would fit; y sends nothing (Filbert N10) (1370.859383ms)
|
||||
✔ a hidden decide box names the way out, and the status line under it does not promise y (Filbert N12) (436.817018ms)
|
||||
✔ a throw while answering the question goes to crash, sends nothing and leaves no job pending (333.788006ms)
|
||||
✔ type-ahead never answers the question: a paste, keys typed before it is drawn, keys inside askMs (1461.075786ms)
|
||||
✔ quitting at the question answers No and sends nothing, armed or not (395.055403ms)
|
||||
✔ a timer tick reads nothing under an overlay, never answers the question, and keeps a resolved status (136.898961ms)
|
||||
✔ a long question is cut inside the question; the action, target, choice and prompt stay (Dewey B1) (410.106575ms)
|
||||
✔ a decide box too big for the terminal says so, and every key answers it No, y included (Dewey B1, round 3) (1236.057157ms)
|
||||
✔ at every size the decide box shows each line y depends on, or says it is hidden (10449.898136ms)
|
||||
✔ outcome unknown: the CLI's sentence, no retry, then the trail or a re-read (165.586567ms)
|
||||
✔ refused and failed reads keep the last list, marked stale; a refused decide says refused (148.546112ms)
|
||||
✔ help or a detail box cut to fit keeps its last line, Esc included (Dewey) (57.216989ms)
|
||||
✔ keys: views, moving, trail of a selection, the command line, help and quit (240.554739ms)
|
||||
✔ one option: the status line says Press 1, not 1-1 (99.280277ms)
|
||||
✔ no business: the app says so and :business picks one (54.186473ms)
|
||||
✔ mosaic with no argument: exit 3 in an agent run, exit 4 with the usage off a terminal, the app on one (1.970097ms)
|
||||
✔ scripts/mosaic with no argument reaches the app entry; the seat usage it printed before has not drifted (179.727157ms)
|
||||
✔ strings to keep exactly live once, in the decide flow and the transport; the app does not restate them (0.476321ms)
|
||||
✔ decide with --note still asks; a No sends nothing (64.94693ms)
|
||||
✔ the app's business comes from a live bus host only (1.142177ms)
|
||||
✔ runApp leaves the terminal as it found it on q, end of input, SIGTERM, SIGINT, SIGHUP and an error, the first frame's included (7.115056ms)
|
||||
✔ draw is pure: same state, same frame (0.788975ms)
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (146.551551ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (144.07419ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (108.402498ms)
|
||||
✔ decide prints a declining choice as declining (124.802835ms)
|
||||
✔ an unknown outcome is reported once and never resent (112.230308ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (163.799469ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (104.723196ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (77.608986ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (96.667544ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (101.576614ms)
|
||||
✔ agents and tasks print through the broker (149.185133ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.986694ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (94.759895ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (111.426375ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (49.658541ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (45.325429ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (46.490754ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (128.882844ms)
|
||||
✔ follow prints an unsolicited message after plain talk returned (204.198612ms)
|
||||
✔ talk --follow continues after reply and prints a later unsolicited message (184.909669ms)
|
||||
✔ inbox lists all unread human messages under decisions, does not deliver or read them (2232.373944ms)
|
||||
✔ follow refuses agent context and bad arguments (111.07556ms)
|
||||
✔ follow Ctrl-C wakes the poll sleep and removes its handler; transport errors also clean up (91.179236ms)
|
||||
✔ plain talk leaves the process SIGINT handlers unchanged (106.382067ms)
|
||||
✔ empty views say so (1.504588ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.732851ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.322891ms)
|
||||
✔ the review loop: the coder pushes and asks, the reviewer fetches and approves, and the task's trail holds each step (968.862063ms)
|
||||
✔ refusals: no action, non-fast-forward, an unknown commit or request, and a push for another task moves only that task's ref (753.27699ms)
|
||||
✔ the size cap refuses a push before any byte (427.261545ms)
|
||||
✔ a fetch over the cap refuses before any byte reaches the reviewer (690.089044ms)
|
||||
✔ the socket refuses a request with no capability, an unknown one, and a bad request line (401.692023ms)
|
||||
✔ a rigged clone runs nothing: hooks, fsmonitor, a filter, a gitlink and a symlinked .git/config leave no marker (793.334326ms)
|
||||
✔ an identity with no role, or of another business, refuses not-a-role and moves no ref (623.462697ms)
|
||||
✔ a review of a project the business no longer has refuses unknown-project (687.581798ms)
|
||||
✔ past maxActive connections the socket refuses git-busy, and a hung-up connection frees its slot (301.593864ms)
|
||||
✔ close() answers host-closing to a connection still sending its request line (377.539946ms)
|
||||
✔ a bundle that trickles in refuses body-timeout at its deadline and frees its slot (755.794012ms)
|
||||
✔ a reviewer gone before its bundle was sent is filed fetch-aborted, not as the host's fault (1488.663651ms)
|
||||
✔ a reviewer that hangs up the moment it has the whole bundle was served, not aborted (1225.223589ms)
|
||||
✔ send: a close after every byte left is served; one that fails a queued write is fetch-aborted (42.084322ms)
|
||||
✔ a bundle with no bytes for bodyIdle refuses body-timeout long before its deadline (817.08035ms)
|
||||
✔ send: a close once the loop is past its last chunk is served, and a failed write there is fetch-aborted (23.609675ms)
|
||||
✔ letGo waits for an answer to leave for at most its cap, then destroys the socket (302.293863ms)
|
||||
✔ a record that fails and can't be undone refuses rollback-failed, which the host logs (522.329676ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1116.24996ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (341.252844ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (198.324365ms)
|
||||
✔ two bus starts racing for one data root leave one host running; the other refuses that a host already runs (659.762948ms)
|
||||
✔ a host takes over a host.lock its dead host left, and puts back a live claim that replaced it meanwhile (1.891988ms)
|
||||
✔ a host.lock that goes between the check and the read is tried again; one that can't be parsed still refuses as unreadable (2.933598ms)
|
||||
✔ a live host's lock stays, however old the file is (0.654458ms)
|
||||
✔ a host.lock left by a host killed with SIGKILL doesn't stop the next bus start (943.8941ms)
|
||||
✔ while a host boots, a reader sees no host: the human commands still ask for --business, stop and status find none (248.543723ms)
|
||||
✔ a host started before host.lock existed, holding only host.json, still refuses a second start, which leaves no lock (36.561302ms)
|
||||
✔ a broker fork that throws leaves no host.lock and no host.json (36.380803ms)
|
||||
✔ a broker send that throws leaves no host.lock and no host.json (38.649239ms)
|
||||
✔ a broker that misses its boot wait leaves no host.lock and no host.json (40.977716ms)
|
||||
✔ a notifier fork that throws stops the broker and leaves no host.lock and no host.json (270.729986ms)
|
||||
✔ a notifier send that throws stops the broker and leaves no host.lock and no host.json (276.678676ms)
|
||||
✔ a boot reply with no reader for the business refuses with exit 3 naming it, before the notifier starts, and leaves no host.lock and no live broker (191.959504ms)
|
||||
✔ a boot reply with no reader for the business refuses with exit 3 naming it, with no notifier, and leaves no host.lock and no live broker (305.928104ms)
|
||||
✔ a host.json that can't be written stops both children and leaves no host.lock (420.10435ms)
|
||||
✔ an error event on the broker before it replies fails the start at once, and leaves no host.lock and no live child (30.975977ms)
|
||||
✔ an error event on the notifier before it replies fails the start at once, and leaves no host.lock and no live child (162.789789ms)
|
||||
✔ an error event on the broker while the notifier starts fails the start once the notifier replies (312.091104ms)
|
||||
✔ an error event on the broker once the host is up stops the host with exit 1, and leaves no host.lock and no host.json (390.008948ms)
|
||||
✔ an error event on the notifier once the host is up stops the host with exit 1, and leaves no host.lock and no host.json (486.094144ms)
|
||||
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1628.349817ms)
|
||||
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (460.582818ms)
|
||||
✔ a broker reply with no request waiting breaks the channel and the host exits 1 (233.104857ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (311.818622ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (388.910278ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (250.57398ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (258.7713ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (166.970554ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (216.968593ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (29.022142ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (203.284613ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (280.901093ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (123.980918ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (812.738025ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (77.960436ms)
|
||||
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (96.652433ms)
|
||||
✔ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (16.404088ms)
|
||||
✔ bus-service.sh refuses a default claude without the execute bit, or in a PATH dir with a space (10.716176ms)
|
||||
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1460.0379ms)
|
||||
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (903.120197ms)
|
||||
✔ each launch makes sure the workspace has a clone of the project: cloned, fast-forwarded, or fetched and logged (2967.941952ms)
|
||||
✔ a project clone that can't be made refuses checkout-failed, from --pm and from a session, with no run and no clone left (265.158388ms)
|
||||
✔ a runner that stops at once ends its launch with the runner's reason (362.09161ms)
|
||||
✔ a Claude Code launch with no session credential refuses before it starts (209.096156ms)
|
||||
✔ a pi launch needs no session credential: pi logs in through its own ENV_ALLOW variables (621.240981ms)
|
||||
✔ a runner killed from the host ends its launch as killed, not failed (491.287888ms)
|
||||
✔ a missing or failing bwrap refuses sandbox-unavailable before anything starts; there is no unconfined fallback (256.537283ms)
|
||||
✔ a real Claude Code coder's shell can't read a stand-in secret or anything else its sandbox doesn't hold (2230.224128ms)
|
||||
✔ 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 (853.217391ms)
|
||||
✔ 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 (3789.381198ms)
|
||||
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (130.127818ms)
|
||||
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (155.508868ms)
|
||||
✔ a launch client that never closes its side doesn't hold the host's close (184.197486ms)
|
||||
✔ a runner that ignores SIGTERM is killed when the host closes (1289.054203ms)
|
||||
✔ claudeVersion asks MOSAIC_CLAUDE_BIN when set, else claude on PATH (8.342349ms)
|
||||
✔ zoned uses the IANA zone across DST (31.111102ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (143.85529ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (131.90741ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.393303ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (119.575338ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (133.004488ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (107.139264ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (159.672944ms)
|
||||
✔ 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 (192.934745ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (131.952964ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (170.540567ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (90.017435ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.507573ms)
|
||||
✔ no Discord id reaches the journal or the log (100.395322ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (17.29805ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (28.719786ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (20.620902ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.971723ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.812811ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (6.260915ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.962219ms)
|
||||
✔ 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.847378ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.696419ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.959088ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.527572ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.214369ms)
|
||||
✔ a human command refused with human-required prints the other-tree line and exits 3 (4.46013ms)
|
||||
✔ a --tester command refused with tester-required prints the other-tree line and exits 3 (2.143911ms)
|
||||
✔ other exit-3 refusals print no other-tree line (2.566135ms)
|
||||
✔ the line names no path (0.17705ms)
|
||||
✔ shared copy: npm ci, pushed reachability, readonly/existing copy, webui delegation (689.025457ms)
|
||||
✔ wrapper preserves Q81 lease and marker, rejects foreign/symlink; check uses effective roots (552.522929ms)
|
||||
✔ bus unit copy/pm pins, install refuses all busy states and failed reads, drop-in refusal (582.721313ms)
|
||||
✔ missing package manifest refuses before npm; npm failure removes the unpublished temp copy (387.917548ms)
|
||||
✔ real pinned install loads bus and Console; same-copy mosaic talk passes proof, other trees refuse (4808.519224ms)
|
||||
✔ bus status reads effective copy/pm/drop-ins without writes; check refuses an overridden ExecStart (616.469443ms)
|
||||
✔ drop-ins in either override field and malformed metadata refuse; uninstall also checks --no-reload (784.480336ms)
|
||||
✔ install never shows a template name on systemd 261; template drop-ins still refuse (259.376092ms)
|
||||
✔ unlisted instance drop-in is found in another unit lookup directory; path discovery errors refuse (234.866869ms)
|
||||
✔ inherited mosaic-.service.d override refuses without changing the unit (283.904225ms)
|
||||
✔ inherited service.d override refuses without changing the unit (413.295324ms)
|
||||
✔ inherited drop-in unreadable directory or file refuses before writing (216.52762ms)
|
||||
✔ talk --tester sends a [mos test] message as tester from inside an agent and prints the role's reply (157.687575ms)
|
||||
✔ talk --tester refuses text without the [mos test] prefix before any call (115.942498ms)
|
||||
✔ the read verbs take --tester inside an agent, and the trail shows the tester as the sender (132.882149ms)
|
||||
✔ decide, stop and launches take no --tester (99.349987ms)
|
||||
✔ the tester transport runs tester-cli.mjs, and the tester's refusals exit 3 (0.346036ms)
|
||||
✔ with no stand-in transport, --tester runs tester-cli.mjs on the data root's socket (0.631123ms)
|
||||
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 (496.62539ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (63.641361ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2240.271227ms)
|
||||
✔ busExit and refuseInsideAgent (0.667845ms)
|
||||
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (227.695871ms)
|
||||
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1126.874499ms)
|
||||
✔ a late reply the Console has listed still reaches the next talk (2137.02312ms)
|
||||
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (123.537327ms)
|
||||
✔ launches off and on go to the broker and change the business's launch state (225.230279ms)
|
||||
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (239.111225ms)
|
||||
ℹ tests 191
|
||||
ℹ suites 0
|
||||
ℹ pass 191
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 22464.36588
|
||||
@@ -0,0 +1,31 @@
|
||||
✔ copy writes the Console paths at the full id, read-only, and leaves an existing copy alone (832.082691ms)
|
||||
✔ copy refuses a commit off refactor, an unknown one, a worktree and a bad copies directory (675.536238ms)
|
||||
✔ copy writes nothing when an archived path is missing at the commit (303.552465ms)
|
||||
✔ a copy that loses the rename race leaves no read-only temp directory behind (396.981883ms)
|
||||
✔ a copy whose rename fails with no target refuses, writes no copy and leaves no temp directory (445.045539ms)
|
||||
✔ a copy that meets a target holding files says exists: and moves nothing into it (585.66412ms)
|
||||
✔ a copy that meets an empty target directory replaces it by rename, not by moving into it (534.862765ms)
|
||||
✔ serve runs from a copy: /healthz 200, the config check, roles and RELEASE from the copy, the queue from --checkout (1214.505224ms)
|
||||
✔ serve refuses a --checkout that is not an absolute directory path (338.760554ms)
|
||||
✔ consoleReads runs the queue read under queueRoot and reads RELEASE under root (78.416931ms)
|
||||
✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (1.469574ms)
|
||||
✔ credential state from the date alone, as the bus states it (0.170558ms)
|
||||
✔ queue: rows, one row, ids and methods, notes; no path or id from a row (535.810046ms)
|
||||
✔ queue: a refused read fails closed with the store's text, redacted (253.014532ms)
|
||||
✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (995.351504ms)
|
||||
✔ business: names, vars on the allowlist, authority, credential metadata only (13.512401ms)
|
||||
✔ business: missing or invalid file refuses with the module's text, redacted (9.949961ms)
|
||||
✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (8.193639ms)
|
||||
✔ settings: release, board, loopback address, notifier binding name only (157.683767ms)
|
||||
✔ task fields: the committed table with no reads configured, GET only; a bad table refuses with no path (18.39597ms)
|
||||
✔ Queue, Business and Settings in their five states, with no move button (3447.829335ms)
|
||||
✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (1896.470804ms)
|
||||
✔ the server log names no config path when the system config refuses (419.936134ms)
|
||||
ℹ tests 23
|
||||
ℹ suites 0
|
||||
ℹ pass 23
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 5880.550952
|
||||
@@ -0,0 +1,4 @@
|
||||
m1-drop-other-tree-line: killed (rc 1, pass 189, fail 2)
|
||||
fail: a --tester command refused with tester-required prints the other-tree line and exits 3 (2.339766ms)
|
||||
fail: a human command refused with human-required prints the other-tree line and exits 3 (5.933705ms)
|
||||
restored: True
|
||||
@@ -0,0 +1 @@
|
||||
m1-drop-other-tree-line killed {'pass': '189', 'fail': '2'}
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Row 130 mutant: drop the extra stderr line from run() in cli.mjs, run the
|
||||
cli suite, and restore the file from .orig with a sha256 check."""
|
||||
import hashlib, os, re, shutil, signal, subprocess
|
||||
|
||||
WT = os.path.expanduser('~/darkwing-scratch/r130/wt')
|
||||
PKT = os.path.expanduser('~/darkwing-scratch/r130/pkt')
|
||||
TMP = os.path.expanduser('~/darkwing-scratch/r130/tmp')
|
||||
F = 'packages/cli/src/cli.mjs'
|
||||
LINE = ' if (OTHER_TREE_CODES.has(error.code)) io.stderr.write(`mosaic: ${OTHER_TREE}\\n`);\n'
|
||||
M = [('m1-drop-other-tree-line', LINE)]
|
||||
|
||||
def sha(p):
|
||||
return hashlib.sha256(open(p, 'rb').read()).hexdigest()
|
||||
|
||||
# Kill only processes left with a cwd under this row's TMPDIR.
|
||||
def sweep():
|
||||
left = []
|
||||
for pid in filter(str.isdigit, os.listdir('/proc')):
|
||||
try:
|
||||
if os.readlink(f'/proc/{pid}/cwd').startswith(TMP + '/'):
|
||||
left.append(int(pid)); os.kill(int(pid), signal.SIGKILL)
|
||||
except OSError:
|
||||
pass
|
||||
return left
|
||||
|
||||
os.chdir(WT)
|
||||
env = dict(os.environ, TMPDIR=TMP, NO_COLOR='1')
|
||||
base = sha(F)
|
||||
os.makedirs(os.path.join(PKT, 'mutants'), exist_ok=True)
|
||||
log = open(os.path.join(PKT, 'mutants-run.log'), 'a')
|
||||
for name, drop in M:
|
||||
src = open(F).read()
|
||||
if src.count(drop) != 1:
|
||||
print(f'{name}: pattern not found once, skipped', file=log, flush=True); print(name, 'BAD PATTERN'); continue
|
||||
shutil.copy2(F, F + '.orig')
|
||||
try:
|
||||
open(F, 'w').write(src.replace(drop, ''))
|
||||
p = subprocess.Popen('node --test tests/*.test.mjs', shell=True, cwd='packages/cli', stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, env=env, start_new_session=True, text=True)
|
||||
try:
|
||||
res, _ = p.communicate(timeout=900)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(p.pid, signal.SIGKILL); res, _ = p.communicate(); res += '\nTIMEOUT\n'
|
||||
open(os.path.join(PKT, 'mutants', f'mutant-{name}.log'), 'w').write(res)
|
||||
fails = sorted({l.strip()[2:] for l in res.splitlines() if l.strip().startswith('✖ ') and l.strip() != '✖ failing tests:'})
|
||||
tally = dict(re.findall(r'^ℹ (pass|fail) (\d+)$', res, re.M))
|
||||
verdict = 'killed' if p.returncode != 0 else 'SURVIVED'
|
||||
print(f'{name}: {verdict} (rc {p.returncode}, pass {tally.get("pass", "?")}, fail {tally.get("fail", "?")})', file=log, flush=True)
|
||||
for l in fails:
|
||||
print(f' fail: {l}', file=log, flush=True)
|
||||
left = sweep()
|
||||
if left:
|
||||
print(f' swept {len(left)} leftover process(es) under TMPDIR: {left}', file=log, flush=True)
|
||||
print(name, verdict, tally, flush=True)
|
||||
finally:
|
||||
shutil.move(F + '.orig', F)
|
||||
assert sha(F) == base, f'{F} not restored'
|
||||
print('restored:', sha(F) == base, file=log, flush=True)
|
||||
@@ -0,0 +1,243 @@
|
||||
✔ every screen and overlay fits 120x36, 80x24 and the narrow layout, row for row (2126.599376ms)
|
||||
✔ width counts terminal cells: wide CJK and emoji 2, U+FE0F after a one-cell base 1, marks 0 (137.152451ms)
|
||||
✔ broker text is shown, never interpreted: no raw control character or bidi override reaches the terminal (269.486077ms)
|
||||
✔ NO_COLOR: no escape codes, except reverse video on the selected row (485.001448ms)
|
||||
✔ 1-9 shows the decide flow's own lines and question; only y resolves (193.124184ms)
|
||||
✔ the app shows the same decide text as mosaic decide (114.223629ms)
|
||||
✔ any key but y or Y at the question answers No; nothing is sent (1953.985898ms)
|
||||
✔ below 40x10 the question is hidden, so every key answers it No, y included (Filbert R3) (781.546334ms)
|
||||
✔ under 40 columns a tall terminal shows the too-small screen even where the decide box would fit; y sends nothing (Filbert N10) (1719.556089ms)
|
||||
✔ a hidden decide box names the way out, and the status line under it does not promise y (Filbert N12) (127.112752ms)
|
||||
✔ a throw while answering the question goes to crash, sends nothing and leaves no job pending (134.330769ms)
|
||||
✔ type-ahead never answers the question: a paste, keys typed before it is drawn, keys inside askMs (1090.306486ms)
|
||||
✔ quitting at the question answers No and sends nothing, armed or not (235.539253ms)
|
||||
✔ a timer tick reads nothing under an overlay, never answers the question, and keeps a resolved status (190.98879ms)
|
||||
✔ a long question is cut inside the question; the action, target, choice and prompt stay (Dewey B1) (190.708121ms)
|
||||
✔ a decide box too big for the terminal says so, and every key answers it No, y included (Dewey B1, round 3) (1522.493996ms)
|
||||
✔ at every size the decide box shows each line y depends on, or says it is hidden (11826.155798ms)
|
||||
✔ outcome unknown: the CLI's sentence, no retry, then the trail or a re-read (198.994614ms)
|
||||
✔ refused and failed reads keep the last list, marked stale; a refused decide says refused (119.613669ms)
|
||||
✔ help or a detail box cut to fit keeps its last line, Esc included (Dewey) (57.656511ms)
|
||||
✔ keys: views, moving, trail of a selection, the command line, help and quit (244.455266ms)
|
||||
✔ one option: the status line says Press 1, not 1-1 (96.865139ms)
|
||||
✔ no business: the app says so and :business picks one (51.671868ms)
|
||||
✔ mosaic with no argument: exit 3 in an agent run, exit 4 with the usage off a terminal, the app on one (2.078217ms)
|
||||
✔ scripts/mosaic with no argument reaches the app entry; the seat usage it printed before has not drifted (197.68834ms)
|
||||
✔ strings to keep exactly live once, in the decide flow and the transport; the app does not restate them (0.602258ms)
|
||||
✔ decide with --note still asks; a No sends nothing (67.480831ms)
|
||||
✔ the app's business comes from a live bus host only (1.119965ms)
|
||||
✔ runApp leaves the terminal as it found it on q, end of input, SIGTERM, SIGINT, SIGHUP and an error, the first frame's included (8.35828ms)
|
||||
✔ draw is pure: same state, same frame (1.325473ms)
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (164.491018ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (129.310483ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (121.197072ms)
|
||||
✔ decide prints a declining choice as declining (118.985719ms)
|
||||
✔ an unknown outcome is reported once and never resent (121.465469ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (159.638204ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (111.30299ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (82.784942ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (102.817672ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (103.310422ms)
|
||||
✔ agents and tasks print through the broker (144.263287ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.256155ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (105.188908ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (150.25924ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (54.331289ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (45.479149ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (43.193713ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (103.192109ms)
|
||||
✔ follow prints an unsolicited message after plain talk returned (219.359777ms)
|
||||
✔ talk --follow continues after reply and prints a later unsolicited message (195.279674ms)
|
||||
✔ inbox lists all unread human messages under decisions, does not deliver or read them (2539.64438ms)
|
||||
✔ follow refuses agent context and bad arguments (69.118021ms)
|
||||
✔ follow Ctrl-C wakes the poll sleep and removes its handler; transport errors also clean up (96.737457ms)
|
||||
✔ plain talk leaves the process SIGINT handlers unchanged (127.228622ms)
|
||||
✔ empty views say so (4.1471ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (6.146011ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.315337ms)
|
||||
✔ the review loop: the coder pushes and asks, the reviewer fetches and approves, and the task's trail holds each step (955.309131ms)
|
||||
✔ refusals: no action, non-fast-forward, an unknown commit or request, and a push for another task moves only that task's ref (924.752065ms)
|
||||
✔ the size cap refuses a push before any byte (532.661715ms)
|
||||
✔ a fetch over the cap refuses before any byte reaches the reviewer (894.523248ms)
|
||||
✔ the socket refuses a request with no capability, an unknown one, and a bad request line (408.585217ms)
|
||||
✔ a rigged clone runs nothing: hooks, fsmonitor, a filter, a gitlink and a symlinked .git/config leave no marker (1345.285237ms)
|
||||
✔ an identity with no role, or of another business, refuses not-a-role and moves no ref (892.035696ms)
|
||||
✔ a review of a project the business no longer has refuses unknown-project (616.829027ms)
|
||||
✔ past maxActive connections the socket refuses git-busy, and a hung-up connection frees its slot (540.694937ms)
|
||||
✔ close() answers host-closing to a connection still sending its request line (367.519159ms)
|
||||
✔ a bundle that trickles in refuses body-timeout at its deadline and frees its slot (781.505759ms)
|
||||
✔ a reviewer gone before its bundle was sent is filed fetch-aborted, not as the host's fault (1187.232064ms)
|
||||
✔ a reviewer that hangs up the moment it has the whole bundle was served, not aborted (1057.814935ms)
|
||||
✔ send: a close after every byte left is served; one that fails a queued write is fetch-aborted (41.877745ms)
|
||||
✔ a bundle with no bytes for bodyIdle refuses body-timeout long before its deadline (709.556775ms)
|
||||
✔ send: a close once the loop is past its last chunk is served, and a failed write there is fetch-aborted (24.146531ms)
|
||||
✔ letGo waits for an answer to leave for at most its cap, then destroys the socket (302.392935ms)
|
||||
✔ a record that fails and can't be undone refuses rollback-failed, which the host logs (461.694373ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1102.59393ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (280.129129ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (253.663722ms)
|
||||
✔ two bus starts racing for one data root leave one host running; the other refuses that a host already runs (857.480299ms)
|
||||
✔ a host takes over a host.lock its dead host left, and puts back a live claim that replaced it meanwhile (5.185736ms)
|
||||
✔ a host.lock that goes between the check and the read is tried again; one that can't be parsed still refuses as unreadable (4.37862ms)
|
||||
✔ a live host's lock stays, however old the file is (1.282987ms)
|
||||
✔ a host.lock left by a host killed with SIGKILL doesn't stop the next bus start (1151.534572ms)
|
||||
✔ while a host boots, a reader sees no host: the human commands still ask for --business, stop and status find none (334.872124ms)
|
||||
✔ a host started before host.lock existed, holding only host.json, still refuses a second start, which leaves no lock (44.528483ms)
|
||||
✔ a broker fork that throws leaves no host.lock and no host.json (56.699553ms)
|
||||
✔ a broker send that throws leaves no host.lock and no host.json (109.798938ms)
|
||||
✔ a broker that misses its boot wait leaves no host.lock and no host.json (47.992746ms)
|
||||
✔ a notifier fork that throws stops the broker and leaves no host.lock and no host.json (278.013536ms)
|
||||
✔ a notifier send that throws stops the broker and leaves no host.lock and no host.json (769.220641ms)
|
||||
✔ a boot reply with no reader for the business refuses with exit 3 naming it, before the notifier starts, and leaves no host.lock and no live broker (274.301695ms)
|
||||
✔ a boot reply with no reader for the business refuses with exit 3 naming it, with no notifier, and leaves no host.lock and no live broker (388.683591ms)
|
||||
✔ a host.json that can't be written stops both children and leaves no host.lock (387.805931ms)
|
||||
✔ an error event on the broker before it replies fails the start at once, and leaves no host.lock and no live child (52.211413ms)
|
||||
✔ an error event on the notifier before it replies fails the start at once, and leaves no host.lock and no live child (479.310143ms)
|
||||
✔ an error event on the broker while the notifier starts fails the start once the notifier replies (434.934157ms)
|
||||
✔ an error event on the broker once the host is up stops the host with exit 1, and leaves no host.lock and no host.json (384.493626ms)
|
||||
✔ an error event on the notifier once the host is up stops the host with exit 1, and leaves no host.lock and no host.json (436.807882ms)
|
||||
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1198.02968ms)
|
||||
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (487.232791ms)
|
||||
✔ a broker reply with no request waiting breaks the channel and the host exits 1 (323.936363ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (340.365502ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (253.051135ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (175.737026ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (245.082631ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (145.387779ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (185.192416ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (27.004412ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (203.687179ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (324.082661ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (129.765017ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (863.547817ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (60.644047ms)
|
||||
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (83.382953ms)
|
||||
✔ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (16.506654ms)
|
||||
✔ bus-service.sh refuses a default claude without the execute bit, or in a PATH dir with a space (11.351179ms)
|
||||
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1494.424574ms)
|
||||
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (1022.105836ms)
|
||||
✔ each launch makes sure the workspace has a clone of the project: cloned, fast-forwarded, or fetched and logged (2825.124713ms)
|
||||
✔ a project clone that can't be made refuses checkout-failed, from --pm and from a session, with no run and no clone left (339.667688ms)
|
||||
✔ a runner that stops at once ends its launch with the runner's reason (443.835703ms)
|
||||
✔ a Claude Code launch with no session credential refuses before it starts (217.634493ms)
|
||||
✔ a pi launch needs no session credential: pi logs in through its own ENV_ALLOW variables (665.85837ms)
|
||||
✔ a runner killed from the host ends its launch as killed, not failed (454.863274ms)
|
||||
✔ a missing or failing bwrap refuses sandbox-unavailable before anything starts; there is no unconfined fallback (268.533001ms)
|
||||
✔ a real Claude Code coder's shell can't read a stand-in secret or anything else its sandbox doesn't hold (1998.61646ms)
|
||||
✔ 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 (1228.467492ms)
|
||||
✔ 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 (3784.772581ms)
|
||||
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (146.141581ms)
|
||||
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (159.521649ms)
|
||||
✔ a launch client that never closes its side doesn't hold the host's close (171.961795ms)
|
||||
✔ a runner that ignores SIGTERM is killed when the host closes (1362.933493ms)
|
||||
✔ claudeVersion asks MOSAIC_CLAUDE_BIN when set, else claude on PATH (6.456531ms)
|
||||
✔ zoned uses the IANA zone across DST (55.750993ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (144.155863ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (124.785784ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.332063ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (131.890183ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (108.451123ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (120.54262ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (166.423783ms)
|
||||
✔ 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 (183.382344ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (154.482204ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (164.952452ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (101.488064ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (1.571298ms)
|
||||
✔ no Discord id reaches the journal or the log (127.187423ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (28.896934ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (48.76524ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (25.220053ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.246813ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.947158ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (10.947939ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.782983ms)
|
||||
✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (1.159513ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.93305ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (1.197222ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.679964ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (113.384514ms)
|
||||
✖ a human command refused with human-required prints the other-tree line and exits 3 (5.933705ms)
|
||||
✖ a --tester command refused with tester-required prints the other-tree line and exits 3 (2.339766ms)
|
||||
✔ other exit-3 refusals print no other-tree line (2.345028ms)
|
||||
✔ the line names no path (0.205149ms)
|
||||
✔ shared copy: npm ci, pushed reachability, readonly/existing copy, webui delegation (720.489938ms)
|
||||
✔ wrapper preserves Q81 lease and marker, rejects foreign/symlink; check uses effective roots (572.265921ms)
|
||||
✔ bus unit copy/pm pins, install refuses all busy states and failed reads, drop-in refusal (761.185488ms)
|
||||
✔ missing package manifest refuses before npm; npm failure removes the unpublished temp copy (513.504898ms)
|
||||
✔ real pinned install loads bus and Console; same-copy mosaic talk passes proof, other trees refuse (6440.649884ms)
|
||||
✔ bus status reads effective copy/pm/drop-ins without writes; check refuses an overridden ExecStart (715.405652ms)
|
||||
✔ drop-ins in either override field and malformed metadata refuse; uninstall also checks --no-reload (225.350953ms)
|
||||
✔ install never shows a template name on systemd 261; template drop-ins still refuse (316.020892ms)
|
||||
✔ unlisted instance drop-in is found in another unit lookup directory; path discovery errors refuse (208.983578ms)
|
||||
✔ inherited mosaic-.service.d override refuses without changing the unit (228.972846ms)
|
||||
✔ inherited service.d override refuses without changing the unit (251.602313ms)
|
||||
✔ inherited drop-in unreadable directory or file refuses before writing (196.752606ms)
|
||||
✔ talk --tester sends a [mos test] message as tester from inside an agent and prints the role's reply (181.536765ms)
|
||||
✔ talk --tester refuses text without the [mos test] prefix before any call (105.182813ms)
|
||||
✔ the read verbs take --tester inside an agent, and the trail shows the tester as the sender (129.405888ms)
|
||||
✔ decide, stop and launches take no --tester (103.763208ms)
|
||||
✔ the tester transport runs tester-cli.mjs, and the tester's refusals exit 3 (0.487123ms)
|
||||
✔ with no stand-in transport, --tester runs tester-cli.mjs on the data root's socket (0.672406ms)
|
||||
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 (512.357846ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (72.48581ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2243.626787ms)
|
||||
✔ busExit and refuseInsideAgent (0.639091ms)
|
||||
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (232.210166ms)
|
||||
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1142.47659ms)
|
||||
✔ a late reply the Console has listed still reaches the next talk (2198.206644ms)
|
||||
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (94.471814ms)
|
||||
✔ launches off and on go to the broker and change the business's launch state (122.983617ms)
|
||||
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (102.735522ms)
|
||||
ℹ tests 191
|
||||
ℹ suites 0
|
||||
ℹ pass 189
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 24365.790052
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at tests/other-tree.test.mjs:15:1
|
||||
✖ a human command refused with human-required prints the other-tree line and exits 3 (5.933705ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'mosaic: human-required\n'
|
||||
- 'mosaic: human-required\n' +
|
||||
- 'mosaic: the bus accepts these commands only from the tree it runs from; on a pinned host run `mosaic` on PATH\n'
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r130/wt/packages/cli/tests/other-tree.test.mjs:20:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'mosaic: human-required\n',
|
||||
expected: 'mosaic: human-required\nmosaic: the bus accepts these commands only from the tree it runs from; on a pinned host run `mosaic` on PATH\n',
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/other-tree.test.mjs:24:1
|
||||
✖ a --tester command refused with tester-required prints the other-tree line and exits 3 (2.339766ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'mosaic: tester-required\n'
|
||||
- 'mosaic: tester-required\n' +
|
||||
- 'mosaic: the bus accepts these commands only from the tree it runs from; on a pinned host run `mosaic` on PATH\n'
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r130/wt/packages/cli/tests/other-tree.test.mjs:28:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'mosaic: tester-required\n',
|
||||
expected: 'mosaic: tester-required\nmosaic: the bus accepts these commands only from the tree it runs from; on a pinned host run `mosaic` on PATH\n',
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
✔ a human command refused with human-required prints the other-tree line and exits 3 (2.167253ms)
|
||||
✔ a --tester command refused with tester-required prints the other-tree line and exits 3 (0.743277ms)
|
||||
✔ other exit-3 refusals print no other-tree line (1.064608ms)
|
||||
✔ the line names no path (0.082266ms)
|
||||
ℹ tests 4
|
||||
ℹ suites 0
|
||||
ℹ pass 4
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 99.661689
|
||||
@@ -0,0 +1 @@
|
||||
DIGESTS_OK
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# Rebuild row 130's candidate: export BASE, apply build.patch, check every
|
||||
# digest in candidate-manifest.sha256. Run from the packet directory.
|
||||
set -euo pipefail
|
||||
BASE=c6570f142176fdabbdafb635f50fe75c4e66905a
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
X=$(mktemp -d)
|
||||
trap 'rm -rf "$X"' EXIT
|
||||
git -C /mnt/storage/src/mosaic-stack archive "$BASE" | tar -x -C "$X"
|
||||
cd "$X"
|
||||
git apply "$HERE/build.patch"
|
||||
sha256sum -c --quiet "$HERE/candidate-manifest.sha256" && echo DIGESTS_OK
|
||||
@@ -0,0 +1,8 @@
|
||||
2026-10-15T00:28:00Z
|
||||
mosaic-launch-JIFGUI 2026-10-14 19:27:07.328937410 -0500 6 entries
|
||||
mosaic-launch-Pv0cP0 2026-10-14 19:27:36.636640357 -0500 6 entries
|
||||
mosaic-launch-qhgEek 2026-10-14 19:27:29.239959379 -0500 6 entries
|
||||
mosaic-launch-tkNbuj 2026-10-14 19:27:33.670224955 -0500 6 entries
|
||||
mosaic-launch-uv0tYT 2026-10-14 19:27:04.658813937 -0500 6 entries
|
||||
mosaic-launch-VwKtTm 2026-10-14 19:27:00.164788011 -0500 6 entries
|
||||
2026-10-15T00:28:06Z removed the 6 mosaic-launch-* dirs above (from the cli suite, 3 per run, #1624); TMPDIR now has 0 entries; /proc cwd scan under TMPDIR found no process
|
||||
Reference in new issue
Block a user