agents: Dewey row 133 round 2 packet (#1627, rebase onto rows 49 and 123)

Base 89fd2d6fe. r1's build.patch on the new base gives the r2 tree byte for
byte, so delta.patch is empty; the interdiff shows only index lines and hunk
offsets. Mutants 5/5 killed with controls passing, targeted gate rc 0.
Candidate manifest sha256 1c0e6f8f260ae51b31c2235193c36ed1c005faefe317e1452a351a3bb5690ef0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-15 12:59:16 -05:00
co-authored by Claude Opus 5.5
parent 593b437fdf
commit 346347c950
26 changed files with 1790 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
# Row 133 (#1627) round 2: rebase onto rows 49 and 123
Builder: Dewey. Reviewer: Filbert, delta review. Landing gate: Sage.
Round 1: agents/dewey/work/queue-133/r1/. Filbert approved r1 in 28589 (rev 1313).
Instruction: sage-0350, which says rows 49 (3a4afdc03) and 123 (89fd2d6fe) are in and that the
rebase builds on 89fd2d6fe.
Base: 89fd2d6fee356e18f7c440ca35abb8c078ae1066 (base.txt). The candidate is build.patch on that
base, and candidate-manifest.sha256 lists every changed path with its sha256.
## What changed from round 1
Nothing in the change itself.
- **delta.patch** is r1's build.patch applied to 89fd2d6fe, diffed against r2, file by file over
the manifest's paths. It is empty: r1's patch on the new base gives the r2 tree byte for byte.
I checked this on a fresh archive of 89fd2d6fe, where every manifest digest matched.
- **build-patch-interdiff.diff** compares the two build.patch files. The only differences are:
- the `index` lines for packages/bus/README.md and packages/bus/src/broker.mjs
- hunk offsets in those two files: README +1 line, broker.mjs +6 and +50
No changed line differs.
- **Manifest against r1:**
- Same digests: decision-route.test.mjs, business.mjs, assistant.test.mjs and business.test.mjs.
- New digests: bus README.md and broker.mjs. The patch is the same, but rows 49 and 123
changed their bases.
## Interaction with rows 49 and 123
- Row 123 gives the chief of staff `role.end` and `organization.change` and lets `role.revoke`
take a reason. None of these inserts a message or calls `#checkMessaging`.
- The raise check sits in `decision.raise`, which row 123 doesn't touch.
- After the rebase there are four callers of `#checkMessaging`: `decision.raise`, `message.send`,
`review.request` and `review.verdict`. These are the same four as in r1.
- Row 123's change to roles/chief-of-staff.json adds `withinRole` actions and no messaging lists.
roles/assistant.json is still the only shipped role with lists, so the validator rule still
passes the shipped example and Jason's arrangement.
- Row 49 changes the store, the migrations and the tasks sync. It touches no code this row changes.
## Tests
No test changed. Run singly on the candidate before the slot, all 0 fail and 0 skipped:
| Package | File | Tests |
|---|---|---|
| bus | decision-route | 5 |
| bus | chief-of-staff (row 123) | 13 |
| bus | messaging | 12 |
| business | assistant | 10 |
| business | chief-of-staff (row 123) | 10 |
## Targeted gate and mutants
Decision 136, and the list in sage-0350. gate.sh runs `node --test tests/*.test.mjs` in
business, bus, tasks, webui and cli, with one log per package under gate/. mutants.py has the
round 1 faults unchanged; only its default worktree path differs. MUTANTS.md is round 1's, with
this round's run appended.
## Receipts
Slot granted by Sage in sage-0353 at 17:55Z. Released at 2026-10-15T17:57:43Z with "slot clear".
- **Mutants**, 2026-10-15T17:55:54Z to 17:55:58Z: both control runs passed, and all 5 mutants were
killed by the same tests as in round 1. The source was restored to the manifest afterwards.
See MUTANTS.md and mutants-run.log.
- **Gate**, 2026-10-15T17:56:00Z to 17:57:38Z, rc 0, all 0 fail and 0 skipped (gate-run.log):
| Package | Tests | Round 1 |
|---|---|---|
| business | 94 | 94 |
| bus | 196 | 176 |
| tasks | 97 | 92 |
| webui | 112 | 112 |
| cli | 195 | 195 |
The bus and tasks increases are the tests rows 49 and 123 added to the base. This row adds
no tests in round 2.
- **Candidate:** candidate-manifest.sha256, sha256
1c0e6f8f260ae51b31c2235193c36ed1c005faefe317e1452a351a3bb5690ef0. build.patch on 89fd2d6fe
gives every digest in the manifest.
+34
View File
@@ -0,0 +1,34 @@
# Row 133 (#1627) mutants (round 1 table; round 2 run appended)
One fault per check, applied to the candidate worktree one at a time by mutants.py, each run in its
own process group with DOCKER_HOST pointed at a nonexistent socket. A control run first runs each
distinct test set on the unmutated source; any failure stops the run. `--dry` checks every
mutation site matches and changes the source, and runs nothing.
| Id | File | Fault | Test |
|---|---|---|---|
| 01-validator-check | business.mjs | the arbiter messaging check never refuses (brief: drop the validator check) | business assistant.test.mjs |
| 02-raise-check | broker.mjs | decision.raise has no route check (brief: drop the raise check) | bus decision-route.test.mjs |
| 03-raise-direction | broker.mjs | checks raiser→route instead of route→raiser (brief: wrong direction) | bus decision-route.test.mjs |
| 04-within-role-exempt | broker.mjs | the within-role exemption is dropped (brief) | bus decision-route.test.mjs |
| 05-raise-own-code | broker.mjs | the raise refusal uses message-*-refused, not its own code | bus decision-route.test.mjs |
Results: mutants-run.log, mutants/results.json, mutants/control.json, one log per run under
mutants/. See BUILD.md "Receipts".
Run 2026-10-15T17:31:07Z to 17:31:11Z: controls 2/2 passed; 5/5 killed.
| Id | Failing tests |
|---|---|
| 01 | the per-kind refusal test |
| 02 | the cross-role, gated and supersede refusal tests |
| 03 | those three, plus the allowed-route test |
| 04 | the assistant within-role test |
| 05 | the cross-role, gated and supersede refusal tests |
## Round 2 run, on 89fd2d6fe
Same five faults on the rebased candidate. Run 2026-10-15T17:55:54Z to 17:55:58Z, in the slot Sage
granted in sage-0353: controls 2/2 passed, 5/5 killed. Each mutant failed the same tests as in
round 1 (01: 1, 02: 3, 03: 4, 04: 1, 05: 3). Afterwards the worktree's changed files matched the
candidate manifest (RESTORED_MATCHES_MANIFEST in mutants-run.log).
+2
View File
@@ -0,0 +1,2 @@
base 89fd2d6fee356e18f7c440ca35abb8c078ae1066
worktree HEAD 89fd2d6fee356e18f7c440ca35abb8c078ae1066
@@ -0,0 +1,71 @@
--- r1/build.patch
+++ r2/build.patch
@@ -1,8 +1,8 @@
diff --git a/packages/bus/README.md b/packages/bus/README.md
-index 4694e08b5..43b9b52a6 100644
+index 3bfd35a55..36965636c 100644
--- a/packages/bus/README.md
+++ b/packages/bus/README.md
-@@ -208,6 +208,13 @@ requires a strict task ref. `role.launch` is gated unless the business launch
+@@ -209,6 +209,13 @@ requires a strict task ref. `role.launch` is gated unless the business launch
block's `by` names the acting role and refused when launch state is revoked. S6 still owns launch allowlists,
capacity and actual spawning.
@@ -17,10 +17,10 @@
| Code | Field | Refused when |
diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs
-index f10e11b37..d0da6270c 100644
+index ecb18f8b8..239af6921 100644
--- a/packages/bus/src/broker.mjs
+++ b/packages/bus/src/broker.mjs
-@@ -330,15 +330,20 @@ export class Broker {
+@@ -336,15 +336,20 @@ export class Broker {
// without the lists is unlimited, as before. The sender's lists are checked first, then the
// target's, so a tester or the human sending to a limited role is refused unless listed. Every
// path that stores a message from a role checks it: message.send, review.request, review.verdict.
@@ -50,7 +50,7 @@
}
#decision(s, id) {
const d = this.#store.get('SELECT * FROM decisions WHERE id=? AND business=?', id, s.business);
-@@ -837,6 +842,10 @@ export class Broker {
+@@ -887,6 +892,10 @@ export class Broker {
if (!['technical', 'delivery'].includes(domain)) fail('invalid-domain');
const proposedRoute = cls === 'gated' ? 'human' : cls === 'cross-role' ? b.arbiters[domain] : s.role;
const route = cls === 'cross-role' && proposedRoute === s.role ? 'human' : proposedRoute;
@@ -61,7 +61,7 @@
if (a.supersedes) {
const old = this.#decision(s, a.supersedes);
if (old.raised_by_run !== s.run || this.#closed(old.id)) fail('supersede-refused');
-@@ -938,6 +947,10 @@ export class Broker {
+@@ -988,6 +997,10 @@ export class Broker {
d.id,
);
// Row 115 (#1609): a close by anyone but the raising run tells the raising role, routed like message.send's.
@@ -72,7 +72,7 @@
if (op !== 'seen' && (s.human || s.run !== d.raised_by_run)) {
const outcome =
op === 'resolved'
-@@ -988,7 +1001,7 @@ export class Broker {
+@@ -1038,7 +1051,7 @@ export class Broker {
if (a.to === 'tester') fail('unknown-role');
}
if (a.to !== 'human' && a.to !== 'tester' && !Object.hasOwn(b.roles, a.to)) fail('unknown-role');
@@ -81,7 +81,7 @@
string(a.body, 32768);
if (
a.class !== undefined &&
-@@ -1093,7 +1106,7 @@ export class Broker {
+@@ -1143,7 +1156,7 @@ export class Broker {
fail(older ? 'stale-commit' : 'unknown-commit');
}
// REQ-ASSIST-2 has no approval exception: a review request is a message to the reviewer.
@@ -90,7 +90,7 @@
const mid = randomUUID();
// Row 105 (#1599): the run holding the reviewer role, or null when nobody holds it and the
// request waits for a claim. #reviewMessage routes on the same holder.
-@@ -1136,7 +1149,7 @@ export class Broker {
+@@ -1186,7 +1199,7 @@ export class Broker {
)
fail('verdict-exists');
// The verdict is a message to the role that asked, so it meets that role's receive list.
+318
View File
@@ -0,0 +1,318 @@
diff --git a/packages/bus/README.md b/packages/bus/README.md
index 3bfd35a55..36965636c 100644
--- a/packages/bus/README.md
+++ b/packages/bus/README.md
@@ -209,6 +209,13 @@ requires a strict task ref. `role.launch` is gated unless the business launch
block's `by` names the acting role and refused when launch state is revoked. S6 still owns launch allowlists,
capacity and actual spawning.
+Whoever closes a decision messages the raising role, so a cross-role or gated raise also needs the
+route to be allowed to message the raiser under the role definitions' messaging lists (row 133,
+#1627). In practice the raiser's `receiveFrom` must list the arbiter's definition, or `human` for a
+gated route. Otherwise the raise refuses `decision-route-refused`, logs `action.refused` and stores
+nothing. A within-role raise is exempt. The close itself isn't checked again; the business validator
+refuses an arbiter whose role definition declares messaging lists.
+
`decision.raise` checks its choices in this order, and each of these codes names one field:
| Code | Field | Refused when |
diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs
index ecb18f8b8..239af6921 100644
--- a/packages/bus/src/broker.mjs
+++ b/packages/bus/src/broker.mjs
@@ -336,15 +336,20 @@ export class Broker {
// without the lists is unlimited, as before. The sender's lists are checked first, then the
// target's, so a tester or the human sending to a limited role is refused unless listed. Every
// path that stores a message from a role checks it: message.send, review.request, review.verdict.
- #checkMessaging(s, to) {
- const b = this.#business(s.business);
- const definition = (role) => b.definitions?.[b.roles[role]?.definition];
- const sender = s.human ? 'human' : s.tester ? 'tester' : b.roles[s.role]?.definition;
- const target = to === 'human' || to === 'tester' ? to : b.roles[to]?.definition;
- const out = s.human || s.tester ? undefined : definition(s.role)?.messaging;
- if (out && !out.sendTo.includes(target)) fail('message-recipient-refused');
- const inbound = to === 'human' || to === 'tester' ? undefined : definition(to)?.messaging;
- if (inbound && !inbound.receiveFrom.includes(sender)) fail('message-sender-refused');
+ // Row 133 (#1627): from and to are 'human', 'tester' or a role instance, so decision.raise can
+ // check its route as the sender of the close message; `refused` replaces both codes when given.
+ #checkMessaging(business, from, to, refused) {
+ const b = this.#business(business);
+ const person = (who) => who === 'human' || who === 'tester';
+ const name = (who) => (person(who) ? who : b.roles[who]?.definition);
+ const lists = (who) => (person(who) ? undefined : b.definitions?.[name(who)]?.messaging);
+ const out = lists(from);
+ if (out && !out.sendTo.includes(name(to))) fail(refused ?? 'message-recipient-refused');
+ const inbound = lists(to);
+ if (inbound && !inbound.receiveFrom.includes(name(from))) fail(refused ?? 'message-sender-refused');
+ }
+ #sender(s) {
+ return s.human ? 'human' : s.tester ? 'tester' : s.role;
}
#decision(s, id) {
const d = this.#store.get('SELECT * FROM decisions WHERE id=? AND business=?', id, s.business);
@@ -887,6 +892,10 @@ export class Broker {
if (!['technical', 'delivery'].includes(domain)) fail('invalid-domain');
const proposedRoute = cls === 'gated' ? 'human' : cls === 'cross-role' ? b.arbiters[domain] : s.role;
const route = cls === 'cross-role' && proposedRoute === s.role ? 'human' : proposedRoute;
+ // Row 133 (#1627): whoever closes the decision messages the raiser, so the route must be
+ // allowed to message the raising role before anything is stored. A within-role decision is
+ // routed to the raiser's own role and closes in this call.
+ if (cls !== 'within-role') this.#checkMessaging(s.business, route, s.role, 'decision-route-refused');
if (a.supersedes) {
const old = this.#decision(s, a.supersedes);
if (old.raised_by_run !== s.run || this.#closed(old.id)) fail('supersede-refused');
@@ -988,6 +997,10 @@ export class Broker {
d.id,
);
// Row 115 (#1609): a close by anyone but the raising run tells the raising role, routed like message.send's.
+ // Row 133 (#1627): no messaging check here. decision.raise refused a route that can't message the
+ // raiser, definitions change only with a pinned copy and arbiters only through the business
+ // validator, so the route can still message the raiser when it closes the decision. The human's
+ // withdrawal of an arbiter-routed decision is unchecked too; no shipped role can reach it.
if (op !== 'seen' && (s.human || s.run !== d.raised_by_run)) {
const outcome =
op === 'resolved'
@@ -1038,7 +1051,7 @@ export class Broker {
if (a.to === 'tester') fail('unknown-role');
}
if (a.to !== 'human' && a.to !== 'tester' && !Object.hasOwn(b.roles, a.to)) fail('unknown-role');
- this.#checkMessaging(s, a.to);
+ this.#checkMessaging(s.business, this.#sender(s), a.to);
string(a.body, 32768);
if (
a.class !== undefined &&
@@ -1143,7 +1156,7 @@ export class Broker {
fail(older ? 'stale-commit' : 'unknown-commit');
}
// REQ-ASSIST-2 has no approval exception: a review request is a message to the reviewer.
- this.#checkMessaging(s, a.to);
+ this.#checkMessaging(s.business, this.#sender(s), a.to);
const mid = randomUUID();
// Row 105 (#1599): the run holding the reviewer role, or null when nobody holds it and the
// request waits for a claim. #reviewMessage routes on the same holder.
@@ -1186,7 +1199,7 @@ export class Broker {
)
fail('verdict-exists');
// The verdict is a message to the role that asked, so it meets that role's receive list.
- this.#checkMessaging(s, r.from);
+ this.#checkMessaging(s.business, this.#sender(s), r.from);
const mid = randomUUID();
const eid = this.#event(
s,
diff --git a/packages/bus/tests/decision-route.test.mjs b/packages/bus/tests/decision-route.test.mjs
new file mode 100644
index 000000000..633521ff7
--- /dev/null
+++ b/packages/bus/tests/decision-route.test.mjs
@@ -0,0 +1,145 @@
+// Row 133 (#1627, REQ-ASSIST-2): whoever closes a decision messages the raising role, so
+// decision.raise refuses a route that can't message the raiser before it stores anything. The route
+// is the sender and the raiser the target, under row 120's messaging rules. A within-role decision,
+// routed to the raiser's own role, is exempt.
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { Store } from '../src/store.mjs';
+import { Broker } from '../src/broker.mjs';
+
+// The assistant's lists come from the shipped role file, as in messaging.test.mjs.
+const assistantLists = JSON.parse(
+ readFileSync(fileURLToPath(new URL('../../../roles/assistant.json', import.meta.url)), 'utf8'),
+).messaging;
+
+const options = [
+ { key: 'yes', text: 'Allow' },
+ { key: 'no', text: 'Decline' },
+];
+const pusher = { authority: { withinRole: ['message.send'], crossRole: ['git.push.working'] } };
+const roles = {
+ pm: { definition: 'pm', authority: { withinRole: ['message.send'], crossRole: [] } },
+ chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send'], crossRole: [] } },
+ assistant: { definition: 'assistant', authority: { withinRole: ['message.send'], crossRole: [] } },
+ // Hears from pm, the technical arbiter, but not from the chief of staff, the delivery arbiter.
+ coder: { definition: 'coder', ...pusher },
+ // Hears from neither arbiter nor the human, but may send to the human: a reversed check passes it.
+ warden: { definition: 'warden', ...pusher },
+ // Hears from the human and both arbiters but sends only to pm: a reversed check refuses it.
+ scribe: { definition: 'scribe', ...pusher },
+};
+const definitions = {
+ pm: {},
+ 'chief-of-staff': {},
+ assistant: { messaging: assistantLists },
+ coder: { messaging: { sendTo: ['human', 'pm', 'chief-of-staff'], receiveFrom: ['human', 'pm'] } },
+ warden: { messaging: { sendTo: ['human', 'pm', 'chief-of-staff'], receiveFrom: ['coder'] } },
+ scribe: { messaging: { sendTo: ['pm'], receiveFrom: ['human', 'pm', 'chief-of-staff'] } },
+};
+const businesses = {
+ demo: { id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'chief' }, roles, definitions },
+};
+const ROUTE = 'decision-route-refused';
+
+function setup(t) {
+ const root = mkdtempSync(join(tmpdir(), 'bus-decision-route-'));
+ const store = new Store(root);
+ t.after(() => {
+ store.close();
+ rmSync(root, { recursive: true, force: true });
+ });
+ const b = new Broker({ store, businesses });
+ 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 });
+ const call = (who, verb, args = {}) => b.request(caps[who], { verb, args });
+ const raise = (who, action, extra = {}) =>
+ call(who, 'decision.raise', { action, question: `${action}?`, options, recommendation: 'no', blocking: false, ...extra });
+ const count = (sql) => store.get(sql).n;
+ const stored = () => ({
+ decisions: count('SELECT count(*) n FROM decisions'),
+ decisionEvents: count('SELECT count(*) n FROM decision_events'),
+ messages: count('SELECT count(*) n FROM messages'),
+ allowed: count("SELECT count(*) n FROM events WHERE kind='action.allowed'"),
+ });
+ const refusals = () =>
+ store
+ .all("SELECT actor_role, body FROM events WHERE kind='action.refused' ORDER BY seq")
+ .map((e) => [e.actor_role, JSON.parse(e.body).code]);
+ return { store, call, raise, stored, refusals };
+}
+
+test('a cross-role raise whose arbiter the raiser does not hear from is refused and stores nothing', (t) => {
+ const { raise, stored, refusals } = setup(t);
+ const before = stored();
+ // Delivery goes to chief (chief-of-staff), which the coder's receiveFrom doesn't list.
+ assert.throws(() => raise('coder', 'git.push.working', { domain: 'delivery' }), { code: ROUTE });
+ assert.throws(() => raise('warden', 'git.push.working', { domain: 'technical' }), { code: ROUTE });
+ assert.deepEqual(refusals(), [['coder', ROUTE], ['warden', ROUTE]]);
+ assert.deepEqual(stored(), before, 'no decision, decision event, message or allowed event');
+});
+
+test('a gated raise by a role that does not hear from the human is refused and stores nothing', (t) => {
+ const { raise, stored, refusals } = setup(t);
+ const before = stored();
+ // deploy is gated, so the route is the human; warden's receiveFrom lacks human.
+ assert.throws(() => raise('warden', 'deploy', { target: 'release-1' }), { code: ROUTE });
+ // A cross-role action outside the raiser's crossRole is gated too.
+ assert.throws(() => raise('warden', 'task.create'), { code: ROUTE });
+ assert.deepEqual(refusals(), [['warden', ROUTE], ['warden', ROUTE]]);
+ assert.deepEqual(stored(), before);
+});
+
+test('a route the raiser hears from is allowed, checked from the route to the raiser', (t) => {
+ const { raise, call, store } = setup(t);
+ const technical = raise('coder', 'git.push.working', { domain: 'technical' });
+ const gated = raise('coder', 'deploy', { target: 'release-1' });
+ assert.deepEqual([technical.route_to, gated.route_to], ['pm', 'human']);
+ // scribe sends only to pm, but every route here may message scribe.
+ const routes = [
+ raise('scribe', 'git.push.working', { domain: 'delivery' }),
+ raise('scribe', 'git.push.working', { domain: 'technical' }),
+ raise('scribe', 'deploy', { target: 'release-1' }),
+ ].map((d) => d.route_to);
+ assert.deepEqual(routes, ['chief', 'pm', 'human']);
+ // The close message the check stands for reaches the raiser.
+ call('pm', 'decision.resolve', { id: technical.id, choice: 'yes' });
+ const close = store.get("SELECT from_role, to_role FROM messages WHERE class='DECISION' AND decision=?", technical.id);
+ assert.deepEqual({ ...close }, { from_role: 'pm', to_role: 'coder' });
+});
+
+test('a refused supersede withdraws nothing', (t) => {
+ const { raise, store } = setup(t);
+ const old = raise('coder', 'git.push.working', { domain: 'technical' });
+ assert.throws(
+ () => raise('coder', 'git.push.working', { domain: 'delivery', supersedes: old.id }),
+ { code: ROUTE },
+ );
+ assert.equal(store.get('SELECT count(*) n FROM decision_events WHERE decision=?', old.id).n, 0);
+ assert.equal(store.get('SELECT count(*) n FROM decisions').n, 1);
+});
+
+test("the assistant's within-role and gated raises still work", (t) => {
+ const { raise, call, store, refusals } = setup(t);
+ // message.send is within role: routed to the assistant itself, which its own lists don't name.
+ const within = raise('assistant', 'message.send', { choice: 'yes' });
+ assert.equal(within.route_to, 'assistant');
+ assert.equal(
+ store.get("SELECT op FROM decision_events WHERE decision=?", within.id).op,
+ 'resolved',
+ );
+ // Anything else is gated: routed to the human, whom the assistant hears from.
+ const gated = raise('assistant', 'task.create');
+ assert.equal(gated.route_to, 'human');
+ call('human', 'decision.resolve', { id: gated.id, choice: 'no' });
+ const close = store.get("SELECT from_role, to_role FROM messages WHERE class='DECISION' AND decision=?", gated.id);
+ assert.deepEqual({ ...close }, { from_role: 'human', to_role: 'assistant' });
+ assert.deepEqual(refusals(), []);
+});
diff --git a/packages/business/src/business.mjs b/packages/business/src/business.mjs
index 49ac1fa45..4fa492d4a 100644
--- a/packages/business/src/business.mjs
+++ b/packages/business/src/business.mjs
@@ -156,6 +156,12 @@ export function validateBusinessDocument(document, file, { rolesDir }) {
const name = document.arbiters[kind];
requireId(name, `${file} arbiters.${kind}`);
if (!Object.hasOwn(roles, name)) refuse(`${file} arbiters.${kind} names ${name}, which the business file doesn't declare`);
+ // Row 133 (#1627): an arbiter messages the raiser when it closes a decision, so it can't be a
+ // role whose definition limits who it sends to and hears from.
+ const definition = roles[name].definition;
+ if (definitions[definition].messaging) {
+ refuse(`${file} arbiters.${kind} names ${name}, whose role definition ${definition} declares messaging lists`);
+ }
arbiters[kind] = name;
}
diff --git a/packages/business/tests/assistant.test.mjs b/packages/business/tests/assistant.test.mjs
index 9cdbff49a..3b9759e96 100644
--- a/packages/business/tests/assistant.test.mjs
+++ b/packages/business/tests/assistant.test.mjs
@@ -122,6 +122,31 @@ test("the instance takes no credential references and no tracker", () => {
);
});
+// Row 133 (#1627): an arbiter messages the raiser when it closes a decision, so a role limited by
+// messaging lists can't be one. The message names the kind, the instance and the definition.
+test("an arbiter whose role definition declares messaging lists is refused, for each kind", () => {
+ for (const kind of ["delivery", "technical"]) {
+ refuses(
+ () => loadFixture((doc) => { doc.arbiters[kind] = INSTANCE; }),
+ new RegExp(`arbiters\\.${kind} names assistant, whose role definition assistant declares messaging lists`),
+ );
+ // An instance name that isn't the definition's: both are named.
+ refuses(
+ () => loadFixture((doc) => {
+ doc.roles.aide = { definition: "assistant" };
+ doc.arbiters[kind] = "aide";
+ }),
+ new RegExp(`arbiters\\.${kind} names aide, whose role definition assistant declares messaging lists`),
+ );
+ }
+});
+
+test("an arbiter without messaging lists is accepted beside the limited assistant", () => {
+ assert.deepEqual(loadFixture().business.arbiters, { delivery: "pm", technical: "cto" });
+ const { business } = loadFixture((doc) => { doc.arbiters.delivery = "chief"; });
+ assert.deepEqual(business.arbiters, { delivery: "chief", technical: "cto" });
+});
+
test("the contract passes work to the chief of staff and directs no one", () => {
const text = readFileSync(join(REPO_ROLES, "assistant.md"), "utf8");
assert.match(text, /chief of staff/);
diff --git a/packages/business/tests/business.test.mjs b/packages/business/tests/business.test.mjs
index 48e181f26..65f696897 100644
--- a/packages/business/tests/business.test.mjs
+++ b/packages/business/tests/business.test.mjs
@@ -215,6 +215,8 @@ test("the shipped example refuses as written and validates once filled in", () =
.replaceAll("/home/you", root));
const business = validateBusinessDocument(filled, file, { rolesDir: REPO_ROLES });
assert.deepEqual(Object.keys(business.roles), ["pm", "cto", "coder", "reviewer", "chief", "assistant"]);
+ // Row 133 (#1627): the arbiters declare no messaging lists, so the limited assistant beside them passes.
+ assert.deepEqual(business.arbiters, { delivery: "pm", technical: "cto" });
assert.equal(business.tracker.sync.credentials.vikunja.file, join(root, ".config/mosaic-dev/secrets/mosaic-stack/sync-vikunja.token"));
assert.equal(id, 15);
});
@@ -0,0 +1,6 @@
d1ce060dc03a426c9f21d9d4ef248da32afa4714a9b371437e324a9804f1b914 packages/bus/README.md
1f93a43e1c272798b7415a023d68527539e450476e4e673c894845338e6ef8e9 packages/bus/src/broker.mjs
3fff07ce07abaf93661b5d3c01555c06475d3c556b08a6b5ce5e603adf3cac8b packages/bus/tests/decision-route.test.mjs
b92caf47feb609f43946057278ae6456df8b427167891d260a8c55ddea0c829d packages/business/src/business.mjs
6fe64b09c87cac4ec645e8a640ce7d9a8a02394844222ac43c9ed44379c6e176 packages/business/tests/assistant.test.mjs
d2c535737ff0695628b2f1e3861266655e235469271cddfc3140aa2516f582bb packages/business/tests/business.test.mjs
@@ -0,0 +1,7 @@
gate start 2026-10-15T17:56:00Z
pkg-business start=2026-10-15T17:56:00Z end=2026-10-15T17:56:02Z rc=0 tests=94 pass=94 fail=0 skipped=0
pkg-bus start=2026-10-15T17:56:02Z end=2026-10-15T17:56:08Z rc=0 tests=196 pass=196 fail=0 skipped=0
pkg-tasks start=2026-10-15T17:56:08Z end=2026-10-15T17:56:13Z rc=0 tests=97 pass=97 fail=0 skipped=0
pkg-webui start=2026-10-15T17:56:13Z end=2026-10-15T17:57:08Z rc=0 tests=112 pass=112 fail=0 skipped=0
pkg-cli start=2026-10-15T17:57:08Z end=2026-10-15T17:57:38Z rc=0 tests=195 pass=195 fail=0 skipped=0
gate end 2026-10-15T17:57:38Z rc=0
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Row 133 round 2 targeted builder gate (decision 136, sage-0350): business, bus and their consumers, on 89fd2d6fe.
# Same command land-work.sh runs per package. One log per package under gate/.
set -uo pipefail
WT=${1:-$HOME/dewey-scratch/r133/wt2}
OUT=$(cd "$(dirname "$0")" && pwd)/gate
mkdir -p "$OUT"
export TMPDIR=$HOME/dewey-scratch/tmp
status=0
for p in business bus tasks webui cli; do
start=$(date -u +%FT%TZ)
(cd "$WT/packages/$p" && node --test tests/*.test.mjs) >"$OUT/pkg-$p.log" 2>&1
rc=$?
counts=$(grep -E '^ℹ (tests|pass|fail|skipped) ' "$OUT/pkg-$p.log" | awk '{printf "%s=%s ", $2, $3}')
echo "pkg-$p start=$start end=$(date -u +%FT%TZ) rc=$rc $counts"
[ "$rc" -eq 0 ] || status=1
done
exit $status
@@ -0,0 +1,204 @@
✔ agents: a held role with nothing on the bus is idle; a role session gets the claim rows unchanged (132.264851ms)
✔ agents: working counts what the holder received and has not read; routed alone is not working (213.559523ms)
✔ agents: input needed is the role's open decisions routed to the human, gated first, then oldest (334.063627ms)
✔ agents: working wins over waiting, and Input needed still shows (187.982772ms)
✔ agents: a new holder doesn't inherit what the last one left unread; decisions stay with the role (229.731005ms)
✔ agents: state reads only its own business, even for a run name both use (200.253713ms)
✔ agents: a message the holder released unread is stranded; the next holder gets nothing and cannot read it (236.281043ms)
✔ agents: a message the holder read is not stranded; one the current holder has not read is not listed (216.257905ms)
✔ agents: stranded is oldest first and capped at 50 for a role (1012.433718ms)
✔ agents: stranded is per role and per business (544.379539ms)
✔ agents: a message routed to a held role and not yet received is not stranded (148.7665ms)
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (172.438397ms)
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (271.571897ms)
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (289.374843ms)
✔ launch events require a human CLI capability; generic emit cannot forge authority events (192.156575ms)
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (216.181566ms)
✔ a recommendation that is not exactly one option key refuses invalid-recommendation after the options (298.35435ms)
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (176.266651ms)
✔ task action subjects and linked decision trail are complete and ordered (220.908454ms)
✔ launch binding is durable and reconnecting requires the identical trusted record (130.365974ms)
✔ business isolation includes inherited object names and cross-business message references (261.964601ms)
✔ authority never transfers between action, run, target, unresolved or replaced role holder (299.399522ms)
✔ task projection uses schema current view, skipping earlier and equal-start polls (171.343305ms)
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (426.558461ms)
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (165.62934ms)
✔ both arbiters require human resolution when their cross-role route is themselves (263.023499ms)
✔ the shipped coder role raises task.reassign as gated to the human; task.scope.change stays cross-role (166.289505ms)
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.236125ms)
✔ REQ-COS-1, REQ-ROLE-2: the chief of staff ends a run with a reason and no decision, and the reason is logged (216.655294ms)
✔ REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke (316.479121ms)
✔ REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither (225.824474ms)
✔ REQ-ROLE-2: the chief of staff may still cite a decision, and a decision alone is the old path (230.376525ms)
✔ REQ-ROLE-2: a role without role.end still ends a run through a resolved decision, which is recorded and has no reason (262.071177ms)
✔ REQ-ROLE-2: a reason is required, bounded, and never sent with a decision (301.447835ms)
✔ REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end (268.774777ms)
✔ REQ-LIMIT-1, REQ-COS-1: organization.set from the chief of staff reaches the shared writer, named as the actor (263.957034ms)
✔ REQ-LIMIT-1, REQ-COS-1: a refused organization.set logs action.refused and no action.allowed, like a refused limits.set (327.822023ms)
✔ REQ-LIMIT-1: organization.set is refused for every caller whose role file lacks organization.change (326.642994ms)
✔ REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys (558.955586ms)
✔ 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 (529.367284ms)
✔ 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 (806.337085ms)
✔ comment cursors isolate businesses, move only forward and remain append-only (135.825145ms)
✔ only trusted sync writes validated cursors; events and cursor share rollback (131.538477ms)
✔ explicit 3b copy migration preserves evidence and opens as 3d; normal startup never migrates (873.126347ms)
✔ migration refuses active writers and drifted legacy schemas, with no partial upgrade (1675.117192ms)
✔ named migration registry chains 3b through 3d to a fixture-only future version in order (1016.47104ms)
✔ migration refuses mismatched source metadata without changing schema or evidence (720.762803ms)
✔ offline command migrates only the supplied fixture root and reports usage errors (706.388955ms)
✔ credentials gives human and reader sessions service, account, role, date and state, and nothing else (112.037206ms)
✔ credentials refuses a role session and any argument (170.034547ms)
✔ credentials reads the status on each request, and a broker without one lists none (223.310474ms)
✔ the broker process answers credentials without a token, path or variable in its output, log or store (194.696042ms)
✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.350137ms)
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.468261ms)
✔ expiry refuses use and env references never become client data (0.828109ms)
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.828517ms)
✔ opaque tokens shorter than 16 characters refuse before use (0.49973ms)
✔ a human resolve sends the raising role one message from the human, and its request is the answer (255.34505ms)
✔ a cross-role decision resolved by its arbiter comes from that role and run, with no request (236.284342ms)
✔ a human withdraw sends one message with the note (239.359032ms)
✔ a human seen before the close leaves the request on the close's answer, not the seen (300.992446ms)
✔ the raising run's withdraw, a supersede, a within-role raise and seen send nothing (310.488755ms)
✔ refused closes send nothing: invalid-choice, decision-closed, not-resolver (314.734151ms)
✔ a raising role nobody holds at the close gets the message on the next claim (300.94416ms)
✔ a human DECISION send keeps its own instruction as the request, not a close's answer (304.872382ms)
✔ a cross-role raise whose arbiter the raiser does not hear from is refused and stores nothing (272.613227ms)
✔ a gated raise by a role that does not hear from the human is refused and stores nothing (283.162504ms)
✔ a route the raiser hears from is allowed, checked from the route to the raiser (333.974075ms)
✔ a refused supersede withdraws nothing (307.34247ms)
✔ the assistant's within-role and gated raises still work (303.000256ms)
✔ roles lists open decisions routed to a role, and never the human inbox (223.514552ms)
✔ recent lists closed decisions of every route, newest close first, with how each closed (296.623159ms)
✔ recent gives the closing event as closed when the decision was seen first (300.053077ms)
✔ recent stops at RECENT rows (879.027385ms)
✔ every row is what trail already gives the same session for that id (296.002935ms)
✔ a role session can't list decisions, so it can't read another role's open decisions (477.541353ms)
✔ decisions is a read: one business, strict arguments, nothing written (604.899669ms)
✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (144.939003ms)
✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (149.232694ms)
✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (144.038781ms)
✔ endLaunch leaves a claim another run took alone (188.8422ms)
✔ refuse records action.refused against the caller with the code only (148.725904ms)
✔ launches off refuses role.launch with launch-revoked until launches on (206.248507ms)
✔ broker process: launch ops authorize role.launch, record refusals and end runs (291.96435ms)
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.506128ms)
✔ process reader gets own kernel identity without exposing environment values (0.495843ms)
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.672786ms)
✔ real pid 1 remains inspectable when its environment is protected (0.267512ms)
✔ limits binds actor from human proof and rejects role, reader, tester and forged actor (189.347603ms)
✔ runtime wires verified human settings to the actual shared file writer and actor receipt (278.906431ms)
✔ uncertain organization receipts leave bus startup and reads alone but refuse writes until reconciled (447.115224ms)
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (209.269792ms)
✔ missing and foreign-business citations refuse and roll back message and grant (186.98284ms)
✔ cross-role sends still need a matching resolved decision and consume it once (261.366123ms)
✔ messages pages all human messages newest first without writes; read state is history (2976.581845ms)
✔ messages refuses role callers, invalid paging and cursors outside this human feed (298.247964ms)
✔ views messages passes only its paging arguments through (0.228953ms)
✔ messages isolates businesses even with a valid foreign cursor (200.201271ms)
✔ the assistant sends to the user and to the chief of staff only (301.837205ms)
✔ an assistant send to any other role, or to the tester, is refused and logged (344.286075ms)
✔ only the user and the chief of staff may send to the assistant; the tester is refused by default (363.642955ms)
✔ a sender-side list alone refuses a send to a role that has no list (332.694829ms)
✔ a target-side list alone refuses a send from a role that has no list (357.330265ms)
✔ the sender list is checked before the target list (334.693627ms)
✔ roles without the field behave exactly as before (467.705514ms)
✔ a business with no role definitions at all is unlimited (483.428124ms)
✔ a malformed messaging field refuses at boot (123.811638ms)
✔ the assistant's review.request to a reviewer is refused and logged, even with an approved decision (355.577487ms)
✔ a review.verdict to the assistant from a role outside its receiveFrom is refused and logged (348.388419ms)
✔ review requests and verdicts between roles without the field are unchanged (265.146356ms)
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (193.772766ms)
✔ startup token refusal returns safe code without value or partial listening broker (45.002777ms)
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (192.764971ms)
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (185.704393ms)
✔ trusted host registers later launches; socket clients never have a registration verb (203.133669ms)
✔ runtime excludes declared project roots even when host supplies no repoRoots (46.55421ms)
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (198.492348ms)
✔ a reply is listed for human and reader sessions, and its delivery rows are unchanged (246.603321ms)
✔ the reply still reaches the human message.receive (mosaic talk) after the Console showed it (208.40342ms)
✔ a message with no reply gives an empty list (178.252316ms)
✔ replies come oldest first and stop at REPLIES (820.665433ms)
✔ an id that names no message in the business refuses message-not-found, and the refusal is recorded (364.236175ms)
✔ strict arguments; a role session gets human-required (432.919603ms)
✔ views passes replies through as one call with the id (0.211792ms)
✔ requirements returns the committed list to a holding role session, and no one else (236.61128ms)
✔ the PM's prd.approve with no task, when no id fits, reaches the human's Inbox (183.801135ms)
✔ the PM's non-blocking role.revoke with no task reaches the human's Inbox (196.254517ms)
✔ a push is recorded as the action.allowed for git.push.working on the task, and only the host records one (267.572808ms)
✔ review.request names the latest pushed commit and sends the reviewer a REVIEW-REQUEST (272.986832ms)
✔ review.request says which run holds the reviewer role, or null, and a null request waits for a claim (312.665046ms)
✔ review.request refuses a commit that isn't the latest push, a non-reviewer and a role without the action (439.791259ms)
✔ reviewFetch answers only the addressed reviewer, for the request it cites (296.718619ms)
✔ review.verdict answers one request once, to the role that asked (324.378815ms)
✔ task.close on a pushed task needs an approve verdict for the latest push (578.588124ms)
✔ a refusal the host decided is filed on the task it names, and only a task ref counts (278.907499ms)
✔ v3b prototype refusals, views and append-only mutations (1275.857393ms)
✔ gated approval authorizes once, survives store reopen, and fresh approval works (279.66094ms)
✔ another run cannot consume an approval; a failed check leaves it usable (231.547873ms)
✔ two scheduled callers have exactly one grant and one consumed refusal (198.625113ms)
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (395.101211ms)
✔ class drift gated to cross-role refuses before consumption (248.251006ms)
✔ class drift cross-role to gated refuses before consumption (266.501889ms)
✔ class drift gated to within-role refuses before consumption (406.710027ms)
✔ class drift cross-role to within-role refuses before consumption (276.272561ms)
✔ class drift within-role to gated refuses before consumption (192.118822ms)
✔ class drift within-role to cross-role refuses before consumption (189.973561ms)
✔ message.send consumes approval and prevents a later send or authorize (209.681081ms)
✔ role.revoke consumes approval and prevents a later revoke or authorize (239.861226ms)
✔ a business that never switched launches reads allowed with no time and no event (148.456882ms)
✔ launches revoked and then restored read allowed, with the restore time and event id (160.465793ms)
✔ a task closed on the tracker leaves the open list, and total counts the open ones (173.182716ms)
✔ a tombstone is not open (157.073494ms)
✔ the open list stops at the cap and says so; total still counts every open task (180.650302ms)
✔ a role and a reader session read state; arguments, and an unbound cap, refuse (167.83733ms)
✔ a human session reads the same state, with the same cap (142.834056ms)
✔ reading state writes nothing: every table keeps its row count (176.699696ms)
✔ creates private WAL store and excludes a second writer until explicit close (132.917151ms)
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (159.068011ms)
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (213.859877ms)
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (198.71312ms)
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (143.403416ms)
✔ async transactions refuse before invoking their function (104.036271ms)
✔ the session default fits under the reply cap and pages to the end with next (325.461761ms)
✔ the short form: no description, assignees by role, every listed key (301.964857ms)
✔ each filter, alone and together, and total counts every match (344.750204ms)
✔ task_ref reads one task in full, as the whole board has it (339.52689ms)
✔ no arguments is the read the Console and mosaic make, unchanged (373.280247ms)
✔ an unknown argument or a bad value refuses invalid-request (921.168517ms)
✔ tasks rows carry priority and requirement for every session that reads tasks (323.146813ms)
✔ the read model rebuilds from the ledger: a reopened bus and each trail give the same values (540.95522ms)
✔ an empty business and one with snapshots but no task.created read cleanly (130.18417ms)
✔ recordTask keeps sync reads and a role write apart (191.988789ms)
✔ read_at must be one canonical UTC format, so the projection compares strings safely (119.564388ms)
✔ a bad entry refuses the whole record (119.851003ms)
✔ taskView reads the projection for one business (292.788123ms)
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (273.877559ms)
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (438.742663ms)
✔ without an adapter the server refuses every task verb (209.803759ms)
✔ the runtime refuses an invalid adapter and closes a valid one (194.172048ms)
✔ the process loads the S3 adapter from plain-data trackers (259.920503ms)
✔ the tester binds only when the business sets bus.tester to exactly true (1039.656457ms)
✔ a business may not name a role tester, so the tester address is never a role (229.831799ms)
✔ every verb outside the tester list refuses tester-refused and is recorded against the tester (538.146557ms)
✔ a tester message is from tester, starts [mos test], and writes no human.input or authority (193.803683ms)
✔ a role answers a tester only in reply to a tester message, and the answer never reaches the human (233.987093ms)
✔ the tester reads the human views a reader reads (144.148141ms)
✔ tester proof binds its own CLI, start time and nonce, refuses a managed run and allows an agent (1.047927ms)
✔ neither CLI's nonce proves the other: each proof is bound to its own CLI and nonce variable (0.329127ms)
✔ testerNonce reads only the tester's variable and gives null on any read error (0.178096ms)
✔ an envelope carries exactly one of cap, human and tester; no tester authenticator refuses (83.632446ms)
✔ tester-cli.mjs binds through the runtime when the switch is on, and refuses off or under a run marker (762.785749ms)
✔ socket capability stamps launch identity; shared views use wire, no SQL client (169.797833ms)
✔ two wire claims serialize; a lost reply never automatically retries (365.153809ms)
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (122.464801ms)
✔ client preserves UTF-8 when a response divides a multibyte character (11.391348ms)
✔ committed mutation followed by dropped reply reports unknown and is never retried (169.723138ms)
ℹ tests 196
ℹ suites 0
ℹ pass 196
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5717.555599
@@ -0,0 +1,102 @@
✔ the role file loads and resolves through loadBusiness (6.48436ms)
✔ the shipped file is exactly the approved list (0.79193ms)
✔ the assistant's authority is message.send within role and nothing else (2.090435ms)
✔ the assistant talks to the user and the chief of staff only, and no other shipped role is limited (0.897037ms)
✔ a business grant through limits does not widen the role (2.163018ms)
✔ a launch block whose by is the assistant is refused (2.547927ms)
✔ the instance takes no credential references and no tracker (3.437784ms)
✔ an arbiter whose role definition declares messaging lists is refused, for each kind (5.172841ms)
✔ an arbiter without messaging lists is accepted beside the limited assistant (2.798003ms)
✔ the contract passes work to the chief of staff and directs no one (0.814633ms)
✔ config directory and file path follow MOSAIC_CONFIG (1.330069ms)
✔ the fixture business validates and comes back frozen (4.689413ms)
✔ two instances may share a definition (1.695013ms)
✔ top-level refusals (6.548175ms)
✔ arbiters and projects (6.835765ms)
✔ role instances (3.343662ms)
✔ Vikunja bots (11.646531ms)
✔ a role without Vikunja takes no tracker block (2.446943ms)
✔ credential references match the definition's services (2.385094ms)
✔ launch (7.340879ms)
✔ loadBusiness: file checks (1.71205ms)
✔ loadBusiness: not a regular file (37.072127ms)
✔ loading writes nothing (1.003507ms)
✔ names that are Object.prototype properties don't count as declared (2.566183ms)
✔ the shipped example refuses as written and validates once filled in (0.890358ms)
✔ the role file loads and resolves through loadBusiness (8.525449ms)
✔ the shipped file is exactly the approved list (0.319491ms)
✔ the resolved record matches the approved list, so a widened file fails (2.310911ms)
✔ the chief of staff holds role.launch, message.send, role.end and organization.change; every other action is gated (1.985391ms)
✔ the pm does not hold role.launch (2.477879ms)
✔ a business grant of a gated action through limits does not widen the role (2.637073ms)
✔ a limit that drops role.launch leaves the chief of staff with no launch block (1.562652ms)
✔ a launch block whose by is the pm instance is refused (3.258623ms)
✔ the contract names no duty to talk to the human and the pm contract no launch (0.440502ms)
✔ the instance takes no credential references and no tracker (2.705358ms)
✔ usage errors exit 4 (273.067411ms)
✔ validate: a good business exits 0 and prints instance digests (65.317297ms)
✔ validate: project files (309.768417ms)
✔ validate: missing files and a broken system config (220.326647ms)
✔ validate: credential reference problems exit 2 and name each one (62.474726ms)
✔ validate: a token file inside the repository is refused (61.375487ms)
✔ validate: role definitions come from MOSAIC_ROLES_DIR (187.345079ms)
✔ resolve: prints one instance's record (250.46355ms)
✔ resolve: refusals (365.482576ms)
✔ parse: exactly one of file or env, plus the service's date (3.283662ms)
✔ check: a good file has no problems (0.996255ms)
✔ check never opens the file: a write-only token passes (0.441572ms)
✔ check: file problems (1.112457ms)
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (1.127821ms)
✔ check: dates and environment references (0.533983ms)
✔ REQ-LIMIT-1 schema: every scope unset or zero, adjustable above four, bad values and unknown models refuse (16.282005ms)
✔ REQ-LIMIT-1 counts each exact scope at/over limit, unlimited and old tuple after model reassignment (1.092842ms)
✔ REQ-ROLE-3 writer: atomic validated revision, immutable previous bytes and authenticated actor/old/new receipt (350.695244ms)
✔ REQ-ROLE-3 writer: pre-rename fault leaves old file, post-rename recovery records commit, conflicting recovery refuses (155.559742ms)
✔ REQ-LIMIT-1 / decision145 migration prints only, preserves values, rejects ambiguity, and is idempotent (2.846054ms)
✔ REQ-LIMIT-1 shared settings validates the complete proposed selection and logs exact actor (101.837033ms)
✔ REQ-LIMIT-1 provider is table data: another provider routes separately and aliases share exact counts (1.718795ms)
✔ REQ-ROLE-3 refuses live writer locks, symlink targets and uncertain recovery without changing bytes (62.797997ms)
✔ REQ-PLUG-1 schema: plugins is optional, names known kinds and checked module names (11.842781ms)
✔ REQ-PLUG-1 REQ-ROLE-3 the setter writes plugins through the organization writer and logs old and new names (388.164472ms)
✔ REQ-PLUG-1 REQ-ROLE-3 the writer checks the bytes it writes: a Date in plugins is refused there too (43.320525ms)
✔ REQ-PLUG-1 REQ-ROLE-3 a refused setter write throws a code, changes nothing and logs nothing (1.478751ms)
✔ path and load (2.126696ms)
✔ refusals (1.211429ms)
✔ systemVars flattens the validated config (2.389598ms)
✔ precedence: system, business, project, project role, agent (5.905551ms)
✔ limits narrow the definition and never widen it (3.129494ms)
✔ role.launch stays within-role only for the instance the launch block names (8.910455ms)
✔ limits.authority without role.launch leaves the launcher with no launch block (1.857009ms)
✔ limits.authority narrows cross-role actions too (0.942924ms)
✔ classify (1.029295ms)
✔ the record carries what the broker and launcher need (1.020621ms)
✔ digest: key order doesn't matter, any value change does (12.557008ms)
✔ refusals (1.76834ms)
✔ the four shipped version 2 roles load (3.956353ms)
✔ shipped role scopes match addendum B section 2 and the SR runbook (1.002803ms)
✔ shipped authority follows the note's table (0.788725ms)
✔ every role file in roles/ loads through the validator, and a v2 contract exists (0.864638ms)
✔ version 1 files keep loading with no authority (0.991291ms)
✔ the conductor policy isn't a role (0.255038ms)
✔ a missing role file is exit 4, a symbolic link too (0.411478ms)
✔ version 2 refusals (1.580408ms)
✔ authority: closed vocabulary, no gated-only action, no overlap (4.291866ms)
✔ messaging: optional, role definitions and human, never the tester (3.978569ms)
✔ credentials: Gitea scopes (1.134703ms)
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (0.978627ms)
✔ credentials: services (1.028646ms)
✔ contract: a non-empty regular Markdown file beside the role file (0.577368ms)
✔ every key names known layers and a merge rule (1.217384ms)
✔ unknown keys and wrong layers refuse (0.988397ms)
✔ types (2.692195ms)
✔ merge: defaults, then the most specific layer wins (0.399674ms)
✔ merge: limits only narrow, and provenance lists each source (0.486127ms)
✔ merge doesn't change its inputs (0.199603ms)
ℹ tests 94
ℹ suites 0
ℹ pass 94
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1870.447171
@@ -0,0 +1,205 @@
✔ every screen and overlay fits 120x36, 80x24 and the narrow layout, row for row (1364.017051ms)
✔ width counts terminal cells: wide CJK and emoji 2, U+FE0F after a one-cell base 1, marks 0 (65.976651ms)
✔ broker text is shown, never interpreted: no raw control character or bidi override reaches the terminal (172.138841ms)
✔ NO_COLOR: no escape codes, except reverse video on the selected row (258.110966ms)
✔ 1-9 shows the decide flow's own lines and question; only y resolves (160.561325ms)
✔ the app shows the same decide text as mosaic decide (102.794665ms)
✔ any key but y or Y at the question answers No; nothing is sent (5841.69119ms)
✔ below 40x10 the question is hidden, so every key answers it No, y included (Filbert R3) (1107.295856ms)
✔ under 40 columns a tall terminal shows the too-small screen even where the decide box would fit; y sends nothing (Filbert N10) (5314.685083ms)
✔ a hidden decide box names the way out, and the status line under it does not promise y (Filbert N12) (237.684296ms)
✔ a throw while answering the question goes to crash, sends nothing and leaves no job pending (222.636126ms)
✔ type-ahead never answers the question: a paste, keys typed before it is drawn, keys inside askMs (1328.29647ms)
✔ quitting at the question answers No and sends nothing, armed or not (252.324795ms)
✔ a timer tick reads nothing under an overlay, never answers the question, and keeps a resolved status (163.617536ms)
✔ a long question is cut inside the question; the action, target, choice and prompt stay (Dewey B1) (200.538022ms)
✔ a decide box too big for the terminal says so, and every key answers it No, y included (Dewey B1, round 3) (1586.565804ms)
✔ at every size the decide box shows each line y depends on, or says it is hidden (9788.596137ms)
✔ outcome unknown: the CLI's sentence, no retry, then the trail or a re-read (159.055987ms)
✔ refused and failed reads keep the last list, marked stale; a refused decide says refused (119.898203ms)
✔ help or a detail box cut to fit keeps its last line, Esc included (Dewey) (58.677614ms)
✔ keys: views, moving, trail of a selection, the command line, help and quit (241.211176ms)
✔ one option: the status line says Press 1, not 1-1 (95.936149ms)
✔ no business: the app says so and :business picks one (50.977605ms)
✔ mosaic with no argument: exit 3 in an agent run, exit 4 with the usage off a terminal, the app on one (2.057316ms)
✔ scripts/mosaic with no argument reaches the app entry; the seat usage it printed before has not drifted (193.671353ms)
✔ strings to keep exactly live once, in the decide flow and the transport; the app does not restate them (0.496106ms)
✔ decide with --note still asks; a No sends nothing (60.165458ms)
✔ the app's business comes from a live bus host only (1.122893ms)
✔ runApp leaves the terminal as it found it on q, end of input, SIGTERM, SIGINT, SIGHUP and an error, the first frame's included (7.902052ms)
✔ draw is pure: same state, same frame (0.871989ms)
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (133.061887ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (126.886912ms)
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (125.322432ms)
✔ decide prints a declining choice as declining (158.493049ms)
✔ an unknown outcome is reported once and never resent (106.978329ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (109.571882ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (97.800115ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (128.393517ms)
✔ every human command refuses inside an agent run before it touches the bus (78.073171ms)
✔ usage errors exit 4; no business and no host is a usage error (104.236329ms)
✔ agents and tasks print through the broker (81.22942ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.191867ms)
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (73.54679ms)
✔ trackers come from the tracker.* variables of the one project that names a tracker project (93.482651ms)
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (45.150094ms)
✔ two projects that each name a tracker project refuse, since the boot shape holds one (34.333446ms)
✔ a business without tracker.baseUrl gets no trackers entry (44.935831ms)
✔ an unknown business and a broken system config refuse with exit 3 (70.05186ms)
✔ follow prints an unsolicited message after plain talk returned (189.331535ms)
✔ talk --follow continues after reply and prints a later unsolicited message (197.268409ms)
✔ inbox lists all unread human messages under decisions, does not deliver or read them (3021.260937ms)
✔ follow refuses agent context and bad arguments (1646.105202ms)
✔ follow Ctrl-C wakes the poll sleep and removes its handler; transport errors also clean up (887.503912ms)
✔ plain talk leaves the process SIGINT handlers unchanged (496.740045ms)
✔ empty views say so (1.04118ms)
✔ the trail keeps the broker's order and names a decision's task without its rows (1.493066ms)
✔ tasks print the tracker fields the snapshot carries (0.224693ms)
✔ the review loop: the coder pushes and asks, the reviewer fetches and approves, and the task's trail holds each step (768.886735ms)
✔ refusals: no action, non-fast-forward, an unknown commit or request, and a push for another task moves only that task's ref (606.148894ms)
✔ the size cap refuses a push before any byte (327.05699ms)
✔ a fetch over the cap refuses before any byte reaches the reviewer (570.788914ms)
✔ the socket refuses a request with no capability, an unknown one, and a bad request line (1045.39052ms)
✔ a rigged clone runs nothing: hooks, fsmonitor, a filter, a gitlink and a symlinked .git/config leave no marker (3266.584113ms)
✔ an identity with no role, or of another business, refuses not-a-role and moves no ref (1655.278286ms)
✔ a review of a project the business no longer has refuses unknown-project (706.842914ms)
✔ past maxActive connections the socket refuses git-busy, and a hung-up connection frees its slot (1110.933056ms)
✔ close() answers host-closing to a connection still sending its request line (2370.561835ms)
✔ a bundle that trickles in refuses body-timeout at its deadline and frees its slot (780.504558ms)
✔ a reviewer gone before its bundle was sent is filed fetch-aborted, not as the host's fault (1823.890766ms)
✔ a reviewer that hangs up the moment it has the whole bundle was served, not aborted (939.748661ms)
✔ send: a close after every byte left is served; one that fails a queued write is fetch-aborted (41.518892ms)
✔ a bundle with no bytes for bodyIdle refuses body-timeout long before its deadline (654.385778ms)
✔ send: a close once the loop is past its last chunk is served, and a failed write there is fetch-aborted (24.893563ms)
✔ letGo waits for an answer to leave for at most its cap, then destroys the socket (300.397999ms)
✔ a record that fails and can't be undone refuses rollback-failed, which the host logs (780.931141ms)
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (998.026376ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (238.124385ms)
✔ a second host for the same data root refuses with exit 3 while the first runs (181.022296ms)
✔ two bus starts racing for one data root leave one host running; the other refuses that a host already runs (595.743156ms)
✔ a host takes over a host.lock its dead host left, and puts back a live claim that replaced it meanwhile (1.329814ms)
✔ 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.840145ms)
✔ a live host's lock stays, however old the file is (0.522917ms)
✔ a host.lock left by a host killed with SIGKILL doesn't stop the next bus start (970.320334ms)
✔ while a host boots, a reader sees no host: the human commands still ask for --business, stop and status find none (224.309253ms)
✔ a host started before host.lock existed, holding only host.json, still refuses a second start, which leaves no lock (29.612913ms)
✔ a broker fork that throws leaves no host.lock and no host.json (36.442007ms)
✔ a broker send that throws leaves no host.lock and no host.json (33.384242ms)
✔ a broker that misses its boot wait leaves no host.lock and no host.json (31.009542ms)
✔ a notifier fork that throws stops the broker and leaves no host.lock and no host.json (1711.492607ms)
✔ a notifier send that throws stops the broker and leaves no host.lock and no host.json (908.308547ms)
✔ 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 (458.535458ms)
✔ 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 (905.162338ms)
✔ a host.json that can't be written stops both children and leaves no host.lock (584.555244ms)
✔ an error event on the broker before it replies fails the start at once, and leaves no host.lock and no live child (29.305611ms)
✔ an error event on the notifier before it replies fails the start at once, and leaves no host.lock and no live child (351.835373ms)
✔ an error event on the broker while the notifier starts fails the start once the notifier replies (337.549496ms)
✔ 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 (440.926525ms)
✔ 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 (662.469913ms)
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (2533.808577ms)
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (425.26081ms)
✔ a broker reply with no request waiting breaks the channel and the host exits 1 (194.516759ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (256.977925ms)
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (1084.747087ms)
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (324.384563ms)
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (292.064419ms)
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (237.549895ms)
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (276.045806ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (20.767382ms)
✔ bus stop refuses to signal a live pid that is not a bus host (202.658587ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (258.878462ms)
✔ bus start refuses with exit 3 without a notifier config (110.151859ms)
✔ bus start runs until bus stop; status reports it while it runs (748.718226ms)
✔ bus-service.sh renders the unit and installs it into a given directory (71.726734ms)
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (110.693638ms)
✔ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (13.765837ms)
✔ bus-service.sh refuses a default claude without the execute bit, or in a PATH dir with a space (8.187716ms)
✔ each launch rereads limits and model selections; lowered limits keep sessions and log exact hit (47.002731ms)
✔ bus start --pm: the launcher instance (the chief of staff) runs under its own PID namespace, registered, with a manifest (1421.024159ms)
✔ the chief of staff launches a coder through its launch tool; the coder answers; refusals name their code (811.234202ms)
✔ each launch makes sure the workspace has a clone of the project: cloned, fast-forwarded, or fetched and logged (3676.19452ms)
✔ a project clone that can't be made refuses checkout-failed, from --chief and from a session, with no run and no clone left (586.460661ms)
✔ a runner that stops at once ends its launch with the runner's reason (1380.089619ms)
✔ a Claude Code launch with no session credential refuses before it starts (359.901401ms)
✔ a pi launch needs no session credential: pi logs in through its own ENV_ALLOW variables (415.843604ms)
✔ a runner killed from the host ends its launch as killed, not failed (499.351173ms)
✔ a missing or failing bwrap refuses sandbox-unavailable before anything starts; there is no unconfined fallback (1054.81004ms)
✔ a real Claude Code coder's shell can't read a stand-in secret or anything else its sandbox doesn't hold (4011.206293ms)
✔ 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 (1221.277284ms)
✔ 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 (4116.503902ms)
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (370.677983ms)
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (143.744318ms)
✔ a launch client that never closes its side doesn't hold the host's close (157.97908ms)
✔ a runner that ignores SIGTERM is killed when the host closes (1317.272793ms)
✔ claudeVersion asks MOSAIC_CLAUDE_BIN when set, else claude on PATH (5.628727ms)
✔ migration needs no bus/config, prints whole converted document, is idempotent and never writes (19.66285ms)
✔ limits view/set use the human bus transport and pass only the explicit patch (2.839408ms)
✔ post-119 example and Jason fixture migrate with checked inherited models; wrong or absent config refuses (224.260529ms)
✔ zoned uses the IANA zone across DST (24.628343ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (131.23502ms)
✔ two blocking decisions get two DMs with different nonces (106.264808ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.268062ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (123.671996ms)
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (113.714897ms)
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (151.674449ms)
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (107.067682ms)
✔ 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 (170.401153ms)
✔ a restart after the second refusal does not send before that refusal's 30 min are up (173.889612ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (117.403576ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (81.23477ms)
✔ an inbox read failure is logged and the next poll retries (0.631322ms)
✔ no Discord id reaches the journal or the log (92.965177ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (33.12397ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (72.27281ms)
✔ the journal: a whole file that is one torn line truncates to empty (22.039262ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.770518ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.636292ms)
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (4.675189ms)
✔ the journal: a symlinked directory refuses and says it is a link (0.408283ms)
✔ 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.556141ms)
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.477775ms)
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.530099ms)
✔ digest content stays within Discord's 2000 characters (0.300721ms)
✔ runLoop never overlaps ticks and stops after the one in flight (111.962109ms)
✔ a human command refused with human-required prints the other-tree line and exits 3 (3.532632ms)
✔ a --tester command refused with tester-required prints the other-tree line and exits 3 (1.436433ms)
✔ other exit-3 refusals print no other-tree line (1.937615ms)
✔ the line names no path (0.12066ms)
✔ shared copy: npm ci, pushed reachability, readonly/existing copy, webui delegation (536.843393ms)
✔ wrapper preserves Q81 lease and marker, rejects foreign/symlink; check uses effective roots (450.439316ms)
✔ bus unit copy/pm pins, install refuses all busy states and failed reads, drop-in refusal (426.178133ms)
✔ missing package manifest refuses before npm; npm failure removes the unpublished temp copy (306.091597ms)
✔ real pinned install loads bus and Console; same-copy mosaic talk passes proof, other trees refuse (5796.802852ms)
✔ bus status reads effective copy/pm/drop-ins without writes; check refuses an overridden ExecStart (556.985502ms)
✔ drop-ins in either override field and malformed metadata refuse; uninstall also checks --no-reload (252.400725ms)
✔ install never shows a template name on systemd 261; template drop-ins still refuse (233.749948ms)
✔ unlisted instance drop-in is found in another unit lookup directory; path discovery errors refuse (211.742076ms)
✔ inherited mosaic-.service.d override refuses without changing the unit (252.722233ms)
✔ inherited service.d override refuses without changing the unit (292.582972ms)
✔ inherited drop-in unreadable directory or file refuses before writing (469.374983ms)
✔ talk --tester sends a [mos test] message as tester from inside an agent and prints the role's reply (149.959804ms)
✔ talk --tester refuses text without the [mos test] prefix before any call (112.568189ms)
✔ the read verbs take --tester inside an agent, and the trail shows the tester as the sender (120.519064ms)
✔ decide, stop and launches take no --tester (136.541659ms)
✔ the tester transport runs tester-cli.mjs, and the tester's refusals exit 3 (0.373784ms)
✔ with no stand-in transport, --tester runs tester-cli.mjs on the data root's socket (0.539558ms)
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 (438.670817ms)
✔ the transport writes {business, verb, args} to the child and reads its JSON (47.852519ms)
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2183.022649ms)
✔ busExit and refuseInsideAgent (0.505998ms)
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (231.745694ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1174.729918ms)
✔ a late reply the Console has listed still reaches the next talk (2387.24051ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (1398.541714ms)
✔ launches off and on go to the broker and change the business's launch state (1001.038436ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (144.713347ms)
ℹ tests 195
ℹ suites 0
ℹ pass 195
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 29322.552854
@@ -0,0 +1,105 @@
✔ the boot config is checked before anything starts (95.150958ms)
✔ a business with no tracker entry refuses task verbs (147.778917ms)
✔ credential.expiring and .expired are recorded once per instance (210.271363ms)
✔ a token file that changes on disk records credential.changed (119.268009ms)
✔ autostart polls, reconciles and retries a startup the tracker was down for (148.539489ms)
✔ a refusal a restart must clear is not retried by the poll (88.811373ms)
✔ a poll that fires while two are queued is dropped (96.740653ms)
✔ close waits for a running verb and refuses one that has not started (200.953806ms)
✔ stopped bus resumes an empty task cursor after a long outage, exactly once (174.48571ms)
✔ persisted IDs avoid replay, exclude bots and bound rows to observed new comments (208.848131ms)
✔ commit followed by crash before cache advance neither loses nor duplicates a comment (152.320267ms)
✔ failed event insert rolls back cursor and a retry still emits the comment once (151.580949ms)
✔ initial cursor accepts a comment within the stated one-minute clock skew margin (109.043238ms)
✔ the bundled Vikunja is the pinned upstream image the runbook names (0.742533ms)
✔ every published port is on 127.0.0.1, and no secret is in the file (0.273347ms)
✔ the committed table loads frozen; its verbs are the broker task verbs (1.993945ms)
✔ a write names a verb the table has as its own key, not one it inherits (0.504028ms)
✔ the schema refuses any other shape (2.085773ms)
✔ the Field table in the slice 1 plan names the verbs each field is written by (0.607781ms)
✔ each verb refuses, asks for a decision and writes as the table and the role files say (1195.638829ms)
✔ the list holds the PRD's requirement ids, in its order, at its version (1.662315ms)
✔ the committed list loads frozen (0.792126ms)
✔ the schema refuses any other shape (2.93181ms)
✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (377.29918ms)
✔ the project list holds recorded project objects in the recorded list envelope (98.121298ms)
✔ the recorded task bodies pass the checks S3 applies to every read (0.58043ms)
✔ the client works against the fake over real HTTP with the platform fetch (247.020943ms)
✔ a correct install starts, and the first reconcile records tasks that already exist (115.92611ms)
✔ verbs refuse while a business is starting and after startup refused it (147.002066ms)
✔ startup refuses a token that can do more than its role needs (406.014849ms)
✔ startup refuses an unsupported version and flags an untested one (284.43309ms)
✔ startup refuses a board that the runbook did not install (358.735865ms)
✔ startup refuses a project the sync bot cannot read (122.263899ms)
✔ startup refuses a configured label the pm bot cannot see (75.505966ms)
✔ startup refuses an expired credential and a missing sync credential (168.772257ms)
✔ an unreachable tracker refuses with tracker-unavailable (74.380089ms)
✔ an edit in the UI is recorded once, with the fields that changed (230.176596ms)
✔ a move between open buckets is seen on the board, though updated does not change (194.627547ms)
✔ a person's comment is counted and a bot's is not (284.187574ms)
✔ the hourly reconcile catches a comment through comment_count (210.827771ms)
✔ a task closed in the UI leaves the open view with its done bucket (377.720106ms)
✔ a task that leaves the board is recorded as deleted, moved or out of reach (290.27356ms)
✔ a poll that read before a verb wrote does not overwrite the verb (181.37352ms)
✔ a tracker fault during a tick is reported and the next tick catches up (249.652124ms)
✔ a malformed answer refuses the tick with tracker-shape (245.922418ms)
✔ no token value reaches the database, the log or a refusal (272.003151ms)
✔ the first look at a task counts only comments inside the window (133.026754ms)
✔ with descendants off the adapter reads no project list and no child, as in v0 (130.628133ms)
✔ a three-level tree is read per project, and a project outside it is not (130.748861ms)
✔ a watch-only child keeps the business ready, and a failed child read refuses as transient (229.181735ms)
✔ a root without the installed buckets refuses with descendants on (100.118718ms)
✔ the project list refuses without read_all, and skips archived projects and saved filters (198.449896ms)
✔ descendants must be true or false (102.791351ms)
✔ a move inside the tree is a moved tombstone plus a snapshot under the new ref (162.864531ms)
✔ a child made after startup joins the tree at the next reconcile (148.630987ms)
✔ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history (171.617726ms)
✔ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses (231.603919ms)
✔ a reconcile that fails keeps the previous tree (123.452911ms)
✔ a closed task the cursor missed takes its own project's done bucket (168.827598ms)
✔ closed tasks read at startup take their own project's done bucket (123.355676ms)
✔ a closed task the cursor returns takes its own project's done bucket (209.811293ms)
✔ a task moved out of a child that leaves the tree at the same time is moved, not no-access (139.170175ms)
✔ a task in a bucket outside the five keeps its raw bucket id (108.468335ms)
✔ bucket 0 on a watch-only task never becomes a bucket or a state (192.41375ms)
✔ a child whose kanban view is deleted still refuses the tick, and the reconcile reads it watch-only (140.175255ms)
✔ a child dropped at a tick comes back at the next reconcile once the bot reads it again (121.415803ms)
✔ task.create needs a recorded human request and a requirement id (200.558595ms)
✔ task.create takes only an approved requirement id (226.004101ms)
✔ task.create cites the request a received message carries, not the message's id (176.495674ms)
✔ only labels named in the business file can be written (216.978714ms)
✔ task.schedule sets and clears a due date and relations (262.197532ms)
✔ assign and reassign move the role bots and record task.assigned (344.602936ms)
✔ task.update.assigned is for the assignee and records task.state (283.754251ms)
✔ a wrong expected digest records task.conflict and writes nothing (258.577537ms)
✔ a cross-role verb needs a resolved decision, used once (355.255274ms)
✔ task.close needs a verdict; after it every verb refuses with task-done (214.716897ms)
✔ a task with pushed work closes only citing an approve verdict for its latest push, and a refusal writes nothing (251.405159ms)
✔ a lost answer is settled by a re-read and never retried (187.442861ms)
✔ a create whose answer is lost is reported uncertain, and the poll finds the task (109.562266ms)
✔ a task the sync bot cannot read refuses and records nothing (115.123529ms)
✔ verbs and polls for one business run one at a time (168.090647ms)
✔ a due date with milliseconds is written to the second (162.625583ms)
✔ every write landed and the final read failed: the verb succeeds and records what it wrote (107.693187ms)
✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (79.159686ms)
✔ a create whose final read fails succeeds and records task.created (91.077312ms)
✔ an edit between the last write and the final read shows as external on the next poll (105.652412ms)
✔ task.created is recorded when a later label write fails (72.97385ms)
✔ every verb acts in an installed child, on the child's own board (251.957434ms)
✔ task.create refuses a project outside the tree, a watch-only one and one that is not an id (178.813371ms)
✔ with descendants off, task.create takes only the root project (79.524507ms)
✔ a verb on a watch-only task, or a relation to one, refuses with project-not-installed (115.193262ms)
✔ a related task is read before authority: in another project than its ref names, task-moved; missing, task-not-found (143.102405ms)
✔ a task moved to another project of the tree refuses its old ref with task-moved and takes its new one (93.986445ms)
✔ a verb and a create in the child record the final read of the child's board (89.135076ms)
✔ a create in the child whose final read fails records the child's todo bucket (98.044916ms)
✔ a lost answer in the child is settled by a re-read of the child's board (122.161564ms)
✔ every related task is read before authority, not only the first (99.675116ms)
ℹ tests 97
ℹ suites 0
ℹ pass 97
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5364.374342
@@ -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 (6984.651063ms)
✔ 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 (7419.909201ms)
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (4939.365815ms)
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (6543.698363ms)
✔ the README says a refused field table names the file or the key at fault, never a path (1.450318ms)
✔ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes (8186.794585ms)
✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (377.258733ms)
✔ decisions route serves the roles and recent lists from the reader view (1457.568745ms)
✔ credentials route serves the broker credential status from the reader view (713.600193ms)
✔ replies route serves the replies to one message from the reader view (2147.67311ms)
✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (61.305438ms)
✔ bus refusals map to statuses and never carry a path or a stack (43.592581ms)
✔ humanCall speaks the human transport protocol without blocking the server (1709.035695ms)
✔ busReader takes --business, else the live bus host, else refuses (321.117357ms)
✔ humanCall kills a transport that runs past its timeout (588.987138ms)
✔ serve refuses a --business value that is not a business id (375.303173ms)
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (4942.264616ms)
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (3959.376951ms)
✔ conversation view: a newer session with no readable history keeps the marker (2054.379985ms)
✔ conversation view: seats without history say so and offer no reply (1818.132509ms)
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (5519.521298ms)
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (54462.046654ms)
✔ messages HTTP read pages and preserves metadata; bad arguments refuse before a read; other reads unchanged (408.514752ms)
✔ 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 (10990.097586ms)
✔ Messages nav count polls on Tasks and Board and clears on a Board refusal; messages 403 refuses local pages (3468.300028ms)
✔ copy writes the Console paths at the full id, read-only, and leaves an existing copy alone (3486.496558ms)
✔ copy refuses a commit off refactor, an unknown one, a worktree and a bad copies directory (6432.28271ms)
✔ copy writes nothing when an archived path is missing at the commit (963.076791ms)
✔ a copy that loses the rename race leaves no read-only temp directory behind (555.742127ms)
✔ a copy whose rename fails with no target refuses, writes no copy and leaves no temp directory (846.498058ms)
✔ a copy that meets a target holding files says exists: and moves nothing into it (798.148065ms)
✔ a copy that meets an empty target directory replaces it by rename, not by moving into it (1013.109203ms)
✔ serve runs from a copy: /healthz 200, the config check, roles and RELEASE from the copy, the queue from --checkout (1453.57591ms)
✔ serve refuses a --checkout that is not an absolute directory path (300.947034ms)
✔ consoleReads runs the queue read under queueRoot and reads RELEASE under root (74.136376ms)
✔ preview options are all three or none, and each is checked (10.668666ms)
✔ the CLI refuses a partial or invalid preview flag set at start (1912.638001ms)
✔ the CLI with all three flags serves the preview mode (538.852846ms)
✔ a request for the public name needs the header equal to the user id, else 401 and nothing read (90.64865ms)
✔ the header name matches without regard to case, and the value compare is exact (4.717085ms)
✔ the public name with the header reads the bus views, and the Host and Origin are checked first (85.606739ms)
✔ in preview mode every write answers 403 read-only, from the public name and loopback, and reaches nothing (33.680139ms)
✔ loopback requests are unchanged in preview mode, and the bus views work with the board down (13.674924ms)
✔ a proxy that rewrites Host to the loopback port gets 403 on its first request, whatever the user (78.430613ms)
✔ a repeated Host is 400 before any other check, in preview mode and without it (22.40447ms)
✔ without the preview flags the public name is refused and writes still reach the board (10.283801ms)
✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (2.369478ms)
✔ credential state from the date alone, as the bus states it (0.381639ms)
✔ queue: rows, one row, ids and methods, notes; no path or id from a row (1685.936496ms)
✔ queue: a refused read fails closed with the store's text, redacted (2129.975327ms)
✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (4629.974148ms)
✔ business: names, vars on the allowlist, authority, credential metadata only (24.924413ms)
✔ business: missing or invalid file refuses with the module's text, redacted (23.303061ms)
✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (13.042748ms)
✔ settings: release, board, loopback address, notifier binding name only (503.313611ms)
✔ task fields: the committed table with no reads configured, GET only; a bad table refuses with no path (182.714819ms)
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (5314.592429ms)
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (23049.807417ms)
✔ 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 (7206.545526ms)
✔ a sent receipt lists its replies as the page refreshes, oldest first and capped, and marks none delivered or read (13824.397353ms)
✔ startServer takes send only as a function and only with the preview mode (5.442663ms)
✔ the CLI refuses --send without the preview flags, and without a configured bus (575.832346ms)
✔ the CLI with --send and a configured bus sends through the human transport and logs one line (131.516228ms)
✔ without --send the route is read-only like every other write, and config says no send (6.328077ms)
✔ with --send every other write is still 403 read-only, and nothing reaches the board or bus (4.978663ms)
✔ config says send only on the public name with --send on (1.662435ms)
✔ a send without the signed-in user is 401 and is neither sent nor logged, loopback included (3.605279ms)
✔ each request check refuses before anything is sent, and logs the refusal without the body (11.269193ms)
✔ a send passes exactly to, body and in_reply_to, answers the id, and logs it (2.507647ms)
✔ a crafted role name cannot forge a log line (1.811582ms)
✔ a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried (5.921987ms)
✔ through the human transport the request is exactly the business, message.send and the keys (79.130848ms)
✔ against a real broker: the message is written once as a human send, and its refusals map (1430.771786ms)
✔ loopback host and board origin fail closed (7.896912ms)
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (176.519679ms)
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (60.016895ms)
✔ unreachable board reports URL; CLI rejects unsupported options (461.016768ms)
✔ render prints the unit without the preview flags (51.553664ms)
✔ render carries every serve flag, the preview set included (10.581437ms)
✔ a partial preview set is refused (18.626088ms)
✔ a value the unit cannot carry is refused, for every flag (358.047032ms)
✔ the rendered unit names none of the human proof agent markers and sets no agent variable (176.877832ms)
✔ install into --dir with --no-reload writes the file whole, then reports unchanged (274.139974ms)
✔ install without --no-reload runs daemon-reload and nothing that starts the unit (115.06996ms)
✔ uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not (263.072908ms)
✔ status prints the unit state and GET /healthz on the installed port (310.318943ms)
✔ usage: an unknown verb or flag, and start or enable, exit 4 (96.092463ms)
✔ without --send, render is the template with its placeholders filled, byte for byte (130.581293ms)
✔ render --send with the preview set ends ExecStart with --send and changes nothing else (73.621429ms)
✔ --send without the whole preview set is refused and writes nothing (253.909184ms)
✔ install --send into --dir with --no-reload writes the sending unit, and status says whether it sends (465.359566ms)
✔ uninstall and status take no --send (48.904336ms)
✔ install refuses while the unit is running, in any state but inactive or failed, and changes nothing (465.92717ms)
✔ when the state read fails, install and uninstall refuse with one line and change nothing (109.702771ms)
✔ install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree (112.422827ms)
✔ install refuses unless the bus unit runs from the same copy, naming both paths, and changes nothing (541.434407ms)
✔ render --copy runs the copy and reads the queue in the checkout; without --copy the unit is unchanged (29.544547ms)
✔ the uid header takes only an Authentik uid; the username header still takes a username (376.363788ms)
✔ a preview value cannot start with - or . (78.284129ms)
✔ status names the copy and whether refactor has moved past it, and says when the unit runs from no copy (554.415861ms)
✔ Settings form shows every scope and held role; gated save, exact payload, retained draft and no automatic retry (3280.601834ms)
2026-10-15T17:56:19.837Z settings user="jason" changed
✔ REQ-WEB-1 signed-in same-origin Settings write reaches exactly one human operation (18.016521ms)
✔ REQ-WEB-1 Settings writes require enabled send, named Host, identity, exact Origin and same-origin Fetch Metadata (6.995485ms)
✔ REQ-WEB-1 Settings validates JSON, size, revision and strict writable keys before broker call (5.158833ms)
✔ tokens.css and icons.svg are byte copies of docs/design (1.435703ms)
✔ JetBrains Mono files match the digests in their sources file, with the OFL text (2.740229ms)
✔ shipped strings are unchanged, and the web keeps its write boundary (2.985481ms)
✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (7573.780732ms)
✔ five states on Board, Inbox, Tasks, Agents and Trail (3470.266117ms)
✔ Queue, Business and Settings in their five states, with no move button (5173.10096ms)
✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (2777.138713ms)
✔ the server log names no config path when the system config refuses (378.013246ms)
ℹ tests 112
ℹ suites 0
ℹ pass 112
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 54943.288737
@@ -0,0 +1,10 @@
mutants start 2026-10-15T17:55:54Z
control-01 packages/business/tests/assistant.test.mjs PASS 0
control-02 packages/bus/tests/decision-route.test.mjs PASS 0
01-validator-check KILLED 1
02-raise-check KILLED 1
03-raise-direction KILLED 1
04-within-role-exempt KILLED 1
05-raise-own-code KILLED 1
mutants end 2026-10-15T17:55:58Z
RESTORED_MATCHES_MANIFEST
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Row 133 (#1627) round 2, on 89fd2d6fe: the round 1 faults, unchanged. One fault per check. Sequential process groups; whole-group timeout cleanup.
--dry checks every mutation site matches and changes source, and runs nothing.
A control run first runs each distinct test set on the unmutated source; any failure stops the run."""
import json,os,pathlib,signal,subprocess,sys
args=[a for a in sys.argv[1:] if a!='--dry'];DRY='--dry' in sys.argv
ROOT=pathlib.Path(args[0] if args else os.path.expanduser('~/dewey-scratch/r133/wt2'))
OUT=pathlib.Path(__file__).resolve().parent/'mutants'
AT='packages/business/tests/assistant.test.mjs';RT='packages/bus/tests/decision-route.test.mjs'
cases=[]
def add(id,path,old,new,test):cases.append(dict(id=id,path=path,old=old,new=new,test=test if isinstance(test,list) else [test]))
add('01-validator-check','packages/business/src/business.mjs','if (definitions[definition].messaging) {','if (false) {',AT)
p='packages/bus/src/broker.mjs'
RAISE="if (cls !== 'within-role') this.#checkMessaging(s.business, route, s.role, 'decision-route-refused');"
add('02-raise-check',p,RAISE,'/* no route check */',RT)
add('03-raise-direction',p,RAISE,"if (cls !== 'within-role') this.#checkMessaging(s.business, s.role, route, 'decision-route-refused');",RT)
add('04-within-role-exempt',p,RAISE,"this.#checkMessaging(s.business, route, s.role, 'decision-route-refused');",RT)
add('05-raise-own-code',p,RAISE,"if (cls !== 'within-role') this.#checkMessaging(s.business, route, s.role);",RT)
def run(name,test):
env={**os.environ,'DOCKER_HOST':'unix:///nonexistent.sock'}
with (OUT/(name+'.log')).open('w') as log:
p=subprocess.Popen(['node','--test',*test],cwd=ROOT,env=env,stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
try:return p.wait(timeout=300)
except subprocess.TimeoutExpired:os.killpg(p.pid,signal.SIGKILL);p.wait();return 'timeout'
results=[];control=[]
if not DRY:
OUT.mkdir(exist_ok=True)
sets=[];[sets.append(c['test']) for c in cases if c['test'] not in sets]
for i,t in enumerate(sets):
rc=run('control-%02d'%(i+1),t);control.append(dict(id='control-%02d'%(i+1),test=t,exit=rc));print('control-%02d'%(i+1),' '.join(t),'PASS' if rc==0 else 'FAIL',rc,flush=True)
if rc!=0:(OUT/'control.json').write_text(json.dumps(control,indent=2)+'\n');sys.exit('control failed: unmutated source fails '+' '.join(t))
(OUT/'control.json').write_text(json.dumps(control,indent=2)+'\n')
for c in cases:
file=ROOT/c['path'];before=file.read_text();matches=before.count(c['old'])
if not matches:raise RuntimeError('missing mutation site '+c['id'])
after=before.replace(c['old'],c['new'])
if after==before:raise RuntimeError('no change '+c['id'])
if DRY:print(c['id'],'sites',matches,flush=True);continue
file.write_text(after)
try:
rc=run(c['id'],c['test'])
verdict='KILLED' if isinstance(rc,int) and rc!=0 else 'SURVIVED' if rc==0 else 'TIMEOUT'
row={k:c[k] for k in ['id','path','test']};row.update(result=verdict,exit=rc,sites=matches);results.append(row);print(c['id'],verdict,rc,flush=True)
finally:file.write_text(before)
if not DRY:(OUT/'results.json').write_text(json.dumps(results,indent=2)+'\n')
@@ -0,0 +1,38 @@
✔ the role file loads and resolves through loadBusiness (4.618509ms)
✔ the shipped file is exactly the approved list (0.566543ms)
✔ the assistant's authority is message.send within role and nothing else (1.375778ms)
✔ the assistant talks to the user and the chief of staff only, and no other shipped role is limited (0.462945ms)
✔ a business grant through limits does not widen the role (1.119217ms)
✔ a launch block whose by is the assistant is refused (1.177881ms)
✔ the instance takes no credential references and no tracker (1.799258ms)
✖ an arbiter whose role definition declares messaging lists is refused, for each kind (1.236465ms)
✔ an arbiter without messaging lists is accepted beside the limited assistant (1.507058ms)
✔ the contract passes work to the chief of staff and directs no one (0.492763ms)
ℹ tests 10
ℹ suites 0
ℹ pass 9
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 65.22936
✖ failing tests:
test at packages/business/tests/assistant.test.mjs:127:1
✖ an arbiter whose role definition declares messaging lists is refused, for each kind (1.236465ms)
AssertionError [ERR_ASSERTION]: Missing expected exception.
at refuses (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/business/tests/assistant.test.mjs:27:10)
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/business/tests/assistant.test.mjs:129:5)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: undefined,
operator: 'throws',
diff: 'simple'
}
@@ -0,0 +1,67 @@
✖ a cross-role raise whose arbiter the raiser does not hear from is refused and stores nothing (110.136396ms)
✖ a gated raise by a role that does not hear from the human is refused and stores nothing (109.141302ms)
✔ a route the raiser hears from is allowed, checked from the route to the raiser (135.078924ms)
✖ a refused supersede withdraws nothing (112.266739ms)
✔ the assistant's within-role and gated raises still work (117.591ms)
ℹ tests 5
ℹ suites 0
ℹ pass 2
ℹ fail 3
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 640.365102
✖ failing tests:
test at packages/bus/tests/decision-route.test.mjs:79:1
✖ a cross-role raise whose arbiter the raiser does not hear from is refused and stores nothing (110.136396ms)
AssertionError [ERR_ASSERTION]: Missing expected exception.
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:83: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: false,
code: 'ERR_ASSERTION',
actual: undefined,
expected: { code: 'decision-route-refused' },
operator: 'throws',
diff: 'simple'
}
test at packages/bus/tests/decision-route.test.mjs:89:1
✖ a gated raise by a role that does not hear from the human is refused and stores nothing (109.141302ms)
AssertionError [ERR_ASSERTION]: Missing expected exception.
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:93: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: false,
code: 'ERR_ASSERTION',
actual: undefined,
expected: { code: 'decision-route-refused' },
operator: 'throws',
diff: 'simple'
}
test at packages/bus/tests/decision-route.test.mjs:118:1
✖ a refused supersede withdraws nothing (112.266739ms)
AssertionError [ERR_ASSERTION]: Missing expected exception.
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:121:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: undefined,
expected: { code: 'decision-route-refused' },
operator: 'throws',
diff: 'simple'
}
@@ -0,0 +1,83 @@
✖ a cross-role raise whose arbiter the raiser does not hear from is refused and stores nothing (112.566466ms)
✖ a gated raise by a role that does not hear from the human is refused and stores nothing (113.085972ms)
✖ a route the raiser hears from is allowed, checked from the route to the raiser (112.523173ms)
✖ a refused supersede withdraws nothing (101.518961ms)
✔ the assistant's within-role and gated raises still work (117.414976ms)
ℹ tests 5
ℹ suites 0
ℹ pass 1
ℹ fail 4
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 611.050195
✖ failing tests:
test at packages/bus/tests/decision-route.test.mjs:79:1
✖ a cross-role raise whose arbiter the raiser does not hear from is refused and stores nothing (112.566466ms)
AssertionError [ERR_ASSERTION]: Missing expected exception.
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:83: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: false,
code: 'ERR_ASSERTION',
actual: undefined,
expected: { code: 'decision-route-refused' },
operator: 'throws',
diff: 'simple'
}
test at packages/bus/tests/decision-route.test.mjs:89:1
✖ a gated raise by a role that does not hear from the human is refused and stores nothing (113.085972ms)
AssertionError [ERR_ASSERTION]: Missing expected exception.
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:93: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: false,
code: 'ERR_ASSERTION',
actual: undefined,
expected: { code: 'decision-route-refused' },
operator: 'throws',
diff: 'simple'
}
test at packages/bus/tests/decision-route.test.mjs:100:1
✖ a route the raiser hears from is allowed, checked from the route to the raiser (112.523173ms)
Error: decision-route-refused
at fail (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:78:9)
at #checkMessaging (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:347:48)
at #dispatch (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:898:56)
at file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:767:38
at Store.transaction (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/store.mjs:217:22)
at Broker.request (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:761:26)
at call (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:62:44)
at raise (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:64:5)
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:107:5)
at Test.runInAsyncScope (node:async_hooks:226:14) {
code: 'decision-route-refused'
}
test at packages/bus/tests/decision-route.test.mjs:118:1
✖ a refused supersede withdraws nothing (101.518961ms)
AssertionError [ERR_ASSERTION]: Missing expected exception.
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:121:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: undefined,
expected: { code: 'decision-route-refused' },
operator: 'throws',
diff: 'simple'
}
@@ -0,0 +1,31 @@
✔ a cross-role raise whose arbiter the raiser does not hear from is refused and stores nothing (115.210216ms)
✔ a gated raise by a role that does not hear from the human is refused and stores nothing (116.29138ms)
✔ a route the raiser hears from is allowed, checked from the route to the raiser (144.335468ms)
✔ a refused supersede withdraws nothing (141.877157ms)
✖ the assistant's within-role and gated raises still work (143.489645ms)
ℹ tests 5
ℹ suites 0
ℹ pass 4
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 717.809088
✖ failing tests:
test at packages/bus/tests/decision-route.test.mjs:129:1
✖ the assistant's within-role and gated raises still work (143.489645ms)
Error: decision-route-refused
at fail (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:78:9)
at #checkMessaging (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:347:48)
at #dispatch (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:898:29)
at file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:767:38
at Store.transaction (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/store.mjs:217:22)
at Broker.request (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:761:26)
at call (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:62:44)
at raise (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:64:5)
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:132:18)
at Test.runInAsyncScope (node:async_hooks:226:14) {
code: 'decision-route-refused'
}
@@ -0,0 +1,118 @@
✖ a cross-role raise whose arbiter the raiser does not hear from is refused and stores nothing (122.201697ms)
✖ a gated raise by a role that does not hear from the human is refused and stores nothing (111.471897ms)
✔ a route the raiser hears from is allowed, checked from the route to the raiser (141.714175ms)
✖ a refused supersede withdraws nothing (115.463772ms)
✔ the assistant's within-role and gated raises still work (125.245115ms)
ℹ tests 5
ℹ suites 0
ℹ pass 2
ℹ fail 3
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 671.538877
✖ failing tests:
test at packages/bus/tests/decision-route.test.mjs:79:1
✖ a cross-role raise whose arbiter the raiser does not hear from is refused and stores nothing (122.201697ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
Comparison {
+ code: 'message-sender-refused'
- code: 'decision-route-refused'
}
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:83: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: Error: message-sender-refused
at fail (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:78:9)
at #checkMessaging (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:349:63)
at #dispatch (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:898:56)
at file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:767:38
at Store.transaction (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/store.mjs:217:22)
at Broker.request (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:761:26)
at call (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:62:44)
at raise (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:64:5)
at assert.throws.code (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:83:23)
at getActual (node:assert:580:5),
expected: { code: 'decision-route-refused' },
operator: 'throws',
diff: 'simple'
}
test at packages/bus/tests/decision-route.test.mjs:89:1
✖ a gated raise by a role that does not hear from the human is refused and stores nothing (111.471897ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
Comparison {
+ code: 'message-sender-refused'
- code: 'decision-route-refused'
}
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:93: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: Error: message-sender-refused
at fail (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:78:9)
at #checkMessaging (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:349:63)
at #dispatch (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:898:56)
at file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:767:38
at Store.transaction (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/store.mjs:217:22)
at Broker.request (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:761:26)
at call (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:62:44)
at raise (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:64:5)
at assert.throws.code (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:93:23)
at getActual (node:assert:580:5),
expected: { code: 'decision-route-refused' },
operator: 'throws',
diff: 'simple'
}
test at packages/bus/tests/decision-route.test.mjs:118:1
✖ a refused supersede withdraws nothing (115.463772ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
Comparison {
+ code: 'message-sender-refused'
- code: 'decision-route-refused'
}
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:121: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: Error: message-sender-refused
at fail (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:78:9)
at #checkMessaging (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:349:63)
at #dispatch (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:898:56)
at file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:767:38
at Store.transaction (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/store.mjs:217:22)
at Broker.request (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/src/broker.mjs:761:26)
at call (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:62:44)
at raise (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:64:5)
at assert.throws.code (file:///home/jwoltje/dewey-scratch/r133/wt2/packages/bus/tests/decision-route.test.mjs:122:11)
at getActual (node:assert:580:5),
expected: { code: 'decision-route-refused' },
operator: 'throws',
diff: 'simple'
}
@@ -0,0 +1,18 @@
✔ the role file loads and resolves through loadBusiness (5.052586ms)
✔ the shipped file is exactly the approved list (0.607094ms)
✔ the assistant's authority is message.send within role and nothing else (1.467985ms)
✔ the assistant talks to the user and the chief of staff only, and no other shipped role is limited (0.534847ms)
✔ a business grant through limits does not widen the role (1.240006ms)
✔ a launch block whose by is the assistant is refused (1.276913ms)
✔ the instance takes no credential references and no tracker (2.219511ms)
✔ an arbiter whose role definition declares messaging lists is refused, for each kind (2.711145ms)
✔ an arbiter without messaging lists is accepted beside the limited assistant (1.481768ms)
✔ the contract passes work to the chief of staff and directs no one (0.49554ms)
ℹ tests 10
ℹ suites 0
ℹ pass 10
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 76.21339
@@ -0,0 +1,13 @@
✔ a cross-role raise whose arbiter the raiser does not hear from is refused and stores nothing (136.749258ms)
✔ a gated raise by a role that does not hear from the human is refused and stores nothing (107.901281ms)
✔ a route the raiser hears from is allowed, checked from the route to the raiser (133.249357ms)
✔ a refused supersede withdraws nothing (112.472722ms)
✔ the assistant's within-role and gated raises still work (122.756139ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 668.529803
@@ -0,0 +1,16 @@
[
{
"id": "control-01",
"test": [
"packages/business/tests/assistant.test.mjs"
],
"exit": 0
},
{
"id": "control-02",
"test": [
"packages/bus/tests/decision-route.test.mjs"
],
"exit": 0
}
]
@@ -0,0 +1,52 @@
[
{
"id": "01-validator-check",
"path": "packages/business/src/business.mjs",
"test": [
"packages/business/tests/assistant.test.mjs"
],
"result": "KILLED",
"exit": 1,
"sites": 1
},
{
"id": "02-raise-check",
"path": "packages/bus/src/broker.mjs",
"test": [
"packages/bus/tests/decision-route.test.mjs"
],
"result": "KILLED",
"exit": 1,
"sites": 1
},
{
"id": "03-raise-direction",
"path": "packages/bus/src/broker.mjs",
"test": [
"packages/bus/tests/decision-route.test.mjs"
],
"result": "KILLED",
"exit": 1,
"sites": 1
},
{
"id": "04-within-role-exempt",
"path": "packages/bus/src/broker.mjs",
"test": [
"packages/bus/tests/decision-route.test.mjs"
],
"result": "KILLED",
"exit": 1,
"sites": 1
},
{
"id": "05-raise-own-code",
"path": "packages/bus/src/broker.mjs",
"test": [
"packages/bus/tests/decision-route.test.mjs"
],
"result": "KILLED",
"exit": 1,
"sites": 1
}
]
@@ -0,0 +1,25 @@
333f3dabcbf1d578b26fde90fd00536218c898b37898e150dbcb44b11694a086 BUILD.md
bf8c27d55fc8cd5c505ad5f570286173f26da9f85efb9ef11eedc78e4f0e6449 MUTANTS.md
4af3e4729ae3f6fadb35b97a7aa53a15e2468ce613efa173f3ae2a7c7d8cf859 base.txt
e6405273fc96b4875562abee906afb7da2c1fa4495aab34e2e703b2db7289880 build-patch-interdiff.diff
393d82c61a41c624c9cc689b59e5bed5ca070e9d3e1bf1d6b5acc03b6e669906 build.patch
1c0e6f8f260ae51b31c2235193c36ed1c005faefe317e1452a351a3bb5690ef0 candidate-manifest.sha256
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 delta.patch
77467d51bbc12d079747495fa15e782f3d9421dbf24dc8b3ae123b29ec4769d4 gate-run.log
aaf3fff484c745e5e70b15470999f06477f36da5cd5c131bb9000ca78897bc23 gate.sh
9f8203cba29dead408b39ea3d72ea7a66d7a5c2935189ec59b2f508bd7c118cb gate/pkg-bus.log
76b751250aafab27e957d70cf7e454e805bf746866a62bf47b4c072c5c9969b6 gate/pkg-business.log
c227562d1ec6e862c01ab7f9d551b30c0652305121e09bfeac199d8f74d9f7e4 gate/pkg-cli.log
048330c01faa7cd81fe110e0dcb9932a19c0be673b97f2558827855620aa0c5a gate/pkg-tasks.log
6f5f55d1c43b7c4a4b651367516d532ddf031b94b8c848a417a3f9634656074e gate/pkg-webui.log
a610c6acc736add352c9491bb7c2d5dfb41bf9535655ab56df0b9706c230338c mutants-run.log
c30717f9740d05c69a5a2a23fe5e5f3590d2b1d4f8719b0d30ea94cbb668bde4 mutants.py
a4a06dae2719a48e98ceb62c0e6a0c976ca4cf4a810750b7e10b158fe4390850 mutants/01-validator-check.log
871133175b69e7faec5ae4d1b18e692eaa4f38f9e16e12f8494672dcb83720e1 mutants/02-raise-check.log
54f7f7fadd9279983d8a3af7d4da25714a23dcc81ae5e454fd52fa25144fee8c mutants/03-raise-direction.log
2aa78b58843ec17444d252c99d051655e9475f0be8c3c3a8f95c68eeb47f790a mutants/04-within-role-exempt.log
b851521a62f7a4368a4f7a6c96407b0bf3a55d4c4bde2effa19183d95d0dc905 mutants/05-raise-own-code.log
c5c9e761224a40130b6a85e3e061fa02dda4421aa5d94924bdb89c9a82685d8d mutants/control-01.log
2d1733f1198a6ba173b693a7cc1f359be2a6796f2bcdfd7977ef0aece4193ea3 mutants/control-02.log
36349265ed1d31d048209fb2408f91264f8fffec23202fc00a47a6b6f84e0526 mutants/control.json
c152e771d671ffdf8d99428876ca3eb28ac78ec894546dbed36ed3a3ba466550 mutants/results.json