Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88ffecaf75 | ||
|
|
80793d824e | ||
|
|
ad6ca79304 | ||
|
|
d3059c2c3d | ||
|
|
6f07930679 | ||
|
|
b01e1c9ee1 | ||
|
|
8716e72f26 | ||
|
|
9beeeb9955 | ||
|
|
9e43fefe60 | ||
|
|
d0460ed475 | ||
|
|
26f93fa746 | ||
|
|
c9e67e7d2d | ||
|
|
52d2a16109 | ||
|
|
afef0ea887 | ||
|
|
09d0d6fafa | ||
|
|
ffbc0d7a6d | ||
|
|
756284e8e9 |
No files matched your search
@@ -5381,3 +5381,4 @@ Gander: after Duckworth-0100 design approval with C1-C5, prepared the two host-s
|
||||
|
||||
2026-10-23T02:21:06Z | Sage, rows 270+271 landing, 571 landing, MVP rows | Before: 270 (#1775) and 271 (#1776) were approved on gizmoduck/row270-271-r4 49cee846e and 2b964a082 after combined gate C2 run 5 GREEN on 53406f3e4 (bus 873, seat 202, cli 905, ten scripts, 38/38 mutants). After: 33 commits cherry-picked onto 767bb62c7 as 7328da8ca, range-diff all "=", 40 paths identical to the candidate; 270 manifest f4174458 matches, 271 manifest 3ab1f5c8 matches on 29 paths, the other 4 moved under 270 and match its manifest. Done revs 3396/3398, #1775 c30962 and #1776 c30964 closed, P19 463 c861. 571 (Gyro, Fenton APPROVE) picked as ee5f7b9ef, 6345aae3d, 767bb62c7 (blobs 9382f85b, 7ec0897b), 1ac76a417 left out; N7 911dad2aa (Fenton-0014) picked after. Rows 293-299 added (574, FED R6, R4, R7, GitHub grant, Q228, 573), queue commit 253346e3c. Correction: the push sync step checked out O..S paths over the canonical worktree without testing for uncommitted edits, so any pending SESSIONS or BUILD-LOG line in the canonical checkout at 02:06Z would have been overwritten; the sync now skips edited paths. Schema 3n is not live (LIVE 0cbe2e9b3 at 3m).
|
||||
2026-10-23T03:59:52Z | Sage, row 293 (Vikunja 574) landing and 3m-3n live window | Before: live 0cbe2e9b3 at schema 3m; 574 approved by Filbert (c30982, cdda41f5) on gander/v574-r2 a9cae2bd6 after run 2 GREEN on 6bfab273b (four files 153/0, seat 202/0, cli 905/0, 312 flagged commits with 0 maintenance children). After: Darkwing's window put live on 464010fbe at 3n (activation 20261023T034557Z-464010fbe4c6, 26/97/5/28, Console 7332 200, W9b pm ra38826f3c6e2 left live, STATE darkwing/window-migrate-3n 8708927e5); 574's six commits cherry-picked onto 464010fbe as e51d11640, range-diff all =, patch-ids equal, only docs differ from a9cae2bd6. #1805 closed c31013, P19 386 c887 and 574 c888. Residual: queue store.mjs, discord git.mjs, harness tools.mjs commit without the maintenance flags; no whole-root test covers them yet.
|
||||
2026-10-23T05:47:17Z | Sage, row 292 (#1797) landing, idle-stop race row 302, rows 300/301, row 294 rounds 2-3 | Before: refactor 4a9c536e8; 292's gate on 0d0021454 hit one red at compactions-launch.test.mjs:229, ['session-invalid',22,null] where ['stopped',0,null] was expected, product and test unchanged. Gizmoduck read it as a race: threadIdle releases the claim, then the launcher sends SIGTERM, and a runner call in between gets not-holder and exits 22. Mos (mos-1411) rejected loosening :229, let 292 count one whole unedited green cli rerun, and allowed one rerun for that exact signature on other gates until the race row lands (hits logged on Vikunja 588). The 10-run diagnostic passed 10/10, but one runner log (r846b8d083502) showed not-holder then exit 22. After: the cli rerun was 910/910. 292 LANDED 6aab979e6 05:18Z (7 cherry-picks, range-diff all =, patch-ids equal), done rev 3447, #1797 closed c31038, Vikunja 588 c891. Briefs 86d5ac69b (R6-N1 #1807, R6-N4 #1808, rows 300/301) and e3ef1eaf6/fdedc56e2 (race, #1809, row 302). Row 302 design d1/d2 approved by Dewey with D1 (role.claim refuses a released run), c31044. Mos ruled landRelease folded in, code launcher-released, and four old-test lines moved to exact values (mos-1414/1415/1416): threads.test:394, runner.test:771-772, land.test:464. Row 294 round 2 stopped at webui G5 granted-user.test:269: max(seq) over an empty events table is NULL. Round 3 is one line, coalesce(max(seq),0) (mos-1417), Duckworth APPROVE c31045 rev 3462, gate pending. Queue commit 756284e8e (revs 3448-3456). Correction: two review-request attempts this window ran without MOSAIC_GITEA_CREDENTIAL_FILE exported and failed (3450 Launchpad, 3451/3452 Sage); the posted request was 3453/3454.
|
||||
@@ -1,111 +0,0 @@
|
||||
# Row 302 (#1809): assertion preservation
|
||||
|
||||
Base 0e30a85fa. `git diff 0e30a85fa -- 'packages/*/tests/*'` on an existing file changes four lines
|
||||
and nothing else. Each one is ruled by Mos (mos-1414, amended by mos-1415 and mos-1416, relayed in
|
||||
sage-3206, sage-3209 and sage-3210). No assertion is removed or loosened. Each one now pins the new
|
||||
code or exit for the same call.
|
||||
|
||||
## 1. packages/bus/tests/threads.test.mjs:394 (mos-1414 a)
|
||||
|
||||
Old:
|
||||
|
||||
```
|
||||
assert.throws(() => call('a', 'message.receive'), { code: 'not-holder' });
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```
|
||||
assert.throws(() => call('a', 'message.receive'), { code: 'launcher-released' });
|
||||
```
|
||||
|
||||
:392 (threadIdle's own second refusal, not-holder) and :395 (the pending check) are unchanged.
|
||||
|
||||
## 2. packages/harness/tests/runner.test.mjs:771 (mos-1415)
|
||||
|
||||
Old:
|
||||
|
||||
```
|
||||
assert.equal(out.code, EXIT.session, out.stderr);
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```
|
||||
assert.equal(out.code, 0, out.stderr);
|
||||
```
|
||||
|
||||
out.stderr stays as the message.
|
||||
|
||||
## 3. packages/harness/tests/runner.test.mjs:772 (mos-1415)
|
||||
|
||||
Old:
|
||||
|
||||
```
|
||||
assert.match(out.stderr, new RegExp(`runner: landing turn 1 for decision ${d1.id} \\(vikunja:1/2\\): ok\n[\\s\\S]*runner: session stopped working: not-holder\n`));
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```
|
||||
assert.match(out.stderr, new RegExp(`runner: landing turn 1 for decision ${d1.id} \\(vikunja:1/2\\): ok\n[\\s\\S]*runner: session released by the launcher\n`));
|
||||
```
|
||||
|
||||
Only the last line of the pattern changes. The landing-turn part is the same, word for word.
|
||||
|
||||
## 4. packages/bus/tests/land.test.mjs:464 (mos-1416)
|
||||
|
||||
Old:
|
||||
|
||||
```
|
||||
assert.throws(() => w.call('merge-gate', 'message.receive'), /not-holder/);
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```
|
||||
assert.throws(() => w.call('merge-gate', 'message.receive'), /launcher-released/);
|
||||
```
|
||||
|
||||
:457, :463 and :466 are unchanged.
|
||||
|
||||
## The not-holder grep, rerun on the build (mos-1414)
|
||||
|
||||
`git grep -n "not-holder\|session stopped working\|session-invalid"` over packages/cli, seat and harness
|
||||
tests and scripts/, outside the new files:
|
||||
|
||||
- runner.test.mjs:558, `session stopped working: unauthenticated`. Not a release. Stays.
|
||||
- runner.test.mjs:628 (a comment) and :647, the limit stop. The limit revoke isn't threadIdle or
|
||||
landRelease, so it stays not-holder and exit 22 (Dewey Q3).
|
||||
- session.test.mjs:116, a list of end reasons. No call.
|
||||
|
||||
In packages/bus/tests, only end-launch, land and threads call threadIdle or landRelease. Their other
|
||||
not-holder pins: end-launch:198, threads:383-385 and :392, land:457 and :463 are the ops' own refusals.
|
||||
threads:493 follows assistant.stop. threads:416 releases a2-run as the test's last line. The fix reaches
|
||||
none of them, so no hit goes to Mos.
|
||||
|
||||
## 5. packages/bus/tests/launcher-released.test.mjs:89, a new test's own line (d6, sage-3294, Mos ruling sage-3293)
|
||||
|
||||
This file is new in row 302, so nothing at the base changes. The line changed between the pushed tests
|
||||
commit f799e8b15 and d6, after the first gate run went red on it (gizmoduck-0388). It's recorded here
|
||||
because the change narrows what the line counts.
|
||||
|
||||
Old:
|
||||
|
||||
```
|
||||
assert.equal(store.get('SELECT count(*) AS n FROM deliveries WHERE message=?', later.id).n, 0, 'nothing delivered to the released run');
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```
|
||||
assert.equal(store.get('SELECT count(*) AS n FROM deliveries WHERE message=? AND holder_run=?', later.id, 'a-run').n, 0, 'nothing delivered to the released run');
|
||||
assert.deepEqual(store.all('SELECT op, holder_run FROM deliveries WHERE message=?', later.id).map((r) => ({ ...r })), [{ op: 'routed', holder_run: null }]);
|
||||
```
|
||||
|
||||
The old line counted every delivery row for the message. `#route` (broker.mjs:6482) writes one routed
|
||||
row for each message, and `#recipient` (:832-834) gives it holder_run null when no run holds the role.
|
||||
After threadIdle no run holds thread A, so the count was 1, with no row for a-run. The first new line
|
||||
keeps the claim it was meant to make: no row names the released run. The second pins the whole set,
|
||||
one routed row held by nobody. That is tighter than the old line on everything except the null-holder
|
||||
row, which it requires.
|
||||
@@ -1,444 +0,0 @@
|
||||
# Row 302 (#1809, Vikunja 588): the idle-stop race
|
||||
|
||||
Owner Gizmoduck. Reviewer Dewey. Gate owner Sage. Brief:
|
||||
docs/plans/2026-10-23_idle-stop-race-row.md, "Brief: the idle-stop race (Vikunja 588)". Base 0e30a85fa.
|
||||
This is the design, written static only. Nothing gets built until Dewey approves it (sage-3200).
|
||||
|
||||
## The race, as the code has it at 0e30a85fa
|
||||
|
||||
- `packages/cli/src/launcher.mjs:703-717`. The idle tick calls the `threadIdle` op. On `released` it adds
|
||||
the run to `stopping`, records `thread-idle`, and only then sends SIGTERM (:716).
|
||||
- `packages/bus/src/broker.mjs:1044-1057`. `threadIdle` releases the claim in the same synchronous call
|
||||
(`#claimEnd(..., 'release', null)`, :1055). The comment at :1040-1043 says the order is deliberate:
|
||||
row 121 r2, Dewey's F3 in 28591.
|
||||
- `broker.mjs:1002-1008`. `#agent` is the only place a thread run's own call gets `not-holder`, because
|
||||
its session's latest claim row isn't a `claim` of its generation. `message.receive` (:5586-5591) and
|
||||
`state` (:5805-5807) both go through it, and so do `message.send` and `message.read`.
|
||||
- `packages/harness/src/runner.mjs:55`. `SESSION_GONE` holds `unauthenticated`, `not-holder` and
|
||||
`run-ended`. Four sites set `code = EXIT.session` (22) on any of them, without checking `stopping`:
|
||||
the landing `state` (:500-506), `message.receive` (:526-531), the turn's `state` (:545-552) and the
|
||||
reply (:575-582).
|
||||
- `packages/seat/src/session.mjs:340` maps 22 to `session-invalid`.
|
||||
|
||||
So if a runner call reaches the broker after the release and before SIGTERM sets `stopping`, the run
|
||||
exits 22 by itself and ends `session-invalid`. A call already in flight when the release lands counts
|
||||
too. Row 292's diagnostic saw this once in about 100 idle stops. The clear test's run r846b8d083502 logged
|
||||
`not-holder` and then `exiting 22`.
|
||||
|
||||
## Design: a distinct answer for a run the idle stop released (Mos's suggestion, adopted)
|
||||
|
||||
### Broker (`packages/bus/src/broker.mjs`)
|
||||
|
||||
1. Add a new in-memory set, `#idled`, keyed `business:run`, next to `#ended`, `#received` and `#stops`.
|
||||
`threadIdle` adds the run's key in the same synchronous call that releases it, and only when it
|
||||
returns `{released:true}`. Release and mark happen together, so no call can be answered between them.
|
||||
2. In `#agent`, the refusal becomes
|
||||
`fail(this.#idled.has(s.business + ':' + s.run) ? 'idle-released' : 'not-holder')`. Nothing else in
|
||||
`#agent` changes. A run the idle stop released now gets `idle-released` at every call after the
|
||||
release, whether it's a receive, `state`, a send or a read, and whether or not another run has
|
||||
claimed the session since. Every other path that takes a claim away still answers `not-holder`:
|
||||
- `assistant.stop` (:5975), `instance.end` (:6009), the chief lifecycle revoke (:4682) and
|
||||
`role.revoke` (:5230, :5235)
|
||||
- the session clear (:1146)
|
||||
- the limit stop (`#limitStop`, :4842-4855). It releases a thread claim and then refuses
|
||||
`not-holder` on purpose, so that the run exits 22.
|
||||
- another run's newer generation, where the run wasn't idle-released
|
||||
3. `endLaunch` (:1300) drops the key along with `#ended.add`. After the end, the run's caps are gone and
|
||||
its calls answer `unauthenticated`, as they do today. The set lives only in memory, like `#runs`. A
|
||||
broker that restarts loses `#runs` too, so an old run's calls answer `unauthenticated` there either
|
||||
way.
|
||||
4. `threadIdle`'s own refusals don't change. A second `threadIdle` on a released run is still
|
||||
`not-holder` (threads.test:392). The answer comes from the `#agent` path only, for the run's own
|
||||
calls.
|
||||
|
||||
Why a new code and not a flag on `not-holder`: the wire carries only `error: <code>`
|
||||
(server.mjs:96-160), and the client accepts only `^[a-z-]{1,64}$` (client.mjs:42-43). A new code changes
|
||||
neither of them. Tools and docs that switch on `not-holder` keep their meaning, because `not-holder`
|
||||
still means "your claim was taken away".
|
||||
|
||||
### Runner (`packages/harness/src/runner.mjs`)
|
||||
|
||||
5. A new constant, `IDLE_RELEASED = "idle-released"`. It is not added to `SESSION_GONE`. At each of the
|
||||
four sites, `idle-released` is checked before `SESSION_GONE`. The runner logs
|
||||
`runner: session released by the launcher's idle stop`, leaves `code` at `EXIT.stopped` (0), sets a
|
||||
local `released`, and leaves the loop the same way the site does for `SESSION_GONE` today.
|
||||
6. The `role.release` at exit (:589-595) is skipped when `released`. The claim is already released, so
|
||||
the call would only be refused and logged.
|
||||
7. If the runner didn't handle the code, the receive loop would count `idle-released` as an
|
||||
unreachable broker and retry (:532-537). The run would then end at SIGTERM (0), or after 30 polls
|
||||
(23). Every site handles it so that neither of those decides how the run ends.
|
||||
|
||||
How this meets mos-1411 condition 1: `unauthenticated` (the caps went at the end), `run-ended`, and every
|
||||
`not-holder` that isn't an idle release still exit 22 and end `session-invalid`. Only the broker's own
|
||||
record that `threadIdle` released this run maps to `stopped/0`. The launcher's `stopping` set and SIGTERM
|
||||
play no part in the mapping. That also covers a poll that lands before the signal, and a call that was
|
||||
in flight when the release landed.
|
||||
|
||||
### Launcher and session.mjs
|
||||
|
||||
8. No change. The runner exits 0 by itself, and SIGTERM's start-time check (:716) then finds no process,
|
||||
which the code already handles. `endReason(0)` is `stopped`.
|
||||
|
||||
## Tests
|
||||
|
||||
### The forcing test (mos-1411 condition 2): new file `packages/cli/tests/idle-stop-race.test.mjs`
|
||||
|
||||
This is a thread launch on the same fixture shape as thread-launch.test.mjs (`start(t, prepared, { ops })`
|
||||
replaces launch ops, :78-101), with `sessionDefaults.pollInterval` set to 100. The test replaces the
|
||||
`threadIdle` op:
|
||||
|
||||
```js
|
||||
threadIdle: async (m) => {
|
||||
const r = runtime.threadIdle(m.record);
|
||||
if (r.released) window.set(m.record.run, await held(m.record.run)); // returns only after the runner's own exit, or 20 s
|
||||
return r;
|
||||
}
|
||||
```
|
||||
|
||||
`held(run)` waits for two things, in order. First, an `action.refused` event whose actor is `run` and
|
||||
whose seq is after the release's `thread_claims` row. That proves a runner call reached the broker inside
|
||||
the window. Second, the runner's own exit (the run's `runner: exiting N` line in runner.log, or its pid
|
||||
gone), with no SIGTERM sent. The launcher sends SIGTERM only after the op returns, so the window stays
|
||||
open until the runner has decided how it ends. The test then asserts all of the following:
|
||||
|
||||
- `held` saw a refusal from the run after the release, and the runner exited before the op returned.
|
||||
- The refusal's code is `idle-released`.
|
||||
- The run's launch `end` event is `['stopped', 0, null]`.
|
||||
- runner.log has the idle-released line and no `session stopped working:` line.
|
||||
|
||||
Against the code today, the refusal is `not-holder`, the runner exits 22 inside the window, and the end
|
||||
is `['session-invalid', 22, null]`. The test fails, and fails every time, not at some rate: the
|
||||
window is held open until a call has landed in it. If the runner fix is dropped, the runner retries and
|
||||
doesn't exit inside the window, so `held` times out and the "exited before the op returned" assertion
|
||||
fails. If the broker fix is dropped, the end is 22.
|
||||
|
||||
A control test in the same file: after an idle release, a `role.revoke` the human approved on another
|
||||
thread's run (or `assistant.stop`) still ends that run `['session-invalid', 22, null]`. That shows a
|
||||
real not-holder still exits 22.
|
||||
|
||||
### Bus: new tests in `packages/bus/tests/idle-release.test.mjs` (a new file, so no existing assertion moves)
|
||||
|
||||
- After `threadIdle` releases a-run, a-run's `message.receive`, `state`, `message.send` and
|
||||
`message.read` each refuse `idle-released`. Each writes an `action.refused` event with code
|
||||
`idle-released`, and none of them delivers a message or writes a receipt.
|
||||
- `not-holder` still comes back for: a run released by `assistant.stop`, a run released by a session
|
||||
clear, a limit stop (stop then receive), a run whose session another run claimed without any idle
|
||||
release, and a second `threadIdle` on a released run. After `endLaunch`, the idle-released run's
|
||||
calls get `unauthenticated`, as today (its caps go at :1310-1311).
|
||||
- A role run's `role.release` followed by its own receive still gets `not-holder` (no idle).
|
||||
|
||||
### Harness: no new runner.test case
|
||||
|
||||
runner.test.mjs's `setup` (:105) binds role runs only, and its claims sit in `role_claims` (:876), so it
|
||||
can't reach `threadIdle`. The runner's own exit at receive is covered by the cli forcing test above,
|
||||
which runs the real runner. The forcing test reaches only the site that lands in the window, which in
|
||||
practice is the receive poll. The landing `state`, the turn's `state` and the reply sites get the same
|
||||
four lines, but no test forces each one. Q4 asks whether Dewey wants a per-site case. The existing 22
|
||||
tests (:539-560, :630-647, :772) stay unchanged and pass unchanged.
|
||||
|
||||
## The one existing assertion that changes (goes to Mos through Sage, per the brief)
|
||||
|
||||
`packages/bus/tests/threads.test.mjs:394`,
|
||||
`assert.throws(() => call('a', 'message.receive'), { code: 'not-holder' })`. This is a receive by the run
|
||||
that `threadIdle` released at :388, so under this design it answers `idle-released`. The assertion is
|
||||
changed, not loosened: the new pin is `{ code: 'idle-released' }`, and the property right below it
|
||||
(:395, the message stays `pending` for the next holder) stays. I've found no other existing assertion
|
||||
that pins `not-holder` for an idle-released run's own call. The build reruns
|
||||
`git grep -n "not-holder" 'packages/*/tests/*'` against every idle path before in-review, and names any
|
||||
other hit for Mos first.
|
||||
|
||||
## Docs
|
||||
|
||||
- packages/bus/README.md:320-326 (threadIdle). Add: after a release, the run's own calls answer
|
||||
`idle-released`, not `not-holder`. Also say why.
|
||||
- packages/harness/README.md:295-302, the exit table. Row 0 becomes "stopped, by a signal, or because the
|
||||
launcher's idle stop released the session (`idle-released`)". Row 22 is unchanged.
|
||||
- packages/bus/README.md:207 (`assistant.stop`): no change. It still gives `not-holder`.
|
||||
|
||||
## Open questions for Dewey
|
||||
|
||||
- Q1. `landRelease` (broker.mjs:4144-4165, launcher.mjs:876-889) has the same shape for a role run's
|
||||
land-relaunch: it releases, then the launcher sends SIGTERM, so a poll in between ends the run 22. I
|
||||
recommend folding it in. That means one line in `landRelease` (add to `#idled` on release), one bus
|
||||
assertion, and one cli forcing case like the one above. It falls outside this brief's words, so it
|
||||
needs Sage's yes. If the answer is no, I'll write it up as a follow-up row with the same evidence.
|
||||
- Q2. The code name `idle-released`. It names the cause, and if Q1 is yes it covers land-relaunch too,
|
||||
since that is also a release made by the launcher. A more general name like `launcher-released` would
|
||||
fit both. I prefer `released-by-launcher` only if Q1 is yes. Dewey picks.
|
||||
- Q3. A limit stop on a run that was already idle-released. `#limitStop` (:4842-4855) finds no claim to
|
||||
end and refuses `not-holder`, so the run ends 22. I'd leave that alone: a limit hit is a real stop and
|
||||
ends session-invalid, as it does today. It's possible only if the limit checker answers inside the
|
||||
window.
|
||||
|
||||
- Q4. Per-site coverage in the runner. Only the receive site is forced. I can add a cli case per site
|
||||
(hold the window until the run's landing `state`, or a turn's `state`, lands in it), at about one
|
||||
fixture each. I'd add them only if Dewey asks, since all four sites share one helper in the build.
|
||||
|
||||
## Gate (brief)
|
||||
|
||||
Dewey approves this design, then the manifest. Runs: the new forcing test, then
|
||||
compactions-launch.test.mjs whole, then the bus, cli and harness suites whole, then every
|
||||
`scripts/test-*.sh`. The mos-1411 point 4 rerun rule doesn't apply to this row's own gate.
|
||||
|
||||
Mutants. Each one must die on the forcing test, with the bus test also named where it applies:
|
||||
|
||||
- M1. `threadIdle` doesn't add to `#idled`. Dies on the forcing test (end 22) and the bus test.
|
||||
- M2. `#agent` ignores `#idled`. Same deaths as M1.
|
||||
- M3. The runner drops the `idle-released` branch at receive. Dies on the forcing test (no exit inside
|
||||
the window).
|
||||
- M4. The runner maps `idle-released` to `EXIT.session`. Dies on the forcing test (end 22).
|
||||
- M5. `#claimEnd` marks every release, not just threadIdle's. Dies on the bus not-holder controls and
|
||||
the cli control.
|
||||
- M6. `endLaunch` doesn't drop the key. This is not a correctness mutant: caps are gone, so the answer
|
||||
is `unauthenticated` either way. I'll drop it unless Dewey wants an assertion on it, and record it as
|
||||
an equivalent mutant.
|
||||
|
||||
Limits will be set in the run plan after the design round (mos-1275: `timeout -k 10` at three times or
|
||||
more the longest run).
|
||||
|
||||
## After Dewey's d1 review (dewey-0875, 05:32Z): APPROVE on D1, folded here before any build
|
||||
|
||||
D1 (required). role.claim's thread branch (broker.mjs:5154-5173) doesn't go through `#agent` and
|
||||
refuses only `run-revoked`, the session fence and `thread-already-held`. So an idle-released run could
|
||||
claim its free session again. Its key would still be in `#idled`, and a later real not-holder on that
|
||||
claim would answer `idle-released` and exit 0. Fix: the thread branch of role.claim refuses
|
||||
`idle-released` for a run in `#idled`, before the insert. The key stays, because the run's claim is over.
|
||||
Bus assertion: after threadIdle releases a-run, a-run's role.claim refuses `idle-released`, and no
|
||||
thread_claims row is added. Mutant M7: drop that check. It dies on that assertion.
|
||||
|
||||
Correction to "Harness: no new runner.test case". That's true for threadIdle only. runner.test reaches
|
||||
landRelease, which is a role-run path: :742-772 releases coder-run with `ctx.broker.landRelease` and no
|
||||
launcher, and pins `out.code === EXIT.session` (:771) and `session stopped working: not-holder` (:772).
|
||||
If Q1 is yes, those two lines change (to stopped/0 and the released line). They go to Mos through Sage
|
||||
with threads.test:394, and runner.test:742-772 then serves as the land-relaunch forcing case. No launcher means no
|
||||
SIGTERM, so the receive always lands after the release. The cli case for land-relaunch is dropped. If Q1
|
||||
is no, the follow-up row cites runner.test:771-772 as its evidence.
|
||||
|
||||
Q2. The code is `launcher-released` if Q1 is yes, and `idle-released` if Q1 is no. Every name in this
|
||||
file reads that way.
|
||||
|
||||
Q3. Agreed. A limit stop on an idled run stays not-holder and exits 22.
|
||||
|
||||
Q4. No per-site cases. Both ops return `released:false` while a delivered message is unread
|
||||
(threadIdle via `t.unread`, landRelease via its count). The runner reads only after the reply
|
||||
(runner.mjs:575-580), so a run can't be released mid-turn. landRelease keeps a run that hasn't received
|
||||
yet (`landing:true`), and thread runs have no landing. So only the receive site can see the code. All
|
||||
four sites keep the shared helper, and the three unreachable ones get a one-line comment saying why.
|
||||
|
||||
N1. The cli control must reach the runner before any launcher signal. It uses a role.revoke on the run
|
||||
(no launcher stop), or holds the signal the same way the forcing test does. assistant.stop gets checked
|
||||
first in case it goes through a launcher stop. It also asserts the run's runner.log has
|
||||
`session stopped working: not-holder` and `exiting 22`.
|
||||
N2. The control keeps "after an idle release", so `#idled` isn't empty when M5 runs.
|
||||
N3. M6 is equivalent. The key drop in endLaunch stays, so the set doesn't grow.
|
||||
N4. The other-pin grep also covers runner.test `session stopped working`. :558 (unauthenticated) and
|
||||
:647 (revoke) stay, and :772 changes only under Q1.
|
||||
Docs: if Q1 is yes, landRelease joins the bus README line and the harness exit-table row 0.
|
||||
|
||||
## Round d2 (sage-3204, 05:32Z)
|
||||
|
||||
Line numbers. Sage's check against 0e30a85fa is right, and the references above are now corrected. The
|
||||
second threadIdle not-holder pin is threads.test:392 and stays. The receive pin that changes is :394,
|
||||
and the pending check is :395 and stays. The idle release is at :388.
|
||||
|
||||
Sage asked for a grep of the land-relaunch tests for a not-holder pin on the relaunched run's own call.
|
||||
`git grep -l "landRelease\|land-relaunch" 'packages/*/tests/*'` lists 8 files. In those files,
|
||||
`not-holder|session stopped working|EXIT.session|session-invalid` hits only land.test.mjs and
|
||||
runner.test.mjs:
|
||||
|
||||
- land.test.mjs:464, `assert.throws(() => w.call('merge-gate', 'message.receive'), /not-holder/)`. This is
|
||||
the released run's own receive after `landRelease` released it (:458). Under Q1 yes, it answers
|
||||
`launcher-released`, and the regex fails. That's a third old-test change for Mos through Sage. The new
|
||||
pin is `/launcher-released/`.
|
||||
- land.test.mjs:457 (a non-holder run's landRelease) and :463 (a second landRelease) are landRelease's
|
||||
own refusals. They stay not-holder, the same as threads.test:392.
|
||||
- runner.test.mjs:771-772 change under Q1, as in the section above. runner.test.mjs:548, :557-558 and
|
||||
:646-647 are unauthenticated, unauthenticated and a limit stop. They stay.
|
||||
- The six cli files have no hit.
|
||||
|
||||
So under Q1 yes, the Mos package is threads.test:394, land.test:464 and runner.test:771-772. Under Q1 no,
|
||||
it's threads.test:394 alone. Nothing gets built until Mos rules and Sage gives the go.
|
||||
|
||||
## Round d3: as built (after mos-1414/1415/1416 and sage-3220)
|
||||
|
||||
Q1 is yes, so the code is `launcher-released` everywhere. The broker set is `#launcherReleased`, not
|
||||
`#idled`. Every `idle-released` above reads `launcher-released`.
|
||||
|
||||
### Correction to d1: message.send doesn't go through `#agent`
|
||||
|
||||
d1 said `message.send` and `message.read` both go through `#agent`. `message.read` does. `message.send`
|
||||
doesn't. So a released run's send isn't refused `launcher-released`. Only `message.receive`, `state`,
|
||||
`message.read` and `role.claim` answer it, and the bus test asserts exactly those four. That narrows
|
||||
what Dewey approved in d1, so I'm sending it to Dewey directly (sage-3220).
|
||||
|
||||
### Broker (packages/bus/src/broker.mjs)
|
||||
|
||||
- `#launcherReleased = new Set()`, keyed `business:run`, next to `#stops`, with a comment.
|
||||
- threadIdle and landRelease add the key after their release transaction.
|
||||
- `#agent` refuses `launcher-released` for a run in the set, and `not-holder` otherwise.
|
||||
- role.claim (D1) refuses `launcher-released` right after the `agent-required` check, before the thread
|
||||
and role branches. So it covers landRelease's role runs as well as thread runs.
|
||||
- endLaunch deletes the key after `#ended.add(key)`.
|
||||
- Q3 needs no code. A limit stop runs `#limitStop` (broker.mjs:4482) before any `#agent` check, and it
|
||||
fails `not-holder` itself (:4862), so a limit stop after a launcher release still exits 22.
|
||||
|
||||
### Runner (packages/harness/src/runner.mjs)
|
||||
|
||||
- `const LAUNCHER_RELEASED = "launcher-released"`.
|
||||
- The shared helper `ended(err, say = true)`. For `launcher-released`, it logs
|
||||
`runner: session released by the launcher`, sets `released` and leaves the exit code at
|
||||
`EXIT.stopped`. For the SESSION_GONE codes it keeps the old behaviour (`EXIT.session`, and the log
|
||||
line unless `say` is false).
|
||||
- It's used at the landing state check, receive, the turn state check and the reply. The reply site
|
||||
passes `say = false`, so its logging is the same as before. The three sites that can't see the code
|
||||
(Q4) carry a comment saying why.
|
||||
- At exit, the final `role.release` is skipped when `released`, because the launcher already released.
|
||||
|
||||
### Tests as built
|
||||
|
||||
- `packages/cli/tests/idle-stop-race.test.mjs`. This is the threadIdle forcing case. Inside the first
|
||||
released run's held threadIdle op, the human's `assistant.stop` revokes the other thread's holder
|
||||
(N1: no launcher signal can come while the op is held, launcher.mjs:662-663). The op waits for both
|
||||
runners' verdicts and end events. The idle run logs `session released by the launcher`, ends
|
||||
`["stopped", 0, null]` before its thread-idle line, and has `exiting 0` and no `session stopped working`.
|
||||
The control logs `session stopped working: not-holder`, ends `["session-invalid", 22, null]` and
|
||||
has `exiting 22`. The Store shows `launcher-released` and no `not-holder` refusals for the idle run,
|
||||
and `not-holder` for the control.
|
||||
- `packages/bus/tests/launcher-released.test.mjs` (renamed from idle-release.test.mjs). It has three
|
||||
tests. (1) threadIdle: receive, state, read and role.claim refuse `launcher-released`, role.claim adds
|
||||
no thread_claims row, the four refusals are logged, nothing is delivered, threadIdle's own second
|
||||
refusal stays not-holder, and after endLaunch the answer is unauthenticated. (2) The control (N2:
|
||||
after an idle release): assistant.stop's run answers not-holder and run-revoked, pm's own role.release
|
||||
answers not-holder, and a new holder for the released thread doesn't change the released run's answer.
|
||||
(3) landRelease: receive and state refuse `launcher-released`, role.claim refuses and adds no
|
||||
role_claims row, landRelease's own second refusal stays not-holder, and coder's own role.release
|
||||
answers not-holder.
|
||||
- runner.test.mjs:742-772 is the land-relaunch forcing case (mos-1415). There's no cli land-relaunch case.
|
||||
- The four old lines Mos ruled are in ASSERTION-PRESERVATION.md. The diff on old test files is those four
|
||||
lines and nothing else.
|
||||
|
||||
### Docs
|
||||
|
||||
The bus README's threadIdle paragraph names `launcher-released`, the four calls that answer it,
|
||||
landRelease, and that every other lost claim stays not-holder. The cli README's land-relaunch paragraph
|
||||
says calls between the release and the signal refuse `launcher-released` and the runner exits 0. The
|
||||
harness README's exit table row 0 names the launcher release.
|
||||
|
||||
### Red first (mos-1415)
|
||||
|
||||
There are two commits. The first holds only the tests and the four ruled pin edits. The second holds the
|
||||
fix (broker.mjs, runner.mjs) and the READMEs. The gate's first stage runs on the tests-only commit, and
|
||||
these two must fail there:
|
||||
|
||||
- idle-stop-race.test.mjs: the idle run ends `["session-invalid", 22, null]`, or its verdict is
|
||||
`session stopped working: not-holder`, whichever lands first.
|
||||
- runner.test.mjs, the case at :742: out.code is 22, not 0.
|
||||
|
||||
The bus test files and threads.test:394 and land.test:464 fail there too, and the packet records them.
|
||||
The red run doesn't count as the gate.
|
||||
|
||||
### Mutants (each run against the fix commit, restored and checked by sha256 after)
|
||||
|
||||
- M1. threadIdle doesn't add the key. Dies: idle-stop-race (end 22), launcher-released.test test 1,
|
||||
threads.test:394.
|
||||
- M2. `#agent` ignores the set. Dies: the same as M1, plus launcher-released.test test 3,
|
||||
land.test:464 and runner.test:771.
|
||||
- M3. The runner's helper drops the `launcher-released` branch. Dies: idle-stop-race (the run doesn't
|
||||
exit in the window, so `window.idle` is null) and runner.test:771-772.
|
||||
- M4. The runner maps `launcher-released` to `EXIT.session`. Dies: idle-stop-race (end 22) and
|
||||
runner.test:771.
|
||||
- M5. `#agent` answers `launcher-released` for every lost claim. Dies: the idle-stop-race control
|
||||
(verdict and end 22), launcher-released.test test 2, and test 3's coder line.
|
||||
- M6. endLaunch doesn't drop the key. Equivalent (N3), not run.
|
||||
- M7. role.claim drops the D1 check. Dies: launcher-released.test tests 1 and 3 (the claim succeeds).
|
||||
- M8. landRelease doesn't add the key. Dies: runner.test:771-772, land.test:464 and
|
||||
launcher-released.test test 3.
|
||||
|
||||
### Run plan (mos-1275 limits, `timeout -k 10`)
|
||||
|
||||
1. idle-stop-race.test.mjs alone, 600 s. Then runner.test.mjs whole, 900 s.
|
||||
2. compactions-launch.test.mjs whole, 600 s.
|
||||
3. The bus, harness and cli suites whole: bus 1200 s, harness 1200 s, cli 2100 s. 292's cli suite took
|
||||
630 s.
|
||||
4. Every scripts/test-*.sh, 900 s each.
|
||||
5. M1-M5, M7 and M8, each on its named files at the limits above.
|
||||
|
||||
TMPDIR comes from `mktemp -d /mnt/storage/scratch/tmp/gXXX`. Every run is slot work and starts only on a
|
||||
CLEAR from Sage.
|
||||
|
||||
## Round d4: Dewey's early static read (dewey-0878, 05:47Z)
|
||||
|
||||
C1. The d3 "correction" about message.send was wrong, and d1 was right. `message.send` calls
|
||||
`#consumeAuthority` (broker.mjs:5506), which calls `#checkAuthority`, which calls `#agent` (:3749).
|
||||
So a released run's send refuses `launcher-released`, and so does every verb that reaches `#agent`. There
|
||||
are 29 call sites, including role.release and every authority check. role.claim refuses it before the
|
||||
claim (D1). I misread the send path in d3, and no code depended on that reading. The runner's reply site
|
||||
already handles the code. The bus README now says "every call that needs the claim", not a list of four.
|
||||
Bus test 1 adds a message.send refusal, so the refused list is five `launcher-released`.
|
||||
|
||||
N1. Nothing killed a mutant that drops `&& !released` at runner exit (runner.mjs:596). That run calls
|
||||
role.release, logs `runner: role.release: launcher-released` and still exits 0. idle-stop-race now
|
||||
asserts the idle run's runner.log has no `runner: role.release:` line.
|
||||
- M9. Drop `&& !released` at runner.mjs:596. It dies on idle-stop-race (the new doesNotMatch).
|
||||
|
||||
N2. idle-stop-race waits for the whole held window (`"revoked" in window`), then asserts
|
||||
`window.control` is non-null with a message. If the other thread's run isn't holding when A's first
|
||||
release lands, the test now fails with that message instead of a bare "timed out waiting".
|
||||
|
||||
The mutants to run are M1-M5, M7, M8 and M9. These test edits are in a third commit, after the fix.
|
||||
ad9fa9f16 stays the red-first commit for the two forcing cases mos-1415 names.
|
||||
|
||||
## Round d5: red-first reordered (sage-3225, 05:49Z)
|
||||
|
||||
Sage pointed out that 8cf2c6d54 changed idle-stop-race.test.mjs and launcher-released.test.mjs after
|
||||
ad9fa9f16. So a red run on ad9fa9f16 would test older copies than the ones that land. I took the first of
|
||||
Sage's two options and reordered the commits locally, before any push. The order is now:
|
||||
|
||||
1. Tests: the final test files and this packet, over unfixed product files.
|
||||
2. The fix: broker.mjs, runner.mjs and the three READMEs.
|
||||
|
||||
ad9fa9f16, 6e5f782ae and 8cf2c6d54 are superseded and were never pushed. The old tip is kept locally as
|
||||
gizmoduck/row302-pre-reorder. The fix commit's tree matches 8cf2c6d54's except for this BUILD.md note.
|
||||
The "third commit" in d4 now lives in these two commits.
|
||||
|
||||
The red run uses the tests commit as it stands, with these test blobs (identical in the fix commit):
|
||||
|
||||
| Blob | File |
|
||||
| --- | --- |
|
||||
| 52343932b231691ba2b4fc117547ea7f6f504704 | packages/bus/tests/land.test.mjs |
|
||||
| 6c83c9ede41247fd50d40eeac3e8a16a75efacbf | packages/bus/tests/launcher-released.test.mjs |
|
||||
| 27193a745c4ed8d4936de96c1768ab010a572856 | packages/bus/tests/threads.test.mjs |
|
||||
| 9dfacfea15a487c4e7056ebae3e311cb514b0467 | packages/cli/tests/idle-stop-race.test.mjs |
|
||||
| 0792431b60314a17ed74b3df0c5b3748dc3c1286 | packages/harness/tests/runner.test.mjs |
|
||||
|
||||
The red run checks these blobs with `git rev-parse HEAD:<path>` before it starts.
|
||||
|
||||
## Round d6: the first gate run's red in launcher-released.test.mjs:89 (gizmoduck-0388, sage-3294)
|
||||
|
||||
The slot opened at 08:15Z (sage-3289). The gate tree was 649ec67ab, a local merge of 64f6558ee onto
|
||||
origin/refactor 1ac655005. The red stage on f799e8b15 passed: seven expected reds, with the pins at
|
||||
threads.test.mjs:394, land.test.mjs:464 and runner.test.mjs:771 (:772 sits behind :771 in one test).
|
||||
On 649ec67ab, idle-stop-race 1/1, runner.test 40/40 and compactions-launch 5/5 went green. The bus
|
||||
suite then had 875 of 876 pass. The one red was this row's own test, at launcher-released.test.mjs:89,
|
||||
1 !== 0. The gate stopped there, and the red went to Sage before any fix or rerun.
|
||||
|
||||
The test was wrong. `#route` (broker.mjs:6482) writes one routed delivery row for every message.
|
||||
After threadIdle no run holds thread A, so `#recipient` (:832-834) returns null and `#delivery`
|
||||
(:6612) writes holder_run null. The line counted every row for the message. Nothing was delivered to
|
||||
a-run. ASSERTION-PRESERVATION.md entry 5 has the old and new lines.
|
||||
|
||||
The fix narrows the count to rows for a-run and adds the line Sage made mandatory: the message has
|
||||
exactly one row, routed, holder_run null. bus rows come from node:sqlite with a null prototype, and the
|
||||
tests use assert/strict, so the second line spreads each row before deepEqual. That is the idiom in
|
||||
sessions.test.mjs:385 and session-clear.test.mjs:564. Sage's draft compared store.all directly, which
|
||||
would fail on the prototype alone.
|
||||
|
||||
Packaging (sage-3294): this is a third commit on gizmoduck/row302 after the pushed f799e8b15 and
|
||||
64f6558ee. It touches only that test line, this note and ASSERTION-PRESERVATION.md. It goes out as a
|
||||
fast-forward push in a gap Sage names, after Mos rules (sage-3293). Then:
|
||||
|
||||
1. The red run uses the final test files over f799e8b15's product: f799e8b15 checked out detached, with
|
||||
the five test files from the new tip checked out on top. The script checks the new blobs first.
|
||||
2. The whole gate runs again on a fresh merge of the new tip onto origin/refactor.
|
||||
|
||||
Dewey reviews the delta. The first run's logs stay in gizmo-302-out/gate-run1/.
|
||||
@@ -1,112 +0,0 @@
|
||||
# Row 302 (#1809): gate results
|
||||
|
||||
The slot ran from 08:15Z on 2026-10-23 (sage-3289), with Sage as gate owner and Dewey as reviewer.
|
||||
Logs: run1/gate-run.log and run2/gate-run.log. Script: run2/run-gate.sh and run2/mutants.py. The
|
||||
full per-file logs stay in /mnt/storage/scratch/gizmo-302-out/gate-run1/ and gate/.
|
||||
|
||||
## Commits
|
||||
|
||||
| Commit | What |
|
||||
| --- | --- |
|
||||
| f799e8b15 | red first: the tests over unfixed product (d5) |
|
||||
| 64f6558ee | the fix |
|
||||
| 5b63ea323 | d6: launcher-released.test.mjs:89, Mos ruling mos-1430 (relayed in sage-3297) |
|
||||
|
||||
The 5b63ea323 subject still says "Mos ruling pending sage-3293; local, not pushed". I wrote it before
|
||||
mos-1430 arrived, and Sage said not to amend. The ruling is mos-1430, and the commit was pushed as a
|
||||
fast-forward at about 08:23Z.
|
||||
|
||||
## Run 1 (08:18Z): stopped on a red outside the four pins
|
||||
|
||||
- Gate tree: 649ec67ab, a local merge of 64f6558ee onto origin/refactor 1ac655005.
|
||||
- Red stage: as expected (the same reds as run 2).
|
||||
- Green on 649ec67ab: idle-stop-race 1/1, runner.test 40/40 and compactions-launch 5/5. Then bus had
|
||||
875 of 876 pass. The red was this row's own test, at launcher-released.test.mjs:89, 1 !== 0.
|
||||
- It stopped at 08:19:28Z and went to Sage (gizmoduck-0388), then Mos. BUILD.md d6 has the cause.
|
||||
|
||||
## Run 2 (08:23:41Z to 08:38:14Z, 873 s)
|
||||
|
||||
The gate tree was 783602e06ed92f99c7c71d1e9c016a0f9e2c9b28, a local merge (not pushed). Parent 1 is
|
||||
origin/refactor 1ac655005, and parent 2 is 5b63ea323. The 12 row paths equal 5b63ea323's, and none
|
||||
of them changes in 0e30a85fa..1ac655005.
|
||||
|
||||
### Red: f799e8b15's product with the final test files from 5b63ea323
|
||||
|
||||
The five final test blobs:
|
||||
|
||||
| Blob | File |
|
||||
| --- | --- |
|
||||
| 52343932b231691ba2b4fc117547ea7f6f504704 | packages/bus/tests/land.test.mjs |
|
||||
| 6fb6e15c2fd34f742f9aab718ff654ab2704e198 | packages/bus/tests/launcher-released.test.mjs (new in d6) |
|
||||
| 27193a745c4ed8d4936de96c1768ab010a572856 | packages/bus/tests/threads.test.mjs |
|
||||
| 9dfacfea15a487c4e7056ebae3e311cb514b0467 | packages/cli/tests/idle-stop-race.test.mjs |
|
||||
| 0792431b60314a17ed74b3df0c5b3748dc3c1286 | packages/harness/tests/runner.test.mjs |
|
||||
|
||||
The script checked each blob with hash-object on the red tree. Only launcher-released differs from
|
||||
f799e8b15, and no product file differs from it.
|
||||
|
||||
There were seven reds, and nothing else failed:
|
||||
|
||||
- idle-stop-race: the released run ended with 'session stopped working: not-holder', exit 22 not 0
|
||||
- runner.test landing case: pin :771, with 22 vs 0
|
||||
- threads.test threadIdle case: pin :394
|
||||
- land.test landRelease case: pin :464
|
||||
- launcher-released: all three tests
|
||||
|
||||
On the bus files, 33 of 38 passed and 5 failed.
|
||||
|
||||
Pin :772 sits after :771 in the same test, so :771 stops the test first. :771's message is out.stderr,
|
||||
and that shows the old "session stopped working: not-holder" line and no "session released by the
|
||||
launcher" line. :89's two new lines never run on red, because the earlier refusals fail first. Run 2's
|
||||
green bus stage is their first green.
|
||||
|
||||
### Green on 783602e06
|
||||
|
||||
| Run | Result | Wall |
|
||||
| --- | --- | --- |
|
||||
| idle-stop-race | 1/1 | 2 s |
|
||||
| runner.test whole | 40/40 | 12 s |
|
||||
| compactions-launch | 5/5 | 14 s |
|
||||
| bus | 876/876 | 24 s |
|
||||
| harness | 151/151 | 13 s |
|
||||
| cli | 918/918 | 615 s |
|
||||
| test-auth | 15 | |
|
||||
| test-conductor | 17 | |
|
||||
| test-config | 55 | |
|
||||
| test-discord | 81 | |
|
||||
| test-extension-package | 18 | |
|
||||
| test-foundation | 44 | |
|
||||
| test-land-work | 86 | |
|
||||
| test-queue | node 156/156, suite 28 | |
|
||||
| test-release | 14 | |
|
||||
| test-task | 91 | |
|
||||
|
||||
Every scripts/test-*.sh ended with rc 0 and 0 failed. The node suites skipped nothing and left no
|
||||
todo. The tree was clean after the tests.
|
||||
|
||||
The shell scripts skipped two things:
|
||||
|
||||
- test-queue: "queue verify and render --check: this checkout is not the queue's canonical root".
|
||||
The gate runs in a worktree, not /mnt/storage/src/mosaic-stack.
|
||||
- test-task: "live provider cases (row 244: no provider call in the gate)", printed twice. It printed
|
||||
no other skip line, so no Docker group was skipped.
|
||||
|
||||
### Mutants on 783602e06
|
||||
|
||||
Each kill file ran whole, and every listed test name had to fail. After each mutant the file was
|
||||
checked out again and matched its sha256. At the end, broker.mjs was 0c6dbbc5… and runner.mjs was
|
||||
72ec39eb…, both equal to 5b63ea323's, and the tree was clean.
|
||||
|
||||
| Mutant | Change | Killed by |
|
||||
| --- | --- | --- |
|
||||
| M1 | no add after threadIdle's release | launcher-released (threadIdle), idle-stop-race |
|
||||
| M2 | #agent always not-holder | land, launcher-released (threadIdle, landRelease), idle-stop-race, runner landing case |
|
||||
| M3 | runner ignores launcher-released | idle-stop-race, runner landing case |
|
||||
| M4 | a released run exits session | idle-stop-race, runner landing case |
|
||||
| M5 | every lost claim reads launcher-released | launcher-released (other way), idle-stop-race |
|
||||
| M7 | role.claim's D1 check removed | launcher-released (threadIdle, landRelease) |
|
||||
| M8 | no add after landRelease's release | land, launcher-released (landRelease), runner landing case |
|
||||
| M9 | exit releases a launcher-released run | idle-stop-race |
|
||||
|
||||
All eight were killed by rule, and none survived. When the run ended, no node process had its cwd in
|
||||
the worktree.
|
||||
@@ -1,57 +0,0 @@
|
||||
blobs: the five d5 test blobs match in f799e8b15561ea2c0f0735e5328ba149c7a1a7b5 and 649ec67abcd315ac877bbad65742017b998137e4
|
||||
merge: 649ec67abcd315ac877bbad65742017b998137e4 = 64f6558ee4cc536447c720eb20b1f675f32ffbf0 onto 1ac655005e611157605a0dcc5adf7a37f9bca2b0; 12 row paths equal to 64f6558ee4cc536447c720eb20b1f675f32ffbf0, none touched in 0e30a85fa..1ac655005e611157605a0dcc5adf7a37f9bca2b0
|
||||
TIP 649ec67abcd315ac877bbad65742017b998137e4 (merge) FIX 64f6558ee4cc536447c720eb20b1f675f32ffbf0 RED f799e8b15561ea2c0f0735e5328ba149c7a1a7b5 base 0e30a85fa onto 1ac655005e611157605a0dcc5adf7a37f9bca2b0 origin/refactor 1ac655005e611157605a0dcc5adf7a37f9bca2b0 start 2026-10-23T08:18:31Z
|
||||
loadavg: 1.63 1.48 1.89 5/6323 1532949
|
||||
== 1. red first on f799e8b15561ea2c0f0735e5328ba149c7a1a7b5 (unfixed product, final tests)
|
||||
TMPDIR /mnt/storage/scratch/tmp/ggiJ
|
||||
idle-stop-race rc=1 ℹ tests 1 ℹ pass 0 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
TMPDIR /mnt/storage/scratch/tmp/g2U9
|
||||
runner-land rc=1 ℹ tests 1 ℹ pass 0 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
TMPDIR /mnt/storage/scratch/tmp/g3Lr
|
||||
bus rc=1 ℹ tests 38 ℹ pass 33 ℹ fail 5 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
✖ failing tests:
|
||||
✖ landRelease releases the holding run by land-relaunch once it has asked message.receive and nothing delivered to it is unread, and waiting messages go to the next holder (34.180349ms)
|
||||
✖ REQ-THREAD-2 REQ-LAUNCH-1: threadIdle releases only a thread with nothing waiting, and only for its holder (33.321208ms)
|
||||
✖ row 302 a run landRelease released answers launcher-released, and claims nothing again (36.389735ms)
|
||||
✖ row 302 a run threadIdle released answers launcher-released on receive, state, read and send, logs each, and claims nothing again (39.497664ms)
|
||||
✖ row 302 with a run launcher-released, a run that lost its claim another way still answers not-holder (39.379523ms)
|
||||
17: AssertionError [ERR_ASSERTION]: runner: acme/assistant claimed by run ra10cf293beb6
|
||||
21: + actual - expected
|
||||
32: actual: 'session stopped working: not-holder',
|
||||
33: expected: 'session released by the launcher',
|
||||
15: AssertionError [ERR_ASSERTION]: runner: demo/coder claimed by run coder-run
|
||||
35: actual: 22,
|
||||
36: expected: 0,
|
||||
pin red at threads.test.mjs:394:
|
||||
pin red at land.test.mjs:464:
|
||||
pin red at runner.test.mjs:771:
|
||||
:772 pattern absent from stderr (old not-holder line present)
|
||||
every red on f799e8b15561ea2c0f0735e5328ba149c7a1a7b5:
|
||||
✖ a run in a landing turn isn't released for a second held decision until its landing turns end and it has read its messages (602.229598ms)
|
||||
✖ failing tests:
|
||||
✖ landRelease releases the holding run by land-relaunch once it has asked message.receive and nothing delivered to it is unread, and waiting messages go to the next holder (34.180349ms)
|
||||
✖ REQ-THREAD-2 REQ-LAUNCH-1: threadIdle releases only a thread with nothing waiting, and only for its holder (33.321208ms)
|
||||
✖ row 302 a run landRelease released answers launcher-released, and claims nothing again (36.389735ms)
|
||||
✖ row 302 a run threadIdle released answers launcher-released on receive, state, read and send, logs each, and claims nothing again (39.497664ms)
|
||||
✖ row 302 REQ-THREAD-2 an idle-released run whose call lands before the SIGTERM ends stopped/0; a run whose claim the human revoked in the same window still ends session-invalid/22 (1672.44448ms)
|
||||
✖ row 302 with a run launcher-released, a run that lost its claim another way still answers not-holder (39.379523ms)
|
||||
red stage: both forcing cases and the four pins red
|
||||
wall 5 s
|
||||
== 2. green on 649ec67abcd315ac877bbad65742017b998137e4
|
||||
TMPDIR /mnt/storage/scratch/tmp/gaYE
|
||||
idle-stop-race rc=0 ℹ tests 1 ℹ pass 1 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
wall 1 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/g9t4
|
||||
runner rc=0 ℹ tests 40 ℹ pass 40 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
wall 12 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gwAp
|
||||
compactions-launch rc=0 ℹ tests 5 ℹ pass 5 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
wall 15 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gPAB
|
||||
bus rc=1 ℹ tests 876 ℹ pass 875 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
RED: bus, stop at 2026-10-23T08:19:28Z, wall 57 s
|
||||
430:✖ row 302 a run threadIdle released answers launcher-released on receive, state, read and send, logs each, and claims nothing again (81.984434ms)
|
||||
931:✖ failing tests:
|
||||
934:✖ row 302 a run threadIdle released answers launcher-released on receive, state, read and send, logs each, and claims nothing again (81.984434ms)
|
||||
935: AssertionError [ERR_ASSERTION]: nothing delivered to the released run
|
||||
exit 1
|
||||
@@ -1,187 +0,0 @@
|
||||
blobs: the five final test blobs (d5, launcher-released from d6) match in 5b63ea3236d82e14956594b41a00cbd7f7dc5f71 and 783602e06ed92f99c7c71d1e9c016a0f9e2c9b28
|
||||
merge: 783602e06ed92f99c7c71d1e9c016a0f9e2c9b28 = 5b63ea3236d82e14956594b41a00cbd7f7dc5f71 onto 1ac655005e611157605a0dcc5adf7a37f9bca2b0; 12 row paths equal to 5b63ea3236d82e14956594b41a00cbd7f7dc5f71, none touched in 0e30a85fa..1ac655005e611157605a0dcc5adf7a37f9bca2b0
|
||||
TIP 783602e06ed92f99c7c71d1e9c016a0f9e2c9b28 (merge) FIX 5b63ea3236d82e14956594b41a00cbd7f7dc5f71 RED f799e8b15561ea2c0f0735e5328ba149c7a1a7b5 base 0e30a85fa onto 1ac655005e611157605a0dcc5adf7a37f9bca2b0 origin/refactor 1ac655005e611157605a0dcc5adf7a37f9bca2b0 start 2026-10-23T08:23:41Z
|
||||
loadavg: 1.49 1.75 1.94 4/6320 1592551
|
||||
== 1. red first: f799e8b15561ea2c0f0735e5328ba149c7a1a7b5 product with the final test files from 5b63ea3236d82e14956594b41a00cbd7f7dc5f71 checked out on top
|
||||
red tree: product of f799e8b15561ea2c0f0735e5328ba149c7a1a7b5, tests of 5b63ea3236d82e14956594b41a00cbd7f7dc5f71: packages/bus/tests/launcher-released.test.mjs
|
||||
TMPDIR /mnt/storage/scratch/tmp/gHpj
|
||||
idle-stop-race rc=1 ℹ tests 1 ℹ pass 0 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
TMPDIR /mnt/storage/scratch/tmp/gIww
|
||||
runner-land rc=1 ℹ tests 1 ℹ pass 0 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
TMPDIR /mnt/storage/scratch/tmp/g8I5
|
||||
bus rc=1 ℹ tests 38 ℹ pass 33 ℹ fail 5 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
✖ failing tests:
|
||||
✖ landRelease releases the holding run by land-relaunch once it has asked message.receive and nothing delivered to it is unread, and waiting messages go to the next holder (37.255433ms)
|
||||
✖ REQ-THREAD-2 REQ-LAUNCH-1: threadIdle releases only a thread with nothing waiting, and only for its holder (30.900781ms)
|
||||
✖ row 302 a run landRelease released answers launcher-released, and claims nothing again (36.454968ms)
|
||||
✖ row 302 a run threadIdle released answers launcher-released on receive, state, read and send, logs each, and claims nothing again (40.167008ms)
|
||||
✖ row 302 with a run launcher-released, a run that lost its claim another way still answers not-holder (38.485853ms)
|
||||
17: AssertionError [ERR_ASSERTION]: runner: acme/assistant claimed by run r3f96e5142838
|
||||
21: + actual - expected
|
||||
32: actual: 'session stopped working: not-holder',
|
||||
33: expected: 'session released by the launcher',
|
||||
15: AssertionError [ERR_ASSERTION]: runner: demo/coder claimed by run coder-run
|
||||
35: actual: 22,
|
||||
36: expected: 0,
|
||||
pin red at threads.test.mjs:394:
|
||||
pin red at land.test.mjs:464:
|
||||
pin red at runner.test.mjs:771:
|
||||
:772 pattern absent from stderr (old not-holder line present)
|
||||
every red on f799e8b15561ea2c0f0735e5328ba149c7a1a7b5:
|
||||
✖ a run in a landing turn isn't released for a second held decision until its landing turns end and it has read its messages (604.606436ms)
|
||||
✖ failing tests:
|
||||
✖ landRelease releases the holding run by land-relaunch once it has asked message.receive and nothing delivered to it is unread, and waiting messages go to the next holder (37.255433ms)
|
||||
✖ REQ-THREAD-2 REQ-LAUNCH-1: threadIdle releases only a thread with nothing waiting, and only for its holder (30.900781ms)
|
||||
✖ row 302 a run landRelease released answers launcher-released, and claims nothing again (36.454968ms)
|
||||
✖ row 302 a run threadIdle released answers launcher-released on receive, state, read and send, logs each, and claims nothing again (40.167008ms)
|
||||
✖ row 302 REQ-THREAD-2 an idle-released run whose call lands before the SIGTERM ends stopped/0; a run whose claim the human revoked in the same window still ends session-invalid/22 (1604.296753ms)
|
||||
✖ row 302 with a run launcher-released, a run that lost its claim another way still answers not-holder (38.485853ms)
|
||||
red stage: both forcing cases and the four pins red
|
||||
wall 4 s
|
||||
== 2. green on 783602e06ed92f99c7c71d1e9c016a0f9e2c9b28
|
||||
TMPDIR /mnt/storage/scratch/tmp/gMtc
|
||||
idle-stop-race rc=0 ℹ tests 1 ℹ pass 1 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
wall 2 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gLSw
|
||||
runner rc=0 ℹ tests 40 ℹ pass 40 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
wall 12 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gTQ3
|
||||
compactions-launch rc=0 ℹ tests 5 ℹ pass 5 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
wall 14 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gAbg
|
||||
bus rc=0 ℹ tests 876 ℹ pass 876 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
wall 24 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gzOd
|
||||
harness rc=0 ℹ tests 151 ℹ pass 151 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
wall 13 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gxSF
|
||||
cli rc=0 ℹ tests 918 ℹ pass 918 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
wall 615 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gL7z
|
||||
test-auth rc=0
|
||||
|
||||
selftest: 15 passed, 0 failed
|
||||
wall 1 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/ghcv
|
||||
test-conductor rc=0
|
||||
|
||||
selftest: 17 passed, 0 failed
|
||||
wall 6 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gCsn
|
||||
test-config rc=0
|
||||
|
||||
selftest: 55 passed, 0 failed
|
||||
wall 3 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/guh1
|
||||
test-discord rc=0
|
||||
|
||||
discord suite: 81 passed, 0 failed
|
||||
wall 7 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gVDP
|
||||
test-extension-package rc=0
|
||||
|
||||
extension package selftest: 18 passed, 0 failed
|
||||
wall 1 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/g1V8
|
||||
test-foundation rc=0
|
||||
|
||||
selftest: 44 passed, 0 failed
|
||||
wall 38 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gYsp
|
||||
test-land-work rc=0
|
||||
---
|
||||
land-work selftests: 86 passed, 0 failed
|
||||
wall 11 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gucV
|
||||
test-queue rc=0 ℹ tests 156 ℹ pass 156 ℹ fail 0
|
||||
|
||||
queue suite: 28 passed, 0 failed
|
||||
wall 21 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gGva
|
||||
test-release rc=0
|
||||
|
||||
selftest: 14 passed, 0 failed
|
||||
wall 3 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/g4vO
|
||||
test-task rc=0
|
||||
|
||||
selftest: 91 passed, 0 failed
|
||||
wall 16 s
|
||||
after tests: clean
|
||||
== skips
|
||||
suite/test-queue.log:skip queue verify and render --check: this checkout (/mnt/storage/scratch/gizmo-302) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
|
||||
suite/test-task.log:skip live provider cases (row 244: no provider call in the gate)
|
||||
suite/test-task.log:skip live provider cases (row 244: no provider call in the gate)
|
||||
== 3. mutants
|
||||
TMPDIR /mnt/storage/scratch/tmp/gDiY
|
||||
M1.launcher-released rc=1 ℹ tests 3 ℹ pass 1 ℹ fail 2 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: row 302 a run threadIdle released answers launcher-released
|
||||
TMPDIR /mnt/storage/scratch/tmp/glVj
|
||||
M1.idle-stop-race rc=1 ℹ tests 1 ℹ pass 0 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: row 302 REQ-THREAD-2 an idle-released run
|
||||
M1 KILLED by rule
|
||||
wall 2 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/grHO
|
||||
M2.land rc=1 ℹ tests 18 ℹ pass 17 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: landRelease releases the holding run by land-relaunch
|
||||
TMPDIR /mnt/storage/scratch/tmp/giID
|
||||
M2.launcher-released rc=1 ℹ tests 3 ℹ pass 0 ℹ fail 3 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: row 302 a run threadIdle released answers launcher-released
|
||||
killed: row 302 a run landRelease released answers launcher-released
|
||||
TMPDIR /mnt/storage/scratch/tmp/gXTo
|
||||
M2.idle-stop-race rc=1 ℹ tests 1 ℹ pass 0 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: row 302 REQ-THREAD-2 an idle-released run
|
||||
TMPDIR /mnt/storage/scratch/tmp/gTkJ
|
||||
M2.runner rc=1 ℹ tests 40 ℹ pass 39 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: a run in a landing turn isn't released for a second held decision
|
||||
M2 KILLED by rule
|
||||
wall 15 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/ge9B
|
||||
M3.idle-stop-race rc=1 ℹ tests 1 ℹ pass 0 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: row 302 REQ-THREAD-2 an idle-released run
|
||||
TMPDIR /mnt/storage/scratch/tmp/gYUi
|
||||
M3.runner rc=1 ℹ tests 40 ℹ pass 39 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: a run in a landing turn isn't released for a second held decision
|
||||
M3 KILLED by rule
|
||||
wall 33 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gw5X
|
||||
M4.idle-stop-race rc=1 ℹ tests 1 ℹ pass 0 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: row 302 REQ-THREAD-2 an idle-released run
|
||||
TMPDIR /mnt/storage/scratch/tmp/g90Y
|
||||
M4.runner rc=1 ℹ tests 40 ℹ pass 39 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: a run in a landing turn isn't released for a second held decision
|
||||
M4 KILLED by rule
|
||||
wall 14 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gNlw
|
||||
M5.launcher-released rc=1 ℹ tests 3 ℹ pass 1 ℹ fail 2 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: row 302 with a run launcher-released, a run that lost its claim
|
||||
TMPDIR /mnt/storage/scratch/tmp/gCk7
|
||||
M5.idle-stop-race rc=1 ℹ tests 1 ℹ pass 0 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: row 302 REQ-THREAD-2 an idle-released run
|
||||
M5 KILLED by rule
|
||||
wall 2 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gaHU
|
||||
M7.launcher-released rc=1 ℹ tests 3 ℹ pass 1 ℹ fail 2 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: row 302 a run threadIdle released answers launcher-released
|
||||
killed: row 302 a run landRelease released answers launcher-released
|
||||
M7 KILLED by rule
|
||||
wall 1 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/g3K9
|
||||
M8.land rc=1 ℹ tests 18 ℹ pass 17 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: landRelease releases the holding run by land-relaunch
|
||||
TMPDIR /mnt/storage/scratch/tmp/ge6z
|
||||
M8.launcher-released rc=1 ℹ tests 3 ℹ pass 2 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: row 302 a run landRelease released answers launcher-released
|
||||
TMPDIR /mnt/storage/scratch/tmp/gWPU
|
||||
M8.runner rc=1 ℹ tests 40 ℹ pass 39 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: a run in a landing turn isn't released for a second held decision
|
||||
M8 KILLED by rule
|
||||
wall 13 s
|
||||
TMPDIR /mnt/storage/scratch/tmp/gqwU
|
||||
M9.idle-stop-race rc=1 ℹ tests 1 ℹ pass 0 ℹ fail 1 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
|
||||
killed: row 302 REQ-THREAD-2 an idle-released run
|
||||
M9 KILLED by rule
|
||||
wall 2 s
|
||||
end 2026-10-23T08:38:14Z wall 873 s
|
||||
node with cwd in the worktree: 0
|
||||
exit 0
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# Row 302 (#1809) mutants, from BUILD.md d3 and d4. Each replaces one exact string, once, in one file.
|
||||
# Kill rule: every (test file, failing test name) listed fails in that file's run.
|
||||
# mutants.py list | apply M | file M | kills M
|
||||
import sys
|
||||
|
||||
BROKER = "packages/bus/src/broker.mjs"
|
||||
RUNNER = "packages/harness/src/runner.mjs"
|
||||
ADD = " this.#launcherReleased.add(key);\n"
|
||||
|
||||
IDLE = ("packages/cli/tests/idle-stop-race.test.mjs", "row 302 REQ-THREAD-2 an idle-released run")
|
||||
LR1 = ("packages/bus/tests/launcher-released.test.mjs", "row 302 a run threadIdle released answers launcher-released")
|
||||
LR2 = ("packages/bus/tests/launcher-released.test.mjs", "row 302 with a run launcher-released, a run that lost its claim")
|
||||
LR3 = ("packages/bus/tests/launcher-released.test.mjs", "row 302 a run landRelease released answers launcher-released")
|
||||
LAND = ("packages/bus/tests/land.test.mjs", "landRelease releases the holding run by land-relaunch")
|
||||
RUN = ("packages/harness/tests/runner.test.mjs", "a run in a landing turn isn't released for a second held decision")
|
||||
|
||||
M = {
|
||||
"M1": (BROKER, "this.#claimEnd(session, this.#own(session), 'release', null));\n" + ADD,
|
||||
"this.#claimEnd(session, this.#own(session), 'release', null));\n", [IDLE, LR1]),
|
||||
"M2": (BROKER, "fail(this.#launcherReleased.has(s.business + ':' + s.run) ? 'launcher-released' : 'not-holder');",
|
||||
"fail('not-holder');", [IDLE, LR1, LR3, LAND, RUN]),
|
||||
"M3": (RUNNER, " if (err.code === LAUNCHER_RELEASED) {", " if (false) {", [IDLE, RUN]),
|
||||
"M4": (RUNNER, " released = true;\n return true;", " released = true;\n code = EXIT.session;\n return true;", [IDLE, RUN]),
|
||||
"M5": (BROKER, "? 'launcher-released' : 'not-holder')", "? 'launcher-released' : 'launcher-released')", [IDLE, LR2]),
|
||||
"M7": (BROKER, " if (this.#launcherReleased.has(s.business + ':' + s.run)) fail('launcher-released');\n", "", [LR1, LR3]),
|
||||
"M8": (BROKER, "'land-relaunch'));\n" + ADD, "'land-relaunch'));\n", [LR3, LAND, RUN]),
|
||||
"M9": (RUNNER, "if (code === EXIT.stopped && !released) {", "if (code === EXIT.stopped) {", [IDLE]),
|
||||
}
|
||||
|
||||
cmd = sys.argv[1]
|
||||
if cmd == "list":
|
||||
print(" ".join(M))
|
||||
elif cmd == "file":
|
||||
print(M[sys.argv[2]][0])
|
||||
elif cmd == "kills":
|
||||
for f, name in M[sys.argv[2]][3]:
|
||||
print(f + "\t" + name)
|
||||
elif cmd == "apply":
|
||||
path, old, new, _ = M[sys.argv[2]]
|
||||
s = open(path).read()
|
||||
if s.count(old) != 1:
|
||||
sys.exit(f"{sys.argv[2]}: anchor found {s.count(old)} times in {path}")
|
||||
open(path, "w").write(s.replace(old, new))
|
||||
print(path)
|
||||
else:
|
||||
sys.exit("usage: mutants.py list|apply M|file M|kills M")
|
||||
@@ -1,147 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Row 302 (#1809, Vikunja 588) gate. Plan: agents/gizmoduck/work/queue-302/BUILD.md d3 "Run plan", d4, d5.
|
||||
# 0. Preflight: worktree at TIP and clean, the five test blobs from d5. TIP is the local merge of 64f6558ee
|
||||
# onto origin/refactor 1ac655005 (sage-3289); the 12 row paths are unchanged in BASE..1ac655005.
|
||||
# 1. Red (mos-1415): f799e8b15 (final tests, unfixed product). idle-stop-race and the runner.test land case
|
||||
# must fail. The bus files are run and recorded. A green forcing case here is a STOP.
|
||||
# 2. On TIP: idle-stop-race alone 600 s, runner.test whole 900 s, compactions-launch whole 600 s,
|
||||
# bus 1200 s, harness 1200 s, cli 2100 s, then the scripts/test-*.sh at 900 s each.
|
||||
# 3. M1-M5, M7, M8, M9 (mutants.py). Each kill-listed file runs whole; every listed test name must fail.
|
||||
# After each: git checkout, sha256 equal to the candidate's, tree clean. A survivor is a STOP to Sage.
|
||||
# mos-1272: a first-run red in a new test is placed and reported after; any other red stops the run.
|
||||
set -o pipefail
|
||||
WT=/mnt/storage/scratch/gizmo-302
|
||||
# Run 2 (BUILD d6): FIX is the d6 tip; TIP is a fresh merge of it onto ONTO, set when the run is cleared.
|
||||
TIP=${TIP:?set TIP to the run-2 merge sha}
|
||||
FIX=5b63ea3236d82e14956594b41a00cbd7f7dc5f71
|
||||
ONTO=${ONTO:?set ONTO to origin/refactor at the merge}
|
||||
BR=gizmoduck/row302-gate
|
||||
RED=f799e8b15561ea2c0f0735e5328ba149c7a1a7b5
|
||||
BASE=0e30a85fa
|
||||
OUT=/mnt/storage/scratch/gizmo-302-out/gate
|
||||
PY=/mnt/storage/scratch/gizmo-302-out/mutants.py
|
||||
cd "$WT" || exit 9
|
||||
[ "$(git rev-parse HEAD)" = "$TIP" ] || { echo "not on the gate commit"; exit 9; }
|
||||
[ -z "$(git status --porcelain --untracked-files=no)" ] || { echo "dirty"; exit 9; }
|
||||
TESTS=""
|
||||
while read -r blob f; do
|
||||
[ "$(git rev-parse "$TIP:$f")" = "$blob" ] && [ "$(git rev-parse "$FIX:$f")" = "$blob" ] || { echo "blob mismatch $f"; exit 9; }
|
||||
TESTS="$TESTS $f"
|
||||
done <<'EOF'
|
||||
52343932b231691ba2b4fc117547ea7f6f504704 packages/bus/tests/land.test.mjs
|
||||
6fb6e15c2fd34f742f9aab718ff654ab2704e198 packages/bus/tests/launcher-released.test.mjs
|
||||
27193a745c4ed8d4936de96c1768ab010a572856 packages/bus/tests/threads.test.mjs
|
||||
9dfacfea15a487c4e7056ebae3e311cb514b0467 packages/cli/tests/idle-stop-race.test.mjs
|
||||
0792431b60314a17ed74b3df0c5b3748dc3c1286 packages/harness/tests/runner.test.mjs
|
||||
EOF
|
||||
echo "blobs: the five final test blobs (d5, launcher-released from d6) match in $FIX and $TIP"
|
||||
[ -L node_modules ] || ln -s /mnt/storage/src/mosaic-stack/node_modules node_modules
|
||||
[ -L node_modules ] || { echo "no node_modules link"; exit 8; }
|
||||
[ "$(git rev-parse $TIP^1) $(git rev-parse $TIP^2)" = "$ONTO $FIX" ] || { echo "merge parents wrong"; exit 9; }
|
||||
paths=$(git diff --name-only $BASE $FIX)
|
||||
[ "$(echo "$paths" | wc -l)" -eq 12 ] || { echo "row paths not 12"; exit 9; }
|
||||
over=$(git diff --name-only $BASE $ONTO -- $paths)
|
||||
[ -z "$over" ] || { echo "BASE..ONTO touches row paths:"; echo "$over"; exit 9; }
|
||||
git diff --quiet $FIX $TIP -- $paths || { echo "merge changed a row path"; exit 9; }
|
||||
echo "merge: $TIP = $FIX onto $ONTO; 12 row paths equal to $FIX, none touched in $BASE..$ONTO"
|
||||
[ -e "$OUT" ] && { echo "$OUT exists; move it first"; exit 9; }
|
||||
mkdir -p "$OUT/red" "$OUT/suite" "$OUT/mut"
|
||||
newtmp() { export TMPDIR=$(mktemp -d /mnt/storage/scratch/tmp/gXXX); echo " TMPDIR $TMPDIR"; }
|
||||
echo "TIP $TIP (merge) FIX $FIX RED $RED base $BASE onto $ONTO origin/refactor $(git rev-parse origin/refactor) start $(date -u +%FT%TZ)"; echo "loadavg: $(cat /proc/loadavg)"
|
||||
T0=$SECONDS; t0=$SECONDS; lap() { echo " wall $((SECONDS - t0)) s"; t0=$SECONDS; }
|
||||
counts() { grep -E '^ℹ (tests|pass|fail|cancelled|skipped|todo) ' "$1" | tr '\n' ' '; echo; }
|
||||
timed() { # log dir limit args... ; sets rc
|
||||
local log=$1 dir=$2 lim=$3; shift 3
|
||||
newtmp
|
||||
(cd "$dir" && timeout -k 10 "$lim" "$@") > "$log" 2>&1
|
||||
rc=$?
|
||||
if [ $rc -eq 124 ] || [ $rc -eq 137 ]; then
|
||||
echo "TIMEOUT after $lim s: $(basename "$log" .log). To Sage."
|
||||
case $(basename "$log") in test-*.log) echo "SHELL-PHASE TIMEOUT (mos-1275): to Mos unplaced";; esac
|
||||
fi
|
||||
echo "$(basename "$log" .log) rc=$rc $(counts "$log")"
|
||||
}
|
||||
green() { # log dir limit args... ; any red stops
|
||||
timed "$@"
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "RED: $(basename "$1" .log), stop at $(date -u +%FT%TZ), wall $((SECONDS - T0)) s"
|
||||
grep -nE '^not ok|✖|AssertionError|Error:' "$1" | head -40
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "== 1. red first: $RED product with the final test files from $FIX checked out on top"
|
||||
git checkout -q --detach "$RED" || exit 9
|
||||
git checkout -q "$FIX" -- $TESTS || exit 9
|
||||
while read -r f; do [ "$(git hash-object "$f")" = "$(git rev-parse "$FIX:$f")" ] || { echo "red tree blob wrong $f"; exit 9; }; done < <(echo $TESTS | tr ' ' '\n')
|
||||
[ -z "$(git diff --name-only HEAD -- 'packages/*/src/*')" ] || exit 9
|
||||
echo " red tree: product of $RED, tests of $FIX: $(git diff --cached --name-only HEAD | tr '\n' ' ')"
|
||||
[ -z "$(git diff --name-only $BASE HEAD -- 'packages/*/src/*')" ] || { echo "red commit changes product files"; git checkout -q $BR; exit 9; }
|
||||
timed "$OUT/red/idle-stop-race.log" "$WT/packages/cli" 600 node --test tests/idle-stop-race.test.mjs; r1=$rc
|
||||
timed "$OUT/red/runner-land.log" "$WT/packages/harness" 600 node --test --test-name-pattern="^a run in a landing turn isn't released for a second held decision" tests/runner.test.mjs; r2=$rc
|
||||
timed "$OUT/red/bus.log" "$WT/packages/bus" 600 node --test tests/launcher-released.test.mjs tests/threads.test.mjs tests/land.test.mjs
|
||||
grep -E '✖ ' "$OUT/red/bus.log" | sort -u | sed 's/^/ /' | head -20
|
||||
git reset -q --hard "$RED" && git checkout -q $BR || exit 9
|
||||
[ "$(git rev-parse HEAD)" = "$TIP" ] && [ -z "$(git status --porcelain --untracked-files=no)" ] || { echo "not back on TIP clean"; exit 9; }
|
||||
red_ok=1
|
||||
[ $r1 -ne 0 ] && grep -qF "✖ row 302 REQ-THREAD-2 an idle-released run" "$OUT/red/idle-stop-race.log" || { echo " idle-stop-race NOT red on $RED"; red_ok=0; }
|
||||
[ $r2 -ne 0 ] && grep -qF "✖ a run in a landing turn isn't released for a second held decision" "$OUT/red/runner-land.log" || { echo " runner.test land case NOT red on $RED"; red_ok=0; }
|
||||
grep -nE 'AssertionError|actual|expected' "$OUT/red/idle-stop-race.log" | head -12 | sed 's/^/ /'
|
||||
grep -nE 'AssertionError|actual|expected' "$OUT/red/runner-land.log" | head -8 | sed 's/^/ /'
|
||||
# The four ruled pins (mos-1414/1415/1416). :772 sits after :771 in one test, so :771 stops it first;
|
||||
# :771's message is out.stderr, so the log shows the old "stopped working: not-holder" line and no
|
||||
# "released by the launcher" line, which is the line :772 now pins.
|
||||
grep -qF "✖ REQ-THREAD-2 REQ-LAUNCH-1: threadIdle releases only a thread with nothing waiting" "$OUT/red/bus.log" || { echo " threads.test threadIdle case NOT red"; red_ok=0; }
|
||||
grep -qF "✖ landRelease releases the holding run by land-relaunch" "$OUT/red/bus.log" || { echo " land.test landRelease case NOT red"; red_ok=0; }
|
||||
for pin in "threads.test.mjs:394:" "land.test.mjs:464:"; do
|
||||
grep -qF "$pin" "$OUT/red/bus.log" && echo " pin red at $pin" || { echo " pin $pin not in the failure stacks"; red_ok=0; }
|
||||
done
|
||||
grep -qF "runner.test.mjs:771:" "$OUT/red/runner-land.log" && echo " pin red at runner.test.mjs:771:" || { echo " pin runner.test.mjs:771 not in the failure stack"; red_ok=0; }
|
||||
grep -qF "session stopped working: not-holder" "$OUT/red/runner-land.log" && ! grep -qF "session released by the launcher" "$OUT/red/runner-land.log" \
|
||||
&& echo " :772 pattern absent from stderr (old not-holder line present)" || { echo " :772 evidence missing"; red_ok=0; }
|
||||
echo " every red on $RED:"; cat "$OUT"/red/*.log | grep -E '^✖ |^ ✖ |✖ ' | sed 's/^ *//' | sort -u | sed 's/^/ /'
|
||||
[ $red_ok -eq 1 ] && echo " red stage: both forcing cases and the four pins red" || { echo "STOP: red stage failed, to Sage"; exit 3; }
|
||||
lap
|
||||
|
||||
echo "== 2. green on $TIP"
|
||||
green "$OUT/suite/idle-stop-race.log" "$WT/packages/cli" 600 node --test tests/idle-stop-race.test.mjs; lap
|
||||
green "$OUT/suite/runner.log" "$WT/packages/harness" 900 node --test tests/runner.test.mjs; lap
|
||||
green "$OUT/suite/compactions-launch.log" "$WT/packages/cli" 600 node --test tests/compactions-launch.test.mjs; lap
|
||||
green "$OUT/suite/bus.log" "$WT/packages/bus" 1200 node --test tests/*.test.mjs; lap
|
||||
green "$OUT/suite/harness.log" "$WT/packages/harness" 1200 node --test tests/*.test.mjs; lap
|
||||
green "$OUT/suite/cli.log" "$WT/packages/cli" 2100 node --test tests/*.test.mjs; lap
|
||||
for s in "$WT"/scripts/test-*.sh; do
|
||||
n=$(basename "$s" .sh)
|
||||
green "$OUT/suite/$n.log" "$WT" 900 bash "$s"
|
||||
tail -2 "$OUT/suite/$n.log" | sed 's/^/ /'; lap
|
||||
done
|
||||
[ -z "$(git status --porcelain --untracked-files=no)" ] && echo "after tests: clean" || { echo "after tests: DIRTY"; exit 1; }
|
||||
echo "== skips"
|
||||
grep -HE '^ℹ skipped [1-9]|^ℹ todo [1-9]' "$OUT"/suite/*.log | sed "s#$OUT/##"
|
||||
grep -HiE '^(skip|# skip|SKIP)|skipped' "$OUT"/suite/test-*.log | grep -v '^ℹ' | sed "s#$OUT/##" | head -40
|
||||
|
||||
echo "== 3. mutants"
|
||||
limit() { case $1 in *cli/*) echo 600;; *runner.test*) echo 900;; *) echo 600;; esac; }
|
||||
for m in $(python3 -B "$PY" list); do
|
||||
src=$(python3 -B "$PY" file "$m")
|
||||
sum=$(git show "HEAD:$src" | sha256sum | cut -c1-64)
|
||||
python3 -B "$PY" apply "$m" >/dev/null || { echo "$m apply-failed"; git checkout -- "$src"; exit 8; }
|
||||
git diff > "$OUT/mut/$m.diff"
|
||||
ok=1
|
||||
for f in $(python3 -B "$PY" kills "$m" | cut -f1 | sort -u); do
|
||||
pkg=${f%%/tests/*}; rel=tests/${f#*/tests/}
|
||||
log="$OUT/mut/$m.$(basename "$f" .test.mjs).log"
|
||||
timed "$log" "$WT/$pkg" "$(limit "$f")" node --test "$rel"
|
||||
[ $rc -ne 0 ] || { echo " $m survived in $f"; ok=0; }
|
||||
while IFS=$'\t' read -r kf name; do
|
||||
[ "$kf" = "$f" ] || continue
|
||||
grep -qF "✖ $name" "$log" && echo " killed: $name" || { echo " NOT failing: $name"; ok=0; }
|
||||
done < <(python3 -B "$PY" kills "$m")
|
||||
done
|
||||
git checkout -- "$src"
|
||||
[ "$(sha256sum "$WT/$src" | cut -c1-64)" = "$sum" ] && [ -z "$(git status --porcelain --untracked-files=no)" ] || { echo "$m restore-failed"; exit 7; }
|
||||
if [ $ok -eq 1 ]; then echo "$m KILLED by rule"; else echo "STOP: $m kill rule not met, to Sage"; exit 4; fi
|
||||
lap
|
||||
done
|
||||
echo "end $(date -u +%FT%TZ) wall $((SECONDS - T0)) s"
|
||||
cd /; echo "node with cwd in the worktree: $(for p in $(pgrep -x node); do readlink /proc/$p/cwd; done 2>/dev/null | grep -c "$WT")"
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"row": 294,
|
||||
"round": 2,
|
||||
"ruling": "mos-1412 (through sage-3195): option (a) approved, (b) rejected",
|
||||
"changes": [
|
||||
{
|
||||
"file": "packages/webui/tests/browser.test.mjs",
|
||||
"line": 21,
|
||||
"kind": "comment",
|
||||
"old": " // Rows 129, 154, 191 and 194: chat.js, editor.js, processing.js and agents.js are the scripts added, same origin; the list is the pin, so another script is a visible change.",
|
||||
"new": " // Rows 129, 154, 191, 194 and 294: chat.js, editor.js, processing.js, agents.js and federation.js are the scripts added, same origin; the list is the pin, so another script is a visible change."
|
||||
},
|
||||
{
|
||||
"file": "packages/webui/tests/browser.test.mjs",
|
||||
"line": 22,
|
||||
"kind": "assertion",
|
||||
"old": " assert.deepEqual(await b.evaluate('[...document.querySelectorAll(\"script\")].map(s => s.getAttribute(\"src\"))'), ['/editor.js', '/processing.js', '/brand.js', '/app.js', '/markdown-preview.js', '/chat.js', '/agents.js', '/bus.js']);",
|
||||
"new": " assert.deepEqual(await b.evaluate('[...document.querySelectorAll(\"script\")].map(s => s.getAttribute(\"src\"))'), ['/editor.js', '/processing.js', '/federation.js', '/brand.js', '/app.js', '/markdown-preview.js', '/chat.js', '/agents.js', '/bus.js']);"
|
||||
}
|
||||
],
|
||||
"unchanged": "every other line of browser.test.mjs is byte-identical to c902a767d"
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
# Row 294 build notes (Launchpad): FED R6, Console granted-user sign-in (#1800)
|
||||
|
||||
Brief: docs/plans/2026-10-23_mvp-readiness-rows.md, "Brief: FED R6, Console granted-user sign-in".
|
||||
Design: agents/gizmoduck/work/federation/DESIGN.md at b93f098f0 (r4.11, sha256 1bcee8df…c8e6),
|
||||
sections 3.1, 3.4, 3.7 and the R6 row of section 7. Assigned in sage-3136. Reviewer Duckworth.
|
||||
|
||||
## Base and chain
|
||||
|
||||
Branch launchpad/row294 on origin/refactor 253346e3c (270+271, schema 3n in packages/bus, and 571).
|
||||
Parents done: 192 (#412 phase 1), R1 (201), R2, R3 (274). Row 208 and R7 (296) wait on this row.
|
||||
|
||||
## Plan
|
||||
|
||||
### What the Console does today
|
||||
|
||||
- `sso.mjs` callback: a `sub` that is not in `console.sso.providers[].subjects` refuses
|
||||
`subject-unknown`. A session record is `{provider, iss, sub, name, ...}` and every signed-in
|
||||
request runs as the business human.
|
||||
- `serve.mjs`: every bus route, chat route and write uses objects built once in `cli.mjs` over the
|
||||
human transport (`busCaller` over `HUMAN_CLI`).
|
||||
- `accessContextFor` (packages/cli/src/access.mjs:62) returns `peers: []`.
|
||||
|
||||
### Build items
|
||||
|
||||
1. **Granted-user sign-in (sso.mjs).** `createSso` takes an optional `users` hook:
|
||||
`users.find({provider, sub})` returns a user id with a live grant, or null. The callback asks it
|
||||
only after the owner subjects miss, so the owner path is unchanged. A user session record carries
|
||||
`user: <id>` and `name: <id>`. A miss on both refuses `subject-unknown`, as today (the log line
|
||||
names the provider, iss and sub; no new code is needed for the refusal).
|
||||
2. **The access hook (new `packages/webui/src/access.mjs`).** `consoleAccess({ load, business })`:
|
||||
- `load` is `() => loadAccess(accessFilePath(configDir()), accessContextFor(system, env))`, R1's
|
||||
loader, called on every use, never cached.
|
||||
- `find({provider, sub})`: the user whose `subjects` hold that pair, with at least one grant in
|
||||
the Console's business that `covers` at level `viewer` (the bus's own liveness test,
|
||||
`#userGrants`). A loader throw finds nobody (fail closed).
|
||||
- `check(session)`: the same test for the session's user and its own `(provider, sub)`. A user
|
||||
whose subject was removed, whose grants were all removed or expired, or whose file now refuses
|
||||
fails it.
|
||||
- `business` resolves as `busCaller` does: `--business`, else the live host state.
|
||||
3. **Per-request re-read (sso.mjs `session()`).** For a user session, `users.check(session)` runs
|
||||
after the expiry check, before any route. A failure deletes the session record and logs
|
||||
`sso refused code=user-revoked provider=<id> user="<id>"`. The request then goes the
|
||||
signed-out way: 401 (`sign-in required`) for /api/ and writes, or the sign-in redirect for a
|
||||
page GET. The cookie is not cleared on that response; it names a session that no longer exists,
|
||||
so it signs nothing in, and the next sign-in replaces it.
|
||||
4. **A user-cli transport per session (new user-bus.mjs, cli.mjs).** `userCall({ socket,
|
||||
business, user })` spawns `USER_CLI` (packages/bus/src/user-cli.mjs) as a
|
||||
direct child of the Console with `MOSAIC_BUS_USER_NONCE` set, so user-cli does not re-exec and
|
||||
the broker's parent check (principal.mjs: parent argv[1] is webui/src/cli.mjs, argv[2] `serve`)
|
||||
sees the Console. The request on stdin is `{business, user, verb, args}`. It never sets
|
||||
`MOSAIC_BUS_CLI_NONCE` (it strips one from the inherited env) and never runs `HUMAN_CLI`.
|
||||
`startServer` takes `userOptions(session)`, a factory that builds the whole bus-backed option
|
||||
set (`consoleBus`, new console-bus.mjs) over that session's user transport, per request.
|
||||
5. **Owner-only controls hidden (serve.mjs and a user page).** A user session reaches only:
|
||||
- the chat routes (`/api/chat/*`), over its own user transport, and
|
||||
- `GET /api/config`, `/healthz`, `/auth/*`, and the user page's static files.
|
||||
Every other path answers 403 `owner-only` before any bus call. Behind that gate, a user request
|
||||
never holds a human-transport object: the handler swaps every bus-backed option (`bus`, `chat`,
|
||||
`send` and each write, `processing`, project files) for the same object built over that
|
||||
session's user transport, and nulls the ones with no bus behind them (`reads`, the board proxy),
|
||||
so with the gate removed an owner route still reaches the broker only as the user, or answers
|
||||
`not-configured`. `GET /` serves a separate page,
|
||||
`user.html` with `user.js`, that renders only projects, threads and messages (no board, inbox,
|
||||
agents, settings, editor, processing or Sign-in-as-owner controls). The owner page's assets are
|
||||
not served to a user session. The gate is one function, `userRoute(path, method)`, so the M1
|
||||
mutant removes one call.
|
||||
6. **The notice.** `user.html` holds row 193's sentence word for word in the thread view:
|
||||
"The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and
|
||||
project files, but not your messages."
|
||||
7. **Hub label and peer links.** `/api/config` returns `federation: {self, peers: [{label,
|
||||
origin}]}` from `system.federation` when set, for owner and user sessions alike. Both pages show
|
||||
"This Console: <self>" and one plain `<a href="<origin>">label</a>` per peer, built with
|
||||
textContent and `setAttribute('href')`; no script, no fetch to a peer, no proxy (the hub proxy
|
||||
route is R8's).
|
||||
8. **F8 peers wiring.** `accessContextFor` returns
|
||||
`peers: (system.federation?.peers ?? []).map((p) => p.id)`, and the ACCESS_USAGE text and the
|
||||
function's comment drop "peer homes are not read yet".
|
||||
9. **Bus boot wiring (packages/cli/src/config.mjs).** `bootConfig` adds
|
||||
`access: { file: accessFilePath(configDir(env)), context: accessContextFor(system, env) }` when
|
||||
the file exists (lstat). No file: no `access` key, so no user binds, as before R6. A context
|
||||
that throws: a warning and no `access`. A file the loader refuses at boot (sage-3153): one
|
||||
warning line, which the host writes to stderr as `mosaic-bus: warning: access file refused at
|
||||
boot (<code>), no granted user binds until it is fixed: <path>`. The access stays wired, so
|
||||
the broker refuses each user bind and the owner's bus boots. R1's loader has no refusal codes,
|
||||
and its messages can quote the file (a JSON parse error, a `got "<value>"`), so the line never
|
||||
carries the message. The code comes from the loader's exit code instead: 4 (the file could not
|
||||
be opened as a regular file, including a symbolic link) is `access-unreadable`, and anything
|
||||
else is `access-invalid`.
|
||||
config.mjs now imports access.mjs, which imports config.mjs (`loadSystem`). The cycle is safe
|
||||
because neither module touches the other's exports at module top level, only inside
|
||||
functions (sage-3153).
|
||||
|
||||
### Choices and departures (for Sage and Duckworth)
|
||||
|
||||
- **C1. A separate user page, not the owner SPA with sections hidden.** "Hidden" here means never
|
||||
sent: the owner page's markup and scripts are not served to a user session. Hiding sections in
|
||||
app.js would ship the owner controls and rely on CSS. Tradeoff: a second small page to keep.
|
||||
- **C2. The Console's gate is an allowlist of user routes**, so a new owner route added later is
|
||||
owner-only by default.
|
||||
- **C2b. No human transport behind a user session.** Without the swap, M1 would send
|
||||
`/api/bus/message` through the owner's `send` over human-cli, and the bus would accept it as the
|
||||
owner. The swap is what makes "the bus refusal stands on its own" true.
|
||||
- **C3. Liveness is the bus's test** (`covers` at `viewer`, in the Console's business), so the
|
||||
Console and the broker agree on who is live. The broker still decides every verb, project,
|
||||
instance and thread (design 3.1 D4).
|
||||
- **C4. The bus's `access` boot option is wired here (sage-3142, Q2; accepted, sage-3153).** Build item 9 above.
|
||||
Sage's ruling: with the file, users bind; without it, none; with a bad file, it "refuses as R1's
|
||||
loader does"; a boot never invents a grant. My reading of "refuses": the broker's `access` is
|
||||
R1's loader, read on every user request, so a bad file refuses every user bind
|
||||
(`access-revoked`) and the owner's bus still boots. Refusing the whole boot would take the
|
||||
owner's bus down over a user file. Tests B3 pin this reading. No live unit or config changes.
|
||||
- **C5. Tests use an in-process Broker behind the user transport seam.** The broker's user
|
||||
principal needs the Console as the user-cli's parent process, which a node:test process is not.
|
||||
So the webui tests inject a `userOptions` factory over a real `Broker` with `bindUser` and an
|
||||
`access` function, and a separate transport test checks the spawn: the `USER_CLI` path, the user
|
||||
nonce variable, no human nonce, and the `{business, user, verb, args}` request. R7 runs the real
|
||||
Console process end to end.
|
||||
|
||||
- **C6. The chat's `plugins` read goes over the owner transport (accepted, sage-3153).**
|
||||
`createChat` asks the bus `plugins` first (createChannelCaller), and `plugins` is not a user
|
||||
verb, so a user chat would refuse before its first read. `createChat({ call, owner })` sends
|
||||
`owner` exactly `plugins` with `{}`, and every other verb goes over `call`. consoleBus passes
|
||||
`owner` through, and cli.mjs gives it `ownerCall`. createChannelCaller
|
||||
(plugins/src/channel.mjs:16-20) uses the answer only to gate the webui channel, server-side.
|
||||
Sage's conditions, each pinned by G9:
|
||||
- the plugins answer never appears in a user's response;
|
||||
- in a user's chat the owner transport carries only `plugins` with `{}`;
|
||||
- every chat verb goes over the user transport.
|
||||
Mutant M8 routes one more chat verb over the owner transport, and G9 kills it. A new broker
|
||||
user verb would have been a policy change, so it is out.
|
||||
- **C7. The user transport is a new file, user-bus.mjs, not a mode in bus.mjs.**
|
||||
bus/tests/channel-proof.test.mjs pins the sha256 of webui/src/bus.mjs (with human-cli.mjs,
|
||||
human.mjs and cli/src/transport.mjs). Changing bus.mjs would mean an old-test change through
|
||||
Mos. user-bus.mjs repeats the spawn and framing code (about 40 lines). bus.mjs is byte-identical
|
||||
to the base. Tradeoff: two copies of the spawn plumbing. Accepted (sage-3153).
|
||||
**Follow-up (not this row):** the next row that re-pins the channel-proof hash folds userCall
|
||||
back into bus.mjs and drops the copy.
|
||||
- **C8. console-bus.mjs.** The bus-backed option set moved out of cli.mjs into `consoleBus(call,
|
||||
{send, plugins})`, so the owner and each user session build the same set the same way. The
|
||||
agent verb lists and `agentCaller` moved with it; the tester socket still builds its own.
|
||||
- **C9. 401 and 403 from the broker.** chat-http's STATUS maps `access-refused` to 403 and
|
||||
`access-revoked` to 401, so a broker refusal reaches the browser as the right status.
|
||||
|
||||
## Exception: the owner transport in a user's chat (C6; mos-1394, sage-3157)
|
||||
|
||||
Duckworth rules on this directly. The same text goes in the manifest notes.
|
||||
|
||||
> In a granted user's chat the Console uses the owner's transport for one call: `plugins` with
|
||||
> `{}`, the channel gate read. Nothing else goes over it. Its answer stays in the Console and is
|
||||
> never sent to the user. No value from the user's session or request reaches the call. Mos ruled
|
||||
> in mos-1394 that this is not an access-rule change, because the Console reads its own channel
|
||||
> gate, and that it holds only while nothing from the user reaches the call. Every chat verb goes
|
||||
> over the user's own transport. Where: createChat's `owner` (chat.mjs) and userOptionsFor
|
||||
> (console-bus.mjs), wired with `ownerCall` in cli.mjs. Pinned by G9 and mutants M8, M10 and M11.
|
||||
> If this reads as owner authority used for the user, it goes back to Mos.
|
||||
|
||||
## Rulings (sage-3153)
|
||||
|
||||
1. Boot wiring is accepted: a bad file refuses every user bind and the owner's bus boots. Added:
|
||||
one stderr line at boot naming the path and a code, never the contents (item 9, B3, M9). The
|
||||
mutual import is fine with no top-level use (item 9).
|
||||
2. C6 is accepted under conditions: the plugins answer stays server-side, the owner transport
|
||||
carries only `plugins` with `{}`, one G test (G9) records both transports, and a mutant (M8)
|
||||
routing one more chat verb over the owner is killed.
|
||||
3. C7 is accepted, with the follow-up note under C7.
|
||||
4. Owned files are accepted. packages/cli/src/access.mjs overlaps Morgana's row 295 (527002f97),
|
||||
and merge-tree is clean. 294 lands first and 295 rebases after it. packages/cli/tests/
|
||||
access.test.mjs is now unchanged: the peer test moved to access-boot.test.mjs, so the only
|
||||
overlap is access.mjs.
|
||||
|
||||
Ruling sage-3157 (mos-1394): the boot line is accepted as built, with no codes from R1. For C6,
|
||||
G9 asserts the owner call is exactly `plugins` with exactly `{}`, and that no value from the
|
||||
session or the request reaches it. Mutants M10 (session value) and M11 (request value) are killed
|
||||
by G9. The exception is written above and goes in the manifest notes. The per-session builder
|
||||
moved into console-bus.mjs (`userOptionsFor`), so the wiring G9 runs is the same code cli.mjs
|
||||
uses, and a session value added there is a mutant G9 can kill.
|
||||
|
||||
## Owned files
|
||||
|
||||
- cli: src/config.mjs, src/access.mjs, tests/access-boot.test.mjs (new)
|
||||
- webui src: sso.mjs, access.mjs (new), console-bus.mjs (new), user-bus.mjs (new), chat.mjs,
|
||||
chat-http.mjs, cli.mjs, serve.mjs
|
||||
- webui public: user.html (new), user.js (new), federation.js (new), index.html, app.js
|
||||
- webui tests: granted-user.test.mjs (new)
|
||||
- this BUILD.md
|
||||
|
||||
No old test changes. Hash pins checked: `git grep` over 'packages/*/tests/*' for each touched
|
||||
path with a 64-hex pin found only channel-proof's bus.mjs pin, which C7 avoids.
|
||||
|
||||
## Tests
|
||||
|
||||
New file packages/webui/tests/granted-user.test.mjs, on Sage's fake OP (tests/helpers/fake-oidc.mjs)
|
||||
and an in-process Broker with an access function, like bus/tests/user-principal.test.mjs:
|
||||
|
||||
- G1 a granted user signs in: session cookie set, `/api/config` says `user`, `GET /` is the user
|
||||
page, the chat lists reach the broker as `user:<id>`.
|
||||
- G2 an ungranted `sub` is refused: a user in the file with no grant, a grant for another business,
|
||||
an expired grant, and a sub in no file; each is `subject-unknown`, no cookie, nothing bound.
|
||||
- G3 revoked mid-session: sign in, then remove the grant, remove the subject, expire the grant,
|
||||
or make the loader refuse. The next chat read is 401 with a `user-revoked` log line and no
|
||||
transport call. Restoring the grant does not bring the session back, and `GET /` redirects to
|
||||
sign-in.
|
||||
- G4 owner verbs: for each owner route (board reads under /api/bus/*, settings, names, instances,
|
||||
routes, agents, processing, project files, /api/bus/message, /api/board), a user session gets
|
||||
403 `owner-only` and the transport ran zero times.
|
||||
- G5 the bus refusal stands alone: the same session's user transport, called directly with each
|
||||
owner verb, gets `access-refused` from the broker. The only write is the broker's own refusal
|
||||
evidence: exactly one `action.refused` event per verb, the count taken from the verb list's length
|
||||
(22 today), each body exactly `{ code: 'access-refused' }`, and every other table's row count
|
||||
unchanged (round 2; run 1 placed the round 1 "events unchanged" pin as a test defect, sage-3194).
|
||||
The broker's `#refused` (bus broker.mjs:4423) writes the code alone and no verb, so the test can't
|
||||
match each event to its verb; pinning the whole body means a field added there shows up here.
|
||||
- G9 the C6 seam (sage-3153, sage-3157), with the test's userOptions built by userOptionsFor as
|
||||
cli.mjs builds them: a user session runs the chat's project list, thread create, list,
|
||||
open, send and messages. The owner transport (a recording `ownerCall`) saw only
|
||||
`['plugins', {}]`. Each chat verb went over the user transport, and `plugins` never did. A
|
||||
marker in the owner's answer appears in no response. Every owner call is exactly
|
||||
`['plugins', {}]`, and none of alice, alice-sub, the thread name, the message body, the project
|
||||
id or the thread id appears in what the owner transport received.
|
||||
- G6 the notice: the user page's notice element text equals the sentence exactly.
|
||||
- G7 hub label and peer links render from config: `/api/config` carries self and the peers'
|
||||
labels and origins; the user and owner pages show "This Console: hub" and an `<a>` per peer with
|
||||
the configured href and label (browser test, sso-browser.test.mjs style).
|
||||
- G8 the transport: `userCall` over a recording unix socket in a scratch dir. While the child
|
||||
waits for the answer, the test reads /proc/<pid>/cmdline (argv[1] is USER_CLI), environ
|
||||
(MOSAIC_BUS_USER_NONCE set; MOSAIC_BUS_CLI_NONCE absent though the parent env passed one) and
|
||||
stat (ppid is the test process, so user-cli did not re-exec). The request line is
|
||||
`{verb, args, user: {business, user, nonce, pid}}`, which user-cli builds from
|
||||
`{business, user, verb, args}`.
|
||||
|
||||
packages/cli/tests/access-boot.test.mjs (sage-3142), over bootConfig and startBroker in a scratch
|
||||
root, the boot config through a JSON round trip as over the host IPC:
|
||||
|
||||
- B1 with access.json: `access.file` and `context.peers` set; alice (home a configured peer)
|
||||
binds; bob gets `access-revoked`.
|
||||
- B2 without the file: no `access` key; alice gets `access-revoked`.
|
||||
- B3 with text that is not JSON, a home that is not a configured peer, and a symbolic link: the
|
||||
bus boots and alice gets `access-revoked`. The warnings are exactly one line, with
|
||||
`access-invalid`, `access-invalid` and `access-unreadable` and the path, and none quotes the
|
||||
file (a marker string in the file is absent from the line).
|
||||
|
||||
access-boot.test.mjs also holds the F8 peer test: a grant whose user's home is a configured peer id
|
||||
validates; an unconfigured peer id refuses `home must be a configured peer`.
|
||||
|
||||
Old tests: the sso.test.mjs and serve tests stay as they are. If one needs a change, it goes to
|
||||
Mos through Sage first (mos-1272).
|
||||
|
||||
## Mutants
|
||||
|
||||
| # | Change | Killed by | Expected |
|
||||
|---|---|---|---|
|
||||
| M1 | remove the `userRoute` gate call in serve.mjs | G4 (:239, first route answers 403 `access-refused`, not `owner-only`) | G4 red at :239 on its first route; G5 green in the same run |
|
||||
| M2 | skip `access.check(session)` per request | G3 | red |
|
||||
| M3 | change one word of the notice in user.html | G6 | red |
|
||||
| M4 | `peers: []` back in accessContextFor | the F8 peer test and B1 (access-boot.test.mjs) | red |
|
||||
| M5 | sign-in skips the grant test (any listed user signs in) | G2 | red |
|
||||
| M6 | userCall sets MOSAIC_BUS_CLI_NONCE or runs HUMAN_CLI | G8 | red |
|
||||
| M7 | bootConfig drops `access` | B1 | red |
|
||||
| M8 | chat.mjs routes `thread.list` over `owner` too | G9 | red |
|
||||
| M9 | accessFor drops the boot refusal line | B3 | red |
|
||||
| M10 | userOptionsFor passes `owner: (v, a) => owner(v, { ...a, sub: session.sub })` | G9 | red |
|
||||
| M11 | chat.mjs keeps the last request's args in `channel()` and passes them to `owner('plugins', …)` | G9 (thread create's `{project, name}` reaches owner) | red |
|
||||
|
||||
M1 is the brief's "owner verb still refused by the bus": with the Console gate gone, the owner
|
||||
verb reaches the broker over the user transport and the broker refuses it (`access-refused`, 403).
|
||||
G4 kills M1 at :239 on its first route, because it pins the Console's own code `owner-only`. The
|
||||
count and "nothing written" asserts at :242-244 come after the loop, so under M1 they never run;
|
||||
the run records G4's failure with the actual value (`access-refused`). The bus refusing on its own
|
||||
is shown by G5, green in the same M1 run. (Corrected per Duckworth C1, duckworth-0249b; the earlier
|
||||
row said G4's "refused, nothing written" stayed green, which cannot be observed under M1.)
|
||||
|
||||
## Run limits
|
||||
|
||||
Each run is `timeout -k 10 LIMIT`. TMPDIR comes from `mktemp -d /mnt/storage/scratch/tmp/gXXXXXX`. A
|
||||
timeout with no failed test goes to Sage first. A timeout in the shell phase goes to Mos unplaced
|
||||
(mos-1275). Under sage-3187, every LIMIT is at least 3x the longest recorded run, set before any run.
|
||||
The order is the webui, cli and bus suites whole, then each scripts/test-*.sh, then the mutants.
|
||||
|
||||
| Run | Longest recorded | Receipt | LIMIT |
|
||||
|---|---|---|---|
|
||||
| webui suite | 57 s | gizmoduck queue-264 BUILD.md:474 (row 278 run 3) | 600 s |
|
||||
| cli suite | 682 s | gizmoduck queue-270 BUILD.md:660 (7328da8ca) | 2100 s |
|
||||
| bus suite | 25 s | gizmoduck queue-270 BUILD.md:658 | 900 s |
|
||||
| scripts/test-auth.sh | 1.9 s | 3b91cfdaf:agents/gander/work/queue-253/run5-gate/SUMMARY.json | 126 s |
|
||||
| scripts/test-conductor.sh | 8.1 s | same | 145 s |
|
||||
| scripts/test-config.sh | 3.2 s | same | 130 s |
|
||||
| scripts/test-discord.sh | 8.8 s | same | 147 s |
|
||||
| scripts/test-extension-package.sh | 3.0 s | same | 130 s |
|
||||
| scripts/test-foundation.sh | 49.4 s | same | 269 s |
|
||||
| scripts/test-land-work.sh | 16.9 s | same | 171 s |
|
||||
| scripts/test-queue.sh | 35.2 s | same | 226 s |
|
||||
| scripts/test-release.sh | 12.2 s | same | 157 s |
|
||||
| scripts/test-task.sh | 36.8 s | same | 231 s |
|
||||
| each mutant probe (its killing file alone) | none: granted-user.test.mjs and access-boot.test.mjs are new | the first green run in the suites sets the record | 300 s |
|
||||
|
||||
The script limits are Gander's SHELL-LIMITS values (3x the longest recorded run, plus 120 s), as
|
||||
recorded in agents/gander/work/queue-276/BUILD.md. If a new file's suite time is over 100 s, its
|
||||
mutant LIMIT goes to 3x that time before the probes run.
|
||||
|
||||
## Review
|
||||
|
||||
Round 1, manifest 0b3f9c9d…94f3 (ca710eabd): Duckworth APPROVE (static), duckworth-0249b, note
|
||||
agents/duckworth/work/row294/ROUND1-ca710eabd.md on duckworth/reviews 5b7089ea1. The owner-transport
|
||||
exception is accepted and does not go to Mos.
|
||||
|
||||
- C1: the M1 row and note above are corrected. The M1 run records G4's :239 failure value and G5.
|
||||
- C2: the gate runs on Sage's CLEAR.
|
||||
- N1 (no test reaches the C2b swap): no M12 in this row. Adding one needs a new test and a new
|
||||
manifest round after approval; the brief does not require it. Left as a follow-up for Sage to
|
||||
place: a test that a user session's options carry no owner caller besides `plugins`, with an M12
|
||||
where serve.mjs keeps the owner's `send` for a user session.
|
||||
- N2, N3: accepted as they stand; no change.
|
||||
- N4: the user-bus.mjs fold-back (C7) is proposed to Sage as a queue row, for the next
|
||||
channel-proof re-pin.
|
||||
|
||||
Round 2 (Mos mos-1412 through Sage sage-3195; for Duckworth): two test changes after run 1, no
|
||||
product change. The manifest adds packages/webui/tests/browser.test.mjs (18 paths).
|
||||
|
||||
- Red 1, old test, Mos approved (a): browser.test.mjs:22 pins index.html's script list. It gains
|
||||
'/federation.js' at index 2, where 294's index.html:7 puts it, ahead of brand.js. The :21 comment
|
||||
adds row 294 and federation.js. No other assertion in the file changes. Old and new lines are in
|
||||
ASSERTION-PRESERVATION.json. (b), loading the federation view another way, was rejected.
|
||||
- Red 2, new test G5: the pin is the one in the G5 note above. Mos's suggestion is taken: the count
|
||||
is `refusals.length`, not a literal. The verb check is not possible, because the event names no
|
||||
verb; the test pins the whole body instead.
|
||||
|
||||
Round 3 (sage-3212; goes to Mos as the row's second red, sage-3211, before any push or run): one
|
||||
changed line, in new test G5. Its baseline was `SELECT max(seq) n FROM events`. The G5 fixture has
|
||||
an empty events table, so that is NULL and `seq > NULL` matches no row: run 2 read 0 events added
|
||||
where run 1's count(*) had shown 22. The line is now `SELECT coalesce(max(seq), 0) n FROM events`.
|
||||
Nothing else in G5 and no product file changes. The manifest stays 18 paths; only
|
||||
granted-user.test.mjs's hash moves. My round 2 check missed the empty-table case.
|
||||
|
||||
Round 4 (Mos mos-1419 through Sage sage-3238, after run 3's third red): two test files, no product
|
||||
file. The manifest adds packages/cli/tests/stack-release.test.mjs (19 paths).
|
||||
|
||||
- stack-release.test.mjs, an old test: the rehearsal fixture copies a fixed list of config.mjs's
|
||||
imports into each scratch copy. 294's config.mjs imports access.mjs, which imports transport.mjs
|
||||
(transport.mjs imports only errors.mjs). The :1991 list gains both, with a row 294 comment at
|
||||
:1987. No assertion changes. The real copy carries all of packages/cli/src (stack-copy.sh
|
||||
COPY_PATHS), so the product was not affected.
|
||||
- access-boot.test.mjs, new test B1-B3: the shared helpers.mjs business sets tracker.baseUrl
|
||||
without tracker.project, so bootConfig warns about it before any access line (trackerFor runs
|
||||
before accessFor in config.mjs). A TRACKER constant holds that line. B1 and B2 pin `[TRACKER]`.
|
||||
B3 pins `[TRACKER, <the access refusal line>]`, and the secret-marker check reads every warning
|
||||
joined. boot() still records every warning. Mos chose a full pin over a filter: it also catches a
|
||||
second tracker line, a reordering, and config.mjs:102's context warning. I proposed a
|
||||
startsWith('access file') filter; Sage found it would hide that :102 warning.
|
||||
- My miss: B1-B3 were new and had not run before the gate.
|
||||
|
||||
## Runs
|
||||
|
||||
### Run 1, 2026-10-23 04:57Z (sage-3187 CLEAR), head 685ff053b, manifest 0b3f9c9d…94f3
|
||||
|
||||
Before the run, the branch was rebased from 253346e3c onto c902a767d. `git range-diff -s` showed all 6
|
||||
commits "=", and the stable patch-ids matched. The head was pushed as launchpad/row294-on-c902a767d
|
||||
024499fc2. TMPDIR was under /mnt/storage/scratch/tmp. npm ci --ignore-scripts ran at the root only.
|
||||
|
||||
| Step | Result | Time | Limit |
|
||||
|---|---|---|---|
|
||||
| webui suite | 365 tests: 362 pass, 2 fail, 1 skip | 55.8 s | 600 s |
|
||||
|
||||
The driver stopped at the first red, so the cli, bus, scripts and mutants did not run. The slot was
|
||||
returned (launchpad-0060).
|
||||
|
||||
- browser.test.mjs:22, an old test, pins index.html's script list. 294 adds `/federation.js` at
|
||||
index 2. It went to Mos through Sage (sage-3192); I made no change before the ruling.
|
||||
- G5:262, a new test, failed `22 !== 0`: the broker writes one `action.refused` per refusal. This
|
||||
was placed as a test defect under mos-1272 and accepted by Sage (sage-3194). It is fixed in round 2.
|
||||
- The skip was the editor rebuild check (editor-provenance.test.mjs:162), which needs esbuild
|
||||
0.28.2 from packages/webui's own lockfile. An environment fix: the next run also runs npm ci
|
||||
--ignore-scripts in packages/webui (sage-3194).
|
||||
- The last landed whole-webui gate skipped the same test: row 264a's run, webui 345 pass of 346 with
|
||||
1 skipped, "the esbuild 0.28.1 editor rebuild" (gizmoduck queue-264 BUILD.md:516-518, DEFERRED
|
||||
264a N2). Row 270's gate ran only webui pinned-copy.test. The fresh 294 gate still runs npm ci in
|
||||
packages/webui first, so the test should run here. If it skips again, the run names it and says why.
|
||||
|
||||
### Run 2, 2026-10-23 05:33Z (sage-3199 CLEAR), head 66c181717, manifest 7ec7b1ea…315b
|
||||
|
||||
Duckworth's round 2 approve was recorded first (duckworth-0253, rev 3455, comment 31029). Drivers
|
||||
and logs are in run2/. TMPDIR was under /mnt/storage/scratch/tmp.
|
||||
|
||||
| Step | Result | Time | Limit |
|
||||
|---|---|---|---|
|
||||
| npm ci --ignore-scripts, root | ok, esbuild 0.28.1 | 2.2 s | 900 s |
|
||||
| npm ci --ignore-scripts, packages/webui | ok, esbuild 0.28.2 | 1.2 s | 900 s |
|
||||
| webui suite | 365 tests: 364 pass, 1 fail, 0 skipped | 58.2 s | 600 s |
|
||||
|
||||
The driver stopped at the first red. The slot was returned (launchpad-0070), and I made no edit.
|
||||
|
||||
- The editor rebuild check ran and passed, and browser.test.mjs passed with the round 2 pin.
|
||||
- G5:269 failed `0 !== 22`, "one event per refused verb". Every verb was still refused with
|
||||
`access-refused`. Placed as a test defect (the NULL baseline, round 3 above); as the row's
|
||||
second red it went to Mos through Sage (sage-3211) before the fix runs.
|
||||
|
||||
### Run 3, 2026-10-23 05:55Z (sage-3231 CLEAR), head f26584089, manifest cb1648f3…78de
|
||||
|
||||
Duckworth's round 3 approve was recorded first (duckworth-0255b, rev 3462, c31045). The drivers,
|
||||
summary, webui log and the cli failure section are in run3/. TMPDIR was under
|
||||
/mnt/storage/scratch/tmp.
|
||||
|
||||
| Step | Result | Time | Limit |
|
||||
|---|---|---|---|
|
||||
| npm ci --ignore-scripts, root | ok | 2.2 s | 900 s |
|
||||
| npm ci --ignore-scripts, packages/webui | ok | 2.2 s | 900 s |
|
||||
| webui suite | 365 tests: 365 pass, 0 fail, 0 skipped | 58.2 s | 600 s |
|
||||
| cli suite | 909 tests: 889 pass, 20 fail, 0 skipped | 541.5 s | 2100 s |
|
||||
|
||||
The driver stopped at the cli suite, so the bus suite, the scripts and the mutants did not run. No
|
||||
failure was compactions-launch, so the :229 rerun rule did not apply. The slot was returned
|
||||
(launchpad-0076), and I made no edit before Mos's ruling (round 4 above).
|
||||
|
||||
- stack-release.test.mjs, 17 failures: R1-R7, R9-R12, R14, R15, row 233's --backup last test, and
|
||||
row196's rehearsal test at :2586 with its socket copy and legacy copy subtests. Each rehearsal
|
||||
failed at scratch-config: "Cannot find module …/packages/cli/src/access.mjs imported from
|
||||
…/packages/cli/src/config.mjs". My launchpad-0076 put the two subtests under row 233; they are
|
||||
row196's.
|
||||
- access-boot.test.mjs B1, B2 and B3: `warnings` also held the fixture's tracker line.
|
||||
|
||||
### Run 4, 2026-10-23 06:20Z (sage-3244 CLEAR, sage-3248 go), head 8616b9a4f, manifest a1ff5d67…9b99
|
||||
|
||||
Duckworth's round 4 approve was recorded first (duckworth-0257b, rev 3469, c31053). The run started
|
||||
at 06:20:53Z, on duckworth-0257b's relay of sage-3247; sage-3248 arrived seconds later with the same
|
||||
go. TMPDIR was /mnt/storage/scratch/tmp/gd3JQ8r. Receipts are in run4/: summary.txt, the drivers,
|
||||
suite-totals.txt (the three suites' node --test totals; the whole logs stay in scratch), every
|
||||
scripts/test-*.sh log, and each mutant's diff and TAP output with mutants.json.
|
||||
|
||||
| Step | Result | Time | Limit |
|
||||
|---|---|---|---|
|
||||
| npm ci --ignore-scripts, root | ok | 1.9 s | 900 s |
|
||||
| npm ci --ignore-scripts, packages/webui | ok | 0.5 s | 900 s |
|
||||
| webui suite | 365 tests: 365 pass, 0 fail, 0 skipped | 57.9 s | 600 s |
|
||||
| cli suite | 909 tests: 909 pass, 0 fail, 0 skipped | 600.1 s | 2100 s |
|
||||
| bus suite | 873 tests: 873 pass, 0 fail, 0 skipped | 24.4 s | 900 s |
|
||||
| scripts/test-auth.sh | ok, 15 passed | 0.8 s | 126 s |
|
||||
| scripts/test-conductor.sh | ok | 6.8 s | 145 s |
|
||||
| scripts/test-config.sh | ok | 2.5 s | 130 s |
|
||||
| scripts/test-discord.sh | ok | 7.3 s | 147 s |
|
||||
| scripts/test-extension-package.sh | ok | 1.2 s | 130 s |
|
||||
| scripts/test-foundation.sh | ok | 39.1 s | 269 s |
|
||||
| scripts/test-land-work.sh | ok | 11.8 s | 171 s |
|
||||
| scripts/test-queue.sh | ok | 20.4 s | 226 s |
|
||||
| scripts/test-release.sh | ok | 2.7 s | 157 s |
|
||||
| scripts/test-task.sh | ok, 91 passed; the two live provider cases skip by design (row 244), no Docker group skipped | 13.9 s | 231 s |
|
||||
|
||||
No step was rerun, so the :229 rule was not used. The worktree was clean after the scripts and after
|
||||
every mutant restore.
|
||||
|
||||
Mutants, each its killing file alone under timeout -k 10 300, all killed (MUTANTS rc=0, 06:34:22Z):
|
||||
|
||||
| # | Seconds | Red |
|
||||
|---|---|---|
|
||||
| M1 | 1.9 | G4 only; G5 ok in the same run |
|
||||
| M2 | 2.1 | G3 |
|
||||
| M3 | 1.8 | G6, and G7 (the page text changed under it) |
|
||||
| M4 | 0.5 | the F8 peer test, B1 |
|
||||
| M5 | 2.1 | G2, G3 |
|
||||
| M6 | 2.0 | G8 |
|
||||
| M7 | 0.3 | B1, B3 |
|
||||
| M8 | 2.1 | G1, G9 |
|
||||
| M9 | 0.4 | B3 |
|
||||
| M10 | 2.0 | G9 |
|
||||
| M11 | 2.0 | G9 |
|
||||
|
||||
M1 record (Duckworth C1): G4's first failure is its first route, `GET /api/bus/inbox: 502
|
||||
{"error":"access-refused","message":"access-refused"}`, expected 403 (run4/M1.tap). The failing
|
||||
assertion is granted-user.test.mjs:241, the loop's status check. The Mutants table above says :239;
|
||||
round 2's `tables` helper moved it down two lines. With the Console gate gone, the broker refuses
|
||||
the owner verb itself, and the Console answers 502 with the broker's code. G5, the bus refusing
|
||||
every owner verb on its own, is `ok 5` in the same TAP.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Row 294 (FED R6, #1800) candidate manifest, round 4
|
||||
|
||||
Round 4 (Mos mos-1419 through Sage sage-3238, after run 3's third red): two test files, no product
|
||||
file. The manifest adds packages/cli/tests/stack-release.test.mjs (19 paths).
|
||||
|
||||
- packages/cli/tests/stack-release.test.mjs (old test): the rehearsal fixture's copy list at :1991
|
||||
gains 'packages/cli/src/access.mjs' and 'packages/cli/src/transport.mjs', with a row 294 comment
|
||||
at :1987. 294's config.mjs imports access.mjs, which imports transport.mjs. No assertion changes.
|
||||
- packages/cli/tests/access-boot.test.mjs (new test): a TRACKER constant holds the tracker warning
|
||||
the shared helpers.mjs business makes. B1 and B2 pin `[TRACKER]`; B3 pins `[TRACKER, <the access
|
||||
refusal line>]` and checks the joined warnings for secret-marker. boot() is unchanged.
|
||||
|
||||
Round 3 (cb1648f3…78de) was approved by Duckworth (rev 3462, c31045). Its text follows unchanged.
|
||||
|
||||
Round 3 (sage-3212, Mos ruling pending in sage-3211): one line in granted-user.test.mjs G5. The
|
||||
baseline is `SELECT coalesce(max(seq), 0) n FROM events`, because max(seq) is NULL on the fixture's
|
||||
empty events table and run 2 read 0 events added. Only that file's hash moves; still 18 paths. Round 2
|
||||
(7ec7b1ea…315b) was approved by Duckworth (rev 3455). Its text follows unchanged.
|
||||
|
||||
|
||||
Branch launchpad/row294, base c902a767d (the round 1 head ee77b3635 rebased with range-diff all "="
|
||||
and stable patch-ids unchanged; receipts in run1/). The round 3 head is the commit that adds this
|
||||
file. MANIFEST.sha256 lists the 18 code and test paths changed from the base, hashed from that
|
||||
commit's blobs (`sha256sum -c` passes on it). BUILD.md, ASSERTION-PRESERVATION.json and run1/ are
|
||||
the row note and are not in the manifest. webui/src/bus.mjs is byte-identical to the base
|
||||
(channel-proof hash pin, C7). packages/cli/tests/access.test.mjs is unchanged;
|
||||
packages/cli/src/access.mjs overlaps row 295, which rebases after this row (sage-3153).
|
||||
|
||||
Round 1 was 0b3f9c9d…94f3, approved by Duckworth (rev 3435, duckworth-0249b). Round 2 changes two
|
||||
tests after run 1 went red, and no product file:
|
||||
|
||||
- packages/webui/tests/browser.test.mjs (new to the manifest; an old test, Mos's ruling (a) in
|
||||
mos-1412): the script-list pin at :22 gains '/federation.js' at index 2 and the :21 comment adds
|
||||
row 294. No other line changes. ASSERTION-PRESERVATION.json has the old and new lines.
|
||||
- packages/webui/tests/granted-user.test.mjs, G5 (new test, a run 1 placement Sage accepted in
|
||||
sage-3194): exactly one `action.refused` per refused verb, the count from the verb list's length,
|
||||
each body exactly `{ code: 'access-refused' }`, and every other table's row count unchanged.
|
||||
|
||||
Run 1 (webui only, stopped at the first red) is recorded in BUILD.md. The whole gate runs fresh on
|
||||
Sage's next CLEAR: npm ci --ignore-scripts at the root and in packages/webui, the webui, cli and
|
||||
bus suites, every scripts/test-*.sh, then mutants M1-M11, with the limits in BUILD.md.
|
||||
|
||||
## Exception for Duckworth to rule on: the owner transport in a user's chat (C6; mos-1394, sage-3157)
|
||||
|
||||
> In a granted user's chat the Console uses the owner's transport for one call: `plugins` with
|
||||
> `{}`, the channel gate read. Nothing else goes over it. Its answer stays in the Console and is
|
||||
> never sent to the user. No value from the user's session or request reaches the call. Mos ruled
|
||||
> in mos-1394 that this is not an access-rule change, because the Console reads its own channel
|
||||
> gate, and that it holds only while nothing from the user reaches the call. Every chat verb goes
|
||||
> over the user's own transport. Where: createChat's `owner` (chat.mjs) and userOptionsFor
|
||||
> (console-bus.mjs), wired with `ownerCall` in cli.mjs. Pinned by G9 and mutants M8, M10 and M11.
|
||||
> If this reads as owner authority used for the user, it goes back to Mos.
|
||||
|
||||
## Other choices
|
||||
|
||||
C1-C9 and rulings sage-3142, sage-3153 and sage-3157 are in BUILD.md. In short: a separate user
|
||||
page (C1); an allowlist gate of user routes (C2); every bus-backed option swapped to the user
|
||||
transport behind the gate (C2b); liveness by the broker's own `covers` test (C3); the bus boot
|
||||
`access` wiring, where a bad file refuses each user bind and writes one stderr line with the path
|
||||
and a code, never the contents (C4, item 9); and user-bus.mjs as a copy of the spawn code (C7).
|
||||
@@ -0,0 +1,19 @@
|
||||
52206bd92b0f223bf9310d50db650f36e12836691d739b499192e3eedd81401c packages/cli/src/access.mjs
|
||||
a948886a8c57d700b3d116097c9597c2b29dc7cc89ff6e18ae57436754195853 packages/cli/src/config.mjs
|
||||
b2745e1a3067d8c439de4f9c9e0df532764d3ea77639d15e5b9d949ec5cd534d packages/cli/tests/access-boot.test.mjs
|
||||
96f239dfd25cd1408d7586016154798da42b1a0f32387d14705345d0aa4db2ce packages/cli/tests/stack-release.test.mjs
|
||||
1be61ba66c3b747decdb3e12de743265b8cc7ba7cd06c033393b157ce1bc0b70 packages/webui/src/access.mjs
|
||||
954dee1535b258d9590e4d102423d081303b640b3bea750bd3653883e67bf309 packages/webui/src/chat-http.mjs
|
||||
230a5759f5027f9208c716b176f9b0a30310d5f433493bfcd8862c71abe182ae packages/webui/src/chat.mjs
|
||||
7fcb28a7a67418a8d3fd26dd157860ec1a89fe1891887f661e53a17d3b3bbc7d packages/webui/src/cli.mjs
|
||||
941c8f91ad40264c5622d60af85e00f6198db31a0504b4b0caa48312d8f57c3e packages/webui/src/console-bus.mjs
|
||||
692c5eabb0fc866240b12931418c17d3392b6097575dfacf7425a6f0127dd291 packages/webui/src/public/app.js
|
||||
c41a34a32f23f742e30ca408bc7deda0fa8feb4f8c25c7036604c336bb1545eb packages/webui/src/public/federation.js
|
||||
e7673188c1d76ecc63cdc24e7d1470922b1d69bedeb994a862e94924edfc27d2 packages/webui/src/public/index.html
|
||||
969a52284b667e075b04e3fa63d45415f3517080784ee372ee4725caf51d15d9 packages/webui/src/public/user.html
|
||||
571ce191c0b5f545e3a65f7b388f754e2ca6396a68038ccc57321d8f490cb86e packages/webui/src/public/user.js
|
||||
9343ea9edb862c1545df2886754244ffd76b714cc90d7791fc32aee4ecfec594 packages/webui/src/serve.mjs
|
||||
70b243ed9a83ff822a4c7a148528ff5153574cc87d5c776fd2e12f340762ed76 packages/webui/src/sso.mjs
|
||||
8cc1185beda9085ced62470f1b9e39d95dcca2dcc8bf9325c18c2f0b2da618dc packages/webui/src/user-bus.mjs
|
||||
58f022643839b78c0bd149927157d1f7b36977212975a4fe549c9661c31c4e40 packages/webui/tests/browser.test.mjs
|
||||
673f09560b7cf4515856ab1181fce8fcc2f70d59fb01bda9d687cc3d9b259e95 packages/webui/tests/granted-user.test.mjs
|
||||
@@ -0,0 +1,6 @@
|
||||
4abc648140eef53c8cb841ae64c22afc4211d86b
|
||||
bee007832071139bac9af318096710aeb95f1f79
|
||||
6de7443b94d28676a42630986d86bac9de1479fb
|
||||
f3ed47f0b12f628cdbf0efe66970e184eef74ae2
|
||||
3cf8180c22ed655b4f163f7a35b06deb47b54493
|
||||
d1689ae8ba689ab811560b5f8ae82d95cc745c3e
|
||||
@@ -0,0 +1,6 @@
|
||||
4abc648140eef53c8cb841ae64c22afc4211d86b
|
||||
bee007832071139bac9af318096710aeb95f1f79
|
||||
6de7443b94d28676a42630986d86bac9de1479fb
|
||||
f3ed47f0b12f628cdbf0efe66970e184eef74ae2
|
||||
3cf8180c22ed655b4f163f7a35b06deb47b54493
|
||||
d1689ae8ba689ab811560b5f8ae82d95cc745c3e
|
||||
@@ -0,0 +1,6 @@
|
||||
1: c866a8a69 = 1: df21d2f4d launchpad: row 294 FED R6 build and plan, granted-user sign-in (unrun, #1800)
|
||||
2: 338e93185 = 2: f392be6ac launchpad: row 294 rulings sage-3153 (boot refusal line, owner transport seam G9, C7 follow-up, access.test.mjs unchanged; unrun, #1800)
|
||||
3: 77514b8b9 = 3: c22dcfdd0 launchpad: row 294 BUILD.md M4 kill names the moved test
|
||||
4: 9450774cb = 4: 0622c2fc8 launchpad: row 294 sage-3157, owner-transport exception written out, userOptionsFor, G9 pins no user value, M10/M11 (unrun, #1800)
|
||||
5: ca710eabd = 5: 65f1b81f3 launchpad: row 294 round 1 manifest (17 paths from 9450774cb) and notes with the owner-transport exception
|
||||
6: ee77b3635 = 6: 024499fc2 launchpad: row 294 BUILD.md M1 expectation corrected (Duckworth C1), review recorded with N1-N4 dispositions
|
||||
@@ -0,0 +1,3 @@
|
||||
TMPDIR /mnt/storage/scratch/tmp/g8gFnfF head 685ff053b6336311668104d08826e923d9f4532d
|
||||
webui rc=1 seconds=55.787 limit=600 04:58:35Z
|
||||
STOP at webui
|
||||
@@ -0,0 +1,13 @@
|
||||
✖ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (3039.999029ms)
|
||||
✖ R6 G5 the bus refuses every owner verb over the user transport on its own (555.936822ms)
|
||||
ℹ tests 365
|
||||
ℹ suites 0
|
||||
ℹ pass 362
|
||||
ℹ fail 2
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 1
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 55747.039135
|
||||
✖ failing tests:
|
||||
✖ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (3039.999029ms)
|
||||
✖ R6 G5 the bus refuses every owner verb over the user transport on its own (555.936822ms)
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 294 gate run 2, sage-3199: npm ci (root and packages/webui), suites whole, every
|
||||
# scripts/test-*.sh, then the mutants. Stops at the first red. FROM=<step> resumes at a step
|
||||
# (the mos-1411 single cli rerun uses FROM=cli TAG=-rerun).
|
||||
set -u -o pipefail
|
||||
WT=/mnt/storage/scratch/launchpad-r294/wt
|
||||
OUT=/mnt/storage/scratch/launchpad-r294/run2
|
||||
FROM=${FROM:-npm-root}; TAG=${TAG:-}
|
||||
export TMPDIR=$(mktemp -d /mnt/storage/scratch/tmp/gXXXXXX)
|
||||
echo "start $(date -u +%H:%M:%SZ) FROM=$FROM TAG=$TAG TMPDIR $TMPDIR head $(git -C $WT rev-parse HEAD)" >> $OUT/summary.txt
|
||||
[ -z "$(git -C $WT status --porcelain)" ] || { echo "STOP worktree not clean" | tee -a $OUT/summary.txt; exit 9; }
|
||||
on=0
|
||||
step() { # name limit dir cmd...
|
||||
local name=$1 limit=$2 dir=$3; shift 3
|
||||
[ $on = 1 ] || [ $name = $FROM ] || return 0; on=1
|
||||
local t0=$(date +%s%N)
|
||||
( cd $dir && timeout -k 10 $limit "$@" ) > $OUT/$name$TAG.log 2>&1
|
||||
local rc=$?
|
||||
local ms=$(( ($(date +%s%N) - t0) / 1000000 )); local dt=$((ms/1000)).$(printf %03d $((ms%1000)))
|
||||
echo "$name$TAG rc=$rc seconds=$dt limit=$limit $(date -u +%H:%M:%SZ)" | tee -a $OUT/summary.txt
|
||||
[ $rc -eq 0 ] || { echo "STOP at $name$TAG" | tee -a $OUT/summary.txt; exit $rc; }
|
||||
}
|
||||
step npm-root 900 $WT npm ci --ignore-scripts
|
||||
step npm-webui 900 $WT/packages/webui npm ci --ignore-scripts
|
||||
step webui 600 $WT/packages/webui node --test tests/
|
||||
step cli 2100 $WT/packages/cli node --test tests/*.test.mjs
|
||||
step bus 900 $WT/packages/bus node --test tests/*.test.mjs
|
||||
declare -A L=([auth]=126 [conductor]=145 [config]=130 [discord]=147 [extension-package]=130 [foundation]=269 [land-work]=171 [queue]=226 [release]=157 [task]=231)
|
||||
for s in auth conductor config discord extension-package foundation land-work queue release task; do
|
||||
step test-$s ${L[$s]} $WT bash scripts/test-$s.sh
|
||||
done
|
||||
[ -z "$(git -C $WT status --porcelain)" ] || { echo "STOP worktree dirty after the scripts" | tee -a $OUT/summary.txt; git -C $WT status --porcelain >> $OUT/summary.txt; exit 9; }
|
||||
echo "SUITES GREEN $(date -u +%H:%M:%SZ)" | tee -a $OUT/summary.txt
|
||||
python3 -B $OUT/mutants.py
|
||||
rc=$?; echo "MUTANTS rc=$rc $(date -u +%H:%M:%SZ)" | tee -a $OUT/summary.txt; exit $rc
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
# Row 294 mutants M1-M11 (BUILD.md "Mutants"). Each mutant edits the worktree (every old text must
|
||||
# match exactly once), runs its killing file alone under timeout -k 10 300 with the TAP reporter,
|
||||
# then restores with git checkout and checks the tree is clean. Killed = the named killing test(s)
|
||||
# report "not ok". M1 also needs G5 "ok" in the same run (Duckworth C1).
|
||||
import json, os, re, subprocess, sys, time
|
||||
|
||||
WT = '/mnt/storage/scratch/launchpad-r294/wt'
|
||||
OUT = '/mnt/storage/scratch/launchpad-r294/run2'
|
||||
GU = ('packages/webui', 'tests/granted-user.test.mjs')
|
||||
AB = ('packages/cli', 'tests/access-boot.test.mjs')
|
||||
|
||||
CHAT_CALL = "verb === 'plugins' ? owner('plugins', {}) : call(verb, args)"
|
||||
MUTANTS = [
|
||||
('M1', [('packages/webui/src/serve.mjs', "if (!userRoute(path, req.method)) return json(res, 403, { error: 'owner-only'",
|
||||
"if (false) return json(res, 403, { error: 'owner-only'")], GU, ['R6 G4'], ['R6 G5']),
|
||||
('M2', [('packages/webui/src/sso.mjs', "if (s.user !== undefined && !users?.check(s)) {", "if (s.user !== undefined && false) {")], GU, ['R6 G3'], []),
|
||||
('M3', [('packages/webui/src/public/user.html', "but not your messages.", "but not your replies.")], GU, ['R6 G6'], []),
|
||||
('M4', [('packages/cli/src/access.mjs', "const peers = (system.federation?.peers ?? []).map((p) => p.id);", "const peers = [];")], AB,
|
||||
['R6 a configured peer id', 'R6 B1'], []),
|
||||
('M5', [('packages/webui/src/access.mjs', "return user && live(doc, user.id, b) ? user.id : null;", "return user ? user.id : null;")], GU, ['R6 G2'], []),
|
||||
('M6', [('packages/webui/src/user-bus.mjs', "env: { ...rest, MOSAIC_BUS_USER_NONCE", "env: { ...env, MOSAIC_BUS_USER_NONCE")], GU, ['R6 G8'], []),
|
||||
('M7', [('packages/cli/src/config.mjs', " if (access) config.access = access;\n", "")], AB, ['R6 B1'], []),
|
||||
('M8', [('packages/webui/src/chat.mjs', CHAT_CALL,
|
||||
"verb === 'plugins' ? owner('plugins', {}) : verb === 'thread.list' ? owner(verb, args) : call(verb, args)")], GU, ['R6 G9'], []),
|
||||
('M9', [('packages/cli/src/config.mjs', " warn(`access file refused at boot (${code}), no granted user binds until it is fixed: ${file}`);\n", "")],
|
||||
AB, ['R6 B3'], []),
|
||||
('M10', [('packages/webui/src/console-bus.mjs', "return session => consoleBus(transport(session.user), { send, owner });",
|
||||
"return session => consoleBus(transport(session.user), { send, owner: (v, a) => owner(v, { ...a, sub: session.sub }) });")], GU, ['R6 G9'], []),
|
||||
('M11', [('packages/webui/src/chat.mjs', "const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => " + CHAT_CALL + " });",
|
||||
"let last = {};\n const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => verb === 'plugins' ? owner('plugins', last) : call(verb, args) });"),
|
||||
('packages/webui/src/chat.mjs', " try { return await dispatch(verb, args); }", " try { last = args; return await dispatch(verb, args); }")], GU, ['R6 G9'], []),
|
||||
]
|
||||
|
||||
def git(*a):
|
||||
return subprocess.run(['git', '-C', WT, *a], capture_output=True, text=True, check=True).stdout
|
||||
|
||||
def restore(files):
|
||||
if files: git('checkout', '--', *sorted(files))
|
||||
if git('status', '--porcelain'): raise SystemExit('worktree not clean after restore')
|
||||
|
||||
def results(tap):
|
||||
# Top-level TAP test lines: "ok N - name" / "not ok N - name" at no indentation.
|
||||
return {m.group(2): m.group(1) == 'ok' for m in re.finditer(r'^(ok|not ok) \d+ - (.*)$', tap, re.M)}
|
||||
|
||||
def main():
|
||||
only = sys.argv[1:]
|
||||
if git('status', '--porcelain'): raise SystemExit('worktree not clean at start')
|
||||
env = dict(os.environ)
|
||||
summary, bad = [], 0
|
||||
for mid, edits, (pkg, test), killers, greens in MUTANTS:
|
||||
if only and mid not in only: continue
|
||||
files = set()
|
||||
try:
|
||||
for path, old, new in edits:
|
||||
p = os.path.join(WT, path); s = open(p).read()
|
||||
if s.count(old) != 1: raise SystemExit(f'{mid}: anchor count {s.count(old)} in {path}')
|
||||
open(p, 'w').write(s.replace(old, new)); files.add(path)
|
||||
diff = git('diff')
|
||||
t0 = time.time()
|
||||
r = subprocess.run(['timeout', '-k', '10', '300', 'node', '--test', '--test-reporter=tap', test],
|
||||
cwd=os.path.join(WT, pkg), capture_output=True, text=True, env=env)
|
||||
dt = round(time.time() - t0, 1)
|
||||
finally:
|
||||
restore(files)
|
||||
open(f'{OUT}/{mid}.diff', 'w').write(diff)
|
||||
open(f'{OUT}/{mid}.tap', 'w').write(r.stdout + '\n# stderr\n' + r.stderr)
|
||||
res = results(r.stdout)
|
||||
hit = lambda pre: [n for n, ok in res.items() if n.startswith(pre)]
|
||||
red = [n for n, ok in res.items() if not ok]
|
||||
killed = r.returncode not in (0, 124, 137) and all(any(not res[n] for n in hit(k)) for k in killers) \
|
||||
and all(hit(g) and all(res[n] for n in hit(g)) for g in greens)
|
||||
bad += not killed
|
||||
line = f'{mid} rc={r.returncode} seconds={dt} killed={killed} red={red}'
|
||||
print(line, flush=True)
|
||||
summary.append({'mutant': mid, 'rc': r.returncode, 'seconds': dt, 'killed': killed, 'red': red,
|
||||
'killers': killers, 'greens': greens})
|
||||
json.dump(summary, open(f'{OUT}/mutants.json', 'w'), indent=1)
|
||||
sys.exit(1 if bad else 0)
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,5 @@
|
||||
start 05:33:44Z FROM=npm-root TAG= TMPDIR /mnt/storage/scratch/tmp/gbuNdnK head 66c18171766ec35d2e84440bd4e62b13cc494dc9
|
||||
npm-root rc=0 seconds=2.179 limit=900 05:33:47Z
|
||||
npm-webui rc=0 seconds=1.204 limit=900 05:33:48Z
|
||||
webui rc=1 seconds=58.219 limit=600 05:34:46Z
|
||||
STOP at webui
|
||||
@@ -0,0 +1,418 @@
|
||||
✔ row 194 T16 Settings Agents: unavailable read-only, launch follows its request, confirms, reason and refusal (21200.447968ms)
|
||||
✔ row 256 T7 T8 the human page offers Delete on a creative instance, the tester page never does (3851.801754ms)
|
||||
✔ row 194 T16: each agent action makes one call with only its keys, logs the user and answers the request (47.103306ms)
|
||||
✔ row 194 T16: the boundary refuses before any bus call (21.672718ms)
|
||||
✔ row 194 T16: read-only when send is off or no writer is wired; no route has a --send exception (10.39829ms)
|
||||
✔ row 194 T16: a bus refusal names its reason and nothing changed; an unknown outcome or a bad shape is never retried (12.273549ms)
|
||||
✔ row 194 T16: the request read takes one id, adds the refusal reason and refuses an unexpected shape (12.546557ms)
|
||||
✔ row 194 T16: delete joins add and change on /api/instances with only revision, instance and op (4.813888ms)
|
||||
✔ row 194 T16 D4.1: the tester listener launches, terminates and reads requests; regenerate, veto and delete are refused with no call (16.913793ms)
|
||||
✔ row 194 N5: agents.js is served as JavaScript and index.html loads it once (5.167141ms)
|
||||
✔ Q169 the signed-in human names a seat and stops the assistant, one bus call each, logged with the user (56.448865ms)
|
||||
✔ Q169 the name and stop routes need send on, the public name, the user, the exact Origin and same-origin Fetch Metadata (30.24353ms)
|
||||
✔ Q169 the name and stop routes take JSON within 4 KiB and only their own keys, all text (13.456376ms)
|
||||
✔ Row 167 a null body on the name and stop routes is 400 invalid-request with no write, and the next request is answered (7.904021ms)
|
||||
✔ Q169 a bus refusal says nothing changed; a lost or odd answer says the change may have been made (15.513711ms)
|
||||
✔ Q169 the names read is a bus read like the others (4.61894ms)
|
||||
✔ 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 (9226.01783ms)
|
||||
✔ a Seen click while a Board read is in flight waits for that read, then posts (3864.427337ms)
|
||||
✔ 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 (12005.864452ms)
|
||||
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (6607.304173ms)
|
||||
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
|
||||
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (8415.467371ms)
|
||||
✔ the README says a refused field table names the file or the key at fault, never a path (2.547364ms)
|
||||
✔ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes (13566.719029ms)
|
||||
✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (200.737611ms)
|
||||
✔ decisions route serves the roles and recent lists from the reader view (125.181042ms)
|
||||
✔ credentials route serves the broker credential status from the reader view (35.913347ms)
|
||||
✔ replies route serves the replies to one message from the reader view (74.493573ms)
|
||||
✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (117.272719ms)
|
||||
✔ bus refusals map to statuses and never carry a path or a stack (84.725063ms)
|
||||
✔ humanCall speaks the human transport protocol without blocking the server (2756.823825ms)
|
||||
✔ busReader takes --business, else the live bus host, else refuses (744.665939ms)
|
||||
✔ humanCall kills a transport that runs past its timeout (590.725094ms)
|
||||
✔ serve refuses a --business value that is not a business id (1190.080637ms)
|
||||
✔ WebUI channel off returns 503 and named recovery for every chat route without a chat operation (77.716579ms)
|
||||
✔ WebUI only selection keeps web chat enabled while retaining the old read envelope (5.930279ms)
|
||||
✔ WebUI channel-disabled recovery text is fixed and never echoes transport text (3.049595ms)
|
||||
✔ chat browser: list/create named projects and threads, hostile labels inert and no implicit writes (3800.342351ms)
|
||||
✔ chat browser: thread drafts, receipts and cursor history stay isolated; refresh never writes (4591.830173ms)
|
||||
✔ chat browser: uncertain writes retain draft, ownership refusal clears it, read-only cannot send (4127.562754ms)
|
||||
✔ chat browser: a late read from A cannot paint B; sign-out clears private state and stops reads (3736.559168ms)
|
||||
✔ chat browser: page forward and deduplicate replies; pending and byte-limit submissions do not duplicate (2856.733288ms)
|
||||
✔ chat browser: scrolled history stays put, New messages and an own send return to the tail (2725.878046ms)
|
||||
✔ chat browser: automatic polling follows a reply without a POST or a lost draft (12630.063778ms)
|
||||
✔ chat browser: display stays bounded while every forward page remains reachable (2518.810917ms)
|
||||
✔ chat browser: a confirmed visible message replaces its pending receipt; malformed receipts stay unknown (2860.187009ms)
|
||||
✔ chat browser: a refusal elsewhere forgets private chat drafts (1658.755391ms)
|
||||
✔ chat browser: a late project creation receipt cannot navigate away from another thread (1330.247015ms)
|
||||
✔ chat reads map only fixed verbs, strict forward paging and pure GET (72.670606ms)
|
||||
✔ chat refuses malformed and repeated query/id values before transport (20.974981ms)
|
||||
✔ chat project.create: one verified write, fixed identity/class and private log (9.42558ms)
|
||||
✔ chat project.create: each existing Console write fence makes zero bus calls (20.315573ms)
|
||||
✔ chat project.create: refusal or lost/malformed receipt never retries or invents success (16.582154ms)
|
||||
✔ chat thread.create: one verified write, fixed identity/class and private log (5.075296ms)
|
||||
✔ chat thread.create: each existing Console write fence makes zero bus calls (20.803577ms)
|
||||
✔ chat thread.create: refusal or lost/malformed receipt never retries or invents success (18.74415ms)
|
||||
✔ chat thread.send: one verified write, fixed identity/class and private log (9.869431ms)
|
||||
✔ chat thread.send: each existing Console write fence makes zero bus calls (21.141103ms)
|
||||
✔ chat thread.send: refusal or lost/malformed receipt never retries or invents success (17.273069ms)
|
||||
✔ chat thread.open: one verified write, fixed identity/class and private log (2.908672ms)
|
||||
✔ chat thread.open: each existing Console write fence makes zero bus calls (42.171683ms)
|
||||
✔ chat thread.open: refusal or lost/malformed receipt never retries or invents success (16.741319ms)
|
||||
✔ chat createThread receipt requires an instance even when its id is valid (4.654023ms)
|
||||
✔ chat write receipts expose only the fields belonging to their operation (9.245546ms)
|
||||
✔ chat validates names, body, project and strict empty open payload (9.689665ms)
|
||||
✔ chat read failures redact ownership and refuse invalid transport arrays (10.834021ms)
|
||||
✔ REQ-USER-2 real broker: owned catalog, create/name/filter, isolated history and 403/404 without mutations (259.430605ms)
|
||||
✔ chat repeats every POST boundary even when called behind an admitted request (2.308805ms)
|
||||
✔ configured Console process injects chat through its existing human transport (901.59636ms)
|
||||
✔ chat assets, GET purity, method and unknown-route refusals are explicit (206.409025ms)
|
||||
✔ the chat seam names the transport it is given: the human one and the tester one both pass, anything else is a TypeError (1.199765ms)
|
||||
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (5146.976274ms)
|
||||
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (5150.763373ms)
|
||||
✔ conversation view: a newer session with no readable history keeps the marker (2848.192209ms)
|
||||
✔ conversation view: seats without history say so and offer no reply (3518.976325ms)
|
||||
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (4400.65459ms)
|
||||
✔ the editor: the list, hostile markdown that runs nothing, the draft, and each save receipt (9331.57255ms)
|
||||
✔ the Console's CSP stands and the editor runs under it; hostile markdown on the load path runs and loads nothing (6067.385422ms)
|
||||
✔ hostile content pasted into the editor runs and loads nothing, and a bad link target is refused in the link bar (4845.559933ms)
|
||||
✔ a pasted image span is kept only for a target the link gate accepts, and a save writes only those (3491.829799ms)
|
||||
✔ a switch of view reads nothing; a reload, and a trip away and back, each read the file exactly once (5866.549827ms)
|
||||
✔ an edit rewrites only the block touched; a save sends the rest byte for byte; the Source view carries the draft (3788.939927ms)
|
||||
✔ a file the editor cannot write back byte for byte opens as Source, says so, cannot save, and the Visual button works once it can (4234.458458ms)
|
||||
✔ a link title pasted with line breaks in it is written on one line, so no HTML block comes of it (2605.728378ms)
|
||||
✔ an image alt pasted with line breaks in it is written on one line, so no HTML block comes of it (1999.78511ms)
|
||||
✔ an image title pasted with line breaks in it is written on one line, so no HTML block comes of it (2077.446601ms)
|
||||
✔ a code block info string pasted with line breaks in it is written on one line, so no HTML block comes of it (3718.637005ms)
|
||||
✔ a link title holding a quote after a backslash then a tag is written with the backslash escaped, so the title stays closed (1909.173557ms)
|
||||
✔ an image title holding a quote after a backslash then a tag is written with the backslash escaped, so the title stays closed (1433.734236ms)
|
||||
✔ a link title holding a trailing backslash is written with the backslash escaped, so the title stays closed (1056.073164ms)
|
||||
✔ an image title holding a trailing backslash is written with the backslash escaped, so the title stays closed (1029.588894ms)
|
||||
✔ a code block info string with a backtick, pasted, is dropped on Save, so the fence stays closed (987.373566ms)
|
||||
✔ a code block info string with a backtick, as a tilde fence can have, is kept as source, not rewritten without it (691.874734ms)
|
||||
✔ the imported source is the tagged upstream source plus the recorded local changes, and nothing else (161.636186ms)
|
||||
✔ every package names its origin and license, and NOTICE lists each (1.293126ms)
|
||||
✔ the build reads only imported source and the editor's own files; the bundle is the recorded build (13.549655ms)
|
||||
✔ MOSAIC_REQUIRE_EDITOR_BUILD: a missing or wrong esbuild skips without it and fails with it; a wrong binary always fails (7.776306ms)
|
||||
✔ the toolchain check, run as a program, exits non-zero without esbuild when the build is required (350.656359ms)
|
||||
▲ [WARNING] Ignoring this import because "third_party/editor/prosemirror-transform/src/mark.ts" was marked as having no side effects [ignored-bare-import]
|
||||
|
||||
third_party/editor/prosemirror-transform/src/index.ts:10:7:
|
||||
10 │ import "./mark"
|
||||
╵ ~~~~~~~~
|
||||
|
||||
"sideEffects" is false in the enclosing "package.json" file:
|
||||
|
||||
third_party/editor/prosemirror-transform/package.json:13:2:
|
||||
13 │ "sideEffects": false,
|
||||
╵ ~~~~~~~~~~~~~
|
||||
|
||||
✔ rebuilding from the source gives the same bytes (needs the pinned esbuild; MOSAIC_REQUIRE_EDITOR_BUILD=1 makes its absence a failure) (555.743104ms)
|
||||
✔ the editor routes serve the built files with their types and no cache (100.21958ms)
|
||||
✔ open and save with no edit writes the same bytes: the goals review and a doc with every element (36.982058ms)
|
||||
✔ open and save with no edit writes the same bytes: every repository doc under docs/ and spec/ (8452.885873ms)
|
||||
✔ the editor source builds markup only with DOM calls, and the bundle has no eval (4.009583ms)
|
||||
✔ the link gate and the image rule are the preview's (2.055746ms)
|
||||
✔ R6 G1 a granted user signs in, gets the user page and reaches the broker as itself (330.267727ms)
|
||||
▶ R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
✔ a listed user with no grant (903.4736ms)
|
||||
✔ a grant in another business (237.185135ms)
|
||||
✔ an expired grant (890.423789ms)
|
||||
✔ another user's grant (453.888332ms)
|
||||
✔ a sub in no file (449.315611ms)
|
||||
✔ a file the loader refuses (388.859391ms)
|
||||
✔ R6 G2 a subject with no live grant in this business is refused subject-unknown (3334.806179ms)
|
||||
▶ R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
✔ grant removed (268.650779ms)
|
||||
✔ subject removed (267.295293ms)
|
||||
✔ grant expired (665.836412ms)
|
||||
✔ file now refused (500.82568ms)
|
||||
✔ R6 G3 a revoke counts on the next request: 401 and the session is gone (1704.325802ms)
|
||||
✔ R6 G4 every owner route answers 403 owner-only before any bus call (340.702221ms)
|
||||
✖ R6 G5 the bus refuses every owner verb over the user transport on its own (485.568404ms)
|
||||
✔ R6 G9 a user chat sends only the plugins read over the owner transport (612.749445ms)
|
||||
✔ R6 G6 the user page holds the notice word for word (324.783385ms)
|
||||
✔ R6 G7 the hub label and plain peer links render from config on both pages (4356.409923ms)
|
||||
✔ R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request (269.315168ms)
|
||||
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (55038.290223ms)
|
||||
✔ row 165 Settings: role instances read, add with a restart named, change without one, a refusal keeps the draft (8162.45209ms)
|
||||
✔ row 165 Console: the signed-in human adds an instance through a real broker; validate exits 0, the log names the human and no restart is needed (row 194) (1333.106508ms)
|
||||
✔ row 165 Console: the instance write needs send on, the public name, the user, the exact Origin and same-origin Fetch Metadata (19.101403ms)
|
||||
✔ row 165 Console: the instance write takes JSON within 16 KiB and only its own keys; holder, credentials and an actor are refused (36.91494ms)
|
||||
✔ row 165 Console: a bus refusal says nothing changed; a lost or odd answer says the change may have been made (34.62602ms)
|
||||
✔ row 165 Console: the instances read takes no arguments, needs a bus and refuses an answer without a revision (24.175094ms)
|
||||
✔ C2-04 CLI: both listener callback pairs dispatch library verbs with their own transport (518.811424ms)
|
||||
✔ row 254: each library sync code maps to its status (1.180458ms)
|
||||
✔ REQ-LIMIT-3 REQ-LIMIT-4 Settings shows token limits counted, not enforced and check failed; Reset posts one path; a save keeps the token limits (6298.863107ms)
|
||||
✔ Row 167 with an assistant draft open, a token limit reset gives the assistant submit its revision (4276.44007ms)
|
||||
✔ REQ-LIMIT-3 limits read returns only the token rows; no query or write is accepted (145.506945ms)
|
||||
✔ REQ-LIMIT-3 a limits answer without a token list is refused, not shown as no limits (49.973437ms)
|
||||
2026-10-23T05:33:56.191Z settings user="jason" changed
|
||||
✔ REQ-LIMIT-3 a Settings reset reaches the human write with its paths; a reset alone is enough (17.215891ms)
|
||||
✔ REQ-LIMIT-3 the Console reader asks the bus limits verb with no arguments (0.883912ms)
|
||||
✔ the preview source names none of the four string-to-markup sinks (5.976009ms)
|
||||
✔ headings, paragraphs, emphasis and inline code render (2.996825ms)
|
||||
✔ lists, block quotes and code blocks render (1.216917ms)
|
||||
✔ a link is made for http, https and relative targets, with rel and the target shown (5.548947ms)
|
||||
✔ a link with any other scheme, or an evasion of the scheme, stays source text (2.805299ms)
|
||||
✔ an image shows alt text and URL, is never an img element and never loads (0.339927ms)
|
||||
✔ raw HTML, entities and unsupported blocks show as source text, never as elements (3.919101ms)
|
||||
✔ markup inside code and inside link text stays text (0.450973ms)
|
||||
✔ hostile and pathological input stays within its look-ahead budget and under the nesting cap (128.726332ms)
|
||||
✔ trailing blanks and closing hashes are cut by a walk from the end, not a regex that retries inside a blank run (84.266204ms)
|
||||
✔ a link opener with no closer searches at most SPAN characters ahead (559.485811ms)
|
||||
✔ the look-ahead work of one render is a fixed multiple of the text, whatever the opener (2605.970638ms)
|
||||
✔ past the budget an opener is source text, through the path all other text takes (80.262453ms)
|
||||
✔ a normal 200 KiB document renders exactly as it did without a budget, and spends little of it (66.666568ms)
|
||||
✔ a line separator inside a heading, a list item or a fence line does not end the marker line (0.96462ms)
|
||||
✔ a thematic-break line is a paragraph, every other marker line opens a list (5.448705ms)
|
||||
✔ every short line over space, tab, - * _ and a letter is a break exactly when the old regex said so (204.427802ms)
|
||||
✔ one line of marks does not throw, at 4 MiB, whatever the mark or the spacing (22559.426214ms)
|
||||
✔ CRLF and tabs are read like LF and spaces; empty input renders nothing (0.262399ms)
|
||||
✔ messages HTTP read pages and preserves metadata; bad arguments refuse before a read; other reads unchanged (504.206798ms)
|
||||
✔ 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 (5038.67421ms)
|
||||
✔ Messages nav count polls on Tasks and Board and clears on a Board refusal; messages 403 refuses local pages (5981.767963ms)
|
||||
✔ copy writes the Console paths at the full id, read-only, and leaves an existing copy alone (9096.497016ms)
|
||||
✔ copy refuses a commit off refactor, an unknown one, a worktree and a bad copies directory (7347.744628ms)
|
||||
✔ copy writes nothing when an archived path is missing at the commit (1868.63621ms)
|
||||
✔ a copy that loses the rename race leaves no read-only temp directory behind (3311.544636ms)
|
||||
✔ a copy whose rename fails with no target refuses, writes no copy and leaves no temp directory (2312.433164ms)
|
||||
✔ a copy that meets a target holding files says exists: and moves nothing into it (2364.990749ms)
|
||||
✔ a copy that meets an empty target directory replaces it by rename, not by moving into it (4067.081185ms)
|
||||
✔ serve runs from a copy: /healthz 200, the config check, roles and RELEASE from the copy, the queue from --checkout (2851.087141ms)
|
||||
✔ serve refuses a --checkout that is not an absolute directory path (670.341187ms)
|
||||
✔ consoleReads runs the queue read under queueRoot and reads RELEASE under root (90.16485ms)
|
||||
✔ preview options are all three or none, and each is checked (13.591058ms)
|
||||
✔ the CLI refuses a partial or invalid preview flag set at start (2803.4313ms)
|
||||
✔ the CLI with all three flags serves the preview mode (415.465059ms)
|
||||
✔ a request for the public name needs the header equal to the user id, else 401 and nothing read (110.760312ms)
|
||||
✔ the header name matches without regard to case, and the value compare is exact (10.083712ms)
|
||||
✔ the public name with the header reads the bus views, and the Host and Origin are checked first (112.090681ms)
|
||||
✔ in preview mode every write answers 403 read-only, from the public name and loopback, and reaches nothing (44.457086ms)
|
||||
✔ loopback requests are unchanged in preview mode, and the bus views work with the board down (10.641941ms)
|
||||
✔ a proxy that rewrites Host to the loopback port gets 403 on its first request, whatever the user (35.919573ms)
|
||||
✔ a repeated Host is 400 before any other check, in preview mode and without it (74.970474ms)
|
||||
✔ without the preview flags the public name is refused and writes still reach the board (23.308441ms)
|
||||
✔ processing browser: keyboard Stop, confirmed Resume, unknown outcome and persistent banner (3245.719335ms)
|
||||
✔ processing browser: Q175 read-only interim keeps both writes disabled and names CLI recovery at 320px (3800.114751ms)
|
||||
✔ processing browser: sign-out from config redirect blocks later refresh and writes (2745.133274ms)
|
||||
✔ processing browser: sign-out from state redirect blocks later refresh and writes (3220.693284ms)
|
||||
✔ processing browser: sign-out from write redirect blocks later refresh and writes (2763.829672ms)
|
||||
✔ processing browser: sign-out from config sign-in required blocks later refresh and writes (2580.25615ms)
|
||||
✔ processing browser: sign-out from state sign-in required blocks later refresh and writes (2291.250621ms)
|
||||
✔ processing browser: sign-out from write sign-in required blocks later refresh and writes (2322.675396ms)
|
||||
ROW191_LAYOUT {"width":400,"height":1000,"controls":97,"tree":324.0625,"main":1450.703125,"top":181.5,"bottom":217.5,"barBottom":129,"projectsTop":352.59375,"order":"0","visible":true}
|
||||
ROW191_LAYOUT {"width":1440,"height":1000,"controls":127,"tree":914,"main":914,"top":135.5,"bottom":171.5,"barBottom":53,"projectsTop":636.59375,"order":"0","visible":true}
|
||||
ROW191_LAYOUT {"width":1440,"height":420,"controls":127,"tree":334,"main":907.734375,"top":135.5,"bottom":171.5,"barBottom":53,"projectsTop":636.59375,"order":"0","visible":true}
|
||||
ROW191_LAYOUT {"width":800,"height":420,"controls":127,"tree":334,"main":1131.65625,"top":135.5,"bottom":171.5,"barBottom":53,"projectsTop":618.59375,"order":"0","visible":true}
|
||||
✔ processing browser: static nav controls keep Stop visible, ordered and keyboard reachable (1993.447545ms)
|
||||
✔ interim read-only controls show CLI recovery, disable both writes and retain a halted banner across views (11.88827ms)
|
||||
✔ Stop sends one empty halt and Resume confirms before sending the displayed generation (6.530287ms)
|
||||
✔ a cancelled resume never calls the writer (2.767886ms)
|
||||
✔ unknown writes are not retried or displayed as success and partial stops remain explicit (4.664918ms)
|
||||
✔ the persistent shell loads the control outside route-specific content with accessible status and native buttons (0.871493ms)
|
||||
✔ read-only Console can read processing state but neither human control writes without --send (56.737865ms)
|
||||
✔ Console halt and generation-bound resume use only the management writer and keep partial stops honest (14.881178ms)
|
||||
✔ processing write fences refuse cross-site, wrong origin, unnamed host, duplicate identity and bad payload before writer (17.485528ms)
|
||||
✔ tester cannot invoke either control writer and failures never echo backend text (42.124054ms)
|
||||
✔ processing writes use the existing SSO signer and still require --send and same-origin (39.331986ms)
|
||||
✔ processing handler independently refuses wrong origin and cross-site before its writer (3.45994ms)
|
||||
✔ a signed-in same-origin save reaches exactly one human operation and returns the new commit (41.25163ms)
|
||||
✔ a save needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata (13.607509ms)
|
||||
✔ a save validates JSON, strict keys, text values and the 1 MiB body before the bus is asked (33.994683ms)
|
||||
✔ a body that is JSON but not an object is refused 400, not thrown on (91.175327ms)
|
||||
✔ the 1 MiB limit is this route only; Settings and messages keep 16 KiB (4.985896ms)
|
||||
✔ a bus refusal is reported as not saved with its own status; stale-base tells the user to reload (66.286554ms)
|
||||
✔ a save whose answer is lost or unusable is reported unknown and is never retried (8.306276ms)
|
||||
✔ the file list and one file read take strict queries and arrive in the expected shape (13.677685ms)
|
||||
✔ reads work without --send; an unusable bus answer is a bad response, a refusal keeps its status (11.604929ms)
|
||||
✔ /api/config says edit only where a save can pass (3.953644ms)
|
||||
✔ a repository-name project refusal is an HTTP conflict (2.625569ms)
|
||||
✔ proof startup refuses partial flags non-preview and invalid fixture file metadata or bytes (5672.724613ms)
|
||||
✔ serve argv carries only fixture proof path and header; startup explicitly reports proof on or off (945.898363ms)
|
||||
✔ CLI applies proof on the main listener and rejects that header on its tester listener (1278.393646ms)
|
||||
✔ proxy proof admits named reads and sends, reads its fixture once, and never exposes the value (92.673688ms)
|
||||
✔ proxy proof refuses missing wrong short or repeated proof before auth and before any read (28.6526ms)
|
||||
✔ proxy proof header is refused on other Hosts and on the tester listener (26.562191ms)
|
||||
✔ proof options reject unsafe or colliding header names without disclosing file contents (6.908814ms)
|
||||
✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (3.570818ms)
|
||||
✔ credential state from the date alone, as the bus states it (0.347595ms)
|
||||
✔ queue: rows, one row, ids and methods, notes; no path or id from a row (2617.257384ms)
|
||||
✔ queue: a refused read fails closed with the store's text, redacted (1351.235521ms)
|
||||
✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (2758.552888ms)
|
||||
✔ business: names, vars on the allowlist, authority, credential metadata only (42.302368ms)
|
||||
✔ business: missing or invalid file refuses with the module's text, redacted (44.173504ms)
|
||||
✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (78.521237ms)
|
||||
✔ settings: release, board, loopback address, notifier binding name only (855.233449ms)
|
||||
✔ task fields: the committed table with no reads configured, GET only; a bad table refuses with no path (301.327869ms)
|
||||
✔ bus-status: a refused boot shows reason, file and last good load; a live host or an ok boot clears it (26.372421ms)
|
||||
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (4998.546596ms)
|
||||
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (24159.990938ms)
|
||||
organization: {"event":"reload-needs-restart","business":"acme"}
|
||||
organization: {"event":"reload-needs-restart","business":"acme"}
|
||||
✔ row 65 Settings: the routing table lists every instance, saves a harness, model, thinking and fallback to the file, and keeps the draft on each refusal (8132.127507ms)
|
||||
organization: {"event":"reload-needs-restart","business":"acme"}
|
||||
✔ row 65 Console: the signed-in human saves a route through a real broker; each refusal says nothing changed and leaves the file and the log (553.361055ms)
|
||||
✔ row 65 Console: the routes write needs send on, a writer, the public name, the user, the exact Origin and same-origin Fetch Metadata (16.864535ms)
|
||||
✔ row 65 Console: the routes write takes JSON within 16 KiB with a revision and routes only; an actor is refused and routes are never logged (22.570214ms)
|
||||
✔ row 65 Console: a bus refusal maps to its status and says nothing changed; a lost or odd answer says the change may have been made (55.26176ms)
|
||||
✔ row 65 Console: the routes read takes no arguments, needs a bus and refuses an odd answer (12.571836ms)
|
||||
✔ 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 (7748.726069ms)
|
||||
✔ a sent receipt lists its replies as the page refreshes, oldest first and capped, and marks none delivered or read (14185.480824ms)
|
||||
✔ startServer takes send only as a function and only with the preview mode (10.695212ms)
|
||||
✔ the CLI refuses --send without the preview flags, and without a configured bus (1879.80579ms)
|
||||
✔ the CLI with --send and a configured bus sends through the human transport and logs one line (971.515355ms)
|
||||
✔ without --send the route is read-only like every other write, and config says no send (39.763344ms)
|
||||
✔ with --send every other write is still 403 read-only, and nothing reaches the board or bus (35.328273ms)
|
||||
✔ config says send only on the public name with --send on (4.531841ms)
|
||||
✔ a send without the signed-in user is 401 and is neither sent nor logged, loopback included (12.334012ms)
|
||||
✔ each request check refuses before anything is sent, and logs the refusal without the body (53.220094ms)
|
||||
✔ a send passes exactly to, body and in_reply_to, answers the id, and logs it (4.98961ms)
|
||||
✔ a crafted role name cannot forge a log line (4.285004ms)
|
||||
✔ a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried (23.523423ms)
|
||||
✔ through the human transport the request is exactly the business, message.send and the keys (323.904475ms)
|
||||
✔ against a real broker: the message is written once as a human send, and its refusals map (297.534568ms)
|
||||
✔ loopback host and board origin fail closed (10.372634ms)
|
||||
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (227.895539ms)
|
||||
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (151.79035ms)
|
||||
✔ unreachable board reports URL; CLI rejects unsupported options (2346.124164ms)
|
||||
✔ tester service render carries the tester port alongside existing flags (927.685029ms)
|
||||
✔ tester service refuses forwarded 7332 even with another main port (684.618276ms)
|
||||
✔ tester service invalid ports refuse render and install with exit 4 and leave the unit alone (4181.817131ms)
|
||||
✔ tester service counts the digits as written before it strips leading zeros (319.721177ms)
|
||||
✔ tester service install and status show the tester port without changing send reporting (2075.300401ms)
|
||||
✔ render prints the unit without the preview flags (168.909905ms)
|
||||
✔ render carries every serve flag, the preview set included (180.372548ms)
|
||||
✔ a partial preview set is refused (423.124536ms)
|
||||
✔ a value the unit cannot carry is refused, for every flag (8912.939939ms)
|
||||
✔ the rendered unit names none of the human proof agent markers and sets no agent variable (430.705298ms)
|
||||
✔ install into --dir with --no-reload writes the file whole, then reports unchanged (856.949864ms)
|
||||
✔ install without --no-reload runs daemon-reload and nothing that starts the unit (136.133629ms)
|
||||
✔ uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not (1608.019156ms)
|
||||
✔ status prints the unit state and GET /healthz on the installed port (664.89403ms)
|
||||
✔ usage: an unknown verb or flag, and start or enable, exit 4 (1125.418813ms)
|
||||
✔ without --send, render is the template with its placeholders filled, byte for byte (453.815489ms)
|
||||
✔ render --send with the preview set ends ExecStart with --send and changes nothing else (615.369315ms)
|
||||
✔ --send without the whole preview set is refused and writes nothing (1116.0282ms)
|
||||
✔ install --send into --dir with --no-reload writes the sending unit, and status says whether it sends (481.528636ms)
|
||||
✔ uninstall and status take no --send (149.39612ms)
|
||||
✔ install refuses while the unit is running, in any state but inactive or failed, and changes nothing (658.273346ms)
|
||||
✔ when the state read fails, install and uninstall refuse with one line and change nothing (215.709624ms)
|
||||
✔ install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree (331.559972ms)
|
||||
✔ install refuses unless the bus unit runs from the same copy, naming both paths, and changes nothing (680.106866ms)
|
||||
✔ render --copy runs the copy and reads the queue in the checkout; without --copy the unit is unchanged (115.093975ms)
|
||||
✔ the uid header takes only an Authentik uid; the username header still takes a username (1862.395215ms)
|
||||
✔ a preview value cannot start with - or . (208.286301ms)
|
||||
✔ status names the copy and whether refactor has moved past it, and says when the unit runs from no copy (584.456927ms)
|
||||
✔ proxy proof render carries both flags but never opens the fixture file (46.254649ms)
|
||||
✔ proxy proof render refuses incomplete non-preview missing unsafe-mode and repo/copy paths (401.932358ms)
|
||||
✔ proxy proof status retains send and tester reporting with both proof flags (147.108399ms)
|
||||
✔ row196 socket flag renders only for supporting targets and refuses mixed transports (96.901062ms)
|
||||
✔ Settings form shows every scope and held role; gated save, exact payload, retained draft and no automatic retry (4689.944901ms)
|
||||
✔ Q169 Assistant section: gated, changed fields only, name and stop posts, retained draft and no automatic retry (4298.630309ms)
|
||||
✔ Row 167 a refused names read gives the refused usage page; another names failure keeps the Assistant section (2381.347611ms)
|
||||
✔ Row 167 with a limits draft open, an assistant save gives the limits submit its revision and the typed limit stays (3422.5705ms)
|
||||
✔ Row 167 with an assistant draft open, a launch save gives the assistant submit its revision, and reload reads the current one (3167.495309ms)
|
||||
2026-10-23T05:34:12.156Z settings user="jason" changed
|
||||
✔ REQ-WEB-1 signed-in same-origin Settings write reaches exactly one human operation (50.41002ms)
|
||||
✔ REQ-WEB-1 Settings writes require enabled send, named Host, identity, exact Origin and same-origin Fetch Metadata (40.80252ms)
|
||||
✔ REQ-WEB-1 Settings validates JSON, size, revision and strict writable keys before broker call (38.254838ms)
|
||||
✔ Q169 a Console change of the assistant reaches the human operation unchanged, and the log names the signed-in user (15.531946ms)
|
||||
✔ tokens.css and icons.svg are byte copies of docs/design (3.198341ms)
|
||||
✔ JetBrains Mono files match the digests in their sources file, with the OFL text (15.351971ms)
|
||||
✔ shipped strings are unchanged, and the web keeps its write boundary (26.807319ms)
|
||||
ROW191_LAYOUT {"width":1440,"height":1000,"controls":212.5,"tree":914,"main":914}
|
||||
✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (11440.609596ms)
|
||||
✔ five states on Board, Inbox, Tasks, Agents and Trail (6836.575873ms)
|
||||
✔ a 401 sign-in required on each fetch path signs the page out with a sign-in link and stops the refresh; another 401 stays an error (10800.915863ms)
|
||||
✔ the Sign out form shows only when /api/config says sso, and posts to /auth/logout (4684.860781ms)
|
||||
✔ positive: an RS256 provider signs in and sends one message, and the session reads (252.249989ms)
|
||||
✔ positive: an ES256 provider signs in and sends (66.12706ms)
|
||||
✔ positive: two providers each sign in their own subjects, and one provider's subject is unknown to the other (277.673504ms)
|
||||
✔ 1: a forged x-authentik header without a cookie is 401 on the API and a sign-in redirect on a page (96.809454ms)
|
||||
✔ 2: an HS256 ID token keyed with the client secret and a real RSA kid is refused (127.831239ms)
|
||||
✔ 3: a wrong aud, and an aud list holding another client with no azp, are refused (308.312945ms)
|
||||
✔ 4: a token that expired 10 minutes ago is refused (126.926096ms)
|
||||
✔ 5: another issuer, and the issuer without its trailing slash, are refused (187.553334ms)
|
||||
✔ 6: a nonce mismatch and a missing nonce are refused (176.286164ms)
|
||||
✔ 7: state mismatch, no login cookie, a replay, an expired record and two login cookies are refused (117.997966ms)
|
||||
✔ 8: the token request carries the PKCE verifier, and the OP refuses a wrong one (259.026782ms)
|
||||
✔ 9: alg none, a changed payload under a real kid, and an unknown kid even after the refetch are refused (324.624454ms)
|
||||
✔ 10: a valid token for a subject not configured is 403 with one subject-unknown line (292.839143ms)
|
||||
✔ 11: start refusals: HS or none algs, reserved names, HS-only discovery, an empty JWKS, no S256, an issuer mismatch, a short RSA key (818.587961ms)
|
||||
✔ 11: slot files: a symlink, mode 0644, mode 0640 and a file under the repository are refused, and no message holds the value (4.916667ms)
|
||||
✔ 11: the CLI refuses --sso with the old flags and without console.sso, and starts with a good one (3619.965965ms)
|
||||
✔ 12: under --sso the tester listener refuses each kind of sign-in, has no /auth, and startServer refuses tester with sso (123.335369ms)
|
||||
✔ 13: idle and absolute expiry, logout then reuse, a cross-origin logout, and background reads that don't keep a session (184.084176ms)
|
||||
✔ 13: the CSP lets the Sign out form reach the provider's end-session origin, and only under --sso (123.933217ms)
|
||||
✔ 14: open redirects land on /, and a path on the Console lands there (694.674531ms)
|
||||
✔ 15: a callback for A carrying B's code and iss, and a provider= query, is refused (231.062943ms)
|
||||
✔ 16: a provider with RS256 and ES256 takes either, and refuses PS256 with a usable PS256 key in the JWKS (401.501434ms)
|
||||
✔ 17: 33 started logins leave 32 records, and the first one's callback finds none (180.860007ms)
|
||||
✔ 18: after a key rotation a sign-in works through one refetch, and the removed kid is refused (237.107167ms)
|
||||
✔ 20 and 21: the store holds no raw session id, and the cookie strings are exact (339.228051ms)
|
||||
✔ 22: a loopback Host is 403 on a page, a read and a write; /healthz on loopback is 200; a redirect_uri never comes from the Host (185.956183ms)
|
||||
✔ 23: a provider down at start leaves the Console up and 503, and the retry makes sign-in work (793.068219ms)
|
||||
✔ 19: no captured log line holds a token, code, state, nonce, verifier, client secret or session id (12.154889ms)
|
||||
✔ tester Console banner exposes only instance route and name editors and saves a route as tester (4052.176134ms)
|
||||
✔ row 177: the tester Console chat composer is enabled and a send shows in its own thread (2058.798981ms)
|
||||
✔ tester-port validates range tunnel main port and requires configured bus before listening (3636.182442ms)
|
||||
✔ CLI tester listener binds a private Unix socket and spawns TESTER_CLI for every Console bus read (11007.753465ms)
|
||||
✔ main listener refuses x-mosaic-tester before any board or bus read for every Host (90.164888ms)
|
||||
✔ tester listener rejects preview binding and requires its fixed logical Host and no proxy auth header (391.197032ms)
|
||||
✔ tester listener writes only instances routes and names with tester receipts and actor (478.315366ms)
|
||||
✔ tester listener refuses every human-only write with no bus call (437.60685ms)
|
||||
✔ tester-off and tester-required reads return HTTP 403 (330.027803ms)
|
||||
✔ tester credentials routes and committed project reads keep secrets and paths bounded (885.561947ms)
|
||||
✔ row 177 W1: the tester chat makes, sends to, opens and reads its own thread as tester (136.597127ms)
|
||||
✔ row 177 W2: tester chat writes refuse a missing or foreign Origin, cross-site, text/plain and the proxy header with no bus call (59.629769ms)
|
||||
✔ row 177 W3: the human thread through the tester chat is thread-not-owned, and bus.tester off answers tester-off (273.894801ms)
|
||||
✔ row 177 W4: the main listener refuses x-mosaic-tester on chat reads and writes before the chat (5.513509ms)
|
||||
✔ private tester HTTP serves on 0600 socket in 0700 parent and fixed origin (150.811311ms)
|
||||
✔ private tester refuses symlink, unsafe parent and long path without repairs (2.151814ms)
|
||||
✔ tester cleanup must leave a replacement inode untouched (3.172069ms)
|
||||
✔ row278 browser lists held claims and release preserves role rows and totals (3219.347343ms)
|
||||
✔ row278 browser claims 403 stays local and role section remains byte-equal (2905.608034ms)
|
||||
✔ row278 browser renders only four escaped claim fields and never instance or other extras (1443.497625ms)
|
||||
✔ row278 browser sign-out removes claims and prevents reuse of cached answers (1144.811802ms)
|
||||
✔ row278 tester browser keeps role rows without requesting the owner claims section (955.86179ms)
|
||||
✔ row278 browser discards a late claims answer after sign-out (1083.591626ms)
|
||||
✔ row278 HTTP read returns held claims without send and refuses arguments before the bus (117.459434ms)
|
||||
✔ row278 HTTP read preserves bus refusal and rejects a malformed result (87.806387ms)
|
||||
✔ row278 tester socket refuses thread claims explicitly with zero bus calls (36.301855ms)
|
||||
✔ row278 Console adapter calls argument-free thread.claims through the normal transport (631.785506ms)
|
||||
✔ REQ-LIMIT-4 Settings shows all token kinds and unknown turns beside every total, as a read-only separate section (3398.492293ms)
|
||||
✔ REQ-WEB-1 usage read keeps unknown counts and each token kind; no query or write is accepted (82.33328ms)
|
||||
✔ REQ-WEB-1 malformed usage is refused instead of shown as zero (51.183905ms)
|
||||
✔ Queue, Business and Settings in their five states, with no move button (6719.50364ms)
|
||||
✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (2114.005252ms)
|
||||
✔ the server log names no config path when the system config refuses (426.110951ms)
|
||||
ℹ tests 365
|
||||
ℹ suites 0
|
||||
ℹ pass 364
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 58189.687162
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at tests/granted-user.test.mjs:250:1
|
||||
✖ R6 G5 the bus refuses every owner verb over the user transport on its own (485.568404ms)
|
||||
AssertionError [ERR_ASSERTION]: one event per refused verb
|
||||
|
||||
0 !== 22
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:269:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 0,
|
||||
expected: 22,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
ℹ tests 909
|
||||
ℹ suites 0
|
||||
ℹ pass 889
|
||||
ℹ fail 20
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 541508.057689
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at tests/access-boot.test.mjs:65:1
|
||||
✖ R6 B1 a boot with access.json binds a granted user, whose home is a configured peer (157.96901ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
+ [
|
||||
+ 'business acme sets tracker.baseUrl, but no project file sets tracker.project; the broker gets no task verbs'
|
||||
+ ]
|
||||
- []
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/access-boot.test.mjs:72:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: [ 'business acme sets tracker.baseUrl, but no project file sets tracker.project; the broker gets no task verbs' ],
|
||||
expected: [],
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/access-boot.test.mjs:75:1
|
||||
✖ R6 B2 a boot without access.json has no access and binds no user (122.067635ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
+ [
|
||||
+ 'business acme sets tracker.baseUrl, but no project file sets tracker.project; the broker gets no task verbs'
|
||||
+ ]
|
||||
- []
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/access-boot.test.mjs:79:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: [ 'business acme sets tracker.baseUrl, but no project file sets tracker.project; the broker gets no task verbs' ],
|
||||
expected: [],
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/access-boot.test.mjs:82:1
|
||||
✖ R6 B3 a boot with a file R1's loader refuses still boots, binds no user and says so once (147.861724ms)
|
||||
AssertionError [ERR_ASSERTION]: not JSON
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
+ 'business acme sets tracker.baseUrl, but no project file sets tracker.project; the broker gets no task verbs',
|
||||
'access file refused at boot (access-invalid), no granted user binds until it is fixed: /mnt/storage/scratch/tmp/g6exC2q/mosaic-access-boot-EQT8MW/config/access.json'
|
||||
]
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/access-boot.test.mjs:91:12)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: [ 'business acme sets tracker.baseUrl, but no project file sets tracker.project; the broker gets no task verbs', 'access file refused at boot (access-invalid), no granted user binds until it is fixed: /mnt/storage/scratch/tmp/g6exC2q/mosaic-access-boot-EQT8MW/config/access.json' ],
|
||||
expected: [ 'access file refused at boot (access-invalid), no granted user binds until it is fixed: /mnt/storage/scratch/tmp/g6exC2q/mosaic-access-boot-EQT8MW/config/access.json' ],
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2052:1
|
||||
✖ R1 rehearse passes on the same schema; the scratch root holds its files and the live files stay byte-identical (257.50956ms)
|
||||
AssertionError [ERR_ASSERTION]: PASS copy: d4d019e3594f3aecd8e61d21da71be7e8900d2aa, schema 3g
|
||||
PASS port: 127.0.0.1:46325 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-jrYCKA/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-jrYCKA/data/rehearsals/20261023T060506Z-d4d019e3594f/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-jrYCKA/data/rehearsals/20261023T060506Z-d4d019e3594f/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-jrYCKA/data/rehearsals/20261023T060506Z-d4d019e3594f
|
||||
|
||||
|
||||
1 !== 0
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2055:9)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 1,
|
||||
expected: 0,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2094:1
|
||||
✖ R2 rehearse runs the migration checks when the schema differs; the live store stays at 3g (257.801433ms)
|
||||
AssertionError [ERR_ASSERTION]: PASS copy: e1435ba233c48f735a0827ffee14159e63152fd6, schema 3h
|
||||
PASS port: 127.0.0.1:42869 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-dlVVff/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-dlVVff/data/rehearsals/20261023T060507Z-e1435ba233c4/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-dlVVff/data/rehearsals/20261023T060507Z-e1435ba233c4/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/e1435ba233c48f735a0827ffee14159e63152fd6/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/e1435ba233c48f735a0827ffee14159e63152fd6/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-dlVVff/data/rehearsals/20261023T060507Z-e1435ba233c4
|
||||
|
||||
|
||||
1 !== 0
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2097:9)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 1,
|
||||
expected: 0,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2106:1
|
||||
✖ R3 a failing migration gives REHEARSAL_FAIL migrate and starts nothing (256.867638ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
'PASS copy',
|
||||
'PASS port',
|
||||
'PASS backup',
|
||||
'PASS scratch-root',
|
||||
+ 'FAIL scratch-config',
|
||||
- 'PASS data-root-live',
|
||||
- 'PASS sessions-live',
|
||||
- 'PASS data-root-scratch',
|
||||
- 'PASS notify-off',
|
||||
- 'PASS tracker-configured',
|
||||
- 'PASS credential-ref-live',
|
||||
- 'PASS new-refuses-old-store',
|
||||
- 'FAIL migrate',
|
||||
'PASS backup-unchanged',
|
||||
'PASS live-config-unchanged'
|
||||
]
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2110:9)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: [ 'PASS copy', 'PASS port', 'PASS backup', 'PASS scratch-root', 'FAIL scratch-config', 'PASS backup-unchanged', 'PASS live-config-unchanged' ],
|
||||
expected: [ 'PASS copy', 'PASS port', 'PASS backup', 'PASS scratch-root', 'PASS data-root-live', 'PASS sessions-live', 'PASS data-root-scratch', 'PASS notify-off', 'PASS tracker-configured', 'PASS credential-ref-live', 'PASS new-refuses-old-store', 'FAIL migrate', 'PASS backup-unchanged', 'PASS live-config-unchanged' ],
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2117:1
|
||||
✖ R4 a host that does not start, does not answer status, reports another pid, launches anyway, makes a launches directory or keeps its lock each fails by name (255.606533ms)
|
||||
AssertionError [ERR_ASSERTION]: host-fails-d4d019e3594f3aecd8e61d21da71be7e8900d2aaPASS copy: d4d019e3594f3aecd8e61d21da71be7e8900d2aa, schema 3g
|
||||
PASS port: 127.0.0.1:34099 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-ytC3K1/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-ytC3K1/data/rehearsals/20261023T060507Z-d4d019e3594f/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-ytC3K1/data/rehearsals/20261023T060507Z-d4d019e3594f/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-ytC3K1/data/rehearsals/20261023T060507Z-d4d019e3594f
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2122:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2130:1
|
||||
✖ R5 a host that ignores SIGTERM gets SIGKILL and FAIL host-stopped (255.514631ms)
|
||||
AssertionError [ERR_ASSERTION]: PASS copy: d4d019e3594f3aecd8e61d21da71be7e8900d2aa, schema 3g
|
||||
PASS port: 127.0.0.1:41779 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-Cw4EoG/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-Cw4EoG/data/rehearsals/20261023T060507Z-d4d019e3594f/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-Cw4EoG/data/rehearsals/20261023T060507Z-d4d019e3594f/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-Cw4EoG/data/rehearsals/20261023T060507Z-d4d019e3594f
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2134:9)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2137:1
|
||||
✖ R6 a Console that does not answer /healthz gives REHEARSAL_FAIL healthz (264.915214ms)
|
||||
AssertionError [ERR_ASSERTION]: PASS copy: d4d019e3594f3aecd8e61d21da71be7e8900d2aa, schema 3g
|
||||
PASS port: 127.0.0.1:34591 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-fxnXNu/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-fxnXNu/data/rehearsals/20261023T060508Z-d4d019e3594f/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-fxnXNu/data/rehearsals/20261023T060508Z-d4d019e3594f/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-fxnXNu/data/rehearsals/20261023T060508Z-d4d019e3594f
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2141:9)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2145:1
|
||||
✖ R7 a host on another copy fails copy-match (259.256456ms)
|
||||
AssertionError [ERR_ASSERTION]: PASS copy: 62e34e3e003a065a18c7601d8b57af548c34043f, schema 3g
|
||||
PASS port: 127.0.0.1:45705 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-nwNoDa/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-nwNoDa/data/rehearsals/20261023T060508Z-62e34e3e003a/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-nwNoDa/data/rehearsals/20261023T060508Z-62e34e3e003a/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/62e34e3e003a065a18c7601d8b57af548c34043f/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/62e34e3e003a065a18c7601d8b57af548c34043f/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-nwNoDa/data/rehearsals/20261023T060508Z-62e34e3e003a
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2149:9)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2168:1
|
||||
✖ R9 mos-0963: a role reference left outside placeholders/, or left on an env variable, fails credential-ref-live before the host starts (339.787292ms)
|
||||
AssertionError [ERR_ASSERTION]: PASS copy: d4d019e3594f3aecd8e61d21da71be7e8900d2aa, schema 3g
|
||||
PASS port: 127.0.0.1:37903 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-JO0FqA/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-JO0FqA/data/rehearsals/20261023T060509Z-d4d019e3594f/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-JO0FqA/data/rehearsals/20261023T060509Z-d4d019e3594f/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-JO0FqA/data/rehearsals/20261023T060509Z-d4d019e3594f
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2177:9)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2191:1
|
||||
✖ R10 mos-0962: a scratch business that still sets a tracker fails tracker-configured (321.198473ms)
|
||||
AssertionError [ERR_ASSERTION]: PASS copy: d4d019e3594f3aecd8e61d21da71be7e8900d2aa, schema 3g
|
||||
PASS port: 127.0.0.1:43203 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-QWVCyZ/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-QWVCyZ/data/rehearsals/20261023T060509Z-d4d019e3594f/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-QWVCyZ/data/rehearsals/20261023T060509Z-d4d019e3594f/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-QWVCyZ/data/rehearsals/20261023T060509Z-d4d019e3594f
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2199:9)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2203:1
|
||||
✖ R11 mos-0961: a scratch config on the live data root or on any root but the scratch one, or a sessions.json naming a running pid, refuses by name (361.597504ms)
|
||||
AssertionError [ERR_ASSERTION]: PASS copy: d4d019e3594f3aecd8e61d21da71be7e8900d2aa, schema 3g
|
||||
PASS port: 127.0.0.1:46203 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-QghCKN/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-QghCKN/data/rehearsals/20261023T060509Z-d4d019e3594f/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-QghCKN/data/rehearsals/20261023T060509Z-d4d019e3594f/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-QghCKN/data/rehearsals/20261023T060509Z-d4d019e3594f
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2210:9)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2231:1
|
||||
✖ R12 mos-0962: a notifier config with a binding fails notify-off (315.255454ms)
|
||||
AssertionError [ERR_ASSERTION]: PASS copy: d4d019e3594f3aecd8e61d21da71be7e8900d2aa, schema 3g
|
||||
PASS port: 127.0.0.1:37283 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-bnXlJ4/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-bnXlJ4/data/rehearsals/20261023T060510Z-d4d019e3594f/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-bnXlJ4/data/rehearsals/20261023T060510Z-d4d019e3594f/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-bnXlJ4/data/rehearsals/20261023T060510Z-d4d019e3594f
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2236:9)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2259:1
|
||||
✖ R14 a host that writes the live config, the live business file or the backup fails the end check by name (266.891617ms)
|
||||
AssertionError [ERR_ASSERTION]: /mnt/storage/scratch/tmp/g6exC2q/sr-B9GExi/cfg/config.jsonPASS copy: d4d019e3594f3aecd8e61d21da71be7e8900d2aa, schema 3g
|
||||
PASS port: 127.0.0.1:36371 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-B9GExi/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-B9GExi/data/rehearsals/20261023T060511Z-d4d019e3594f/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-B9GExi/data/rehearsals/20261023T060511Z-d4d019e3594f/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-B9GExi/data/rehearsals/20261023T060511Z-d4d019e3594f
|
||||
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
+ 'FAIL scratch-config'
|
||||
- 'FAIL live-config-unchanged'
|
||||
]
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2264:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: [ 'FAIL scratch-config' ],
|
||||
expected: [ 'FAIL live-config-unchanged' ],
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2270:1
|
||||
✖ R15 a rehearsal stopped by TERM, INT or HUP fails signal and still stops the host and Console and runs the end checks (255.998581ms)
|
||||
AssertionError [ERR_ASSERTION]: SIGTERMPASS copy: d4d019e3594f3aecd8e61d21da71be7e8900d2aa, schema 3g
|
||||
PASS port: 127.0.0.1:40583 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-fcuE2e/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-fcuE2e/data/rehearsals/20261023T060511Z-d4d019e3594f/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-fcuE2e/data/rehearsals/20261023T060511Z-d4d019e3594f/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-fcuE2e/data/rehearsals/20261023T060511Z-d4d019e3594f
|
||||
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
'PASS copy',
|
||||
'PASS port',
|
||||
'PASS backup',
|
||||
'PASS scratch-root',
|
||||
+ 'FAIL scratch-config',
|
||||
- 'PASS data-root-live',
|
||||
- 'PASS sessions-live',
|
||||
- 'PASS data-root-scratch',
|
||||
- 'PASS notify-off',
|
||||
- 'PASS tracker-configured',
|
||||
- 'PASS credential-ref-live',
|
||||
- 'PASS migration',
|
||||
- 'FAIL signal',
|
||||
- 'PASS console-stopped',
|
||||
- 'PASS host-stopped',
|
||||
- 'PASS host-released-store',
|
||||
'PASS backup-unchanged',
|
||||
'PASS live-config-unchanged'
|
||||
]
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2277:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: [ 'PASS copy', 'PASS port', 'PASS backup', 'PASS scratch-root', 'FAIL scratch-config', 'PASS backup-unchanged', 'PASS live-config-unchanged' ],
|
||||
expected: [ 'PASS copy', 'PASS port', 'PASS backup', 'PASS scratch-root', 'PASS data-root-live', 'PASS sessions-live', 'PASS data-root-scratch', 'PASS notify-off', 'PASS tracker-configured', 'PASS credential-ref-live', 'PASS migration', 'FAIL signal', 'PASS console-stopped', 'PASS host-stopped', 'PASS host-released-store', 'PASS backup-unchanged', 'PASS live-config-unchanged' ],
|
||||
operator: 'deepStrictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2293:1
|
||||
✖ row 233: --backup last takes the newest backup the log records; a later line with backup null does not hide it, and the store line names it (296.220218ms)
|
||||
AssertionError [ERR_ASSERTION]: PASS backup-last: the log's newest backup is 20261020T000000Z-d4d019e3594f
|
||||
PASS copy: d4d019e3594f3aecd8e61d21da71be7e8900d2aa, schema 3g
|
||||
PASS port: 127.0.0.1:45521 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-aiaiqc/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-aiaiqc/data/rehearsals/20261023T060511Z-d4d019e3594f/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-aiaiqc/data/rehearsals/20261023T060511Z-d4d019e3594f/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-aiaiqc/data/rehearsals/20261023T060511Z-d4d019e3594f
|
||||
|
||||
|
||||
1 !== 0
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2299:9)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 1,
|
||||
expected: 0,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2589:43
|
||||
✖ socket copy (260.302823ms)
|
||||
AssertionError [ERR_ASSERTION]: PASS copy: d4d019e3594f3aecd8e61d21da71be7e8900d2aa, schema 3g
|
||||
PASS port: 127.0.0.1:44555 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-srFiMG/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-srFiMG/data/rehearsals/20261023T060549Z-d4d019e3594f/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-srFiMG/data/rehearsals/20261023T060549Z-d4d019e3594f/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-srFiMG/data/rehearsals/20261023T060549Z-d4d019e3594f
|
||||
|
||||
|
||||
1 !== 0
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2592:86)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2589:35)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 1,
|
||||
expected: 0,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at tests/stack-release.test.mjs:2589:43
|
||||
✖ legacy copy (254.164814ms)
|
||||
AssertionError [ERR_ASSERTION]: PASS copy: d4d019e3594f3aecd8e61d21da71be7e8900d2aa, schema 3g
|
||||
PASS port: 127.0.0.1:34283 is free
|
||||
PASS backup: /mnt/storage/scratch/tmp/g6exC2q/sr-32Tddm/data/backups/stack/20261020T000000Z-d4d019e3594f matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/tmp/g6exC2q/sr-32Tddm/data/rehearsals/20261023T060550Z-d4d019e3594f/data from the backup, config and business file under /mnt/storage/scratch/tmp/g6exC2q/sr-32Tddm/data/rehearsals/20261023T060550Z-d4d019e3594f/config, business.diff beside them
|
||||
FAIL scratch-config: Cannot find module '/mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/access.mjs' imported from /mnt/storage/scratch/tmp/g6exC2q/stack-release-vxoJVb/rcopies/d4d019e3594f3aecd8e61d21da71be7e8900d2aa/packages/cli/src/config.mjs
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/tmp/g6exC2q/sr-32Tddm/data/rehearsals/20261023T060550Z-d4d019e3594f
|
||||
|
||||
|
||||
1 !== 0
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2592:86)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/stack-release.test.mjs:2589:35)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 1,
|
||||
expected: 0,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 294 gate run 3, sage-3199: npm ci (root and packages/webui), suites whole, every
|
||||
# scripts/test-*.sh, then the mutants. Stops at the first red. FROM=<step> resumes at a step
|
||||
# (the mos-1411 single cli rerun uses FROM=cli TAG=-rerun).
|
||||
set -u -o pipefail
|
||||
WT=/mnt/storage/scratch/launchpad-r294/wt
|
||||
OUT=/mnt/storage/scratch/launchpad-r294/run3
|
||||
FROM=${FROM:-npm-root}; TAG=${TAG:-}
|
||||
export TMPDIR=$(mktemp -d /mnt/storage/scratch/tmp/gXXXXXX)
|
||||
echo "start $(date -u +%H:%M:%SZ) FROM=$FROM TAG=$TAG TMPDIR $TMPDIR head $(git -C $WT rev-parse HEAD)" >> $OUT/summary.txt
|
||||
[ -z "$(git -C $WT status --porcelain)" ] || { echo "STOP worktree not clean" | tee -a $OUT/summary.txt; exit 9; }
|
||||
on=0
|
||||
step() { # name limit dir cmd...
|
||||
local name=$1 limit=$2 dir=$3; shift 3
|
||||
[ $on = 1 ] || [ $name = $FROM ] || return 0; on=1
|
||||
local t0=$(date +%s%N)
|
||||
( cd $dir && timeout -k 10 $limit "$@" ) > $OUT/$name$TAG.log 2>&1
|
||||
local rc=$?
|
||||
local ms=$(( ($(date +%s%N) - t0) / 1000000 )); local dt=$((ms/1000)).$(printf %03d $((ms%1000)))
|
||||
echo "$name$TAG rc=$rc seconds=$dt limit=$limit $(date -u +%H:%M:%SZ)" | tee -a $OUT/summary.txt
|
||||
[ $rc -eq 0 ] || { echo "STOP at $name$TAG" | tee -a $OUT/summary.txt; exit $rc; }
|
||||
}
|
||||
step npm-root 900 $WT npm ci --ignore-scripts
|
||||
step npm-webui 900 $WT/packages/webui npm ci --ignore-scripts
|
||||
step webui 600 $WT/packages/webui node --test tests/
|
||||
step cli 2100 $WT/packages/cli node --test tests/*.test.mjs
|
||||
step bus 900 $WT/packages/bus node --test tests/*.test.mjs
|
||||
declare -A L=([auth]=126 [conductor]=145 [config]=130 [discord]=147 [extension-package]=130 [foundation]=269 [land-work]=171 [queue]=226 [release]=157 [task]=231)
|
||||
for s in auth conductor config discord extension-package foundation land-work queue release task; do
|
||||
step test-$s ${L[$s]} $WT bash scripts/test-$s.sh
|
||||
done
|
||||
[ -z "$(git -C $WT status --porcelain)" ] || { echo "STOP worktree dirty after the scripts" | tee -a $OUT/summary.txt; git -C $WT status --porcelain >> $OUT/summary.txt; exit 9; }
|
||||
echo "SUITES GREEN $(date -u +%H:%M:%SZ)" | tee -a $OUT/summary.txt
|
||||
python3 -B $OUT/mutants.py
|
||||
rc=$?; echo "MUTANTS rc=$rc $(date -u +%H:%M:%SZ)" | tee -a $OUT/summary.txt; exit $rc
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
# Row 294 mutants M1-M11 (BUILD.md "Mutants"). Each mutant edits the worktree (every old text must
|
||||
# match exactly once), runs its killing file alone under timeout -k 10 300 with the TAP reporter,
|
||||
# then restores with git checkout and checks the tree is clean. Killed = the named killing test(s)
|
||||
# report "not ok". M1 also needs G5 "ok" in the same run (Duckworth C1).
|
||||
import json, os, re, subprocess, sys, time
|
||||
|
||||
WT = '/mnt/storage/scratch/launchpad-r294/wt'
|
||||
OUT = '/mnt/storage/scratch/launchpad-r294/run3'
|
||||
GU = ('packages/webui', 'tests/granted-user.test.mjs')
|
||||
AB = ('packages/cli', 'tests/access-boot.test.mjs')
|
||||
|
||||
CHAT_CALL = "verb === 'plugins' ? owner('plugins', {}) : call(verb, args)"
|
||||
MUTANTS = [
|
||||
('M1', [('packages/webui/src/serve.mjs', "if (!userRoute(path, req.method)) return json(res, 403, { error: 'owner-only'",
|
||||
"if (false) return json(res, 403, { error: 'owner-only'")], GU, ['R6 G4'], ['R6 G5']),
|
||||
('M2', [('packages/webui/src/sso.mjs', "if (s.user !== undefined && !users?.check(s)) {", "if (s.user !== undefined && false) {")], GU, ['R6 G3'], []),
|
||||
('M3', [('packages/webui/src/public/user.html', "but not your messages.", "but not your replies.")], GU, ['R6 G6'], []),
|
||||
('M4', [('packages/cli/src/access.mjs', "const peers = (system.federation?.peers ?? []).map((p) => p.id);", "const peers = [];")], AB,
|
||||
['R6 a configured peer id', 'R6 B1'], []),
|
||||
('M5', [('packages/webui/src/access.mjs', "return user && live(doc, user.id, b) ? user.id : null;", "return user ? user.id : null;")], GU, ['R6 G2'], []),
|
||||
('M6', [('packages/webui/src/user-bus.mjs', "env: { ...rest, MOSAIC_BUS_USER_NONCE", "env: { ...env, MOSAIC_BUS_USER_NONCE")], GU, ['R6 G8'], []),
|
||||
('M7', [('packages/cli/src/config.mjs', " if (access) config.access = access;\n", "")], AB, ['R6 B1'], []),
|
||||
('M8', [('packages/webui/src/chat.mjs', CHAT_CALL,
|
||||
"verb === 'plugins' ? owner('plugins', {}) : verb === 'thread.list' ? owner(verb, args) : call(verb, args)")], GU, ['R6 G9'], []),
|
||||
('M9', [('packages/cli/src/config.mjs', " warn(`access file refused at boot (${code}), no granted user binds until it is fixed: ${file}`);\n", "")],
|
||||
AB, ['R6 B3'], []),
|
||||
('M10', [('packages/webui/src/console-bus.mjs', "return session => consoleBus(transport(session.user), { send, owner });",
|
||||
"return session => consoleBus(transport(session.user), { send, owner: (v, a) => owner(v, { ...a, sub: session.sub }) });")], GU, ['R6 G9'], []),
|
||||
('M11', [('packages/webui/src/chat.mjs', "const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => " + CHAT_CALL + " });",
|
||||
"let last = {};\n const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => verb === 'plugins' ? owner('plugins', last) : call(verb, args) });"),
|
||||
('packages/webui/src/chat.mjs', " try { return await dispatch(verb, args); }", " try { last = args; return await dispatch(verb, args); }")], GU, ['R6 G9'], []),
|
||||
]
|
||||
|
||||
def git(*a):
|
||||
return subprocess.run(['git', '-C', WT, *a], capture_output=True, text=True, check=True).stdout
|
||||
|
||||
def restore(files):
|
||||
if files: git('checkout', '--', *sorted(files))
|
||||
if git('status', '--porcelain'): raise SystemExit('worktree not clean after restore')
|
||||
|
||||
def results(tap):
|
||||
# Top-level TAP test lines: "ok N - name" / "not ok N - name" at no indentation.
|
||||
return {m.group(2): m.group(1) == 'ok' for m in re.finditer(r'^(ok|not ok) \d+ - (.*)$', tap, re.M)}
|
||||
|
||||
def main():
|
||||
only = sys.argv[1:]
|
||||
if git('status', '--porcelain'): raise SystemExit('worktree not clean at start')
|
||||
env = dict(os.environ)
|
||||
summary, bad = [], 0
|
||||
for mid, edits, (pkg, test), killers, greens in MUTANTS:
|
||||
if only and mid not in only: continue
|
||||
files = set()
|
||||
try:
|
||||
for path, old, new in edits:
|
||||
p = os.path.join(WT, path); s = open(p).read()
|
||||
if s.count(old) != 1: raise SystemExit(f'{mid}: anchor count {s.count(old)} in {path}')
|
||||
open(p, 'w').write(s.replace(old, new)); files.add(path)
|
||||
diff = git('diff')
|
||||
t0 = time.time()
|
||||
r = subprocess.run(['timeout', '-k', '10', '300', 'node', '--test', '--test-reporter=tap', test],
|
||||
cwd=os.path.join(WT, pkg), capture_output=True, text=True, env=env)
|
||||
dt = round(time.time() - t0, 1)
|
||||
finally:
|
||||
restore(files)
|
||||
open(f'{OUT}/{mid}.diff', 'w').write(diff)
|
||||
open(f'{OUT}/{mid}.tap', 'w').write(r.stdout + '\n# stderr\n' + r.stderr)
|
||||
res = results(r.stdout)
|
||||
hit = lambda pre: [n for n, ok in res.items() if n.startswith(pre)]
|
||||
red = [n for n, ok in res.items() if not ok]
|
||||
killed = r.returncode not in (0, 124, 137) and all(any(not res[n] for n in hit(k)) for k in killers) \
|
||||
and all(hit(g) and all(res[n] for n in hit(g)) for g in greens)
|
||||
bad += not killed
|
||||
line = f'{mid} rc={r.returncode} seconds={dt} killed={killed} red={red}'
|
||||
print(line, flush=True)
|
||||
summary.append({'mutant': mid, 'rc': r.returncode, 'seconds': dt, 'killed': killed, 'red': red,
|
||||
'killers': killers, 'greens': greens})
|
||||
json.dump(summary, open(f'{OUT}/mutants.json', 'w'), indent=1)
|
||||
sys.exit(1 if bad else 0)
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,6 @@
|
||||
start 05:55:46Z FROM=npm-root TAG= TMPDIR /mnt/storage/scratch/tmp/g6exC2q head f265840892582e93dd9e0df794b041fa44a65cc9
|
||||
npm-root rc=0 seconds=2.205 limit=900 05:55:48Z
|
||||
npm-webui rc=0 seconds=2.210 limit=900 05:55:50Z
|
||||
webui rc=0 seconds=58.248 limit=600 05:56:48Z
|
||||
cli rc=1 seconds=541.546 limit=2100 06:05:50Z
|
||||
STOP at cli
|
||||
@@ -0,0 +1,398 @@
|
||||
✔ row 194 T16 Settings Agents: unavailable read-only, launch follows its request, confirms, reason and refusal (16720.521143ms)
|
||||
✔ row 256 T7 T8 the human page offers Delete on a creative instance, the tester page never does (2852.289705ms)
|
||||
✔ row 194 T16: each agent action makes one call with only its keys, logs the user and answers the request (44.263887ms)
|
||||
✔ row 194 T16: the boundary refuses before any bus call (24.767569ms)
|
||||
✔ row 194 T16: read-only when send is off or no writer is wired; no route has a --send exception (13.994313ms)
|
||||
✔ row 194 T16: a bus refusal names its reason and nothing changed; an unknown outcome or a bad shape is never retried (9.155566ms)
|
||||
✔ row 194 T16: the request read takes one id, adds the refusal reason and refuses an unexpected shape (14.564434ms)
|
||||
✔ row 194 T16: delete joins add and change on /api/instances with only revision, instance and op (7.253291ms)
|
||||
✔ row 194 T16 D4.1: the tester listener launches, terminates and reads requests; regenerate, veto and delete are refused with no call (11.162178ms)
|
||||
✔ row 194 N5: agents.js is served as JavaScript and index.html loads it once (30.106713ms)
|
||||
✔ Q169 the signed-in human names a seat and stops the assistant, one bus call each, logged with the user (40.050689ms)
|
||||
✔ Q169 the name and stop routes need send on, the public name, the user, the exact Origin and same-origin Fetch Metadata (19.272954ms)
|
||||
✔ Q169 the name and stop routes take JSON within 4 KiB and only their own keys, all text (11.282795ms)
|
||||
✔ Row 167 a null body on the name and stop routes is 400 invalid-request with no write, and the next request is answered (4.994216ms)
|
||||
✔ Q169 a bus refusal says nothing changed; a lost or odd answer says the change may have been made (12.379465ms)
|
||||
✔ Q169 the names read is a bus read like the others (2.865136ms)
|
||||
✔ 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 (5737.182545ms)
|
||||
✔ a Seen click while a Board read is in flight waits for that read, then posts (3195.611274ms)
|
||||
✔ 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 (10520.973395ms)
|
||||
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (4062.134308ms)
|
||||
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
|
||||
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (4765.269731ms)
|
||||
✔ the README says a refused field table names the file or the key at fault, never a path (1.548276ms)
|
||||
✔ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes (12822.589993ms)
|
||||
✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (274.981733ms)
|
||||
✔ decisions route serves the roles and recent lists from the reader view (951.179295ms)
|
||||
✔ credentials route serves the broker credential status from the reader view (373.299586ms)
|
||||
✔ replies route serves the replies to one message from the reader view (576.340048ms)
|
||||
✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (409.499347ms)
|
||||
✔ bus refusals map to statuses and never carry a path or a stack (96.876034ms)
|
||||
✔ humanCall speaks the human transport protocol without blocking the server (2873.709982ms)
|
||||
✔ busReader takes --business, else the live bus host, else refuses (657.2204ms)
|
||||
✔ humanCall kills a transport that runs past its timeout (545.733473ms)
|
||||
✔ serve refuses a --business value that is not a business id (1007.016245ms)
|
||||
✔ WebUI channel off returns 503 and named recovery for every chat route without a chat operation (64.000782ms)
|
||||
✔ WebUI only selection keeps web chat enabled while retaining the old read envelope (9.90156ms)
|
||||
✔ WebUI channel-disabled recovery text is fixed and never echoes transport text (4.892393ms)
|
||||
✔ chat browser: list/create named projects and threads, hostile labels inert and no implicit writes (2698.78817ms)
|
||||
✔ chat browser: thread drafts, receipts and cursor history stay isolated; refresh never writes (3465.806245ms)
|
||||
✔ chat browser: uncertain writes retain draft, ownership refusal clears it, read-only cannot send (2702.258947ms)
|
||||
✔ chat browser: a late read from A cannot paint B; sign-out clears private state and stops reads (3063.331627ms)
|
||||
✔ chat browser: page forward and deduplicate replies; pending and byte-limit submissions do not duplicate (2245.473621ms)
|
||||
✔ chat browser: scrolled history stays put, New messages and an own send return to the tail (2501.795942ms)
|
||||
✔ chat browser: automatic polling follows a reply without a POST or a lost draft (11909.934403ms)
|
||||
✔ chat browser: display stays bounded while every forward page remains reachable (2289.281059ms)
|
||||
✔ chat browser: a confirmed visible message replaces its pending receipt; malformed receipts stay unknown (1563.981695ms)
|
||||
✔ chat browser: a refusal elsewhere forgets private chat drafts (1556.414937ms)
|
||||
✔ chat browser: a late project creation receipt cannot navigate away from another thread (1250.044689ms)
|
||||
✔ chat reads map only fixed verbs, strict forward paging and pure GET (47.806243ms)
|
||||
✔ chat refuses malformed and repeated query/id values before transport (15.670005ms)
|
||||
✔ chat project.create: one verified write, fixed identity/class and private log (9.157717ms)
|
||||
✔ chat project.create: each existing Console write fence makes zero bus calls (23.018108ms)
|
||||
✔ chat project.create: refusal or lost/malformed receipt never retries or invents success (10.631623ms)
|
||||
✔ chat thread.create: one verified write, fixed identity/class and private log (3.12818ms)
|
||||
✔ chat thread.create: each existing Console write fence makes zero bus calls (18.351076ms)
|
||||
✔ chat thread.create: refusal or lost/malformed receipt never retries or invents success (16.198408ms)
|
||||
✔ chat thread.send: one verified write, fixed identity/class and private log (2.931924ms)
|
||||
✔ chat thread.send: each existing Console write fence makes zero bus calls (11.100991ms)
|
||||
✔ chat thread.send: refusal or lost/malformed receipt never retries or invents success (15.246593ms)
|
||||
✔ chat thread.open: one verified write, fixed identity/class and private log (2.362502ms)
|
||||
✔ chat thread.open: each existing Console write fence makes zero bus calls (36.439667ms)
|
||||
✔ chat thread.open: refusal or lost/malformed receipt never retries or invents success (60.012186ms)
|
||||
✔ chat createThread receipt requires an instance even when its id is valid (5.521269ms)
|
||||
✔ chat write receipts expose only the fields belonging to their operation (12.998722ms)
|
||||
✔ chat validates names, body, project and strict empty open payload (18.76858ms)
|
||||
✔ chat read failures redact ownership and refuse invalid transport arrays (81.075413ms)
|
||||
✔ REQ-USER-2 real broker: owned catalog, create/name/filter, isolated history and 403/404 without mutations (2093.207514ms)
|
||||
✔ chat repeats every POST boundary even when called behind an admitted request (9.410724ms)
|
||||
✔ configured Console process injects chat through its existing human transport (1026.519709ms)
|
||||
✔ chat assets, GET purity, method and unknown-route refusals are explicit (50.085764ms)
|
||||
✔ the chat seam names the transport it is given: the human one and the tester one both pass, anything else is a TypeError (1.552402ms)
|
||||
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (3428.989303ms)
|
||||
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (4257.61001ms)
|
||||
✔ conversation view: a newer session with no readable history keeps the marker (2730.88369ms)
|
||||
✔ conversation view: seats without history say so and offer no reply (2555.490105ms)
|
||||
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (5135.219765ms)
|
||||
✔ the editor: the list, hostile markdown that runs nothing, the draft, and each save receipt (5117.140341ms)
|
||||
✔ the Console's CSP stands and the editor runs under it; hostile markdown on the load path runs and loads nothing (3810.19765ms)
|
||||
✔ hostile content pasted into the editor runs and loads nothing, and a bad link target is refused in the link bar (3350.615142ms)
|
||||
✔ a pasted image span is kept only for a target the link gate accepts, and a save writes only those (2660.570821ms)
|
||||
✔ a switch of view reads nothing; a reload, and a trip away and back, each read the file exactly once (4912.983805ms)
|
||||
✔ an edit rewrites only the block touched; a save sends the rest byte for byte; the Source view carries the draft (3047.926419ms)
|
||||
✔ a file the editor cannot write back byte for byte opens as Source, says so, cannot save, and the Visual button works once it can (4172.092495ms)
|
||||
✔ a link title pasted with line breaks in it is written on one line, so no HTML block comes of it (1873.293983ms)
|
||||
✔ an image alt pasted with line breaks in it is written on one line, so no HTML block comes of it (1618.078827ms)
|
||||
✔ an image title pasted with line breaks in it is written on one line, so no HTML block comes of it (1494.351085ms)
|
||||
✔ a code block info string pasted with line breaks in it is written on one line, so no HTML block comes of it (1166.0551ms)
|
||||
✔ a link title holding a quote after a backslash then a tag is written with the backslash escaped, so the title stays closed (2428.660903ms)
|
||||
✔ an image title holding a quote after a backslash then a tag is written with the backslash escaped, so the title stays closed (1724.288739ms)
|
||||
✔ a link title holding a trailing backslash is written with the backslash escaped, so the title stays closed (1547.539704ms)
|
||||
✔ an image title holding a trailing backslash is written with the backslash escaped, so the title stays closed (1162.149461ms)
|
||||
✔ a code block info string with a backtick, pasted, is dropped on Save, so the fence stays closed (988.925646ms)
|
||||
✔ a code block info string with a backtick, as a tilde fence can have, is kept as source, not rewritten without it (842.809992ms)
|
||||
✔ the imported source is the tagged upstream source plus the recorded local changes, and nothing else (75.543324ms)
|
||||
✔ every package names its origin and license, and NOTICE lists each (1.110618ms)
|
||||
✔ the build reads only imported source and the editor's own files; the bundle is the recorded build (12.903261ms)
|
||||
✔ MOSAIC_REQUIRE_EDITOR_BUILD: a missing or wrong esbuild skips without it and fails with it; a wrong binary always fails (95.305213ms)
|
||||
✔ the toolchain check, run as a program, exits non-zero without esbuild when the build is required (1874.081911ms)
|
||||
▲ [WARNING] Ignoring this import because "third_party/editor/prosemirror-transform/src/mark.ts" was marked as having no side effects [ignored-bare-import]
|
||||
|
||||
third_party/editor/prosemirror-transform/src/index.ts:10:7:
|
||||
10 │ import "./mark"
|
||||
╵ ~~~~~~~~
|
||||
|
||||
"sideEffects" is false in the enclosing "package.json" file:
|
||||
|
||||
third_party/editor/prosemirror-transform/package.json:13:2:
|
||||
13 │ "sideEffects": false,
|
||||
╵ ~~~~~~~~~~~~~
|
||||
|
||||
✔ rebuilding from the source gives the same bytes (needs the pinned esbuild; MOSAIC_REQUIRE_EDITOR_BUILD=1 makes its absence a failure) (476.810244ms)
|
||||
✔ the editor routes serve the built files with their types and no cache (161.579863ms)
|
||||
✔ open and save with no edit writes the same bytes: the goals review and a doc with every element (97.514253ms)
|
||||
✔ open and save with no edit writes the same bytes: every repository doc under docs/ and spec/ (7764.590067ms)
|
||||
✔ the editor source builds markup only with DOM calls, and the bundle has no eval (4.954503ms)
|
||||
✔ the link gate and the image rule are the preview's (4.139233ms)
|
||||
✔ R6 G1 a granted user signs in, gets the user page and reaches the broker as itself (1316.977947ms)
|
||||
▶ R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
✔ a listed user with no grant (824.475999ms)
|
||||
✔ a grant in another business (523.520932ms)
|
||||
✔ an expired grant (296.216812ms)
|
||||
✔ another user's grant (388.90561ms)
|
||||
✔ a sub in no file (388.51449ms)
|
||||
✔ a file the loader refuses (507.682985ms)
|
||||
✔ R6 G2 a subject with no live grant in this business is refused subject-unknown (2931.971402ms)
|
||||
▶ R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
✔ grant removed (357.100075ms)
|
||||
✔ subject removed (319.782039ms)
|
||||
✔ grant expired (210.53958ms)
|
||||
✔ file now refused (398.324268ms)
|
||||
✔ R6 G3 a revoke counts on the next request: 401 and the session is gone (1287.842624ms)
|
||||
✔ R6 G4 every owner route answers 403 owner-only before any bus call (496.654858ms)
|
||||
✔ R6 G5 the bus refuses every owner verb over the user transport on its own (460.217578ms)
|
||||
✔ R6 G9 a user chat sends only the plugins read over the owner transport (356.084377ms)
|
||||
✔ R6 G6 the user page holds the notice word for word (198.72429ms)
|
||||
✔ R6 G7 the hub label and plain peer links render from config on both pages (2799.705194ms)
|
||||
✔ R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request (191.000701ms)
|
||||
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (53790.304186ms)
|
||||
✔ row 165 Settings: role instances read, add with a restart named, change without one, a refusal keeps the draft (5167.475123ms)
|
||||
✔ row 165 Console: the signed-in human adds an instance through a real broker; validate exits 0, the log names the human and no restart is needed (row 194) (1132.361479ms)
|
||||
✔ row 165 Console: the instance write needs send on, the public name, the user, the exact Origin and same-origin Fetch Metadata (32.171901ms)
|
||||
✔ row 165 Console: the instance write takes JSON within 16 KiB and only its own keys; holder, credentials and an actor are refused (41.99937ms)
|
||||
✔ row 165 Console: a bus refusal says nothing changed; a lost or odd answer says the change may have been made (59.978508ms)
|
||||
✔ row 165 Console: the instances read takes no arguments, needs a bus and refuses an answer without a revision (22.608682ms)
|
||||
✔ C2-04 CLI: both listener callback pairs dispatch library verbs with their own transport (675.474919ms)
|
||||
✔ row 254: each library sync code maps to its status (1.109598ms)
|
||||
✔ REQ-LIMIT-3 REQ-LIMIT-4 Settings shows token limits counted, not enforced and check failed; Reset posts one path; a save keeps the token limits (5088.145963ms)
|
||||
✔ Row 167 with an assistant draft open, a token limit reset gives the assistant submit its revision (2992.385147ms)
|
||||
✔ REQ-LIMIT-3 limits read returns only the token rows; no query or write is accepted (69.276708ms)
|
||||
✔ REQ-LIMIT-3 a limits answer without a token list is refused, not shown as no limits (26.410715ms)
|
||||
2026-10-23T05:55:56.654Z settings user="jason" changed
|
||||
✔ REQ-LIMIT-3 a Settings reset reaches the human write with its paths; a reset alone is enough (33.515226ms)
|
||||
✔ REQ-LIMIT-3 the Console reader asks the bus limits verb with no arguments (0.445728ms)
|
||||
✔ the preview source names none of the four string-to-markup sinks (11.337961ms)
|
||||
✔ headings, paragraphs, emphasis and inline code render (4.988648ms)
|
||||
✔ lists, block quotes and code blocks render (1.032775ms)
|
||||
✔ a link is made for http, https and relative targets, with rel and the target shown (1.973988ms)
|
||||
✔ a link with any other scheme, or an evasion of the scheme, stays source text (3.200642ms)
|
||||
✔ an image shows alt text and URL, is never an img element and never loads (0.396015ms)
|
||||
✔ raw HTML, entities and unsupported blocks show as source text, never as elements (0.934518ms)
|
||||
✔ markup inside code and inside link text stays text (0.310785ms)
|
||||
✔ hostile and pathological input stays within its look-ahead budget and under the nesting cap (155.505718ms)
|
||||
✔ trailing blanks and closing hashes are cut by a walk from the end, not a regex that retries inside a blank run (105.66919ms)
|
||||
✔ a link opener with no closer searches at most SPAN characters ahead (510.399238ms)
|
||||
✔ the look-ahead work of one render is a fixed multiple of the text, whatever the opener (2237.958836ms)
|
||||
✔ past the budget an opener is source text, through the path all other text takes (68.634869ms)
|
||||
✔ a normal 200 KiB document renders exactly as it did without a budget, and spends little of it (72.297348ms)
|
||||
✔ a line separator inside a heading, a list item or a fence line does not end the marker line (0.782009ms)
|
||||
✔ a thematic-break line is a paragraph, every other marker line opens a list (1.265264ms)
|
||||
✔ every short line over space, tab, - * _ and a letter is a break exactly when the old regex said so (91.507655ms)
|
||||
✔ one line of marks does not throw, at 4 MiB, whatever the mark or the spacing (18693.732276ms)
|
||||
✔ CRLF and tabs are read like LF and spaces; empty input renders nothing (0.290659ms)
|
||||
✔ messages HTTP read pages and preserves metadata; bad arguments refuse before a read; other reads unchanged (331.712949ms)
|
||||
✔ 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 (3963.006159ms)
|
||||
✔ Messages nav count polls on Tasks and Board and clears on a Board refusal; messages 403 refuses local pages (5544.18983ms)
|
||||
✔ copy writes the Console paths at the full id, read-only, and leaves an existing copy alone (4586.516465ms)
|
||||
✔ copy refuses a commit off refactor, an unknown one, a worktree and a bad copies directory (8390.471397ms)
|
||||
✔ copy writes nothing when an archived path is missing at the commit (2233.69143ms)
|
||||
✔ a copy that loses the rename race leaves no read-only temp directory behind (3185.466552ms)
|
||||
✔ a copy whose rename fails with no target refuses, writes no copy and leaves no temp directory (1737.482631ms)
|
||||
✔ a copy that meets a target holding files says exists: and moves nothing into it (1915.193777ms)
|
||||
✔ a copy that meets an empty target directory replaces it by rename, not by moving into it (3183.833741ms)
|
||||
✔ serve runs from a copy: /healthz 200, the config check, roles and RELEASE from the copy, the queue from --checkout (3034.564865ms)
|
||||
✔ serve refuses a --checkout that is not an absolute directory path (517.815946ms)
|
||||
✔ consoleReads runs the queue read under queueRoot and reads RELEASE under root (96.617937ms)
|
||||
✔ preview options are all three or none, and each is checked (11.976568ms)
|
||||
✔ the CLI refuses a partial or invalid preview flag set at start (2800.366165ms)
|
||||
✔ the CLI with all three flags serves the preview mode (415.062455ms)
|
||||
✔ a request for the public name needs the header equal to the user id, else 401 and nothing read (201.85949ms)
|
||||
✔ the header name matches without regard to case, and the value compare is exact (21.145493ms)
|
||||
✔ the public name with the header reads the bus views, and the Host and Origin are checked first (70.824341ms)
|
||||
✔ in preview mode every write answers 403 read-only, from the public name and loopback, and reaches nothing (44.206826ms)
|
||||
✔ loopback requests are unchanged in preview mode, and the bus views work with the board down (24.51805ms)
|
||||
✔ a proxy that rewrites Host to the loopback port gets 403 on its first request, whatever the user (52.938487ms)
|
||||
✔ a repeated Host is 400 before any other check, in preview mode and without it (21.718312ms)
|
||||
✔ without the preview flags the public name is refused and writes still reach the board (33.356546ms)
|
||||
✔ processing browser: keyboard Stop, confirmed Resume, unknown outcome and persistent banner (2808.628055ms)
|
||||
✔ processing browser: Q175 read-only interim keeps both writes disabled and names CLI recovery at 320px (2523.998629ms)
|
||||
✔ processing browser: sign-out from config redirect blocks later refresh and writes (2308.208488ms)
|
||||
✔ processing browser: sign-out from state redirect blocks later refresh and writes (2696.70657ms)
|
||||
✔ processing browser: sign-out from write redirect blocks later refresh and writes (1956.916302ms)
|
||||
✔ processing browser: sign-out from config sign-in required blocks later refresh and writes (2238.37605ms)
|
||||
✔ processing browser: sign-out from state sign-in required blocks later refresh and writes (1495.983937ms)
|
||||
✔ processing browser: sign-out from write sign-in required blocks later refresh and writes (1578.528628ms)
|
||||
ROW191_LAYOUT {"width":400,"height":1000,"controls":97,"tree":324.0625,"main":1450.703125,"top":181.5,"bottom":217.5,"barBottom":129,"projectsTop":352.59375,"order":"0","visible":true}
|
||||
ROW191_LAYOUT {"width":1440,"height":1000,"controls":127,"tree":914,"main":914,"top":135.5,"bottom":171.5,"barBottom":53,"projectsTop":636.59375,"order":"0","visible":true}
|
||||
ROW191_LAYOUT {"width":1440,"height":420,"controls":127,"tree":334,"main":907.734375,"top":135.5,"bottom":171.5,"barBottom":53,"projectsTop":636.59375,"order":"0","visible":true}
|
||||
ROW191_LAYOUT {"width":800,"height":420,"controls":127,"tree":334,"main":1131.65625,"top":135.5,"bottom":171.5,"barBottom":53,"projectsTop":618.59375,"order":"0","visible":true}
|
||||
✔ processing browser: static nav controls keep Stop visible, ordered and keyboard reachable (1989.328122ms)
|
||||
✔ interim read-only controls show CLI recovery, disable both writes and retain a halted banner across views (11.626158ms)
|
||||
✔ Stop sends one empty halt and Resume confirms before sending the displayed generation (10.67315ms)
|
||||
✔ a cancelled resume never calls the writer (3.179669ms)
|
||||
✔ unknown writes are not retried or displayed as success and partial stops remain explicit (5.868146ms)
|
||||
✔ the persistent shell loads the control outside route-specific content with accessible status and native buttons (1.133568ms)
|
||||
✔ read-only Console can read processing state but neither human control writes without --send (66.218139ms)
|
||||
✔ Console halt and generation-bound resume use only the management writer and keep partial stops honest (10.982172ms)
|
||||
✔ processing write fences refuse cross-site, wrong origin, unnamed host, duplicate identity and bad payload before writer (14.633353ms)
|
||||
✔ tester cannot invoke either control writer and failures never echo backend text (11.806188ms)
|
||||
✔ processing writes use the existing SSO signer and still require --send and same-origin (21.711519ms)
|
||||
✔ processing handler independently refuses wrong origin and cross-site before its writer (3.989692ms)
|
||||
✔ a signed-in same-origin save reaches exactly one human operation and returns the new commit (53.222367ms)
|
||||
✔ a save needs send, the named Host, the user, the exact Origin and same-origin Fetch Metadata (27.153666ms)
|
||||
✔ a save validates JSON, strict keys, text values and the 1 MiB body before the bus is asked (61.625111ms)
|
||||
✔ a body that is JSON but not an object is refused 400, not thrown on (22.703984ms)
|
||||
✔ the 1 MiB limit is this route only; Settings and messages keep 16 KiB (16.671197ms)
|
||||
✔ a bus refusal is reported as not saved with its own status; stale-base tells the user to reload (14.557833ms)
|
||||
✔ a save whose answer is lost or unusable is reported unknown and is never retried (6.863996ms)
|
||||
✔ the file list and one file read take strict queries and arrive in the expected shape (12.074836ms)
|
||||
✔ reads work without --send; an unusable bus answer is a bad response, a refusal keeps its status (10.728105ms)
|
||||
✔ /api/config says edit only where a save can pass (4.761488ms)
|
||||
✔ a repository-name project refusal is an HTTP conflict (0.984724ms)
|
||||
✔ proof startup refuses partial flags non-preview and invalid fixture file metadata or bytes (4347.589056ms)
|
||||
✔ serve argv carries only fixture proof path and header; startup explicitly reports proof on or off (1057.032471ms)
|
||||
✔ CLI applies proof on the main listener and rejects that header on its tester listener (993.00971ms)
|
||||
✔ proxy proof admits named reads and sends, reads its fixture once, and never exposes the value (130.333148ms)
|
||||
✔ proxy proof refuses missing wrong short or repeated proof before auth and before any read (37.21869ms)
|
||||
✔ proxy proof header is refused on other Hosts and on the tester listener (27.767421ms)
|
||||
✔ proof options reject unsafe or colliding header names without disclosing file contents (1.779971ms)
|
||||
✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (2.717157ms)
|
||||
✔ credential state from the date alone, as the bus states it (0.509147ms)
|
||||
✔ queue: rows, one row, ids and methods, notes; no path or id from a row (1632.086256ms)
|
||||
✔ queue: a refused read fails closed with the store's text, redacted (980.994864ms)
|
||||
✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (2529.760674ms)
|
||||
✔ business: names, vars on the allowlist, authority, credential metadata only (70.870478ms)
|
||||
✔ business: missing or invalid file refuses with the module's text, redacted (53.602689ms)
|
||||
✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (35.39629ms)
|
||||
✔ settings: release, board, loopback address, notifier binding name only (650.376903ms)
|
||||
✔ task fields: the committed table with no reads configured, GET only; a bad table refuses with no path (110.672173ms)
|
||||
✔ bus-status: a refused boot shows reason, file and last good load; a live host or an ok boot clears it (82.128753ms)
|
||||
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (3047.931743ms)
|
||||
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (22564.00615ms)
|
||||
organization: {"event":"reload-needs-restart","business":"acme"}
|
||||
organization: {"event":"reload-needs-restart","business":"acme"}
|
||||
✔ row 65 Settings: the routing table lists every instance, saves a harness, model, thinking and fallback to the file, and keeps the draft on each refusal (7355.766569ms)
|
||||
organization: {"event":"reload-needs-restart","business":"acme"}
|
||||
✔ row 65 Console: the signed-in human saves a route through a real broker; each refusal says nothing changed and leaves the file and the log (549.54912ms)
|
||||
✔ row 65 Console: the routes write needs send on, a writer, the public name, the user, the exact Origin and same-origin Fetch Metadata (85.124486ms)
|
||||
✔ row 65 Console: the routes write takes JSON within 16 KiB with a revision and routes only; an actor is refused and routes are never logged (68.236202ms)
|
||||
✔ row 65 Console: a bus refusal maps to its status and says nothing changed; a lost or odd answer says the change may have been made (171.700585ms)
|
||||
✔ row 65 Console: the routes read takes no arguments, needs a bus and refuses an odd answer (19.702141ms)
|
||||
✔ 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 (6601.970612ms)
|
||||
✔ a sent receipt lists its replies as the page refreshes, oldest first and capped, and marks none delivered or read (12991.110365ms)
|
||||
✔ startServer takes send only as a function and only with the preview mode (43.083476ms)
|
||||
✔ the CLI refuses --send without the preview flags, and without a configured bus (1335.336644ms)
|
||||
✔ the CLI with --send and a configured bus sends through the human transport and logs one line (543.898784ms)
|
||||
✔ without --send the route is read-only like every other write, and config says no send (64.471753ms)
|
||||
✔ with --send every other write is still 403 read-only, and nothing reaches the board or bus (21.210729ms)
|
||||
✔ config says send only on the public name with --send on (4.288813ms)
|
||||
✔ a send without the signed-in user is 401 and is neither sent nor logged, loopback included (7.458773ms)
|
||||
✔ each request check refuses before anything is sent, and logs the refusal without the body (39.792757ms)
|
||||
✔ a send passes exactly to, body and in_reply_to, answers the id, and logs it (13.437587ms)
|
||||
✔ a crafted role name cannot forge a log line (10.514752ms)
|
||||
✔ a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried (17.460171ms)
|
||||
✔ through the human transport the request is exactly the business, message.send and the keys (340.25205ms)
|
||||
✔ against a real broker: the message is written once as a human send, and its refusals map (161.0439ms)
|
||||
✔ loopback host and board origin fail closed (10.573253ms)
|
||||
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (220.576472ms)
|
||||
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (135.506401ms)
|
||||
✔ unreachable board reports URL; CLI rejects unsupported options (1562.351502ms)
|
||||
✔ tester service render carries the tester port alongside existing flags (656.27483ms)
|
||||
✔ tester service refuses forwarded 7332 even with another main port (317.551113ms)
|
||||
✔ tester service invalid ports refuse render and install with exit 4 and leave the unit alone (3244.971758ms)
|
||||
✔ tester service counts the digits as written before it strips leading zeros (241.348549ms)
|
||||
✔ tester service install and status show the tester port without changing send reporting (1728.399913ms)
|
||||
✔ render prints the unit without the preview flags (104.271618ms)
|
||||
✔ render carries every serve flag, the preview set included (125.076023ms)
|
||||
✔ a partial preview set is refused (310.9585ms)
|
||||
✔ a value the unit cannot carry is refused, for every flag (5999.104416ms)
|
||||
✔ the rendered unit names none of the human proof agent markers and sets no agent variable (383.481456ms)
|
||||
✔ install into --dir with --no-reload writes the file whole, then reports unchanged (615.325081ms)
|
||||
✔ install without --no-reload runs daemon-reload and nothing that starts the unit (97.441716ms)
|
||||
✔ uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not (1659.738962ms)
|
||||
✔ status prints the unit state and GET /healthz on the installed port (785.185866ms)
|
||||
✔ usage: an unknown verb or flag, and start or enable, exit 4 (1191.552146ms)
|
||||
✔ without --send, render is the template with its placeholders filled, byte for byte (271.93262ms)
|
||||
✔ render --send with the preview set ends ExecStart with --send and changes nothing else (541.599437ms)
|
||||
✔ --send without the whole preview set is refused and writes nothing (672.70952ms)
|
||||
✔ install --send into --dir with --no-reload writes the sending unit, and status says whether it sends (400.744531ms)
|
||||
✔ uninstall and status take no --send (135.813954ms)
|
||||
✔ install refuses while the unit is running, in any state but inactive or failed, and changes nothing (558.066163ms)
|
||||
✔ when the state read fails, install and uninstall refuse with one line and change nothing (182.083951ms)
|
||||
✔ install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree (274.396406ms)
|
||||
✔ install refuses unless the bus unit runs from the same copy, naming both paths, and changes nothing (659.576563ms)
|
||||
✔ render --copy runs the copy and reads the queue in the checkout; without --copy the unit is unchanged (94.07392ms)
|
||||
✔ the uid header takes only an Authentik uid; the username header still takes a username (1670.240068ms)
|
||||
✔ a preview value cannot start with - or . (178.652961ms)
|
||||
✔ status names the copy and whether refactor has moved past it, and says when the unit runs from no copy (498.936817ms)
|
||||
✔ proxy proof render carries both flags but never opens the fixture file (35.896092ms)
|
||||
✔ proxy proof render refuses incomplete non-preview missing unsafe-mode and repo/copy paths (382.337328ms)
|
||||
✔ proxy proof status retains send and tester reporting with both proof flags (129.652865ms)
|
||||
✔ row196 socket flag renders only for supporting targets and refuses mixed transports (91.132376ms)
|
||||
✔ Settings form shows every scope and held role; gated save, exact payload, retained draft and no automatic retry (3601.134961ms)
|
||||
✔ Q169 Assistant section: gated, changed fields only, name and stop posts, retained draft and no automatic retry (3393.396967ms)
|
||||
✔ Row 167 a refused names read gives the refused usage page; another names failure keeps the Assistant section (1958.342906ms)
|
||||
✔ Row 167 with a limits draft open, an assistant save gives the limits submit its revision and the typed limit stays (2373.207713ms)
|
||||
✔ Row 167 with an assistant draft open, a launch save gives the assistant submit its revision, and reload reads the current one (2271.900541ms)
|
||||
2026-10-23T05:56:09.758Z settings user="jason" changed
|
||||
✔ REQ-WEB-1 signed-in same-origin Settings write reaches exactly one human operation (41.040768ms)
|
||||
✔ REQ-WEB-1 Settings writes require enabled send, named Host, identity, exact Origin and same-origin Fetch Metadata (16.337548ms)
|
||||
✔ REQ-WEB-1 Settings validates JSON, size, revision and strict writable keys before broker call (26.736686ms)
|
||||
✔ Q169 a Console change of the assistant reaches the human operation unchanged, and the log names the signed-in user (10.295391ms)
|
||||
✔ tokens.css and icons.svg are byte copies of docs/design (5.033002ms)
|
||||
✔ JetBrains Mono files match the digests in their sources file, with the OFL text (3.419534ms)
|
||||
✔ shipped strings are unchanged, and the web keeps its write boundary (7.185612ms)
|
||||
ROW191_LAYOUT {"width":1440,"height":1000,"controls":212.5,"tree":914,"main":914}
|
||||
✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (9997.335631ms)
|
||||
✔ five states on Board, Inbox, Tasks, Agents and Trail (5100.598536ms)
|
||||
✔ a 401 sign-in required on each fetch path signs the page out with a sign-in link and stops the refresh; another 401 stays an error (8947.165195ms)
|
||||
✔ the Sign out form shows only when /api/config says sso, and posts to /auth/logout (3103.277152ms)
|
||||
✔ positive: an RS256 provider signs in and sends one message, and the session reads (475.641336ms)
|
||||
✔ positive: an ES256 provider signs in and sends (193.349473ms)
|
||||
✔ positive: two providers each sign in their own subjects, and one provider's subject is unknown to the other (169.921509ms)
|
||||
✔ 1: a forged x-authentik header without a cookie is 401 on the API and a sign-in redirect on a page (278.514365ms)
|
||||
✔ 2: an HS256 ID token keyed with the client secret and a real RSA kid is refused (134.191938ms)
|
||||
✔ 3: a wrong aud, and an aud list holding another client with no azp, are refused (101.185717ms)
|
||||
✔ 4: a token that expired 10 minutes ago is refused (138.60402ms)
|
||||
✔ 5: another issuer, and the issuer without its trailing slash, are refused (148.289879ms)
|
||||
✔ 6: a nonce mismatch and a missing nonce are refused (70.339871ms)
|
||||
✔ 7: state mismatch, no login cookie, a replay, an expired record and two login cookies are refused (131.822465ms)
|
||||
✔ 8: the token request carries the PKCE verifier, and the OP refuses a wrong one (122.964705ms)
|
||||
✔ 9: alg none, a changed payload under a real kid, and an unknown kid even after the refetch are refused (222.329164ms)
|
||||
✔ 10: a valid token for a subject not configured is 403 with one subject-unknown line (204.225028ms)
|
||||
✔ 11: start refusals: HS or none algs, reserved names, HS-only discovery, an empty JWKS, no S256, an issuer mismatch, a short RSA key (433.127154ms)
|
||||
✔ 11: slot files: a symlink, mode 0644, mode 0640 and a file under the repository are refused, and no message holds the value (5.347397ms)
|
||||
✔ 11: the CLI refuses --sso with the old flags and without console.sso, and starts with a good one (2880.406681ms)
|
||||
✔ 12: under --sso the tester listener refuses each kind of sign-in, has no /auth, and startServer refuses tester with sso (134.115947ms)
|
||||
✔ 13: idle and absolute expiry, logout then reuse, a cross-origin logout, and background reads that don't keep a session (110.692872ms)
|
||||
✔ 13: the CSP lets the Sign out form reach the provider's end-session origin, and only under --sso (126.33035ms)
|
||||
✔ 14: open redirects land on /, and a path on the Console lands there (125.809379ms)
|
||||
✔ 15: a callback for A carrying B's code and iss, and a provider= query, is refused (183.037053ms)
|
||||
✔ 16: a provider with RS256 and ES256 takes either, and refuses PS256 with a usable PS256 key in the JWKS (169.444622ms)
|
||||
✔ 17: 33 started logins leave 32 records, and the first one's callback finds none (177.30703ms)
|
||||
✔ 18: after a key rotation a sign-in works through one refetch, and the removed kid is refused (176.858541ms)
|
||||
✔ 20 and 21: the store holds no raw session id, and the cookie strings are exact (144.099594ms)
|
||||
✔ 22: a loopback Host is 403 on a page, a read and a write; /healthz on loopback is 200; a redirect_uri never comes from the Host (115.906034ms)
|
||||
✔ 23: a provider down at start leaves the Console up and 503, and the retry makes sign-in work (319.452618ms)
|
||||
✔ 19: no captured log line holds a token, code, state, nonce, verifier, client secret or session id (9.722633ms)
|
||||
✔ tester Console banner exposes only instance route and name editors and saves a route as tester (2919.456183ms)
|
||||
✔ row 177: the tester Console chat composer is enabled and a send shows in its own thread (2039.018085ms)
|
||||
✔ tester-port validates range tunnel main port and requires configured bus before listening (2901.241331ms)
|
||||
✔ CLI tester listener binds a private Unix socket and spawns TESTER_CLI for every Console bus read (8304.201603ms)
|
||||
✔ main listener refuses x-mosaic-tester before any board or bus read for every Host (43.969937ms)
|
||||
✔ tester listener rejects preview binding and requires its fixed logical Host and no proxy auth header (239.871872ms)
|
||||
✔ tester listener writes only instances routes and names with tester receipts and actor (329.224367ms)
|
||||
✔ tester listener refuses every human-only write with no bus call (403.357909ms)
|
||||
✔ tester-off and tester-required reads return HTTP 403 (423.519709ms)
|
||||
✔ tester credentials routes and committed project reads keep secrets and paths bounded (486.303217ms)
|
||||
✔ row 177 W1: the tester chat makes, sends to, opens and reads its own thread as tester (96.490199ms)
|
||||
✔ row 177 W2: tester chat writes refuse a missing or foreign Origin, cross-site, text/plain and the proxy header with no bus call (36.32174ms)
|
||||
✔ row 177 W3: the human thread through the tester chat is thread-not-owned, and bus.tester off answers tester-off (100.76962ms)
|
||||
✔ row 177 W4: the main listener refuses x-mosaic-tester on chat reads and writes before the chat (4.368266ms)
|
||||
✔ private tester HTTP serves on 0600 socket in 0700 parent and fixed origin (123.499396ms)
|
||||
✔ private tester refuses symlink, unsafe parent and long path without repairs (1.978231ms)
|
||||
✔ tester cleanup must leave a replacement inode untouched (3.005429ms)
|
||||
✔ row278 browser lists held claims and release preserves role rows and totals (2401.149351ms)
|
||||
✔ row278 browser claims 403 stays local and role section remains byte-equal (2043.04796ms)
|
||||
✔ row278 browser renders only four escaped claim fields and never instance or other extras (1539.461412ms)
|
||||
✔ row278 browser sign-out removes claims and prevents reuse of cached answers (1189.306078ms)
|
||||
✔ row278 tester browser keeps role rows without requesting the owner claims section (994.783844ms)
|
||||
✔ row278 browser discards a late claims answer after sign-out (909.523235ms)
|
||||
✔ row278 HTTP read returns held claims without send and refuses arguments before the bus (76.323822ms)
|
||||
✔ row278 HTTP read preserves bus refusal and rejects a malformed result (21.823555ms)
|
||||
✔ row278 tester socket refuses thread claims explicitly with zero bus calls (16.224768ms)
|
||||
✔ row278 Console adapter calls argument-free thread.claims through the normal transport (400.259747ms)
|
||||
✔ REQ-LIMIT-4 Settings shows all token kinds and unknown turns beside every total, as a read-only separate section (2328.431483ms)
|
||||
✔ REQ-WEB-1 usage read keeps unknown counts and each token kind; no query or write is accepted (85.831497ms)
|
||||
✔ REQ-WEB-1 malformed usage is refused instead of shown as zero (14.660937ms)
|
||||
✔ Queue, Business and Settings in their five states, with no move button (5837.604193ms)
|
||||
✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (1874.584545ms)
|
||||
✔ the server log names no config path when the system config refuses (313.34961ms)
|
||||
ℹ tests 365
|
||||
ℹ suites 0
|
||||
ℹ pass 365
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 58218.927473
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/packages/webui/src/serve.mjs b/packages/webui/src/serve.mjs
|
||||
index cd51a0b6e..f93fb49e0 100644
|
||||
--- a/packages/webui/src/serve.mjs
|
||||
+++ b/packages/webui/src/serve.mjs
|
||||
@@ -683,7 +683,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
// FED R6 (#1800): a granted user. The Console's gate first; behind it no human-transport
|
||||
// object is left, so a route past the gate reaches the broker only as this user.
|
||||
if (session?.user !== undefined) {
|
||||
- if (!userRoute(path, req.method)) return json(res, 403, { error: 'owner-only', message: 'this part of the Console is the business owner\'s' });
|
||||
+ if (false) return json(res, 403, { error: 'owner-only', message: 'this part of the Console is the business owner\'s' });
|
||||
if (userOptions === null) return json(res, 503, { error: 'not-configured', message: 'the Console has no user transport' });
|
||||
({ bus, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing } = userOptions(session));
|
||||
reads = null; upstream = null; files = USER_FILES; granted = true;
|
||||
@@ -0,0 +1,145 @@
|
||||
TAP version 13
|
||||
# Subtest: R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
ok 1 - R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
---
|
||||
duration_ms: 97.950367
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
# Subtest: a listed user with no grant
|
||||
ok 1 - a listed user with no grant
|
||||
---
|
||||
duration_ms: 97.105064
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a grant in another business
|
||||
ok 2 - a grant in another business
|
||||
---
|
||||
duration_ms: 60.372976
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: an expired grant
|
||||
ok 3 - an expired grant
|
||||
---
|
||||
duration_ms: 65.745937
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: another user's grant
|
||||
ok 4 - another user's grant
|
||||
---
|
||||
duration_ms: 53.325523
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a sub in no file
|
||||
ok 5 - a sub in no file
|
||||
---
|
||||
duration_ms: 68.487478
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a file the loader refuses
|
||||
ok 6 - a file the loader refuses
|
||||
---
|
||||
duration_ms: 86.961472
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
ok 2 - R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
---
|
||||
duration_ms: 432.952739
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
# Subtest: grant removed
|
||||
ok 1 - grant removed
|
||||
---
|
||||
duration_ms: 59.298593
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: subject removed
|
||||
ok 2 - subject removed
|
||||
---
|
||||
duration_ms: 67.687849
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: grant expired
|
||||
ok 3 - grant expired
|
||||
---
|
||||
duration_ms: 59.604679
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: file now refused
|
||||
ok 4 - file now refused
|
||||
---
|
||||
duration_ms: 71.375547
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
ok 3 - R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
---
|
||||
duration_ms: 258.570476
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
not ok 4 - R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
---
|
||||
duration_ms: 67.489703
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:235:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
GET /api/bus/inbox: 502 {"error":"access-refused","message":"access-refused"}
|
||||
|
||||
502 !== 403
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 403
|
||||
actual: 502
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:241:12)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
ok 5 - R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
---
|
||||
duration_ms: 73.143926
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
ok 6 - R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
---
|
||||
duration_ms: 65.906106
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G6 the user page holds the notice word for word
|
||||
ok 7 - R6 G6 the user page holds the notice word for word
|
||||
---
|
||||
duration_ms: 55.03254
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G7 the hub label and plain peer links render from config on both pages
|
||||
ok 8 - R6 G7 the hub label and plain peer links render from config on both pages
|
||||
---
|
||||
duration_ms: 682.517872
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
ok 9 - R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
---
|
||||
duration_ms: 39.36235
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 19
|
||||
# suites 0
|
||||
# pass 18
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 1903.614611
|
||||
|
||||
# stderr
|
||||
@@ -0,0 +1,11 @@
|
||||
diff --git a/packages/webui/src/console-bus.mjs b/packages/webui/src/console-bus.mjs
|
||||
index a446370c2..41a77074c 100644
|
||||
--- a/packages/webui/src/console-bus.mjs
|
||||
+++ b/packages/webui/src/console-bus.mjs
|
||||
@@ -45,5 +45,5 @@ export function consoleBus(call, { send: sendOn = false, owner = null } = {}) {
|
||||
// request reaches `owner` (mos-1394, sage-3157): createChat sends it only `plugins` with {}.
|
||||
export function userOptionsFor({ owner, transport, send = false }) {
|
||||
if (typeof owner !== 'function' || typeof transport !== 'function') throw new TypeError('userOptionsFor needs owner and transport functions');
|
||||
- return session => consoleBus(transport(session.user), { send, owner });
|
||||
+ return session => consoleBus(transport(session.user), { send, owner: (v, a) => owner(v, { ...a, sub: session.sub }) });
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
TAP version 13
|
||||
# Subtest: R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
ok 1 - R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
---
|
||||
duration_ms: 109.420278
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
# Subtest: a listed user with no grant
|
||||
ok 1 - a listed user with no grant
|
||||
---
|
||||
duration_ms: 92.156219
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a grant in another business
|
||||
ok 2 - a grant in another business
|
||||
---
|
||||
duration_ms: 64.042376
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: an expired grant
|
||||
ok 3 - an expired grant
|
||||
---
|
||||
duration_ms: 46.110537
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: another user's grant
|
||||
ok 4 - another user's grant
|
||||
---
|
||||
duration_ms: 55.140611
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a sub in no file
|
||||
ok 5 - a sub in no file
|
||||
---
|
||||
duration_ms: 98.591293
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a file the loader refuses
|
||||
ok 6 - a file the loader refuses
|
||||
---
|
||||
duration_ms: 121.289506
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
ok 2 - R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
---
|
||||
duration_ms: 478.250572
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
# Subtest: grant removed
|
||||
ok 1 - grant removed
|
||||
---
|
||||
duration_ms: 47.439106
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: subject removed
|
||||
ok 2 - subject removed
|
||||
---
|
||||
duration_ms: 65.33328
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: grant expired
|
||||
ok 3 - grant expired
|
||||
---
|
||||
duration_ms: 70.57336
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: file now refused
|
||||
ok 4 - file now refused
|
||||
---
|
||||
duration_ms: 52.579125
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
ok 3 - R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
---
|
||||
duration_ms: 236.514118
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
ok 4 - R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
---
|
||||
duration_ms: 68.104497
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
ok 5 - R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
---
|
||||
duration_ms: 73.362804
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
not ok 6 - R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
---
|
||||
duration_ms: 59.549676
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:279:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
'plugins',
|
||||
+ {
|
||||
+ sub: 'alice-sub'
|
||||
+ }
|
||||
- {}
|
||||
]
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0: 'plugins'
|
||||
1:
|
||||
actual:
|
||||
0: 'plugins'
|
||||
1:
|
||||
sub: 'alice-sub'
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:297:46)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: R6 G6 the user page holds the notice word for word
|
||||
ok 7 - R6 G6 the user page holds the notice word for word
|
||||
---
|
||||
duration_ms: 64.639126
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G7 the hub label and plain peer links render from config on both pages
|
||||
ok 8 - R6 G7 the hub label and plain peer links render from config on both pages
|
||||
---
|
||||
duration_ms: 754.648348
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
ok 9 - R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
---
|
||||
duration_ms: 44.114514
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 19
|
||||
# suites 0
|
||||
# pass 18
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 2018.907276
|
||||
|
||||
# stderr
|
||||
@@ -0,0 +1,17 @@
|
||||
diff --git a/packages/webui/src/chat.mjs b/packages/webui/src/chat.mjs
|
||||
index 1b0a41dce..311048755 100644
|
||||
--- a/packages/webui/src/chat.mjs
|
||||
+++ b/packages/webui/src/chat.mjs
|
||||
@@ -13,9 +13,10 @@ import { BusReadError } from './bus.mjs';
|
||||
export function createChat({ call, owner = null }) {
|
||||
if (typeof call !== 'function') throw new TypeError('chat needs a bus transport function');
|
||||
if (owner !== null && typeof owner !== 'function') throw new TypeError('chat owner must be a bus transport function');
|
||||
- const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => verb === 'plugins' ? owner('plugins', {}) : call(verb, args) });
|
||||
+ let last = {};
|
||||
+ const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => verb === 'plugins' ? owner('plugins', last) : call(verb, args) });
|
||||
const channel = async (verb, args) => {
|
||||
- try { return await dispatch(verb, args); }
|
||||
+ try { last = args; return await dispatch(verb, args); }
|
||||
catch (err) {
|
||||
if (err instanceof PluginError) throw new BusReadError(err.code, err.message);
|
||||
throw err;
|
||||
@@ -0,0 +1,159 @@
|
||||
TAP version 13
|
||||
# Subtest: R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
ok 1 - R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
---
|
||||
duration_ms: 102.153087
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
# Subtest: a listed user with no grant
|
||||
ok 1 - a listed user with no grant
|
||||
---
|
||||
duration_ms: 41.816765
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a grant in another business
|
||||
ok 2 - a grant in another business
|
||||
---
|
||||
duration_ms: 53.103013
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: an expired grant
|
||||
ok 3 - an expired grant
|
||||
---
|
||||
duration_ms: 66.865735
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: another user's grant
|
||||
ok 4 - another user's grant
|
||||
---
|
||||
duration_ms: 59.528254
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a sub in no file
|
||||
ok 5 - a sub in no file
|
||||
---
|
||||
duration_ms: 77.000699
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a file the loader refuses
|
||||
ok 6 - a file the loader refuses
|
||||
---
|
||||
duration_ms: 59.00091
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
ok 2 - R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
---
|
||||
duration_ms: 358.182177
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
# Subtest: grant removed
|
||||
ok 1 - grant removed
|
||||
---
|
||||
duration_ms: 43.655065
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: subject removed
|
||||
ok 2 - subject removed
|
||||
---
|
||||
duration_ms: 82.752965
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: grant expired
|
||||
ok 3 - grant expired
|
||||
---
|
||||
duration_ms: 75.927345
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: file now refused
|
||||
ok 4 - file now refused
|
||||
---
|
||||
duration_ms: 52.720211
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
ok 3 - R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
---
|
||||
duration_ms: 255.656191
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
ok 4 - R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
---
|
||||
duration_ms: 61.988092
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
ok 5 - R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
---
|
||||
duration_ms: 72.391514
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
not ok 6 - R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
---
|
||||
duration_ms: 100.121943
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:279:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
'plugins',
|
||||
+ {
|
||||
+ name: 'alice work',
|
||||
+ project: '6ca7e806-cb2e-432f-8456-32d161a596c5'
|
||||
+ }
|
||||
- {}
|
||||
]
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0: 'plugins'
|
||||
1:
|
||||
actual:
|
||||
0: 'plugins'
|
||||
1:
|
||||
project: '6ca7e806-cb2e-432f-8456-32d161a596c5'
|
||||
name: 'alice work'
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:297:46)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: R6 G6 the user page holds the notice word for word
|
||||
ok 7 - R6 G6 the user page holds the notice word for word
|
||||
---
|
||||
duration_ms: 81.778691
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G7 the hub label and plain peer links render from config on both pages
|
||||
ok 8 - R6 G7 the hub label and plain peer links render from config on both pages
|
||||
---
|
||||
duration_ms: 736.353706
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
ok 9 - R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
---
|
||||
duration_ms: 36.965803
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 19
|
||||
# suites 0
|
||||
# pass 18
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 1931.825146
|
||||
|
||||
# stderr
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/packages/webui/src/sso.mjs b/packages/webui/src/sso.mjs
|
||||
index dd5ad8f61..e9506af29 100644
|
||||
--- a/packages/webui/src/sso.mjs
|
||||
+++ b/packages/webui/src/sso.mjs
|
||||
@@ -306,7 +306,7 @@ export async function createSso(config, { secrets, fetch: customFetch = null, lo
|
||||
if (now - s.created >= SESSION_ABSOLUTE || now - s.seen >= SESSION_IDLE) { sessions.delete(key); return null; }
|
||||
// FED R6 (design 3.1 D5): a user's grant is read again on every request, so a user revoked,
|
||||
// removed or expired since sign-in has no session from the next request on.
|
||||
- if (s.user !== undefined && !users?.check(s)) {
|
||||
+ if (s.user !== undefined && false) {
|
||||
sessions.delete(key);
|
||||
refuse('user-revoked', s.provider, ` user=${JSON.stringify(s.user)}`);
|
||||
return null;
|
||||
@@ -0,0 +1,214 @@
|
||||
TAP version 13
|
||||
# Subtest: R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
ok 1 - R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
---
|
||||
duration_ms: 99.922321
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
# Subtest: a listed user with no grant
|
||||
ok 1 - a listed user with no grant
|
||||
---
|
||||
duration_ms: 92.761557
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a grant in another business
|
||||
ok 2 - a grant in another business
|
||||
---
|
||||
duration_ms: 92.20228
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: an expired grant
|
||||
ok 3 - an expired grant
|
||||
---
|
||||
duration_ms: 63.082847
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: another user's grant
|
||||
ok 4 - another user's grant
|
||||
---
|
||||
duration_ms: 71.423611
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a sub in no file
|
||||
ok 5 - a sub in no file
|
||||
---
|
||||
duration_ms: 53.437758
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a file the loader refuses
|
||||
ok 6 - a file the loader refuses
|
||||
---
|
||||
duration_ms: 88.763112
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
ok 2 - R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
---
|
||||
duration_ms: 462.628046
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
# Subtest: grant removed
|
||||
not ok 1 - grant removed
|
||||
---
|
||||
duration_ms: 41.439982
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:199:13'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'access-revoked'
|
||||
- 'sign-in required'
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 'sign-in required'
|
||||
actual: 'access-revoked'
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:207:14)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:199:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: subject removed
|
||||
not ok 2 - subject removed
|
||||
---
|
||||
duration_ms: 77.978266
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:199:13'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
subject removed: {"rows":[{"id":"76c13564-e781-4b83-ad16-cd1f57b1a97f","at":"2026-10-23T06:34:07.869Z","name":"shared","repo":null},{"id":"3019668b-2d1d-47d9-a523-5e76b50de295","at":"2026-10-23T06:34:07.870Z","name":"private","repo":null}],"at":"2026-10-23T06:34:07.876Z"}
|
||||
|
||||
200 !== 401
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 401
|
||||
actual: 200
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:206:14)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:199:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: grant expired
|
||||
not ok 3 - grant expired
|
||||
---
|
||||
duration_ms: 100.74638
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:199:13'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'access-revoked'
|
||||
- 'sign-in required'
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 'sign-in required'
|
||||
actual: 'access-revoked'
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:207:14)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:199:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: file now refused
|
||||
not ok 4 - file now refused
|
||||
---
|
||||
duration_ms: 64.871699
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:199:13'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'access-revoked'
|
||||
- 'sign-in required'
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 'sign-in required'
|
||||
actual: 'access-revoked'
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:207:14)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:199:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
1..4
|
||||
not ok 3 - R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
---
|
||||
duration_ms: 285.771094
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:192:1'
|
||||
failureType: 'subtestsFailed'
|
||||
error: '4 subtests failed'
|
||||
code: 'ERR_TEST_FAILURE'
|
||||
...
|
||||
# Subtest: R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
ok 4 - R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
---
|
||||
duration_ms: 120.543137
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
ok 5 - R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
---
|
||||
duration_ms: 101.715208
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
ok 6 - R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
---
|
||||
duration_ms: 44.655459
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G6 the user page holds the notice word for word
|
||||
ok 7 - R6 G6 the user page holds the notice word for word
|
||||
---
|
||||
duration_ms: 78.519066
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G7 the hub label and plain peer links render from config on both pages
|
||||
ok 8 - R6 G7 the hub label and plain peer links render from config on both pages
|
||||
---
|
||||
duration_ms: 747.453516
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
ok 9 - R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
---
|
||||
duration_ms: 37.353078
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 19
|
||||
# suites 0
|
||||
# pass 14
|
||||
# fail 5
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 2105.529578
|
||||
|
||||
# stderr
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/packages/webui/src/public/user.html b/packages/webui/src/public/user.html
|
||||
index 0397597af..39e937f10 100644
|
||||
--- a/packages/webui/src/public/user.html
|
||||
+++ b/packages/webui/src/public/user.html
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="cmd-right"><form id="sign-out" method="post" action="/auth/logout"><button class="btn" type="submit">Sign out</button></form></div></header>
|
||||
<main id="main" class="content" tabindex="-1">
|
||||
<div id="signed-out" class="banner ref" role="alert" tabindex="-1" hidden>You are signed out. <a href="/">Sign in again</a>.</div>
|
||||
-<p id="user-notice" class="banner ref">The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your messages.</p>
|
||||
+<p id="user-notice" class="banner ref">The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your replies.</p>
|
||||
<div id="chat"></div>
|
||||
</main>
|
||||
<footer class="foot"><span id="footer">Mosaic Stack</span></footer>
|
||||
@@ -0,0 +1,170 @@
|
||||
TAP version 13
|
||||
# Subtest: R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
ok 1 - R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
---
|
||||
duration_ms: 99.908034
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
# Subtest: a listed user with no grant
|
||||
ok 1 - a listed user with no grant
|
||||
---
|
||||
duration_ms: 38.222854
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a grant in another business
|
||||
ok 2 - a grant in another business
|
||||
---
|
||||
duration_ms: 35.871094
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: an expired grant
|
||||
ok 3 - an expired grant
|
||||
---
|
||||
duration_ms: 62.921189
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: another user's grant
|
||||
ok 4 - another user's grant
|
||||
---
|
||||
duration_ms: 48.162704
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a sub in no file
|
||||
ok 5 - a sub in no file
|
||||
---
|
||||
duration_ms: 49.598628
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a file the loader refuses
|
||||
ok 6 - a file the loader refuses
|
||||
---
|
||||
duration_ms: 41.671478
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
ok 2 - R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
---
|
||||
duration_ms: 277.403073
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
# Subtest: grant removed
|
||||
ok 1 - grant removed
|
||||
---
|
||||
duration_ms: 63.714717
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: subject removed
|
||||
ok 2 - subject removed
|
||||
---
|
||||
duration_ms: 46.793543
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: grant expired
|
||||
ok 3 - grant expired
|
||||
---
|
||||
duration_ms: 103.148596
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: file now refused
|
||||
ok 4 - file now refused
|
||||
---
|
||||
duration_ms: 62.737012
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
ok 3 - R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
---
|
||||
duration_ms: 276.979306
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
ok 4 - R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
---
|
||||
duration_ms: 50.94161
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
ok 5 - R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
---
|
||||
duration_ms: 139.368171
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
ok 6 - R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
---
|
||||
duration_ms: 69.328685
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G6 the user page holds the notice word for word
|
||||
not ok 7 - R6 G6 the user page holds the notice word for word
|
||||
---
|
||||
duration_ms: 1.0549
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:306:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your replies."
|
||||
- "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your messages."
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your messages."
|
||||
actual: "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your replies."
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:310:10)
|
||||
Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
Test.run (node:internal/test_runner/test:1402:25)
|
||||
Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
Test.run (node:internal/test_runner/test:1467:12)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: R6 G7 the hub label and plain peer links render from config on both pages
|
||||
not ok 8 - R6 G7 the hub label and plain peer links render from config on both pages
|
||||
---
|
||||
duration_ms: 659.67841
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:333:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your replies."
|
||||
- "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your messages."
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your messages."
|
||||
actual: "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your replies."
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:352:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
ok 9 - R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
---
|
||||
duration_ms: 39.006102
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 19
|
||||
# suites 0
|
||||
# pass 17
|
||||
# fail 2
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 1740.402277
|
||||
|
||||
# stderr
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/packages/cli/src/access.mjs b/packages/cli/src/access.mjs
|
||||
index 9f1134347..cd3c503ce 100644
|
||||
--- a/packages/cli/src/access.mjs
|
||||
+++ b/packages/cli/src/access.mjs
|
||||
@@ -59,7 +59,7 @@ export function accessContextFor(system, env) {
|
||||
businesses[id] = { human: doc.human, instances: Object.keys(doc.roles) };
|
||||
}
|
||||
}
|
||||
- const peers = (system.federation?.peers ?? []).map((p) => p.id);
|
||||
+ const peers = [];
|
||||
return { providers: providers.map((p) => p.id), peers, businesses, ssoSubjects };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
TAP version 13
|
||||
# Subtest: R6 a configured peer id validates as a user's home and an unconfigured one refuses
|
||||
not ok 1 - R6 a configured peer id validates as a user's home and an unconfigured one refuses
|
||||
---
|
||||
duration_ms: 8.951306
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/access-boot.test.mjs:31:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
+ []
|
||||
- [
|
||||
- 'factory'
|
||||
- ]
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0: 'factory'
|
||||
actual:
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/access-boot.test.mjs:35:10)
|
||||
Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
Test.run (node:internal/test_runner/test:1402:25)
|
||||
Test.start (node:internal/test_runner/test:1262:17)
|
||||
startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17)
|
||||
...
|
||||
# Subtest: R6 B1 a boot with access.json binds a granted user, whose home is a configured peer
|
||||
not ok 2 - R6 B1 a boot with access.json binds a granted user, whose home is a configured peer
|
||||
---
|
||||
duration_ms: 80.96728
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/access-boot.test.mjs:68:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
+ []
|
||||
- [
|
||||
- 'factory'
|
||||
- ]
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0: 'factory'
|
||||
actual:
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/access-boot.test.mjs:71:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: R6 B2 a boot without access.json has no access and binds no user
|
||||
ok 3 - R6 B2 a boot without access.json has no access and binds no user
|
||||
---
|
||||
duration_ms: 62.765959
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 B3 a boot with a file R1's loader refuses still boots, binds no user and says so once
|
||||
ok 4 - R6 B3 a boot with a file R1's loader refuses still boots, binds no user and says so once
|
||||
---
|
||||
duration_ms: 180.02261
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
# tests 4
|
||||
# suites 0
|
||||
# pass 2
|
||||
# fail 2
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 449.373472
|
||||
|
||||
# stderr
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/packages/webui/src/access.mjs b/packages/webui/src/access.mjs
|
||||
index 7446814da..721a09207 100644
|
||||
--- a/packages/webui/src/access.mjs
|
||||
+++ b/packages/webui/src/access.mjs
|
||||
@@ -14,7 +14,7 @@ export function consoleAccess({ load, business }) {
|
||||
try {
|
||||
const doc = load(), b = business();
|
||||
const user = doc.users.find(u => u.subjects.some(s => s.provider === provider && s.sub === sub));
|
||||
- return user && live(doc, user.id, b) ? user.id : null;
|
||||
+ return user ? user.id : null;
|
||||
} catch { return null; }
|
||||
};
|
||||
return Object.freeze({
|
||||
@@ -0,0 +1,254 @@
|
||||
TAP version 13
|
||||
# Subtest: R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
ok 1 - R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
---
|
||||
duration_ms: 137.858619
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
# Subtest: a listed user with no grant
|
||||
not ok 1 - a listed user with no grant
|
||||
---
|
||||
duration_ms: 69.429562
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:173:13'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
a listed user with no grant
|
||||
|
||||
303 !== 403
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 403
|
||||
actual: 303
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:176:14)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:173:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: a grant in another business
|
||||
not ok 2 - a grant in another business
|
||||
---
|
||||
duration_ms: 49.204153
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:173:13'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
a grant in another business
|
||||
|
||||
303 !== 403
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 403
|
||||
actual: 303
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:176:14)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:173:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: an expired grant
|
||||
not ok 3 - an expired grant
|
||||
---
|
||||
duration_ms: 120.517717
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:173:13'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
an expired grant
|
||||
|
||||
303 !== 403
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 403
|
||||
actual: 303
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:176:14)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:173:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: another user's grant
|
||||
not ok 4 - another user's grant
|
||||
---
|
||||
duration_ms: 77.385678
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:173:13'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
another user's grant
|
||||
|
||||
303 !== 403
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 403
|
||||
actual: 303
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:176:14)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:173:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: a sub in no file
|
||||
ok 5 - a sub in no file
|
||||
---
|
||||
duration_ms: 40.652981
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a file the loader refuses
|
||||
ok 6 - a file the loader refuses
|
||||
---
|
||||
duration_ms: 74.92896
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
not ok 2 - R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
---
|
||||
duration_ms: 433.064399
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:164:1'
|
||||
failureType: 'subtestsFailed'
|
||||
error: '4 subtests failed'
|
||||
code: 'ERR_TEST_FAILURE'
|
||||
...
|
||||
# Subtest: R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
# Subtest: grant removed
|
||||
not ok 1 - grant removed
|
||||
---
|
||||
duration_ms: 64.630321
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:199:13'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'access-revoked'
|
||||
- 'sign-in required'
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 'sign-in required'
|
||||
actual: 'access-revoked'
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:207:14)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:199:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: subject removed
|
||||
ok 2 - subject removed
|
||||
---
|
||||
duration_ms: 50.619797
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: grant expired
|
||||
not ok 3 - grant expired
|
||||
---
|
||||
duration_ms: 123.640684
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:199:13'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'access-revoked'
|
||||
- 'sign-in required'
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 'sign-in required'
|
||||
actual: 'access-revoked'
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:207:14)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:199:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: file now refused
|
||||
ok 4 - file now refused
|
||||
---
|
||||
duration_ms: 78.007438
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
not ok 3 - R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
---
|
||||
duration_ms: 317.562485
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:192:1'
|
||||
failureType: 'subtestsFailed'
|
||||
error: '2 subtests failed'
|
||||
code: 'ERR_TEST_FAILURE'
|
||||
...
|
||||
# Subtest: R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
ok 4 - R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
---
|
||||
duration_ms: 72.835794
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
ok 5 - R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
---
|
||||
duration_ms: 105.145618
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
ok 6 - R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
---
|
||||
duration_ms: 59.180853
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G6 the user page holds the notice word for word
|
||||
ok 7 - R6 G6 the user page holds the notice word for word
|
||||
---
|
||||
duration_ms: 37.327463
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G7 the hub label and plain peer links render from config on both pages
|
||||
ok 8 - R6 G7 the hub label and plain peer links render from config on both pages
|
||||
---
|
||||
duration_ms: 775.474923
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
ok 9 - R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
---
|
||||
duration_ms: 39.045355
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 19
|
||||
# suites 0
|
||||
# pass 11
|
||||
# fail 8
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 2107.663356
|
||||
|
||||
# stderr
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/packages/webui/src/user-bus.mjs b/packages/webui/src/user-bus.mjs
|
||||
index 29c6302bf..9a4b344ee 100644
|
||||
--- a/packages/webui/src/user-bus.mjs
|
||||
+++ b/packages/webui/src/user-bus.mjs
|
||||
@@ -18,7 +18,7 @@ export function userCall({ socket, business, user, env = process.env, timeoutMs
|
||||
return (verb, args = {}) => new Promise((resolve, reject) => {
|
||||
let child;
|
||||
try {
|
||||
- child = spawn(process.execPath, [USER_CLI, socket], { env: { ...rest, MOSAIC_BUS_USER_NONCE: randomBytes(32).toString('hex') }, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
+ child = spawn(process.execPath, [USER_CLI, socket], { env: { ...env, MOSAIC_BUS_USER_NONCE: randomBytes(32).toString('hex') }, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
} catch { return reject(new BusReadError('outcome-unknown', 'the bus transport did not start')); }
|
||||
const out = [], err = [];
|
||||
let size = 0, settled = false;
|
||||
@@ -0,0 +1,145 @@
|
||||
TAP version 13
|
||||
# Subtest: R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
ok 1 - R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
---
|
||||
duration_ms: 122.653189
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
# Subtest: a listed user with no grant
|
||||
ok 1 - a listed user with no grant
|
||||
---
|
||||
duration_ms: 56.174331
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a grant in another business
|
||||
ok 2 - a grant in another business
|
||||
---
|
||||
duration_ms: 44.586843
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: an expired grant
|
||||
ok 3 - an expired grant
|
||||
---
|
||||
duration_ms: 95.990798
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: another user's grant
|
||||
ok 4 - another user's grant
|
||||
---
|
||||
duration_ms: 39.137341
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a sub in no file
|
||||
ok 5 - a sub in no file
|
||||
---
|
||||
duration_ms: 93.933879
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a file the loader refuses
|
||||
ok 6 - a file the loader refuses
|
||||
---
|
||||
duration_ms: 46.821824
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
ok 2 - R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
---
|
||||
duration_ms: 377.544309
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
# Subtest: grant removed
|
||||
ok 1 - grant removed
|
||||
---
|
||||
duration_ms: 54.902614
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: subject removed
|
||||
ok 2 - subject removed
|
||||
---
|
||||
duration_ms: 42.309062
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: grant expired
|
||||
ok 3 - grant expired
|
||||
---
|
||||
duration_ms: 95.824061
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: file now refused
|
||||
ok 4 - file now refused
|
||||
---
|
||||
duration_ms: 55.098887
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
ok 3 - R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
---
|
||||
duration_ms: 248.867763
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
ok 4 - R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
---
|
||||
duration_ms: 52.680841
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
ok 5 - R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
---
|
||||
duration_ms: 82.486074
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
ok 6 - R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
---
|
||||
duration_ms: 80.801522
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G6 the user page holds the notice word for word
|
||||
ok 7 - R6 G6 the user page holds the notice word for word
|
||||
---
|
||||
duration_ms: 107.231563
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G7 the hub label and plain peer links render from config on both pages
|
||||
ok 8 - R6 G7 the hub label and plain peer links render from config on both pages
|
||||
---
|
||||
duration_ms: 762.991755
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
not ok 9 - R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
---
|
||||
duration_ms: 42.199074
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:359:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
true !== false
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: false
|
||||
actual: true
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:388:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
1..9
|
||||
# tests 19
|
||||
# suites 0
|
||||
# pass 18
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 2018.947231
|
||||
|
||||
# stderr
|
||||
@@ -0,0 +1,12 @@
|
||||
diff --git a/packages/cli/src/config.mjs b/packages/cli/src/config.mjs
|
||||
index 6de5fdc6c..7f7c45bcd 100644
|
||||
--- a/packages/cli/src/config.mjs
|
||||
+++ b/packages/cli/src/config.mjs
|
||||
@@ -133,7 +133,6 @@ export function bootConfig({ system, businessId, env = process.env, warn = () =>
|
||||
const tracker = trackerFor(vars, b, warn);
|
||||
if (tracker) config.trackers = { [b.id]: tracker };
|
||||
const access = accessFor(system, env, warn);
|
||||
- if (access) config.access = access;
|
||||
return config;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
TAP version 13
|
||||
# Subtest: R6 a configured peer id validates as a user's home and an unconfigured one refuses
|
||||
ok 1 - R6 a configured peer id validates as a user's home and an unconfigured one refuses
|
||||
---
|
||||
duration_ms: 8.577936
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 B1 a boot with access.json binds a granted user, whose home is a configured peer
|
||||
not ok 2 - R6 B1 a boot with access.json binds a granted user, whose home is a configured peer
|
||||
---
|
||||
duration_ms: 74.086833
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/access-boot.test.mjs:68:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: "Cannot read properties of undefined (reading 'file')"
|
||||
code: 'ERR_TEST_FAILURE'
|
||||
name: 'TypeError'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/access-boot.test.mjs:70:32)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: R6 B2 a boot without access.json has no access and binds no user
|
||||
ok 3 - R6 B2 a boot without access.json has no access and binds no user
|
||||
---
|
||||
duration_ms: 60.651858
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 B3 a boot with a file R1's loader refuses still boots, binds no user and says so once
|
||||
not ok 4 - R6 B3 a boot with a file R1's loader refuses still boots, binds no user and says so once
|
||||
---
|
||||
duration_ms: 61.005239
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/access-boot.test.mjs:85:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: "Cannot read properties of undefined (reading 'file')"
|
||||
code: 'ERR_TEST_FAILURE'
|
||||
name: 'TypeError'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/access-boot.test.mjs:92:34)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
1..4
|
||||
# tests 4
|
||||
# suites 0
|
||||
# pass 2
|
||||
# fail 2
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 316.211304
|
||||
|
||||
# stderr
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/packages/webui/src/chat.mjs b/packages/webui/src/chat.mjs
|
||||
index 1b0a41dce..2a1c203f6 100644
|
||||
--- a/packages/webui/src/chat.mjs
|
||||
+++ b/packages/webui/src/chat.mjs
|
||||
@@ -13,7 +13,7 @@ import { BusReadError } from './bus.mjs';
|
||||
export function createChat({ call, owner = null }) {
|
||||
if (typeof call !== 'function') throw new TypeError('chat needs a bus transport function');
|
||||
if (owner !== null && typeof owner !== 'function') throw new TypeError('chat owner must be a bus transport function');
|
||||
- const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => verb === 'plugins' ? owner('plugins', {}) : call(verb, args) });
|
||||
+ const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => verb === 'plugins' ? owner('plugins', {}) : verb === 'thread.list' ? owner(verb, args) : call(verb, args) });
|
||||
const channel = async (verb, args) => {
|
||||
try { return await dispatch(verb, args); }
|
||||
catch (err) {
|
||||
@@ -0,0 +1,156 @@
|
||||
TAP version 13
|
||||
# Subtest: R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
not ok 1 - R6 G1 a granted user signs in, gets the user page and reaches the broker as itself
|
||||
---
|
||||
duration_ms: 129.678949
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:130:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: "Cannot read properties of undefined (reading 'map')"
|
||||
code: 'ERR_TEST_FAILURE'
|
||||
name: 'TypeError'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:152:38)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
|
||||
...
|
||||
# Subtest: R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
# Subtest: a listed user with no grant
|
||||
ok 1 - a listed user with no grant
|
||||
---
|
||||
duration_ms: 102.308353
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a grant in another business
|
||||
ok 2 - a grant in another business
|
||||
---
|
||||
duration_ms: 53.531302
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: an expired grant
|
||||
ok 3 - an expired grant
|
||||
---
|
||||
duration_ms: 44.641678
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: another user's grant
|
||||
ok 4 - another user's grant
|
||||
---
|
||||
duration_ms: 68.283139
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a sub in no file
|
||||
ok 5 - a sub in no file
|
||||
---
|
||||
duration_ms: 90.290255
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a file the loader refuses
|
||||
ok 6 - a file the loader refuses
|
||||
---
|
||||
duration_ms: 41.803535
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
ok 2 - R6 G2 a subject with no live grant in this business is refused subject-unknown
|
||||
---
|
||||
duration_ms: 401.809515
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
# Subtest: grant removed
|
||||
ok 1 - grant removed
|
||||
---
|
||||
duration_ms: 77.095691
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: subject removed
|
||||
ok 2 - subject removed
|
||||
---
|
||||
duration_ms: 90.099697
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: grant expired
|
||||
ok 3 - grant expired
|
||||
---
|
||||
duration_ms: 74.152955
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: file now refused
|
||||
ok 4 - file now refused
|
||||
---
|
||||
duration_ms: 55.749108
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
ok 3 - R6 G3 a revoke counts on the next request: 401 and the session is gone
|
||||
---
|
||||
duration_ms: 297.743475
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
ok 4 - R6 G4 every owner route answers 403 owner-only before any bus call
|
||||
---
|
||||
duration_ms: 70.614829
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
ok 5 - R6 G5 the bus refuses every owner verb over the user transport on its own
|
||||
---
|
||||
duration_ms: 79.166288
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
not ok 6 - R6 G9 a user chat sends only the plugins read over the owner transport
|
||||
---
|
||||
duration_ms: 75.311089
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:279:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
GET /api/chat/threads?project=ec96b734-6752-4afc-bc54-58a188b804a1: {"error":"invalid-response","message":"Chat operation refused (invalid-response)."}
|
||||
|
||||
502 !== 200
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 200
|
||||
actual: 502
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
go (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:285:12)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/webui/tests/granted-user.test.mjs:291:3)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: R6 G6 the user page holds the notice word for word
|
||||
ok 7 - R6 G6 the user page holds the notice word for word
|
||||
---
|
||||
duration_ms: 84.352581
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G7 the hub label and plain peer links render from config on both pages
|
||||
ok 8 - R6 G7 the hub label and plain peer links render from config on both pages
|
||||
---
|
||||
duration_ms: 775.624362
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
ok 9 - R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request
|
||||
---
|
||||
duration_ms: 40.63858
|
||||
type: 'test'
|
||||
...
|
||||
1..9
|
||||
# tests 19
|
||||
# suites 0
|
||||
# pass 17
|
||||
# fail 2
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 2083.017773
|
||||
|
||||
# stderr
|
||||
@@ -0,0 +1,12 @@
|
||||
diff --git a/packages/cli/src/config.mjs b/packages/cli/src/config.mjs
|
||||
index 6de5fdc6c..ff3733104 100644
|
||||
--- a/packages/cli/src/config.mjs
|
||||
+++ b/packages/cli/src/config.mjs
|
||||
@@ -109,7 +109,6 @@ function accessFor(system, env, warn) {
|
||||
loadAccess(file, context);
|
||||
} catch (e) {
|
||||
const code = e instanceof BusinessError && e.exitCode !== 4 ? "access-invalid" : "access-unreadable";
|
||||
- warn(`access file refused at boot (${code}), no granted user binds until it is fixed: ${file}`);
|
||||
}
|
||||
return { file, context };
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
TAP version 13
|
||||
# Subtest: R6 a configured peer id validates as a user's home and an unconfigured one refuses
|
||||
ok 1 - R6 a configured peer id validates as a user's home and an unconfigured one refuses
|
||||
---
|
||||
duration_ms: 9.41985
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 B1 a boot with access.json binds a granted user, whose home is a configured peer
|
||||
ok 2 - R6 B1 a boot with access.json binds a granted user, whose home is a configured peer
|
||||
---
|
||||
duration_ms: 74.90319
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 B2 a boot without access.json has no access and binds no user
|
||||
ok 3 - R6 B2 a boot without access.json has no access and binds no user
|
||||
---
|
||||
duration_ms: 62.418469
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: R6 B3 a boot with a file R1's loader refuses still boots, binds no user and says so once
|
||||
not ok 4 - R6 B3 a boot with a file R1's loader refuses still boots, binds no user and says so once
|
||||
---
|
||||
duration_ms: 65.709307
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/access-boot.test.mjs:85:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
not JSON
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
'business acme sets tracker.baseUrl, but no project file sets tracker.project; the broker gets no task verbs',
|
||||
- 'access file refused at boot (access-invalid), no granted user binds until it is fixed: /mnt/storage/scratch/tmp/gd3JQ8r/mosaic-access-boot-UPOnK2/config/access.json'
|
||||
]
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0: 'business acme sets tracker.baseUrl, but no project file sets tracker.project; the broker gets no task verbs'
|
||||
1: 'access file refused at boot (access-invalid), no granted user binds until it is fixed: /mnt/storage/scratch/tmp/gd3JQ8r/mosaic-access-boot-UPOnK2/config/access.json'
|
||||
actual:
|
||||
0: 'business acme sets tracker.baseUrl, but no project file sets tracker.project; the broker gets no task verbs'
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/launchpad-r294/wt/packages/cli/tests/access-boot.test.mjs:94:12)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
1..4
|
||||
# tests 4
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 328.382551
|
||||
|
||||
# stderr
|
||||
@@ -0,0 +1,28 @@
|
||||
npm-root rc=0 seconds=1.926 limit=900 06:20:55Z
|
||||
npm-webui rc=0 seconds=0.452 limit=900 06:20:55Z
|
||||
webui rc=0 seconds=57.861 limit=600 06:21:53Z
|
||||
cli rc=0 seconds=600.132 limit=2100 06:31:53Z
|
||||
bus rc=0 seconds=24.429 limit=900 06:32:18Z
|
||||
test-auth rc=0 seconds=0.781 limit=126 06:32:19Z
|
||||
test-conductor rc=0 seconds=6.825 limit=145 06:32:25Z
|
||||
test-config rc=0 seconds=2.482 limit=130 06:32:28Z
|
||||
test-discord rc=0 seconds=7.314 limit=147 06:32:35Z
|
||||
test-extension-package rc=0 seconds=1.229 limit=130 06:32:37Z
|
||||
test-foundation rc=0 seconds=39.067 limit=269 06:33:16Z
|
||||
test-land-work rc=0 seconds=11.848 limit=171 06:33:27Z
|
||||
test-queue rc=0 seconds=20.432 limit=226 06:33:48Z
|
||||
test-release rc=0 seconds=2.663 limit=157 06:33:51Z
|
||||
test-task rc=0 seconds=13.923 limit=231 06:34:04Z
|
||||
SUITES GREEN 06:34:04Z
|
||||
M1 rc=1 seconds=1.9 killed=True red=['R6 G4 every owner route answers 403 owner-only before any bus call']
|
||||
M2 rc=1 seconds=2.1 killed=True red=['R6 G3 a revoke counts on the next request: 401 and the session is gone']
|
||||
M3 rc=1 seconds=1.8 killed=True red=['R6 G6 the user page holds the notice word for word', 'R6 G7 the hub label and plain peer links render from config on both pages']
|
||||
M4 rc=1 seconds=0.5 killed=True red=["R6 a configured peer id validates as a user's home and an unconfigured one refuses", 'R6 B1 a boot with access.json binds a granted user, whose home is a configured peer']
|
||||
M5 rc=1 seconds=2.1 killed=True red=['R6 G2 a subject with no live grant in this business is refused subject-unknown', 'R6 G3 a revoke counts on the next request: 401 and the session is gone']
|
||||
M6 rc=1 seconds=2.0 killed=True red=['R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request']
|
||||
M7 rc=1 seconds=0.3 killed=True red=['R6 B1 a boot with access.json binds a granted user, whose home is a configured peer', "R6 B3 a boot with a file R1's loader refuses still boots, binds no user and says so once"]
|
||||
M8 rc=1 seconds=2.1 killed=True red=['R6 G1 a granted user signs in, gets the user page and reaches the broker as itself', 'R6 G9 a user chat sends only the plugins read over the owner transport']
|
||||
M9 rc=1 seconds=0.4 killed=True red=["R6 B3 a boot with a file R1's loader refuses still boots, binds no user and says so once"]
|
||||
M10 rc=1 seconds=2.0 killed=True red=['R6 G9 a user chat sends only the plugins read over the owner transport']
|
||||
M11 rc=1 seconds=2.0 killed=True red=['R6 G9 a user chat sends only the plugins read over the owner transport']
|
||||
MUTANTS rc=0 06:34:22Z
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 294 gate run 3, sage-3199: npm ci (root and packages/webui), suites whole, every
|
||||
# scripts/test-*.sh, then the mutants. Stops at the first red. FROM=<step> resumes at a step
|
||||
# (the mos-1411 single cli rerun uses FROM=cli TAG=-rerun).
|
||||
set -u -o pipefail
|
||||
WT=/mnt/storage/scratch/launchpad-r294/wt
|
||||
OUT=/mnt/storage/scratch/launchpad-r294/run4
|
||||
FROM=${FROM:-npm-root}; TAG=${TAG:-}
|
||||
export TMPDIR=$(mktemp -d /mnt/storage/scratch/tmp/gXXXXXX)
|
||||
echo "start $(date -u +%H:%M:%SZ) FROM=$FROM TAG=$TAG TMPDIR $TMPDIR head $(git -C $WT rev-parse HEAD)" >> $OUT/summary.txt
|
||||
[ -z "$(git -C $WT status --porcelain)" ] || { echo "STOP worktree not clean" | tee -a $OUT/summary.txt; exit 9; }
|
||||
on=0
|
||||
step() { # name limit dir cmd...
|
||||
local name=$1 limit=$2 dir=$3; shift 3
|
||||
[ $on = 1 ] || [ $name = $FROM ] || return 0; on=1
|
||||
local t0=$(date +%s%N)
|
||||
( cd $dir && timeout -k 10 $limit "$@" ) > $OUT/$name$TAG.log 2>&1
|
||||
local rc=$?
|
||||
local ms=$(( ($(date +%s%N) - t0) / 1000000 )); local dt=$((ms/1000)).$(printf %03d $((ms%1000)))
|
||||
echo "$name$TAG rc=$rc seconds=$dt limit=$limit $(date -u +%H:%M:%SZ)" | tee -a $OUT/summary.txt
|
||||
[ $rc -eq 0 ] || { echo "STOP at $name$TAG" | tee -a $OUT/summary.txt; exit $rc; }
|
||||
}
|
||||
step npm-root 900 $WT npm ci --ignore-scripts
|
||||
step npm-webui 900 $WT/packages/webui npm ci --ignore-scripts
|
||||
step webui 600 $WT/packages/webui node --test tests/
|
||||
step cli 2100 $WT/packages/cli node --test tests/*.test.mjs
|
||||
step bus 900 $WT/packages/bus node --test tests/*.test.mjs
|
||||
declare -A L=([auth]=126 [conductor]=145 [config]=130 [discord]=147 [extension-package]=130 [foundation]=269 [land-work]=171 [queue]=226 [release]=157 [task]=231)
|
||||
for s in auth conductor config discord extension-package foundation land-work queue release task; do
|
||||
step test-$s ${L[$s]} $WT bash scripts/test-$s.sh
|
||||
done
|
||||
[ -z "$(git -C $WT status --porcelain)" ] || { echo "STOP worktree dirty after the scripts" | tee -a $OUT/summary.txt; git -C $WT status --porcelain >> $OUT/summary.txt; exit 9; }
|
||||
echo "SUITES GREEN $(date -u +%H:%M:%SZ)" | tee -a $OUT/summary.txt
|
||||
python3 -B $OUT/mutants.py
|
||||
rc=$?; echo "MUTANTS rc=$rc $(date -u +%H:%M:%SZ)" | tee -a $OUT/summary.txt; exit $rc
|
||||
@@ -0,0 +1,153 @@
|
||||
[
|
||||
{
|
||||
"mutant": "M1",
|
||||
"rc": 1,
|
||||
"seconds": 1.9,
|
||||
"killed": true,
|
||||
"red": [
|
||||
"R6 G4 every owner route answers 403 owner-only before any bus call"
|
||||
],
|
||||
"killers": [
|
||||
"R6 G4"
|
||||
],
|
||||
"greens": [
|
||||
"R6 G5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mutant": "M2",
|
||||
"rc": 1,
|
||||
"seconds": 2.1,
|
||||
"killed": true,
|
||||
"red": [
|
||||
"R6 G3 a revoke counts on the next request: 401 and the session is gone"
|
||||
],
|
||||
"killers": [
|
||||
"R6 G3"
|
||||
],
|
||||
"greens": []
|
||||
},
|
||||
{
|
||||
"mutant": "M3",
|
||||
"rc": 1,
|
||||
"seconds": 1.8,
|
||||
"killed": true,
|
||||
"red": [
|
||||
"R6 G6 the user page holds the notice word for word",
|
||||
"R6 G7 the hub label and plain peer links render from config on both pages"
|
||||
],
|
||||
"killers": [
|
||||
"R6 G6"
|
||||
],
|
||||
"greens": []
|
||||
},
|
||||
{
|
||||
"mutant": "M4",
|
||||
"rc": 1,
|
||||
"seconds": 0.5,
|
||||
"killed": true,
|
||||
"red": [
|
||||
"R6 a configured peer id validates as a user's home and an unconfigured one refuses",
|
||||
"R6 B1 a boot with access.json binds a granted user, whose home is a configured peer"
|
||||
],
|
||||
"killers": [
|
||||
"R6 a configured peer id",
|
||||
"R6 B1"
|
||||
],
|
||||
"greens": []
|
||||
},
|
||||
{
|
||||
"mutant": "M5",
|
||||
"rc": 1,
|
||||
"seconds": 2.1,
|
||||
"killed": true,
|
||||
"red": [
|
||||
"R6 G2 a subject with no live grant in this business is refused subject-unknown",
|
||||
"R6 G3 a revoke counts on the next request: 401 and the session is gone"
|
||||
],
|
||||
"killers": [
|
||||
"R6 G2"
|
||||
],
|
||||
"greens": []
|
||||
},
|
||||
{
|
||||
"mutant": "M6",
|
||||
"rc": 1,
|
||||
"seconds": 2.0,
|
||||
"killed": true,
|
||||
"red": [
|
||||
"R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request"
|
||||
],
|
||||
"killers": [
|
||||
"R6 G8"
|
||||
],
|
||||
"greens": []
|
||||
},
|
||||
{
|
||||
"mutant": "M7",
|
||||
"rc": 1,
|
||||
"seconds": 0.3,
|
||||
"killed": true,
|
||||
"red": [
|
||||
"R6 B1 a boot with access.json binds a granted user, whose home is a configured peer",
|
||||
"R6 B3 a boot with a file R1's loader refuses still boots, binds no user and says so once"
|
||||
],
|
||||
"killers": [
|
||||
"R6 B1"
|
||||
],
|
||||
"greens": []
|
||||
},
|
||||
{
|
||||
"mutant": "M8",
|
||||
"rc": 1,
|
||||
"seconds": 2.1,
|
||||
"killed": true,
|
||||
"red": [
|
||||
"R6 G1 a granted user signs in, gets the user page and reaches the broker as itself",
|
||||
"R6 G9 a user chat sends only the plugins read over the owner transport"
|
||||
],
|
||||
"killers": [
|
||||
"R6 G9"
|
||||
],
|
||||
"greens": []
|
||||
},
|
||||
{
|
||||
"mutant": "M9",
|
||||
"rc": 1,
|
||||
"seconds": 0.4,
|
||||
"killed": true,
|
||||
"red": [
|
||||
"R6 B3 a boot with a file R1's loader refuses still boots, binds no user and says so once"
|
||||
],
|
||||
"killers": [
|
||||
"R6 B3"
|
||||
],
|
||||
"greens": []
|
||||
},
|
||||
{
|
||||
"mutant": "M10",
|
||||
"rc": 1,
|
||||
"seconds": 2.0,
|
||||
"killed": true,
|
||||
"red": [
|
||||
"R6 G9 a user chat sends only the plugins read over the owner transport"
|
||||
],
|
||||
"killers": [
|
||||
"R6 G9"
|
||||
],
|
||||
"greens": []
|
||||
},
|
||||
{
|
||||
"mutant": "M11",
|
||||
"rc": 1,
|
||||
"seconds": 2.0,
|
||||
"killed": true,
|
||||
"red": [
|
||||
"R6 G9 a user chat sends only the plugins read over the owner transport"
|
||||
],
|
||||
"killers": [
|
||||
"R6 G9"
|
||||
],
|
||||
"greens": []
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
# Row 294 mutants M1-M11 (BUILD.md "Mutants"). Each mutant edits the worktree (every old text must
|
||||
# match exactly once), runs its killing file alone under timeout -k 10 300 with the TAP reporter,
|
||||
# then restores with git checkout and checks the tree is clean. Killed = the named killing test(s)
|
||||
# report "not ok". M1 also needs G5 "ok" in the same run (Duckworth C1).
|
||||
import json, os, re, subprocess, sys, time
|
||||
|
||||
WT = '/mnt/storage/scratch/launchpad-r294/wt'
|
||||
OUT = '/mnt/storage/scratch/launchpad-r294/run4'
|
||||
GU = ('packages/webui', 'tests/granted-user.test.mjs')
|
||||
AB = ('packages/cli', 'tests/access-boot.test.mjs')
|
||||
|
||||
CHAT_CALL = "verb === 'plugins' ? owner('plugins', {}) : call(verb, args)"
|
||||
MUTANTS = [
|
||||
('M1', [('packages/webui/src/serve.mjs', "if (!userRoute(path, req.method)) return json(res, 403, { error: 'owner-only'",
|
||||
"if (false) return json(res, 403, { error: 'owner-only'")], GU, ['R6 G4'], ['R6 G5']),
|
||||
('M2', [('packages/webui/src/sso.mjs', "if (s.user !== undefined && !users?.check(s)) {", "if (s.user !== undefined && false) {")], GU, ['R6 G3'], []),
|
||||
('M3', [('packages/webui/src/public/user.html', "but not your messages.", "but not your replies.")], GU, ['R6 G6'], []),
|
||||
('M4', [('packages/cli/src/access.mjs', "const peers = (system.federation?.peers ?? []).map((p) => p.id);", "const peers = [];")], AB,
|
||||
['R6 a configured peer id', 'R6 B1'], []),
|
||||
('M5', [('packages/webui/src/access.mjs', "return user && live(doc, user.id, b) ? user.id : null;", "return user ? user.id : null;")], GU, ['R6 G2'], []),
|
||||
('M6', [('packages/webui/src/user-bus.mjs', "env: { ...rest, MOSAIC_BUS_USER_NONCE", "env: { ...env, MOSAIC_BUS_USER_NONCE")], GU, ['R6 G8'], []),
|
||||
('M7', [('packages/cli/src/config.mjs', " if (access) config.access = access;\n", "")], AB, ['R6 B1'], []),
|
||||
('M8', [('packages/webui/src/chat.mjs', CHAT_CALL,
|
||||
"verb === 'plugins' ? owner('plugins', {}) : verb === 'thread.list' ? owner(verb, args) : call(verb, args)")], GU, ['R6 G9'], []),
|
||||
('M9', [('packages/cli/src/config.mjs', " warn(`access file refused at boot (${code}), no granted user binds until it is fixed: ${file}`);\n", "")],
|
||||
AB, ['R6 B3'], []),
|
||||
('M10', [('packages/webui/src/console-bus.mjs', "return session => consoleBus(transport(session.user), { send, owner });",
|
||||
"return session => consoleBus(transport(session.user), { send, owner: (v, a) => owner(v, { ...a, sub: session.sub }) });")], GU, ['R6 G9'], []),
|
||||
('M11', [('packages/webui/src/chat.mjs', "const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => " + CHAT_CALL + " });",
|
||||
"let last = {};\n const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => verb === 'plugins' ? owner('plugins', last) : call(verb, args) });"),
|
||||
('packages/webui/src/chat.mjs', " try { return await dispatch(verb, args); }", " try { last = args; return await dispatch(verb, args); }")], GU, ['R6 G9'], []),
|
||||
]
|
||||
|
||||
def git(*a):
|
||||
return subprocess.run(['git', '-C', WT, *a], capture_output=True, text=True, check=True).stdout
|
||||
|
||||
def restore(files):
|
||||
if files: git('checkout', '--', *sorted(files))
|
||||
if git('status', '--porcelain'): raise SystemExit('worktree not clean after restore')
|
||||
|
||||
def results(tap):
|
||||
# Top-level TAP test lines: "ok N - name" / "not ok N - name" at no indentation.
|
||||
return {m.group(2): m.group(1) == 'ok' for m in re.finditer(r'^(ok|not ok) \d+ - (.*)$', tap, re.M)}
|
||||
|
||||
def main():
|
||||
only = sys.argv[1:]
|
||||
if git('status', '--porcelain'): raise SystemExit('worktree not clean at start')
|
||||
env = dict(os.environ)
|
||||
summary, bad = [], 0
|
||||
for mid, edits, (pkg, test), killers, greens in MUTANTS:
|
||||
if only and mid not in only: continue
|
||||
files = set()
|
||||
try:
|
||||
for path, old, new in edits:
|
||||
p = os.path.join(WT, path); s = open(p).read()
|
||||
if s.count(old) != 1: raise SystemExit(f'{mid}: anchor count {s.count(old)} in {path}')
|
||||
open(p, 'w').write(s.replace(old, new)); files.add(path)
|
||||
diff = git('diff')
|
||||
t0 = time.time()
|
||||
r = subprocess.run(['timeout', '-k', '10', '300', 'node', '--test', '--test-reporter=tap', test],
|
||||
cwd=os.path.join(WT, pkg), capture_output=True, text=True, env=env)
|
||||
dt = round(time.time() - t0, 1)
|
||||
finally:
|
||||
restore(files)
|
||||
open(f'{OUT}/{mid}.diff', 'w').write(diff)
|
||||
open(f'{OUT}/{mid}.tap', 'w').write(r.stdout + '\n# stderr\n' + r.stderr)
|
||||
res = results(r.stdout)
|
||||
hit = lambda pre: [n for n, ok in res.items() if n.startswith(pre)]
|
||||
red = [n for n, ok in res.items() if not ok]
|
||||
killed = r.returncode not in (0, 124, 137) and all(any(not res[n] for n in hit(k)) for k in killers) \
|
||||
and all(hit(g) and all(res[n] for n in hit(g)) for g in greens)
|
||||
bad += not killed
|
||||
line = f'{mid} rc={r.returncode} seconds={dt} killed={killed} red={red}'
|
||||
print(line, flush=True)
|
||||
summary.append({'mutant': mid, 'rc': r.returncode, 'seconds': dt, 'killed': killed, 'red': red,
|
||||
'killers': killers, 'greens': greens})
|
||||
json.dump(summary, open(f'{OUT}/mutants.json', 'w'), indent=1)
|
||||
sys.exit(1 if bad else 0)
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,16 @@
|
||||
npm warn deprecated [email protected]: Use your platform's native DOMException instead
|
||||
|
||||
added 128 packages, and audited 129 packages in 2s
|
||||
|
||||
10 packages are looking for funding
|
||||
run `npm fund` for details
|
||||
|
||||
3 vulnerabilities (1 moderate, 2 high)
|
||||
|
||||
To address issues that do not require attention, run:
|
||||
npm audit fix
|
||||
|
||||
To address all issues (including breaking changes), run:
|
||||
npm audit fix --force
|
||||
|
||||
Run `npm audit` for details.
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
added 2 packages, and audited 3 packages in 401ms
|
||||
|
||||
found 0 vulnerabilities
|
||||
@@ -0,0 +1,27 @@
|
||||
== webui
|
||||
ℹ tests 365
|
||||
ℹ suites 0
|
||||
ℹ pass 365
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 57832.913871
|
||||
== cli
|
||||
ℹ tests 909
|
||||
ℹ suites 0
|
||||
ℹ pass 909
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 600102.466154
|
||||
== bus
|
||||
ℹ tests 873
|
||||
ℹ suites 0
|
||||
ℹ pass 873
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 24399.905747
|
||||
@@ -0,0 +1,18 @@
|
||||
start 06:20:53Z FROM=npm-root TAG= TMPDIR /mnt/storage/scratch/tmp/gd3JQ8r head 8616b9a4f61d2546ac640f8f63e0a2ae558a49d5
|
||||
npm-root rc=0 seconds=1.926 limit=900 06:20:55Z
|
||||
npm-webui rc=0 seconds=0.452 limit=900 06:20:55Z
|
||||
webui rc=0 seconds=57.861 limit=600 06:21:53Z
|
||||
cli rc=0 seconds=600.132 limit=2100 06:31:53Z
|
||||
bus rc=0 seconds=24.429 limit=900 06:32:18Z
|
||||
test-auth rc=0 seconds=0.781 limit=126 06:32:19Z
|
||||
test-conductor rc=0 seconds=6.825 limit=145 06:32:25Z
|
||||
test-config rc=0 seconds=2.482 limit=130 06:32:28Z
|
||||
test-discord rc=0 seconds=7.314 limit=147 06:32:35Z
|
||||
test-extension-package rc=0 seconds=1.229 limit=130 06:32:37Z
|
||||
test-foundation rc=0 seconds=39.067 limit=269 06:33:16Z
|
||||
test-land-work rc=0 seconds=11.848 limit=171 06:33:27Z
|
||||
test-queue rc=0 seconds=20.432 limit=226 06:33:48Z
|
||||
test-release rc=0 seconds=2.663 limit=157 06:33:51Z
|
||||
test-task rc=0 seconds=13.923 limit=231 06:34:04Z
|
||||
SUITES GREEN 06:34:04Z
|
||||
MUTANTS rc=0 06:34:22Z
|
||||
@@ -0,0 +1,17 @@
|
||||
OK status with missing harness credential exits 3 and still lists accounts
|
||||
OK status reports harness credential (read-only) + mosaic accounts
|
||||
OK api key material never reaches output
|
||||
OK oauth token material never reaches output
|
||||
OK unparseable credential file exits 2
|
||||
OK symlinked credential file exits 4
|
||||
OK env-side credential names reported
|
||||
OK env var values never reach output
|
||||
OK accounts without an accounts dir reports none and creates nothing
|
||||
OK accounts lists files and marks the active one
|
||||
OK loose account perms flagged in listing
|
||||
OK agent --auth with missing account file refuses (exit 4)
|
||||
OK agent --auth with non-0600 account file refuses
|
||||
OK agent --auth with invalid account name refuses
|
||||
OK auth.sh without valid config refuses
|
||||
|
||||
selftest: 15 passed, 0 failed
|
||||
@@ -0,0 +1,23 @@
|
||||
On branch launchpad/row294
|
||||
Your branch is up to date with 'origin/launchpad/row294'.
|
||||
|
||||
nothing to commit, working tree clean
|
||||
OK dry-run: allowed change, exit 0, nothing committed (exit 0)
|
||||
OK dry-run committed nothing
|
||||
OK apply: allowed change exits 0 (exit 0)
|
||||
OK apply: attribution in commit subject
|
||||
OK apply: target tree clean after commit
|
||||
OK disallowed path refused (exit 1)
|
||||
OK disallowed path: target untouched
|
||||
OK syntax gate refused broken .mjs (exit 1)
|
||||
OK syntax gate: target untouched
|
||||
OK suite failure refused (exit 1)
|
||||
OK suite failure: target reverted to clean
|
||||
OK disabled policy refused (exit 2)
|
||||
OK disabled policy: target untouched
|
||||
OK failed run refused (exit 1)
|
||||
OK failed run: target untouched
|
||||
OK missing run exits 4 (exit 4)
|
||||
OK invalid policy exits 2 (exit 2)
|
||||
|
||||
selftest: 17 passed, 0 failed
|
||||
@@ -0,0 +1,57 @@
|
||||
OK absent adapter defaults to pi
|
||||
OK adapter mock validates (exit 0)
|
||||
OK unsupported adapter exits 2 (exit 2)
|
||||
OK env exports adapter
|
||||
OK bootstrap creates default when absent (exit 0)
|
||||
OK bootstrap wrote config file
|
||||
OK bootstrap is idempotent on existing config (exit 0)
|
||||
OK bootstrap did not rewrite existing config
|
||||
OK validate missing config exits 3 (exit 3)
|
||||
OK malformed JSON exits 2 (exit 2)
|
||||
OK unsupported configVersion exits 2 (exit 2)
|
||||
OK unknown top-level key exits 2 (exit 2)
|
||||
OK unknown execution key exits 2 (exit 2)
|
||||
OK unsupported backend exits 2 (exit 2)
|
||||
OK unsupported environment exits 2 (exit 2)
|
||||
OK relative dataRoot exits 2 (exit 2)
|
||||
OK non-canonical dataRoot exits 2 (exit 2)
|
||||
OK filesystem root dataRoot exits 2 (exit 2)
|
||||
OK home directory dataRoot exits 2 (exit 2)
|
||||
OK dataRoot containing config dir exits 2 (exit 2)
|
||||
OK control character in provider exits 2 (exit 2)
|
||||
OK symlinked config file exits 2 (exit 2)
|
||||
OK env exports resolve correctly
|
||||
OK valid console.sso loads and is in the output
|
||||
OK console.sso HS256 alg exits 2 naming the path (exit 2)
|
||||
OK console.sso alg none exits 2 naming the path (exit 2)
|
||||
OK console.sso missing slot field exits 2 naming the path (exit 2)
|
||||
OK console.sso slot name outside the pattern exits 2 naming the path (exit 2)
|
||||
OK unknown key under console exits 2 naming the path (exit 2)
|
||||
OK valid federation key loads and is in the output
|
||||
OK federation unknown sub-key exits 2 naming the key (exit 2)
|
||||
OK federation peer unknown sub-key exits 2 naming the key (exit 2)
|
||||
OK federation peer origin over http exits 2 naming the key (exit 2)
|
||||
OK federation peer origin with a path exits 2 naming the key (exit 2)
|
||||
OK federation peer origin with userinfo exits 2 naming the key (exit 2)
|
||||
OK federation peer id equal to self exits 2 naming the key (exit 2)
|
||||
OK federation peer label empty exits 2 naming the key (exit 2)
|
||||
OK federation not an object exits 2 naming the key (exit 2)
|
||||
OK federation self pattern exits 2 naming the key (exit 2)
|
||||
OK federation peers not an array exits 2 naming the key (exit 2)
|
||||
OK federation peer not an object exits 2 naming the key (exit 2)
|
||||
OK federation peer id pattern exits 2 naming the key (exit 2)
|
||||
OK federation peer label over 80 characters exits 2 naming the key (exit 2)
|
||||
OK federation duplicate peer id exits 2 naming the key (exit 2)
|
||||
OK federation peer origin with a trailing slash exits 2 naming the key (exit 2)
|
||||
OK federation self missing exits 2 naming the key (exit 2)
|
||||
OK federation peer id missing exits 2 naming the key (exit 2)
|
||||
OK federation peer label missing exits 2 naming the key (exit 2)
|
||||
OK config without federation loads and has no federation output
|
||||
OK config without console loads with unchanged output
|
||||
OK compactionReadBytes 8388608 loads and is in the output as written
|
||||
OK compactionReadBytes "x" loads and is in the output as written
|
||||
OK compactionStreams 2048 loads and is in the output as written
|
||||
OK compactionStreams "x" loads and is in the output as written
|
||||
OK failed validation modified nothing
|
||||
|
||||
selftest: 55 passed, 0 failed
|
||||
@@ -0,0 +1,85 @@
|
||||
toolchain: node v26.8.1
|
||||
|
||||
OK syntax: packages/discord/src/approvals.mjs
|
||||
OK syntax: packages/discord/src/authorize.mjs
|
||||
OK syntax: packages/discord/src/binding.mjs
|
||||
OK syntax: packages/discord/src/cli.mjs
|
||||
OK syntax: packages/discord/src/connector.mjs
|
||||
OK syntax: packages/discord/src/context.mjs
|
||||
OK syntax: packages/discord/src/engine-pi.mjs
|
||||
OK syntax: packages/discord/src/errors.mjs
|
||||
OK syntax: packages/discord/src/gateway.mjs
|
||||
OK syntax: packages/discord/src/git.mjs
|
||||
OK syntax: packages/discord/src/journal.mjs
|
||||
OK syntax: packages/discord/src/notify.mjs
|
||||
OK syntax: packages/discord/src/processing.mjs
|
||||
OK syntax: packages/discord/src/rest.mjs
|
||||
OK syntax: packages/discord/src/setspark.mjs
|
||||
OK syntax: packages/discord/src/tools.mjs
|
||||
OK syntax: packages/discord/src/web.mjs
|
||||
OK syntax: packages/discord/bin/git-credential.mjs
|
||||
OK syntax: packages/discord/extension/tools.mjs
|
||||
OK syntax: packages/discord/tests/approvals.test.mjs
|
||||
OK syntax: packages/discord/tests/authorize.test.mjs
|
||||
OK syntax: packages/discord/tests/binding.test.mjs
|
||||
OK syntax: packages/discord/tests/connector.test.mjs
|
||||
OK syntax: packages/discord/tests/context.test.mjs
|
||||
OK syntax: packages/discord/tests/copy-service.test.mjs
|
||||
OK syntax: packages/discord/tests/engine.test.mjs
|
||||
OK syntax: packages/discord/tests/fake-pi.mjs
|
||||
OK syntax: packages/discord/tests/gateway.test.mjs
|
||||
OK syntax: packages/discord/tests/git.test.mjs
|
||||
OK syntax: packages/discord/tests/helpers.mjs
|
||||
OK syntax: packages/discord/tests/journal.test.mjs
|
||||
OK syntax: packages/discord/tests/notify.test.mjs
|
||||
OK syntax: packages/discord/tests/offline-check.test.mjs
|
||||
OK syntax: packages/discord/tests/processing-stop.test.mjs
|
||||
OK syntax: packages/discord/tests/processing.test.mjs
|
||||
OK syntax: packages/discord/tests/recover.test.mjs
|
||||
OK syntax: packages/discord/tests/rest.test.mjs
|
||||
OK syntax: packages/discord/tests/setspark.test.mjs
|
||||
OK syntax: packages/discord/tests/tools.test.mjs
|
||||
OK syntax: packages/discord/tests/web.test.mjs
|
||||
OK syntax: packages/discord/fixtures/claim-worker.mjs
|
||||
OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs
|
||||
OK syntax: scripts/discord.sh
|
||||
OK syntax: scripts/discord-service.sh
|
||||
OK packages/discord declares no dependencies
|
||||
OK no bot-token-shaped string in packages/discord
|
||||
OK fixture binding uses placeholder ids only
|
||||
OK fixture binding validates
|
||||
OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool
|
||||
OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start
|
||||
OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root
|
||||
OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token
|
||||
OK real pi with protocol vault adds reserve_id to the git verbs
|
||||
OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key
|
||||
OK real pi refuses a git key on a read-only root (fail closed)
|
||||
OK real pi with the pilot flags (--no-tools) exposes no tool at all
|
||||
OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed)
|
||||
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
|
||||
OK node --test packages/discord/tests/ (ℹ pass 192)
|
||||
OK scripts/discord.sh --help exits 0
|
||||
OK scripts/discord.sh check without a binding exits 4
|
||||
OK scripts/discord.sh recover without a binding exits 4
|
||||
OK scripts/discord.sh reload without a binding exits 4
|
||||
OK scripts/discord-service.sh without a command exits 4
|
||||
OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP
|
||||
OK service install writes the rendered unit (0644) and leaves no temp file
|
||||
OK service install a second time reports unchanged
|
||||
OK systemd-analyze verify accepts the rendered unit
|
||||
OK service uninstall removes the unit file
|
||||
OK service install with an unknown flag exits 4
|
||||
OK service install with USER unset finishes and names the account for lingering
|
||||
OK pinned render runs copy CLI with copy cwd/repo and no checkout executable
|
||||
OK pinned install writes the rendered unit in a temporary unit directory
|
||||
OK pinned install refuses short prerequisite and writes no unit
|
||||
OK pinned install refuses missing prerequisite and writes no unit
|
||||
OK pinned install refuses cli prerequisite and writes no unit
|
||||
OK pinned install refuses pi prerequisite and writes no unit
|
||||
OK status names the copy from fake systemctl effective cwd
|
||||
OK checkout install explicitly recommends --copy
|
||||
OK wrapper check --offline passes local fixture and makes no network call
|
||||
OK wrapper run --offline refuses usage
|
||||
|
||||
discord suite: 81 passed, 0 failed
|
||||
@@ -0,0 +1,21 @@
|
||||
OK initial ordinary-file install
|
||||
OK installed tree matches canonical source
|
||||
OK installed tree has no symlinks
|
||||
OK check detects installation drift
|
||||
OK sync refuses to overwrite installation drift
|
||||
OK check detects an extra destination file
|
||||
OK check detects an extra destination directory
|
||||
OK check rejects a destination symlink
|
||||
OK sync accepts a canonical source update
|
||||
OK updated installation matches canonical source
|
||||
scripts/test-extension-package.sh: line 14: 3315913 Killed "$@" > /dev/null 2>&1
|
||||
OK forced interruption kills the replacing process
|
||||
OK next invocation recovers old consistent installation
|
||||
OK interrupted replacement rolled back
|
||||
OK sync succeeds after interruption recovery
|
||||
OK unlocked stale lock file does not block
|
||||
OK active lock refuses a concurrent sync
|
||||
OK source symlink fails closed
|
||||
OK nested second entrypoint fails closed
|
||||
|
||||
extension package selftest: 18 passed, 0 failed
|
||||
@@ -0,0 +1,53 @@
|
||||
toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0
|
||||
|
||||
OK syntax: scripts/foundation-inspect.mjs
|
||||
OK syntax: scripts/foundation/strict-json.mjs
|
||||
OK syntax: scripts/foundation/canonical.mjs
|
||||
OK syntax: scripts/foundation/resolve.mjs
|
||||
OK syntax: scripts/foundation/validate-record.mjs
|
||||
OK syntax: scripts/foundation/fixtures/build-fixtures.mjs
|
||||
OK syntax: scripts/foundation/canonical.test.mjs
|
||||
OK syntax: scripts/foundation/cli.test.mjs
|
||||
OK syntax: scripts/foundation/fixtures.test.mjs
|
||||
OK syntax: scripts/foundation/resolve.test.mjs
|
||||
OK syntax: scripts/foundation/strict-json.test.mjs
|
||||
OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written)
|
||||
OK fixture generator runs
|
||||
OK checked-in fixtures/bundles equal a fresh generation
|
||||
OK checked-in fixtures/raw equal a fresh generation
|
||||
OK checked-in fixtures/index.json equal a fresh generation
|
||||
OK checked-in demo bundles equal a fresh generation
|
||||
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
|
||||
OK node --test scripts/foundation/ (ℹ pass 80)
|
||||
OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof)
|
||||
platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999)
|
||||
node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases)
|
||||
schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10
|
||||
profile column (schema-valid records only): profile-valid 510, profile-invalid 30
|
||||
profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns
|
||||
OK oracle: zero schema-column disagreements with the pinned checker
|
||||
OK oracle: strict-only profile refusals are counted and asserted
|
||||
OK demo: permitted read preview exits 0 (exit 0)
|
||||
OK demo: permitted file.change preview exits 0 (exit 0)
|
||||
OK demo: assignment.change proposal is unresolved (exit 3) (exit 3)
|
||||
OK demo: revoked registration is refused (exit 3) (exit 3)
|
||||
OK demo: message is not authority (exit 3) (exit 3)
|
||||
OK usage: no arguments exits 2 (exit 2)
|
||||
OK io: missing file exits 4 (exit 4)
|
||||
OK io: directory exits 4 (exit 4)
|
||||
OK io: symlink exits 4 (O_NOFOLLOW) (exit 4)
|
||||
OK bound: oversize fixture exits 2 (exit 2)
|
||||
OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2)
|
||||
OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2)
|
||||
OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0)
|
||||
OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed
|
||||
OK text output starts with the disclaimer
|
||||
OK json output is valid JSON with result allowed and exactly the charter §7 fields
|
||||
OK json golden matches byte-for-byte
|
||||
OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal
|
||||
OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs
|
||||
OK canary never printed (bundle run and credential-file run)
|
||||
OK a non-bundle JSON file is refused at the shape gate, not read into output
|
||||
OK no field of the non-bundle file is echoed
|
||||
|
||||
selftest: 44 passed, 0 failed
|
||||
@@ -0,0 +1,88 @@
|
||||
OK args: exit 2, target-branch
|
||||
OK args: exit 2, target-branch
|
||||
OK args: exit 4
|
||||
OK args: exit 4
|
||||
OK args: exit 4
|
||||
OK --base next refuses before the config is read (got 2)
|
||||
OK a missing system config exits 3 (got 3)
|
||||
OK nobus: exit 3
|
||||
OK schema-table: exit 2, bus-schema
|
||||
OK schema-meta: exit 2, bus-schema
|
||||
OK openBus opens the database read-only
|
||||
OK other-business: exit 2, decision-not-found
|
||||
OK no-raise: exit 2, decision-not-found
|
||||
OK not-a-merge: exit 2, not-a-merge
|
||||
OK open: exit 2, not-resolved
|
||||
OK withdrawn: exit 2, not-resolved
|
||||
OK by-agent: exit 2, not-human
|
||||
OK via-webui: exit 2, not-human
|
||||
OK no-answer: exit 2, not-human
|
||||
OK answer-role: exit 2, not-human
|
||||
OK answer-run: exit 2, not-human
|
||||
OK answer-choice: exit 2, not-human
|
||||
OK two-answers: exit 2, not-human
|
||||
OK arbiter-cross: exit 2, not-human
|
||||
OK arbiter-cited: exit 2, not-human
|
||||
OK chose-no: exit 2, not-merge-option
|
||||
OK two-refs: exit 2, work-ref
|
||||
OK bad-ref: exit 2, work-ref
|
||||
OK task-ref-disagrees: exit 2, work-ref
|
||||
OK raise-task-disagrees: exit 2, work-ref
|
||||
OK target-main: exit 2, target-branch
|
||||
OK superseded-question: exit 2, superseded
|
||||
OK superseded-task: exit 2, superseded
|
||||
OK superseded-by: exit 2, superseded
|
||||
OK no-verdict: exit 2, no-verdict
|
||||
OK changes-later: exit 2, not-approved
|
||||
OK short-commit: exit 2, not-approved
|
||||
OK wrong-task-body: exit 2, not-approved
|
||||
OK args: exit 2, decision-not-found
|
||||
OK earlier-merge: exit 0
|
||||
OK ref-moved: exit 2, ref-moved
|
||||
OK root-top: exit 2, root-file
|
||||
OK root-newdir: exit 2, root-file
|
||||
OK root-src: exit 2, root-file
|
||||
OK unrelated: exit 2, base-conflict
|
||||
OK empty: exit 2, base-conflict
|
||||
OK an empty reviewed diff says so
|
||||
OK dirty-changed: exit 2, checkout-dirty
|
||||
OK dirty-staged: exit 2, checkout-dirty
|
||||
OK dirty-untracked: exit 2, checkout-dirty
|
||||
OK conflict: exit 2, base-conflict
|
||||
OK a refusal after the worktree is added leaves no worktree or scratch
|
||||
OK cleanup looks for the worktree's Docker network
|
||||
OK dirty-during: exit 2, checkout-dirty
|
||||
OK a reviewed path dirtied during the gate moves no ref
|
||||
OK drift: exit 2, patch-drift
|
||||
OK patch-drift leaves no worktree or scratch
|
||||
OK gate-pkg: exit 2, gate-failed
|
||||
OK gate-failed names the failing step's log, and it exists
|
||||
OK gate-failed leaves no worktree or scratch
|
||||
OK gate-script: exit 2, gate-failed
|
||||
OK a failing scripts/test-*.sh fails the gate
|
||||
OK a failed gate moves no ref
|
||||
OK nomods: exit 4
|
||||
OK a target without node_modules leaves no worktree or scratch
|
||||
OK moved: exit 2, base-moved
|
||||
OK base-moved keeps the base where it moved to
|
||||
OK dry: exit 0
|
||||
OK --dry-run writes no ref
|
||||
OK --dry-run prints the commit with --subject and --attribution
|
||||
OK --dry-run runs the gate
|
||||
OK --dry-run leaves no worktree or scratch
|
||||
OK land: exit 0
|
||||
OK the landing is one commit on the old base
|
||||
OK its diff is the reviewed diff by patch-id
|
||||
OK its tree is the dry run's
|
||||
OK the message has the option's subject, the citations and the attribution last
|
||||
OK it prints the line for the PM
|
||||
OK the checked-out base has the reviewed paths and keeps the unrelated edit
|
||||
OK only refs/heads/refactor exists in the target
|
||||
OK the work ref is unchanged
|
||||
OK the bus database is unchanged
|
||||
OK the gate ran each package suite but queue, and each scripts/test-*.sh
|
||||
OK the landing leaves no worktree or scratch
|
||||
OK the target's node_modules is still there, with its contents
|
||||
OK the worktree's Docker network is removed
|
||||
---
|
||||
land-work selftests: 86 passed, 0 failed
|
||||
@@ -0,0 +1,36 @@
|
||||
toolchain: node v26.8.1, git version 2.55.0
|
||||
|
||||
OK syntax: packages/queue/src/cli.mjs
|
||||
OK syntax: packages/queue/src/errors.mjs
|
||||
OK syntax: packages/queue/src/io.mjs
|
||||
OK syntax: packages/queue/src/lock.mjs
|
||||
OK syntax: packages/queue/src/queue.mjs
|
||||
OK syntax: packages/queue/src/review.mjs
|
||||
OK syntax: packages/queue/src/store.mjs
|
||||
OK syntax: packages/queue/tests/commit.test.mjs
|
||||
OK syntax: packages/queue/tests/data.test.mjs
|
||||
OK syntax: packages/queue/tests/dispatch.test.mjs
|
||||
OK syntax: packages/queue/tests/gate-owner.test.mjs
|
||||
OK syntax: packages/queue/tests/helpers.mjs
|
||||
OK syntax: packages/queue/tests/lock.test.mjs
|
||||
OK syntax: packages/queue/tests/migration.test.mjs
|
||||
OK syntax: packages/queue/tests/review.test.mjs
|
||||
OK syntax: packages/queue/tests/store.test.mjs
|
||||
OK syntax: packages/queue/tests/write.test.mjs
|
||||
OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs
|
||||
OK syntax: packages/queue/tests/fixtures/kill-at.mjs
|
||||
OK syntax: packages/queue/tests/fixtures/lock-child.mjs
|
||||
OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh
|
||||
OK syntax: scripts/queue-commit.sh
|
||||
OK syntax: scripts/git-hooks/pre-commit
|
||||
OK syntax: scripts/mosaic
|
||||
OK queue-commit.sh, the guard and scripts/mosaic are executable
|
||||
OK packages/queue declares no dependencies
|
||||
ℹ tests 156
|
||||
ℹ pass 156
|
||||
ℹ fail 0
|
||||
OK node --test packages/queue/tests/
|
||||
OK scripts/mosaic queue help
|
||||
skip queue verify and render --check: this checkout (/mnt/storage/scratch/launchpad-r294/wt) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
|
||||
|
||||
queue suite: 28 passed, 0 failed
|
||||
@@ -0,0 +1,16 @@
|
||||
OK valid RELEASE resolves (exit 0)
|
||||
OK invalid RELEASE exits 1 (exit 1)
|
||||
OK missing RELEASE exits 1 (exit 1)
|
||||
OK valid RELEASE leaves image tag consistent with version
|
||||
OK status safe on empty state (exit 0)
|
||||
OK status created no pointer
|
||||
OK fault-injected activation refuses (exit 1)
|
||||
OK refused activation wrote no pointer
|
||||
OK refusal logged exactly once with valid fields
|
||||
OK healthy activation succeeds (exit 0)
|
||||
OK pointer written with valid fields
|
||||
OK repeat activation succeeds (log grows) (exit 0)
|
||||
OK log is append-only across activations
|
||||
OK rollback without previous refuses (exit 1)
|
||||
|
||||
selftest: 14 passed, 0 failed
|
||||
@@ -0,0 +1,96 @@
|
||||
OK valid task validates (exit 0)
|
||||
OK unknown task key exits 2 (exit 2)
|
||||
OK unsupported taskVersion exits 2 (exit 2)
|
||||
OK invalid task id exits 2 (exit 2)
|
||||
OK empty prompt exits 2 (exit 2)
|
||||
OK NUL in expectExact exits 2 (exit 2)
|
||||
OK out-of-range timeout exits 2 (exit 2)
|
||||
OK missing mission file exits 4 (exit 4)
|
||||
OK task with valid mission validates (exit 0)
|
||||
OK invalid mission exits 2 (exit 2)
|
||||
OK validate missing task exits 4 (exit 4)
|
||||
OK validation does not modify the task file
|
||||
OK prune dry-run exits 0 (exit 0)
|
||||
OK dry-run deleted nothing
|
||||
OK prune --keep=2 --yes removes oldest (exit 0)
|
||||
OK kept exactly 2 newest runs
|
||||
OK newest run kept, oldest pruned
|
||||
OK append-only receipt written (3 entries)
|
||||
OK sessions/workspaces untouched by prune
|
||||
OK prune with invalid keep exits 4 (exit 4)
|
||||
OK mock adapter: gate passes on matching mock response (exit 0)
|
||||
OK mock adapter: expect-mismatch recorded (exit 1)
|
||||
OK mismatch reason recorded
|
||||
OK unknown adapter fails closed (exit 1)
|
||||
OK mission task runs via mock adapter (exit 0)
|
||||
OK mission section injected into generated prompt
|
||||
OK retry of mission run succeeds (exit 0)
|
||||
OK retriedFrom lineage recorded
|
||||
OK mission section present after retry (relative path resolved)
|
||||
OK retry of missing run exits 4 (exit 4)
|
||||
OK skill install bundled ms-tools (exit 0)
|
||||
OK installed to skills-available
|
||||
OK double install refuses (exit 1)
|
||||
OK activate enables skill (exit 0)
|
||||
OK enabled dir populated
|
||||
OK uninstall while enabled refuses (exit 1)
|
||||
OK deactivate moves back to available (exit 0)
|
||||
OK uninstall removes from available (exit 0)
|
||||
OK seat with enabled skill launches
|
||||
OK skill path delivered to adapter
|
||||
OK shipped researcher contract resolves (ceiling + network)
|
||||
OK resolve-role refuses a non-role document (conductor policy) (exit 2)
|
||||
OK resolve-role refuses name/filename mismatch (exit 2)
|
||||
OK resolve-role refuses unknown network declaration (exit 2)
|
||||
OK resolve-role refuses duplicate role tool (exit 2)
|
||||
OK resolve-role refuses unsupported tool (exit 2)
|
||||
OK resolve-role refuses missing contract file (exit 4)
|
||||
OK version 1 role prints no contract line
|
||||
OK shipped version 2 roles resolve with their contracts (pm, cto, coder, reviewer)
|
||||
OK resolve-role accepts a minimal version 2 role (exit 0)
|
||||
OK resolve-role refuses an action outside the vocabulary (exit 2)
|
||||
OK resolve-role refuses a gated-only action in a role file (exit 2)
|
||||
OK resolve-role refuses a Vikunja verb no role may hold (exit 2)
|
||||
OK resolve-role refuses a version 2 role whose contract is missing (exit 2)
|
||||
OK resolve-role refuses a contract that is a symbolic link (exit 2)
|
||||
OK seat launches under role ceiling
|
||||
OK role ceiling narrows seat tools (read,write,bash -> read,bash)
|
||||
OK narrowing recorded loudly
|
||||
OK missing role contract refuses launch (exit 2)
|
||||
OK missing-contract refusal names the role
|
||||
OK empty ceiling intersection -> tool-free seat
|
||||
OK tool-free outcome recorded loudly
|
||||
OK overridden agents dir without seat definition refuses (exit 4)
|
||||
OK seat-resolution refusal names the seat
|
||||
OK policy: mission only -> mission tools
|
||||
OK policy: task only -> task tools
|
||||
OK policy: both -> intersection (task narrowed)
|
||||
OK policy: empty intersection -> tool-free
|
||||
OK invalid mission capabilities rejected (exit 2)
|
||||
mosaic-task: unsupported mission tool: "sudo" (supported: read, write, edit, bash, grep, find, ls)
|
||||
OK task run with user layer present (exit 0)
|
||||
OK user context dispatched into generated prompt
|
||||
OK workspace+tools task runs via mock (exit 0)
|
||||
OK workspace path + tools delivered to adapter
|
||||
OK persistent workspace created on host
|
||||
OK plain task still runs (no workspace/tools) (exit 0)
|
||||
OK workspace var present but empty when absent
|
||||
OK tools empty when absent
|
||||
OK unknown tool exits 2 (exit 2)
|
||||
OK workspace traversal exits 2 (exit 2)
|
||||
OK hello task succeeds with exact marker
|
||||
OK result.json written in run dir
|
||||
OK result.json contents are correct
|
||||
OK wrong expectExact fails with exit 1 (reason: expect-mismatch)
|
||||
OK each run gets a distinct run dir (no clobber)
|
||||
OK list shows both runs
|
||||
skip live provider cases (row 244: no provider call in the gate)
|
||||
OK onboard without name exits 4 (non-interactive) (exit 4)
|
||||
OK onboard --name renders profile (exit 0)
|
||||
OK profile written
|
||||
OK canon structure: required filled, optional placeholdered
|
||||
OK canon sections present
|
||||
skip live provider cases (row 244: no provider call in the gate)
|
||||
OK no agent identity on headless run
|
||||
|
||||
selftest: 91 passed, 0 failed
|
||||
@@ -0,0 +1,26 @@
|
||||
# Row 303 build notes (Launchpad): R9 shows its output when rehearse makes no directory (#1810)
|
||||
|
||||
Brief: docs/plans/2026-10-23_r9-guard-row.md (Vikunja 589). Reviewer Duckworth, gate owner Sage.
|
||||
Base 80793d824 (refactor after row 294 landed at d3059c2c3). Branch launchpad/row303.
|
||||
|
||||
## Change
|
||||
|
||||
One added line in packages/cli/tests/stack-release.test.mjs, R9 only, at :2178:
|
||||
|
||||
assert.ok(r.dir!==null, r.out+r.err);
|
||||
|
||||
It sits right after R9's exit-code check and right before the `includes` check whose message builds
|
||||
`join(r.dir,'placeholders')`. rehearse() (:2040) sets `dir` to null when the output has no
|
||||
`rehearsal directory:` line, so the guard fails first and its message carries the rehearsal's
|
||||
output and stderr, including the FAIL line that stopped it. This is a test-only change to a
|
||||
pre-existing test, approved under mos-1272 and mos-1316 (mos-1422 point 2). It adds an assertion
|
||||
and changes none. No other line in R9 or any other test changes. The lines the brief names (:2058,
|
||||
:2113, :2126, :2267, :2280) put r.dir in a template, where a null prints as `null` and the message
|
||||
still carries r.out, so they get no guard.
|
||||
|
||||
MANIFEST.sha256 lists the one changed path, hashed from the commit that adds this file.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves the diff. stack-release.test.mjs runs whole and green on Sage's gate, in a gap.
|
||||
Not run yet.
|
||||
@@ -0,0 +1 @@
|
||||
e936808bb12361acf164b675fe40ead416720dfd48c5d51ea5bfee1e1893cc33 packages/cli/tests/stack-release.test.mjs
|
||||
@@ -1570,3 +1570,4 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-23T03:15:01Z | Filbert (Opus 5.5, gander-0182) | row 293 round 1 verdict, manifest cdda41f5 at a9cae2bd6 (tested 6bfab273b) | APPROVE c30982, queue rev 3411 (filbert-293-r1-approve). Run 2 GREEN is Gander's receipt (153/202/905, 312 flagged commits, 0 maintenance), not rerun by me. N1 doc-only: lifecycle.test needs TMPDIR of about 50 bytes or less.
|
||||
2026-10-23 | darkwing (T3 7525c8e4) | schema 3m->3n migrate window 0cbe2e9b3->464010fbe (sage-3135, mos-1385/1405) | DONE: activation 20261023T034557Z-464010fbe4c6, 26/97/5/28, W9b pm ra38826f3c6e2 left live, Gitea #1776 c31009 and #1775 c31010, window files on darkwing/window-migrate-3n 8708927e5; P19 386 c887 posted by Sage
|
||||
2026-10-23T03:59:52Z | sage (Opus 5.5, 1ef1e4f8) | 3m-3n window slot and 574 landing | Darkwing's window CLEAR 03:39Z (sage-3172), slot returned 03:53Z (darkwing-0216), P19 386 c887, Mos told (sage-3173, mos-1407); row 293 (574, #1805) LANDED e51d11640 03:55Z (6 cherry-picks, range-diff all =, range patch-id equal), #1805 closed c31013, P19 574 c888, note and done move; queue-commit 464010fbe (revs 3435-3436)
|
||||
2026-10-23T05:47:17Z | sage (Opus 5.5, 1ef1e4f8) | 292 landing, race row 302, rows 300/301, 294 r2/r3 | 292 LANDED 6aab979e6 (done 3447, #1797 c31038, Vikunja 588 c891); rows 300-302 briefed (86d5ac69b, e3ef1eaf6, fdedc56e2); 302 rulings carried to Gizmoduck/Dewey (mos-1414..1416); 294 r2 red to Mos, r3 approved (mos-1417), requests c31042/c31049, records 3455/3462; queue-commit 756284e8e; slot to Rocko 219 05:46Z
|
||||
@@ -0,0 +1,33 @@
|
||||
# R9's lost output: a null rehearsal directory hides the failing check
|
||||
|
||||
Found by Sage's merged-tree check for row 294's landing (#1800) at 06:50Z on 10-23. Vikunja 589.
|
||||
Mos ruled on it in mos-1422.
|
||||
|
||||
## Brief: R9 shows its output when rehearse makes no directory (Vikunja 589)
|
||||
|
||||
Issue #1810.
|
||||
|
||||
Owner Launchpad. Reviewer Duckworth. Gate owner Sage. After row 294 is done.
|
||||
|
||||
`packages/cli/tests/stack-release.test.mjs` R9 (mos-0963, about :2169) failed once in a whole-file
|
||||
run on d3059c2c3. The error was a TypeError, ERR_INVALID_ARG_TYPE: a null path at :2178. The
|
||||
assertion message there evaluates `join(r.dir,'placeholders')` before `includes()` runs. `r.dir` is
|
||||
null when `scripts/stack-release.sh rehearse` exits before it sets R (:2254), because `r_end` prints
|
||||
the `rehearsal directory:` line only when R is set (:2039). The join throws, so `r.out`, which
|
||||
holds the FAIL line that stopped the rehearsal, never reaches the report. R9 then passed 6/6 run
|
||||
alone on three trees, and the whole-file rerun under mos-1422 decided the landing. So the cause is
|
||||
still unknown, and the next hit has to show it.
|
||||
|
||||
Change, a test-only change to a pre-existing test, approved under mos-1272 and mos-1316 (mos-1422
|
||||
point 2): in R9 only, add
|
||||
|
||||
assert.ok(r.dir!==null, r.out+r.err);
|
||||
|
||||
right before the `:2178` `includes` check. Nothing else in R9 or in any other test changes. Lines
|
||||
2058, 2113, 2126, 2267 and 2280 put `r.dir` straight into a template, so a null prints as `null`
|
||||
and their message still carries `r.out`. They don't get the guard.
|
||||
|
||||
Any other change to an existing assertion goes to Mos through Sage first.
|
||||
|
||||
Gate: Duckworth approves the diff. `packages/cli/tests/stack-release.test.mjs` runs whole and green
|
||||
on Sage's gate, in a gap.
|
||||
+5
-4
@@ -33,7 +33,7 @@ Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
|
||||
|
||||
<!-- mosaic-queue:begin -->
|
||||
|
||||
Generated from `docs/plans/queue.json` revision 3447 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
|
||||
Generated from `docs/plans/queue.json` revision 3473 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
|
||||
|
||||
| # | Piece | Owner | Issues | State | After | Gate | Brief | Note |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
@@ -255,7 +255,7 @@ Generated from `docs/plans/queue.json` revision 3447 by `queue render`. Do not e
|
||||
| 216 | Case 2 C2-05a lookup index and queries | researcher; reviewers filbert | #1717 | briefed | 268 done | Filbert approves the manifest. The library suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-05a lookup index and queries | Rulings on phase A: agents/sage/work/queue-216/RULINGS.md (da9555ca2). r2 rebases on row 268's landing and adds the four PRD 1.14 playground tests. |
|
||||
| 217 | Case 2 C2-06 the citation guard, the page store, the runner texts and the retry | launchpad; reviewers webby | #1718 | briefed | 268 done | Webby approves the manifest. The library, bus and harness suites are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-06 the citation guard, the page store, the runner texts and the retry | — |
|
||||
| 218 | Case 2 C2-12 the setup sheet for Melanie and the survey run sheet | sage; reviewers webby | #1719 | done | — | Doc only. Webby approves it against C2-01's real flags. Before landing, grep `packages/*/tests` and `scripts/test-*.sh` to confirm no test reads either file. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-12 the setup sheet for Melanie and the survey run sheet | LANDED f8276c5eb (pushed, on Darkwing sessions b65f3a0e8). Webby APPROVE r1-r3 (revs 2848, 2856, 2860; r3 candidate 1a38baf5af2a = 89ba79d94). Patch-id 18ba1e40ad29 seat 5c6f73ed7..89ba79d94 equals landed b65f3a0e8..f8276c5eb; three blobs equal 89ba79d94. Gate: doc only, 0 test readers (control 3). Melanie handover waits on C2-06 and Jason (#419). |
|
||||
| 219 | Case 2 C2-09a one thread per creative instance | rocko; reviewers dewey | #1720 | in-progress | — | Dewey approves the manifest. The bus, cli and webui suites are green on Sage's gate, along with every `scripts/test-*.sh`. Watch for hash pins on `bus.js` and `broker.mjs` (`packages/bus/tests/channel-proof.test.mjs` pins a webui file). (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-09a one thread per creative instance | — |
|
||||
| 219 | Case 2 C2-09a one thread per creative instance | rocko; reviewers dewey | #1720 | in-progress | — | Dewey approves the manifest. The bus, cli and webui suites are green on Sage's gate, along with every `scripts/test-*.sh`. Watch for hash pins on `bus.js` and `broker.mjs` (`packages/bus/tests/channel-proof.test.mjs` pins a webui file). (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-09a one thread per creative instance | sage-3224 restart: claimant rocko resumes b97a3ff9757ec2ac39d9ab4cebb9769e471df932, manifest a973912fe2fb9c64606cd99a385305b8f51498a096511b3a980bca6b0a5b98aa; full rerun under mos-1408, no prior test carry |
|
||||
| 220 | Case 2 C2-10a no raw session view (M7) and no raw Discord engine (M8) | researcher; reviewers filbert | #1721 | briefed | — | Filbert approves the manifest. The conversation and discord suites and `scripts/test-discord.sh` are green on Sage's gate, along with every `scripts/test-*.sh`. No test reaches a Discord server. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-10a no raw session view (M7) and no raw Discord engine (M8) | Rulings on phase A: agents/sage/work/queue-220/RULINGS.md (da9555ca2). Scope adds packages/business/src/library-seats.mjs (imported by path, no index export), its test, and control-board serve.mjs. r2 building as a Haiku subagent. |
|
||||
| 221 | Case 2 C2-20 Bean and Chet persona import, with a count-only dry run | gosalyn; reviewers webby | #1722 | done | — | Webby approves the manifest. The test file runs green on its own and the cli suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-20 Bean and Chet persona import, with a count-only dry run | LANDED 300766450, pushed. Cand 8f403d0d4, manifest 20f05e3a6ad5, 22 paths, verify-commit ok, patch-id dfbb754c6fb4 equals 62ddc96cc..8f403d0d4. Gate 11:07:50-11:19Z on b1fece6f0: test file alone 11/0, harness 140/0, cli 677/0, all scripts green. Brief Gate met: webby-0073 approve, test file alone, cli, scripts. No live run in the Gate; the import itself waits on Jason. N1 N2 N4 go to C2-20b, N3 to DEFERRED. |
|
||||
| 222 | Case 2 C2-03a fetch-only sync | megavolt; reviewers dewey | #1723 | done | — | Dewey approves the manifest. The library and bus suites are green on Sage's gate, along with every `scripts/test-*.sh`, and the mutant is killed. A search of the test run's output for the fixture token finds nothing. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-03a fetch-only sync | sage-2024 on megavolt-0089: core.createObject=rename in the fetch argv is accepted as a brief departure, since C2-04 inspectMirror refuses nlink 2. Dewey checks it. The askpass env keys, export SAFE in project/src/git.mjs, bus/tests/library-sync.test.mjs and the SURVEY-RUN user line are accepted. The 5 s client timeout residual goes in BUILD.md and Dewey rules fix or defer. Slot after 197 proof 3 and the 249 gate. |
|
||||
@@ -330,7 +330,7 @@ Generated from `docs/plans/queue.json` revision 3447 by `queue render`. Do not e
|
||||
| 291 | PRD 1.16 library.write routes under REQ-LIB-6 (mos-1328) | gosalyn; reviewers webby | #1796 | in-review, round 1, request posted | 282 done | Webby approves the text. Jason says yes to the exact text (Mos asks him, mos-1328 step 3); no landing before that. requirements.test.mjs and every scripts/test-*.sh that reads the PRD or requirements.json are green on Sage's gate. Mos gets the landing commit. (sage) | `docs/plans/2026-10-22_prd-1-16-brief.md` § PRD 1.16 library.write routes under REQ-LIB-6 (mos-1328) | — |
|
||||
| 292 | stack-release.sh config_checks runs the target loader on any config (row 270 rollback hazard) | gizmoduck; reviewers duckworth | #1797 | done | 203 done, 270 done, 271 done | Duckworth approves the manifest. The touched test files run whole, the cli suite and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-22_config-checks-any-key-brief.md` § stack-release.sh config_checks runs the target loader on any config (row 270 rollback hazard) | Landed 6aab979e6 (cherry-pick of c902a767d..90be6cccd, range-diff all =, patch-id a45c9ba4 equal). Tested head 0d0021454: stack-release.test 224/224, cli suite r2 red only compactions-launch:229 session-invalid 22 (idle-stop race, Vikunja 588, row 302), r3 rerun 910/910 under mos-1411, ten shells green, M1-M9 killed. Duckworth APPROVE rev 3433 on 2fa7ae17. |
|
||||
| 293 | 574: maintenance-free test fixture commits (Vikunja 574) | gander; reviewers filbert | #1805 | done | — | Filbert approves. The four whole files, then the seat and cli suites, are green on Sage's gate. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: 574, maintenance-free test fixture commits (Vikunja 574) | LANDED e51d11640 03:55Z 10-23 (cherry-pick of gander b23783f39..a9cae2bd6 onto 464010fbe; range-diff 6/6 equal, range patch-id equal). Filbert APPROVE c30982 cdda41f5; run 2 4 files 153/0, seat 202/0, cli 905/0, 312 flagged/0 maintenance children. N1: lifecycle.test needs TMPDIR of 50 bytes or less. Residual: queue store.mjs, discord git.mjs, harness tools.mjs unflagged. #1805 closed c31013, P19 574 c888. |
|
||||
| 294 | FED R6: Console granted-user sign-in, user-cli transport, peer links | launchpad; reviewers duckworth | #1800 | in-review, round 1, request posted | — | Duckworth approves the manifest. The webui, cli and bus suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: FED R6, Console granted-user sign-in | — |
|
||||
| 294 | FED R6: Console granted-user sign-in, user-cli transport, peer links | launchpad; reviewers duckworth | #1800 | done | — | Duckworth approves the manifest. The webui, cli and bus suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: FED R6, Console granted-user sign-in | landed d3059c2c3 on refactor 07:04Z: cherry-pick of c902a767d..24b745447 onto 09d0d6faf, range-diff 11 =, patch-id 8371405a equal. Duckworth approve c31053 rev 3469 on a1ff5d67, C1/C2 met. Run 4 on 8616b9a4f from npm ci: webui 365, cli 909, bus 873, 10 scripts, M1-M11 killed. Merged tree: stack-release-external 25, stack-upgrade 21, stack-release 223/224 (R9 transient, row 1810) then 224/224 rerun under mos-1422. Follow-ups rows 300, 301. Closes #1800. |
|
||||
| 295 | FED R4: revocation | morgana; reviewers dewey | #1801 | in-review, round 1, request posted | — | Dewey approves the manifest. The bus, cli and business suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: FED R4, revocation | — |
|
||||
| 296 | FED R7: scratch two-instance acceptance | quackerjack; reviewers filbert | #1802 | briefed | 190 done, 294 done, 295 done | Filbert approves the manifest. The script test and every scripts/test-*.sh are green on Sage's gate, and no unit, port or socket of the script's outlives the run. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: FED R7, scratch two-instance acceptance | — |
|
||||
| 297 | GitHub grant method (Bushroot's half of #436b) | bushroot; reviewers dewey | #1803 | briefed | 198 done | Dewey approves the manifest. The touched suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: the GitHub grant method (Bushroot's half of #436b) | — |
|
||||
@@ -338,7 +338,8 @@ Generated from `docs/plans/queue.json` revision 3447 by `queue render`. Do not e
|
||||
| 299 | 573: the P10 hop band (Vikunja 573) | gander; reviewers filbert | #1806 | briefed | 293 done | Filbert approves. The whole file under load at least 3 times, then the cli suite, are green on Sage's gate. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: 573, the P10 hop band (Vikunja 573) | — |
|
||||
| 300 | R6-N1: HTTP-level test of the user-session option swap (294 N1) | launchpad; reviewers duckworth | #1807 | briefed | 294 done | Duckworth approves the manifest; new test file whole, webui suite, mutant M12 killed (sage) | `docs/plans/2026-10-23_r6-followup-rows.md` § Brief: R6-N1, an HTTP-level test of the user-session option swap | — |
|
||||
| 301 | R6-N4: fold user-bus.mjs back into bus.mjs (294 N4) | launchpad; reviewers duckworth | #1808 | briefed | 294 done | Duckworth approves the manifest; channel-proof.test, webui suite and 294 user-transport tests whole and green; re-pin only with Mos go (sage) | `docs/plans/2026-10-23_r6-followup-rows.md` § Brief: R6-N4, fold user-bus.mjs back into bus.mjs | — |
|
||||
| 302 | Idle-stop race: an idle-released run's last call ends it session-invalid/22 (Vikunja 588) | gizmoduck; reviewers dewey | #1809 | briefed | 292 done | Dewey approves the design, then the manifest; forcing test, compactions-launch.test whole, bus/cli/harness suites, all scripts/test-*.sh; fix-drop mutant dies (sage) | `docs/plans/2026-10-23_idle-stop-race-row.md` § Brief: the idle-stop race (Vikunja 588) | — |
|
||||
| 302 | Idle-stop race: an idle-released run's last call ends it session-invalid/22 (Vikunja 588) | gizmoduck; reviewers dewey | #1809 | in-progress | 292 done | Dewey approves the design, then the manifest; forcing test, compactions-launch.test whole, bus/cli/harness suites, all scripts/test-*.sh; fix-drop mutant dies (sage) | `docs/plans/2026-10-23_idle-stop-race-row.md` § Brief: the idle-stop race (Vikunja 588) | — |
|
||||
| 303 | R9 shows its output when rehearse makes no directory (Vikunja 589) | launchpad; reviewers duckworth | #1810 | briefed | 294 done | Duckworth approves the diff; stack-release.test.mjs whole and green on Sage's gate, in a gap (sage) | `docs/plans/2026-10-23_r9-guard-row.md` § Brief: R9 shows its output when rehearse makes no directory (Vikunja 589) | — |
|
||||
|
||||
<!-- mosaic-queue:end -->
|
||||
|
||||
|
||||
+858
-14
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"canonicalRoot": "/mnt/storage/src/mosaic-stack",
|
||||
"revision": 3447,
|
||||
"revision": 3473,
|
||||
"rows": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -23376,10 +23376,10 @@
|
||||
"seat": "rocko",
|
||||
"op": "rocko-219-claim-4a9c-bare"
|
||||
},
|
||||
"note": null,
|
||||
"note": "sage-3224 restart: claimant rocko resumes b97a3ff9757ec2ac39d9ab4cebb9769e471df932, manifest a973912fe2fb9c64606cd99a385305b8f51498a096511b3a980bca6b0a5b98aa; full rerun under mos-1408, no prior test carry",
|
||||
"blockedReason": null,
|
||||
"createdAt": "2026-10-20T20:49:21.233Z",
|
||||
"updatedAt": "2026-10-23T04:06:34.702Z",
|
||||
"updatedAt": "2026-10-23T05:48:31.319Z",
|
||||
"updatedBy": "rocko"
|
||||
},
|
||||
{
|
||||
@@ -29590,7 +29590,7 @@
|
||||
"closes": [
|
||||
1800
|
||||
],
|
||||
"state": "in-review",
|
||||
"state": "done",
|
||||
"previousState": null,
|
||||
"required": false,
|
||||
"requiredSince": null,
|
||||
@@ -29660,18 +29660,191 @@
|
||||
"candidate": "0b3f9c9de7438a66fa0aee4e813a05d7a85fbb650f451d015a1ea041ca2794f3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": 2,
|
||||
"op": "launchpad-0068",
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T05:25:28.273Z",
|
||||
"issue": 1800,
|
||||
"candidate": {
|
||||
"kind": "manifest",
|
||||
"digest": "7ec7b1eae074b9143aeca98c82a5e8be43aff79e36ae88344ac7a93c10b3315b",
|
||||
"text": "52206bd92b0f223bf9310d50db650f36e12836691d739b499192e3eedd81401c packages/cli/src/access.mjs\na948886a8c57d700b3d116097c9597c2b29dc7cc89ff6e18ae57436754195853 packages/cli/src/config.mjs\nabb5ccb77b8d5c97c81e84fb1fb32367bf69890e8ea1f1912d739a3dd1678738 packages/cli/tests/access-boot.test.mjs\n1be61ba66c3b747decdb3e12de743265b8cc7ba7cd06c033393b157ce1bc0b70 packages/webui/src/access.mjs\n954dee1535b258d9590e4d102423d081303b640b3bea750bd3653883e67bf309 packages/webui/src/chat-http.mjs\n230a5759f5027f9208c716b176f9b0a30310d5f433493bfcd8862c71abe182ae packages/webui/src/chat.mjs\n7fcb28a7a67418a8d3fd26dd157860ec1a89fe1891887f661e53a17d3b3bbc7d packages/webui/src/cli.mjs\n941c8f91ad40264c5622d60af85e00f6198db31a0504b4b0caa48312d8f57c3e packages/webui/src/console-bus.mjs\n692c5eabb0fc866240b12931418c17d3392b6097575dfacf7425a6f0127dd291 packages/webui/src/public/app.js\nc41a34a32f23f742e30ca408bc7deda0fa8feb4f8c25c7036604c336bb1545eb packages/webui/src/public/federation.js\ne7673188c1d76ecc63cdc24e7d1470922b1d69bedeb994a862e94924edfc27d2 packages/webui/src/public/index.html\n969a52284b667e075b04e3fa63d45415f3517080784ee372ee4725caf51d15d9 packages/webui/src/public/user.html\n571ce191c0b5f545e3a65f7b388f754e2ca6396a68038ccc57321d8f490cb86e packages/webui/src/public/user.js\n9343ea9edb862c1545df2886754244ffd76b714cc90d7791fc32aee4ecfec594 packages/webui/src/serve.mjs\n70b243ed9a83ff822a4c7a148528ff5153574cc87d5c776fd2e12f340762ed76 packages/webui/src/sso.mjs\n8cc1185beda9085ced62470f1b9e39d95dcca2dcc8bf9325c18c2f0b2da618dc packages/webui/src/user-bus.mjs\n58f022643839b78c0bd149927157d1f7b36977212975a4fe549c9661c31c4e40 packages/webui/tests/browser.test.mjs\n8bf20e514817d21356e59ca21a8a99ee25609f084f4a26c5a409d71a90b6f6a4 packages/webui/tests/granted-user.test.mjs\n"
|
||||
},
|
||||
"request": "comment",
|
||||
"attempts": [
|
||||
{
|
||||
"op": "launchpad-0068",
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T05:25:28.273Z",
|
||||
"state": "failed",
|
||||
"comment": null,
|
||||
"transport": {
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"resolutions": []
|
||||
},
|
||||
{
|
||||
"op": "sage-294-request-r2",
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T05:27:35.711Z",
|
||||
"state": "failed",
|
||||
"comment": null,
|
||||
"transport": {
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"resolutions": []
|
||||
},
|
||||
{
|
||||
"op": "sage-294-request-r2b",
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T05:29:16.284Z",
|
||||
"state": "posted",
|
||||
"comment": 31042,
|
||||
"transport": {
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 31042,
|
||||
"detail": "created"
|
||||
},
|
||||
"resolutions": []
|
||||
}
|
||||
],
|
||||
"duplicateRisk": false,
|
||||
"receipts": [
|
||||
{
|
||||
"reviewer": "duckworth",
|
||||
"op": "duckworth-0253-record",
|
||||
"at": "2026-10-23T05:33:13.294Z",
|
||||
"verdict": "approve",
|
||||
"comment": 31029,
|
||||
"candidate": "7ec7b1eae074b9143aeca98c82a5e8be43aff79e36ae88344ac7a93c10b3315b"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": 3,
|
||||
"op": "launchpad-0074",
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T05:41:56.570Z",
|
||||
"issue": 1800,
|
||||
"candidate": {
|
||||
"kind": "manifest",
|
||||
"digest": "cb1648f3e67a53d36b0cb344f8fc86bd7020e2018121c1b56be8eb3893e378de",
|
||||
"text": "52206bd92b0f223bf9310d50db650f36e12836691d739b499192e3eedd81401c packages/cli/src/access.mjs\na948886a8c57d700b3d116097c9597c2b29dc7cc89ff6e18ae57436754195853 packages/cli/src/config.mjs\nabb5ccb77b8d5c97c81e84fb1fb32367bf69890e8ea1f1912d739a3dd1678738 packages/cli/tests/access-boot.test.mjs\n1be61ba66c3b747decdb3e12de743265b8cc7ba7cd06c033393b157ce1bc0b70 packages/webui/src/access.mjs\n954dee1535b258d9590e4d102423d081303b640b3bea750bd3653883e67bf309 packages/webui/src/chat-http.mjs\n230a5759f5027f9208c716b176f9b0a30310d5f433493bfcd8862c71abe182ae packages/webui/src/chat.mjs\n7fcb28a7a67418a8d3fd26dd157860ec1a89fe1891887f661e53a17d3b3bbc7d packages/webui/src/cli.mjs\n941c8f91ad40264c5622d60af85e00f6198db31a0504b4b0caa48312d8f57c3e packages/webui/src/console-bus.mjs\n692c5eabb0fc866240b12931418c17d3392b6097575dfacf7425a6f0127dd291 packages/webui/src/public/app.js\nc41a34a32f23f742e30ca408bc7deda0fa8feb4f8c25c7036604c336bb1545eb packages/webui/src/public/federation.js\ne7673188c1d76ecc63cdc24e7d1470922b1d69bedeb994a862e94924edfc27d2 packages/webui/src/public/index.html\n969a52284b667e075b04e3fa63d45415f3517080784ee372ee4725caf51d15d9 packages/webui/src/public/user.html\n571ce191c0b5f545e3a65f7b388f754e2ca6396a68038ccc57321d8f490cb86e packages/webui/src/public/user.js\n9343ea9edb862c1545df2886754244ffd76b714cc90d7791fc32aee4ecfec594 packages/webui/src/serve.mjs\n70b243ed9a83ff822a4c7a148528ff5153574cc87d5c776fd2e12f340762ed76 packages/webui/src/sso.mjs\n8cc1185beda9085ced62470f1b9e39d95dcca2dcc8bf9325c18c2f0b2da618dc packages/webui/src/user-bus.mjs\n58f022643839b78c0bd149927157d1f7b36977212975a4fe549c9661c31c4e40 packages/webui/tests/browser.test.mjs\n673f09560b7cf4515856ab1181fce8fcc2f70d59fb01bda9d687cc3d9b259e95 packages/webui/tests/granted-user.test.mjs\n"
|
||||
},
|
||||
"request": "comment",
|
||||
"attempts": [
|
||||
{
|
||||
"op": "launchpad-0074",
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T05:41:56.570Z",
|
||||
"state": "failed",
|
||||
"comment": null,
|
||||
"transport": {
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"resolutions": []
|
||||
},
|
||||
{
|
||||
"op": "sage-294-request-r3",
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T05:44:07.281Z",
|
||||
"state": "posted",
|
||||
"comment": 31049,
|
||||
"transport": {
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 31049,
|
||||
"detail": "created"
|
||||
},
|
||||
"resolutions": []
|
||||
}
|
||||
],
|
||||
"duplicateRisk": false,
|
||||
"receipts": [
|
||||
{
|
||||
"reviewer": "duckworth",
|
||||
"op": "duckworth-0255-record",
|
||||
"at": "2026-10-23T05:46:03.199Z",
|
||||
"verdict": "approve",
|
||||
"comment": 31045,
|
||||
"candidate": "cb1648f3e67a53d36b0cb344f8fc86bd7020e2018121c1b56be8eb3893e378de"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": 4,
|
||||
"op": "launchpad-0082",
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T06:15:11.051Z",
|
||||
"issue": 1800,
|
||||
"candidate": {
|
||||
"kind": "manifest",
|
||||
"digest": "a1ff5d67a2ac303ac693a97b4902d607d295415ac4c278082e2078fe9ca53b99",
|
||||
"text": "52206bd92b0f223bf9310d50db650f36e12836691d739b499192e3eedd81401c packages/cli/src/access.mjs\na948886a8c57d700b3d116097c9597c2b29dc7cc89ff6e18ae57436754195853 packages/cli/src/config.mjs\nb2745e1a3067d8c439de4f9c9e0df532764d3ea77639d15e5b9d949ec5cd534d packages/cli/tests/access-boot.test.mjs\n96f239dfd25cd1408d7586016154798da42b1a0f32387d14705345d0aa4db2ce packages/cli/tests/stack-release.test.mjs\n1be61ba66c3b747decdb3e12de743265b8cc7ba7cd06c033393b157ce1bc0b70 packages/webui/src/access.mjs\n954dee1535b258d9590e4d102423d081303b640b3bea750bd3653883e67bf309 packages/webui/src/chat-http.mjs\n230a5759f5027f9208c716b176f9b0a30310d5f433493bfcd8862c71abe182ae packages/webui/src/chat.mjs\n7fcb28a7a67418a8d3fd26dd157860ec1a89fe1891887f661e53a17d3b3bbc7d packages/webui/src/cli.mjs\n941c8f91ad40264c5622d60af85e00f6198db31a0504b4b0caa48312d8f57c3e packages/webui/src/console-bus.mjs\n692c5eabb0fc866240b12931418c17d3392b6097575dfacf7425a6f0127dd291 packages/webui/src/public/app.js\nc41a34a32f23f742e30ca408bc7deda0fa8feb4f8c25c7036604c336bb1545eb packages/webui/src/public/federation.js\ne7673188c1d76ecc63cdc24e7d1470922b1d69bedeb994a862e94924edfc27d2 packages/webui/src/public/index.html\n969a52284b667e075b04e3fa63d45415f3517080784ee372ee4725caf51d15d9 packages/webui/src/public/user.html\n571ce191c0b5f545e3a65f7b388f754e2ca6396a68038ccc57321d8f490cb86e packages/webui/src/public/user.js\n9343ea9edb862c1545df2886754244ffd76b714cc90d7791fc32aee4ecfec594 packages/webui/src/serve.mjs\n70b243ed9a83ff822a4c7a148528ff5153574cc87d5c776fd2e12f340762ed76 packages/webui/src/sso.mjs\n8cc1185beda9085ced62470f1b9e39d95dcca2dcc8bf9325c18c2f0b2da618dc packages/webui/src/user-bus.mjs\n58f022643839b78c0bd149927157d1f7b36977212975a4fe549c9661c31c4e40 packages/webui/tests/browser.test.mjs\n673f09560b7cf4515856ab1181fce8fcc2f70d59fb01bda9d687cc3d9b259e95 packages/webui/tests/granted-user.test.mjs\n"
|
||||
},
|
||||
"request": "comment",
|
||||
"attempts": [
|
||||
{
|
||||
"op": "launchpad-0082",
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T06:15:11.051Z",
|
||||
"state": "failed",
|
||||
"comment": null,
|
||||
"transport": {
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"resolutions": []
|
||||
},
|
||||
{
|
||||
"op": "sage-294-request-r4",
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T06:17:21.470Z",
|
||||
"state": "posted",
|
||||
"comment": 31055,
|
||||
"transport": {
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 31055,
|
||||
"detail": "created"
|
||||
},
|
||||
"resolutions": []
|
||||
}
|
||||
],
|
||||
"duplicateRisk": false,
|
||||
"receipts": [
|
||||
{
|
||||
"reviewer": "duckworth",
|
||||
"op": "duckworth-0257-record",
|
||||
"at": "2026-10-23T06:19:14.480Z",
|
||||
"verdict": "approve",
|
||||
"comment": 31053,
|
||||
"candidate": "a1ff5d67a2ac303ac693a97b4902d607d295415ac4c278082e2078fe9ca53b99"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"claim": {
|
||||
"seat": "launchpad",
|
||||
"op": "launchpad-0051"
|
||||
},
|
||||
"note": null,
|
||||
"claim": null,
|
||||
"note": "landed d3059c2c3 on refactor 07:04Z: cherry-pick of c902a767d..24b745447 onto 09d0d6faf, range-diff 11 =, patch-id 8371405a equal. Duckworth approve c31053 rev 3469 on a1ff5d67, C1/C2 met. Run 4 on 8616b9a4f from npm ci: webui 365, cli 909, bus 873, 10 scripts, M1-M11 killed. Merged tree: stack-release-external 25, stack-upgrade 21, stack-release 223/224 (R9 transient, row 1810) then 224/224 rerun under mos-1422. Follow-ups rows 300, 301. Closes #1800.",
|
||||
"blockedReason": null,
|
||||
"createdAt": "2026-10-23T01:52:56.603Z",
|
||||
"updatedAt": "2026-10-23T03:35:03.475Z",
|
||||
"updatedBy": "duckworth"
|
||||
"updatedAt": "2026-10-23T07:06:04.303Z",
|
||||
"updatedBy": "sage"
|
||||
},
|
||||
{
|
||||
"id": 295,
|
||||
@@ -30016,7 +30189,7 @@
|
||||
"closes": [
|
||||
1809
|
||||
],
|
||||
"state": "briefed",
|
||||
"state": "in-progress",
|
||||
"previousState": null,
|
||||
"required": false,
|
||||
"requiredSince": null,
|
||||
@@ -30037,11 +30210,52 @@
|
||||
"dewey"
|
||||
],
|
||||
"review": null,
|
||||
"claim": null,
|
||||
"claim": {
|
||||
"seat": "gizmoduck",
|
||||
"op": "gizmoduck-302-start-1"
|
||||
},
|
||||
"note": null,
|
||||
"blockedReason": null,
|
||||
"createdAt": "2026-10-23T05:18:48.280Z",
|
||||
"updatedAt": "2026-10-23T05:19:30.723Z",
|
||||
"updatedAt": "2026-10-23T05:37:37.179Z",
|
||||
"updatedBy": "gizmoduck"
|
||||
},
|
||||
{
|
||||
"id": 303,
|
||||
"piece": "R9 shows its output when rehearse makes no directory (Vikunja 589)",
|
||||
"owner": "launchpad",
|
||||
"issues": [
|
||||
1810
|
||||
],
|
||||
"closes": [
|
||||
1810
|
||||
],
|
||||
"state": "briefed",
|
||||
"previousState": null,
|
||||
"required": false,
|
||||
"requiredSince": null,
|
||||
"gate": "Duckworth approves the diff; stack-release.test.mjs whole and green on Sage's gate, in a gap",
|
||||
"gateOwner": "sage",
|
||||
"brief": {
|
||||
"path": "docs/plans/2026-10-23_r9-guard-row.md",
|
||||
"anchor": "Brief: R9 shows its output when rehearse makes no directory (Vikunja 589)",
|
||||
"blob": "d1f91b74e5f6c3e0d6a06f4bccaa2199d8090e13"
|
||||
},
|
||||
"after": [
|
||||
{
|
||||
"id": 294,
|
||||
"when": "done"
|
||||
}
|
||||
],
|
||||
"reviewers": [
|
||||
"duckworth"
|
||||
],
|
||||
"review": null,
|
||||
"claim": null,
|
||||
"note": null,
|
||||
"blockedReason": null,
|
||||
"createdAt": "2026-10-23T07:07:24.046Z",
|
||||
"updatedAt": "2026-10-23T07:08:10.756Z",
|
||||
"updatedBy": "sage"
|
||||
}
|
||||
],
|
||||
@@ -120863,6 +121077,636 @@
|
||||
"receipt": "ok sage-292-done rev 3447 row 292 in-review→done round 1"
|
||||
},
|
||||
"viewSha": "b0d32409d45fb79fa58f69018578255b4d7505d750f93cf0e4b5411cf6a1a7cc"
|
||||
},
|
||||
{
|
||||
"rev": 3448,
|
||||
"op": "launchpad-0067",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"to": "in-progress",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T05:24:42.639Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"from": "in-review",
|
||||
"to": "in-progress",
|
||||
"round": null,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok launchpad-0067 rev 3448 row 294 in-review→in-progress"
|
||||
},
|
||||
"viewSha": "932561329ef4c5f53f964e65032633e9afd77f311d77749ff1ae1390749aea81"
|
||||
},
|
||||
{
|
||||
"rev": 3449,
|
||||
"op": "launchpad-0068",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"to": "in-review",
|
||||
"reason": null,
|
||||
"candidate": "/mnt/storage/scratch/launchpad-r294/wt/agents/launchpad/work/queue-294/MANIFEST.sha256",
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T05:25:28.273Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"from": "in-progress",
|
||||
"to": "in-review",
|
||||
"round": 2,
|
||||
"issue": 1800,
|
||||
"candidate": {
|
||||
"kind": "manifest",
|
||||
"digest": "7ec7b1eae074b9143aeca98c82a5e8be43aff79e36ae88344ac7a93c10b3315b",
|
||||
"text": "52206bd92b0f223bf9310d50db650f36e12836691d739b499192e3eedd81401c packages/cli/src/access.mjs\na948886a8c57d700b3d116097c9597c2b29dc7cc89ff6e18ae57436754195853 packages/cli/src/config.mjs\nabb5ccb77b8d5c97c81e84fb1fb32367bf69890e8ea1f1912d739a3dd1678738 packages/cli/tests/access-boot.test.mjs\n1be61ba66c3b747decdb3e12de743265b8cc7ba7cd06c033393b157ce1bc0b70 packages/webui/src/access.mjs\n954dee1535b258d9590e4d102423d081303b640b3bea750bd3653883e67bf309 packages/webui/src/chat-http.mjs\n230a5759f5027f9208c716b176f9b0a30310d5f433493bfcd8862c71abe182ae packages/webui/src/chat.mjs\n7fcb28a7a67418a8d3fd26dd157860ec1a89fe1891887f661e53a17d3b3bbc7d packages/webui/src/cli.mjs\n941c8f91ad40264c5622d60af85e00f6198db31a0504b4b0caa48312d8f57c3e packages/webui/src/console-bus.mjs\n692c5eabb0fc866240b12931418c17d3392b6097575dfacf7425a6f0127dd291 packages/webui/src/public/app.js\nc41a34a32f23f742e30ca408bc7deda0fa8feb4f8c25c7036604c336bb1545eb packages/webui/src/public/federation.js\ne7673188c1d76ecc63cdc24e7d1470922b1d69bedeb994a862e94924edfc27d2 packages/webui/src/public/index.html\n969a52284b667e075b04e3fa63d45415f3517080784ee372ee4725caf51d15d9 packages/webui/src/public/user.html\n571ce191c0b5f545e3a65f7b388f754e2ca6396a68038ccc57321d8f490cb86e packages/webui/src/public/user.js\n9343ea9edb862c1545df2886754244ffd76b714cc90d7791fc32aee4ecfec594 packages/webui/src/serve.mjs\n70b243ed9a83ff822a4c7a148528ff5153574cc87d5c776fd2e12f340762ed76 packages/webui/src/sso.mjs\n8cc1185beda9085ced62470f1b9e39d95dcca2dcc8bf9325c18c2f0b2da618dc packages/webui/src/user-bus.mjs\n58f022643839b78c0bd149927157d1f7b36977212975a4fe549c9661c31c4e40 packages/webui/tests/browser.test.mjs\n8bf20e514817d21356e59ca21a8a99ee25609f084f4a26c5a409d71a90b6f6a4 packages/webui/tests/granted-user.test.mjs\n"
|
||||
},
|
||||
"receipt": "ok launchpad-0068 rev 3449 row 294 in-progress→in-review round 2 on #1800; request launchpad-0068 requesting"
|
||||
},
|
||||
"viewSha": "fc10652a3a63af980e00b6117f3171b8beee545ad8dd55579549e8a6a5e779ff"
|
||||
},
|
||||
{
|
||||
"rev": 3450,
|
||||
"op": "launchpad-0068.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"attempt": "launchpad-0068",
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T05:26:11.810Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"round": 2,
|
||||
"attempt": "launchpad-0068",
|
||||
"state": "failed",
|
||||
"receipt": "ok launchpad-0068.outcome rev 3450 row 294 round 2 request launchpad-0068 failed"
|
||||
},
|
||||
"viewSha": "bd3667fd9ffd9a188d35ab2e06a4de1d488cbf56b7913b3f3abf08ada95ee4e1"
|
||||
},
|
||||
{
|
||||
"rev": 3451,
|
||||
"op": "sage-294-request-r2",
|
||||
"verb": "review-request",
|
||||
"args": {
|
||||
"id": 294
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T05:27:35.711Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"round": 2,
|
||||
"attempt": "sage-294-request-r2",
|
||||
"state": "requesting",
|
||||
"receipt": "ok sage-294-request-r2 rev 3451 row 294 round 2 request sage-294-request-r2 requesting"
|
||||
},
|
||||
"viewSha": "1ca64da7ce30b02e52733760f954e68edd711f6d7c3baa559441a5ec9d04d3e1"
|
||||
},
|
||||
{
|
||||
"rev": 3452,
|
||||
"op": "sage-294-request-r2.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"attempt": "sage-294-request-r2",
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T05:28:19.179Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"round": 2,
|
||||
"attempt": "sage-294-request-r2",
|
||||
"state": "failed",
|
||||
"receipt": "ok sage-294-request-r2.outcome rev 3452 row 294 round 2 request sage-294-request-r2 failed"
|
||||
},
|
||||
"viewSha": "bfb8c88d12acccc7da90bb31d525120a8688eae88921a26e47d611d04a8f0cc3"
|
||||
},
|
||||
{
|
||||
"rev": 3453,
|
||||
"op": "sage-294-request-r2b",
|
||||
"verb": "review-request",
|
||||
"args": {
|
||||
"id": 294
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T05:29:16.284Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"round": 2,
|
||||
"attempt": "sage-294-request-r2b",
|
||||
"state": "requesting",
|
||||
"receipt": "ok sage-294-request-r2b rev 3453 row 294 round 2 request sage-294-request-r2b requesting"
|
||||
},
|
||||
"viewSha": "73f9c773b76688af61c929188af282bd9ede83157ea04d2937b591bd18026556"
|
||||
},
|
||||
{
|
||||
"rev": 3454,
|
||||
"op": "sage-294-request-r2b.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"attempt": "sage-294-request-r2b",
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 31042,
|
||||
"detail": "created"
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T05:30:03.778Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"round": 2,
|
||||
"attempt": "sage-294-request-r2b",
|
||||
"state": "posted",
|
||||
"receipt": "ok sage-294-request-r2b.outcome rev 3454 row 294 round 2 request sage-294-request-r2b posted comment 31042"
|
||||
},
|
||||
"viewSha": "fc66cb56b0db295ca125a696e7e2a838c5ffbd3cc0fe8950dc67a49952326e12"
|
||||
},
|
||||
{
|
||||
"rev": 3455,
|
||||
"op": "duckworth-0253-record",
|
||||
"verb": "review-record",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"verdict": "approve",
|
||||
"comment": 31029,
|
||||
"candidate": "7ec7b1eae074b9143aeca98c82a5e8be43aff79e36ae88344ac7a93c10b3315b"
|
||||
},
|
||||
"by": "duckworth",
|
||||
"at": "2026-10-23T05:33:13.294Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"round": 2,
|
||||
"verdict": "approve",
|
||||
"receipt": "ok duckworth-0253-record rev 3455 row 294 round 2 approve by duckworth (comment 31029)"
|
||||
},
|
||||
"viewSha": "66b577e2b23ba773fd0acf682876bcb2f5d8b5b659f130f4a1214d2eb4e4d7e7"
|
||||
},
|
||||
{
|
||||
"rev": 3456,
|
||||
"op": "gizmoduck-302-start-1",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 302,
|
||||
"to": "in-progress",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "gizmoduck",
|
||||
"at": "2026-10-23T05:37:37.179Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 302,
|
||||
"from": "briefed",
|
||||
"to": "in-progress",
|
||||
"round": null,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok gizmoduck-302-start-1 rev 3456 row 302 briefed→in-progress"
|
||||
},
|
||||
"viewSha": "ae83465e13184b0e566e05d97fff393f8ed5c1433492d7bff9fed4eecc5af59f"
|
||||
},
|
||||
{
|
||||
"rev": 3457,
|
||||
"op": "launchpad-0073",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"to": "in-progress",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T05:41:13.327Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"from": "in-review",
|
||||
"to": "in-progress",
|
||||
"round": null,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok launchpad-0073 rev 3457 row 294 in-review→in-progress"
|
||||
},
|
||||
"viewSha": "1e9b559cea27976700e3b87df434ca86dd2cd234ce8ec41306901def074873ca"
|
||||
},
|
||||
{
|
||||
"rev": 3458,
|
||||
"op": "launchpad-0074",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"to": "in-review",
|
||||
"reason": null,
|
||||
"candidate": "/mnt/storage/scratch/launchpad-r294/wt/agents/launchpad/work/queue-294/MANIFEST.sha256",
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T05:41:56.570Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"from": "in-progress",
|
||||
"to": "in-review",
|
||||
"round": 3,
|
||||
"issue": 1800,
|
||||
"candidate": {
|
||||
"kind": "manifest",
|
||||
"digest": "cb1648f3e67a53d36b0cb344f8fc86bd7020e2018121c1b56be8eb3893e378de",
|
||||
"text": "52206bd92b0f223bf9310d50db650f36e12836691d739b499192e3eedd81401c packages/cli/src/access.mjs\na948886a8c57d700b3d116097c9597c2b29dc7cc89ff6e18ae57436754195853 packages/cli/src/config.mjs\nabb5ccb77b8d5c97c81e84fb1fb32367bf69890e8ea1f1912d739a3dd1678738 packages/cli/tests/access-boot.test.mjs\n1be61ba66c3b747decdb3e12de743265b8cc7ba7cd06c033393b157ce1bc0b70 packages/webui/src/access.mjs\n954dee1535b258d9590e4d102423d081303b640b3bea750bd3653883e67bf309 packages/webui/src/chat-http.mjs\n230a5759f5027f9208c716b176f9b0a30310d5f433493bfcd8862c71abe182ae packages/webui/src/chat.mjs\n7fcb28a7a67418a8d3fd26dd157860ec1a89fe1891887f661e53a17d3b3bbc7d packages/webui/src/cli.mjs\n941c8f91ad40264c5622d60af85e00f6198db31a0504b4b0caa48312d8f57c3e packages/webui/src/console-bus.mjs\n692c5eabb0fc866240b12931418c17d3392b6097575dfacf7425a6f0127dd291 packages/webui/src/public/app.js\nc41a34a32f23f742e30ca408bc7deda0fa8feb4f8c25c7036604c336bb1545eb packages/webui/src/public/federation.js\ne7673188c1d76ecc63cdc24e7d1470922b1d69bedeb994a862e94924edfc27d2 packages/webui/src/public/index.html\n969a52284b667e075b04e3fa63d45415f3517080784ee372ee4725caf51d15d9 packages/webui/src/public/user.html\n571ce191c0b5f545e3a65f7b388f754e2ca6396a68038ccc57321d8f490cb86e packages/webui/src/public/user.js\n9343ea9edb862c1545df2886754244ffd76b714cc90d7791fc32aee4ecfec594 packages/webui/src/serve.mjs\n70b243ed9a83ff822a4c7a148528ff5153574cc87d5c776fd2e12f340762ed76 packages/webui/src/sso.mjs\n8cc1185beda9085ced62470f1b9e39d95dcca2dcc8bf9325c18c2f0b2da618dc packages/webui/src/user-bus.mjs\n58f022643839b78c0bd149927157d1f7b36977212975a4fe549c9661c31c4e40 packages/webui/tests/browser.test.mjs\n673f09560b7cf4515856ab1181fce8fcc2f70d59fb01bda9d687cc3d9b259e95 packages/webui/tests/granted-user.test.mjs\n"
|
||||
},
|
||||
"receipt": "ok launchpad-0074 rev 3458 row 294 in-progress→in-review round 3 on #1800; request launchpad-0074 requesting"
|
||||
},
|
||||
"viewSha": "db386f1e2ae819fa7fe0f69453779e9664b59efe17421c5d63319b2fcb443b88"
|
||||
},
|
||||
{
|
||||
"rev": 3459,
|
||||
"op": "launchpad-0074.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"attempt": "launchpad-0074",
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T05:42:38.434Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"round": 3,
|
||||
"attempt": "launchpad-0074",
|
||||
"state": "failed",
|
||||
"receipt": "ok launchpad-0074.outcome rev 3459 row 294 round 3 request launchpad-0074 failed"
|
||||
},
|
||||
"viewSha": "40008e73bad7c4fed7a3e29435eab5da0af93921c6564259a58ec976a21ace02"
|
||||
},
|
||||
{
|
||||
"rev": 3460,
|
||||
"op": "sage-294-request-r3",
|
||||
"verb": "review-request",
|
||||
"args": {
|
||||
"id": 294
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T05:44:07.281Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"round": 3,
|
||||
"attempt": "sage-294-request-r3",
|
||||
"state": "requesting",
|
||||
"receipt": "ok sage-294-request-r3 rev 3460 row 294 round 3 request sage-294-request-r3 requesting"
|
||||
},
|
||||
"viewSha": "d319ec6a487978364f24aaaac2c32d61bee9e2e960165ee94417173ba2e03187"
|
||||
},
|
||||
{
|
||||
"rev": 3461,
|
||||
"op": "sage-294-request-r3.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"attempt": "sage-294-request-r3",
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 31049,
|
||||
"detail": "created"
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T05:44:52.118Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"round": 3,
|
||||
"attempt": "sage-294-request-r3",
|
||||
"state": "posted",
|
||||
"receipt": "ok sage-294-request-r3.outcome rev 3461 row 294 round 3 request sage-294-request-r3 posted comment 31049"
|
||||
},
|
||||
"viewSha": "750c545f2fdf03ad7913e3c606bd634fc1039e8dda771e739f060bc335acf6a2"
|
||||
},
|
||||
{
|
||||
"rev": 3462,
|
||||
"op": "duckworth-0255-record",
|
||||
"verb": "review-record",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"verdict": "approve",
|
||||
"comment": 31045,
|
||||
"candidate": "cb1648f3e67a53d36b0cb344f8fc86bd7020e2018121c1b56be8eb3893e378de"
|
||||
},
|
||||
"by": "duckworth",
|
||||
"at": "2026-10-23T05:46:03.199Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"round": 3,
|
||||
"verdict": "approve",
|
||||
"receipt": "ok duckworth-0255-record rev 3462 row 294 round 3 approve by duckworth (comment 31045)"
|
||||
},
|
||||
"viewSha": "be0d2b080e849ee52dbcb329a08b30b36a0613791eaf947535e36079aa521b2e"
|
||||
},
|
||||
{
|
||||
"rev": 3463,
|
||||
"op": "rocko-219-reclaim-b97a-mos1408",
|
||||
"verb": "note",
|
||||
"args": {
|
||||
"id": 219,
|
||||
"text": "sage-3224 restart: claimant rocko resumes b97a3ff9757ec2ac39d9ab4cebb9769e471df932, manifest a973912fe2fb9c64606cd99a385305b8f51498a096511b3a980bca6b0a5b98aa; full rerun under mos-1408, no prior test carry"
|
||||
},
|
||||
"by": "rocko",
|
||||
"at": "2026-10-23T05:48:31.319Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 219,
|
||||
"field": "note",
|
||||
"receipt": "ok rocko-219-reclaim-b97a-mos1408 rev 3463 row 219 note"
|
||||
},
|
||||
"viewSha": "d4a3d5799b47b340f22802e079a901606f173b76980472218ce5ab553cec2574"
|
||||
},
|
||||
{
|
||||
"rev": 3464,
|
||||
"op": "launchpad-0081",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"to": "in-progress",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T06:14:28.283Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"from": "in-review",
|
||||
"to": "in-progress",
|
||||
"round": null,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok launchpad-0081 rev 3464 row 294 in-review→in-progress"
|
||||
},
|
||||
"viewSha": "926d0ad09dc224e9a1f5f387716041d2a1e9f88f3f544a3b58662b8e4c13fce8"
|
||||
},
|
||||
{
|
||||
"rev": 3465,
|
||||
"op": "launchpad-0082",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"to": "in-review",
|
||||
"reason": null,
|
||||
"candidate": "/mnt/storage/scratch/launchpad-r294/wt/agents/launchpad/work/queue-294/MANIFEST.sha256",
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T06:15:11.051Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"from": "in-progress",
|
||||
"to": "in-review",
|
||||
"round": 4,
|
||||
"issue": 1800,
|
||||
"candidate": {
|
||||
"kind": "manifest",
|
||||
"digest": "a1ff5d67a2ac303ac693a97b4902d607d295415ac4c278082e2078fe9ca53b99",
|
||||
"text": "52206bd92b0f223bf9310d50db650f36e12836691d739b499192e3eedd81401c packages/cli/src/access.mjs\na948886a8c57d700b3d116097c9597c2b29dc7cc89ff6e18ae57436754195853 packages/cli/src/config.mjs\nb2745e1a3067d8c439de4f9c9e0df532764d3ea77639d15e5b9d949ec5cd534d packages/cli/tests/access-boot.test.mjs\n96f239dfd25cd1408d7586016154798da42b1a0f32387d14705345d0aa4db2ce packages/cli/tests/stack-release.test.mjs\n1be61ba66c3b747decdb3e12de743265b8cc7ba7cd06c033393b157ce1bc0b70 packages/webui/src/access.mjs\n954dee1535b258d9590e4d102423d081303b640b3bea750bd3653883e67bf309 packages/webui/src/chat-http.mjs\n230a5759f5027f9208c716b176f9b0a30310d5f433493bfcd8862c71abe182ae packages/webui/src/chat.mjs\n7fcb28a7a67418a8d3fd26dd157860ec1a89fe1891887f661e53a17d3b3bbc7d packages/webui/src/cli.mjs\n941c8f91ad40264c5622d60af85e00f6198db31a0504b4b0caa48312d8f57c3e packages/webui/src/console-bus.mjs\n692c5eabb0fc866240b12931418c17d3392b6097575dfacf7425a6f0127dd291 packages/webui/src/public/app.js\nc41a34a32f23f742e30ca408bc7deda0fa8feb4f8c25c7036604c336bb1545eb packages/webui/src/public/federation.js\ne7673188c1d76ecc63cdc24e7d1470922b1d69bedeb994a862e94924edfc27d2 packages/webui/src/public/index.html\n969a52284b667e075b04e3fa63d45415f3517080784ee372ee4725caf51d15d9 packages/webui/src/public/user.html\n571ce191c0b5f545e3a65f7b388f754e2ca6396a68038ccc57321d8f490cb86e packages/webui/src/public/user.js\n9343ea9edb862c1545df2886754244ffd76b714cc90d7791fc32aee4ecfec594 packages/webui/src/serve.mjs\n70b243ed9a83ff822a4c7a148528ff5153574cc87d5c776fd2e12f340762ed76 packages/webui/src/sso.mjs\n8cc1185beda9085ced62470f1b9e39d95dcca2dcc8bf9325c18c2f0b2da618dc packages/webui/src/user-bus.mjs\n58f022643839b78c0bd149927157d1f7b36977212975a4fe549c9661c31c4e40 packages/webui/tests/browser.test.mjs\n673f09560b7cf4515856ab1181fce8fcc2f70d59fb01bda9d687cc3d9b259e95 packages/webui/tests/granted-user.test.mjs\n"
|
||||
},
|
||||
"receipt": "ok launchpad-0082 rev 3465 row 294 in-progress→in-review round 4 on #1800; request launchpad-0082 requesting"
|
||||
},
|
||||
"viewSha": "2c992abd7484303eba8ee81722770c75a0b02c67ea0cbe65546b874c0623775c"
|
||||
},
|
||||
{
|
||||
"rev": 3466,
|
||||
"op": "launchpad-0082.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"attempt": "launchpad-0082",
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"by": "launchpad",
|
||||
"at": "2026-10-23T06:15:55.171Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"round": 4,
|
||||
"attempt": "launchpad-0082",
|
||||
"state": "failed",
|
||||
"receipt": "ok launchpad-0082.outcome rev 3466 row 294 round 4 request launchpad-0082 failed"
|
||||
},
|
||||
"viewSha": "4ad7a8668b36d3a0f87961c5c2169736e4c364a2bbebe053dc7696777c674424"
|
||||
},
|
||||
{
|
||||
"rev": 3467,
|
||||
"op": "sage-294-request-r4",
|
||||
"verb": "review-request",
|
||||
"args": {
|
||||
"id": 294
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T06:17:21.470Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"round": 4,
|
||||
"attempt": "sage-294-request-r4",
|
||||
"state": "requesting",
|
||||
"receipt": "ok sage-294-request-r4 rev 3467 row 294 round 4 request sage-294-request-r4 requesting"
|
||||
},
|
||||
"viewSha": "b018092245cd378ceba7bb3c2b5c6e633e6cb6d699e871caf371ac15317ddcc3"
|
||||
},
|
||||
{
|
||||
"rev": 3468,
|
||||
"op": "sage-294-request-r4.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"attempt": "sage-294-request-r4",
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 31055,
|
||||
"detail": "created"
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T06:18:04.990Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"round": 4,
|
||||
"attempt": "sage-294-request-r4",
|
||||
"state": "posted",
|
||||
"receipt": "ok sage-294-request-r4.outcome rev 3468 row 294 round 4 request sage-294-request-r4 posted comment 31055"
|
||||
},
|
||||
"viewSha": "2f21cc42edf5b068ad1e03d13f21ffdf57690a128ac932d7cc3c308bf4660cd5"
|
||||
},
|
||||
{
|
||||
"rev": 3469,
|
||||
"op": "duckworth-0257-record",
|
||||
"verb": "review-record",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"verdict": "approve",
|
||||
"comment": 31053,
|
||||
"candidate": "a1ff5d67a2ac303ac693a97b4902d607d295415ac4c278082e2078fe9ca53b99"
|
||||
},
|
||||
"by": "duckworth",
|
||||
"at": "2026-10-23T06:19:14.480Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"round": 4,
|
||||
"verdict": "approve",
|
||||
"receipt": "ok duckworth-0257-record rev 3469 row 294 round 4 approve by duckworth (comment 31053)"
|
||||
},
|
||||
"viewSha": "382319e5fc722afefff65161aca6f838a395b3fd831c241a04c1e8e50b23e9d2"
|
||||
},
|
||||
{
|
||||
"rev": 3470,
|
||||
"op": "sage-294-landnote",
|
||||
"verb": "note",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"text": "landed d3059c2c3 on refactor 07:04Z: cherry-pick of c902a767d..24b745447 onto 09d0d6faf, range-diff 11 =, patch-id 8371405a equal. Duckworth approve c31053 rev 3469 on a1ff5d67, C1/C2 met. Run 4 on 8616b9a4f from npm ci: webui 365, cli 909, bus 873, 10 scripts, M1-M11 killed. Merged tree: stack-release-external 25, stack-upgrade 21, stack-release 223/224 (R9 transient, row 1810) then 224/224 rerun under mos-1422. Follow-ups rows 300, 301. Closes #1800."
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T07:05:20.954Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"field": "note",
|
||||
"receipt": "ok sage-294-landnote rev 3470 row 294 note"
|
||||
},
|
||||
"viewSha": "bafbed11bb4812b7e9d38b38daf9186160b77f46e048407a2188a5bbb01543ba"
|
||||
},
|
||||
{
|
||||
"rev": 3471,
|
||||
"op": "sage-294-done",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 294,
|
||||
"to": "done",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T07:06:04.303Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 294,
|
||||
"from": "in-review",
|
||||
"to": "done",
|
||||
"round": 4,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok sage-294-done rev 3471 row 294 in-review→done round 4"
|
||||
},
|
||||
"viewSha": "2c22a2f755cd35b8e5fddfe66c0a565403e8c759b1520030274f83e09a74560d"
|
||||
},
|
||||
{
|
||||
"rev": 3472,
|
||||
"op": "sage-r9-add",
|
||||
"verb": "add",
|
||||
"args": {
|
||||
"piece": "R9 shows its output when rehearse makes no directory (Vikunja 589)",
|
||||
"gate": "Duckworth approves the diff; stack-release.test.mjs whole and green on Sage's gate, in a gap",
|
||||
"brief": "docs/plans/2026-10-23_r9-guard-row.md#Brief: R9 shows its output when rehearse makes no directory (Vikunja 589)",
|
||||
"issues": [
|
||||
1810
|
||||
],
|
||||
"note": null,
|
||||
"owner": "launchpad",
|
||||
"gateOwner": "sage",
|
||||
"after": [
|
||||
{
|
||||
"id": 294,
|
||||
"when": "done"
|
||||
}
|
||||
],
|
||||
"reviewers": [
|
||||
"duckworth"
|
||||
],
|
||||
"required": null
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T07:07:24.046Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 303,
|
||||
"from": null,
|
||||
"to": "queued",
|
||||
"brief": {
|
||||
"path": "docs/plans/2026-10-23_r9-guard-row.md",
|
||||
"anchor": "Brief: R9 shows its output when rehearse makes no directory (Vikunja 589)",
|
||||
"blob": "d1f91b74e5f6c3e0d6a06f4bccaa2199d8090e13"
|
||||
},
|
||||
"receipt": "ok sage-r9-add rev 3472 row 303 none→queued"
|
||||
},
|
||||
"viewSha": "05bf4605473679152180ed21bce514d494c4d2005a9d1d3359b4b4ff891fbbc8"
|
||||
},
|
||||
{
|
||||
"rev": 3473,
|
||||
"op": "sage-303-brief",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 303,
|
||||
"to": "briefed",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T07:08:10.756Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 303,
|
||||
"from": "queued",
|
||||
"to": "briefed",
|
||||
"round": null,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok sage-303-brief rev 3473 row 303 queued→briefed"
|
||||
},
|
||||
"viewSha": "6d8164c0b7e0467462c76e5e0b9a979b225ca7259b727c38068d28ce7f3b2a24"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -324,14 +324,6 @@ verb. It rechecks the thread and releases the run's claim in one call, and retur
|
||||
launcher's snapshot. The launcher signals the run only after a release, so a message sent
|
||||
after the snapshot gets a turn from the same run or waits as `pending` for the next one. It
|
||||
refuses `not-holder` for a run that doesn't hold that thread and `run-ended` for an ended run.
|
||||
After a release, and until the run ends, every call of the run's that needs the claim
|
||||
(`message.receive`, `state`, `message.read`, `message.send` and every other verb that checks
|
||||
the holder) refuses `launcher-released`, not `not-holder` (row 302). `role.claim` refuses it
|
||||
too, before the claim, so the run can't claim again. A call
|
||||
the broker answers after the release and before the signal is that stop, not a lost claim, so
|
||||
the runner exits 0. The launch-landing pass's `landRelease` (`packages/cli/README.md`) answers
|
||||
the same way for the role run it releases. Any other lost claim (a stop, a revoke, a clear, a
|
||||
limit, a newer holder) still refuses `not-holder`.
|
||||
|
||||
A message delivered to a thread run that then ended without reading it is stranded, as
|
||||
`stranded` (Agent state, below) describes for a role. It counts as neither `pending` nor
|
||||
|
||||
@@ -419,10 +419,6 @@ export class Broker {
|
||||
#hostRuns = new Map();
|
||||
// Row 125 (#1619, REQ-LIMIT-4): a run a limit stops, by business:run, until its next receive or state.
|
||||
#stops = new Map();
|
||||
// Row 302 (#1809): runs the launcher released, by business:run, until endLaunch. threadIdle and
|
||||
// landRelease release and then signal, so the run's own calls in between answer launcher-released,
|
||||
// not not-holder, and the runner exits stopped instead of as a lost session.
|
||||
#launcherReleased = new Set();
|
||||
#clock = 0;
|
||||
#processing;
|
||||
// credentialStatus is the runtime's metadata-only Credentials.status(); the broker never gets use().
|
||||
@@ -1007,8 +1003,7 @@ export class Broker {
|
||||
if (s.reader) fail('read-only');
|
||||
if (s.human) fail('agent-required');
|
||||
const h = this.#own(s);
|
||||
if (h?.op !== 'claim' || (s.thread ? h.generation !== this.#runGeneration(s) : h.holder_run !== s.run))
|
||||
fail(this.#launcherReleased.has(s.business + ':' + s.run) ? 'launcher-released' : 'not-holder');
|
||||
if (h?.op !== 'claim' || (s.thread ? h.generation !== this.#runGeneration(s) : h.holder_run !== s.run)) fail('not-holder');
|
||||
return h;
|
||||
}
|
||||
// Row 264a (#1769, T2): the generation a thread run holds, its latest claim's in its session, or
|
||||
@@ -1058,7 +1053,6 @@ export class Broker {
|
||||
if (t?.holder !== record.run) fail('not-holder');
|
||||
if (t.pending || t.unread) return { released: false, pending: t.pending, unread: t.unread };
|
||||
this.#store.transaction(() => this.#claimEnd(session, this.#own(session), 'release', null));
|
||||
this.#launcherReleased.add(key);
|
||||
return { released: true };
|
||||
}
|
||||
// Row 271 (264d, #1776, D7 steps 3-5): the business's clears still in the launcher's hands, as of
|
||||
@@ -1313,7 +1307,6 @@ export class Broker {
|
||||
const exitCode = record.exitCode ?? null;
|
||||
if (exitCode !== null && !Number.isSafeInteger(exitCode)) fail('invalid-request');
|
||||
this.#ended.add(key);
|
||||
this.#launcherReleased.delete(key);
|
||||
for (const [cap, s] of this.#sessions)
|
||||
if (!s.human && !s.reader && s.business === record.business && s.run === record.run) this.#sessions.delete(cap);
|
||||
return this.#store.transaction(() => {
|
||||
@@ -4168,7 +4161,6 @@ export class Broker {
|
||||
);
|
||||
if (unread) return { released: false, unread };
|
||||
this.#store.transaction(() => this.#claimEnd(session, h, 'release', null, null, 'land-relaunch'));
|
||||
this.#launcherReleased.add(key);
|
||||
return { released: true };
|
||||
}
|
||||
// Row 194 (#1692, D1.1): the launcher's lifecycle pass. Trusted launcher API, not socket verbs.
|
||||
@@ -5157,9 +5149,6 @@ export class Broker {
|
||||
case 'role.claim': {
|
||||
keys(a, []);
|
||||
if (s.human) fail('agent-required');
|
||||
// Row 302 (#1809, Dewey's D1): a run the launcher released is being stopped and doesn't claim
|
||||
// again, or a later not-holder on that claim would answer launcher-released.
|
||||
if (this.#launcherReleased.has(s.business + ':' + s.run)) fail('launcher-released');
|
||||
// Row 121: a thread run claims its thread, so idle threads hold nothing (REQ-THREAD-2).
|
||||
// Row 264a (#1769): the claim is the run's session's, with the session's next generation.
|
||||
if (s.thread) {
|
||||
|
||||
@@ -461,7 +461,7 @@ test('landRelease releases the holding run by land-relaunch once it has asked me
|
||||
{ op: 'release', holder_run: 'merge-gate-run', by: 'land-relaunch', decision: null, reason: null },
|
||||
);
|
||||
assert.throws(() => release(), /not-holder/, 'released once');
|
||||
assert.throws(() => w.call('merge-gate', 'message.receive'), /launcher-released/);
|
||||
assert.throws(() => w.call('merge-gate', 'message.receive'), /not-holder/);
|
||||
w.b.endLaunch({ business: 'demo', run: 'merge-gate-run', reason: 'exited' });
|
||||
assert.throws(() => release(), /run-ended/);
|
||||
assert.equal(w.store.get("SELECT count(*) AS n FROM deliveries WHERE message=? AND op='delivered'", waiting.id).n, 0, 'still waiting');
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
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';
|
||||
|
||||
// Row 302 (#1809, Vikunja 588): threadIdle and landRelease release a run's claim and the launcher
|
||||
// signals it after. The run's own calls in between answer launcher-released, so its runner exits
|
||||
// stopped. Every other way a run loses its claim still answers not-holder, and a released run
|
||||
// doesn't claim again (Dewey's D1 in dewey-0875).
|
||||
|
||||
const shipped = (name) =>
|
||||
JSON.parse(readFileSync(fileURLToPath(new URL(`../../../roles/${name}.json`, import.meta.url)), 'utf8'));
|
||||
|
||||
function setup(t, roles, definitions) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'bus-launcher-released-'));
|
||||
const store = new Store(root);
|
||||
t.after(() => {
|
||||
store.close();
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
const businesses = {
|
||||
demo: {
|
||||
id: 'demo',
|
||||
human: 'jason',
|
||||
arbiters: { technical: 'pm', delivery: 'pm' },
|
||||
roles,
|
||||
...(definitions ? { definitions } : {}),
|
||||
projects: { stack: { root: '/nonexistent' } },
|
||||
},
|
||||
};
|
||||
const b = new Broker({ store, businesses });
|
||||
const caps = { human: b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }) };
|
||||
const call = (who, verb, args = {}) => b.request(caps[who], { verb, args });
|
||||
const launch = (name, role, extra = {}) => {
|
||||
caps[name] = b.bindLaunch({ business: 'demo', role, run: `${name}-run`, harness: 'pi', ...extra });
|
||||
};
|
||||
const refused = (run) =>
|
||||
store.all("SELECT body FROM events WHERE kind='action.refused' AND actor_run=? ORDER BY seq", run).map((e) => JSON.parse(e.body).code);
|
||||
return { b, store, caps, call, launch, refused };
|
||||
}
|
||||
|
||||
// Two threads of the assistant, each with its own run holding it.
|
||||
function threads(t) {
|
||||
const w = setup(
|
||||
t,
|
||||
{
|
||||
chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send'], crossRole: [] } },
|
||||
assistant: { definition: 'assistant', authority: { withinRole: ['message.send'], crossRole: [] } },
|
||||
pm: { definition: 'pm', authority: { withinRole: ['message.send'], crossRole: [] } },
|
||||
},
|
||||
{ 'chief-of-staff': {}, assistant: { messaging: shipped('assistant').messaging }, pm: {} },
|
||||
);
|
||||
const project = w.call('human', 'project.create', { name: 'stack work', repo: 'stack' }).id;
|
||||
const A = w.call('human', 'thread.create', { project, name: 'alpha' }).id;
|
||||
const B = w.call('human', 'thread.create', { project, name: 'beta' }).id;
|
||||
for (const r of ['chief', 'pm']) {
|
||||
w.launch(r, r);
|
||||
w.call(r, 'role.claim');
|
||||
}
|
||||
w.launch('a', 'assistant', { thread: A });
|
||||
w.launch('b', 'assistant', { thread: B });
|
||||
w.call('a', 'role.claim');
|
||||
w.call('b', 'role.claim');
|
||||
const idle = (run, thread = A) => w.b.threadIdle({ business: 'demo', thread, run });
|
||||
return { ...w, A, B, idle };
|
||||
}
|
||||
|
||||
test('row 302 a run threadIdle released answers launcher-released on receive, state, read and send, logs each, and claims nothing again', (t) => {
|
||||
const { b, store, call, refused, idle, A } = threads(t);
|
||||
const sent = call('human', 'thread.send', { thread: A, body: 'for A' });
|
||||
assert.deepEqual(call('a', 'message.receive').map((m) => m.id), [sent.id]);
|
||||
call('a', 'message.read', { id: sent.id });
|
||||
assert.deepEqual(idle('a-run'), { released: true });
|
||||
const later = call('human', 'thread.send', { thread: A, body: 'later for A' });
|
||||
assert.throws(() => call('a', 'message.receive'), { code: 'launcher-released' });
|
||||
assert.throws(() => call('a', 'state'), { code: 'launcher-released' });
|
||||
assert.throws(() => call('a', 'message.read', { id: later.id }), { code: 'launcher-released' });
|
||||
// message.send needs the claim too, through #consumeAuthority (dewey-0878 C1).
|
||||
assert.throws(() => call('a', 'message.send', { to: 'chief', body: 'from a released run' }), { code: 'launcher-released' });
|
||||
// D1: no new claim for the released run, so a later not-holder can't be misreported.
|
||||
const claims = store.get('SELECT count(*) AS n FROM thread_claims').n;
|
||||
assert.throws(() => call('a', 'role.claim'), { code: 'launcher-released' });
|
||||
assert.equal(store.get('SELECT count(*) AS n FROM thread_claims').n, claims);
|
||||
assert.deepEqual(refused('a-run'), Array(5).fill('launcher-released'));
|
||||
// #route writes one routed row per message; with no holder its holder_run is null (broker #recipient).
|
||||
assert.equal(store.get('SELECT count(*) AS n FROM deliveries WHERE message=? AND holder_run=?', later.id, 'a-run').n, 0, 'nothing delivered to the released run');
|
||||
assert.deepEqual(store.all('SELECT op, holder_run FROM deliveries WHERE message=?', later.id).map((r) => ({ ...r })), [{ op: 'routed', holder_run: null }]);
|
||||
// threadIdle's own refusal is unchanged.
|
||||
assert.throws(() => idle('a-run'), { code: 'not-holder' });
|
||||
// After the end the run's caps are gone.
|
||||
b.endLaunch({ business: 'demo', run: 'a-run', reason: 'idle' });
|
||||
assert.throws(() => call('a', 'message.receive'), { code: 'unauthenticated' });
|
||||
});
|
||||
|
||||
test('row 302 with a run launcher-released, a run that lost its claim another way still answers not-holder', (t) => {
|
||||
const { call, launch, idle, A, B } = threads(t);
|
||||
assert.deepEqual(idle('a-run'), { released: true });
|
||||
// The human's stop revokes the other thread's holder.
|
||||
assert.deepEqual(call('human', 'assistant.stop', { instance: 'assistant' }).stopped, [{ thread: B, run: 'b-run' }]);
|
||||
assert.throws(() => call('b', 'message.receive'), { code: 'not-holder' });
|
||||
assert.throws(() => call('b', 'state'), { code: 'not-holder' });
|
||||
assert.throws(() => call('b', 'role.claim'), { code: 'run-revoked' });
|
||||
// A role run that released its own claim.
|
||||
call('pm', 'role.release');
|
||||
assert.throws(() => call('pm', 'message.receive'), { code: 'not-holder' });
|
||||
// A new run holds the released thread; the released run's answer stays its own.
|
||||
launch('a2', 'assistant', { thread: A });
|
||||
call('a2', 'role.claim');
|
||||
assert.throws(() => call('a', 'message.receive'), { code: 'launcher-released' });
|
||||
assert.deepEqual(call('a2', 'message.receive'), []);
|
||||
});
|
||||
|
||||
const MERGE = 'git.merge.protected';
|
||||
const TASK = 'vikunja:1/2';
|
||||
|
||||
test('row 302 a run landRelease released answers launcher-released, and claims nothing again', (t) => {
|
||||
const w = setup(t, {
|
||||
pm: { authority: { withinRole: ['message.send'], crossRole: [] } },
|
||||
coder: { authority: { withinRole: ['git.push.working', 'review.request', 'message.send'], crossRole: [] } },
|
||||
reviewer: { authority: { withinRole: ['review.verdict', 'message.send'], crossRole: [] } },
|
||||
'merge-gate': { authority: { withinRole: [], crossRole: [MERGE] } },
|
||||
});
|
||||
for (const role of ['pm', 'coder', 'reviewer', 'merge-gate']) {
|
||||
w.launch(role, role);
|
||||
w.call(role, 'role.claim');
|
||||
}
|
||||
const commit = 'a'.repeat(40);
|
||||
w.b.checkPush(w.caps.coder, TASK);
|
||||
w.b.recordPush(w.caps.coder, { task_ref: TASK, project: 'stack', branch: 'feat', ref: 'work/vikunja-1-2', commit, previous: null });
|
||||
const r = w.call('coder', 'review.request', { task_ref: TASK, commit, to: 'reviewer' });
|
||||
w.call('reviewer', 'review.verdict', { task_ref: TASK, request: r.request, verdict: 'approve', body: 'good' });
|
||||
const options = [{ key: 'yes', text: 'Land it' }, { key: 'no', text: 'Not yet' }];
|
||||
const d = w.call('merge-gate', 'decision.raise', { action: MERGE, target: TASK, question: 'Land?', options, recommendation: 'yes', blocking: false });
|
||||
w.call('pm', 'decision.resolve', { id: d.id, choice: 'yes' });
|
||||
const release = () => w.b.landRelease({ business: 'demo', run: 'merge-gate-run', decision: d.id });
|
||||
for (const m of w.call('merge-gate', 'message.receive')) w.call('merge-gate', 'message.read', { id: m.id });
|
||||
assert.deepEqual(release(), { released: true });
|
||||
assert.throws(() => w.call('merge-gate', 'message.receive'), { code: 'launcher-released' });
|
||||
assert.throws(() => w.call('merge-gate', 'state'), { code: 'launcher-released' });
|
||||
const claims = w.store.get('SELECT count(*) AS n FROM role_claims').n;
|
||||
assert.throws(() => w.call('merge-gate', 'role.claim'), { code: 'launcher-released' });
|
||||
assert.equal(w.store.get('SELECT count(*) AS n FROM role_claims').n, claims);
|
||||
// landRelease's own refusal is unchanged, and a role run that released itself still answers not-holder.
|
||||
assert.throws(() => release(), { code: 'not-holder' });
|
||||
w.call('coder', 'role.release');
|
||||
assert.throws(() => w.call('coder', 'message.receive'), { code: 'not-holder' });
|
||||
});
|
||||
@@ -391,7 +391,7 @@ test('REQ-THREAD-2 REQ-LAUNCH-1: threadIdle releases only a thread with nothing
|
||||
// Once released, the run can't take the thread's next message; it waits for the next holder.
|
||||
assert.throws(() => idle('a-run'), { code: 'not-holder' });
|
||||
const later = call('human', 'thread.send', { thread: A, body: 'later for A' });
|
||||
assert.throws(() => call('a', 'message.receive'), { code: 'launcher-released' });
|
||||
assert.throws(() => call('a', 'message.receive'), { code: 'not-holder' });
|
||||
assert.equal(state()[A].pending, 1);
|
||||
assert.equal(b.endLaunch({ business: 'demo', run: 'a-run', reason: 'idle' }).released, false);
|
||||
assert.throws(() => idle('a-run'), { code: 'run-ended' });
|
||||
|
||||
@@ -563,8 +563,6 @@ each holder that `launch.instances` lists:
|
||||
per run as `run <run>: landRelease refused: <code>`. After a release the
|
||||
launch log gets a `land-relaunch` record (`instance`, `run`,
|
||||
`decision`) and the runner gets SIGTERM, as `mosaic stop` sends it. The
|
||||
run's calls between the release and the signal refuse `launcher-released`,
|
||||
so the runner exits 0 either way (row 302). The
|
||||
next tick starts the instance with the decision in its `landing`.
|
||||
|
||||
The launcher keeps none of this on disk. A host that restarts ends the
|
||||
|
||||
@@ -26,9 +26,9 @@ export const ACCESS_USAGE = [
|
||||
// The help text. A grant's `instances` is the real limit: the granted instance
|
||||
// runs with the owner's role, so a collaborator drives it with that authority.
|
||||
// Removing a user acts at once. Removing a provider from console.sso waits for a
|
||||
// Console restart, so take the users out of access.json first. Peer homes are
|
||||
// empty until the federation key lands (row 203), so a grant naming a peer
|
||||
// home refuses until then. A grant naming a subject needs console.sso.
|
||||
// Console restart, so take the users out of access.json first. A user's home is
|
||||
// local or a peer named in federation.peers (FED R6, #1800). A grant naming a
|
||||
// subject needs console.sso.
|
||||
export const ACCESS_HELP = `${ACCESS_USAGE}
|
||||
|
||||
A grant gives a user exactly what the granted instances can do. The instances
|
||||
@@ -37,16 +37,16 @@ the owner's role.
|
||||
To cut a user off, revoke them here first. It acts at once. Removing a provider
|
||||
from console.sso takes effect only after a Console restart, so do that second.
|
||||
user remove refuses while the user has a grant at any business: revoke those first.
|
||||
Peer homes are not read yet (row 203 adds the federation key), so a grant that
|
||||
names a peer home refuses. A grant that names a sign-in subject needs console.sso
|
||||
A user's home is local or a peer id from federation.peers in the system config;
|
||||
any other home refuses. A grant that names a sign-in subject needs console.sso
|
||||
in the system config; without it every such grant refuses.
|
||||
The command runs as the instance's uid: access.json must belong to it and must
|
||||
not be writable by group or other, or the command refuses (exit 3) and writes nothing.`;
|
||||
|
||||
// The instance's context for checkAccess, read from the system config and the business
|
||||
// files. providers and ssoSubjects come from console.sso (row 192's shape: each provider
|
||||
// has an id and a subjects list). peers is empty until row 203. A business file that
|
||||
// fails to load fails the whole command.
|
||||
// has an id and a subjects list). peers are the ids in federation.peers (FED R6, F8),
|
||||
// empty without the key. A business file that fails to load fails the whole command.
|
||||
export function accessContextFor(system, env) {
|
||||
const providers = system.console?.sso?.providers ?? [];
|
||||
const ssoSubjects = providers.flatMap((p) => p.subjects.map((s) => ({ provider: p.id, sub: s.sub })));
|
||||
@@ -59,7 +59,8 @@ export function accessContextFor(system, env) {
|
||||
businesses[id] = { human: doc.human, instances: Object.keys(doc.roles) };
|
||||
}
|
||||
}
|
||||
return { providers: providers.map((p) => p.id), peers: [], businesses, ssoSubjects };
|
||||
const peers = (system.federation?.peers ?? []).map((p) => p.id);
|
||||
return { providers: providers.map((p) => p.id), peers, businesses, ssoSubjects };
|
||||
}
|
||||
|
||||
// Every verb needs --business: the human proof runs through that business's bus.
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
// Everything here reads and refuses; nothing writes.
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { existsSync, lstatSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { BusinessError, configDir, loadBusiness, loadProject, projectFilePath, resolveInstance, systemVars } from "../../business/src/index.mjs";
|
||||
import { BusinessError, accessFilePath, configDir, loadAccess, loadBusiness, loadProject, projectFilePath, resolveInstance, systemVars } from "../../business/src/index.mjs";
|
||||
import { busBusiness } from "../../bus/src/business.mjs";
|
||||
import { BusError } from "../../bus/src/broker.mjs";
|
||||
import { CliError } from "./errors.mjs";
|
||||
import { accessContextFor } from "./access.mjs";
|
||||
|
||||
export const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
||||
|
||||
@@ -83,6 +84,36 @@ function trackerFor(system, b, warn) {
|
||||
};
|
||||
}
|
||||
|
||||
// FED R6 (#1800, sage-3142): the runtime's `access`, { file, context }. The broker reads the file
|
||||
// with R1's loadAccess on every user request, so a bad file leaves every user with no grant and
|
||||
// the owner's bus still boots (sage-3153). The context is the cli's accessContextFor, peers included. No file,
|
||||
// or a context that can't be built, means no `access`: no user binds, as before R6.
|
||||
function accessFor(system, env, warn) {
|
||||
const file = accessFilePath(configDir(env));
|
||||
try {
|
||||
lstatSync(file);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
let context;
|
||||
try {
|
||||
context = accessContextFor(system, env);
|
||||
} catch (e) {
|
||||
warn(`access.json is present but its context can't be read, so no granted user binds: ${e.message}`);
|
||||
return null;
|
||||
}
|
||||
// sage-3153: one line at boot when the loader refuses the file, so the owner sees why no user
|
||||
// binds. The loader's messages can quote the file, so the line names only the path and a code
|
||||
// from its exit code: 4 is a file it could not open as a regular file, anything else invalid.
|
||||
try {
|
||||
loadAccess(file, context);
|
||||
} catch (e) {
|
||||
const code = e instanceof BusinessError && e.exitCode !== 4 ? "access-invalid" : "access-unreadable";
|
||||
warn(`access file refused at boot (${code}), no granted user binds until it is fixed: ${file}`);
|
||||
}
|
||||
return { file, context };
|
||||
}
|
||||
|
||||
// The `{op: 'boot'}` config for packages/bus/src/process.mjs, for one
|
||||
// business. The host holds a reader capability for the notifier and binds
|
||||
// launches later through bindLaunch, so `launches` starts empty.
|
||||
@@ -101,6 +132,8 @@ export function bootConfig({ system, businessId, env = process.env, warn = () =>
|
||||
};
|
||||
const tracker = trackerFor(vars, b, warn);
|
||||
if (tracker) config.trackers = { [b.id]: tracker };
|
||||
const access = accessFor(system, env, warn);
|
||||
if (access) config.access = access;
|
||||
return config;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// FED R6 (#1800, sage-3142, sage-3153): the bus host's boot config carries the runtime's `access`
|
||||
// ({ file, context }) when <configDir>/access.json exists, with the cli's accessContextFor as the
|
||||
// context. The broker then binds a granted user; with no file, or a file R1's loader refuses, no
|
||||
// user binds and the owner's bus still boots. A refused file gives one boot warning naming the path
|
||||
// and a code, never the file's contents. Scratch roots only. The boot config goes through a JSON
|
||||
// round trip first, as it does over the host's IPC.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { chmodSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { startBroker } from "../../bus/src/runtime.mjs";
|
||||
import { checkAccess } from "../../business/src/index.mjs";
|
||||
import { accessContextFor } from "../src/access.mjs";
|
||||
import { bootConfig, loadSystem } from "../src/config.mjs";
|
||||
import { fixture, tmp } from "./helpers.mjs";
|
||||
|
||||
const SSO = { sso: { providers: [{ id: "authentik", subjects: [{ sub: "jason-sub" }] }] } };
|
||||
const FEDERATION = { self: "hub", peers: [{ id: "factory", origin: "https://factory.example.test", label: "Software factory" }] };
|
||||
const DOC = {
|
||||
accessVersion: 1,
|
||||
revision: 1,
|
||||
users: [{ id: "alice", home: "factory", subjects: [{ provider: "authentik", sub: "alice-sub" }] }],
|
||||
grants: [{ id: "g-1", user: "alice", business: "acme", projects: ["*"], instances: ["*"], level: "collaborator" }],
|
||||
};
|
||||
// The shared helpers.mjs business sets tracker.baseUrl without tracker.project, so every boot warns
|
||||
// this first: trackerFor runs before accessFor in bootConfig.
|
||||
const TRACKER = "business acme sets tracker.baseUrl, but no project file sets tracker.project; the broker gets no task verbs";
|
||||
|
||||
// F8: the context's peers are federation.peers' ids, so a user whose home is a configured peer
|
||||
// validates and one whose home is any other id, the hub's own included, refuses.
|
||||
test("R6 a configured peer id validates as a user's home and an unconfigured one refuses", (t) => {
|
||||
const root = tmp(t, "mosaic-access-peers-");
|
||||
fixture(root, "acme");
|
||||
const ctx = accessContextFor({ console: SSO, federation: FEDERATION }, { MOSAIC_CONFIG: join(root, "config", "config.json") });
|
||||
assert.deepEqual(ctx.peers, ["factory"]);
|
||||
const doc = (home) => ({ ...DOC, users: [{ ...DOC.users[0], home }] });
|
||||
assert.equal(checkAccess(doc("factory"), ctx).users[0].home, "factory");
|
||||
assert.throws(() => checkAccess(doc("elsewhere"), ctx), /home must be a configured peer/);
|
||||
assert.throws(() => checkAccess(doc("hub"), ctx), /home must be a configured peer/);
|
||||
});
|
||||
|
||||
// A scratch instance whose system config names the sign-in provider and one peer, and the boot
|
||||
// config bootConfig makes for it. `file` is the access file's text, null for none, or a function
|
||||
// that makes the file at the path.
|
||||
async function boot(t, file) {
|
||||
const f = fixture(tmp(t, "mosaic-access-boot-"), "acme");
|
||||
const system = { ...loadSystem({ env: f.env }), console: SSO, federation: FEDERATION };
|
||||
const path = join(dirname(f.config), "access.json");
|
||||
if (typeof file === "function") file(path);
|
||||
else if (file !== null) {
|
||||
writeFileSync(path, file);
|
||||
chmodSync(path, 0o600);
|
||||
}
|
||||
const warnings = [];
|
||||
const { trackers, ...config } = JSON.parse(JSON.stringify(bootConfig({ system, businessId: "acme", env: f.env, warn: (w) => warnings.push(w) })));
|
||||
const runtime = await startBroker(config);
|
||||
t.after(() => runtime.close());
|
||||
const bind = (user) => {
|
||||
try {
|
||||
return runtime.broker.bindUser({ business: "acme", user }) ? "bound" : "none";
|
||||
} catch (e) {
|
||||
return e.code;
|
||||
}
|
||||
};
|
||||
return { config, path, bind, warnings };
|
||||
}
|
||||
|
||||
test("R6 B1 a boot with access.json binds a granted user, whose home is a configured peer", async (t) => {
|
||||
const s = await boot(t, JSON.stringify(DOC));
|
||||
assert.equal(s.config.access.file, s.path);
|
||||
assert.deepEqual(s.config.access.context.peers, ["factory"]);
|
||||
assert.deepEqual(s.config.access.context.ssoSubjects, [{ provider: "authentik", sub: "jason-sub" }]);
|
||||
assert.equal(s.bind("alice"), "bound");
|
||||
assert.equal(s.bind("bob"), "access-revoked");
|
||||
assert.deepEqual(s.warnings, [TRACKER]);
|
||||
});
|
||||
|
||||
test("R6 B2 a boot without access.json has no access and binds no user", async (t) => {
|
||||
const s = await boot(t, null);
|
||||
assert.equal(Object.hasOwn(s.config, "access"), false);
|
||||
assert.equal(s.bind("alice"), "access-revoked");
|
||||
assert.deepEqual(s.warnings, [TRACKER]);
|
||||
});
|
||||
|
||||
test("R6 B3 a boot with a file R1's loader refuses still boots, binds no user and says so once", async (t) => {
|
||||
for (const [why, file, code] of [
|
||||
["not JSON", '{"secret-marker"', "access-invalid"],
|
||||
["a home that is not a configured peer", JSON.stringify({ ...DOC, users: [{ ...DOC.users[0], home: "secret-marker" }] }), "access-invalid"],
|
||||
["a symbolic link", (path) => { writeFileSync(`${path}.real`, JSON.stringify(DOC), { mode: 0o600 }); symlinkSync(`${path}.real`, path); }, "access-unreadable"],
|
||||
]) {
|
||||
const s = await boot(t, file);
|
||||
assert.equal(s.config.access.file, s.path, why);
|
||||
assert.equal(s.bind("alice"), "access-revoked", why);
|
||||
assert.deepEqual(s.warnings, [TRACKER, `access file refused at boot (${code}), no granted user binds until it is fixed: ${s.path}`], why);
|
||||
assert.doesNotMatch(s.warnings.join("\n"), /secret-marker/, why);
|
||||
}
|
||||
});
|
||||
@@ -1,204 +0,0 @@
|
||||
// Row 302 (#1809, Vikunja 588): the idle stop releases a thread run's claim in the threadIdle op
|
||||
// and sends SIGTERM only after the op returns. A runner call the broker answers in between used to
|
||||
// get not-holder, and the run ended session-invalid/22 instead of stopped/0. This test holds the op
|
||||
// open until a call has landed in that window and the runner has exited, so it doesn't depend on
|
||||
// timing. The launcher skips a tick while one is running (launcher.mjs, the thread pass), so nothing
|
||||
// else signals a run while the op is held.
|
||||
//
|
||||
// The host is in process, as in thread-launch.test.mjs, with the same fixture.
|
||||
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { launchDoc } from "../../business/tests/helpers.mjs";
|
||||
import { BusError } from "../../bus/src/broker.mjs";
|
||||
import { startBroker } from "../../bus/src/runtime.mjs";
|
||||
import { Store } from "../../bus/src/store.mjs";
|
||||
import { gitEnv } from "../../seat/src/checkout.mjs";
|
||||
import { sandboxProblem } from "../../seat/src/sandbox.mjs";
|
||||
import { launchLogFile, SESSION_CREDENTIALS } from "../../seat/src/session.mjs";
|
||||
import { bootConfig, loadSystem } from "../src/config.mjs";
|
||||
import { createLauncher } from "../src/launcher.mjs";
|
||||
import { fakeHarnesses, fixture, tmp } from "./helpers.mjs";
|
||||
|
||||
const ADAPTER = fileURLToPath(new URL("./fixtures/thread-adapter.mjs", import.meta.url));
|
||||
const skip = sandboxProblem() ?? false;
|
||||
|
||||
async function until(fn, ms = 20000) {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {
|
||||
const v = await fn();
|
||||
if (v) return v;
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
}
|
||||
throw new Error("timed out waiting");
|
||||
}
|
||||
|
||||
function gitIn(cwd, ...args) {
|
||||
const r = spawnSync("git", ["-c", "user.name=test", "-c", "[email protected]", "-C", cwd, ...args], { env: gitEnv(), encoding: "utf8" });
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
}
|
||||
|
||||
function prepare(t) {
|
||||
const root = tmp(t, "mosaic-idle-race-");
|
||||
const f = fixture(root, "acme", (doc) => {
|
||||
doc.roles.chief.vars = { harness: "pi", model: "claude-opus-5-5", thinking: "high" };
|
||||
doc.roles.assistant = { definition: "assistant", vars: { harness: "pi", model: "claude-sonnet-5-5", thinking: "high" } };
|
||||
doc.launch = launchDoc("chief", ["coder", "assistant"], { opus: 1, sonnet: 2 });
|
||||
doc.launch.threadIdleSeconds = 1;
|
||||
return doc;
|
||||
});
|
||||
for (const k of SESSION_CREDENTIALS.flat()) delete f.env[k];
|
||||
const project = f.doc.projects.stack.root;
|
||||
gitIn(project, "init", "-q", "-b", "main");
|
||||
writeFileSync(join(project, "file.txt"), "one\n");
|
||||
gitIn(project, "add", "file.txt");
|
||||
gitIn(project, "commit", "-q", "-m", "one");
|
||||
const adapter = join(root, "adapter.sh");
|
||||
writeFileSync(adapter, `exec '${process.execPath}' '${ADAPTER}'\n`, { mode: 0o644 });
|
||||
return { root, f, adapter };
|
||||
}
|
||||
|
||||
// thread-launch.test.mjs's start(), with threadIdle replaced by the caller.
|
||||
async function start(t, { f, adapter }, threadIdle) {
|
||||
const system = loadSystem({ env: f.env });
|
||||
const { trackers, ...boot } = bootConfig({ system, businessId: "acme", env: f.env });
|
||||
assert.equal(trackers, undefined);
|
||||
const runtime = await startBroker(boot);
|
||||
const ops = {
|
||||
identity: (m) => runtime.identity(m.cap),
|
||||
authorizeLaunch: (m) => runtime.authorizeLaunch(m.cap, m.instance),
|
||||
refuse: (m) => runtime.refuse(m.cap, m.code, m.subject),
|
||||
endLaunch: (m) => runtime.endLaunch(m.record),
|
||||
credentialStatus: (m) => runtime.credentialStatus(m.business, m.role),
|
||||
threadsState: (m) => runtime.threadsState(m.business),
|
||||
threadIdle: (m) => threadIdle(runtime, m),
|
||||
mayLaunch: (m) => runtime.mayLaunch(m),
|
||||
landHeld: (m) => runtime.landHeld(m.record),
|
||||
landHolders: (m) => runtime.landHolders(m.business),
|
||||
landRelease: (m) => runtime.landRelease(m.record),
|
||||
};
|
||||
const refusing = (what, fn) => async (m) => {
|
||||
try {
|
||||
return await fn(m);
|
||||
} catch (e) {
|
||||
const code = e instanceof BusError ? e.code : `${what}-refused`;
|
||||
throw Object.assign(new Error(`${what} refused: ${code}`), { code });
|
||||
}
|
||||
};
|
||||
const hooks = [];
|
||||
let closing = null;
|
||||
const host = {
|
||||
business: "acme",
|
||||
path: runtime.path,
|
||||
dataRoot: f.dataRoot,
|
||||
op: refusing("launch-op", (m) => {
|
||||
if (!Object.hasOwn(ops, m.op)) throw new BusError("invalid-host-request");
|
||||
return ops[m.op](m);
|
||||
}),
|
||||
bindLaunch: refusing("launch bind", (r) => runtime.bindLaunch(r)),
|
||||
beforeClose: (fn) => void hooks.push(fn),
|
||||
};
|
||||
const launcher = createLauncher({
|
||||
host,
|
||||
system,
|
||||
env: f.env,
|
||||
makeLoader: await fakeHarnesses(t, { pi: adapter, "claude-code": adapter }),
|
||||
sessionDefaults: { pollInterval: 100 },
|
||||
harnessVersion: "0.85.1",
|
||||
readOnly: [ADAPTER],
|
||||
threadTickMs: 100,
|
||||
log: () => {},
|
||||
});
|
||||
const close = () =>
|
||||
(closing ??= (async () => {
|
||||
for (const fn of hooks) await fn(0);
|
||||
await runtime.close();
|
||||
})());
|
||||
t.after(close);
|
||||
await launcher.listen();
|
||||
const human = runtime.broker.bindHuman({ business: "acme", human: f.doc.human, via: "cli", outsideAgent: true });
|
||||
const call = (verb, args = {}) => runtime.broker.request(human, { verb, args });
|
||||
return { runtime, launcher, call, close };
|
||||
}
|
||||
|
||||
const launchLog = (f) =>
|
||||
existsSync(launchLogFile(f.dataRoot, "acme"))
|
||||
? readFileSync(launchLogFile(f.dataRoot, "acme"), "utf8").trim().split("\n").map((l) => JSON.parse(l))
|
||||
: [];
|
||||
const runnerLog = (f, run) => {
|
||||
const p = join(f.dataRoot, "launches", "acme", run, "runner.log");
|
||||
return existsSync(p) ? readFileSync(p, "utf8") : "";
|
||||
};
|
||||
const ended = (f, run) => launchLog(f).find((e) => e.event === "end" && e.run === run);
|
||||
// The runner's own verdict on a refusal: the launcher-release line, or a lost session's.
|
||||
const verdict = (f, run) => /runner: (session released by the launcher|session stopped working: [a-z-]+)\n/.exec(runnerLog(f, run))?.[1] ?? null;
|
||||
const settled = async (fn) => {
|
||||
try {
|
||||
return await until(fn);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
async function ask(call, thread, body) {
|
||||
const sent = call("thread.send", { thread, body });
|
||||
await until(() => call("thread.messages", { thread }).find((m) => m.in_reply_to === sent.id && m.class === "RESULT"));
|
||||
}
|
||||
|
||||
test("row 302 REQ-THREAD-2 an idle-released run whose call lands before the SIGTERM ends stopped/0; a run whose claim the human revoked in the same window still ends session-invalid/22", { skip, timeout: 120000 }, async (t) => {
|
||||
const prepared = prepare(t);
|
||||
const { f } = prepared;
|
||||
// The first run the idle stop releases. Inside its held window, the human stops the other
|
||||
// thread's run, which no launcher pass can signal while the op is held.
|
||||
let window = null;
|
||||
let host = null;
|
||||
const threadIdle = async (runtime, m) => {
|
||||
const r = runtime.threadIdle(m.record);
|
||||
if (!r.released || window) return r;
|
||||
window = { run: m.record.run, thread: m.record.thread };
|
||||
const other = runtime.threadsState("acme").find((x) => x.thread !== m.record.thread && x.holder);
|
||||
window.control = other ? { run: other.holder, stopped: host.call("assistant.stop", { instance: "assistant" }).stopped } : null;
|
||||
// Both runners decide on their own before the op returns and the launcher can signal.
|
||||
window.idle = await settled(() => verdict(f, window.run) && ended(f, window.run) && verdict(f, window.run));
|
||||
window.revoked = other ? await settled(() => verdict(f, other.holder) && ended(f, other.holder) && verdict(f, other.holder)) : null;
|
||||
return r;
|
||||
};
|
||||
host = await start(t, prepared, threadIdle);
|
||||
const project = host.call("project.create", { name: "stack work", repo: "stack" }).id;
|
||||
const A = host.call("thread.create", { project, name: "alpha" }).id;
|
||||
const B = host.call("thread.create", { project, name: "beta" }).id;
|
||||
await Promise.all([ask(host.call, A, "for A"), ask(host.call, B, "for B")]);
|
||||
// The op sets window.revoked last, so this waits for the whole window (dewey-0878 N2).
|
||||
await until(() => window && "revoked" in window, 60000);
|
||||
assert.ok(window.control, "the other thread's run held B when A's first release landed");
|
||||
await until(() => ended(f, window.run) && ended(f, window.control.run), 60000);
|
||||
|
||||
const { run, control } = window;
|
||||
assert.deepEqual(control.stopped.map((s) => s.run), [control.run], "the human's stop took only the other thread's run");
|
||||
// The released run refused in the window and exited by itself before the op returned.
|
||||
assert.equal(window.idle, "session released by the launcher", runnerLog(f, run));
|
||||
const log = launchLog(f);
|
||||
const end = (r) => log.find((e) => e.event === "end" && e.run === r);
|
||||
assert.deepEqual([end(run).reason, end(run).exitCode, end(run).signal], ["stopped", 0, null]);
|
||||
assert.ok(log.indexOf(end(run)) < log.findIndex((e) => e.event === "thread-idle" && e.run === run), "the end came before the idle stop's signal");
|
||||
assert.doesNotMatch(runnerLog(f, run), /session stopped working/);
|
||||
assert.match(runnerLog(f, run), /runner: exiting 0\n/);
|
||||
// The launcher already released the claim, so the runner doesn't release it at exit (dewey-0878 N1).
|
||||
assert.doesNotMatch(runnerLog(f, run), /runner: role\.release:/);
|
||||
// The control: a real not-holder in the same window still ends 22, with no signal.
|
||||
assert.equal(window.revoked, "session stopped working: not-holder", runnerLog(f, control.run));
|
||||
assert.deepEqual([end(control.run).reason, end(control.run).exitCode, end(control.run).signal], ["session-invalid", 22, null]);
|
||||
assert.match(runnerLog(f, control.run), /runner: exiting 22\n/);
|
||||
|
||||
await host.close();
|
||||
const store = new Store(f.dataRoot);
|
||||
t.after(() => store.close());
|
||||
const codes = (r) => store.all("SELECT body FROM events WHERE kind='action.refused' AND actor_run=? ORDER BY seq", r).map((e) => JSON.parse(e.body).code);
|
||||
assert.ok(codes(run).includes("launcher-released"), JSON.stringify(codes(run)));
|
||||
assert.ok(!codes(run).includes("not-holder"), JSON.stringify(codes(run)));
|
||||
assert.ok(codes(control.run).includes("not-holder"), JSON.stringify(codes(control.run)));
|
||||
});
|
||||
@@ -1984,10 +1984,11 @@ export class Store {
|
||||
|
||||
// B (3g) and C (3h) have --no-launch. A is a copy from before row 163, whose host has no such flag.
|
||||
// C1's scripts/mosaic runs B's host, as a copy-check failure needs.
|
||||
// Row 294: config.mjs imports access.mjs, which imports transport.mjs, so each copy carries both.
|
||||
const RCOPIES=join(base,'rcopies');mkdirSync(RCOPIES);
|
||||
for(const [id,v,froms] of [[B,'3g',['3f']],[C,'3h',['3f','3g']],[A,'3g',['3f']],[C1,'3g',['3f']]]){
|
||||
const d=join(RCOPIES,id);
|
||||
for(const p of ['scripts/mosaic','scripts/mosaic-config.mjs','packages/cli/src/config.mjs','packages/cli/src/errors.mjs','packages/bus/src/business.mjs','packages/plugins/src/kinds.mjs','packages/business/src','packages/library/src','roles'])cpSync(join(repo,p),join(d,p),{recursive:true});
|
||||
for(const p of ['scripts/mosaic','scripts/mosaic-config.mjs','packages/cli/src/config.mjs','packages/cli/src/errors.mjs','packages/cli/src/access.mjs','packages/cli/src/transport.mjs','packages/bus/src/business.mjs','packages/plugins/src/kinds.mjs','packages/business/src','packages/library/src','roles'])cpSync(join(repo,p),join(d,p),{recursive:true});
|
||||
if(id===C1)writeFileSync(join(d,'scripts','mosaic'),`#!/usr/bin/env bash\nexec node "${join(RCOPIES,B,'packages','cli','src','cli.mjs')}" "$@"\n`,{mode:0o755});
|
||||
for(const [f,text] of [['packages/cli/src/cli.mjs',id===A?'// fixture host from before row 163\nprocess.exit(1);\n':script(rHost)],['packages/webui/src/cli.mjs',script(rConsole)],['packages/bus/src/store.mjs',rStore(v,froms)],['packages/bus/src/migrate-schema.mjs',script(rMigrate)],['packages/bus/src/broker.mjs','export class BusError extends Error { constructor(code) { super(code); this.code = code; } }\n']]){mkdirSync(dirname(join(d,f)),{recursive:true});writeFileSync(join(d,f),text)}
|
||||
execFileSync('chmod',['-R','a-w',d]);
|
||||
@@ -2174,6 +2175,7 @@ test('R9 mos-0963: a role reference left outside placeholders/, or left on an en
|
||||
ownScript(e,'for(const [name,r] of Object.entries(b.roles??{})){strip(r?.vars);swap(name,r?.credentials)}','for(const [name,r] of Object.entries(b.roles??{})){strip(r?.vars);if(name!=="pm")swap(name,r?.credentials)}');
|
||||
const r=await rehearse(e,B,'--backup',e.act,'--port',await freePort(),'--timeout','3');
|
||||
assert.equal(r.code,1,r.out+r.err);
|
||||
assert.ok(r.dir!==null, r.out+r.err);
|
||||
assert.ok(r.out.includes(`FAIL credential-ref-live: pm gitea is ${tok}, not a file in ${join(r.dir,'placeholders')}\n`),r.out);
|
||||
assert.deepEqual(r.names.slice(-3),['FAIL credential-ref-live','PASS backup-unchanged','PASS live-config-unchanged']);
|
||||
assert.equal(existsSync(rh(e,'host-args.log')),false);assert.equal(existsSync(rh(e,'console-args.log')),false);
|
||||
|
||||
@@ -293,7 +293,7 @@ it never touches the disk and isn't in the environment or argv.
|
||||
|
||||
| Exit | Meaning | Launch end reason |
|
||||
|---|---|---|
|
||||
| 0 | stopped: by a signal, or the launcher's idle stop or land-relaunch released the claim (`launcher-released`) | `stopped` |
|
||||
| 0 | stopped | `stopped` |
|
||||
| 2 | bad run directory or stdin | `failed` |
|
||||
| 20 | founder credentials | `founder-credentials` |
|
||||
| 21 | `role.claim` refused | `claim-refused` |
|
||||
|
||||
@@ -53,9 +53,6 @@ export const FOUNDER_ENV = Object.freeze([
|
||||
const USABLE = new Set(["valid", "expiring", "rotation-due"]);
|
||||
// Capabilities that stopped working: the run ended, the claim was revoked.
|
||||
const SESSION_GONE = new Set(["unauthenticated", "not-holder", "run-ended"]);
|
||||
// Row 302 (#1809): the launcher's idle stop or land-relaunch released the claim and will stop the run.
|
||||
// That is a stop, not a lost session, so the run exits stopped even when its call lands before the signal.
|
||||
const LAUNCHER_RELEASED = "launcher-released";
|
||||
const BODY_MAX = 32000;
|
||||
const DEFAULTS = Object.freeze({ turnTimeout: 900, pollInterval: 2000, brokerRetries: 30 });
|
||||
|
||||
@@ -485,20 +482,6 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
|
||||
|
||||
let code = EXIT.stopped;
|
||||
let failures = 0;
|
||||
// Whether a broker refusal ends the run, and how: a launcher release stops it, a gone session is 22.
|
||||
// The reply site has logged its failure already, so it passes say=false.
|
||||
let released = false;
|
||||
const ended = (err, say = true) => {
|
||||
if (err.code === LAUNCHER_RELEASED) {
|
||||
log("runner: session released by the launcher");
|
||||
released = true;
|
||||
return true;
|
||||
}
|
||||
if (!SESSION_GONE.has(err.code)) return false;
|
||||
if (say) log(`runner: session stopped working: ${err.code}`);
|
||||
code = EXIT.session;
|
||||
return true;
|
||||
};
|
||||
let n = 0;
|
||||
const sleep = (ms) => new Promise((r) => {
|
||||
const t = setTimeout(r, ms);
|
||||
@@ -517,8 +500,9 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
|
||||
try {
|
||||
block = stateBlock(await client.call("state"), new Date().toISOString());
|
||||
} catch (err) {
|
||||
// A run in its landing turns hasn't received, so landRelease keeps it; thread runs have no landing.
|
||||
if (ended(err)) {
|
||||
if (SESSION_GONE.has(err.code)) {
|
||||
log(`runner: session stopped working: ${err.code}`);
|
||||
code = EXIT.session;
|
||||
gone = true;
|
||||
break;
|
||||
}
|
||||
@@ -540,7 +524,11 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
|
||||
messages = await client.call("message.receive");
|
||||
failures = 0;
|
||||
} catch (e) {
|
||||
if (ended(e)) break;
|
||||
if (SESSION_GONE.has(e.code)) {
|
||||
log(`runner: session stopped working: ${e.code}`);
|
||||
code = EXIT.session;
|
||||
break;
|
||||
}
|
||||
if (++failures >= session.brokerRetries) {
|
||||
log(`runner: broker unreachable after ${failures} tries: ${e.code ?? e.message}`);
|
||||
code = EXIT.broker;
|
||||
@@ -557,8 +545,11 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
|
||||
try {
|
||||
block = stateBlock(await client.call("state"), new Date().toISOString());
|
||||
} catch (e) {
|
||||
// Neither release happens while a delivered message is unread, so this sees one only by a bug.
|
||||
if (ended(e)) break loop;
|
||||
if (SESSION_GONE.has(e.code)) {
|
||||
log(`runner: session stopped working: ${e.code}`);
|
||||
code = EXIT.session;
|
||||
break loop;
|
||||
}
|
||||
unread = `could not read the current state: ${e.code ?? e.message}`;
|
||||
}
|
||||
++n;
|
||||
@@ -585,15 +576,17 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
|
||||
await client.call("message.read", { id: m.id });
|
||||
} catch (e) {
|
||||
log(`runner: reply to ${m.id} failed: ${e.code ?? e.message}`);
|
||||
// As for the state above: the message isn't read yet, so no launcher release has happened.
|
||||
if (ended(e, false)) break loop;
|
||||
if (SESSION_GONE.has(e.code)) {
|
||||
code = EXIT.session;
|
||||
break loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!messages.length) await sleep(session.pollInterval);
|
||||
}
|
||||
server.close();
|
||||
rmSync(session.toolSocket, { force: true });
|
||||
if (code === EXIT.stopped && !released) {
|
||||
if (code === EXIT.stopped) {
|
||||
try {
|
||||
await client.call("role.release");
|
||||
} catch (e) {
|
||||
|
||||
@@ -768,8 +768,8 @@ test("a run in a landing turn isn't released for a second held decision until it
|
||||
);
|
||||
assert.equal(ctx.store.get("SELECT count(*) AS n FROM deliveries WHERE holder_run='coder-run' AND op='read'").n, 2);
|
||||
const out = await r.exited;
|
||||
assert.equal(out.code, 0, out.stderr);
|
||||
assert.match(out.stderr, new RegExp(`runner: landing turn 1 for decision ${d1.id} \\(vikunja:1/2\\): ok\n[\\s\\S]*runner: session released by the launcher\n`));
|
||||
assert.equal(out.code, EXIT.session, out.stderr);
|
||||
assert.match(out.stderr, new RegExp(`runner: landing turn 1 for decision ${d1.id} \\(vikunja:1/2\\): ok\n[\\s\\S]*runner: session stopped working: not-holder\n`));
|
||||
});
|
||||
|
||||
test("a malformed landing exits 2 before the claim, with no turn", async (t) => {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// FED R6 (#1800, federation design 3.1 D3 and D5): who besides the owner may sign in to this
|
||||
// Console. `load` is R1's loadAccess over this instance's access.json and context, called on every
|
||||
// use and never cached, so a revoke, a removed subject or an expired grant counts on the next
|
||||
// request. A user is live while one grant in the Console's business covers at `viewer`, the same
|
||||
// test as the broker's #userGrants. The broker still decides every verb, project, instance and
|
||||
// thread (D4). Any failure to load, or to name the business, finds nobody (fail closed).
|
||||
import { covers } from '../../business/src/vocabulary.mjs';
|
||||
|
||||
export function consoleAccess({ load, business }) {
|
||||
if (typeof load !== 'function' || typeof business !== 'function') throw new TypeError('consoleAccess needs load and business functions');
|
||||
const live = (doc, id, b) => doc.grants.some(g => g.user === id && g.business === b &&
|
||||
covers(g, { business: b, project: g.projects[0], instance: g.instances[0] }, new Date(), { level: 'viewer' }));
|
||||
const lookup = (provider, sub) => {
|
||||
try {
|
||||
const doc = load(), b = business();
|
||||
const user = doc.users.find(u => u.subjects.some(s => s.provider === provider && s.sub === sub));
|
||||
return user && live(doc, user.id, b) ? user.id : null;
|
||||
} catch { return null; }
|
||||
};
|
||||
return Object.freeze({
|
||||
// The user id this (provider, sub) signs in as, or null.
|
||||
find: ({ provider, sub }) => lookup(provider, sub),
|
||||
// Is this session's user still the holder of its own subject, with a live grant?
|
||||
check: session => typeof session?.user === 'string' && lookup(session.provider, session.sub) === session.user,
|
||||
});
|
||||
}
|
||||
@@ -17,6 +17,8 @@ const STATUS = {
|
||||
'unknown-business': 403, 'read-only': 403, 'not-configured': 503, 'no-bus-host': 503,
|
||||
'thread-instance-missing': 503, 'thread-instance-ambiguous': 503, 'outcome-unknown': 503,
|
||||
'tester-off': 403, 'tester-required': 403, 'tester-refused': 403,
|
||||
// FED R6 (#1800): the broker's refusals of a granted user.
|
||||
'access-refused': 403, 'access-revoked': 401,
|
||||
};
|
||||
function safeMessage(code) {
|
||||
if (code === 'channel-disabled') return 'Run mosaic plugins to read the revision, then mosaic plugins set --file PATCH with plugins.channel set to ["cli","webui"].';
|
||||
|
||||
@@ -6,9 +6,14 @@ import { createChannelCaller } from '../../plugins/src/channel.mjs';
|
||||
import { PluginError } from '../../plugins/src/kinds.mjs';
|
||||
import { BusReadError } from './bus.mjs';
|
||||
|
||||
export function createChat({ call }) {
|
||||
// FED R6 (#1800, sage-3153): `owner`, when given, is the owner's transport, and it carries only
|
||||
// the channel settings read, `plugins` with {}. A granted user's chat needs it since `plugins` is
|
||||
// not a user verb. The result gates the webui channel server-side and is never sent to the user.
|
||||
// Every chat verb goes over the user's own `call`.
|
||||
export function createChat({ call, owner = null }) {
|
||||
if (typeof call !== 'function') throw new TypeError('chat needs a bus transport function');
|
||||
const dispatch = createChannelCaller({ name: 'webui', call });
|
||||
if (owner !== null && typeof owner !== 'function') throw new TypeError('chat owner must be a bus transport function');
|
||||
const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => verb === 'plugins' ? owner('plugins', {}) : call(verb, args) });
|
||||
const channel = async (verb, args) => {
|
||||
try { return await dispatch(verb, args); }
|
||||
catch (err) {
|
||||
|
||||
+21
-26
@@ -3,6 +3,11 @@
|
||||
import { createChat } from './chat.mjs';
|
||||
import { startServer, previewOptions, proofOptions, DEFAULT_BOARD } from './serve.mjs';
|
||||
import { busCaller } from './bus.mjs';
|
||||
import { busBusiness, userCaller } from './user-bus.mjs';
|
||||
import { consoleBus, userOptionsFor, agentCaller, TESTER_AGENT_VERBS } from './console-bus.mjs';
|
||||
import { consoleAccess } from './access.mjs';
|
||||
import { accessContextFor } from '../../cli/src/access.mjs';
|
||||
import { accessFilePath, loadAccess } from '../../business/src/access.mjs';
|
||||
import { startTesterSocket } from './tester-socket.mjs';
|
||||
import { TESTER_CLI } from '../../cli/src/transport.mjs';
|
||||
import { views } from '../../bus/src/views.mjs';
|
||||
@@ -13,14 +18,6 @@ import { createSso, readSlot } from './sso.mjs';
|
||||
import { statSync } from 'node:fs';
|
||||
import { isAbsolute } from 'node:path';
|
||||
import { ID_PATTERN } from '../../business/src/vocabulary.mjs';
|
||||
// Row 194 (#1692, D7): the Agents section's verbs. The caller refuses any other, so the server's
|
||||
// agentWrite reaches only these.
|
||||
const AGENT_VERBS = ['instance.launch', 'instance.regenerate', 'instance.end', 'veto.set', 'instance.request'];
|
||||
const TESTER_AGENT_VERBS = ['instance.launch', 'instance.end', 'instance.request'];
|
||||
const agentCaller = (call, verbs) => async (verb, args) => {
|
||||
if (!verbs.includes(verb)) throw new Error(`the Console does not call ${verb}`);
|
||||
return call(verb, args);
|
||||
};
|
||||
const args = process.argv.slice(2);
|
||||
const usage = 'node packages/webui/src/cli.mjs serve [--port N] [--tester-socket] [--checkout PATH] [--board http://127.0.0.1:7331] [--business ID] [--sso [--send]] [--public-host NAME --auth-header HEADER --allow-user ID [--send] [--proof-header NAME --proof-file PATH]]';
|
||||
try {
|
||||
@@ -73,24 +70,13 @@ try {
|
||||
// no notifier. A refusal printed here names no path (redactor).
|
||||
// --send without a bus refuses to start rather than serve a composer that can't send.
|
||||
let bus = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, libraryFile = null, libraryStatus = null, librarySync = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, busLine, system = null;
|
||||
let processing = null;
|
||||
let processing = null, ownerCall = null;
|
||||
try {
|
||||
system = loadSystem();
|
||||
const { dataRoot } = system;
|
||||
const call = busCaller({ dataRoot, socket: socketPath(dataRoot), business: options.business ?? null });
|
||||
processing = { read: () => call('processing.status'), halt: args => call('processing.halt', args), resume: args => call('processing.resume', args) };
|
||||
chat = createChat({ call });
|
||||
// Row 155 (#1649): `names` is a webui read only, so it sits beside the shared views. Row 165
|
||||
// (#1662): so is `instances`, and its write `instances.set` is only under --send. Row 65
|
||||
// (mos-0840): so is `routes`, and its write `routes.set`.
|
||||
// Row 278: this owner/reader read belongs to the Console, not the shared
|
||||
// role views or the tester adapter. The bus decides caller eligibility.
|
||||
bus = { ...views({ call }), names: () => call('names'), instances: () => call('instances'), routes: () => call('routes'), 'thread.claims': () => call('thread.claims') };
|
||||
// Row 148: the spec and docs editor reads like the other reads and saves only under --send.
|
||||
projectFiles = args => call('project.files', args);
|
||||
projectFile = args => call('project.file', args);
|
||||
libraryFile = args => call('library.file', args); libraryStatus = args => call('library.status', args);
|
||||
if (sendOn) {send = args => call('message.send', args);librarySync=args=>call('library.sync',args);settingsWrite=args=>call('limits.set',args);projectSave=args=>call('project.file.save',args);nameWrite=args=>call('name.set',args);assistantStop=args=>call('assistant.stop',args);instancesWrite=args=>call('instances.set',args);routesWrite=args=>call('routes.set',args);agentWrite=agentCaller(call,AGENT_VERBS);}
|
||||
const call = ownerCall = busCaller({ dataRoot, socket: socketPath(dataRoot), business: options.business ?? null });
|
||||
// FED R6 (#1800): the bus-backed options, shared with each granted user's session (console-bus.mjs).
|
||||
({ processing, chat, bus, projectFiles, projectFile, libraryFile, libraryStatus, send, librarySync, settingsWrite, projectSave, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite } = consoleBus(call, { send: sendOn }));
|
||||
busLine = `Bus: ${options.business ?? 'the running bus host\'s business'}`;
|
||||
} catch (err) {
|
||||
const why = redactor({ configDir: configDir() })(err.message);
|
||||
@@ -102,15 +88,24 @@ try {
|
||||
const reads = consoleReads({ root: REPO, queueRoot: options.checkout ?? REPO, system, configDir: configDir(), rolesDir: rolesDir(), business: options.business ?? null });
|
||||
// The sign-in config is checked again here, then each slot file, before anything listens. A
|
||||
// provider that can't be reached starts degraded; one whose discovery is refused stops the start.
|
||||
let sso = null;
|
||||
let sso = null, userOptions = null;
|
||||
if (ssoOn) {
|
||||
if (!system.console?.sso) throw new Error('--sso needs console.sso in the system config');
|
||||
const secrets = new Map(system.console.sso.providers.map(p => [p.id, readSlot(configDir(), p, { repo: REPO, checkout: options.checkout ?? null })]));
|
||||
sso = await createSso(system.console.sso, { secrets });
|
||||
// FED R6 (#1800): access.json users sign in too. R1's loader reads the file on every check,
|
||||
// with the same context as `mosaic access`; a missing or refused file signs nobody in.
|
||||
const users = consoleAccess({
|
||||
load: () => loadAccess(accessFilePath(configDir()), accessContextFor(system, process.env)),
|
||||
business: busBusiness({ dataRoot: system.dataRoot, business: options.business ?? null }),
|
||||
});
|
||||
sso = await createSso(system.console.sso, { secrets, users });
|
||||
// Each request of a granted user's session gets the bus options over that user's own
|
||||
// user-cli transport. The human transport above serves only the chat's `plugins` read (C6).
|
||||
userOptions = userOptionsFor({ owner: ownerCall, send: sendOn, transport: user => userCaller({ dataRoot: system.dataRoot, socket: socketPath(system.dataRoot), business: options.business ?? null, user }) });
|
||||
}
|
||||
let server;
|
||||
try {
|
||||
server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview, sso, proof, send, settingsWrite, projectFiles, projectFile, libraryFile, libraryStatus, librarySync, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing });
|
||||
server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview, sso, proof, send, settingsWrite, projectFiles, projectFile, libraryFile, libraryStatus, librarySync, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, userOptions, federation: system?.federation ?? null });
|
||||
} catch (err) { sso?.close(); throw err; }
|
||||
let testerServer = null;
|
||||
if (testerSocket) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// FED R6 (#1800): the Console's bus-backed options for startServer, built over one transport
|
||||
// `call`. cli.mjs builds them once over the human transport. A granted user's session gets the same
|
||||
// set over its own user transport (user-bus.mjs userCaller), so no route a user reaches holds a
|
||||
// human-transport object, and the broker decides every verb the user sends (design 3.1 D4).
|
||||
import { createChat } from './chat.mjs';
|
||||
import { views } from '../../bus/src/views.mjs';
|
||||
|
||||
// Row 194 (#1692, D7): the Agents section's verbs. The caller refuses any other, so the server's
|
||||
// agentWrite reaches only these.
|
||||
export const AGENT_VERBS = ['instance.launch', 'instance.regenerate', 'instance.end', 'veto.set', 'instance.request'];
|
||||
export const TESTER_AGENT_VERBS = ['instance.launch', 'instance.end', 'instance.request'];
|
||||
export const agentCaller = (call, verbs) => async (verb, args) => {
|
||||
if (!verbs.includes(verb)) throw new Error(`the Console does not call ${verb}`);
|
||||
return call(verb, args);
|
||||
};
|
||||
|
||||
// `send` is the --send flag: without it every write is null. `owner` is the owner's transport for
|
||||
// a granted user's options; it goes to createChat, which sends it only the `plugins` read.
|
||||
export function consoleBus(call, { send: sendOn = false, owner = null } = {}) {
|
||||
const o = {
|
||||
processing: { read: () => call('processing.status'), halt: args => call('processing.halt', args), resume: args => call('processing.resume', args) },
|
||||
chat: createChat({ call, owner }),
|
||||
// Row 155 (#1649): `names` is a webui read only, so it sits beside the shared views. Row 165
|
||||
// (#1662): so is `instances`, and its write `instances.set` is only under --send. Row 65
|
||||
// (mos-0840): so is `routes`, and its write `routes.set`.
|
||||
// Row 278: this owner/reader read belongs to the Console, not the shared
|
||||
// role views or the tester adapter. The bus decides caller eligibility.
|
||||
bus: { ...views({ call }), names: () => call('names'), instances: () => call('instances'), routes: () => call('routes'), 'thread.claims': () => call('thread.claims') },
|
||||
// Row 148: the spec and docs editor reads like the other reads and saves only under --send.
|
||||
projectFiles: args => call('project.files', args),
|
||||
projectFile: args => call('project.file', args),
|
||||
libraryFile: args => call('library.file', args), libraryStatus: args => call('library.status', args),
|
||||
send: null, librarySync: null, settingsWrite: null, projectSave: null, nameWrite: null, assistantStop: null, instancesWrite: null, routesWrite: null, agentWrite: null,
|
||||
};
|
||||
if (sendOn) {
|
||||
o.send = args => call('message.send', args); o.librarySync = args => call('library.sync', args); o.settingsWrite = args => call('limits.set', args);
|
||||
o.projectSave = args => call('project.file.save', args); o.nameWrite = args => call('name.set', args); o.assistantStop = args => call('assistant.stop', args);
|
||||
o.instancesWrite = args => call('instances.set', args); o.routesWrite = args => call('routes.set', args); o.agentWrite = agentCaller(call, AGENT_VERBS);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
// A granted user's options per session: consoleBus over `transport(session.user)`, that user's own
|
||||
// transport, with the owner's transport `owner` passed as it is. Nothing from the session or the
|
||||
// request reaches `owner` (mos-1394, sage-3157): createChat sends it only `plugins` with {}.
|
||||
export function userOptionsFor({ owner, transport, send = false }) {
|
||||
if (typeof owner !== 'function' || typeof transport !== 'function') throw new TypeError('userOptionsFor needs owner and transport functions');
|
||||
return session => consoleBus(transport(session.user), { send, owner });
|
||||
}
|
||||
@@ -476,5 +476,5 @@
|
||||
setTimeout(() => { const a = document.activeElement; if (!a || a === document.body || a.disabled || !a.getClientRects().length) line.focus(); });
|
||||
});
|
||||
// Sign out (sage-1347, F7): a plain same-origin form POST to /auth/logout, shown only under --sso.
|
||||
api('/api/config').then(config => { board = config.board; $('board-url').textContent = `Board: ${board}`; if (config.sso === true) { sso = true; $('sign-out').hidden = out(); } }).catch(err => { if (machine) error(err); }).finally(refresh);
|
||||
api('/api/config').then(config => { board = config.board; $('board-url').textContent = `Board: ${board}`; window.MosaicFederation?.render(config.federation, $('federation')); if (config.sso === true) { sso = true; $('sign-out').hidden = out(); } }).catch(err => { if (machine) error(err); }).finally(refresh);
|
||||
})();
|
||||
@@ -0,0 +1,26 @@
|
||||
/* FED R6 (#1800): the hub label and plain peer links from /api/config's `federation`. Text and
|
||||
href only: no script, no fetch to a peer and no proxy (the hub proxy route is R8's). */
|
||||
(() => {
|
||||
'use strict';
|
||||
function render(federation, container) {
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
if (!federation || typeof federation !== 'object') { container.hidden = true; return; }
|
||||
if (typeof federation.self === 'string') {
|
||||
const label = document.createElement('span');
|
||||
label.id = 'hub-label';
|
||||
label.textContent = `This Console: ${federation.self}`;
|
||||
container.append(label);
|
||||
}
|
||||
for (const peer of Array.isArray(federation.peers) ? federation.peers : []) {
|
||||
if (typeof peer?.origin !== 'string' || !/^https?:\/\//.test(peer.origin)) continue;
|
||||
const a = document.createElement('a');
|
||||
a.className = 'peer-link';
|
||||
a.setAttribute('href', peer.origin);
|
||||
a.textContent = typeof peer.label === 'string' && peer.label ? peer.label : peer.origin;
|
||||
container.append(' ', a);
|
||||
}
|
||||
container.hidden = !container.childNodes.length;
|
||||
}
|
||||
window.MosaicFederation = Object.freeze({ render });
|
||||
})();
|
||||
@@ -4,11 +4,12 @@
|
||||
<link rel="stylesheet" href="/shared/app.css"><link rel="stylesheet" href="/tokens.css"><link rel="stylesheet" href="/console.css"><link rel="stylesheet" href="/live.css"><link rel="stylesheet" href="/bus.css"><link rel="stylesheet" href="/chat.css"><link rel="stylesheet" href="/shell.css">
|
||||
<link rel="stylesheet" href="/editor.css"><script src="/editor.js" defer></script>
|
||||
<link rel="stylesheet" href="/processing.css"><script src="/processing.js" defer></script>
|
||||
<script src="/brand.js" defer></script><script src="/app.js" defer></script><script src="/markdown-preview.js" defer></script><script src="/chat.js" defer></script><script src="/agents.js" defer></script><script src="/bus.js" defer></script></head>
|
||||
<script src="/federation.js" defer></script><script src="/brand.js" defer></script><script src="/app.js" defer></script><script src="/markdown-preview.js" defer></script><script src="/chat.js" defer></script><script src="/agents.js" defer></script><script src="/bus.js" defer></script></head>
|
||||
<body>
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
<header class="cmdbar"><a href="/" class="wordmark"><svg class="mark" viewBox="0 0 16 17" aria-hidden="true"><use href="/icons.svg#mark"/></svg>Mosaic<small>Console</small></a>
|
||||
<button type="button" class="cmdk" id="cmdk" aria-haspopup="dialog" aria-controls="cmdk-dialog"><svg class="i" aria-hidden="true"><use href="/icons.svg#i-search"/></svg><span>Go to a view, decision or task</span><kbd>Ctrl K</kbd></button>
|
||||
<nav id="federation" class="federation small" aria-label="Consoles" hidden></nav>
|
||||
<p class="fresh" id="fresh"><span id="fresh-board"></span><span id="fresh-bus"></span></p>
|
||||
<div class="cmd-right"><label>Palette<select id="palette"></select></label><label>Appearance<select id="mode"><option value="system">system</option><option value="light">light</option><option value="dim">dim</option><option value="dark">dark</option></select></label><button class="btn" id="refresh" type="button">Refresh</button><button class="btn" id="pause" type="button" aria-pressed="false">Pause</button><form id="sign-out" method="post" action="/auth/logout" hidden><button class="btn" type="submit">Sign out</button></form></div></header>
|
||||
<div id="processing-banner" class="processing-banner" role="alert" hidden></div>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-design="console" data-palette="harbor">
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Mosaic Console</title>
|
||||
<link rel="stylesheet" href="/tokens.css"><link rel="stylesheet" href="/console.css"><link rel="stylesheet" href="/chat.css">
|
||||
<script src="/federation.js" defer></script><script src="/chat.js" defer></script><script src="/user.js" defer></script></head>
|
||||
<body>
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
<header class="cmdbar"><a href="/" class="wordmark"><svg class="mark" viewBox="0 0 16 17" aria-hidden="true"><use href="/icons.svg#mark"/></svg>Mosaic<small>Console</small></a>
|
||||
<nav id="federation" class="federation small" aria-label="Consoles" hidden></nav>
|
||||
<div class="cmd-right"><form id="sign-out" method="post" action="/auth/logout"><button class="btn" type="submit">Sign out</button></form></div></header>
|
||||
<main id="main" class="content" tabindex="-1">
|
||||
<div id="signed-out" class="banner ref" role="alert" tabindex="-1" hidden>You are signed out. <a href="/">Sign in again</a>.</div>
|
||||
<p id="user-notice" class="banner ref">The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your messages.</p>
|
||||
<div id="chat"></div>
|
||||
</main>
|
||||
<footer class="foot"><span id="footer">Mosaic Stack</span></footer>
|
||||
</body></html>
|
||||
@@ -0,0 +1,23 @@
|
||||
/* FED R6 (#1800): a granted user's page. Projects, threads and messages through chat.js; the
|
||||
broker decides what this user reads and sends. No owner control is on this page. */
|
||||
(() => {
|
||||
'use strict';
|
||||
const $ = id => document.getElementById(id);
|
||||
let canSend = false, ready = false;
|
||||
const route = () => {
|
||||
const path = location.hash.slice(1).split('?')[0];
|
||||
return path.startsWith('/chat') ? path : '/chat';
|
||||
};
|
||||
function show(navigated) {
|
||||
if (ready && window.MosaicChat) void window.MosaicChat.render({ path: route(), container: $('chat'), canSend, navigated });
|
||||
}
|
||||
document.addEventListener('mosaic-signed-out', () => { $('signed-out').hidden = false; $('sign-out').hidden = true; });
|
||||
window.addEventListener('hashchange', () => show(true));
|
||||
fetch('/api/config', { cache: 'no-store', redirect: 'manual' }).then(async res => {
|
||||
if (res.type === 'opaqueredirect' || res.status === 401) { document.dispatchEvent(new Event('mosaic-signed-out')); return; }
|
||||
const config = await res.json();
|
||||
canSend = config.send === true;
|
||||
window.MosaicFederation?.render(config.federation, $('federation'));
|
||||
ready = true; show(true);
|
||||
}).catch(() => { $('chat').textContent = 'The Console could not be read. Refresh to try again.'; });
|
||||
})();
|
||||
@@ -27,7 +27,7 @@ export function boardURL(value) {
|
||||
const root = resolve(import.meta.dirname, 'public');
|
||||
const files = new Map([
|
||||
['/', ['index.html', 'text/html; charset=utf-8']],
|
||||
...['app.js', 'brand.js', 'bus.js', 'markdown-preview.js', 'chat.js', 'processing.js', 'agents.js'].map(f => ['/' + f, [f, 'text/javascript; charset=utf-8']]),
|
||||
...['app.js', 'brand.js', 'bus.js', 'markdown-preview.js', 'chat.js', 'processing.js', 'agents.js', 'federation.js'].map(f => ['/' + f, [f, 'text/javascript; charset=utf-8']]),
|
||||
...['shared/app.css', 'tokens.css', 'console.css', 'live.css', 'bus.css', 'shell.css', 'chat.css', 'processing.css'].map(f => ['/' + f, [f, 'text/css; charset=utf-8']]),
|
||||
['/editor.js', ['editor.js', 'text/javascript; charset=utf-8']],
|
||||
['/editor.css', ['editor.css', 'text/css; charset=utf-8']],
|
||||
@@ -35,6 +35,26 @@ const files = new Map([
|
||||
...[400, 500, 600, 700].map(w => [`/assets/fonts/manrope-${w}.woff2`, [`assets/fonts/manrope-${w}.woff2`, 'font/woff2']]),
|
||||
...[400, 500, 700].map(w => [`/assets/fonts/jetbrains-mono-${w}.woff2`, [`assets/fonts/jetbrains-mono-${w}.woff2`, 'font/woff2']]),
|
||||
]);
|
||||
// FED R6 (#1800): a granted user's page. The owner page's markup and scripts are never served to a
|
||||
// user session, so its controls are not sent at all (design 3.1 D5).
|
||||
const USER_FILES = new Map([
|
||||
['/', ['user.html', 'text/html; charset=utf-8']],
|
||||
...['user.js', 'chat.js', 'federation.js'].map(f => ['/' + f, [f, 'text/javascript; charset=utf-8']]),
|
||||
...['tokens.css', 'console.css', 'chat.css'].map(f => ['/' + f, [f, 'text/css; charset=utf-8']]),
|
||||
...[...files].filter(([p]) => p.startsWith('/assets/fonts/') || p === '/icons.svg'),
|
||||
]);
|
||||
// The only routes a granted user's session reaches: the chat, the page config and the user page.
|
||||
// Anything else is the owner's, so a route added later is owner-only until it is added here.
|
||||
export function userRoute(path, method) {
|
||||
if (path === '/api/chat' || path.startsWith('/api/chat/')) return true;
|
||||
if (method !== 'GET' && method !== 'HEAD') return false;
|
||||
return path === '/api/config' || path === '/favicon.ico' || USER_FILES.has(path);
|
||||
}
|
||||
// The hub label and plain peer links, from system.federation: ids, labels and origins only.
|
||||
function federationView(federation) {
|
||||
if (federation === null || typeof federation !== 'object') return null;
|
||||
return { self: federation.self ?? null, peers: (federation.peers ?? []).map(({ id, label, origin }) => ({ id, label, origin })) };
|
||||
}
|
||||
function json(res, status, body) {
|
||||
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
||||
res.end(JSON.stringify(body));
|
||||
@@ -579,7 +599,7 @@ async function projectFileRead(res, read, path, search) {
|
||||
// `log` takes one line per send attempt that got past sign-in; never the body. `fieldsFile` is for
|
||||
// tests; the field table is otherwise the committed packages/tasks/fields.json.
|
||||
// `agentWrite` (row 194, D7) is `(verb, args) => Promise` for the Agents section's verbs and instance.request.
|
||||
export async function startServer({ host = '127.0.0.1', port = 7330, board = DEFAULT_BOARD, timeout = 20000, bus = null, reads = null, preview = null, sso = null, tester = false, testerSocket = null, testerAuthHeader = null, proof = null, testerProofHeader = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, processing = null, log = line => console.log(line), fieldsFile = undefined } = {}) {
|
||||
export async function startServer({ host = '127.0.0.1', port = 7330, board = DEFAULT_BOARD, timeout = 20000, bus = null, reads = null, preview = null, sso = null, tester = false, testerSocket = null, testerAuthHeader = null, proof = null, testerProofHeader = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, processing = null, userOptions = null, federation = null, log = line => console.log(line), fieldsFile = undefined } = {}) {
|
||||
if (!isLoopback(host)) throw new Error('refusing to bind to non-loopback host');
|
||||
if (host === 'localhost') host = '127.0.0.1';
|
||||
if (tester && preview !== null) throw new Error('tester listener cannot use preview mode');
|
||||
@@ -591,6 +611,11 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
if (tester && !testerSocket) throw new Error('tester listener requires private socket');
|
||||
if (!tester && testerSocket) throw new Error('tester socket requires tester listener');
|
||||
const upstream = boardURL(board);
|
||||
// `userOptions` (FED R6, #1800) is `session => options`, the bus-backed options over that session
|
||||
// user's own transport (console-bus.mjs). Only a signed-in listener has user sessions.
|
||||
if (userOptions !== null && (sso === null || typeof userOptions !== 'function')) throw new Error('user sessions need --sso');
|
||||
const owner = { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files };
|
||||
const hub = federationView(federation);
|
||||
// An object with none of the three is refused too: asking for the preview never serves without it.
|
||||
if (preview !== null && !(preview = previewOptions({ publicHost: preview.host, authHeader: preview.header, allowUser: preview.user }))) {
|
||||
throw new Error('preview mode needs --public-host, --auth-header and --allow-user together');
|
||||
@@ -598,6 +623,8 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
if (proof !== null && (preview === null || tester)) throw new Error('proxy proof needs preview mode');
|
||||
if (send !== null && ((preview === null && sso === null) || typeof send !== 'function')) throw new Error('--send needs the preview mode (--public-host, --auth-header and --allow-user) or --sso');
|
||||
const server = createServer(async (req, res) => {
|
||||
// A granted user's request rebinds these below; every other request keeps the owner's.
|
||||
let { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files } = owner, granted = false;
|
||||
res.setHeader('x-content-type-options', 'nosniff');
|
||||
res.setHeader('referrer-policy', 'no-referrer');
|
||||
// Under --sso the Sign out form's POST is redirected to the provider's end-session endpoint, and
|
||||
@@ -653,6 +680,14 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
return res.end();
|
||||
}
|
||||
if (session) signer = { sso: true, user: session.name, host: sso.publicHost, origin: sso.publicOrigin };
|
||||
// FED R6 (#1800): a granted user. The Console's gate first; behind it no human-transport
|
||||
// object is left, so a route past the gate reaches the broker only as this user.
|
||||
if (session?.user !== undefined) {
|
||||
if (!userRoute(path, req.method)) return json(res, 403, { error: 'owner-only', message: 'this part of the Console is the business owner\'s' });
|
||||
if (userOptions === null) return json(res, 503, { error: 'not-configured', message: 'the Console has no user transport' });
|
||||
({ bus, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing } = userOptions(session));
|
||||
reads = null; upstream = null; files = USER_FILES; granted = true;
|
||||
}
|
||||
}
|
||||
if (named && preview !== null) {
|
||||
// One value, equal to the user id; the proxy strips client copies and sets it after sign-in.
|
||||
@@ -715,6 +750,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
const allowed = path === '/api/board' || conversation ? 'GET' : ['/api/seen', '/api/reply'].includes(path) ? 'POST' : null;
|
||||
if (allowed) {
|
||||
if (req.method !== allowed) return json(res, 405, { error: 'method not allowed' });
|
||||
if (upstream === null) return json(res, 503, { error: 'not-configured', message: 'the Console has no board for this session' });
|
||||
let bytes;
|
||||
if (allowed === 'POST') {
|
||||
try { bytes = await body(req); } catch (err) { return json(res, 400, { error: err.message }); }
|
||||
@@ -763,7 +799,8 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') return json(res, 405, { error: 'method not allowed' });
|
||||
// `send`: the page shows the composer only where a send can pass (the public name, --send on).
|
||||
// `edit`: the editor shows Save only where a save can pass, the same test as the route's.
|
||||
if (path === '/api/config') return json(res, 200, { board: upstream, ...(tester ? { tester: true } : {}), ...(sso ? { sso: true } : {}), send: !tester && Boolean(send) && named, edit: !tester && Boolean(send) && Boolean(projectSave) && named });
|
||||
// `user` and `federation` (FED R6) only when there is one, so the owner's config is unchanged.
|
||||
if (path === '/api/config') return json(res, 200, { board: upstream, ...(tester ? { tester: true } : {}), ...(sso ? { sso: true } : {}), ...(granted ? { user: true } : {}), ...(hub ? { federation: hub } : {}), send: !tester && Boolean(send) && named, edit: !tester && !granted && Boolean(send) && Boolean(projectSave) && named });
|
||||
if (path === '/healthz') return json(res, 200, { ok: true, ...(sso ? { sso: sso.health() } : {}) });
|
||||
if (path === '/favicon.ico') { res.writeHead(204); return res.end(); }
|
||||
const file = files.get(path);
|
||||
|
||||
@@ -22,7 +22,7 @@ const SESSION_ABSOLUTE = 8 * 3600 * 1000, SESSION_IDLE = 3600 * 1000;
|
||||
const MAX_SESSIONS = 64, MAX_LOGINS = 32;
|
||||
const FETCH_TIMEOUT = 10000;
|
||||
const RETRY_CAP = 300000;
|
||||
const CODES = new Set(['login-record-missing', 'state-mismatch', 'auth-error-response', 'grant-failed', 'id-token-invalid', 'signature-invalid', 'alg-refused', 'issuer-mismatch', 'subject-unknown', 'discovery-failed', 'jwks-failed']);
|
||||
const CODES = new Set(['login-record-missing', 'state-mismatch', 'auth-error-response', 'grant-failed', 'id-token-invalid', 'signature-invalid', 'alg-refused', 'issuer-mismatch', 'subject-unknown', 'user-revoked', 'discovery-failed', 'jwks-failed']);
|
||||
|
||||
const sha256 = value => createHash('sha256').update(value).digest('hex');
|
||||
const escape = text => String(text).replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
||||
@@ -123,7 +123,9 @@ async function discover(p, fetchOptions) {
|
||||
// `config` is console.sso, checked again here; `secrets` maps provider id to its client secret
|
||||
// (readSlot). `fetch` is for tests: a fetch that trusts only the fake OP's certificate.
|
||||
// `retryFirst` is for tests too; production waits 5 s, doubling, capped at 5 minutes.
|
||||
export async function createSso(config, { secrets, fetch: customFetch = null, log = line => console.log(line), retryFirst = 5000 } = {}) {
|
||||
// `users` (FED R6, #1800) is consoleAccess() from access.mjs, or null: a subject that is not the
|
||||
// owner's signs in as the access-file user with a live grant, and each request checks it again.
|
||||
export async function createSso(config, { secrets, fetch: customFetch = null, log = line => console.log(line), retryFirst = 5000, users = null } = {}) {
|
||||
checkConsole({ sso: config });
|
||||
oauth ??= await import('oauth4webapi');
|
||||
const publicOrigin = config.publicOrigin;
|
||||
@@ -278,14 +280,18 @@ export async function createSso(config, { secrets, fetch: customFetch = null, lo
|
||||
try { header = JSON.parse(Buffer.from(result.id_token.split('.')[0], 'base64url').toString('utf8')); } catch { header = null; }
|
||||
if (!header || !entry.config.algs.includes(header.alg)) { refuse('alg-refused', pid); return failed(res, clear); }
|
||||
if (claims.iss !== entry.config.issuer) { refuse('issuer-mismatch', pid); return failed(res, clear); }
|
||||
const name = typeof claims.sub === 'string' ? entry.subjects.get(claims.sub) : undefined;
|
||||
let name = typeof claims.sub === 'string' ? entry.subjects.get(claims.sub) : undefined;
|
||||
// FED R6: console.sso subjects and access-file subjects are disjoint (R1), so the owner's
|
||||
// list is asked first and a user is asked only on a miss.
|
||||
const user = name === undefined && typeof claims.sub === 'string' ? users?.find({ provider: pid, sub: claims.sub }) ?? null : null;
|
||||
if (user !== null) name = user;
|
||||
if (name === undefined) {
|
||||
refuse('subject-unknown', pid, ` iss=${JSON.stringify(String(claims.iss).slice(0, 255))} sub=${JSON.stringify(String(claims.sub).slice(0, 255))}`);
|
||||
return failed(res, clear);
|
||||
}
|
||||
// Step 7: the session, then the return path.
|
||||
const id = randomBytes(32).toString('base64url'), now = Date.now();
|
||||
remember(sessions, sha256(id), { provider: pid, iss: claims.iss, sub: claims.sub, name, idToken: result.id_token, created: now, seen: now }, MAX_SESSIONS);
|
||||
remember(sessions, sha256(id), { provider: pid, iss: claims.iss, sub: claims.sub, name, ...(user !== null ? { user } : {}), idToken: result.id_token, created: now, seen: now }, MAX_SESSIONS);
|
||||
return redirect(res, returnURL(record.ret), [...clear, `${SESSION_COOKIE}=${id}; ${ATTRS}`]);
|
||||
}
|
||||
|
||||
@@ -298,6 +304,13 @@ export async function createSso(config, { secrets, fetch: customFetch = null, lo
|
||||
if (!s) return null;
|
||||
const now = Date.now();
|
||||
if (now - s.created >= SESSION_ABSOLUTE || now - s.seen >= SESSION_IDLE) { sessions.delete(key); return null; }
|
||||
// FED R6 (design 3.1 D5): a user's grant is read again on every request, so a user revoked,
|
||||
// removed or expired since sign-in has no session from the next request on.
|
||||
if (s.user !== undefined && !users?.check(s)) {
|
||||
sessions.delete(key);
|
||||
refuse('user-revoked', s.provider, ` user=${JSON.stringify(s.user)}`);
|
||||
return null;
|
||||
}
|
||||
if (touch) s.seen = now;
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// FED R6 (#1800): a granted user's bus transport, R3's user-cli.mjs (#414, design 3.6). Kept apart
|
||||
// from bus.mjs so the human transport's reviewed bytes stay as row 164 froze them
|
||||
// (bus/tests/channel-proof.test.mjs). Same protocol as humanCall: the request on stdin, JSON on
|
||||
// stdout, or a bus error code as the last `^[a-z-]{1,64}$` line of stderr.
|
||||
import { spawn } from 'node:child_process';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { readHostState } from '../../cli/src/host.mjs';
|
||||
import { BusReadError } from './bus.mjs';
|
||||
|
||||
export const USER_CLI = fileURLToPath(new URL('../../bus/src/user-cli.mjs', import.meta.url));
|
||||
|
||||
// The Console sets the user nonce itself, so user-cli.mjs doesn't re-exec and the broker sees the
|
||||
// Console (`cli.mjs serve`) as its parent (principal.mjs). The human nonce is dropped, so this child
|
||||
// can never pass as human-cli, and it only ever runs USER_CLI.
|
||||
export function userCall({ socket, business, user, env = process.env, timeoutMs = 30000, maxBytes = 8 * 1024 * 1024 }) {
|
||||
const { MOSAIC_BUS_CLI_NONCE, ...rest } = env;
|
||||
return (verb, args = {}) => new Promise((resolve, reject) => {
|
||||
let child;
|
||||
try {
|
||||
child = spawn(process.execPath, [USER_CLI, socket], { env: { ...rest, MOSAIC_BUS_USER_NONCE: randomBytes(32).toString('hex') }, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
} catch { return reject(new BusReadError('outcome-unknown', 'the bus transport did not start')); }
|
||||
const out = [], err = [];
|
||||
let size = 0, settled = false;
|
||||
const done = (fn, value) => { if (!settled) { settled = true; clearTimeout(timer); fn(value); } };
|
||||
const timer = setTimeout(() => { child.kill('SIGKILL'); done(reject, new BusReadError('outcome-unknown', 'the bus transport did not finish in time')); }, timeoutMs);
|
||||
child.stdout.on('data', chunk => {
|
||||
size += chunk.length;
|
||||
if (size > maxBytes) { child.kill('SIGKILL'); return done(reject, new BusReadError('response-too-large')); }
|
||||
out.push(chunk);
|
||||
});
|
||||
child.stderr.on('data', chunk => { if (err.length < 64) err.push(chunk); });
|
||||
child.on('error', () => done(reject, new BusReadError('outcome-unknown', 'the bus transport did not start')));
|
||||
child.on('close', status => {
|
||||
if (status === null) return done(reject, new BusReadError('outcome-unknown', 'the bus transport did not finish'));
|
||||
if (status !== 0) {
|
||||
const code = Buffer.concat(err).toString('utf8').trim().split('\n').reverse().find(l => /^[a-z-]{1,64}$/.test(l)) ?? 'invalid-response';
|
||||
return done(reject, new BusReadError(code));
|
||||
}
|
||||
try { done(resolve, JSON.parse(Buffer.concat(out).toString('utf8'))); }
|
||||
catch { done(reject, new BusReadError('invalid-response')); }
|
||||
});
|
||||
child.stdin.on('error', () => {}); // A child that exits early reports through 'close'.
|
||||
child.stdin.end(`${JSON.stringify({ business, user, verb, args })}\n`);
|
||||
});
|
||||
}
|
||||
|
||||
// The business is the --business flag, or else the live bus host's, read on every call, as busCaller does.
|
||||
export function busBusiness({ dataRoot, business = null }) {
|
||||
return () => {
|
||||
if (business) return business;
|
||||
let state;
|
||||
try { state = readHostState(dataRoot); } catch { throw new BusReadError('no-bus-host', 'the bus host state file is unreadable'); }
|
||||
if (!state?.live) throw new BusReadError('no-bus-host', 'no bus host is running and no --business was given');
|
||||
return state.business;
|
||||
};
|
||||
}
|
||||
|
||||
// One granted user's caller, for that user's session only.
|
||||
export function userCaller({ dataRoot, socket, business = null, user, env = process.env, timeoutMs = 30000 }) {
|
||||
const which = busBusiness({ dataRoot, business });
|
||||
return async (verb, args) => userCall({ socket, business: which(), user, env, timeoutMs })(verb, args);
|
||||
}
|
||||
@@ -18,8 +18,8 @@ test('served Console browser: real board fixtures, keyboard, drafts, receipts, t
|
||||
assert.equal(await b.evaluate('document.querySelector("#session-count").textContent'), '3 of 4');
|
||||
await b.evaluate('document.querySelector("#hide-offline").click()');
|
||||
assert.equal(await b.evaluate('document.querySelectorAll("table.sessions tbody tr").length'), 4);
|
||||
// Rows 129, 154, 191 and 194: chat.js, editor.js, processing.js and agents.js are the scripts added, same origin; the list is the pin, so another script is a visible change.
|
||||
assert.deepEqual(await b.evaluate('[...document.querySelectorAll("script")].map(s => s.getAttribute("src"))'), ['/editor.js', '/processing.js', '/brand.js', '/app.js', '/markdown-preview.js', '/chat.js', '/agents.js', '/bus.js']);
|
||||
// Rows 129, 154, 191, 194 and 294: chat.js, editor.js, processing.js, agents.js and federation.js are the scripts added, same origin; the list is the pin, so another script is a visible change.
|
||||
assert.deepEqual(await b.evaluate('[...document.querySelectorAll("script")].map(s => s.getAttribute("src"))'), ['/editor.js', '/processing.js', '/federation.js', '/brand.js', '/app.js', '/markdown-preview.js', '/chat.js', '/agents.js', '/bus.js']);
|
||||
assert.equal(await b.evaluate(`document.querySelectorAll('script[src="/processing.js"]').length`), 1);
|
||||
assert.equal(await b.evaluate(`document.querySelectorAll('script[src="/chat.js"]').length`), 1);
|
||||
assert.equal(await b.evaluate('!!window.injected'), false);
|
||||
|
||||
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user