Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
# Row 123 (#1617), round 2: 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.
|
||||
Round 1 (candidate c8bf62fb, packet commit 2209ac4c1, `../`) is left as committed. This directory is round 2.
|
||||
|
||||
**Base: eaeb5ebf2 plus row 49 r1 candidate 87ef1321a1a08be64a20499889fc24b5549b47d6d33abfd86db95f62fb3cb6d0**
|
||||
(Rocko's packet `agents/rocko/work/queue-49/r1/`, `build.patch` applied uncommitted).
|
||||
Round 1 was built on 0bdd573ff; row 131 (plugins) landed as eaeb5ebf2 since, so I rebased. Both patches apply cleanly.
|
||||
`row123.patch` is a diff of my changes only, against base + row 49 r1 (13 files, +721 −34).
|
||||
It applies with `git apply --cached --check` on that tree, and the result round-trips to the candidate tree
|
||||
(`7a8092d2974b6d8046c8326784cc494b037c5491`; base + row 49 r1 is `73a4929ca7d5bbfe5dbea988acbeb8bfdd33ef79`).
|
||||
Candidate manifest `candidate-manifest.sha256` (13 files): file sha256 `255a960582f1018b8d400e8d193e8e95713a078930277652d59ef4153197d9f6`.
|
||||
Nothing is committed in the worktree, nothing is pushed, and no live store, unit or `~/.mosaic` file was touched.
|
||||
No candidate path changed between eaeb5ebf2 and canonical HEAD be2ea5f7c (those commits touch docs and the queue only).
|
||||
|
||||
## Delta round 1 → round 2 (for Filbert)
|
||||
|
||||
`delta-r1-to-r2.patch` is round 1's `row123.patch` applied to the new base, diffed against round 2: **3 files, +39 −3**.
|
||||
Four candidate files differ from the round 1 manifest by content (these three plus `packages/business/README.md`, which
|
||||
only differs because row 131 changed the neighbouring text in the base).
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `packages/bus/src/broker.mjs` | **F1.** `organization.set` from a role checks the grant (`#checkAuthority`) before the writer and consumes it (`#consumeAuthority`, which logs `action.allowed`) only after the writer returns |
|
||||
| `packages/bus/tests/chief-of-staff.test.mjs` | `setup()` gains a failing-writer switch and a `refused()` reader; one new test (13 tests now, was 12) |
|
||||
| `packages/bus/README.md` | the stray blank line after the `organization.set` row is gone, so the table is one table again |
|
||||
|
||||
### F1: a refused change must not leave `action.allowed`
|
||||
|
||||
Round 1 consumed `organization.change` before calling the writer. The `limits`/`limits.set`/`organization.set` branch runs
|
||||
outside the store transaction that wraps most verbs, so when the writer refused (stale revision, invalid value, writer
|
||||
failure) the consume had already committed: the log showed `action.allowed` followed by `action.refused`. A refused human
|
||||
`limits.set` leaves `action.refused` alone. Now the chief's refused change leaves the same trace.
|
||||
|
||||
If the grant check fails, nothing is consumed and the writer is not reached. If the writer throws, the error is mapped as
|
||||
before (`revision-conflict`, `organization-*`, `invalid-request`, `outcome-unknown`) and the consume line is never reached.
|
||||
Only a committed change logs `action.allowed`.
|
||||
|
||||
Test (`packages/bus/tests/chief-of-staff.test.mjs:195`): **`REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set`**.
|
||||
It makes the stub writer throw a revision conflict, a validation error and a plain failure, and sends each as `chief organization.set`
|
||||
and as `human limits.set`. It asserts the same code for both, `action.allowed` for `organization.change` is empty, and the
|
||||
`action.refused` events are exactly the expected `(actor_role, code)` pairs (a human has `actor_role` null), plus a refusal
|
||||
before the writer (a key outside the write path). Then it lets the writer succeed and asserts exactly one `action.allowed`
|
||||
and no further refusal. Mutant **m22** (consume before the writer, the round 1 behaviour) fails it. Mutants m9, m14 and m17
|
||||
also fail it now.
|
||||
|
||||
Known edge, not handled: the consume is a second transaction, so if it fails after the writer committed (disk error), the change
|
||||
stands and the receipt names the chief, but no `action.allowed` was logged. The receipt in `changes.jsonl` is the evidence in that
|
||||
case. Making both atomic would mean the writer joins the store transaction, which is a different design. I did not do it.
|
||||
|
||||
### Control characters in `reason`: not refused, my call, and the tradeoff
|
||||
|
||||
Filbert's note on control characters in the `role.revoke` reason. I did not add a rule. The bus `text()` helper refuses only NUL
|
||||
for every free-text field (messages, decision text, notes). The event log stores the body as escaped JSON, so a newline or an
|
||||
escape byte in `reason` cannot forge a log line or break a record. A rule for `reason` alone would make one field stricter than
|
||||
all the others for no log-integrity gain. **Tradeoff:** a chief's reason may contain a newline or a terminal escape sequence, and a
|
||||
reader that prints the stored text raw (a terminal tail of `role_claims.reason`, say) shows it raw. Anything rendering free text
|
||||
has to escape it, as it must for message and note text today. If the owner wants a stricter shared rule, it belongs in `text()`
|
||||
for every free-text field, in its own row. No test and no mutant for this.
|
||||
|
||||
### role-end-refused after refusals
|
||||
|
||||
Filbert asked about no `action.allowed` after a `role-end-refused`. That assertion is already there:
|
||||
`chief-of-staff.test.mjs:171` (`allowed('role.end').length === 0` after the self and assistant refusals). Nothing added.
|
||||
|
||||
## What the row does (unchanged from round 1)
|
||||
|
||||
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, line 99). `organization.set` beside `limits`/`limits.set`, check-then-write-then-consume (F1). 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 |
|
||||
| `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) | 13 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"]` (Sage accepted, sage-0317; 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: `'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. If row 49 goes to round 2 and Rocko makes that test independent of the latest version,
|
||||
I rebase and drop my edit. If row 49 lands as is, the edit rides with row 123. Row 49 has no verdicts yet. 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:239`). 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 (`packages/bus/tests/chief-of-staff.test.mjs`):
|
||||
- `REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke` (line 102): 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. Mutants m1 and m16 fail it.
|
||||
- `REQ-ROLE-2: a reason is required, bounded, and never sent with a decision` (line 156): 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 171):
|
||||
`role-end-refused` for both; nothing inserted, no `role.end` allowed event. Mutants m4, m5.
|
||||
- `REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither` (line 116):
|
||||
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 84 (`role_claims.reason` and the `action.allowed` body) |
|
||||
| another role still needs a decision | line 102 (refused), line 136 (a resolved decision still ends the run, reason null) |
|
||||
| chief change validated, written, logged like a human's | line 267, runtime test with the real writer: same receipt shape as `limits.set` |
|
||||
| invalid change refused, and logged as refused only | line 267 (negative, non-numeric and unknown limits, unknown model, unknown instance, empty model table, stale `revision` → `revision-conflict`; file bytes and receipts unchanged), line 239 (outside keys), line 195 (event trace, new) |
|
||||
| `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 376, below |
|
||||
|
||||
## Migration 3c → 3d (unchanged)
|
||||
|
||||
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 376): the 3c database 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. The test 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.
|
||||
|
||||
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 rides in the same window as row 49.
|
||||
|
||||
## Mutants (one per check, each fails a test)
|
||||
|
||||
22 mutants, run one at a time in their own process group with a whole-group kill on timeout, on the round 2 tree.
|
||||
Controls: bus 13/13, business 10/10. **Every mutant exits 1; 22/22 killed.** 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). Runner `mutants.sh`.
|
||||
Started 2026-10-15T16:59:59Z (`mutants-started.txt`). Busy check by exit status before each run.
|
||||
|
||||
| # | 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 136) |
|
||||
| m9 | `organization.set` skips the check | 2 tests (incl. the new one) |
|
||||
| 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, new refusal-trace test |
|
||||
| m15 | vocabulary drops `role.end` | 8 business tests |
|
||||
| m16 | chief file drops `role.end` | 4 bus tests |
|
||||
| m17 | chief file drops `organization.change` | 4 bus tests (incl. the new one) |
|
||||
| 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`) |
|
||||
| **m22** | **consume before the writer (round 1 behaviour)** | **the new refusal-trace test (line 195)** |
|
||||
|
||||
## Targeted gate (decision 136), run in the q123 worktree, finished 2026-10-15T17:01:57Z
|
||||
|
||||
Round 2 adds `plugins` (row 131 is in the base). No flake of #258, #261, #262 or #263 was seen in this gate.
|
||||
|
||||
| Suite | Result |
|
||||
|---|---|
|
||||
| business | 92/92 |
|
||||
| bus | 191/191 |
|
||||
| plugins | 21/21 |
|
||||
| tasks | 97/97 |
|
||||
| cli | 195/195 |
|
||||
| webui | 112/112 |
|
||||
| harness | 84/84 |
|
||||
| seat | 132/132 |
|
||||
| `scripts/test-land-work.sh` | 84 passed, 0 failed |
|
||||
|
||||
Logs in `suites/`, summary `suites-run.txt`; runner `suites.sh`. The gate created no Docker networks (before and after lists
|
||||
identical) and left no process under the worktree. 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 (`tools.test.mjs`). Sage makes a harness tools row. **Ordering rule (Sage, #1617 comment
|
||||
28565): that row lands before the 3d re-pin goes live.**
|
||||
- `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.
|
||||
- A shared rule for control characters in free text, if the owner wants one, goes in `text()` in its own row.
|
||||
- #1627 (arbiters) not started.
|
||||
|
||||
## Verify
|
||||
|
||||
```sh
|
||||
cd <packet dir>
|
||||
sha256sum -c packet-manifest.sha256
|
||||
git apply --check row123.patch # on eaeb5ebf2 + row 49 r1 (agents/rocko/work/queue-49/r1/build.patch)
|
||||
sha256sum -c candidate-manifest.sha256 # from the repo root of the candidate tree (13 files)
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
d7dc974f4fd2817c15bbf051230d8b222265e03008c7a773cbc8a96ac38bf16e packages/bus/README.md
|
||||
e4f1de9e16d2fab5e051fd3810189b28cc76a670cfe573b752d9f53bf9ca39b5 packages/bus/schema.sql
|
||||
b94703a3d77a41ba93d058129ba512b3e4677dccfc0a402ca920658e40cee12b packages/bus/src/broker.mjs
|
||||
0423832d05aa68f9191d7362827bd19310001165b895fb2de40b962fe68f9d56 packages/bus/src/migrations/3c-3d.sql
|
||||
22487c0a9205f1cf93b19a47037a2db8c21443728bc904acf079e34d6e86b82a packages/bus/src/store.mjs
|
||||
08c4ecd2d4740a29fd7d5aee4d8bd0a25d20718922a639e7dc621737ba594207 packages/bus/tests/chief-of-staff.test.mjs
|
||||
c89cb508ca2cdb98e48341e3f5ff0ff2cb646511cfb6a8bc5962bd278c388095 packages/bus/tests/comment-migration.test.mjs
|
||||
7587ac1df813e5148fdb73dc4733ec8064e6a142e4647f0da71be009a73eebdb packages/bus/tests/fixtures/schema-3c.sql
|
||||
681b5a7fe3f8cd54b6421b07559231e91569061addd4318c75af5fd5c27560de 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,102 @@
|
||||
diff --git a/packages/bus/README.md b/packages/bus/README.md
|
||||
index 015b8a7fc..3bfd35a55 100644
|
||||
--- a/packages/bus/README.md
|
||||
+++ b/packages/bus/README.md
|
||||
@@ -140,7 +140,6 @@ an operator inspect content and delivery evidence after an uncertain result.
|
||||
| `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. 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. |
|
||||
diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs
|
||||
index ddff2cc54..ecb18f8b8 100644
|
||||
--- a/packages/bus/src/broker.mjs
|
||||
+++ b/packages/bus/src/broker.mjs
|
||||
@@ -731,8 +731,11 @@ export class Broker {
|
||||
if (request.verb === "limits") keys(args, []);
|
||||
else keys(args, ORGANIZATION_KEYS, ["revision"]);
|
||||
let actor = s.human;
|
||||
+ // This branch runs outside the store transaction below, so the grant is checked before the writer
|
||||
+ // and consumed only once the writer has committed: a refused change logs action.refused alone,
|
||||
+ // as a refused human limits.set does.
|
||||
if (role) {
|
||||
- this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));
|
||||
+ this.#checkAuthority(s, "organization.change");
|
||||
actor = this.#business(s.business).roles[s.role].definition + "." + s.role;
|
||||
}
|
||||
let result;
|
||||
@@ -746,6 +749,7 @@ export class Broker {
|
||||
}
|
||||
throw new BusError("outcome-unknown");
|
||||
}
|
||||
+ if (role) this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));
|
||||
this.#secretCheck(result);
|
||||
return result;
|
||||
}
|
||||
diff --git a/packages/bus/tests/chief-of-staff.test.mjs b/packages/bus/tests/chief-of-staff.test.mjs
|
||||
index 645b41dec..a224c7546 100644
|
||||
--- a/packages/bus/tests/chief-of-staff.test.mjs
|
||||
+++ b/packages/bus/tests/chief-of-staff.test.mjs
|
||||
@@ -49,11 +49,13 @@ function setup(t) {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
const organization = [];
|
||||
+ const writer = { error: null };
|
||||
const b = new Broker({
|
||||
store,
|
||||
businesses: demo(),
|
||||
organization: (...args) => {
|
||||
organization.push(args);
|
||||
+ if (writer.error) throw writer.error;
|
||||
return { revision: 'b'.repeat(64) };
|
||||
},
|
||||
});
|
||||
@@ -74,7 +76,9 @@ function setup(t) {
|
||||
.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 };
|
||||
+ const refused = () =>
|
||||
+ store.all("SELECT actor_role, body FROM events WHERE kind='action.refused' ORDER BY seq").map((e) => ({ ...e, body: JSON.parse(e.body) }));
|
||||
+ return { b, store, caps, call, lastClaim, allowed, refused, organization, writer };
|
||||
}
|
||||
|
||||
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) => {
|
||||
@@ -188,6 +192,35 @@ test('REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches t
|
||||
assert.deepEqual(organization.at(-1), ['demo', 'limits.set', PATCH, 'jason']);
|
||||
});
|
||||
|
||||
+test('REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set', (t) => {
|
||||
+ const { call, allowed, refused, organization, writer } = setup(t);
|
||||
+ // The grant is consumed only after the writer commits, so a refusal leaves no allowed event behind.
|
||||
+ const codes = [];
|
||||
+ for (const [error, code] of [
|
||||
+ [Object.assign(new Error('stale'), { code: 'revision-conflict' }), 'revision-conflict'],
|
||||
+ [Object.assign(new Error('bad'), { name: 'BusinessError' }), 'invalid-request'],
|
||||
+ [new Error('disk'), 'outcome-unknown'],
|
||||
+ ]) {
|
||||
+ writer.error = error;
|
||||
+ assert.throws(() => call('chief', 'organization.set', PATCH), new RegExp(code));
|
||||
+ assert.throws(() => call('human', 'limits.set', PATCH), new RegExp(code));
|
||||
+ codes.push(code, code);
|
||||
+ }
|
||||
+ // A refusal before the writer (a key outside the write path) leaves the same trace.
|
||||
+ assert.throws(() => call('chief', 'organization.set', { ...PATCH, human: 'mallory' }), /invalid-request/);
|
||||
+ assert.equal(organization.length, 6);
|
||||
+ assert.equal(allowed('organization.change').length, 0);
|
||||
+ assert.deepEqual(refused().map((e) => [e.actor_role, e.body.code]), [
|
||||
+ ...codes.map((code, i) => [i % 2 ? null : 'chief', code]), // a human has no role
|
||||
+ ['chief', 'invalid-request'],
|
||||
+ ]);
|
||||
+ // A change that commits leaves exactly one allowed event, and no further refusal.
|
||||
+ writer.error = null;
|
||||
+ call('chief', 'organization.set', PATCH);
|
||||
+ assert.equal(allowed('organization.change').length, 1);
|
||||
+ assert.equal(refused().length, 7);
|
||||
+});
|
||||
+
|
||||
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'])
|
||||
@@ -0,0 +1,102 @@
|
||||
control-bus: exit 0; ℹ tests 13 ℹ pass 13 ℹ fail 0
|
||||
control-business: exit 0; ℹ tests 10 ℹ pass 10 ℹ fail 0
|
||||
m1-role-end-unchecked: exit 1; ℹ tests 13 ℹ pass 10 ℹ 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 13 ℹ pass 12 ℹ 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 13 ℹ pass 12 ℹ 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 13 ℹ pass 12 ℹ 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 13 ℹ pass 12 ℹ 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 13 ℹ pass 9 ℹ 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 13 ℹ pass 12 ℹ 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 13 ℹ pass 12 ℹ 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 13 ℹ pass 11 ℹ fail 2
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set
|
||||
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 13 ℹ pass 12 ℹ 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 13 ℹ pass 12 ℹ 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 13 ℹ pass 11 ℹ 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 13 ℹ pass 12 ℹ 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 13 ℹ pass 11 ℹ fail 2
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set
|
||||
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
|
||||
m22-consume-before-writer: exit 1; ℹ tests 13 ℹ pass 12 ℹ fail 1
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set
|
||||
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 13 ℹ pass 9 ℹ 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 13 ℹ pass 9 ℹ fail 4
|
||||
failed: ✖ failing tests:
|
||||
failed: ✖ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set
|
||||
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 13 ℹ pass 8 ℹ 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 13 ℹ pass 11 ℹ 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:59:59Z
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 123 (#1617) round 2: the chief of staff ends runs and changes the organization file.
|
||||
# Same 21 mutants as r1 plus m22 (F1: the grant consumed before the writer). Mutants m1 to m21 run again against the r2 tree.
|
||||
# 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"'
|
||||
|
||||
# m22 (round 2, F1): the grant is consumed before the writer, as in r1, so a refused change leaves an action.allowed behind
|
||||
edit "$B" 'this.#checkAuthority(s, "organization.change");' 'this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));' &&
|
||||
edit "$B" 'if (role) this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));
|
||||
this.#secretCheck(result);' 'this.#secretCheck(result);' || echo "m22: edit failed"
|
||||
diff -u "$BAK/$B" "$B" > "$OUT/m22-consume-before-writer.patch"
|
||||
run m22-consume-before-writer "$CS"
|
||||
cp "$BAK/$B" "$B"
|
||||
|
||||
# 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,21 @@
|
||||
✔ 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.599872ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.499379ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.433733ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (11.919354ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.555276ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (16.493773ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (15.21153ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (11.455873ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (16.018647ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.874716ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (22.565852ms)
|
||||
✔ 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 (40.258926ms)
|
||||
✔ 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.754775ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 13
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 310.32856
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1,18 @@
|
||||
✔ the role file loads and resolves through loadBusiness (4.771359ms)
|
||||
✔ the shipped file is exactly the approved list (0.180405ms)
|
||||
✔ the resolved record matches the approved list, so a widened file fails (1.215488ms)
|
||||
✔ the chief of staff holds role.launch, message.send, role.end and organization.change; every other action is gated (1.048882ms)
|
||||
✔ the pm does not hold role.launch (1.199589ms)
|
||||
✔ a business grant of a gated action through limits does not widen the role (1.419807ms)
|
||||
✔ a limit that drops role.launch leaves the chief of staff with no launch block (0.823168ms)
|
||||
✔ a launch block whose by is the pm instance is refused (1.226778ms)
|
||||
✔ the contract names no duty to talk to the human and the pm contract no launch (0.266804ms)
|
||||
✔ the instance takes no credential references and no tracker (1.643655ms)
|
||||
ℹ tests 10
|
||||
ℹ suites 0
|
||||
ℹ pass 10
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 68.69798
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1,71 @@
|
||||
✖ 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.720423ms)
|
||||
✖ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (12.119747ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.365672ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (11.930418ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.59432ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.270607ms)
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.805327ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (12.573453ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (15.284614ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (15.818214ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (20.145718ms)
|
||||
✔ 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 (40.723261ms)
|
||||
✔ 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.053588ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 10
|
||||
ℹ fail 3
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 301.447439
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:84: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.720423ms)
|
||||
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:92: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:102:1
|
||||
✖ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (12.119747ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: pm
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:105: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:171:1
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.805327ms)
|
||||
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:182: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/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:00.354977497 -0500
|
||||
@@ -831,7 +831,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,41 @@
|
||||
✔ 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.070205ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.916802ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.266898ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.969737ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.102543ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.325904ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.106349ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.296578ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (14.520224ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.45189ms)
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (12.467441ms)
|
||||
✔ 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 (39.0404ms)
|
||||
✔ 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.936516ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 12
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 289.777494
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:239:1
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (12.467441ms)
|
||||
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:258: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/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:03.496026105 -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,41 @@
|
||||
✔ 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.855272ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (15.370259ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.646869ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.341655ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.878577ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.452931ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.610886ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.135886ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (14.563403ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.344626ms)
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (18.58806ms)
|
||||
✔ 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 (39.045666ms)
|
||||
✔ 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.351692ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 12
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 288.264313
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:239:1
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (18.58806ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:263: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/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:03.834031333 -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;
|
||||
// This branch runs outside the store transaction below, so the grant is checked before the writer
|
||||
// and consumed only once the writer has committed: a refused change logs action.refused alone,
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,77 @@
|
||||
✔ 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.679874ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (17.654085ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.17048ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (11.093444ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (14.906631ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.043148ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (14.43196ms)
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (12.665598ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (15.925824ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (15.015369ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (19.179538ms)
|
||||
✖ 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 (30.487117ms)
|
||||
✔ 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.412439ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 301.115506
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:185:1
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (12.665598ms)
|
||||
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:190: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:267: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 (30.487117ms)
|
||||
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:304: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/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:04.170036529 -0500
|
||||
@@ -736,7 +736,7 @@
|
||||
// as a refused human limits.set does.
|
||||
if (role) {
|
||||
this.#checkAuthority(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,41 @@
|
||||
✔ 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.611418ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.951691ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.133691ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.466369ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.710486ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.859597ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.319571ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.660022ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (14.394258ms)
|
||||
✖ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (13.25928ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (17.273755ms)
|
||||
✔ 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 (39.110325ms)
|
||||
✔ 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.8944ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 12
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 293.031166
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:224:1
|
||||
✖ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (13.25928ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: limits
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:235: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/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:04.520041940 -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,77 @@
|
||||
✔ 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.489636ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (18.096088ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (12.473725ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.868164ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.130288ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (15.093797ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.586422ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.407087ms)
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (12.194274ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.709876ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (18.407814ms)
|
||||
✖ 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 (32.287025ms)
|
||||
✔ 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.835113ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 290.623394
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:195:1
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (12.194274ms)
|
||||
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /invalid-request/. Input:
|
||||
|
||||
'Error: outcome-unknown'
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:205: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: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: Error: outcome-unknown
|
||||
at Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:750:17)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:70:44)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:205:25
|
||||
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:205: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),
|
||||
expected: /invalid-request/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:267: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 (32.287025ms)
|
||||
AssertionError [ERR_ASSERTION]: {"revision":"a8913f94c7189f9d7f3fcea64ca3cdf93a1d4f3bf15c28cacf7aff42cbe6b512","limits":{"total":-1}}
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:321: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:750:17)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:322: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:321: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/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:04.865047274 -0500
|
||||
@@ -744,7 +744,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.178612ms)
|
||||
✔ the shipped file is exactly the approved list (0.539971ms)
|
||||
✖ the resolved record matches the approved list, so a widened file fails (0.879013ms)
|
||||
✖ the chief of staff holds role.launch, message.send, role.end and organization.change; every other action is gated (0.797163ms)
|
||||
✖ the pm does not hold role.launch (0.817087ms)
|
||||
✖ a business grant of a gated action through limits does not widen the role (0.736095ms)
|
||||
✖ a limit that drops role.launch leaves the chief of staff with no launch block (0.700889ms)
|
||||
✖ a launch block whose by is the pm instance is refused (1.625346ms)
|
||||
✔ the contract names no duty to talk to the human and the pm contract no launch (0.241044ms)
|
||||
✖ the instance takes no credential references and no tracker (0.890907ms)
|
||||
ℹ tests 10
|
||||
ℹ suites 0
|
||||
ℹ pass 2
|
||||
ℹ fail 8
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 62.717116
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:49:1
|
||||
✖ the role file loads and resolves through loadBusiness (3.178612ms)
|
||||
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:88:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:150:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:202: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.879013ms)
|
||||
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:88:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:150:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:202: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.797163ms)
|
||||
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:88:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:150:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:202:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:97:1
|
||||
✖ the pm does not hold role.launch (0.817087ms)
|
||||
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:88:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:150:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:202: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.736095ms)
|
||||
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:88:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:150:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:202: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.700889ms)
|
||||
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:88:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:150:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:202: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.625346ms)
|
||||
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 (0.890907ms)
|
||||
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/r2/mutants/.bak.hq5f18/packages/business/src/vocabulary.mjs 2026-10-15 12:00:05.567274227 -0500
|
||||
+++ packages/business/src/vocabulary.mjs 2026-10-15 12:00:05.584058388 -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,99 @@
|
||||
✖ 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.055545ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (17.721434ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.715978ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.583615ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.020796ms)
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.447756ms)
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (11.35312ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.347495ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (14.526949ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.634392ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (17.483568ms)
|
||||
✔ 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 (39.303261ms)
|
||||
✖ 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.408468ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 9
|
||||
ℹ fail 4
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 293.272573
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:84: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.055545ms)
|
||||
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:834:33)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:762: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:756:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:70:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:86:20)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14) {
|
||||
code: 'decision-required'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:156:1
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.447756ms)
|
||||
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:834:33)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:762: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:756:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:70:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:168:20)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14) {
|
||||
code: 'decision-required'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:171:1
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (11.35312ms)
|
||||
AssertionError [ERR_ASSERTION]: chief
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:174: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:834:33)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:762: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:756:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:70:44)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:174: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:376: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.408468ms)
|
||||
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:834:33)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:762: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:756:26)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:393: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/r2/mutants/.bak.hq5f18/roles/chief-of-staff.json 2026-10-15 12:00:05.687013662 -0500
|
||||
+++ roles/chief-of-staff.json 2026-10-15 12:00:05.707060289 -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
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
✔ 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.809455ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.939574ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.946884ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.847147ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.46099ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.036987ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.304999ms)
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.527913ms)
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (11.017493ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.705925ms)
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (10.954894ms)
|
||||
✖ 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 (24.550964ms)
|
||||
✔ 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.819067ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 9
|
||||
ℹ fail 4
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 270.558568
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:185:1
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.527913ms)
|
||||
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 Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:738:31)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:70:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:187:16)
|
||||
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) {
|
||||
code: 'decision-required'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:195:1
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (11.017493ms)
|
||||
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /revision-conflict/. Input:
|
||||
|
||||
'Error: decision-required'
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:205: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: true,
|
||||
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 Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:738:31)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:70:44)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:205:25
|
||||
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:205:12)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25),
|
||||
expected: /revision-conflict/,
|
||||
operator: 'throws',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:239:1
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (10.954894ms)
|
||||
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 Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:738:31)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:70:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:241: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) {
|
||||
code: 'decision-required'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:267: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 (24.550964ms)
|
||||
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 Broker.request (file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:738:31)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:296: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/r2/mutants/.bak.hq5f18/roles/chief-of-staff.json 2026-10-15 12:00:05.687013662 -0500
|
||||
+++ roles/chief-of-staff.json 2026-10-15 12:00:06.051065606 -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": []
|
||||
+1
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,197 @@
|
||||
✖ the role file loads and resolves through loadBusiness (3.324615ms)
|
||||
✖ the shipped file is exactly the approved list (1.2475ms)
|
||||
✖ the resolved record matches the approved list, so a widened file fails (0.947396ms)
|
||||
✖ the chief of staff holds role.launch, message.send, role.end and organization.change; every other action is gated (0.845485ms)
|
||||
✖ the pm does not hold role.launch (0.83896ms)
|
||||
✖ a business grant of a gated action through limits does not widen the role (0.749386ms)
|
||||
✖ a limit that drops role.launch leaves the chief of staff with no launch block (0.745226ms)
|
||||
✖ a launch block whose by is the pm instance is refused (1.141259ms)
|
||||
✔ the contract names no duty to talk to the human and the pm contract no launch (0.549949ms)
|
||||
✖ the instance takes no credential references and no tracker (0.93912ms)
|
||||
ℹ tests 10
|
||||
ℹ suites 0
|
||||
ℹ pass 1
|
||||
ℹ fail 9
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 62.317057
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:49:1
|
||||
✖ the role file loads and resolves through loadBusiness (3.324615ms)
|
||||
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:88:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:150:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:202:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:64:1
|
||||
✖ the shipped file is exactly the approved list (1.2475ms)
|
||||
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.947396ms)
|
||||
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:88:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:150:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:202: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.845485ms)
|
||||
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:88:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:150:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:202:10) {
|
||||
exitCode: 2
|
||||
}
|
||||
|
||||
test at packages/business/tests/chief-of-staff.test.mjs:97:1
|
||||
✖ the pm does not hold role.launch (0.83896ms)
|
||||
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:88:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:150:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:202: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.749386ms)
|
||||
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:88:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:150:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:202: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.745226ms)
|
||||
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:88:103)
|
||||
at validateBusinessDocument (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:150:34)
|
||||
at loadBusiness (file:///mnt/storage/scratch/researcher/q123/packages/business/src/business.mjs:202: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.141259ms)
|
||||
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.93912ms)
|
||||
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/r2/mutants/.bak.hq5f18/roles/chief-of-staff.json 2026-10-15 12:00:05.687013662 -0500
|
||||
+++ roles/chief-of-staff.json 2026-10-15 12:00:06.374070596 -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,99 @@
|
||||
✖ 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.872529ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.875803ms)
|
||||
✖ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.150954ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.517412ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.45374ms)
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.492681ms)
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.100495ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.658562ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (14.923389ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.226542ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (18.390428ms)
|
||||
✔ 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 (47.924215ms)
|
||||
✖ 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.19302ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 8
|
||||
ℹ fail 5
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 305.178306
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:84: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.872529ms)
|
||||
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:767:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:70:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:86: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:116:1
|
||||
✖ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.150954ms)
|
||||
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:121:11)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/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) {
|
||||
code: 'ERR_SQLITE_ERROR',
|
||||
errcode: 1811,
|
||||
errstr: 'constraint failed'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:156:1
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.492681ms)
|
||||
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:767:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:70:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:168: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:171:1
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.100495ms)
|
||||
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:767:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:70:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:180: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:376: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 (43.19302ms)
|
||||
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:767:26)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:393: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/r2/mutants/.bak.hq5f18/packages/bus/schema.sql 2026-10-15 12:00:06.478273870 -0500
|
||||
+++ packages/bus/schema.sql 2026-10-15 12:00:06.495072466 -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/r2/mutants/.bak.hq5f18/packages/bus/src/migrations/3c-3d.sql 2026-10-15 12:00:06.499244177 -0500
|
||||
+++ packages/bus/src/migrations/3c-3d.sql 2026-10-15 12:00:06.516072790 -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/r2/mutants/.bak.hq5f18/packages/bus/src/store.mjs 2026-10-15 12:00:06.519669829 -0500
|
||||
+++ packages/bus/src/store.mjs 2026-10-15 12:00:06.538073130 -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,41 @@
|
||||
✔ 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.006746ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.261494ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.750918ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (11.200223ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.528957ms)
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.937134ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (15.633073ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (11.461747ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (16.34046ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (15.226758ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (18.856332ms)
|
||||
✔ 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.464143ms)
|
||||
✔ 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.962979ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 12
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 307.25561
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:156:1
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.937134ms)
|
||||
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:165: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/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:00.709982993 -0500
|
||||
@@ -829,7 +829,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,69 @@
|
||||
✔ 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.846632ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (17.945369ms)
|
||||
✖ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.03369ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.979554ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.918601ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.882406ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.23966ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.107865ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (14.890599ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (15.223091ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (18.179345ms)
|
||||
✔ 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.356063ms)
|
||||
✖ 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.019779ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 296.810565
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:116:1
|
||||
✖ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.03369ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:126: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:376: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.019779ms)
|
||||
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:400: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:400: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:400: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/r2/mutants/.bak.hq5f18/packages/bus/schema.sql 2026-10-15 12:00:06.478273870 -0500
|
||||
+++ packages/bus/schema.sql 2026-10-15 12:00:06.898078692 -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/r2/mutants/.bak.hq5f18/packages/bus/src/migrations/3c-3d.sql 2026-10-15 12:00:06.499244177 -0500
|
||||
+++ packages/bus/src/migrations/3c-3d.sql 2026-10-15 12:00:06.918079002 -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/r2/mutants/.bak.hq5f18/packages/bus/src/store.mjs 2026-10-15 12:00:06.519669829 -0500
|
||||
+++ packages/bus/src/store.mjs 2026-10-15 12:00:06.941079357 -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.593483ms)
|
||||
ℹ tests 1
|
||||
ℹ suites 0
|
||||
ℹ pass 0
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 51.048326
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:1:1
|
||||
✖ packages/bus/tests/chief-of-staff.test.mjs (45.593483ms)
|
||||
'test failed'
|
||||
@@ -0,0 +1,8 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/r2/mutants/.bak.hq5f18/packages/bus/src/migrations/3c-3d.sql 2026-10-15 12:00:06.499244177 -0500
|
||||
+++ packages/bus/src/migrations/3c-3d.sql 2026-10-15 12:00:07.273411946 -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,44 @@
|
||||
✔ 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.48694ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.954384ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (12.599573ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (11.407078ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.884822ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.037554ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.601103ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (11.0359ms)
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (16.637592ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.549388ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (17.834602ms)
|
||||
✔ 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.711751ms)
|
||||
✔ 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.33232ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 12
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 308.210729
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:195:1
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (16.637592ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
|
||||
3 !== 0
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:212: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: 3,
|
||||
expected: 0,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
--- /mnt/storage/src/mosaic-stack/agents/researcher/work/queue-123/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:05.225052839 -0500
|
||||
@@ -735,7 +735,7 @@
|
||||
// and consumed only once the writer has committed: a refused change logs action.refused alone,
|
||||
// as a refused human limits.set does.
|
||||
if (role) {
|
||||
- this.#checkAuthority(s, "organization.change");
|
||||
+ this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));
|
||||
actor = this.#business(s.business).roles[s.role].definition + "." + s.role;
|
||||
}
|
||||
let result;
|
||||
@@ -749,7 +749,6 @@
|
||||
}
|
||||
throw new BusError("outcome-unknown");
|
||||
}
|
||||
- if (role) this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));
|
||||
this.#secretCheck(result);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,41 @@
|
||||
✔ 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.545658ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (18.35868ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.060723ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (12.353229ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.010306ms)
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (11.668532ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.392685ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.611945ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (13.855071ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (13.505006ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (17.270022ms)
|
||||
✔ 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 (39.808878ms)
|
||||
✔ 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.600272ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 12
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 292.27179
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:156:1
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (11.668532ms)
|
||||
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:165: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/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:01.066988520 -0500
|
||||
@@ -830,7 +830,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,41 @@
|
||||
✔ 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.372761ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (18.720862ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.878283ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.866388ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.540988ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.980104ms)
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (11.001711ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.108914ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (14.376315ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.109381ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (16.854347ms)
|
||||
✔ 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.723177ms)
|
||||
✔ 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.154833ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 12
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 294.338611
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:171:1
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (11.001711ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: chief
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:174: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/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:01.413993891 -0500
|
||||
@@ -833,7 +833,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,41 @@
|
||||
✔ 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.251015ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.329821ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (10.992459ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (11.128452ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.934863ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.079579ms)
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (11.764329ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.395192ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (14.522322ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (13.890308ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (18.300655ms)
|
||||
✔ 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.594065ms)
|
||||
✔ 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 (45.053154ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 12
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 305.305039
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:171:1
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (11.764329ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception: assistant
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:174: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/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:01.758999231 -0500
|
||||
@@ -833,7 +833,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,81 @@
|
||||
✖ 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.313238ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (17.519195ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.205752ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (11.658741ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.353967ms)
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.606021ms)
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.822169ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (11.475779ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (14.133784ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.109682ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (18.56275ms)
|
||||
✔ 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.144352ms)
|
||||
✖ 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.597007ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 9
|
||||
ℹ fail 4
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 309.971891
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:84: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.313238ms)
|
||||
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:767:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:70:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:86: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:156:1
|
||||
✖ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.606021ms)
|
||||
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:767:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:70:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:168: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:171:1
|
||||
✖ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.822169ms)
|
||||
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:767:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:70:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:180: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:376: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 (43.597007ms)
|
||||
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:767:26)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:393: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/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:02.115004740 -0500
|
||||
@@ -840,7 +840,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,44 @@
|
||||
✖ 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.457352ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.658359ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.37142ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.933543ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.808619ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.711565ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.566058ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (9.914221ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (14.641505ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (14.141564ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (17.367796ms)
|
||||
✔ 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.679452ms)
|
||||
✔ 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.451142ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 12
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 289.179007
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:84: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.457352ms)
|
||||
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:94: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/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:02.479010372 -0500
|
||||
@@ -831,7 +831,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,39 @@
|
||||
✔ 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.857787ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (16.373235ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.439287ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.595349ms)
|
||||
✖ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.342167ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (13.508687ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (12.68155ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.169111ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (15.116546ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (15.752065ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (17.65354ms)
|
||||
✔ 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.904404ms)
|
||||
✔ 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.465377ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 12
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 294.069102
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:136:1
|
||||
✖ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (12.342167ms)
|
||||
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:839:33)
|
||||
at file:///mnt/storage/scratch/researcher/q123/packages/bus/src/broker.mjs:762: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:756:26)
|
||||
at call (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:70:44)
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q123/packages/bus/tests/chief-of-staff.test.mjs:150: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/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:02.820015648 -0500
|
||||
@@ -836,7 +836,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,55 @@
|
||||
✔ 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.010382ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (15.730426ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (11.530674ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (10.961111ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (13.132015ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (14.044986ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (13.496126ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (10.113187ms)
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (14.500927ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (13.857639ms)
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (9.550959ms)
|
||||
✔ 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 (37.452543ms)
|
||||
✔ 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.246445ms)
|
||||
ℹ tests 13
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 279.75667
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:195:1
|
||||
✖ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (14.500927ms)
|
||||
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:220: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'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/chief-of-staff.test.mjs:239:1
|
||||
✖ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (9.550959ms)
|
||||
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:243: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/r2/mutants/.bak.hq5f18/packages/bus/src/broker.mjs 2026-10-15 12:00:00.334855540 -0500
|
||||
+++ packages/bus/src/broker.mjs 2026-10-15 12:00:03.163020955 -0500
|
||||
@@ -749,7 +749,7 @@
|
||||
}
|
||||
throw new BusError("outcome-unknown");
|
||||
}
|
||||
- if (role) this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));
|
||||
+ if (role) void 0;
|
||||
this.#secretCheck(result);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,90 @@
|
||||
d3e9a2f6c4ed19b70148c063dc91f55ef8081421a7d09067acde210d7d9deb2e build.md
|
||||
255a960582f1018b8d400e8d193e8e95713a078930277652d59ef4153197d9f6 candidate-manifest.sha256
|
||||
570118925e5818d5dc8c8a113c9cfe4c2ee983f6d4cb670c4ced6ba092179f1d changed-paths.txt
|
||||
3482c5d7e5036c56573ac3675ab596d523c224d6ba578530d494e03c89b8d375 delta-r1-to-r2.patch
|
||||
b2c13b33d0076f9ed0f25bdc2d599b1024eb360a8d413f91b23821d1a78622da mutants-run.txt
|
||||
b0582d394a4b4e7504588dd5451dd36ad698fc84d88759301988c7d3f3cebfea mutants-started.txt
|
||||
38896cdeead792c7ce5b183d1acfde1047b12c6ec28e7ffee31b6e24c6f29bfd mutants.sh
|
||||
7908d57a3c471cb14fe02ab35a3d140ed33ac759145f5319b3e04b4a60036104 mutants/control-bus.out
|
||||
9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa mutants/control-bus.rc
|
||||
185d05d4c5bcf073369e1ddca7bdb7c26a10b710c46ba661cbd613cf97b4fb4a mutants/control-business.out
|
||||
9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa mutants/control-business.rc
|
||||
3074d434c1ef6bfbc11c6f8185fd0af7cffd1f620a6a4d175e849019672b8747 mutants/m1-role-end-unchecked.out
|
||||
c248ce2b3da08046a6548eba12e9b0401deac3425ca4a69559b32d1946b898d8 mutants/m1-role-end-unchecked.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m1-role-end-unchecked.rc
|
||||
4f91f53082e1dcc02ebf31916249424ede331350d85720017e3420a7274e5a5a mutants/m10-key-list-widened.out
|
||||
b7b525afe2465d574988f8c0a53142dfb970c3d000291807b464921221ad6fd4 mutants/m10-key-list-widened.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m10-key-list-widened.rc
|
||||
dcd5ed2c9f0e4c45fa37e23161246a189171857b5f9e2d71f33d63e3f128e8af mutants/m11-revision-optional.out
|
||||
aaf583722b05254b3b5742eefca4660b9add7a2576876516be79633cc421d9fa mutants/m11-revision-optional.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m11-revision-optional.rc
|
||||
149f61a3efb928830f34ee18e134dad0dfa474c2cf6c1d46ec3d2fa2c04b94fc mutants/m12-wrong-actor.out
|
||||
1aeaf1cd1737fa5f99a842322e6fb2ff380cd3c0b3561f659799e586d4c2574e mutants/m12-wrong-actor.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m12-wrong-actor.rc
|
||||
9f0ae698709691a2c93a65137eed4e2210b64c4f038eb0f4e1a3089676d13722 mutants/m13-limits-open-to-agents.out
|
||||
26b85e4f739c5c3359354a474f0eab90ede6b5a4e884070ddaec35fe7066923b mutants/m13-limits-open-to-agents.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m13-limits-open-to-agents.rc
|
||||
06ac2d634a4369b80afa8a7f22dd79afbe55913a51879934c2dcba6f7fd37eef mutants/m14-invalid-change-not-refused.out
|
||||
3d5f5a5a856bf1dd67ed0cff54ebb6807fa0f853716f19c5f7a07ab8c02165ca mutants/m14-invalid-change-not-refused.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m14-invalid-change-not-refused.rc
|
||||
ebbbdf76b1610505f0ab7f2d460d11c375f9736df793baf2f145eab720424d1a mutants/m15-vocabulary-drops-role-end.out
|
||||
478c16107c2c5ee974d6decf570aab72a28ca91b9a6a3fba6e2068ecd3245df5 mutants/m15-vocabulary-drops-role-end.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m15-vocabulary-drops-role-end.rc
|
||||
36d034e158c418b20989927c99c6f1d4dfb3fefa24be23dbdecd0aa0b1f4a0d6 mutants/m16-chief-file-drops-role-end.out
|
||||
a1d2f28e0d168b8b364c109e19415087616deffc5b17261e5c246241ebfd6c9c mutants/m16-chief-file-drops-role-end.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m16-chief-file-drops-role-end.rc
|
||||
3c9cce995931207039ac6037cd35c4ee3e1c06816c54ec0c24331d4a4de5c3a5 mutants/m17-chief-file-drops-organization-change.out
|
||||
73f070152ba971c697cffb0e7af76f83336cdd651fc70147e3d5faf52b83b039 mutants/m17-chief-file-drops-organization-change.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m17-chief-file-drops-organization-change.rc
|
||||
3a8f7a6cc7cad747a08bba81646f4a5faf03ee87f6f96a54f3eb1c7a1de7d78b mutants/m18-chief-file-grants-role-revoke.out
|
||||
74703e61a235efa8246ebe2ecee50c6013cab5c08d97a1bc7aa567619dd72a0d mutants/m18-chief-file-grants-role-revoke.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m18-chief-file-grants-role-revoke.rc
|
||||
5abcd1944abf384406e0c3260a0acb5e16cd661e0177e9a024d45598310071a9 mutants/m19-trigger-decision-only.out
|
||||
90ff61a6818f48c80ee1b7db35e78c48459e9dbd5d97fb19ce5c57d2ecb1a991 mutants/m19-trigger-decision-only.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m19-trigger-decision-only.rc
|
||||
e71e853b533e6d823ce896ced46e6ff82ea150f5bc8a275f2c7d98a3b99df0a6 mutants/m2-reason-and-decision-accepted.out
|
||||
901baee47d76cf1503b3c957965e952be74fe8d361f9a93ba070fdf401ea0fe4 mutants/m2-reason-and-decision-accepted.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m2-reason-and-decision-accepted.rc
|
||||
4a03434538dc32fbf5b3cf58baaea67a1ea70f981ebd0ddfffc292ac14ae6716 mutants/m20-trigger-never-fires.out
|
||||
4e763f46c2caf10d6476226752e2a3351cdf90cdce82dd803d1bbb8a0995ee3c mutants/m20-trigger-never-fires.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m20-trigger-never-fires.rc
|
||||
e58ad64933f51c8284f6370b9dec09afb94e57d0f35bf78e0bd96bc9ee531d55 mutants/m21-step-empty.out
|
||||
f11597d741d34070051dee25af4be3bfbb44bf61b13981519fb033e30b1f76fd mutants/m21-step-empty.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m21-step-empty.rc
|
||||
ae1cfc84b6f4e1985963a3ff6ee0bad97dc8d3dd8dc46a70e39109c830d49a10 mutants/m22-consume-before-writer.out
|
||||
46b4cd76e78b16d719ee083f65dc3926353954a1e2ac4516eb14c5181d242ab9 mutants/m22-consume-before-writer.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m22-consume-before-writer.rc
|
||||
ee62288b1950e6d1702f03fbeee6e818463864db1af315a078707e3294f903d7 mutants/m3-reason-unbounded.out
|
||||
c57a1e05f85770b3ec4060885575b2099ddf81d08628e93c0029176469b7e9b9 mutants/m3-reason-unbounded.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m3-reason-unbounded.rc
|
||||
7a18c404b8491396eef673fc1e3660ecd78a808f7ae888ad86d5f1e450eb23b9 mutants/m4-self-end-allowed.out
|
||||
53044381433cb203572be2df1516fcfe455baca2e6fe7be36f8684ad1248b15d mutants/m4-self-end-allowed.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m4-self-end-allowed.rc
|
||||
52e4408785ff26374d08b8adfcdbc45f66ccc9e6daa78cd453e004e1deb5d217 mutants/m5-assistant-end-allowed.out
|
||||
4213c3cb93935c9382df9d6b44a7b34ace804b6c5cc9f7d86536246088732f3b mutants/m5-assistant-end-allowed.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m5-assistant-end-allowed.rc
|
||||
6530b40173fa3c3fe0cda112f933f2b73071e32042867cc01d252391c909b960 mutants/m6-reason-not-stored.out
|
||||
60e7ea5bc44b9bbd520f36ca4f500e1ec8108dad3c935ee4bd9be0a1bab90464 mutants/m6-reason-not-stored.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m6-reason-not-stored.rc
|
||||
66c3c51416f51a86bfbc34ec250c2c2e23c56af60b6274ca673c7fe047309962 mutants/m7-reason-not-logged.out
|
||||
6cf09fcb05eea93b59d2974a7bea692465675b21381830dd4be7eb628b3705d1 mutants/m7-reason-not-logged.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m7-reason-not-logged.rc
|
||||
3904aabaebc7a0aa4140c21a8be7994dd131ea48d9f971ca5273af326beaf08d mutants/m8-decision-path-wrong-action.out
|
||||
d6ce05fa5436d494a9d6dfcad7d8bf992d3084fcd3f597702819f3b4997e823e mutants/m8-decision-path-wrong-action.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m8-decision-path-wrong-action.rc
|
||||
07eb3ce0b53afc4ffc8f99b3fcd0c68a2941f06c9ad6b0a0677d98a725653c9e mutants/m9-organization-set-unchecked.out
|
||||
3d6122297a4c1182d38f927f6c5b8f71d9e4e987b70f8faf3c6a1f8da21f6ece mutants/m9-organization-set-unchecked.patch
|
||||
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m9-organization-set-unchecked.rc
|
||||
6fe838e6ae0eb70f0311c2440ae6e80d06d20e0802fe68cb8f6d45f6d7b0bb11 row123.patch
|
||||
fd8f661918281cb57b9c37fee962d522e157bd26c22855da96b9719c4cd8e008 suites-run.txt
|
||||
522398109cbae8f9637c2e66e21c523244c761c36eea672ca8fceb2322925f65 suites-started.txt
|
||||
4cc52a46664e56a74832566d9038f03c14df18fe8686953d0e975a3d9249caf7 suites.sh
|
||||
f74b084560f0cbddf6819f6e6a55dff6403c6b3f40007ce27319c148baabedf7 suites/bus.txt
|
||||
a7d8125fe335b028386b33072909e63105b515a001162ec0971fa2452bde642d suites/business.txt
|
||||
47823e3632a54703200370342aa271f84bc14908995e42fcbe3ba33d6f479cdb suites/cli.txt
|
||||
e987484584512f9f395541f532149d23018ec6431b5ea86758854b0d26eca2d1 suites/harness.txt
|
||||
b95fe3a422adc56b3e71234772f3a8596cf00909a4e3314dac628c68f4df72fb suites/plugins.txt
|
||||
d8ba97792c5c84c26e53af2ead995978d3f3a754a9a240db75f612c01dc07d86 suites/seat.txt
|
||||
aa577163f9eda27f06e860d3623df3e0988ab5d7e16ff5e8b796170325474732 suites/tasks.txt
|
||||
8e2254aa3e1bb9b205ab72c68273d447c0328c9f156ccba2a46f3a0476468b58 suites/test-land-work.txt
|
||||
005f92ff93bd1714078066aebc26d0eb0c94d79ec0d9aa4b1eb69bca4e52eafa suites/webui.txt
|
||||
@@ -0,0 +1,997 @@
|
||||
diff --git a/packages/bus/README.md b/packages/bus/README.md
|
||||
index 2973590c5..3bfd35a55 100644
|
||||
--- a/packages/bus/README.md
|
||||
+++ b/packages/bus/README.md
|
||||
@@ -138,7 +138,8 @@ 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 +476,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..ecb18f8b8 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,26 @@ 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;
|
||||
+ // This branch runs outside the store transaction below, so the grant is checked before the writer
|
||||
+ // and consumed only once the writer has committed: a refused change logs action.refused alone,
|
||||
+ // as a refused human limits.set does.
|
||||
+ if (role) {
|
||||
+ this.#checkAuthority(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);
|
||||
@@ -731,6 +749,7 @@ export class Broker {
|
||||
}
|
||||
throw new BusError("outcome-unknown");
|
||||
}
|
||||
+ if (role) this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));
|
||||
this.#secretCheck(result);
|
||||
return result;
|
||||
}
|
||||
@@ -804,13 +823,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 +1397,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 +1410,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..a224c7546
|
||||
--- /dev/null
|
||||
+++ b/packages/bus/tests/chief-of-staff.test.mjs
|
||||
@@ -0,0 +1,401 @@
|
||||
+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 writer = { error: null };
|
||||
+ const b = new Broker({
|
||||
+ store,
|
||||
+ businesses: demo(),
|
||||
+ organization: (...args) => {
|
||||
+ organization.push(args);
|
||||
+ if (writer.error) throw writer.error;
|
||||
+ 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);
|
||||
+ const refused = () =>
|
||||
+ store.all("SELECT actor_role, body FROM events WHERE kind='action.refused' ORDER BY seq").map((e) => ({ ...e, body: JSON.parse(e.body) }));
|
||||
+ return { b, store, caps, call, lastClaim, allowed, refused, organization, writer };
|
||||
+}
|
||||
+
|
||||
+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, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set', (t) => {
|
||||
+ const { call, allowed, refused, organization, writer } = setup(t);
|
||||
+ // The grant is consumed only after the writer commits, so a refusal leaves no allowed event behind.
|
||||
+ const codes = [];
|
||||
+ for (const [error, code] of [
|
||||
+ [Object.assign(new Error('stale'), { code: 'revision-conflict' }), 'revision-conflict'],
|
||||
+ [Object.assign(new Error('bad'), { name: 'BusinessError' }), 'invalid-request'],
|
||||
+ [new Error('disk'), 'outcome-unknown'],
|
||||
+ ]) {
|
||||
+ writer.error = error;
|
||||
+ assert.throws(() => call('chief', 'organization.set', PATCH), new RegExp(code));
|
||||
+ assert.throws(() => call('human', 'limits.set', PATCH), new RegExp(code));
|
||||
+ codes.push(code, code);
|
||||
+ }
|
||||
+ // A refusal before the writer (a key outside the write path) leaves the same trace.
|
||||
+ assert.throws(() => call('chief', 'organization.set', { ...PATCH, human: 'mallory' }), /invalid-request/);
|
||||
+ assert.equal(organization.length, 6);
|
||||
+ assert.equal(allowed('organization.change').length, 0);
|
||||
+ assert.deepEqual(refused().map((e) => [e.actor_role, e.body.code]), [
|
||||
+ ...codes.map((code, i) => [i % 2 ? null : 'chief', code]), // a human has no role
|
||||
+ ['chief', 'invalid-request'],
|
||||
+ ]);
|
||||
+ // A change that commits leaves exactly one allowed event, and no further refusal.
|
||||
+ writer.error = null;
|
||||
+ call('chief', 'organization.set', PATCH);
|
||||
+ assert.equal(allowed('organization.change').length, 1);
|
||||
+ assert.equal(refused().length, 7);
|
||||
+});
|
||||
+
|
||||
+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 d2967cb33..1d3802b87 100644
|
||||
--- a/packages/business/README.md
|
||||
+++ b/packages/business/README.md
|
||||
@@ -123,8 +123,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,10 @@
|
||||
business: rc=0 ℹ tests 92 ℹ pass 92 ℹ fail 0
|
||||
bus: rc=0 ℹ tests 191 ℹ pass 191 ℹ fail 0
|
||||
plugins: rc=0 ℹ tests 21 ℹ pass 21 ℹ 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=0 ℹ tests 84 ℹ pass 84 ℹ fail 0
|
||||
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 @@
|
||||
2026-10-15T17:00:15Z
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 123 (#1617) round 2: the targeted gate (plugins added after row 131) (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 plugins 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,199 @@
|
||||
✔ agents: a held role with nothing on the bus is idle; a role session gets the claim rows unchanged (25.578503ms)
|
||||
✔ agents: working counts what the holder received and has not read; routed alone is not working (23.388837ms)
|
||||
✔ agents: input needed is the role's open decisions routed to the human, gated first, then oldest (42.628907ms)
|
||||
✔ agents: working wins over waiting, and Input needed still shows (18.703786ms)
|
||||
✔ agents: a new holder doesn't inherit what the last one left unread; decisions stay with the role (24.063556ms)
|
||||
✔ agents: state reads only its own business, even for a run name both use (21.979338ms)
|
||||
✔ agents: a message the holder released unread is stranded; the next holder gets nothing and cannot read it (28.477831ms)
|
||||
✔ agents: a message the holder read is not stranded; one the current holder has not read is not listed (25.483032ms)
|
||||
✔ agents: stranded is oldest first and capped at 50 for a role (120.899162ms)
|
||||
✔ agents: stranded is per role and per business (38.979045ms)
|
||||
✔ agents: a message routed to a held role and not yet received is not stranded (17.236218ms)
|
||||
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (26.941985ms)
|
||||
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (31.779783ms)
|
||||
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (33.006443ms)
|
||||
✔ launch events require a human CLI capability; generic emit cannot forge authority events (18.827581ms)
|
||||
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (20.233314ms)
|
||||
✔ a recommendation that is not exactly one option key refuses invalid-recommendation after the options (34.476886ms)
|
||||
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (21.685969ms)
|
||||
✔ task action subjects and linked decision trail are complete and ordered (33.106061ms)
|
||||
✔ launch binding is durable and reconnecting requires the identical trusted record (15.657506ms)
|
||||
✔ business isolation includes inherited object names and cross-business message references (25.454131ms)
|
||||
✔ authority never transfers between action, run, target, unresolved or replaced role holder (38.479205ms)
|
||||
✔ task projection uses schema current view, skipping earlier and equal-start polls (19.494793ms)
|
||||
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (27.594749ms)
|
||||
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (19.332062ms)
|
||||
✔ both arbiters require human resolution when their cross-role route is themselves (37.277406ms)
|
||||
✔ the shipped coder role raises task.reassign as gated to the human; task.scope.change stays cross-role (32.528243ms)
|
||||
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.144851ms)
|
||||
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (31.035017ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (30.851981ms)
|
||||
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (23.688118ms)
|
||||
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (23.873994ms)
|
||||
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (27.81874ms)
|
||||
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (32.058334ms)
|
||||
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (31.93706ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (26.085001ms)
|
||||
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (36.900254ms)
|
||||
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (33.360592ms)
|
||||
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (43.200799ms)
|
||||
✔ 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 (91.619431ms)
|
||||
✔ 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 (81.699257ms)
|
||||
✔ comment cursors isolate businesses, move only forward and remain append-only (18.909736ms)
|
||||
✔ only trusted sync writes validated cursors; events and cursor share rollback (14.796135ms)
|
||||
✔ explicit 3b copy migration preserves evidence and opens as 3d; normal startup never migrates (69.615868ms)
|
||||
✔ migration refuses active writers and drifted legacy schemas, with no partial upgrade (130.940414ms)
|
||||
✔ named migration registry chains 3b through 3d to a fixture-only future version in order (87.142403ms)
|
||||
✔ migration refuses mismatched source metadata without changing schema or evidence (66.042257ms)
|
||||
✔ offline command migrates only the supplied fixture root and reports usage errors (186.163036ms)
|
||||
✔ credentials gives human and reader sessions service, account, role, date and state, and nothing else (20.422342ms)
|
||||
✔ credentials refuses a role session and any argument (18.040028ms)
|
||||
✔ credentials reads the status on each request, and a broker without one lists none (23.874757ms)
|
||||
✔ the broker process answers credentials without a token, path or variable in its output, log or store (93.442491ms)
|
||||
✔ only validated broker references load; returned data and exceptions cannot expose a known token (3.961576ms)
|
||||
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.171697ms)
|
||||
✔ expiry refuses use and env references never become client data (0.80386ms)
|
||||
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.572831ms)
|
||||
✔ opaque tokens shorter than 16 characters refuse before use (0.305569ms)
|
||||
✔ a human resolve sends the raising role one message from the human, and its request is the answer (41.081762ms)
|
||||
✔ a cross-role decision resolved by its arbiter comes from that role and run, with no request (25.946603ms)
|
||||
✔ a human withdraw sends one message with the note (26.57486ms)
|
||||
✔ a human seen before the close leaves the request on the close's answer, not the seen (35.123252ms)
|
||||
✔ the raising run's withdraw, a supersede, a within-role raise and seen send nothing (35.252267ms)
|
||||
✔ refused closes send nothing: invalid-choice, decision-closed, not-resolver (38.277691ms)
|
||||
✔ a raising role nobody holds at the close gets the message on the next claim (33.149687ms)
|
||||
✔ a human DECISION send keeps its own instruction as the request, not a close's answer (34.858391ms)
|
||||
✔ roles lists open decisions routed to a role, and never the human inbox (33.234272ms)
|
||||
✔ recent lists closed decisions of every route, newest close first, with how each closed (33.240242ms)
|
||||
✔ recent gives the closing event as closed when the decision was seen first (34.966124ms)
|
||||
✔ recent stops at RECENT rows (117.194055ms)
|
||||
✔ every row is what trail already gives the same session for that id (55.180271ms)
|
||||
✔ a role session can't list decisions, so it can't read another role's open decisions (49.654688ms)
|
||||
✔ decisions is a read: one business, strict arguments, nothing written (53.840096ms)
|
||||
✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (21.043404ms)
|
||||
✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (15.449843ms)
|
||||
✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (15.339919ms)
|
||||
✔ endLaunch leaves a claim another run took alone (19.407439ms)
|
||||
✔ refuse records action.refused against the caller with the code only (16.107301ms)
|
||||
✔ launches off refuses role.launch with launch-revoked until launches on (26.173029ms)
|
||||
✔ broker process: launch ops authorize role.launch, record refusals and end runs (116.418592ms)
|
||||
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.483255ms)
|
||||
✔ process reader gets own kernel identity without exposing environment values (0.697264ms)
|
||||
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.766713ms)
|
||||
✔ real pid 1 remains inspectable when its environment is protected (0.294323ms)
|
||||
✔ limits binds actor from human proof and rejects role, reader, tester and forged actor (24.651199ms)
|
||||
✔ runtime wires verified human settings to the actual shared file writer and actor receipt (52.820042ms)
|
||||
✔ uncertain organization receipts leave bus startup and reads alone but refuse writes until reconciled (53.335184ms)
|
||||
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (29.700314ms)
|
||||
✔ missing and foreign-business citations refuse and roll back message and grant (22.991627ms)
|
||||
✔ cross-role sends still need a matching resolved decision and consume it once (29.766247ms)
|
||||
✔ messages pages all human messages newest first without writes; read state is history (410.428637ms)
|
||||
✔ messages refuses role callers, invalid paging and cursors outside this human feed (34.171016ms)
|
||||
✔ views messages passes only its paging arguments through (0.287091ms)
|
||||
✔ messages isolates businesses even with a valid foreign cursor (21.096131ms)
|
||||
✔ the assistant sends to the user and to the chief of staff only (38.44306ms)
|
||||
✔ an assistant send to any other role, or to the tester, is refused and logged (40.219226ms)
|
||||
✔ only the user and the chief of staff may send to the assistant; the tester is refused by default (39.896916ms)
|
||||
✔ a sender-side list alone refuses a send to a role that has no list (38.887246ms)
|
||||
✔ a target-side list alone refuses a send from a role that has no list (39.107411ms)
|
||||
✔ the sender list is checked before the target list (33.385093ms)
|
||||
✔ roles without the field behave exactly as before (46.727725ms)
|
||||
✔ a business with no role definitions at all is unlimited (43.62866ms)
|
||||
✔ a malformed messaging field refuses at boot (18.269547ms)
|
||||
✔ the assistant's review.request to a reviewer is refused and logged, even with an approved decision (44.373783ms)
|
||||
✔ a review.verdict to the assistant from a role outside its receiveFrom is refused and logged (43.049145ms)
|
||||
✔ review requests and verdicts between roles without the field are unchanged (30.854225ms)
|
||||
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (92.818921ms)
|
||||
✔ startup token refusal returns safe code without value or partial listening broker (68.43042ms)
|
||||
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (94.915036ms)
|
||||
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (146.969129ms)
|
||||
✔ trusted host registers later launches; socket clients never have a registration verb (81.05628ms)
|
||||
✔ runtime excludes declared project roots even when host supplies no repoRoots (46.849678ms)
|
||||
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (64.618516ms)
|
||||
✔ a reply is listed for human and reader sessions, and its delivery rows are unchanged (40.786256ms)
|
||||
✔ the reply still reaches the human message.receive (mosaic talk) after the Console showed it (25.029921ms)
|
||||
✔ a message with no reply gives an empty list (19.664532ms)
|
||||
✔ replies come oldest first and stop at REPLIES (99.008574ms)
|
||||
✔ an id that names no message in the business refuses message-not-found, and the refusal is recorded (41.580952ms)
|
||||
✔ strict arguments; a role session gets human-required (42.977345ms)
|
||||
✔ views passes replies through as one call with the id (0.297022ms)
|
||||
✔ requirements returns the committed list to a holding role session, and no one else (31.356539ms)
|
||||
✔ the PM's prd.approve with no task, when no id fits, reaches the human's Inbox (19.085787ms)
|
||||
✔ the PM's non-blocking role.revoke with no task reaches the human's Inbox (22.505345ms)
|
||||
✔ a push is recorded as the action.allowed for git.push.working on the task, and only the host records one (34.834638ms)
|
||||
✔ review.request names the latest pushed commit and sends the reviewer a REVIEW-REQUEST (29.629424ms)
|
||||
✔ review.request says which run holds the reviewer role, or null, and a null request waits for a claim (37.821088ms)
|
||||
✔ review.request refuses a commit that isn't the latest push, a non-reviewer and a role without the action (46.988422ms)
|
||||
✔ reviewFetch answers only the addressed reviewer, for the request it cites (29.00738ms)
|
||||
✔ review.verdict answers one request once, to the role that asked (40.895006ms)
|
||||
✔ task.close on a pushed task needs an approve verdict for the latest push (53.036953ms)
|
||||
✔ a refusal the host decided is filed on the task it names, and only a task ref counts (31.778978ms)
|
||||
✔ v3b prototype refusals, views and append-only mutations (107.664693ms)
|
||||
✔ gated approval authorizes once, survives store reopen, and fresh approval works (40.707666ms)
|
||||
✔ another run cannot consume an approval; a failed check leaves it usable (32.321745ms)
|
||||
✔ two scheduled callers have exactly one grant and one consumed refusal (19.893803ms)
|
||||
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (42.349908ms)
|
||||
✔ class drift gated to cross-role refuses before consumption (31.391335ms)
|
||||
✔ class drift cross-role to gated refuses before consumption (35.951582ms)
|
||||
✔ class drift gated to within-role refuses before consumption (33.724708ms)
|
||||
✔ class drift cross-role to within-role refuses before consumption (29.936425ms)
|
||||
✔ class drift within-role to gated refuses before consumption (23.785584ms)
|
||||
✔ class drift within-role to cross-role refuses before consumption (22.216088ms)
|
||||
✔ message.send consumes approval and prevents a later send or authorize (21.580415ms)
|
||||
✔ role.revoke consumes approval and prevents a later revoke or authorize (20.018232ms)
|
||||
✔ a business that never switched launches reads allowed with no time and no event (23.42386ms)
|
||||
✔ launches revoked and then restored read allowed, with the restore time and event id (22.623067ms)
|
||||
✔ a task closed on the tracker leaves the open list, and total counts the open ones (20.25188ms)
|
||||
✔ a tombstone is not open (17.970873ms)
|
||||
✔ the open list stops at the cap and says so; total still counts every open task (21.166223ms)
|
||||
✔ a role and a reader session read state; arguments, and an unbound cap, refuse (19.19386ms)
|
||||
✔ a human session reads the same state, with the same cap (23.337588ms)
|
||||
✔ reading state writes nothing: every table keeps its row count (20.645998ms)
|
||||
✔ creates private WAL store and excludes a second writer until explicit close (18.52899ms)
|
||||
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (21.924079ms)
|
||||
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (21.413137ms)
|
||||
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (72.761332ms)
|
||||
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (15.738643ms)
|
||||
✔ async transactions refuse before invoking their function (12.685621ms)
|
||||
✔ the session default fits under the reply cap and pages to the end with next (195.566652ms)
|
||||
✔ the short form: no description, assignees by role, every listed key (80.800641ms)
|
||||
✔ each filter, alone and together, and total counts every match (154.547115ms)
|
||||
✔ task_ref reads one task in full, as the whole board has it (90.071383ms)
|
||||
✔ no arguments is the read the Console and mosaic make, unchanged (71.152743ms)
|
||||
✔ an unknown argument or a bad value refuses invalid-request (348.620787ms)
|
||||
✔ tasks rows carry priority and requirement for every session that reads tasks (51.383441ms)
|
||||
✔ the read model rebuilds from the ledger: a reopened bus and each trail give the same values (58.283892ms)
|
||||
✔ an empty business and one with snapshots but no task.created read cleanly (13.418383ms)
|
||||
✔ recordTask keeps sync reads and a role write apart (30.128193ms)
|
||||
✔ read_at must be one canonical UTC format, so the projection compares strings safely (13.921857ms)
|
||||
✔ a bad entry refuses the whole record (12.842559ms)
|
||||
✔ taskView reads the projection for one business (15.712668ms)
|
||||
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (27.603972ms)
|
||||
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (273.138079ms)
|
||||
✔ without an adapter the server refuses every task verb (16.29688ms)
|
||||
✔ the runtime refuses an invalid adapter and closes a valid one (16.571155ms)
|
||||
✔ the process loads the S3 adapter from plain-data trackers (117.493475ms)
|
||||
✔ the tester binds only when the business sets bus.tester to exactly true (88.004326ms)
|
||||
✔ a business may not name a role tester, so the tester address is never a role (20.022289ms)
|
||||
✔ every verb outside the tester list refuses tester-refused and is recorded against the tester (34.002079ms)
|
||||
✔ a tester message is from tester, starts [mos test], and writes no human.input or authority (13.368214ms)
|
||||
✔ a role answers a tester only in reply to a tester message, and the answer never reaches the human (19.460499ms)
|
||||
✔ the tester reads the human views a reader reads (20.809476ms)
|
||||
✔ tester proof binds its own CLI, start time and nonce, refuses a managed run and allows an agent (1.2294ms)
|
||||
✔ neither CLI's nonce proves the other: each proof is bound to its own CLI and nonce variable (0.370636ms)
|
||||
✔ testerNonce reads only the tester's variable and gives null on any read error (0.226916ms)
|
||||
✔ an envelope carries exactly one of cap, human and tester; no tester authenticator refuses (17.771392ms)
|
||||
✔ tester-cli.mjs binds through the runtime when the switch is on, and refuses off or under a run marker (664.392514ms)
|
||||
✔ socket capability stamps launch identity; shared views use wire, no SQL client (33.568384ms)
|
||||
✔ two wire claims serialize; a lost reply never automatically retries (23.970038ms)
|
||||
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (14.562818ms)
|
||||
✔ client preserves UTF-8 when a response divides a multibyte character (12.020527ms)
|
||||
✔ committed mutation followed by dropped reply reports unknown and is never retried (15.180547ms)
|
||||
ℹ tests 191
|
||||
ℹ suites 0
|
||||
ℹ pass 191
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1426.522917
|
||||
@@ -0,0 +1,100 @@
|
||||
✔ the role file loads and resolves through loadBusiness (7.789735ms)
|
||||
✔ the shipped file is exactly the approved list (1.010614ms)
|
||||
✔ the assistant's authority is message.send within role and nothing else (2.606629ms)
|
||||
✔ the assistant talks to the user and the chief of staff only, and no other shipped role is limited (0.896788ms)
|
||||
✔ a business grant through limits does not widen the role (2.404013ms)
|
||||
✔ a launch block whose by is the assistant is refused (1.923749ms)
|
||||
✔ the instance takes no credential references and no tracker (3.637378ms)
|
||||
✔ the contract passes work to the chief of staff and directs no one (0.810589ms)
|
||||
✔ config directory and file path follow MOSAIC_CONFIG (1.376543ms)
|
||||
✔ the fixture business validates and comes back frozen (5.528553ms)
|
||||
✔ two instances may share a definition (2.404309ms)
|
||||
✔ top-level refusals (5.466705ms)
|
||||
✔ arbiters and projects (8.475247ms)
|
||||
✔ role instances (5.015159ms)
|
||||
✔ Vikunja bots (8.321229ms)
|
||||
✔ a role without Vikunja takes no tracker block (2.674738ms)
|
||||
✔ credential references match the definition's services (3.834921ms)
|
||||
✔ launch (10.898213ms)
|
||||
✔ loadBusiness: file checks (1.986603ms)
|
||||
✔ loadBusiness: not a regular file (44.663331ms)
|
||||
✔ loading writes nothing (1.612377ms)
|
||||
✔ names that are Object.prototype properties don't count as declared (3.32128ms)
|
||||
✔ the shipped example refuses as written and validates once filled in (1.161375ms)
|
||||
✔ the role file loads and resolves through loadBusiness (8.937896ms)
|
||||
✔ the shipped file is exactly the approved list (0.332306ms)
|
||||
✔ the resolved record matches the approved list, so a widened file fails (2.352654ms)
|
||||
✔ the chief of staff holds role.launch, message.send, role.end and organization.change; every other action is gated (2.003787ms)
|
||||
✔ the pm does not hold role.launch (2.040342ms)
|
||||
✔ a business grant of a gated action through limits does not widen the role (2.730758ms)
|
||||
✔ a limit that drops role.launch leaves the chief of staff with no launch block (1.569037ms)
|
||||
✔ a launch block whose by is the pm instance is refused (2.223948ms)
|
||||
✔ the contract names no duty to talk to the human and the pm contract no launch (0.450326ms)
|
||||
✔ the instance takes no credential references and no tracker (3.312749ms)
|
||||
✔ usage errors exit 4 (298.937403ms)
|
||||
✔ validate: a good business exits 0 and prints instance digests (64.907263ms)
|
||||
✔ validate: project files (302.97894ms)
|
||||
✔ validate: missing files and a broken system config (237.546921ms)
|
||||
✔ validate: credential reference problems exit 2 and name each one (63.854228ms)
|
||||
✔ validate: a token file inside the repository is refused (59.738486ms)
|
||||
✔ validate: role definitions come from MOSAIC_ROLES_DIR (177.174476ms)
|
||||
✔ resolve: prints one instance's record (254.341173ms)
|
||||
✔ resolve: refusals (362.006449ms)
|
||||
✔ parse: exactly one of file or env, plus the service's date (3.271512ms)
|
||||
✔ check: a good file has no problems (1.046037ms)
|
||||
✔ check never opens the file: a write-only token passes (0.438583ms)
|
||||
✔ check: file problems (1.143927ms)
|
||||
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (1.499187ms)
|
||||
✔ check: dates and environment references (0.532974ms)
|
||||
✔ REQ-LIMIT-1 schema: every scope unset or zero, adjustable above four, bad values and unknown models refuse (20.190428ms)
|
||||
✔ REQ-LIMIT-1 counts each exact scope at/over limit, unlimited and old tuple after model reassignment (1.055917ms)
|
||||
✔ REQ-ROLE-3 writer: atomic validated revision, immutable previous bytes and authenticated actor/old/new receipt (26.367974ms)
|
||||
✔ REQ-ROLE-3 writer: pre-rename fault leaves old file, post-rename recovery records commit, conflicting recovery refuses (14.372404ms)
|
||||
✔ REQ-LIMIT-1 / decision145 migration prints only, preserves values, rejects ambiguity, and is idempotent (2.908539ms)
|
||||
✔ REQ-LIMIT-1 shared settings validates the complete proposed selection and logs exact actor (21.862083ms)
|
||||
✔ REQ-LIMIT-1 provider is table data: another provider routes separately and aliases share exact counts (2.027261ms)
|
||||
✔ REQ-ROLE-3 refuses live writer locks, symlink targets and uncertain recovery without changing bytes (11.355316ms)
|
||||
✔ REQ-PLUG-1 schema: plugins is optional, names known kinds and checked module names (14.819485ms)
|
||||
✔ REQ-PLUG-1 REQ-ROLE-3 the setter writes plugins through the organization writer and logs old and new names (37.217264ms)
|
||||
✔ REQ-PLUG-1 REQ-ROLE-3 the writer checks the bytes it writes: a Date in plugins is refused there too (4.737518ms)
|
||||
✔ REQ-PLUG-1 REQ-ROLE-3 a refused setter write throws a code, changes nothing and logs nothing (1.67011ms)
|
||||
✔ path and load (2.958193ms)
|
||||
✔ refusals (1.654248ms)
|
||||
✔ systemVars flattens the validated config (2.514785ms)
|
||||
✔ precedence: system, business, project, project role, agent (5.281174ms)
|
||||
✔ limits narrow the definition and never widen it (2.80785ms)
|
||||
✔ role.launch stays within-role only for the instance the launch block names (8.878825ms)
|
||||
✔ limits.authority without role.launch leaves the launcher with no launch block (4.262562ms)
|
||||
✔ limits.authority narrows cross-role actions too (1.500213ms)
|
||||
✔ classify (1.546821ms)
|
||||
✔ the record carries what the broker and launcher need (1.477034ms)
|
||||
✔ digest: key order doesn't matter, any value change does (7.0311ms)
|
||||
✔ refusals (2.180314ms)
|
||||
✔ the four shipped version 2 roles load (5.020656ms)
|
||||
✔ shipped role scopes match addendum B section 2 and the SR runbook (1.515809ms)
|
||||
✔ shipped authority follows the note's table (1.462214ms)
|
||||
✔ every role file in roles/ loads through the validator, and a v2 contract exists (1.075023ms)
|
||||
✔ version 1 files keep loading with no authority (1.236948ms)
|
||||
✔ the conductor policy isn't a role (0.276326ms)
|
||||
✔ a missing role file is exit 4, a symbolic link too (0.67357ms)
|
||||
✔ version 2 refusals (1.729544ms)
|
||||
✔ authority: closed vocabulary, no gated-only action, no overlap (3.304955ms)
|
||||
✔ messaging: optional, role definitions and human, never the tester (2.272802ms)
|
||||
✔ credentials: Gitea scopes (1.359058ms)
|
||||
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (1.008686ms)
|
||||
✔ credentials: services (0.612416ms)
|
||||
✔ contract: a non-empty regular Markdown file beside the role file (0.81672ms)
|
||||
✔ every key names known layers and a merge rule (1.275784ms)
|
||||
✔ unknown keys and wrong layers refuse (0.948922ms)
|
||||
✔ types (2.897403ms)
|
||||
✔ merge: defaults, then the most specific layer wins (0.434177ms)
|
||||
✔ merge: limits only narrow, and provenance lists each source (0.517546ms)
|
||||
✔ merge doesn't change its inputs (0.629634ms)
|
||||
ℹ tests 92
|
||||
ℹ suites 0
|
||||
ℹ pass 92
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1901.561197
|
||||
@@ -0,0 +1,205 @@
|
||||
✔ every screen and overlay fits 120x36, 80x24 and the narrow layout, row for row (1441.467369ms)
|
||||
✔ width counts terminal cells: wide CJK and emoji 2, U+FE0F after a one-cell base 1, marks 0 (81.538081ms)
|
||||
✔ broker text is shown, never interpreted: no raw control character or bidi override reaches the terminal (157.147271ms)
|
||||
✔ NO_COLOR: no escape codes, except reverse video on the selected row (247.307745ms)
|
||||
✔ 1-9 shows the decide flow's own lines and question; only y resolves (55.018139ms)
|
||||
✔ the app shows the same decide text as mosaic decide (23.560729ms)
|
||||
✔ any key but y or Y at the question answers No; nothing is sent (208.574837ms)
|
||||
✔ below 40x10 the question is hidden, so every key answers it No, y included (Filbert R3) (66.611093ms)
|
||||
✔ under 40 columns a tall terminal shows the too-small screen even where the decide box would fit; y sends nothing (Filbert N10) (273.011414ms)
|
||||
✔ a hidden decide box names the way out, and the status line under it does not promise y (Filbert N12) (37.061379ms)
|
||||
✔ a throw while answering the question goes to crash, sends nothing and leaves no job pending (22.966079ms)
|
||||
✔ type-ahead never answers the question: a paste, keys typed before it is drawn, keys inside askMs (411.35367ms)
|
||||
✔ quitting at the question answers No and sends nothing, armed or not (41.61611ms)
|
||||
✔ a timer tick reads nothing under an overlay, never answers the question, and keeps a resolved status (39.032811ms)
|
||||
✔ a long question is cut inside the question; the action, target, choice and prompt stay (Dewey B1) (55.961178ms)
|
||||
✔ a decide box too big for the terminal says so, and every key answers it No, y included (Dewey B1, round 3) (461.737785ms)
|
||||
✔ at every size the decide box shows each line y depends on, or says it is hidden (8432.399036ms)
|
||||
✔ outcome unknown: the CLI's sentence, no retry, then the trail or a re-read (65.298268ms)
|
||||
✔ refused and failed reads keep the last list, marked stale; a refused decide says refused (30.081428ms)
|
||||
✔ help or a detail box cut to fit keeps its last line, Esc included (Dewey) (14.943089ms)
|
||||
✔ keys: views, moving, trail of a selection, the command line, help and quit (75.726602ms)
|
||||
✔ one option: the status line says Press 1, not 1-1 (17.6928ms)
|
||||
✔ no business: the app says so and :business picks one (8.542377ms)
|
||||
✔ mosaic with no argument: exit 3 in an agent run, exit 4 with the usage off a terminal, the app on one (1.703266ms)
|
||||
✔ scripts/mosaic with no argument reaches the app entry; the seat usage it printed before has not drifted (170.756276ms)
|
||||
✔ strings to keep exactly live once, in the decide flow and the transport; the app does not restate them (0.416464ms)
|
||||
✔ decide with --note still asks; a No sends nothing (9.301045ms)
|
||||
✔ the app's business comes from a live bus host only (0.956849ms)
|
||||
✔ 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.785331ms)
|
||||
✔ draw is pure: same state, same frame (0.737795ms)
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (34.910427ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (24.533356ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (21.418979ms)
|
||||
✔ decide prints a declining choice as declining (29.825758ms)
|
||||
✔ an unknown outcome is reported once and never resent (18.58138ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (19.194614ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (19.682591ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (16.518183ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (18.481159ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (18.829099ms)
|
||||
✔ agents and tasks print through the broker (13.894834ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (3.600305ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (76.774062ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (111.275283ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (48.839222ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (48.132486ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (51.025449ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (76.809324ms)
|
||||
✔ follow prints an unsolicited message after plain talk returned (44.779154ms)
|
||||
✔ talk --follow continues after reply and prints a later unsolicited message (39.932226ms)
|
||||
✔ inbox lists all unread human messages under decisions, does not deliver or read them (364.024937ms)
|
||||
✔ follow refuses agent context and bad arguments (8.695053ms)
|
||||
✔ follow Ctrl-C wakes the poll sleep and removes its handler; transport errors also clean up (10.215659ms)
|
||||
✔ plain talk leaves the process SIGINT handlers unchanged (11.041138ms)
|
||||
✔ empty views say so (1.052182ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.482052ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.333555ms)
|
||||
✔ the review loop: the coder pushes and asks, the reviewer fetches and approves, and the task's trail holds each step (514.333431ms)
|
||||
✔ refusals: no action, non-fast-forward, an unknown commit or request, and a push for another task moves only that task's ref (330.059886ms)
|
||||
✔ the size cap refuses a push before any byte (149.032053ms)
|
||||
✔ a fetch over the cap refuses before any byte reaches the reviewer (314.465261ms)
|
||||
✔ the socket refuses a request with no capability, an unknown one, and a bad request line (163.19369ms)
|
||||
✔ a rigged clone runs nothing: hooks, fsmonitor, a filter, a gitlink and a symlinked .git/config leave no marker (264.185233ms)
|
||||
✔ an identity with no role, or of another business, refuses not-a-role and moves no ref (204.536972ms)
|
||||
✔ a review of a project the business no longer has refuses unknown-project (202.280999ms)
|
||||
✔ past maxActive connections the socket refuses git-busy, and a hung-up connection frees its slot (142.662751ms)
|
||||
✔ close() answers host-closing to a connection still sending its request line (130.588407ms)
|
||||
✔ a bundle that trickles in refuses body-timeout at its deadline and frees its slot (548.24908ms)
|
||||
✔ a reviewer gone before its bundle was sent is filed fetch-aborted, not as the host's fault (446.071201ms)
|
||||
✔ a reviewer that hangs up the moment it has the whole bundle was served, not aborted (685.235911ms)
|
||||
✔ send: a close after every byte left is served; one that fails a queued write is fetch-aborted (41.785803ms)
|
||||
✔ a bundle with no bytes for bodyIdle refuses body-timeout long before its deadline (467.062158ms)
|
||||
✔ send: a close once the loop is past its last chunk is served, and a failed write there is fetch-aborted (24.55142ms)
|
||||
✔ letGo waits for an answer to leave for at most its cap, then destroys the socket (300.757404ms)
|
||||
✔ a record that fails and can't be undone refuses rollback-failed, which the host logs (346.699387ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (959.210648ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (156.656008ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (83.683945ms)
|
||||
✔ two bus starts racing for one data root leave one host running; the other refuses that a host already runs (478.724196ms)
|
||||
✔ a host takes over a host.lock its dead host left, and puts back a live claim that replaced it meanwhile (1.37734ms)
|
||||
✔ 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.739053ms)
|
||||
✔ a live host's lock stays, however old the file is (0.504995ms)
|
||||
✔ a host.lock left by a host killed with SIGKILL doesn't stop the next bus start (771.855752ms)
|
||||
✔ while a host boots, a reader sees no host: the human commands still ask for --business, stop and status find none (216.602659ms)
|
||||
✔ a host started before host.lock existed, holding only host.json, still refuses a second start, which leaves no lock (30.601827ms)
|
||||
✔ a broker fork that throws leaves no host.lock and no host.json (39.573308ms)
|
||||
✔ a broker send that throws leaves no host.lock and no host.json (32.02317ms)
|
||||
✔ a broker that misses its boot wait leaves no host.lock and no host.json (33.474007ms)
|
||||
✔ a notifier fork that throws stops the broker and leaves no host.lock and no host.json (94.141862ms)
|
||||
✔ a notifier send that throws stops the broker and leaves no host.lock and no host.json (101.763979ms)
|
||||
✔ 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 (97.013325ms)
|
||||
✔ 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 (96.92904ms)
|
||||
✔ a host.json that can't be written stops both children and leaves no host.lock (207.218842ms)
|
||||
✔ an error event on the broker before it replies fails the start at once, and leaves no host.lock and no live child (92.454961ms)
|
||||
✔ an error event on the notifier before it replies fails the start at once, and leaves no host.lock and no live child (152.82228ms)
|
||||
✔ an error event on the broker while the notifier starts fails the start once the notifier replies (266.667135ms)
|
||||
✔ 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 (211.917687ms)
|
||||
✔ 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 (195.385678ms)
|
||||
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1089.57928ms)
|
||||
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (189.886761ms)
|
||||
✔ a broker reply with no request waiting breaks the channel and the host exits 1 (93.770546ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (152.821995ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (141.681556ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (86.9077ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (140.458798ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (93.680669ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (168.158495ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (28.306748ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (202.298384ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (277.181991ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (118.516282ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (722.642531ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (54.304474ms)
|
||||
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (62.637745ms)
|
||||
✔ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (12.533478ms)
|
||||
✔ bus-service.sh refuses a default claude without the execute bit, or in a PATH dir with a space (8.066786ms)
|
||||
✔ each launch rereads limits and model selections; lowered limits keep sessions and log exact hit (51.231981ms)
|
||||
✔ bus start --pm: the launcher instance (the chief of staff) runs under its own PID namespace, registered, with a manifest (1306.190286ms)
|
||||
✔ the chief of staff launches a coder through its launch tool; the coder answers; refusals name their code (528.849205ms)
|
||||
✔ each launch makes sure the workspace has a clone of the project: cloned, fast-forwarded, or fetched and logged (3585.17238ms)
|
||||
✔ a project clone that can't be made refuses checkout-failed, from --chief and from a session, with no run and no clone left (129.846817ms)
|
||||
✔ a runner that stops at once ends its launch with the runner's reason (215.48375ms)
|
||||
✔ a Claude Code launch with no session credential refuses before it starts (110.21756ms)
|
||||
✔ a pi launch needs no session credential: pi logs in through its own ENV_ALLOW variables (181.263081ms)
|
||||
✔ a runner killed from the host ends its launch as killed, not failed (251.379059ms)
|
||||
✔ a missing or failing bwrap refuses sandbox-unavailable before anything starts; there is no unconfined fallback (110.783031ms)
|
||||
✔ a real Claude Code coder's shell can't read a stand-in secret or anything else its sandbox doesn't hold (1218.591944ms)
|
||||
✔ 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 (626.099572ms)
|
||||
✔ 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 (3570.651695ms)
|
||||
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (89.807687ms)
|
||||
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (93.737954ms)
|
||||
✔ a launch client that never closes its side doesn't hold the host's close (92.118422ms)
|
||||
✔ a runner that ignores SIGTERM is killed when the host closes (1211.545787ms)
|
||||
✔ claudeVersion asks MOSAIC_CLAUDE_BIN when set, else claude on PATH (4.248334ms)
|
||||
✔ migration needs no bus/config, prints whole converted document, is idempotent and never writes (17.626632ms)
|
||||
✔ limits view/set use the human bus transport and pass only the explicit patch (2.099069ms)
|
||||
✔ post-119 example and Jason fixture migrate with checked inherited models; wrong or absent config refuses (299.777968ms)
|
||||
✔ zoned uses the IANA zone across DST (24.129137ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (34.970589ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (28.529497ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.357527ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (25.953138ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (30.944661ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (27.991172ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (22.535598ms)
|
||||
✔ 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 (133.397194ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (50.102045ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (13.529563ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (10.507222ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.759151ms)
|
||||
✔ no Discord id reaches the journal or the log (11.542125ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (2.481071ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (2.908404ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (1.63057ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.504103ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (2.439423ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.599354ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.371551ms)
|
||||
✔ 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.489656ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.387045ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.45388ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.315004ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (111.708167ms)
|
||||
✔ a human command refused with human-required prints the other-tree line and exits 3 (3.464728ms)
|
||||
✔ a --tester command refused with tester-required prints the other-tree line and exits 3 (1.459768ms)
|
||||
✔ other exit-3 refusals print no other-tree line (2.368433ms)
|
||||
✔ the line names no path (0.18329ms)
|
||||
✔ shared copy: npm ci, pushed reachability, readonly/existing copy, webui delegation (582.156024ms)
|
||||
✔ wrapper preserves Q81 lease and marker, rejects foreign/symlink; check uses effective roots (428.100659ms)
|
||||
✔ bus unit copy/pm pins, install refuses all busy states and failed reads, drop-in refusal (434.091801ms)
|
||||
✔ missing package manifest refuses before npm; npm failure removes the unpublished temp copy (293.322969ms)
|
||||
✔ real pinned install loads bus and Console; same-copy mosaic talk passes proof, other trees refuse (3550.281634ms)
|
||||
✔ bus status reads effective copy/pm/drop-ins without writes; check refuses an overridden ExecStart (498.870008ms)
|
||||
✔ drop-ins in either override field and malformed metadata refuse; uninstall also checks --no-reload (194.745712ms)
|
||||
✔ install never shows a template name on systemd 261; template drop-ins still refuse (163.826816ms)
|
||||
✔ unlisted instance drop-in is found in another unit lookup directory; path discovery errors refuse (163.78909ms)
|
||||
✔ inherited mosaic-.service.d override refuses without changing the unit (195.117471ms)
|
||||
✔ inherited service.d override refuses without changing the unit (189.749972ms)
|
||||
✔ inherited drop-in unreadable directory or file refuses before writing (165.591312ms)
|
||||
✔ talk --tester sends a [mos test] message as tester from inside an agent and prints the role's reply (39.873223ms)
|
||||
✔ talk --tester refuses text without the [mos test] prefix before any call (21.406982ms)
|
||||
✔ the read verbs take --tester inside an agent, and the trail shows the tester as the sender (26.94186ms)
|
||||
✔ decide, stop and launches take no --tester (19.977975ms)
|
||||
✔ the tester transport runs tester-cli.mjs, and the tester's refusals exit 3 (0.441629ms)
|
||||
✔ with no stand-in transport, --tester runs tester-cli.mjs on the data root's socket (0.792132ms)
|
||||
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 (352.89883ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (47.224346ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2236.872648ms)
|
||||
✔ busExit and refuseInsideAgent (0.472514ms)
|
||||
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (65.877724ms)
|
||||
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1026.24375ms)
|
||||
✔ a late reply the Console has listed still reaches the next talk (2033.328162ms)
|
||||
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (13.209854ms)
|
||||
✔ launches off and on go to the broker and change the business's launch state (13.145754ms)
|
||||
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (11.534588ms)
|
||||
ℹ tests 195
|
||||
ℹ suites 0
|
||||
ℹ pass 195
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 13491.102048
|
||||
@@ -0,0 +1,92 @@
|
||||
✔ sessionModel: agent vars win, then the system's execution settings (13.399692ms)
|
||||
✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.584534ms)
|
||||
✔ a chief-of-staff bundle lists launch among the bus tools and the manifest's typed tools (2.296631ms)
|
||||
✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.053174ms)
|
||||
✔ the prompt's built-in tools line is the harness's own names, as the manifest lists them (9.249709ms)
|
||||
✔ the prompt names the project clones in the workspace, and none when there are none (7.272513ms)
|
||||
✔ the prompt explains the review loop to a role with push_working, and reviews to one with fetch_review (7.638611ms)
|
||||
✔ a bundle is written once: an existing file refuses (1.460462ms)
|
||||
✔ a path with a single quote can't go into the hook command (2.389599ms)
|
||||
✔ allow exits 0, a deny exits 2 with the reason on stderr (125.530748ms)
|
||||
✔ a missing or wrong policy, or a bad event, exits 2 (74.968745ms)
|
||||
✔ the bundle's wrapped command: a missing gate or node still blocks (1084.255248ms)
|
||||
✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (706.733004ms)
|
||||
✔ claude: the hook alone blocks a path outside the workspace (457.849325ms)
|
||||
✔ claude: a second turn resumes the first turn's session (679.61127ms)
|
||||
✔ claude adapter: --restricted is always passed (5.13109ms)
|
||||
✔ claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable (16.156421ms)
|
||||
✔ claude adapter: a directory pin exits 2 before it runs; a pin with a space runs (5.548467ms)
|
||||
✔ claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do (785.931942ms)
|
||||
✔ claude: a missing hook or MCP file refuses before claude starts (6.918007ms)
|
||||
✔ pi: policy tools and typed tools pass, anything else is blocked (3.349528ms)
|
||||
✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.737366ms)
|
||||
✔ file tool paths must resolve inside the workspace (1.213461ms)
|
||||
✔ pi's own path normalisation can't be used to step out (1.111981ms)
|
||||
✔ a symlink inside the workspace that points out is outside (0.969621ms)
|
||||
✔ a dangling symlink is refused at any depth, in both harnesses (2.411056ms)
|
||||
✔ read is checked under every spelling pi's read would open, in both harnesses (15.05931ms)
|
||||
✔ other spellings cover directories, dangling links and pi's cwd (1.761293ms)
|
||||
✔ a relative path climbs from the workspace's real path, in both harnesses (4.545519ms)
|
||||
✔ claude path fields per tool (1.123275ms)
|
||||
✔ glob patterns stay inside the workspace (0.76606ms)
|
||||
✔ a path that can't be checked is blocked (0.364174ms)
|
||||
✔ fetch_review's have lists the working refs before origin's, and no more than HAVE_MAX (198.382116ms)
|
||||
✔ fetch_review's have orders the working refs newest first within each clone, one clone after another (133.129597ms)
|
||||
✔ a runner git past its time: SIGTERM reaches its process group, and SIGKILL follows after the grace period (1810.286267ms)
|
||||
✔ once a signalled runner git has closed, its group gets SIGKILL, so a process that ignored SIGTERM dies too (304.639152ms)
|
||||
✔ initialize, ping and tools/list (47.853177ms)
|
||||
✔ tools/call goes through the tool socket; a refusal is an isError result (32.118526ms)
|
||||
✔ replies are compact JSON; over the cap a tool error names the size and the cap (33.501923ms)
|
||||
✔ unknown tools and methods are JSON-RPC errors and never reach the socket (29.955629ms)
|
||||
✔ a missing argument is a usage error (26.151906ms)
|
||||
✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (364.983523ms)
|
||||
✔ pi: a reply over the cap is a tool error naming the size and the cap; list_tasks asks for open tasks (310.922905ms)
|
||||
✔ pi: a write through a dangling symlink is blocked, and nothing appears outside (356.448684ms)
|
||||
✔ pi: a read is refused when pi would open another spelling outside (313.987297ms)
|
||||
✔ pi: a relative path climbs from the real path of a workspace behind a symlink (309.279237ms)
|
||||
✔ pi: a relative path climbs from the real path of a dataRoot behind a symlink (299.482402ms)
|
||||
✔ pi: a missing extension refuses before any model call (6.408932ms)
|
||||
✔ pi: an extension without its configuration fails pi's start (242.65042ms)
|
||||
✔ founderCheck: founder variables, then a needed service without a usable token (1.387414ms)
|
||||
✔ turnRequest names the sender, class, reply and decision (0.231666ms)
|
||||
✔ turnRequest says a tester's message is a test and never Jason's instruction (0.191222ms)
|
||||
✔ turnRequest shows the message's own sent time, a day old as well as new (0.140888ms)
|
||||
✔ stateBlock names the launch state with its time and event, and the open tasks with the cap (0.231872ms)
|
||||
✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (211.037409ms)
|
||||
✔ launches revoked and then restored: the turn's block reads allowed and gives the restore time (168.641165ms)
|
||||
✔ a task closed on the tracker is missing from the next turn's open list (168.874887ms)
|
||||
✔ a message read a day after it was sent shows the time it was sent (88.493867ms)
|
||||
✔ a decision the human resolves becomes a turn citing the decision and the answer (167.958612ms)
|
||||
✔ a failed state read fails the turn: no adapter runs, the sender hears why, the message is read (64.332042ms)
|
||||
✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (48.971564ms)
|
||||
✔ typed tools carry the runner's capability; launch goes to the host's launch socket (272.7423ms)
|
||||
✔ a RESULT gets no automatic reply; failed turns reply with the reason (1197.197203ms)
|
||||
✔ SIGTERM during a turn kills the turn's process group and still exits 0 (92.595515ms)
|
||||
✔ founder credentials stop before the claim (20) (96.308192ms)
|
||||
✔ a refused claim exits 21; an ended run's capability exits 22 (95.162912ms)
|
||||
✔ the launch ending under a running session exits 22 (99.199269ms)
|
||||
✔ a broker that stays unreachable exits 23 after brokerRetries polls (204.764604ms)
|
||||
✔ a broker that is down at the claim exits 23, not 21 (50.652923ms)
|
||||
✔ no capability, or a malformed one, on stdin exits 2 (142.013049ms)
|
||||
✔ a missing or malformed policy exits 2 before the claim (85.249607ms)
|
||||
✔ the PM gets its task verbs and the reads, and no launch (7.157617ms)
|
||||
✔ the chief of staff gets launch and send_message, and no task verbs (2.836501ms)
|
||||
✔ the assistant gets send_message and the reads, no launch (2.833563ms)
|
||||
✔ list_requirements reads the requirement ids, for a task.create holder with a tracker (3.577265ms)
|
||||
✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (1.237907ms)
|
||||
✔ a coder with a tracker gets its own task tools and no task_reassign (1.569331ms)
|
||||
✔ launch only when the business's launch block names the instance as launcher (2.600909ms)
|
||||
✔ an action outside the instance's authority has no tool (1.452998ms)
|
||||
✔ the review loop: push_working and request_review for the coder, fetch_review and review_verdict for the reviewer, none for the PM (4.485505ms)
|
||||
✔ callTool: one JSON line out, the result back, a refusal rejects (11.338377ms)
|
||||
✔ list_tasks takes the broker's filters and pages; get_task takes a task_ref (3.681892ms)
|
||||
✔ callTool sends list_tasks done false unless the session names done; other tools as given (3.583033ms)
|
||||
✔ replyText: compact JSON up to the cap, an error naming the size and the cap over it (1.246407ms)
|
||||
ℹ tests 84
|
||||
ℹ suites 0
|
||||
ℹ pass 84
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 10606.422363
|
||||
@@ -0,0 +1,29 @@
|
||||
✔ REQ-PLUG-1 REQ-LIMIT-4 the registry: usage-source and limit-check at interface version 1, each a list (1.027331ms)
|
||||
✔ REQ-PLUG-1 checkPlugins accepts one key per kind, each a list of names, empty included (0.235072ms)
|
||||
✔ REQ-PLUG-1 checkPlugins refuses bad shapes, unknown kinds, bad names and duplicates (4.09361ms)
|
||||
✔ REQ-PLUG-1 a kind that switches on one module takes one name, not a list (0.157184ms)
|
||||
✔ REQ-PLUG-1 checkPlugins reports through the caller's fail callback (0.140848ms)
|
||||
✔ REQ-PLUG-1 fixture modules of each kind switch on from a test organization file, no core file changed (7.622662ms)
|
||||
✔ REQ-PLUG-1 a name not matching ^[a-z][a-z0-9-]*$ refuses and keeps the old setting (3.500062ms)
|
||||
✔ REQ-PLUG-1 a missing module directory or index.mjs refuses and keeps the old setting (3.36445ms)
|
||||
✔ REQ-PLUG-1 a symlinked module directory or index.mjs refuses and keeps the old setting (2.495043ms)
|
||||
✔ REQ-PLUG-1 a symlinked kind directory refuses and keeps the old setting (2.172335ms)
|
||||
✔ REQ-PLUG-1 a symlinked packages/plugins refuses (run with --preserve-symlinks so the path keeps it) (25.146233ms)
|
||||
✔ REQ-PLUG-1 index.mjs swapped for a symlink after the lstat checks refuses at the realpath check (2.774086ms)
|
||||
✔ REQ-PLUG-1 a path that can't be read refuses as unreadable, is logged and keeps the old setting (2.218967ms)
|
||||
✔ REQ-PLUG-1 a path that becomes unreadable after the lstat checks refuses at either realpath (2.439158ms)
|
||||
✔ REQ-PLUG-1 a module that fails to load refuses and keeps the old setting (3.153953ms)
|
||||
✔ REQ-PLUG-1 a missing export refuses and keeps the old setting (3.317677ms)
|
||||
✔ REQ-PLUG-1 a different interface version refuses and keeps the old setting (2.454003ms)
|
||||
✔ REQ-PLUG-1 a kind refused on its first load has nothing switched on, and the log names kind and name (1.779205ms)
|
||||
✔ REQ-PLUG-1 the setting's shape: unknown kinds refuse alone, a bad whole setting keeps everything (3.050519ms)
|
||||
✔ REQ-PLUG-1 loads run one at a time, so the last requested setting wins (102.642943ms)
|
||||
✔ REQ-PLUG-1 the loader needs a log callback (1.114576ms)
|
||||
ℹ tests 21
|
||||
ℹ suites 0
|
||||
ℹ pass 21
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 223.916143
|
||||
@@ -0,0 +1,145 @@
|
||||
✔ a first launch clones the root at its branch, with the root's path as origin (34.875337ms)
|
||||
✔ the clone is not at its place until git clone has finished (27.741804ms)
|
||||
✔ a later launch fast-forwards a clean clone after a commit to the root, then finds it current (76.322452ms)
|
||||
✔ a clone ahead of the root keeps its commits and counts as current (39.242032ms)
|
||||
▶ a dirty clone and one on another branch are fetched, and nothing but refs/remotes moves
|
||||
✔ a modified file (45.106476ms)
|
||||
✔ an untracked file (45.994302ms)
|
||||
✔ a staged file (49.952007ms)
|
||||
✔ another branch (50.146522ms)
|
||||
✔ a detached HEAD (46.226905ms)
|
||||
✔ a dirty clone and one on another branch are fetched, and nothing but refs/remotes moves (238.621303ms)
|
||||
✔ a diverged clone gets the fetch into refs/remotes and is kept, not-fast-forward (43.414766ms)
|
||||
▶ a clone kept before the branch check, or for a failed status, gets no fetch
|
||||
✔ not-a-clone (27.70706ms)
|
||||
✔ hooks-present (32.667349ms)
|
||||
✔ submodule (34.698309ms)
|
||||
✔ config-changed (31.666155ms)
|
||||
✔ root-unreadable (39.061326ms)
|
||||
✔ root-detached (45.828785ms)
|
||||
✔ status-failed, on the root's branch and clean (51.78043ms)
|
||||
✔ status-failed, dirty (46.495272ms)
|
||||
✔ a clone kept before the branch check, or for a failed status, gets no fetch (311.499633ms)
|
||||
✔ a dirty clone whose fetch fails is kept, fetch-failed, exactly as it was (45.555542ms)
|
||||
✔ every git call the host makes, for a new clone and an existing one, carries maintenance.auto=false and gc.auto=0 (49.594313ms)
|
||||
✔ a clone with loose objects enough for gc --auto gets no maintenance or gc from the fetch and the merge (80.481666ms)
|
||||
▶ a clone with user.name or user.email in its config is kept, and git runs nothing in it
|
||||
✔ user.name (26.005414ms)
|
||||
✔ user.email (26.319705ms)
|
||||
✔ a clone with user.name or user.email in its config is kept, and git runs nothing in it (52.763437ms)
|
||||
✔ a commit git can't read after the fast-forward is null (48.00762ms)
|
||||
✔ a new clone whose commit can't be read refuses the launch and leaves nothing behind (19.411803ms)
|
||||
▶ a clone the session rigged is kept, and git runs nothing and writes nothing outside it
|
||||
✔ core.fsmonitor (34.520589ms)
|
||||
✔ core.hooksPath (34.518041ms)
|
||||
✔ a hook file (33.407408ms)
|
||||
✔ include.path (38.8982ms)
|
||||
✔ includeIf (38.007121ms)
|
||||
✔ a filter driver (34.356934ms)
|
||||
✔ origin moved (36.896178ms)
|
||||
✔ a second origin url (34.13191ms)
|
||||
✔ the origin url twice, both the root (41.588321ms)
|
||||
✔ an ext:: origin (34.870104ms)
|
||||
✔ a branch tracking another remote (35.48466ms)
|
||||
✔ a branch merging a tag (38.941866ms)
|
||||
✔ a fetch line onto local branches (37.049265ms)
|
||||
✔ no origin (38.557693ms)
|
||||
✔ a config file over the size cap (30.827345ms)
|
||||
✔ config a symlink to /dev/zero (30.446881ms)
|
||||
✔ the clone a symlink to the bait (31.123812ms)
|
||||
✔ the clone a file (30.558879ms)
|
||||
✔ the clone a plain directory (31.6311ms)
|
||||
✔ .git a gitdir: file (32.071133ms)
|
||||
✔ .git a symlink (35.219107ms)
|
||||
✔ .git/logs a symlink (31.582809ms)
|
||||
✔ .git/refs/heads a symlink (31.411062ms)
|
||||
✔ .git/objects a symlink (30.599472ms)
|
||||
✔ a reflog hard-linked to a file outside (30.858439ms)
|
||||
✔ HEAD a fifo (31.459736ms)
|
||||
✔ a commondir file (32.186796ms)
|
||||
✔ an alternates file (31.4754ms)
|
||||
✔ a gitlink staged, no .gitmodules (44.891473ms)
|
||||
✔ a gitlink and .gitmodules staged (45.188944ms)
|
||||
✔ a gitlink to a repository with its own gitlink (53.221316ms)
|
||||
✔ a gitlink and .gitmodules committed (48.483679ms)
|
||||
✔ a gitlink and submodule.active in config (45.46646ms)
|
||||
✔ a .git/modules with no gitlink (34.940179ms)
|
||||
✔ a clone the session rigged is kept, and git runs nothing and writes nothing outside it (1229.250678ms)
|
||||
▶ a root that can't be cloned refuses with the project id and leaves nothing behind
|
||||
✔ missing (10.669759ms)
|
||||
✔ not a repository (10.253699ms)
|
||||
✔ a plain directory inside another repository (14.74689ms)
|
||||
✔ unreadable (10.261765ms)
|
||||
✔ on no branch (12.822276ms)
|
||||
✔ missing an object (15.388173ms)
|
||||
✔ a root that can't be cloned refuses with the project id and leaves nothing behind (75.072395ms)
|
||||
✔ a clone whose root is on no branch, or gone, is kept and named (29.039185ms)
|
||||
✔ no credential or forge remote of the root reaches a clone (48.605035ms)
|
||||
✔ every project in the business file gets its own clone at its own branch (37.202988ms)
|
||||
✔ a clone past its time gets SIGTERM and refuses the launch, with nothing left in the workspace (514.276414ms)
|
||||
✔ a clone that ignores SIGTERM gets SIGKILL once the grace period is over (1015.978128ms)
|
||||
✔ a timeout signals git's process group, so a process git started dies with it (514.085016ms)
|
||||
✔ once a signalled git has closed, its group gets SIGKILL, so a process that ignored SIGTERM dies too (515.219777ms)
|
||||
✔ RUNNER_FILES is exactly what the runner and the harness sources it starts import (1.63634ms)
|
||||
✔ sandboxProblem names a missing bwrap and a bwrap that fails (8.641367ms)
|
||||
✔ every bind says why; the bundle and session file stay read-only over the run directory (1.369955ms)
|
||||
✔ the host's git socket is bound only when given, read-only and before the writable binds (1.039055ms)
|
||||
✔ the git identity is the instance's, at <instance>@<business>.mosaic.invalid, and sandboxArgv refuses without it (1.084317ms)
|
||||
✔ a confined session's shell reaches its own workspace and run directory and nothing of the founder's or another session's (1726.142975ms)
|
||||
✔ a bind that exposes a stand-in, or a missing workspace bind, fails the matching check (2670.175895ms)
|
||||
✔ a confined session that runs tester-cli.mjs is refused as tester, and the same shim outside the sandbox binds (206.95412ms)
|
||||
✔ resolveSeat: by name under --repo resolves the repo layout (1.232023ms)
|
||||
✔ resolveSeat: by path resolves the fleet layout (0.333451ms)
|
||||
✔ resolveSeat: refusals for missing dir, missing launch.sh, non-executable launch.sh, invalid name, and unknown layout (0.705028ms)
|
||||
✔ tmuxContext: outside tmux, default socket, custom socket, and exec failure (0.655851ms)
|
||||
✔ makeRegistration produces a record that validates; each shape violation throws SeatError (0.802278ms)
|
||||
✔ writeRegistration/readRegistration: round trip, permissions, absence, and malformed records (1.259719ms)
|
||||
✔ updateTask: changes task, taskSetBy and updatedAt only, and refuses appropriately (1.644774ms)
|
||||
✔ CLI launch: registers, execs the fake launch script, and passes args through (28.172346ms)
|
||||
✔ CLI launch: --harness lands in the record (34.878383ms)
|
||||
✔ CLI launch: the launch script's own exit code passes through (35.815063ms)
|
||||
✔ CLI launch: relaunching a seat rewrites the one registration record (62.985662ms)
|
||||
✔ CLI launch: omitting --task records an empty string, not null (27.95698ms)
|
||||
✔ CLI seat task: updates only the task after a launch, and refuses on an unlaunched seat (85.340796ms)
|
||||
✔ CLI refusals: no args, unknown flag, missing config, already-registered env, and exec failure (149.392727ms)
|
||||
✔ samePath: equal paths, symlinked dirs, distinct dirs, and non-strings (0.379119ms)
|
||||
✔ resolveSetBy: explicit --by wins over the environment; absent or empty environment gives unknown; invalid explicit or environment values refuse with exit 4 (0.596011ms)
|
||||
✔ 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.744001ms)
|
||||
✔ 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.287917ms)
|
||||
✔ 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 (303.007657ms)
|
||||
✔ REQ-LIMIT-1: family substring matching is no longer exported (0.736279ms)
|
||||
✔ sessionEnv passes only the allowlist, the repo's bin on PATH, and the run id (0.696161ms)
|
||||
✔ sessionExit gives a runner's signal death back as the signal; a session credential is one whole set of nonempty variables (0.251991ms)
|
||||
✔ newRun: short ids, 0700 directories, and a refusal when the socket path won't fit (1.458291ms)
|
||||
✔ session file and launch log: 0600, the session file written once (1.171606ms)
|
||||
✔ endReason maps the runner's exit codes; a signal is killed (0.124648ms)
|
||||
✔ Registry mirrors to sessions.json; readSessions marks live entries; bad files refuse (1.216918ms)
|
||||
✔ stopSession refuses an unknown run, reports a stale one, and won't signal a pid that isn't a runner (0.98007ms)
|
||||
✔ a session runs in its sandbox as pid 1 of its namespace, claims, answers, and stops on mosaic stop (244.532619ms)
|
||||
✔ workRef and validBranch take only what a ref name can hold (1.131054ms)
|
||||
✔ a push lands as the task's work ref, a fast-forward lands after it, and main is the root's (120.790467ms)
|
||||
✔ a rewritten branch refuses non-fast-forward and the work ref stays (91.91966ms)
|
||||
✔ a branch with no history in common with main refuses unrelated-history (47.113844ms)
|
||||
✔ a bundle that needs commits the working repository lacks refuses missing-prerequisite (44.132169ms)
|
||||
✔ main is fetched from the root again before each push, so a clone of a newer root pushes (105.090196ms)
|
||||
✔ a root moved back after the clone leaves the push's base unknown: missing-prerequisite (65.677917ms)
|
||||
✔ a commit with a broken object refuses invalid-bundle, and only because fsck is on (72.126398ms)
|
||||
✔ only a v2 bundle with the named branch is read (55.714504ms)
|
||||
✔ a branch named main, or named for another task, moves only this task's work ref (105.571456ms)
|
||||
✔ a record that throws moves the work ref back, or removes a new one (124.808195ms)
|
||||
✔ a work ref that can't go back refuses rollback-failed, naming the ref and the commit (349.918665ms)
|
||||
✔ a root that can't be read refuses root-unreadable (32.774783ms)
|
||||
✔ a git past its time gets SIGTERM, leaves no ref lock, and the next push lands (2118.910365ms)
|
||||
✔ a git that ignores SIGTERM gets SIGKILL once the grace period is over (1028.062689ms)
|
||||
✔ a working repository path that isn't a directory refuses (25.57549ms)
|
||||
✔ review bundles a pushed commit as refs/mosaic/review, leaving out what the reviewer has (97.911924ms)
|
||||
✔ review of a project with no working repository refuses unknown-commit (13.466123ms)
|
||||
✔ the runner's git tools hold the host's limits and branch rule (1.150774ms)
|
||||
ℹ tests 132
|
||||
ℹ suites 0
|
||||
ℹ pass 132
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 5111.544504
|
||||
@@ -0,0 +1,105 @@
|
||||
✔ the boot config is checked before anything starts (31.121752ms)
|
||||
✔ a business with no tracker entry refuses task verbs (15.160232ms)
|
||||
✔ credential.expiring and .expired are recorded once per instance (34.944956ms)
|
||||
✔ a token file that changes on disk records credential.changed (14.915956ms)
|
||||
✔ autostart polls, reconciles and retries a startup the tracker was down for (38.330682ms)
|
||||
✔ a refusal a restart must clear is not retried by the poll (10.856907ms)
|
||||
✔ a poll that fires while two are queued is dropped (12.858225ms)
|
||||
✔ close waits for a running verb and refuses one that has not started (43.438836ms)
|
||||
✔ stopped bus resumes an empty task cursor after a long outage, exactly once (54.98856ms)
|
||||
✔ persisted IDs avoid replay, exclude bots and bound rows to observed new comments (38.375555ms)
|
||||
✔ commit followed by crash before cache advance neither loses nor duplicates a comment (22.311189ms)
|
||||
✔ failed event insert rolls back cursor and a retry still emits the comment once (19.771093ms)
|
||||
✔ initial cursor accepts a comment within the stated one-minute clock skew margin (16.864747ms)
|
||||
✔ the bundled Vikunja is the pinned upstream image the runbook names (1.026315ms)
|
||||
✔ every published port is on 127.0.0.1, and no secret is in the file (0.474044ms)
|
||||
✔ the committed table loads frozen; its verbs are the broker task verbs (2.223615ms)
|
||||
✔ a write names a verb the table has as its own key, not one it inherits (0.576169ms)
|
||||
✔ the schema refuses any other shape (2.607215ms)
|
||||
✔ the Field table in the slice 1 plan names the verbs each field is written by (0.741696ms)
|
||||
✔ each verb refuses, asks for a decision and writes as the table and the role files say (185.630593ms)
|
||||
✔ the list holds the PRD's requirement ids, in its order, at its version (2.093712ms)
|
||||
✔ the committed list loads frozen (0.985866ms)
|
||||
✔ the schema refuses any other shape (3.771499ms)
|
||||
✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (102.057253ms)
|
||||
✔ the project list holds recorded project objects in the recorded list envelope (11.670136ms)
|
||||
✔ the recorded task bodies pass the checks S3 applies to every read (0.514495ms)
|
||||
✔ the client works against the fake over real HTTP with the platform fetch (98.766162ms)
|
||||
✔ a correct install starts, and the first reconcile records tasks that already exist (30.699595ms)
|
||||
✔ verbs refuse while a business is starting and after startup refused it (19.621606ms)
|
||||
✔ startup refuses a token that can do more than its role needs (47.988299ms)
|
||||
✔ startup refuses an unsupported version and flags an untested one (32.044155ms)
|
||||
✔ startup refuses a board that the runbook did not install (39.759946ms)
|
||||
✔ startup refuses a project the sync bot cannot read (8.670932ms)
|
||||
✔ startup refuses a configured label the pm bot cannot see (8.796369ms)
|
||||
✔ startup refuses an expired credential and a missing sync credential (15.173294ms)
|
||||
✔ an unreachable tracker refuses with tracker-unavailable (6.2927ms)
|
||||
✔ an edit in the UI is recorded once, with the fields that changed (84.724161ms)
|
||||
✔ a move between open buckets is seen on the board, though updated does not change (41.612684ms)
|
||||
✔ a person's comment is counted and a bot's is not (69.690991ms)
|
||||
✔ the hourly reconcile catches a comment through comment_count (78.846912ms)
|
||||
✔ a task closed in the UI leaves the open view with its done bucket (114.699406ms)
|
||||
✔ a task that leaves the board is recorded as deleted, moved or out of reach (65.810782ms)
|
||||
✔ a poll that read before a verb wrote does not overwrite the verb (34.127077ms)
|
||||
✔ a tracker fault during a tick is reported and the next tick catches up (36.748651ms)
|
||||
✔ a malformed answer refuses the tick with tracker-shape (15.076083ms)
|
||||
✔ no token value reaches the database, the log or a refusal (46.78952ms)
|
||||
✔ the first look at a task counts only comments inside the window (34.521549ms)
|
||||
✔ with descendants off the adapter reads no project list and no child, as in v0 (51.841049ms)
|
||||
✔ a three-level tree is read per project, and a project outside it is not (24.753682ms)
|
||||
✔ a watch-only child keeps the business ready, and a failed child read refuses as transient (27.600618ms)
|
||||
✔ a root without the installed buckets refuses with descendants on (9.870645ms)
|
||||
✔ the project list refuses without read_all, and skips archived projects and saved filters (24.568675ms)
|
||||
✔ descendants must be true or false (10.699061ms)
|
||||
✔ a move inside the tree is a moved tombstone plus a snapshot under the new ref (61.025297ms)
|
||||
✔ a child made after startup joins the tree at the next reconcile (54.261479ms)
|
||||
✔ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history (59.459914ms)
|
||||
✔ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses (85.068419ms)
|
||||
✔ a reconcile that fails keeps the previous tree (33.707474ms)
|
||||
✔ a closed task the cursor missed takes its own project's done bucket (31.992503ms)
|
||||
✔ closed tasks read at startup take their own project's done bucket (9.978982ms)
|
||||
✔ a closed task the cursor returns takes its own project's done bucket (37.2709ms)
|
||||
✔ a task moved out of a child that leaves the tree at the same time is moved, not no-access (35.31806ms)
|
||||
✔ a task in a bucket outside the five keeps its raw bucket id (30.57272ms)
|
||||
✔ bucket 0 on a watch-only task never becomes a bucket or a state (78.816935ms)
|
||||
✔ a child whose kanban view is deleted still refuses the tick, and the reconcile reads it watch-only (56.388459ms)
|
||||
✔ a child dropped at a tick comes back at the next reconcile once the bot reads it again (35.584486ms)
|
||||
✔ task.create needs a recorded human request and a requirement id (44.384267ms)
|
||||
✔ task.create takes only an approved requirement id (27.379205ms)
|
||||
✔ task.create cites the request a received message carries, not the message's id (21.048112ms)
|
||||
✔ only labels named in the business file can be written (29.68317ms)
|
||||
✔ task.schedule sets and clears a due date and relations (35.101646ms)
|
||||
✔ assign and reassign move the role bots and record task.assigned (54.290108ms)
|
||||
✔ task.update.assigned is for the assignee and records task.state (48.12765ms)
|
||||
✔ a wrong expected digest records task.conflict and writes nothing (18.295048ms)
|
||||
✔ a cross-role verb needs a resolved decision, used once (22.068299ms)
|
||||
✔ task.close needs a verdict; after it every verb refuses with task-done (21.028465ms)
|
||||
✔ a task with pushed work closes only citing an approve verdict for its latest push, and a refusal writes nothing (29.690654ms)
|
||||
✔ a lost answer is settled by a re-read and never retried (23.890832ms)
|
||||
✔ a create whose answer is lost is reported uncertain, and the poll finds the task (36.778463ms)
|
||||
✔ a task the sync bot cannot read refuses and records nothing (15.878304ms)
|
||||
✔ verbs and polls for one business run one at a time (86.397159ms)
|
||||
✔ a due date with milliseconds is written to the second (65.765733ms)
|
||||
✔ every write landed and the final read failed: the verb succeeds and records what it wrote (39.029463ms)
|
||||
✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (13.696109ms)
|
||||
✔ a create whose final read fails succeeds and records task.created (32.079502ms)
|
||||
✔ an edit between the last write and the final read shows as external on the next poll (37.250343ms)
|
||||
✔ task.created is recorded when a later label write fails (12.29171ms)
|
||||
✔ every verb acts in an installed child, on the child's own board (43.725725ms)
|
||||
✔ task.create refuses a project outside the tree, a watch-only one and one that is not an id (25.316542ms)
|
||||
✔ with descendants off, task.create takes only the root project (12.917187ms)
|
||||
✔ a verb on a watch-only task, or a relation to one, refuses with project-not-installed (17.088778ms)
|
||||
✔ a related task is read before authority: in another project than its ref names, task-moved; missing, task-not-found (22.925977ms)
|
||||
✔ a task moved to another project of the tree refuses its old ref with task-moved and takes its new one (14.450606ms)
|
||||
✔ a verb and a create in the child record the final read of the child's board (13.687874ms)
|
||||
✔ a create in the child whose final read fails records the child's todo bucket (33.437577ms)
|
||||
✔ a lost answer in the child is settled by a re-read of the child's board (21.127354ms)
|
||||
✔ every related task is read before authority, not only the first (15.19141ms)
|
||||
ℹ tests 97
|
||||
ℹ suites 0
|
||||
ℹ pass 97
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1051.072883
|
||||
@@ -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 (5264.904615ms)
|
||||
✔ 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 (6849.113241ms)
|
||||
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (3394.947468ms)
|
||||
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
|
||||
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (4239.0484ms)
|
||||
✔ the README says a refused field table names the file or the key at fault, never a path (1.211982ms)
|
||||
✔ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes (6003.239026ms)
|
||||
✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (132.500516ms)
|
||||
✔ decisions route serves the roles and recent lists from the reader view (252.746133ms)
|
||||
✔ credentials route serves the broker credential status from the reader view (275.277045ms)
|
||||
✔ replies route serves the replies to one message from the reader view (861.609933ms)
|
||||
✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (279.465846ms)
|
||||
✔ bus refusals map to statuses and never carry a path or a stack (118.017692ms)
|
||||
✔ humanCall speaks the human transport protocol without blocking the server (2555.035688ms)
|
||||
✔ busReader takes --business, else the live bus host, else refuses (513.203873ms)
|
||||
✔ humanCall kills a transport that runs past its timeout (566.878299ms)
|
||||
✔ serve refuses a --business value that is not a business id (988.274243ms)
|
||||
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (3445.429948ms)
|
||||
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (2524.339588ms)
|
||||
✔ conversation view: a newer session with no readable history keeps the marker (2221.82666ms)
|
||||
✔ conversation view: seats without history say so and offer no reply (1221.624116ms)
|
||||
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (3011.06364ms)
|
||||
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (54576.693789ms)
|
||||
✔ messages HTTP read pages and preserves metadata; bad arguments refuse before a read; other reads unchanged (172.675499ms)
|
||||
✔ 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 (4537.16862ms)
|
||||
✔ Messages nav count polls on Tasks and Board and clears on a Board refusal; messages 403 refuses local pages (3557.092683ms)
|
||||
✔ copy writes the Console paths at the full id, read-only, and leaves an existing copy alone (4189.993712ms)
|
||||
✔ copy refuses a commit off refactor, an unknown one, a worktree and a bad copies directory (2532.347539ms)
|
||||
✔ copy writes nothing when an archived path is missing at the commit (827.334825ms)
|
||||
✔ a copy that loses the rename race leaves no read-only temp directory behind (605.686286ms)
|
||||
✔ a copy whose rename fails with no target refuses, writes no copy and leaves no temp directory (1019.382073ms)
|
||||
✔ a copy that meets a target holding files says exists: and moves nothing into it (573.649196ms)
|
||||
✔ a copy that meets an empty target directory replaces it by rename, not by moving into it (501.401979ms)
|
||||
✔ serve runs from a copy: /healthz 200, the config check, roles and RELEASE from the copy, the queue from --checkout (770.228875ms)
|
||||
✔ serve refuses a --checkout that is not an absolute directory path (221.912175ms)
|
||||
✔ consoleReads runs the queue read under queueRoot and reads RELEASE under root (51.612716ms)
|
||||
✔ preview options are all three or none, and each is checked (11.234171ms)
|
||||
✔ the CLI refuses a partial or invalid preview flag set at start (3021.734431ms)
|
||||
✔ the CLI with all three flags serves the preview mode (474.888324ms)
|
||||
✔ a request for the public name needs the header equal to the user id, else 401 and nothing read (57.757028ms)
|
||||
✔ the header name matches without regard to case, and the value compare is exact (10.566872ms)
|
||||
✔ the public name with the header reads the bus views, and the Host and Origin are checked first (142.521147ms)
|
||||
✔ in preview mode every write answers 403 read-only, from the public name and loopback, and reaches nothing (27.050603ms)
|
||||
✔ loopback requests are unchanged in preview mode, and the bus views work with the board down (10.908607ms)
|
||||
✔ a proxy that rewrites Host to the loopback port gets 403 on its first request, whatever the user (61.716427ms)
|
||||
✔ a repeated Host is 400 before any other check, in preview mode and without it (20.159937ms)
|
||||
✔ without the preview flags the public name is refused and writes still reach the board (11.240967ms)
|
||||
✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (2.124145ms)
|
||||
✔ credential state from the date alone, as the bus states it (0.278232ms)
|
||||
✔ queue: rows, one row, ids and methods, notes; no path or id from a row (2863.869902ms)
|
||||
✔ queue: a refused read fails closed with the store's text, redacted (855.965177ms)
|
||||
✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (2023.482178ms)
|
||||
✔ business: names, vars on the allowlist, authority, credential metadata only (80.339588ms)
|
||||
✔ business: missing or invalid file refuses with the module's text, redacted (89.877986ms)
|
||||
✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (41.689242ms)
|
||||
✔ settings: release, board, loopback address, notifier binding name only (721.778158ms)
|
||||
✔ task fields: the committed table with no reads configured, GET only; a bad table refuses with no path (55.292394ms)
|
||||
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (2985.585747ms)
|
||||
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (22755.620617ms)
|
||||
✔ 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 (5096.920643ms)
|
||||
✔ a sent receipt lists its replies as the page refreshes, oldest first and capped, and marks none delivered or read (12812.974139ms)
|
||||
✔ startServer takes send only as a function and only with the preview mode (14.908626ms)
|
||||
✔ the CLI refuses --send without the preview flags, and without a configured bus (1210.78239ms)
|
||||
✔ the CLI with --send and a configured bus sends through the human transport and logs one line (339.133654ms)
|
||||
✔ without --send the route is read-only like every other write, and config says no send (25.854396ms)
|
||||
✔ with --send every other write is still 403 read-only, and nothing reaches the board or bus (33.808182ms)
|
||||
✔ config says send only on the public name with --send on (7.510429ms)
|
||||
✔ a send without the signed-in user is 401 and is neither sent nor logged, loopback included (32.437576ms)
|
||||
✔ each request check refuses before anything is sent, and logs the refusal without the body (53.318748ms)
|
||||
✔ a send passes exactly to, body and in_reply_to, answers the id, and logs it (21.659186ms)
|
||||
✔ a crafted role name cannot forge a log line (6.432655ms)
|
||||
✔ a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried (23.901346ms)
|
||||
✔ through the human transport the request is exactly the business, message.send and the keys (307.501702ms)
|
||||
✔ against a real broker: the message is written once as a human send, and its refusals map (153.785088ms)
|
||||
✔ loopback host and board origin fail closed (25.222855ms)
|
||||
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (196.567313ms)
|
||||
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (151.70734ms)
|
||||
✔ unreachable board reports URL; CLI rejects unsupported options (959.88192ms)
|
||||
✔ render prints the unit without the preview flags (52.452634ms)
|
||||
✔ render carries every serve flag, the preview set included (79.492078ms)
|
||||
✔ a partial preview set is refused (113.346915ms)
|
||||
✔ a value the unit cannot carry is refused, for every flag (1322.95227ms)
|
||||
✔ the rendered unit names none of the human proof agent markers and sets no agent variable (67.624502ms)
|
||||
✔ install into --dir with --no-reload writes the file whole, then reports unchanged (368.686618ms)
|
||||
✔ install without --no-reload runs daemon-reload and nothing that starts the unit (167.691565ms)
|
||||
✔ uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not (1127.068415ms)
|
||||
✔ status prints the unit state and GET /healthz on the installed port (654.328807ms)
|
||||
✔ usage: an unknown verb or flag, and start or enable, exit 4 (95.794372ms)
|
||||
✔ without --send, render is the template with its placeholders filled, byte for byte (50.297628ms)
|
||||
✔ render --send with the preview set ends ExecStart with --send and changes nothing else (64.213703ms)
|
||||
✔ --send without the whole preview set is refused and writes nothing (196.748384ms)
|
||||
✔ install --send into --dir with --no-reload writes the sending unit, and status says whether it sends (473.059788ms)
|
||||
✔ uninstall and status take no --send (84.682541ms)
|
||||
✔ install refuses while the unit is running, in any state but inactive or failed, and changes nothing (903.985629ms)
|
||||
✔ when the state read fails, install and uninstall refuse with one line and change nothing (106.680834ms)
|
||||
✔ install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree (71.449652ms)
|
||||
✔ install refuses unless the bus unit runs from the same copy, naming both paths, and changes nothing (275.612378ms)
|
||||
✔ render --copy runs the copy and reads the queue in the checkout; without --copy the unit is unchanged (19.256655ms)
|
||||
✔ the uid header takes only an Authentik uid; the username header still takes a username (385.001654ms)
|
||||
✔ a preview value cannot start with - or . (29.311419ms)
|
||||
✔ status names the copy and whether refactor has moved past it, and says when the unit runs from no copy (418.755982ms)
|
||||
✔ Settings form shows every scope and held role; gated save, exact payload, retained draft and no automatic retry (3064.951244ms)
|
||||
2026-10-15T17:00:38.927Z settings user="jason" changed
|
||||
✔ REQ-WEB-1 signed-in same-origin Settings write reaches exactly one human operation (66.409188ms)
|
||||
✔ REQ-WEB-1 Settings writes require enabled send, named Host, identity, exact Origin and same-origin Fetch Metadata (14.548245ms)
|
||||
✔ REQ-WEB-1 Settings validates JSON, size, revision and strict writable keys before broker call (11.685139ms)
|
||||
✔ tokens.css and icons.svg are byte copies of docs/design (2.792135ms)
|
||||
✔ JetBrains Mono files match the digests in their sources file, with the OFL text (3.444631ms)
|
||||
✔ shipped strings are unchanged, and the web keeps its write boundary (4.374134ms)
|
||||
✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (7966.727862ms)
|
||||
✔ five states on Board, Inbox, Tasks, Agents and Trail (2232.220164ms)
|
||||
✔ Queue, Business and Settings in their five states, with no move button (5297.612022ms)
|
||||
✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (1043.215719ms)
|
||||
✔ the server log names no config path when the system config refuses (419.452662ms)
|
||||
ℹ tests 112
|
||||
ℹ suites 0
|
||||
ℹ pass 112
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 55118.019432
|
||||
Reference in New Issue
Block a user