docs: Researcher row 109 candidate packet (#1603, PM reads current launch state and task status before it plans)
Candidate patch (state verb, runner state block, header time, pm.md rule, fake-adapter fixture), six new harness tests, six mutants, full suite outputs, candidate manifest. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
# Row 109 (#1603) build notes
|
||||||
|
|
||||||
|
Researcher, 2026-10-13. Brief: `docs/plans/2026-10-13_pm-tester-and-fresh-state.md`, "A new PM run reads current launch state and task status before it plans". BASE: `02fe2d9d5` (origin/refactor after row 106 landed as `8b58cb7e4`). Reviewer: Filbert. Gate owner: Sage.
|
||||||
|
|
||||||
|
Candidate change: `row109.patch` (8 files: 7 modified, 1 new). Built and run in `/mnt/storage/scratch/researcher/q109`, detached at `02fe2d9d5`, `node_modules` linked to the canonical one. The patch applies to BASE; the packet is not in it.
|
||||||
|
|
||||||
|
## What ships
|
||||||
|
|
||||||
|
- `packages/bus/src/broker.mjs`: read verb `state`, no arguments, open to role, human and reader sessions (reader allowlist gains `state`). It returns `{launch:{state,at,event}, tasks:{open:[{task_ref,title}], total, cap}}` for the capability's business.
|
||||||
|
- `launch` is the latest `launch.revoked` / `launch.restored` event of the business: `state` `revoked`, else `allowed`; `at` and `event` are that event's time and id, null when no event exists (`allowed`).
|
||||||
|
- `tasks.open` is `#tasks(business,{done:false})`, so it is the tracker snapshot's open tasks, tombstones left out, in `task_ref` order, refs and titles only. The cap is the `tasks` page size (`TASK_PAGE`, 50), stated as `cap`; `total` counts every open task.
|
||||||
|
- Nothing is stored. No schema change.
|
||||||
|
- `packages/harness/src/runner.mjs`: before each turn the runner calls `state` and prefixes the turn request with `stateBlock(state, readAt)`: `Current state, read at <iso>:`, `Launches: ...`, `Open tasks: N[, the first K of cap C].`, one line per task. The read time is the runner's clock at the call. A failed read (anything but a dead session) fails that turn without running the adapter: the sender gets the usual RESULT `The turn failed: could not read the current state: <code>`, the failure is logged, and `message.read` still runs. A dead session (`SESSION_GONE`) exits as it does on `message.receive`.
|
||||||
|
- `turnRequest()`: the header gains `sent <at>` from the message row's `at`, after the sender and before the class.
|
||||||
|
- `roles/pm.md`: one rule in Protocol: the block is the truth for launch state and task status; a message or a note that disagrees is out of date; before planning to resume or launch work on a task, check it with `get_task`.
|
||||||
|
- READMEs: one table row in `packages/bus/README.md`, one paragraph in `packages/harness/README.md`.
|
||||||
|
- `packages/harness/src/tools.mjs` is untouched: `state` is not exposed as a tool; only the runner calls it.
|
||||||
|
|
||||||
|
## Fixture edit (accepted by Sage, rows 109 scope)
|
||||||
|
|
||||||
|
`packages/harness/tests/fixtures/fake-adapter.mjs` took the message body as everything after the first blank line of the request. The state block adds a blank line above the `Message ...:` header, which broke every TOOL/SLEEP/FAIL/NOMARKER test. It now takes the body after the `Message ...:` header line (regex), with the old split as fallback. This is the only reason for the edit; no other harness test needed a change. Every other test using the fixture passes in the full harness suite (79/79).
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- `packages/bus/tests/state.test.mjs` (new, 6): never switched reads `allowed` with null time and event; revoked then restored reads `allowed` with the restore event's time and id (and `revoked` with the revoke id in between); a task closed on the tracker leaves the open list and `total` drops; a tombstone is not open; the list stops at the cap and says so while `total` counts all; role and reader sessions read the same state, an argument refuses `invalid-request`, an unbound cap refuses `unauthenticated`.
|
||||||
|
- `packages/harness/tests/runner.test.mjs` (6 new tests, 20 in the file; harness suite 73 -> 79). By name:
|
||||||
|
1. `turnRequest shows the message's own sent time, a day old as well as new`
|
||||||
|
2. `stateBlock names the launch state with its time and event, and the open tasks with the cap`
|
||||||
|
3. `launches revoked and then restored: the turn's block reads allowed and gives the restore time`
|
||||||
|
4. `a task closed on the tracker is missing from the next turn's open list`
|
||||||
|
5. `a message read a day after it was sent shows the time it was sent` (the broker's clock jumps a day after the send)
|
||||||
|
6. `a failed state read fails the turn: no adapter runs, the sender hears why, the message is read` (no adapter turn file, RESULT with the reason, message read, log line)
|
||||||
|
|
||||||
|
My message to Sage before the gate run said five new runner tests. That was a miscount: it left out test 2, the `stateBlock` unit test, which I wrote as a unit test beside test 1. The suite count (+6) is the evidence. The existing first end-to-end test, `a message becomes a turn, the answer goes back as a RESULT, ...`, is changed, not new: it now checks the exact block and the header with `sent <at>`.
|
||||||
|
- The state block and the time are checked by the runner tests through the fake adapter, which echoes the request.
|
||||||
|
|
||||||
|
## Mutants (the kills)
|
||||||
|
|
||||||
|
`mutants.sh <worktree> <outdir>` runs one test file for the control and one per mutant, each mutant one exact-once text edit, and restores every source on exit. Outputs in `mutants/`; run summary in `mutants-run.txt`. `mutants/<name>.patch` is `git diff` of that file against BASE, so it also shows the candidate's own edits; the mutation is the one changed expression. The candidate was byte-identical to `row109.patch` after the run.
|
||||||
|
|
||||||
|
| mutant | edit | result | failing tests |
|
||||||
|
|---|---|---|---|
|
||||||
|
| control | none | runner 20/20, bus 6/6 | none |
|
||||||
|
| m1 | request is `turnRequest(m)`, no block | runner 17/20 | the first end-to-end test; "launches revoked and then restored"; "a task closed on the tracker is missing" |
|
||||||
|
| m2 | header drops `sent <at>` | runner 17/20 | the first end-to-end test; "a message read a day after it was sent"; "turnRequest shows the message's own sent time" |
|
||||||
|
| m3 | a failed read sets `block = ""` instead of failing the turn | runner 19/20 | "a failed state read fails the turn" |
|
||||||
|
| m4 | launch event read oldest-first | bus 5/6 | "launches revoked and then restored read allowed, with the restore time and event id" |
|
||||||
|
| m5 | open list built without the `done:false` filter | bus 5/6 | "a task closed on the tracker leaves the open list" |
|
||||||
|
| m6 | `cap` dropped from the output | bus 4/6 | the closed-task test and the cap test |
|
||||||
|
|
||||||
|
The `roles/pm.md` rule has no test; it is prose. Out of scope as the brief says: expiring old messages, tracker poller or `list_tasks` defaults, and who may switch launches.
|
||||||
|
|
||||||
|
## Suites (all in q109, output teed to `suites/`)
|
||||||
|
|
||||||
|
BASE `02fe2d9d5`; slot held 23:04:51Z to 23:08:36Z, "clear" sent; no process of mine left in q109.
|
||||||
|
|
||||||
|
| suite | result | Sage's gate count |
|
||||||
|
|---|---|---|
|
||||||
|
| `node --test packages/bus/tests/*.test.mjs` | 134 pass, 0 fail | 128 (+6 new) |
|
||||||
|
| `node --test packages/harness/tests/*.test.mjs` | 79 pass, 0 fail | 73 (+6 new) |
|
||||||
|
| `node --test packages/queue/tests/*.test.mjs` | 156 pass, 0 fail | 156 |
|
||||||
|
| `scripts/test-release.sh` | 14 passed, 0 failed | 14 |
|
||||||
|
| `test-auth` 15, `test-conductor` 17, `test-config` 24, `test-discord` 66, `test-extension-package` 18, `test-foundation` 44, `test-task` 98 | 0 failed each | n/a |
|
||||||
|
| `scripts/test-queue.sh` | 28 passed, 0 failed | n/a |
|
||||||
|
|
||||||
|
The `queue verify and render --check` case of `test-queue.sh` is skipped in a worktree (q109 is not the queue's canonical root `/mnt/storage/src/mosaic-stack`; Filbert's N3 from row 101). Sage's canonical-root gate rerun is the evidence of record. I did not apply the patch to or run suites in the canonical checkout.
|
||||||
|
|
||||||
|
## PM workspace (read-only)
|
||||||
|
|
||||||
|
`pm-workspace-names.txt`: the PM's persistent workspace holds one planning-notes file, `findings-2026-10-12.md` (7737 bytes, 2026-10-12T14:53Z), and `stack/`, a git checkout of the repository. Names only; no contents read. The note is a likely second source of the stale picture alongside old inbox messages; the pm.md rule covers it. The file is left alone.
|
||||||
|
|
||||||
|
## Scope and live
|
||||||
|
|
||||||
|
Owned files only, plus the accepted fixture. Live use needs a new copy and re-pin under Mos's go, with decision 109 notices before and after (runner.mjs reaches new role sessions at their next start; the broker verb needs a bus restart).
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
62bce061032623636835100f60b3ab29d2d6399603c0c89100cfec4eaf98ee28 agents/researcher/work/queue-109/build.md
|
||||||
|
1a92b106886608a836d2b4683d5d0f4cfa7c6ea7a6c0eff0e0d2527ef60b6413 agents/researcher/work/queue-109/mutants/control-bus.out
|
||||||
|
9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa agents/researcher/work/queue-109/mutants/control-bus.rc
|
||||||
|
23883a50bb236ccbfdeb123097f29c4db25e8322c3b68549891fa825fc5149ab agents/researcher/work/queue-109/mutants/control-runner.out
|
||||||
|
9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa agents/researcher/work/queue-109/mutants/control-runner.rc
|
||||||
|
f48ac9ad7b681fbfa417604338d6cd320259e746d097b98bf7cf64c85544ac17 agents/researcher/work/queue-109/mutants/m1-drop-block.out
|
||||||
|
55662e82c005a5042cb09bed61ab28d088cf424871d0ec6200bf0d56aaa3d11b agents/researcher/work/queue-109/mutants/m1-drop-block.patch
|
||||||
|
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 agents/researcher/work/queue-109/mutants/m1-drop-block.rc
|
||||||
|
fea2ebac6cedf9a2018a018454ae3ac16f744b5bec3c4ee2e031d03ffc7912aa agents/researcher/work/queue-109/mutants/m2-drop-time.out
|
||||||
|
f4598e1f4f1182dfdc5de727acb53f84632d81c16f0f52adff4262a4c73f5a08 agents/researcher/work/queue-109/mutants/m2-drop-time.patch
|
||||||
|
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 agents/researcher/work/queue-109/mutants/m2-drop-time.rc
|
||||||
|
9a1ef422b9c1163cb95f58e481090d700bc077589f9d651d1ef822189c4f7cb6 agents/researcher/work/queue-109/mutants/m3-drop-failure-check.out
|
||||||
|
b30ee17aa4144cb6f4f2c64ea09a0eff00a2ba7a09f64ebb9a6c59a6ca06523a agents/researcher/work/queue-109/mutants/m3-drop-failure-check.patch
|
||||||
|
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 agents/researcher/work/queue-109/mutants/m3-drop-failure-check.rc
|
||||||
|
d65461b462514360d80d7e1677b0f6dc1223041d1bb19f867f4d253e4a1ab603 agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.out
|
||||||
|
ced0a5f192f1302bffb5d73608dbc3dddae4ee0c117ad92a8513d92c37c09ce7 agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.patch
|
||||||
|
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 agents/researcher/work/queue-109/mutants/m4-oldest-launch-event.rc
|
||||||
|
b7aeeda30f679b5bcb2d0132cdf32f90e15a092ca5cc618090b07a62e8ff3d6a agents/researcher/work/queue-109/mutants/m5-closed-in-open.out
|
||||||
|
758218267fd42cba65d4cb39a03dbd45b0cc54dbb8191499a29f1ec3b067dcdf agents/researcher/work/queue-109/mutants/m5-closed-in-open.patch
|
||||||
|
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 agents/researcher/work/queue-109/mutants/m5-closed-in-open.rc
|
||||||
|
b4b2bec5b1ca6acf5f830c6eb3aeba6a74a1964168749c04d45ad673b15d9503 agents/researcher/work/queue-109/mutants/m6-cap-unstated.out
|
||||||
|
f047e709535a10ba427eb2a6585c7dde3c7ea16b9934a295450a6d8eea5db815 agents/researcher/work/queue-109/mutants/m6-cap-unstated.patch
|
||||||
|
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 agents/researcher/work/queue-109/mutants/m6-cap-unstated.rc
|
||||||
|
c3ff3575420cebbb57aa665592c17c2e7cc49da90f9800f0d49283f58c129863 agents/researcher/work/queue-109/mutants-run.txt
|
||||||
|
34ba363fba2e5b9384a8662e2402bc1abcb6a796ad718b01571b218c2018f797 agents/researcher/work/queue-109/mutants.sh
|
||||||
|
b0ef977027ad0a39b69347dc2d3f20765d4d8e1d57d783b03c1cc653b96e5fdb agents/researcher/work/queue-109/pm-workspace-names.txt
|
||||||
|
55fcdf13135818abde7157f78f4f9afbfcedc292d95fe02109908392f69fc4c7 agents/researcher/work/queue-109/row109.patch
|
||||||
|
bc33dae0ca339fb80f8ed5a0de7d84ea73df1f8f684f5001eed10dcb1abad9ec agents/researcher/work/queue-109/suites/bus-node-suite.out
|
||||||
|
d5731e9121956e7629a2e8040b4ee329f74996a680707ed3a9ae8193568df775 agents/researcher/work/queue-109/suites/harness-node-suite.out
|
||||||
|
b996f93d6f321014cd4e9ef327b6004bf8c525accf4e7c74cf3ca6ff3b145dd7 agents/researcher/work/queue-109/suites/queue-node-suite.out
|
||||||
|
c7ed4247eb9916e67bf5a2dd86687110fbf3e0eaaaa234b31b551d26497c5856 agents/researcher/work/queue-109/suites.sh
|
||||||
|
f91fc95a173339137f981e24fbf630acfde957e32c3cd71e28304532083c6a77 agents/researcher/work/queue-109/suites/test-auth.out
|
||||||
|
e1206366f07f3d9029530dcdcb55c0282d3f052adc60fc7fd51642d5d4ec3600 agents/researcher/work/queue-109/suites/test-conductor.out
|
||||||
|
52d211444489ea729dafc3c9745d4474d9dfd50f41055cbad792424197f04073 agents/researcher/work/queue-109/suites/test-config.out
|
||||||
|
7d5016ded1994048642f8b9ae845e52e8af4a069ba05c056c8f580a4589bacbe agents/researcher/work/queue-109/suites/test-discord.out
|
||||||
|
bd12416272ae0a9d9d78e3a340663c68a8c7423a2c7ed0ad2c2558ec97d8be90 agents/researcher/work/queue-109/suites/test-extension-package.out
|
||||||
|
83dfaa21c4e4ad941088efab3f69e87ebd2f6bd3cc72ede938e21dc06da8a34f agents/researcher/work/queue-109/suites/test-foundation.out
|
||||||
|
4636814ca4819fa843fe9abf6285bb6e32a295997221252269cc4bf112f1aabf agents/researcher/work/queue-109/suites/test-queue.out
|
||||||
|
fa832794ee35025fb4559d58d01a270f6bff2eb65e863e9075b797f47fa68545 agents/researcher/work/queue-109/suites/test-release.out
|
||||||
|
ccea37f4ffde1f160dcd1a1864988083a645d974e892065c956b338d0f8c853a agents/researcher/work/queue-109/suites/test-task.out
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
control-runner: exit 0; ℹ tests 20 ℹ pass 20 ℹ fail 0
|
||||||
|
control-bus: exit 0; ℹ tests 6 ℹ pass 6 ℹ fail 0
|
||||||
|
m1-drop-block: exit 1; ℹ tests 20 ℹ pass 17 ℹ fail 3
|
||||||
|
failed: ✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0
|
||||||
|
failed: ✖ a task closed on the tracker is missing from the next turn's open list
|
||||||
|
failed: ✖ failing tests:
|
||||||
|
failed: ✖ launches revoked and then restored: the turn's block reads allowed and gives the restore time
|
||||||
|
m2-drop-time: exit 1; ℹ tests 20 ℹ pass 17 ℹ fail 3
|
||||||
|
failed: ✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0
|
||||||
|
failed: ✖ a message read a day after it was sent shows the time it was sent
|
||||||
|
failed: ✖ failing tests:
|
||||||
|
failed: ✖ turnRequest shows the message's own sent time, a day old as well as new
|
||||||
|
m3-drop-failure-check: exit 1; ℹ tests 20 ℹ pass 19 ℹ fail 1
|
||||||
|
failed: ✖ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read
|
||||||
|
failed: ✖ failing tests:
|
||||||
|
m4-oldest-launch-event: exit 1; ℹ tests 6 ℹ pass 5 ℹ fail 1
|
||||||
|
failed: ✖ failing tests:
|
||||||
|
failed: ✖ launches revoked and then restored read allowed, with the restore time and event id
|
||||||
|
m5-closed-in-open: exit 1; ℹ tests 6 ℹ pass 5 ℹ fail 1
|
||||||
|
failed: ✖ a task closed on the tracker leaves the open list, and total counts the open ones
|
||||||
|
failed: ✖ failing tests:
|
||||||
|
m6-cap-unstated: exit 1; ℹ tests 6 ℹ pass 4 ℹ fail 2
|
||||||
|
failed: ✖ a task closed on the tracker leaves the open list, and total counts the open ones
|
||||||
|
failed: ✖ failing tests:
|
||||||
|
failed: ✖ the open list stops at the cap and says so; total still counts every open task
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Row 109 (#1603): each new check must fail on a mutant. For each mutant this runs the named test
|
||||||
|
# file against the real source (control) and against the source with one exact-once text edit.
|
||||||
|
# Runs in the given worktree, one test file at a time, and restores every source file on exit.
|
||||||
|
# Usage: mutants.sh <worktree> <outdir>
|
||||||
|
set -u
|
||||||
|
WT=$(cd "$1" && pwd); OUT=$(mkdir -p "$2" && cd "$2" && pwd)
|
||||||
|
cd "$WT" || exit 2
|
||||||
|
BAK=$(mktemp -d "$OUT/.bak.XXXXXX")
|
||||||
|
declare -A SAVED
|
||||||
|
save() { [ -n "${SAVED[$1]:-}" ] || { mkdir -p "$BAK/$(dirname "$1")"; cp "$1" "$BAK/$1"; SAVED[$1]=1; }; }
|
||||||
|
restore() { for f in "${!SAVED[@]}"; do cp "$BAK/$f" "$f"; done; rm -rf "$BAK"; }
|
||||||
|
trap restore EXIT
|
||||||
|
edit() { # file old new: exact-once replace
|
||||||
|
save "$1"
|
||||||
|
node -e '
|
||||||
|
const fs = require("fs"); const [f, a, b] = process.argv.slice(1);
|
||||||
|
const s = fs.readFileSync(f, "utf8"); const n = s.split(a).length - 1;
|
||||||
|
if (n !== 1) { console.error("expected one match, found " + n); process.exit(3); }
|
||||||
|
fs.writeFileSync(f, s.replace(a, () => b));
|
||||||
|
' "$1" "$2" "$3"
|
||||||
|
}
|
||||||
|
run() { # name testfile
|
||||||
|
setsid -w timeout -k 10 240 node --test "$2" > "$OUT/$1.out" 2>&1; echo "$?" > "$OUT/$1.rc"
|
||||||
|
echo "$1: exit $(cat "$OUT/$1.rc"); $(grep -E '^ℹ (tests|pass|fail)' "$OUT/$1.out" | tr '\n' ' ')"
|
||||||
|
grep -E '^✖' "$OUT/$1.out" | sort -u | sed 's/ ([0-9.]*ms)$//' | sed "s/^/ failed: /"
|
||||||
|
}
|
||||||
|
mutant() { # name testfile file old new
|
||||||
|
edit "$3" "$4" "$5" || { echo "$1: edit failed"; return; }
|
||||||
|
git diff -- "$3" > "$OUT/$1.patch"
|
||||||
|
run "$1" "$2"
|
||||||
|
cp "$BAK/$3" "$3"
|
||||||
|
}
|
||||||
|
R=packages/harness/src/runner.mjs; RT=packages/harness/tests/runner.test.mjs
|
||||||
|
B=packages/bus/src/broker.mjs; BT=packages/bus/tests/state.test.mjs
|
||||||
|
run control-runner "$RT"
|
||||||
|
run control-bus "$BT"
|
||||||
|
# m1: the turn request no longer carries the state block
|
||||||
|
mutant m1-drop-block "$RT" "$R" 'request: `${block}\n${turnRequest(m)}`' 'request: turnRequest(m)'
|
||||||
|
# m2: the header no longer carries the message time
|
||||||
|
mutant m2-drop-time "$RT" "$R" ' if (m.at) head.push(`sent ${m.at}`);
|
||||||
|
' ''
|
||||||
|
# m3: a failed state read no longer fails the turn; the turn runs with no block
|
||||||
|
mutant m3-drop-failure-check "$RT" "$R" 'unread = `could not read the current state: ${e.code ?? e.message}`;' 'block = "";'
|
||||||
|
# m4: the block's launch line shows the older event instead of the latest
|
||||||
|
mutant m4-oldest-launch-event "$BT" "$B" "kind IN ('launch.revoked','launch.restored') ORDER BY seq DESC LIMIT 1" "kind IN ('launch.revoked','launch.restored') ORDER BY seq ASC LIMIT 1"
|
||||||
|
# m5: the open list includes closed tasks
|
||||||
|
mutant m5-closed-in-open "$BT" "$B" 'const open = this.#tasks(s.business, { done: false });' 'const open = this.#tasks(s.business, { limit: TASK_PAGE });'
|
||||||
|
# m6: the cap isn't stated in the output
|
||||||
|
mutant m6-cap-unstated "$BT" "$B" 'total: open.total, cap: TASK_PAGE }' 'total: open.total }'
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
✔ a business that never switched launches reads allowed with no time and no event (12.740463ms)
|
||||||
|
✔ launches revoked and then restored read allowed, with the restore time and event id (10.115322ms)
|
||||||
|
✔ a task closed on the tracker leaves the open list, and total counts the open ones (10.612196ms)
|
||||||
|
✔ a tombstone is not open (7.867781ms)
|
||||||
|
✔ the open list stops at the cap and says so; total still counts every open task (12.014832ms)
|
||||||
|
✔ a role and a reader session read state; arguments, and an unbound cap, refuse (9.286701ms)
|
||||||
|
ℹ tests 6
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 6
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 117.997122
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
✔ founderCheck: founder variables, then a needed service without a usable token (1.105369ms)
|
||||||
|
✔ turnRequest names the sender, class, reply and decision (0.21491ms)
|
||||||
|
✔ turnRequest shows the message's own sent time, a day old as well as new (0.126433ms)
|
||||||
|
✔ stateBlock names the launch state with its time and event, and the open tasks with the cap (0.195396ms)
|
||||||
|
✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (152.361989ms)
|
||||||
|
✔ launches revoked and then restored: the turn's block reads allowed and gives the restore time (169.022648ms)
|
||||||
|
✔ a task closed on the tracker is missing from the next turn's open list (167.718931ms)
|
||||||
|
✔ a message read a day after it was sent shows the time it was sent (87.662171ms)
|
||||||
|
✔ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read (63.771972ms)
|
||||||
|
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (48.50038ms)
|
||||||
|
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (271.762253ms)
|
||||||
|
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1192.335593ms)
|
||||||
|
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (93.322815ms)
|
||||||
|
✔ founder credentials stop before the claim (20) (96.496233ms)
|
||||||
|
✔ a refused claim exits 21; an ended run's capability exits 22 (100.411332ms)
|
||||||
|
✔ the launch ending under a running session exits 22 (102.903092ms)
|
||||||
|
✔ a broker that stays unreachable exits 23 after brokerRetries polls (204.000168ms)
|
||||||
|
✔ a broker that is down at the claim exits 23, not 21 (47.277428ms)
|
||||||
|
✔ no capability, or a malformed one, on stdin exits 2 (151.489917ms)
|
||||||
|
✔ a missing or malformed policy exits 2 before the claim (87.115027ms)
|
||||||
|
ℹ tests 20
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 20
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 10345.427283
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
✔ founderCheck: founder variables, then a needed service without a usable token (0.927996ms)
|
||||||
|
✔ turnRequest names the sender, class, reply and decision (0.187137ms)
|
||||||
|
✔ turnRequest shows the message's own sent time, a day old as well as new (0.121749ms)
|
||||||
|
✔ stateBlock names the launch state with its time and event, and the open tasks with the cap (0.176786ms)
|
||||||
|
✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (147.584047ms)
|
||||||
|
✖ launches revoked and then restored: the turn's block reads allowed and gives the restore time (91.206024ms)
|
||||||
|
✖ a task closed on the tracker is missing from the next turn's open list (90.236176ms)
|
||||||
|
✔ a message read a day after it was sent shows the time it was sent (89.018287ms)
|
||||||
|
✔ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read (63.256302ms)
|
||||||
|
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (48.492078ms)
|
||||||
|
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (247.488656ms)
|
||||||
|
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1191.072755ms)
|
||||||
|
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (93.718876ms)
|
||||||
|
✔ founder credentials stop before the claim (20) (94.802529ms)
|
||||||
|
✔ a refused claim exits 21; an ended run's capability exits 22 (98.777833ms)
|
||||||
|
✔ the launch ending under a running session exits 22 (102.953767ms)
|
||||||
|
✔ a broker that stays unreachable exits 23 after brokerRetries polls (206.247136ms)
|
||||||
|
✔ a broker that is down at the claim exits 23, not 21 (45.3418ms)
|
||||||
|
✔ no capability, or a malformed one, on stdin exits 2 (156.779329ms)
|
||||||
|
✔ a missing or malformed policy exits 2 before the claim (83.562291ms)
|
||||||
|
ℹ tests 20
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 17
|
||||||
|
ℹ fail 3
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 10156.567034
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/harness/tests/runner.test.mjs:174:1
|
||||||
|
✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (147.584047ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /^echo: Current state, read at \S+:\nLaunches: allowed \(never switched\)\.\nOpen tasks: 0\.\n\nMessage 0d34f793-bec7-4d76-9425-fb1079503a1b from pm, sent 2026-10-13T23:07:58.732Z, class REQUEST:\n\nhello$/. Input:
|
||||||
|
|
||||||
|
'echo: Message 0d34f793-bec7-4d76-9425-fb1079503a1b from pm, sent 2026-10-13T23:07:58.732Z, class REQUEST:\n' +
|
||||||
|
'\n' +
|
||||||
|
'hello'
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q109/packages/harness/tests/runner.test.mjs:186:10)
|
||||||
|
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: true,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: 'echo: Message 0d34f793-bec7-4d76-9425-fb1079503a1b from pm, sent 2026-10-13T23:07:58.732Z, class REQUEST:\n\nhello',
|
||||||
|
expected: /^echo: Current state, read at \S+:\nLaunches: allowed \(never switched\)\.\nOpen tasks: 0\.\n\nMessage 0d34f793-bec7-4d76-9425-fb1079503a1b from pm, sent 2026-10-13T23:07:58.732Z, class REQUEST:\n\nhello$/,
|
||||||
|
operator: 'match',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
|
|
||||||
|
test at packages/harness/tests/runner.test.mjs:217:1
|
||||||
|
✖ launches revoked and then restored: the turn's block reads allowed and gives the restore time (91.206024ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /\nLaunches: revoked since \S+ \(event \S+\)\.\n/. Input:
|
||||||
|
|
||||||
|
'echo: Message 4fe85a96-a724-4368-a91f-9a7c6dfb1888 from pm, sent 2026-10-13T23:07:58.825Z, class REQUEST:\n' +
|
||||||
|
'\n' +
|
||||||
|
'what now'
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q109/packages/harness/tests/runner.test.mjs:223:10)
|
||||||
|
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: true,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: 'echo: Message 4fe85a96-a724-4368-a91f-9a7c6dfb1888 from pm, sent 2026-10-13T23:07:58.825Z, class REQUEST:\n\nwhat now',
|
||||||
|
expected: /\nLaunches: revoked since \S+ \(event \S+\)\.\n/,
|
||||||
|
operator: 'match',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
|
|
||||||
|
test at packages/harness/tests/runner.test.mjs:229:1
|
||||||
|
✖ a task closed on the tracker is missing from the next turn's open list (90.236176ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /\nOpen tasks: 2\.\n- vikunja:3\/1 Task 1\n- vikunja:3\/2 Task 2\n/. Input:
|
||||||
|
|
||||||
|
'echo: Message a189d6e4-745d-49d2-9ab6-60e13bf1b5e6 from pm, sent 2026-10-13T23:07:58.914Z, class REQUEST:\n' +
|
||||||
|
'\n' +
|
||||||
|
'what now'
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q109/packages/harness/tests/runner.test.mjs:236:10)
|
||||||
|
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: true,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: 'echo: Message a189d6e4-745d-49d2-9ab6-60e13bf1b5e6 from pm, sent 2026-10-13T23:07:58.914Z, class REQUEST:\n\nwhat now',
|
||||||
|
expected: /\nOpen tasks: 2\.\n- vikunja:3\/1 Task 1\n- vikunja:3\/2 Task 2\n/,
|
||||||
|
operator: 'match',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
diff --git a/packages/harness/src/runner.mjs b/packages/harness/src/runner.mjs
|
||||||
|
index 00595542b..87d66dd36 100644
|
||||||
|
--- a/packages/harness/src/runner.mjs
|
||||||
|
+++ b/packages/harness/src/runner.mjs
|
||||||
|
@@ -62,7 +62,10 @@ export function founderCheck(policy, env) {
|
||||||
|
|
||||||
|
// The request text one message becomes.
|
||||||
|
export function turnRequest(m) {
|
||||||
|
- const head = [`Message ${m.id} from ${m.from_role}`, `class ${m.class}`];
|
||||||
|
+ const head = [`Message ${m.id} from ${m.from_role}`];
|
||||||
|
+ // Row 109 (#1603): when the sender sent it, so a message that waited reads as old.
|
||||||
|
+ if (m.at) head.push(`sent ${m.at}`);
|
||||||
|
+ head.push(`class ${m.class}`);
|
||||||
|
if (m.in_reply_to) head.push(`in reply to ${m.in_reply_to}`);
|
||||||
|
if (m.decision) head.push(`citing decision ${m.decision}`);
|
||||||
|
// Row 90 (#1584): the human input id task_create cites as its request.
|
||||||
|
@@ -70,6 +73,22 @@ export function turnRequest(m) {
|
||||||
|
return `${head.join(", ")}:\n\n${m.body}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
+// Row 109 (#1603): the block at the top of a turn's request. It is what the broker said at `readAt`,
|
||||||
|
+// the truth for launch state and task status over any older message.
|
||||||
|
+export function stateBlock(state, readAt) {
|
||||||
|
+ const { launch, tasks } = state;
|
||||||
|
+ const since = launch.at ? ` since ${launch.at} (event ${launch.event})` : " (never switched)";
|
||||||
|
+ const shown = tasks.open.length < tasks.total ? `, the first ${tasks.open.length} of cap ${tasks.cap}` : "";
|
||||||
|
+ const open = tasks.open.map((t) => `- ${t.task_ref} ${t.title ?? ""}`.trimEnd());
|
||||||
|
+ return [
|
||||||
|
+ `Current state, read at ${readAt}:`,
|
||||||
|
+ `Launches: ${launch.state}${since}.`,
|
||||||
|
+ `Open tasks: ${tasks.total}${shown}.`,
|
||||||
|
+ ...open,
|
||||||
|
+ "",
|
||||||
|
+ ].join("\n");
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
function clip(text) {
|
||||||
|
return text.length > BODY_MAX ? text.slice(0, BODY_MAX) + "\n[answer cut at 32000 characters]" : text;
|
||||||
|
}
|
||||||
|
@@ -339,7 +358,23 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
|
||||||
|
}
|
||||||
|
for (const m of messages) {
|
||||||
|
if (stopping) break loop;
|
||||||
|
- const turn = await runTurn({ session, request: turnRequest(m), n: ++n, env: turnEnv, onChild: (c) => (child = c) });
|
||||||
|
+ // Row 109 (#1603): read the state now, not when the message was sent. A failed read fails the turn.
|
||||||
|
+ let block = null;
|
||||||
|
+ let unread = null;
|
||||||
|
+ try {
|
||||||
|
+ block = stateBlock(await client.call("state"), new Date().toISOString());
|
||||||
|
+ } catch (e) {
|
||||||
|
+ if (SESSION_GONE.has(e.code)) {
|
||||||
|
+ log(`runner: session stopped working: ${e.code}`);
|
||||||
|
+ code = EXIT.session;
|
||||||
|
+ break loop;
|
||||||
|
+ }
|
||||||
|
+ unread = `could not read the current state: ${e.code ?? e.message}`;
|
||||||
|
+ }
|
||||||
|
+ ++n;
|
||||||
|
+ const turn = unread
|
||||||
|
+ ? { ok: false, reason: unread }
|
||||||
|
+ : await runTurn({ session, request: turnRequest(m), n, env: turnEnv, onChild: (c) => (child = c) });
|
||||||
|
if (stopping) break loop;
|
||||||
|
if (!turn.ok) log(`runner: turn ${n} for message ${m.id} failed: ${turn.reason}`);
|
||||||
|
try {
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
✔ founderCheck: founder variables, then a needed service without a usable token (0.987417ms)
|
||||||
|
✔ turnRequest names the sender, class, reply and decision (0.198796ms)
|
||||||
|
✖ turnRequest shows the message's own sent time, a day old as well as new (0.813441ms)
|
||||||
|
✔ stateBlock names the launch state with its time and event, and the open tasks with the cap (0.188045ms)
|
||||||
|
✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (145.435504ms)
|
||||||
|
✔ launches revoked and then restored: the turn's block reads allowed and gives the restore time (169.165152ms)
|
||||||
|
✔ a task closed on the tracker is missing from the next turn's open list (168.933118ms)
|
||||||
|
✖ a message read a day after it was sent shows the time it was sent (90.269575ms)
|
||||||
|
✔ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read (63.164189ms)
|
||||||
|
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (50.491583ms)
|
||||||
|
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (272.210562ms)
|
||||||
|
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1192.639985ms)
|
||||||
|
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (94.793583ms)
|
||||||
|
✔ founder credentials stop before the claim (20) (93.850655ms)
|
||||||
|
✔ a refused claim exits 21; an ended run's capability exits 22 (99.075117ms)
|
||||||
|
✔ the launch ending under a running session exits 22 (101.888229ms)
|
||||||
|
✔ a broker that stays unreachable exits 23 after brokerRetries polls (204.216765ms)
|
||||||
|
✔ a broker that is down at the claim exits 23, not 21 (49.593739ms)
|
||||||
|
✔ no capability, or a malformed one, on stdin exits 2 (156.481136ms)
|
||||||
|
✔ a missing or malformed policy exits 2 before the claim (91.206713ms)
|
||||||
|
ℹ tests 20
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 17
|
||||||
|
ℹ fail 3
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 10339.878414
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/harness/tests/runner.test.mjs:60:1
|
||||||
|
✖ turnRequest shows the message's own sent time, a day old as well as new (0.813441ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||||
|
+ actual - expected
|
||||||
|
|
||||||
|
+ 'Message m5 from jason, class REQUEST:\n\nresume'
|
||||||
|
- 'Message m5 from jason, sent 2026-10-12T09:30:00.000Z, class REQUEST:\n\nresume'
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q109/packages/harness/tests/runner.test.mjs:62: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: 'Message m5 from jason, class REQUEST:\n\nresume',
|
||||||
|
expected: 'Message m5 from jason, sent 2026-10-12T09:30:00.000Z, class REQUEST:\n\nresume',
|
||||||
|
operator: 'strictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
|
|
||||||
|
test at packages/harness/tests/runner.test.mjs:174:1
|
||||||
|
✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (145.435504ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /^echo: Current state, read at \S+:\nLaunches: allowed \(never switched\)\.\nOpen tasks: 0\.\n\nMessage 2b684cd9-8412-4498-9700-651ca8840067 from pm, sent 2026-10-13T23:08:08.949Z, class REQUEST:\n\nhello$/. Input:
|
||||||
|
|
||||||
|
'echo: Current state, read at 2026-10-13T23:08:08.986Z:\n' +
|
||||||
|
'Launches: allowed (never switched).\n' +
|
||||||
|
'Open tasks: 0.\n' +
|
||||||
|
'\n' +
|
||||||
|
'Message 2b684cd9-8412-4498-9700-651ca8840067 from pm, class REQUEST:\n' +
|
||||||
|
'\n' +
|
||||||
|
'hello'
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q109/packages/harness/tests/runner.test.mjs:186:10)
|
||||||
|
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: true,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: 'echo: Current state, read at 2026-10-13T23:08:08.986Z:\nLaunches: allowed (never switched).\nOpen tasks: 0.\n\nMessage 2b684cd9-8412-4498-9700-651ca8840067 from pm, class REQUEST:\n\nhello',
|
||||||
|
expected: /^echo: Current state, read at \S+:\nLaunches: allowed \(never switched\)\.\nOpen tasks: 0\.\n\nMessage 2b684cd9-8412-4498-9700-651ca8840067 from pm, sent 2026-10-13T23:08:08.949Z, class REQUEST:\n\nhello$/,
|
||||||
|
operator: 'match',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
|
|
||||||
|
test at packages/harness/tests/runner.test.mjs:243:1
|
||||||
|
✖ a message read a day after it was sent shows the time it was sent (90.269575ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
|
||||||
|
|
||||||
|
assert.ok(reply.body.includes(`Message ${sent.id} from pm, sent ${sentAt}, class REQUEST:`))
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q109/packages/harness/tests/runner.test.mjs:255:10)
|
||||||
|
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: true,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: false,
|
||||||
|
expected: true,
|
||||||
|
operator: '==',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
diff --git a/packages/harness/src/runner.mjs b/packages/harness/src/runner.mjs
|
||||||
|
index 00595542b..57f4b141a 100644
|
||||||
|
--- a/packages/harness/src/runner.mjs
|
||||||
|
+++ b/packages/harness/src/runner.mjs
|
||||||
|
@@ -62,7 +62,9 @@ export function founderCheck(policy, env) {
|
||||||
|
|
||||||
|
// The request text one message becomes.
|
||||||
|
export function turnRequest(m) {
|
||||||
|
- const head = [`Message ${m.id} from ${m.from_role}`, `class ${m.class}`];
|
||||||
|
+ const head = [`Message ${m.id} from ${m.from_role}`];
|
||||||
|
+ // Row 109 (#1603): when the sender sent it, so a message that waited reads as old.
|
||||||
|
+ head.push(`class ${m.class}`);
|
||||||
|
if (m.in_reply_to) head.push(`in reply to ${m.in_reply_to}`);
|
||||||
|
if (m.decision) head.push(`citing decision ${m.decision}`);
|
||||||
|
// Row 90 (#1584): the human input id task_create cites as its request.
|
||||||
|
@@ -70,6 +72,22 @@ export function turnRequest(m) {
|
||||||
|
return `${head.join(", ")}:\n\n${m.body}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
+// Row 109 (#1603): the block at the top of a turn's request. It is what the broker said at `readAt`,
|
||||||
|
+// the truth for launch state and task status over any older message.
|
||||||
|
+export function stateBlock(state, readAt) {
|
||||||
|
+ const { launch, tasks } = state;
|
||||||
|
+ const since = launch.at ? ` since ${launch.at} (event ${launch.event})` : " (never switched)";
|
||||||
|
+ const shown = tasks.open.length < tasks.total ? `, the first ${tasks.open.length} of cap ${tasks.cap}` : "";
|
||||||
|
+ const open = tasks.open.map((t) => `- ${t.task_ref} ${t.title ?? ""}`.trimEnd());
|
||||||
|
+ return [
|
||||||
|
+ `Current state, read at ${readAt}:`,
|
||||||
|
+ `Launches: ${launch.state}${since}.`,
|
||||||
|
+ `Open tasks: ${tasks.total}${shown}.`,
|
||||||
|
+ ...open,
|
||||||
|
+ "",
|
||||||
|
+ ].join("\n");
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
function clip(text) {
|
||||||
|
return text.length > BODY_MAX ? text.slice(0, BODY_MAX) + "\n[answer cut at 32000 characters]" : text;
|
||||||
|
}
|
||||||
|
@@ -339,7 +357,23 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
|
||||||
|
}
|
||||||
|
for (const m of messages) {
|
||||||
|
if (stopping) break loop;
|
||||||
|
- const turn = await runTurn({ session, request: turnRequest(m), n: ++n, env: turnEnv, onChild: (c) => (child = c) });
|
||||||
|
+ // Row 109 (#1603): read the state now, not when the message was sent. A failed read fails the turn.
|
||||||
|
+ let block = null;
|
||||||
|
+ let unread = null;
|
||||||
|
+ try {
|
||||||
|
+ block = stateBlock(await client.call("state"), new Date().toISOString());
|
||||||
|
+ } catch (e) {
|
||||||
|
+ if (SESSION_GONE.has(e.code)) {
|
||||||
|
+ log(`runner: session stopped working: ${e.code}`);
|
||||||
|
+ code = EXIT.session;
|
||||||
|
+ break loop;
|
||||||
|
+ }
|
||||||
|
+ unread = `could not read the current state: ${e.code ?? e.message}`;
|
||||||
|
+ }
|
||||||
|
+ ++n;
|
||||||
|
+ const turn = unread
|
||||||
|
+ ? { ok: false, reason: unread }
|
||||||
|
+ : await runTurn({ session, request: `${block}\n${turnRequest(m)}`, n, env: turnEnv, onChild: (c) => (child = c) });
|
||||||
|
if (stopping) break loop;
|
||||||
|
if (!turn.ok) log(`runner: turn ${n} for message ${m.id} failed: ${turn.reason}`);
|
||||||
|
try {
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
✔ founderCheck: founder variables, then a needed service without a usable token (1.027678ms)
|
||||||
|
✔ turnRequest names the sender, class, reply and decision (0.175568ms)
|
||||||
|
✔ turnRequest shows the message's own sent time, a day old as well as new (0.108091ms)
|
||||||
|
✔ stateBlock names the launch state with its time and event, and the open tasks with the cap (0.164473ms)
|
||||||
|
✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (151.370525ms)
|
||||||
|
✔ launches revoked and then restored: the turn's block reads allowed and gives the restore time (168.631533ms)
|
||||||
|
✔ a task closed on the tracker is missing from the next turn's open list (193.462068ms)
|
||||||
|
✔ a message read a day after it was sent shows the time it was sent (88.274542ms)
|
||||||
|
✖ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read (89.644081ms)
|
||||||
|
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (50.931607ms)
|
||||||
|
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (273.817356ms)
|
||||||
|
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1189.440166ms)
|
||||||
|
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (95.175856ms)
|
||||||
|
✔ founder credentials stop before the claim (20) (89.610808ms)
|
||||||
|
✔ a refused claim exits 21; an ended run's capability exits 22 (96.280534ms)
|
||||||
|
✔ the launch ending under a running session exits 22 (100.621249ms)
|
||||||
|
✔ a broker that stays unreachable exits 23 after brokerRetries polls (202.570602ms)
|
||||||
|
✔ a broker that is down at the claim exits 23, not 21 (49.247418ms)
|
||||||
|
✔ no capability, or a malformed one, on stdin exits 2 (161.38161ms)
|
||||||
|
✔ a missing or malformed policy exits 2 before the claim (86.000718ms)
|
||||||
|
ℹ tests 20
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 19
|
||||||
|
ℹ fail 1
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 10397.655899
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/harness/tests/runner.test.mjs:259:1
|
||||||
|
✖ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read (89.644081ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||||
|
+ actual - expected
|
||||||
|
|
||||||
|
+ 'echo: \n' +
|
||||||
|
+ 'Message 586075ac-c0cc-4abf-9f4f-2d1289199157 from pm, sent 2026-10-13T23:08:19.896Z, class REQUEST:\n' +
|
||||||
|
+ '\n' +
|
||||||
|
+ 'plan the week'
|
||||||
|
- 'The turn failed: could not read the current state: state-unavailable'
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q109/packages/harness/tests/runner.test.mjs:273:10)
|
||||||
|
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||||
|
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||||
|
generatedMessage: true,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: 'echo: \nMessage 586075ac-c0cc-4abf-9f4f-2d1289199157 from pm, sent 2026-10-13T23:08:19.896Z, class REQUEST:\n\nplan the week',
|
||||||
|
expected: 'The turn failed: could not read the current state: state-unavailable',
|
||||||
|
operator: 'strictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
diff --git a/packages/harness/src/runner.mjs b/packages/harness/src/runner.mjs
|
||||||
|
index 00595542b..c56e9fc38 100644
|
||||||
|
--- a/packages/harness/src/runner.mjs
|
||||||
|
+++ b/packages/harness/src/runner.mjs
|
||||||
|
@@ -62,7 +62,10 @@ export function founderCheck(policy, env) {
|
||||||
|
|
||||||
|
// The request text one message becomes.
|
||||||
|
export function turnRequest(m) {
|
||||||
|
- const head = [`Message ${m.id} from ${m.from_role}`, `class ${m.class}`];
|
||||||
|
+ const head = [`Message ${m.id} from ${m.from_role}`];
|
||||||
|
+ // Row 109 (#1603): when the sender sent it, so a message that waited reads as old.
|
||||||
|
+ if (m.at) head.push(`sent ${m.at}`);
|
||||||
|
+ head.push(`class ${m.class}`);
|
||||||
|
if (m.in_reply_to) head.push(`in reply to ${m.in_reply_to}`);
|
||||||
|
if (m.decision) head.push(`citing decision ${m.decision}`);
|
||||||
|
// Row 90 (#1584): the human input id task_create cites as its request.
|
||||||
|
@@ -70,6 +73,22 @@ export function turnRequest(m) {
|
||||||
|
return `${head.join(", ")}:\n\n${m.body}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
+// Row 109 (#1603): the block at the top of a turn's request. It is what the broker said at `readAt`,
|
||||||
|
+// the truth for launch state and task status over any older message.
|
||||||
|
+export function stateBlock(state, readAt) {
|
||||||
|
+ const { launch, tasks } = state;
|
||||||
|
+ const since = launch.at ? ` since ${launch.at} (event ${launch.event})` : " (never switched)";
|
||||||
|
+ const shown = tasks.open.length < tasks.total ? `, the first ${tasks.open.length} of cap ${tasks.cap}` : "";
|
||||||
|
+ const open = tasks.open.map((t) => `- ${t.task_ref} ${t.title ?? ""}`.trimEnd());
|
||||||
|
+ return [
|
||||||
|
+ `Current state, read at ${readAt}:`,
|
||||||
|
+ `Launches: ${launch.state}${since}.`,
|
||||||
|
+ `Open tasks: ${tasks.total}${shown}.`,
|
||||||
|
+ ...open,
|
||||||
|
+ "",
|
||||||
|
+ ].join("\n");
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
function clip(text) {
|
||||||
|
return text.length > BODY_MAX ? text.slice(0, BODY_MAX) + "\n[answer cut at 32000 characters]" : text;
|
||||||
|
}
|
||||||
|
@@ -339,7 +358,23 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
|
||||||
|
}
|
||||||
|
for (const m of messages) {
|
||||||
|
if (stopping) break loop;
|
||||||
|
- const turn = await runTurn({ session, request: turnRequest(m), n: ++n, env: turnEnv, onChild: (c) => (child = c) });
|
||||||
|
+ // Row 109 (#1603): read the state now, not when the message was sent. A failed read fails the turn.
|
||||||
|
+ let block = null;
|
||||||
|
+ let unread = null;
|
||||||
|
+ try {
|
||||||
|
+ block = stateBlock(await client.call("state"), new Date().toISOString());
|
||||||
|
+ } catch (e) {
|
||||||
|
+ if (SESSION_GONE.has(e.code)) {
|
||||||
|
+ log(`runner: session stopped working: ${e.code}`);
|
||||||
|
+ code = EXIT.session;
|
||||||
|
+ break loop;
|
||||||
|
+ }
|
||||||
|
+ block = "";
|
||||||
|
+ }
|
||||||
|
+ ++n;
|
||||||
|
+ const turn = unread
|
||||||
|
+ ? { ok: false, reason: unread }
|
||||||
|
+ : await runTurn({ session, request: `${block}\n${turnRequest(m)}`, n, env: turnEnv, onChild: (c) => (child = c) });
|
||||||
|
if (stopping) break loop;
|
||||||
|
if (!turn.ok) log(`runner: turn ${n} for message ${m.id} failed: ${turn.reason}`);
|
||||||
|
try {
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
✔ a business that never switched launches reads allowed with no time and no event (11.927647ms)
|
||||||
|
✖ launches revoked and then restored read allowed, with the restore time and event id (10.621051ms)
|
||||||
|
✔ a task closed on the tracker leaves the open list, and total counts the open ones (9.618177ms)
|
||||||
|
✔ a tombstone is not open (7.741209ms)
|
||||||
|
✔ the open list stops at the cap and says so; total still counts every open task (11.030111ms)
|
||||||
|
✔ a role and a reader session read state; arguments, and an unbound cap, refuse (8.605084ms)
|
||||||
|
ℹ tests 6
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 5
|
||||||
|
ℹ fail 1
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 112.662835
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/bus/tests/state.test.mjs:42:1
|
||||||
|
✖ launches revoked and then restored read allowed, with the restore time and event id (10.621051ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
|
||||||
|
+ actual - expected
|
||||||
|
|
||||||
|
{
|
||||||
|
+ at: '2026-10-13T23:08:29.735Z',
|
||||||
|
+ event: '03630f89-6d18-4f43-8ced-8f36a81f8fbc',
|
||||||
|
+ state: 'revoked'
|
||||||
|
- at: '2026-10-13T23:08:29.737Z',
|
||||||
|
- event: 'f2c5df5c-6b5b-4f25-91ca-2137aa67912d',
|
||||||
|
- state: 'allowed'
|
||||||
|
}
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q109/packages/bus/tests/state.test.mjs:51: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 startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||||
|
generatedMessage: true,
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
actual: { state: 'revoked', at: '2026-10-13T23:08:29.735Z', event: '03630f89-6d18-4f43-8ced-8f36a81f8fbc' },
|
||||||
|
expected: { state: 'allowed', at: '2026-10-13T23:08:29.737Z', event: 'f2c5df5c-6b5b-4f25-91ca-2137aa67912d' },
|
||||||
|
operator: 'deepStrictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs
|
||||||
|
index 732c7310e..054b7acff 100644
|
||||||
|
--- a/packages/bus/src/broker.mjs
|
||||||
|
+++ b/packages/bus/src/broker.mjs
|
||||||
|
@@ -654,7 +654,7 @@ export class Broker {
|
||||||
|
if (!object(args)) fail('invalid-request');
|
||||||
|
this.#secretCheck(args);
|
||||||
|
return this.#store.transaction(() => {
|
||||||
|
- if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages'].includes(request.verb))
|
||||||
|
+ if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages', 'state'].includes(request.verb))
|
||||||
|
fail('read-only');
|
||||||
|
const result = this.#dispatch(s, request.verb, args);
|
||||||
|
this.#secretCheck(result);
|
||||||
|
@@ -1086,6 +1086,23 @@ export class Broker {
|
||||||
|
this.#agent(s);
|
||||||
|
return this.#requirements;
|
||||||
|
}
|
||||||
|
+ // Row 109 (#1603): what a run needs to know before it plans. The business's launch state with
|
||||||
|
+ // the time and id of the event that set it (`allowed`, no time and no id when none has), and
|
||||||
|
+ // the open tasks as the tracker snapshot shows them: refs and titles, at most `cap` of them,
|
||||||
|
+ // `total` counting every open task. A read for role and reader sessions; nothing is stored.
|
||||||
|
+ case 'state': {
|
||||||
|
+ keys(a, []);
|
||||||
|
+ if (!s.human && !s.reader) this.#agent(s);
|
||||||
|
+ const set = this.#store.get(
|
||||||
|
+ "SELECT id,kind,at FROM events WHERE business=? AND kind IN ('launch.revoked','launch.restored') ORDER BY seq ASC LIMIT 1",
|
||||||
|
+ s.business,
|
||||||
|
+ );
|
||||||
|
+ const open = this.#tasks(s.business, { done: false });
|
||||||
|
+ return {
|
||||||
|
+ launch: { state: set?.kind === 'launch.revoked' ? 'revoked' : 'allowed', at: set?.at ?? null, event: set?.id ?? null },
|
||||||
|
+ tasks: { open: open.tasks.map(({ task_ref, title }) => ({ task_ref, title })), total: open.total, cap: TASK_PAGE },
|
||||||
|
+ };
|
||||||
|
+ }
|
||||||
|
case 'inbox':
|
||||||
|
case 'agents':
|
||||||
|
case 'tasks':
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
✔ a business that never switched launches reads allowed with no time and no event (11.379419ms)
|
||||||
|
✔ launches revoked and then restored read allowed, with the restore time and event id (9.235032ms)
|
||||||
|
✖ a task closed on the tracker leaves the open list, and total counts the open ones (10.543996ms)
|
||||||
|
✔ a tombstone is not open (8.500886ms)
|
||||||
|
✔ the open list stops at the cap and says so; total still counts every open task (11.900535ms)
|
||||||
|
✔ a role and a reader session read state; arguments, and an unbound cap, refuse (8.938047ms)
|
||||||
|
ℹ tests 6
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 5
|
||||||
|
ℹ fail 1
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 117.78508
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/bus/tests/state.test.mjs:55:1
|
||||||
|
✖ a task closed on the tracker leaves the open list, and total counts the open ones (10.543996ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
|
||||||
|
+ actual - expected
|
||||||
|
|
||||||
|
[
|
||||||
|
'vikunja:3/1',
|
||||||
|
+ 'vikunja:3/2',
|
||||||
|
'vikunja:3/3'
|
||||||
|
]
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q109/packages/bus/tests/state.test.mjs:65: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: [ 'vikunja:3/1', 'vikunja:3/2', 'vikunja:3/3' ],
|
||||||
|
expected: [ 'vikunja:3/1', 'vikunja:3/3' ],
|
||||||
|
operator: 'deepStrictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs
|
||||||
|
index 732c7310e..138991706 100644
|
||||||
|
--- a/packages/bus/src/broker.mjs
|
||||||
|
+++ b/packages/bus/src/broker.mjs
|
||||||
|
@@ -654,7 +654,7 @@ export class Broker {
|
||||||
|
if (!object(args)) fail('invalid-request');
|
||||||
|
this.#secretCheck(args);
|
||||||
|
return this.#store.transaction(() => {
|
||||||
|
- if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages'].includes(request.verb))
|
||||||
|
+ if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages', 'state'].includes(request.verb))
|
||||||
|
fail('read-only');
|
||||||
|
const result = this.#dispatch(s, request.verb, args);
|
||||||
|
this.#secretCheck(result);
|
||||||
|
@@ -1086,6 +1086,23 @@ export class Broker {
|
||||||
|
this.#agent(s);
|
||||||
|
return this.#requirements;
|
||||||
|
}
|
||||||
|
+ // Row 109 (#1603): what a run needs to know before it plans. The business's launch state with
|
||||||
|
+ // the time and id of the event that set it (`allowed`, no time and no id when none has), and
|
||||||
|
+ // the open tasks as the tracker snapshot shows them: refs and titles, at most `cap` of them,
|
||||||
|
+ // `total` counting every open task. A read for role and reader sessions; nothing is stored.
|
||||||
|
+ case 'state': {
|
||||||
|
+ keys(a, []);
|
||||||
|
+ if (!s.human && !s.reader) this.#agent(s);
|
||||||
|
+ const set = this.#store.get(
|
||||||
|
+ "SELECT id,kind,at FROM events WHERE business=? AND kind IN ('launch.revoked','launch.restored') ORDER BY seq DESC LIMIT 1",
|
||||||
|
+ s.business,
|
||||||
|
+ );
|
||||||
|
+ const open = this.#tasks(s.business, { limit: TASK_PAGE });
|
||||||
|
+ return {
|
||||||
|
+ launch: { state: set?.kind === 'launch.revoked' ? 'revoked' : 'allowed', at: set?.at ?? null, event: set?.id ?? null },
|
||||||
|
+ tasks: { open: open.tasks.map(({ task_ref, title }) => ({ task_ref, title })), total: open.total, cap: TASK_PAGE },
|
||||||
|
+ };
|
||||||
|
+ }
|
||||||
|
case 'inbox':
|
||||||
|
case 'agents':
|
||||||
|
case 'tasks':
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
✔ a business that never switched launches reads allowed with no time and no event (11.422277ms)
|
||||||
|
✔ launches revoked and then restored read allowed, with the restore time and event id (8.798469ms)
|
||||||
|
✖ a task closed on the tracker leaves the open list, and total counts the open ones (9.437594ms)
|
||||||
|
✔ a tombstone is not open (7.259551ms)
|
||||||
|
✖ the open list stops at the cap and says so; total still counts every open task (10.864472ms)
|
||||||
|
✔ a role and a reader session read state; arguments, and an unbound cap, refuse (7.816773ms)
|
||||||
|
ℹ tests 6
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 4
|
||||||
|
ℹ fail 2
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 110.771289
|
||||||
|
|
||||||
|
✖ failing tests:
|
||||||
|
|
||||||
|
test at packages/bus/tests/state.test.mjs:55:1
|
||||||
|
✖ a task closed on the tracker leaves the open list, and total counts the open ones (9.437594ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
|
||||||
|
+ actual - expected
|
||||||
|
|
||||||
|
{
|
||||||
|
- cap: 50,
|
||||||
|
open: [
|
||||||
|
{
|
||||||
|
task_ref: 'vikunja:3/1',
|
||||||
|
title: 'Task 1'
|
||||||
|
},
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q109/packages/bus/tests/state.test.mjs:58: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: { open: [ [Object], [Object], [Object] ], total: 3 },
|
||||||
|
expected: { open: [ [Object], [Object], [Object] ], total: 3, cap: 50 },
|
||||||
|
operator: 'deepStrictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
|
|
||||||
|
test at packages/bus/tests/state.test.mjs:75:1
|
||||||
|
✖ the open list stops at the cap and says so; total still counts every open task (10.864472ms)
|
||||||
|
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||||
|
|
||||||
|
undefined !== 50
|
||||||
|
|
||||||
|
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q109/packages/bus/tests/state.test.mjs:81: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: undefined,
|
||||||
|
expected: 50,
|
||||||
|
operator: 'strictEqual',
|
||||||
|
diff: 'simple'
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs
|
||||||
|
index 732c7310e..6df80db83 100644
|
||||||
|
--- a/packages/bus/src/broker.mjs
|
||||||
|
+++ b/packages/bus/src/broker.mjs
|
||||||
|
@@ -654,7 +654,7 @@ export class Broker {
|
||||||
|
if (!object(args)) fail('invalid-request');
|
||||||
|
this.#secretCheck(args);
|
||||||
|
return this.#store.transaction(() => {
|
||||||
|
- if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages'].includes(request.verb))
|
||||||
|
+ if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages', 'state'].includes(request.verb))
|
||||||
|
fail('read-only');
|
||||||
|
const result = this.#dispatch(s, request.verb, args);
|
||||||
|
this.#secretCheck(result);
|
||||||
|
@@ -1086,6 +1086,23 @@ export class Broker {
|
||||||
|
this.#agent(s);
|
||||||
|
return this.#requirements;
|
||||||
|
}
|
||||||
|
+ // Row 109 (#1603): what a run needs to know before it plans. The business's launch state with
|
||||||
|
+ // the time and id of the event that set it (`allowed`, no time and no id when none has), and
|
||||||
|
+ // the open tasks as the tracker snapshot shows them: refs and titles, at most `cap` of them,
|
||||||
|
+ // `total` counting every open task. A read for role and reader sessions; nothing is stored.
|
||||||
|
+ case 'state': {
|
||||||
|
+ keys(a, []);
|
||||||
|
+ if (!s.human && !s.reader) this.#agent(s);
|
||||||
|
+ const set = this.#store.get(
|
||||||
|
+ "SELECT id,kind,at FROM events WHERE business=? AND kind IN ('launch.revoked','launch.restored') ORDER BY seq DESC LIMIT 1",
|
||||||
|
+ s.business,
|
||||||
|
+ );
|
||||||
|
+ const open = this.#tasks(s.business, { done: false });
|
||||||
|
+ return {
|
||||||
|
+ launch: { state: set?.kind === 'launch.revoked' ? 'revoked' : 'allowed', at: set?.at ?? null, event: set?.id ?? null },
|
||||||
|
+ tasks: { open: open.tasks.map(({ task_ref, title }) => ({ task_ref, title })), total: open.total },
|
||||||
|
+ };
|
||||||
|
+ }
|
||||||
|
case 'inbox':
|
||||||
|
case 'agents':
|
||||||
|
case 'tasks':
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
Row 109 (#1603): the PM's persistent workspace, read-only, names/sizes/mtimes only (no contents read).
|
||||||
|
Checked 2026-10-13, before 22:54Z with ls and find -printf; nothing opened, nothing changed.
|
||||||
|
Path: <dataRoot>/workspaces/mosaic-stack/pm (dataRoot /home/jwoltje/.mosaic-dev)
|
||||||
|
|
||||||
|
Top level (2 entries):
|
||||||
|
f 7737 bytes 2026-10-12T14:53Z findings-2026-10-12.md <- planning notes from an earlier run
|
||||||
|
d 2026-10-13T16:48Z stack/ <- a git checkout of this repository (10356 entries in the workspace in all), not notes
|
||||||
|
|
||||||
|
Finding: findings-2026-10-12.md is the only planning-notes file. It predates the current launch state and task
|
||||||
|
status, so it is a likely second source of the stale picture a PM run planned from, next to old inbox
|
||||||
|
messages. The roles/pm.md rule covers it ("a message or a note of yours that disagrees with the block is out
|
||||||
|
of date"). The file is left alone; this row doesn't expire, move or edit workspace files.
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
diff --git a/packages/bus/README.md b/packages/bus/README.md
|
||||||
|
index 0cbbc8192..b0c469263 100644
|
||||||
|
--- a/packages/bus/README.md
|
||||||
|
+++ b/packages/bus/README.md
|
||||||
|
@@ -157,6 +157,7 @@ an operator inspect content and delivery evidence after an uncertain result.
|
||||||
|
| `decisions` | `{list}`: `roles` or `recent`; human and reader sessions only. Open decisions routed to a role, or the last 50 closed. |
|
||||||
|
| `credentials` | No args; human and reader sessions only. `[{service,account,role,dateKind,date,state}]` for the capability's business, never a token, file path or variable name. |
|
||||||
|
| `requirements` | No args; role sessions only, as `task.create` is (row 90, #1584). The approved PRD's requirement ids, `{prd,version,requirements:[{id,title}]}`, from `packages/tasks/requirements.json`, loaded when the broker starts; a bad file refuses the start. A human session refuses `agent-required`, a reader `read-only`. |
|
||||||
|
+| `state` | No args; role, human and reader sessions (row 109, #1603). `{launch:{state,at,event},tasks:{open:[{task_ref,title}],total,cap}}` for the capability's business. `launch` is the latest `launch.revoked` or `launch.restored` event: `allowed` with `at` and `event` null when none exists. `open` is the tracker snapshot's open tasks in `task_ref` order, at most `cap` (the `tasks` page size, 50); `total` counts every open task. The harness runner reads it before each turn. Writes nothing. |
|
||||||
|
| `replies` | `{id}`: a message UUID; human and reader sessions only. The messages whose `in_reply_to` is that id, oldest first, at most 50. Writes nothing. |
|
||||||
|
|
||||||
|
### Agent state (row 59, #1548)
|
||||||
|
diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs
|
||||||
|
index 732c7310e..7565e82da 100644
|
||||||
|
--- a/packages/bus/src/broker.mjs
|
||||||
|
+++ b/packages/bus/src/broker.mjs
|
||||||
|
@@ -654,7 +654,7 @@ export class Broker {
|
||||||
|
if (!object(args)) fail('invalid-request');
|
||||||
|
this.#secretCheck(args);
|
||||||
|
return this.#store.transaction(() => {
|
||||||
|
- if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages'].includes(request.verb))
|
||||||
|
+ if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages', 'state'].includes(request.verb))
|
||||||
|
fail('read-only');
|
||||||
|
const result = this.#dispatch(s, request.verb, args);
|
||||||
|
this.#secretCheck(result);
|
||||||
|
@@ -1086,6 +1086,23 @@ export class Broker {
|
||||||
|
this.#agent(s);
|
||||||
|
return this.#requirements;
|
||||||
|
}
|
||||||
|
+ // Row 109 (#1603): what a run needs to know before it plans. The business's launch state with
|
||||||
|
+ // the time and id of the event that set it (`allowed`, no time and no id when none has), and
|
||||||
|
+ // the open tasks as the tracker snapshot shows them: refs and titles, at most `cap` of them,
|
||||||
|
+ // `total` counting every open task. A read for role and reader sessions; nothing is stored.
|
||||||
|
+ case 'state': {
|
||||||
|
+ keys(a, []);
|
||||||
|
+ if (!s.human && !s.reader) this.#agent(s);
|
||||||
|
+ const set = this.#store.get(
|
||||||
|
+ "SELECT id,kind,at FROM events WHERE business=? AND kind IN ('launch.revoked','launch.restored') ORDER BY seq DESC LIMIT 1",
|
||||||
|
+ s.business,
|
||||||
|
+ );
|
||||||
|
+ const open = this.#tasks(s.business, { done: false });
|
||||||
|
+ return {
|
||||||
|
+ launch: { state: set?.kind === 'launch.revoked' ? 'revoked' : 'allowed', at: set?.at ?? null, event: set?.id ?? null },
|
||||||
|
+ tasks: { open: open.tasks.map(({ task_ref, title }) => ({ task_ref, title })), total: open.total, cap: TASK_PAGE },
|
||||||
|
+ };
|
||||||
|
+ }
|
||||||
|
case 'inbox':
|
||||||
|
case 'agents':
|
||||||
|
case 'tasks':
|
||||||
|
diff --git a/packages/bus/tests/state.test.mjs b/packages/bus/tests/state.test.mjs
|
||||||
|
new file mode 100644
|
||||||
|
index 000000000..3a14c7ae9
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/packages/bus/tests/state.test.mjs
|
||||||
|
@@ -0,0 +1,89 @@
|
||||||
|
+import test from 'node:test';
|
||||||
|
+import assert from 'node:assert/strict';
|
||||||
|
+import { mkdtempSync, rmSync } from 'node:fs';
|
||||||
|
+import { tmpdir } from 'node:os';
|
||||||
|
+import { join } from 'node:path';
|
||||||
|
+import { Store } from '../src/store.mjs';
|
||||||
|
+import { Broker, TASK_PAGE } from '../src/broker.mjs';
|
||||||
|
+// Row 109 (#1603): `state` is what a run reads before it plans: the launch state with the time and
|
||||||
|
+// id of the event that set it, and the open tasks from the tracker snapshot, capped.
|
||||||
|
+const authority = { withinRole: ['message.send', 'role.launch'], crossRole: [] };
|
||||||
|
+const roles = { pm: { authority }, coder: { authority } };
|
||||||
|
+const businesses = {
|
||||||
|
+ demo: { id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'pm' }, launch: { by: 'pm', instances: ['coder'], max: { opus: 2, sonnet: 2 } }, roles },
|
||||||
|
+};
|
||||||
|
+const at = '2026-10-12T12:00:00.000Z';
|
||||||
|
+const digest = 'd'.repeat(64);
|
||||||
|
+const ref = (i) => `vikunja:3/${i}`;
|
||||||
|
+const fields = (i, done = false) => ({ project_id: 3, title: `Task ${i}`, description: `<p>${i}</p>`, done, bucket: 11 });
|
||||||
|
+
|
||||||
|
+function setup(t) {
|
||||||
|
+ const root = mkdtempSync(join(tmpdir(), 'bus-state-'));
|
||||||
|
+ const store = new Store(root);
|
||||||
|
+ const b = new Broker({ store, businesses });
|
||||||
|
+ t.after(() => {
|
||||||
|
+ store.close();
|
||||||
|
+ rmSync(root, { recursive: true, force: true });
|
||||||
|
+ });
|
||||||
|
+ const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
||||||
|
+ const pm = b.bindLaunch({ business: 'demo', role: 'pm', run: 'demo-pm', harness: 'claude-code' });
|
||||||
|
+ b.request(pm, { verb: 'role.claim' });
|
||||||
|
+ const snap = (i, over = {}, read_at = at) => ({ task_ref: ref(i), updated: read_at, digest, fields: { ...fields(i), ...over }, via: 'board', read_at });
|
||||||
|
+ const record = (...snapshots) => b.recordTask({ business: 'demo', snapshots });
|
||||||
|
+ const state = (cap = pm, args = {}) => b.request(cap, { verb: 'state', args });
|
||||||
|
+ return { b, store, human, pm, snap, record, state, reader: b.bindReader({ business: 'demo' }) };
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+test('a business that never switched launches reads allowed with no time and no event', (t) => {
|
||||||
|
+ const { state } = setup(t);
|
||||||
|
+ assert.deepEqual(state().launch, { state: 'allowed', at: null, event: null });
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
+test('launches revoked and then restored read allowed, with the restore time and event id', (t) => {
|
||||||
|
+ const { b, store, human, pm, state } = setup(t);
|
||||||
|
+ const revoked = b.request(human, { verb: 'launch.revoke' }).id;
|
||||||
|
+ assert.deepEqual(
|
||||||
|
+ { state: state().launch.state, event: state().launch.event },
|
||||||
|
+ { state: 'revoked', event: revoked },
|
||||||
|
+ );
|
||||||
|
+ const restored = b.request(human, { verb: 'launch.restore' }).id;
|
||||||
|
+ const row = store.get('SELECT at FROM events WHERE id=?', restored);
|
||||||
|
+ assert.deepEqual(state(pm).launch, { state: 'allowed', at: row.at, event: restored });
|
||||||
|
+ assert.notEqual(restored, revoked);
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
+test('a task closed on the tracker leaves the open list, and total counts the open ones', (t) => {
|
||||||
|
+ const { snap, record, state } = setup(t);
|
||||||
|
+ record(snap(1), snap(2), snap(3));
|
||||||
|
+ assert.deepEqual(state().tasks, {
|
||||||
|
+ open: [1, 2, 3].map((i) => ({ task_ref: ref(i), title: `Task ${i}` })),
|
||||||
|
+ total: 3,
|
||||||
|
+ cap: TASK_PAGE,
|
||||||
|
+ });
|
||||||
|
+ record(snap(2, { done: true }, '2026-10-12T13:00:00.000Z'));
|
||||||
|
+ const { open, total } = state().tasks;
|
||||||
|
+ assert.deepEqual(open.map((x) => x.task_ref), [ref(1), ref(3)]);
|
||||||
|
+ assert.equal(total, 2);
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
+test('a tombstone is not open', (t) => {
|
||||||
|
+ const { snap, record, state } = setup(t);
|
||||||
|
+ record(snap(1), { ...snap(2), fields: { gone: 'no-access' }, via: 'task' });
|
||||||
|
+ assert.deepEqual(state().tasks.open.map((x) => x.task_ref), [ref(1)]);
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
+test('the open list stops at the cap and says so; total still counts every open task', (t) => {
|
||||||
|
+ const { snap, record, state } = setup(t);
|
||||||
|
+ record(...Array.from({ length: TASK_PAGE + 5 }, (_, k) => snap(k + 1)));
|
||||||
|
+ const { open, total, cap } = state().tasks;
|
||||||
|
+ assert.equal(open.length, TASK_PAGE);
|
||||||
|
+ assert.equal(total, TASK_PAGE + 5);
|
||||||
|
+ assert.equal(cap, TASK_PAGE);
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
+test('a role and a reader session read state; arguments, and an unbound cap, refuse', (t) => {
|
||||||
|
+ const { reader, pm, state } = setup(t);
|
||||||
|
+ assert.deepEqual(state(reader), state(pm));
|
||||||
|
+ assert.throws(() => state(pm, { business: 'other' }), { code: 'invalid-request' });
|
||||||
|
+ assert.throws(() => state('f'.repeat(64)), /unauthenticated/);
|
||||||
|
+});
|
||||||
|
diff --git a/packages/harness/README.md b/packages/harness/README.md
|
||||||
|
index 6f87ac731..4d2d6fb96 100644
|
||||||
|
--- a/packages/harness/README.md
|
||||||
|
+++ b/packages/harness/README.md
|
||||||
|
@@ -182,6 +182,15 @@ it never touches the disk and isn't in the environment or argv.
|
||||||
|
Its stdout goes back to the sender as a RESULT; a failed turn replies
|
||||||
|
`The turn failed: <reason>`; an incoming RESULT never gets an automatic
|
||||||
|
reply. Then `message.read`.
|
||||||
|
+
|
||||||
|
+ Row 109 (#1603): before each turn the runner calls the broker's `state`
|
||||||
|
+ verb and puts a block at the top of the request, headed `Current state,
|
||||||
|
+ read at <time>`: the launch state with the time and id of the event that
|
||||||
|
+ set it, and the open tasks (refs and titles, at most the stated cap, with
|
||||||
|
+ the total). The block is the truth over any older message. The message
|
||||||
|
+ header after it names when the message was sent. A failed read fails the
|
||||||
|
+ turn without running the adapter: the sender gets `The turn failed: could
|
||||||
|
+ not read the current state: <code>`, and the message is read.
|
||||||
|
5. SIGTERM or SIGINT kills the turn's process group, releases the role and
|
||||||
|
exits 0. The handlers go in before anything else, because pid 1 of a
|
||||||
|
PID namespace never sees a signal it has no handler for; a stop before
|
||||||
|
diff --git a/packages/harness/src/runner.mjs b/packages/harness/src/runner.mjs
|
||||||
|
index 00595542b..f806cabed 100644
|
||||||
|
--- a/packages/harness/src/runner.mjs
|
||||||
|
+++ b/packages/harness/src/runner.mjs
|
||||||
|
@@ -62,7 +62,10 @@ export function founderCheck(policy, env) {
|
||||||
|
|
||||||
|
// The request text one message becomes.
|
||||||
|
export function turnRequest(m) {
|
||||||
|
- const head = [`Message ${m.id} from ${m.from_role}`, `class ${m.class}`];
|
||||||
|
+ const head = [`Message ${m.id} from ${m.from_role}`];
|
||||||
|
+ // Row 109 (#1603): when the sender sent it, so a message that waited reads as old.
|
||||||
|
+ if (m.at) head.push(`sent ${m.at}`);
|
||||||
|
+ head.push(`class ${m.class}`);
|
||||||
|
if (m.in_reply_to) head.push(`in reply to ${m.in_reply_to}`);
|
||||||
|
if (m.decision) head.push(`citing decision ${m.decision}`);
|
||||||
|
// Row 90 (#1584): the human input id task_create cites as its request.
|
||||||
|
@@ -70,6 +73,22 @@ export function turnRequest(m) {
|
||||||
|
return `${head.join(", ")}:\n\n${m.body}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
+// Row 109 (#1603): the block at the top of a turn's request. It is what the broker said at `readAt`,
|
||||||
|
+// the truth for launch state and task status over any older message.
|
||||||
|
+export function stateBlock(state, readAt) {
|
||||||
|
+ const { launch, tasks } = state;
|
||||||
|
+ const since = launch.at ? ` since ${launch.at} (event ${launch.event})` : " (never switched)";
|
||||||
|
+ const shown = tasks.open.length < tasks.total ? `, the first ${tasks.open.length} of cap ${tasks.cap}` : "";
|
||||||
|
+ const open = tasks.open.map((t) => `- ${t.task_ref} ${t.title ?? ""}`.trimEnd());
|
||||||
|
+ return [
|
||||||
|
+ `Current state, read at ${readAt}:`,
|
||||||
|
+ `Launches: ${launch.state}${since}.`,
|
||||||
|
+ `Open tasks: ${tasks.total}${shown}.`,
|
||||||
|
+ ...open,
|
||||||
|
+ "",
|
||||||
|
+ ].join("\n");
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
function clip(text) {
|
||||||
|
return text.length > BODY_MAX ? text.slice(0, BODY_MAX) + "\n[answer cut at 32000 characters]" : text;
|
||||||
|
}
|
||||||
|
@@ -339,7 +358,23 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
|
||||||
|
}
|
||||||
|
for (const m of messages) {
|
||||||
|
if (stopping) break loop;
|
||||||
|
- const turn = await runTurn({ session, request: turnRequest(m), n: ++n, env: turnEnv, onChild: (c) => (child = c) });
|
||||||
|
+ // Row 109 (#1603): read the state now, not when the message was sent. A failed read fails the turn.
|
||||||
|
+ let block = null;
|
||||||
|
+ let unread = null;
|
||||||
|
+ try {
|
||||||
|
+ block = stateBlock(await client.call("state"), new Date().toISOString());
|
||||||
|
+ } catch (e) {
|
||||||
|
+ if (SESSION_GONE.has(e.code)) {
|
||||||
|
+ log(`runner: session stopped working: ${e.code}`);
|
||||||
|
+ code = EXIT.session;
|
||||||
|
+ break loop;
|
||||||
|
+ }
|
||||||
|
+ unread = `could not read the current state: ${e.code ?? e.message}`;
|
||||||
|
+ }
|
||||||
|
+ ++n;
|
||||||
|
+ const turn = unread
|
||||||
|
+ ? { ok: false, reason: unread }
|
||||||
|
+ : await runTurn({ session, request: `${block}\n${turnRequest(m)}`, n, env: turnEnv, onChild: (c) => (child = c) });
|
||||||
|
if (stopping) break loop;
|
||||||
|
if (!turn.ok) log(`runner: turn ${n} for message ${m.id} failed: ${turn.reason}`);
|
||||||
|
try {
|
||||||
|
diff --git a/packages/harness/tests/fixtures/fake-adapter.mjs b/packages/harness/tests/fixtures/fake-adapter.mjs
|
||||||
|
index 4024172d1..4e1ec9bd3 100644
|
||||||
|
--- a/packages/harness/tests/fixtures/fake-adapter.mjs
|
||||||
|
+++ b/packages/harness/tests/fixtures/fake-adapter.mjs
|
||||||
|
@@ -11,7 +11,8 @@ import { appendFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { callTool } from "../../src/tools.mjs";
|
||||||
|
|
||||||
|
const request = process.env.MOSAIC_REQUEST ?? "";
|
||||||
|
-const body = request.split("\n\n").slice(1).join("\n\n");
|
||||||
|
+// The message body follows the "Message ...:" header line; a state block (row 109) may sit above it.
|
||||||
|
+const body = request.match(/^Message [^\n]*:\n\n([\s\S]*)$/m)?.[1] ?? request.split("\n\n").slice(1).join("\n\n");
|
||||||
|
if (process.env.FAKE_PIDS) appendFileSync(process.env.FAKE_PIDS, `${process.pid}\n`);
|
||||||
|
process.stderr.write(`fake adapter: ${process.env.MOSAIC_MODEL}\n`);
|
||||||
|
const done = () => writeFileSync(process.env.MOSAIC_TURN_MARKER, "done\n");
|
||||||
|
diff --git a/packages/harness/tests/runner.test.mjs b/packages/harness/tests/runner.test.mjs
|
||||||
|
index 777d16148..12b683cb7 100644
|
||||||
|
--- a/packages/harness/tests/runner.test.mjs
|
||||||
|
+++ b/packages/harness/tests/runner.test.mjs
|
||||||
|
@@ -11,9 +11,9 @@ import { open } from "node:fs/promises";
|
||||||
|
import { createServer } from "node:net";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { Store } from "../../bus/src/store.mjs";
|
||||||
|
-import { Broker } from "../../bus/src/broker.mjs";
|
||||||
|
+import { Broker, BusError } from "../../bus/src/broker.mjs";
|
||||||
|
import { serve } from "../../bus/src/server.mjs";
|
||||||
|
-import { EXIT, founderCheck, turnRequest } from "../src/runner.mjs";
|
||||||
|
+import { EXIT, founderCheck, stateBlock, turnRequest } from "../src/runner.mjs";
|
||||||
|
import { scratch } from "./helpers.mjs";
|
||||||
|
|
||||||
|
const RUNNER = new URL("../src/runner.mjs", import.meta.url).pathname;
|
||||||
|
@@ -56,6 +56,25 @@ test("turnRequest names the sender, class, reply and decision", () => {
|
||||||
|
assert.equal(turnRequest({ id: "m4", from_role: "cto", class: "RESULT", request: null, body: "b" }), "Message m4 from cto, class RESULT:\n\nb");
|
||||||
|
});
|
||||||
|
|
||||||
|
+// Row 109 (#1603): the header carries when the message was sent, not when it is read.
|
||||||
|
+test("turnRequest shows the message's own sent time, a day old as well as new", () => {
|
||||||
|
+ const sent = "2026-10-12T09:30:00.000Z";
|
||||||
|
+ assert.equal(
|
||||||
|
+ turnRequest({ id: "m5", from_role: "jason", class: "REQUEST", at: sent, body: "resume" }),
|
||||||
|
+ `Message m5 from jason, sent ${sent}, class REQUEST:\n\nresume`,
|
||||||
|
+ );
|
||||||
|
+ assert.equal(turnRequest({ id: "m6", from_role: "cto", class: "RESULT", at: sent, in_reply_to: "m5", body: "b" }), `Message m6 from cto, sent ${sent}, class RESULT, in reply to m5:\n\nb`);
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
+test("stateBlock names the launch state with its time and event, and the open tasks with the cap", () => {
|
||||||
|
+ const read = "2026-10-13T10:00:00.000Z";
|
||||||
|
+ const restored = stateBlock({ launch: { state: "allowed", at: "2026-10-12T20:00:00.000Z", event: "ev-9" }, tasks: { open: [{ task_ref: "vikunja:3/7", title: "Seven" }], total: 1, cap: 50 } }, read);
|
||||||
|
+ assert.equal(restored, "Current state, read at 2026-10-13T10:00:00.000Z:\nLaunches: allowed since 2026-10-12T20:00:00.000Z (event ev-9).\nOpen tasks: 1.\n- vikunja:3/7 Seven\n");
|
||||||
|
+ const capped = stateBlock({ launch: { state: "allowed", at: null, event: null }, tasks: { open: [{ task_ref: "vikunja:3/1", title: "One" }], total: 9, cap: 1 } }, read);
|
||||||
|
+ assert.match(capped, /Launches: allowed \(never switched\)\./);
|
||||||
|
+ assert.match(capped, /Open tasks: 9, the first 1 of cap 1\./);
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
async function setup(t, { session = {}, policy = {}, tools } = {}) {
|
||||||
|
const dir = scratch(t);
|
||||||
|
const store = new Store(dir);
|
||||||
|
@@ -113,7 +132,8 @@ async function setup(t, { session = {}, policy = {}, tools } = {}) {
|
||||||
|
const pm = broker.bindLaunch({ business: "demo", role: "pm", run: "pm-run", harness: "pi" });
|
||||||
|
const call = (c, verb, args = {}) => broker.request(c, { verb, args });
|
||||||
|
call(pm, "role.claim");
|
||||||
|
- return { dir, runDir, store, broker, server, cap, pm, call, launches, pids: join(dir, "pids") };
|
||||||
|
+ const human = broker.bindHuman({ business: "demo", human: "jason", via: "cli", outsideAgent: true });
|
||||||
|
+ return { dir, runDir, store, broker, server, cap, pm, human, call, launches, pids: join(dir, "pids") };
|
||||||
|
}
|
||||||
|
|
||||||
|
// A runner that hasn't exited after a minute is killed, so a test that
|
||||||
|
@@ -162,7 +182,8 @@ test("a message becomes a turn, the answer goes back as a RESULT, SIGTERM releas
|
||||||
|
assert.equal(reply.from_role, "coder");
|
||||||
|
assert.equal(reply.from_run, "coder-run");
|
||||||
|
assert.equal(reply.in_reply_to, sent.id);
|
||||||
|
- assert.equal(reply.body, `echo: Message ${sent.id} from pm, class REQUEST:\n\nhello`);
|
||||||
|
+ const sentAt = ctx.store.get("SELECT at FROM messages WHERE id=?", sent.id).at;
|
||||||
|
+ assert.match(reply.body, new RegExp(`^echo: Current state, read at \\S+:\nLaunches: allowed \\(never switched\\)\\.\nOpen tasks: 0\\.\n\nMessage ${sent.id} from pm, sent ${sentAt}, class REQUEST:\n\nhello$`));
|
||||||
|
await until(() => ctx.store.get("SELECT 1 FROM deliveries WHERE message=? AND op='read'", sent.id));
|
||||||
|
const err = join(ctx.runDir, "turns", "0001.stderr");
|
||||||
|
assert.equal(statSync(err).mode & 0o777, 0o600);
|
||||||
|
@@ -176,6 +197,85 @@ test("a message becomes a turn, the answer goes back as a RESULT, SIGTERM releas
|
||||||
|
assert.ok(!existsSync(join(ctx.runDir, "tools.sock")));
|
||||||
|
});
|
||||||
|
|
||||||
|
+// Row 109 (#1603): each turn reads the state when it starts. The fake adapter echoes the request,
|
||||||
|
+// so a reply shows the block the turn was given.
|
||||||
|
+const ask = async (ctx, body = "what now") => {
|
||||||
|
+ const before = ctx.inbox.length;
|
||||||
|
+ ctx.call(ctx.pm, "message.send", { to: "coder", body, class: "REQUEST" });
|
||||||
|
+ await until(() => replies(ctx)() && ctx.inbox.length > before);
|
||||||
|
+ return ctx.inbox.at(-1);
|
||||||
|
+};
|
||||||
|
+const snapshot = (i, over = {}, read = "2026-10-12T12:00:00.000Z") => ({
|
||||||
|
+ task_ref: `vikunja:3/${i}`,
|
||||||
|
+ updated: read,
|
||||||
|
+ digest: "d".repeat(64),
|
||||||
|
+ fields: { project_id: 3, title: `Task ${i}`, description: "", done: false, bucket: 11, ...over },
|
||||||
|
+ via: "board",
|
||||||
|
+ read_at: read,
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
+test("launches revoked and then restored: the turn's block reads allowed and gives the restore time", async (t) => {
|
||||||
|
+ const ctx = await setup(t);
|
||||||
|
+ ctx.inbox = [];
|
||||||
|
+ const r = startRunner(ctx.runDir, ctx.cap);
|
||||||
|
+ t.after(() => r.child.kill("SIGKILL"));
|
||||||
|
+ ctx.call(ctx.human, "launch.revoke");
|
||||||
|
+ assert.match((await ask(ctx)).body, /\nLaunches: revoked since \S+ \(event \S+\)\.\n/);
|
||||||
|
+ ctx.call(ctx.human, "launch.restore");
|
||||||
|
+ const restored = ctx.store.get("SELECT id,at FROM events WHERE kind='launch.restored'");
|
||||||
|
+ assert.ok((await ask(ctx)).body.includes(`\nLaunches: allowed since ${restored.at} (event ${restored.id}).\n`));
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
+test("a task closed on the tracker is missing from the next turn's open list", async (t) => {
|
||||||
|
+ const ctx = await setup(t);
|
||||||
|
+ ctx.inbox = [];
|
||||||
|
+ ctx.broker.recordTask({ business: "demo", snapshots: [snapshot(1), snapshot(2)] });
|
||||||
|
+ const r = startRunner(ctx.runDir, ctx.cap);
|
||||||
|
+ t.after(() => r.child.kill("SIGKILL"));
|
||||||
|
+ const first = (await ask(ctx)).body;
|
||||||
|
+ assert.match(first, /\nOpen tasks: 2\.\n- vikunja:3\/1 Task 1\n- vikunja:3\/2 Task 2\n/);
|
||||||
|
+ ctx.broker.recordTask({ business: "demo", snapshots: [snapshot(2, { done: true }, "2026-10-12T13:00:00.000Z")] });
|
||||||
|
+ const second = (await ask(ctx)).body;
|
||||||
|
+ assert.match(second, /\nOpen tasks: 1\.\n- vikunja:3\/1 Task 1\n\nMessage /);
|
||||||
|
+ assert.ok(!second.includes("vikunja:3/2"));
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
+test("a message read a day after it was sent shows the time it was sent", async (t) => {
|
||||||
|
+ const ctx = await setup(t);
|
||||||
|
+ ctx.inbox = [];
|
||||||
|
+ const sent = ctx.call(ctx.pm, "message.send", { to: "coder", body: "resume the launch", class: "REQUEST" });
|
||||||
|
+ const sentAt = ctx.store.get("SELECT at FROM messages WHERE id=?", sent.id).at;
|
||||||
|
+ // The broker's clock jumps a day before the runner reads: the header keeps the original time.
|
||||||
|
+ const now = Date.now;
|
||||||
|
+ Date.now = () => now() + 86_400_000;
|
||||||
|
+ t.after(() => (Date.now = now));
|
||||||
|
+ const r = startRunner(ctx.runDir, ctx.cap);
|
||||||
|
+ t.after(() => r.child.kill("SIGKILL"));
|
||||||
|
+ const [reply] = await until(replies(ctx));
|
||||||
|
+ assert.ok(reply.body.includes(`Message ${sent.id} from pm, sent ${sentAt}, class REQUEST:`));
|
||||||
|
+ assert.ok(Date.parse(reply.at) - Date.parse(sentAt) >= 86_000_000);
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
+test("a failed state read fails the turn: no adapter runs, the sender hears why, the message is read", async (t) => {
|
||||||
|
+ const ctx = await setup(t);
|
||||||
|
+ ctx.inbox = [];
|
||||||
|
+ const request = ctx.broker.request.bind(ctx.broker);
|
||||||
|
+ ctx.broker.request = (cap, req) => {
|
||||||
|
+ if (req.verb === "state") throw new BusError("state-unavailable");
|
||||||
|
+ return request(cap, req);
|
||||||
|
+ };
|
||||||
|
+ const r = startRunner(ctx.runDir, ctx.cap);
|
||||||
|
+ t.after(() => r.child.kill("SIGKILL"));
|
||||||
|
+ const sent = ctx.call(ctx.pm, "message.send", { to: "coder", body: "plan the week", class: "REQUEST" });
|
||||||
|
+ const [reply] = await until(replies(ctx));
|
||||||
|
+ assert.equal(reply.class, "RESULT");
|
||||||
|
+ assert.equal(reply.in_reply_to, sent.id);
|
||||||
|
+ assert.equal(reply.body, "The turn failed: could not read the current state: state-unavailable");
|
||||||
|
+ await until(() => ctx.store.get("SELECT 1 FROM deliveries WHERE message=? AND op='read'", sent.id));
|
||||||
|
+ assert.ok(!existsSync(join(ctx.runDir, "turns", "0001.stderr")));
|
||||||
|
+ assert.match(r.stderr(), /turn 1 for message \S+ failed: could not read the current state: state-unavailable/);
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
test("a SIGTERM before the claim stops the runner with exit 0 and no claim", async (t) => {
|
||||||
|
const ctx = await setup(t);
|
||||||
|
// session.json becomes a FIFO: main opens it right after its handlers go
|
||||||
|
diff --git a/roles/pm.md b/roles/pm.md
|
||||||
|
index 522ac9c0e..8ef433f58 100644
|
||||||
|
--- a/roles/pm.md
|
||||||
|
+++ b/roles/pm.md
|
||||||
|
@@ -53,5 +53,10 @@ cross-role. Raise it as a decision for the technical arbiter.
|
||||||
|
|
||||||
|
- You act on the tracker and on Gitea only through the typed tools the
|
||||||
|
broker gives you. You never hold a token.
|
||||||
|
+- Each turn opens with a "Current state" block, read when the turn
|
||||||
|
+ started. It is the truth for launch state and task status. A message
|
||||||
|
+ or a note of yours that disagrees with it is out of date. Before you
|
||||||
|
+ plan to resume or launch work on a task, check the task with
|
||||||
|
+ `get_task`.
|
||||||
|
- Send messages through the bus, addressed to a role, not a person.
|
||||||
|
- Report a blocker as soon as you find it, with what you tried.
|
||||||
Executable
+17
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Row 109 (#1603): the gate suites, run in the given worktree with every output teed to <outdir>.
|
||||||
|
# Usage: suites.sh <worktree> <outdir>
|
||||||
|
set -u -o pipefail
|
||||||
|
WT=$(cd "$1" && pwd); OUT=$(mkdir -p "$2" && cd "$2" && pwd)
|
||||||
|
cd "$WT" || exit 2
|
||||||
|
node_suite() { # name dir
|
||||||
|
setsid -w timeout -k 10 900 node --test "$2"/tests/*.test.mjs 2>&1 | tee "$OUT/$1-node-suite.out" >/dev/null
|
||||||
|
echo "$1 node suite: rc=${PIPESTATUS[0]} $(grep -E '^ℹ (tests|pass|fail) ' "$OUT/$1-node-suite.out" | tr '\n' ' ')"
|
||||||
|
}
|
||||||
|
script() { # name
|
||||||
|
setsid -w timeout -k 10 1200 bash "scripts/$1.sh" 2>&1 | tee "$OUT/$1.out" >/dev/null
|
||||||
|
echo "$1: rc=${PIPESTATUS[0]} $(grep -iE 'passed|failed' "$OUT/$1.out" | tail -1)"
|
||||||
|
}
|
||||||
|
for d in bus harness; do node_suite $d packages/$d; done
|
||||||
|
for s in test-auth test-conductor test-config test-discord test-extension-package test-foundation test-queue test-release test-task; do script $s; done
|
||||||
|
echo done
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
✔ agents: a held role with nothing on the bus is idle; a role session gets the claim rows unchanged (28.297351ms)
|
||||||
|
✔ agents: working counts what the holder received and has not read; routed alone is not working (26.151014ms)
|
||||||
|
✔ agents: input needed is the role's open decisions routed to the human, gated first, then oldest (50.067427ms)
|
||||||
|
✔ agents: working wins over waiting, and Input needed still shows (22.161722ms)
|
||||||
|
✔ agents: a new holder doesn't inherit what the last one left unread; decisions stay with the role (31.653342ms)
|
||||||
|
✔ agents: state reads only its own business, even for a run name both use (33.064525ms)
|
||||||
|
✔ agents: a message the holder released unread is stranded; the next holder gets nothing and cannot read it (36.106172ms)
|
||||||
|
✔ agents: a message the holder read is not stranded; one the current holder has not read is not listed (30.478949ms)
|
||||||
|
✔ agents: stranded is oldest first and capped at 50 for a role (123.330395ms)
|
||||||
|
✔ agents: stranded is per role and per business (35.147374ms)
|
||||||
|
✔ agents: a message routed to a held role and not yet received is not stranded (14.397001ms)
|
||||||
|
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (29.665485ms)
|
||||||
|
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (32.302635ms)
|
||||||
|
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (36.864032ms)
|
||||||
|
✔ launch events require a human CLI capability; generic emit cannot forge authority events (22.624026ms)
|
||||||
|
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (26.418409ms)
|
||||||
|
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (25.730416ms)
|
||||||
|
✔ task action subjects and linked decision trail are complete and ordered (42.04217ms)
|
||||||
|
✔ launch binding is durable and reconnecting requires the identical trusted record (19.549861ms)
|
||||||
|
✔ business isolation includes inherited object names and cross-business message references (30.891376ms)
|
||||||
|
✔ authority never transfers between action, run, target, unresolved or replaced role holder (36.279429ms)
|
||||||
|
✔ task projection uses schema current view, skipping earlier and equal-start polls (23.916643ms)
|
||||||
|
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (33.145318ms)
|
||||||
|
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (17.342213ms)
|
||||||
|
✔ both arbiters require human resolution when their cross-role route is themselves (26.232801ms)
|
||||||
|
✔ the shipped coder role raises task.reassign as gated to the human; task.scope.change stays cross-role (20.62886ms)
|
||||||
|
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.778225ms)
|
||||||
|
✔ credentials gives human and reader sessions service, account, role, date and state, and nothing else (23.765412ms)
|
||||||
|
✔ credentials refuses a role session and any argument (20.404226ms)
|
||||||
|
✔ credentials reads the status on each request, and a broker without one lists none (24.326082ms)
|
||||||
|
✔ the broker process answers credentials without a token, path or variable in its output, log or store (93.242027ms)
|
||||||
|
✔ only validated broker references load; returned data and exceptions cannot expose a known token (7.033452ms)
|
||||||
|
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (3.013949ms)
|
||||||
|
✔ expiry refuses use and env references never become client data (1.544583ms)
|
||||||
|
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (2.277426ms)
|
||||||
|
✔ opaque tokens shorter than 16 characters refuse before use (0.764888ms)
|
||||||
|
✔ roles lists open decisions routed to a role, and never the human inbox (39.554268ms)
|
||||||
|
✔ recent lists closed decisions of every route, newest close first, with how each closed (37.199033ms)
|
||||||
|
✔ recent gives the closing event as closed when the decision was seen first (40.823498ms)
|
||||||
|
✔ recent stops at RECENT rows (146.209586ms)
|
||||||
|
✔ every row is what trail already gives the same session for that id (50.272751ms)
|
||||||
|
✔ a role session can't list decisions, so it can't read another role's open decisions (53.874347ms)
|
||||||
|
✔ decisions is a read: one business, strict arguments, nothing written (44.63042ms)
|
||||||
|
✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (24.633583ms)
|
||||||
|
✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (15.749064ms)
|
||||||
|
✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (16.279704ms)
|
||||||
|
✔ endLaunch leaves a claim another run took alone (23.202804ms)
|
||||||
|
✔ refuse records action.refused against the caller with the code only (18.116231ms)
|
||||||
|
✔ launches off refuses role.launch with launch-revoked until launches on (21.321294ms)
|
||||||
|
✔ broker process: launch ops authorize role.launch, record refusals and end runs (133.544824ms)
|
||||||
|
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.828563ms)
|
||||||
|
✔ process reader gets own kernel identity without exposing environment values (0.926592ms)
|
||||||
|
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.124436ms)
|
||||||
|
✔ real pid 1 remains inspectable when its environment is protected (0.65044ms)
|
||||||
|
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (32.979603ms)
|
||||||
|
✔ missing and foreign-business citations refuse and roll back message and grant (20.474351ms)
|
||||||
|
✔ cross-role sends still need a matching resolved decision and consume it once (34.541326ms)
|
||||||
|
✔ messages pages all human messages newest first without writes; read state is history (444.138897ms)
|
||||||
|
✔ messages refuses role callers, invalid paging and cursors outside this human feed (20.427029ms)
|
||||||
|
✔ views messages passes only its paging arguments through (0.347001ms)
|
||||||
|
✔ messages isolates businesses even with a valid foreign cursor (13.183145ms)
|
||||||
|
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (102.394931ms)
|
||||||
|
✔ startup token refusal returns safe code without value or partial listening broker (63.271273ms)
|
||||||
|
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (98.670383ms)
|
||||||
|
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (137.631589ms)
|
||||||
|
✔ trusted host registers later launches; socket clients never have a registration verb (74.175173ms)
|
||||||
|
✔ runtime excludes declared project roots even when host supplies no repoRoots (41.416891ms)
|
||||||
|
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (60.256132ms)
|
||||||
|
✔ a reply is listed for human and reader sessions, and its delivery rows are unchanged (49.259344ms)
|
||||||
|
✔ the reply still reaches the human message.receive (mosaic talk) after the Console showed it (25.624504ms)
|
||||||
|
✔ a message with no reply gives an empty list (20.016322ms)
|
||||||
|
✔ replies come oldest first and stop at REPLIES (120.444271ms)
|
||||||
|
✔ an id that names no message in the business refuses message-not-found, and the refusal is recorded (44.587049ms)
|
||||||
|
✔ strict arguments; a role session gets human-required (44.089256ms)
|
||||||
|
✔ views passes replies through as one call with the id (2.541981ms)
|
||||||
|
✔ requirements returns the committed list to a holding role session, and no one else (32.077319ms)
|
||||||
|
✔ the PM's prd.approve with no task, when no id fits, reaches the human's Inbox (22.074738ms)
|
||||||
|
✔ the PM's non-blocking role.revoke with no task reaches the human's Inbox (20.246744ms)
|
||||||
|
✔ a push is recorded as the action.allowed for git.push.working on the task, and only the host records one (36.151608ms)
|
||||||
|
✔ review.request names the latest pushed commit and sends the reviewer a REVIEW-REQUEST (30.682906ms)
|
||||||
|
✔ review.request says which run holds the reviewer role, or null, and a null request waits for a claim (40.310639ms)
|
||||||
|
✔ review.request refuses a commit that isn't the latest push, a non-reviewer and a role without the action (57.238224ms)
|
||||||
|
✔ reviewFetch answers only the addressed reviewer, for the request it cites (41.290709ms)
|
||||||
|
✔ review.verdict answers one request once, to the role that asked (46.002266ms)
|
||||||
|
✔ task.close on a pushed task needs an approve verdict for the latest push (55.690651ms)
|
||||||
|
✔ a refusal the host decided is filed on the task it names, and only a task ref counts (26.625638ms)
|
||||||
|
✔ v3b prototype refusals, views and append-only mutations (107.680716ms)
|
||||||
|
✔ gated approval authorizes once, survives store reopen, and fresh approval works (41.650704ms)
|
||||||
|
✔ another run cannot consume an approval; a failed check leaves it usable (32.015671ms)
|
||||||
|
✔ two scheduled callers have exactly one grant and one consumed refusal (26.932983ms)
|
||||||
|
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (51.60255ms)
|
||||||
|
✔ class drift gated to cross-role refuses before consumption (36.98624ms)
|
||||||
|
✔ class drift cross-role to gated refuses before consumption (31.240912ms)
|
||||||
|
✔ class drift gated to within-role refuses before consumption (34.31432ms)
|
||||||
|
✔ class drift cross-role to within-role refuses before consumption (28.552349ms)
|
||||||
|
✔ class drift within-role to gated refuses before consumption (24.790443ms)
|
||||||
|
✔ class drift within-role to cross-role refuses before consumption (20.366658ms)
|
||||||
|
✔ message.send consumes approval and prevents a later send or authorize (23.511105ms)
|
||||||
|
✔ role.revoke consumes approval and prevents a later revoke or authorize (22.585085ms)
|
||||||
|
✔ a business that never switched launches reads allowed with no time and no event (21.965042ms)
|
||||||
|
✔ launches revoked and then restored read allowed, with the restore time and event id (21.91704ms)
|
||||||
|
✔ a task closed on the tracker leaves the open list, and total counts the open ones (21.469025ms)
|
||||||
|
✔ a tombstone is not open (22.705282ms)
|
||||||
|
✔ the open list stops at the cap and says so; total still counts every open task (30.531354ms)
|
||||||
|
✔ a role and a reader session read state; arguments, and an unbound cap, refuse (22.61769ms)
|
||||||
|
✔ creates private WAL store and excludes a second writer until explicit close (21.22909ms)
|
||||||
|
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (23.375799ms)
|
||||||
|
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (24.087794ms)
|
||||||
|
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (80.380382ms)
|
||||||
|
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (16.980031ms)
|
||||||
|
✔ async transactions refuse before invoking their function (12.165568ms)
|
||||||
|
✔ the session default fits under the reply cap and pages to the end with next (210.151753ms)
|
||||||
|
✔ the short form: no description, assignees by role, every listed key (101.057075ms)
|
||||||
|
✔ each filter, alone and together, and total counts every match (170.585166ms)
|
||||||
|
✔ task_ref reads one task in full, as the whole board has it (95.139805ms)
|
||||||
|
✔ no arguments is the read the Console and mosaic make, unchanged (78.859592ms)
|
||||||
|
✔ an unknown argument or a bad value refuses invalid-request (327.211074ms)
|
||||||
|
✔ tasks rows carry priority and requirement for every session that reads tasks (62.480313ms)
|
||||||
|
✔ the read model rebuilds from the ledger: a reopened bus and each trail give the same values (62.965212ms)
|
||||||
|
✔ an empty business and one with snapshots but no task.created read cleanly (16.835199ms)
|
||||||
|
✔ recordTask keeps sync reads and a role write apart (33.465124ms)
|
||||||
|
✔ read_at must be one canonical UTC format, so the projection compares strings safely (12.037918ms)
|
||||||
|
✔ a bad entry refuses the whole record (12.462407ms)
|
||||||
|
✔ taskView reads the projection for one business (15.861849ms)
|
||||||
|
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (35.14223ms)
|
||||||
|
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (281.596722ms)
|
||||||
|
✔ without an adapter the server refuses every task verb (17.498175ms)
|
||||||
|
✔ the runtime refuses an invalid adapter and closes a valid one (18.80034ms)
|
||||||
|
✔ the process loads the S3 adapter from plain-data trackers (114.455343ms)
|
||||||
|
✔ socket capability stamps launch identity; shared views use wire, no SQL client (42.344319ms)
|
||||||
|
✔ two wire claims serialize; a lost reply never automatically retries (28.837779ms)
|
||||||
|
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (20.272911ms)
|
||||||
|
✔ client preserves UTF-8 when a response divides a multibyte character (14.60498ms)
|
||||||
|
✔ committed mutation followed by dropped reply reports unknown and is never retried (17.674993ms)
|
||||||
|
ℹ tests 134
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 134
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 1259.645185
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
✔ sessionModel: agent vars win, then the system's execution settings (12.417381ms)
|
||||||
|
✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (6.775936ms)
|
||||||
|
✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.193608ms)
|
||||||
|
✔ the prompt's built-in tools line is the harness's own names, as the manifest lists them (12.569584ms)
|
||||||
|
✔ the prompt names the project clones in the workspace, and none when there are none (9.16228ms)
|
||||||
|
✔ the prompt explains the review loop to a role with push_working, and reviews to one with fetch_review (10.201009ms)
|
||||||
|
✔ a bundle is written once: an existing file refuses (2.631683ms)
|
||||||
|
✔ a path with a single quote can't go into the hook command (2.440308ms)
|
||||||
|
✔ allow exits 0, a deny exits 2 with the reason on stderr (153.578839ms)
|
||||||
|
✔ a missing or wrong policy, or a bad event, exits 2 (95.04587ms)
|
||||||
|
✔ the bundle's wrapped command: a missing gate or node still blocks (1120.585558ms)
|
||||||
|
✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (898.889587ms)
|
||||||
|
✔ claude: the hook alone blocks a path outside the workspace (622.876249ms)
|
||||||
|
✔ claude: a second turn resumes the first turn's session (979.48539ms)
|
||||||
|
✔ claude adapter: --restricted is always passed (7.782982ms)
|
||||||
|
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (30.908728ms)
|
||||||
|
✔ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs (10.149509ms)
|
||||||
|
✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (1244.844694ms)
|
||||||
|
✔ claude: a missing hook or MCP file refuses before claude starts (13.654284ms)
|
||||||
|
✔ pi: policy tools and typed tools pass, anything else is blocked (4.589988ms)
|
||||||
|
✔ claude: builtins map from pi names, typed tools need the mcp prefix (1.054042ms)
|
||||||
|
✔ file tool paths must resolve inside the workspace (2.167273ms)
|
||||||
|
✔ pi's own path normalisation can't be used to step out (1.5888ms)
|
||||||
|
✔ a symlink inside the workspace that points out is outside (1.29361ms)
|
||||||
|
✔ a dangling symlink is refused at any depth, in both harnesses (4.585817ms)
|
||||||
|
✔ read is checked under every spelling pi's read would open, in both harnesses (21.533924ms)
|
||||||
|
✔ other spellings cover directories, dangling links and pi's cwd (2.312075ms)
|
||||||
|
✔ a relative path climbs from the workspace's real path, in both harnesses (6.537201ms)
|
||||||
|
✔ claude path fields per tool (1.068644ms)
|
||||||
|
✔ glob patterns stay inside the workspace (0.989193ms)
|
||||||
|
✔ a path that can't be checked is blocked (0.54534ms)
|
||||||
|
✔ fetch_review's have lists the working refs before origin's, and no more than HAVE_MAX (281.931805ms)
|
||||||
|
✔ fetch_review's have orders the working refs newest first within each clone, one clone after another (199.917049ms)
|
||||||
|
✔ a runner git past its time: SIGTERM reaches its process group, and SIGKILL follows after the grace period (1810.425036ms)
|
||||||
|
✔ once a signalled runner git has closed, its group gets SIGKILL, so a process that ignored SIGTERM dies too (305.978604ms)
|
||||||
|
✔ initialize, ping and tools/list (56.659114ms)
|
||||||
|
✔ tools/call goes through the tool socket; a refusal is an isError result (38.511849ms)
|
||||||
|
✔ replies are compact JSON; over the cap a tool error names the size and the cap (38.453315ms)
|
||||||
|
✔ unknown tools and methods are JSON-RPC errors and never reach the socket (35.634665ms)
|
||||||
|
✔ a missing argument is a usage error (34.545934ms)
|
||||||
|
✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (478.514817ms)
|
||||||
|
✔ pi: a reply over the cap is a tool error naming the size and the cap; list_tasks asks for open tasks (398.769327ms)
|
||||||
|
✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (473.025619ms)
|
||||||
|
✔ pi: a read is refused when pi would open another spelling outside (492.946075ms)
|
||||||
|
✔ pi: a relative path climbs from the real path of a workspace behind a symlink (408.34998ms)
|
||||||
|
✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (505.304754ms)
|
||||||
|
✔ pi: a missing extension refuses before any model call (10.805917ms)
|
||||||
|
✔ pi: an extension without its configuration fails pi's start (379.830008ms)
|
||||||
|
✔ founderCheck: founder variables, then a needed service without a usable token (1.385847ms)
|
||||||
|
✔ turnRequest names the sender, class, reply and decision (0.237243ms)
|
||||||
|
✔ turnRequest shows the message's own sent time, a day old as well as new (0.145817ms)
|
||||||
|
✔ stateBlock names the launch state with its time and event, and the open tasks with the cap (0.250355ms)
|
||||||
|
✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (194.770986ms)
|
||||||
|
✔ launches revoked and then restored: the turn's block reads allowed and gives the restore time (201.810113ms)
|
||||||
|
✔ a task closed on the tracker is missing from the next turn's open list (199.027864ms)
|
||||||
|
✔ a message read a day after it was sent shows the time it was sent (118.425302ms)
|
||||||
|
✔ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read (65.822541ms)
|
||||||
|
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (60.525033ms)
|
||||||
|
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (331.868858ms)
|
||||||
|
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1291.178798ms)
|
||||||
|
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (132.669006ms)
|
||||||
|
✔ founder credentials stop before the claim (20) (126.496724ms)
|
||||||
|
✔ a refused claim exits 21; an ended run's capability exits 22 (134.206179ms)
|
||||||
|
✔ the launch ending under a running session exits 22 (131.363335ms)
|
||||||
|
✔ a broker that stays unreachable exits 23 after brokerRetries polls (169.977483ms)
|
||||||
|
✔ a broker that is down at the claim exits 23, not 21 (71.616218ms)
|
||||||
|
✔ no capability, or a malformed one, on stdin exits 2 (242.257316ms)
|
||||||
|
✔ a missing or malformed policy exits 2 before the claim (160.454735ms)
|
||||||
|
✔ the PM gets launch, its task verbs and the reads (9.750552ms)
|
||||||
|
✔ list_requirements reads the requirement ids, for a task.create holder with a tracker (4.999651ms)
|
||||||
|
✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.97782ms)
|
||||||
|
✔ a coder with a tracker gets its own task tools and no task_reassign (1.807452ms)
|
||||||
|
✔ launch only when the business's launch block names the instance as launcher (2.467031ms)
|
||||||
|
✔ an action outside the instance's authority has no tool (2.321265ms)
|
||||||
|
✔ the review loop: push_working and request_review for the coder, fetch_review and review_verdict for the reviewer, none for the PM (7.019703ms)
|
||||||
|
✔ callTool: one JSON line out, the result back, a refusal rejects (11.392769ms)
|
||||||
|
✔ list_tasks takes the broker's filters and pages; get_task takes a task_ref (3.6841ms)
|
||||||
|
✔ callTool sends list_tasks done false unless the session names done; other tools as given (3.669431ms)
|
||||||
|
✔ replyText: compact JSON up to the cap, an error naming the size and the cap over it (1.135804ms)
|
||||||
|
ℹ tests 79
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 79
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 10721.559659
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
✔ F1: an ordinary commit after update-ref is refused until step 8; then it commits and the queue stays at C (1331.537794ms)
|
||||||
|
✔ F1: a plain `commit -e` whose guard ran before update-ref fails at its own HEAD update (1040.166322ms)
|
||||||
|
ℹ git commit -e: index.lock free during the editor
|
||||||
|
✔ F1: a `commit -e -- path` whose guard ran before update-ref fails at its own HEAD update (1036.333657ms)
|
||||||
|
ℹ git commit -e -- src.txt: index.lock held during the editor
|
||||||
|
✔ F1: step 8 with index.lock held exits 3, and ordinary commits stay refused until the printed command runs (1078.962776ms)
|
||||||
|
✔ F1: HEAD moving after the step-7 guard check and before update-ref: refused, nothing published (1029.985274ms)
|
||||||
|
✔ F1: H is recorded before the canary, so HEAD moving during the step-1 canary is refused at step 7 (1009.630991ms)
|
||||||
|
✔ F1: a queue commit landing after H is recorded: step 1 says HEAD moved, not the guard (1307.844422ms)
|
||||||
|
✔ F1: a queue commit landing between the HEAD check and the canary: the failed clean run is reported as HEAD moved (908.42089ms)
|
||||||
|
✔ F1: a shared-index change during the procedure is not committed (1016.921274ms)
|
||||||
|
✔ F1: a queue path staged after update-ref: step 8 stops and touches nothing (995.733784ms)
|
||||||
|
✔ F1: a missing or a different hook refuses (701.787272ms)
|
||||||
|
✔ F1: same bytes without the exec bit, a symlinked hook, and core.hooksPath in the local or global scope each refuse before update-ref (1169.662432ms)
|
||||||
|
✔ F1: the canary refuses a hook that git would not run (604.167816ms)
|
||||||
|
✔ F1: the guard deactivated after step 1 is refused at the step-7 recheck (862.581163ms)
|
||||||
|
✔ bootstrap: implementation-only HEAD, the guard, genesis, the --genesis commit, then an extending commit (883.291923ms)
|
||||||
|
✔ bootstrap: --genesis with a base present, no base without --genesis, an op before the first commit, a changed map, another branch (893.86907ms)
|
||||||
|
✔ bootstrap: the archived tests and validator run outside any repository (852.213168ms)
|
||||||
|
✔ general: an unrelated staged file stays staged, and the committed blobs are the snapshot bytes (846.129531ms)
|
||||||
|
✔ general: a queue write after the snapshot is not committed (1207.54959ms)
|
||||||
|
✔ general: a snapshot whose log does not extend the base refuses (908.263093ms)
|
||||||
|
✔ general: install-hook privilege, repair of a same-bytes hook, and its refusals (177.738465ms)
|
||||||
|
✔ general: environment overrides, a linked worktree and usage (559.853207ms)
|
||||||
|
✔ general: a queue path staged before the run refuses at step 1 (626.879303ms)
|
||||||
|
✔ general: HEAD's queue tests failing in the archive refuse (803.167551ms)
|
||||||
|
✔ genesis document serializes deterministically and replays (5.417804ms)
|
||||||
|
✔ a hand edit that stays valid JSON fails replay; a formatting-only edit fails re-serialization (2.706153ms)
|
||||||
|
✔ a tampered result, receipt or viewSha fails replay (3.234876ms)
|
||||||
|
✔ op ids: 8 to 72 characters for callers, 80 in the log for .outcome entries (0.256698ms)
|
||||||
|
✔ add: defaults for an ordinary seat, privileged extras, refusals (4.916827ms)
|
||||||
|
✔ matrix: queued→briefed privileged; briefed→in-progress owner with after satisfied (7.42298ms)
|
||||||
|
✔ matrix: release, review round, changes requested and waiting-on-jason (17.817401ms)
|
||||||
|
✔ matrix J5: in-review→done by the gate owner with evidence naming the current round (11.320296ms)
|
||||||
|
✔ review issue, lead decision 23: none refuses, one is used, several need --issue, later rounds keep it (26.418251ms)
|
||||||
|
✔ the row schema refuses a round with a null issue, and the A1 review shape (P2) (3.58877ms)
|
||||||
|
✔ matrix R1: every state × target × actor class matches 8.7, gate owner jason or not, required or not (1595.202608ms)
|
||||||
|
✔ matrix: blocked keeps the claim and returns only to previousState (4.237519ms)
|
||||||
|
✔ matrix J4: parking is Jason's, refused while required; unpark returns to queued (2.783084ms)
|
||||||
|
✔ field edits: who may change what (5.405367ms)
|
||||||
|
✔ set issues keeps a logged narrowing of closes (N10) (2.929429ms)
|
||||||
|
✔ text the table shows refuses \ and <, everywhere it enters (N8) (2.096091ms)
|
||||||
|
✔ every accepted text renders to nine cells on every row (N8) (24.693365ms)
|
||||||
|
✔ genesis: the map refuses an owner among its row's reviewers; replay doesn't (2026-10-04) (0.614657ms)
|
||||||
|
✔ times and dates must be calendar values, not just the shape (2026-10-04) (1.724399ms)
|
||||||
|
✔ replay holds every op id to the caller's rule (N11) (5.456925ms)
|
||||||
|
✔ note: owner, listed reviewer or privileged; empty clears (1.553456ms)
|
||||||
|
✔ assign moves the claim with the owner; done clears it (2.816043ms)
|
||||||
|
✔ render is byte-stable and escapes pipes (0.527543ms)
|
||||||
|
✔ view classification: current, genuine stale, edited stale marker, changed current body, markers (0.650843ms)
|
||||||
|
✔ next: resume, then review, then start, then wait, then nothing; lowest id first (18.726734ms)
|
||||||
|
✔ canonical args make a retry's identity independent of list order (0.342479ms)
|
||||||
|
✔ manifests, headings and blob ids (0.45433ms)
|
||||||
|
✔ the migration map: one queue-map block, exact keys (0.64276ms)
|
||||||
|
✔ every call but `queue` reaches the seat CLI exactly as before A2 (672.149433ms)
|
||||||
|
✔ no argument reaches the app entry in packages/cli, and only that call (77.444955ms)
|
||||||
|
✔ `queue` reaches the queue CLI with the rest of the arguments (210.095797ms)
|
||||||
|
✔ the pre-A2 fixture is the script A2 changed (0.312678ms)
|
||||||
|
✔ an old privileged add with no gate owner still replays through loadDoc, and gets jason (8.892933ms)
|
||||||
|
✔ a non-privileged add gets jason through the replay path and can't name a gate owner (3.365067ms)
|
||||||
|
✔ the CLI refuses a privileged add without --gate-owner, for jason and sage, and writes nothing (285.866234ms)
|
||||||
|
✔ the CLI add with --gate-owner carries the named owner and records a log entry (215.649063ms)
|
||||||
|
✔ the CLI add by a non-privileged seat gets jason and can't name a gate owner (262.213362ms)
|
||||||
|
✔ a retried sage add recorded with no gate owner answers (already recorded at rev N) and writes nothing (260.821426ms)
|
||||||
|
✔ acquire publishes the record by link; release removes only its own lock (6.854666ms)
|
||||||
|
✔ a kill between the temp write and the link leaves no lock (55.939657ms)
|
||||||
|
✔ a short or failed temp write refuses and leaves no lock and no temp (3.138078ms)
|
||||||
|
✔ a link error other than EEXIST refuses (1.76434ms)
|
||||||
|
✔ an error after the link releases the lock: unreadable gate, failing temp stat (4.523265ms)
|
||||||
|
✔ a release that fails on a gate path is reported, never a stack trace (P1) (5.276907ms)
|
||||||
|
✔ a paused holder: another writer waits 10 s, then refuses naming it live (10072.141373ms)
|
||||||
|
✔ two concurrent unlockers: the second refuses on the gate (23.173044ms)
|
||||||
|
✔ a writer publishing during an unlock, lock first: unlock sees it live and refuses (1.816655ms)
|
||||||
|
✔ a writer publishing during an unlock, gate first: the writer releases and refuses (1.682757ms)
|
||||||
|
✔ a gate swapped while held is left in place and reported, on success and on refusal (N1) (1.896234ms)
|
||||||
|
✔ a reused pid within one boot is mismatch; unlock removes the lock and never signals the process (3.512083ms)
|
||||||
|
✔ the same pid and start on a different boot is mismatch (0.360087ms)
|
||||||
|
✔ a foreign host is unknown whatever the local pid says; unlock refuses (48.27042ms)
|
||||||
|
✔ unreadable /proc: classification is unknown and acquire refuses (0.703917ms)
|
||||||
|
✔ invalid records: empty, unparsable, wrong keys, bad start or boot (0.220087ms)
|
||||||
|
✔ a stale gate blocks writers; --check-gate says mismatch for a reused pid (4.406312ms)
|
||||||
|
✔ a delayed release by a dead owner, after unlock and a new owner: the inode check keeps the new lock (2.295958ms)
|
||||||
|
✔ release checks the inode too: a byte-identical lock file with a new inode is left in place (0.932111ms)
|
||||||
|
✔ unlock refuses a live, unknown or invalid lock, and does nothing without one (3.308213ms)
|
||||||
|
✔ the migration map validates and renders the golden genesis table (5.787879ms)
|
||||||
|
✔ the marked QUEUE.md holds every row and parked item between its markers (2.211878ms)
|
||||||
|
✔ map-check reports each kind of drift (5.877366ms)
|
||||||
|
✔ a request posts once as the requester; a retry sends nothing (685.874397ms)
|
||||||
|
✔ each transport answer maps to posted, failed or uncertain (8.9 step 3) (3329.01943ms)
|
||||||
|
✔ the pre-send checks: GET user must name the requester, under the deadline (1040.62195ms)
|
||||||
|
✔ the lead's request refuses a token for login sage (556.54807ms)
|
||||||
|
✔ the credential file: the seat's own, 0600, no symlink, never the shared default (470.212133ms)
|
||||||
|
✔ an unresolved request blocks a new request, a new round, waiting-on-jason and done (1485.004114ms)
|
||||||
|
✔ a same-op retry after a kill sends nothing, even with a stale view (2261.101072ms)
|
||||||
|
✔ a held lock at the outcome exits 3 and names what the transport said (558.825927ms)
|
||||||
|
✔ late outcomes: after an abandon, and after a resolve with the same or another id (1499.434114ms)
|
||||||
|
✔ resolve checks the comment: issue, markers, round, candidate and author (1536.428058ms)
|
||||||
|
✔ the lead resolves a seat's request: the comment's author is the requester, fetched with the lead's token (486.17057ms)
|
||||||
|
✔ validateRow checks a request round's shape, which every replayed entry must keep (347.76106ms)
|
||||||
|
✔ request, changes, a new candidate, approval: every round pinned; no review files (1401.680358ms)
|
||||||
|
✔ a row with no reviewers opens a round that posts nothing (833.544903ms)
|
||||||
|
✔ verify-commit: a prospective tree must hold exactly the candidate's paths (1149.158313ms)
|
||||||
|
✔ semantics: v1 entries replay as before; review entries need v2 (336.448283ms)
|
||||||
|
✔ set reviewers refuses the row's owner (2026-09-28) (255.893084ms)
|
||||||
|
✔ the owner records no verdict, even as a listed reviewer (349.043307ms)
|
||||||
|
✔ a request comment over the length limit is not sent (371.761724ms)
|
||||||
|
✔ a late POST on a closed row leaves a conflict nothing can resolve, and resolve asks nothing (396.978823ms)
|
||||||
|
✔ a Jason-gated row reaches waiting-on-jason only on every reviewer's approval (1795.737561ms)
|
||||||
|
✔ genesis: refusals before anything is written (496.110406ms)
|
||||||
|
✔ genesis: the map must be committed, well formed, with committed briefs and seats (655.508284ms)
|
||||||
|
✔ genesis: markers, a stray witness, once only; a retry returns the receipt (549.119088ms)
|
||||||
|
✔ genesis: a file holding genesis alone with no witness is confirmed by sync or a retry (317.370597ms)
|
||||||
|
✔ canonical checks: worktree, second clone, detached HEAD, wrong branch, GIT_DIR, foreign code; a symlink works (629.929622ms)
|
||||||
|
✔ --by that differs from MOSAIC_AGENT_NAME warns on stderr and logs nothing more (N12) (577.934068ms)
|
||||||
|
✔ op ids: missing, too long, reserved; a retry answers; another payload refuses (633.221973ms)
|
||||||
|
✔ a retried add returns the id it first allocated, after reassignment and after done (670.461078ms)
|
||||||
|
✔ Rocko's S4 schedule: a lost result, another writer, then the retry opens no second round (453.67716ms)
|
||||||
|
✔ the review issue and the evidence round through the CLI (lead decision 23, 8.7) (888.043954ms)
|
||||||
|
✔ claims and add defaults through the CLI; candidates are manifests or reachable commits (609.035102ms)
|
||||||
|
✔ add, set reviewers and assign refuse the row's owner as a reviewer (384.942062ms)
|
||||||
|
✔ the working-brief check: a changed working copy refuses the start and flags next (558.05983ms)
|
||||||
|
✔ next: resume first, then nothing for an idle seat; needs a seat (283.289384ms)
|
||||||
|
✔ view stale: new ops and verify refuse naming the unshown op; retries answer; reads warn; render fixes (580.917293ms)
|
||||||
|
✔ view unknown: a hand edit, an old marker over an edited body, missing or duplicate markers (1029.920152ms)
|
||||||
|
✔ a hand edit to queue.json refuses every verb, reads included (529.36113ms)
|
||||||
|
✔ verify and render --check leave bytes and mtimes unchanged (392.789035ms)
|
||||||
|
✔ render is byte-stable across runs and repositories (238.578069ms)
|
||||||
|
✔ snapshot and verify --snapshot (689.489097ms)
|
||||||
|
✔ usage errors exit 4 (534.162651ms)
|
||||||
|
✔ a short write, ENOSPC or a file fsync failure: nothing visible, temp removed (220.128937ms)
|
||||||
|
✔ a rename failure: nothing visible, temp removed (155.157612ms)
|
||||||
|
✔ a directory fsync failure: uncertain, exit 3, no receipt; the tail refuses new ops; a retry confirms (192.032261ms)
|
||||||
|
✔ a directory fsync failure, then sync names the op (340.351152ms)
|
||||||
|
✔ a witness write failure: uncertain, durable, exit 3; the view is untouched (148.386967ms)
|
||||||
|
✔ the .git fsync after the witness rename fails: uncertain, exit 3, the witness says so (132.830175ms)
|
||||||
|
✔ confirming a tail fsyncs queue.json and docs/plans before the witness; either failure changes nothing (172.902304ms)
|
||||||
|
✔ the docs/plans fsync after the view rename fails: the op stands, the view is written, a warning says so (133.544513ms)
|
||||||
|
✔ a lock swapped while held is left in place and reported, on a receipt and on a refusal (141.93407ms)
|
||||||
|
✔ a lock that cannot be released after an op is reported, on a receipt and on a refusal (144.003935ms)
|
||||||
|
✔ unlock prints a swapped gate's warning on stderr, the result on stdout (122.309735ms)
|
||||||
|
✔ a view write that fails keeps the op and reports a stale view (108.058779ms)
|
||||||
|
✔ SIGKILL before the rename: nothing recorded; the retry removes the leftover temp (574.209053ms)
|
||||||
|
✔ SIGKILL after the rename, before the witness: the tail refuses new ops and sync names the op (630.590549ms)
|
||||||
|
✔ SIGKILL after the witness, before the view: the stale refusal names the op (579.42521ms)
|
||||||
|
✔ SIGKILL after the view, before the receipt: the retry returns the receipt (606.861953ms)
|
||||||
|
✔ git checkout between steps 1 and 7: step 7 refuses and nothing is written (140.748903ms)
|
||||||
|
✔ git stash restoring an older valid pair: history lost; accept-history needs privilege, a reason and --yes (837.169137ms)
|
||||||
|
✔ a deleted witness: refused after the locked recheck; accept-history records it absent (208.102677ms)
|
||||||
|
✔ a header edit during a write: the op stands, the view write is skipped with a warning (137.985522ms)
|
||||||
|
✔ a reader paused between the witness and the file while a writer finishes: no lost-history report (127.028152ms)
|
||||||
|
✔ rows: every row as show prints it, in list order; writes nothing; same notes and refusals as list (173.306074ms)
|
||||||
|
✔ file-then-witness order forced by a hook: the locked recheck prevents a false report (123.782197ms)
|
||||||
|
✔ a writer paused before and after the witness rename: readers see a tail, then a match (135.502219ms)
|
||||||
|
✔ a true rollback is reported only after the locked recheck; a held lock names its holder instead (491.896817ms)
|
||||||
|
✔ an accept-history in progress: an unlocked reader waits on the lock and never reports lost history (461.117436ms)
|
||||||
|
✔ the platform check refuses other filesystems (116.02771ms)
|
||||||
|
✔ tmpfs passes only a test layer that allows it (N5) (146.043828ms)
|
||||||
|
✔ unlock keeps a multi-line lock record on stdout (P3) (169.982853ms)
|
||||||
|
ℹ tests 156
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 156
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 21929.78614
|
||||||
@@ -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,23 @@
|
|||||||
|
On branch refactor
|
||||||
|
Your branch is up to date with 'origin/refactor'.
|
||||||
|
|
||||||
|
nothing to commit, working tree clean
|
||||||
|
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: 3879204 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,36 @@
|
|||||||
|
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/gate-owner.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 156
|
||||||
|
ℹ pass 156
|
||||||
|
ℹ fail 0
|
||||||
|
OK node --test packages/queue/tests/
|
||||||
|
OK scripts/mosaic queue help
|
||||||
|
skip queue verify and render --check: this checkout (/mnt/storage/scratch/researcher/q109) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
|
||||||
|
|
||||||
|
queue suite: 28 passed, 0 failed
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
|
OK status safe on empty state (exit 0)
|
||||||
|
OK status created no pointer
|
||||||
|
OK fault-injected activation refuses (exit 1)
|
||||||
|
OK refused activation wrote no pointer
|
||||||
|
OK refusal logged exactly once with valid fields
|
||||||
|
OK healthy activation succeeds (exit 0)
|
||||||
|
OK pointer written with valid fields
|
||||||
|
OK repeat activation succeeds (log grows) (exit 0)
|
||||||
|
OK log is append-only across activations
|
||||||
|
OK rollback without previous refuses (exit 1)
|
||||||
|
|
||||||
|
selftest: 14 passed, 0 failed
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
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)
|
||||||
|
OK mock adapter: gate passes on matching mock response (exit 0)
|
||||||
|
OK mock adapter: expect-mismatch recorded (exit 1)
|
||||||
|
OK mismatch reason recorded
|
||||||
|
OK unknown adapter fails closed (exit 1)
|
||||||
|
OK mission task runs via mock adapter (exit 0)
|
||||||
|
OK mission section injected into generated prompt
|
||||||
|
OK retry of mission run succeeds (exit 0)
|
||||||
|
OK retriedFrom lineage recorded
|
||||||
|
OK mission section present after retry (relative path resolved)
|
||||||
|
OK retry of missing run exits 4 (exit 4)
|
||||||
|
OK skill install bundled ms-tools (exit 0)
|
||||||
|
OK installed to skills-available
|
||||||
|
OK double install refuses (exit 1)
|
||||||
|
OK activate enables skill (exit 0)
|
||||||
|
OK enabled dir populated
|
||||||
|
OK uninstall while enabled refuses (exit 1)
|
||||||
|
OK deactivate moves back to available (exit 0)
|
||||||
|
OK uninstall removes from available (exit 0)
|
||||||
|
OK seat with enabled skill launches
|
||||||
|
OK skill path delivered to adapter
|
||||||
|
OK shipped researcher contract resolves (ceiling + network)
|
||||||
|
OK resolve-role refuses a non-role document (conductor policy) (exit 2)
|
||||||
|
OK resolve-role refuses name/filename mismatch (exit 2)
|
||||||
|
OK resolve-role refuses unknown network declaration (exit 2)
|
||||||
|
OK resolve-role refuses duplicate role tool (exit 2)
|
||||||
|
OK resolve-role refuses unsupported tool (exit 2)
|
||||||
|
OK resolve-role refuses missing contract file (exit 4)
|
||||||
|
OK version 1 role prints no contract line
|
||||||
|
OK shipped version 2 roles resolve with their contracts (pm, cto, coder, reviewer)
|
||||||
|
OK resolve-role accepts a minimal version 2 role (exit 0)
|
||||||
|
OK resolve-role refuses an action outside the vocabulary (exit 2)
|
||||||
|
OK resolve-role refuses a gated-only action in a role file (exit 2)
|
||||||
|
OK resolve-role refuses a Vikunja verb no role may hold (exit 2)
|
||||||
|
OK resolve-role refuses a version 2 role whose contract is missing (exit 2)
|
||||||
|
OK resolve-role refuses a contract that is a symbolic link (exit 2)
|
||||||
|
OK seat launches under role ceiling
|
||||||
|
OK role ceiling narrows seat tools (read,write,bash -> read,bash)
|
||||||
|
OK narrowing recorded loudly
|
||||||
|
OK missing role contract refuses launch (exit 2)
|
||||||
|
OK missing-contract refusal names the role
|
||||||
|
OK empty ceiling intersection -> tool-free seat
|
||||||
|
OK tool-free outcome recorded loudly
|
||||||
|
OK overridden agents dir without seat definition refuses (exit 4)
|
||||||
|
OK seat-resolution refusal names the seat
|
||||||
|
OK policy: mission only -> mission tools
|
||||||
|
OK policy: task only -> task tools
|
||||||
|
OK policy: both -> intersection (task narrowed)
|
||||||
|
OK policy: empty intersection -> tool-free
|
||||||
|
OK invalid mission capabilities rejected (exit 2)
|
||||||
|
mosaic-task: unsupported mission tool: "sudo" (supported: read, write, edit, bash, grep, find, ls)
|
||||||
|
OK task run with user layer present (exit 0)
|
||||||
|
OK user context dispatched into generated prompt
|
||||||
|
OK workspace+tools task runs via mock (exit 0)
|
||||||
|
OK workspace path + tools delivered to adapter
|
||||||
|
OK persistent workspace created on host
|
||||||
|
OK plain task still runs (no workspace/tools) (exit 0)
|
||||||
|
OK workspace var present but empty when absent
|
||||||
|
OK tools empty when absent
|
||||||
|
OK unknown tool exits 2 (exit 2)
|
||||||
|
OK workspace traversal exits 2 (exit 2)
|
||||||
|
OK live hello task succeeds with exact marker
|
||||||
|
OK result.json written in run dir
|
||||||
|
OK result.json contents are correct
|
||||||
|
OK wrong expectExact fails with exit 1 (reason: expect-mismatch)
|
||||||
|
OK each run gets a distinct run dir (no clobber)
|
||||||
|
OK list shows both runs
|
||||||
|
OK fork base: teach succeeds (exit 0)
|
||||||
|
OK fork child recalls ancestor context (exit 0)
|
||||||
|
OK forked child recalled ancestor code word
|
||||||
|
OK ancestor session untouched by fork
|
||||||
|
OK child session dir has its own branch file
|
||||||
|
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
|
||||||
|
OK user recall run succeeds
|
||||||
|
OK recalled user name (response: Jason)
|
||||||
|
OK no agent identity on headless run
|
||||||
|
|
||||||
|
selftest: 98 passed, 0 failed
|
||||||
Reference in New Issue
Block a user