docs: Dewey row 133 round 1 packet (#1627, decision closes and messaging lists)
Validator refuses a messaging-limited arbiter; decision.raise refuses a route
that can't message the raiser (decision-route-refused). Candidate on e6e5b41bf,
manifest sha256 edb43aed. Targeted gate green, 5/5 mutants killed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# Row 133 (#1627) round 1: decision closes and messaging lists
|
||||
|
||||
Builder: Dewey. Reviewer: Filbert. Landing gate: Sage.
|
||||
Brief: docs/plans/2026-10-14_prd-1.2-build.md, section "Decision closes and messaging lists"
|
||||
(commit 07c12b51b, blob 333adea8). Ruling: Sage's comment 28580 on #1627. Finding: Filbert's
|
||||
28548 on #1614. Requirement: REQ-ASSIST-2.
|
||||
|
||||
Base: see base.txt. The candidate is build.patch on that base; candidate-manifest.sha256 lists
|
||||
every changed path with its sha256.
|
||||
|
||||
## What ships
|
||||
|
||||
1. **The validator refuses a messaging-limited arbiter.**
|
||||
- Where: `packages/business/src/business.mjs`, in the arbiter loop. Definitions are already resolved by `checkRoles`.
|
||||
- What: an arbiter whose role definition declares messaging lists is refused. The message names the kind, the instance and the definition: `arbiters.delivery names aide, whose role definition assistant declares messaging lists`.
|
||||
- What passes: pm and cto declare no lists, so the shipped example and Jason's arrangement pass.
|
||||
|
||||
2. **`decision.raise` checks that the route may message the raiser.**
|
||||
- `#checkMessaging(business, from, to, refused)` now takes a sender as well as a target. Each is `'human'`, `'tester'` or a role instance; the rules are unchanged.
|
||||
- `refused`, when given, replaces both refusal codes.
|
||||
- A new `#sender(s)` gives `message.send`, `review.request` and `review.verdict` the same sender as before. Their behaviour is unchanged, and row 120's messaging.test.mjs passes unedited.
|
||||
- `decision.raise` calls `#checkMessaging(s.business, route, s.role, 'decision-route-refused')` once the route is computed.
|
||||
- The check comes before the supersede withdrawal and the decision insert.
|
||||
- A refusal therefore logs `action.refused` (the request transaction rolls back and `#refused` records the code) and stores no decision, decision event or withdrawal.
|
||||
- A within-role raise is exempt.
|
||||
- A cross-role raise whose arbiter is the raiser has the human as its route, and the check uses that route.
|
||||
|
||||
3. **The close insert stays as it is.** A comment at the insert says why: decision.raise refused any route that can't message the raiser, definitions change only with a pinned copy, and arbiters change only through the validator.
|
||||
|
||||
4. **Bus README:** one paragraph after the routing rules gives the new code and the validator rule.
|
||||
|
||||
## Correction to row 120's packet
|
||||
|
||||
`agents/researcher/work/queue-120/build.md`, Round 2 section, says the assistant "can't be the
|
||||
raiser". That is wrong. The packet is not edited; this is the correction, in Filbert's words from
|
||||
28548:
|
||||
|
||||
> The assistant can raise decisions, but with crossRole [] they close only by the human or within
|
||||
> its own role, so the DECISION close message reaches it only from the human. Gate that insert if a
|
||||
> role with messaging lists gets crossRole actions or is named an arbiter.
|
||||
|
||||
This row gates the raise and refuses a limited arbiter, which covers both conditions in the last
|
||||
sentence.
|
||||
|
||||
## Observation, not fixed
|
||||
|
||||
- **The case:** the human can withdraw any open decision, including one routed to an arbiter. That close message comes from `human`, while the raise check tested the arbiter as the sender.
|
||||
- **Effect:** a raiser whose `receiveFrom` lists the arbiter but not `human` would get a DECISION message from the human.
|
||||
- **Why it isn't reachable now:** no shipped role can get there. The assistant, the only limited role, has crossRole [] and lists `human`.
|
||||
- **What I did:** the close comment names the case. Gating it would mean either checking the human→raiser direction at raise for every cross-role decision, or checking the withdrawal itself. That is a policy choice, so it is left as a follow-up; see below.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- The human's withdrawal of an arbiter-routed decision gets a messaging check only if a role that refuses messages from the human ever becomes the raiser of such a decision (Sage, sage-0341). The brief leaves close inserts unchecked, and nothing shipped reaches the case.
|
||||
|
||||
## Tests
|
||||
|
||||
- `packages/bus/tests/decision-route.test.mjs` (new, 5 tests):
|
||||
- A cross-role raise is refused when the raiser doesn't hear from the arbiter: coder to the delivery arbiter, warden to the technical one. The refusal is logged with the code, and the decision, decision-event, message and allowed-event counts are unchanged.
|
||||
- A gated raise is refused for a raiser that doesn't hear from the human. Both `deploy` and an action outside the raiser's authority are covered, with the same assertions.
|
||||
- Routes the raiser hears from are allowed, checked from the route to the raiser.
|
||||
- scribe sends only to pm, so a reversed check would refuse three of its raises.
|
||||
- warden may send to the human and both arbiters, so a reversed check would pass its refused raises.
|
||||
- The pm close of an allowed raise stores the DECISION message to the coder.
|
||||
- A refused supersede withdraws nothing.
|
||||
- The assistant's within-role raise (`message.send`, routed to itself) and gated raise (`task.create`, routed to the human) still work, and the human's resolve stores the DECISION message to the assistant.
|
||||
- `packages/business/tests/assistant.test.mjs`, 2 new tests:
|
||||
- For each kind, the validator refuses the assistant as arbiter, and refuses an instance `aide` backed by the assistant definition. Each message names the kind, instance and definition.
|
||||
- An arbiter without lists is accepted beside the limited assistant: the fixture's pm/cto, and chief for delivery.
|
||||
- `packages/business/tests/business.test.mjs`: the shipped-example test now also asserts its arbiters. The example includes the limited assistant as an instance, and it still validates.
|
||||
|
||||
## Targeted gate
|
||||
|
||||
Decision 136. Sage runs the full gate at the landing target. gate.sh runs `node --test
|
||||
tests/*.test.mjs` in business, bus, tasks, webui and cli, with one log per package under gate/.
|
||||
Results are in gate-run.log; see "Receipts" below.
|
||||
|
||||
Run singly before the slot, all 0 fail and 0 skipped:
|
||||
|
||||
| Package | File | Tests |
|
||||
|---|---|---|
|
||||
| bus | decision-route | 5 |
|
||||
| bus | messaging | 12 |
|
||||
| bus | review | 8 |
|
||||
| bus | decision-close | 8 |
|
||||
| bus | decisions | 7 |
|
||||
| business | assistant | 10 |
|
||||
| business | business | 15 |
|
||||
| business | role | 14 |
|
||||
| tasks | fields | 5 |
|
||||
| tasks | verbs | 31 |
|
||||
| harness | runner | 22 |
|
||||
| seat | session | 9 |
|
||||
|
||||
Harness and seat use decision.raise or arbiters in fixtures without messaging lists. They aren't in the gate list.
|
||||
|
||||
## Mutants
|
||||
|
||||
See MUTANTS.md.
|
||||
|
||||
## Receipts
|
||||
|
||||
Slot granted by Sage in sage-0342 (busy check clear at 17:30Z); released 2026-10-15T17:32:54Z.
|
||||
|
||||
- **Mutants**, 2026-10-15T17:31:07Z to 17:31:11Z: both control runs passed. All 5 mutants were killed, each by the tests named in MUTANTS.md.
|
||||
- **Gate**, 2026-10-15T17:31:17Z to 17:32:48Z, all 0 fail and 0 skipped:
|
||||
|
||||
| Package | Tests | Change |
|
||||
|---|---|---|
|
||||
| business | 94 | +2 |
|
||||
| bus | 176 | +5 |
|
||||
| tasks | 92 | |
|
||||
| webui | 112 | |
|
||||
| cli | 195 | |
|
||||
|
||||
- **Candidate:** candidate-manifest.sha256, sha256 edb43aed618ef11af55effeb161060c398c300ad0c3dd6ae90c22d7552f4ddd4. build.patch applies to the base with `git apply --cached --check`, and every applied blob matches the manifest.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Row 133 (#1627) round 1 mutants
|
||||
|
||||
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 |
|
||||
@@ -0,0 +1,3 @@
|
||||
base e6e5b41bf0d02069704754d208d4a36070c7d97d
|
||||
worktree HEAD e6e5b41bf0d02069704754d208d4a36070c7d97d
|
||||
canonical HEAD at slot 31d4cf077
|
||||
@@ -0,0 +1,318 @@
|
||||
diff --git a/packages/bus/README.md b/packages/bus/README.md
|
||||
index 4694e08b5..43b9b52a6 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
|
||||
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 f10e11b37..d0da6270c 100644
|
||||
--- a/packages/bus/src/broker.mjs
|
||||
+++ b/packages/bus/src/broker.mjs
|
||||
@@ -330,15 +330,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);
|
||||
@@ -837,6 +842,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');
|
||||
@@ -938,6 +947,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'
|
||||
@@ -988,7 +1001,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 &&
|
||||
@@ -1093,7 +1106,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.
|
||||
@@ -1136,7 +1149,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 @@
|
||||
3a97fb2070dbc04d39bb04168b0cf14967bd34fd31f1fe821223801a0969cf2e packages/bus/README.md
|
||||
82023e02a3d693948b9b61f4868919c842989ce34d6566f500008e1298091e70 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:31:17Z
|
||||
pkg-business start=2026-10-15T17:31:17Z end=2026-10-15T17:31:19Z rc=0 tests=94 pass=94 fail=0 skipped=0
|
||||
pkg-bus start=2026-10-15T17:31:19Z end=2026-10-15T17:31:24Z rc=0 tests=176 pass=176 fail=0 skipped=0
|
||||
pkg-tasks start=2026-10-15T17:31:24Z end=2026-10-15T17:31:35Z rc=0 tests=92 pass=92 fail=0 skipped=0
|
||||
pkg-webui start=2026-10-15T17:31:35Z end=2026-10-15T17:32:30Z rc=0 tests=112 pass=112 fail=0 skipped=0
|
||||
pkg-cli start=2026-10-15T17:32:30Z end=2026-10-15T17:32:48Z rc=0 tests=195 pass=195 fail=0 skipped=0
|
||||
gate end 2026-10-15T17:32:48Z rc=0
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 133 targeted builder gate (decision 136): business, bus and their consumers.
|
||||
# Same command land-work.sh runs per package. One log per package under gate/.
|
||||
set -uo pipefail
|
||||
WT=${1:-$HOME/dewey-scratch/r133/wt}
|
||||
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,184 @@
|
||||
✔ agents: a held role with nothing on the bus is idle; a role session gets the claim rows unchanged (113.639301ms)
|
||||
✔ agents: working counts what the holder received and has not read; routed alone is not working (210.328019ms)
|
||||
✔ agents: input needed is the role's open decisions routed to the human, gated first, then oldest (320.993726ms)
|
||||
✔ agents: working wins over waiting, and Input needed still shows (166.946864ms)
|
||||
✔ agents: a new holder doesn't inherit what the last one left unread; decisions stay with the role (224.821564ms)
|
||||
✔ agents: state reads only its own business, even for a run name both use (204.474976ms)
|
||||
✔ agents: a message the holder released unread is stranded; the next holder gets nothing and cannot read it (255.408568ms)
|
||||
✔ agents: a message the holder read is not stranded; one the current holder has not read is not listed (259.399318ms)
|
||||
✔ agents: stranded is oldest first and capped at 50 for a role (930.235279ms)
|
||||
✔ agents: stranded is per role and per business (538.808961ms)
|
||||
✔ agents: a message routed to a held role and not yet received is not stranded (151.470619ms)
|
||||
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (148.28619ms)
|
||||
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (265.401992ms)
|
||||
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (278.752881ms)
|
||||
✔ launch events require a human CLI capability; generic emit cannot forge authority events (175.782206ms)
|
||||
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (207.014862ms)
|
||||
✔ a recommendation that is not exactly one option key refuses invalid-recommendation after the options (290.396211ms)
|
||||
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (192.079009ms)
|
||||
✔ task action subjects and linked decision trail are complete and ordered (223.466686ms)
|
||||
✔ launch binding is durable and reconnecting requires the identical trusted record (131.648665ms)
|
||||
✔ business isolation includes inherited object names and cross-business message references (199.984372ms)
|
||||
✔ authority never transfers between action, run, target, unresolved or replaced role holder (301.676168ms)
|
||||
✔ task projection uses schema current view, skipping earlier and equal-start polls (160.831182ms)
|
||||
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (261.949451ms)
|
||||
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (276.068727ms)
|
||||
✔ both arbiters require human resolution when their cross-role route is themselves (326.998658ms)
|
||||
✔ the shipped coder role raises task.reassign as gated to the human; task.scope.change stays cross-role (171.831633ms)
|
||||
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.554368ms)
|
||||
✔ credentials gives human and reader sessions service, account, role, date and state, and nothing else (101.955397ms)
|
||||
✔ credentials refuses a role session and any argument (164.348212ms)
|
||||
✔ credentials reads the status on each request, and a broker without one lists none (232.853331ms)
|
||||
✔ the broker process answers credentials without a token, path or variable in its output, log or store (190.586381ms)
|
||||
✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.056848ms)
|
||||
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.52363ms)
|
||||
✔ expiry refuses use and env references never become client data (0.816158ms)
|
||||
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.304229ms)
|
||||
✔ opaque tokens shorter than 16 characters refuse before use (0.448438ms)
|
||||
✔ a human resolve sends the raising role one message from the human, and its request is the answer (213.868058ms)
|
||||
✔ a cross-role decision resolved by its arbiter comes from that role and run, with no request (230.922873ms)
|
||||
✔ a human withdraw sends one message with the note (227.47768ms)
|
||||
✔ a human seen before the close leaves the request on the close's answer, not the seen (307.198125ms)
|
||||
✔ the raising run's withdraw, a supersede, a within-role raise and seen send nothing (289.398314ms)
|
||||
✔ refused closes send nothing: invalid-choice, decision-closed, not-resolver (334.869108ms)
|
||||
✔ a raising role nobody holds at the close gets the message on the next claim (313.160477ms)
|
||||
✔ a human DECISION send keeps its own instruction as the request, not a close's answer (269.637381ms)
|
||||
✔ a cross-role raise whose arbiter the raiser does not hear from is refused and stores nothing (239.989577ms)
|
||||
✔ a gated raise by a role that does not hear from the human is refused and stores nothing (279.72353ms)
|
||||
✔ a route the raiser hears from is allowed, checked from the route to the raiser (326.052149ms)
|
||||
✔ a refused supersede withdraws nothing (281.203678ms)
|
||||
✔ the assistant's within-role and gated raises still work (319.15161ms)
|
||||
✔ roles lists open decisions routed to a role, and never the human inbox (191.883603ms)
|
||||
✔ recent lists closed decisions of every route, newest close first, with how each closed (290.182662ms)
|
||||
✔ recent gives the closing event as closed when the decision was seen first (286.329478ms)
|
||||
✔ recent stops at RECENT rows (873.44767ms)
|
||||
✔ every row is what trail already gives the same session for that id (311.685422ms)
|
||||
✔ a role session can't list decisions, so it can't read another role's open decisions (435.698138ms)
|
||||
✔ decisions is a read: one business, strict arguments, nothing written (429.04931ms)
|
||||
✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (126.761879ms)
|
||||
✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (129.492896ms)
|
||||
✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (160.252177ms)
|
||||
✔ endLaunch leaves a claim another run took alone (157.678494ms)
|
||||
✔ refuse records action.refused against the caller with the code only (140.480076ms)
|
||||
✔ launches off refuses role.launch with launch-revoked until launches on (211.710005ms)
|
||||
✔ broker process: launch ops authorize role.launch, record refusals and end runs (280.940091ms)
|
||||
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.307363ms)
|
||||
✔ process reader gets own kernel identity without exposing environment values (0.555848ms)
|
||||
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.728737ms)
|
||||
✔ real pid 1 remains inspectable when its environment is protected (0.382437ms)
|
||||
✔ limits binds actor from human proof and rejects role, reader, tester and forged actor (185.954042ms)
|
||||
✔ runtime wires verified human settings to the actual shared file writer and actor receipt (286.668674ms)
|
||||
✔ uncertain organization receipts leave bus startup and reads alone but refuse writes until reconciled (429.318126ms)
|
||||
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (201.010715ms)
|
||||
✔ missing and foreign-business citations refuse and roll back message and grant (186.304063ms)
|
||||
✔ cross-role sends still need a matching resolved decision and consume it once (253.982011ms)
|
||||
✔ messages pages all human messages newest first without writes; read state is history (2741.932132ms)
|
||||
✔ messages refuses role callers, invalid paging and cursors outside this human feed (441.058119ms)
|
||||
✔ views messages passes only its paging arguments through (0.2601ms)
|
||||
✔ messages isolates businesses even with a valid foreign cursor (188.212887ms)
|
||||
✔ the assistant sends to the user and to the chief of staff only (300.105706ms)
|
||||
✔ an assistant send to any other role, or to the tester, is refused and logged (326.517595ms)
|
||||
✔ only the user and the chief of staff may send to the assistant; the tester is refused by default (356.062802ms)
|
||||
✔ a sender-side list alone refuses a send to a role that has no list (337.058845ms)
|
||||
✔ a target-side list alone refuses a send from a role that has no list (382.85875ms)
|
||||
✔ the sender list is checked before the target list (298.633275ms)
|
||||
✔ roles without the field behave exactly as before (437.282824ms)
|
||||
✔ a business with no role definitions at all is unlimited (341.985526ms)
|
||||
✔ a malformed messaging field refuses at boot (237.053948ms)
|
||||
✔ the assistant's review.request to a reviewer is refused and logged, even with an approved decision (396.402856ms)
|
||||
✔ a review.verdict to the assistant from a role outside its receiveFrom is refused and logged (339.648511ms)
|
||||
✔ review requests and verdicts between roles without the field are unchanged (277.984027ms)
|
||||
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (180.28406ms)
|
||||
✔ startup token refusal returns safe code without value or partial listening broker (43.082912ms)
|
||||
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (176.663244ms)
|
||||
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (193.324985ms)
|
||||
✔ trusted host registers later launches; socket clients never have a registration verb (209.840212ms)
|
||||
✔ runtime excludes declared project roots even when host supplies no repoRoots (43.166835ms)
|
||||
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (199.296074ms)
|
||||
✔ a reply is listed for human and reader sessions, and its delivery rows are unchanged (231.976065ms)
|
||||
✔ the reply still reaches the human message.receive (mosaic talk) after the Console showed it (208.791849ms)
|
||||
✔ a message with no reply gives an empty list (159.441995ms)
|
||||
✔ replies come oldest first and stop at REPLIES (834.408489ms)
|
||||
✔ an id that names no message in the business refuses message-not-found, and the refusal is recorded (343.939683ms)
|
||||
✔ strict arguments; a role session gets human-required (399.346636ms)
|
||||
✔ views passes replies through as one call with the id (0.236074ms)
|
||||
✔ requirements returns the committed list to a holding role session, and no one else (225.446861ms)
|
||||
✔ the PM's prd.approve with no task, when no id fits, reaches the human's Inbox (159.08707ms)
|
||||
✔ the PM's non-blocking role.revoke with no task reaches the human's Inbox (161.335098ms)
|
||||
✔ a push is recorded as the action.allowed for git.push.working on the task, and only the host records one (254.213224ms)
|
||||
✔ review.request names the latest pushed commit and sends the reviewer a REVIEW-REQUEST (243.370481ms)
|
||||
✔ review.request says which run holds the reviewer role, or null, and a null request waits for a claim (306.660186ms)
|
||||
✔ review.request refuses a commit that isn't the latest push, a non-reviewer and a role without the action (406.674598ms)
|
||||
✔ reviewFetch answers only the addressed reviewer, for the request it cites (272.637768ms)
|
||||
✔ review.verdict answers one request once, to the role that asked (354.885601ms)
|
||||
✔ task.close on a pushed task needs an approve verdict for the latest push (391.050957ms)
|
||||
✔ a refusal the host decided is filed on the task it names, and only a task ref counts (266.667471ms)
|
||||
✔ v3b prototype refusals, views and append-only mutations (1159.061855ms)
|
||||
✔ gated approval authorizes once, survives store reopen, and fresh approval works (275.615436ms)
|
||||
✔ another run cannot consume an approval; a failed check leaves it usable (249.291089ms)
|
||||
✔ two scheduled callers have exactly one grant and one consumed refusal (198.767882ms)
|
||||
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (376.578017ms)
|
||||
✔ class drift gated to cross-role refuses before consumption (246.652723ms)
|
||||
✔ class drift cross-role to gated refuses before consumption (276.414713ms)
|
||||
✔ class drift gated to within-role refuses before consumption (228.236576ms)
|
||||
✔ class drift cross-role to within-role refuses before consumption (288.223891ms)
|
||||
✔ class drift within-role to gated refuses before consumption (405.232302ms)
|
||||
✔ class drift within-role to cross-role refuses before consumption (219.375011ms)
|
||||
✔ message.send consumes approval and prevents a later send or authorize (219.245267ms)
|
||||
✔ role.revoke consumes approval and prevents a later revoke or authorize (261.173952ms)
|
||||
✔ a business that never switched launches reads allowed with no time and no event (144.237218ms)
|
||||
✔ launches revoked and then restored read allowed, with the restore time and event id (169.225307ms)
|
||||
✔ a task closed on the tracker leaves the open list, and total counts the open ones (150.998569ms)
|
||||
✔ a tombstone is not open (174.149246ms)
|
||||
✔ the open list stops at the cap and says so; total still counts every open task (153.82236ms)
|
||||
✔ a role and a reader session read state; arguments, and an unbound cap, refuse (165.901516ms)
|
||||
✔ a human session reads the same state, with the same cap (164.716366ms)
|
||||
✔ reading state writes nothing: every table keeps its row count (158.365942ms)
|
||||
✔ creates private WAL store and excludes a second writer until explicit close (150.772076ms)
|
||||
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (190.365011ms)
|
||||
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (211.310324ms)
|
||||
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (209.332216ms)
|
||||
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (99.699595ms)
|
||||
✔ async transactions refuse before invoking their function (104.193625ms)
|
||||
✔ the session default fits under the reply cap and pages to the end with next (306.282481ms)
|
||||
✔ the short form: no description, assignees by role, every listed key (275.650245ms)
|
||||
✔ each filter, alone and together, and total counts every match (377.777422ms)
|
||||
✔ task_ref reads one task in full, as the whole board has it (292.974211ms)
|
||||
✔ no arguments is the read the Console and mosaic make, unchanged (257.30124ms)
|
||||
✔ an unknown argument or a bad value refuses invalid-request (1082.176109ms)
|
||||
✔ tasks rows carry priority and requirement for every session that reads tasks (347.76244ms)
|
||||
✔ the read model rebuilds from the ledger: a reopened bus and each trail give the same values (387.345251ms)
|
||||
✔ an empty business and one with snapshots but no task.created read cleanly (121.998119ms)
|
||||
✔ recordTask keeps sync reads and a role write apart (204.02661ms)
|
||||
✔ read_at must be one canonical UTC format, so the projection compares strings safely (128.284865ms)
|
||||
✔ a bad entry refuses the whole record (111.045193ms)
|
||||
✔ taskView reads the projection for one business (143.782225ms)
|
||||
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (264.587942ms)
|
||||
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (415.542357ms)
|
||||
✔ without an adapter the server refuses every task verb (345.927739ms)
|
||||
✔ the runtime refuses an invalid adapter and closes a valid one (299.875555ms)
|
||||
✔ the process loads the S3 adapter from plain-data trackers (271.720489ms)
|
||||
✔ the tester binds only when the business sets bus.tester to exactly true (863.11553ms)
|
||||
✔ a business may not name a role tester, so the tester address is never a role (363.927376ms)
|
||||
✔ every verb outside the tester list refuses tester-refused and is recorded against the tester (646.115165ms)
|
||||
✔ a tester message is from tester, starts [mos test], and writes no human.input or authority (226.158028ms)
|
||||
✔ a role answers a tester only in reply to a tester message, and the answer never reaches the human (171.274214ms)
|
||||
✔ the tester reads the human views a reader reads (88.100172ms)
|
||||
✔ tester proof binds its own CLI, start time and nonce, refuses a managed run and allows an agent (1.091276ms)
|
||||
✔ neither CLI's nonce proves the other: each proof is bound to its own CLI and nonce variable (0.328747ms)
|
||||
✔ testerNonce reads only the tester's variable and gives null on any read error (0.208119ms)
|
||||
✔ an envelope carries exactly one of cap, human and tester; no tester authenticator refuses (56.389929ms)
|
||||
✔ tester-cli.mjs binds through the runtime when the switch is on, and refuses off or under a run marker (761.394808ms)
|
||||
✔ socket capability stamps launch identity; shared views use wire, no SQL client (168.498311ms)
|
||||
✔ two wire claims serialize; a lost reply never automatically retries (191.141606ms)
|
||||
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (113.175651ms)
|
||||
✔ client preserves UTF-8 when a response divides a multibyte character (11.469425ms)
|
||||
✔ committed mutation followed by dropped reply reports unknown and is never retried (146.666309ms)
|
||||
ℹ tests 176
|
||||
ℹ suites 0
|
||||
ℹ pass 176
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 5148.144278
|
||||
@@ -0,0 +1,102 @@
|
||||
✔ the role file loads and resolves through loadBusiness (8.67336ms)
|
||||
✔ the shipped file is exactly the approved list (1.006747ms)
|
||||
✔ the assistant's authority is message.send within role and nothing else (8.961821ms)
|
||||
✔ the assistant talks to the user and the chief of staff only, and no other shipped role is limited (0.933506ms)
|
||||
✔ a business grant through limits does not widen the role (2.107627ms)
|
||||
✔ a launch block whose by is the assistant is refused (1.987377ms)
|
||||
✔ the instance takes no credential references and no tracker (3.259347ms)
|
||||
✔ an arbiter whose role definition declares messaging lists is refused, for each kind (7.469917ms)
|
||||
✔ an arbiter without messaging lists is accepted beside the limited assistant (32.910405ms)
|
||||
✔ the contract passes work to the chief of staff and directs no one (1.022944ms)
|
||||
✔ config directory and file path follow MOSAIC_CONFIG (2.037107ms)
|
||||
✔ the fixture business validates and comes back frozen (6.190925ms)
|
||||
✔ two instances may share a definition (1.778575ms)
|
||||
✔ top-level refusals (6.820417ms)
|
||||
✔ arbiters and projects (20.183036ms)
|
||||
✔ role instances (19.813173ms)
|
||||
✔ Vikunja bots (13.298326ms)
|
||||
✔ a role without Vikunja takes no tracker block (3.435104ms)
|
||||
✔ credential references match the definition's services (2.709108ms)
|
||||
✔ launch (9.649741ms)
|
||||
✔ loadBusiness: file checks (2.724833ms)
|
||||
✔ loadBusiness: not a regular file (39.940039ms)
|
||||
✔ loading writes nothing (0.991718ms)
|
||||
✔ names that are Object.prototype properties don't count as declared (2.697252ms)
|
||||
✔ the shipped example refuses as written and validates once filled in (0.925767ms)
|
||||
✔ the role file loads and resolves through loadBusiness (7.979543ms)
|
||||
✔ the shipped file is exactly the approved list (0.38855ms)
|
||||
✔ the resolved record matches the approved list, so a widened file fails (3.769571ms)
|
||||
✔ the chief of staff holds role.launch and message.send; every other action is gated (2.806016ms)
|
||||
✔ the pm does not hold role.launch (2.329452ms)
|
||||
✔ a business grant of a gated action through limits does not widen the role (3.249079ms)
|
||||
✔ a limit that drops role.launch leaves the chief of staff with no launch block (1.724999ms)
|
||||
✔ a launch block whose by is the pm instance is refused (22.248989ms)
|
||||
✔ the contract names no duty to talk to the human and the pm contract no launch (0.81713ms)
|
||||
✔ the instance takes no credential references and no tracker (3.923251ms)
|
||||
✔ usage errors exit 4 (296.757606ms)
|
||||
✔ validate: a good business exits 0 and prints instance digests (62.287862ms)
|
||||
✔ validate: project files (307.394421ms)
|
||||
✔ validate: missing files and a broken system config (231.973872ms)
|
||||
✔ validate: credential reference problems exit 2 and name each one (60.413902ms)
|
||||
✔ validate: a token file inside the repository is refused (64.027837ms)
|
||||
✔ validate: role definitions come from MOSAIC_ROLES_DIR (177.408704ms)
|
||||
✔ resolve: prints one instance's record (256.2375ms)
|
||||
✔ resolve: refusals (364.84442ms)
|
||||
✔ parse: exactly one of file or env, plus the service's date (3.310467ms)
|
||||
✔ check: a good file has no problems (1.114534ms)
|
||||
✔ check never opens the file: a write-only token passes (1.478162ms)
|
||||
✔ check: file problems (1.471196ms)
|
||||
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (1.383278ms)
|
||||
✔ check: dates and environment references (0.865408ms)
|
||||
✔ REQ-LIMIT-1 schema: every scope unset or zero, adjustable above four, bad values and unknown models refuse (25.802732ms)
|
||||
✔ REQ-LIMIT-1 counts each exact scope at/over limit, unlimited and old tuple after model reassignment (1.150553ms)
|
||||
✔ REQ-ROLE-3 writer: atomic validated revision, immutable previous bytes and authenticated actor/old/new receipt (218.851866ms)
|
||||
✔ REQ-ROLE-3 writer: pre-rename fault leaves old file, post-rename recovery records commit, conflicting recovery refuses (165.494761ms)
|
||||
✔ REQ-LIMIT-1 / decision145 migration prints only, preserves values, rejects ambiguity, and is idempotent (3.088332ms)
|
||||
✔ REQ-LIMIT-1 shared settings validates the complete proposed selection and logs exact actor (96.814221ms)
|
||||
✔ REQ-LIMIT-1 provider is table data: another provider routes separately and aliases share exact counts (1.720979ms)
|
||||
✔ REQ-ROLE-3 refuses live writer locks, symlink targets and uncertain recovery without changing bytes (68.511984ms)
|
||||
✔ REQ-PLUG-1 schema: plugins is optional, names known kinds and checked module names (15.07854ms)
|
||||
✔ REQ-PLUG-1 REQ-ROLE-3 the setter writes plugins through the organization writer and logs old and new names (276.81782ms)
|
||||
✔ REQ-PLUG-1 REQ-ROLE-3 the writer checks the bytes it writes: a Date in plugins is refused there too (37.468939ms)
|
||||
✔ REQ-PLUG-1 REQ-ROLE-3 a refused setter write throws a code, changes nothing and logs nothing (1.510308ms)
|
||||
✔ path and load (3.211281ms)
|
||||
✔ refusals (1.598893ms)
|
||||
✔ systemVars flattens the validated config (2.489176ms)
|
||||
✔ precedence: system, business, project, project role, agent (6.991743ms)
|
||||
✔ limits narrow the definition and never widen it (5.062735ms)
|
||||
✔ role.launch stays within-role only for the instance the launch block names (24.769109ms)
|
||||
✔ limits.authority without role.launch leaves the launcher with no launch block (3.365624ms)
|
||||
✔ limits.authority narrows cross-role actions too (1.965043ms)
|
||||
✔ classify (2.608484ms)
|
||||
✔ the record carries what the broker and launcher need (3.488353ms)
|
||||
✔ digest: key order doesn't matter, any value change does (9.876216ms)
|
||||
✔ refusals (2.54046ms)
|
||||
✔ the four shipped version 2 roles load (4.172478ms)
|
||||
✔ shipped role scopes match addendum B section 2 and the SR runbook (1.165876ms)
|
||||
✔ shipped authority follows the note's table (2.743676ms)
|
||||
✔ every role file in roles/ loads through the validator, and a v2 contract exists (1.625396ms)
|
||||
✔ version 1 files keep loading with no authority (1.678635ms)
|
||||
✔ the conductor policy isn't a role (0.271787ms)
|
||||
✔ a missing role file is exit 4, a symbolic link too (0.507242ms)
|
||||
✔ version 2 refusals (1.693899ms)
|
||||
✔ authority: closed vocabulary, no gated-only action, no overlap (18.604578ms)
|
||||
✔ messaging: optional, role definitions and human, never the tester (2.939494ms)
|
||||
✔ credentials: Gitea scopes (1.794644ms)
|
||||
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (1.526261ms)
|
||||
✔ credentials: services (0.611319ms)
|
||||
✔ contract: a non-empty regular Markdown file beside the role file (1.573573ms)
|
||||
✔ every key names known layers and a merge rule (1.264385ms)
|
||||
✔ unknown keys and wrong layers refuse (1.004238ms)
|
||||
✔ types (9.71511ms)
|
||||
✔ merge: defaults, then the most specific layer wins (0.479499ms)
|
||||
✔ merge: limits only narrow, and provenance lists each source (0.490679ms)
|
||||
✔ merge doesn't change its inputs (3.185291ms)
|
||||
ℹ tests 94
|
||||
ℹ suites 0
|
||||
ℹ pass 94
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1897.635822
|
||||
@@ -0,0 +1,205 @@
|
||||
✔ every screen and overlay fits 120x36, 80x24 and the narrow layout, row for row (1283.721102ms)
|
||||
✔ width counts terminal cells: wide CJK and emoji 2, U+FE0F after a one-cell base 1, marks 0 (65.073635ms)
|
||||
✔ broker text is shown, never interpreted: no raw control character or bidi override reaches the terminal (149.397882ms)
|
||||
✔ NO_COLOR: no escape codes, except reverse video on the selected row (231.646827ms)
|
||||
✔ 1-9 shows the decide flow's own lines and question; only y resolves (152.332226ms)
|
||||
✔ the app shows the same decide text as mosaic decide (101.83054ms)
|
||||
✔ any key but y or Y at the question answers No; nothing is sent (2198.823171ms)
|
||||
✔ below 40x10 the question is hidden, so every key answers it No, y included (Filbert R3) (417.284679ms)
|
||||
✔ under 40 columns a tall terminal shows the too-small screen even where the decide box would fit; y sends nothing (Filbert N10) (1486.075959ms)
|
||||
✔ a hidden decide box names the way out, and the status line under it does not promise y (Filbert N12) (90.309884ms)
|
||||
✔ a throw while answering the question goes to crash, sends nothing and leaves no job pending (140.865274ms)
|
||||
✔ type-ahead never answers the question: a paste, keys typed before it is drawn, keys inside askMs (1137.969319ms)
|
||||
✔ quitting at the question answers No and sends nothing, armed or not (253.60686ms)
|
||||
✔ a timer tick reads nothing under an overlay, never answers the question, and keeps a resolved status (130.89062ms)
|
||||
✔ a long question is cut inside the question; the action, target, choice and prompt stay (Dewey B1) (119.562859ms)
|
||||
✔ a decide box too big for the terminal says so, and every key answers it No, y included (Dewey B1, round 3) (929.281874ms)
|
||||
✔ at every size the decide box shows each line y depends on, or says it is hidden (8076.750792ms)
|
||||
✔ outcome unknown: the CLI's sentence, no retry, then the trail or a re-read (260.730172ms)
|
||||
✔ refused and failed reads keep the last list, marked stale; a refused decide says refused (109.013175ms)
|
||||
✔ help or a detail box cut to fit keeps its last line, Esc included (Dewey) (56.834189ms)
|
||||
✔ keys: views, moving, trail of a selection, the command line, help and quit (240.982914ms)
|
||||
✔ one option: the status line says Press 1, not 1-1 (97.856893ms)
|
||||
✔ no business: the app says so and :business picks one (50.67336ms)
|
||||
✔ mosaic with no argument: exit 3 in an agent run, exit 4 with the usage off a terminal, the app on one (1.844883ms)
|
||||
✔ scripts/mosaic with no argument reaches the app entry; the seat usage it printed before has not drifted (166.684996ms)
|
||||
✔ strings to keep exactly live once, in the decide flow and the transport; the app does not restate them (0.469432ms)
|
||||
✔ decide with --note still asks; a No sends nothing (49.803788ms)
|
||||
✔ the app's business comes from a live bus host only (0.987805ms)
|
||||
✔ runApp leaves the terminal as it found it on q, end of input, SIGTERM, SIGINT, SIGHUP and an error, the first frame's included (6.634959ms)
|
||||
✔ draw is pure: same state, same frame (0.693674ms)
|
||||
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (153.920652ms)
|
||||
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (118.679933ms)
|
||||
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (115.555247ms)
|
||||
✔ decide prints a declining choice as declining (145.173132ms)
|
||||
✔ an unknown outcome is reported once and never resent (119.859835ms)
|
||||
✔ a decision closed before the answer arrives exits 2 and points at its trail (104.003576ms)
|
||||
✔ a prefix that matches two open decisions exits 2 and resolves neither (103.822883ms)
|
||||
✔ without --business a command uses the live host's business, and a stale host.json is not a host (133.460259ms)
|
||||
✔ every human command refuses inside an agent run before it touches the bus (114.00611ms)
|
||||
✔ usage errors exit 4; no business and no host is a usage error (94.687899ms)
|
||||
✔ agents and tasks print through the broker (99.578896ms)
|
||||
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.230515ms)
|
||||
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (64.972754ms)
|
||||
✔ trackers come from the tracker.* variables of the one project that names a tracker project (89.440884ms)
|
||||
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (44.637002ms)
|
||||
✔ two projects that each name a tracker project refuse, since the boot shape holds one (33.373932ms)
|
||||
✔ a business without tracker.baseUrl gets no trackers entry (27.336432ms)
|
||||
✔ an unknown business and a broken system config refuse with exit 3 (71.892716ms)
|
||||
✔ follow prints an unsolicited message after plain talk returned (210.296984ms)
|
||||
✔ talk --follow continues after reply and prints a later unsolicited message (175.813979ms)
|
||||
✔ inbox lists all unread human messages under decisions, does not deliver or read them (3028.692965ms)
|
||||
✔ follow refuses agent context and bad arguments (211.036084ms)
|
||||
✔ follow Ctrl-C wakes the poll sleep and removes its handler; transport errors also clean up (305.941091ms)
|
||||
✔ plain talk leaves the process SIGINT handlers unchanged (227.33473ms)
|
||||
✔ empty views say so (0.997669ms)
|
||||
✔ the trail keeps the broker's order and names a decision's task without its rows (1.314215ms)
|
||||
✔ tasks print the tracker fields the snapshot carries (0.208373ms)
|
||||
✔ the review loop: the coder pushes and asks, the reviewer fetches and approves, and the task's trail holds each step (738.882675ms)
|
||||
✔ refusals: no action, non-fast-forward, an unknown commit or request, and a push for another task moves only that task's ref (629.101303ms)
|
||||
✔ the size cap refuses a push before any byte (323.996837ms)
|
||||
✔ a fetch over the cap refuses before any byte reaches the reviewer (535.973139ms)
|
||||
✔ the socket refuses a request with no capability, an unknown one, and a bad request line (534.793708ms)
|
||||
✔ a rigged clone runs nothing: hooks, fsmonitor, a filter, a gitlink and a symlinked .git/config leave no marker (1338.991371ms)
|
||||
✔ an identity with no role, or of another business, refuses not-a-role and moves no ref (364.819751ms)
|
||||
✔ a review of a project the business no longer has refuses unknown-project (389.292568ms)
|
||||
✔ past maxActive connections the socket refuses git-busy, and a hung-up connection frees its slot (340.265523ms)
|
||||
✔ close() answers host-closing to a connection still sending its request line (470.424268ms)
|
||||
✔ a bundle that trickles in refuses body-timeout at its deadline and frees its slot (695.199336ms)
|
||||
✔ a reviewer gone before its bundle was sent is filed fetch-aborted, not as the host's fault (861.46024ms)
|
||||
✔ a reviewer that hangs up the moment it has the whole bundle was served, not aborted (654.430248ms)
|
||||
✔ send: a close after every byte left is served; one that fails a queued write is fetch-aborted (41.479404ms)
|
||||
✔ a bundle with no bytes for bodyIdle refuses body-timeout long before its deadline (626.733425ms)
|
||||
✔ send: a close once the loop is past its last chunk is served, and a failed write there is fetch-aborted (23.185299ms)
|
||||
✔ letGo waits for an answer to leave for at most its cap, then destroys the socket (301.566953ms)
|
||||
✔ a record that fails and can't be undone refuses rollback-failed, which the host logs (506.107162ms)
|
||||
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (996.046683ms)
|
||||
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (231.377362ms)
|
||||
✔ a second host for the same data root refuses with exit 3 while the first runs (213.607681ms)
|
||||
✔ two bus starts racing for one data root leave one host running; the other refuses that a host already runs (570.560559ms)
|
||||
✔ a host takes over a host.lock its dead host left, and puts back a live claim that replaced it meanwhile (1.372621ms)
|
||||
✔ 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.789151ms)
|
||||
✔ a live host's lock stays, however old the file is (0.86465ms)
|
||||
✔ a host.lock left by a host killed with SIGKILL doesn't stop the next bus start (807.307324ms)
|
||||
✔ while a host boots, a reader sees no host: the human commands still ask for --business, stop and status find none (236.74971ms)
|
||||
✔ a host started before host.lock existed, holding only host.json, still refuses a second start, which leaves no lock (27.542858ms)
|
||||
✔ a broker fork that throws leaves no host.lock and no host.json (46.162268ms)
|
||||
✔ a broker send that throws leaves no host.lock and no host.json (29.319211ms)
|
||||
✔ a broker that misses its boot wait leaves no host.lock and no host.json (31.046343ms)
|
||||
✔ a notifier fork that throws stops the broker and leaves no host.lock and no host.json (292.92784ms)
|
||||
✔ a notifier send that throws stops the broker and leaves no host.lock and no host.json (380.284155ms)
|
||||
✔ 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 (255.283508ms)
|
||||
✔ 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 (172.298775ms)
|
||||
✔ a host.json that can't be written stops both children and leaves no host.lock (255.183997ms)
|
||||
✔ an error event on the broker before it replies fails the start at once, and leaves no host.lock and no live child (45.276273ms)
|
||||
✔ an error event on the notifier before it replies fails the start at once, and leaves no host.lock and no live child (142.3426ms)
|
||||
✔ an error event on the broker while the notifier starts fails the start once the notifier replies (252.672716ms)
|
||||
✔ 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 (387.482383ms)
|
||||
✔ 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 (296.809274ms)
|
||||
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1192.092454ms)
|
||||
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (431.041701ms)
|
||||
✔ a broker reply with no request waiting breaks the channel and the host exits 1 (168.869583ms)
|
||||
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (248.568229ms)
|
||||
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (198.570816ms)
|
||||
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (163.866551ms)
|
||||
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (196.27253ms)
|
||||
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (121.277963ms)
|
||||
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (193.615466ms)
|
||||
✔ watchChildren reports a child that died before it was called, and one that dies later (21.656716ms)
|
||||
✔ bus stop refuses to signal a live pid that is not a bus host (201.926279ms)
|
||||
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (246.550281ms)
|
||||
✔ bus start refuses with exit 3 without a notifier config (119.672292ms)
|
||||
✔ bus start runs until bus stop; status reports it while it runs (710.10342ms)
|
||||
✔ bus-service.sh renders the unit and installs it into a given directory (51.584279ms)
|
||||
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (62.69042ms)
|
||||
✔ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (12.673945ms)
|
||||
✔ bus-service.sh refuses a default claude without the execute bit, or in a PATH dir with a space (8.060593ms)
|
||||
✔ each launch rereads limits and model selections; lowered limits keep sessions and log exact hit (39.315251ms)
|
||||
✔ bus start --pm: the launcher instance (the chief of staff) runs under its own PID namespace, registered, with a manifest (1368.323138ms)
|
||||
✔ the chief of staff launches a coder through its launch tool; the coder answers; refusals name their code (767.015503ms)
|
||||
✔ each launch makes sure the workspace has a clone of the project: cloned, fast-forwarded, or fetched and logged (2393.882003ms)
|
||||
✔ a project clone that can't be made refuses checkout-failed, from --chief and from a session, with no run and no clone left (233.68395ms)
|
||||
✔ a runner that stops at once ends its launch with the runner's reason (306.753408ms)
|
||||
✔ a Claude Code launch with no session credential refuses before it starts (423.928337ms)
|
||||
✔ a pi launch needs no session credential: pi logs in through its own ENV_ALLOW variables (256.608005ms)
|
||||
✔ a runner killed from the host ends its launch as killed, not failed (336.826017ms)
|
||||
✔ a missing or failing bwrap refuses sandbox-unavailable before anything starts; there is no unconfined fallback (191.594945ms)
|
||||
✔ a real Claude Code coder's shell can't read a stand-in secret or anything else its sandbox doesn't hold (1534.225395ms)
|
||||
✔ 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 (792.744405ms)
|
||||
✔ 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 (3705.886195ms)
|
||||
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (125.423999ms)
|
||||
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (148.981708ms)
|
||||
✔ a launch client that never closes its side doesn't hold the host's close (142.701121ms)
|
||||
✔ a runner that ignores SIGTERM is killed when the host closes (1587.812309ms)
|
||||
✔ claudeVersion asks MOSAIC_CLAUDE_BIN when set, else claude on PATH (4.548629ms)
|
||||
✔ migration needs no bus/config, prints whole converted document, is idempotent and never writes (15.493151ms)
|
||||
✔ limits view/set use the human bus transport and pass only the explicit patch (1.262966ms)
|
||||
✔ post-119 example and Jason fixture migrate with checked inherited models; wrong or absent config refuses (223.387889ms)
|
||||
✔ zoned uses the IANA zone across DST (21.698015ms)
|
||||
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (144.977586ms)
|
||||
✔ two blocking decisions get two DMs with different nonces (124.487868ms)
|
||||
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.355541ms)
|
||||
✔ a failed DM is journaled, backs off, and is retried until it lands (109.928991ms)
|
||||
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (109.130957ms)
|
||||
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (164.711362ms)
|
||||
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (109.853016ms)
|
||||
✔ 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 (155.172903ms)
|
||||
✔ a restart after the second refusal does not send before that refusal's 30 min are up (196.260058ms)
|
||||
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (114.936707ms)
|
||||
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (100.623919ms)
|
||||
✔ an inbox read failure is logged and the next poll retries (0.679924ms)
|
||||
✔ no Discord id reaches the journal or the log (154.001347ms)
|
||||
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (31.104118ms)
|
||||
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (45.671635ms)
|
||||
✔ the journal: a whole file that is one torn line truncates to empty (26.162088ms)
|
||||
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.632092ms)
|
||||
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.620543ms)
|
||||
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (5.055956ms)
|
||||
✔ the journal: a symlinked directory refuses and says it is a link (0.73333ms)
|
||||
✔ 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.562999ms)
|
||||
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.412491ms)
|
||||
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.471988ms)
|
||||
✔ digest content stays within Discord's 2000 characters (0.302547ms)
|
||||
✔ runLoop never overlaps ticks and stops after the one in flight (110.763847ms)
|
||||
✔ a human command refused with human-required prints the other-tree line and exits 3 (2.453514ms)
|
||||
✔ a --tester command refused with tester-required prints the other-tree line and exits 3 (0.834856ms)
|
||||
✔ other exit-3 refusals print no other-tree line (1.537005ms)
|
||||
✔ the line names no path (0.150729ms)
|
||||
✔ shared copy: npm ci, pushed reachability, readonly/existing copy, webui delegation (542.329905ms)
|
||||
✔ wrapper preserves Q81 lease and marker, rejects foreign/symlink; check uses effective roots (429.708823ms)
|
||||
✔ bus unit copy/pm pins, install refuses all busy states and failed reads, drop-in refusal (430.42662ms)
|
||||
✔ missing package manifest refuses before npm; npm failure removes the unpublished temp copy (303.497807ms)
|
||||
✔ real pinned install loads bus and Console; same-copy mosaic talk passes proof, other trees refuse (3717.954861ms)
|
||||
✔ bus status reads effective copy/pm/drop-ins without writes; check refuses an overridden ExecStart (522.105444ms)
|
||||
✔ drop-ins in either override field and malformed metadata refuse; uninstall also checks --no-reload (173.694182ms)
|
||||
✔ install never shows a template name on systemd 261; template drop-ins still refuse (160.805054ms)
|
||||
✔ unlisted instance drop-in is found in another unit lookup directory; path discovery errors refuse (153.64472ms)
|
||||
✔ inherited mosaic-.service.d override refuses without changing the unit (229.529356ms)
|
||||
✔ inherited service.d override refuses without changing the unit (209.737135ms)
|
||||
✔ inherited drop-in unreadable directory or file refuses before writing (189.531136ms)
|
||||
✔ talk --tester sends a [mos test] message as tester from inside an agent and prints the role's reply (171.660026ms)
|
||||
✔ talk --tester refuses text without the [mos test] prefix before any call (93.189222ms)
|
||||
✔ the read verbs take --tester inside an agent, and the trail shows the tester as the sender (120.50124ms)
|
||||
✔ decide, stop and launches take no --tester (148.979797ms)
|
||||
✔ the tester transport runs tester-cli.mjs, and the tester's refusals exit 3 (0.404605ms)
|
||||
✔ with no stand-in transport, --tester runs tester-cli.mjs on the data root's socket (0.669963ms)
|
||||
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 (424.344936ms)
|
||||
✔ the transport writes {business, verb, args} to the child and reads its JSON (36.364281ms)
|
||||
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2160.982866ms)
|
||||
✔ busExit and refuseInsideAgent (3.680818ms)
|
||||
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (209.160311ms)
|
||||
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1114.389683ms)
|
||||
✔ a late reply the Console has listed still reaches the next talk (2296.359599ms)
|
||||
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (291.816235ms)
|
||||
✔ launches off and on go to the broker and change the business's launch state (130.266829ms)
|
||||
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (94.738993ms)
|
||||
ℹ tests 195
|
||||
ℹ suites 0
|
||||
ℹ pass 195
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 18169.504777
|
||||
@@ -0,0 +1,100 @@
|
||||
✔ the boot config is checked before anything starts (77.10775ms)
|
||||
✔ a business with no tracker entry refuses task verbs (136.89089ms)
|
||||
✔ credential.expiring and .expired are recorded once per instance (243.309912ms)
|
||||
✔ a token file that changes on disk records credential.changed (116.704516ms)
|
||||
✔ autostart polls, reconciles and retries a startup the tracker was down for (142.706563ms)
|
||||
✔ a refusal a restart must clear is not retried by the poll (105.06387ms)
|
||||
✔ a poll that fires while two are queued is dropped (122.924829ms)
|
||||
✔ close waits for a running verb and refuses one that has not started (334.807276ms)
|
||||
✔ the bundled Vikunja is the pinned upstream image the runbook names (0.90642ms)
|
||||
✔ every published port is on 127.0.0.1, and no secret is in the file (0.270639ms)
|
||||
✔ the committed table loads frozen; its verbs are the broker task verbs (1.42883ms)
|
||||
✔ a write names a verb the table has as its own key, not one it inherits (0.548859ms)
|
||||
✔ the schema refuses any other shape (2.642934ms)
|
||||
✔ the Field table in the slice 1 plan names the verbs each field is written by (0.633423ms)
|
||||
✔ each verb refuses, asks for a decision and writes as the table and the role files say (1435.056316ms)
|
||||
✔ the list holds the PRD's requirement ids, in its order, at its version (1.484333ms)
|
||||
✔ the committed list loads frozen (0.855432ms)
|
||||
✔ the schema refuses any other shape (2.699377ms)
|
||||
✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (374.186995ms)
|
||||
✔ the project list holds recorded project objects in the recorded list envelope (94.394387ms)
|
||||
✔ the recorded task bodies pass the checks S3 applies to every read (0.512092ms)
|
||||
✔ the client works against the fake over real HTTP with the platform fetch (255.939095ms)
|
||||
✔ a correct install starts, and the first reconcile records tasks that already exist (109.758945ms)
|
||||
✔ verbs refuse while a business is starting and after startup refused it (146.486264ms)
|
||||
✔ startup refuses a token that can do more than its role needs (410.684016ms)
|
||||
✔ startup refuses an unsupported version and flags an untested one (417.249704ms)
|
||||
✔ startup refuses a board that the runbook did not install (568.124006ms)
|
||||
✔ startup refuses a project the sync bot cannot read (119.234423ms)
|
||||
✔ startup refuses a configured label the pm bot cannot see (123.945862ms)
|
||||
✔ startup refuses an expired credential and a missing sync credential (292.449387ms)
|
||||
✔ an unreachable tracker refuses with tracker-unavailable (228.248273ms)
|
||||
✔ an edit in the UI is recorded once, with the fields that changed (196.925141ms)
|
||||
✔ a move between open buckets is seen on the board, though updated does not change (187.122789ms)
|
||||
✔ a person's comment is counted and a bot's is not (286.558782ms)
|
||||
✔ the hourly reconcile catches a comment through comment_count (268.417224ms)
|
||||
✔ a task closed in the UI leaves the open view with its done bucket (596.886426ms)
|
||||
✔ a task that leaves the board is recorded as deleted, moved or out of reach (363.337457ms)
|
||||
✔ a poll that read before a verb wrote does not overwrite the verb (252.428352ms)
|
||||
✔ a tracker fault during a tick is reported and the next tick catches up (368.424718ms)
|
||||
✔ a malformed answer refuses the tick with tracker-shape (226.143602ms)
|
||||
✔ no token value reaches the database, the log or a refusal (328.292716ms)
|
||||
✔ the first look at a task counts only comments inside the window (173.550759ms)
|
||||
✔ with descendants off the adapter reads no project list and no child, as in v0 (120.445743ms)
|
||||
✔ a three-level tree is read per project, and a project outside it is not (129.914623ms)
|
||||
✔ a watch-only child keeps the business ready, and a failed child read refuses as transient (229.752211ms)
|
||||
✔ a root without the installed buckets refuses with descendants on (99.028883ms)
|
||||
✔ the project list refuses without read_all, and skips archived projects and saved filters (230.181278ms)
|
||||
✔ descendants must be true or false (89.038655ms)
|
||||
✔ a move inside the tree is a moved tombstone plus a snapshot under the new ref (277.624452ms)
|
||||
✔ a child made after startup joins the tree at the next reconcile (187.646692ms)
|
||||
✔ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history (258.830597ms)
|
||||
✔ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses (226.829647ms)
|
||||
✔ a reconcile that fails keeps the previous tree (206.247ms)
|
||||
✔ a closed task the cursor missed takes its own project's done bucket (242.82779ms)
|
||||
✔ closed tasks read at startup take their own project's done bucket (174.267067ms)
|
||||
✔ a closed task the cursor returns takes its own project's done bucket (193.559254ms)
|
||||
✔ a task moved out of a child that leaves the tree at the same time is moved, not no-access (201.101772ms)
|
||||
✔ a task in a bucket outside the five keeps its raw bucket id (139.927301ms)
|
||||
✔ bucket 0 on a watch-only task never becomes a bucket or a state (226.030316ms)
|
||||
✔ a child whose kanban view is deleted still refuses the tick, and the reconcile reads it watch-only (194.953886ms)
|
||||
✔ a child dropped at a tick comes back at the next reconcile once the bot reads it again (200.21378ms)
|
||||
✔ task.create needs a recorded human request and a requirement id (209.981662ms)
|
||||
✔ task.create takes only an approved requirement id (234.573297ms)
|
||||
✔ task.create cites the request a received message carries, not the message's id (184.516486ms)
|
||||
✔ only labels named in the business file can be written (270.821644ms)
|
||||
✔ task.schedule sets and clears a due date and relations (415.080424ms)
|
||||
✔ assign and reassign move the role bots and record task.assigned (460.938596ms)
|
||||
✔ task.update.assigned is for the assignee and records task.state (403.382088ms)
|
||||
✔ a wrong expected digest records task.conflict and writes nothing (398.795632ms)
|
||||
✔ a cross-role verb needs a resolved decision, used once (325.735211ms)
|
||||
✔ task.close needs a verdict; after it every verb refuses with task-done (271.785075ms)
|
||||
✔ a task with pushed work closes only citing an approve verdict for its latest push, and a refusal writes nothing (361.927894ms)
|
||||
✔ a lost answer is settled by a re-read and never retried (737.456543ms)
|
||||
✔ a create whose answer is lost is reported uncertain, and the poll finds the task (1384.986701ms)
|
||||
✔ a task the sync bot cannot read refuses and records nothing (203.869517ms)
|
||||
✔ verbs and polls for one business run one at a time (277.57295ms)
|
||||
✔ a due date with milliseconds is written to the second (691.772386ms)
|
||||
✔ every write landed and the final read failed: the verb succeeds and records what it wrote (435.330233ms)
|
||||
✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (170.699262ms)
|
||||
✔ a create whose final read fails succeeds and records task.created (321.017764ms)
|
||||
✔ an edit between the last write and the final read shows as external on the next poll (308.961502ms)
|
||||
✔ task.created is recorded when a later label write fails (237.877151ms)
|
||||
✔ every verb acts in an installed child, on the child's own board (609.850306ms)
|
||||
✔ task.create refuses a project outside the tree, a watch-only one and one that is not an id (249.444171ms)
|
||||
✔ with descendants off, task.create takes only the root project (155.300692ms)
|
||||
✔ a verb on a watch-only task, or a relation to one, refuses with project-not-installed (196.091707ms)
|
||||
✔ a related task is read before authority: in another project than its ref names, task-moved; missing, task-not-found (273.135809ms)
|
||||
✔ a task moved to another project of the tree refuses its old ref with task-moved and takes its new one (136.452202ms)
|
||||
✔ a verb and a create in the child record the final read of the child's board (172.979303ms)
|
||||
✔ a create in the child whose final read fails records the child's todo bucket (175.711364ms)
|
||||
✔ a lost answer in the child is settled by a re-read of the child's board (247.014887ms)
|
||||
✔ every related task is read before authority, not only the first (194.208956ms)
|
||||
ℹ tests 92
|
||||
ℹ suites 0
|
||||
ℹ pass 92
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 10815.189382
|
||||
@@ -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 (7803.952779ms)
|
||||
✔ 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 (7199.18921ms)
|
||||
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (5599.087577ms)
|
||||
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
|
||||
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (7059.45333ms)
|
||||
✔ the README says a refused field table names the file or the key at fault, never a path (1.530847ms)
|
||||
✔ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes (8329.506486ms)
|
||||
✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (363.433076ms)
|
||||
✔ decisions route serves the roles and recent lists from the reader view (531.091336ms)
|
||||
✔ credentials route serves the broker credential status from the reader view (700.353226ms)
|
||||
✔ replies route serves the replies to one message from the reader view (1221.723474ms)
|
||||
✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (66.159549ms)
|
||||
✔ bus refusals map to statuses and never carry a path or a stack (29.245201ms)
|
||||
✔ humanCall speaks the human transport protocol without blocking the server (1771.765219ms)
|
||||
✔ busReader takes --business, else the live bus host, else refuses (308.490836ms)
|
||||
✔ humanCall kills a transport that runs past its timeout (531.085485ms)
|
||||
✔ serve refuses a --business value that is not a business id (344.537675ms)
|
||||
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (5028.030688ms)
|
||||
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (4037.876905ms)
|
||||
✔ conversation view: a newer session with no readable history keeps the marker (2823.875574ms)
|
||||
✔ conversation view: seats without history say so and offer no reply (1796.701003ms)
|
||||
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (5427.408382ms)
|
||||
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (54839.17033ms)
|
||||
✔ messages HTTP read pages and preserves metadata; bad arguments refuse before a read; other reads unchanged (512.662519ms)
|
||||
✔ 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 (11976.878996ms)
|
||||
✔ Messages nav count polls on Tasks and Board and clears on a Board refusal; messages 403 refuses local pages (3446.302817ms)
|
||||
✔ copy writes the Console paths at the full id, read-only, and leaves an existing copy alone (3172.832028ms)
|
||||
✔ copy refuses a commit off refactor, an unknown one, a worktree and a bad copies directory (8077.483683ms)
|
||||
✔ copy writes nothing when an archived path is missing at the commit (726.207141ms)
|
||||
✔ a copy that loses the rename race leaves no read-only temp directory behind (710.993178ms)
|
||||
✔ a copy whose rename fails with no target refuses, writes no copy and leaves no temp directory (1178.307532ms)
|
||||
✔ a copy that meets a target holding files says exists: and moves nothing into it (1336.477061ms)
|
||||
✔ a copy that meets an empty target directory replaces it by rename, not by moving into it (370.684193ms)
|
||||
✔ serve runs from a copy: /healthz 200, the config check, roles and RELEASE from the copy, the queue from --checkout (835.001497ms)
|
||||
✔ serve refuses a --checkout that is not an absolute directory path (274.801238ms)
|
||||
✔ consoleReads runs the queue read under queueRoot and reads RELEASE under root (55.168265ms)
|
||||
✔ preview options are all three or none, and each is checked (8.953008ms)
|
||||
✔ the CLI refuses a partial or invalid preview flag set at start (2110.383667ms)
|
||||
✔ the CLI with all three flags serves the preview mode (531.464401ms)
|
||||
✔ a request for the public name needs the header equal to the user id, else 401 and nothing read (60.251324ms)
|
||||
✔ the header name matches without regard to case, and the value compare is exact (16.750668ms)
|
||||
✔ the public name with the header reads the bus views, and the Host and Origin are checked first (60.85988ms)
|
||||
✔ in preview mode every write answers 403 read-only, from the public name and loopback, and reaches nothing (18.096438ms)
|
||||
✔ loopback requests are unchanged in preview mode, and the bus views work with the board down (6.156451ms)
|
||||
✔ a proxy that rewrites Host to the loopback port gets 403 on its first request, whatever the user (21.537281ms)
|
||||
✔ a repeated Host is 400 before any other check, in preview mode and without it (7.987106ms)
|
||||
✔ without the preview flags the public name is refused and writes still reach the board (4.417878ms)
|
||||
✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (1.507294ms)
|
||||
✔ credential state from the date alone, as the bus states it (0.190928ms)
|
||||
✔ queue: rows, one row, ids and methods, notes; no path or id from a row (1575.275569ms)
|
||||
✔ queue: a refused read fails closed with the store's text, redacted (1632.131749ms)
|
||||
✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (4924.644161ms)
|
||||
✔ business: names, vars on the allowlist, authority, credential metadata only (17.018962ms)
|
||||
✔ business: missing or invalid file refuses with the module's text, redacted (65.848299ms)
|
||||
✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (18.909155ms)
|
||||
✔ settings: release, board, loopback address, notifier binding name only (758.135924ms)
|
||||
✔ task fields: the committed table with no reads configured, GET only; a bad table refuses with no path (95.56435ms)
|
||||
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (5586.414828ms)
|
||||
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (23558.371036ms)
|
||||
✔ 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 (8114.659564ms)
|
||||
✔ a sent receipt lists its replies as the page refreshes, oldest first and capped, and marks none delivered or read (13382.967905ms)
|
||||
✔ startServer takes send only as a function and only with the preview mode (6.330874ms)
|
||||
✔ the CLI refuses --send without the preview flags, and without a configured bus (578.90703ms)
|
||||
✔ the CLI with --send and a configured bus sends through the human transport and logs one line (184.3136ms)
|
||||
✔ without --send the route is read-only like every other write, and config says no send (8.381189ms)
|
||||
✔ with --send every other write is still 403 read-only, and nothing reaches the board or bus (8.691288ms)
|
||||
✔ config says send only on the public name with --send on (2.241976ms)
|
||||
✔ a send without the signed-in user is 401 and is neither sent nor logged, loopback included (4.260276ms)
|
||||
✔ each request check refuses before anything is sent, and logs the refusal without the body (16.30483ms)
|
||||
✔ a send passes exactly to, body and in_reply_to, answers the id, and logs it (4.988282ms)
|
||||
✔ a crafted role name cannot forge a log line (3.293243ms)
|
||||
✔ a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried (8.100443ms)
|
||||
✔ through the human transport the request is exactly the business, message.send and the keys (99.561379ms)
|
||||
✔ against a real broker: the message is written once as a human send, and its refusals map (2675.882911ms)
|
||||
✔ loopback host and board origin fail closed (7.371975ms)
|
||||
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (346.627856ms)
|
||||
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (56.233699ms)
|
||||
✔ unreachable board reports URL; CLI rejects unsupported options (448.204872ms)
|
||||
✔ render prints the unit without the preview flags (11.977986ms)
|
||||
✔ render carries every serve flag, the preview set included (8.514007ms)
|
||||
✔ a partial preview set is refused (19.189075ms)
|
||||
✔ a value the unit cannot carry is refused, for every flag (415.641935ms)
|
||||
✔ the rendered unit names none of the human proof agent markers and sets no agent variable (31.271428ms)
|
||||
✔ install into --dir with --no-reload writes the file whole, then reports unchanged (236.498774ms)
|
||||
✔ install without --no-reload runs daemon-reload and nothing that starts the unit (39.572571ms)
|
||||
✔ uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not (373.068688ms)
|
||||
✔ status prints the unit state and GET /healthz on the installed port (385.043961ms)
|
||||
✔ usage: an unknown verb or flag, and start or enable, exit 4 (128.716774ms)
|
||||
✔ without --send, render is the template with its placeholders filled, byte for byte (95.406925ms)
|
||||
✔ render --send with the preview set ends ExecStart with --send and changes nothing else (88.297505ms)
|
||||
✔ --send without the whole preview set is refused and writes nothing (228.225907ms)
|
||||
✔ install --send into --dir with --no-reload writes the sending unit, and status says whether it sends (407.137573ms)
|
||||
✔ uninstall and status take no --send (110.264705ms)
|
||||
✔ install refuses while the unit is running, in any state but inactive or failed, and changes nothing (480.071251ms)
|
||||
✔ when the state read fails, install and uninstall refuse with one line and change nothing (110.986362ms)
|
||||
✔ install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree (126.709566ms)
|
||||
✔ install refuses unless the bus unit runs from the same copy, naming both paths, and changes nothing (605.504872ms)
|
||||
✔ render --copy runs the copy and reads the queue in the checkout; without --copy the unit is unchanged (22.072825ms)
|
||||
✔ the uid header takes only an Authentik uid; the username header still takes a username (362.479975ms)
|
||||
✔ a preview value cannot start with - or . (55.761013ms)
|
||||
✔ status names the copy and whether refactor has moved past it, and says when the unit runs from no copy (535.576981ms)
|
||||
✔ Settings form shows every scope and held role; gated save, exact payload, retained draft and no automatic retry (4025.513753ms)
|
||||
2026-10-15T17:31:41.471Z settings user="jason" changed
|
||||
✔ REQ-WEB-1 signed-in same-origin Settings write reaches exactly one human operation (27.071774ms)
|
||||
✔ REQ-WEB-1 Settings writes require enabled send, named Host, identity, exact Origin and same-origin Fetch Metadata (9.819193ms)
|
||||
✔ REQ-WEB-1 Settings validates JSON, size, revision and strict writable keys before broker call (6.180822ms)
|
||||
✔ tokens.css and icons.svg are byte copies of docs/design (1.402553ms)
|
||||
✔ JetBrains Mono files match the digests in their sources file, with the OFL text (2.641789ms)
|
||||
✔ shipped strings are unchanged, and the web keeps its write boundary (2.628132ms)
|
||||
✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (7154.822283ms)
|
||||
✔ five states on Board, Inbox, Tasks, Agents and Trail (3271.719909ms)
|
||||
✔ Queue, Business and Settings in their five states, with no move button (5117.157944ms)
|
||||
✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (3227.845614ms)
|
||||
✔ the server log names no config path when the system config refuses (314.440383ms)
|
||||
ℹ tests 112
|
||||
ℹ suites 0
|
||||
ℹ pass 112
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 55301.349684
|
||||
@@ -0,0 +1,9 @@
|
||||
mutants start 2026-10-15T17:31:07Z
|
||||
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:31:11Z rc=0
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Row 133 (#1627): 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/wt'))
|
||||
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.533324ms)
|
||||
✔ the shipped file is exactly the approved list (0.546743ms)
|
||||
✔ the assistant's authority is message.send within role and nothing else (1.412775ms)
|
||||
✔ the assistant talks to the user and the chief of staff only, and no other shipped role is limited (0.449919ms)
|
||||
✔ a business grant through limits does not widen the role (1.149508ms)
|
||||
✔ a launch block whose by is the assistant is refused (1.172191ms)
|
||||
✔ the instance takes no credential references and no tracker (1.846757ms)
|
||||
✖ an arbiter whose role definition declares messaging lists is refused, for each kind (1.190833ms)
|
||||
✔ an arbiter without messaging lists is accepted beside the limited assistant (1.423913ms)
|
||||
✔ the contract passes work to the chief of staff and directs no one (0.523389ms)
|
||||
ℹ tests 10
|
||||
ℹ suites 0
|
||||
ℹ pass 9
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 66.126492
|
||||
|
||||
✖ 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.190833ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at refuses (file:///home/jwoltje/dewey-scratch/r133/wt/packages/business/tests/assistant.test.mjs:27:10)
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt/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 (114.037998ms)
|
||||
✖ a gated raise by a role that does not hear from the human is refused and stores nothing (109.454815ms)
|
||||
✔ a route the raiser hears from is allowed, checked from the route to the raiser (128.962936ms)
|
||||
✖ a refused supersede withdraws nothing (115.426296ms)
|
||||
✔ the assistant's within-role and gated raises still work (107.168921ms)
|
||||
ℹ tests 5
|
||||
ℹ suites 0
|
||||
ℹ pass 2
|
||||
ℹ fail 3
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 628.959197
|
||||
|
||||
✖ 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 (114.037998ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt/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.454815ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt/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 (115.426296ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt/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 (100.109522ms)
|
||||
✖ a gated raise by a role that does not hear from the human is refused and stores nothing (104.516241ms)
|
||||
✖ a route the raiser hears from is allowed, checked from the route to the raiser (117.038068ms)
|
||||
✖ a refused supersede withdraws nothing (111.487252ms)
|
||||
✔ the assistant's within-role and gated raises still work (115.110466ms)
|
||||
ℹ tests 5
|
||||
ℹ suites 0
|
||||
ℹ pass 1
|
||||
ℹ fail 4
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 601.697344
|
||||
|
||||
✖ 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 (100.109522ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt/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 (104.516241ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt/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 (117.038068ms)
|
||||
Error: decision-route-refused
|
||||
at fail (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:76:9)
|
||||
at #checkMessaging (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:341:48)
|
||||
at #dispatch (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:848:56)
|
||||
at file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:728:38
|
||||
at Store.transaction (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/store.mjs:156:22)
|
||||
at Broker.request (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:722:26)
|
||||
at call (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/tests/decision-route.test.mjs:62:44)
|
||||
at raise (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/tests/decision-route.test.mjs:64:5)
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt/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 (111.487252ms)
|
||||
AssertionError [ERR_ASSERTION]: Missing expected exception.
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt/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 (118.766253ms)
|
||||
✔ a gated raise by a role that does not hear from the human is refused and stores nothing (118.859595ms)
|
||||
✔ a route the raiser hears from is allowed, checked from the route to the raiser (135.928871ms)
|
||||
✔ a refused supersede withdraws nothing (116.619906ms)
|
||||
✖ the assistant's within-role and gated raises still work (119.579577ms)
|
||||
ℹ tests 5
|
||||
ℹ suites 0
|
||||
ℹ pass 4
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 661.485091
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/decision-route.test.mjs:129:1
|
||||
✖ the assistant's within-role and gated raises still work (119.579577ms)
|
||||
Error: decision-route-refused
|
||||
at fail (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:76:9)
|
||||
at #checkMessaging (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:341:48)
|
||||
at #dispatch (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:848:29)
|
||||
at file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:728:38
|
||||
at Store.transaction (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/store.mjs:156:22)
|
||||
at Broker.request (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:722:26)
|
||||
at call (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/tests/decision-route.test.mjs:62:44)
|
||||
at raise (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/tests/decision-route.test.mjs:64:5)
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/dewey-scratch/r133/wt/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 (117.97674ms)
|
||||
✖ a gated raise by a role that does not hear from the human is refused and stores nothing (106.649961ms)
|
||||
✔ a route the raiser hears from is allowed, checked from the route to the raiser (137.041565ms)
|
||||
✖ a refused supersede withdraws nothing (104.635942ms)
|
||||
✔ the assistant's within-role and gated raises still work (110.623837ms)
|
||||
ℹ tests 5
|
||||
ℹ suites 0
|
||||
ℹ pass 2
|
||||
ℹ fail 3
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 630.670653
|
||||
|
||||
✖ 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 (117.97674ms)
|
||||
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/wt/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/wt/packages/bus/src/broker.mjs:76:9)
|
||||
at #checkMessaging (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:343:63)
|
||||
at #dispatch (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:848:56)
|
||||
at file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:728:38
|
||||
at Store.transaction (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/store.mjs:156:22)
|
||||
at Broker.request (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:722:26)
|
||||
at call (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/tests/decision-route.test.mjs:62:44)
|
||||
at raise (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/tests/decision-route.test.mjs:64:5)
|
||||
at assert.throws.code (file:///home/jwoltje/dewey-scratch/r133/wt/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 (106.649961ms)
|
||||
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/wt/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/wt/packages/bus/src/broker.mjs:76:9)
|
||||
at #checkMessaging (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:343:63)
|
||||
at #dispatch (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:848:56)
|
||||
at file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:728:38
|
||||
at Store.transaction (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/store.mjs:156:22)
|
||||
at Broker.request (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:722:26)
|
||||
at call (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/tests/decision-route.test.mjs:62:44)
|
||||
at raise (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/tests/decision-route.test.mjs:64:5)
|
||||
at assert.throws.code (file:///home/jwoltje/dewey-scratch/r133/wt/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 (104.635942ms)
|
||||
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/wt/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/wt/packages/bus/src/broker.mjs:76:9)
|
||||
at #checkMessaging (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:343:63)
|
||||
at #dispatch (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:848:56)
|
||||
at file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:728:38
|
||||
at Store.transaction (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/store.mjs:156:22)
|
||||
at Broker.request (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/src/broker.mjs:722:26)
|
||||
at call (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/tests/decision-route.test.mjs:62:44)
|
||||
at raise (file:///home/jwoltje/dewey-scratch/r133/wt/packages/bus/tests/decision-route.test.mjs:64:5)
|
||||
at assert.throws.code (file:///home/jwoltje/dewey-scratch/r133/wt/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 (4.549773ms)
|
||||
✔ the shipped file is exactly the approved list (0.567962ms)
|
||||
✔ the assistant's authority is message.send within role and nothing else (1.430885ms)
|
||||
✔ the assistant talks to the user and the chief of staff only, and no other shipped role is limited (0.517688ms)
|
||||
✔ a business grant through limits does not widen the role (1.106688ms)
|
||||
✔ a launch block whose by is the assistant is refused (1.208324ms)
|
||||
✔ the instance takes no credential references and no tracker (1.8055ms)
|
||||
✔ an arbiter whose role definition declares messaging lists is refused, for each kind (2.736963ms)
|
||||
✔ an arbiter without messaging lists is accepted beside the limited assistant (1.410618ms)
|
||||
✔ the contract passes work to the chief of staff and directs no one (0.497261ms)
|
||||
ℹ tests 10
|
||||
ℹ suites 0
|
||||
ℹ pass 10
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 66.486302
|
||||
@@ -0,0 +1,13 @@
|
||||
✔ a cross-role raise whose arbiter the raiser does not hear from is refused and stores nothing (130.371093ms)
|
||||
✔ a gated raise by a role that does not hear from the human is refused and stores nothing (116.396112ms)
|
||||
✔ a route the raiser hears from is allowed, checked from the route to the raiser (132.69987ms)
|
||||
✔ a refused supersede withdraws nothing (118.187868ms)
|
||||
✔ the assistant's within-role and gated raises still work (117.502637ms)
|
||||
ℹ tests 5
|
||||
ℹ suites 0
|
||||
ℹ pass 5
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 668.909481
|
||||
@@ -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,23 @@
|
||||
ffcce1ae4dcee7fdee8f58136e8cba87bc4c417041a5b2250228144338228765 BUILD.md
|
||||
9f74cb67c3cc575a958a7a2e00ede4be5c5ec962a28d7f5d79ea72b58ee43900 MUTANTS.md
|
||||
303c6ca093d432a62fa0d92c0f60e0672765d29ddf94d6379079316dbf1239f3 base.txt
|
||||
ae9a7630e10251edc956defb45b4aedac4914cec3f9b50d2caf956a46919ca25 build.patch
|
||||
edb43aed618ef11af55effeb161060c398c300ad0c3dd6ae90c22d7552f4ddd4 candidate-manifest.sha256
|
||||
607fe11a9841de972a9e77685dc0b4749a63c93154843317fc536ceffc5d44fa gate-run.log
|
||||
11ca232b4223262b6926f8aad5cb784e14c5975fb338a9a4f8ffc030bb40a9d9 gate.sh
|
||||
e1985aa4098ace897959f5671cfb270a64e7154e30277c6af9aadb9c9b5f74b7 gate/pkg-bus.log
|
||||
e58a4819dabc598c5d7369d6484ae464c68cf393a890cddee6d76070476cea70 gate/pkg-business.log
|
||||
34a9376336f7b8e66670c5fd63144415040dea59846f8967bd885b30cfee870b gate/pkg-cli.log
|
||||
56508089225469d0f785d042211593a192058db1c3f9cbd5125eee6539df0b0b gate/pkg-tasks.log
|
||||
cf3f325acbf8f4a75cdf29a3688cb412d323e9f73e371dd9ad46c6cbb80827ea gate/pkg-webui.log
|
||||
5ae7c330b9bce8988e7e90e6674bc003c4170477bc52fa7202b08db2047b5b84 mutants-run.log
|
||||
781541695faafcf47fe470574ba710f48c226c0f95813ddafc6c29b430d489eb mutants.py
|
||||
8921a79421d3a8e7e1fa09d0dfbc6b93178559fe9f53fc75eccda8f588505fa1 mutants/01-validator-check.log
|
||||
817ba313b8085fca885ec3b3f6ffe2fddb988a80fcb52f23d742863572320fbf mutants/02-raise-check.log
|
||||
9949eaec7b4f95dfaf5f1307ac55c514753523e55520e1310bce5ebcca76cb5a mutants/03-raise-direction.log
|
||||
2b83db35632567161d4dbf429898859253858a662dffebf9ab54bdc1decb954f mutants/04-within-role-exempt.log
|
||||
4c19152f21e755fd4bf70b21240aa80be5ad0c87b6499072d852a569c0f81f5e mutants/05-raise-own-code.log
|
||||
79c999ac02e9b00000e924c21916c386a9a229d9e861294cae56b09600dbd93f mutants/control-01.log
|
||||
48e4f067179ce137a6a2d6a208a5a7cbe06292782c6b1184e6bb00d7c3df03a8 mutants/control-02.log
|
||||
36349265ed1d31d048209fb2408f91264f8fffec23202fc00a47a6b6f84e0526 mutants/control.json
|
||||
c152e771d671ffdf8d99428876ca3eb28ac78ec894546dbed36ed3a3ba466550 mutants/results.json
|
||||
Reference in New Issue
Block a user