docs(review): row 41 slice 1 S6 round 1 review, changes (darkwing)
R1 and R3 block: a late broker reply shifts later replies on the host IPC channel, and a dangling workspace symlink passes the gate. R2 (close waits on an open launch.sock client) should be fixed in the same round. Probes, mutants and suite outputs alongside. Comment 26995 on #1523, queue rev 247. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,117 @@
|
|||||||
|
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (102.91821ms)
|
||||||
|
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (121.245477ms)
|
||||||
|
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (104.328016ms)
|
||||||
|
✔ decide prints a declining choice as declining (98.264991ms)
|
||||||
|
✔ an unknown outcome is reported once and never resent (87.04748ms)
|
||||||
|
✔ a decision closed before the answer arrives exits 2 and points at its trail (81.339494ms)
|
||||||
|
✔ a prefix that matches two open decisions exits 2 and resolves neither (92.410729ms)
|
||||||
|
✔ without --business a command uses the live host's business, and a stale host.json is not a host (85.608463ms)
|
||||||
|
✔ every human command refuses inside an agent run before it touches the bus (69.207672ms)
|
||||||
|
✔ usage errors exit 4; no business and no host is a usage error (83.827733ms)
|
||||||
|
✔ agents and tasks print through the broker (66.23017ms)
|
||||||
|
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.136984ms)
|
||||||
|
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (53.128351ms)
|
||||||
|
✔ trackers come from the tracker.* variables of the one project that names a tracker project (33.918861ms)
|
||||||
|
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (32.322342ms)
|
||||||
|
✔ two projects that each name a tracker project refuse, since the boot shape holds one (29.633405ms)
|
||||||
|
✔ a business without tracker.baseUrl gets no trackers entry (28.530151ms)
|
||||||
|
✔ an unknown business and a broken system config refuse with exit 3 (49.933711ms)
|
||||||
|
✔ empty views say so (0.95895ms)
|
||||||
|
✔ the trail keeps the broker's order and names a decision's task without its rows (1.176214ms)
|
||||||
|
✔ tasks print the tracker fields the snapshot carries (0.206492ms)
|
||||||
|
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (897.060907ms)
|
||||||
|
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (218.96706ms)
|
||||||
|
✔ a second host for the same data root refuses with exit 3 while the first runs (166.998187ms)
|
||||||
|
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (240.380587ms)
|
||||||
|
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (176.050616ms)
|
||||||
|
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (99.085127ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (157.614738ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (106.266039ms)
|
||||||
|
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (172.254634ms)
|
||||||
|
✔ watchChildren reports a child that died before it was called, and one that dies later (24.320455ms)
|
||||||
|
✔ bus stop refuses to signal a live pid that is not a bus host (203.668627ms)
|
||||||
|
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (209.251282ms)
|
||||||
|
✔ bus start refuses with exit 3 without a notifier config (90.631351ms)
|
||||||
|
✔ bus start runs until bus stop; status reports it while it runs (651.145497ms)
|
||||||
|
✔ bus-service.sh renders the unit and installs it into a given directory (26.252918ms)
|
||||||
|
✖ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (30261.274586ms)
|
||||||
|
✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (18484.12709ms)
|
||||||
|
✔ a runner that stops at once ends its launch with the runner's reason (192.822298ms)
|
||||||
|
✔ 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 (30672.073519ms)
|
||||||
|
✔ 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 (33689.424955ms)
|
||||||
|
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (170.672928ms)
|
||||||
|
✔ zoned uses the IANA zone across DST (24.830415ms)
|
||||||
|
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (116.216116ms)
|
||||||
|
✔ two blocking decisions get two DMs with different nonces (115.607596ms)
|
||||||
|
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.182767ms)
|
||||||
|
✔ a failed DM is journaled, backs off, and is retried until it lands (108.508712ms)
|
||||||
|
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (94.134315ms)
|
||||||
|
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (100.255285ms)
|
||||||
|
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (94.210215ms)
|
||||||
|
✔ 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 (164.847367ms)
|
||||||
|
✔ a restart after the second refusal does not send before that refusal's 30 min are up (129.956437ms)
|
||||||
|
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (114.751541ms)
|
||||||
|
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (46.672456ms)
|
||||||
|
✔ an inbox read failure is logged and the next poll retries (0.651746ms)
|
||||||
|
✔ no Discord id reaches the journal or the log (74.269566ms)
|
||||||
|
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (13.045248ms)
|
||||||
|
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (31.448374ms)
|
||||||
|
✔ the journal: a whole file that is one torn line truncates to empty (25.364473ms)
|
||||||
|
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.986317ms)
|
||||||
|
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.773563ms)
|
||||||
|
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (5.098288ms)
|
||||||
|
✔ the journal: a symlinked directory refuses and says it is a link (0.437801ms)
|
||||||
|
✔ 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.763012ms)
|
||||||
|
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.638287ms)
|
||||||
|
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.733887ms)
|
||||||
|
✔ digest content stays within Discord's 2000 characters (0.483725ms)
|
||||||
|
✔ runLoop never overlaps ticks and stops after the one in flight (111.972535ms)
|
||||||
|
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 (365.138347ms)
|
||||||
|
✔ the transport writes {business, verb, args} to the child and reads its JSON (41.26363ms)
|
||||||
|
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2139.875576ms)
|
||||||
|
✔ busExit and refuseInsideAgent (0.410677ms)
|
||||||
|
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (201.635337ms)
|
||||||
|
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1135.61941ms)
|
||||||
|
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (70.768797ms)
|
||||||
|
✔ launches off and on go to the broker and change the business's launch state (75.752156ms)
|
||||||
|
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (56.90734ms)
|
||||||
|
ℹ tests 77
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 75
|
||||||
|
ℹ fail 2
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 113545.957339
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/cli/tests/launcher.test.mjs:105:1
|
||||||
|
✖ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (30261.274586ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||||
|
+ actual - expected
|
||||||
|
|
||||||
|
+ 'killed'
|
||||||
|
- 'stopped'
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:171:10)
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||||
|
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: 'killed',
|
||||||
|
expected: 'stopped',
|
||||||
|
operator: 'strictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
|
|
||||||
|
test at packages/cli/tests/launcher.test.mjs:175:1
|
||||||
|
✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (18484.12709ms)
|
||||||
|
Error: timed out waiting
|
||||||
|
at until (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:43:9)
|
||||||
|
at async TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:215:3)
|
||||||
|
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
✔ sessionModel: agent vars win, then the system's execution settings (14.060701ms)
|
||||||
|
✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (6.264076ms)
|
||||||
|
✔ a claude-code bundle adds the wrapped gate hook and the MCP config (2.839664ms)
|
||||||
|
✔ a bundle is written once: an existing file refuses (3.015861ms)
|
||||||
|
✔ a path with a single quote can't go into the hook command (2.544498ms)
|
||||||
|
✔ allow exits 0, a deny exits 2 with the reason on stderr (132.672334ms)
|
||||||
|
✔ a missing or wrong policy, or a bad event, exits 2 (87.579364ms)
|
||||||
|
✔ the bundle's wrapped command: a missing gate or node still blocks (1096.85555ms)
|
||||||
|
✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (822.993933ms)
|
||||||
|
✔ claude: the hook alone blocks a path outside the workspace (474.272951ms)
|
||||||
|
✔ claude: a second turn resumes the first turn's session (795.497289ms)
|
||||||
|
✔ claude: a missing hook or MCP file refuses before claude starts (7.958534ms)
|
||||||
|
✔ pi: policy tools and typed tools pass, anything else is blocked (2.905317ms)
|
||||||
|
✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.824151ms)
|
||||||
|
✔ file tool paths must resolve inside the workspace (0.924751ms)
|
||||||
|
✔ pi's own path normalisation can't be used to step out (0.989216ms)
|
||||||
|
✔ a symlink inside the workspace that points out is outside (0.928767ms)
|
||||||
|
✔ claude path fields per tool (1.135022ms)
|
||||||
|
✔ glob patterns stay inside the workspace (0.905393ms)
|
||||||
|
✔ a path that can't be checked is blocked (0.641327ms)
|
||||||
|
✔ initialize, ping and tools/list (52.803314ms)
|
||||||
|
✔ tools/call goes through the tool socket; a refusal is an isError result (36.588106ms)
|
||||||
|
✔ unknown tools and methods are JSON-RPC errors and never reach the socket (33.504222ms)
|
||||||
|
✔ a missing argument is a usage error (31.403397ms)
|
||||||
|
✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (415.995746ms)
|
||||||
|
✔ pi: a missing extension refuses before any model call (8.458682ms)
|
||||||
|
✔ pi: an extension without its configuration fails pi's start (279.315528ms)
|
||||||
|
✔ founderCheck: founder variables, then a needed service without a usable token (1.858056ms)
|
||||||
|
✔ turnRequest names the sender, class, reply and decision (0.265717ms)
|
||||||
|
✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (193.748977ms)
|
||||||
|
✖ a SIGTERM before the claim stops the runner with exit 0 and no claim (92.328678ms)
|
||||||
|
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (443.407365ms)
|
||||||
|
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1296.372554ms)
|
||||||
|
✖ SIGTERM during a turn kills the turn's process group and still exits 0 (144.59476ms)
|
||||||
|
✔ founder credentials stop before the claim (20) (171.480323ms)
|
||||||
|
✔ a refused claim exits 21; an ended run's capability exits 22 (197.025228ms)
|
||||||
|
✔ the launch ending under a running session exits 22 (142.65891ms)
|
||||||
|
✔ a broker that stays unreachable exits 23 after brokerRetries polls (248.859517ms)
|
||||||
|
✔ a broker that is down at the claim exits 23, not 21 (106.087394ms)
|
||||||
|
✔ no capability, or a malformed one, on stdin exits 2 (164.168036ms)
|
||||||
|
✔ the PM gets launch, its task verbs and the reads (7.809666ms)
|
||||||
|
✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.544128ms)
|
||||||
|
✔ launch only when the business's launch block names the instance as launcher (2.508715ms)
|
||||||
|
✔ an action outside the instance's authority has no tool (3.166666ms)
|
||||||
|
✔ callTool: one JSON line out, the result back, a refusal rejects (11.382644ms)
|
||||||
|
ℹ tests 45
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 42
|
||||||
|
ℹ fail 3
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 10318.14779
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/harness/tests/runner.test.mjs:141:1
|
||||||
|
✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (193.748977ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: runner: demo/coder claimed by run coder-run
|
||||||
|
|
||||||
|
|
||||||
|
null !== 0
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/harness/tests/runner.test.mjs:160:10)
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||||
|
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: false,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: null,
|
||||||
|
expected: 0,
|
||||||
|
operator: 'strictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
|
|
||||||
|
test at packages/harness/tests/runner.test.mjs:166:1
|
||||||
|
✖ a SIGTERM before the claim stops the runner with exit 0 and no claim (92.328678ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
|
||||||
|
+ actual - expected
|
||||||
|
|
||||||
|
[
|
||||||
|
- 0,
|
||||||
|
null,
|
||||||
|
+ 'SIGTERM'
|
||||||
|
]
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/harness/tests/runner.test.mjs:185:10)
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||||
|
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: [ null, 'SIGTERM' ],
|
||||||
|
expected: [ 0, null ],
|
||||||
|
operator: 'deepStrictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
|
|
||||||
|
test at packages/harness/tests/runner.test.mjs:232:1
|
||||||
|
✖ SIGTERM during a turn kills the turn's process group and still exits 0 (144.59476ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: runner: demo/coder claimed by run coder-run
|
||||||
|
|
||||||
|
|
||||||
|
null !== 0
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/harness/tests/runner.test.mjs:242:10)
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||||
|
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: false,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: null,
|
||||||
|
expected: 0,
|
||||||
|
operator: 'strictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (184.047781ms)
|
||||||
|
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (257.734218ms)
|
||||||
|
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (278.500051ms)
|
||||||
|
✔ launch events require a human CLI capability; generic emit cannot forge authority events (165.068907ms)
|
||||||
|
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (161.335881ms)
|
||||||
|
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (159.243496ms)
|
||||||
|
✔ task action subjects and linked decision trail are complete and ordered (176.904294ms)
|
||||||
|
✔ launch binding is durable and reconnecting requires the identical trusted record (109.155948ms)
|
||||||
|
✔ business isolation includes inherited object names and cross-business message references (174.305239ms)
|
||||||
|
✔ authority never transfers between action, run, target, unresolved or replaced role holder (270.690431ms)
|
||||||
|
✔ task projection uses schema current view, skipping earlier and equal-start polls (127.365414ms)
|
||||||
|
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (183.630887ms)
|
||||||
|
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (137.706215ms)
|
||||||
|
✔ both arbiters require human resolution when their cross-role route is themselves (271.610917ms)
|
||||||
|
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (1.748448ms)
|
||||||
|
✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.508833ms)
|
||||||
|
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.886979ms)
|
||||||
|
✔ expiry refuses use and env references never become client data (0.832119ms)
|
||||||
|
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.473483ms)
|
||||||
|
✔ opaque tokens shorter than 16 characters refuse before use (0.359737ms)
|
||||||
|
✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (133.186192ms)
|
||||||
|
✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (129.549343ms)
|
||||||
|
✖ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (152.425639ms)
|
||||||
|
✔ endLaunch leaves a claim another run took alone (181.719438ms)
|
||||||
|
✔ refuse records action.refused against the caller with the code only (145.867162ms)
|
||||||
|
✔ launches off refuses role.launch with launch-revoked until launches on (176.571624ms)
|
||||||
|
✔ broker process: launch ops authorize role.launch, record refusals and end runs (214.227705ms)
|
||||||
|
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.122501ms)
|
||||||
|
✔ process reader gets own kernel identity without exposing environment values (0.56463ms)
|
||||||
|
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.712195ms)
|
||||||
|
✔ real pid 1 remains inspectable when its environment is protected (0.361585ms)
|
||||||
|
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (199.323709ms)
|
||||||
|
✔ missing and foreign-business citations refuse and roll back message and grant (191.07183ms)
|
||||||
|
✔ cross-role sends still need a matching resolved decision and consume it once (246.333185ms)
|
||||||
|
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (188.169376ms)
|
||||||
|
✔ startup token refusal returns safe code without value or partial listening broker (41.838933ms)
|
||||||
|
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (166.55587ms)
|
||||||
|
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (176.255856ms)
|
||||||
|
✔ trusted host registers later launches; socket clients never have a registration verb (178.685487ms)
|
||||||
|
✔ runtime excludes declared project roots even when host supplies no repoRoots (36.737248ms)
|
||||||
|
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (165.960804ms)
|
||||||
|
✔ v3b prototype refusals, views and append-only mutations (1032.22821ms)
|
||||||
|
✔ gated approval authorizes once, survives store reopen, and fresh approval works (249.596775ms)
|
||||||
|
✔ another run cannot consume an approval; a failed check leaves it usable (224.168828ms)
|
||||||
|
✔ two scheduled callers have exactly one grant and one consumed refusal (170.880883ms)
|
||||||
|
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (308.703039ms)
|
||||||
|
✔ class drift gated to cross-role refuses before consumption (198.719962ms)
|
||||||
|
✔ class drift cross-role to gated refuses before consumption (226.403993ms)
|
||||||
|
✔ class drift gated to within-role refuses before consumption (207.238596ms)
|
||||||
|
✔ class drift cross-role to within-role refuses before consumption (227.695738ms)
|
||||||
|
✔ class drift within-role to gated refuses before consumption (194.611101ms)
|
||||||
|
✔ class drift within-role to cross-role refuses before consumption (170.531727ms)
|
||||||
|
✔ message.send consumes approval and prevents a later send or authorize (209.047544ms)
|
||||||
|
✔ role.revoke consumes approval and prevents a later revoke or authorize (321.964213ms)
|
||||||
|
✔ creates private WAL store and excludes a second writer until explicit close (137.579044ms)
|
||||||
|
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (172.969185ms)
|
||||||
|
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (191.788232ms)
|
||||||
|
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (171.439266ms)
|
||||||
|
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (105.58886ms)
|
||||||
|
✔ async transactions refuse before invoking their function (85.925133ms)
|
||||||
|
✔ recordTask keeps sync reads and a role write apart (184.805478ms)
|
||||||
|
✔ read_at must be one canonical UTC format, so the projection compares strings safely (108.899917ms)
|
||||||
|
✔ a bad entry refuses the whole record (103.269817ms)
|
||||||
|
✔ taskView reads the projection for one business (126.877852ms)
|
||||||
|
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (239.547908ms)
|
||||||
|
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (395.224422ms)
|
||||||
|
✔ without an adapter the server refuses every task verb (198.533632ms)
|
||||||
|
✔ the runtime refuses an invalid adapter and closes a valid one (212.245707ms)
|
||||||
|
✔ the process loads the S3 adapter from plain-data trackers (258.328732ms)
|
||||||
|
✔ socket capability stamps launch identity; shared views use wire, no SQL client (160.333179ms)
|
||||||
|
✔ two wire claims serialize; a lost reply never automatically retries (180.845064ms)
|
||||||
|
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (99.411062ms)
|
||||||
|
✔ client preserves UTF-8 when a response divides a multibyte character (11.674905ms)
|
||||||
|
✔ committed mutation followed by dropped reply reports unknown and is never retried (135.16461ms)
|
||||||
|
ℹ tests 74
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 73
|
||||||
|
ℹ fail 1
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 2789.931252
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/bus/tests/end-launch.test.mjs:73:1
|
||||||
|
✖ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (152.425639ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/bus/tests/end-launch.test.mjs:80:10)
|
||||||
|
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||||
|
at Test.run (node:internal/test_runner/test:1402:25)
|
||||||
|
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||||
|
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||||
|
at Test.run (node:internal/test_runner/test:1467:12)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: false,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: undefined,
|
||||||
|
expected: /unknown-run/,
|
||||||
|
operator: 'throws',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (175.975906ms)
|
||||||
|
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (184.05482ms)
|
||||||
|
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (135.975643ms)
|
||||||
|
✔ decide prints a declining choice as declining (115.597908ms)
|
||||||
|
✔ an unknown outcome is reported once and never resent (107.994328ms)
|
||||||
|
✔ a decision closed before the answer arrives exits 2 and points at its trail (114.399879ms)
|
||||||
|
✔ a prefix that matches two open decisions exits 2 and resolves neither (118.753309ms)
|
||||||
|
✔ without --business a command uses the live host's business, and a stale host.json is not a host (98.356969ms)
|
||||||
|
✔ every human command refuses inside an agent run before it touches the bus (96.122047ms)
|
||||||
|
✔ usage errors exit 4; no business and no host is a usage error (104.825903ms)
|
||||||
|
✔ agents and tasks print through the broker (109.999782ms)
|
||||||
|
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.3094ms)
|
||||||
|
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (38.79278ms)
|
||||||
|
✔ trackers come from the tracker.* variables of the one project that names a tracker project (37.77004ms)
|
||||||
|
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (32.111855ms)
|
||||||
|
✔ two projects that each name a tracker project refuse, since the boot shape holds one (32.831006ms)
|
||||||
|
✔ a business without tracker.baseUrl gets no trackers entry (28.202059ms)
|
||||||
|
✔ an unknown business and a broken system config refuse with exit 3 (56.936951ms)
|
||||||
|
✔ empty views say so (0.745144ms)
|
||||||
|
✔ the trail keeps the broker's order and names a decision's task without its rows (1.076708ms)
|
||||||
|
✔ tasks print the tracker fields the snapshot carries (0.205381ms)
|
||||||
|
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (950.611218ms)
|
||||||
|
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (227.116354ms)
|
||||||
|
✔ a second host for the same data root refuses with exit 3 while the first runs (176.539657ms)
|
||||||
|
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (211.488732ms)
|
||||||
|
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (225.739981ms)
|
||||||
|
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (176.746236ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (269.362482ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (165.128453ms)
|
||||||
|
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (259.705176ms)
|
||||||
|
✔ watchChildren reports a child that died before it was called, and one that dies later (24.783932ms)
|
||||||
|
✔ bus stop refuses to signal a live pid that is not a bus host (205.447122ms)
|
||||||
|
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (219.571849ms)
|
||||||
|
✔ bus start refuses with exit 3 without a notifier config (107.523707ms)
|
||||||
|
✔ bus start runs until bus stop; status reports it while it runs (731.219938ms)
|
||||||
|
✔ bus-service.sh renders the unit and installs it into a given directory (38.872725ms)
|
||||||
|
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (331.308668ms)
|
||||||
|
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (667.342883ms)
|
||||||
|
✔ a runner that stops at once ends its launch with the runner's reason (255.258443ms)
|
||||||
|
✔ 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 (843.785719ms)
|
||||||
|
✔ 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 (3808.232229ms)
|
||||||
|
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (167.609852ms)
|
||||||
|
✔ zoned uses the IANA zone across DST (13.817706ms)
|
||||||
|
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (148.840172ms)
|
||||||
|
✔ two blocking decisions get two DMs with different nonces (156.482282ms)
|
||||||
|
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.184902ms)
|
||||||
|
✔ a failed DM is journaled, backs off, and is retried until it lands (155.373324ms)
|
||||||
|
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (119.234353ms)
|
||||||
|
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (114.466929ms)
|
||||||
|
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (120.568701ms)
|
||||||
|
✔ 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 (173.689096ms)
|
||||||
|
✔ a restart after the second refusal does not send before that refusal's 30 min are up (139.718846ms)
|
||||||
|
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (137.675725ms)
|
||||||
|
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (114.287003ms)
|
||||||
|
✔ an inbox read failure is logged and the next poll retries (0.671341ms)
|
||||||
|
✔ no Discord id reaches the journal or the log (113.963173ms)
|
||||||
|
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (22.026662ms)
|
||||||
|
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (42.879376ms)
|
||||||
|
✔ the journal: a whole file that is one torn line truncates to empty (19.168903ms)
|
||||||
|
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (3.911022ms)
|
||||||
|
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.753617ms)
|
||||||
|
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (4.426854ms)
|
||||||
|
✔ the journal: a symlinked directory refuses and says it is a link (0.578878ms)
|
||||||
|
✔ 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.669505ms)
|
||||||
|
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.410289ms)
|
||||||
|
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.471842ms)
|
||||||
|
✔ digest content stays within Discord's 2000 characters (0.330777ms)
|
||||||
|
✔ runLoop never overlaps ticks and stops after the one in flight (111.304879ms)
|
||||||
|
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 (411.263109ms)
|
||||||
|
✔ the transport writes {business, verb, args} to the child and reads its JSON (32.559089ms)
|
||||||
|
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2156.033595ms)
|
||||||
|
✔ busExit and refuseInsideAgent (0.434997ms)
|
||||||
|
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (295.088871ms)
|
||||||
|
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1177.149209ms)
|
||||||
|
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (89.322782ms)
|
||||||
|
✔ launches off and on go to the broker and change the business's launch state (112.778973ms)
|
||||||
|
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (164.101361ms)
|
||||||
|
ℹ tests 77
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 77
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 6161.026277
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (292.561005ms)
|
||||||
|
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (277.551361ms)
|
||||||
|
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (497.078545ms)
|
||||||
|
✔ launch events require a human CLI capability; generic emit cannot forge authority events (226.410623ms)
|
||||||
|
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (200.406094ms)
|
||||||
|
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (167.322227ms)
|
||||||
|
✔ task action subjects and linked decision trail are complete and ordered (167.929413ms)
|
||||||
|
✔ launch binding is durable and reconnecting requires the identical trusted record (115.499176ms)
|
||||||
|
✔ business isolation includes inherited object names and cross-business message references (191.409782ms)
|
||||||
|
✔ authority never transfers between action, run, target, unresolved or replaced role holder (274.594256ms)
|
||||||
|
✔ task projection uses schema current view, skipping earlier and equal-start polls (108.292348ms)
|
||||||
|
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (189.599503ms)
|
||||||
|
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (122.287696ms)
|
||||||
|
✔ both arbiters require human resolution when their cross-role route is themselves (209.820218ms)
|
||||||
|
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.954793ms)
|
||||||
|
✔ only validated broker references load; returned data and exceptions cannot expose a known token (6.947483ms)
|
||||||
|
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (3.716844ms)
|
||||||
|
✔ expiry refuses use and env references never become client data (1.082525ms)
|
||||||
|
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.931891ms)
|
||||||
|
✔ opaque tokens shorter than 16 characters refuse before use (0.446136ms)
|
||||||
|
✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (247.796173ms)
|
||||||
|
✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (130.726344ms)
|
||||||
|
✖ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (157.253366ms)
|
||||||
|
✔ endLaunch leaves a claim another run took alone (289.241075ms)
|
||||||
|
✔ refuse records action.refused against the caller with the code only (251.654803ms)
|
||||||
|
✔ launches off refuses role.launch with launch-revoked until launches on (250.720391ms)
|
||||||
|
✔ broker process: launch ops authorize role.launch, record refusals and end runs (262.370088ms)
|
||||||
|
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (3.987867ms)
|
||||||
|
✔ process reader gets own kernel identity without exposing environment values (0.776973ms)
|
||||||
|
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.162267ms)
|
||||||
|
✔ real pid 1 remains inspectable when its environment is protected (0.450215ms)
|
||||||
|
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (318.86415ms)
|
||||||
|
✔ missing and foreign-business citations refuse and roll back message and grant (196.815379ms)
|
||||||
|
✔ cross-role sends still need a matching resolved decision and consume it once (373.987519ms)
|
||||||
|
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (309.978292ms)
|
||||||
|
✔ startup token refusal returns safe code without value or partial listening broker (40.260658ms)
|
||||||
|
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (212.318126ms)
|
||||||
|
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (284.283419ms)
|
||||||
|
✔ trusted host registers later launches; socket clients never have a registration verb (331.796722ms)
|
||||||
|
✔ runtime excludes declared project roots even when host supplies no repoRoots (50.863608ms)
|
||||||
|
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (199.936442ms)
|
||||||
|
✔ v3b prototype refusals, views and append-only mutations (1474.221154ms)
|
||||||
|
✔ gated approval authorizes once, survives store reopen, and fresh approval works (370.765558ms)
|
||||||
|
✔ another run cannot consume an approval; a failed check leaves it usable (254.968901ms)
|
||||||
|
✔ two scheduled callers have exactly one grant and one consumed refusal (269.788235ms)
|
||||||
|
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (497.173983ms)
|
||||||
|
✔ class drift gated to cross-role refuses before consumption (223.510713ms)
|
||||||
|
✔ class drift cross-role to gated refuses before consumption (231.466212ms)
|
||||||
|
✔ class drift gated to within-role refuses before consumption (212.371643ms)
|
||||||
|
✔ class drift cross-role to within-role refuses before consumption (210.13879ms)
|
||||||
|
✔ class drift within-role to gated refuses before consumption (186.617229ms)
|
||||||
|
✔ class drift within-role to cross-role refuses before consumption (166.777678ms)
|
||||||
|
✔ message.send consumes approval and prevents a later send or authorize (201.323203ms)
|
||||||
|
✔ role.revoke consumes approval and prevents a later revoke or authorize (232.747756ms)
|
||||||
|
✔ creates private WAL store and excludes a second writer until explicit close (245.691122ms)
|
||||||
|
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (172.009427ms)
|
||||||
|
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (256.716305ms)
|
||||||
|
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (351.248691ms)
|
||||||
|
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (149.511218ms)
|
||||||
|
✔ async transactions refuse before invoking their function (119.740418ms)
|
||||||
|
✔ recordTask keeps sync reads and a role write apart (277.097949ms)
|
||||||
|
✔ read_at must be one canonical UTC format, so the projection compares strings safely (114.028215ms)
|
||||||
|
✔ a bad entry refuses the whole record (116.960536ms)
|
||||||
|
✔ taskView reads the projection for one business (210.868484ms)
|
||||||
|
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (438.207826ms)
|
||||||
|
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (438.027227ms)
|
||||||
|
✔ without an adapter the server refuses every task verb (198.146102ms)
|
||||||
|
✔ the runtime refuses an invalid adapter and closes a valid one (216.186052ms)
|
||||||
|
✔ the process loads the S3 adapter from plain-data trackers (273.619163ms)
|
||||||
|
✔ socket capability stamps launch identity; shared views use wire, no SQL client (260.103868ms)
|
||||||
|
✔ two wire claims serialize; a lost reply never automatically retries (186.295991ms)
|
||||||
|
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (122.398394ms)
|
||||||
|
✔ client preserves UTF-8 when a response divides a multibyte character (12.28368ms)
|
||||||
|
✔ committed mutation followed by dropped reply reports unknown and is never retried (244.901528ms)
|
||||||
|
ℹ tests 74
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 73
|
||||||
|
ℹ fail 1
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 3166.363958
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/bus/tests/end-launch.test.mjs:73:1
|
||||||
|
✖ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (157.253366ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/bus/tests/end-launch.test.mjs:79:10)
|
||||||
|
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||||
|
at Test.run (node:internal/test_runner/test:1402:25)
|
||||||
|
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||||
|
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||||
|
at Test.run (node:internal/test_runner/test:1467:12)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: false,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: undefined,
|
||||||
|
expected: /run-ended/,
|
||||||
|
operator: 'throws',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (126.592557ms)
|
||||||
|
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (130.171901ms)
|
||||||
|
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (125.162483ms)
|
||||||
|
✔ decide prints a declining choice as declining (104.33608ms)
|
||||||
|
✔ an unknown outcome is reported once and never resent (89.318318ms)
|
||||||
|
✔ a decision closed before the answer arrives exits 2 and points at its trail (105.532354ms)
|
||||||
|
✔ a prefix that matches two open decisions exits 2 and resolves neither (105.868605ms)
|
||||||
|
✔ without --business a command uses the live host's business, and a stale host.json is not a host (65.576879ms)
|
||||||
|
✔ every human command refuses inside an agent run before it touches the bus (111.297337ms)
|
||||||
|
✔ usage errors exit 4; no business and no host is a usage error (98.771393ms)
|
||||||
|
✔ agents and tasks print through the broker (255.048787ms)
|
||||||
|
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.546141ms)
|
||||||
|
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (45.672646ms)
|
||||||
|
✔ trackers come from the tracker.* variables of the one project that names a tracker project (32.240318ms)
|
||||||
|
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (29.603951ms)
|
||||||
|
✔ two projects that each name a tracker project refuse, since the boot shape holds one (33.350544ms)
|
||||||
|
✔ a business without tracker.baseUrl gets no trackers entry (26.705721ms)
|
||||||
|
✔ an unknown business and a broken system config refuse with exit 3 (54.028045ms)
|
||||||
|
✔ empty views say so (0.997992ms)
|
||||||
|
✔ the trail keeps the broker's order and names a decision's task without its rows (1.124414ms)
|
||||||
|
✔ tasks print the tracker fields the snapshot carries (0.15378ms)
|
||||||
|
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (895.526588ms)
|
||||||
|
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (212.725435ms)
|
||||||
|
✔ a second host for the same data root refuses with exit 3 while the first runs (273.362879ms)
|
||||||
|
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (245.235107ms)
|
||||||
|
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (198.483689ms)
|
||||||
|
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (167.789165ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (197.473695ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (124.052455ms)
|
||||||
|
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (201.202702ms)
|
||||||
|
✔ watchChildren reports a child that died before it was called, and one that dies later (23.250612ms)
|
||||||
|
✔ bus stop refuses to signal a live pid that is not a bus host (202.761707ms)
|
||||||
|
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (213.465669ms)
|
||||||
|
✔ bus start refuses with exit 3 without a notifier config (86.816406ms)
|
||||||
|
✔ bus start runs until bus stop; status reports it while it runs (658.567869ms)
|
||||||
|
✔ bus-service.sh renders the unit and installs it into a given directory (28.128558ms)
|
||||||
|
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1360.589465ms)
|
||||||
|
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (776.641584ms)
|
||||||
|
✔ a runner that stops at once ends its launch with the runner's reason (199.870832ms)
|
||||||
|
✖ 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 (711.911393ms)
|
||||||
|
✖ 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 (3581.287492ms)
|
||||||
|
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (123.48589ms)
|
||||||
|
✔ zoned uses the IANA zone across DST (17.451622ms)
|
||||||
|
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (132.26037ms)
|
||||||
|
✔ two blocking decisions get two DMs with different nonces (118.678545ms)
|
||||||
|
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.218622ms)
|
||||||
|
✔ a failed DM is journaled, backs off, and is retried until it lands (129.429198ms)
|
||||||
|
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (109.642392ms)
|
||||||
|
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (90.790543ms)
|
||||||
|
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (101.894504ms)
|
||||||
|
✔ 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 (159.050941ms)
|
||||||
|
✔ a restart after the second refusal does not send before that refusal's 30 min are up (147.116141ms)
|
||||||
|
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (142.57825ms)
|
||||||
|
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (249.598656ms)
|
||||||
|
✔ an inbox read failure is logged and the next poll retries (0.635093ms)
|
||||||
|
✔ no Discord id reaches the journal or the log (112.597633ms)
|
||||||
|
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (24.68035ms)
|
||||||
|
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (66.147162ms)
|
||||||
|
✔ the journal: a whole file that is one torn line truncates to empty (50.965235ms)
|
||||||
|
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.213804ms)
|
||||||
|
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.663316ms)
|
||||||
|
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.799538ms)
|
||||||
|
✔ the journal: a symlinked directory refuses and says it is a link (0.308098ms)
|
||||||
|
✔ 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.665486ms)
|
||||||
|
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.365494ms)
|
||||||
|
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.426602ms)
|
||||||
|
✔ digest content stays within Discord's 2000 characters (0.260771ms)
|
||||||
|
✔ runLoop never overlaps ticks and stops after the one in flight (110.010563ms)
|
||||||
|
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 (365.325466ms)
|
||||||
|
✔ the transport writes {business, verb, args} to the child and reads its JSON (42.334679ms)
|
||||||
|
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2132.640014ms)
|
||||||
|
✔ busExit and refuseInsideAgent (0.431379ms)
|
||||||
|
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (226.693198ms)
|
||||||
|
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1151.320994ms)
|
||||||
|
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (103.217521ms)
|
||||||
|
✔ launches off and on go to the broker and change the business's launch state (173.616665ms)
|
||||||
|
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (134.088458ms)
|
||||||
|
ℹ tests 77
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 75
|
||||||
|
ℹ fail 2
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 6835.150141
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/cli/tests/launcher.test.mjs:252:3
|
||||||
|
✖ 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 (711.911393ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: run r3e4b2104c041 (pm) from an earlier host ended: host-lost
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:297:12)
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||||
|
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: false,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: false,
|
||||||
|
expected: true,
|
||||||
|
operator: '==',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
|
|
||||||
|
test at packages/cli/tests/launcher.test.mjs:252:3
|
||||||
|
✖ 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 (3581.287492ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: run r86d98d19d98a (pm) from an earlier host ended: host-lost
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:297:12)
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||||
|
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: false,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: false,
|
||||||
|
expected: true,
|
||||||
|
operator: '==',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (134.382091ms)
|
||||||
|
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (127.538676ms)
|
||||||
|
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (116.860604ms)
|
||||||
|
✔ decide prints a declining choice as declining (125.22075ms)
|
||||||
|
✔ an unknown outcome is reported once and never resent (103.496002ms)
|
||||||
|
✔ a decision closed before the answer arrives exits 2 and points at its trail (125.776748ms)
|
||||||
|
✔ a prefix that matches two open decisions exits 2 and resolves neither (117.149997ms)
|
||||||
|
✔ without --business a command uses the live host's business, and a stale host.json is not a host (115.624806ms)
|
||||||
|
✔ every human command refuses inside an agent run before it touches the bus (223.103098ms)
|
||||||
|
✔ usage errors exit 4; no business and no host is a usage error (150.021496ms)
|
||||||
|
✔ agents and tasks print through the broker (124.340719ms)
|
||||||
|
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.57056ms)
|
||||||
|
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (41.155739ms)
|
||||||
|
✔ trackers come from the tracker.* variables of the one project that names a tracker project (35.962477ms)
|
||||||
|
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (34.977988ms)
|
||||||
|
✔ two projects that each name a tracker project refuse, since the boot shape holds one (45.484448ms)
|
||||||
|
✔ a business without tracker.baseUrl gets no trackers entry (35.743739ms)
|
||||||
|
✔ an unknown business and a broken system config refuse with exit 3 (70.823037ms)
|
||||||
|
✔ empty views say so (1.141349ms)
|
||||||
|
✔ the trail keeps the broker's order and names a decision's task without its rows (1.432144ms)
|
||||||
|
✔ tasks print the tracker fields the snapshot carries (0.228737ms)
|
||||||
|
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (911.580879ms)
|
||||||
|
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (276.115157ms)
|
||||||
|
✔ a second host for the same data root refuses with exit 3 while the first runs (200.288603ms)
|
||||||
|
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (202.357798ms)
|
||||||
|
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (207.000651ms)
|
||||||
|
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (112.164695ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (185.066354ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (134.881776ms)
|
||||||
|
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (189.591959ms)
|
||||||
|
✔ watchChildren reports a child that died before it was called, and one that dies later (22.719956ms)
|
||||||
|
✔ bus stop refuses to signal a live pid that is not a bus host (203.112654ms)
|
||||||
|
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (213.10587ms)
|
||||||
|
✔ bus start refuses with exit 3 without a notifier config (86.928226ms)
|
||||||
|
✔ bus start runs until bus stop; status reports it while it runs (656.036208ms)
|
||||||
|
✔ bus-service.sh renders the unit and installs it into a given directory (26.867061ms)
|
||||||
|
✖ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (282.417559ms)
|
||||||
|
✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (575.753358ms)
|
||||||
|
✔ a runner that stops at once ends its launch with the runner's reason (272.021572ms)
|
||||||
|
✔ 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 (770.912983ms)
|
||||||
|
✔ 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 (3659.382952ms)
|
||||||
|
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (135.231912ms)
|
||||||
|
✔ zoned uses the IANA zone across DST (17.476571ms)
|
||||||
|
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (132.004501ms)
|
||||||
|
✔ two blocking decisions get two DMs with different nonces (122.871191ms)
|
||||||
|
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.292362ms)
|
||||||
|
✔ a failed DM is journaled, backs off, and is retried until it lands (105.352783ms)
|
||||||
|
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (109.51429ms)
|
||||||
|
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (121.525089ms)
|
||||||
|
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (118.943528ms)
|
||||||
|
✔ 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 (173.203968ms)
|
||||||
|
✔ a restart after the second refusal does not send before that refusal's 30 min are up (138.607623ms)
|
||||||
|
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (259.309314ms)
|
||||||
|
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (157.557174ms)
|
||||||
|
✔ an inbox read failure is logged and the next poll retries (0.717398ms)
|
||||||
|
✔ no Discord id reaches the journal or the log (113.42138ms)
|
||||||
|
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (20.122071ms)
|
||||||
|
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (38.603439ms)
|
||||||
|
✔ the journal: a whole file that is one torn line truncates to empty (25.084268ms)
|
||||||
|
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.048053ms)
|
||||||
|
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.595689ms)
|
||||||
|
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.751411ms)
|
||||||
|
✔ the journal: a symlinked directory refuses and says it is a link (0.383996ms)
|
||||||
|
✔ 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.480832ms)
|
||||||
|
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.369812ms)
|
||||||
|
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.429485ms)
|
||||||
|
✔ digest content stays within Discord's 2000 characters (0.264703ms)
|
||||||
|
✔ runLoop never overlaps ticks and stops after the one in flight (110.764422ms)
|
||||||
|
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 (394.904188ms)
|
||||||
|
✔ the transport writes {business, verb, args} to the child and reads its JSON (41.671676ms)
|
||||||
|
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2160.080078ms)
|
||||||
|
✔ busExit and refuseInsideAgent (0.4448ms)
|
||||||
|
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (219.683221ms)
|
||||||
|
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1121.161112ms)
|
||||||
|
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (108.204627ms)
|
||||||
|
✔ launches off and on go to the broker and change the business's launch state (117.554155ms)
|
||||||
|
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (112.64814ms)
|
||||||
|
ℹ tests 77
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 75
|
||||||
|
ℹ fail 2
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 5778.01493
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/cli/tests/launcher.test.mjs:105:1
|
||||||
|
✖ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (282.417559ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /PM launch refused: instance-running/. Input:
|
||||||
|
|
||||||
|
'CliError: PM launch refused: capacity-full (opus sessions: 1 of 1)'
|
||||||
|
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||||
|
at async TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:137:3)
|
||||||
|
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: CliError: PM launch refused: capacity-full (opus sessions: 1 of 1)
|
||||||
|
at file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/src/launcher.mjs:295:15
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:104:5),
|
||||||
|
expected: /PM launch refused: instance-running/,
|
||||||
|
operator: 'rejects',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
|
|
||||||
|
test at packages/cli/tests/launcher.test.mjs:175:1
|
||||||
|
✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (575.753358ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: coder
|
||||||
|
+ actual - expected
|
||||||
|
|
||||||
|
{
|
||||||
|
+ error: 'capacity-full',
|
||||||
|
- error: 'instance-running',
|
||||||
|
ok: false
|
||||||
|
}
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:209:12)
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||||
|
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: false,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: { ok: false, error: 'capacity-full' },
|
||||||
|
expected: { ok: false, error: 'instance-running' },
|
||||||
|
operator: 'deepStrictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (138.757292ms)
|
||||||
|
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (181.365968ms)
|
||||||
|
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (135.995008ms)
|
||||||
|
✔ decide prints a declining choice as declining (131.321963ms)
|
||||||
|
✔ an unknown outcome is reported once and never resent (121.033541ms)
|
||||||
|
✔ a decision closed before the answer arrives exits 2 and points at its trail (143.568257ms)
|
||||||
|
✔ a prefix that matches two open decisions exits 2 and resolves neither (133.382442ms)
|
||||||
|
✔ without --business a command uses the live host's business, and a stale host.json is not a host (103.046809ms)
|
||||||
|
✔ every human command refuses inside an agent run before it touches the bus (101.665231ms)
|
||||||
|
✔ usage errors exit 4; no business and no host is a usage error (137.587049ms)
|
||||||
|
✔ agents and tasks print through the broker (166.273722ms)
|
||||||
|
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (36.002104ms)
|
||||||
|
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (46.154581ms)
|
||||||
|
✔ trackers come from the tracker.* variables of the one project that names a tracker project (45.097471ms)
|
||||||
|
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (45.020635ms)
|
||||||
|
✔ two projects that each name a tracker project refuse, since the boot shape holds one (41.702747ms)
|
||||||
|
✔ a business without tracker.baseUrl gets no trackers entry (33.239399ms)
|
||||||
|
✔ an unknown business and a broken system config refuse with exit 3 (72.015913ms)
|
||||||
|
✔ empty views say so (1.166082ms)
|
||||||
|
✔ the trail keeps the broker's order and names a decision's task without its rows (1.362349ms)
|
||||||
|
✔ tasks print the tracker fields the snapshot carries (0.2314ms)
|
||||||
|
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (952.770524ms)
|
||||||
|
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (235.027778ms)
|
||||||
|
✔ a second host for the same data root refuses with exit 3 while the first runs (216.642435ms)
|
||||||
|
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (263.246996ms)
|
||||||
|
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (215.728182ms)
|
||||||
|
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (226.22554ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (166.772705ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (166.509966ms)
|
||||||
|
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (257.036469ms)
|
||||||
|
✔ watchChildren reports a child that died before it was called, and one that dies later (22.899723ms)
|
||||||
|
✔ bus stop refuses to signal a live pid that is not a bus host (203.205762ms)
|
||||||
|
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (215.565389ms)
|
||||||
|
✔ bus start refuses with exit 3 without a notifier config (87.058049ms)
|
||||||
|
✔ bus start runs until bus stop; status reports it while it runs (657.293355ms)
|
||||||
|
✔ bus-service.sh renders the unit and installs it into a given directory (25.693957ms)
|
||||||
|
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1333.711945ms)
|
||||||
|
✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (861.939206ms)
|
||||||
|
✔ a runner that stops at once ends its launch with the runner's reason (231.210908ms)
|
||||||
|
✔ 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 (770.734104ms)
|
||||||
|
✔ 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 (3630.367619ms)
|
||||||
|
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (122.084335ms)
|
||||||
|
✔ zoned uses the IANA zone across DST (18.085725ms)
|
||||||
|
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (143.101836ms)
|
||||||
|
✔ two blocking decisions get two DMs with different nonces (178.859924ms)
|
||||||
|
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.256723ms)
|
||||||
|
✔ a failed DM is journaled, backs off, and is retried until it lands (138.729402ms)
|
||||||
|
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (128.495778ms)
|
||||||
|
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (123.094867ms)
|
||||||
|
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (138.652432ms)
|
||||||
|
✔ 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.760709ms)
|
||||||
|
✔ a restart after the second refusal does not send before that refusal's 30 min are up (148.067127ms)
|
||||||
|
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (156.1858ms)
|
||||||
|
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (164.926157ms)
|
||||||
|
✔ an inbox read failure is logged and the next poll retries (0.636575ms)
|
||||||
|
✔ no Discord id reaches the journal or the log (194.997457ms)
|
||||||
|
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (25.301543ms)
|
||||||
|
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (84.875465ms)
|
||||||
|
✔ the journal: a whole file that is one torn line truncates to empty (36.937581ms)
|
||||||
|
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (3.6726ms)
|
||||||
|
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.589677ms)
|
||||||
|
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.820138ms)
|
||||||
|
✔ the journal: a symlinked directory refuses and says it is a link (0.315697ms)
|
||||||
|
✔ 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.459806ms)
|
||||||
|
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.363338ms)
|
||||||
|
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.442958ms)
|
||||||
|
✔ digest content stays within Discord's 2000 characters (0.260548ms)
|
||||||
|
✔ runLoop never overlaps ticks and stops after the one in flight (110.734543ms)
|
||||||
|
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 (442.592712ms)
|
||||||
|
✔ the transport writes {business, verb, args} to the child and reads its JSON (36.742582ms)
|
||||||
|
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2185.380382ms)
|
||||||
|
✔ busExit and refuseInsideAgent (0.412571ms)
|
||||||
|
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (260.22186ms)
|
||||||
|
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1168.161409ms)
|
||||||
|
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (158.427277ms)
|
||||||
|
✔ launches off and on go to the broker and change the business's launch state (175.506781ms)
|
||||||
|
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (137.45557ms)
|
||||||
|
ℹ tests 77
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 76
|
||||||
|
ℹ fail 1
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 7039.056371
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/cli/tests/launcher.test.mjs:175:1
|
||||||
|
✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (861.939206ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||||
|
|
||||||
|
true !== false
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:217:10)
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||||
|
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: true,
|
||||||
|
expected: false,
|
||||||
|
operator: 'strictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
✔ sessionModel: agent vars win, then the system's execution settings (11.121928ms)
|
||||||
|
✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.728183ms)
|
||||||
|
✖ a claude-code bundle adds the wrapped gate hook and the MCP config (4.014424ms)
|
||||||
|
✔ a bundle is written once: an existing file refuses (1.996239ms)
|
||||||
|
✔ a path with a single quote can't go into the hook command (1.855058ms)
|
||||||
|
✔ allow exits 0, a deny exits 2 with the reason on stderr (130.812334ms)
|
||||||
|
✔ a missing or wrong policy, or a bad event, exits 2 (79.031735ms)
|
||||||
|
✔ the bundle's wrapped command: a missing gate or node still blocks (1094.514702ms)
|
||||||
|
✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (764.062618ms)
|
||||||
|
✔ claude: the hook alone blocks a path outside the workspace (589.82994ms)
|
||||||
|
✔ claude: a second turn resumes the first turn's session (736.915159ms)
|
||||||
|
✔ claude: a missing hook or MCP file refuses before claude starts (8.629216ms)
|
||||||
|
✔ pi: policy tools and typed tools pass, anything else is blocked (2.876463ms)
|
||||||
|
✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.752765ms)
|
||||||
|
✔ file tool paths must resolve inside the workspace (0.962332ms)
|
||||||
|
✔ pi's own path normalisation can't be used to step out (0.907476ms)
|
||||||
|
✔ a symlink inside the workspace that points out is outside (0.808361ms)
|
||||||
|
✔ claude path fields per tool (0.860736ms)
|
||||||
|
✔ glob patterns stay inside the workspace (0.904636ms)
|
||||||
|
✔ a path that can't be checked is blocked (0.617929ms)
|
||||||
|
✔ initialize, ping and tools/list (42.391174ms)
|
||||||
|
✔ tools/call goes through the tool socket; a refusal is an isError result (37.12388ms)
|
||||||
|
✔ unknown tools and methods are JSON-RPC errors and never reach the socket (31.935515ms)
|
||||||
|
✔ a missing argument is a usage error (33.805981ms)
|
||||||
|
✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (359.224518ms)
|
||||||
|
✔ pi: a missing extension refuses before any model call (8.186443ms)
|
||||||
|
✔ pi: an extension without its configuration fails pi's start (270.982753ms)
|
||||||
|
✔ founderCheck: founder variables, then a needed service without a usable token (1.711642ms)
|
||||||
|
✔ turnRequest names the sender, class, reply and decision (0.259584ms)
|
||||||
|
✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (269.324752ms)
|
||||||
|
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (131.430937ms)
|
||||||
|
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (464.429598ms)
|
||||||
|
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1520.134523ms)
|
||||||
|
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (187.836217ms)
|
||||||
|
✔ founder credentials stop before the claim (20) (208.81483ms)
|
||||||
|
✔ a refused claim exits 21; an ended run's capability exits 22 (244.227937ms)
|
||||||
|
✔ the launch ending under a running session exits 22 (172.416748ms)
|
||||||
|
✔ a broker that stays unreachable exits 23 after brokerRetries polls (253.75994ms)
|
||||||
|
✔ a broker that is down at the claim exits 23, not 21 (92.919627ms)
|
||||||
|
✔ no capability, or a malformed one, on stdin exits 2 (191.524811ms)
|
||||||
|
✔ the PM gets launch, its task verbs and the reads (7.204459ms)
|
||||||
|
✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.916872ms)
|
||||||
|
✔ launch only when the business's launch block names the instance as launcher (2.364338ms)
|
||||||
|
✔ an action outside the instance's authority has no tool (2.35004ms)
|
||||||
|
✔ callTool: one JSON line out, the result back, a refusal rejects (8.053898ms)
|
||||||
|
ℹ tests 45
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 44
|
||||||
|
ℹ fail 1
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 10694.510151
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/harness/tests/bundle.test.mjs:70:1
|
||||||
|
✖ a claude-code bundle adds the wrapped gate hook and the MCP config (4.014424ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||||
|
+ actual - expected
|
||||||
|
|
||||||
|
+ "timeout -k 2 10 '/usr/bin/node' '/home/jwoltje/darkwing-scratch/r41/wt/packages/harness/src/claude-gate.mjs' '/home/jwoltje/darkwing-scratch/tmp/mosaic-harness-F6vGQM/bundle/policy.json'"
|
||||||
|
- "timeout -k 2 10 '/usr/bin/node' '/home/jwoltje/darkwing-scratch/r41/wt/packages/harness/src/claude-gate.mjs' '/home/jwoltje/darkwing-scratch/tmp/mosaic-harness-F6vGQM/bundle/policy.json' || exit 2"
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/harness/tests/bundle.test.mjs:78:10)
|
||||||
|
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||||
|
at Test.run (node:internal/test_runner/test:1402:25)
|
||||||
|
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||||
|
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||||
|
at Test.run (node:internal/test_runner/test:1467:12)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: true,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: "timeout -k 2 10 '/usr/bin/node' '/home/jwoltje/darkwing-scratch/r41/wt/packages/harness/src/claude-gate.mjs' '/home/jwoltje/darkwing-scratch/tmp/mosaic-harness-F6vGQM/bundle/policy.json'",
|
||||||
|
expected: "timeout -k 2 10 '/usr/bin/node' '/home/jwoltje/darkwing-scratch/r41/wt/packages/harness/src/claude-gate.mjs' '/home/jwoltje/darkwing-scratch/tmp/mosaic-harness-F6vGQM/bundle/policy.json' || exit 2",
|
||||||
|
operator: 'strictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (130.720681ms)
|
||||||
|
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (144.44443ms)
|
||||||
|
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (123.061895ms)
|
||||||
|
✔ decide prints a declining choice as declining (244.593115ms)
|
||||||
|
✔ an unknown outcome is reported once and never resent (208.332046ms)
|
||||||
|
✔ a decision closed before the answer arrives exits 2 and points at its trail (147.08676ms)
|
||||||
|
✔ a prefix that matches two open decisions exits 2 and resolves neither (117.042615ms)
|
||||||
|
✔ without --business a command uses the live host's business, and a stale host.json is not a host (119.363758ms)
|
||||||
|
✔ every human command refuses inside an agent run before it touches the bus (292.507856ms)
|
||||||
|
✔ usage errors exit 4; no business and no host is a usage error (138.115749ms)
|
||||||
|
✔ agents and tasks print through the broker (140.189507ms)
|
||||||
|
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.09492ms)
|
||||||
|
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (49.445915ms)
|
||||||
|
✔ trackers come from the tracker.* variables of the one project that names a tracker project (39.563603ms)
|
||||||
|
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (33.21625ms)
|
||||||
|
✔ two projects that each name a tracker project refuse, since the boot shape holds one (30.793644ms)
|
||||||
|
✔ a business without tracker.baseUrl gets no trackers entry (36.275401ms)
|
||||||
|
✔ an unknown business and a broken system config refuse with exit 3 (56.275821ms)
|
||||||
|
✔ empty views say so (1.306748ms)
|
||||||
|
✔ the trail keeps the broker's order and names a decision's task without its rows (1.509261ms)
|
||||||
|
✔ tasks print the tracker fields the snapshot carries (0.246762ms)
|
||||||
|
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (911.121735ms)
|
||||||
|
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (235.366643ms)
|
||||||
|
✔ a second host for the same data root refuses with exit 3 while the first runs (369.841177ms)
|
||||||
|
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (262.305547ms)
|
||||||
|
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (236.663948ms)
|
||||||
|
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (143.842312ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (190.154398ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (108.391088ms)
|
||||||
|
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (206.604045ms)
|
||||||
|
✔ watchChildren reports a child that died before it was called, and one that dies later (22.186881ms)
|
||||||
|
✔ bus stop refuses to signal a live pid that is not a bus host (202.80689ms)
|
||||||
|
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (229.010884ms)
|
||||||
|
✔ bus start refuses with exit 3 without a notifier config (93.774875ms)
|
||||||
|
✔ bus start runs until bus stop; status reports it while it runs (668.258443ms)
|
||||||
|
✔ bus-service.sh renders the unit and installs it into a given directory (26.758168ms)
|
||||||
|
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1323.665538ms)
|
||||||
|
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (848.147276ms)
|
||||||
|
✔ a runner that stops at once ends its launch with the runner's reason (245.519218ms)
|
||||||
|
✔ 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 (647.385463ms)
|
||||||
|
✔ 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.234805ms)
|
||||||
|
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (104.246508ms)
|
||||||
|
✔ zoned uses the IANA zone across DST (24.971532ms)
|
||||||
|
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (124.019745ms)
|
||||||
|
✔ two blocking decisions get two DMs with different nonces (127.853015ms)
|
||||||
|
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.200872ms)
|
||||||
|
✔ a failed DM is journaled, backs off, and is retried until it lands (116.476651ms)
|
||||||
|
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (183.577337ms)
|
||||||
|
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (261.864425ms)
|
||||||
|
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (161.040869ms)
|
||||||
|
✔ 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 (179.477962ms)
|
||||||
|
✔ a restart after the second refusal does not send before that refusal's 30 min are up (244.681013ms)
|
||||||
|
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (245.72771ms)
|
||||||
|
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (140.118308ms)
|
||||||
|
✔ an inbox read failure is logged and the next poll retries (0.686691ms)
|
||||||
|
✔ no Discord id reaches the journal or the log (138.080337ms)
|
||||||
|
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (32.66773ms)
|
||||||
|
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (49.31037ms)
|
||||||
|
✔ the journal: a whole file that is one torn line truncates to empty (32.685563ms)
|
||||||
|
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.076912ms)
|
||||||
|
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.597882ms)
|
||||||
|
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.010512ms)
|
||||||
|
✔ the journal: a symlinked directory refuses and says it is a link (0.388819ms)
|
||||||
|
✔ 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.471387ms)
|
||||||
|
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.381919ms)
|
||||||
|
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.426331ms)
|
||||||
|
✔ digest content stays within Discord's 2000 characters (0.247464ms)
|
||||||
|
✔ runLoop never overlaps ticks and stops after the one in flight (111.169144ms)
|
||||||
|
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 (395.541301ms)
|
||||||
|
✔ the transport writes {business, verb, args} to the child and reads its JSON (38.675564ms)
|
||||||
|
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2159.439268ms)
|
||||||
|
✔ busExit and refuseInsideAgent (0.44803ms)
|
||||||
|
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (219.100073ms)
|
||||||
|
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1125.936788ms)
|
||||||
|
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (247.706439ms)
|
||||||
|
✔ launches off and on go to the broker and change the business's launch state (174.665863ms)
|
||||||
|
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (131.345212ms)
|
||||||
|
ℹ tests 77
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 77
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 6883.778277
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
✔ sessionModel: agent vars win, then the system's execution settings (14.067777ms)
|
||||||
|
✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.83597ms)
|
||||||
|
✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.203511ms)
|
||||||
|
✔ a bundle is written once: an existing file refuses (2.324752ms)
|
||||||
|
✔ a path with a single quote can't go into the hook command (2.432801ms)
|
||||||
|
✔ allow exits 0, a deny exits 2 with the reason on stderr (125.69164ms)
|
||||||
|
✔ a missing or wrong policy, or a bad event, exits 2 (87.248095ms)
|
||||||
|
✔ the bundle's wrapped command: a missing gate or node still blocks (1096.130839ms)
|
||||||
|
✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (822.887941ms)
|
||||||
|
✔ claude: the hook alone blocks a path outside the workspace (619.788359ms)
|
||||||
|
✔ claude: a second turn resumes the first turn's session (720.544276ms)
|
||||||
|
✔ claude: a missing hook or MCP file refuses before claude starts (7.807494ms)
|
||||||
|
✔ pi: policy tools and typed tools pass, anything else is blocked (2.760889ms)
|
||||||
|
✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.858294ms)
|
||||||
|
✔ file tool paths must resolve inside the workspace (1.329669ms)
|
||||||
|
✔ pi's own path normalisation can't be used to step out (1.208492ms)
|
||||||
|
✔ a symlink inside the workspace that points out is outside (1.109705ms)
|
||||||
|
✔ claude path fields per tool (0.783786ms)
|
||||||
|
✔ glob patterns stay inside the workspace (1.009446ms)
|
||||||
|
✔ a path that can't be checked is blocked (0.632633ms)
|
||||||
|
✔ initialize, ping and tools/list (44.810011ms)
|
||||||
|
✔ tools/call goes through the tool socket; a refusal is an isError result (34.700842ms)
|
||||||
|
✔ unknown tools and methods are JSON-RPC errors and never reach the socket (35.585978ms)
|
||||||
|
✔ a missing argument is a usage error (39.424495ms)
|
||||||
|
✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (377.597538ms)
|
||||||
|
✔ pi: a missing extension refuses before any model call (8.384513ms)
|
||||||
|
✔ pi: an extension without its configuration fails pi's start (294.902223ms)
|
||||||
|
✖ founderCheck: founder variables, then a needed service without a usable token (2.011371ms)
|
||||||
|
✔ turnRequest names the sender, class, reply and decision (0.351869ms)
|
||||||
|
✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (239.104388ms)
|
||||||
|
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (134.155238ms)
|
||||||
|
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (534.9943ms)
|
||||||
|
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1584.330846ms)
|
||||||
|
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (150.826229ms)
|
||||||
|
✔ the PM gets launch, its task verbs and the reads (7.17446ms)
|
||||||
|
✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.29576ms)
|
||||||
|
✔ launch only when the business's launch block names the instance as launcher (1.646169ms)
|
||||||
|
✔ an action outside the instance's authority has no tool (1.594448ms)
|
||||||
|
✔ callTool: one JSON line out, the result back, a refusal rejects (8.633147ms)
|
||||||
|
ℹ tests 39
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 38
|
||||||
|
ℹ fail 1
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 263663.299994
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/harness/tests/runner.test.mjs:34:1
|
||||||
|
✖ founderCheck: founder variables, then a needed service without a usable token (2.011371ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. Received type object (null)
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/harness/tests/runner.test.mjs:38:10)
|
||||||
|
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||||
|
at Test.run (node:internal/test_runner/test:1402:25)
|
||||||
|
at Test.start (node:internal/test_runner/test:1262:17)
|
||||||
|
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
|
||||||
|
generatedMessage: true,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: null,
|
||||||
|
expected: /GITEA_TOKEN, SSH_AUTH_SOCK/,
|
||||||
|
operator: 'match',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (125.138875ms)
|
||||||
|
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (132.971762ms)
|
||||||
|
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (117.374088ms)
|
||||||
|
✔ decide prints a declining choice as declining (108.459149ms)
|
||||||
|
✔ an unknown outcome is reported once and never resent (70.01985ms)
|
||||||
|
✔ a decision closed before the answer arrives exits 2 and points at its trail (94.875686ms)
|
||||||
|
✔ a prefix that matches two open decisions exits 2 and resolves neither (94.31331ms)
|
||||||
|
✔ without --business a command uses the live host's business, and a stale host.json is not a host (50.391423ms)
|
||||||
|
✔ every human command refuses inside an agent run before it touches the bus (72.351222ms)
|
||||||
|
✔ usage errors exit 4; no business and no host is a usage error (83.26574ms)
|
||||||
|
✔ agents and tasks print through the broker (91.938302ms)
|
||||||
|
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.1808ms)
|
||||||
|
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (45.455893ms)
|
||||||
|
✔ trackers come from the tracker.* variables of the one project that names a tracker project (34.445063ms)
|
||||||
|
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (33.484263ms)
|
||||||
|
✔ two projects that each name a tracker project refuse, since the boot shape holds one (31.217508ms)
|
||||||
|
✔ a business without tracker.baseUrl gets no trackers entry (32.020055ms)
|
||||||
|
✔ an unknown business and a broken system config refuse with exit 3 (59.190349ms)
|
||||||
|
✔ empty views say so (1.181996ms)
|
||||||
|
✔ the trail keeps the broker's order and names a decision's task without its rows (1.453134ms)
|
||||||
|
✔ tasks print the tracker fields the snapshot carries (0.228609ms)
|
||||||
|
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (911.109415ms)
|
||||||
|
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (229.643058ms)
|
||||||
|
✔ a second host for the same data root refuses with exit 3 while the first runs (307.116557ms)
|
||||||
|
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (203.545959ms)
|
||||||
|
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (165.693521ms)
|
||||||
|
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (138.295568ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (180.904134ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (136.928153ms)
|
||||||
|
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (167.809839ms)
|
||||||
|
✔ watchChildren reports a child that died before it was called, and one that dies later (26.616224ms)
|
||||||
|
✔ bus stop refuses to signal a live pid that is not a bus host (202.697476ms)
|
||||||
|
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (216.070056ms)
|
||||||
|
✔ bus start refuses with exit 3 without a notifier config (91.812758ms)
|
||||||
|
✔ bus start runs until bus stop; status reports it while it runs (661.600894ms)
|
||||||
|
✔ bus-service.sh renders the unit and installs it into a given directory (25.834051ms)
|
||||||
|
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1429.993014ms)
|
||||||
|
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (639.118083ms)
|
||||||
|
✔ a runner that stops at once ends its launch with the runner's reason (266.413468ms)
|
||||||
|
✔ 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 (635.92366ms)
|
||||||
|
✔ 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 (3671.785556ms)
|
||||||
|
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (133.951668ms)
|
||||||
|
✔ zoned uses the IANA zone across DST (21.770408ms)
|
||||||
|
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (124.613973ms)
|
||||||
|
✔ two blocking decisions get two DMs with different nonces (127.527978ms)
|
||||||
|
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.311431ms)
|
||||||
|
✔ a failed DM is journaled, backs off, and is retried until it lands (112.494043ms)
|
||||||
|
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (111.51636ms)
|
||||||
|
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (80.954353ms)
|
||||||
|
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (81.322222ms)
|
||||||
|
✔ 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 (153.159571ms)
|
||||||
|
✔ a restart after the second refusal does not send before that refusal's 30 min are up (110.188277ms)
|
||||||
|
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (115.163887ms)
|
||||||
|
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (75.162747ms)
|
||||||
|
✔ an inbox read failure is logged and the next poll retries (0.6174ms)
|
||||||
|
✔ no Discord id reaches the journal or the log (68.352032ms)
|
||||||
|
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (16.452539ms)
|
||||||
|
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (16.506609ms)
|
||||||
|
✔ the journal: a whole file that is one torn line truncates to empty (10.938296ms)
|
||||||
|
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.635215ms)
|
||||||
|
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.620336ms)
|
||||||
|
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.874754ms)
|
||||||
|
✔ the journal: a symlinked directory refuses and says it is a link (0.330028ms)
|
||||||
|
✔ 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.49249ms)
|
||||||
|
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.414306ms)
|
||||||
|
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.448777ms)
|
||||||
|
✔ digest content stays within Discord's 2000 characters (0.259028ms)
|
||||||
|
✔ runLoop never overlaps ticks and stops after the one in flight (110.716598ms)
|
||||||
|
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 (378.128826ms)
|
||||||
|
✔ the transport writes {business, verb, args} to the child and reads its JSON (41.478941ms)
|
||||||
|
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2151.535751ms)
|
||||||
|
✔ busExit and refuseInsideAgent (0.435856ms)
|
||||||
|
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (215.850821ms)
|
||||||
|
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1133.958331ms)
|
||||||
|
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (108.172473ms)
|
||||||
|
✔ launches off and on go to the broker and change the business's launch state (120.665848ms)
|
||||||
|
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (88.354039ms)
|
||||||
|
ℹ tests 77
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 77
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 6865.453741
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (137.376158ms)
|
||||||
|
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (121.751693ms)
|
||||||
|
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (116.798959ms)
|
||||||
|
✔ decide prints a declining choice as declining (146.977151ms)
|
||||||
|
✔ an unknown outcome is reported once and never resent (119.334436ms)
|
||||||
|
✔ a decision closed before the answer arrives exits 2 and points at its trail (115.298648ms)
|
||||||
|
✔ a prefix that matches two open decisions exits 2 and resolves neither (125.351114ms)
|
||||||
|
✔ without --business a command uses the live host's business, and a stale host.json is not a host (94.326095ms)
|
||||||
|
✔ every human command refuses inside an agent run before it touches the bus (119.08327ms)
|
||||||
|
✔ usage errors exit 4; no business and no host is a usage error (101.827088ms)
|
||||||
|
✔ agents and tasks print through the broker (95.910812ms)
|
||||||
|
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.360098ms)
|
||||||
|
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (48.404329ms)
|
||||||
|
✔ trackers come from the tracker.* variables of the one project that names a tracker project (33.696218ms)
|
||||||
|
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (33.178759ms)
|
||||||
|
✔ two projects that each name a tracker project refuse, since the boot shape holds one (33.332481ms)
|
||||||
|
✔ a business without tracker.baseUrl gets no trackers entry (29.07756ms)
|
||||||
|
✔ an unknown business and a broken system config refuse with exit 3 (59.228455ms)
|
||||||
|
✔ empty views say so (0.801616ms)
|
||||||
|
✔ the trail keeps the broker's order and names a decision's task without its rows (0.937632ms)
|
||||||
|
✔ tasks print the tracker fields the snapshot carries (0.142201ms)
|
||||||
|
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (908.509723ms)
|
||||||
|
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (259.291592ms)
|
||||||
|
✔ a second host for the same data root refuses with exit 3 while the first runs (155.72522ms)
|
||||||
|
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (216.453668ms)
|
||||||
|
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (215.732438ms)
|
||||||
|
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (133.198746ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (165.465891ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (98.613063ms)
|
||||||
|
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (240.0965ms)
|
||||||
|
✔ watchChildren reports a child that died before it was called, and one that dies later (24.289345ms)
|
||||||
|
✔ bus stop refuses to signal a live pid that is not a bus host (202.292309ms)
|
||||||
|
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (225.9222ms)
|
||||||
|
✔ bus start refuses with exit 3 without a notifier config (88.973332ms)
|
||||||
|
✔ bus start runs until bus stop; status reports it while it runs (751.784886ms)
|
||||||
|
✔ bus-service.sh renders the unit and installs it into a given directory (49.720154ms)
|
||||||
|
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1296.224536ms)
|
||||||
|
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (656.132557ms)
|
||||||
|
✔ a runner that stops at once ends its launch with the runner's reason (217.485134ms)
|
||||||
|
✔ 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 (728.931825ms)
|
||||||
|
✔ 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 (3650.060252ms)
|
||||||
|
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (146.530135ms)
|
||||||
|
✔ zoned uses the IANA zone across DST (18.184875ms)
|
||||||
|
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (145.69019ms)
|
||||||
|
✔ two blocking decisions get two DMs with different nonces (118.99032ms)
|
||||||
|
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.176236ms)
|
||||||
|
✔ a failed DM is journaled, backs off, and is retried until it lands (118.676676ms)
|
||||||
|
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (133.002797ms)
|
||||||
|
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (118.116242ms)
|
||||||
|
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (110.099936ms)
|
||||||
|
✔ 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 (184.568054ms)
|
||||||
|
✔ a restart after the second refusal does not send before that refusal's 30 min are up (144.754853ms)
|
||||||
|
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (138.783123ms)
|
||||||
|
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (89.036985ms)
|
||||||
|
✔ an inbox read failure is logged and the next poll retries (0.62497ms)
|
||||||
|
✔ no Discord id reaches the journal or the log (111.308913ms)
|
||||||
|
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (31.479031ms)
|
||||||
|
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (46.260958ms)
|
||||||
|
✔ the journal: a whole file that is one torn line truncates to empty (25.374439ms)
|
||||||
|
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.988813ms)
|
||||||
|
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.581731ms)
|
||||||
|
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.786572ms)
|
||||||
|
✔ the journal: a symlinked directory refuses and says it is a link (0.340661ms)
|
||||||
|
✔ 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.466709ms)
|
||||||
|
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.374083ms)
|
||||||
|
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.410585ms)
|
||||||
|
✔ digest content stays within Discord's 2000 characters (0.25301ms)
|
||||||
|
✔ runLoop never overlaps ticks and stops after the one in flight (125.526296ms)
|
||||||
|
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 (399.433333ms)
|
||||||
|
✔ the transport writes {business, verb, args} to the child and reads its JSON (46.256649ms)
|
||||||
|
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2150.449936ms)
|
||||||
|
✔ busExit and refuseInsideAgent (0.391979ms)
|
||||||
|
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (236.876993ms)
|
||||||
|
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1149.345411ms)
|
||||||
|
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (98.170237ms)
|
||||||
|
✔ launches off and on go to the broker and change the business's launch state (98.79469ms)
|
||||||
|
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (103.804047ms)
|
||||||
|
ℹ tests 77
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 77
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 6770.811975
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
✔ sessionModel: agent vars win, then the system's execution settings (14.050443ms)
|
||||||
|
✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (6.120541ms)
|
||||||
|
✔ a claude-code bundle adds the wrapped gate hook and the MCP config (4.081354ms)
|
||||||
|
✔ a bundle is written once: an existing file refuses (2.996025ms)
|
||||||
|
✔ a path with a single quote can't go into the hook command (2.951571ms)
|
||||||
|
✔ allow exits 0, a deny exits 2 with the reason on stderr (150.595498ms)
|
||||||
|
✔ a missing or wrong policy, or a bad event, exits 2 (124.742454ms)
|
||||||
|
✔ the bundle's wrapped command: a missing gate or node still blocks (1126.482389ms)
|
||||||
|
✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (909.741969ms)
|
||||||
|
✔ claude: the hook alone blocks a path outside the workspace (516.900249ms)
|
||||||
|
✔ claude: a second turn resumes the first turn's session (763.872943ms)
|
||||||
|
✔ claude: a missing hook or MCP file refuses before claude starts (8.211736ms)
|
||||||
|
✔ pi: policy tools and typed tools pass, anything else is blocked (3.953963ms)
|
||||||
|
✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.997202ms)
|
||||||
|
✔ file tool paths must resolve inside the workspace (1.423588ms)
|
||||||
|
✔ pi's own path normalisation can't be used to step out (1.401389ms)
|
||||||
|
✔ a symlink inside the workspace that points out is outside (1.139946ms)
|
||||||
|
✔ claude path fields per tool (0.791831ms)
|
||||||
|
✔ glob patterns stay inside the workspace (0.945422ms)
|
||||||
|
✔ a path that can't be checked is blocked (0.646247ms)
|
||||||
|
✔ initialize, ping and tools/list (51.210531ms)
|
||||||
|
✔ tools/call goes through the tool socket; a refusal is an isError result (41.206446ms)
|
||||||
|
✔ unknown tools and methods are JSON-RPC errors and never reach the socket (38.168479ms)
|
||||||
|
✔ a missing argument is a usage error (39.579956ms)
|
||||||
|
✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (496.281275ms)
|
||||||
|
✔ pi: a missing extension refuses before any model call (9.103805ms)
|
||||||
|
✔ pi: an extension without its configuration fails pi's start (301.173776ms)
|
||||||
|
✔ founderCheck: founder variables, then a needed service without a usable token (1.727546ms)
|
||||||
|
✔ turnRequest names the sender, class, reply and decision (0.275096ms)
|
||||||
|
✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (264.146564ms)
|
||||||
|
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (143.576835ms)
|
||||||
|
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (536.40783ms)
|
||||||
|
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1382.766716ms)
|
||||||
|
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (214.519665ms)
|
||||||
|
✔ founder credentials stop before the claim (20) (304.717878ms)
|
||||||
|
✔ a refused claim exits 21; an ended run's capability exits 22 (320.859843ms)
|
||||||
|
✔ the launch ending under a running session exits 22 (207.759761ms)
|
||||||
|
✔ a broker that stays unreachable exits 23 after brokerRetries polls (302.795855ms)
|
||||||
|
✔ a broker that is down at the claim exits 23, not 21 (148.704368ms)
|
||||||
|
✔ no capability, or a malformed one, on stdin exits 2 (210.336697ms)
|
||||||
|
✔ the PM gets launch, its task verbs and the reads (9.712494ms)
|
||||||
|
✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (3.23337ms)
|
||||||
|
✔ launch only when the business's launch block names the instance as launcher (2.566048ms)
|
||||||
|
✔ an action outside the instance's authority has no tool (3.156249ms)
|
||||||
|
✔ callTool: one JSON line out, the result back, a refusal rejects (9.65517ms)
|
||||||
|
ℹ tests 45
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 45
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 10678.399832
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (132.317557ms)
|
||||||
|
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (134.090645ms)
|
||||||
|
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (132.53407ms)
|
||||||
|
✔ decide prints a declining choice as declining (127.229793ms)
|
||||||
|
✔ an unknown outcome is reported once and never resent (97.531419ms)
|
||||||
|
✔ a decision closed before the answer arrives exits 2 and points at its trail (98.822586ms)
|
||||||
|
✔ a prefix that matches two open decisions exits 2 and resolves neither (113.290637ms)
|
||||||
|
✔ without --business a command uses the live host's business, and a stale host.json is not a host (87.563392ms)
|
||||||
|
✔ every human command refuses inside an agent run before it touches the bus (89.267983ms)
|
||||||
|
✔ usage errors exit 4; no business and no host is a usage error (90.872874ms)
|
||||||
|
✔ agents and tasks print through the broker (99.389871ms)
|
||||||
|
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.229007ms)
|
||||||
|
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (65.988943ms)
|
||||||
|
✔ trackers come from the tracker.* variables of the one project that names a tracker project (40.683718ms)
|
||||||
|
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (45.804031ms)
|
||||||
|
✔ two projects that each name a tracker project refuse, since the boot shape holds one (35.221951ms)
|
||||||
|
✔ a business without tracker.baseUrl gets no trackers entry (38.611374ms)
|
||||||
|
✔ an unknown business and a broken system config refuse with exit 3 (64.054106ms)
|
||||||
|
✔ empty views say so (1.179352ms)
|
||||||
|
✔ the trail keeps the broker's order and names a decision's task without its rows (1.468164ms)
|
||||||
|
✔ tasks print the tracker fields the snapshot carries (0.245647ms)
|
||||||
|
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (956.305599ms)
|
||||||
|
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (235.464033ms)
|
||||||
|
✔ a second host for the same data root refuses with exit 3 while the first runs (175.440963ms)
|
||||||
|
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (221.774919ms)
|
||||||
|
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (183.250261ms)
|
||||||
|
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (146.195016ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (195.557326ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (228.386699ms)
|
||||||
|
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (193.898946ms)
|
||||||
|
✔ watchChildren reports a child that died before it was called, and one that dies later (28.035345ms)
|
||||||
|
✔ bus stop refuses to signal a live pid that is not a bus host (202.365135ms)
|
||||||
|
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (215.714196ms)
|
||||||
|
✔ bus start refuses with exit 3 without a notifier config (97.320284ms)
|
||||||
|
✔ bus start runs until bus stop; status reports it while it runs (733.744653ms)
|
||||||
|
✔ bus-service.sh renders the unit and installs it into a given directory (30.592503ms)
|
||||||
|
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1316.305561ms)
|
||||||
|
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (735.280709ms)
|
||||||
|
✔ a runner that stops at once ends its launch with the runner's reason (282.231928ms)
|
||||||
|
✔ 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 (707.643793ms)
|
||||||
|
✔ 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 (3693.537166ms)
|
||||||
|
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (139.911386ms)
|
||||||
|
✔ zoned uses the IANA zone across DST (29.966188ms)
|
||||||
|
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (134.629552ms)
|
||||||
|
✔ two blocking decisions get two DMs with different nonces (130.426366ms)
|
||||||
|
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.212076ms)
|
||||||
|
✔ a failed DM is journaled, backs off, and is retried until it lands (143.19933ms)
|
||||||
|
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (118.110025ms)
|
||||||
|
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (95.961197ms)
|
||||||
|
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (107.204382ms)
|
||||||
|
✔ 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 (178.413565ms)
|
||||||
|
✔ a restart after the second refusal does not send before that refusal's 30 min are up (126.22777ms)
|
||||||
|
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (124.406448ms)
|
||||||
|
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (92.254829ms)
|
||||||
|
✔ an inbox read failure is logged and the next poll retries (0.653785ms)
|
||||||
|
✔ no Discord id reaches the journal or the log (111.290157ms)
|
||||||
|
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (27.715695ms)
|
||||||
|
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (93.436044ms)
|
||||||
|
✔ the journal: a whole file that is one torn line truncates to empty (35.121818ms)
|
||||||
|
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.199765ms)
|
||||||
|
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.630155ms)
|
||||||
|
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.853005ms)
|
||||||
|
✔ the journal: a symlinked directory refuses and says it is a link (0.494157ms)
|
||||||
|
✔ 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.50078ms)
|
||||||
|
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.37415ms)
|
||||||
|
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.425391ms)
|
||||||
|
✔ digest content stays within Discord's 2000 characters (0.250758ms)
|
||||||
|
✔ runLoop never overlaps ticks and stops after the one in flight (110.832081ms)
|
||||||
|
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 (419.495349ms)
|
||||||
|
✔ the transport writes {business, verb, args} to the child and reads its JSON (50.573663ms)
|
||||||
|
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2173.090633ms)
|
||||||
|
✔ busExit and refuseInsideAgent (0.401847ms)
|
||||||
|
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (224.32061ms)
|
||||||
|
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1170.213512ms)
|
||||||
|
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (116.532473ms)
|
||||||
|
✔ launches off and on go to the broker and change the business's launch state (97.462875ms)
|
||||||
|
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (83.439542ms)
|
||||||
|
ℹ tests 77
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 77
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 6998.23078
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
✔ sessionModel: agent vars win, then the system's execution settings (11.615827ms)
|
||||||
|
✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.608262ms)
|
||||||
|
✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.093679ms)
|
||||||
|
✔ a bundle is written once: an existing file refuses (2.743247ms)
|
||||||
|
✔ a path with a single quote can't go into the hook command (1.960427ms)
|
||||||
|
✔ allow exits 0, a deny exits 2 with the reason on stderr (129.070397ms)
|
||||||
|
✔ a missing or wrong policy, or a bad event, exits 2 (95.751067ms)
|
||||||
|
✔ the bundle's wrapped command: a missing gate or node still blocks (1098.628901ms)
|
||||||
|
✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (833.557723ms)
|
||||||
|
✔ claude: the hook alone blocks a path outside the workspace (501.161697ms)
|
||||||
|
✔ claude: a second turn resumes the first turn's session (799.047884ms)
|
||||||
|
✔ claude: a missing hook or MCP file refuses before claude starts (7.620084ms)
|
||||||
|
✔ pi: policy tools and typed tools pass, anything else is blocked (3.560331ms)
|
||||||
|
✔ claude: builtins map from pi names, typed tools need the mcp prefix (1.195475ms)
|
||||||
|
✔ file tool paths must resolve inside the workspace (1.373421ms)
|
||||||
|
✔ pi's own path normalisation can't be used to step out (1.395868ms)
|
||||||
|
✔ a symlink inside the workspace that points out is outside (1.15816ms)
|
||||||
|
✔ claude path fields per tool (1.025541ms)
|
||||||
|
✖ glob patterns stay inside the workspace (2.186725ms)
|
||||||
|
✔ a path that can't be checked is blocked (0.852835ms)
|
||||||
|
✔ initialize, ping and tools/list (48.515972ms)
|
||||||
|
✔ tools/call goes through the tool socket; a refusal is an isError result (31.715873ms)
|
||||||
|
✔ unknown tools and methods are JSON-RPC errors and never reach the socket (30.592543ms)
|
||||||
|
✔ a missing argument is a usage error (32.791709ms)
|
||||||
|
✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (403.4094ms)
|
||||||
|
✔ pi: a missing extension refuses before any model call (8.601035ms)
|
||||||
|
✔ pi: an extension without its configuration fails pi's start (303.980444ms)
|
||||||
|
✔ founderCheck: founder variables, then a needed service without a usable token (1.638328ms)
|
||||||
|
✔ turnRequest names the sender, class, reply and decision (0.265375ms)
|
||||||
|
✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (353.187333ms)
|
||||||
|
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (165.33647ms)
|
||||||
|
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (717.750899ms)
|
||||||
|
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1432.11811ms)
|
||||||
|
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (276.246822ms)
|
||||||
|
✔ founder credentials stop before the claim (20) (215.437887ms)
|
||||||
|
✔ a refused claim exits 21; an ended run's capability exits 22 (382.043681ms)
|
||||||
|
✔ the launch ending under a running session exits 22 (173.218595ms)
|
||||||
|
✔ a broker that stays unreachable exits 23 after brokerRetries polls (348.026841ms)
|
||||||
|
✔ a broker that is down at the claim exits 23, not 21 (243.327611ms)
|
||||||
|
✔ no capability, or a malformed one, on stdin exits 2 (273.537344ms)
|
||||||
|
✔ the PM gets launch, its task verbs and the reads (9.380584ms)
|
||||||
|
✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.545292ms)
|
||||||
|
✔ launch only when the business's launch block names the instance as launcher (1.844344ms)
|
||||||
|
✔ an action outside the instance's authority has no tool (2.256037ms)
|
||||||
|
✔ callTool: one JSON line out, the result back, a refusal rejects (7.218466ms)
|
||||||
|
ℹ tests 45
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 44
|
||||||
|
ℹ fail 1
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 11069.564681
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/harness/tests/gate.test.mjs:92:1
|
||||||
|
✖ glob patterns stay inside the workspace (2.186725ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||||
|
|
||||||
|
true !== false
|
||||||
|
|
||||||
|
at blocked (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/harness/tests/gate.test.mjs:19:10)
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/harness/tests/gate.test.mjs:98:5)
|
||||||
|
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||||
|
at Test.run (node:internal/test_runner/test:1402:25)
|
||||||
|
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||||
|
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||||
|
at Test.run (node:internal/test_runner/test:1467:12)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: true,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: true,
|
||||||
|
expected: false,
|
||||||
|
operator: 'strictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (163.89862ms)
|
||||||
|
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (275.331186ms)
|
||||||
|
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (282.996644ms)
|
||||||
|
✔ launch events require a human CLI capability; generic emit cannot forge authority events (195.063187ms)
|
||||||
|
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (208.388066ms)
|
||||||
|
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (157.831971ms)
|
||||||
|
✔ task action subjects and linked decision trail are complete and ordered (156.524887ms)
|
||||||
|
✔ launch binding is durable and reconnecting requires the identical trusted record (97.415614ms)
|
||||||
|
✔ business isolation includes inherited object names and cross-business message references (159.041055ms)
|
||||||
|
✔ authority never transfers between action, run, target, unresolved or replaced role holder (227.530616ms)
|
||||||
|
✔ task projection uses schema current view, skipping earlier and equal-start polls (94.890129ms)
|
||||||
|
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (180.592353ms)
|
||||||
|
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (106.579275ms)
|
||||||
|
✔ both arbiters require human resolution when their cross-role route is themselves (189.732423ms)
|
||||||
|
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (1.899132ms)
|
||||||
|
✔ only validated broker references load; returned data and exceptions cannot expose a known token (5.641078ms)
|
||||||
|
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (3.241221ms)
|
||||||
|
✔ expiry refuses use and env references never become client data (0.737712ms)
|
||||||
|
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.517077ms)
|
||||||
|
✔ opaque tokens shorter than 16 characters refuse before use (0.297143ms)
|
||||||
|
✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (122.779602ms)
|
||||||
|
✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (127.653631ms)
|
||||||
|
✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (155.030205ms)
|
||||||
|
✔ endLaunch leaves a claim another run took alone (193.274277ms)
|
||||||
|
✔ refuse records action.refused against the caller with the code only (145.282316ms)
|
||||||
|
✖ launches off refuses role.launch with launch-revoked until launches on (183.043385ms)
|
||||||
|
✔ broker process: launch ops authorize role.launch, record refusals and end runs (266.177254ms)
|
||||||
|
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (3.887495ms)
|
||||||
|
✔ process reader gets own kernel identity without exposing environment values (0.762853ms)
|
||||||
|
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.082879ms)
|
||||||
|
✔ real pid 1 remains inspectable when its environment is protected (0.430481ms)
|
||||||
|
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (196.161745ms)
|
||||||
|
✔ missing and foreign-business citations refuse and roll back message and grant (183.836168ms)
|
||||||
|
✔ cross-role sends still need a matching resolved decision and consume it once (274.522528ms)
|
||||||
|
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (190.003233ms)
|
||||||
|
✔ startup token refusal returns safe code without value or partial listening broker (40.621827ms)
|
||||||
|
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (184.028601ms)
|
||||||
|
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (188.935171ms)
|
||||||
|
✔ trusted host registers later launches; socket clients never have a registration verb (175.835554ms)
|
||||||
|
✔ runtime excludes declared project roots even when host supplies no repoRoots (39.471345ms)
|
||||||
|
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (178.989186ms)
|
||||||
|
✔ v3b prototype refusals, views and append-only mutations (1120.730725ms)
|
||||||
|
✔ gated approval authorizes once, survives store reopen, and fresh approval works (251.703532ms)
|
||||||
|
✔ another run cannot consume an approval; a failed check leaves it usable (238.891458ms)
|
||||||
|
✔ two scheduled callers have exactly one grant and one consumed refusal (180.906441ms)
|
||||||
|
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (351.290843ms)
|
||||||
|
✔ class drift gated to cross-role refuses before consumption (218.014846ms)
|
||||||
|
✔ class drift cross-role to gated refuses before consumption (203.570384ms)
|
||||||
|
✔ class drift gated to within-role refuses before consumption (183.6351ms)
|
||||||
|
✔ class drift cross-role to within-role refuses before consumption (196.755743ms)
|
||||||
|
✔ class drift within-role to gated refuses before consumption (134.864109ms)
|
||||||
|
✔ class drift within-role to cross-role refuses before consumption (160.50952ms)
|
||||||
|
✔ message.send consumes approval and prevents a later send or authorize (173.816965ms)
|
||||||
|
✔ role.revoke consumes approval and prevents a later revoke or authorize (195.598037ms)
|
||||||
|
✔ creates private WAL store and excludes a second writer until explicit close (123.44453ms)
|
||||||
|
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (181.784127ms)
|
||||||
|
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (215.291682ms)
|
||||||
|
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (169.453598ms)
|
||||||
|
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (119.878794ms)
|
||||||
|
✔ async transactions refuse before invoking their function (107.045531ms)
|
||||||
|
✔ recordTask keeps sync reads and a role write apart (179.866702ms)
|
||||||
|
✔ read_at must be one canonical UTC format, so the projection compares strings safely (109.539813ms)
|
||||||
|
✔ a bad entry refuses the whole record (118.580361ms)
|
||||||
|
✔ taskView reads the projection for one business (143.124952ms)
|
||||||
|
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (262.005489ms)
|
||||||
|
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (413.469594ms)
|
||||||
|
✔ without an adapter the server refuses every task verb (173.691791ms)
|
||||||
|
✔ the runtime refuses an invalid adapter and closes a valid one (180.508213ms)
|
||||||
|
✔ the process loads the S3 adapter from plain-data trackers (232.75675ms)
|
||||||
|
✔ socket capability stamps launch identity; shared views use wire, no SQL client (146.93786ms)
|
||||||
|
✔ two wire claims serialize; a lost reply never automatically retries (182.474377ms)
|
||||||
|
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (113.923911ms)
|
||||||
|
✔ client preserves UTF-8 when a response divides a multibyte character (11.855361ms)
|
||||||
|
✔ committed mutation followed by dropped reply reports unknown and is never retried (162.548925ms)
|
||||||
|
ℹ tests 74
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 73
|
||||||
|
ℹ fail 1
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 2579.991609
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/bus/tests/end-launch.test.mjs:121:1
|
||||||
|
✖ launches off refuses role.launch with launch-revoked until launches on (183.043385ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/bus/tests/end-launch.test.mjs:128:10)
|
||||||
|
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||||
|
at Test.run (node:internal/test_runner/test:1402:25)
|
||||||
|
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||||
|
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||||
|
at Test.run (node:internal/test_runner/test:1467:12)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: false,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: undefined,
|
||||||
|
expected: { code: 'launch-revoked' },
|
||||||
|
operator: 'throws',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (109.862086ms)
|
||||||
|
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (129.587767ms)
|
||||||
|
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (92.553872ms)
|
||||||
|
✔ decide prints a declining choice as declining (105.185722ms)
|
||||||
|
✔ an unknown outcome is reported once and never resent (74.403125ms)
|
||||||
|
✔ a decision closed before the answer arrives exits 2 and points at its trail (75.320493ms)
|
||||||
|
✔ a prefix that matches two open decisions exits 2 and resolves neither (97.310508ms)
|
||||||
|
✔ without --business a command uses the live host's business, and a stale host.json is not a host (80.781684ms)
|
||||||
|
✔ every human command refuses inside an agent run before it touches the bus (84.435133ms)
|
||||||
|
✔ usage errors exit 4; no business and no host is a usage error (80.337038ms)
|
||||||
|
✔ agents and tasks print through the broker (75.861924ms)
|
||||||
|
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.835921ms)
|
||||||
|
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (54.390762ms)
|
||||||
|
✔ trackers come from the tracker.* variables of the one project that names a tracker project (37.014457ms)
|
||||||
|
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (34.755279ms)
|
||||||
|
✔ two projects that each name a tracker project refuse, since the boot shape holds one (35.304414ms)
|
||||||
|
✔ a business without tracker.baseUrl gets no trackers entry (27.453313ms)
|
||||||
|
✔ an unknown business and a broken system config refuse with exit 3 (73.51488ms)
|
||||||
|
✔ empty views say so (1.16644ms)
|
||||||
|
✔ the trail keeps the broker's order and names a decision's task without its rows (1.434549ms)
|
||||||
|
✔ tasks print the tracker fields the snapshot carries (0.234705ms)
|
||||||
|
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (886.74793ms)
|
||||||
|
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (207.220461ms)
|
||||||
|
✔ a second host for the same data root refuses with exit 3 while the first runs (125.163127ms)
|
||||||
|
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (193.933367ms)
|
||||||
|
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (204.121221ms)
|
||||||
|
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (121.372003ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (175.827333ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (143.726724ms)
|
||||||
|
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (167.137229ms)
|
||||||
|
✔ watchChildren reports a child that died before it was called, and one that dies later (22.294447ms)
|
||||||
|
✔ bus stop refuses to signal a live pid that is not a bus host (202.775467ms)
|
||||||
|
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (210.492927ms)
|
||||||
|
✔ bus start refuses with exit 3 without a notifier config (90.517024ms)
|
||||||
|
✔ bus start runs until bus stop; status reports it while it runs (670.225504ms)
|
||||||
|
✔ bus-service.sh renders the unit and installs it into a given directory (30.367746ms)
|
||||||
|
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1264.763565ms)
|
||||||
|
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (659.045675ms)
|
||||||
|
✔ a runner that stops at once ends its launch with the runner's reason (207.163503ms)
|
||||||
|
✔ 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 (617.947072ms)
|
||||||
|
✔ 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 (3574.705922ms)
|
||||||
|
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (110.311682ms)
|
||||||
|
✔ zoned uses the IANA zone across DST (16.358337ms)
|
||||||
|
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (116.467328ms)
|
||||||
|
✔ two blocking decisions get two DMs with different nonces (124.106016ms)
|
||||||
|
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.199104ms)
|
||||||
|
✔ a failed DM is journaled, backs off, and is retried until it lands (95.468473ms)
|
||||||
|
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (99.017605ms)
|
||||||
|
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (75.330303ms)
|
||||||
|
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (81.013867ms)
|
||||||
|
✔ 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 (159.689637ms)
|
||||||
|
✔ a restart after the second refusal does not send before that refusal's 30 min are up (116.245282ms)
|
||||||
|
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (96.001449ms)
|
||||||
|
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (75.69486ms)
|
||||||
|
✔ an inbox read failure is logged and the next poll retries (0.661409ms)
|
||||||
|
✔ no Discord id reaches the journal or the log (59.545832ms)
|
||||||
|
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (16.205132ms)
|
||||||
|
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (20.246556ms)
|
||||||
|
✔ the journal: a whole file that is one torn line truncates to empty (21.610334ms)
|
||||||
|
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.917923ms)
|
||||||
|
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.705192ms)
|
||||||
|
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.917939ms)
|
||||||
|
✔ the journal: a symlinked directory refuses and says it is a link (0.529388ms)
|
||||||
|
✔ 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.533089ms)
|
||||||
|
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.461178ms)
|
||||||
|
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.564232ms)
|
||||||
|
✔ digest content stays within Discord's 2000 characters (0.288698ms)
|
||||||
|
✔ runLoop never overlaps ticks and stops after the one in flight (110.579344ms)
|
||||||
|
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 (358.584995ms)
|
||||||
|
✔ the transport writes {business, verb, args} to the child and reads its JSON (41.579301ms)
|
||||||
|
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2145.123853ms)
|
||||||
|
✔ busExit and refuseInsideAgent (0.399961ms)
|
||||||
|
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (198.808191ms)
|
||||||
|
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1102.511621ms)
|
||||||
|
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (118.315699ms)
|
||||||
|
✔ launches off and on go to the broker and change the business's launch state (127.484544ms)
|
||||||
|
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (83.416693ms)
|
||||||
|
ℹ tests 77
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 77
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 6523.228172
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (122.875541ms)
|
||||||
|
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (130.94748ms)
|
||||||
|
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (111.565521ms)
|
||||||
|
✔ decide prints a declining choice as declining (100.598618ms)
|
||||||
|
✔ an unknown outcome is reported once and never resent (81.113719ms)
|
||||||
|
✔ a decision closed before the answer arrives exits 2 and points at its trail (83.224029ms)
|
||||||
|
✔ a prefix that matches two open decisions exits 2 and resolves neither (84.988204ms)
|
||||||
|
✔ without --business a command uses the live host's business, and a stale host.json is not a host (70.293143ms)
|
||||||
|
✔ every human command refuses inside an agent run before it touches the bus (81.875595ms)
|
||||||
|
✔ usage errors exit 4; no business and no host is a usage error (95.688893ms)
|
||||||
|
✔ agents and tasks print through the broker (87.007356ms)
|
||||||
|
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.678919ms)
|
||||||
|
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (42.09848ms)
|
||||||
|
✔ trackers come from the tracker.* variables of the one project that names a tracker project (31.644567ms)
|
||||||
|
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (32.461392ms)
|
||||||
|
✔ two projects that each name a tracker project refuse, since the boot shape holds one (39.422053ms)
|
||||||
|
✔ a business without tracker.baseUrl gets no trackers entry (31.112022ms)
|
||||||
|
✔ an unknown business and a broken system config refuse with exit 3 (56.891472ms)
|
||||||
|
✔ empty views say so (0.786498ms)
|
||||||
|
✔ the trail keeps the broker's order and names a decision's task without its rows (0.964626ms)
|
||||||
|
✔ tasks print the tracker fields the snapshot carries (0.156842ms)
|
||||||
|
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (924.290753ms)
|
||||||
|
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (224.387012ms)
|
||||||
|
✔ a second host for the same data root refuses with exit 3 while the first runs (131.886124ms)
|
||||||
|
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (200.781729ms)
|
||||||
|
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (202.118243ms)
|
||||||
|
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (133.013545ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (183.058103ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (111.514305ms)
|
||||||
|
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (204.993453ms)
|
||||||
|
✔ watchChildren reports a child that died before it was called, and one that dies later (28.041817ms)
|
||||||
|
✔ bus stop refuses to signal a live pid that is not a bus host (203.159141ms)
|
||||||
|
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (229.44384ms)
|
||||||
|
✔ bus start refuses with exit 3 without a notifier config (105.05832ms)
|
||||||
|
✔ bus start runs until bus stop; status reports it while it runs (658.38108ms)
|
||||||
|
✔ bus-service.sh renders the unit and installs it into a given directory (27.340512ms)
|
||||||
|
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1284.454067ms)
|
||||||
|
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (660.213353ms)
|
||||||
|
✔ a runner that stops at once ends its launch with the runner's reason (228.790342ms)
|
||||||
|
✖ 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 (471.399509ms)
|
||||||
|
✖ 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 (3406.092202ms)
|
||||||
|
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (137.539713ms)
|
||||||
|
✔ zoned uses the IANA zone across DST (17.45739ms)
|
||||||
|
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (131.994345ms)
|
||||||
|
✔ two blocking decisions get two DMs with different nonces (124.836571ms)
|
||||||
|
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.179365ms)
|
||||||
|
✔ a failed DM is journaled, backs off, and is retried until it lands (97.474367ms)
|
||||||
|
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (99.938485ms)
|
||||||
|
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (91.272173ms)
|
||||||
|
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (85.503667ms)
|
||||||
|
✔ 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 (145.863192ms)
|
||||||
|
✔ a restart after the second refusal does not send before that refusal's 30 min are up (119.177833ms)
|
||||||
|
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (124.013706ms)
|
||||||
|
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (84.553923ms)
|
||||||
|
✔ an inbox read failure is logged and the next poll retries (0.616339ms)
|
||||||
|
✔ no Discord id reaches the journal or the log (72.213113ms)
|
||||||
|
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (12.627127ms)
|
||||||
|
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (15.576535ms)
|
||||||
|
✔ the journal: a whole file that is one torn line truncates to empty (17.63237ms)
|
||||||
|
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.965226ms)
|
||||||
|
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.641952ms)
|
||||||
|
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.809363ms)
|
||||||
|
✔ the journal: a symlinked directory refuses and says it is a link (0.310274ms)
|
||||||
|
✔ 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.45827ms)
|
||||||
|
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.364092ms)
|
||||||
|
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.445383ms)
|
||||||
|
✔ digest content stays within Discord's 2000 characters (0.25583ms)
|
||||||
|
✔ runLoop never overlaps ticks and stops after the one in flight (110.775491ms)
|
||||||
|
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 (378.369694ms)
|
||||||
|
✔ the transport writes {business, verb, args} to the child and reads its JSON (35.260579ms)
|
||||||
|
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2151.84806ms)
|
||||||
|
✔ busExit and refuseInsideAgent (0.392495ms)
|
||||||
|
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (229.052611ms)
|
||||||
|
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1132.31844ms)
|
||||||
|
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (120.754589ms)
|
||||||
|
✔ launches off and on go to the broker and change the business's launch state (115.404453ms)
|
||||||
|
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (77.278529ms)
|
||||||
|
ℹ tests 77
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 75
|
||||||
|
ℹ fail 2
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 6277.504144
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/cli/tests/launcher.test.mjs:252:3
|
||||||
|
✖ 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 (471.399509ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||||
|
|
||||||
|
0 !== 1
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:281:12)
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||||
|
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: 0,
|
||||||
|
expected: 1,
|
||||||
|
operator: 'strictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
|
|
||||||
|
test at packages/cli/tests/launcher.test.mjs:252:3
|
||||||
|
✖ 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 (3406.092202ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||||
|
|
||||||
|
0 !== 1
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:281:12)
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||||
|
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: 0,
|
||||||
|
expected: 1,
|
||||||
|
operator: 'strictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
R=~/darkwing-scratch/r41/mut
|
||||||
|
rm -f $R/summary.txt
|
||||||
|
$R/run.sh Ma-late-signals harness cli
|
||||||
|
$R/run.sh Mb-runs-set-early bus cli
|
||||||
|
$R/run.sh Mc-no-run-ended bus cli
|
||||||
|
$R/run.sh Md-no-instance-running cli
|
||||||
|
$R/run.sh Me-no-authorize cli
|
||||||
|
$R/run.sh Mf-no-exit2-wrapper harness
|
||||||
|
$R/run.sh Mg-no-founder-check harness cli
|
||||||
|
$R/run.sh Mh-no-close-sigkill cli
|
||||||
|
$R/run.sh Mi-recover-no-kill cli
|
||||||
|
$R/run.sh Mj-no-stdin-cap harness
|
||||||
|
$R/run.sh Mk-launch-line-max cli
|
||||||
|
$R/run.sh Ml-gate-pattern harness
|
||||||
|
$R/run.sh Mm-no-revoke bus cli
|
||||||
|
$R/run.sh Mn-recover-no-end cli
|
||||||
|
echo DONE >> $R/summary.txt
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# mutate.py <file> <id>: apply one named mutant (exact text, must match once).
|
||||||
|
import sys
|
||||||
|
M = {
|
||||||
|
"Ma-late-signals": ("packages/harness/src/runner.mjs",
|
||||||
|
' process.on("SIGTERM", stop);\n process.on("SIGINT", stop);\n\n let session, cap;',
|
||||||
|
' let session, cap;', ),
|
||||||
|
"Mb-runs-set-early": ("packages/bus/src/broker.mjs",
|
||||||
|
" const session = { ...record, address: record.address ?? null, human: false };\n",
|
||||||
|
" const session = { ...record, address: record.address ?? null, human: false };\n this.#runs.set(key, session);\n"),
|
||||||
|
"Mc-no-run-ended": ("packages/bus/src/broker.mjs",
|
||||||
|
" fail('run-ended');\n } else", " void 0;\n } else"),
|
||||||
|
"Md-no-instance-running": ("packages/cli/src/launcher.mjs",
|
||||||
|
' if (registry.running(b.id, instance)) throw new Refusal("instance-running");\n', ""),
|
||||||
|
"Me-no-authorize": ("packages/cli/src/launcher.mjs",
|
||||||
|
' await host.op({ op: "authorizeLaunch", cap, instance });\n', ""),
|
||||||
|
"Mf-no-exit2-wrapper": ("packages/harness/src/bundle.mjs",
|
||||||
|
"${quote(files.policy)} || exit 2`", "${quote(files.policy)}`"),
|
||||||
|
"Mg-no-founder-check": ("packages/harness/src/runner.mjs",
|
||||||
|
" const found = FOUNDER_ENV.filter((k) => env[k] !== undefined);", " const found = [];"),
|
||||||
|
"Mh-no-close-sigkill": ("packages/cli/src/launcher.mjs",
|
||||||
|
' if (startTimeOf(s.pid) === s.startTime) process.kill(s.pid, "SIGKILL");', " void 0;"),
|
||||||
|
"Mi-recover-no-kill": ("packages/cli/src/launcher.mjs",
|
||||||
|
' process.kill(s.pid, "SIGKILL");', " void 0;"),
|
||||||
|
"Mj-no-stdin-cap": ("packages/harness/src/runner.mjs",
|
||||||
|
" if (input.length > 4096) reject", " if (false) reject"),
|
||||||
|
"Mk-launch-line-max": ("packages/cli/src/launcher.mjs",
|
||||||
|
" if (buf.length > LINE_MAX) return reply", " if (false) return reply"),
|
||||||
|
"Ml-gate-pattern": ("packages/harness/src/gate.mjs",
|
||||||
|
"/(^|[/\\\\])\\.\\.([/\\\\]|$)/.test(pattern)", "false"),
|
||||||
|
"Mm-no-revoke": ("packages/bus/src/broker.mjs",
|
||||||
|
" fail('launch-revoked');", " void 0;"),
|
||||||
|
"Mn-recover-no-end": ("packages/cli/src/launcher.mjs",
|
||||||
|
' await endRun(s.run, "host-lost", null);\n', ""),
|
||||||
|
}
|
||||||
|
f, old, new = M[sys.argv[1]]
|
||||||
|
s = open(f).read()
|
||||||
|
n = s.count(old)
|
||||||
|
if n != 1: sys.exit(f"{sys.argv[1]}: {n} matches in {f}")
|
||||||
|
open(f, "w").write(s.replace(old, new))
|
||||||
|
print(f)
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# parse.sh: pass/fail and failing test names per mutant output (spec reporter).
|
||||||
|
cd ~/darkwing-scratch/r41/mut
|
||||||
|
for f in M*-*.txt; do
|
||||||
|
p=$(grep -E '^ℹ pass' "$f" | awk '{print $3}'); x=$(grep -E '^ℹ fail' "$f" | awk '{print $3}')
|
||||||
|
echo "$f: pass $p fail $x"
|
||||||
|
[ "$x" != 0 ] && awk '/^✖ failing tests:/{on=1;next} on && /^✖ /{sub(/^✖ /," "); sub(/ \([0-9.]+ms\)$/,""); print}' "$f" | sort -u
|
||||||
|
done
|
||||||
Executable
+17
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# run.sh <mutant> <pkg>...: mutate, run each package's node suite, restore.
|
||||||
|
set -u
|
||||||
|
cd ~/darkwing-scratch/r41/wt
|
||||||
|
export TMPDIR=~/darkwing-scratch/tmp DOCKER_HOST=unix:///nonexistent.sock
|
||||||
|
m=$1; shift
|
||||||
|
f=$(python3 ../mut/mutate.py "$m") || { echo "$m: no match" | tee -a ../mut/summary.txt; exit 1; }
|
||||||
|
line="$m ($f):"
|
||||||
|
for p in "$@"; do
|
||||||
|
out=../mut/$m-$p.txt
|
||||||
|
node --test "packages/$p/tests/*.test.mjs" > "$out" 2>&1
|
||||||
|
pass=$(grep -E '^# pass' "$out" | awk '{print $3}'); fail=$(grep -E '^# fail' "$out" | awk '{print $3}')
|
||||||
|
line="$line $p pass $pass fail $fail;"
|
||||||
|
[ "$fail" != 0 ] && line="$line [$(grep -E '^not ok' "$out" | sed 's/^not ok [0-9]* - //' | head -3 | paste -sd'|')]"
|
||||||
|
done
|
||||||
|
git checkout -- "$f"
|
||||||
|
echo "$line" | tee -a ../mut/summary.txt
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
Ma-late-signals (packages/harness/src/runner.mjs): harness pass fail ; [] cli pass fail ; []
|
||||||
|
Mb-runs-set-early (packages/bus/src/broker.mjs): bus pass fail ; [] cli pass fail ; []
|
||||||
|
Mc-no-run-ended (packages/bus/src/broker.mjs): bus pass fail ; [] cli pass fail ; []
|
||||||
|
Md-no-instance-running (packages/cli/src/launcher.mjs): cli pass fail ; []
|
||||||
|
Me-no-authorize (packages/cli/src/launcher.mjs): cli pass fail ; []
|
||||||
|
Mf-no-exit2-wrapper (packages/harness/src/bundle.mjs): harness pass fail ; []
|
||||||
|
Mg-no-founder-check (packages/harness/src/runner.mjs): harness pass fail ; [] cli pass fail ; []
|
||||||
|
Mh-no-close-sigkill (packages/cli/src/launcher.mjs): cli pass fail ; []
|
||||||
|
Mi-recover-no-kill (packages/cli/src/launcher.mjs): cli pass fail ; []
|
||||||
|
Mj-no-stdin-cap (packages/harness/src/runner.mjs): harness pass fail ; []
|
||||||
|
Mk-launch-line-max (packages/cli/src/launcher.mjs): cli pass fail ; []
|
||||||
|
Ml-gate-pattern (packages/harness/src/gate.mjs): harness pass fail ; []
|
||||||
|
Mm-no-revoke (packages/bus/src/broker.mjs): bus pass fail ; [] cli pass fail ; []
|
||||||
|
Mn-recover-no-end (packages/cli/src/launcher.mjs): cli pass fail ; []
|
||||||
|
DONE
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (230.298902ms)
|
||||||
|
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (495.596263ms)
|
||||||
|
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (295.350922ms)
|
||||||
|
✔ launch events require a human CLI capability; generic emit cannot forge authority events (209.247055ms)
|
||||||
|
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (201.016991ms)
|
||||||
|
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (146.820046ms)
|
||||||
|
✔ task action subjects and linked decision trail are complete and ordered (186.279171ms)
|
||||||
|
✔ launch binding is durable and reconnecting requires the identical trusted record (124.058295ms)
|
||||||
|
✔ business isolation includes inherited object names and cross-business message references (201.009327ms)
|
||||||
|
✔ authority never transfers between action, run, target, unresolved or replaced role holder (325.185099ms)
|
||||||
|
✔ task projection uses schema current view, skipping earlier and equal-start polls (123.998135ms)
|
||||||
|
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (220.538954ms)
|
||||||
|
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (170.419881ms)
|
||||||
|
✔ both arbiters require human resolution when their cross-role route is themselves (217.901165ms)
|
||||||
|
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.544455ms)
|
||||||
|
✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.455749ms)
|
||||||
|
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (6.670204ms)
|
||||||
|
✔ expiry refuses use and env references never become client data (0.783979ms)
|
||||||
|
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.944049ms)
|
||||||
|
✔ opaque tokens shorter than 16 characters refuse before use (0.278578ms)
|
||||||
|
✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (212.9763ms)
|
||||||
|
✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (283.135446ms)
|
||||||
|
✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (239.880561ms)
|
||||||
|
✔ endLaunch leaves a claim another run took alone (189.377591ms)
|
||||||
|
✔ refuse records action.refused against the caller with the code only (148.68694ms)
|
||||||
|
✔ launches off refuses role.launch with launch-revoked until launches on (221.873558ms)
|
||||||
|
✔ broker process: launch ops authorize role.launch, record refusals and end runs (234.098929ms)
|
||||||
|
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (1.844295ms)
|
||||||
|
✔ process reader gets own kernel identity without exposing environment values (1.229094ms)
|
||||||
|
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.718827ms)
|
||||||
|
✔ real pid 1 remains inspectable when its environment is protected (0.280884ms)
|
||||||
|
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (336.734778ms)
|
||||||
|
✔ missing and foreign-business citations refuse and roll back message and grant (356.763341ms)
|
||||||
|
✔ cross-role sends still need a matching resolved decision and consume it once (272.056987ms)
|
||||||
|
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (271.449396ms)
|
||||||
|
✔ startup token refusal returns safe code without value or partial listening broker (38.528737ms)
|
||||||
|
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (319.293579ms)
|
||||||
|
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (181.982551ms)
|
||||||
|
✔ trusted host registers later launches; socket clients never have a registration verb (178.757199ms)
|
||||||
|
✔ runtime excludes declared project roots even when host supplies no repoRoots (35.654181ms)
|
||||||
|
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (188.093502ms)
|
||||||
|
✔ v3b prototype refusals, views and append-only mutations (1445.847272ms)
|
||||||
|
✔ gated approval authorizes once, survives store reopen, and fresh approval works (478.15649ms)
|
||||||
|
✔ another run cannot consume an approval; a failed check leaves it usable (310.533789ms)
|
||||||
|
✔ two scheduled callers have exactly one grant and one consumed refusal (193.236882ms)
|
||||||
|
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (384.018937ms)
|
||||||
|
✔ class drift gated to cross-role refuses before consumption (200.035157ms)
|
||||||
|
✔ class drift cross-role to gated refuses before consumption (257.75596ms)
|
||||||
|
✔ class drift gated to within-role refuses before consumption (192.144254ms)
|
||||||
|
✔ class drift cross-role to within-role refuses before consumption (267.10218ms)
|
||||||
|
✔ class drift within-role to gated refuses before consumption (197.394414ms)
|
||||||
|
✔ class drift within-role to cross-role refuses before consumption (210.140443ms)
|
||||||
|
✔ message.send consumes approval and prevents a later send or authorize (243.201049ms)
|
||||||
|
✔ role.revoke consumes approval and prevents a later revoke or authorize (221.001388ms)
|
||||||
|
✔ creates private WAL store and excludes a second writer until explicit close (190.861398ms)
|
||||||
|
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (370.583805ms)
|
||||||
|
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (240.634985ms)
|
||||||
|
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (172.61397ms)
|
||||||
|
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (114.410444ms)
|
||||||
|
✔ async transactions refuse before invoking their function (124.463303ms)
|
||||||
|
✔ recordTask keeps sync reads and a role write apart (290.376296ms)
|
||||||
|
✔ read_at must be one canonical UTC format, so the projection compares strings safely (254.023515ms)
|
||||||
|
✔ a bad entry refuses the whole record (152.38741ms)
|
||||||
|
✔ taskView reads the projection for one business (132.084924ms)
|
||||||
|
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (283.971235ms)
|
||||||
|
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (431.972902ms)
|
||||||
|
✔ without an adapter the server refuses every task verb (229.494873ms)
|
||||||
|
✔ the runtime refuses an invalid adapter and closes a valid one (221.815058ms)
|
||||||
|
✔ the process loads the S3 adapter from plain-data trackers (297.187861ms)
|
||||||
|
✔ socket capability stamps launch identity; shared views use wire, no SQL client (247.764449ms)
|
||||||
|
✔ two wire claims serialize; a lost reply never automatically retries (371.117662ms)
|
||||||
|
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (125.675324ms)
|
||||||
|
✔ client preserves UTF-8 when a response divides a multibyte character (11.780579ms)
|
||||||
|
✔ committed mutation followed by dropped reply reports unknown and is never retried (155.539343ms)
|
||||||
|
ℹ tests 74
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 74
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 3244.829626
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
✔ config directory and file path follow MOSAIC_CONFIG (1.46993ms)
|
||||||
|
✔ the fixture business validates and comes back frozen (3.65864ms)
|
||||||
|
✔ two instances may share a definition (1.05404ms)
|
||||||
|
✔ top-level refusals (3.346619ms)
|
||||||
|
✔ arbiters and projects (5.222003ms)
|
||||||
|
✔ role instances (2.840985ms)
|
||||||
|
✔ Vikunja bots (5.529147ms)
|
||||||
|
✔ a role without Vikunja takes no tracker block (1.73776ms)
|
||||||
|
✔ credential references match the definition's services (2.08883ms)
|
||||||
|
✔ launch (6.709177ms)
|
||||||
|
✔ loadBusiness: file checks (1.526452ms)
|
||||||
|
✔ loadBusiness: not a regular file (37.652055ms)
|
||||||
|
✔ loading writes nothing (1.309657ms)
|
||||||
|
✔ names that are Object.prototype properties don't count as declared (2.476127ms)
|
||||||
|
✔ the shipped example refuses as written and validates once filled in (0.52605ms)
|
||||||
|
✔ usage errors exit 4 (283.72903ms)
|
||||||
|
✔ validate: a good business exits 0 and prints instance digests (67.029589ms)
|
||||||
|
✔ validate: project files (305.887244ms)
|
||||||
|
✔ validate: missing files and a broken system config (248.794647ms)
|
||||||
|
✔ validate: credential reference problems exit 2 and name each one (62.924086ms)
|
||||||
|
✔ validate: a token file inside the repository is refused (65.917526ms)
|
||||||
|
✔ validate: role definitions come from MOSAIC_ROLES_DIR (186.048198ms)
|
||||||
|
✔ resolve: prints one instance's record (193.663903ms)
|
||||||
|
✔ resolve: refusals (386.422626ms)
|
||||||
|
✔ parse: exactly one of file or env, plus the service's date (1.918165ms)
|
||||||
|
✔ check: a good file has no problems (0.606329ms)
|
||||||
|
✔ check never opens the file: a write-only token passes (0.253188ms)
|
||||||
|
✔ check: file problems (0.698464ms)
|
||||||
|
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (0.647238ms)
|
||||||
|
✔ check: dates and environment references (0.304771ms)
|
||||||
|
✔ path and load (1.699579ms)
|
||||||
|
✔ refusals (0.864022ms)
|
||||||
|
✔ systemVars flattens the validated config (1.491298ms)
|
||||||
|
✔ precedence: system, business, project, project role, agent (3.709113ms)
|
||||||
|
✔ limits narrow the definition and never widen it (1.852486ms)
|
||||||
|
✔ role.launch stays within-role only for the instance the launch block names (3.761048ms)
|
||||||
|
✔ limits.authority without role.launch leaves the launcher with no launch block (1.43637ms)
|
||||||
|
✔ limits.authority narrows cross-role actions too (0.814065ms)
|
||||||
|
✔ classify (0.916153ms)
|
||||||
|
✔ the record carries what the broker and launcher need (0.797372ms)
|
||||||
|
✔ digest: key order doesn't matter, any value change does (4.852509ms)
|
||||||
|
✔ refusals (1.887646ms)
|
||||||
|
✔ the four shipped version 2 roles load (2.349244ms)
|
||||||
|
✔ shipped role scopes match addendum B section 2 and the SR runbook (0.971249ms)
|
||||||
|
✔ shipped authority follows the note's table (0.495674ms)
|
||||||
|
✔ version 1 files keep loading with no authority (0.755756ms)
|
||||||
|
✔ the conductor policy isn't a role (0.203739ms)
|
||||||
|
✔ a missing role file is exit 4, a symbolic link too (0.312205ms)
|
||||||
|
✔ version 2 refusals (0.981826ms)
|
||||||
|
✔ authority: closed vocabulary, no gated-only action, no overlap (2.054732ms)
|
||||||
|
✔ credentials: Gitea scopes (0.783621ms)
|
||||||
|
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (0.892118ms)
|
||||||
|
✔ credentials: services (0.458892ms)
|
||||||
|
✔ contract: a non-empty regular Markdown file beside the role file (0.624834ms)
|
||||||
|
✔ every key names known layers and a merge rule (0.76137ms)
|
||||||
|
✔ unknown keys and wrong layers refuse (0.780944ms)
|
||||||
|
✔ types (1.595196ms)
|
||||||
|
✔ merge: defaults, then the most specific layer wins (0.253108ms)
|
||||||
|
✔ merge: limits only narrow, and provenance lists each source (0.323743ms)
|
||||||
|
✔ merge doesn't change its inputs (0.108269ms)
|
||||||
|
ℹ tests 60
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 60
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 1859.606824
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (127.407326ms)
|
||||||
|
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (141.689489ms)
|
||||||
|
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (142.605184ms)
|
||||||
|
✔ decide prints a declining choice as declining (121.229846ms)
|
||||||
|
✔ an unknown outcome is reported once and never resent (112.51702ms)
|
||||||
|
✔ a decision closed before the answer arrives exits 2 and points at its trail (98.802976ms)
|
||||||
|
✔ a prefix that matches two open decisions exits 2 and resolves neither (99.474469ms)
|
||||||
|
✔ without --business a command uses the live host's business, and a stale host.json is not a host (96.405106ms)
|
||||||
|
✔ every human command refuses inside an agent run before it touches the bus (135.991574ms)
|
||||||
|
✔ usage errors exit 4; no business and no host is a usage error (113.200529ms)
|
||||||
|
✔ agents and tasks print through the broker (164.909648ms)
|
||||||
|
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.055998ms)
|
||||||
|
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (44.45945ms)
|
||||||
|
✔ trackers come from the tracker.* variables of the one project that names a tracker project (33.551109ms)
|
||||||
|
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (31.918045ms)
|
||||||
|
✔ two projects that each name a tracker project refuse, since the boot shape holds one (28.986033ms)
|
||||||
|
✔ a business without tracker.baseUrl gets no trackers entry (27.034417ms)
|
||||||
|
✔ an unknown business and a broken system config refuse with exit 3 (51.63733ms)
|
||||||
|
✔ empty views say so (0.744068ms)
|
||||||
|
✔ the trail keeps the broker's order and names a decision's task without its rows (0.991007ms)
|
||||||
|
✔ tasks print the tracker fields the snapshot carries (0.136071ms)
|
||||||
|
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (896.785596ms)
|
||||||
|
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (256.455544ms)
|
||||||
|
✔ a second host for the same data root refuses with exit 3 while the first runs (239.608612ms)
|
||||||
|
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (291.720324ms)
|
||||||
|
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (265.305714ms)
|
||||||
|
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (122.588192ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (178.527251ms)
|
||||||
|
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (197.615861ms)
|
||||||
|
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (180.946075ms)
|
||||||
|
✔ watchChildren reports a child that died before it was called, and one that dies later (21.7181ms)
|
||||||
|
✔ bus stop refuses to signal a live pid that is not a bus host (203.058669ms)
|
||||||
|
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (212.928287ms)
|
||||||
|
✔ bus start refuses with exit 3 without a notifier config (89.140811ms)
|
||||||
|
✔ bus start runs until bus stop; status reports it while it runs (656.935804ms)
|
||||||
|
✔ bus-service.sh renders the unit and installs it into a given directory (28.348121ms)
|
||||||
|
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (270.794964ms)
|
||||||
|
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (815.154892ms)
|
||||||
|
✔ a runner that stops at once ends its launch with the runner's reason (263.406741ms)
|
||||||
|
✔ 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 (815.211823ms)
|
||||||
|
✔ 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 (3657.484394ms)
|
||||||
|
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (113.489374ms)
|
||||||
|
✔ zoned uses the IANA zone across DST (15.474129ms)
|
||||||
|
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (129.254695ms)
|
||||||
|
✔ two blocking decisions get two DMs with different nonces (128.049389ms)
|
||||||
|
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.188199ms)
|
||||||
|
✔ a failed DM is journaled, backs off, and is retried until it lands (133.43126ms)
|
||||||
|
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (124.968283ms)
|
||||||
|
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (116.127597ms)
|
||||||
|
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (93.4847ms)
|
||||||
|
✔ 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 (165.887479ms)
|
||||||
|
✔ a restart after the second refusal does not send before that refusal's 30 min are up (141.037869ms)
|
||||||
|
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (153.200525ms)
|
||||||
|
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (137.038182ms)
|
||||||
|
✔ an inbox read failure is logged and the next poll retries (0.650764ms)
|
||||||
|
✔ no Discord id reaches the journal or the log (170.603977ms)
|
||||||
|
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (49.136453ms)
|
||||||
|
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (81.494654ms)
|
||||||
|
✔ the journal: a whole file that is one torn line truncates to empty (22.271123ms)
|
||||||
|
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.728536ms)
|
||||||
|
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.584967ms)
|
||||||
|
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.950866ms)
|
||||||
|
✔ the journal: a symlinked directory refuses and says it is a link (0.292651ms)
|
||||||
|
✔ 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.45137ms)
|
||||||
|
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.395917ms)
|
||||||
|
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.410196ms)
|
||||||
|
✔ digest content stays within Discord's 2000 characters (0.266032ms)
|
||||||
|
✔ runLoop never overlaps ticks and stops after the one in flight (110.668485ms)
|
||||||
|
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 (375.657886ms)
|
||||||
|
✔ the transport writes {business, verb, args} to the child and reads its JSON (34.268882ms)
|
||||||
|
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2137.373473ms)
|
||||||
|
✔ busExit and refuseInsideAgent (0.46046ms)
|
||||||
|
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (229.138287ms)
|
||||||
|
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1162.869992ms)
|
||||||
|
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (170.664262ms)
|
||||||
|
✔ launches off and on go to the broker and change the business's launch state (117.53746ms)
|
||||||
|
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (82.164709ms)
|
||||||
|
ℹ tests 77
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 77
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 6022.109024
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
✔ sessionModel: agent vars win, then the system's execution settings (17.278444ms)
|
||||||
|
✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (6.894082ms)
|
||||||
|
✔ a claude-code bundle adds the wrapped gate hook and the MCP config (5.689389ms)
|
||||||
|
✔ a bundle is written once: an existing file refuses (2.33278ms)
|
||||||
|
✔ a path with a single quote can't go into the hook command (3.881591ms)
|
||||||
|
✔ allow exits 0, a deny exits 2 with the reason on stderr (130.760929ms)
|
||||||
|
✔ a missing or wrong policy, or a bad event, exits 2 (76.614744ms)
|
||||||
|
✔ the bundle's wrapped command: a missing gate or node still blocks (1087.842947ms)
|
||||||
|
✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (731.662814ms)
|
||||||
|
✔ claude: the hook alone blocks a path outside the workspace (467.79837ms)
|
||||||
|
✔ claude: a second turn resumes the first turn's session (708.774651ms)
|
||||||
|
✔ claude: a missing hook or MCP file refuses before claude starts (16.048526ms)
|
||||||
|
✔ pi: policy tools and typed tools pass, anything else is blocked (3.616285ms)
|
||||||
|
✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.753166ms)
|
||||||
|
✔ file tool paths must resolve inside the workspace (1.154562ms)
|
||||||
|
✔ pi's own path normalisation can't be used to step out (1.161134ms)
|
||||||
|
✔ a symlink inside the workspace that points out is outside (1.095973ms)
|
||||||
|
✔ claude path fields per tool (0.776238ms)
|
||||||
|
✔ glob patterns stay inside the workspace (1.201128ms)
|
||||||
|
✔ a path that can't be checked is blocked (0.581822ms)
|
||||||
|
✔ initialize, ping and tools/list (44.402251ms)
|
||||||
|
✔ tools/call goes through the tool socket; a refusal is an isError result (31.674476ms)
|
||||||
|
✔ unknown tools and methods are JSON-RPC errors and never reach the socket (30.522533ms)
|
||||||
|
✔ a missing argument is a usage error (28.622595ms)
|
||||||
|
✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (350.095579ms)
|
||||||
|
✔ pi: a missing extension refuses before any model call (8.3472ms)
|
||||||
|
✔ pi: an extension without its configuration fails pi's start (275.717684ms)
|
||||||
|
✔ founderCheck: founder variables, then a needed service without a usable token (1.095529ms)
|
||||||
|
✔ turnRequest names the sender, class, reply and decision (0.177223ms)
|
||||||
|
✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (238.692774ms)
|
||||||
|
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (99.758588ms)
|
||||||
|
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (439.551997ms)
|
||||||
|
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1312.043711ms)
|
||||||
|
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (151.833299ms)
|
||||||
|
✔ founder credentials stop before the claim (20) (166.088906ms)
|
||||||
|
✔ a refused claim exits 21; an ended run's capability exits 22 (212.018616ms)
|
||||||
|
✔ the launch ending under a running session exits 22 (155.468877ms)
|
||||||
|
✔ a broker that stays unreachable exits 23 after brokerRetries polls (248.508739ms)
|
||||||
|
✔ a broker that is down at the claim exits 23, not 21 (105.883143ms)
|
||||||
|
✔ no capability, or a malformed one, on stdin exits 2 (153.856802ms)
|
||||||
|
✔ the PM gets launch, its task verbs and the reads (8.254629ms)
|
||||||
|
✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (4.257605ms)
|
||||||
|
✔ launch only when the business's launch block names the instance as launcher (1.639872ms)
|
||||||
|
✔ an action outside the instance's authority has no tool (1.401759ms)
|
||||||
|
✔ callTool: one JSON line out, the result back, a refusal rejects (8.517699ms)
|
||||||
|
ℹ tests 45
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 45
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 10363.182249
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
✔ resolveSeat: by name under --repo resolves the repo layout (1.254502ms)
|
||||||
|
✔ resolveSeat: by path resolves the fleet layout (0.516747ms)
|
||||||
|
✔ resolveSeat: refusals for missing dir, missing launch.sh, non-executable launch.sh, invalid name, and unknown layout (0.847962ms)
|
||||||
|
✔ tmuxContext: outside tmux, default socket, custom socket, and exec failure (0.635368ms)
|
||||||
|
✔ makeRegistration produces a record that validates; each shape violation throws SeatError (0.803431ms)
|
||||||
|
✔ writeRegistration/readRegistration: round trip, permissions, absence, and malformed records (1.24324ms)
|
||||||
|
✔ updateTask: changes task, taskSetBy and updatedAt only, and refuses appropriately (1.239321ms)
|
||||||
|
✔ CLI launch: registers, execs the fake launch script, and passes args through (30.059497ms)
|
||||||
|
✔ CLI launch: --harness lands in the record (31.801058ms)
|
||||||
|
✔ CLI launch: the launch script's own exit code passes through (28.546759ms)
|
||||||
|
✔ CLI launch: relaunching a seat rewrites the one registration record (58.693016ms)
|
||||||
|
✔ CLI launch: omitting --task records an empty string, not null (28.874274ms)
|
||||||
|
✔ CLI seat task: updates only the task after a launch, and refuses on an unlaunched seat (80.75812ms)
|
||||||
|
✔ CLI refusals: no args, unknown flag, missing config, already-registered env, and exec failure (135.711596ms)
|
||||||
|
✔ samePath: equal paths, symlinked dirs, distinct dirs, and non-strings (0.40395ms)
|
||||||
|
✔ resolveSetBy: explicit --by wins over the environment; absent or empty environment gives unknown; invalid explicit or environment values refuse with exit 4 (0.555598ms)
|
||||||
|
✔ 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.899447ms)
|
||||||
|
✔ 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.861976ms)
|
||||||
|
✔ 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 (271.018867ms)
|
||||||
|
✔ family: exactly one launch.max key in the model name, else null (0.617808ms)
|
||||||
|
✔ sessionEnv passes only the allowlist, the repo's bin on PATH, and the run id (0.665325ms)
|
||||||
|
✔ newRun: short ids, 0700 directories, and a refusal when the socket path won't fit (1.463053ms)
|
||||||
|
✔ session file and launch log: 0600, the session file written once (1.346008ms)
|
||||||
|
✔ endReason maps the runner's exit codes; a signal is killed (0.107394ms)
|
||||||
|
✔ Registry mirrors to sessions.json; readSessions marks live entries; bad files refuse (1.244129ms)
|
||||||
|
✔ stopSession refuses an unknown run, reports a stale one, and won't signal a pid that isn't a runner (1.325051ms)
|
||||||
|
✔ a session runs under unshare as pid 1 of its namespace, claims, answers, and stops on mosaic stop (260.809593ms)
|
||||||
|
ℹ tests 27
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 27
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 726.22422
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
node-harness exit=0 ℹ pass 45 ℹ fail 0
|
||||||
|
node-seat exit=0 ℹ pass 27 ℹ fail 0
|
||||||
|
node-cli exit=0 ℹ pass 77 ℹ fail 0
|
||||||
|
node-bus exit=0 ℹ pass 74 ℹ fail 0
|
||||||
|
node-business exit=0 ℹ pass 60 ℹ fail 0
|
||||||
|
test-auth exit=0 selftest: 15 passed, 0 failed
|
||||||
|
test-config exit=0 selftest: 24 passed, 0 failed
|
||||||
|
test-conductor exit=0 selftest: 17 passed, 0 failed
|
||||||
|
test-queue exit=0 queue suite: 27 passed, 0 failed
|
||||||
|
test-foundation exit=0 selftest: 44 passed, 0 failed
|
||||||
|
test-extension-package exit=0 extension package selftest: 18 passed, 0 failed
|
||||||
|
test-release exit=0 selftest: 4 passed, 0 failed
|
||||||
|
test-discord exit=0 discord suite: 66 passed, 0 failed
|
||||||
|
test-task exit=1 selftest: 26 passed, 2 failed
|
||||||
|
GATE-DONE
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
OK status with missing harness credential exits 3 and still lists accounts
|
||||||
|
OK status reports harness credential (read-only) + mosaic accounts
|
||||||
|
OK api key material never reaches output
|
||||||
|
OK oauth token material never reaches output
|
||||||
|
OK unparseable credential file exits 2
|
||||||
|
OK symlinked credential file exits 4
|
||||||
|
OK env-side credential names reported
|
||||||
|
OK env var values never reach output
|
||||||
|
OK accounts without an accounts dir reports none and creates nothing
|
||||||
|
OK accounts lists files and marks the active one
|
||||||
|
OK loose account perms flagged in listing
|
||||||
|
OK agent --auth with missing account file refuses (exit 4)
|
||||||
|
OK agent --auth with non-0600 account file refuses
|
||||||
|
OK agent --auth with invalid account name refuses
|
||||||
|
OK auth.sh without valid config refuses
|
||||||
|
|
||||||
|
selftest: 15 passed, 0 failed
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
Note: switching to '2855e628ee91d046f4ea365b45dba5b0ccf4fd2a'.
|
||||||
|
|
||||||
|
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 '2855e628ee91d046f4ea365b45dba5b0ccf4fd2a'.
|
||||||
|
|
||||||
|
You are in 'detached HEAD' state. You can look around, make experimental
|
||||||
|
changes and commit them, and you can discard any commits you make in this
|
||||||
|
state without impacting any branches by switching back to a branch.
|
||||||
|
|
||||||
|
If you want to create a new branch to retain commits you create, you may
|
||||||
|
do so (now or later) by using -c with the switch command. Example:
|
||||||
|
|
||||||
|
git switch -c <new-branch-name>
|
||||||
|
|
||||||
|
Or undo this operation with:
|
||||||
|
|
||||||
|
git switch -
|
||||||
|
|
||||||
|
Turn off this advice by setting config variable advice.detachedHead to false
|
||||||
|
|
||||||
|
OK dry-run: allowed change, exit 0, nothing committed (exit 0)
|
||||||
|
OK dry-run committed nothing
|
||||||
|
OK apply: allowed change exits 0 (exit 0)
|
||||||
|
OK apply: attribution in commit subject
|
||||||
|
OK apply: target tree clean after commit
|
||||||
|
OK disallowed path refused (exit 1)
|
||||||
|
OK disallowed path: target untouched
|
||||||
|
OK syntax gate refused broken .mjs (exit 1)
|
||||||
|
OK syntax gate: target untouched
|
||||||
|
OK suite failure refused (exit 1)
|
||||||
|
OK suite failure: target reverted to clean
|
||||||
|
OK disabled policy refused (exit 2)
|
||||||
|
OK disabled policy: target untouched
|
||||||
|
OK failed run refused (exit 1)
|
||||||
|
OK failed run: target untouched
|
||||||
|
OK missing run exits 4 (exit 4)
|
||||||
|
OK invalid policy exits 2 (exit 2)
|
||||||
|
|
||||||
|
selftest: 17 passed, 0 failed
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
OK absent adapter defaults to pi
|
||||||
|
OK adapter mock validates (exit 0)
|
||||||
|
OK unsupported adapter exits 2 (exit 2)
|
||||||
|
OK env exports adapter
|
||||||
|
OK bootstrap creates default when absent (exit 0)
|
||||||
|
OK bootstrap wrote config file
|
||||||
|
OK bootstrap is idempotent on existing config (exit 0)
|
||||||
|
OK bootstrap did not rewrite existing config
|
||||||
|
OK validate missing config exits 3 (exit 3)
|
||||||
|
OK malformed JSON exits 2 (exit 2)
|
||||||
|
OK unsupported configVersion exits 2 (exit 2)
|
||||||
|
OK unknown top-level key exits 2 (exit 2)
|
||||||
|
OK unknown execution key exits 2 (exit 2)
|
||||||
|
OK unsupported backend exits 2 (exit 2)
|
||||||
|
OK unsupported environment exits 2 (exit 2)
|
||||||
|
OK relative dataRoot exits 2 (exit 2)
|
||||||
|
OK non-canonical dataRoot exits 2 (exit 2)
|
||||||
|
OK filesystem root dataRoot exits 2 (exit 2)
|
||||||
|
OK home directory dataRoot exits 2 (exit 2)
|
||||||
|
OK dataRoot containing config dir exits 2 (exit 2)
|
||||||
|
OK control character in provider exits 2 (exit 2)
|
||||||
|
OK symlinked config file exits 2 (exit 2)
|
||||||
|
OK env exports resolve correctly
|
||||||
|
OK failed validation modified nothing
|
||||||
|
|
||||||
|
selftest: 24 passed, 0 failed
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
toolchain: node v26.8.1
|
||||||
|
|
||||||
|
OK syntax: packages/discord/src/approvals.mjs
|
||||||
|
OK syntax: packages/discord/src/authorize.mjs
|
||||||
|
OK syntax: packages/discord/src/binding.mjs
|
||||||
|
OK syntax: packages/discord/src/cli.mjs
|
||||||
|
OK syntax: packages/discord/src/connector.mjs
|
||||||
|
OK syntax: packages/discord/src/context.mjs
|
||||||
|
OK syntax: packages/discord/src/engine-pi.mjs
|
||||||
|
OK syntax: packages/discord/src/errors.mjs
|
||||||
|
OK syntax: packages/discord/src/gateway.mjs
|
||||||
|
OK syntax: packages/discord/src/git.mjs
|
||||||
|
OK syntax: packages/discord/src/journal.mjs
|
||||||
|
OK syntax: packages/discord/src/notify.mjs
|
||||||
|
OK syntax: packages/discord/src/rest.mjs
|
||||||
|
OK syntax: packages/discord/src/setspark.mjs
|
||||||
|
OK syntax: packages/discord/src/tools.mjs
|
||||||
|
OK syntax: packages/discord/src/web.mjs
|
||||||
|
OK syntax: packages/discord/bin/git-credential.mjs
|
||||||
|
OK syntax: packages/discord/extension/tools.mjs
|
||||||
|
OK syntax: packages/discord/tests/approvals.test.mjs
|
||||||
|
OK syntax: packages/discord/tests/authorize.test.mjs
|
||||||
|
OK syntax: packages/discord/tests/binding.test.mjs
|
||||||
|
OK syntax: packages/discord/tests/connector.test.mjs
|
||||||
|
OK syntax: packages/discord/tests/context.test.mjs
|
||||||
|
OK syntax: packages/discord/tests/engine.test.mjs
|
||||||
|
OK syntax: packages/discord/tests/fake-pi.mjs
|
||||||
|
OK syntax: packages/discord/tests/gateway.test.mjs
|
||||||
|
OK syntax: packages/discord/tests/git.test.mjs
|
||||||
|
OK syntax: packages/discord/tests/helpers.mjs
|
||||||
|
OK syntax: packages/discord/tests/journal.test.mjs
|
||||||
|
OK syntax: packages/discord/tests/notify.test.mjs
|
||||||
|
OK syntax: packages/discord/tests/recover.test.mjs
|
||||||
|
OK syntax: packages/discord/tests/rest.test.mjs
|
||||||
|
OK syntax: packages/discord/tests/setspark.test.mjs
|
||||||
|
OK syntax: packages/discord/tests/tools.test.mjs
|
||||||
|
OK syntax: packages/discord/tests/web.test.mjs
|
||||||
|
OK syntax: packages/discord/fixtures/claim-worker.mjs
|
||||||
|
OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs
|
||||||
|
OK syntax: scripts/discord.sh
|
||||||
|
OK syntax: scripts/discord-service.sh
|
||||||
|
OK packages/discord declares no dependencies
|
||||||
|
OK no bot-token-shaped string in packages/discord
|
||||||
|
OK fixture binding uses placeholder ids only
|
||||||
|
OK fixture binding validates
|
||||||
|
OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool
|
||||||
|
OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start
|
||||||
|
OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root
|
||||||
|
OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token
|
||||||
|
OK real pi with protocol vault adds reserve_id to the git verbs
|
||||||
|
OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key
|
||||||
|
OK real pi refuses a git key on a read-only root (fail closed)
|
||||||
|
OK real pi with the pilot flags (--no-tools) exposes no tool at all
|
||||||
|
OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed)
|
||||||
|
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
|
||||||
|
OK node --test packages/discord/tests/ (ℹ pass 178)
|
||||||
|
OK scripts/discord.sh --help exits 0
|
||||||
|
OK scripts/discord.sh check without a binding exits 4
|
||||||
|
OK scripts/discord.sh recover without a binding exits 4
|
||||||
|
OK scripts/discord.sh reload without a binding exits 4
|
||||||
|
OK scripts/discord-service.sh without a command exits 4
|
||||||
|
OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP
|
||||||
|
OK service install writes the rendered unit (0644) and leaves no temp file
|
||||||
|
OK service install a second time reports unchanged
|
||||||
|
OK systemd-analyze verify accepts the rendered unit
|
||||||
|
OK service uninstall removes the unit file
|
||||||
|
OK service install with an unknown flag exits 4
|
||||||
|
OK service install with USER unset finishes and names the account for lingering
|
||||||
|
|
||||||
|
discord suite: 66 passed, 0 failed
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
OK initial ordinary-file install
|
||||||
|
OK installed tree matches canonical source
|
||||||
|
OK installed tree has no symlinks
|
||||||
|
OK check detects installation drift
|
||||||
|
OK sync refuses to overwrite installation drift
|
||||||
|
OK check detects an extra destination file
|
||||||
|
OK check detects an extra destination directory
|
||||||
|
OK check rejects a destination symlink
|
||||||
|
OK sync accepts a canonical source update
|
||||||
|
OK updated installation matches canonical source
|
||||||
|
scripts/test-extension-package.sh: line 14: 42746 Killed "$@" > /dev/null 2>&1
|
||||||
|
OK forced interruption kills the replacing process
|
||||||
|
OK next invocation recovers old consistent installation
|
||||||
|
OK interrupted replacement rolled back
|
||||||
|
OK sync succeeds after interruption recovery
|
||||||
|
OK unlocked stale lock file does not block
|
||||||
|
OK active lock refuses a concurrent sync
|
||||||
|
OK source symlink fails closed
|
||||||
|
OK nested second entrypoint fails closed
|
||||||
|
|
||||||
|
extension package selftest: 18 passed, 0 failed
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0
|
||||||
|
|
||||||
|
OK syntax: scripts/foundation-inspect.mjs
|
||||||
|
OK syntax: scripts/foundation/strict-json.mjs
|
||||||
|
OK syntax: scripts/foundation/canonical.mjs
|
||||||
|
OK syntax: scripts/foundation/resolve.mjs
|
||||||
|
OK syntax: scripts/foundation/validate-record.mjs
|
||||||
|
OK syntax: scripts/foundation/fixtures/build-fixtures.mjs
|
||||||
|
OK syntax: scripts/foundation/canonical.test.mjs
|
||||||
|
OK syntax: scripts/foundation/cli.test.mjs
|
||||||
|
OK syntax: scripts/foundation/fixtures.test.mjs
|
||||||
|
OK syntax: scripts/foundation/resolve.test.mjs
|
||||||
|
OK syntax: scripts/foundation/strict-json.test.mjs
|
||||||
|
OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written)
|
||||||
|
OK fixture generator runs
|
||||||
|
OK checked-in fixtures/bundles equal a fresh generation
|
||||||
|
OK checked-in fixtures/raw equal a fresh generation
|
||||||
|
OK checked-in fixtures/index.json equal a fresh generation
|
||||||
|
OK checked-in demo bundles equal a fresh generation
|
||||||
|
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
|
||||||
|
OK node --test scripts/foundation/ (ℹ pass 80)
|
||||||
|
OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof)
|
||||||
|
platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999)
|
||||||
|
node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases)
|
||||||
|
schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10
|
||||||
|
profile column (schema-valid records only): profile-valid 510, profile-invalid 30
|
||||||
|
profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns
|
||||||
|
OK oracle: zero schema-column disagreements with the pinned checker
|
||||||
|
OK oracle: strict-only profile refusals are counted and asserted
|
||||||
|
OK demo: permitted read preview exits 0 (exit 0)
|
||||||
|
OK demo: permitted file.change preview exits 0 (exit 0)
|
||||||
|
OK demo: assignment.change proposal is unresolved (exit 3) (exit 3)
|
||||||
|
OK demo: revoked registration is refused (exit 3) (exit 3)
|
||||||
|
OK demo: message is not authority (exit 3) (exit 3)
|
||||||
|
OK usage: no arguments exits 2 (exit 2)
|
||||||
|
OK io: missing file exits 4 (exit 4)
|
||||||
|
OK io: directory exits 4 (exit 4)
|
||||||
|
OK io: symlink exits 4 (O_NOFOLLOW) (exit 4)
|
||||||
|
OK bound: oversize fixture exits 2 (exit 2)
|
||||||
|
OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2)
|
||||||
|
OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2)
|
||||||
|
OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0)
|
||||||
|
OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed
|
||||||
|
OK text output starts with the disclaimer
|
||||||
|
OK json output is valid JSON with result allowed and exactly the charter §7 fields
|
||||||
|
OK json golden matches byte-for-byte
|
||||||
|
OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal
|
||||||
|
OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs
|
||||||
|
OK canary never printed (bundle run and credential-file run)
|
||||||
|
OK a non-bundle JSON file is refused at the shape gate, not read into output
|
||||||
|
OK no field of the non-bundle file is echoed
|
||||||
|
|
||||||
|
selftest: 44 passed, 0 failed
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
toolchain: node v26.8.1, git version 2.55.0
|
||||||
|
|
||||||
|
OK syntax: packages/queue/src/cli.mjs
|
||||||
|
OK syntax: packages/queue/src/errors.mjs
|
||||||
|
OK syntax: packages/queue/src/io.mjs
|
||||||
|
OK syntax: packages/queue/src/lock.mjs
|
||||||
|
OK syntax: packages/queue/src/queue.mjs
|
||||||
|
OK syntax: packages/queue/src/review.mjs
|
||||||
|
OK syntax: packages/queue/src/store.mjs
|
||||||
|
OK syntax: packages/queue/tests/commit.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/data.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/dispatch.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/helpers.mjs
|
||||||
|
OK syntax: packages/queue/tests/lock.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/migration.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/review.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/store.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/write.test.mjs
|
||||||
|
OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs
|
||||||
|
OK syntax: packages/queue/tests/fixtures/kill-at.mjs
|
||||||
|
OK syntax: packages/queue/tests/fixtures/lock-child.mjs
|
||||||
|
OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh
|
||||||
|
OK syntax: scripts/queue-commit.sh
|
||||||
|
OK syntax: scripts/git-hooks/pre-commit
|
||||||
|
OK syntax: scripts/mosaic
|
||||||
|
OK queue-commit.sh, the guard and scripts/mosaic are executable
|
||||||
|
OK packages/queue declares no dependencies
|
||||||
|
ℹ tests 148
|
||||||
|
ℹ pass 148
|
||||||
|
ℹ fail 0
|
||||||
|
OK node --test packages/queue/tests/
|
||||||
|
OK scripts/mosaic queue help
|
||||||
|
skip queue verify and render --check: this checkout (/home/jwoltje/darkwing-scratch/r41/wt) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
|
||||||
|
|
||||||
|
queue suite: 27 passed, 0 failed
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
OK valid RELEASE resolves (exit 0)
|
||||||
|
OK invalid RELEASE exits 1 (exit 1)
|
||||||
|
OK missing RELEASE exits 1 (exit 1)
|
||||||
|
OK valid RELEASE leaves image tag consistent with version
|
||||||
|
skip state-machine cases (docker daemon unavailable)
|
||||||
|
|
||||||
|
selftest: 4 passed, 0 failed
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
OK valid task validates (exit 0)
|
||||||
|
OK unknown task key exits 2 (exit 2)
|
||||||
|
OK unsupported taskVersion exits 2 (exit 2)
|
||||||
|
OK invalid task id exits 2 (exit 2)
|
||||||
|
OK empty prompt exits 2 (exit 2)
|
||||||
|
OK NUL in expectExact exits 2 (exit 2)
|
||||||
|
OK out-of-range timeout exits 2 (exit 2)
|
||||||
|
OK missing mission file exits 4 (exit 4)
|
||||||
|
OK task with valid mission validates (exit 0)
|
||||||
|
OK invalid mission exits 2 (exit 2)
|
||||||
|
OK validate missing task exits 4 (exit 4)
|
||||||
|
OK validation does not modify the task file
|
||||||
|
OK prune dry-run exits 0 (exit 0)
|
||||||
|
OK dry-run deleted nothing
|
||||||
|
OK prune --keep=2 --yes removes oldest (exit 0)
|
||||||
|
OK kept exactly 2 newest runs
|
||||||
|
OK newest run kept, oldest pruned
|
||||||
|
OK append-only receipt written (3 entries)
|
||||||
|
OK sessions/workspaces untouched by prune
|
||||||
|
OK prune with invalid keep exits 4 (exit 4)
|
||||||
|
skip adapter seam cases (docker daemon unavailable)
|
||||||
|
skip workspace/capability cases (docker daemon unavailable)
|
||||||
|
skip live task cases (docker unavailable)
|
||||||
|
OK onboard without name exits 4 (non-interactive) (exit 4)
|
||||||
|
OK onboard --name renders profile (exit 0)
|
||||||
|
OK profile written
|
||||||
|
OK canon structure: required filled, optional placeholdered
|
||||||
|
OK canon sections present
|
||||||
|
FAIL user recall run succeeds (exit 1)
|
||||||
|
FAIL recalled user name (response: )
|
||||||
|
OK no agent identity on headless run
|
||||||
|
|
||||||
|
selftest: 26 passed, 2 failed
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# The dangling-symlink write through a real Claude Code session: the
|
||||||
|
# candidate adapter, a settings.json in the bundle's form running the real
|
||||||
|
# claude-gate.mjs, --tools Write, and a mock API whose first answer calls
|
||||||
|
# Write on the link. Dummy key, 127.0.0.1 only.
|
||||||
|
set -eu
|
||||||
|
WT=$1; OUT=$2
|
||||||
|
R=$(mktemp -d "$TMPDIR/r41-cdangle-XXXX")
|
||||||
|
mkdir -p "$R/home" "$R/ws" "$R/sess" "$R/bundle" "$R/outside"
|
||||||
|
ln -s "$R/outside/planted.txt" "$R/ws/notes.md"
|
||||||
|
echo "the generated prompt" > "$R/bundle/prompt.md"
|
||||||
|
printf '{"harness":"claude-code","workspace":"%s","tools":["write"],"typed":[]}\n' "$R/ws" > "$R/bundle/policy.json"
|
||||||
|
printf '{"hooks":{"PreToolUse":[{"matcher":"*","hooks":[{"type":"command","command":"timeout -k 2 10 %s %s %s || exit 2","timeout":20}]}]}}\n' \
|
||||||
|
"$(command -v node)" "$WT/packages/harness/src/claude-gate.mjs" "$R/bundle/policy.json" > "$R/bundle/settings.json"
|
||||||
|
echo '{"mcpServers":{}}' > "$R/bundle/mcp.json"
|
||||||
|
TOOL_USE=$(printf '{"name":"Write","input":{"file_path":"%s","content":"written through the gate\\n"}}' "$R/ws/notes.md") \
|
||||||
|
node "$(dirname "$0")/mock-api.mjs" "$R/api.log" > "$R/port" & MOCK=$!
|
||||||
|
while [ ! -s "$R/port" ]; do sleep 0.1; done
|
||||||
|
set +e
|
||||||
|
env -i PATH="$PATH" HOME="$R/home" USER="$USER" LANG=C.UTF-8 \
|
||||||
|
ANTHROPIC_BASE_URL="http://127.0.0.1:$(cat "$R/port")" ANTHROPIC_API_KEY=sk-ant-dummy-not-a-key \
|
||||||
|
DISABLE_TELEMETRY=1 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 MOSAIC_TOOLS=Write \
|
||||||
|
MOSAIC_SYSTEM_PROMPT_FILE="$R/bundle/prompt.md" MOSAIC_REQUEST="Message m1 from jason: write notes" \
|
||||||
|
MOSAIC_WORKSPACE="$R/ws" MOSAIC_SESSION_DIR="$R/sess" MOSAIC_MODEL=claude-sonnet-5-5 \
|
||||||
|
MOSAIC_CLAUDE_SETTINGS="$R/bundle/settings.json" MOSAIC_CLAUDE_MCP_CONFIG="$R/bundle/mcp.json" \
|
||||||
|
timeout 90 /bin/sh "$WT/adapters/claude/adapter.sh" < /dev/null > "$R/stdout" 2> "$R/stderr"
|
||||||
|
echo "exit $?" > "$R/exit"
|
||||||
|
kill $MOCK
|
||||||
|
{
|
||||||
|
echo "$(cat "$R/exit"); stdout: $(head -c 200 "$R/stdout"); stderr: $(head -c 300 "$R/stderr")"
|
||||||
|
echo "POST /v1/messages: $(grep -c '"url":"/v1/messages' "$R/api.log")"
|
||||||
|
echo "tool_result sent back: $(node -e '
|
||||||
|
const ls=require("fs").readFileSync(process.argv[1],"utf8").trim().split("\n").map(l=>JSON.parse(l)).filter(l=>l.url.startsWith("/v1/messages"));
|
||||||
|
const b=JSON.parse(ls.at(-1).body); const r=b.messages.flatMap(m=>Array.isArray(m.content)?m.content:[]).filter(c=>c.type==="tool_result");
|
||||||
|
console.log(JSON.stringify(r.map(c=>({is_error:c.is_error??false,content:c.content}))));' "$R/api.log")"
|
||||||
|
echo "outside file: $( [ -e "$R/outside/planted.txt" ] && cat "$R/outside/planted.txt" || echo absent)"
|
||||||
|
} | tee -a "$OUT"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
exit 0; stdout: mock answer; stderr:
|
||||||
|
POST /v1/messages: 2
|
||||||
|
tool_result sent back: [{"is_error":true,"content":"/home/jwoltje/darkwing-scratch/tmp/r41-cdangle-0sjd/ws/notes.md resolves through a symlink to /home/jwoltje/darkwing-scratch/tmp/r41-cdangle-0sjd/outside/planted.txt, which is outside /home/jwoltje/darkwing-scratch/tmp/r41-cdangle-0sjd/ws; --restricted confines the file tools to the working directory."}]
|
||||||
|
outside file: absent
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Does a Claude Code session started by adapters/claude/adapter.sh load
|
||||||
|
# CLAUDE.md files (fake HOME, parent and workspace) and auto-memory?
|
||||||
|
# Runs the host's claude against the mock API with a dummy key.
|
||||||
|
set -eu
|
||||||
|
WT=$1; OUT=$2; VARIANT=$3 # VARIANT: candidate | no-restricted
|
||||||
|
R=$(mktemp -d "$TMPDIR/r41-claude-XXXX")
|
||||||
|
mkdir -p "$R/home/.claude" "$R/work/ws" "$R/sess" "$R/bundle"
|
||||||
|
echo "MARKER-USER-CLAUDE-MD" > "$R/home/.claude/CLAUDE.md"
|
||||||
|
echo "MARKER-PARENT-CLAUDE-MD" > "$R/work/CLAUDE.md"
|
||||||
|
echo "MARKER-WS-CLAUDE-MD" > "$R/work/ws/CLAUDE.md"
|
||||||
|
SLUG=$(printf '%s' "$R/work/ws" | sed 's|[/.]|-|g')
|
||||||
|
mkdir -p "$R/home/.claude/projects/$SLUG/memory"
|
||||||
|
echo "MARKER-AUTOMEMORY" > "$R/home/.claude/projects/$SLUG/memory/MEMORY.md"
|
||||||
|
echo "the generated prompt" > "$R/bundle/prompt.md"
|
||||||
|
echo '{}' > "$R/bundle/settings.json"
|
||||||
|
echo '{"mcpServers":{}}' > "$R/bundle/mcp.json"
|
||||||
|
cp "$WT/adapters/claude/adapter.sh" "$R/adapter.sh"
|
||||||
|
[ "$VARIANT" = candidate ] || sed -i '/--restricted \\/d' "$R/adapter.sh"
|
||||||
|
node "$(dirname "$0")/mock-api.mjs" "$R/api.log" > "$R/port" & MOCK=$!
|
||||||
|
while [ ! -s "$R/port" ]; do sleep 0.1; done
|
||||||
|
set +e
|
||||||
|
env -i PATH="$PATH" HOME="$R/home" USER="$USER" LANG=C.UTF-8 \
|
||||||
|
ANTHROPIC_BASE_URL="http://127.0.0.1:$(cat "$R/port")" ANTHROPIC_API_KEY=sk-ant-dummy-not-a-key \
|
||||||
|
DISABLE_TELEMETRY=1 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||||
|
MOSAIC_SYSTEM_PROMPT_FILE="$R/bundle/prompt.md" MOSAIC_REQUEST="Message m1 from jason: hi" \
|
||||||
|
MOSAIC_WORKSPACE="$R/work/ws" MOSAIC_SESSION_DIR="$R/sess" MOSAIC_MODEL=claude-sonnet-5-5 \
|
||||||
|
MOSAIC_CLAUDE_SETTINGS="$R/bundle/settings.json" MOSAIC_CLAUDE_MCP_CONFIG="$R/bundle/mcp.json" \
|
||||||
|
timeout 60 /bin/sh "$R/adapter.sh" > "$R/stdout" 2> "$R/stderr"
|
||||||
|
echo "exit $?" > "$R/exit"
|
||||||
|
kill $MOCK
|
||||||
|
{
|
||||||
|
echo "variant $VARIANT: $(cat "$R/exit"); stdout: $(head -c 200 "$R/stdout"); stderr: $(head -c 300 "$R/stderr")"
|
||||||
|
echo "requests: $(wc -l < "$R/api.log"); POST /v1/messages: $(grep -c '"url":"/v1/messages' "$R/api.log")"
|
||||||
|
for m in MARKER-USER-CLAUDE-MD MARKER-PARENT-CLAUDE-MD MARKER-WS-CLAUDE-MD MARKER-AUTOMEMORY "the generated prompt"; do
|
||||||
|
printf ' %-26s %s\n' "$m" "$(grep -c "$m" "$R/api.log" || true) request(s)"
|
||||||
|
done
|
||||||
|
} | tee -a "$OUT"
|
||||||
|
echo "$R" >> "$OUT.dirs"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
variant candidate: exit 0; stdout: mock answer; stderr: Warning: no stdin data received in 3s, proceeding without it. If piping from a slow command, redirect stdin explicitly: < /dev/null to skip, or wait longer.
|
||||||
|
requests: 1; POST /v1/messages: 1
|
||||||
|
MARKER-USER-CLAUDE-MD 0 request(s)
|
||||||
|
MARKER-PARENT-CLAUDE-MD 0 request(s)
|
||||||
|
MARKER-WS-CLAUDE-MD 0 request(s)
|
||||||
|
MARKER-AUTOMEMORY 0 request(s)
|
||||||
|
the generated prompt 1 request(s)
|
||||||
|
variant no-restricted: exit 0; stdout: mock answer; stderr: Warning: no stdin data received in 3s, proceeding without it. If piping from a slow command, redirect stdin explicitly: < /dev/null to skip, or wait longer.
|
||||||
|
requests: 1; POST /v1/messages: 1
|
||||||
|
MARKER-USER-CLAUDE-MD 1 request(s)
|
||||||
|
MARKER-PARENT-CLAUDE-MD 1 request(s)
|
||||||
|
MARKER-WS-CLAUDE-MD 1 request(s)
|
||||||
|
MARKER-AUTOMEMORY 1 request(s)
|
||||||
|
the generated prompt 1 request(s)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Does launcher.close() (a beforeClose hook) wait on a launch.sock client
|
||||||
|
// that never closes its side? MODE=silent sends nothing; MODE=line sends a
|
||||||
|
// request line and keeps the socket open after the reply.
|
||||||
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { connect } from "node:net";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
const WT = process.env.WT, MODE = process.env.MODE ?? "silent";
|
||||||
|
const { bootConfig, loadSystem } = await import(`${WT}/packages/cli/src/config.mjs`);
|
||||||
|
const { startHost } = await import(`${WT}/packages/cli/src/host.mjs`);
|
||||||
|
const { createLauncher } = await import(`${WT}/packages/cli/src/launcher.mjs`);
|
||||||
|
const { launchSocketPath } = await import(`${WT}/packages/seat/src/session.mjs`);
|
||||||
|
const { fixture } = await import(`${WT}/packages/cli/tests/helpers.mjs`);
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "r41-close-"));
|
||||||
|
const f = fixture(root, "acme", (doc) => ((doc.launch = { by: "pm", instances: ["coder"], max: { opus: 1, sonnet: 1 } }), doc));
|
||||||
|
mkdirSync(f.dataRoot, { mode: 0o700 });
|
||||||
|
const adapter = join(root, "adapter.sh");
|
||||||
|
writeFileSync(adapter, `exec '${process.execPath}' '${WT}/packages/harness/tests/fixtures/fake-adapter.mjs'\n`);
|
||||||
|
const system = loadSystem({ env: f.env });
|
||||||
|
const host = await startHost({ boot: bootConfig({ system, businessId: "acme", env: f.env }), business: "acme", log: (l) => console.log("host:", l) });
|
||||||
|
const launcher = createLauncher({ host, system, env: f.env, adapters: { pi: adapter, "claude-code": adapter }, sessionDefaults: { pollInterval: 100 }, versions: { pi: "0.85.1", claude: null }, log: (l) => console.log("launcher:", l) });
|
||||||
|
await launcher.listen();
|
||||||
|
const s = connect(launchSocketPath(f.dataRoot));
|
||||||
|
s.allowHalfOpen = true;
|
||||||
|
await new Promise((r) => s.on("connect", r));
|
||||||
|
s.on("data", (b) => console.log(`client got ${b.toString().trim()} at ${Date.now() - t0} ms`));
|
||||||
|
s.on("end", () => console.log(`client got FIN at ${Date.now() - t0} ms (not ending its side)`));
|
||||||
|
const t0 = Date.now();
|
||||||
|
if (MODE === "line") s.write(`{"cap":"${"0".repeat(64)}","instance":"coder"}\n`);
|
||||||
|
await new Promise((r) => setTimeout(r, 200));
|
||||||
|
console.log(`host.close(0) at ${Date.now() - t0} ms`);
|
||||||
|
const closing = host.close(0).then((c) => `closed ${c}`);
|
||||||
|
const limit = new Promise((r) => setTimeout(() => r("still not closed"), Number(process.env.LIMIT ?? 25000)));
|
||||||
|
console.log(`${await Promise.race([closing, limit])} at ${Date.now() - t0} ms`);
|
||||||
|
s.destroy();
|
||||||
|
console.log(`client destroyed; ${await closing} at ${Date.now() - t0} ms`);
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
== MODE=silent
|
||||||
|
host.close(0) at 201 ms
|
||||||
|
client got {"ok":false,"error":"invalid-request"} at 10050 ms
|
||||||
|
client got FIN at 10050 ms (not ending its side)
|
||||||
|
still not closed at 25227 ms
|
||||||
|
client destroyed; closed 0 at 25240 ms
|
||||||
|
== MODE=line
|
||||||
|
client got {"ok":false,"error":"unauthenticated"} at 2 ms
|
||||||
|
client got FIN at 2 ms (not ending its side)
|
||||||
|
host.close(0) at 201 ms
|
||||||
|
still not closed at 25227 ms
|
||||||
|
client destroyed; closed 0 at 25246 ms
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// Does launcher.close() SIGKILL a session whose runner never answers its
|
||||||
|
// SIGTERM? Launch the PM, SIGSTOP its runner from outside the namespace (a
|
||||||
|
// stopped process handles no signal but SIGKILL), close the host, and time
|
||||||
|
// it. Imports from the review worktree by absolute path.
|
||||||
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
const WT = process.env.WT;
|
||||||
|
const { bootConfig, loadSystem } = await import(`${WT}/packages/cli/src/config.mjs`);
|
||||||
|
const { startHost, startTimeOf } = await import(`${WT}/packages/cli/src/host.mjs`);
|
||||||
|
const { createLauncher } = await import(`${WT}/packages/cli/src/launcher.mjs`);
|
||||||
|
const { readSessions, launchLogFile } = await import(`${WT}/packages/seat/src/session.mjs`);
|
||||||
|
const { fixture } = await import(`${WT}/packages/cli/tests/helpers.mjs`);
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "r41-kill-"));
|
||||||
|
const f = fixture(root, "acme", (doc) => {
|
||||||
|
doc.roles.coder.vars.model = "claude-sonnet-5-5";
|
||||||
|
doc.launch = { by: "pm", instances: ["coder"], max: { opus: 1, sonnet: 1 } };
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
mkdirSync(f.dataRoot, { mode: 0o700 });
|
||||||
|
const adapter = join(root, "adapter.sh");
|
||||||
|
writeFileSync(adapter, `exec '${process.execPath}' '${WT}/packages/harness/tests/fixtures/fake-adapter.mjs'\n`);
|
||||||
|
const system = loadSystem({ env: f.env });
|
||||||
|
const logs = [];
|
||||||
|
const host = await startHost({ boot: bootConfig({ system, businessId: "acme", env: f.env }), business: "acme", log: (l) => logs.push(`host: ${l}`) });
|
||||||
|
const launcher = createLauncher({ host, system, env: f.env, adapters: { pi: adapter, "claude-code": adapter }, sessionDefaults: { pollInterval: 100 }, versions: { pi: "0.85.1", claude: null }, log: (l) => logs.push(`launcher: ${l}`) });
|
||||||
|
await launcher.listen();
|
||||||
|
const pm = await launcher.launchPm();
|
||||||
|
const [s] = readSessions(f.dataRoot);
|
||||||
|
const runnerLog = join(f.dataRoot, "launches", "acme", pm.run, "runner.log");
|
||||||
|
for (let i = 0; i < 300 && !readFileSync(runnerLog, "utf8").includes("claimed by run"); i++) await new Promise((r) => setTimeout(r, 50));
|
||||||
|
console.log(`pm run ${pm.run}: session pid ${s.pid}, runner pid ${s.runnerPid}, claimed`);
|
||||||
|
process.kill(s.runnerPid, "SIGSTOP");
|
||||||
|
console.log("runner SIGSTOPped");
|
||||||
|
const t0 = Date.now();
|
||||||
|
const code = await host.close(0);
|
||||||
|
console.log(`host.close -> ${code} after ${Date.now() - t0} ms`);
|
||||||
|
console.log(`session pid alive: ${startTimeOf(s.pid) === s.startTime}; runner pid alive: ${startTimeOf(s.runnerPid) === s.runnerStartTime}`);
|
||||||
|
const ends = readFileSync(launchLogFile(f.dataRoot, "acme"), "utf8").trim().split("\n").map((l) => JSON.parse(l)).filter((e) => e.event === "end");
|
||||||
|
console.log("launch-log end lines:", JSON.stringify(ends.map((e) => ({ run: e.run, reason: e.reason, exitCode: e.exitCode, signal: e.signal }))));
|
||||||
|
console.log("sessions.json after:", JSON.stringify(readSessions(f.dataRoot)));
|
||||||
|
console.log(logs.filter((l) => /ended|run /.test(l)).join("\n"));
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pm run r497e2e1c6477: session pid 316699, runner pid 316700, claimed
|
||||||
|
runner SIGSTOPped
|
||||||
|
host.close -> 0 after 30033 ms
|
||||||
|
session pid alive: false; runner pid alive: false
|
||||||
|
launch-log end lines: [{"run":"r497e2e1c6477","reason":"killed","exitCode":null,"signal":"SIGKILL"}]
|
||||||
|
sessions.json after: []
|
||||||
|
launcher: run r497e2e1c6477 (pm) ended: killed
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// A dangling symlink in the workspace that points outside it. decide() walks
|
||||||
|
// up to the nearest existing ancestor with existsSync, which follows links,
|
||||||
|
// so the link itself counts as "missing" and the path passes. Then the same
|
||||||
|
// two calls pi's write tool makes (core/tools/write.js:45-48: mkdir of the
|
||||||
|
// dirname, recursive, then writeFile) create the file outside.
|
||||||
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync } from "node:fs";
|
||||||
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
const { decide } = await import(`${process.env.WT}/packages/harness/src/gate.mjs`);
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "r41-dangle-"));
|
||||||
|
const ws = join(root, "ws");
|
||||||
|
const outside = join(root, "outside", "planted.txt");
|
||||||
|
mkdirSync(dirname(outside), { recursive: true });
|
||||||
|
mkdirSync(ws);
|
||||||
|
symlinkSync(outside, join(ws, "notes.md")); // as a checked-out repo could carry it
|
||||||
|
for (const harness of ["pi", "claude-code"]) {
|
||||||
|
const policy = { harness, workspace: ws, tools: ["write", "edit"], typed: [] };
|
||||||
|
const [tool, field] = harness === "pi" ? ["write", "path"] : ["Write", "file_path"];
|
||||||
|
console.log(`${harness} ${tool} {${field}: "notes.md"}:`, JSON.stringify(decide(policy, tool, { [field]: harness === "pi" ? "notes.md" : join(ws, "notes.md") })));
|
||||||
|
}
|
||||||
|
console.log("outside file exists before:", existsSync(outside));
|
||||||
|
const target = join(ws, "notes.md");
|
||||||
|
await mkdir(dirname(target), { recursive: true });
|
||||||
|
await writeFile(target, "written through the gate\n", "utf-8");
|
||||||
|
console.log("outside file after pi's write calls:", existsSync(outside) ? JSON.stringify(readFileSync(outside, "utf8")) : "absent");
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pi write {path: "notes.md"}: {"allow":true}
|
||||||
|
claude-code Write {file_path: "notes.md"}: {"allow":true}
|
||||||
|
outside file exists before: false
|
||||||
|
outside file after pi's write calls: "written through the gate\n"
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// R1: host->broker IPC replies carry no id. Stall the broker past the 10 s
|
||||||
|
// firstReply wait with two requests queued, resume it, and see which reply
|
||||||
|
// each request gets. Imports from the review worktree by absolute path.
|
||||||
|
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
const WT = process.env.WT;
|
||||||
|
const { Client } = await import(`${WT}/packages/bus/src/client.mjs`);
|
||||||
|
const { bootConfig, loadSystem } = await import(`${WT}/packages/cli/src/config.mjs`);
|
||||||
|
const { startHost, startTimeOf } = await import(`${WT}/packages/cli/src/host.mjs`);
|
||||||
|
const { fixture } = await import(`${WT}/packages/cli/tests/helpers.mjs`);
|
||||||
|
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "r41-desync-"));
|
||||||
|
const f = fixture(root, "acme", (doc) => {
|
||||||
|
doc.launch = { by: "pm", instances: ["coder", "reviewer", "cto"], max: { opus: 1, sonnet: 1 } };
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
mkdirSync(f.dataRoot, { mode: 0o700 });
|
||||||
|
const system = loadSystem({ env: f.env });
|
||||||
|
const boot = bootConfig({ system, businessId: "acme", env: f.env });
|
||||||
|
const host = await startHost({ boot, business: "acme", log: (l) => console.log("host:", l) });
|
||||||
|
const bind = async (role, run) => {
|
||||||
|
const b = await host.bindLaunch({ business: "acme", role, run, harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) });
|
||||||
|
await new Client({ path: host.path, cap: b.cap }).call("role.claim");
|
||||||
|
return b.cap;
|
||||||
|
};
|
||||||
|
const pm = await bind("pm", "pm-run");
|
||||||
|
const cto = await bind("cto", "cto-run");
|
||||||
|
const show = (p) => p.then((v) => ({ ok: v }), (e) => ({ err: e.code ?? e.message }));
|
||||||
|
|
||||||
|
console.log("baseline, no stall:");
|
||||||
|
console.log(" authorizeLaunch(pm, coder):", JSON.stringify(await show(host.op({ op: "authorizeLaunch", cap: pm, instance: "coder" }))));
|
||||||
|
console.log(" authorizeLaunch(cto, coder):", JSON.stringify(await show(host.op({ op: "authorizeLaunch", cap: cto, instance: "coder" }))));
|
||||||
|
console.log(" identity(bogus):", JSON.stringify(await show(host.op({ op: "identity", cap: "bogus" }))));
|
||||||
|
|
||||||
|
console.log(`SIGSTOP broker ${host.pids.broker}`);
|
||||||
|
process.kill(host.pids.broker, "SIGSTOP");
|
||||||
|
const t0 = Date.now();
|
||||||
|
const p1 = show(host.op({ op: "authorizeLaunch", cap: pm, instance: "coder" }));
|
||||||
|
const p2 = show(host.op({ op: "authorizeLaunch", cap: cto, instance: "coder" }));
|
||||||
|
const p3 = show(host.op({ op: "identity", cap: "bogus" }));
|
||||||
|
const r1 = await p1;
|
||||||
|
console.log(`p1 authorizeLaunch(pm) after ${Date.now() - t0} ms:`, JSON.stringify(r1));
|
||||||
|
process.kill(host.pids.broker, "SIGCONT");
|
||||||
|
console.log("SIGCONT");
|
||||||
|
console.log("p2 authorizeLaunch(cto), should refuse:", JSON.stringify(await p2));
|
||||||
|
console.log("p3 identity(bogus), should refuse:", JSON.stringify(await p3));
|
||||||
|
const p4 = await show(host.op({ op: "identity", cap: cto }));
|
||||||
|
console.log("p4 identity(cto), should be cto:", JSON.stringify(p4));
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
console.log("--- bind case: two binds queued during a stall");
|
||||||
|
process.kill(host.pids.broker, "SIGSTOP");
|
||||||
|
const rec = (run) => ({ business: "acme", role: "coder", run, harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) });
|
||||||
|
const b1 = show(host.bindLaunch(rec("run-x")));
|
||||||
|
const b2 = show(host.bindLaunch({ ...rec("run-y"), role: "reviewer" }));
|
||||||
|
console.log("b1 bind run-x:", JSON.stringify(await b1));
|
||||||
|
process.kill(host.pids.broker, "SIGCONT");
|
||||||
|
const r2 = await b2;
|
||||||
|
console.log("b2 bind run-y (reviewer) got:", r2.ok ? JSON.stringify({ run: r2.ok.run, role: r2.ok.role }) : JSON.stringify(r2));
|
||||||
|
if (r2.ok?.cap) {
|
||||||
|
const who = await show(new Client({ path: host.path, cap: r2.ok.cap }).call("role.claim"));
|
||||||
|
console.log("claim with b2's cap:", JSON.stringify(who));
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, 3000));
|
||||||
|
console.log("after 3 s idle, identity(pm), should be pm:", JSON.stringify(await show(host.op({ op: "identity", cap: pm }))));
|
||||||
|
console.log("then identity(pm) again:", JSON.stringify(await show(host.op({ op: "identity", cap: pm }))));
|
||||||
|
await host.close(0);
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
baseline, no stall:
|
||||||
|
authorizeLaunch(pm, coder): {"ok":{"class":"within-role"}}
|
||||||
|
authorizeLaunch(cto, coder): {"err":"decision-required"}
|
||||||
|
identity(bogus): {"err":"unauthenticated"}
|
||||||
|
SIGSTOP broker 86383
|
||||||
|
p1 authorizeLaunch(pm) after 10024 ms: {"err":"broker did not reply within 10 s"}
|
||||||
|
SIGCONT
|
||||||
|
p2 authorizeLaunch(cto), should refuse: {"ok":{"class":"within-role"}}
|
||||||
|
p3 identity(bogus), should refuse: {"err":"decision-required"}
|
||||||
|
p4 identity(cto), should be cto: {"err":"unauthenticated"}
|
||||||
|
--- bind case: two binds queued during a stall
|
||||||
|
b1 bind run-x: {}
|
||||||
|
b2 bind run-y (reviewer) got: {"run":"run-x"}
|
||||||
|
claim with b2's cap: {"ok":{"role":"coder","run":"run-x"}}
|
||||||
|
after 3 s idle, identity(pm), should be pm: {}
|
||||||
|
then identity(pm) again: {"ok":{"business":"acme","role":"pm","run":"pm-run","human":null}}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// decide() on path spellings that might leave the workspace.
|
||||||
|
import { mkdirSync, mkdtempSync, symlinkSync, rmSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
const { decide } = await import(`${process.env.WT}/packages/harness/src/gate.mjs`);
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "r41-gate-"));
|
||||||
|
const ws = join(root, "ws");
|
||||||
|
mkdirSync(join(ws, "sub"), { recursive: true });
|
||||||
|
symlinkSync("/etc", join(ws, "etc-link"));
|
||||||
|
symlinkSync(join(root, "not-yet"), join(ws, "dangling"));
|
||||||
|
const pi = { harness: "pi", workspace: ws, tools: ["read", "write", "grep", "find", "ls"], typed: ["message.send"] };
|
||||||
|
const cc = { ...pi, harness: "claude-code" };
|
||||||
|
const cases = [
|
||||||
|
[pi, "read", { path: "sub/../../x" }], [pi, "read", { path: "@/etc/passwd" }], [pi, "read", { path: "~/x" }],
|
||||||
|
[pi, "read", { path: "file:///etc/passwd" }], [pi, "read", { path: " /etc/passwd" }], [pi, "read", { path: "etc-link/passwd" }],
|
||||||
|
[pi, "write", { path: "dangling/x" }], [pi, "read", { path: "sub/./x" }], [pi, "grep", { path: "..", pattern: "x" }],
|
||||||
|
[pi, "find", { pattern: "{..,x}/*" }], [pi, "find", { pattern: "*", path: "/" }], [pi, "ls", {}],
|
||||||
|
[pi, "bash", { command: "cat /etc/passwd" }], [pi, "message.send", {}],
|
||||||
|
[cc, "Read", { file_path: "/etc/passwd" }], [cc, "Read", { file_path: `${ws}/sub/x` }], [cc, "Glob", { pattern: "/etc/*" }],
|
||||||
|
[cc, "Glob", { pattern: "**/x", path: "/" }], [cc, "Grep", { pattern: "x", path: "/etc" }], [cc, "Grep", { pattern: "x", glob: "../*" }],
|
||||||
|
[cc, "Bash", { command: "id" }], [cc, "WebFetch", { url: "http://x" }], [cc, "mcp__mosaic__message.send", {}],
|
||||||
|
[cc, "mcp__other__message.send", {}], [cc, "message.send", {}], [cc, "Read", { file_path: 7 }],
|
||||||
|
];
|
||||||
|
for (const [p, tool, input] of cases) {
|
||||||
|
const d = decide(p, tool, input);
|
||||||
|
console.log(`${p.harness.padEnd(11)} ${tool.padEnd(28)} ${JSON.stringify(input).padEnd(40)} ${d.allow ? "ALLOW" : "deny: " + d.reason.replace("mosaic gate: ", "")}`);
|
||||||
|
}
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
pi read {"path":"sub/../../x"} deny: read path is outside the workspace: sub/../../x
|
||||||
|
pi read {"path":"@/etc/passwd"} deny: read path is outside the workspace: @/etc/passwd
|
||||||
|
pi read {"path":"~/x"} deny: read path is outside the workspace: ~/x
|
||||||
|
pi read {"path":"file:///etc/passwd"} deny: read path is outside the workspace: file:///etc/passwd
|
||||||
|
pi read {"path":" /etc/passwd"} ALLOW
|
||||||
|
pi read {"path":"etc-link/passwd"} deny: read path is outside the workspace: etc-link/passwd
|
||||||
|
pi write {"path":"dangling/x"} ALLOW
|
||||||
|
pi read {"path":"sub/./x"} ALLOW
|
||||||
|
pi grep {"path":"..","pattern":"x"} deny: grep path is outside the workspace: ..
|
||||||
|
pi find {"pattern":"{..,x}/*"} ALLOW
|
||||||
|
pi find {"pattern":"*","path":"/"} deny: find path is outside the workspace: /
|
||||||
|
pi ls {} ALLOW
|
||||||
|
pi bash {"command":"cat /etc/passwd"} deny: bash isn't in this session's policy
|
||||||
|
pi message.send {} ALLOW
|
||||||
|
claude-code Read {"file_path":"/etc/passwd"} deny: Read path is outside the workspace: /etc/passwd
|
||||||
|
claude-code Read {"file_path":"/home/jwoltje/darkwing-scratch/tmp/r41-gate-1f41z7/ws/sub/x"} ALLOW
|
||||||
|
claude-code Glob {"pattern":"/etc/*"} deny: Glob pattern must stay inside the workspace: /etc/*
|
||||||
|
claude-code Glob {"pattern":"**/x","path":"/"} deny: Glob path is outside the workspace: /
|
||||||
|
claude-code Grep {"pattern":"x","path":"/etc"} deny: Grep path is outside the workspace: /etc
|
||||||
|
claude-code Grep {"pattern":"x","glob":"../*"} ALLOW
|
||||||
|
claude-code Bash {"command":"id"} deny: Bash isn't in this session's policy
|
||||||
|
claude-code WebFetch {"url":"http://x"} deny: WebFetch isn't in this session's policy
|
||||||
|
claude-code mcp__mosaic__message.send {} ALLOW
|
||||||
|
claude-code mcp__other__message.send {} deny: mcp__other__message.send isn't in this session's policy
|
||||||
|
claude-code message.send {} deny: message.send isn't in this session's policy
|
||||||
|
claude-code Read {"file_path":7} deny: Read.file_path isn't a string
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// A mock Messages API on 127.0.0.1: logs each request body to LOG and
|
||||||
|
// streams back one short text answer. No real model is reached.
|
||||||
|
import { createServer } from "node:http";
|
||||||
|
import { appendFileSync } from "node:fs";
|
||||||
|
const LOG = process.argv[2];
|
||||||
|
// TOOL_USE: optional JSON {name, input}; the first streamed answer calls it.
|
||||||
|
let toolUse = process.env.TOOL_USE ? JSON.parse(process.env.TOOL_USE) : null;
|
||||||
|
const sse = (res, ev, data) => res.write(`event: ${ev}\ndata: ${JSON.stringify(data)}\n\n`);
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
let body = "";
|
||||||
|
req.on("data", (b) => (body += b));
|
||||||
|
req.on("end", () => {
|
||||||
|
appendFileSync(LOG, `${JSON.stringify({ method: req.method, url: req.url, body })}\n`);
|
||||||
|
if (req.method !== "POST" || !req.url.startsWith("/v1/messages") || req.url.includes("count_tokens")) {
|
||||||
|
res.writeHead(200, { "content-type": "application/json" });
|
||||||
|
return res.end(req.url.includes("count_tokens") ? '{"input_tokens":1}' : "{}");
|
||||||
|
}
|
||||||
|
let stream = false;
|
||||||
|
try { stream = JSON.parse(body).stream === true; } catch {}
|
||||||
|
const msg = { id: "msg_mock", type: "message", role: "assistant", model: "claude-sonnet-5-5", content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 1, output_tokens: 1 } };
|
||||||
|
if (!stream) {
|
||||||
|
res.writeHead(200, { "content-type": "application/json" });
|
||||||
|
return res.end(JSON.stringify({ ...msg, content: [{ type: "text", text: "mock answer" }], stop_reason: "end_turn" }));
|
||||||
|
}
|
||||||
|
res.writeHead(200, { "content-type": "text/event-stream" });
|
||||||
|
sse(res, "message_start", { type: "message_start", message: msg });
|
||||||
|
if (toolUse) {
|
||||||
|
const t = toolUse;
|
||||||
|
toolUse = null;
|
||||||
|
sse(res, "content_block_start", { type: "content_block_start", index: 0, content_block: { type: "tool_use", id: "toolu_mock1", name: t.name, input: {} } });
|
||||||
|
sse(res, "content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "input_json_delta", partial_json: JSON.stringify(t.input) } });
|
||||||
|
sse(res, "content_block_stop", { type: "content_block_stop", index: 0 });
|
||||||
|
sse(res, "message_delta", { type: "message_delta", delta: { stop_reason: "tool_use", stop_sequence: null }, usage: { output_tokens: 2 } });
|
||||||
|
sse(res, "message_stop", { type: "message_stop" });
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
sse(res, "content_block_start", { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } });
|
||||||
|
sse(res, "content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "mock answer" } });
|
||||||
|
sse(res, "content_block_stop", { type: "content_block_stop", index: 0 });
|
||||||
|
sse(res, "message_delta", { type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: 2 } });
|
||||||
|
sse(res, "message_stop", { type: "message_stop" });
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
server.listen(0, "127.0.0.1", () => console.log(server.address().port));
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# R3 through a real Pi 0.85.1 session: the candidate adapters/pi/adapter.sh,
|
||||||
|
# the candidate pi-extension.mjs (the gate) loaded with -e, --tools write,
|
||||||
|
# and a mock Messages API on 127.0.0.1 whose first answer calls write on a
|
||||||
|
# dangling symlink in the workspace. Dummy key; no real model.
|
||||||
|
set -eu
|
||||||
|
WT=$1; OUT=$2
|
||||||
|
R=$(mktemp -d "$TMPDIR/r41-pdangle-XXXX")
|
||||||
|
mkdir -p "$R/home" "$R/agent" "$R/ws" "$R/bundle" "$R/outside"
|
||||||
|
ln -s "$R/outside/planted.txt" "$R/ws/notes.md"
|
||||||
|
# MODE=existing: the control, the link target exists, so the link isn't dangling.
|
||||||
|
if [ "${MODE:-dangling}" = existing ]; then echo "was here before" > "$R/outside/planted.txt"; fi
|
||||||
|
echo "== MODE=${MODE:-dangling}" | tee -a "$OUT"
|
||||||
|
echo "the generated prompt" > "$R/bundle/prompt.md"
|
||||||
|
printf '{"harness":"pi","workspace":"%s","tools":["write"],"typed":[]}\n' "$R/ws" > "$R/bundle/policy.json"
|
||||||
|
echo '[]' > "$R/bundle/tools.json"
|
||||||
|
TOOL_USE='{"name":"write","input":{"path":"notes.md","content":"written through the gate\n"}}' \
|
||||||
|
node "$(dirname "$0")/mock-api.mjs" "$R/api.log" > "$R/port" & MOCK=$!
|
||||||
|
while [ ! -s "$R/port" ]; do sleep 0.1; done
|
||||||
|
printf '{"providers":{"mock":{"baseUrl":"http://127.0.0.1:%s","api":"anthropic-messages","apiKey":"dummy-not-a-key","models":[{"id":"claude-sonnet-5-5"}]}}}\n' "$(cat "$R/port")" > "$R/agent/models.json"
|
||||||
|
set +e
|
||||||
|
env -i PATH="$WT/node_modules/.bin:$PATH" HOME="$R/home" USER="$USER" LANG=C.UTF-8 \
|
||||||
|
PI_CODING_AGENT_DIR="$R/agent" PI_PROVIDER=mock PI_MODEL=claude-sonnet-5-5 \
|
||||||
|
MOSAIC_SYSTEM_PROMPT_FILE="$R/bundle/prompt.md" MOSAIC_REQUEST="Message m1 from jason: write notes" \
|
||||||
|
MOSAIC_WORKSPACE="$R/ws" MOSAIC_TOOLS=write \
|
||||||
|
MOSAIC_EXTENSIONS="$WT/packages/harness/src/pi-extension.mjs" \
|
||||||
|
MOSAIC_POLICY_FILE="$R/bundle/policy.json" MOSAIC_TOOLS_FILE="$R/bundle/tools.json" \
|
||||||
|
MOSAIC_TOOL_SOCKET="$R/no-socket" MOSAIC_TURN_MARKER="$R/marker" \
|
||||||
|
timeout 90 /bin/sh "$WT/adapters/pi/adapter.sh" < /dev/null > "$R/stdout" 2> "$R/stderr"
|
||||||
|
echo "exit $?" > "$R/exit"
|
||||||
|
kill $MOCK
|
||||||
|
{
|
||||||
|
echo "$(cat "$R/exit"); stdout: $(head -c 200 "$R/stdout"); stderr: $(head -c 300 "$R/stderr")"
|
||||||
|
echo "POST /v1/messages: $(grep -c '"url":"/v1/messages' "$R/api.log")"
|
||||||
|
echo "tool_result sent back: $(node -e '
|
||||||
|
const ls=require("fs").readFileSync(process.argv[1],"utf8").trim().split("\n").map(l=>JSON.parse(l)).filter(l=>l.url.startsWith("/v1/messages"));
|
||||||
|
const b=JSON.parse(ls.at(-1).body); const r=b.messages.flatMap(m=>Array.isArray(m.content)?m.content:[]).filter(c=>c.type==="tool_result");
|
||||||
|
console.log(JSON.stringify(r.map(c=>({is_error:c.is_error??false,content:c.content}))));' "$R/api.log")"
|
||||||
|
echo "turn marker: $( [ -e "$R/marker" ] && echo written || echo absent)"
|
||||||
|
echo "outside file: $( [ -e "$R/outside/planted.txt" ] && cat "$R/outside/planted.txt" || echo absent)"
|
||||||
|
} | tee -a "$OUT"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
== MODE=dangling
|
||||||
|
exit 0; stdout: mock answer; stderr:
|
||||||
|
POST /v1/messages: 2
|
||||||
|
tool_result sent back: [{"is_error":false,"content":"Successfully wrote to notes.md"}]
|
||||||
|
turn marker: written
|
||||||
|
outside file: written through the gate
|
||||||
|
== MODE=existing
|
||||||
|
exit 0; stdout: mock answer; stderr:
|
||||||
|
POST /v1/messages: 2
|
||||||
|
tool_result sent back: [{"is_error":true,"content":"mosaic gate: write path is outside the workspace: notes.md"}]
|
||||||
|
turn marker: written
|
||||||
|
outside file: was here before
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
# Row 41, slice 1 S6 (meta-harness and launching), round 1 review (Darkwing)
|
||||||
|
|
||||||
|
Issue #1523, request comment 26991, queue revs 244 and 245 (`c7a2c703`).
|
||||||
|
Packet: `agents/filbert/work/s6/` at `9b670e27`, base `915e00e5`, gate at
|
||||||
|
`2855e628`, 41 files, +4302/−55. Candidate manifest sha256
|
||||||
|
`5b3a934d01eb518e23b842623aeb3cbfc287f1b88110edc1e044d4131b2927f3`,
|
||||||
|
`build.patch` sha256
|
||||||
|
`dc346027db9b650c70b57ff390b57a8d76d3e2ccd434538cb083fe9a97ea4454`.
|
||||||
|
Rulings: lead decisions 77 and 78.
|
||||||
|
|
||||||
|
Verdict: **changes**, comment 26995. Two findings block. R1: a broker
|
||||||
|
reply that misses the host's 10 s wait shifts every later reply by one, so a
|
||||||
|
refused `authorizeLaunch` comes back allowed and one run's capability goes
|
||||||
|
to another. R3: a dangling symlink in the workspace passes the gate, and a
|
||||||
|
real Pi session writes through it to a file outside. R2 should be fixed in
|
||||||
|
the same round: `launcher.close()` waits on any `launch.sock` client that
|
||||||
|
keeps its side open, so the runners never get their SIGTERM. The rest of
|
||||||
|
the brief holds under my probes and mutants.
|
||||||
|
|
||||||
|
## Method
|
||||||
|
|
||||||
|
- A detached worktree at `2855e628`, then `git apply --index build.patch`
|
||||||
|
and `sha256sum -c candidate-manifest.sha256`: 41 OK. After the mutants
|
||||||
|
and probes I checked again: 41 OK, and `git diff --name-only` empty.
|
||||||
|
- I read the diff for the five places Filbert named, plus `gate.mjs`,
|
||||||
|
`claude-gate.mjs`, `pi-extension.mjs`, `session.mjs` and the READMEs,
|
||||||
|
against the S6 brief, the S0 lines and decisions 77 and 78.
|
||||||
|
- Probes in `probe/`. They import from my scratch worktree by absolute
|
||||||
|
path (`WT`), so they won't run as committed without setting it. The
|
||||||
|
model calls go to `probe/mock-api.mjs`, a mock Messages API on
|
||||||
|
127.0.0.1 with a dummy key; no real model was reached and nothing was
|
||||||
|
spent. Claude Code is 2.1.296, Pi is the pinned 0.85.1.
|
||||||
|
- Fourteen mutants of my own (`mut/mutate.py`, `mut/run.sh`), each run
|
||||||
|
against the node suites of the packages it touches, then restored.
|
||||||
|
|
||||||
|
Node is v26.8.1, `TMPDIR=~/darkwing-scratch/tmp`,
|
||||||
|
`DOCKER_HOST=unix:///nonexistent.sock`.
|
||||||
|
|
||||||
|
## Suites
|
||||||
|
|
||||||
|
Run sequentially in the worktree, each output in `out/`.
|
||||||
|
|
||||||
|
| Suite | Result | File |
|
||||||
|
|---|---|---|
|
||||||
|
| `node --test 'packages/harness/tests/*.test.mjs'` | 45/45 | `out/node-harness.txt` |
|
||||||
|
| `node --test 'packages/seat/tests/*.test.mjs'` | 27/27 | `out/node-seat.txt` |
|
||||||
|
| `node --test 'packages/cli/tests/*.test.mjs'` | 77/77 | `out/node-cli.txt` |
|
||||||
|
| `node --test 'packages/bus/tests/*.test.mjs'` | 74/74 | `out/node-bus.txt` |
|
||||||
|
| `node --test 'packages/business/tests/*.test.mjs'` | 60/60 | `out/node-business.txt` |
|
||||||
|
| `test-auth`, `test-config`, `test-conductor` | 15, 24, 17 passed, 0 failed | `out/test-*.txt` |
|
||||||
|
| `test-queue`, `test-foundation`, `test-extension-package` | 27, 44, 18 passed, 0 failed | |
|
||||||
|
| `test-release`, `test-discord` | 4, 66 passed, 0 failed | |
|
||||||
|
| `test-task` | 26 passed, 2 failed | `out/test-task.txt` |
|
||||||
|
|
||||||
|
The two `test-task` failures are the live user-recall pair, which needs
|
||||||
|
Docker. They match Filbert's base run. Filbert's gate covers the other
|
||||||
|
node suites.
|
||||||
|
|
||||||
|
## R1: broker replies carry no id, and a late one shifts every later reply (blocking)
|
||||||
|
|
||||||
|
`host.mjs:157-171`. `request()` sends one message to the broker child and
|
||||||
|
takes the first IPC message that arrives within 10 s (`firstReply`) as the
|
||||||
|
answer. The comment at `:155` says the replies carry no request id, so
|
||||||
|
requests go one at a time. That holds only while every reply arrives
|
||||||
|
inside the wait. When one doesn't, `firstReply` rejects and the queue
|
||||||
|
moves on, but the broker still answers. That late answer is the first
|
||||||
|
message the next request sees. From then on every request gets the reply
|
||||||
|
to the one before it.
|
||||||
|
|
||||||
|
`probe/desync.mjs` starts a real host, binds a `pm` and a `cto` run, and
|
||||||
|
stalls the broker with SIGSTOP while three requests wait
|
||||||
|
(`probe/desync.txt`):
|
||||||
|
|
||||||
|
```
|
||||||
|
baseline, no stall:
|
||||||
|
authorizeLaunch(pm, coder): {"ok":{"class":"within-role"}}
|
||||||
|
authorizeLaunch(cto, coder): {"err":"decision-required"}
|
||||||
|
identity(bogus): {"err":"unauthenticated"}
|
||||||
|
p1 authorizeLaunch(pm) after 10024 ms: {"err":"broker did not reply within 10 s"}
|
||||||
|
p2 authorizeLaunch(cto), should refuse: {"ok":{"class":"within-role"}}
|
||||||
|
p3 identity(bogus), should refuse: {"err":"decision-required"}
|
||||||
|
p4 identity(cto), should be cto: {"err":"unauthenticated"}
|
||||||
|
--- bind case: two binds queued during a stall
|
||||||
|
b1 bind run-x: {}
|
||||||
|
b2 bind run-y (reviewer) got: {"run":"run-x"}
|
||||||
|
claim with b2's cap: {"ok":{"role":"coder","run":"run-x"}}
|
||||||
|
after 3 s idle, identity(pm), should be pm: {}
|
||||||
|
then identity(pm) again: {"ok":{"business":"acme","role":"pm","run":"pm-run","human":null}}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `cto` instance needs a decision to launch a coder, and after the stall
|
||||||
|
it is allowed. In the bind case the reviewer launch `run-y` receives
|
||||||
|
`run-x`'s capability, and a `role.claim` with it acts as `coder`, run
|
||||||
|
`run-x`. In the launcher that capability goes on the new runner's stdin
|
||||||
|
(`launcher.mjs:217`), so the reviewer process would run as the coder. The
|
||||||
|
shift doesn't heal with idle time: 3 s later `identity(pm)` still gets the
|
||||||
|
bind reply (`{}`, no `result`). The last line looks right only because it
|
||||||
|
asks the same question as the one before it.
|
||||||
|
|
||||||
|
This fails open at the boundary decision 77 names. The trigger in my probe
|
||||||
|
is SIGSTOP. Without it, anything that holds the broker past 10 s does the
|
||||||
|
same: a slow disk under a checkpoint, swap, or a same-UID process that asks
|
||||||
|
the systemd user manager to stop it (the README's own limit). I didn't find
|
||||||
|
a path inside the broker alone that blocks for 10 s; sqlite's busy timeout
|
||||||
|
is 5 s per statement. The pattern itself came with `bindLaunch` in S4
|
||||||
|
(`2f5303c1`), but nothing called it until this row, which now routes all
|
||||||
|
six launch ops through it.
|
||||||
|
|
||||||
|
Either fix works:
|
||||||
|
|
||||||
|
1. Put an id on each request, echo it in `process.mjs`, and have
|
||||||
|
`firstReply` ignore (and log) a reply whose id doesn't match. That
|
||||||
|
needs a test that stalls the broker past the wait and checks the next
|
||||||
|
request's answer.
|
||||||
|
2. Treat a `firstReply` timeout on the broker as fatal to the channel:
|
||||||
|
close the host with exit 1, and the unit restarts it. Simpler, and the
|
||||||
|
launches in flight fail closed.
|
||||||
|
|
||||||
|
## R2: `launcher.close()` waits on any `launch.sock` client (should fix)
|
||||||
|
|
||||||
|
`launcher.mjs:377-381`. `close()` awaits `server.close(r)`, which waits
|
||||||
|
until every connection on `launch.sock` is gone. The reply path
|
||||||
|
(`:343`) calls `socket.end()`, which sends FIN but doesn't destroy the
|
||||||
|
socket; a client that keeps its write side open keeps the connection, and
|
||||||
|
`close()` waits. The SIGTERM to the runners comes after that line, so they
|
||||||
|
get none.
|
||||||
|
|
||||||
|
`probe/close-hang.mjs` connects with `allowHalfOpen` and never ends its
|
||||||
|
side (`probe/close-hang.txt`):
|
||||||
|
|
||||||
|
```
|
||||||
|
== MODE=silent
|
||||||
|
host.close(0) at 201 ms
|
||||||
|
client got {"ok":false,"error":"invalid-request"} at 10050 ms
|
||||||
|
client got FIN at 10050 ms (not ending its side)
|
||||||
|
still not closed at 25227 ms
|
||||||
|
client destroyed; closed 0 at 25240 ms
|
||||||
|
== MODE=line
|
||||||
|
client got {"ok":false,"error":"unauthenticated"} at 2 ms
|
||||||
|
client got FIN at 2 ms (not ending its side)
|
||||||
|
host.close(0) at 201 ms
|
||||||
|
still not closed at 25227 ms
|
||||||
|
client destroyed; closed 0 at 25246 ms
|
||||||
|
```
|
||||||
|
|
||||||
|
The host closes only when the client goes away. A default Node client
|
||||||
|
would end its side on FIN, so this takes a client that doesn't: a stopped
|
||||||
|
or hung process, or a session that does it on purpose. The socket is 0600
|
||||||
|
and same UID, and a session with `bash` can reach it. Then `bus stop`
|
||||||
|
waits until systemd's stop timeout kills the host, and the runners die by
|
||||||
|
SIGKILL with no `role.release` and no end record from this host. Filbert
|
||||||
|
asked about the 30 s close. The 30 s SIGKILL timer starts only after this
|
||||||
|
wait, so it doesn't bound it.
|
||||||
|
|
||||||
|
Fix: keep the accepted sockets in a set and destroy them in `close()`
|
||||||
|
before awaiting `server.close`, or `destroySoon()` after the reply. A test
|
||||||
|
with a half-open client would cover it.
|
||||||
|
|
||||||
|
## R3: a dangling symlink in the workspace passes the gate (blocking for Pi)
|
||||||
|
|
||||||
|
`gate.mjs:45-55`. `real()` walks up from the target until `existsSync`
|
||||||
|
returns true, then realpaths that ancestor and keeps the missing tail.
|
||||||
|
`existsSync` follows links, so a symlink whose target doesn't exist counts
|
||||||
|
as missing. Its name stays in the tail, under the workspace root, and
|
||||||
|
`insideWorkspace` says yes. The harness README says every path "must
|
||||||
|
resolve inside the workspace after symlinks"; for a dangling link it
|
||||||
|
doesn't.
|
||||||
|
|
||||||
|
The gate on its own (`probe/dangling-write.mjs`, `dangling-write.txt`),
|
||||||
|
with `ws/notes.md -> ../outside/planted.txt` and the target absent:
|
||||||
|
|
||||||
|
```
|
||||||
|
pi write {path: "notes.md"}: {"allow":true}
|
||||||
|
claude-code Write {file_path: "notes.md"}: {"allow":true}
|
||||||
|
outside file exists before: false
|
||||||
|
outside file after pi's write calls: "written through the gate\n"
|
||||||
|
```
|
||||||
|
|
||||||
|
The last line repeats the two calls Pi's write tool makes
|
||||||
|
(`core/tools/write.js:45-48`: a recursive `mkdir` of the dirname, then
|
||||||
|
`writeFile`).
|
||||||
|
|
||||||
|
Then a real Pi 0.85.1 session (`probe/pi-dangling.sh`,
|
||||||
|
`pi-dangling.txt`): the candidate `adapters/pi/adapter.sh` with the
|
||||||
|
candidate `pi-extension.mjs`, `--tools write`, and a mock API whose first
|
||||||
|
answer calls `write` on `notes.md`. The control run gives the link a target
|
||||||
|
that exists.
|
||||||
|
|
||||||
|
```
|
||||||
|
== MODE=dangling
|
||||||
|
exit 0; stdout: mock answer; stderr:
|
||||||
|
POST /v1/messages: 2
|
||||||
|
tool_result sent back: [{"is_error":false,"content":"Successfully wrote to notes.md"}]
|
||||||
|
turn marker: written
|
||||||
|
outside file: written through the gate
|
||||||
|
== MODE=existing
|
||||||
|
exit 0; stdout: mock answer; stderr:
|
||||||
|
POST /v1/messages: 2
|
||||||
|
tool_result sent back: [{"is_error":true,"content":"mosaic gate: write path is outside the workspace: notes.md"}]
|
||||||
|
turn marker: written
|
||||||
|
outside file: was here before
|
||||||
|
```
|
||||||
|
|
||||||
|
The gate refuses the link when its target exists. Remove the target and
|
||||||
|
the same write lands outside the workspace.
|
||||||
|
|
||||||
|
Claude Code through the candidate adapter and the real `claude-gate.mjs`
|
||||||
|
(`probe/claude-dangling.sh`, `claude-dangling.txt`): the gate allowed the
|
||||||
|
`Write`, and Claude Code refused it itself.
|
||||||
|
|
||||||
|
```
|
||||||
|
tool_result sent back: [{"is_error":true,"content":".../ws/notes.md resolves through a symlink to .../outside/planted.txt, which is outside .../ws; --restricted confines the file tools to the working directory."}]
|
||||||
|
outside file: absent
|
||||||
|
```
|
||||||
|
|
||||||
|
So Claude Code is held by `--restricted`, which the README calls an extra
|
||||||
|
layer the S0 lines don't depend on. Pi has no second layer.
|
||||||
|
|
||||||
|
The link doesn't have to come from the session. Workspaces persist per
|
||||||
|
instance (`workspaceDir(dataRoot, business, instance)`), so a checked-out
|
||||||
|
repository can carry one, and so can an earlier session of the same
|
||||||
|
instance whose role had `bash`. A link under a missing directory
|
||||||
|
(`dangling/x` in `probe/gate-edges.txt`) passes the same way.
|
||||||
|
|
||||||
|
Fix: in `real()`, `lstat` each component that doesn't exist. If it is a
|
||||||
|
symlink, either refuse the path or `readlink` it, resolve the target
|
||||||
|
against its directory and keep walking. Add a gate test for a dangling
|
||||||
|
link and for a link under a missing directory, for both harnesses.
|
||||||
|
|
||||||
|
## What holds
|
||||||
|
|
||||||
|
- **Early signals.** `runner.mjs:256-257` installs SIGTERM and SIGINT
|
||||||
|
before anything reads stdin or the session file. Mutant Ma moves them
|
||||||
|
after setup and fails three harness tests and two cli tests.
|
||||||
|
- **Stdin cap.** `runner.mjs:78` refuses more than 4096 bytes. Mutant Mj (no cap) survives; see note 5.
|
||||||
|
- **Exit codes 20-23.** The runner tests cover each. Mutant Mg (no
|
||||||
|
founder variable check) fails the `founderCheck` unit test, and the runner test that expects
|
||||||
|
exit 20 hangs instead; I killed it after four minutes.
|
||||||
|
- **Launcher check order.** identity, not-a-role, instance-not-listed,
|
||||||
|
instance-running, harness and role contract, model family, capacity,
|
||||||
|
then the broker's `authorizeLaunch`, all before `start`. Mutant Md
|
||||||
|
(no instance-running check) fails two cli tests, Me (no
|
||||||
|
`authorizeLaunch`) fails one.
|
||||||
|
- **`recover()`.** It SIGKILLs a live leftover whose start time matches and
|
||||||
|
whose cmdline is `unshare`, rebinds the run with its recorded launch and
|
||||||
|
ends it `host-lost`; a rebind refused with `run-ended` is logged and
|
||||||
|
skipped. Mutant Mn (no `endRun`) fails both host-died-hard tests, and so
|
||||||
|
does Mc. Mutant Mi (no SIGKILL) survives; see note 5.
|
||||||
|
- **The 30 s close.** `probe/close-kill.mjs` SIGSTOPs a claimed PM's
|
||||||
|
runner from outside the namespace and closes the host
|
||||||
|
(`probe/close-kill.txt`): `host.close -> 0 after 30033 ms`, the session
|
||||||
|
and runner are both gone, the launch log has one end line
|
||||||
|
(`killed`, `SIGKILL`) and `sessions.json` is empty. Mutant Mh (no
|
||||||
|
SIGKILL) survives, so only the probe shows it.
|
||||||
|
- **Broker.** `bindLaunch` records the run only after its checks, and a
|
||||||
|
rebind of an ended run refuses with `run-ended`
|
||||||
|
(`broker.mjs:160-193`, `:203`). Mutant Mb (record first) and Mc (no
|
||||||
|
`run-ended`) each fail "a restarted broker refuses to rebind an ended
|
||||||
|
run; a refused rebind leaves the run unbound". Mutant Mm (no `launch-revoked`) fails "launches off refuses
|
||||||
|
role.launch with launch-revoked until launches on". No new socket verb,
|
||||||
|
event kind or schema change: the new ops are in `process.mjs`'s
|
||||||
|
`LAUNCH_OPS` only.
|
||||||
|
- **Claude Code hook.** The bundle writes
|
||||||
|
`timeout -k 2 10 <gate> <policy> || exit 2` with hook timeout 20, and
|
||||||
|
the adapter never passes `--bare`. That matches S0 lines 1-4. Mutant Mf
|
||||||
|
(no `|| exit 2`) fails the bundle test.
|
||||||
|
- **Gate spellings.** Every escape I tried is denied except the dangling
|
||||||
|
link (`probe/gate-edges.txt`): `sub/../../x`, `@/etc/passwd`, `~/x`,
|
||||||
|
`file:///etc/passwd`, a symlink to `/etc`, absolute paths, `Glob /etc/*`,
|
||||||
|
`Glob` with path `/`, `Grep /etc`, `Bash`, `WebFetch`, a foreign `mcp__`
|
||||||
|
prefix, the bare typed name for Claude Code, and a non-string path. Pi
|
||||||
|
`read " /etc/passwd"` is allowed, and Pi resolves it the same way, as a
|
||||||
|
relative path under the workspace. `find "{..,x}/*"` and `Grep` glob
|
||||||
|
`../*` are allowed, and they're harmless: fd and rg filter files under
|
||||||
|
the search root and can't leave it. Mutant Ml (no `..` check on glob patterns) fails "glob
|
||||||
|
patterns stay inside the workspace".
|
||||||
|
- **`--no-approve`.** Both `adapters/pi/adapter.sh` and
|
||||||
|
`scripts/agent-host-dev.sh` pass it, which matches Filbert's table.
|
||||||
|
- **Adapter stdin** is `/dev/null` (the runner spawns it with
|
||||||
|
`["ignore", "pipe", errFd]`), and the session environment is the
|
||||||
|
`ENV_ALLOW` list.
|
||||||
|
|
||||||
|
## Mutants
|
||||||
|
|
||||||
|
`mut/summary.txt` has the raw lines; `mut/parse.sh` reads the outputs.
|
||||||
|
|
||||||
|
| Mutant | Change | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| Ma | signal handlers installed after setup | harness 42/3, cli 75/2 |
|
||||||
|
| Mb | broker records the run before its checks | bus 73/1, cli 77/0 |
|
||||||
|
| Mc | no `run-ended` refusal on rebind | bus 73/1, cli 75/2 |
|
||||||
|
| Md | no instance-running check | cli 75/2 |
|
||||||
|
| Me | no `authorizeLaunch` before start | cli 76/1 |
|
||||||
|
| Mf | hook command without `\|\| exit 2` | harness 44/1 |
|
||||||
|
| Mg | `founderCheck` finds no founder variables | harness: unit test fails, runner test hangs (killed); cli 77/0 |
|
||||||
|
| Mh | no SIGKILL after 30 s in `close()` | cli 77/0, **survives** |
|
||||||
|
| Mi | no SIGKILL in `recover()` | cli 77/0, **survives** |
|
||||||
|
| Mj | no 4096-byte stdin cap in the runner | harness 45/0, **survives** |
|
||||||
|
| Mk | no `LINE_MAX` on `launch.sock` | cli 77/0, **survives** |
|
||||||
|
| Ml | no `..` check on glob patterns | harness 44/1 |
|
||||||
|
| Mm | no `launch-revoked` refusal | bus 73/1, cli 77/0 |
|
||||||
|
| Mn | `recover()` doesn't end the run | cli 75/2 |
|
||||||
|
|
||||||
|
## Notes (not blocking)
|
||||||
|
|
||||||
|
1. **`--restricted` does more than the README says.** `probe/claude-memory.sh`
|
||||||
|
plants marker lines in `~/.claude/CLAUDE.md`, a parent `CLAUDE.md`, the
|
||||||
|
workspace `CLAUDE.md` and the auto-memory file, and runs the candidate
|
||||||
|
adapter against the mock API (`probe/claude-memory.txt`). With
|
||||||
|
`--restricted`, none of the four markers reaches the request and the
|
||||||
|
generated prompt does. Without it, all four reach it. So `--restricted`
|
||||||
|
is what keeps founder and repository memory out of the session's
|
||||||
|
prompt, and with R3 it is the layer that holds for Claude Code. The
|
||||||
|
README ("no test treats it as the layer that holds") and the adapter
|
||||||
|
comment should say so, and a test should fail if the adapter drops the
|
||||||
|
flag.
|
||||||
|
2. **`launchPm` skips `authorizeLaunch`.** `mosaic launches off` doesn't
|
||||||
|
stop `bus start --pm`. That looks intended (the human launches the PM),
|
||||||
|
but one README sentence would say so.
|
||||||
|
3. **A bad policy file is an uncaught rejection.** `runner.mjs:268` reads
|
||||||
|
and parses the policy outside the `try` at `:260`, so a missing or
|
||||||
|
malformed one exits 1 with a stack trace instead of the usage exit.
|
||||||
|
4. **The system prompt is in argv** for both adapters
|
||||||
|
(`--system-prompt "$PROMPT_CONTENT"`), readable in `/proc/<pid>/cmdline`
|
||||||
|
by the same UID. The PID namespace hides it from other sessions. It
|
||||||
|
holds no secret today; noting it for when it might.
|
||||||
|
5. **Test gaps.** Four mutants survive the suites: Mh, Mi, Mj and Mk
|
||||||
|
(table above). The code does the right thing in each as far as I
|
||||||
|
checked (`close-kill.txt` for Mh), but no test would notice it going.
|
||||||
|
For Mi, the host-died-hard test checks that the old session is gone once
|
||||||
|
the next host is up, and it is gone even without the kill; I didn't
|
||||||
|
trace why. A leftover kept alive (a SIGSTOPped runner, as in
|
||||||
|
`close-kill.mjs`) would separate the two. Mg is caught by the unit test,
|
||||||
|
but its runner-level test hangs rather than fails; a timeout on it would
|
||||||
|
turn that into a failure. With R1, R2 and R3 fixed, a test for each
|
||||||
|
belongs in the same round.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `review-r1.md`, this file.
|
||||||
|
- `out/`: the suite outputs and `summary.txt`.
|
||||||
|
- `probe/desync.mjs`, `desync.txt`: R1.
|
||||||
|
- `probe/close-hang.mjs`, `close-hang.txt`: R2.
|
||||||
|
- `probe/close-kill.mjs`, `close-kill.txt`: the 30 s SIGKILL in `close()`.
|
||||||
|
- `probe/dangling-write.mjs`, `dangling-write.txt`, `pi-dangling.sh`,
|
||||||
|
`pi-dangling.txt`, `claude-dangling.sh`, `claude-dangling.txt`: R3.
|
||||||
|
- `probe/gate-edges.mjs`, `gate-edges.txt`: gate spellings.
|
||||||
|
- `probe/claude-memory.sh`, `claude-memory.txt`: note 1.
|
||||||
|
- `probe/mock-api.mjs`: the mock Messages API.
|
||||||
|
- `mut/`: `mutate.py`, `run.sh`, `all.sh`, `parse.sh`, each mutant's
|
||||||
|
output and `summary.txt`.
|
||||||
Reference in New Issue
Block a user