docs: add Researcher row 123 packet (chief of staff ends runs and changes the organization file)
Candidate on 0bdd573ff + row 49 r1 (manifest 87ef1321...), 21 mutants killed,
targeted gate green. Nothing landed or pushed.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
8bcd4418f7
commit
2209ac4c17
85 files changed
+4273
No files matched your search
@@ -0,0 +1,194 @@
|
||||
# Row 123 (#1617): the chief of staff ends runs and changes the organization file
|
||||
|
||||
Seat: Researcher. Reviewer: Filbert (broker authority). Gate owner: Sage.
|
||||
Requirements: REQ-ROLE-2, REQ-ROLE-3, REQ-LIMIT-1, REQ-COS-1.
|
||||
Brief: `docs/plans/2026-10-14_prd-1.2-build.md` build step 6.
|
||||
|
||||
**Base: 0bdd573ff + row 49 r1, candidate manifest 87ef1321a1a08be64a20499889fc24b5549b47d6d33abfd86db95f62fb3cb6d0**
|
||||
(Rocko's packet `agents/rocko/work/queue-49/r1/`, applied uncommitted).
|
||||
`row123.patch` is a diff of my changes only, against that combined tree (13 files, +685 −34).
|
||||
It applies with `git apply --cached --check` on base + row 49 r1, and the result round-trips to the candidate tree.
|
||||
Nothing is committed in the worktree, nothing is pushed, and no live store, unit or `~/.mosaic` file was touched.
|
||||
|
||||
## What the row does
|
||||
|
||||
1. **`role.end`**: a new action. A role whose file grants it (only the chief of staff) ends another role's run with
|
||||
`role.revoke {role, reason}`: a non-empty `reason` of at most 500 characters and **no `decision`**. The reason is
|
||||
written to `role_claims.reason` and to the `action.allowed` event body (`class: within-role`, `reason`).
|
||||
`role.revoke` itself stays gated for every role (decision 46), so any other role still needs a resolved decision.
|
||||
2. **`organization.change`**: a new action. A role whose file grants it calls the new broker verb
|
||||
`organization.set`, which takes exactly the keys of row 122's write path and goes through the same
|
||||
`launchSettings.set` writer the human's `limits.set` uses (same validation, same history receipt).
|
||||
3. Schema 3c → 3d: the `role_revoke_needs_decision` trigger accepts a revoke with a reason and no decision.
|
||||
4. The broker has no role-name checks for the grants. The grant lives in `roles/chief-of-staff.json`.
|
||||
The only name the broker knows is `END_PROTECTED = ['assistant']` (a definition name) in the refusal below.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `packages/business/src/vocabulary.mjs` | `role.end`, `organization.change` added to the action vocabulary |
|
||||
| `roles/chief-of-staff.json` | `withinRole`: `message.send`, `role.launch`, `role.end`, `organization.change`. `role.revoke` is not granted |
|
||||
| `packages/bus/src/broker.mjs` | `ACTIONS` gets both. `ORGANIZATION_KEYS` (one constant). `organization.set` beside `limits`/`limits.set`. The `role.revoke` reason form. `#claimEnd` stores the reason |
|
||||
| `packages/bus/schema.sql` | trigger `WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL`; sha256 `e4f1de9e…39b5` |
|
||||
| `packages/bus/src/migrations/3c-3d.sql` (new) | `DROP TRIGGER` + `CREATE TRIGGER` with the new text, nothing else |
|
||||
| `packages/bus/src/store.mjs` | `SCHEMA_SHA256`, `SCHEMA_VERSION = '3d'`, one more entry in Rocko's `migrations` array |
|
||||
| `packages/bus/tests/fixtures/schema-3c.sql` (new) | Rocko's schema.sql as it stands in row 49 r1 (sha 7587ac1d…eebdb) |
|
||||
| `packages/bus/tests/comment-migration.test.mjs` | **Eight lines in a row 49 file, with the reason: see below** |
|
||||
| `packages/bus/tests/chief-of-staff.test.mjs` (new) | 12 tests |
|
||||
| `packages/business/tests/chief-of-staff.test.mjs` | approved list gains the two actions; `role.revoke` asserted gated |
|
||||
| `packages/harness/tests/tools.test.mjs` | the chief's `untypedActions` is now `["role.end", "organization.change"]` (see Follow-ups) |
|
||||
| `packages/bus/README.md`, `packages/business/README.md` | verb rows, chief-of-staff paragraph, 3c→3d sentence |
|
||||
|
||||
### Eight lines in a row 49 file, and why
|
||||
|
||||
`packages/bus/tests/comment-migration.test.mjs` is Rocko's. Three of its tests pin the current version as `'3c'`
|
||||
(the "ready" string, the pinned version, the future-version fixture). Adding the 3c→3d step makes `3d` current,
|
||||
so those three fail with no change to Rocko's step. I changed 8 lines (8 deleted, 8 added): `'3c'` → `'3d'`, the future
|
||||
fixture `'3e'`, the copy list gains `3c-3d.sql`, the expected line is `bus schema 3d ready`, titles updated.
|
||||
Sage accepted this for row 123 and posts a note on #1535. If row 49 goes to round 2, Rocko makes that test
|
||||
independent of the latest version, and I rebase and drop my edit. If row 49 lands as is, the edit rides with row 123.
|
||||
No other row 49 file is changed.
|
||||
|
||||
## Every field the chief of staff can change through `organization.set`
|
||||
|
||||
Condition 1. This is the whole list, and it is the same list `limits.set` takes from the human (`ORGANIZATION_KEYS`,
|
||||
`broker.mjs:99`, used by both verbs):
|
||||
|
||||
| Key | Rule |
|
||||
|---|---|
|
||||
| `revision` | **required** concurrency token; a stale one is refused by the writer |
|
||||
| `limits` | counts such as `limits.launch` (row 122's write path); the writer validates |
|
||||
| `models` | the model table |
|
||||
| `instances` | per-instance model selections |
|
||||
|
||||
Nothing else. Fields outside the write path are refused with `invalid-request` before the writer is called, for the
|
||||
chief exactly as for the human: `launch` (so `launch.by`), `human`, `arbiters`, `roles`, `actor`, and the literal key
|
||||
`'launch.by'`. Credential references and plugins are not in the write path either. Arbiters are #1627 and not started.
|
||||
Test: **`REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys`**
|
||||
(`chief-of-staff.test.mjs:206`). It runs each outside key as `chief organization.set` and `human limits.set`, expects
|
||||
`invalid-request` in every case, and checks that the writer received no call. Mutants m10 and m11 fail it.
|
||||
|
||||
The writer is told the actor `chief-of-staff.chief` (definition + instance, matching `ID_PATTERN`), so the receipt in
|
||||
`businesses/.history/<id>/changes.jsonl` names the chief, not a human.
|
||||
|
||||
## Storage now accepts a revoke with a reason and no decision
|
||||
|
||||
Stated plainly, as required: **after 3d, SQLite accepts an `op='revoke'` row with a reason and no decision.** The trigger
|
||||
aborts only a revoke with neither. Storage no longer enforces "a decision is required" for `role_claims`. **Only the broker
|
||||
decides who may revoke that way.** The broker refuses the reason form unless the caller's role file grants `role.end`,
|
||||
and it checks before any insert.
|
||||
|
||||
Tests at the broker, for Condition 2 and 3:
|
||||
- `REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke` (line 98): pm, coder and assistant sending
|
||||
`{role, reason}` get `decision-required`; sending `{role}` gets `invalid-request`; the `human` gets `agent-required`;
|
||||
`reader` and `tester` are refused; the `role_claims` row is unchanged and no `role.end` event is logged. This is the
|
||||
non-chief reason-without-decision case refused before any insert. Mutants m1 and m16 fail it.
|
||||
- `REQ-ROLE-2: a reason is required, bounded, and never sent with a decision` (line 152): empty, non-string and over-500
|
||||
reasons and an extra key are refused (the 500-character bound is inclusive), and a `reason` together with a `decision` is refused for the chief too (`invalid-request`), with the claim row unchanged. Mutants m2, m3.
|
||||
- `REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end` (line 167):
|
||||
`role-end-refused` for both; nothing inserted. Mutants m4, m5.
|
||||
- `REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither` (line 112):
|
||||
the schema rule alone. Mutants m19, m20.
|
||||
|
||||
## Acceptance checks against the brief
|
||||
|
||||
| Brief check | Test (file `packages/bus/tests/chief-of-staff.test.mjs` unless stated) |
|
||||
|---|---|
|
||||
| chief ends a run without a decision, logged with its reason | line 80 (`role_claims.reason` and the `action.allowed` body) |
|
||||
| another role still needs a decision | line 98 (refused), line 132 (a resolved decision still ends the run, reason null) |
|
||||
| chief change validated, written, logged like a human's | line 234, runtime test with the real writer: same receipt shape as `limits.set` |
|
||||
| invalid change refused | line 234 (negative, non-numeric and unknown limits, unknown model, unknown instance, empty model table, stale `revision` → `revision-conflict`; file bytes and receipts unchanged) and line 206 (outside keys) |
|
||||
| `role.revoke` gated for every role, the grant is the chief's file | `packages/business/tests/chief-of-staff.test.mjs:86` |
|
||||
| upgrade of a real 3c store | line 343, below |
|
||||
|
||||
## Migration 3c → 3d
|
||||
|
||||
Built on Rocko's engine and version scheme, no second scheme: a `{from:'3c', to:'3d', sql}` entry in `migrations`.
|
||||
The engine validates digest, version, WAL, timestamps and integrity per step, runs the step in a transaction, and
|
||||
verifies the target digest. The step is two statements (drop and recreate one trigger) and touches no row.
|
||||
Test `REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused`
|
||||
(line 343): the 3c database is not hand-built DDL. It starts from Rocko's pinned 3b fixture (`schema-3b.sql`, with an event and a claim row)
|
||||
and goes through Rocko's real `3b.sql` and `3b-3c.sql` steps, run by a copy of `store.mjs` with only the 3c→3d entry removed and
|
||||
`schema-3c.sql` as its schema (so the 3c digest is pinned by `schema-3c.sql`). The test then shows the old trigger refuses a
|
||||
reason-only revoke, a 3d `Store` refuses the 3c database (`schema-mismatch`) until the explicit step runs, and after the step
|
||||
the version is `3d`, the event and claim rows are preserved, the chief ends the held role with a reason (stored, decision null),
|
||||
and a revoke with neither is still aborted. Mutants m6, m16, m19, m20 also fail it, and m21 fails at import.
|
||||
|
||||
Cutover: no seat touches a live store. Mos's single re-pin window takes the live store 3b → 3c → 3d with
|
||||
`node <new-copy>/packages/bus/src/migrate-schema.mjs <absolute dataRoot>` (Rocko's `MIGRATION.md` steps, unchanged).
|
||||
The bus schema changed, so this row needs the re-pin; it should ride in the same window as row 49.
|
||||
|
||||
## Mutants (one per check, each fails a test)
|
||||
|
||||
21 mutants, run one at a time in their own process group with a whole-group kill on timeout.
|
||||
Controls: bus 12/12, business 10/10. Every mutant exits 1. Log `mutants-run.txt`; per-mutant `.patch`, `.out`, `.rc` in `mutants/` (the `.patch` header paths name the runner's temp backup directory, which is gone).
|
||||
|
||||
| # | Mutant | Fails |
|
||||
|---|---|---|
|
||||
| m1 | reason form skips the `role.end` check | 3 bus tests |
|
||||
| m2 | reason and decision together accepted | reason/decision test |
|
||||
| m3 | reason unbounded | reason test |
|
||||
| m4 | chief may end its own run | self/assistant test |
|
||||
| m5 | chief may end the assistant's run | self/assistant test |
|
||||
| m6 | reason not stored | 4 tests incl. migration |
|
||||
| m7 | reason not logged | chief-ends-run test |
|
||||
| m8 | decision path checks `role.end` instead of `role.revoke` | decision-path test (line 132) |
|
||||
| m9 | `organization.set` skips the check | 2 tests |
|
||||
| m10 | key list widened with `launch` | outside-keys test |
|
||||
| m11 | `revision` optional | outside-keys test |
|
||||
| m12 | wrong actor passed to the writer | actor test, real-writer test |
|
||||
| m13 | `limits` open to agents | non-grant refusal test |
|
||||
| m14 | writer failure no longer maps to `invalid-request` | real-writer test |
|
||||
| m15 | vocabulary drops `role.end` | 8 business tests |
|
||||
| m16 | chief file drops `role.end` | 4 bus tests |
|
||||
| m17 | chief file drops `organization.change` | 3 bus tests |
|
||||
| m18 | chief file grants `role.revoke` | 9 business tests |
|
||||
| m19 | trigger back to decision-only | 5 bus tests |
|
||||
| m20 | trigger never fires | storage and migration tests |
|
||||
| m21 | 3c→3d step file empty | file fails at import (`migration-target-mismatch`); a step that does not reach the pinned schema cannot ship |
|
||||
|
||||
m8 survived my first run (11/11). The first draft had no test of the decision path with a resolved decision. I added the
|
||||
line-132 test and re-ran all 21 for this log.
|
||||
|
||||
## Targeted gate (decision 136), run in the q123 worktree 2026-10-15T16:27Z
|
||||
|
||||
| Suite | Result |
|
||||
|---|---|
|
||||
| business | 88/88 |
|
||||
| bus | 190/190 |
|
||||
| tasks | 97/97 |
|
||||
| cli | 195/195 |
|
||||
| webui | 112/112 |
|
||||
| harness | 84/84 (first run 83/84, see below) |
|
||||
| seat | 132/132 |
|
||||
| `scripts/test-land-work.sh` | 84 passed, 0 failed |
|
||||
|
||||
Logs in `suites/`, summary `suites-run.txt` (shows the first harness failure; `suites/harness.txt` is the rerun).
|
||||
First harness run failed `the chief of staff gets launch and send_message, and no task verbs` (`tools.test.mjs:41`): it
|
||||
asserts `untypedActions(chief, tools)` is empty, and the chief now holds two actions with no typed harness tool.
|
||||
I changed that assertion to `["role.end", "organization.change"]` with a row 123 comment. I did not add harness tools.
|
||||
The gate created no Docker networks (22 before, 22 after, same names).
|
||||
|
||||
Sage's full gate at the landing target still applies.
|
||||
|
||||
## Follow-ups (not in this row)
|
||||
|
||||
- **No harness tool yet.** The chief's session cannot call `role.revoke {role, reason}` or `organization.set` through a typed
|
||||
tool; the manifest lists both as untyped. The brief's row 123 does not name harness tools, and adding them widens the row.
|
||||
A harness row is needed before the chief can use either from a session. Sage decides.
|
||||
- `packages/harness/src/bundle.mjs:92` still tells every role "Only the human stops a run, with `mosaic stop`" and to raise a
|
||||
`role.revoke` decision. That is wrong for the chief once it has a tool. Change it with the tool row.
|
||||
- Row 49 round 2: rebase, drop the eight-line edit if Rocko makes his test version-independent.
|
||||
- #1627 (arbiters) not started.
|
||||
|
||||
## Verify
|
||||
|
||||
```sh
|
||||
cd <packet dir>
|
||||
sha256sum -c packet-manifest.sha256
|
||||
git apply --check row123.patch # on base 0bdd573ff + row 49 r1
|
||||
sha256sum -c candidate-manifest.sha256 # from the repo root of the candidate tree (13 files)
|
||||
```
|
||||
|
||||
`mutants.sh` and `suites.sh` are the runners; both take a worktree and an output directory.
|
||||
@@ -0,0 +1,13 @@
|
||||
df19b5e3562246b99999588053e04bc49bd51d7a2cdbd001d9d9e4090949700c packages/bus/README.md
|
||||
e4f1de9e16d2fab5e051fd3810189b28cc76a670cfe573b752d9f53bf9ca39b5 packages/bus/schema.sql
|
||||
cecf7b624536d14e97ae5fcbd03d87cbd9443f4e31d9ebdd0ff6476638d93acf packages/bus/src/broker.mjs
|
||||
0423832d05aa68f9191d7362827bd19310001165b895fb2de40b962fe68f9d56 packages/bus/src/migrations/3c-3d.sql
|
||||
22487c0a9205f1cf93b19a47037a2db8c21443728bc904acf079e34d6e86b82a packages/bus/src/store.mjs
|
||||
ecad53f2499dab3f138cf3c4b21ece4f3b1482f09aa2db254cd86ae03f97db16 packages/bus/tests/chief-of-staff.test.mjs
|
||||
c89cb508ca2cdb98e48341e3f5ff0ff2cb646511cfb6a8bc5962bd278c388095 packages/bus/tests/comment-migration.test.mjs
|
||||
7587ac1df813e5148fdb73dc4733ec8064e6a142e4647f0da71be009a73eebdb packages/bus/tests/fixtures/schema-3c.sql
|
||||
b38767b785432661997381835c31753d3bc4ff67f3c2a01c463f1a5e8440f708 packages/business/README.md
|
||||
0c387e95a17d6576eca42b9bcdf207dbccdbaed34f80a630cd3f8a175032a6bc packages/business/src/vocabulary.mjs
|
||||
28a02144e8ae774bd006b10ad5d07b984e07e0293df86102f82576c7670c6291 packages/business/tests/chief-of-staff.test.mjs
|
||||
f7a7247ed4a88a84d9ff41ec40013c795c4a2eff70534e59141a0c3462b3625e packages/harness/tests/tools.test.mjs
|
||||
a9b4b30b9a37c937b0f0418bc03476aae5ea447ea66494eb5e2c335da13bf4fe roles/chief-of-staff.json
|
||||
@@ -0,0 +1,13 @@
|
||||
M packages/bus/README.md
|
||||
M packages/bus/schema.sql
|
||||
M packages/bus/src/broker.mjs
|
||||
A packages/bus/src/migrations/3c-3d.sql
|
||||
M packages/bus/src/store.mjs
|
||||
A packages/bus/tests/chief-of-staff.test.mjs
|
||||
M packages/bus/tests/comment-migration.test.mjs
|
||||
A packages/bus/tests/fixtures/schema-3c.sql
|
||||
M packages/business/README.md
|
||||
M packages/business/src/vocabulary.mjs
|
||||
M packages/business/tests/chief-of-staff.test.mjs
|
||||
M packages/harness/tests/tools.test.mjs
|
||||
M roles/chief-of-staff.json
|
||||
@@ -0,0 +1,97 @@
|
||||
control-bus: exit 0; ℹ tests 12 ℹ pass 12 ℹ fail 0
|
||||
control-business: exit 0; ℹ tests 10 ℹ pass 10 ℹ fail 0
|
||||
m1-role-end-unchecked: exit 1; ℹ tests 12 ℹ pass 9 ℹ fail 3
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged
|
||||
failed: ✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end
|
||||
failed: ✖ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke
|
||||
m2-reason-and-decision-accepted: exit 1; ℹ tests 12 ℹ pass 11 ℹ fail 1
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision
|
||||
m3-reason-unbounded: exit 1; ℹ tests 12 ℹ pass 11 ℹ fail 1
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision
|
||||
m4-self-end-allowed: exit 1; ℹ tests 12 ℹ pass 11 ℹ fail 1
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end
|
||||
m5-assistant-end-allowed: exit 1; ℹ tests 12 ℹ pass 11 ℹ fail 1
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end
|
||||
m6-reason-not-stored: exit 1; ℹ tests 12 ℹ pass 8 ℹ fail 4
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged
|
||||
failed: ✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end
|
||||
failed: ✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision
|
||||
failed: ✖ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused
|
||||
m7-reason-not-logged: exit 1; ℹ tests 12 ℹ pass 11 ℹ fail 1
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged
|
||||
m8-decision-path-wrong-action: exit 1; ℹ tests 12 ℹ pass 11 ℹ fail 1
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason
|
||||
m9-organization-set-unchecked: exit 1; ℹ tests 12 ℹ pass 10 ℹ fail 2
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change
|
||||
failed: ✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys
|
||||
m10-key-list-widened: exit 1; ℹ tests 12 ℹ pass 11 ℹ fail 1
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys
|
||||
m11-revision-optional: exit 1; ℹ tests 12 ℹ pass 11 ℹ fail 1
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys
|
||||
m12-wrong-actor: exit 1; ℹ tests 12 ℹ pass 10 ℹ fail 2
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor
|
||||
failed: ✖ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused
|
||||
m13-limits-open-to-agents: exit 1; ℹ tests 12 ℹ pass 11 ℹ fail 1
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change
|
||||
m14-invalid-change-not-refused: exit 1; ℹ tests 12 ℹ pass 11 ℹ fail 1
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused
|
||||
m15-vocabulary-drops-role-end: exit 1; ℹ tests 10 ℹ pass 2 ℹ fail 8
|
||||
failed: ✖ a business grant of a gated action through limits does not widen the role
|
||||
failed: ✖ a launch block whose by is the pm instance is refused
|
||||
failed: ✖ a limit that drops role.launch leaves the chief of staff with no launch block
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ the chief of staff holds role.launch, message.send, role.end and organization.change; every other action is gated
|
||||
failed: ✖ the instance takes no credential references and no tracker
|
||||
failed: ✖ the pm does not hold role.launch
|
||||
failed: ✖ the resolved record matches the approved list, so a widened file fails
|
||||
failed: ✖ the role file loads and resolves through loadBusiness
|
||||
m16-chief-file-drops-role-end: exit 1; ℹ tests 12 ℹ pass 8 ℹ fail 4
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged
|
||||
failed: ✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end
|
||||
failed: ✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision
|
||||
failed: ✖ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused
|
||||
m17-chief-file-drops-organization-change: exit 1; ℹ tests 12 ℹ pass 9 ℹ fail 3
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor
|
||||
failed: ✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys
|
||||
failed: ✖ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused
|
||||
m18-chief-file-grants-role-revoke: exit 1; ℹ tests 10 ℹ pass 1 ℹ fail 9
|
||||
failed: ✖ a business grant of a gated action through limits does not widen the role
|
||||
failed: ✖ a launch block whose by is the pm instance is refused
|
||||
failed: ✖ a limit that drops role.launch leaves the chief of staff with no launch block
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ the chief of staff holds role.launch, message.send, role.end and organization.change; every other action is gated
|
||||
failed: ✖ the instance takes no credential references and no tracker
|
||||
failed: ✖ the pm does not hold role.launch
|
||||
failed: ✖ the resolved record matches the approved list, so a widened file fails
|
||||
failed: ✖ the role file loads and resolves through loadBusiness
|
||||
failed: ✖ the shipped file is exactly the approved list
|
||||
m19-trigger-decision-only: exit 1; ℹ tests 12 ℹ pass 7 ℹ fail 5
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged
|
||||
failed: ✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end
|
||||
failed: ✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision
|
||||
failed: ✖ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused
|
||||
failed: ✖ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither
|
||||
m20-trigger-never-fires: exit 1; ℹ tests 12 ℹ pass 10 ℹ fail 2
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused
|
||||
failed: ✖ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither
|
||||
m21-step-empty: exit 1; ℹ tests 1 ℹ pass 0 ℹ fail 1
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ packages/bus/tests/chief-of-staff.test.mjs
|
||||
@@ -0,0 +1 @@
|
||||
2026-10-15T16:24:32Z
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 123 (#1617): the chief of staff ends runs and changes the organization file.
|
||||
# Each check must fail on a mutant. For each mutant this runs one named test file against the
|
||||
# real source (control) and against the source with one exact-once text edit, in the given worktree,
|
||||
# one test file at a time, and restores every source file on exit.
|
||||
# Usage: mutants.sh <worktree> <outdir> (MUTANT_DRY=1 only proves every edit matches exactly once)
|
||||
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
|
||||
if [ -n "${MUTANT_DRY:-}" ]; then echo "$1: edit ok (dry)"; return; fi
|
||||
setsid -w timeout -k 10 400 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; }
|
||||
diff -u "$BAK/$3" "$3" > "$OUT/$1.patch"
|
||||
run "$1" "$2"
|
||||
cp "$BAK/$3" "$3"
|
||||
}
|
||||
CS=packages/bus/tests/chief-of-staff.test.mjs; BCS=packages/business/tests/chief-of-staff.test.mjs
|
||||
B=packages/bus/src/broker.mjs; V=packages/business/src/vocabulary.mjs; C=roles/chief-of-staff.json
|
||||
for t in "$CS" "$BCS"; do run "control-$(basename "$(dirname "$(dirname "$t")")")" "$t"; done
|
||||
|
||||
# role.end (REQ-ROLE-2, REQ-COS-1)
|
||||
# m1: the reason form skips the authority check, so any caller may end a run with a reason
|
||||
mutant m1-role-end-unchecked "$CS" "$B" "this.#consumeAuthority(s, 'role.end', { target: a.role }, { reason: a.reason });" "void 0;"
|
||||
# m2: a reason and a decision together are accepted
|
||||
mutant m2-reason-and-decision-accepted "$CS" "$B" "if ('decision' in a) fail('invalid-request');" "void 0;"
|
||||
# m3: the reason has no length bound
|
||||
mutant m3-reason-unbounded "$CS" "$B" "string(a.reason, 500);" "string(a.reason, 100000);"
|
||||
# m4: the chief may end its own run
|
||||
mutant m4-self-end-allowed "$CS" "$B" "if (a.role === s.role || END_PROTECTED" "if (END_PROTECTED"
|
||||
# m5: the chief may end the assistant's run
|
||||
mutant m5-assistant-end-allowed "$CS" "$B" "END_PROTECTED.includes(b.roles[a.role]?.definition)" "false"
|
||||
# m6: the reason is not written to role_claims.reason
|
||||
mutant m6-reason-not-stored "$CS" "$B" "this.#claimEnd(s, h, 'revoke', a.decision ?? null, a.reason ?? null);" "this.#claimEnd(s, h, 'revoke', a.decision ?? null, null);"
|
||||
# m7: the reason is not in the action.allowed body
|
||||
mutant m7-reason-not-logged "$CS" "$B" "{ target: a.role }, { reason: a.reason });" "{ target: a.role }, {});"
|
||||
# m8: the decision path checks role.end instead of role.revoke, so another role's decision no longer ends a run
|
||||
mutant m8-decision-path-wrong-action "$CS" "$B" "this.#consumeAuthority(s, 'role.revoke', { decision: a.decision, target: a.role });" "this.#consumeAuthority(s, 'role.end', { decision: a.decision, target: a.role });"
|
||||
|
||||
# organization.set (REQ-LIMIT-1, REQ-ROLE-3, REQ-COS-1)
|
||||
# m9: organization.set skips the authority check, so any agent may change the file
|
||||
mutant m9-organization-set-unchecked "$CS" "$B" 'this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));' "void 0;"
|
||||
# m10: organization.set accepts a key beyond the write path (launch)
|
||||
mutant m10-key-list-widened "$CS" "$B" "const ORGANIZATION_KEYS = ['revision', 'limits', 'models', 'instances'];" "const ORGANIZATION_KEYS = ['revision', 'limits', 'models', 'instances', 'launch'];"
|
||||
# m11: the revision is optional
|
||||
mutant m11-revision-optional "$CS" "$B" 'else keys(args, ORGANIZATION_KEYS, ["revision"]);' "else keys(args, ORGANIZATION_KEYS, []);"
|
||||
# m12: the writer is told the wrong actor
|
||||
mutant m12-wrong-actor "$CS" "$B" 'actor = this.#business(s.business).roles[s.role].definition + "." + s.role;' "actor = s.role;"
|
||||
# m13: limits and limits.set stop being human-only
|
||||
mutant m13-limits-open-to-agents "$CS" "$B" 'if (!role && !s.human) fail("human-required");' "void 0;"
|
||||
# m14: a validation failure from the shared writer no longer maps to invalid-request
|
||||
mutant m14-invalid-change-not-refused "$CS" "$B" 'if (e.name === "BusinessError" || e.code === "invalid-request"' 'if (e.name === "NoSuchError" || e.code === "invalid-request"'
|
||||
|
||||
# vocabulary and the shipped role file (REQ-COS-1: role.revoke stays gated for every role)
|
||||
# m15: role.end leaves the action vocabulary
|
||||
mutant m15-vocabulary-drops-role-end "$BCS" "$V" ' "role.end",
|
||||
' ''
|
||||
# m16: the shipped chief file drops its role.end grant
|
||||
mutant m16-chief-file-drops-role-end "$CS" "$C" '"role.launch", "role.end", "organization.change"' '"role.launch", "organization.change"'
|
||||
# m17: the shipped chief file drops its organization.change grant
|
||||
mutant m17-chief-file-drops-organization-change "$CS" "$C" '"role.end", "organization.change"' '"role.end"'
|
||||
# m18: the shipped chief file lists role.revoke as within-role
|
||||
mutant m18-chief-file-grants-role-revoke "$BCS" "$C" '"role.launch", "role.end"' '"role.launch", "role.revoke", "role.end"'
|
||||
|
||||
# schema (REQ-ROLE-2): the 3c→3d trigger. A schema edit must keep the pinned digest honest, so each of these
|
||||
# edits schema.sql and the 3c-3d step the same way and re-seals SCHEMA_SHA256 from the edited schema.sql.
|
||||
SQ=packages/bus/schema.sql; ST=packages/bus/src/store.mjs; MG=packages/bus/src/migrations/3c-3d.sql
|
||||
reseal() {
|
||||
save "$ST"
|
||||
local sha; sha=$(sha256sum "$SQ" | cut -d' ' -f1)
|
||||
node -e '
|
||||
const fs = require("fs"); const [f, sha] = process.argv.slice(1);
|
||||
const s = fs.readFileSync(f, "utf8");
|
||||
fs.writeFileSync(f, s.replace(/export const SCHEMA_SHA256 = \x27[a-f0-9]+\x27;/, "export const SCHEMA_SHA256 = \x27" + sha + "\x27;"));
|
||||
' "$ST" "$sha"
|
||||
}
|
||||
schema_mutant() { # name testfile old new
|
||||
edit "$SQ" "$3" "$4" && edit "$MG" "$3" "$4" || { echo "$1: edit failed"; return; }
|
||||
reseal
|
||||
{ diff -u "$BAK/$SQ" "$SQ"; diff -u "$BAK/$MG" "$MG"; diff -u "$BAK/$ST" "$ST"; } > "$OUT/$1.patch"
|
||||
run "$1" "$2"
|
||||
for f in "$SQ" "$MG" "$ST"; do cp "$BAK/$f" "$f"; done
|
||||
}
|
||||
# m19: the trigger goes back to the 3c rule (a revoke needs a decision), so a reason alone is refused by storage
|
||||
schema_mutant m19-trigger-decision-only "$CS" "WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL" "WHEN NEW.op = 'revoke' AND NEW.decision IS NULL"
|
||||
# m20: the trigger never fires, so a revoke with neither a decision nor a reason is accepted
|
||||
schema_mutant m20-trigger-never-fires "$CS" "WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL" "WHEN 0"
|
||||
# m21: the 3c→3d step is a no-op on the trigger: the shipped schema has it, a migrated 3c database keeps the old rule.
|
||||
# (Run against the migration test only; the step file keeps its text but the DROP/CREATE are removed, so the engine
|
||||
# refuses at import with migration-target-mismatch. That refusal is the check: a step that does not reach the
|
||||
# pinned schema cannot ship.)
|
||||
save "$MG"; : > "$MG"
|
||||
{ diff -u "$BAK/$MG" "$MG"; } > "$OUT/m21-step-empty.patch"
|
||||
run m21-step-empty "$CS"
|
||||
cp "$BAK/$MG" "$MG"
|
||||
@@ -0,0 +1,20 @@
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (17.772772ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.727244ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.336202ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.45774ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.033773ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.621307ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.690826ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.397125ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.251123ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (18.049647ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (44.96479ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (40.076384ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 12
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 280.24687
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1,18 @@
|
||||
✔ the role file loads and resolves through loadBusiness (4.647682ms)
|
||||
✔ the shipped file is exactly the approved list (0.153555ms)
|
||||
✔ the resolved record matches the approved list, so a widened file fails (1.17741ms)
|
||||
✔ the chief of staff holds role.launch, message.send, role.end and organization.change; every other action is gated (1.059054ms)
|
||||
✔ the pm does not hold role.launch (1.0778ms)
|
||||
✔ a business grant of a gated action through limits does not widen the role (1.095518ms)
|
||||
✔ a limit that drops role.launch leaves the chief of staff with no launch block (1.036977ms)
|
||||
✔ a launch block whose by is the pm instance is refused (1.074436ms)
|
||||
✔ the contract names no duty to talk to the human and the pm contract no launch (0.260955ms)
|
||||
✔ the instance takes no credential references and no tracker (1.45996ms)
|
||||
ℹ tests 10
|
||||
ℹ suites 0
|
||||
ℹ pass 10
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 64.41285
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1,70 @@
|
||||
✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (16.511335ms)
|
||||
✖ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (12.039358ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.971595ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.862498ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.833213ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.256894ms)
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.584527ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.14651ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (13.819842ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (16.907204ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (41.228719ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (40.346079ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 9
|
||||
ℹ fail 3
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 274.510753
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:80:1
|
||||
✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (16.511335ms)
|
||||
TypeError: Cannot read properties of undefined (reading 'actor_role')
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:88:22)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.start (node:internal/test_runner/test:1262:17)
|
||||
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17)
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:98:1
|
||||
✖ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (12.039358ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: pm
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:101:12)
|
||||
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: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
expected: /decision-required/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:167:1
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.584527ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
|
||||
0 !== 1
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:178: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: 0,
|
||||
expected: 1,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/broker.mjs 2026-10-15 11:27:01.818643237 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 11:27:01.835458515 -0500
|
||||
@@ -827,7 +827,7 @@
|
||||
if ('reason' in a) {
|
||||
if ('decision' in a) fail('invalid-request');
|
||||
string(a.reason, 500);
|
||||
- this.#consumeAuthority(s, 'role.end', { target: a.role }, { reason: a.reason });
|
||||
+ void 0;
|
||||
const b = this.#business(s.business);
|
||||
if (a.role === s.role || END_PROTECTED.includes(b.roles[a.role]?.definition)) fail('role-end-refused');
|
||||
} else {
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,40 @@
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (17.048234ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (17.563411ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.052712ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (11.31737ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.710263ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.56242ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (14.184867ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (11.116329ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.125113ms)
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (11.421403ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (41.626117ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (43.481986ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 280.681256
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:206:1
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (11.421403ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: chief launch
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:225:14)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
expected: /invalid-request/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/broker.mjs 2026-10-15 11:27:01.818643237 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 11:27:04.826487819 -0500
|
||||
@@ -96,7 +96,7 @@
|
||||
// A git commit id, SHA-1 or SHA-256.
|
||||
const oid = (x) => typeof x === 'string' && /^[0-9a-f]{40}([0-9a-f]{24})?$/.test(x);
|
||||
// Row 123 (#1617): the one list of organization-file keys limits.set and organization.set accept.
|
||||
-const ORGANIZATION_KEYS = ['revision', 'limits', 'models', 'instances'];
|
||||
+const ORGANIZATION_KEYS = ['revision', 'limits', 'models', 'instances', 'launch'];
|
||||
// Row 123 (#1617): role.end never reaches the assistant (the user's seat) or the caller's own run.
|
||||
const END_PROTECTED = ['assistant'];
|
||||
const VERDICTS = ['approve', 'changes'];
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,40 @@
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (17.526377ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (15.752169ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (12.286669ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (11.416717ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.691689ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.13766ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.4308ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.106255ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (13.664858ms)
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (17.573152ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (42.829307ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (41.116106ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 280.859438
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:206:1
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (17.573152ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:230:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
expected: /invalid-request/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/broker.mjs 2026-10-15 11:27:01.818643237 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 11:27:05.163491123 -0500
|
||||
@@ -729,7 +729,7 @@
|
||||
if (!role && !s.human) fail("human-required");
|
||||
if (!this.#organization) fail("not-configured");
|
||||
if (request.verb === "limits") keys(args, []);
|
||||
- else keys(args, ORGANIZATION_KEYS, ["revision"]);
|
||||
+ else keys(args, ORGANIZATION_KEYS, []);
|
||||
let actor = s.human;
|
||||
if (role) {
|
||||
this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,76 @@
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (16.060801ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (15.076802ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.147256ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.179528ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (11.725069ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.831721ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.170772ms)
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.933856ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (13.906404ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (16.945879ms)
|
||||
✖ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (28.564801ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (41.410438ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 10
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 259.680774
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:181:1
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.933856ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
... Skipped lines
|
||||
|
||||
[
|
||||
[
|
||||
'demo',
|
||||
'limits.set',
|
||||
{
|
||||
...
|
||||
},
|
||||
+ 'chief'
|
||||
- 'chief-of-staff.chief'
|
||||
]
|
||||
]
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:186: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: [ [ 'demo', 'limits.set', [Object], 'chief' ] ],
|
||||
expected: [ [ 'demo', 'limits.set', [Object], 'chief-of-staff.chief' ] ],
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:234:1
|
||||
✖ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (28.564801ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'chief'
|
||||
- 'chief-of-staff.chief'
|
||||
^
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:271:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'chief',
|
||||
expected: 'chief-of-staff.chief',
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/broker.mjs 2026-10-15 11:27:01.818643237 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 11:27:05.495494377 -0500
|
||||
@@ -733,7 +733,7 @@
|
||||
let actor = s.human;
|
||||
if (role) {
|
||||
this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));
|
||||
- actor = this.#business(s.business).roles[s.role].definition + "." + s.role;
|
||||
+ actor = s.role;
|
||||
}
|
||||
let result;
|
||||
try {
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,40 @@
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (16.982484ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (17.526752ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.731084ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (11.758238ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (17.094692ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (17.047739ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (15.006381ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (11.070735ms)
|
||||
✖ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (13.686376ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (17.740788ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (42.642907ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (38.54837ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 294.224777
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:191:1
|
||||
✖ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (13.686376ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: limits
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:202:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
expected: /human-required/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/broker.mjs 2026-10-15 11:27:01.818643237 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 11:27:05.804497407 -0500
|
||||
@@ -726,7 +726,7 @@
|
||||
// file lists organization.change. It takes the same keys, runs the same write path and
|
||||
// logs the same receipt; only the actor differs: the role definition and instance.
|
||||
const role = request.verb === "organization.set";
|
||||
- if (!role && !s.human) fail("human-required");
|
||||
+ void 0;
|
||||
if (!this.#organization) fail("not-configured");
|
||||
if (request.verb === "limits") keys(args, []);
|
||||
else keys(args, ORGANIZATION_KEYS, ["revision"]);
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,45 @@
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (16.834638ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.796337ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.813996ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (11.262626ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.618592ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.567433ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.266849ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.283475ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.147234ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (17.123186ms)
|
||||
✖ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (29.996918ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (40.005864ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 266.340199
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:234:1
|
||||
✖ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (29.996918ms)
|
||||
AssertionError [ERR_ASSERTION]: {"revision":"e43371ee12abd3a693fb49900f44858fc76e4e2c36afe52d6c14ac13656bd007","limits":{"total":-1}}
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:288:12)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: Error: outcome-unknown
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:747:17)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:289:20
|
||||
at getActual (node:assert:580:5)
|
||||
at strict.throws (node:assert:728:24)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:288:12)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7),
|
||||
expected: /invalid-request|unknown-model/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/broker.mjs 2026-10-15 11:27:01.818643237 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 11:27:06.150500800 -0500
|
||||
@@ -741,7 +741,7 @@
|
||||
} catch (e) {
|
||||
if (e.code === "revision-conflict") fail("revision-conflict");
|
||||
if (e.code?.startsWith("organization-")) fail(e.code);
|
||||
- if (e.name === "BusinessError" || e.code === "invalid-request" || e.code === "unknown-model") {
|
||||
+ if (e.name === "NoSuchError" || e.code === "invalid-request" || e.code === "unknown-model") {
|
||||
fail("invalid-request");
|
||||
}
|
||||
throw new BusError("outcome-unknown");
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,164 @@
|
||||
✖ the role file loads and resolves through loadBusiness (3.247294ms)
|
||||
✔ the shipped file is exactly the approved list (0.560302ms)
|
||||
✖ the resolved record matches the approved list, so a widened file fails (0.892565ms)
|
||||
✖ the chief of staff holds role.launch, message.send, role.end and organization.change; every other action is gated (0.809401ms)
|
||||
✖ the pm does not hold role.launch (0.809484ms)
|
||||
✖ a business grant of a gated action through limits does not widen the role (0.704092ms)
|
||||
✖ a limit that drops role.launch leaves the chief of staff with no launch block (0.716759ms)
|
||||
✖ a launch block whose by is the pm instance is refused (1.26737ms)
|
||||
✔ the contract names no duty to talk to the human and the pm contract no launch (0.235726ms)
|
||||
✖ the instance takes no credential references and no tracker (1.130019ms)
|
||||
ℹ tests 10
|
||||
ℹ suites 0
|
||||
ℹ pass 2
|
||||
ℹ fail 8
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 62.867539
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:49:1
|
||||
✖ the role file loads and resolves through loadBusiness (3.247294ms)
|
||||
Error [BusinessError]: unknown action in authority.withinRole: "role.end"
|
||||
at refuse (file:///mnt/storage/scratch/researcher/q123/packages/business/src/errors.mjs:12:9)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:40:38
|
||||
at requireDistinctList (file:///mnt/storage/scratch/researcher/q123/packages/business/src/util.mjs:46:5)
|
||||
at checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:39:18)
|
||||
at validateRoleDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:153:16)
|
||||
at loadRoleFile (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:160:10)
|
||||
at loadRole (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:165:10)
|
||||
at checkRoles (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:87:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:149:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:197:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:76:1
|
||||
✖ the resolved record matches the approved list, so a widened file fails (0.892565ms)
|
||||
Error [BusinessError]: unknown action in authority.withinRole: "role.end"
|
||||
at refuse (file:///mnt/storage/scratch/researcher/q123/packages/business/src/errors.mjs:12:9)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:40:38
|
||||
at requireDistinctList (file:///mnt/storage/scratch/researcher/q123/packages/business/src/util.mjs:46:5)
|
||||
at checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:39:18)
|
||||
at validateRoleDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:153:16)
|
||||
at loadRoleFile (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:160:10)
|
||||
at loadRole (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:165:10)
|
||||
at checkRoles (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:87:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:149:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:197:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:86:1
|
||||
✖ the chief of staff holds role.launch, message.send, role.end and organization.change; every other action is gated (0.809401ms)
|
||||
Error [BusinessError]: unknown action in authority.withinRole: "role.end"
|
||||
at refuse (file:///mnt/storage/scratch/researcher/q123/packages/business/src/errors.mjs:12:9)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:40:38
|
||||
at requireDistinctList (file:///mnt/storage/scratch/researcher/q123/packages/business/src/util.mjs:46:5)
|
||||
at checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:39:18)
|
||||
at validateRoleDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:153:16)
|
||||
at loadRoleFile (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:160:10)
|
||||
at loadRole (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:165:10)
|
||||
at checkRoles (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:87:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:149:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:197:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:97:1
|
||||
✖ the pm does not hold role.launch (0.809484ms)
|
||||
Error [BusinessError]: unknown action in authority.withinRole: "role.end"
|
||||
at refuse (file:///mnt/storage/scratch/researcher/q123/packages/business/src/errors.mjs:12:9)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:40:38
|
||||
at requireDistinctList (file:///mnt/storage/scratch/researcher/q123/packages/business/src/util.mjs:46:5)
|
||||
at checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:39:18)
|
||||
at validateRoleDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:153:16)
|
||||
at loadRoleFile (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:160:10)
|
||||
at loadRole (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:165:10)
|
||||
at checkRoles (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:87:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:149:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:197:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:106:1
|
||||
✖ a business grant of a gated action through limits does not widen the role (0.704092ms)
|
||||
Error [BusinessError]: unknown action in authority.withinRole: "role.end"
|
||||
at refuse (file:///mnt/storage/scratch/researcher/q123/packages/business/src/errors.mjs:12:9)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:40:38
|
||||
at requireDistinctList (file:///mnt/storage/scratch/researcher/q123/packages/business/src/util.mjs:46:5)
|
||||
at checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:39:18)
|
||||
at validateRoleDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:153:16)
|
||||
at loadRoleFile (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:160:10)
|
||||
at loadRole (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:165:10)
|
||||
at checkRoles (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:87:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:149:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:197:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:118:1
|
||||
✖ a limit that drops role.launch leaves the chief of staff with no launch block (0.716759ms)
|
||||
Error [BusinessError]: unknown action in authority.withinRole: "role.end"
|
||||
at refuse (file:///mnt/storage/scratch/researcher/q123/packages/business/src/errors.mjs:12:9)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:40:38
|
||||
at requireDistinctList (file:///mnt/storage/scratch/researcher/q123/packages/business/src/util.mjs:46:5)
|
||||
at checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:39:18)
|
||||
at validateRoleDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:153:16)
|
||||
at loadRoleFile (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:160:10)
|
||||
at loadRole (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:165:10)
|
||||
at checkRoles (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:87:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:149:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:197:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:126:1
|
||||
✖ a launch block whose by is the pm instance is refused (1.26737ms)
|
||||
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /launch\.by names pm, whose role pm doesn't hold role\.launch within-role/. Input:
|
||||
|
||||
'unknown action in authority.withinRole: "role.end"'
|
||||
|
||||
at Object.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/business/tests/chief-of-staff.test.mjs:30:12)
|
||||
at expectedException (node:assert:547:17)
|
||||
at expectsError (node:assert:672:21)
|
||||
at strict.throws (node:assert:728:3)
|
||||
at refuses (file:///mnt/storage/scratch/researcher/q123/packages/business/tests/chief-of-staff.test.mjs:27:10)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/business/tests/chief-of-staff.test.mjs:127:3)
|
||||
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) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'unknown action in authority.withinRole: "role.end"',
|
||||
expected: /launch\.by names pm, whose role pm doesn't hold role\.launch within-role/,
|
||||
operator: 'match',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:145:1
|
||||
✖ the instance takes no credential references and no tracker (1.130019ms)
|
||||
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /must reference exactly the services its role definition needs \(none; got gitea\)/. Input:
|
||||
|
||||
'unknown action in authority.withinRole: "role.end"'
|
||||
|
||||
at Object.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/business/tests/chief-of-staff.test.mjs:30:12)
|
||||
at expectedException (node:assert:547:17)
|
||||
at expectsError (node:assert:672:21)
|
||||
at strict.throws (node:assert:728:3)
|
||||
at refuses (file:///mnt/storage/scratch/researcher/q123/packages/business/tests/chief-of-staff.test.mjs:27:10)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/business/tests/chief-of-staff.test.mjs:146:3)
|
||||
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) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'unknown action in authority.withinRole: "role.end"',
|
||||
expected: /must reference exactly the services its role definition needs \(none; got gitea\)/,
|
||||
operator: 'match',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/business/src/vocabulary.mjs 2026-10-15 11:27:06.452804217 -0500
|
||||
+++ packages/business/src/vocabulary.mjs 2026-10-15 11:27:06.472503957 -0500
|
||||
@@ -29,7 +29,6 @@
|
||||
// Row 123 (#1617, REQ-COS-1): the chief of staff ends a run with a reason and no decision,
|
||||
// and changes the organization file through the shared write path. Neither is gated-only,
|
||||
// so the grant lives in a role file; role.revoke itself stays gated for every role.
|
||||
- "role.end",
|
||||
"organization.change",
|
||||
"credential.mint",
|
||||
"spend",
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,98 @@
|
||||
✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (15.921558ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (17.268547ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.705163ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.788134ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.589161ms)
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.10088ms)
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (10.684979ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.603405ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.519929ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (19.843937ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (41.306378ms)
|
||||
✖ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (40.683794ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 8
|
||||
ℹ fail 4
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 279.354048
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:80:1
|
||||
✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (15.921558ms)
|
||||
Error: decision-required
|
||||
at fail (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:78:9)
|
||||
at #checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:458:20)
|
||||
at #consumeAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:484:40)
|
||||
at #dispatch (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:830:33)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:758:38
|
||||
at Store.transaction (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:217:22)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:752:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:68:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:82:20)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14) {
|
||||
code: 'decision-required'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:152:1
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.10088ms)
|
||||
Error: decision-required
|
||||
at fail (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:78:9)
|
||||
at #checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:458:20)
|
||||
at #consumeAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:484:40)
|
||||
at #dispatch (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:830:33)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:758:38
|
||||
at Store.transaction (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:217:22)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:752:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:68:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:164:20)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14) {
|
||||
code: 'decision-required'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:167:1
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (10.684979ms)
|
||||
AssertionError [ERR_ASSERTION]: chief
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:170:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: Error: decision-required
|
||||
at fail (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:78:9)
|
||||
at #checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:458:20)
|
||||
at #consumeAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:484:40)
|
||||
at #dispatch (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:830:33)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:758:38
|
||||
at Store.transaction (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:217:22)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:752:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:68:44)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:170:25
|
||||
at getActual (node:assert:580:5),
|
||||
expected: /role-end-refused/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:343:1
|
||||
✖ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (40.683794ms)
|
||||
Error: decision-required
|
||||
at fail (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:78:9)
|
||||
at #checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:458:20)
|
||||
at #consumeAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:484:40)
|
||||
at #dispatch (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:830:33)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:758:38
|
||||
at Store.transaction (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:217:22)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:752:26)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:360:22)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7) {
|
||||
code: 'decision-required'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/roles/chief-of-staff.json 2026-10-15 11:27:06.572159185 -0500
|
||||
+++ roles/chief-of-staff.json 2026-10-15 11:27:06.589505105 -0500
|
||||
@@ -6,7 +6,7 @@
|
||||
"tools": ["read", "grep", "find", "ls"],
|
||||
"network": "api-only",
|
||||
"authority": {
|
||||
- "withinRole": ["message.send", "role.launch", "role.end", "organization.change"],
|
||||
+ "withinRole": ["message.send", "role.launch", "organization.change"],
|
||||
"crossRole": []
|
||||
},
|
||||
"credentials": []
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,70 @@
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (19.4111ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.230588ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.582657ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.406735ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.4052ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.470159ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.723245ms)
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.300496ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.498151ms)
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (10.497898ms)
|
||||
✖ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (22.935509ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (39.385592ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 9
|
||||
ℹ fail 3
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 254.03076
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:181:1
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.300496ms)
|
||||
Error: decision-required
|
||||
at fail (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:78:9)
|
||||
at #checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:458:20)
|
||||
at #consumeAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:484:40)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:735:63
|
||||
at Store.transaction (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:217:22)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:735:23)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:68:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:183:16)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25) {
|
||||
code: 'decision-required'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:206:1
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (10.497898ms)
|
||||
Error: decision-required
|
||||
at fail (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:78:9)
|
||||
at #checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:458:20)
|
||||
at #consumeAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:484:40)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:735:63
|
||||
at Store.transaction (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:217:22)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:735:23)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:68:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:208:3)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25) {
|
||||
code: 'decision-required'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:234:1
|
||||
✖ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (22.935509ms)
|
||||
Error: decision-required
|
||||
at fail (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:78:9)
|
||||
at #checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:458:20)
|
||||
at #consumeAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:484:40)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:735:63
|
||||
at Store.transaction (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:217:22)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:735:23)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:263:26)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
code: 'decision-required'
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/roles/chief-of-staff.json 2026-10-15 11:27:06.572159185 -0500
|
||||
+++ roles/chief-of-staff.json 2026-10-15 11:27:06.918508331 -0500
|
||||
@@ -6,7 +6,7 @@
|
||||
"tools": ["read", "grep", "find", "ls"],
|
||||
"network": "api-only",
|
||||
"authority": {
|
||||
- "withinRole": ["message.send", "role.launch", "role.end", "organization.change"],
|
||||
+ "withinRole": ["message.send", "role.launch", "role.end"],
|
||||
"crossRole": []
|
||||
},
|
||||
"credentials": []
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,197 @@
|
||||
✖ the role file loads and resolves through loadBusiness (3.384721ms)
|
||||
✖ the shipped file is exactly the approved list (1.326853ms)
|
||||
✖ the resolved record matches the approved list, so a widened file fails (0.941869ms)
|
||||
✖ the chief of staff holds role.launch, message.send, role.end and organization.change; every other action is gated (0.833954ms)
|
||||
✖ the pm does not hold role.launch (0.900791ms)
|
||||
✖ a business grant of a gated action through limits does not widen the role (0.767866ms)
|
||||
✖ a limit that drops role.launch leaves the chief of staff with no launch block (0.745581ms)
|
||||
✖ a launch block whose by is the pm instance is refused (1.076913ms)
|
||||
✔ the contract names no duty to talk to the human and the pm contract no launch (0.527782ms)
|
||||
✖ the instance takes no credential references and no tracker (0.891961ms)
|
||||
ℹ tests 10
|
||||
ℹ suites 0
|
||||
ℹ pass 1
|
||||
ℹ fail 9
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 63.560084
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:49:1
|
||||
✖ the role file loads and resolves through loadBusiness (3.384721ms)
|
||||
Error [BusinessError]: authority.withinRole lists role.revoke, which is always gated
|
||||
at refuse (file:///mnt/storage/scratch/researcher/q123/packages/business/src/errors.mjs:12:9)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:41:40
|
||||
at requireDistinctList (file:///mnt/storage/scratch/researcher/q123/packages/business/src/util.mjs:46:5)
|
||||
at checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:39:18)
|
||||
at validateRoleDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:153:16)
|
||||
at loadRoleFile (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:160:10)
|
||||
at loadRole (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:165:10)
|
||||
at checkRoles (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:87:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:149:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:197:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:64:1
|
||||
✖ the shipped file is exactly the approved list (1.326853ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
{
|
||||
authority: {
|
||||
crossRole: [],
|
||||
withinRole: [
|
||||
'message.send',
|
||||
'role.launch',
|
||||
+ 'role.revoke',
|
||||
'role.end',
|
||||
'organization.change'
|
||||
]
|
||||
},
|
||||
contract: 'chief-of-staff.md',
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/business/tests/chief-of-staff.test.mjs:68: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: { roleVersion: 2, name: 'chief-of-staff', contract: 'chief-of-staff.md', tools: [ 'read', 'grep', 'find', 'ls' ], network: 'api-only', authority: { withinRole: [Array], crossRole: [] }, credentials: [] },
|
||||
expected: { roleVersion: 2, name: 'chief-of-staff', contract: 'chief-of-staff.md', tools: [ 'read', 'grep', 'find', 'ls' ], network: 'api-only', authority: { withinRole: [Array], crossRole: [] }, credentials: [] },
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:76:1
|
||||
✖ the resolved record matches the approved list, so a widened file fails (0.941869ms)
|
||||
Error [BusinessError]: authority.withinRole lists role.revoke, which is always gated
|
||||
at refuse (file:///mnt/storage/scratch/researcher/q123/packages/business/src/errors.mjs:12:9)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:41:40
|
||||
at requireDistinctList (file:///mnt/storage/scratch/researcher/q123/packages/business/src/util.mjs:46:5)
|
||||
at checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:39:18)
|
||||
at validateRoleDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:153:16)
|
||||
at loadRoleFile (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:160:10)
|
||||
at loadRole (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:165:10)
|
||||
at checkRoles (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:87:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:149:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:197:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:86:1
|
||||
✖ the chief of staff holds role.launch, message.send, role.end and organization.change; every other action is gated (0.833954ms)
|
||||
Error [BusinessError]: authority.withinRole lists role.revoke, which is always gated
|
||||
at refuse (file:///mnt/storage/scratch/researcher/q123/packages/business/src/errors.mjs:12:9)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:41:40
|
||||
at requireDistinctList (file:///mnt/storage/scratch/researcher/q123/packages/business/src/util.mjs:46:5)
|
||||
at checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:39:18)
|
||||
at validateRoleDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:153:16)
|
||||
at loadRoleFile (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:160:10)
|
||||
at loadRole (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:165:10)
|
||||
at checkRoles (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:87:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:149:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:197:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:97:1
|
||||
✖ the pm does not hold role.launch (0.900791ms)
|
||||
Error [BusinessError]: authority.withinRole lists role.revoke, which is always gated
|
||||
at refuse (file:///mnt/storage/scratch/researcher/q123/packages/business/src/errors.mjs:12:9)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:41:40
|
||||
at requireDistinctList (file:///mnt/storage/scratch/researcher/q123/packages/business/src/util.mjs:46:5)
|
||||
at checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:39:18)
|
||||
at validateRoleDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:153:16)
|
||||
at loadRoleFile (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:160:10)
|
||||
at loadRole (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:165:10)
|
||||
at checkRoles (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:87:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:149:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:197:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:106:1
|
||||
✖ a business grant of a gated action through limits does not widen the role (0.767866ms)
|
||||
Error [BusinessError]: authority.withinRole lists role.revoke, which is always gated
|
||||
at refuse (file:///mnt/storage/scratch/researcher/q123/packages/business/src/errors.mjs:12:9)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:41:40
|
||||
at requireDistinctList (file:///mnt/storage/scratch/researcher/q123/packages/business/src/util.mjs:46:5)
|
||||
at checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:39:18)
|
||||
at validateRoleDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:153:16)
|
||||
at loadRoleFile (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:160:10)
|
||||
at loadRole (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:165:10)
|
||||
at checkRoles (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:87:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:149:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:197:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:118:1
|
||||
✖ a limit that drops role.launch leaves the chief of staff with no launch block (0.745581ms)
|
||||
Error [BusinessError]: authority.withinRole lists role.revoke, which is always gated
|
||||
at refuse (file:///mnt/storage/scratch/researcher/q123/packages/business/src/errors.mjs:12:9)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:41:40
|
||||
at requireDistinctList (file:///mnt/storage/scratch/researcher/q123/packages/business/src/util.mjs:46:5)
|
||||
at checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:39:18)
|
||||
at validateRoleDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:153:16)
|
||||
at loadRoleFile (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:160:10)
|
||||
at loadRole (file:///mnt/storage/scratch/researcher/q123/packages/business/src/role.mjs:165:10)
|
||||
at checkRoles (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:87:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:149:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:197:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:126:1
|
||||
✖ a launch block whose by is the pm instance is refused (1.076913ms)
|
||||
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /launch\.by names pm, whose role pm doesn't hold role\.launch within-role/. Input:
|
||||
|
||||
'authority.withinRole lists role.revoke, which is always gated'
|
||||
|
||||
at Object.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/business/tests/chief-of-staff.test.mjs:30:12)
|
||||
at expectedException (node:assert:547:17)
|
||||
at expectsError (node:assert:672:21)
|
||||
at strict.throws (node:assert:728:3)
|
||||
at refuses (file:///mnt/storage/scratch/researcher/q123/packages/business/tests/chief-of-staff.test.mjs:27:10)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/business/tests/chief-of-staff.test.mjs:127:3)
|
||||
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) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'authority.withinRole lists role.revoke, which is always gated',
|
||||
expected: /launch\.by names pm, whose role pm doesn't hold role\.launch within-role/,
|
||||
operator: 'match',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:145:1
|
||||
✖ the instance takes no credential references and no tracker (0.891961ms)
|
||||
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /must reference exactly the services its role definition needs \(none; got gitea\)/. Input:
|
||||
|
||||
'authority.withinRole lists role.revoke, which is always gated'
|
||||
|
||||
at Object.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/business/tests/chief-of-staff.test.mjs:30:12)
|
||||
at expectedException (node:assert:547:17)
|
||||
at expectsError (node:assert:672:21)
|
||||
at strict.throws (node:assert:728:3)
|
||||
at refuses (file:///mnt/storage/scratch/researcher/q123/packages/business/tests/chief-of-staff.test.mjs:27:10)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/business/tests/chief-of-staff.test.mjs:146:3)
|
||||
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) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'authority.withinRole lists role.revoke, which is always gated',
|
||||
expected: /must reference exactly the services its role definition needs \(none; got gitea\)/,
|
||||
operator: 'match',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/roles/chief-of-staff.json 2026-10-15 11:27:06.572159185 -0500
|
||||
+++ roles/chief-of-staff.json 2026-10-15 11:27:07.223511323 -0500
|
||||
@@ -6,7 +6,7 @@
|
||||
"tools": ["read", "grep", "find", "ls"],
|
||||
"network": "api-only",
|
||||
"authority": {
|
||||
- "withinRole": ["message.send", "role.launch", "role.end", "organization.change"],
|
||||
+ "withinRole": ["message.send", "role.launch", "role.revoke", "role.end", "organization.change"],
|
||||
"crossRole": []
|
||||
},
|
||||
"credentials": []
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,98 @@
|
||||
✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (17.642412ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (17.54841ms)
|
||||
✖ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.332393ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.243966ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.205348ms)
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.204748ms)
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.206405ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.617023ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (13.707567ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (17.34257ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (42.250197ms)
|
||||
✖ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (41.357652ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 7
|
||||
ℹ fail 5
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 280.565048
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:80:1
|
||||
✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (17.642412ms)
|
||||
Error: storage-refused
|
||||
at #refused (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:696:47)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:763:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:68:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:82:20)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.start (node:internal/test_runner/test:1262:17)
|
||||
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
|
||||
code: 'storage-refused'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:112:1
|
||||
✖ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.332393ms)
|
||||
Error: revoke needs a resolved decision or a reason
|
||||
at Store.run (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:196:34)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:195:66
|
||||
at Store.transaction (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:217:22)
|
||||
at Store.run (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:195:43)
|
||||
at insert (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:117:11)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:123:3)
|
||||
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) {
|
||||
code: 'ERR_SQLITE_ERROR',
|
||||
errcode: 1811,
|
||||
errstr: 'constraint failed'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:152:1
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.204748ms)
|
||||
Error: storage-refused
|
||||
at #refused (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:696:47)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:763:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:68:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:164:20)
|
||||
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) {
|
||||
code: 'storage-refused'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:167:1
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.206405ms)
|
||||
Error: storage-refused
|
||||
at #refused (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:696:47)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:763:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:68:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:176:3)
|
||||
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) {
|
||||
code: 'storage-refused'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:343:1
|
||||
✖ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (41.357652ms)
|
||||
Error: storage-refused
|
||||
at #refused (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:696:47)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:763:26)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:360:22)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
code: 'storage-refused'
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/schema.sql 2026-10-15 11:27:07.327315441 -0500
|
||||
+++ packages/bus/schema.sql 2026-10-15 11:27:07.345512520 -0500
|
||||
@@ -124,7 +124,7 @@
|
||||
WHEN NEW.op = 'release' AND (SELECT holder_run FROM role_claims WHERE business = NEW.business AND role = NEW.role ORDER BY seq DESC LIMIT 1) <> NEW.holder_run
|
||||
BEGIN SELECT RAISE(ABORT, 'only the holder releases; others revoke'); END;
|
||||
CREATE TRIGGER role_revoke_needs_decision BEFORE INSERT ON role_claims
|
||||
- WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL
|
||||
+ WHEN NEW.op = 'revoke' AND NEW.decision IS NULL
|
||||
BEGIN SELECT RAISE(ABORT, 'revoke needs a resolved decision or a reason'); END;
|
||||
CREATE TRIGGER meta_no_update BEFORE UPDATE ON meta BEGIN SELECT RAISE(ABORT, 'meta is append-only'); END;
|
||||
CREATE TRIGGER meta_no_delete BEFORE DELETE ON meta BEGIN SELECT RAISE(ABORT, 'meta is append-only'); END;
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/migrations/3c-3d.sql 2026-10-15 11:27:07.349924931 -0500
|
||||
+++ packages/bus/src/migrations/3c-3d.sql 2026-10-15 11:27:07.366512726 -0500
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Row 123 (#1617): a revoke needs a resolved decision or a reason (the chief of staff's role.end).
|
||||
DROP TRIGGER role_revoke_needs_decision;
|
||||
CREATE TRIGGER role_revoke_needs_decision BEFORE INSERT ON role_claims
|
||||
- WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL
|
||||
+ WHEN NEW.op = 'revoke' AND NEW.decision IS NULL
|
||||
BEGIN SELECT RAISE(ABORT, 'revoke needs a resolved decision or a reason'); END;
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/store.mjs 2026-10-15 11:27:07.371076466 -0500
|
||||
+++ packages/bus/src/store.mjs 2026-10-15 11:27:07.393512991 -0500
|
||||
@@ -13,7 +13,7 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
const ddl = readFileSync(new URL('../schema.sql', import.meta.url), 'utf8');
|
||||
-export const SCHEMA_SHA256 = 'e4f1de9e16d2fab5e051fd3810189b28cc76a670cfe573b752d9f53bf9ca39b5';
|
||||
+export const SCHEMA_SHA256 = 'f60a271cb339141a8bddeb98a926fefda2864a12e1f5bc78291c476984ad7cc7';
|
||||
export const SCHEMA_VERSION = '3d';
|
||||
// Ordered, named offline steps. New schema changes append a step and keep prior DDL pinned.
|
||||
const legacyDDL = readFileSync(new URL('./migrations/3b.sql', import.meta.url), 'utf8');
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,40 @@
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (16.956808ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (15.904618ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.960441ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.849862ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.638632ms)
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.753203ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.512476ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.484615ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.164553ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (18.21928ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (42.932036ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (41.920583ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 285.393637
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:152:1
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.753203ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: {"role":"coder","reason":"why","decision":"some-decision"}
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:161:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
expected: /invalid-request/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/broker.mjs 2026-10-15 11:27:01.818643237 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 11:27:02.162461717 -0500
|
||||
@@ -825,7 +825,7 @@
|
||||
// reason and no decision; the reason rides in the action.allowed body. Every other caller
|
||||
// still cites a resolved decision for this exact target and holder incarnation.
|
||||
if ('reason' in a) {
|
||||
- if ('decision' in a) fail('invalid-request');
|
||||
+ void 0;
|
||||
string(a.reason, 500);
|
||||
this.#consumeAuthority(s, 'role.end', { target: a.role }, { reason: a.reason });
|
||||
const b = this.#business(s.business);
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,68 @@
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (18.734456ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.815085ms)
|
||||
✖ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (12.444429ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (11.429491ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.960729ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.921362ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.711164ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (11.022758ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.515492ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (18.60963ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (42.370287ms)
|
||||
✖ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (39.221611ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 10
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 285.21673
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:112:1
|
||||
✖ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (12.444429ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:122:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
expected: /revoke needs a resolved decision or a reason/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:343:1
|
||||
✖ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (39.221611ms)
|
||||
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /revoke needs a resolved decision or a reason/. Input:
|
||||
|
||||
'Error: role is not held'
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:367:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: Error: role is not held
|
||||
at Store.run (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:196:34)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:195:66
|
||||
at Store.transaction (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:217:22)
|
||||
at Store.run (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:195:43)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:367:29
|
||||
at getActual (node:assert:580:5)
|
||||
at strict.throws (node:assert:728:24)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:367:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7),
|
||||
expected: /revoke needs a resolved decision or a reason/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/schema.sql 2026-10-15 11:27:07.327315441 -0500
|
||||
+++ packages/bus/schema.sql 2026-10-15 11:27:07.730516297 -0500
|
||||
@@ -124,7 +124,7 @@
|
||||
WHEN NEW.op = 'release' AND (SELECT holder_run FROM role_claims WHERE business = NEW.business AND role = NEW.role ORDER BY seq DESC LIMIT 1) <> NEW.holder_run
|
||||
BEGIN SELECT RAISE(ABORT, 'only the holder releases; others revoke'); END;
|
||||
CREATE TRIGGER role_revoke_needs_decision BEFORE INSERT ON role_claims
|
||||
- WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL
|
||||
+ WHEN 0
|
||||
BEGIN SELECT RAISE(ABORT, 'revoke needs a resolved decision or a reason'); END;
|
||||
CREATE TRIGGER meta_no_update BEFORE UPDATE ON meta BEGIN SELECT RAISE(ABORT, 'meta is append-only'); END;
|
||||
CREATE TRIGGER meta_no_delete BEFORE DELETE ON meta BEGIN SELECT RAISE(ABORT, 'meta is append-only'); END;
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/migrations/3c-3d.sql 2026-10-15 11:27:07.349924931 -0500
|
||||
+++ packages/bus/src/migrations/3c-3d.sql 2026-10-15 11:27:07.751516503 -0500
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Row 123 (#1617): a revoke needs a resolved decision or a reason (the chief of staff's role.end).
|
||||
DROP TRIGGER role_revoke_needs_decision;
|
||||
CREATE TRIGGER role_revoke_needs_decision BEFORE INSERT ON role_claims
|
||||
- WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL
|
||||
+ WHEN 0
|
||||
BEGIN SELECT RAISE(ABORT, 'revoke needs a resolved decision or a reason'); END;
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/store.mjs 2026-10-15 11:27:07.371076466 -0500
|
||||
+++ packages/bus/src/store.mjs 2026-10-15 11:27:07.775516739 -0500
|
||||
@@ -13,7 +13,7 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
const ddl = readFileSync(new URL('../schema.sql', import.meta.url), 'utf8');
|
||||
-export const SCHEMA_SHA256 = 'e4f1de9e16d2fab5e051fd3810189b28cc76a670cfe573b752d9f53bf9ca39b5';
|
||||
+export const SCHEMA_SHA256 = '93eba5f30b6e76c6c6aa2f76242cf08ffc8fd48789e8b315c8bcdb8cbccda699';
|
||||
export const SCHEMA_VERSION = '3d';
|
||||
// Ordered, named offline steps. New schema changes append a step and keep prior DDL pinned.
|
||||
const legacyDDL = readFileSync(new URL('./migrations/3b.sql', import.meta.url), 'utf8');
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,26 @@
|
||||
file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:67
|
||||
throw Error('migration-target-mismatch');
|
||||
^
|
||||
|
||||
Error: migration-target-mismatch
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:67:9
|
||||
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
|
||||
at async node:internal/modules/esm/loader:650:26
|
||||
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
|
||||
|
||||
Node.js v26.8.1
|
||||
✖ packages/bus/tests/chief-of-staff.test.mjs (45.605072ms)
|
||||
ℹ tests 1
|
||||
ℹ suites 0
|
||||
ℹ pass 0
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 51.02533
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:1:1
|
||||
✖ packages/bus/tests/chief-of-staff.test.mjs (45.605072ms)
|
||||
'test failed'
|
||||
@@ -0,0 +1,8 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/migrations/3c-3d.sql 2026-10-15 11:27:07.349924931 -0500
|
||||
+++ packages/bus/src/migrations/3c-3d.sql 2026-10-15 11:27:08.098581965 -0500
|
||||
@@ -1,5 +0,0 @@
|
||||
--- Row 123 (#1617): a revoke needs a resolved decision or a reason (the chief of staff's role.end).
|
||||
-DROP TRIGGER role_revoke_needs_decision;
|
||||
-CREATE TRIGGER role_revoke_needs_decision BEFORE INSERT ON role_claims
|
||||
- WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL
|
||||
- BEGIN SELECT RAISE(ABORT, 'revoke needs a resolved decision or a reason'); END;
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,40 @@
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (17.804956ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (15.339567ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.452623ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.570202ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.48182ms)
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (10.970968ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.067251ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.308663ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (13.651001ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (17.318227ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (41.207544ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (42.170697ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 274.881436
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:152:1
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (10.970968ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: {"role":"coder","reason":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:161:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
expected: /invalid-request/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/broker.mjs 2026-10-15 11:27:01.818643237 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 11:27:02.504465067 -0500
|
||||
@@ -826,7 +826,7 @@
|
||||
// still cites a resolved decision for this exact target and holder incarnation.
|
||||
if ('reason' in a) {
|
||||
if ('decision' in a) fail('invalid-request');
|
||||
- string(a.reason, 500);
|
||||
+ string(a.reason, 100000);
|
||||
this.#consumeAuthority(s, 'role.end', { target: a.role }, { reason: a.reason });
|
||||
const b = this.#business(s.business);
|
||||
if (a.role === s.role || END_PROTECTED.includes(b.roles[a.role]?.definition)) fail('role-end-refused');
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,40 @@
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (16.868041ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (15.721293ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.175749ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.4355ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (11.931358ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (12.822776ms)
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (11.660188ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.586562ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.402499ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (17.868012ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (42.068739ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (39.214415ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 268.753904
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:167:1
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (11.660188ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: chief
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:170:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
expected: /role-end-refused/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/broker.mjs 2026-10-15 11:27:01.818643237 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 11:27:02.832647509 -0500
|
||||
@@ -829,7 +829,7 @@
|
||||
string(a.reason, 500);
|
||||
this.#consumeAuthority(s, 'role.end', { target: a.role }, { reason: a.reason });
|
||||
const b = this.#business(s.business);
|
||||
- if (a.role === s.role || END_PROTECTED.includes(b.roles[a.role]?.definition)) fail('role-end-refused');
|
||||
+ if (END_PROTECTED.includes(b.roles[a.role]?.definition)) fail('role-end-refused');
|
||||
} else {
|
||||
identifier(a.decision);
|
||||
this.#consumeAuthority(s, 'role.revoke', { decision: a.decision, target: a.role });
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,40 @@
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (16.688665ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.84104ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.622971ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.817225ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.533153ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.37057ms)
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.569256ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.447056ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.650873ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (18.950692ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (42.888563ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (41.779958ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 283.79036
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:167:1
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.569256ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: assistant
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:170:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
expected: /role-end-refused/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/broker.mjs 2026-10-15 11:27:01.818643237 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 11:27:03.156471454 -0500
|
||||
@@ -829,7 +829,7 @@
|
||||
string(a.reason, 500);
|
||||
this.#consumeAuthority(s, 'role.end', { target: a.role }, { reason: a.reason });
|
||||
const b = this.#business(s.business);
|
||||
- if (a.role === s.role || END_PROTECTED.includes(b.roles[a.role]?.definition)) fail('role-end-refused');
|
||||
+ if (a.role === s.role || false) fail('role-end-refused');
|
||||
} else {
|
||||
identifier(a.decision);
|
||||
this.#consumeAuthority(s, 'role.revoke', { decision: a.decision, target: a.role });
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,80 @@
|
||||
✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (15.834586ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.067511ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.400916ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.957141ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.576864ms)
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.240822ms)
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.423212ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.042589ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (13.792477ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (17.192128ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (42.380119ms)
|
||||
✖ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (40.97602ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 8
|
||||
ℹ fail 4
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 277.433024
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:80:1
|
||||
✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (15.834586ms)
|
||||
Error: storage-refused
|
||||
at #refused (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:696:47)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:763:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:68:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:82:20)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.start (node:internal/test_runner/test:1262:17)
|
||||
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
|
||||
code: 'storage-refused'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:152:1
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.240822ms)
|
||||
Error: storage-refused
|
||||
at #refused (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:696:47)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:763:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:68:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:164:20)
|
||||
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) {
|
||||
code: 'storage-refused'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:167:1
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.423212ms)
|
||||
Error: storage-refused
|
||||
at #refused (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:696:47)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:763:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:68:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:176:3)
|
||||
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) {
|
||||
code: 'storage-refused'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:343:1
|
||||
✖ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (40.97602ms)
|
||||
Error: storage-refused
|
||||
at #refused (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:696:47)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:763:26)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:360:22)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
code: 'storage-refused'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/broker.mjs 2026-10-15 11:27:01.818643237 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 11:27:03.492474746 -0500
|
||||
@@ -836,7 +836,7 @@
|
||||
}
|
||||
const h = this.#holder(s.business, a.role);
|
||||
if (h?.op !== 'claim') fail('not-held');
|
||||
- this.#claimEnd(s, h, 'revoke', a.decision ?? null, a.reason ?? null);
|
||||
+ this.#claimEnd(s, h, 'revoke', a.decision ?? null, null);
|
||||
return { revoked: true };
|
||||
}
|
||||
case 'decision.raise': {
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,43 @@
|
||||
✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (18.402755ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (17.17512ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.517519ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (11.243111ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.538741ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.728742ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.595541ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (11.642824ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.679882ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (18.537079ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (42.79756ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (43.190748ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 288.330098
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:80:1
|
||||
✖ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (18.402755ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ undefined
|
||||
- 'stuck on the same test for an hour'
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:90:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.start (node:internal/test_runner/test:1262:17)
|
||||
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
expected: 'stuck on the same test for an hour',
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/broker.mjs 2026-10-15 11:27:01.818643237 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 11:27:03.822477979 -0500
|
||||
@@ -827,7 +827,7 @@
|
||||
if ('reason' in a) {
|
||||
if ('decision' in a) fail('invalid-request');
|
||||
string(a.reason, 500);
|
||||
- this.#consumeAuthority(s, 'role.end', { target: a.role }, { reason: a.reason });
|
||||
+ this.#consumeAuthority(s, 'role.end', { target: a.role }, {});
|
||||
const b = this.#business(s.business);
|
||||
if (a.role === s.role || END_PROTECTED.includes(b.roles[a.role]?.definition)) fail('role-end-refused');
|
||||
} else {
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,38 @@
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (17.872596ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.456392ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.517399ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (12.040366ms)
|
||||
✖ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.917042ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.707948ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (14.216337ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (11.368877ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (15.529151ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (19.106258ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (43.841046ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (41.815129ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 287.518091
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:132:1
|
||||
✖ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.917042ms)
|
||||
Error: decision-mismatch
|
||||
at fail (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:78:9)
|
||||
at #checkAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:475:7)
|
||||
at #consumeAuthority (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:484:40)
|
||||
at #dispatch (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:835:33)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:758:38
|
||||
at Store.transaction (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/store.mjs:217:22)
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:752:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:68:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:146:20)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14) {
|
||||
code: 'decision-mismatch'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/broker.mjs 2026-10-15 11:27:01.818643237 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 11:27:04.161481301 -0500
|
||||
@@ -832,7 +832,7 @@
|
||||
if (a.role === s.role || END_PROTECTED.includes(b.roles[a.role]?.definition)) fail('role-end-refused');
|
||||
} else {
|
||||
identifier(a.decision);
|
||||
- this.#consumeAuthority(s, 'role.revoke', { decision: a.decision, target: a.role });
|
||||
+ this.#consumeAuthority(s, 'role.end', { decision: a.decision, target: a.role });
|
||||
}
|
||||
const h = this.#holder(s.business, a.role);
|
||||
if (h?.op !== 'claim') fail('not-held');
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,51 @@
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (17.643653ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (17.470189ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.354049ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (11.050901ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.971711ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.601823ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.898411ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.855122ms)
|
||||
✖ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (10.520196ms)
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (10.085506ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (38.865155ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (43.66983ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 10
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 275.676468
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:191:1
|
||||
✖ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (10.520196ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: pm
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:194:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: undefined,
|
||||
expected: /decision-required/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:206:1
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (10.085506ms)
|
||||
TypeError: Cannot read properties of undefined (reading 'actor_role')
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:210:22)
|
||||
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)
|
||||
@@ -0,0 +1,11 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/mutants/.bak.GBHIwo/packages/bus/src/broker.mjs 2026-10-15 11:27:01.818643237 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 11:27:04.498484604 -0500
|
||||
@@ -732,7 +732,7 @@
|
||||
else keys(args, ORGANIZATION_KEYS, ["revision"]);
|
||||
let actor = s.human;
|
||||
if (role) {
|
||||
- this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));
|
||||
+ void 0;
|
||||
actor = this.#business(s.business).roles[s.role].definition + "." + s.role;
|
||||
}
|
||||
let result;
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,84 @@
|
||||
1d3a431fd243bac2c12670e32c44050f7d7faed021b095fe7ad5d26258f5989a build.md
|
||||
c8bf62fb0675d2976f6bd2130150707734c8a866290854fc67a8294a96ec0118 candidate-manifest.sha256
|
||||
570118925e5818d5dc8c8a113c9cfe4c2ee983f6d4cb670c4ced6ba092179f1d changed-paths.txt
|
||||
3fe6d8bd065c5a0dbac2d7766187101fd616a9e9b7d1abf61ed7640fbce5e8f4 mutants-run.txt
|
||||
f84bd6e6115a37834c9963efaf3c36a131fad90d38129d35f29f5ba0608833de mutants-started.txt
|
||||
2fafa225a0c10736415235a668bca2ea411f8034dce3a70b3a62b5c924226483 mutants.sh
|
||||
7f62260a26640124bd7f6929f0aa4c4d9bd4c96defc7e0ac4b1292745a6f42f2 mutants/control-bus.out
|
||||
9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa mutants/control-bus.rc
|
||||
5bdd21820a7044f9d1ee4a7ab0e275f7c36e809e4b24ffb39f439b868adb3905 mutants/control-business.out
|
||||
9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa mutants/control-business.rc
|
||||
2fa6c107f224039ce592bf3d1722e7bb136a04edfb67b4e0aad64328bbacbbb9 mutants/m1-role-end-unchecked.out
|
||||
fc145e900c0f587c312b94d8c8aa7c54381f40d8ba12bfbbb721ca6c9d2f5532 mutants/m1-role-end-unchecked.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m1-role-end-unchecked.rc
|
||||
a89b9d6e28b4ac9569406cf7465c26edf088592712efeeabf2bd8b9135467f9a mutants/m10-key-list-widened.out
|
||||
cb47eece4fa577b0cc50f1cc949acde3d9a8e6c7de6195cb2d7cf558f977a092 mutants/m10-key-list-widened.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m10-key-list-widened.rc
|
||||
4d4c02b9f543a1471729e47d4e328962ca6e5535573ccd51c0e92a1e9e254dff mutants/m11-revision-optional.out
|
||||
a615504f051b2f44ff3fee7d9b04ee0a67d89dfb9cec3c9c81d59089346f5e83 mutants/m11-revision-optional.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m11-revision-optional.rc
|
||||
fac7aadc319661a40e484497d53c9ce83b78fac2b94beb71d658e785d6371c32 mutants/m12-wrong-actor.out
|
||||
c869c91ce86751b7477749cf01d7e778794f16c72a52ea3a6b233118ce0dbbeb mutants/m12-wrong-actor.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m12-wrong-actor.rc
|
||||
cf50cd38cd0053bdc2e02bb3a0fe6a6d31c0508e1eb6c307033a61ea48783b61 mutants/m13-limits-open-to-agents.out
|
||||
d33cddaa618ca86933bb1a82a3964835374f7b4a9bb94a860a94e40d63fc4b6e mutants/m13-limits-open-to-agents.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m13-limits-open-to-agents.rc
|
||||
a0d9fa942cdba916ab5b43961953f758b961460b9061b4b7bba4e4da81cd9877 mutants/m14-invalid-change-not-refused.out
|
||||
b02226f3d2c28d40ddcd74117f66204f24c488b14c66a2118814cd9bdbf0aae2 mutants/m14-invalid-change-not-refused.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m14-invalid-change-not-refused.rc
|
||||
847ad5ddb7c0f876fa4680fed68caa48d1096786d5a28af89d95b133367b00ac mutants/m15-vocabulary-drops-role-end.out
|
||||
8745ae2f48c6fb038237b8e765547fc20902100c66d4c4b31d0a9eae0b70f69b mutants/m15-vocabulary-drops-role-end.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m15-vocabulary-drops-role-end.rc
|
||||
236a1d50c2af213a60e51932c0936ec3ebdb8e923b35faf246f2ffc3181731c1 mutants/m16-chief-file-drops-role-end.out
|
||||
a8e5f259c2e7ab9239344ff80b7807fa4725427e8deb21469aeafd4a7abf141f mutants/m16-chief-file-drops-role-end.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m16-chief-file-drops-role-end.rc
|
||||
d385a17a058b3e0c1fc1a23fe41bebb6297249101a1f98a3ad6eb0ea27560355 mutants/m17-chief-file-drops-organization-change.out
|
||||
d45db4a95b69db50fec1fe8df24f82648e75a97ec49e228e08887f757ecd6d8d mutants/m17-chief-file-drops-organization-change.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m17-chief-file-drops-organization-change.rc
|
||||
5e25ef93f9b4de4b09900bc356c4f88f971ae3ed4dfc52544e8ba181af322bbc mutants/m18-chief-file-grants-role-revoke.out
|
||||
0f2f73543dd07f9f78d468fb3fa2237a6b25a75eefe3b5c64c7a0e85f2d3c070 mutants/m18-chief-file-grants-role-revoke.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m18-chief-file-grants-role-revoke.rc
|
||||
2b0d8f0fd77727b4ee5af634e2d7b20ef67d1a699f12591b7d1c60b162871253 mutants/m19-trigger-decision-only.out
|
||||
ccbab1719a528fa0534b4be1f2219c1c8a115d28c08401938135c25cf5ac9edb mutants/m19-trigger-decision-only.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m19-trigger-decision-only.rc
|
||||
ccbce5eeb239eb60a9c14e481c98b691b0f38d49c7aa74307ffc6c3a54897abb mutants/m2-reason-and-decision-accepted.out
|
||||
10541b804e6319e7dfb8a1954385bf06a6b704284b9254786a2c03486ee247ef mutants/m2-reason-and-decision-accepted.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m2-reason-and-decision-accepted.rc
|
||||
7cf9fed283cbb5b06b3d313598bd7ad13ea6465b3efc0cb37ca383f717c8e9ca mutants/m20-trigger-never-fires.out
|
||||
d0864f9352a6b67504e21d9d3e5af77d5cb8a5bea9f95ffda06d6455209c0230 mutants/m20-trigger-never-fires.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m20-trigger-never-fires.rc
|
||||
622ca5a82ea434da1678d796956f9e5cab30440b2219f76fdfba6896e6f9c3c1 mutants/m21-step-empty.out
|
||||
4233ba4748a89596946fbabef18da22851db07c5aef7011efd9321098417fabd mutants/m21-step-empty.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m21-step-empty.rc
|
||||
e4f8a0a5cd9b042a5fca66bc108892118680aad69ee4e81c20e9d62252d75e52 mutants/m3-reason-unbounded.out
|
||||
27981ca3d99e80265633a496aaa4688e567535e656c08917e4e80aa9c5b096c8 mutants/m3-reason-unbounded.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m3-reason-unbounded.rc
|
||||
e2b67a773beec76eb8a42b55c330f4f08614ed3423e22c6b2c9d02dcf9d8c6e9 mutants/m4-self-end-allowed.out
|
||||
ed8fabb5af1650b45d080e02c1aa2eb2e1cef6519b122b0a89c4ce18a914641c mutants/m4-self-end-allowed.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m4-self-end-allowed.rc
|
||||
7744511546c87f26ad3b5fc9db5c9a8c1d287b48e7fd10ad6903ef986c4ddcb5 mutants/m5-assistant-end-allowed.out
|
||||
973dec900d8fa9f860e1d84835c7badac89c6981c5543399ace773ab4340a35d mutants/m5-assistant-end-allowed.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m5-assistant-end-allowed.rc
|
||||
e8d896e67fe2cdf767b11cea0be635a86aa4847cbc1c356b51cd5635a71b42b1 mutants/m6-reason-not-stored.out
|
||||
63a6bea6e998aa931b0ece443de0b4bfe8ebf49c67ec361b011ee4286e2c673b mutants/m6-reason-not-stored.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m6-reason-not-stored.rc
|
||||
f653f25f35e82ba5cbe4d1c5786b7919b5df4daf4e5f59cfa0e857021d9248e7 mutants/m7-reason-not-logged.out
|
||||
2d4a827bb738b827d26192569efbf242684ea1364c51e3f40454f4e19132c009 mutants/m7-reason-not-logged.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m7-reason-not-logged.rc
|
||||
824f68b64ada87d97dcd4d58c6dfd1891af174bd19cc7976e596ca8032c828ea mutants/m8-decision-path-wrong-action.out
|
||||
202e127bb907ab0c7df16f2b47b31ade1595fa7b0154336040cfa8173b5627b2 mutants/m8-decision-path-wrong-action.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m8-decision-path-wrong-action.rc
|
||||
087d98aaaef71cd57f53a2107c9d7ffb9fb9e40055ca09dd82959f7a0f7d692a mutants/m9-organization-set-unchecked.out
|
||||
136a065d6820adb957974215245ee48b20c22472cd1a51b20fc8c019e39a835d mutants/m9-organization-set-unchecked.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m9-organization-set-unchecked.rc
|
||||
5de88be63e02be0d76aa78f1a5af3485e0cd9285c7a2d14e0d3785f0342db8a4 row123.patch
|
||||
a86c8727489d6c40ac93a5b577bddab68f6f48741e7a335404b9b0174bf3b080 suites-run.txt
|
||||
c109a6a346a90d580d6da7d576afe7b8e37018500a16b4768867e53b4b90589e suites.sh
|
||||
919e2eed9e000b216146c74441951cc4e8ed78b45314e2b2514edf2e07450783 suites/bus.txt
|
||||
2e8847d123646ef8582b6a7fff84f3f952df8af11facbf798feba041977cb074 suites/business.txt
|
||||
8976be3e88de3081d667ada5bad2d067c4ad2302a1157caa13fc3185652dddee suites/cli.txt
|
||||
274941628ddc86b09efe0a998f684b1abfe0923532ee51870385f111ea31bdec suites/harness.txt
|
||||
303fdf3144ca829d9a3c0520802da18d601ce6717ea619676c83cf8b3141ce96 suites/seat.txt
|
||||
7e4a47618f4f930b4f61f4c06298e22b86612e7395822e77109b38cc93f5dd1e suites/tasks.txt
|
||||
8e2254aa3e1bb9b205ab72c68273d447c0328c9f156ccba2a46f3a0476468b58 suites/test-land-work.txt
|
||||
39815dd4ba0b889d7b54350c6d9ca459c3f04f41541d34f6732ac82b04b8beb8 suites/webui.txt
|
||||
@@ -0,0 +1,954 @@
|
||||
diff --git a/packages/bus/README.md b/packages/bus/README.md
|
||||
index 2973590c5..015b8a7fc 100644
|
||||
--- a/packages/bus/README.md
|
||||
+++ b/packages/bus/README.md
|
||||
@@ -138,7 +138,9 @@ an operator inspect content and delivery evidence after an uncertain result.
|
||||
|---|---|
|
||||
| `role.claim` | No args; claims capability's role/run. One current holder per business/instance; a revoked run cannot reclaim that role, including after restart. |
|
||||
| `role.release` | No args; only its current holder releases. |
|
||||
-| `role.revoke` | `{role,decision}`; needs human-approved gated decision for this exact target and holder incarnation. |
|
||||
+| `role.revoke` | `{role,decision}`; needs human-approved gated decision for this exact target and holder incarnation. A role whose file lists `role.end` (the chief of staff, row 123, REQ-COS-1) sends `{role,reason}` instead: no decision, a reason of 1 to 500 characters, logged in the `action.allowed` body. Sending both is `invalid-request`; the caller's own run and the assistant's are `role-end-refused`; any other role sending `reason` gets `decision-required`. |
|
||||
+| `organization.set` | `{revision,limits?,models?,instances?}`; the keys `limits.set` takes, through the same writer and receipt. Needs `organization.change` in the caller's role file (the chief of staff); the receipt's actor is `<definition>.<instance>`, e.g. `chief-of-staff.chief`. Every other key is `invalid-request`. `limits` and `limits.set` stay human-only. |
|
||||
+
|
||||
| `decision.raise` | `{action,question,options:[{key,text}],recommendation,blocking,domain?,target?,project?,task_ref?,requirement_ref?,supersedes?,choice?,approvalChoice?}`. Returns decision with decoded options and authorization context. A blocking decision holds a task: `blocking` true with no `task_ref` refuses `task-required`. A non-blocking one needs no task, and a gated action such as `prd.approve` or `role.revoke` still routes to the human's inbox. |
|
||||
| `decision.resolve` | `{id,choice,note?}`; option must exist, resolver must be routed role or outside-agent human CLI. Sends the raising role a DECISION message (Decision close messages, below). |
|
||||
| `decision.seen` | `{id,note?}`; acknowledgement by resolver, not a resolution. |
|
||||
@@ -475,7 +477,10 @@ fixtures. It takes the same exclusive writer lock, checks the pinned source
|
||||
schema and metadata, WAL, timestamps and integrity, then runs named migration
|
||||
steps in order. Each step atomically adds its DDL, changes only schema version
|
||||
and digest metadata, restores the metadata update guard, and verifies its target
|
||||
-digest. The 3b→3c step also records an immutable `comment_cursor_epoch`. Prior
|
||||
+digest. The 3b→3c step also records an immutable `comment_cursor_epoch`. The 3c→3d
|
||||
+step (row 123) replaces the `role_revoke_needs_decision` trigger: a revoke needs a resolved
|
||||
+decision or a reason. Storage accepts a revoke with a reason and no decision, so only the
|
||||
+broker decides who may revoke that way (`role.end`, in the `role.revoke` row above). Prior
|
||||
steps stay pinned under `src/migrations/`; later versions append a step. A completed
|
||||
step remains valid if a later step refuses; rerunning resumes from that version.
|
||||
Already-current migration is a validated no-op. Missing stores refuse.
|
||||
diff --git a/packages/bus/schema.sql b/packages/bus/schema.sql
|
||||
index e68424252..337ae80b9 100644
|
||||
--- a/packages/bus/schema.sql
|
||||
+++ b/packages/bus/schema.sql
|
||||
@@ -124,8 +124,8 @@ CREATE TRIGGER role_release_same_run BEFORE INSERT ON role_claims
|
||||
WHEN NEW.op = 'release' AND (SELECT holder_run FROM role_claims WHERE business = NEW.business AND role = NEW.role ORDER BY seq DESC LIMIT 1) <> NEW.holder_run
|
||||
BEGIN SELECT RAISE(ABORT, 'only the holder releases; others revoke'); END;
|
||||
CREATE TRIGGER role_revoke_needs_decision BEFORE INSERT ON role_claims
|
||||
- WHEN NEW.op = 'revoke' AND NEW.decision IS NULL
|
||||
- BEGIN SELECT RAISE(ABORT, 'revoke needs a resolved decision'); END;
|
||||
+ WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL
|
||||
+ BEGIN SELECT RAISE(ABORT, 'revoke needs a resolved decision or a reason'); END;
|
||||
CREATE TRIGGER meta_no_update BEFORE UPDATE ON meta BEGIN SELECT RAISE(ABORT, 'meta is append-only'); END;
|
||||
CREATE TRIGGER meta_no_delete BEFORE DELETE ON meta BEGIN SELECT RAISE(ABORT, 'meta is append-only'); END;
|
||||
CREATE TRIGGER events_no_update BEFORE UPDATE ON events BEGIN SELECT RAISE(ABORT, 'events is append-only'); END;
|
||||
diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs
|
||||
index 7c281d635..ddff2cc54 100644
|
||||
--- a/packages/bus/src/broker.mjs
|
||||
+++ b/packages/bus/src/broker.mjs
|
||||
@@ -20,6 +20,8 @@ export const ACTIONS = Object.freeze([
|
||||
'message.external',
|
||||
'role.launch',
|
||||
'role.revoke',
|
||||
+ 'role.end',
|
||||
+ 'organization.change',
|
||||
'credential.mint',
|
||||
'spend',
|
||||
'deploy',
|
||||
@@ -93,6 +95,10 @@ function identifier(x) {
|
||||
const taskRef = (x) => typeof x === 'string' && /^vikunja:[1-9][0-9]*\/[1-9][0-9]*$/.test(x);
|
||||
// A git commit id, SHA-1 or SHA-256.
|
||||
const oid = (x) => typeof x === 'string' && /^[0-9a-f]{40}([0-9a-f]{24})?$/.test(x);
|
||||
+// Row 123 (#1617): the one list of organization-file keys limits.set and organization.set accept.
|
||||
+const ORGANIZATION_KEYS = ['revision', 'limits', 'models', 'instances'];
|
||||
+// Row 123 (#1617): role.end never reaches the assistant (the user's seat) or the caller's own run.
|
||||
+const END_PROTECTED = ['assistant'];
|
||||
const VERDICTS = ['approve', 'changes'];
|
||||
// Row 93 (#1587): the review loop's evidence, written only by the verbs below. A push is an
|
||||
// action.allowed for git.push.working that names its commit, written only by recordPush.
|
||||
@@ -715,14 +721,23 @@ export class Broker {
|
||||
const args = request.args ?? {};
|
||||
if (!object(args)) fail('invalid-request');
|
||||
this.#secretCheck(args);
|
||||
- if (["limits", "limits.set"].includes(request.verb)) {
|
||||
- if (!s.human) fail("human-required");
|
||||
+ if (["limits", "limits.set", "organization.set"].includes(request.verb)) {
|
||||
+ // Row 123 (#1617, REQ-COS-1, REQ-LIMIT-1): organization.set is limits.set for a role whose
|
||||
+ // file lists organization.change. It takes the same keys, runs the same write path and
|
||||
+ // logs the same receipt; only the actor differs: the role definition and instance.
|
||||
+ const role = request.verb === "organization.set";
|
||||
+ if (!role && !s.human) fail("human-required");
|
||||
if (!this.#organization) fail("not-configured");
|
||||
if (request.verb === "limits") keys(args, []);
|
||||
- else keys(args, ["revision", "limits", "models", "instances"], ["revision"]);
|
||||
+ else keys(args, ORGANIZATION_KEYS, ["revision"]);
|
||||
+ let actor = s.human;
|
||||
+ if (role) {
|
||||
+ this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));
|
||||
+ actor = this.#business(s.business).roles[s.role].definition + "." + s.role;
|
||||
+ }
|
||||
let result;
|
||||
try {
|
||||
- result = this.#organization(s.business, request.verb, args, s.human);
|
||||
+ result = this.#organization(s.business, role ? "limits.set" : request.verb, args, actor);
|
||||
} catch (e) {
|
||||
if (e.code === "revision-conflict") fail("revision-conflict");
|
||||
if (e.code?.startsWith("organization-")) fail(e.code);
|
||||
@@ -804,13 +819,24 @@ export class Broker {
|
||||
return { released: true };
|
||||
}
|
||||
case 'role.revoke': {
|
||||
- keys(a, ['role', 'decision'], ['role', 'decision']);
|
||||
+ keys(a, ['role', 'decision', 'reason'], ['role']);
|
||||
identifier(a.role);
|
||||
- identifier(a.decision);
|
||||
- this.#consumeAuthority(s, 'role.revoke', { decision: a.decision, target: a.role });
|
||||
+ // Row 123 (#1617, REQ-ROLE-2, REQ-COS-1): a role whose file lists role.end ends a run with a
|
||||
+ // reason and no decision; the reason rides in the action.allowed body. Every other caller
|
||||
+ // still cites a resolved decision for this exact target and holder incarnation.
|
||||
+ if ('reason' in a) {
|
||||
+ if ('decision' in a) fail('invalid-request');
|
||||
+ string(a.reason, 500);
|
||||
+ this.#consumeAuthority(s, 'role.end', { target: a.role }, { reason: a.reason });
|
||||
+ const b = this.#business(s.business);
|
||||
+ if (a.role === s.role || END_PROTECTED.includes(b.roles[a.role]?.definition)) fail('role-end-refused');
|
||||
+ } else {
|
||||
+ identifier(a.decision);
|
||||
+ this.#consumeAuthority(s, 'role.revoke', { decision: a.decision, target: a.role });
|
||||
+ }
|
||||
const h = this.#holder(s.business, a.role);
|
||||
if (h?.op !== 'claim') fail('not-held');
|
||||
- this.#claimEnd(s, h, 'revoke', a.decision);
|
||||
+ this.#claimEnd(s, h, 'revoke', a.decision ?? null, a.reason ?? null);
|
||||
return { revoked: true };
|
||||
}
|
||||
case 'decision.raise': {
|
||||
@@ -1367,9 +1393,10 @@ export class Broker {
|
||||
).map((m) => ({ id: m.id, from: m.from, run: m.run, at: m.at }));
|
||||
return { ...c, state, working, inputNeeded, stranded };
|
||||
}
|
||||
- #claimEnd(s, h, op, decision) {
|
||||
+ // Row 123 (#1617): a role.end revoke carries its reason in role_claims.reason and no decision.
|
||||
+ #claimEnd(s, h, op, decision, reason = null) {
|
||||
this.#store.run(
|
||||
- 'INSERT INTO role_claims(at,business,role,op,holder_run,harness,address,by,decision) VALUES(?,?,?,?,?,?,?,?,?)',
|
||||
+ 'INSERT INTO role_claims(at,business,role,op,holder_run,harness,address,by,decision,reason) VALUES(?,?,?,?,?,?,?,?,?,?)',
|
||||
this.#now(),
|
||||
s.business,
|
||||
h.role,
|
||||
@@ -1379,6 +1406,7 @@ export class Broker {
|
||||
h.address,
|
||||
s.run ?? s.human,
|
||||
decision,
|
||||
+ reason,
|
||||
);
|
||||
}
|
||||
// A message's originating human.input: a human instruction's own, or for the
|
||||
diff --git a/packages/bus/src/migrations/3c-3d.sql b/packages/bus/src/migrations/3c-3d.sql
|
||||
new file mode 100644
|
||||
index 000000000..af600bae5
|
||||
--- /dev/null
|
||||
+++ b/packages/bus/src/migrations/3c-3d.sql
|
||||
@@ -0,0 +1,5 @@
|
||||
+-- Row 123 (#1617): a revoke needs a resolved decision or a reason (the chief of staff's role.end).
|
||||
+DROP TRIGGER role_revoke_needs_decision;
|
||||
+CREATE TRIGGER role_revoke_needs_decision BEFORE INSERT ON role_claims
|
||||
+ WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL
|
||||
+ BEGIN SELECT RAISE(ABORT, 'revoke needs a resolved decision or a reason'); END;
|
||||
diff --git a/packages/bus/src/store.mjs b/packages/bus/src/store.mjs
|
||||
index 0c4d14f2d..99e1f1ac0 100644
|
||||
--- a/packages/bus/src/store.mjs
|
||||
+++ b/packages/bus/src/store.mjs
|
||||
@@ -13,8 +13,8 @@ import { join, isAbsolute } from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
const ddl = readFileSync(new URL('../schema.sql', import.meta.url), 'utf8');
|
||||
-export const SCHEMA_SHA256 = '7587ac1df813e5148fdb73dc4733ec8064e6a142e4647f0da71be009a73eebdb';
|
||||
-export const SCHEMA_VERSION = '3c';
|
||||
+export const SCHEMA_SHA256 = 'e4f1de9e16d2fab5e051fd3810189b28cc76a670cfe573b752d9f53bf9ca39b5';
|
||||
+export const SCHEMA_VERSION = '3d';
|
||||
// Ordered, named offline steps. New schema changes append a step and keep prior DDL pinned.
|
||||
const legacyDDL = readFileSync(new URL('./migrations/3b.sql', import.meta.url), 'utf8');
|
||||
const LEGACY_SHA256 = '179ffe356d4ff19a49b5ebad39b6c6bfd7771deb8e1b7c55b5e746f039d69e65';
|
||||
@@ -25,6 +25,10 @@ const migrations = [{
|
||||
initialize: (store) => store.run(
|
||||
'INSERT INTO meta(key,value) VALUES (?,?)', 'comment_cursor_epoch', new Date().toISOString(),
|
||||
),
|
||||
+}, {
|
||||
+ from: '3c',
|
||||
+ to: '3d',
|
||||
+ sql: readFileSync(new URL('./migrations/3c-3d.sql', import.meta.url), 'utf8'),
|
||||
}];
|
||||
const timedTables = [
|
||||
'events',
|
||||
diff --git a/packages/bus/tests/chief-of-staff.test.mjs b/packages/bus/tests/chief-of-staff.test.mjs
|
||||
new file mode 100644
|
||||
index 000000000..645b41dec
|
||||
--- /dev/null
|
||||
+++ b/packages/bus/tests/chief-of-staff.test.mjs
|
||||
@@ -0,0 +1,368 @@
|
||||
+import test from 'node:test';
|
||||
+import assert from 'node:assert/strict';
|
||||
+import { chmodSync, copyFileSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
+import { tmpdir } from 'node:os';
|
||||
+import { join } from 'node:path';
|
||||
+import { createHash } from 'node:crypto';
|
||||
+import { DatabaseSync } from 'node:sqlite';
|
||||
+import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
+import { Store, schemaDigest } from '../src/store.mjs';
|
||||
+import { Broker } from '../src/broker.mjs';
|
||||
+import { startBroker } from '../src/runtime.mjs';
|
||||
+import { businessDoc, rolesCopy, systemFor, writeJson } from '../../business/tests/helpers.mjs';
|
||||
+
|
||||
+// Row 123 (#1617): the chief of staff ends runs without a decision and changes the organization
|
||||
+// file through the shared write path. REQ-ROLE-2 (one holder; revoke), REQ-ROLE-3 (the
|
||||
+// organization file), REQ-LIMIT-1 (limits are settings), REQ-COS-1 (the chief of staff runs the
|
||||
+// organization). The grant is read from the shipped roles/chief-of-staff.json, so the test pins the
|
||||
+// file the stack runs, not a copy of it.
|
||||
+const shipped = (name) =>
|
||||
+ JSON.parse(readFileSync(fileURLToPath(new URL(`../../../roles/${name}.json`, import.meta.url)), 'utf8'));
|
||||
+const chiefAuthority = shipped('chief-of-staff').authority;
|
||||
+const none = { authority: { withinRole: ['message.send'], crossRole: [] } };
|
||||
+
|
||||
+const roles = {
|
||||
+ chief: { definition: 'chief-of-staff', authority: chiefAuthority },
|
||||
+ assistant: { definition: 'assistant', ...none },
|
||||
+ pm: { definition: 'pm', ...none },
|
||||
+ coder: { definition: 'coder', ...none },
|
||||
+};
|
||||
+const demo = () => ({
|
||||
+ demo: {
|
||||
+ id: 'demo',
|
||||
+ human: 'jason',
|
||||
+ arbiters: { technical: 'pm', delivery: 'chief' },
|
||||
+ roles,
|
||||
+ definitions: {},
|
||||
+ projects: { stack: { root: '/nonexistent' } },
|
||||
+ vars: { 'bus.tester': true },
|
||||
+ },
|
||||
+});
|
||||
+
|
||||
+const PATCH = { revision: 'a'.repeat(64), limits: { total: 3 } };
|
||||
+
|
||||
+function setup(t) {
|
||||
+ const root = mkdtempSync(join(tmpdir(), 'bus-chief-'));
|
||||
+ const store = new Store(root);
|
||||
+ t.after(() => {
|
||||
+ store.close();
|
||||
+ rmSync(root, { recursive: true, force: true });
|
||||
+ });
|
||||
+ const organization = [];
|
||||
+ const b = new Broker({
|
||||
+ store,
|
||||
+ businesses: demo(),
|
||||
+ organization: (...args) => {
|
||||
+ organization.push(args);
|
||||
+ return { revision: 'b'.repeat(64) };
|
||||
+ },
|
||||
+ });
|
||||
+ const caps = {};
|
||||
+ for (const role of Object.keys(roles)) {
|
||||
+ caps[role] = b.bindLaunch({ business: 'demo', role, run: `${role}-run`, harness: 'pi' });
|
||||
+ b.request(caps[role], { verb: 'role.claim' });
|
||||
+ }
|
||||
+ caps.human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
||||
+ caps.reader = b.bindReader({ business: 'demo' });
|
||||
+ caps.tester = b.bindTester({ business: 'demo' });
|
||||
+ const call = (who, verb, args = {}) => b.request(caps[who], { verb, args });
|
||||
+ const lastClaim = (role) => ({
|
||||
+ ...store.get('SELECT op, decision, reason, by FROM role_claims WHERE role=? ORDER BY rowid DESC LIMIT 1', role),
|
||||
+ });
|
||||
+ const allowed = (action) =>
|
||||
+ store
|
||||
+ .all("SELECT actor_role, subject, body FROM events WHERE kind='action.allowed' ORDER BY seq")
|
||||
+ .map((e) => ({ ...e, body: JSON.parse(e.body) }))
|
||||
+ .filter((e) => e.body.action === action);
|
||||
+ return { b, store, caps, call, lastClaim, allowed, organization };
|
||||
+}
|
||||
+
|
||||
+test('REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged', (t) => {
|
||||
+ const { call, lastClaim, allowed } = setup(t);
|
||||
+ assert.deepEqual(call('chief', 'role.revoke', { role: 'coder', reason: 'stuck on the same test for an hour' }), {
|
||||
+ revoked: true,
|
||||
+ });
|
||||
+ assert.deepEqual(lastClaim('coder'), { op: 'revoke', decision: null, reason: 'stuck on the same test for an hour', by: 'chief-run' });
|
||||
+ const [event, ...rest] = allowed('role.end');
|
||||
+ assert.equal(rest.length, 0);
|
||||
+ assert.equal(event.actor_role, 'chief');
|
||||
+ assert.equal(event.subject, 'coder');
|
||||
+ assert.equal(event.body.reason, 'stuck on the same test for an hour');
|
||||
+ assert.equal(event.body.target, 'coder');
|
||||
+ assert.equal(event.body.class, 'within-role');
|
||||
+ assert.ok(!('decision' in event.body));
|
||||
+ // The coder's run is over: it no longer holds the role.
|
||||
+ assert.throws(() => call('coder', 'message.send', { to: 'pm', body: 'hi' }), /not-holder/);
|
||||
+});
|
||||
+
|
||||
+test('REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke', (t) => {
|
||||
+ const { call, lastClaim, allowed } = setup(t);
|
||||
+ for (const who of ['pm', 'coder', 'assistant']) {
|
||||
+ assert.throws(() => call(who, 'role.revoke', { role: 'coder', reason: 'no decision' }), /decision-required/, who);
|
||||
+ assert.throws(() => call(who, 'role.revoke', { role: 'coder' }), /invalid-request/, who);
|
||||
+ }
|
||||
+ assert.deepEqual(lastClaim('coder'), { op: 'claim', decision: null, reason: null, by: 'coder-run' });
|
||||
+ assert.equal(allowed('role.end').length, 0);
|
||||
+ // The human cannot use the reason path either: it has no role to hold role.end.
|
||||
+ assert.throws(() => call('human', 'role.revoke', { role: 'coder', reason: 'no' }), /agent-required/);
|
||||
+ assert.throws(() => call('reader', 'role.revoke', { role: 'coder', reason: 'no' }), /read-only/);
|
||||
+ assert.throws(() => call('tester', 'role.revoke', { role: 'coder', reason: 'no' }), /tester-refused/);
|
||||
+});
|
||||
+
|
||||
+test('REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither', (t) => {
|
||||
+ // Storage does not decide who may revoke by reason; the broker does (tests above). It only
|
||||
+ // refuses a revoke that carries nothing.
|
||||
+ const { store } = setup(t);
|
||||
+ const insert = (decision, reason) =>
|
||||
+ store.run(
|
||||
+ "INSERT INTO role_claims(at,business,role,op,holder_run,harness,address,by,decision,reason) VALUES('2026-10-15T00:00:00.000Z','demo','pm','revoke','pm-run','pi',NULL,'chief-run',?,?)",
|
||||
+ decision,
|
||||
+ reason,
|
||||
+ );
|
||||
+ assert.throws(() => insert(null, null), /revoke needs a resolved decision or a reason/);
|
||||
+ insert(null, 'a reason');
|
||||
+});
|
||||
+
|
||||
+test('REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path', (t) => {
|
||||
+ const { call, lastClaim } = setup(t);
|
||||
+ assert.throws(() => call('chief', 'role.revoke', { role: 'coder', decision: 'missing' }), /decision-not-found/);
|
||||
+ assert.deepEqual(lastClaim('coder'), { op: 'claim', decision: null, reason: null, by: 'coder-run' });
|
||||
+});
|
||||
+
|
||||
+test('REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason', (t) => {
|
||||
+ const { call, lastClaim, allowed } = setup(t);
|
||||
+ const d = call('pm', 'decision.raise', {
|
||||
+ action: 'role.revoke',
|
||||
+ target: 'coder',
|
||||
+ question: 'Stop the coder?',
|
||||
+ options: [
|
||||
+ { key: 'yes', text: 'Yes' },
|
||||
+ { key: 'no', text: 'No' },
|
||||
+ ],
|
||||
+ recommendation: 'yes',
|
||||
+ blocking: false,
|
||||
+ });
|
||||
+ call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
|
||||
+ assert.deepEqual(call('pm', 'role.revoke', { role: 'coder', decision: d.id }), { revoked: true });
|
||||
+ assert.deepEqual(lastClaim('coder'), { op: 'revoke', decision: d.id, reason: null, by: 'pm-run' });
|
||||
+ assert.equal(allowed('role.revoke').filter((e) => e.body.class === 'gated' && e.body.decision === d.id && e.body.target === 'coder').length, 1);
|
||||
+ assert.equal(allowed('role.end').length, 0);
|
||||
+});
|
||||
+
|
||||
+test('REQ-ROLE-2: a reason is required, bounded, and never sent with a decision', (t) => {
|
||||
+ const { call, lastClaim } = setup(t);
|
||||
+ for (const args of [
|
||||
+ { role: 'coder', reason: '' },
|
||||
+ { role: 'coder', reason: 'x'.repeat(501) },
|
||||
+ { role: 'coder', reason: 7 },
|
||||
+ { role: 'coder', reason: 'why', decision: 'some-decision' },
|
||||
+ { role: 'coder', reason: 'why', extra: true },
|
||||
+ ])
|
||||
+ assert.throws(() => call('chief', 'role.revoke', args), /invalid-request/, JSON.stringify(args).slice(0, 60));
|
||||
+ assert.deepEqual(lastClaim('coder'), { op: 'claim', decision: null, reason: null, by: 'coder-run' });
|
||||
+ // The bound is inclusive.
|
||||
+ assert.deepEqual(call('chief', 'role.revoke', { role: 'coder', reason: 'x'.repeat(500) }), { revoked: true });
|
||||
+});
|
||||
+
|
||||
+test('REQ-COS-1: the chief of staff cannot end its own run or the assistant\'s through role.end', (t) => {
|
||||
+ const { call, lastClaim, allowed } = setup(t);
|
||||
+ for (const role of ['chief', 'assistant'])
|
||||
+ assert.throws(() => call('chief', 'role.revoke', { role, reason: 'no' }), /role-end-refused/, role);
|
||||
+ assert.deepEqual(lastClaim('chief'), { op: 'claim', decision: null, reason: null, by: 'chief-run' });
|
||||
+ assert.deepEqual(lastClaim('assistant'), { op: 'claim', decision: null, reason: null, by: 'assistant-run' });
|
||||
+ // The refusal rolls its action.allowed back with the transaction.
|
||||
+ assert.equal(allowed('role.end').length, 0);
|
||||
+ // A role that is not held is refused as before, and logs nothing.
|
||||
+ call('chief', 'role.revoke', { role: 'pm', reason: 'first' });
|
||||
+ assert.throws(() => call('chief', 'role.revoke', { role: 'pm', reason: 'again' }), /not-held/);
|
||||
+ assert.equal(allowed('role.end').length, 1);
|
||||
+});
|
||||
+
|
||||
+test('REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor', (t) => {
|
||||
+ const { call, organization } = setup(t);
|
||||
+ const done = call('chief', 'organization.set', PATCH);
|
||||
+ assert.deepEqual(done, { revision: 'b'.repeat(64) });
|
||||
+ // Same callback verb as a human's limits.set; only the actor differs.
|
||||
+ assert.deepEqual(organization, [['demo', 'limits.set', PATCH, 'chief-of-staff.chief']]);
|
||||
+ call('human', 'limits.set', PATCH);
|
||||
+ assert.deepEqual(organization.at(-1), ['demo', 'limits.set', PATCH, 'jason']);
|
||||
+});
|
||||
+
|
||||
+test('REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change', (t) => {
|
||||
+ const { call, organization, allowed } = setup(t);
|
||||
+ for (const who of ['pm', 'coder', 'assistant'])
|
||||
+ assert.throws(() => call(who, 'organization.set', PATCH), /decision-required/, who);
|
||||
+ assert.throws(() => call('human', 'organization.set', PATCH), /agent-required/);
|
||||
+ assert.throws(() => call('reader', 'organization.set', PATCH), /read-only/);
|
||||
+ assert.throws(() => call('tester', 'organization.set', PATCH), /tester-refused/);
|
||||
+ assert.equal(organization.length, 0);
|
||||
+ assert.equal(allowed('organization.change').length, 0);
|
||||
+ // limits.set and limits stay with the human.
|
||||
+ for (const verb of ['limits', 'limits.set'])
|
||||
+ assert.throws(() => call('chief', verb, verb === 'limits' ? {} : PATCH), /human-required/, verb);
|
||||
+ assert.equal(organization.length, 0);
|
||||
+});
|
||||
+
|
||||
+test('REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human\'s keys', (t) => {
|
||||
+ const { call, organization, allowed } = setup(t);
|
||||
+ call('chief', 'organization.set', PATCH);
|
||||
+ const [event] = allowed('organization.change');
|
||||
+ assert.equal(event.actor_role, 'chief');
|
||||
+ assert.equal(event.body.class, 'within-role');
|
||||
+ // Fields outside the write path are refused for the chief of staff exactly as for a human, and
|
||||
+ // nothing reaches the writer.
|
||||
+ const outside = [
|
||||
+ { launch: { by: 'coder' } },
|
||||
+ { human: 'mallory' },
|
||||
+ { arbiters: { technical: 'coder', delivery: 'coder' } },
|
||||
+ { roles: {} },
|
||||
+ { actor: 'jason' },
|
||||
+ { 'launch.by': 'coder' },
|
||||
+ ];
|
||||
+ const before = organization.length;
|
||||
+ for (const extra of outside)
|
||||
+ for (const [who, verb] of [['chief', 'organization.set'], ['human', 'limits.set']])
|
||||
+ assert.throws(
|
||||
+ () => call(who, verb, { ...PATCH, ...extra }),
|
||||
+ /invalid-request/,
|
||||
+ `${who} ${Object.keys(extra)[0]}`,
|
||||
+ );
|
||||
+ assert.throws(() => call('chief', 'organization.set', { limits: { total: 1 } }), /invalid-request/);
|
||||
+ assert.equal(organization.length, before);
|
||||
+});
|
||||
+
|
||||
+test('REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human\'s; an invalid one is refused', async (t) => {
|
||||
+ const root = mkdtempSync(join(tmpdir(), 'bus-chief-runtime-'));
|
||||
+ const rolesDir = rolesCopy(root);
|
||||
+ const file = join(root, 'businesses/demo.json');
|
||||
+ writeJson(file, businessDoc(root, 'demo'));
|
||||
+ mkdirSync(join(root, 'data'), { mode: 0o700 });
|
||||
+ const bus = await startBroker({
|
||||
+ dataRoot: join(root, 'data'),
|
||||
+ businesses: demo(),
|
||||
+ organization: { dir: root, rolesDir, system: systemFor(root) },
|
||||
+ });
|
||||
+ t.after(async () => {
|
||||
+ await bus.close();
|
||||
+ rmSync(root, { recursive: true, force: true });
|
||||
+ });
|
||||
+ const { broker } = bus;
|
||||
+ const human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
||||
+ const chief = broker.bindLaunch({ business: 'demo', role: 'chief', run: 'chief-run', harness: 'pi' });
|
||||
+ broker.request(chief, { verb: 'role.claim' });
|
||||
+ const receipts = () =>
|
||||
+ readFileSync(join(root, 'businesses/.history/demo/changes.jsonl'), 'utf8').trim().split('\n').map(JSON.parse);
|
||||
+ const document = () => JSON.parse(readFileSync(file, 'utf8'));
|
||||
+
|
||||
+ // A human's change, for comparison.
|
||||
+ const first = broker.request(human, { verb: 'limits' });
|
||||
+ const byHuman = broker.request(human, { verb: 'limits.set', args: { revision: first.revision, limits: { total: 5 } } });
|
||||
+ assert.equal(document().launch.limits.total, 5);
|
||||
+
|
||||
+ // The chief of staff's change: validated, written, logged, with the same shape of receipt.
|
||||
+ const byChief = broker.request(chief, {
|
||||
+ verb: 'organization.set',
|
||||
+ args: { revision: byHuman.revision, limits: { total: 9 }, models: document().launch.models },
|
||||
+ });
|
||||
+ assert.equal(byChief.limits.total, 9);
|
||||
+ assert.equal(document().launch.limits.total, 9);
|
||||
+ const [h, c] = receipts().filter((r) => r.state === 'committed');
|
||||
+ assert.equal(h.actor, 'jason');
|
||||
+ assert.equal(c.actor, 'chief-of-staff.chief');
|
||||
+ assert.deepEqual(Object.keys(c).sort(), Object.keys(h).sort());
|
||||
+ assert.equal(c.after, byChief.revision);
|
||||
+ assert.equal(c.business, 'demo');
|
||||
+ assert.ok(receipts().some((r) => r.state === 'prepared' && r.transaction === c.transaction));
|
||||
+
|
||||
+ // An invalid change is refused, leaves the file and the receipts alone, and names no state change.
|
||||
+ const bytes = readFileSync(file, 'utf8');
|
||||
+ const count = receipts().length;
|
||||
+ for (const args of [
|
||||
+ { revision: byChief.revision, limits: { total: -1 } },
|
||||
+ { revision: byChief.revision, limits: { total: 'many' } },
|
||||
+ { revision: byChief.revision, limits: { surprise: 1 } },
|
||||
+ { revision: byChief.revision, instances: { chief: 'no-such-model' } },
|
||||
+ { revision: byChief.revision, instances: { nobody: 'claude-opus-5-5' } },
|
||||
+ { revision: byChief.revision, models: {} },
|
||||
+ ])
|
||||
+ assert.throws(
|
||||
+ () => broker.request(chief, { verb: 'organization.set', args }),
|
||||
+ /invalid-request|unknown-model/,
|
||||
+ JSON.stringify(args),
|
||||
+ );
|
||||
+ assert.throws(
|
||||
+ () => broker.request(chief, { verb: 'organization.set', args: { revision: byHuman.revision, limits: { total: 1 } } }),
|
||||
+ /revision-conflict/,
|
||||
+ );
|
||||
+ assert.equal(readFileSync(file, 'utf8'), bytes);
|
||||
+ assert.equal(receipts().length, count);
|
||||
+});
|
||||
+
|
||||
+// REQ-ROLE-2, REQ-COS-1: the 3c→3d step. The 3c database comes out of row 49's real 3b→3c step, run by a
|
||||
+// copy of the engine pinned at 3c (its schema.sql is tests/fixtures/schema-3c.sql), not hand-built DDL.
|
||||
+const here = (path) => new URL(path, import.meta.url);
|
||||
+const REVOKE_NEITHER =
|
||||
+ "INSERT INTO role_claims(at,business,role,op,holder_run,harness,address,by,decision,reason) VALUES('2026-10-15T00:00:00.000Z','demo','coder','revoke','coder-run','pi',NULL,'chief-run',NULL,NULL)";
|
||||
+async function database3c(t) {
|
||||
+ const root = mkdtempSync(join(tmpdir(), 'bus-chief-migrate-'));
|
||||
+ t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
+ const ddl3b = readFileSync(here('./fixtures/schema-3b.sql'), 'utf8');
|
||||
+ mkdirSync(join(root, 'bus'), { mode: 0o700 });
|
||||
+ const path = join(root, 'bus/bus.sqlite');
|
||||
+ const db = new DatabaseSync(path);
|
||||
+ db.exec(ddl3b);
|
||||
+ db.prepare('INSERT INTO meta VALUES (?,?)').run('schema_version', '3b');
|
||||
+ db.prepare('INSERT INTO meta VALUES (?,?)').run('schema_digest', schemaDigest(db));
|
||||
+ db.prepare('INSERT INTO events(id,at,business,kind,body) VALUES(?,?,?,?,?)')
|
||||
+ .run('preserved', '2026-10-15T00:00:00.000Z', 'demo', 'action.allowed', '{"fixture":true}');
|
||||
+ db.prepare(
|
||||
+ "INSERT INTO role_claims(at,business,role,op,holder_run,harness,address,by) VALUES('2026-10-15T00:00:00.000Z','demo','coder','claim','coder-run','pi',NULL,'coder-run')",
|
||||
+ ).run();
|
||||
+ db.close();
|
||||
+ chmodSync(path, 0o600);
|
||||
+ // The engine pinned at 3c: current store.mjs with the 3c→3d step removed and the 3c schema.
|
||||
+ const ddl3c = readFileSync(here('./fixtures/schema-3c.sql'), 'utf8');
|
||||
+ const moduleRoot = join(root, 'engine-3c');
|
||||
+ mkdirSync(join(moduleRoot, 'src/migrations'), { recursive: true });
|
||||
+ let source = readFileSync(here('../src/store.mjs'), 'utf8');
|
||||
+ const pinned = source
|
||||
+ .replace(/export const SCHEMA_SHA256 = '[a-f0-9]+';/, `export const SCHEMA_SHA256 = '${createHash('sha256').update(ddl3c).digest('hex')}';`)
|
||||
+ .replace("export const SCHEMA_VERSION = '3d';", "export const SCHEMA_VERSION = '3c';")
|
||||
+ .replace(/\}, \{\n {2}from: '3c',[\s\S]*?\}\];\nconst timedTables/, '}];\nconst timedTables');
|
||||
+ assert.notEqual(pinned, source);
|
||||
+ assert.ok(!pinned.includes('3c-3d'), 'the 3c engine has no 3c→3d step');
|
||||
+ writeFileSync(join(moduleRoot, 'src/store.mjs'), pinned);
|
||||
+ writeFileSync(join(moduleRoot, 'schema.sql'), ddl3c);
|
||||
+ for (const name of ['3b.sql', '3b-3c.sql'])
|
||||
+ copyFileSync(here('../src/migrations/' + name), join(moduleRoot, 'src/migrations', name));
|
||||
+ const { Store: Store3c } = await import(pathToFileURL(join(moduleRoot, 'src/store.mjs')));
|
||||
+ new Store3c(root, { migrateSchema: true }).close();
|
||||
+ return { root, path };
|
||||
+}
|
||||
+
|
||||
+test('REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused', async (t) => {
|
||||
+ const { root, path } = await database3c(t);
|
||||
+ // The fixture really is 3c: the old trigger refuses a revoke that carries only a reason.
|
||||
+ const old = new DatabaseSync(path);
|
||||
+ assert.equal(old.prepare("SELECT value FROM meta WHERE key='schema_version'").get().value, '3c');
|
||||
+ assert.throws(() => old.exec(REVOKE_NEITHER.replace('NULL,NULL)', "NULL,'a reason')")), /revoke needs a resolved decision$/);
|
||||
+ old.close();
|
||||
+ // A 3c database does not open as 3d until the explicit step runs.
|
||||
+ assert.throws(() => new Store(root), /schema-mismatch/);
|
||||
+ const store = new Store(root, { migrateSchema: true });
|
||||
+ t.after(() => store.close());
|
||||
+ assert.equal(store.get("SELECT value FROM meta WHERE key='schema_version'").value, '3d');
|
||||
+ assert.equal(store.get("SELECT body FROM events WHERE id='preserved'").body, '{"fixture":true}');
|
||||
+ assert.equal(store.get("SELECT holder_run FROM role_claims WHERE role='coder'").holder_run, 'coder-run');
|
||||
+ const b = new Broker({ store, businesses: demo(), organization: null });
|
||||
+ const chief = b.bindLaunch({ business: 'demo', role: 'chief', run: 'chief-run', harness: 'pi' });
|
||||
+ b.request(chief, { verb: 'role.claim' });
|
||||
+ assert.deepEqual(b.request(chief, { verb: 'role.revoke', args: { role: 'coder', reason: 'migrated and ended' } }), {
|
||||
+ revoked: true,
|
||||
+ });
|
||||
+ assert.deepEqual(
|
||||
+ { ...store.get("SELECT op, decision, reason FROM role_claims WHERE role='coder' ORDER BY seq DESC LIMIT 1") },
|
||||
+ { op: 'revoke', decision: null, reason: 'migrated and ended' },
|
||||
+ );
|
||||
+ assert.throws(() => store.run(REVOKE_NEITHER), /revoke needs a resolved decision or a reason/);
|
||||
+});
|
||||
diff --git a/packages/bus/tests/comment-migration.test.mjs b/packages/bus/tests/comment-migration.test.mjs
|
||||
index 8d730e1eb..d2ac6928a 100644
|
||||
--- a/packages/bus/tests/comment-migration.test.mjs
|
||||
+++ b/packages/bus/tests/comment-migration.test.mjs
|
||||
@@ -28,12 +28,12 @@ function legacy(t) {
|
||||
return { root, source, path };
|
||||
}
|
||||
|
||||
-test('explicit 3b copy migration preserves evidence and opens as 3c; normal startup never migrates', t => {
|
||||
+test('explicit 3b copy migration preserves evidence and opens as 3d; normal startup never migrates', t => {
|
||||
const { root, source } = legacy(t);
|
||||
const original = readFileSync(source);
|
||||
assert.throws(() => new Store(root), /schema-mismatch/);
|
||||
const store = new Store(root, { migrateSchema: true });
|
||||
- assert.equal(store.get("SELECT value FROM meta WHERE key='schema_version'").value, '3c');
|
||||
+ assert.equal(store.get("SELECT value FROM meta WHERE key='schema_version'").value, '3d');
|
||||
assert.equal(store.get("SELECT body FROM events WHERE id='preserved'").body, '{"fixture":true}');
|
||||
assert.equal(store.get('SELECT count(*) n FROM task_comment_cursors').n, 0);
|
||||
const epoch = store.get("SELECT value FROM meta WHERE key='comment_cursor_epoch'").value;
|
||||
@@ -62,7 +62,7 @@ test('migration refuses active writers and drifted legacy schemas, with no parti
|
||||
assert.throws(() => new Store(other.root, { migrateSchema: true }), /writer-locked/);
|
||||
});
|
||||
|
||||
-test('named migration registry chains 3b through 3c to a fixture-only future version in order', async t => {
|
||||
+test('named migration registry chains 3b through 3d to a fixture-only future version in order', async t => {
|
||||
const { root } = legacy(t);
|
||||
const { writeFileSync } = await import('node:fs');
|
||||
const { pathToFileURL } = await import('node:url');
|
||||
@@ -75,15 +75,15 @@ test('named migration registry chains 3b through 3c to a fixture-only future ver
|
||||
const sha = createHash('sha256').update(futureDDL).digest('hex');
|
||||
let source = readFileSync(new URL('../src/store.mjs', import.meta.url), 'utf8');
|
||||
source = source.replace(/export const SCHEMA_SHA256 = '[a-f0-9]+';/, `export const SCHEMA_SHA256 = '${sha}';`)
|
||||
- .replace("export const SCHEMA_VERSION = '3c';", "export const SCHEMA_VERSION = '3d';")
|
||||
- .replace('}];\nconst timedTables', `}, { from: '3c', to: '3d', sql: ${JSON.stringify(futureSQL)} }];\nconst timedTables`);
|
||||
+ .replace("export const SCHEMA_VERSION = '3d';", "export const SCHEMA_VERSION = '3e';")
|
||||
+ .replace('}];\nconst timedTables', `}, { from: '3d', to: '3e', sql: ${JSON.stringify(futureSQL)} }];\nconst timedTables`);
|
||||
writeFileSync(join(moduleRoot, 'src/store.mjs'), source);
|
||||
writeFileSync(join(moduleRoot, 'schema.sql'), futureDDL);
|
||||
- for (const name of ['3b.sql', '3b-3c.sql'])
|
||||
+ for (const name of ['3b.sql', '3b-3c.sql', '3c-3d.sql'])
|
||||
copyFileSync(new URL('../src/migrations/' + name, import.meta.url), join(moduleRoot, 'src/migrations', name));
|
||||
const { Store: FutureStore } = await import(pathToFileURL(join(moduleRoot, 'src/store.mjs')));
|
||||
const future = new FutureStore(root, { migrateSchema: true });
|
||||
- assert.equal(future.get("SELECT value FROM meta WHERE key='schema_version'").value, '3d');
|
||||
+ assert.equal(future.get("SELECT value FROM meta WHERE key='schema_version'").value, '3e');
|
||||
assert.equal(future.get("SELECT name FROM sqlite_master WHERE name='future_fixture'").name, 'future_fixture');
|
||||
assert.equal(future.get("SELECT count(*) n FROM task_comment_cursors").n, 0);
|
||||
assert.equal(future.get("SELECT id FROM events").id, 'preserved');
|
||||
@@ -109,7 +109,7 @@ test('offline command migrates only the supplied fixture root and reports usage
|
||||
const command = new URL('../src/migrate-schema.mjs', import.meta.url).pathname;
|
||||
const migrated = spawnSync(process.execPath, [command, root], { encoding: 'utf8' });
|
||||
assert.equal(migrated.status, 0, migrated.stderr);
|
||||
- assert.match(migrated.stdout, /bus schema 3c ready/);
|
||||
+ assert.match(migrated.stdout, /bus schema 3d ready/);
|
||||
new Store(root).close();
|
||||
const usage = spawnSync(process.execPath, [command], { encoding: 'utf8' });
|
||||
assert.equal(usage.status, 2);
|
||||
diff --git a/packages/bus/tests/fixtures/schema-3c.sql b/packages/bus/tests/fixtures/schema-3c.sql
|
||||
new file mode 100644
|
||||
index 000000000..e68424252
|
||||
--- /dev/null
|
||||
+++ b/packages/bus/tests/fixtures/schema-3c.sql
|
||||
@@ -0,0 +1,229 @@
|
||||
+PRAGMA journal_mode = WAL;
|
||||
+PRAGMA foreign_keys = ON;
|
||||
+CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT;
|
||||
+CREATE TABLE events (
|
||||
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
+ id TEXT NOT NULL UNIQUE,
|
||||
+ at TEXT NOT NULL,
|
||||
+ business TEXT NOT NULL,
|
||||
+ kind TEXT NOT NULL CHECK (kind IN (
|
||||
+ 'session.launched',
|
||||
+ 'session.ended',
|
||||
+ 'action.allowed',
|
||||
+ 'action.refused',
|
||||
+ 'task.created',
|
||||
+ 'task.assigned',
|
||||
+ 'task.state',
|
||||
+ 'task.closed',
|
||||
+ 'task.changed.external',
|
||||
+ 'task.conflict',
|
||||
+ 'task.missing',
|
||||
+ 'review.requested',
|
||||
+ 'review.verdict',
|
||||
+ 'human.input',
|
||||
+ 'config.refused',
|
||||
+ 'credential.expiring',
|
||||
+ 'credential.expired',
|
||||
+ 'credential.changed',
|
||||
+ 'launch.revoked',
|
||||
+ 'launch.restored',
|
||||
+ 'digest.sent')),
|
||||
+ actor_role TEXT, actor_run TEXT,
|
||||
+ subject TEXT,
|
||||
+ corrects TEXT REFERENCES events(id),
|
||||
+ body TEXT NOT NULL CHECK (json_valid(body))
|
||||
+) STRICT;
|
||||
+CREATE TABLE role_claims (
|
||||
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
+ at TEXT NOT NULL,
|
||||
+ business TEXT NOT NULL, role TEXT NOT NULL,
|
||||
+ op TEXT NOT NULL CHECK (op IN ('claim','release','revoke')),
|
||||
+ holder_run TEXT NOT NULL,
|
||||
+ harness TEXT NOT NULL, address TEXT,
|
||||
+ by TEXT NOT NULL, reason TEXT,
|
||||
+ decision TEXT
|
||||
+) STRICT;
|
||||
+CREATE TABLE decisions (
|
||||
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
+ id TEXT NOT NULL UNIQUE,
|
||||
+ at TEXT NOT NULL,
|
||||
+ business TEXT NOT NULL, project TEXT,
|
||||
+ raised_by_role TEXT NOT NULL, raised_by_run TEXT NOT NULL,
|
||||
+ class TEXT NOT NULL CHECK (class IN ('routine','within-role','cross-role','gated')),
|
||||
+ action TEXT NOT NULL,
|
||||
+ route_to TEXT NOT NULL,
|
||||
+ question TEXT NOT NULL,
|
||||
+ options TEXT NOT NULL CHECK (json_valid(options) AND json_array_length(options) BETWEEN 2 AND 9),
|
||||
+ recommendation TEXT NOT NULL,
|
||||
+ task_ref TEXT CHECK (task_ref IS NULL OR (task_ref GLOB 'vikunja:[1-9]*/[1-9]*' AND NOT substr(task_ref, 9) GLOB '*[^0-9/]*' AND NOT substr(task_ref, 9) GLOB '*/*/*')), requirement_ref TEXT,
|
||||
+ blocking INTEGER NOT NULL CHECK (blocking IN (0,1)),
|
||||
+ supersedes TEXT REFERENCES decisions(id)
|
||||
+) STRICT;
|
||||
+CREATE TABLE decision_events (
|
||||
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
+ decision TEXT NOT NULL REFERENCES decisions(id),
|
||||
+ at TEXT NOT NULL,
|
||||
+ op TEXT NOT NULL CHECK (op IN ('seen','resolved','withdrawn','expired')),
|
||||
+ by TEXT NOT NULL,
|
||||
+ choice TEXT, note TEXT, via TEXT
|
||||
+) STRICT;
|
||||
+CREATE TABLE messages (
|
||||
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
+ id TEXT NOT NULL UNIQUE,
|
||||
+ at TEXT NOT NULL,
|
||||
+ business TEXT NOT NULL,
|
||||
+ from_role TEXT NOT NULL, from_run TEXT NOT NULL,
|
||||
+ to_role TEXT NOT NULL,
|
||||
+ class TEXT NOT NULL,
|
||||
+ in_reply_to TEXT REFERENCES messages(id),
|
||||
+ decision TEXT REFERENCES decisions(id),
|
||||
+ corrects TEXT REFERENCES messages(id),
|
||||
+ body TEXT NOT NULL
|
||||
+) STRICT;
|
||||
+CREATE TABLE deliveries (
|
||||
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
+ message TEXT NOT NULL REFERENCES messages(id),
|
||||
+ at TEXT NOT NULL,
|
||||
+ op TEXT NOT NULL CHECK (op IN ('routed','delivered','failed','read')),
|
||||
+ holder_run TEXT, transport TEXT, address TEXT, detail TEXT
|
||||
+) STRICT;
|
||||
+CREATE TABLE task_snapshots (
|
||||
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
+ at TEXT NOT NULL,
|
||||
+ business TEXT NOT NULL,
|
||||
+ task_ref TEXT NOT NULL CHECK (task_ref GLOB 'vikunja:[1-9]*/[1-9]*' AND NOT substr(task_ref, 9) GLOB '*[^0-9/]*' AND NOT substr(task_ref, 9) GLOB '*/*/*'),
|
||||
+ updated TEXT NOT NULL,
|
||||
+ etag TEXT,
|
||||
+ digest TEXT NOT NULL CHECK (length(digest) = 64 AND NOT digest GLOB '*[^0-9a-f]*'),
|
||||
+ fields TEXT NOT NULL CHECK (json_valid(fields)),
|
||||
+ source TEXT NOT NULL CHECK (source IN ('self','poll')),
|
||||
+ via TEXT CHECK (via IN ('board','cursor','task','reconcile')),
|
||||
+ read_at TEXT,
|
||||
+ role TEXT, run TEXT,
|
||||
+ CHECK ((source = 'self') = (role IS NOT NULL AND run IS NOT NULL)),
|
||||
+ CHECK ((source = 'poll') = (via IS NOT NULL AND read_at IS NOT NULL)),
|
||||
+ CHECK (json_type(fields, '$.bucket') IS 'integer'
|
||||
+ OR (source IS 'poll' AND via IS 'task' AND json_type(fields, '$.gone') IS 'text'))
|
||||
+) STRICT;
|
||||
+CREATE INDEX task_snapshots_ref ON task_snapshots (business, task_ref, seq);
|
||||
+CREATE TRIGGER decisions_resolve_once BEFORE INSERT ON decision_events
|
||||
+ WHEN NEW.op IN ('resolved','withdrawn','expired') AND EXISTS (
|
||||
+ SELECT 1 FROM decision_events WHERE decision = NEW.decision AND op IN ('resolved','withdrawn','expired'))
|
||||
+ BEGIN SELECT RAISE(ABORT, 'decision already closed'); END;
|
||||
+CREATE TRIGGER decisions_resolved_choice BEFORE INSERT ON decision_events
|
||||
+ WHEN NEW.op = 'resolved' AND (NEW.choice IS NULL OR NOT EXISTS (
|
||||
+ SELECT 1 FROM decisions d, json_each(d.options) o WHERE d.id = NEW.decision AND json_extract(o.value,'$.key') = NEW.choice))
|
||||
+ BEGIN SELECT RAISE(ABORT, 'resolution must name one of the options'); END;
|
||||
+CREATE TRIGGER role_one_holder BEFORE INSERT ON role_claims
|
||||
+ WHEN NEW.op = 'claim' AND (SELECT op FROM role_claims WHERE business = NEW.business AND role = NEW.role ORDER BY seq DESC LIMIT 1) = 'claim'
|
||||
+ BEGIN SELECT RAISE(ABORT, 'role already held'); END;
|
||||
+CREATE TRIGGER role_release_by_holder BEFORE INSERT ON role_claims
|
||||
+ WHEN NEW.op IN ('release','revoke') AND COALESCE((SELECT op FROM role_claims WHERE business = NEW.business AND role = NEW.role ORDER BY seq DESC LIMIT 1),'') <> 'claim'
|
||||
+ BEGIN SELECT RAISE(ABORT, 'role is not held'); END;
|
||||
+CREATE TRIGGER role_release_same_run BEFORE INSERT ON role_claims
|
||||
+ WHEN NEW.op = 'release' AND (SELECT holder_run FROM role_claims WHERE business = NEW.business AND role = NEW.role ORDER BY seq DESC LIMIT 1) <> NEW.holder_run
|
||||
+ BEGIN SELECT RAISE(ABORT, 'only the holder releases; others revoke'); END;
|
||||
+CREATE TRIGGER role_revoke_needs_decision BEFORE INSERT ON role_claims
|
||||
+ WHEN NEW.op = 'revoke' AND NEW.decision IS NULL
|
||||
+ BEGIN SELECT RAISE(ABORT, 'revoke needs a resolved decision'); END;
|
||||
+CREATE TRIGGER meta_no_update BEFORE UPDATE ON meta BEGIN SELECT RAISE(ABORT, 'meta is append-only'); END;
|
||||
+CREATE TRIGGER meta_no_delete BEFORE DELETE ON meta BEGIN SELECT RAISE(ABORT, 'meta is append-only'); END;
|
||||
+CREATE TRIGGER events_no_update BEFORE UPDATE ON events BEGIN SELECT RAISE(ABORT, 'events is append-only'); END;
|
||||
+CREATE TRIGGER events_no_delete BEFORE DELETE ON events BEGIN SELECT RAISE(ABORT, 'events is append-only'); END;
|
||||
+CREATE TRIGGER role_claims_no_update BEFORE UPDATE ON role_claims BEGIN SELECT RAISE(ABORT, 'role_claims is append-only'); END;
|
||||
+CREATE TRIGGER role_claims_no_delete BEFORE DELETE ON role_claims BEGIN SELECT RAISE(ABORT, 'role_claims is append-only'); END;
|
||||
+CREATE TRIGGER decisions_no_update BEFORE UPDATE ON decisions BEGIN SELECT RAISE(ABORT, 'decisions is append-only'); END;
|
||||
+CREATE TRIGGER decisions_no_delete BEFORE DELETE ON decisions BEGIN SELECT RAISE(ABORT, 'decisions is append-only'); END;
|
||||
+CREATE TRIGGER decision_events_no_update BEFORE UPDATE ON decision_events BEGIN SELECT RAISE(ABORT, 'decision_events is append-only'); END;
|
||||
+CREATE TRIGGER decision_events_no_delete BEFORE DELETE ON decision_events BEGIN SELECT RAISE(ABORT, 'decision_events is append-only'); END;
|
||||
+CREATE TRIGGER messages_no_update BEFORE UPDATE ON messages BEGIN SELECT RAISE(ABORT, 'messages is append-only'); END;
|
||||
+CREATE TRIGGER messages_no_delete BEFORE DELETE ON messages BEGIN SELECT RAISE(ABORT, 'messages is append-only'); END;
|
||||
+CREATE TRIGGER deliveries_no_update BEFORE UPDATE ON deliveries BEGIN SELECT RAISE(ABORT, 'deliveries is append-only'); END;
|
||||
+CREATE TRIGGER deliveries_no_delete BEFORE DELETE ON deliveries BEGIN SELECT RAISE(ABORT, 'deliveries is append-only'); END;
|
||||
+CREATE TRIGGER meta_no_replace BEFORE INSERT ON meta WHEN EXISTS (SELECT 1 FROM meta WHERE key = NEW.key) BEGIN SELECT RAISE(ABORT, 'meta is append-only'); END;
|
||||
+CREATE TRIGGER events_no_replace BEFORE INSERT ON events WHEN EXISTS (SELECT 1 FROM events WHERE seq = NEW.seq OR id = NEW.id) BEGIN SELECT RAISE(ABORT, 'events is append-only'); END;
|
||||
+CREATE TRIGGER role_claims_no_replace BEFORE INSERT ON role_claims WHEN EXISTS (SELECT 1 FROM role_claims WHERE seq = NEW.seq) BEGIN SELECT RAISE(ABORT, 'role_claims is append-only'); END;
|
||||
+CREATE TRIGGER decisions_no_replace BEFORE INSERT ON decisions WHEN EXISTS (SELECT 1 FROM decisions WHERE seq = NEW.seq OR id = NEW.id) BEGIN SELECT RAISE(ABORT, 'decisions is append-only'); END;
|
||||
+CREATE TRIGGER decision_events_no_replace BEFORE INSERT ON decision_events WHEN EXISTS (SELECT 1 FROM decision_events WHERE seq = NEW.seq) BEGIN SELECT RAISE(ABORT, 'decision_events is append-only'); END;
|
||||
+CREATE TRIGGER messages_no_replace BEFORE INSERT ON messages WHEN EXISTS (SELECT 1 FROM messages WHERE seq = NEW.seq OR id = NEW.id) BEGIN SELECT RAISE(ABORT, 'messages is append-only'); END;
|
||||
+CREATE TRIGGER deliveries_no_replace BEFORE INSERT ON deliveries WHEN EXISTS (SELECT 1 FROM deliveries WHERE seq = NEW.seq) BEGIN SELECT RAISE(ABORT, 'deliveries is append-only'); END;
|
||||
+CREATE TRIGGER task_snapshots_no_update BEFORE UPDATE ON task_snapshots BEGIN SELECT RAISE(ABORT, 'task_snapshots is append-only'); END;
|
||||
+CREATE TRIGGER task_snapshots_no_delete BEFORE DELETE ON task_snapshots BEGIN SELECT RAISE(ABORT, 'task_snapshots is append-only'); END;
|
||||
+CREATE TRIGGER task_snapshots_no_replace BEFORE INSERT ON task_snapshots WHEN EXISTS (SELECT 1 FROM task_snapshots WHERE seq = NEW.seq) BEGIN SELECT RAISE(ABORT, 'task_snapshots is append-only'); END;
|
||||
+CREATE TRIGGER decisions_blocking_needs_task BEFORE INSERT ON decisions
|
||||
+ WHEN NEW.blocking = 1 AND NEW.task_ref IS NULL
|
||||
+ BEGIN SELECT RAISE(ABORT, 'a blocking decision cites the task it blocks'); END;
|
||||
+CREATE TRIGGER events_launch_by_human BEFORE INSERT ON events
|
||||
+ WHEN NEW.kind IN ('launch.revoked','launch.restored') AND (NEW.actor_role IS NOT NULL OR NEW.actor_run IS NOT NULL)
|
||||
+ BEGIN SELECT RAISE(ABORT, 'only the human revokes or restores launching'); END;
|
||||
+CREATE TRIGGER events_credential_body BEFORE INSERT ON events
|
||||
+ WHEN NEW.kind GLOB 'credential.*' AND (
|
||||
+ json_extract(NEW.body, '$.service') IS NULL OR json_extract(NEW.body, '$.service') NOT IN ('gitea','vikunja')
|
||||
+ OR json_extract(NEW.body, '$.instance') IS NULL)
|
||||
+ BEGIN SELECT RAISE(ABORT, 'credential events name a service and a role instance'); END;
|
||||
+CREATE TRIGGER events_task_missing_body BEFORE INSERT ON events
|
||||
+ WHEN NEW.kind = 'task.missing' AND (
|
||||
+ json_extract(NEW.body, '$.reason') IS NULL OR json_extract(NEW.body, '$.reason') NOT IN ('moved','not-found','no-access')
|
||||
+ OR (json_extract(NEW.body, '$.reason') = 'moved' AND json_type(NEW.body, '$.project') IS NOT 'integer'))
|
||||
+ BEGIN SELECT RAISE(ABORT, 'task.missing carries a reason, and the new project when moved'); END;
|
||||
+CREATE TRIGGER events_task_subject BEFORE INSERT ON events
|
||||
+ WHEN NEW.kind GLOB 'task.*' AND (NEW.subject IS NULL OR NOT (NEW.subject GLOB 'vikunja:[1-9]*/[1-9]*' AND NOT substr(NEW.subject, 9) GLOB '*[^0-9/]*' AND NOT substr(NEW.subject, 9) GLOB '*/*/*'))
|
||||
+ BEGIN SELECT RAISE(ABORT, 'a task event names its task in subject'); END;
|
||||
+CREATE TRIGGER events_task_created_body BEFORE INSERT ON events
|
||||
+ WHEN NEW.kind = 'task.created' AND (
|
||||
+ json_type(NEW.body, '$.request') IS NOT 'text'
|
||||
+ OR NOT EXISTS (SELECT 1 FROM events WHERE id = json_extract(NEW.body, '$.request')
|
||||
+ AND kind = 'human.input' AND business = NEW.business)
|
||||
+ OR json_type(NEW.body, '$.requirement') IS NOT 'text'
|
||||
+ OR NOT json_extract(NEW.body, '$.requirement') GLOB 'REQ-[A-Z]*-[1-9]*'
|
||||
+ OR json_extract(NEW.body, '$.requirement') GLOB 'REQ-*[^A-Z0-9-]*')
|
||||
+ BEGIN SELECT RAISE(ABORT, 'task.created cites the human.input that asked for it and a requirement id'); END;
|
||||
+CREATE VIEW launch_state AS
|
||||
+ SELECT business, CASE kind WHEN 'launch.revoked' THEN 'revoked' ELSE 'allowed' END AS state, at
|
||||
+ FROM events e WHERE kind IN ('launch.revoked','launch.restored')
|
||||
+ AND seq = (SELECT max(seq) FROM events WHERE business = e.business AND kind IN ('launch.revoked','launch.restored'));
|
||||
+CREATE VIEW task_external_changes AS
|
||||
+ SELECT p.business, p.task_ref, p.seq, p.via, p.updated, p.digest, ls.digest AS self_digest
|
||||
+ FROM task_snapshots p
|
||||
+ LEFT JOIN task_snapshots ls ON ls.seq = (SELECT max(seq) FROM task_snapshots
|
||||
+ WHERE business = p.business AND task_ref = p.task_ref AND source = 'self')
|
||||
+ WHERE p.source = 'poll'
|
||||
+ AND p.seq = (SELECT max(seq) FROM task_snapshots WHERE business = p.business AND task_ref = p.task_ref)
|
||||
+ AND (ls.seq IS NULL OR (p.updated >= ls.updated AND p.read_at > ls.at AND p.digest <> ls.digest));
|
||||
+CREATE VIEW task_current AS
|
||||
+ SELECT s.seq, s.at, s.business, s.task_ref, s.updated, s.etag, s.digest, s.fields, s.source, s.via, s.read_at, s.role, s.run
|
||||
+ FROM task_snapshots s
|
||||
+ WHERE s.seq = (SELECT max(c.seq) FROM task_snapshots c
|
||||
+ WHERE c.business = s.business AND c.task_ref = s.task_ref
|
||||
+ AND (c.source = 'self'
|
||||
+ OR c.read_at > coalesce((SELECT ls.at FROM task_snapshots ls
|
||||
+ WHERE ls.business = c.business AND ls.task_ref = c.task_ref AND ls.source = 'self'
|
||||
+ ORDER BY ls.seq DESC LIMIT 1), '')));
|
||||
+CREATE VIEW tasks_open AS
|
||||
+ SELECT s.business, s.task_ref, json_extract(s.fields, '$.bucket') AS bucket, s.seq
|
||||
+ FROM task_current s
|
||||
+ WHERE json_type(s.fields, '$.gone') IS NULL
|
||||
+ AND json_extract(s.fields, '$.done') = 0;
|
||||
+CREATE VIEW urgent_inbox AS
|
||||
+ SELECT d.id, d.business, d.task_ref, d.question, d.at
|
||||
+ FROM decisions d
|
||||
+ WHERE d.class = 'gated' AND d.blocking = 1
|
||||
+ AND NOT EXISTS (SELECT 1 FROM decision_events x WHERE x.decision = d.id AND x.op IN ('resolved','withdrawn','expired'));
|
||||
+CREATE TABLE task_comment_cursors (
|
||||
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
+ business TEXT NOT NULL,
|
||||
+ task_id INTEGER NOT NULL CHECK (task_id > 0),
|
||||
+ last_comment INTEGER NOT NULL CHECK (last_comment > 0)
|
||||
+) STRICT;
|
||||
+CREATE INDEX task_comment_cursors_latest ON task_comment_cursors (business, task_id, seq);
|
||||
+CREATE TRIGGER task_comment_cursors_no_update BEFORE UPDATE ON task_comment_cursors
|
||||
+ BEGIN SELECT RAISE(ABORT, 'task_comment_cursors is append-only'); END;
|
||||
+CREATE TRIGGER task_comment_cursors_no_delete BEFORE DELETE ON task_comment_cursors
|
||||
+ BEGIN SELECT RAISE(ABORT, 'task_comment_cursors is append-only'); END;
|
||||
+CREATE TRIGGER task_comment_cursors_no_replace BEFORE INSERT ON task_comment_cursors
|
||||
+ WHEN EXISTS (SELECT 1 FROM task_comment_cursors WHERE seq = NEW.seq)
|
||||
+ BEGIN SELECT RAISE(ABORT, 'task_comment_cursors is append-only'); END;
|
||||
+CREATE TRIGGER task_comment_cursors_forward BEFORE INSERT ON task_comment_cursors
|
||||
+ WHEN NEW.last_comment <= COALESCE((SELECT last_comment FROM task_comment_cursors
|
||||
+ WHERE business = NEW.business AND task_id = NEW.task_id ORDER BY seq DESC LIMIT 1), 0)
|
||||
+ BEGIN SELECT RAISE(ABORT, 'comment cursor must advance'); END;
|
||||
diff --git a/packages/business/README.md b/packages/business/README.md
|
||||
index 9a6baad3c..3cc21756d 100644
|
||||
--- a/packages/business/README.md
|
||||
+++ b/packages/business/README.md
|
||||
@@ -116,8 +116,11 @@ The `chief-of-staff` role (`roles/chief-of-staff.json`, contract
|
||||
`roles/chief-of-staff.md`, REQ-COS-1) runs the organization: it hires and
|
||||
terminates role instances, owns the structure, takes work from the
|
||||
assistant and never talks to the user. It holds `message.send` and
|
||||
-`role.launch` within role (decision 139), with no credentials and the same
|
||||
-read-only tools. It is the only shipped role that holds `role.launch`, so
|
||||
+`role.launch` within role (decision 139), plus `role.end` and
|
||||
+`organization.change` (row 123): it ends a run with a reason and no decision
|
||||
+(`role.revoke` stays gated for every role), and it changes the organization
|
||||
+file through the broker's `organization.set`, limited to the keys the human's
|
||||
+`limits.set` takes. It has no credentials and the same read-only tools. It is the only shipped role that holds `role.launch`, so
|
||||
it is the only role `launch.by` can name. The PM holds no `role.launch`
|
||||
(REQ-LAUNCH-1); it asks the chief of staff for a session.
|
||||
|
||||
diff --git a/packages/business/src/vocabulary.mjs b/packages/business/src/vocabulary.mjs
|
||||
index 592a476aa..c24993250 100644
|
||||
--- a/packages/business/src/vocabulary.mjs
|
||||
+++ b/packages/business/src/vocabulary.mjs
|
||||
@@ -26,6 +26,11 @@ export const ACTIONS = Object.freeze([
|
||||
"message.external",
|
||||
"role.launch",
|
||||
"role.revoke",
|
||||
+ // Row 123 (#1617, REQ-COS-1): the chief of staff ends a run with a reason and no decision,
|
||||
+ // and changes the organization file through the shared write path. Neither is gated-only,
|
||||
+ // so the grant lives in a role file; role.revoke itself stays gated for every role.
|
||||
+ "role.end",
|
||||
+ "organization.change",
|
||||
"credential.mint",
|
||||
"spend",
|
||||
"deploy",
|
||||
diff --git a/packages/business/tests/chief-of-staff.test.mjs b/packages/business/tests/chief-of-staff.test.mjs
|
||||
index b993cf5c6..c92c4cc6e 100644
|
||||
--- a/packages/business/tests/chief-of-staff.test.mjs
|
||||
+++ b/packages/business/tests/chief-of-staff.test.mjs
|
||||
@@ -11,12 +11,12 @@ import { join } from "node:path";
|
||||
import { ACTIONS, BusinessError, classify, loadBusiness, loadRole, resolveInstance } from "../src/index.mjs";
|
||||
import { businessDoc, launchDoc, REPO_ROLES, systemFor, tmp, writeJson } from "./helpers.mjs";
|
||||
|
||||
-// Decision 104's list plus role.launch (decision 139). A role file that
|
||||
-// differs from it fails.
|
||||
+// Decision 104's list plus role.launch (decision 139) plus role.end and
|
||||
+// organization.change (row 123, REQ-COS-1). A role file that differs from it fails.
|
||||
const APPROVED = Object.freeze({
|
||||
tools: ["read", "grep", "find", "ls"],
|
||||
network: "api-only",
|
||||
- authority: Object.freeze({ withinRole: Object.freeze(["message.send", "role.launch"]), crossRole: Object.freeze([]) }),
|
||||
+ authority: Object.freeze({ withinRole: Object.freeze(["message.send", "role.launch", "role.end", "organization.change"]), crossRole: Object.freeze([]) }),
|
||||
credentials: Object.freeze([]),
|
||||
});
|
||||
|
||||
@@ -83,11 +83,14 @@ test("the resolved record matches the approved list, so a widened file fails", (
|
||||
assert.deepEqual(record.credentials, {});
|
||||
});
|
||||
|
||||
-test("the chief of staff holds role.launch and message.send; every other action is gated", () => {
|
||||
+test("the chief of staff holds role.launch, message.send, role.end and organization.change; every other action is gated", () => {
|
||||
const record = resolved(loadFixture());
|
||||
+ const held = ["message.send", "role.launch", "role.end", "organization.change"];
|
||||
for (const action of ACTIONS) {
|
||||
- assert.equal(classify(record, action), action === "message.send" || action === "role.launch" ? "within" : "gated", action);
|
||||
+ assert.equal(classify(record, action), held.includes(action) ? "within" : "gated", action);
|
||||
}
|
||||
+ // role.revoke itself stays gated for the chief of staff: role.end is the grant, not a loophole.
|
||||
+ assert.equal(classify(record, "role.revoke"), "gated");
|
||||
assert.equal(classify(record, "task.create"), "gated");
|
||||
});
|
||||
|
||||
diff --git a/packages/harness/tests/tools.test.mjs b/packages/harness/tests/tools.test.mjs
|
||||
index 6ab36b534..582fd6385 100644
|
||||
--- a/packages/harness/tests/tools.test.mjs
|
||||
+++ b/packages/harness/tests/tools.test.mjs
|
||||
@@ -29,7 +29,7 @@ test("the PM gets its task verbs and the reads, and no launch", (t) => {
|
||||
}
|
||||
});
|
||||
|
||||
-// Row 119 (#1613): the chief of staff holds launch and message.send, and nothing else.
|
||||
+// Row 119 (#1613): the chief of staff holds launch and message.send; row 123 adds role.end and organization.change (no typed tools).
|
||||
test("the chief of staff gets launch and send_message, and no task verbs", (t) => {
|
||||
const chief = resolvedFor(t, "chief");
|
||||
const tools = typedTools({ resolved: chief, tracker: true });
|
||||
@@ -38,7 +38,8 @@ test("the chief of staff gets launch and send_message, and no task verbs", (t) =
|
||||
assert.equal(launch.verb, "launch");
|
||||
assert.equal(launch.action, "role.launch");
|
||||
assert.deepEqual(launch.parameters.properties.instance.enum, ["coder", "reviewer"]);
|
||||
- assert.deepEqual(untypedActions(chief, tools), []);
|
||||
+ // Row 123 (#1617, REQ-COS-1): the chief also holds role.end and organization.change on the bus; no typed tool yet.
|
||||
+ assert.deepEqual(untypedActions(chief, tools), ["role.end", "organization.change"]);
|
||||
});
|
||||
|
||||
// Row 119 (#1613): the assistant holds message.send alone.
|
||||
diff --git a/roles/chief-of-staff.json b/roles/chief-of-staff.json
|
||||
index aafbd2a6e..8613710a4 100644
|
||||
--- a/roles/chief-of-staff.json
|
||||
+++ b/roles/chief-of-staff.json
|
||||
@@ -6,7 +6,7 @@
|
||||
"tools": ["read", "grep", "find", "ls"],
|
||||
"network": "api-only",
|
||||
"authority": {
|
||||
- "withinRole": ["message.send", "role.launch"],
|
||||
+ "withinRole": ["message.send", "role.launch", "role.end", "organization.change"],
|
||||
"crossRole": []
|
||||
},
|
||||
"credentials": []
|
||||
@@ -0,0 +1,9 @@
|
||||
business: rc=0 ℹ tests 88 ℹ pass 88 ℹ fail 0
|
||||
bus: rc=0 ℹ tests 190 ℹ pass 190 ℹ fail 0
|
||||
tasks: rc=0 ℹ tests 97 ℹ pass 97 ℹ fail 0
|
||||
cli: rc=0 ℹ tests 195 ℹ pass 195 ℹ fail 0
|
||||
webui: rc=0 ℹ tests 112 ℹ pass 112 ℹ fail 0
|
||||
harness: rc=1 ℹ tests 84 ℹ pass 83 ℹ fail 1
|
||||
seat: rc=0 ℹ tests 132 ℹ pass 132 ℹ fail 0
|
||||
test-land-work: rc=0 land-work selftests: 84 passed, 0 failed
|
||||
done
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 123 (#1617): the targeted gate (decision 136), run in the given worktree, 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.txt" >/dev/null
|
||||
echo "$1: rc=${PIPESTATUS[0]} $(grep -E '^ℹ (tests|pass|fail) ' "$OUT/$1.txt" | tr '\n' ' ')"
|
||||
}
|
||||
for d in business bus tasks cli webui harness seat; do node_suite $d packages/$d; done
|
||||
setsid -w timeout -k 10 1200 bash scripts/test-land-work.sh 2>&1 | tee "$OUT/test-land-work.txt" >/dev/null
|
||||
echo "test-land-work: rc=${PIPESTATUS[0]} $(grep -iE 'passed' "$OUT/test-land-work.txt" | tail -1)"
|
||||
echo done
|
||||
@@ -0,0 +1,198 @@
|
||||
✔ agents: a held role with nothing on the bus is idle; a role session gets the claim rows unchanged (25.773808ms)
|
||||
✔ agents: working counts what the holder received and has not read; routed alone is not working (25.73112ms)
|
||||
✔ agents: input needed is the role's open decisions routed to the human, gated first, then oldest (44.674625ms)
|
||||
✔ agents: working wins over waiting, and Input needed still shows (18.747604ms)
|
||||
✔ agents: a new holder doesn't inherit what the last one left unread; decisions stay with the role (22.989227ms)
|
||||
✔ agents: state reads only its own business, even for a run name both use (25.705905ms)
|
||||
✔ agents: a message the holder released unread is stranded; the next holder gets nothing and cannot read it (26.230589ms)
|
||||
✔ agents: a message the holder read is not stranded; one the current holder has not read is not listed (22.895582ms)
|
||||
✔ agents: stranded is oldest first and capped at 50 for a role (105.36242ms)
|
||||
✔ agents: stranded is per role and per business (43.302129ms)
|
||||
✔ agents: a message routed to a held role and not yet received is not stranded (18.543389ms)
|
||||
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (26.161553ms)
|
||||
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (29.535307ms)
|
||||
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (31.724054ms)
|
||||
✔ launch events require a human CLI capability; generic emit cannot forge authority events (20.313484ms)
|
||||
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (21.895459ms)
|
||||
✔ a recommendation that is not exactly one option key refuses invalid-recommendation after the options (34.326618ms)
|
||||
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (21.660459ms)
|
||||
✔ task action subjects and linked decision trail are complete and ordered (31.119535ms)
|
||||
✔ launch binding is durable and reconnecting requires the identical trusted record (15.187412ms)
|
||||
✔ business isolation includes inherited object names and cross-business message references (23.257914ms)
|
||||
✔ authority never transfers between action, run, target, unresolved or replaced role holder (32.848503ms)
|
||||
✔ task projection uses schema current view, skipping earlier and equal-start polls (17.38437ms)
|
||||
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (25.895619ms)
|
||||
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (19.570396ms)
|
||||
✔ both arbiters require human resolution when their cross-role route is themselves (37.727859ms)
|
||||
✔ the shipped coder role raises task.reassign as gated to the human; task.scope.change stays cross-role (29.431582ms)
|
||||
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.943328ms)
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (30.895851ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (35.753284ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (23.066663ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (23.204301ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (29.268033ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (31.74908ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (29.356407ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (25.940257ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (32.542532ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (39.93075ms)
|
||||
✔ REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human's; an invalid one is refused (100.503491ms)
|
||||
✔ REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused (78.024708ms)
|
||||
✔ comment cursors isolate businesses, move only forward and remain append-only (19.630868ms)
|
||||
✔ only trusted sync writes validated cursors; events and cursor share rollback (16.363071ms)
|
||||
✔ explicit 3b copy migration preserves evidence and opens as 3d; normal startup never migrates (71.252625ms)
|
||||
✔ migration refuses active writers and drifted legacy schemas, with no partial upgrade (128.225858ms)
|
||||
✔ named migration registry chains 3b through 3d to a fixture-only future version in order (75.784739ms)
|
||||
✔ migration refuses mismatched source metadata without changing schema or evidence (66.555965ms)
|
||||
✔ offline command migrates only the supplied fixture root and reports usage errors (174.755119ms)
|
||||
✔ credentials gives human and reader sessions service, account, role, date and state, and nothing else (22.224788ms)
|
||||
✔ credentials refuses a role session and any argument (19.999647ms)
|
||||
✔ credentials reads the status on each request, and a broker without one lists none (26.870491ms)
|
||||
✔ the broker process answers credentials without a token, path or variable in its output, log or store (91.125665ms)
|
||||
✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.212458ms)
|
||||
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.386226ms)
|
||||
✔ expiry refuses use and env references never become client data (0.724428ms)
|
||||
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.804819ms)
|
||||
✔ opaque tokens shorter than 16 characters refuse before use (0.487571ms)
|
||||
✔ a human resolve sends the raising role one message from the human, and its request is the answer (43.333826ms)
|
||||
✔ a cross-role decision resolved by its arbiter comes from that role and run, with no request (29.468769ms)
|
||||
✔ a human withdraw sends one message with the note (26.503631ms)
|
||||
✔ a human seen before the close leaves the request on the close's answer, not the seen (35.151947ms)
|
||||
✔ the raising run's withdraw, a supersede, a within-role raise and seen send nothing (36.093875ms)
|
||||
✔ refused closes send nothing: invalid-choice, decision-closed, not-resolver (32.205194ms)
|
||||
✔ a raising role nobody holds at the close gets the message on the next claim (28.20074ms)
|
||||
✔ a human DECISION send keeps its own instruction as the request, not a close's answer (27.053115ms)
|
||||
✔ roles lists open decisions routed to a role, and never the human inbox (32.386149ms)
|
||||
✔ recent lists closed decisions of every route, newest close first, with how each closed (34.4117ms)
|
||||
✔ recent gives the closing event as closed when the decision was seen first (35.689923ms)
|
||||
✔ recent stops at RECENT rows (114.415214ms)
|
||||
✔ every row is what trail already gives the same session for that id (45.33563ms)
|
||||
✔ a role session can't list decisions, so it can't read another role's open decisions (46.032782ms)
|
||||
✔ decisions is a read: one business, strict arguments, nothing written (53.917363ms)
|
||||
✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (23.034545ms)
|
||||
✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (14.605691ms)
|
||||
✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (17.436483ms)
|
||||
✔ endLaunch leaves a claim another run took alone (20.399476ms)
|
||||
✔ refuse records action.refused against the caller with the code only (15.382884ms)
|
||||
✔ launches off refuses role.launch with launch-revoked until launches on (20.285954ms)
|
||||
✔ broker process: launch ops authorize role.launch, record refusals and end runs (118.376196ms)
|
||||
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.659496ms)
|
||||
✔ process reader gets own kernel identity without exposing environment values (0.699238ms)
|
||||
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.036386ms)
|
||||
✔ real pid 1 remains inspectable when its environment is protected (0.407212ms)
|
||||
✔ limits binds actor from human proof and rejects role, reader, tester and forged actor (28.160309ms)
|
||||
✔ runtime wires verified human settings to the actual shared file writer and actor receipt (53.571078ms)
|
||||
✔ uncertain organization receipts leave bus startup and reads alone but refuse writes until reconciled (47.930668ms)
|
||||
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (30.885692ms)
|
||||
✔ missing and foreign-business citations refuse and roll back message and grant (21.938091ms)
|
||||
✔ cross-role sends still need a matching resolved decision and consume it once (28.818866ms)
|
||||
✔ messages pages all human messages newest first without writes; read state is history (396.283171ms)
|
||||
✔ messages refuses role callers, invalid paging and cursors outside this human feed (30.247993ms)
|
||||
✔ views messages passes only its paging arguments through (0.276415ms)
|
||||
✔ messages isolates businesses even with a valid foreign cursor (20.754868ms)
|
||||
✔ the assistant sends to the user and to the chief of staff only (38.449145ms)
|
||||
✔ an assistant send to any other role, or to the tester, is refused and logged (40.432425ms)
|
||||
✔ only the user and the chief of staff may send to the assistant; the tester is refused by default (41.309501ms)
|
||||
✔ a sender-side list alone refuses a send to a role that has no list (34.398026ms)
|
||||
✔ a target-side list alone refuses a send from a role that has no list (36.984568ms)
|
||||
✔ the sender list is checked before the target list (30.065694ms)
|
||||
✔ roles without the field behave exactly as before (47.299833ms)
|
||||
✔ a business with no role definitions at all is unlimited (39.29234ms)
|
||||
✔ a malformed messaging field refuses at boot (20.574435ms)
|
||||
✔ the assistant's review.request to a reviewer is refused and logged, even with an approved decision (40.662428ms)
|
||||
✔ a review.verdict to the assistant from a role outside its receiveFrom is refused and logged (39.617684ms)
|
||||
✔ review requests and verdicts between roles without the field are unchanged (31.27377ms)
|
||||
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (92.278443ms)
|
||||
✔ startup token refusal returns safe code without value or partial listening broker (62.658517ms)
|
||||
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (85.185144ms)
|
||||
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (145.434609ms)
|
||||
✔ trusted host registers later launches; socket clients never have a registration verb (79.175339ms)
|
||||
✔ runtime excludes declared project roots even when host supplies no repoRoots (45.742883ms)
|
||||
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (60.638219ms)
|
||||
✔ a reply is listed for human and reader sessions, and its delivery rows are unchanged (38.673516ms)
|
||||
✔ the reply still reaches the human message.receive (mosaic talk) after the Console showed it (24.384568ms)
|
||||
✔ a message with no reply gives an empty list (22.116339ms)
|
||||
✔ replies come oldest first and stop at REPLIES (93.406423ms)
|
||||
✔ an id that names no message in the business refuses message-not-found, and the refusal is recorded (34.403351ms)
|
||||
✔ strict arguments; a role session gets human-required (40.896632ms)
|
||||
✔ views passes replies through as one call with the id (0.290766ms)
|
||||
✔ requirements returns the committed list to a holding role session, and no one else (29.757504ms)
|
||||
✔ the PM's prd.approve with no task, when no id fits, reaches the human's Inbox (19.865047ms)
|
||||
✔ the PM's non-blocking role.revoke with no task reaches the human's Inbox (24.025224ms)
|
||||
✔ a push is recorded as the action.allowed for git.push.working on the task, and only the host records one (33.267158ms)
|
||||
✔ review.request names the latest pushed commit and sends the reviewer a REVIEW-REQUEST (30.495994ms)
|
||||
✔ review.request says which run holds the reviewer role, or null, and a null request waits for a claim (31.757232ms)
|
||||
✔ review.request refuses a commit that isn't the latest push, a non-reviewer and a role without the action (43.911281ms)
|
||||
✔ reviewFetch answers only the addressed reviewer, for the request it cites (31.266201ms)
|
||||
✔ review.verdict answers one request once, to the role that asked (41.357627ms)
|
||||
✔ task.close on a pushed task needs an approve verdict for the latest push (48.703263ms)
|
||||
✔ a refusal the host decided is filed on the task it names, and only a task ref counts (26.228236ms)
|
||||
✔ v3b prototype refusals, views and append-only mutations (98.92542ms)
|
||||
✔ gated approval authorizes once, survives store reopen, and fresh approval works (37.321417ms)
|
||||
✔ another run cannot consume an approval; a failed check leaves it usable (23.660742ms)
|
||||
✔ two scheduled callers have exactly one grant and one consumed refusal (18.786474ms)
|
||||
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (40.370916ms)
|
||||
✔ class drift gated to cross-role refuses before consumption (34.308629ms)
|
||||
✔ class drift cross-role to gated refuses before consumption (30.782374ms)
|
||||
✔ class drift gated to within-role refuses before consumption (28.631253ms)
|
||||
✔ class drift cross-role to within-role refuses before consumption (27.298881ms)
|
||||
✔ class drift within-role to gated refuses before consumption (24.314312ms)
|
||||
✔ class drift within-role to cross-role refuses before consumption (21.643172ms)
|
||||
✔ message.send consumes approval and prevents a later send or authorize (22.047302ms)
|
||||
✔ role.revoke consumes approval and prevents a later revoke or authorize (21.530776ms)
|
||||
✔ a business that never switched launches reads allowed with no time and no event (19.55543ms)
|
||||
✔ launches revoked and then restored read allowed, with the restore time and event id (17.443934ms)
|
||||
✔ a task closed on the tracker leaves the open list, and total counts the open ones (18.201631ms)
|
||||
✔ a tombstone is not open (17.661488ms)
|
||||
✔ the open list stops at the cap and says so; total still counts every open task (22.183501ms)
|
||||
✔ a role and a reader session read state; arguments, and an unbound cap, refuse (18.641086ms)
|
||||
✔ a human session reads the same state, with the same cap (24.653856ms)
|
||||
✔ reading state writes nothing: every table keeps its row count (20.896878ms)
|
||||
✔ creates private WAL store and excludes a second writer until explicit close (17.133075ms)
|
||||
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (21.302025ms)
|
||||
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (22.379434ms)
|
||||
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (78.978841ms)
|
||||
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (14.018098ms)
|
||||
✔ async transactions refuse before invoking their function (10.278068ms)
|
||||
✔ the session default fits under the reply cap and pages to the end with next (184.393388ms)
|
||||
✔ the short form: no description, assignees by role, every listed key (72.431288ms)
|
||||
✔ each filter, alone and together, and total counts every match (141.508643ms)
|
||||
✔ task_ref reads one task in full, as the whole board has it (83.95337ms)
|
||||
✔ no arguments is the read the Console and mosaic make, unchanged (68.191997ms)
|
||||
✔ an unknown argument or a bad value refuses invalid-request (300.463593ms)
|
||||
✔ tasks rows carry priority and requirement for every session that reads tasks (52.387232ms)
|
||||
✔ the read model rebuilds from the ledger: a reopened bus and each trail give the same values (55.105986ms)
|
||||
✔ an empty business and one with snapshots but no task.created read cleanly (14.147074ms)
|
||||
✔ recordTask keeps sync reads and a role write apart (28.271461ms)
|
||||
✔ read_at must be one canonical UTC format, so the projection compares strings safely (12.427738ms)
|
||||
✔ a bad entry refuses the whole record (12.764877ms)
|
||||
✔ taskView reads the projection for one business (14.4416ms)
|
||||
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (25.558773ms)
|
||||
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (274.045632ms)
|
||||
✔ without an adapter the server refuses every task verb (15.264937ms)
|
||||
✔ the runtime refuses an invalid adapter and closes a valid one (15.479719ms)
|
||||
✔ the process loads the S3 adapter from plain-data trackers (110.678642ms)
|
||||
✔ the tester binds only when the business sets bus.tester to exactly true (82.605543ms)
|
||||
✔ a business may not name a role tester, so the tester address is never a role (17.872848ms)
|
||||
✔ every verb outside the tester list refuses tester-refused and is recorded against the tester (33.609694ms)
|
||||
✔ a tester message is from tester, starts [mos test], and writes no human.input or authority (15.097474ms)
|
||||
✔ a role answers a tester only in reply to a tester message, and the answer never reaches the human (17.074788ms)
|
||||
✔ the tester reads the human views a reader reads (21.073675ms)
|
||||
✔ tester proof binds its own CLI, start time and nonce, refuses a managed run and allows an agent (1.245924ms)
|
||||
✔ neither CLI's nonce proves the other: each proof is bound to its own CLI and nonce variable (0.373079ms)
|
||||
✔ testerNonce reads only the tester's variable and gives null on any read error (0.229022ms)
|
||||
✔ an envelope carries exactly one of cap, human and tester; no tester authenticator refuses (15.673178ms)
|
||||
✔ tester-cli.mjs binds through the runtime when the switch is on, and refuses off or under a run marker (633.225331ms)
|
||||
✔ socket capability stamps launch identity; shared views use wire, no SQL client (31.311708ms)
|
||||
✔ two wire claims serialize; a lost reply never automatically retries (22.691352ms)
|
||||
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (14.584751ms)
|
||||
✔ client preserves UTF-8 when a response divides a multibyte character (11.684825ms)
|
||||
✔ committed mutation followed by dropped reply reports unknown and is never retried (13.54732ms)
|
||||
ℹ tests 190
|
||||
ℹ suites 0
|
||||
ℹ pass 190
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1373.080014
|
||||
@@ -0,0 +1,96 @@
|
||||
✔ the role file loads and resolves through loadBusiness (8.561873ms)
|
||||
✔ the shipped file is exactly the approved list (1.306182ms)
|
||||
✔ the assistant's authority is message.send within role and nothing else (3.689303ms)
|
||||
✔ the assistant talks to the user and the chief of staff only, and no other shipped role is limited (1.103253ms)
|
||||
✔ a business grant through limits does not widen the role (4.09007ms)
|
||||
✔ a launch block whose by is the assistant is refused (5.954562ms)
|
||||
✔ the instance takes no credential references and no tracker (4.746321ms)
|
||||
✔ the contract passes work to the chief of staff and directs no one (0.816155ms)
|
||||
✔ config directory and file path follow MOSAIC_CONFIG (2.302997ms)
|
||||
✔ the fixture business validates and comes back frozen (6.992597ms)
|
||||
✔ two instances may share a definition (2.753507ms)
|
||||
✔ top-level refusals (6.98224ms)
|
||||
✔ arbiters and projects (12.53187ms)
|
||||
✔ role instances (4.415255ms)
|
||||
✔ Vikunja bots (10.535515ms)
|
||||
✔ a role without Vikunja takes no tracker block (5.174273ms)
|
||||
✔ credential references match the definition's services (3.920098ms)
|
||||
✔ launch (10.627572ms)
|
||||
✔ loadBusiness: file checks (2.41745ms)
|
||||
✔ loadBusiness: not a regular file (46.672344ms)
|
||||
✔ loading writes nothing (1.271379ms)
|
||||
✔ names that are Object.prototype properties don't count as declared (3.432286ms)
|
||||
✔ the shipped example refuses as written and validates once filled in (1.055734ms)
|
||||
✔ the role file loads and resolves through loadBusiness (9.659896ms)
|
||||
✔ the shipped file is exactly the approved list (0.558953ms)
|
||||
✔ the resolved record matches the approved list, so a widened file fails (3.616578ms)
|
||||
✔ the chief of staff holds role.launch, message.send, role.end and organization.change; every other action is gated (2.338486ms)
|
||||
✔ the pm does not hold role.launch (3.735546ms)
|
||||
✔ a business grant of a gated action through limits does not widen the role (3.060774ms)
|
||||
✔ a limit that drops role.launch leaves the chief of staff with no launch block (2.412819ms)
|
||||
✔ a launch block whose by is the pm instance is refused (1.934564ms)
|
||||
✔ the contract names no duty to talk to the human and the pm contract no launch (0.457905ms)
|
||||
✔ the instance takes no credential references and no tracker (4.130674ms)
|
||||
✔ usage errors exit 4 (332.323502ms)
|
||||
✔ validate: a good business exits 0 and prints instance digests (65.629708ms)
|
||||
✔ validate: project files (347.370137ms)
|
||||
✔ validate: missing files and a broken system config (262.072883ms)
|
||||
✔ validate: credential reference problems exit 2 and name each one (74.392562ms)
|
||||
✔ validate: a token file inside the repository is refused (73.30194ms)
|
||||
✔ validate: role definitions come from MOSAIC_ROLES_DIR (223.756145ms)
|
||||
✔ resolve: prints one instance's record (295.387961ms)
|
||||
✔ resolve: refusals (401.32677ms)
|
||||
✔ parse: exactly one of file or env, plus the service's date (5.203886ms)
|
||||
✔ check: a good file has no problems (1.238334ms)
|
||||
✔ check never opens the file: a write-only token passes (0.464445ms)
|
||||
✔ check: file problems (1.303127ms)
|
||||
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (1.175034ms)
|
||||
✔ check: dates and environment references (0.633458ms)
|
||||
✔ REQ-LIMIT-1 schema: every scope unset or zero, adjustable above four, bad values and unknown models refuse (22.194976ms)
|
||||
✔ REQ-LIMIT-1 counts each exact scope at/over limit, unlimited and old tuple after model reassignment (1.160287ms)
|
||||
✔ REQ-ROLE-3 writer: atomic validated revision, immutable previous bytes and authenticated actor/old/new receipt (29.302232ms)
|
||||
✔ REQ-ROLE-3 writer: pre-rename fault leaves old file, post-rename recovery records commit, conflicting recovery refuses (15.892012ms)
|
||||
✔ REQ-LIMIT-1 / decision145 migration prints only, preserves values, rejects ambiguity, and is idempotent (4.534665ms)
|
||||
✔ REQ-LIMIT-1 shared settings validates the complete proposed selection and logs exact actor (19.76335ms)
|
||||
✔ REQ-LIMIT-1 provider is table data: another provider routes separately and aliases share exact counts (1.967774ms)
|
||||
✔ REQ-ROLE-3 refuses live writer locks, symlink targets and uncertain recovery without changing bytes (10.538642ms)
|
||||
✔ path and load (3.971512ms)
|
||||
✔ refusals (1.772584ms)
|
||||
✔ systemVars flattens the validated config (3.951868ms)
|
||||
✔ precedence: system, business, project, project role, agent (8.302109ms)
|
||||
✔ limits narrow the definition and never widen it (3.37866ms)
|
||||
✔ role.launch stays within-role only for the instance the launch block names (8.469044ms)
|
||||
✔ limits.authority without role.launch leaves the launcher with no launch block (2.896144ms)
|
||||
✔ limits.authority narrows cross-role actions too (1.488279ms)
|
||||
✔ classify (1.300713ms)
|
||||
✔ the record carries what the broker and launcher need (1.523966ms)
|
||||
✔ digest: key order doesn't matter, any value change does (7.684425ms)
|
||||
✔ refusals (2.372658ms)
|
||||
✔ the four shipped version 2 roles load (5.482421ms)
|
||||
✔ shipped role scopes match addendum B section 2 and the SR runbook (2.146612ms)
|
||||
✔ shipped authority follows the note's table (1.023964ms)
|
||||
✔ every role file in roles/ loads through the validator, and a v2 contract exists (1.212693ms)
|
||||
✔ version 1 files keep loading with no authority (1.267437ms)
|
||||
✔ the conductor policy isn't a role (0.248898ms)
|
||||
✔ a missing role file is exit 4, a symbolic link too (0.536987ms)
|
||||
✔ version 2 refusals (1.633254ms)
|
||||
✔ authority: closed vocabulary, no gated-only action, no overlap (3.811763ms)
|
||||
✔ messaging: optional, role definitions and human, never the tester (2.762898ms)
|
||||
✔ credentials: Gitea scopes (1.712099ms)
|
||||
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (3.59082ms)
|
||||
✔ credentials: services (0.839819ms)
|
||||
✔ contract: a non-empty regular Markdown file beside the role file (1.112948ms)
|
||||
✔ every key names known layers and a merge rule (1.873041ms)
|
||||
✔ unknown keys and wrong layers refuse (1.272693ms)
|
||||
✔ types (3.090585ms)
|
||||
✔ merge: defaults, then the most specific layer wins (0.511374ms)
|
||||
✔ merge: limits only narrow, and provenance lists each source (0.504299ms)
|
||||
✔ merge doesn't change its inputs (0.196935ms)
|
||||
ℹ tests 88
|
||||
ℹ suites 0
|
||||
ℹ pass 88
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 2173.776709
|
||||
@@ -0,0 +1,205 @@
|
||||
✔ every screen and overlay fits 120x36, 80x24 and the narrow layout, row for row (1451.453894ms)
|
||||
✔ width counts terminal cells: wide CJK and emoji 2, U+FE0F after a one-cell base 1, marks 0 (72.43413ms)
|
||||
✔ broker text is shown, never interpreted: no raw control character or bidi override reaches the terminal (150.540225ms)
|
||||
✔ NO_COLOR: no escape codes, except reverse video on the selected row (229.653534ms)
|
||||
✔ 1-9 shows the decide flow's own lines and question; only y resolves (51.522798ms)
|
||||
✔ the app shows the same decide text as mosaic decide (24.612202ms)
|
||||
✔ any key but y or Y at the question answers No; nothing is sent (205.529664ms)
|
||||
✔ below 40x10 the question is hidden, so every key answers it No, y included (Filbert R3) (71.010255ms)
|
||||
✔ under 40 columns a tall terminal shows the too-small screen even where the decide box would fit; y sends nothing (Filbert N10) (267.658118ms)
|
||||
✔ a hidden decide box names the way out, and the status line under it does not promise y (Filbert N12) (35.07959ms)
|
||||
✔ a throw while answering the question goes to crash, sends nothing and leaves no job pending (20.422401ms)
|
||||
✔ type-ahead never answers the question: a paste, keys typed before it is drawn, keys inside askMs (416.508844ms)
|
||||
✔ quitting at the question answers No and sends nothing, armed or not (36.738341ms)
|
||||
✔ a timer tick reads nothing under an overlay, never answers the question, and keeps a resolved status (31.72981ms)
|
||||
✔ a long question is cut inside the question; the action, target, choice and prompt stay (Dewey B1) (53.514761ms)
|
||||
✔ a decide box too big for the terminal says so, and every key answers it No, y included (Dewey B1, round 3) (338.227171ms)
|
||||
✔ at every size the decide box shows each line y depends on, or says it is hidden (8386.791715ms)
|
||||
✔ outcome unknown: the CLI's sentence, no retry, then the trail or a re-read (62.928228ms)
|
||||
✔ refused and failed reads keep the last list, marked stale; a refused decide says refused (32.090445ms)
|
||||
✔ help or a detail box cut to fit keeps its last line, Esc included (Dewey) (15.038783ms)
|
||||
✔ keys: views, moving, trail of a selection, the command line, help and quit (84.674229ms)
|
||||
✔ one option: the status line says Press 1, not 1-1 (15.614699ms)
|
||||
✔ no business: the app says so and :business picks one (9.889539ms)
|
||||
✔ mosaic with no argument: exit 3 in an agent run, exit 4 with the usage off a terminal, the app on one (1.873408ms)
|
||||
✔ scripts/mosaic with no argument reaches the app entry; the seat usage it printed before has not drifted (169.422321ms)
|
||||
✔ strings to keep exactly live once, in the decide flow and the transport; the app does not restate them (0.426614ms)
|
||||
✔ decide with --note still asks; a No sends nothing (9.513772ms)
|
||||
✔ the app's business comes from a live bus host only (0.948838ms)
|
||||
✔ runApp leaves the terminal as it found it on q, end of input, SIGTERM, SIGINT, SIGHUP and an error, the first frame's included (6.815874ms)
|
||||
✔ draw is pure: same state, same frame (0.719311ms)
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (36.52909ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (26.866563ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (20.675139ms)
|
||||
✔ decide prints a declining choice as declining (19.071954ms)
|
||||
✔ an unknown outcome is reported once and never resent (19.235945ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (19.235879ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (16.484757ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (16.11062ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (14.074243ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (16.448863ms)
|
||||
✔ agents and tasks print through the broker (17.656099ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.147761ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (75.916684ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (94.62716ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (52.247872ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (47.720556ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (50.178499ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (77.33387ms)
|
||||
✔ follow prints an unsolicited message after plain talk returned (44.551594ms)
|
||||
✔ talk --follow continues after reply and prints a later unsolicited message (39.19874ms)
|
||||
✔ inbox lists all unread human messages under decisions, does not deliver or read them (321.804928ms)
|
||||
✔ follow refuses agent context and bad arguments (11.984015ms)
|
||||
✔ follow Ctrl-C wakes the poll sleep and removes its handler; transport errors also clean up (11.564046ms)
|
||||
✔ plain talk leaves the process SIGINT handlers unchanged (11.272751ms)
|
||||
✔ empty views say so (1.055387ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.357964ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.218502ms)
|
||||
✔ the review loop: the coder pushes and asks, the reviewer fetches and approves, and the task's trail holds each step (503.172616ms)
|
||||
✔ refusals: no action, non-fast-forward, an unknown commit or request, and a push for another task moves only that task's ref (331.51736ms)
|
||||
✔ the size cap refuses a push before any byte (164.410572ms)
|
||||
✔ a fetch over the cap refuses before any byte reaches the reviewer (328.849775ms)
|
||||
✔ the socket refuses a request with no capability, an unknown one, and a bad request line (155.369345ms)
|
||||
✔ a rigged clone runs nothing: hooks, fsmonitor, a filter, a gitlink and a symlinked .git/config leave no marker (249.6899ms)
|
||||
✔ an identity with no role, or of another business, refuses not-a-role and moves no ref (193.442056ms)
|
||||
✔ a review of a project the business no longer has refuses unknown-project (203.974686ms)
|
||||
✔ past maxActive connections the socket refuses git-busy, and a hung-up connection frees its slot (137.607471ms)
|
||||
✔ close() answers host-closing to a connection still sending its request line (132.646756ms)
|
||||
✔ a bundle that trickles in refuses body-timeout at its deadline and frees its slot (545.665213ms)
|
||||
✔ a reviewer gone before its bundle was sent is filed fetch-aborted, not as the host's fault (455.941843ms)
|
||||
✔ a reviewer that hangs up the moment it has the whole bundle was served, not aborted (498.216167ms)
|
||||
✔ send: a close after every byte left is served; one that fails a queued write is fetch-aborted (42.566997ms)
|
||||
✔ a bundle with no bytes for bodyIdle refuses body-timeout long before its deadline (453.914475ms)
|
||||
✔ send: a close once the loop is past its last chunk is served, and a failed write there is fetch-aborted (44.922149ms)
|
||||
✔ letGo waits for an answer to leave for at most its cap, then destroys the socket (301.853556ms)
|
||||
✔ a record that fails and can't be undone refuses rollback-failed, which the host logs (334.961044ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (952.140628ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (167.0638ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (86.032917ms)
|
||||
✔ two bus starts racing for one data root leave one host running; the other refuses that a host already runs (527.263851ms)
|
||||
✔ a host takes over a host.lock its dead host left, and puts back a live claim that replaced it meanwhile (1.503219ms)
|
||||
✔ a host.lock that goes between the check and the read is tried again; one that can't be parsed still refuses as unreadable (0.799004ms)
|
||||
✔ a live host's lock stays, however old the file is (0.554906ms)
|
||||
✔ a host.lock left by a host killed with SIGKILL doesn't stop the next bus start (768.247279ms)
|
||||
✔ while a host boots, a reader sees no host: the human commands still ask for --business, stop and status find none (203.609377ms)
|
||||
✔ a host started before host.lock existed, holding only host.json, still refuses a second start, which leaves no lock (27.280601ms)
|
||||
✔ a broker fork that throws leaves no host.lock and no host.json (28.350671ms)
|
||||
✔ a broker send that throws leaves no host.lock and no host.json (28.952963ms)
|
||||
✔ a broker that misses its boot wait leaves no host.lock and no host.json (30.780255ms)
|
||||
✔ a notifier fork that throws stops the broker and leaves no host.lock and no host.json (85.625469ms)
|
||||
✔ a notifier send that throws stops the broker and leaves no host.lock and no host.json (91.019001ms)
|
||||
✔ a boot reply with no reader for the business refuses with exit 3 naming it, before the notifier starts, and leaves no host.lock and no live broker (87.173063ms)
|
||||
✔ a boot reply with no reader for the business refuses with exit 3 naming it, with no notifier, and leaves no host.lock and no live broker (95.215899ms)
|
||||
✔ a host.json that can't be written stops both children and leaves no host.lock (212.471486ms)
|
||||
✔ an error event on the broker before it replies fails the start at once, and leaves no host.lock and no live child (35.176092ms)
|
||||
✔ an error event on the notifier before it replies fails the start at once, and leaves no host.lock and no live child (97.604341ms)
|
||||
✔ an error event on the broker while the notifier starts fails the start once the notifier replies (214.977402ms)
|
||||
✔ an error event on the broker once the host is up stops the host with exit 1, and leaves no host.lock and no host.json (194.089653ms)
|
||||
✔ an error event on the notifier once the host is up stops the host with exit 1, and leaves no host.lock and no host.json (257.370639ms)
|
||||
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1161.088921ms)
|
||||
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (163.972732ms)
|
||||
✔ a broker reply with no request waiting breaks the channel and the host exits 1 (86.738003ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (137.551979ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (142.092059ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (91.264129ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (163.869337ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (92.014275ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (155.492403ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (23.086097ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.571109ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (248.159432ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (105.191151ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (730.878605ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (53.176496ms)
|
||||
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (65.174709ms)
|
||||
✔ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (13.023771ms)
|
||||
✔ bus-service.sh refuses a default claude without the execute bit, or in a PATH dir with a space (8.207904ms)
|
||||
✔ each launch rereads limits and model selections; lowered limits keep sessions and log exact hit (52.215903ms)
|
||||
✔ bus start --pm: the launcher instance (the chief of staff) runs under its own PID namespace, registered, with a manifest (1293.914282ms)
|
||||
✔ the chief of staff launches a coder through its launch tool; the coder answers; refusals name their code (576.199446ms)
|
||||
✔ each launch makes sure the workspace has a clone of the project: cloned, fast-forwarded, or fetched and logged (3586.554233ms)
|
||||
✔ a project clone that can't be made refuses checkout-failed, from --chief and from a session, with no run and no clone left (126.719175ms)
|
||||
✔ a runner that stops at once ends its launch with the runner's reason (204.322958ms)
|
||||
✔ a Claude Code launch with no session credential refuses before it starts (96.198662ms)
|
||||
✔ a pi launch needs no session credential: pi logs in through its own ENV_ALLOW variables (207.460258ms)
|
||||
✔ a runner killed from the host ends its launch as killed, not failed (250.515715ms)
|
||||
✔ a missing or failing bwrap refuses sandbox-unavailable before anything starts; there is no unconfined fallback (100.37284ms)
|
||||
✔ a real Claude Code coder's shell can't read a stand-in secret or anything else its sandbox doesn't hold (1042.093979ms)
|
||||
✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (628.038024ms)
|
||||
✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3600.653171ms)
|
||||
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (99.891453ms)
|
||||
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (95.092436ms)
|
||||
✔ a launch client that never closes its side doesn't hold the host's close (99.640589ms)
|
||||
✔ a runner that ignores SIGTERM is killed when the host closes (1214.825367ms)
|
||||
✔ claudeVersion asks MOSAIC_CLAUDE_BIN when set, else claude on PATH (4.233692ms)
|
||||
✔ migration needs no bus/config, prints whole converted document, is idempotent and never writes (15.993207ms)
|
||||
✔ limits view/set use the human bus transport and pass only the explicit patch (1.91513ms)
|
||||
✔ post-119 example and Jason fixture migrate with checked inherited models; wrong or absent config refuses (266.611778ms)
|
||||
✔ zoned uses the IANA zone across DST (22.828984ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (41.511205ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (20.541865ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.299575ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (22.49783ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (24.754761ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (27.12388ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (24.915703ms)
|
||||
✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (124.059716ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (61.249451ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (14.765405ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (9.9925ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.795316ms)
|
||||
✔ no Discord id reaches the journal or the log (12.064943ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (1.854443ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.617116ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.536413ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.523497ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (2.282227ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.240425ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.390666ms)
|
||||
✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.535979ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.452441ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.533555ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.276792ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.168605ms)
|
||||
✔ a human command refused with human-required prints the other-tree line and exits 3 (3.173393ms)
|
||||
✔ a --tester command refused with tester-required prints the other-tree line and exits 3 (1.191649ms)
|
||||
✔ other exit-3 refusals print no other-tree line (1.815449ms)
|
||||
✔ the line names no path (0.142408ms)
|
||||
✔ shared copy: npm ci, pushed reachability, readonly/existing copy, webui delegation (598.017947ms)
|
||||
✔ wrapper preserves Q81 lease and marker, rejects foreign/symlink; check uses effective roots (452.605547ms)
|
||||
✔ bus unit copy/pm pins, install refuses all busy states and failed reads, drop-in refusal (420.031968ms)
|
||||
✔ missing package manifest refuses before npm; npm failure removes the unpublished temp copy (274.007474ms)
|
||||
✔ real pinned install loads bus and Console; same-copy mosaic talk passes proof, other trees refuse (3397.423775ms)
|
||||
✔ bus status reads effective copy/pm/drop-ins without writes; check refuses an overridden ExecStart (449.680467ms)
|
||||
✔ drop-ins in either override field and malformed metadata refuse; uninstall also checks --no-reload (166.44779ms)
|
||||
✔ install never shows a template name on systemd 261; template drop-ins still refuse (172.227753ms)
|
||||
✔ unlisted instance drop-in is found in another unit lookup directory; path discovery errors refuse (168.360624ms)
|
||||
✔ inherited mosaic-.service.d override refuses without changing the unit (190.573197ms)
|
||||
✔ inherited service.d override refuses without changing the unit (176.079136ms)
|
||||
✔ inherited drop-in unreadable directory or file refuses before writing (148.81259ms)
|
||||
✔ talk --tester sends a [mos test] message as tester from inside an agent and prints the role's reply (35.273713ms)
|
||||
✔ talk --tester refuses text without the [mos test] prefix before any call (15.367154ms)
|
||||
✔ the read verbs take --tester inside an agent, and the trail shows the tester as the sender (24.061962ms)
|
||||
✔ decide, stop and launches take no --tester (19.094467ms)
|
||||
✔ the tester transport runs tester-cli.mjs, and the tester's refusals exit 3 (0.65752ms)
|
||||
✔ with no stand-in transport, --tester runs tester-cli.mjs on the data root's socket (0.905242ms)
|
||||
task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200
|
||||
task.close on a missing task answered: task-not-found; it made GET /tasks/999 404
|
||||
✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (347.712798ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (46.724797ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2216.0206ms)
|
||||
✔ busExit and refuseInsideAgent (0.464275ms)
|
||||
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (54.464367ms)
|
||||
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1022.819071ms)
|
||||
✔ a late reply the Console has listed still reaches the next talk (2016.069356ms)
|
||||
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (14.974876ms)
|
||||
✔ launches off and on go to the broker and change the business's launch state (12.716854ms)
|
||||
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (12.775711ms)
|
||||
ℹ tests 195
|
||||
ℹ suites 0
|
||||
ℹ pass 195
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 13413.451273
|
||||
@@ -0,0 +1,92 @@
|
||||
✔ sessionModel: agent vars win, then the system's execution settings (10.048767ms)
|
||||
✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.634914ms)
|
||||
✔ a chief-of-staff bundle lists launch among the bus tools and the manifest's typed tools (2.080685ms)
|
||||
✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.437311ms)
|
||||
✔ the prompt's built-in tools line is the harness's own names, as the manifest lists them (9.866908ms)
|
||||
✔ the prompt names the project clones in the workspace, and none when there are none (6.781517ms)
|
||||
✔ the prompt explains the review loop to a role with push_working, and reviews to one with fetch_review (6.919561ms)
|
||||
✔ a bundle is written once: an existing file refuses (1.41878ms)
|
||||
✔ a path with a single quote can't go into the hook command (2.060506ms)
|
||||
✔ allow exits 0, a deny exits 2 with the reason on stderr (123.843185ms)
|
||||
✔ a missing or wrong policy, or a bad event, exits 2 (90.1554ms)
|
||||
✔ the bundle's wrapped command: a missing gate or node still blocks (1085.285412ms)
|
||||
✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (715.872701ms)
|
||||
✔ claude: the hook alone blocks a path outside the workspace (396.842733ms)
|
||||
✔ claude: a second turn resumes the first turn's session (675.324224ms)
|
||||
✔ claude adapter: --restricted is always passed (5.113874ms)
|
||||
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (15.968539ms)
|
||||
✔ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs (5.491949ms)
|
||||
✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (673.010689ms)
|
||||
✔ claude: a missing hook or MCP file refuses before claude starts (7.278188ms)
|
||||
✔ pi: policy tools and typed tools pass, anything else is blocked (3.021177ms)
|
||||
✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.671484ms)
|
||||
✔ file tool paths must resolve inside the workspace (1.135303ms)
|
||||
✔ pi's own path normalisation can't be used to step out (0.908645ms)
|
||||
✔ a symlink inside the workspace that points out is outside (0.861558ms)
|
||||
✔ a dangling symlink is refused at any depth, in both harnesses (3.418987ms)
|
||||
✔ read is checked under every spelling pi's read would open, in both harnesses (17.283939ms)
|
||||
✔ other spellings cover directories, dangling links and pi's cwd (1.538465ms)
|
||||
✔ a relative path climbs from the workspace's real path, in both harnesses (4.223439ms)
|
||||
✔ claude path fields per tool (0.723098ms)
|
||||
✔ glob patterns stay inside the workspace (0.592633ms)
|
||||
✔ a path that can't be checked is blocked (0.332574ms)
|
||||
✔ fetch_review's have lists the working refs before origin's, and no more than HAVE_MAX (203.456827ms)
|
||||
✔ fetch_review's have orders the working refs newest first within each clone, one clone after another (133.126841ms)
|
||||
✔ a runner git past its time: SIGTERM reaches its process group, and SIGKILL follows after the grace period (1809.566064ms)
|
||||
✔ once a signalled runner git has closed, its group gets SIGKILL, so a process that ignored SIGTERM dies too (303.965675ms)
|
||||
✔ initialize, ping and tools/list (42.948271ms)
|
||||
✔ tools/call goes through the tool socket; a refusal is an isError result (30.748104ms)
|
||||
✔ replies are compact JSON; over the cap a tool error names the size and the cap (29.895394ms)
|
||||
✔ unknown tools and methods are JSON-RPC errors and never reach the socket (29.603791ms)
|
||||
✔ a missing argument is a usage error (25.386995ms)
|
||||
✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (366.708708ms)
|
||||
✔ pi: a reply over the cap is a tool error naming the size and the cap; list_tasks asks for open tasks (309.348032ms)
|
||||
✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (308.497607ms)
|
||||
✔ pi: a read is refused when pi would open another spelling outside (309.154814ms)
|
||||
✔ pi: a relative path climbs from the real path of a workspace behind a symlink (303.441152ms)
|
||||
✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (302.81619ms)
|
||||
✔ pi: a missing extension refuses before any model call (5.992404ms)
|
||||
✔ pi: an extension without its configuration fails pi's start (242.42306ms)
|
||||
✔ founderCheck: founder variables, then a needed service without a usable token (1.155287ms)
|
||||
✔ turnRequest names the sender, class, reply and decision (0.196394ms)
|
||||
✔ turnRequest says a tester's message is a test and never Jason's instruction (0.209333ms)
|
||||
✔ turnRequest shows the message's own sent time, a day old as well as new (0.139768ms)
|
||||
✔ stateBlock names the launch state with its time and event, and the open tasks with the cap (0.250475ms)
|
||||
✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (185.189675ms)
|
||||
✔ launches revoked and then restored: the turn's block reads allowed and gives the restore time (171.487353ms)
|
||||
✔ a task closed on the tracker is missing from the next turn's open list (168.48297ms)
|
||||
✔ a message read a day after it was sent shows the time it was sent (89.217829ms)
|
||||
✔ a decision the human resolves becomes a turn citing the decision and the answer (141.278924ms)
|
||||
✔ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read (63.403916ms)
|
||||
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (45.158438ms)
|
||||
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (272.020151ms)
|
||||
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1187.308453ms)
|
||||
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (91.699356ms)
|
||||
✔ founder credentials stop before the claim (20) (88.126919ms)
|
||||
✔ a refused claim exits 21; an ended run's capability exits 22 (85.532224ms)
|
||||
✔ the launch ending under a running session exits 22 (101.90788ms)
|
||||
✔ a broker that stays unreachable exits 23 after brokerRetries polls (199.674854ms)
|
||||
✔ a broker that is down at the claim exits 23, not 21 (44.586464ms)
|
||||
✔ no capability, or a malformed one, on stdin exits 2 (152.072792ms)
|
||||
✔ a missing or malformed policy exits 2 before the claim (95.383418ms)
|
||||
✔ the PM gets its task verbs and the reads, and no launch (8.22019ms)
|
||||
✔ the chief of staff gets launch and send_message, and no task verbs (2.277062ms)
|
||||
✔ the assistant gets send_message and the reads, no launch (3.44139ms)
|
||||
✔ list_requirements reads the requirement ids, for a task.create holder with a tracker (3.8741ms)
|
||||
✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (1.430727ms)
|
||||
✔ a coder with a tracker gets its own task tools and no task_reassign (1.405098ms)
|
||||
✔ launch only when the business's launch block names the instance as launcher (2.760494ms)
|
||||
✔ an action outside the instance's authority has no tool (1.471441ms)
|
||||
✔ the review loop: push_working and request_review for the coder, fetch_review and review_verdict for the reviewer, none for the PM (5.708079ms)
|
||||
✔ callTool: one JSON line out, the result back, a refusal rejects (11.46274ms)
|
||||
✔ list_tasks takes the broker's filters and pages; get_task takes a task_ref (3.780256ms)
|
||||
✔ callTool sends list_tasks done false unless the session names done; other tools as given (3.529335ms)
|
||||
✔ replyText: compact JSON up to the cap, an error naming the size and the cap over it (1.199585ms)
|
||||
ℹ tests 84
|
||||
ℹ suites 0
|
||||
ℹ pass 84
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 10533.737954
|
||||
@@ -0,0 +1,145 @@
|
||||
✔ a first launch clones the root at its branch, with the root's path as origin (35.647035ms)
|
||||
✔ the clone is not at its place until git clone has finished (22.376355ms)
|
||||
✔ a later launch fast-forwards a clean clone after a commit to the root, then finds it current (63.135317ms)
|
||||
✔ a clone ahead of the root keeps its commits and counts as current (46.371ms)
|
||||
▶ a dirty clone and one on another branch are fetched, and nothing but refs/remotes moves
|
||||
✔ a modified file (52.782825ms)
|
||||
✔ an untracked file (46.785253ms)
|
||||
✔ a staged file (44.138035ms)
|
||||
✔ another branch (41.41113ms)
|
||||
✔ a detached HEAD (41.084853ms)
|
||||
✔ a dirty clone and one on another branch are fetched, and nothing but refs/remotes moves (227.354359ms)
|
||||
✔ a diverged clone gets the fetch into refs/remotes and is kept, not-fast-forward (43.343549ms)
|
||||
▶ a clone kept before the branch check, or for a failed status, gets no fetch
|
||||
✔ not-a-clone (24.028631ms)
|
||||
✔ hooks-present (24.983644ms)
|
||||
✔ submodule (31.430732ms)
|
||||
✔ config-changed (29.455606ms)
|
||||
✔ root-unreadable (31.832256ms)
|
||||
✔ root-detached (38.425646ms)
|
||||
✔ status-failed, on the root's branch and clean (38.25342ms)
|
||||
✔ status-failed, dirty (36.850619ms)
|
||||
✔ a clone kept before the branch check, or for a failed status, gets no fetch (256.537714ms)
|
||||
✔ a dirty clone whose fetch fails is kept, fetch-failed, exactly as it was (39.118317ms)
|
||||
✔ every git call the host makes, for a new clone and an existing one, carries maintenance.auto=false and gc.auto=0 (45.6933ms)
|
||||
✔ a clone with loose objects enough for gc --auto gets no maintenance or gc from the fetch and the merge (78.104038ms)
|
||||
▶ a clone with user.name or user.email in its config is kept, and git runs nothing in it
|
||||
✔ user.name (28.347137ms)
|
||||
✔ user.email (33.583628ms)
|
||||
✔ a clone with user.name or user.email in its config is kept, and git runs nothing in it (62.482889ms)
|
||||
✔ a commit git can't read after the fast-forward is null (60.495848ms)
|
||||
✔ a new clone whose commit can't be read refuses the launch and leaves nothing behind (18.104512ms)
|
||||
▶ a clone the session rigged is kept, and git runs nothing and writes nothing outside it
|
||||
✔ core.fsmonitor (33.188891ms)
|
||||
✔ core.hooksPath (32.046871ms)
|
||||
✔ a hook file (30.624906ms)
|
||||
✔ include.path (33.114049ms)
|
||||
✔ includeIf (32.350107ms)
|
||||
✔ a filter driver (33.189818ms)
|
||||
✔ origin moved (32.197296ms)
|
||||
✔ a second origin url (33.295408ms)
|
||||
✔ the origin url twice, both the root (33.64937ms)
|
||||
✔ an ext:: origin (34.866393ms)
|
||||
✔ a branch tracking another remote (33.137582ms)
|
||||
✔ a branch merging a tag (33.348092ms)
|
||||
✔ a fetch line onto local branches (32.990777ms)
|
||||
✔ no origin (33.734166ms)
|
||||
✔ a config file over the size cap (30.82859ms)
|
||||
✔ config a symlink to /dev/zero (30.899775ms)
|
||||
✔ the clone a symlink to the bait (30.964907ms)
|
||||
✔ the clone a file (30.129544ms)
|
||||
✔ the clone a plain directory (31.611479ms)
|
||||
✔ .git a gitdir: file (32.499149ms)
|
||||
✔ .git a symlink (30.777692ms)
|
||||
✔ .git/logs a symlink (31.697703ms)
|
||||
✔ .git/refs/heads a symlink (31.440492ms)
|
||||
✔ .git/objects a symlink (30.999916ms)
|
||||
✔ a reflog hard-linked to a file outside (30.668297ms)
|
||||
✔ HEAD a fifo (31.456417ms)
|
||||
✔ a commondir file (30.830089ms)
|
||||
✔ an alternates file (30.610475ms)
|
||||
✔ a gitlink staged, no .gitmodules (42.192991ms)
|
||||
✔ a gitlink and .gitmodules staged (43.942958ms)
|
||||
✔ a gitlink to a repository with its own gitlink (51.623533ms)
|
||||
✔ a gitlink and .gitmodules committed (46.102611ms)
|
||||
✔ a gitlink and submodule.active in config (44.464334ms)
|
||||
✔ a .git/modules with no gitlink (33.614222ms)
|
||||
✔ a clone the session rigged is kept, and git runs nothing and writes nothing outside it (1163.374327ms)
|
||||
▶ a root that can't be cloned refuses with the project id and leaves nothing behind
|
||||
✔ missing (9.971354ms)
|
||||
✔ not a repository (10.247813ms)
|
||||
✔ a plain directory inside another repository (14.886223ms)
|
||||
✔ unreadable (9.927159ms)
|
||||
✔ on no branch (13.321705ms)
|
||||
✔ missing an object (15.514718ms)
|
||||
✔ a root that can't be cloned refuses with the project id and leaves nothing behind (74.745474ms)
|
||||
✔ a clone whose root is on no branch, or gone, is kept and named (28.933223ms)
|
||||
✔ no credential or forge remote of the root reaches a clone (49.521173ms)
|
||||
✔ every project in the business file gets its own clone at its own branch (37.711965ms)
|
||||
✔ a clone past its time gets SIGTERM and refuses the launch, with nothing left in the workspace (513.600392ms)
|
||||
✔ a clone that ignores SIGTERM gets SIGKILL once the grace period is over (1016.528235ms)
|
||||
✔ a timeout signals git's process group, so a process git started dies with it (514.528173ms)
|
||||
✔ once a signalled git has closed, its group gets SIGKILL, so a process that ignored SIGTERM dies too (514.368538ms)
|
||||
✔ RUNNER_FILES is exactly what the runner and the harness sources it starts import (1.557312ms)
|
||||
✔ sandboxProblem names a missing bwrap and a bwrap that fails (8.395704ms)
|
||||
✔ every bind says why; the bundle and session file stay read-only over the run directory (1.334023ms)
|
||||
✔ the host's git socket is bound only when given, read-only and before the writable binds (0.946165ms)
|
||||
✔ the git identity is the instance's, at <instance>@<business>.mosaic.invalid, and sandboxArgv refuses without it (1.025063ms)
|
||||
✔ a confined session's shell reaches its own workspace and run directory and nothing of the founder's or another session's (1733.679443ms)
|
||||
✔ a bind that exposes a stand-in, or a missing workspace bind, fails the matching check (2509.911739ms)
|
||||
✔ a confined session that runs tester-cli.mjs is refused as tester, and the same shim outside the sandbox binds (207.727479ms)
|
||||
✔ resolveSeat: by name under --repo resolves the repo layout (1.264824ms)
|
||||
✔ resolveSeat: by path resolves the fleet layout (0.331277ms)
|
||||
✔ resolveSeat: refusals for missing dir, missing launch.sh, non-executable launch.sh, invalid name, and unknown layout (0.783024ms)
|
||||
✔ tmuxContext: outside tmux, default socket, custom socket, and exec failure (0.639252ms)
|
||||
✔ makeRegistration produces a record that validates; each shape violation throws SeatError (0.79552ms)
|
||||
✔ writeRegistration/readRegistration: round trip, permissions, absence, and malformed records (1.35882ms)
|
||||
✔ updateTask: changes task, taskSetBy and updatedAt only, and refuses appropriately (1.196079ms)
|
||||
✔ CLI launch: registers, execs the fake launch script, and passes args through (28.255519ms)
|
||||
✔ CLI launch: --harness lands in the record (30.089922ms)
|
||||
✔ CLI launch: the launch script's own exit code passes through (27.686196ms)
|
||||
✔ CLI launch: relaunching a seat rewrites the one registration record (61.500163ms)
|
||||
✔ CLI launch: omitting --task records an empty string, not null (28.82776ms)
|
||||
✔ CLI seat task: updates only the task after a launch, and refuses on an unlaunched seat (91.857553ms)
|
||||
✔ CLI refusals: no args, unknown flag, missing config, already-registered env, and exec failure (130.666949ms)
|
||||
✔ samePath: equal paths, symlinked dirs, distinct dirs, and non-strings (0.447688ms)
|
||||
✔ resolveSetBy: explicit --by wins over the environment; absent or empty environment gives unknown; invalid explicit or environment values refuse with exit 4 (0.574108ms)
|
||||
✔ validateRegistration/readRegistration: taskSetBy is optional; a record without it (written before #1511) still loads unchanged; an invalid one is refused; the version does not change (0.690009ms)
|
||||
✔ updateTask: records setBy, preserves startedAt and every unrelated field, upgrades an old record in place only when the task is set, and replaces a previous attribution (1.077275ms)
|
||||
✔ CLI seat task: --by beats MOSAIC_AGENT_NAME, the environment beats nothing, empty environment is unknown, invalid --by or environment refuses with exit 4 and leaves the record byte for byte (272.491047ms)
|
||||
✔ REQ-LIMIT-1: family substring matching is no longer exported (0.727586ms)
|
||||
✔ sessionEnv passes only the allowlist, the repo's bin on PATH, and the run id (0.66976ms)
|
||||
✔ sessionExit gives a runner's signal death back as the signal; a session credential is one whole set of nonempty variables (0.244351ms)
|
||||
✔ newRun: short ids, 0700 directories, and a refusal when the socket path won't fit (1.468007ms)
|
||||
✔ session file and launch log: 0600, the session file written once (1.009259ms)
|
||||
✔ endReason maps the runner's exit codes; a signal is killed (0.135636ms)
|
||||
✔ Registry mirrors to sessions.json; readSessions marks live entries; bad files refuse (1.192228ms)
|
||||
✔ stopSession refuses an unknown run, reports a stale one, and won't signal a pid that isn't a runner (1.033956ms)
|
||||
✔ a session runs in its sandbox as pid 1 of its namespace, claims, answers, and stops on mosaic stop (243.518594ms)
|
||||
✔ workRef and validBranch take only what a ref name can hold (1.112451ms)
|
||||
✔ a push lands as the task's work ref, a fast-forward lands after it, and main is the root's (103.351569ms)
|
||||
✔ a rewritten branch refuses non-fast-forward and the work ref stays (101.827029ms)
|
||||
✔ a branch with no history in common with main refuses unrelated-history (51.090868ms)
|
||||
✔ a bundle that needs commits the working repository lacks refuses missing-prerequisite (39.471595ms)
|
||||
✔ main is fetched from the root again before each push, so a clone of a newer root pushes (92.007477ms)
|
||||
✔ a root moved back after the clone leaves the push's base unknown: missing-prerequisite (61.518363ms)
|
||||
✔ a commit with a broken object refuses invalid-bundle, and only because fsck is on (60.417915ms)
|
||||
✔ only a v2 bundle with the named branch is read (49.184037ms)
|
||||
✔ a branch named main, or named for another task, moves only this task's work ref (83.134374ms)
|
||||
✔ a record that throws moves the work ref back, or removes a new one (104.585341ms)
|
||||
✔ a work ref that can't go back refuses rollback-failed, naming the ref and the commit (365.973568ms)
|
||||
✔ a root that can't be read refuses root-unreadable (27.569353ms)
|
||||
✔ a git past its time gets SIGTERM, leaves no ref lock, and the next push lands (2117.477605ms)
|
||||
✔ a git that ignores SIGTERM gets SIGKILL once the grace period is over (1029.531387ms)
|
||||
✔ a working repository path that isn't a directory refuses (23.54545ms)
|
||||
✔ review bundles a pushed commit as refs/mosaic/review, leaving out what the reviewer has (96.311967ms)
|
||||
✔ review of a project with no working repository refuses unknown-commit (12.915778ms)
|
||||
✔ the runner's git tools hold the host's limits and branch rule (1.128271ms)
|
||||
ℹ tests 132
|
||||
ℹ suites 0
|
||||
ℹ pass 132
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 4969.535212
|
||||
@@ -0,0 +1,105 @@
|
||||
✔ the boot config is checked before anything starts (24.173815ms)
|
||||
✔ a business with no tracker entry refuses task verbs (14.523849ms)
|
||||
✔ credential.expiring and .expired are recorded once per instance (32.2283ms)
|
||||
✔ a token file that changes on disk records credential.changed (14.421671ms)
|
||||
✔ autostart polls, reconciles and retries a startup the tracker was down for (35.392511ms)
|
||||
✔ a refusal a restart must clear is not retried by the poll (11.819795ms)
|
||||
✔ a poll that fires while two are queued is dropped (14.67114ms)
|
||||
✔ close waits for a running verb and refuses one that has not started (42.419524ms)
|
||||
✔ stopped bus resumes an empty task cursor after a long outage, exactly once (51.706093ms)
|
||||
✔ persisted IDs avoid replay, exclude bots and bound rows to observed new comments (31.141275ms)
|
||||
✔ commit followed by crash before cache advance neither loses nor duplicates a comment (20.702111ms)
|
||||
✔ failed event insert rolls back cursor and a retry still emits the comment once (17.171064ms)
|
||||
✔ initial cursor accepts a comment within the stated one-minute clock skew margin (15.046976ms)
|
||||
✔ the bundled Vikunja is the pinned upstream image the runbook names (0.788128ms)
|
||||
✔ every published port is on 127.0.0.1, and no secret is in the file (0.278236ms)
|
||||
✔ the committed table loads frozen; its verbs are the broker task verbs (2.048849ms)
|
||||
✔ a write names a verb the table has as its own key, not one it inherits (0.491768ms)
|
||||
✔ the schema refuses any other shape (2.217402ms)
|
||||
✔ the Field table in the slice 1 plan names the verbs each field is written by (0.643655ms)
|
||||
✔ each verb refuses, asks for a decision and writes as the table and the role files say (176.120038ms)
|
||||
✔ the list holds the PRD's requirement ids, in its order, at its version (1.865877ms)
|
||||
✔ the committed list loads frozen (0.886833ms)
|
||||
✔ the schema refuses any other shape (2.65661ms)
|
||||
✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (96.64509ms)
|
||||
✔ the project list holds recorded project objects in the recorded list envelope (10.437882ms)
|
||||
✔ the recorded task bodies pass the checks S3 applies to every read (0.454297ms)
|
||||
✔ the client works against the fake over real HTTP with the platform fetch (94.600766ms)
|
||||
✔ a correct install starts, and the first reconcile records tasks that already exist (32.125226ms)
|
||||
✔ verbs refuse while a business is starting and after startup refused it (15.873204ms)
|
||||
✔ startup refuses a token that can do more than its role needs (42.617416ms)
|
||||
✔ startup refuses an unsupported version and flags an untested one (32.565285ms)
|
||||
✔ startup refuses a board that the runbook did not install (41.10271ms)
|
||||
✔ startup refuses a project the sync bot cannot read (8.843707ms)
|
||||
✔ startup refuses a configured label the pm bot cannot see (9.96455ms)
|
||||
✔ startup refuses an expired credential and a missing sync credential (17.505925ms)
|
||||
✔ an unreachable tracker refuses with tracker-unavailable (6.398348ms)
|
||||
✔ an edit in the UI is recorded once, with the fields that changed (77.87175ms)
|
||||
✔ a move between open buckets is seen on the board, though updated does not change (39.972498ms)
|
||||
✔ a person's comment is counted and a bot's is not (69.947194ms)
|
||||
✔ the hourly reconcile catches a comment through comment_count (78.333463ms)
|
||||
✔ a task closed in the UI leaves the open view with its done bucket (112.638221ms)
|
||||
✔ a task that leaves the board is recorded as deleted, moved or out of reach (63.026194ms)
|
||||
✔ a poll that read before a verb wrote does not overwrite the verb (33.690592ms)
|
||||
✔ a tracker fault during a tick is reported and the next tick catches up (32.755588ms)
|
||||
✔ a malformed answer refuses the tick with tracker-shape (14.152114ms)
|
||||
✔ no token value reaches the database, the log or a refusal (44.414588ms)
|
||||
✔ the first look at a task counts only comments inside the window (33.29866ms)
|
||||
✔ with descendants off the adapter reads no project list and no child, as in v0 (47.258971ms)
|
||||
✔ a three-level tree is read per project, and a project outside it is not (22.724171ms)
|
||||
✔ a watch-only child keeps the business ready, and a failed child read refuses as transient (26.181446ms)
|
||||
✔ a root without the installed buckets refuses with descendants on (11.922483ms)
|
||||
✔ the project list refuses without read_all, and skips archived projects and saved filters (22.770241ms)
|
||||
✔ descendants must be true or false (10.042493ms)
|
||||
✔ a move inside the tree is a moved tombstone plus a snapshot under the new ref (62.22219ms)
|
||||
✔ a child made after startup joins the tree at the next reconcile (52.567993ms)
|
||||
✔ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history (58.351817ms)
|
||||
✔ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses (82.906777ms)
|
||||
✔ a reconcile that fails keeps the previous tree (32.002586ms)
|
||||
✔ a closed task the cursor missed takes its own project's done bucket (30.799356ms)
|
||||
✔ closed tasks read at startup take their own project's done bucket (9.609022ms)
|
||||
✔ a closed task the cursor returns takes its own project's done bucket (35.959777ms)
|
||||
✔ a task moved out of a child that leaves the tree at the same time is moved, not no-access (32.587821ms)
|
||||
✔ a task in a bucket outside the five keeps its raw bucket id (30.460402ms)
|
||||
✔ bucket 0 on a watch-only task never becomes a bucket or a state (79.101692ms)
|
||||
✔ a child whose kanban view is deleted still refuses the tick, and the reconcile reads it watch-only (55.045348ms)
|
||||
✔ a child dropped at a tick comes back at the next reconcile once the bot reads it again (33.554956ms)
|
||||
✔ task.create needs a recorded human request and a requirement id (38.564788ms)
|
||||
✔ task.create takes only an approved requirement id (25.250244ms)
|
||||
✔ task.create cites the request a received message carries, not the message's id (21.4923ms)
|
||||
✔ only labels named in the business file can be written (29.8162ms)
|
||||
✔ task.schedule sets and clears a due date and relations (34.549107ms)
|
||||
✔ assign and reassign move the role bots and record task.assigned (54.772459ms)
|
||||
✔ task.update.assigned is for the assignee and records task.state (46.012602ms)
|
||||
✔ a wrong expected digest records task.conflict and writes nothing (17.100256ms)
|
||||
✔ a cross-role verb needs a resolved decision, used once (21.142491ms)
|
||||
✔ task.close needs a verdict; after it every verb refuses with task-done (21.682078ms)
|
||||
✔ a task with pushed work closes only citing an approve verdict for its latest push, and a refusal writes nothing (28.138571ms)
|
||||
✔ a lost answer is settled by a re-read and never retried (23.538151ms)
|
||||
✔ a create whose answer is lost is reported uncertain, and the poll finds the task (35.773059ms)
|
||||
✔ a task the sync bot cannot read refuses and records nothing (13.695499ms)
|
||||
✔ verbs and polls for one business run one at a time (85.120535ms)
|
||||
✔ a due date with milliseconds is written to the second (63.798117ms)
|
||||
✔ every write landed and the final read failed: the verb succeeds and records what it wrote (38.164569ms)
|
||||
✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (13.051392ms)
|
||||
✔ a create whose final read fails succeeds and records task.created (31.703262ms)
|
||||
✔ an edit between the last write and the final read shows as external on the next poll (34.596425ms)
|
||||
✔ task.created is recorded when a later label write fails (12.342266ms)
|
||||
✔ every verb acts in an installed child, on the child's own board (43.432831ms)
|
||||
✔ task.create refuses a project outside the tree, a watch-only one and one that is not an id (22.367545ms)
|
||||
✔ with descendants off, task.create takes only the root project (11.183974ms)
|
||||
✔ a verb on a watch-only task, or a relation to one, refuses with project-not-installed (15.672377ms)
|
||||
✔ a related task is read before authority: in another project than its ref names, task-moved; missing, task-not-found (21.592443ms)
|
||||
✔ a task moved to another project of the tree refuses its old ref with task-moved and takes its new one (15.07741ms)
|
||||
✔ a verb and a create in the child record the final read of the child's board (14.136623ms)
|
||||
✔ a create in the child whose final read fails records the child's todo bucket (32.151316ms)
|
||||
✔ a lost answer in the child is settled by a re-read of the child's board (19.504887ms)
|
||||
✔ every related task is read before authority, not only the first (14.466905ms)
|
||||
ℹ tests 97
|
||||
ℹ suites 0
|
||||
ℹ pass 97
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1013.144106
|
||||
@@ -0,0 +1,86 @@
|
||||
OK args: exit 2, target-branch
|
||||
OK args: exit 2, target-branch
|
||||
OK args: exit 4
|
||||
OK args: exit 4
|
||||
OK args: exit 4
|
||||
OK --base next refuses before the config is read (got 2)
|
||||
OK a missing system config exits 3 (got 3)
|
||||
OK nobus: exit 3
|
||||
OK schema-table: exit 2, bus-schema
|
||||
OK schema-meta: exit 2, bus-schema
|
||||
OK openBus opens the database read-only
|
||||
OK other-business: exit 2, decision-not-found
|
||||
OK no-raise: exit 2, decision-not-found
|
||||
OK not-a-merge: exit 2, not-a-merge
|
||||
OK open: exit 2, not-resolved
|
||||
OK withdrawn: exit 2, not-resolved
|
||||
OK by-agent: exit 2, not-human
|
||||
OK via-webui: exit 2, not-human
|
||||
OK no-answer: exit 2, not-human
|
||||
OK answer-role: exit 2, not-human
|
||||
OK answer-run: exit 2, not-human
|
||||
OK answer-choice: exit 2, not-human
|
||||
OK two-answers: exit 2, not-human
|
||||
OK chose-no: exit 2, not-merge-option
|
||||
OK two-refs: exit 2, work-ref
|
||||
OK bad-ref: exit 2, work-ref
|
||||
OK task-ref-disagrees: exit 2, work-ref
|
||||
OK raise-task-disagrees: exit 2, work-ref
|
||||
OK target-main: exit 2, target-branch
|
||||
OK superseded-question: exit 2, superseded
|
||||
OK superseded-task: exit 2, superseded
|
||||
OK superseded-by: exit 2, superseded
|
||||
OK no-verdict: exit 2, no-verdict
|
||||
OK changes-later: exit 2, not-approved
|
||||
OK short-commit: exit 2, not-approved
|
||||
OK wrong-task-body: exit 2, not-approved
|
||||
OK args: exit 2, decision-not-found
|
||||
OK earlier-merge: exit 0
|
||||
OK ref-moved: exit 2, ref-moved
|
||||
OK root-top: exit 2, root-file
|
||||
OK root-newdir: exit 2, root-file
|
||||
OK root-src: exit 2, root-file
|
||||
OK unrelated: exit 2, base-conflict
|
||||
OK empty: exit 2, base-conflict
|
||||
OK an empty reviewed diff says so
|
||||
OK dirty-changed: exit 2, checkout-dirty
|
||||
OK dirty-staged: exit 2, checkout-dirty
|
||||
OK dirty-untracked: exit 2, checkout-dirty
|
||||
OK conflict: exit 2, base-conflict
|
||||
OK a refusal after the worktree is added leaves no worktree or scratch
|
||||
OK cleanup looks for the worktree's Docker network
|
||||
OK dirty-during: exit 2, checkout-dirty
|
||||
OK a reviewed path dirtied during the gate moves no ref
|
||||
OK drift: exit 2, patch-drift
|
||||
OK patch-drift leaves no worktree or scratch
|
||||
OK gate-pkg: exit 2, gate-failed
|
||||
OK gate-failed names the failing step's log, and it exists
|
||||
OK gate-failed leaves no worktree or scratch
|
||||
OK gate-script: exit 2, gate-failed
|
||||
OK a failing scripts/test-*.sh fails the gate
|
||||
OK a failed gate moves no ref
|
||||
OK nomods: exit 4
|
||||
OK a target without node_modules leaves no worktree or scratch
|
||||
OK moved: exit 2, base-moved
|
||||
OK base-moved keeps the base where it moved to
|
||||
OK dry: exit 0
|
||||
OK --dry-run writes no ref
|
||||
OK --dry-run prints the commit with --subject and --attribution
|
||||
OK --dry-run runs the gate
|
||||
OK --dry-run leaves no worktree or scratch
|
||||
OK land: exit 0
|
||||
OK the landing is one commit on the old base
|
||||
OK its diff is the reviewed diff by patch-id
|
||||
OK its tree is the dry run's
|
||||
OK the message has the option's subject, the citations and the attribution last
|
||||
OK it prints the line for the PM
|
||||
OK the checked-out base has the reviewed paths and keeps the unrelated edit
|
||||
OK only refs/heads/refactor exists in the target
|
||||
OK the work ref is unchanged
|
||||
OK the bus database is unchanged
|
||||
OK the gate ran each package suite but queue, and each scripts/test-*.sh
|
||||
OK the landing leaves no worktree or scratch
|
||||
OK the target's node_modules is still there, with its contents
|
||||
OK the worktree's Docker network is removed
|
||||
---
|
||||
land-work selftests: 84 passed, 0 failed
|
||||
@@ -0,0 +1,122 @@
|
||||
✔ the Board: held roles with no machine sessions, a claim with no address, the toggle kept across a reload, and Message addressed to the role (5753.877446ms)
|
||||
✔ a redirect answer on a Board read, a bus read, a conversation read, a board POST, a module read and a send signs the page out: one line, nothing stale, no further reads (5742.58308ms)
|
||||
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (3927.610636ms)
|
||||
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
|
||||
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (5400.335632ms)
|
||||
✔ the README says a refused field table names the file or the key at fault, never a path (1.083308ms)
|
||||
✔ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes (7141.527061ms)
|
||||
✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (107.53998ms)
|
||||
✔ decisions route serves the roles and recent lists from the reader view (78.609651ms)
|
||||
✔ credentials route serves the broker credential status from the reader view (55.342642ms)
|
||||
✔ replies route serves the replies to one message from the reader view (119.054716ms)
|
||||
✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (174.686291ms)
|
||||
✔ bus refusals map to statuses and never carry a path or a stack (58.698206ms)
|
||||
✔ humanCall speaks the human transport protocol without blocking the server (2744.058669ms)
|
||||
✔ busReader takes --business, else the live bus host, else refuses (477.305113ms)
|
||||
✔ humanCall kills a transport that runs past its timeout (540.491004ms)
|
||||
✔ serve refuses a --business value that is not a business id (819.823643ms)
|
||||
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (3896.865625ms)
|
||||
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (2706.154191ms)
|
||||
✔ conversation view: a newer session with no readable history keeps the marker (1450.55718ms)
|
||||
✔ conversation view: seats without history say so and offer no reply (764.043476ms)
|
||||
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (3407.26358ms)
|
||||
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (54067.241903ms)
|
||||
✔ messages HTTP read pages and preserves metadata; bad arguments refuse before a read; other reads unchanged (164.212522ms)
|
||||
✔ Console Messages lists late unsolicited messages on polling with sender class time read state and trail; nav count includes beyond the page; reads never deliver (4120.996607ms)
|
||||
✔ Messages nav count polls on Tasks and Board and clears on a Board refusal; messages 403 refuses local pages (3046.303004ms)
|
||||
✔ copy writes the Console paths at the full id, read-only, and leaves an existing copy alone (3329.660499ms)
|
||||
✔ copy refuses a commit off refactor, an unknown one, a worktree and a bad copies directory (2330.730675ms)
|
||||
✔ copy writes nothing when an archived path is missing at the commit (773.352012ms)
|
||||
✔ a copy that loses the rename race leaves no read-only temp directory behind (1024.768167ms)
|
||||
✔ a copy whose rename fails with no target refuses, writes no copy and leaves no temp directory (428.393633ms)
|
||||
✔ a copy that meets a target holding files says exists: and moves nothing into it (464.773986ms)
|
||||
✔ a copy that meets an empty target directory replaces it by rename, not by moving into it (496.755252ms)
|
||||
✔ serve runs from a copy: /healthz 200, the config check, roles and RELEASE from the copy, the queue from --checkout (767.25041ms)
|
||||
✔ serve refuses a --checkout that is not an absolute directory path (246.841746ms)
|
||||
✔ consoleReads runs the queue read under queueRoot and reads RELEASE under root (56.234771ms)
|
||||
✔ preview options are all three or none, and each is checked (10.700416ms)
|
||||
✔ the CLI refuses a partial or invalid preview flag set at start (2846.48774ms)
|
||||
✔ the CLI with all three flags serves the preview mode (265.374577ms)
|
||||
✔ a request for the public name needs the header equal to the user id, else 401 and nothing read (32.673011ms)
|
||||
✔ the header name matches without regard to case, and the value compare is exact (8.986037ms)
|
||||
✔ the public name with the header reads the bus views, and the Host and Origin are checked first (61.45051ms)
|
||||
✔ in preview mode every write answers 403 read-only, from the public name and loopback, and reaches nothing (22.748942ms)
|
||||
✔ loopback requests are unchanged in preview mode, and the bus views work with the board down (19.18759ms)
|
||||
✔ a proxy that rewrites Host to the loopback port gets 403 on its first request, whatever the user (51.04117ms)
|
||||
✔ a repeated Host is 400 before any other check, in preview mode and without it (14.102868ms)
|
||||
✔ without the preview flags the public name is refused and writes still reach the board (12.394702ms)
|
||||
✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (2.102864ms)
|
||||
✔ credential state from the date alone, as the bus states it (0.259309ms)
|
||||
✔ queue: rows, one row, ids and methods, notes; no path or id from a row (1907.345924ms)
|
||||
✔ queue: a refused read fails closed with the store's text, redacted (1130.124905ms)
|
||||
✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (1866.413622ms)
|
||||
✔ business: names, vars on the allowlist, authority, credential metadata only (53.506559ms)
|
||||
✔ business: missing or invalid file refuses with the module's text, redacted (65.86322ms)
|
||||
✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (32.875347ms)
|
||||
✔ settings: release, board, loopback address, notifier binding name only (463.523889ms)
|
||||
✔ task fields: the committed table with no reads configured, GET only; a bad table refuses with no path (37.880978ms)
|
||||
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (3435.133684ms)
|
||||
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (23095.108813ms)
|
||||
✔ the send box: held roles, the draft and caret across a refresh, each receipt, one POST per Send, and the trail link; the board Reply on a 4xx and a 5xx (6010.575111ms)
|
||||
✔ a sent receipt lists its replies as the page refreshes, oldest first and capped, and marks none delivered or read (11824.815916ms)
|
||||
✔ startServer takes send only as a function and only with the preview mode (13.477522ms)
|
||||
✔ the CLI refuses --send without the preview flags, and without a configured bus (1147.830401ms)
|
||||
✔ the CLI with --send and a configured bus sends through the human transport and logs one line (444.327838ms)
|
||||
✔ without --send the route is read-only like every other write, and config says no send (17.162815ms)
|
||||
✔ with --send every other write is still 403 read-only, and nothing reaches the board or bus (16.143961ms)
|
||||
✔ config says send only on the public name with --send on (4.808956ms)
|
||||
✔ a send without the signed-in user is 401 and is neither sent nor logged, loopback included (29.019861ms)
|
||||
✔ each request check refuses before anything is sent, and logs the refusal without the body (31.053771ms)
|
||||
✔ a send passes exactly to, body and in_reply_to, answers the id, and logs it (5.450351ms)
|
||||
✔ a crafted role name cannot forge a log line (5.642906ms)
|
||||
✔ a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried (12.487115ms)
|
||||
✔ through the human transport the request is exactly the business, message.send and the keys (227.914049ms)
|
||||
✔ against a real broker: the message is written once as a human send, and its refusals map (97.7367ms)
|
||||
✔ loopback host and board origin fail closed (15.799602ms)
|
||||
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (227.570497ms)
|
||||
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (89.448351ms)
|
||||
✔ unreachable board reports URL; CLI rejects unsupported options (1106.415653ms)
|
||||
✔ render prints the unit without the preview flags (39.695237ms)
|
||||
✔ render carries every serve flag, the preview set included (34.281289ms)
|
||||
✔ a partial preview set is refused (91.018032ms)
|
||||
✔ a value the unit cannot carry is refused, for every flag (1464.757695ms)
|
||||
✔ the rendered unit names none of the human proof agent markers and sets no agent variable (70.543903ms)
|
||||
✔ install into --dir with --no-reload writes the file whole, then reports unchanged (232.294601ms)
|
||||
✔ install without --no-reload runs daemon-reload and nothing that starts the unit (83.229677ms)
|
||||
✔ uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not (381.842783ms)
|
||||
✔ status prints the unit state and GET /healthz on the installed port (511.630538ms)
|
||||
✔ usage: an unknown verb or flag, and start or enable, exit 4 (252.491444ms)
|
||||
✔ without --send, render is the template with its placeholders filled, byte for byte (117.626988ms)
|
||||
✔ render --send with the preview set ends ExecStart with --send and changes nothing else (180.477801ms)
|
||||
✔ --send without the whole preview set is refused and writes nothing (319.874061ms)
|
||||
✔ install --send into --dir with --no-reload writes the sending unit, and status says whether it sends (260.014637ms)
|
||||
✔ uninstall and status take no --send (46.36219ms)
|
||||
✔ install refuses while the unit is running, in any state but inactive or failed, and changes nothing (277.233134ms)
|
||||
✔ when the state read fails, install and uninstall refuse with one line and change nothing (78.543751ms)
|
||||
✔ install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree (63.685273ms)
|
||||
✔ install refuses unless the bus unit runs from the same copy, naming both paths, and changes nothing (287.539265ms)
|
||||
✔ render --copy runs the copy and reads the queue in the checkout; without --copy the unit is unchanged (26.331577ms)
|
||||
✔ the uid header takes only an Authentik uid; the username header still takes a username (400.995629ms)
|
||||
✔ a preview value cannot start with - or . (33.772603ms)
|
||||
✔ status names the copy and whether refactor has moved past it, and says when the unit runs from no copy (449.702413ms)
|
||||
✔ Settings form shows every scope and held role; gated save, exact payload, retained draft and no automatic retry (2342.183943ms)
|
||||
2026-10-15T16:27:38.990Z settings user="jason" changed
|
||||
✔ REQ-WEB-1 signed-in same-origin Settings write reaches exactly one human operation (36.669548ms)
|
||||
✔ REQ-WEB-1 Settings writes require enabled send, named Host, identity, exact Origin and same-origin Fetch Metadata (15.37443ms)
|
||||
✔ REQ-WEB-1 Settings validates JSON, size, revision and strict writable keys before broker call (11.720669ms)
|
||||
✔ tokens.css and icons.svg are byte copies of docs/design (1.575846ms)
|
||||
✔ JetBrains Mono files match the digests in their sources file, with the OFL text (3.046767ms)
|
||||
✔ shipped strings are unchanged, and the web keeps its write boundary (3.262304ms)
|
||||
✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (6578.759778ms)
|
||||
✔ five states on Board, Inbox, Tasks, Agents and Trail (2325.753254ms)
|
||||
✔ Queue, Business and Settings in their five states, with no move button (4066.537744ms)
|
||||
✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (1040.582961ms)
|
||||
✔ the server log names no config path when the system config refuses (263.976333ms)
|
||||
ℹ tests 112
|
||||
ℹ suites 0
|
||||
ℹ pass 112
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 54514.066701
|
||||
Reference in new issue
Block a user