Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24b7454470 | ||
|
|
8616b9a4f6 | ||
|
|
f265840892 | ||
|
|
66c1817176 | ||
|
|
685ff053b6 | ||
|
|
024499fc29 | ||
|
|
65f1b81f3b | ||
|
|
0622c2fc8c | ||
|
|
c22dcfdd04 | ||
|
|
f392be6ac9 | ||
|
|
df21d2f4de |
No files matched your search
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
// FED R6 (#1800, federation design 3.1 D3-D5, 3.4 and 3.7): a granted user signs in to the
|
||||
// Console. Every provider is the fake OP in helpers/fake-oidc.mjs on loopback; no test reaches a
|
||||
// real provider, the live Console or a real config, slot or access file. The broker is an
|
||||
// in-process Broker with an access function (bus/tests/user-principal.test.mjs), reached through a
|
||||
// per-session user transport that binds the user on every call, as user-cli.mjs does. The real
|
||||
// user-cli spawn is checked on its own in G8 against a recording socket.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { request, createServer as createHttpServer } from 'node:http';
|
||||
import { createServer as createNetServer } from 'node:net';
|
||||
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 '../../bus/src/store.mjs';
|
||||
import { Broker } from '../../bus/src/broker.mjs';
|
||||
import { HUMAN_CLI } from '../../cli/src/transport.mjs';
|
||||
import { startServer, userRoute } from '../src/serve.mjs';
|
||||
import { createSso, SESSION_COOKIE } from '../src/sso.mjs';
|
||||
import { consoleAccess } from '../src/access.mjs';
|
||||
import { consoleBus, userOptionsFor } from '../src/console-bus.mjs';
|
||||
import { BusReadError } from '../src/bus.mjs';
|
||||
import { USER_CLI, userCall } from '../src/user-bus.mjs';
|
||||
import { close, fixture } from './fixture.mjs';
|
||||
import { browser } from './browser.mjs';
|
||||
import { fakeOidc } from './helpers/fake-oidc.mjs';
|
||||
|
||||
const HOST = 'console.example.test', ORIGIN = `https://${HOST}`;
|
||||
const PUBLIC = new URL('../src/public/', import.meta.url);
|
||||
const NOTICE = "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your messages.";
|
||||
const OWNER_MARKER = 'owner-plugins-marker-7f3a';
|
||||
const FEDERATION = { self: 'hub', peers: [{ id: 'factory', origin: 'https://factory.example.test', label: 'Software factory' }] };
|
||||
|
||||
function call(port, { method = 'GET', path = '/', headers = {}, body, host = HOST } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const length = body === undefined ? {} : { 'content-length': Buffer.byteLength(body) };
|
||||
const req = request({ host: '127.0.0.1', port, method, path, headers: { host, ...length, ...headers } }, res => {
|
||||
let text = '';
|
||||
res.on('data', c => (text += c));
|
||||
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: text && (res.headers['content-type'] ?? '').startsWith('application/json') ? JSON.parse(text) : text }));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.end(body);
|
||||
});
|
||||
}
|
||||
const setCookies = r => [r.headers['set-cookie'] ?? []].flat();
|
||||
const sessionOf = r => setCookies(r).find(c => c.startsWith(`${SESSION_COOKIE}=`) && !c.includes('Max-Age=0'))?.split(';')[0] ?? null;
|
||||
const writeHeaders = cookie => ({ cookie, origin: ORIGIN, 'sec-fetch-site': 'same-origin', 'content-type': 'application/json' });
|
||||
const grant = (over = {}) => ({ id: 'g-1', user: 'alice', business: 'demo', projects: ['*'], instances: ['assistant'], level: 'collaborator', ...over });
|
||||
const ALICE = { id: 'alice', home: 'local', subjects: [{ provider: 'a', sub: 'alice-sub' }] };
|
||||
|
||||
// The fake OP, an in-process broker for business `demo`, createSso with the access hook, and the
|
||||
// human listener. The owner's options are counting stubs; each user session's options are
|
||||
// consoleBus over that user's transport. `counts` records every owner stub and user transport call.
|
||||
async function setup(t, { users = [ALICE], grants = [grant()] } = {}) {
|
||||
const op = await fakeOidc();
|
||||
t.after(() => op.close());
|
||||
const iss = op.issuer('a');
|
||||
const root = mkdtempSync(join(tmpdir(), 'webui-granted-user-'));
|
||||
const store = new Store(root);
|
||||
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
|
||||
const access = { doc: { accessVersion: 1, revision: 1, users, grants }, fail: false };
|
||||
const load = () => { if (access.fail) throw new Error('access file refused'); return access.doc; };
|
||||
const broker = new Broker({
|
||||
store,
|
||||
businesses: {
|
||||
demo: {
|
||||
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: {},
|
||||
roles: {
|
||||
chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send'], crossRole: [] } },
|
||||
assistant: { definition: 'assistant', authority: { withinRole: ['message.send'], crossRole: [] } },
|
||||
},
|
||||
definitions: { 'chief-of-staff': {}, assistant: { messaging: JSON.parse(readFileSync(new URL('../../../roles/assistant.json', import.meta.url), 'utf8')).messaging } },
|
||||
},
|
||||
},
|
||||
access: load,
|
||||
});
|
||||
const human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
||||
const counts = { owner: 0, user: 0 };
|
||||
// As user-cli.mjs: a fresh bind per call, and the broker's refusal code as the transport's.
|
||||
// `userVerbs` and `ownerVerbs` record what each transport carried for a user session.
|
||||
const userVerbs = [], ownerVerbs = [];
|
||||
const userTransport = user => async (verb, args = {}) => {
|
||||
counts.user++; userVerbs.push(verb);
|
||||
try { return broker.request(broker.bindUser({ business: 'demo', user }), { verb, args }); }
|
||||
catch (e) { throw new BusReadError(e.code ?? 'invalid-response'); }
|
||||
};
|
||||
// The owner's transport as cli.mjs passes it (ownerCall). Its answer carries a marker that must
|
||||
// never reach a user's response.
|
||||
const ownerCall = async (verb, args) => { ownerVerbs.push([verb, args]); return { plugins: { channel: ['webui'] }, marker: OWNER_MARKER }; };
|
||||
const opened = [];
|
||||
const userOptions = userOptionsFor({ owner: ownerCall, send: true, transport: user => { opened.push(user); return userTransport(user); } });
|
||||
const lines = [];
|
||||
const log = line => lines.push(line);
|
||||
const config = { publicOrigin: ORIGIN, providers: [{ id: 'a', issuer: iss.issuer, clientId: iss.clientId, clientSecret: { slot: 'a' }, algs: ['RS256'], subjects: [{ sub: 'owner-sub', name: 'jason' }] }] };
|
||||
const sso = await createSso(config, { secrets: new Map([['a', iss.clientSecret]]), fetch: op.fetch, log, users: consoleAccess({ load, business: () => 'demo' }) });
|
||||
t.after(() => sso.close());
|
||||
const stub = name => async () => { counts.owner++; return name === 'send' ? { id: 'msg-0001' } : [{ verb: name }]; };
|
||||
const ownerBus = Object.fromEntries(['inbox', 'tasks', 'agents', 'trail', 'decisions', 'names', 'instances', 'routes', 'thread.claims'].map(v => [v, stub(v)]));
|
||||
const owner = {
|
||||
bus: ownerBus, send: stub('send'), settingsWrite: stub('settings'), projectFiles: stub('files'), projectFile: stub('file'), projectSave: stub('save'),
|
||||
nameWrite: stub('name'), assistantStop: stub('stop'), instancesWrite: stub('instances'), routesWrite: stub('routes'), agentWrite: stub('agent'),
|
||||
processing: { read: stub('processing'), halt: stub('halt'), resume: stub('resume') },
|
||||
reads: Object.fromEntries(['settings', 'business', 'busStatus', 'queue', 'row'].map(v => [v, stub(v)])),
|
||||
chat: Object.fromEntries(['projects', 'threads', 'messages', 'send', 'open', 'createProject', 'createThread'].map(v => [v, stub(v)])),
|
||||
};
|
||||
const web = await startServer({ port: 0, board: 'http://127.0.0.1:9', sso, log, ...owner, userOptions, federation: FEDERATION });
|
||||
t.after(() => close(web));
|
||||
const p1 = broker.request(human, { verb: 'project.create', args: { name: 'shared' } }).id;
|
||||
const p2 = broker.request(human, { verb: 'project.create', args: { name: 'private' } }).id;
|
||||
return { op, iss, broker, human, store, access, counts, opened, lines, port: web.address().port, p1, p2, userTransport, ownerCall, userVerbs, ownerVerbs };
|
||||
}
|
||||
|
||||
// A whole sign-in with `sub`: start, a code at the OP, the callback.
|
||||
async function signIn(env, sub) {
|
||||
const start = await call(env.port, { path: '/auth/login' });
|
||||
assert.equal(start.status, 303);
|
||||
const cookie = setCookies(start)[0].split(';')[0];
|
||||
const location = start.headers.location;
|
||||
const g = env.op.grant(env.iss, location, { sub });
|
||||
const q = new URLSearchParams({ code: g.code, state: new URL(location).searchParams.get('state') });
|
||||
const r = await call(env.port, { path: `/auth/callback?${q}`, headers: { cookie } });
|
||||
return { r, session: sessionOf(r) };
|
||||
}
|
||||
const events = env => env.store.get('SELECT count(*) n FROM events').n;
|
||||
// Every table's row count but events', for a "nothing else written" pin.
|
||||
const tables = env => Object.fromEntries(env.store.all("SELECT name FROM sqlite_master WHERE type='table' AND name NOT IN ('events', 'sqlite_sequence') ORDER BY name")
|
||||
.map(({ name }) => [name, env.store.get(`SELECT count(*) n FROM "${name}"`).n]));
|
||||
|
||||
test('R6 G1 a granted user signs in, gets the user page and reaches the broker as itself', async t => {
|
||||
const env = await setup(t);
|
||||
env.access.doc.grants = [grant({ projects: [env.p1] })];
|
||||
const { r, session } = await signIn(env, 'alice-sub');
|
||||
assert.equal(r.status, 303, JSON.stringify(r.body));
|
||||
assert.ok(session, 'a session cookie was set');
|
||||
const config = await call(env.port, { path: '/api/config', headers: { cookie: session } });
|
||||
assert.equal(config.status, 200);
|
||||
assert.equal(config.body.user, true);
|
||||
assert.equal(config.body.edit, false);
|
||||
assert.deepEqual(config.body.federation, FEDERATION);
|
||||
const page = await call(env.port, { path: '/', headers: { cookie: session } });
|
||||
assert.equal(page.status, 200);
|
||||
assert.match(page.body, /<script src="\/user\.js" defer><\/script>/);
|
||||
assert.doesNotMatch(page.body, /app\.js|bus\.js|agents\.js|processing\.js|editor\.js/);
|
||||
// Only the granted project shows: the broker answered as alice, not as the owner.
|
||||
const projects = await call(env.port, { path: '/api/chat/projects', headers: { cookie: session } });
|
||||
assert.equal(projects.status, 200, JSON.stringify(projects.body));
|
||||
assert.deepEqual(projects.body.rows.map(p => p.id), [env.p1]);
|
||||
const made = await call(env.port, { method: 'POST', path: '/api/chat/threads', headers: writeHeaders(session), body: JSON.stringify({ project: env.p1, name: 'alice work' }) });
|
||||
assert.equal(made.status, 200, JSON.stringify(made.body));
|
||||
const threads = await call(env.port, { path: '/api/chat/threads', headers: { cookie: session } });
|
||||
assert.deepEqual(threads.body.rows.map(x => x.id), [made.body.id]);
|
||||
assert.equal(env.counts.owner, 0, 'an owner object ran for a user session');
|
||||
assert.ok(env.opened.length > 0 && env.opened.every(u => u === 'alice'));
|
||||
assert.ok(env.lines.some(l => / chat action=createThread user="alice" id=/.test(l)));
|
||||
// The owner signs in as before and gets the owner page and config.
|
||||
const ownerSession = (await signIn(env, 'owner-sub')).session;
|
||||
const ownerConfig = await call(env.port, { path: '/api/config', headers: { cookie: ownerSession } });
|
||||
assert.equal(Object.hasOwn(ownerConfig.body, 'user'), false);
|
||||
assert.deepEqual(ownerConfig.body.federation, FEDERATION);
|
||||
assert.match((await call(env.port, { path: '/', headers: { cookie: ownerSession } })).body, /<script src="\/app\.js" defer><\/script>/);
|
||||
});
|
||||
|
||||
test('R6 G2 a subject with no live grant in this business is refused subject-unknown', async t => {
|
||||
const past = new Date(Date.now() - 60000).toISOString();
|
||||
for (const [why, users, grants, sub] of [
|
||||
['a listed user with no grant', [ALICE], [], 'alice-sub'],
|
||||
['a grant in another business', [ALICE], [grant({ business: 'other' })], 'alice-sub'],
|
||||
['an expired grant', [ALICE], [grant({ expires: past })], 'alice-sub'],
|
||||
["another user's grant", [ALICE, { id: 'bob', home: 'local', subjects: [{ provider: 'a', sub: 'bob-sub' }] }], [grant({ user: 'bob' })], 'alice-sub'],
|
||||
['a sub in no file', [ALICE], [grant()], 'stranger-sub'],
|
||||
]) {
|
||||
await t.test(why, async t => {
|
||||
const env = await setup(t, { users, grants });
|
||||
const { r, session } = await signIn(env, sub);
|
||||
assert.equal(r.status, 403, why);
|
||||
assert.equal(session, null, `${why}: a session cookie was set`);
|
||||
assert.match(env.lines.at(-1), / sso refused code=subject-unknown provider=a /);
|
||||
assert.deepEqual(env.opened, []);
|
||||
assert.equal(env.counts.user + env.counts.owner, 0);
|
||||
});
|
||||
}
|
||||
await t.test('a file the loader refuses', async t => {
|
||||
const env = await setup(t);
|
||||
env.access.fail = true;
|
||||
const { r, session } = await signIn(env, 'alice-sub');
|
||||
assert.equal(r.status, 403);
|
||||
assert.equal(session, null);
|
||||
});
|
||||
});
|
||||
|
||||
test('R6 G3 a revoke counts on the next request: 401 and the session is gone', async t => {
|
||||
for (const [why, revoke] of [
|
||||
['grant removed', env => { env.access.doc.grants = []; }],
|
||||
['subject removed', env => { env.access.doc.users = [{ ...ALICE, subjects: [] }]; }],
|
||||
['grant expired', env => { env.access.doc.grants = [grant({ expires: new Date(Date.now() - 1000).toISOString() })]; }],
|
||||
['file now refused', env => { env.access.fail = true; }],
|
||||
]) {
|
||||
await t.test(why, async t => {
|
||||
const env = await setup(t);
|
||||
const { session } = await signIn(env, 'alice-sub');
|
||||
assert.equal((await call(env.port, { path: '/api/chat/projects', headers: { cookie: session } })).status, 200);
|
||||
revoke(env);
|
||||
const before = env.counts.user;
|
||||
const r = await call(env.port, { path: '/api/chat/projects', headers: { cookie: session } });
|
||||
assert.equal(r.status, 401, `${why}: ${JSON.stringify(r.body)}`);
|
||||
assert.equal(r.body.error, 'sign-in required');
|
||||
assert.match(env.lines.at(-1), / sso refused code=user-revoked provider=a user="alice"$/);
|
||||
assert.equal(env.counts.user, before, 'the transport ran after the revoke');
|
||||
// Restoring the grant does not bring the session back: it was deleted.
|
||||
env.access.fail = false; env.access.doc.users = [ALICE]; env.access.doc.grants = [grant()];
|
||||
assert.equal((await call(env.port, { path: '/api/chat/projects', headers: { cookie: session } })).status, 401);
|
||||
const page = await call(env.port, { path: '/', headers: { cookie: session } });
|
||||
assert.equal(page.status, 303);
|
||||
assert.match(page.headers.location, /^\/auth\/login\?return=/);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Every owner route a user session might try; none is a chat route.
|
||||
const OWNER_ROUTES = [
|
||||
['GET', '/api/bus/inbox'], ['GET', '/api/bus/tasks'], ['GET', '/api/bus/names'], ['GET', '/api/bus/thread.claims'],
|
||||
['POST', '/api/bus/message', { to: 'coder', body: 'hi' }],
|
||||
['GET', '/api/settings'], ['POST', '/api/settings', { revision: 'x' }],
|
||||
['POST', '/api/name', { role: 'assistant', name: 'x' }], ['POST', '/api/assistant/stop', {}],
|
||||
['POST', '/api/instances', { instance: 'x' }], ['POST', '/api/routes', { routes: [] }],
|
||||
['POST', '/api/agent/launch', { instance: 'x' }], ['GET', '/api/agent/request?id=x'],
|
||||
['GET', '/api/processing'], ['POST', '/api/processing/halt', {}],
|
||||
['GET', '/api/project-files?project=p'], ['GET', '/api/project-file?project=p&path=a'], ['POST', '/api/project-file', { project: 'p' }],
|
||||
['GET', '/api/board'], ['GET', '/api/conversations'], ['POST', '/api/seen', {}], ['POST', '/api/reply', {}],
|
||||
['GET', '/api/queue'], ['GET', '/api/queue/1'], ['GET', '/api/business'], ['GET', '/api/bus-status'], ['GET', '/api/task-fields'],
|
||||
['GET', '/app.js'], ['GET', '/bus.js'], ['GET', '/agents.js'], ['GET', '/editor.js'], ['GET', '/shared/app.css'], ['GET', '/index.html'],
|
||||
];
|
||||
|
||||
test('R6 G4 every owner route answers 403 owner-only before any bus call', async t => {
|
||||
const env = await setup(t);
|
||||
const { session } = await signIn(env, 'alice-sub');
|
||||
const before = events(env);
|
||||
for (const [method, path, value] of OWNER_ROUTES) {
|
||||
const r = await call(env.port, { method, path, headers: value === undefined ? { cookie: session } : writeHeaders(session), body: value === undefined ? undefined : JSON.stringify(value) });
|
||||
assert.equal(r.status, 403, `${method} ${path}: ${r.status} ${JSON.stringify(r.body)}`);
|
||||
assert.equal(r.body.error, 'owner-only', `${method} ${path}`);
|
||||
assert.equal(userRoute(new URL(path, 'http://x').pathname, method), false, `${method} ${path}`);
|
||||
}
|
||||
assert.deepEqual(env.counts, { owner: 0, user: 0 });
|
||||
assert.deepEqual(env.opened, [], 'a user transport was built for an owner route');
|
||||
assert.equal(events(env), before, 'something was written');
|
||||
});
|
||||
|
||||
test('R6 G5 the bus refuses every owner verb over the user transport on its own', async t => {
|
||||
const env = await setup(t);
|
||||
const o = consoleBus(env.userTransport('alice'), { send: true, owner: env.ownerCall });
|
||||
const before = env.store.get('SELECT coalesce(max(seq), 0) n FROM events').n, rest = tables(env);
|
||||
const refusals = [
|
||||
['inbox', () => o.bus.inbox()], ['tasks', () => o.bus.tasks()], ['agents', () => o.bus.agents()], ['trail', () => o.bus.trail()],
|
||||
['decisions', () => o.bus.decisions()], ['instances', () => o.bus.instances()], ['routes', () => o.bus.routes()], ['thread.claims', () => o.bus['thread.claims']()],
|
||||
['message.send', () => o.send({ to: 'chief', body: 'as the owner' })], ['limits.set', () => o.settingsWrite({ revision: 'x' })],
|
||||
['project.files', () => o.projectFiles({ project: env.p1 })], ['project.file', () => o.projectFile({ project: env.p1, path: 'a' })],
|
||||
['project.file.save', () => o.projectSave({ project: env.p1 })], ['name.set', () => o.nameWrite({ role: 'assistant', name: 'x' })],
|
||||
['assistant.stop', () => o.assistantStop({})], ['instances.set', () => o.instancesWrite({})], ['routes.set', () => o.routesWrite({})],
|
||||
['instance.launch', () => o.agentWrite('instance.launch', {})], ['processing.status', () => o.processing.read()],
|
||||
['processing.halt', () => o.processing.halt({})], ['library.status', () => o.libraryStatus({})],
|
||||
['project.create', () => o.chat.createProject({ name: 'mine' })],
|
||||
];
|
||||
for (const [why, run] of refusals) await assert.rejects(Promise.resolve().then(run), e => e.code === 'access-refused', why);
|
||||
// The broker records each refusal (row 37): one action.refused per verb, its body the code
|
||||
// alone (#refused names no verb). No other table gains or loses a row.
|
||||
const added = env.store.all('SELECT kind, body FROM events WHERE seq > ? ORDER BY seq', before);
|
||||
assert.equal(added.length, refusals.length, 'one event per refused verb');
|
||||
for (const e of added) {
|
||||
assert.equal(e.kind, 'action.refused');
|
||||
assert.deepEqual(JSON.parse(e.body), { code: 'access-refused' });
|
||||
}
|
||||
assert.deepEqual(tables(env), rest, 'a refused verb wrote');
|
||||
});
|
||||
|
||||
// sage-3153 (C6): in a user's chat the owner transport carries `plugins` with {} and nothing else,
|
||||
// every chat verb goes over the user transport, and the owner's answer never reaches the user.
|
||||
test('R6 G9 a user chat sends only the plugins read over the owner transport', async t => {
|
||||
const env = await setup(t);
|
||||
const { session } = await signIn(env, 'alice-sub');
|
||||
const bodies = [];
|
||||
const go = async (method, path, value) => {
|
||||
const r = await call(env.port, { method, path, headers: value === undefined ? { cookie: session } : writeHeaders(session), body: value === undefined ? undefined : JSON.stringify(value) });
|
||||
assert.equal(r.status, 200, `${method} ${path}: ${JSON.stringify(r.body)}`);
|
||||
bodies.push(JSON.stringify(r.body));
|
||||
return r.body;
|
||||
};
|
||||
await go('GET', '/api/chat/projects');
|
||||
const thread = (await go('POST', '/api/chat/threads', { project: env.p1, name: 'alice work' })).id;
|
||||
await go('GET', `/api/chat/threads?project=${env.p1}`);
|
||||
await go('POST', `/api/chat/threads/${thread}/open`, {});
|
||||
await go('POST', `/api/chat/threads/${thread}/messages`, { body: 'hello' });
|
||||
await go('GET', `/api/chat/threads/${thread}/messages`);
|
||||
assert.ok(env.ownerVerbs.length > 0, 'the chat never read plugins');
|
||||
// Exactly the verb `plugins` and exactly {}: no value from the session or a request reaches it.
|
||||
for (const entry of env.ownerVerbs) assert.deepEqual(entry, ['plugins', {}]);
|
||||
const sent = JSON.stringify(env.ownerVerbs);
|
||||
for (const value of ['alice', 'alice-sub', 'alice work', 'hello', env.p1, thread]) assert.equal(sent.includes(value), false, `${value} reached the owner transport`);
|
||||
for (const verb of ['project.list', 'thread.create', 'thread.list', 'thread.open', 'thread.send', 'thread.messages']) assert.ok(env.userVerbs.includes(verb), `${verb} did not go over the user transport`);
|
||||
assert.equal(env.userVerbs.includes('plugins'), false);
|
||||
for (const body of bodies) assert.doesNotMatch(body, new RegExp(OWNER_MARKER));
|
||||
assert.equal(env.counts.owner, 0);
|
||||
});
|
||||
|
||||
test('R6 G6 the user page holds the notice word for word', async t => {
|
||||
const html = readFileSync(new URL('user.html', PUBLIC), 'utf8');
|
||||
const notice = /<p id="user-notice"[^>]*>([^<]*)<\/p>/.exec(html);
|
||||
assert.ok(notice, 'no #user-notice paragraph');
|
||||
assert.equal(notice[1], NOTICE);
|
||||
const env = await setup(t);
|
||||
const { session } = await signIn(env, 'alice-sub');
|
||||
assert.ok((await call(env.port, { path: '/', headers: { cookie: session } })).body.includes(NOTICE));
|
||||
});
|
||||
|
||||
// The user page outside a session: a small static server over the public files, /api/config stubbed.
|
||||
async function userPage(t, config) {
|
||||
const types = { html: 'text/html', js: 'text/javascript', css: 'text/css', svg: 'image/svg+xml' };
|
||||
const server = createHttpServer((req, res) => {
|
||||
const path = new URL(req.url, 'http://x').pathname;
|
||||
if (path === '/api/config') { res.writeHead(200, { 'content-type': 'application/json' }); return res.end(JSON.stringify(config)); }
|
||||
if (path.startsWith('/api/chat/')) { res.writeHead(200, { 'content-type': 'application/json' }); return res.end(JSON.stringify({ rows: [], at: new Date().toISOString() })); }
|
||||
const name = path === '/' ? 'user.html' : path.slice(1);
|
||||
if (!/^[a-z.]+$/.test(name)) { res.writeHead(404); return res.end(); }
|
||||
try { const body = readFileSync(new URL(name, PUBLIC)); res.writeHead(200, { 'content-type': types[name.split('.').pop()] ?? 'text/plain' }); res.end(body); }
|
||||
catch { res.writeHead(404); res.end(); }
|
||||
});
|
||||
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
||||
t.after(() => close(server));
|
||||
return server.address().port;
|
||||
}
|
||||
|
||||
test('R6 G7 the hub label and plain peer links render from config on both pages', async t => {
|
||||
const links = 'JSON.stringify({label:document.querySelector("#hub-label")?.textContent,hidden:document.querySelector("#federation").hidden,peers:[...document.querySelectorAll("#federation a")].map(a=>[a.getAttribute("href"),a.textContent,a.attributes.length])})';
|
||||
const expected = { label: 'This Console: hub', hidden: false, peers: [['https://factory.example.test', 'Software factory', 2]] };
|
||||
const wait = (b, expression) => b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(${expression})return;await new Promise(r=>setTimeout(r,30));}throw new Error(${JSON.stringify(expression)});})()`);
|
||||
const b = await browser(); t.after(() => b.close());
|
||||
// The owner page, over the real server.
|
||||
const f = await fixture(); t.after(() => f.close());
|
||||
const owner = await startServer({ port: 0, board: f.boardURL, federation: FEDERATION }); t.after(() => close(owner));
|
||||
await b.navigate(`http://127.0.0.1:${owner.address().port}/`);
|
||||
await wait(b, '!document.querySelector("#federation").hidden');
|
||||
assert.deepEqual(JSON.parse(await b.evaluate(links)), expected);
|
||||
// The user page, with a hostile label kept as text.
|
||||
const hostile = { self: 'hub', peers: [...FEDERATION.peers, { id: 'x', origin: 'https://x.example.test', label: '<img src=x onerror="window.injected=1">' }, { id: 'y', origin: 'javascript:alert(1)', label: 'bad' }] };
|
||||
await b.navigate(`http://127.0.0.1:${await userPage(t, { user: true, send: true, federation: hostile })}/`);
|
||||
await wait(b, '!document.querySelector("#federation").hidden');
|
||||
const user = JSON.parse(await b.evaluate(links));
|
||||
assert.equal(user.label, 'This Console: hub');
|
||||
assert.deepEqual(user.peers, [...expected.peers, ['https://x.example.test', '<img src=x onerror="window.injected=1">', 2]]);
|
||||
assert.equal(await b.evaluate('window.injected'), undefined);
|
||||
assert.equal(await b.evaluate('document.querySelector("#user-notice").textContent'), NOTICE);
|
||||
// No federation in config: nothing is shown.
|
||||
await b.navigate(`http://127.0.0.1:${await userPage(t, { user: true, send: true })}/`);
|
||||
await wait(b, 'document.querySelector(".chat-area")');
|
||||
assert.equal(await b.evaluate('document.querySelector("#federation").hidden'), true);
|
||||
});
|
||||
|
||||
test('R6 G8 userCall spawns user-cli directly with the user nonce, no human nonce, and the user request', async t => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'webui-user-call-'));
|
||||
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
const socket = join(dir, 'bus.sock');
|
||||
const seen = [];
|
||||
const server = createNetServer(conn => {
|
||||
let input = '';
|
||||
conn.on('data', chunk => {
|
||||
input += chunk;
|
||||
if (!input.includes('\n')) return;
|
||||
const message = JSON.parse(input);
|
||||
const pid = message.user?.pid;
|
||||
// Read the child while it waits on this answer.
|
||||
const proc = name => readFileSync(`/proc/${pid}/${name}`, 'utf8');
|
||||
seen.push({ message, argv: proc('cmdline').split('\0'), env: proc('environ').split('\0').map(e => e.split('=')[0]), ppid: Number(proc('stat').replace(/^.*\) /s, '').split(' ')[1]) });
|
||||
conn.end(JSON.stringify({ ok: true, result: { answered: true } }) + '\n');
|
||||
});
|
||||
});
|
||||
await new Promise(r => server.listen(socket, r));
|
||||
t.after(() => server.close());
|
||||
const result = await userCall({ socket, business: 'demo', user: 'alice', env: { ...process.env, MOSAIC_BUS_CLI_NONCE: 'f'.repeat(64) } })('thread.list', { project: 'p' });
|
||||
assert.deepEqual(result, { answered: true });
|
||||
assert.equal(seen.length, 1);
|
||||
const [{ message, argv, env, ppid }] = seen;
|
||||
assert.equal(argv[1], USER_CLI);
|
||||
assert.notEqual(argv[1], HUMAN_CLI);
|
||||
assert.equal(argv[2], socket);
|
||||
assert.equal(ppid, process.pid, 'user-cli re-executed: the Console is not its parent');
|
||||
assert.ok(env.includes('MOSAIC_BUS_USER_NONCE'));
|
||||
assert.equal(env.includes('MOSAIC_BUS_CLI_NONCE'), false);
|
||||
assert.deepEqual(Object.keys(message).sort(), ['args', 'user', 'verb']);
|
||||
assert.equal(message.verb, 'thread.list');
|
||||
assert.deepEqual(message.args, { project: 'p' });
|
||||
assert.equal(message.user.business, 'demo');
|
||||
assert.equal(message.user.user, 'alice');
|
||||
assert.match(message.user.nonce, /^[0-9a-f]{64}$/);
|
||||
assert.equal(USER_CLI, fileURLToPath(new URL('../../bus/src/user-cli.mjs', import.meta.url)));
|
||||
});
|
||||
Reference in new issue
Block a user