Built onbbd22f1b, rebased on row 60 (3d298920): the bus.mjs header comment conflict merged, serve.mjs BUS_VERBS and the README credentials paragraphs merged cleanly. 21 of 21 mutants killed, full gate green. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,346 @@
|
|||||||
|
# Row 76 (#1570): Console sends a human message to a role, round 1
|
||||||
|
|
||||||
|
Owner: Dewey. Reviewers: Darkwing and Filbert. Brief:
|
||||||
|
`docs/plans/2026-10-11_dogfood-cutover.md` § "Console sends a message to a
|
||||||
|
role", blob `f5d56554` (rev 552). Jason's Q95 answer is lead decision 95;
|
||||||
|
the reply view question is lead decision 97, option (a). Base `3d298920`
|
||||||
|
(row 60 landed). None of the candidate's paths changed between `3d298920`
|
||||||
|
and `refactor` HEAD (`3d298920`) when the packet was built.
|
||||||
|
|
||||||
|
`build.patch` is `git diff --binary 3d298920 -- packages/webui`, the two
|
||||||
|
new test files included. Apply it with `git apply --index` to a detached
|
||||||
|
worktree of the base; `candidate-manifest.sha256` then checks 9/9 OK.
|
||||||
|
|
||||||
|
## What ships
|
||||||
|
|
||||||
|
`serve --public-host NAME --auth-header HEADER --allow-user ID --send`.
|
||||||
|
`--send` is off by default; without it the Console behaves as after row 74
|
||||||
|
(`preview.test.mjs` and `serve.test.mjs` unchanged and green).
|
||||||
|
|
||||||
|
**Start (`cli.mjs`, `serve.mjs`).** `--send` is the one flag without a
|
||||||
|
value. Without all three preview flags the CLI exits 2 with `refused:
|
||||||
|
--send needs the preview mode (...)`, and `startServer` throws the same
|
||||||
|
for a direct caller, or for a `send` that isn't a function. With `--send`
|
||||||
|
and no system config the start refuses (`--send needs the bus configured
|
||||||
|
(...)`, the reason redacted as before) instead of serving a box that can't
|
||||||
|
send; a read-only Console still serves without the config. The `Preview:`
|
||||||
|
line says reads and human messages to roles, one line per send below.
|
||||||
|
`busReader` is split into `busCaller` (the human transport call, the
|
||||||
|
business resolved per request as before) and `views` over it, so the
|
||||||
|
reads and the send use the same transport.
|
||||||
|
|
||||||
|
**The route.** With `--send`, `POST /api/bus/message` is the one write
|
||||||
|
preview mode lets past its read-only check; any other method on that path
|
||||||
|
is 405, and every other write is still 403 `read-only`. Order, each on
|
||||||
|
every request:
|
||||||
|
|
||||||
|
1. The shared checks from row 74 run first: repeated Host 400, HEADER with
|
||||||
|
a Host other than NAME 403 `proxy Host refused`, a NAME request without
|
||||||
|
HEADER = ID once 401, a sent Origin other than `https://NAME` 403. These
|
||||||
|
write no log line.
|
||||||
|
2. `busSend` checks HEADER = ID once again itself (401), so the route
|
||||||
|
never runs on a loopback Host: a loopback request without HEADER is
|
||||||
|
401, with it 403 `proxy Host refused`. There is no loopback send.
|
||||||
|
3. Origin sent once and exactly `https://NAME` (a missing Origin passes
|
||||||
|
the shared check and stops here), else 403 `cross-origin request
|
||||||
|
refused`.
|
||||||
|
4. `Sec-Fetch-Site` sent once and exactly `same-origin`, else 403
|
||||||
|
`cross-site request refused`.
|
||||||
|
5. `Content-Type: application/json`, at most 16384 bytes, a JSON object
|
||||||
|
with `to` and `body`, optionally `in_reply_to`, no other key; else 400
|
||||||
|
`invalid-request`.
|
||||||
|
6. `message.send` with exactly those keys. The broker judges `to` and the
|
||||||
|
body. Its limit, 32768 characters, is past the request limit, so 16 KiB
|
||||||
|
binds first.
|
||||||
|
|
||||||
|
Every answer after step 2 carries `sent`: `false` for a refusal, `"unknown"`
|
||||||
|
when the message may exist. Success is 200 `{id, at}`.
|
||||||
|
|
||||||
|
**Outcomes (`bus.mjs` `sendOutcome`).** The broker writes in one
|
||||||
|
transaction, so any code it returns means nothing was written:
|
||||||
|
`{error: code, sent: false}` at the read routes' statuses (`busStatus`),
|
||||||
|
with `unknown-role`, `message-not-found` and `request-too-large` added as
|
||||||
|
400. `outcome-unknown`, `invalid-response` and `response-too-large` from
|
||||||
|
the transport, any error that isn't a `BusReadError`, and an answer
|
||||||
|
without a valid message id leave it open: 503 `outcome-unknown` with
|
||||||
|
`sent: "unknown"`. Nothing retries.
|
||||||
|
|
||||||
|
**Log.** One stdout line per request that gets past step 2: the time,
|
||||||
|
`send`, `user=` (JSON-quoted), `to=` (JSON-quoted, cut to 64 characters, or
|
||||||
|
`none`), then `id=<message id>` or `error=<code>`. Never the body. A test
|
||||||
|
shows a `to` with a newline can't forge a second line.
|
||||||
|
|
||||||
|
**`/api/config`** gains `send`: true only with `--send` on a NAME request,
|
||||||
|
so the operator's loopback page never shows a box that would get 401.
|
||||||
|
|
||||||
|
**The page (`public/bus.js`).** With `send` true the Inbox has "Message a
|
||||||
|
role": a picker of held roles from the agents read, a text box and Send.
|
||||||
|
|
||||||
|
- The draft (role and text) lives in page memory, so the ten-second
|
||||||
|
refresh redraws the form without losing it; focus and the caret are
|
||||||
|
kept. A reload loses it. A role no longer held stays selected, marked
|
||||||
|
"(not held now)".
|
||||||
|
- Send is disabled while a send is pending; a second press sends nothing.
|
||||||
|
An empty role or text is refused in the page without a request.
|
||||||
|
- One POST per Send, `redirect: 'manual'`. Sent: an id came back; the
|
||||||
|
receipt shows it and links `#/trail/message/<id>` (a new trail kind: the
|
||||||
|
message and its deliveries, back link to the Inbox, not-found for an id
|
||||||
|
the trail doesn't hold). The text is cleared unless it was edited during
|
||||||
|
the send.
|
||||||
|
- Not sent: a 4xx, or the server's `sent: false` (a 503 `no-bus-host`
|
||||||
|
too), or a redirect. Only the sign-in proxy redirects, when its session
|
||||||
|
has expired, so the request never reached the Console; the receipt asks
|
||||||
|
for a reload. The draft is kept.
|
||||||
|
- Anything else, a lost answer included: outcome unknown. The receipt says
|
||||||
|
the message may have been sent, that nothing was sent again, and that
|
||||||
|
there is no trail to link without an id; the draft is kept.
|
||||||
|
- The box says a reply comes back as a message the Console doesn't show
|
||||||
|
yet, and the next `mosaic talk` prints it after sending its own message
|
||||||
|
(decision 97).
|
||||||
|
|
||||||
|
**Reply in the board (`public/app.js`).** A refused Reply said "Delivery
|
||||||
|
may be unknown; check the seat before sending again" for a 403. A 4xx now
|
||||||
|
says "Not sent."; no answer, an unreadable one or a 5xx keeps the old
|
||||||
|
text. Row 74 left this for this row.
|
||||||
|
|
||||||
|
**README.** A "Message a role" section: the command, what `--send` grants
|
||||||
|
(anyone the proxy lets through as ID, and any local process that reaches
|
||||||
|
the port and sets HEADER with `Host: NAME`; decisions 62 and 95), the
|
||||||
|
request and each check, the broker's refusals and their statuses, the
|
||||||
|
unknown outcome, the log line, the page's three receipts, and the reply
|
||||||
|
path with `mosaic talk`'s limits (decision 97). The routes list gains the
|
||||||
|
message trail; the "nothing writes" lines name the exception. Folded in
|
||||||
|
from row 74 per the row note: Filbert N1 ("its first signed-in request"),
|
||||||
|
Darkwing's tunnel sentence (anything reaching a tunnel's local end can
|
||||||
|
send a loopback Host without HEADER, so bind it to loopback), and a
|
||||||
|
pointer to `scripts/webui-service.sh`, which takes no `--send`.
|
||||||
|
|
||||||
|
## Decisions and tradeoffs
|
||||||
|
|
||||||
|
1. **No loopback send.** The brief asks for the header on every request,
|
||||||
|
loopback included. A loopback request can't carry it (row 74 refuses
|
||||||
|
HEADER with a loopback Host), so the operator's browser on `127.0.0.1`
|
||||||
|
reads but can't send. `/api/config` hides the box there.
|
||||||
|
2. **The route checks the header itself** even though the shared check
|
||||||
|
already did for NAME requests. It costs one line and keeps the route
|
||||||
|
closed if the shared check changes.
|
||||||
|
3. **A missing Origin is refused on the send** (the shared check allows a
|
||||||
|
missing one for reads). The brief asks for the request's own origin,
|
||||||
|
and browsers send Origin on a POST.
|
||||||
|
4. **`Sec-Fetch-Site` is compared exactly**, so `Same-Origin` and a
|
||||||
|
repeated header are refused. Browsers send the lowercase value once.
|
||||||
|
5. **Which errors mean "not sent".** The brief maps broker codes like the
|
||||||
|
reads'. I treat every coded broker answer as definite, since the broker
|
||||||
|
writes in one transaction, and only the transport's three lost-answer
|
||||||
|
codes, an uncoded error or a missing id as unknown. The page adds 4xx
|
||||||
|
and the redirect as definite. A proxy 4xx means the request wasn't
|
||||||
|
accepted for forwarding; a proxy 5xx (502, 504) can come after the
|
||||||
|
Console got it, so it stays unknown.
|
||||||
|
6. **The draft is in page memory, not storage.** The brief asks for it to
|
||||||
|
survive the periodic refresh. Keeping it out of `localStorage` keeps a
|
||||||
|
message body off disk on a shared browser profile.
|
||||||
|
7. **`--send` without the bus refuses to start.** The brief doesn't say.
|
||||||
|
Serving a box that can only answer `not-configured` seemed worse than
|
||||||
|
a refusal at start.
|
||||||
|
|
||||||
|
## Where the brief and the code differ (for Sage)
|
||||||
|
|
||||||
|
- "Replies show in the inbox as today": they don't; the inbox shows
|
||||||
|
decisions. Decision 97 settled it: the README and the box give `mosaic
|
||||||
|
talk`'s limits, and the reply view is row 81 (#1575).
|
||||||
|
- "Points at the trail" on an unknown outcome: there's no id to link, so
|
||||||
|
the receipt points at the role's side and the bus trail from a terminal.
|
||||||
|
- The log's `error=` value is the code as answered, unquoted; for the 403s
|
||||||
|
it has spaces (`error=cross-origin request refused`). It is always last
|
||||||
|
on the line.
|
||||||
|
|
||||||
|
## Rebase on row 60
|
||||||
|
|
||||||
|
I built this row on `bbd22f1b`. Row 60 landed first (`1771bf98`), so I
|
||||||
|
applied my patch to `3d298920` with `git apply --3way`. Row 60's hunks in
|
||||||
|
this row's files, and how each merged:
|
||||||
|
|
||||||
|
- `packages/webui/src/serve.mjs`: row 60's `BUS_VERBS` line (adds
|
||||||
|
`credentials`). It merged cleanly; my changes there are elsewhere in the
|
||||||
|
file.
|
||||||
|
- `packages/webui/README.md`: row 60's credentials read and its line in
|
||||||
|
the exposure list. Merged cleanly, in different paragraphs from my
|
||||||
|
"Message a role" section and the "nothing writes" lines.
|
||||||
|
- `packages/webui/src/bus.mjs`: the header comment, the one conflict. Row
|
||||||
|
60 named its `credentials` read and I named the one write. The merged
|
||||||
|
comment says both:
|
||||||
|
|
||||||
|
```
|
||||||
|
// Only the four verbs of the Q1 module (lead decision 56), row 57's
|
||||||
|
// `decisions` and row 60's `credentials` reads are reachable as reads: no
|
||||||
|
// read writes, not even seen (Q4). The one write is row 76's human
|
||||||
|
// `message.send` (lead decision 95), built by the CLI only with --send. The
|
||||||
|
// bus does the proof; a server started inside an agent run gets
|
||||||
|
// `human-required` from the broker.
|
||||||
|
```
|
||||||
|
|
||||||
|
Row 60's `tests/bus.test.mjs` change isn't in a file this row touches. The
|
||||||
|
mutants, the suites and the demonstration below ran on the rebased tree.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `packages/webui/src/serve.mjs` | `busSend`, the route under `send`, `body()` limit parameter, `send`/`log` options, `/api/config` `send` |
|
||||||
|
| `packages/webui/src/bus.mjs` | `busCaller` split out of `busReader`; `sendOutcome`; three codes added to the status map |
|
||||||
|
| `packages/webui/src/cli.mjs` | `--send`, refused without the preview flags or the bus; the `Preview:` line |
|
||||||
|
| `packages/webui/src/public/bus.js` | the "Message a role" box, the message trail kind, focus and caret kept across a refresh |
|
||||||
|
| `packages/webui/src/public/app.js` | the Reply 4xx text |
|
||||||
|
| `packages/webui/README.md` | the "Message a role" section and the folded-in row 74 notes |
|
||||||
|
| `packages/webui/tests/send.test.mjs` | new, 13 tests |
|
||||||
|
| `packages/webui/tests/send-browser.test.mjs` | new, 1 browser test |
|
||||||
|
| `packages/webui/tests/shell.test.mjs` | the pinned write boundary: bus.js has exactly one write, this one |
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
`send.test.mjs` (13): `startServer` refuses `send` without the preview
|
||||||
|
mode or as a non-function; the CLI refuses `--send` without the flags and
|
||||||
|
without a configured bus (exit 2, nothing on stdout, the config path not
|
||||||
|
named); the CLI with `--send` and a configured bus but no bus host running
|
||||||
|
prints the `Preview:` line, answers a send 503 `no-bus-host` with `sent:
|
||||||
|
false` through the real human transport, and logs one line without the
|
||||||
|
body; without `--send` the route is 403 `read-only` and
|
||||||
|
config says no send; with `--send` every other write (POST, PUT, DELETE,
|
||||||
|
PATCH on the board, bus and page routes) is still 403 `read-only` and
|
||||||
|
nothing reaches the board or bus; config says send only on NAME; 401 for no
|
||||||
|
header, another user, a suffix, two values, loopback without the header
|
||||||
|
and `localhost` with no Origin, and 403 `proxy Host refused` for loopback
|
||||||
|
with it, none sent or logged; each request check (no Origin, no or wrong
|
||||||
|
or repeated `Sec-Fetch-Site`, no or wrong Content-Type, a form, 16385
|
||||||
|
bytes, extra keys, `business`, missing `to` or `body`, not JSON, null, an
|
||||||
|
array, a string) refused with `sent: false`, logged without the body, and
|
||||||
|
16384 bytes accepted; wrong Origins refused by the shared check; GET and
|
||||||
|
PUT 405; exactly `to`, `body` and `in_reply_to` passed on; a crafted `to`
|
||||||
|
can't forge a log line; each bus refusal with its status and `sent:
|
||||||
|
false`, each open outcome 503 `unknown`, a missing id unknown, and one
|
||||||
|
call each (no retry); the human transport request is exactly the business,
|
||||||
|
`message.send` and the keys; against a real broker the message is written
|
||||||
|
once as a human send (`from_role` human, `from_run` human-cli, class INFO)
|
||||||
|
and `unknown-role`, `invalid-request` and `message-not-found` map to 400.
|
||||||
|
|
||||||
|
`send-browser.test.mjs` drives the page in Chrome with an in-process
|
||||||
|
broker for the roles and the trail and a stub for the POST (a send passes
|
||||||
|
only with the proxy's https Origin, which a loopback page can't have): no
|
||||||
|
box without `send`; the held roles; empty role and text refused without a
|
||||||
|
request; the draft across a refresh; a 400 and a 503 `sent: false`
|
||||||
|
refused with the draft kept; a redirect refused as `sign-in`; an unknown
|
||||||
|
outcome, a network failure and a 200 without an id each unknown, with no
|
||||||
|
trail link and no resend; one POST per Send, pinned as
|
||||||
|
`{method, headers, body, redirect: 'manual'}`; Send disabled while
|
||||||
|
pending and a second press ignored; text typed during a send kept; a plain
|
||||||
|
success clears the text and keeps the role; the receipt's trail link opens
|
||||||
|
the message's trail; an unknown message id is not found.
|
||||||
|
|
||||||
|
## Mutants
|
||||||
|
|
||||||
|
`mutants.py` makes 21 single-site edits, one at a time, in a separate
|
||||||
|
worktree of the rebased candidate (never the build tree). Each mutant runs
|
||||||
|
against the whole webui suite, and the file is restored from the copy read
|
||||||
|
before the edit. The baseline passed, the worktree was unchanged afterwards,
|
||||||
|
and **21 of 21 were killed** (2026-10-12T00:40:39Z to 01:02:24Z).
|
||||||
|
`mutants.txt` lists each mutant with the tests that killed it.
|
||||||
|
|
||||||
|
- Route (`serve.mjs`): the route's own header check removed; Origin
|
||||||
|
unchecked; `Sec-Fetch-Site` unchecked, or `same-site` let through; the
|
||||||
|
limit raised to 64 KiB; extra keys allowed; `in_reply_to` dropped; the
|
||||||
|
id check removed; the log's `to` unquoted and uncut; `/api/config`
|
||||||
|
`send` true on loopback; the route open without `--send`; every write
|
||||||
|
opened by `--send` (`send-opens-writes`, killed by the new "every other
|
||||||
|
write is still 403" test).
|
||||||
|
- Outcomes (`bus.mjs`): only `outcome-unknown` left open; `unknown-role`
|
||||||
|
dropped from the status map.
|
||||||
|
- Page (`bus.js`): no pending guard; the text cleared though edited during
|
||||||
|
the send; a 5xx `sent: false` treated as unknown; an unknown outcome
|
||||||
|
resent; a redirect treated as unknown; redirects followed; the message
|
||||||
|
trail shown for any id.
|
||||||
|
|
||||||
|
I ran 20 of these on the `bbd22f1b` build before the rebase. The 18 that
|
||||||
|
finished there were killed too; the run was stopped for the rebase and its
|
||||||
|
log is not in the packet.
|
||||||
|
|
||||||
|
## Demonstration
|
||||||
|
|
||||||
|
`demo.mjs` runs the candidate's `startServer` with `--send`'s options
|
||||||
|
against a real in-process broker (pm, cto and coder; coder claimed),
|
||||||
|
driven over `node:http` on loopback with the headers the proxy and the
|
||||||
|
browser send (`Host: NAME`, `x-authentik-uid: jason`, Origin,
|
||||||
|
`Sec-Fetch-Site`). Three servers: `send`, `lost` (the broker writes, then
|
||||||
|
the call throws `outcome-unknown`, as when the transport loses the
|
||||||
|
answer) and `readonly` (no `--send`). `demo.txt` is its output on the gate
|
||||||
|
worktree:
|
||||||
|
|
||||||
|
```text
|
||||||
|
== a send, and a reply to the coder
|
||||||
|
to coder 200 {"id":"8f66630d-cb38-4879-8119-fb3defd532c8","at":"2026-10-12T00:40:48.392Z"}
|
||||||
|
to coder, in_reply_to the coder's message 200 {"id":"3a35d7b4-2020-464e-9bc3-5ee870c8b53f","at":"2026-10-12T00:40:49.041Z"}
|
||||||
|
== refused before the bus
|
||||||
|
no sign-in header (401, no log line) 401 {"error":"sign-in required"}
|
||||||
|
loopback Host with the header 403 {"error":"proxy Host refused","message":"the proxy must forward Host unchanged"}
|
||||||
|
another Origin (shared check, no log line) 403 {"error":"cross-origin request refused"}
|
||||||
|
no Origin 403 {"error":"cross-origin request refused","message":"a send needs the Console's own Origin","sent":false}
|
||||||
|
Sec-Fetch-Site: same-site 403 {"error":"cross-site request refused","message":"a send needs Sec-Fetch-Site: same-origin","sent":false}
|
||||||
|
Content-Type: text/plain 400 {"error":"invalid-request","message":"Content-Type must be application/json","sent":false}
|
||||||
|
16385 bytes 400 {"error":"invalid-request","message":"body larger than 16384 bytes","sent":false}
|
||||||
|
an extra key 400 {"error":"invalid-request","message":"a send takes to, body and an optional in_reply_to, nothing else","sent":false}
|
||||||
|
GET 405 {"error":"method not allowed"}
|
||||||
|
== refused by the broker
|
||||||
|
a role the business lacks 400 {"error":"unknown-role","message":"not sent: the bus refused it (unknown-role)","sent":false}
|
||||||
|
an empty body 400 {"error":"invalid-request","message":"not sent: the bus refused it (invalid-request)","sent":false}
|
||||||
|
in_reply_to a missing message 400 {"error":"message-not-found","message":"not sent: the bus refused it (message-not-found)","sent":false}
|
||||||
|
== the answer lost after the broker wrote
|
||||||
|
unknown, not retried 503 {"error":"outcome-unknown","message":"the message may have been sent; nothing was retried. Check the trail before sending again","sent":"unknown"}
|
||||||
|
== without --send
|
||||||
|
POST is read-only 403 {"error":"read-only","message":"the Console preview is read-only"}
|
||||||
|
== what the broker holds
|
||||||
|
8f66630d from=human/human-cli to=coder class=INFO in_reply_to=- body="Please look at row 76 before the cutover."
|
||||||
|
3a35d7b4 from=human/human-cli to=coder class=INFO in_reply_to=0536126f-c54a-40a5-999d-c7fe1c01d9a2 body="refactor"
|
||||||
|
pm receives 1 message(s) from the lost-answer send: ["Lost answer test"]
|
||||||
|
== the Console log, one line per send past sign-in
|
||||||
|
2026-10-12T00:40:48.392Z send user="jason" to="coder" id=8f66630d-cb38-4879-8119-fb3defd532c8
|
||||||
|
2026-10-12T00:40:49.041Z send user="jason" to="coder" id=3a35d7b4-2020-464e-9bc3-5ee870c8b53f
|
||||||
|
2026-10-12T00:40:49.057Z send user="jason" to=none error=cross-origin request refused
|
||||||
|
2026-10-12T00:40:49.058Z send user="jason" to=none error=cross-site request refused
|
||||||
|
2026-10-12T00:40:49.061Z send user="jason" to=none error=invalid-request
|
||||||
|
2026-10-12T00:40:49.066Z send user="jason" to=none error=invalid-request
|
||||||
|
2026-10-12T00:40:49.072Z send user="jason" to=none error=invalid-request
|
||||||
|
2026-10-12T00:40:49.281Z send user="jason" to="auditor" error=unknown-role
|
||||||
|
2026-10-12T00:40:49.535Z send user="jason" to="coder" error=invalid-request
|
||||||
|
2026-10-12T00:40:49.802Z send user="jason" to="coder" error=message-not-found
|
||||||
|
2026-10-12T00:40:50.173Z send user="jason" to="pm" error=outcome-unknown
|
||||||
|
a body in the log: false
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gate (my run on the candidate)
|
||||||
|
|
||||||
|
Detached worktree of `3d298920`, `build.patch` applied with `git apply
|
||||||
|
--index`, the manifest 9/9 OK before and after. Suites one after another,
|
||||||
|
`TMPDIR` in my scratch, Docker on, 2026-10-12T01:02:24Z to 01:09:40Z:
|
||||||
|
|
||||||
|
| Suite | Result |
|
||||||
|
|---|---|
|
||||||
|
| `packages/webui` | 76 pass, 0 fail, 0 skipped, the browser tests included (14 new) |
|
||||||
|
| `packages/bus` | 88 pass, 0 fail |
|
||||||
|
| `packages/cli` | 114 pass, 0 fail |
|
||||||
|
| `packages/control-board` | 124 pass, 0 fail |
|
||||||
|
| business, conversation, discord, harness, ledger, mosaic, queue, runs, seat, tasks | all 0 fail (60, 182, 178, 59, 78, 69, 150, 41, 27, 71 pass) |
|
||||||
|
| build-tokens `--check` | exit 0 |
|
||||||
|
| test-auth, test-conductor, test-config, test-discord, test-extension-package, test-foundation | exit 0 (15, 17, 24, 66, 18, 44 passed) |
|
||||||
|
| test-queue | 27 passed, 0 failed; its `verify`/`render --check` case skips outside the canonical checkout |
|
||||||
|
| test-release | 14 passed, 0 failed (Docker, nothing skipped) |
|
||||||
|
| test-task | 98 passed, 0 failed (Docker, nothing skipped) |
|
||||||
|
|
||||||
|
`git status` afterwards shows only the patch's 9 staged files and the
|
||||||
|
`node_modules` link. `core.hooksPath` stayed unset.
|
||||||
|
|
||||||
|
## Questions for reviewers
|
||||||
|
|
||||||
|
1. Decision 5: is a proxy 4xx safe to show as "not sent"? I think so for
|
||||||
|
the proxies we'd use, but a reviewer who knows one that answers 4xx
|
||||||
|
after forwarding should say.
|
||||||
|
2. Decision 7: refusing `--send` at start without the bus config, rather
|
||||||
|
than serving reads and answering `not-configured` on each send.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
|||||||
|
c6cf1714407803d21a5757f39333a396c76abf582da0df51aeee4294e00cff06 packages/webui/README.md
|
||||||
|
a0661abb7de79ce2854884c99f7d8db114d8028d99048e7387a9b2f8534cd422 packages/webui/src/bus.mjs
|
||||||
|
c8f9845c66793b0516d5f356f6be9f8ae2d46c40fdd044945aac2a606d024bee packages/webui/src/cli.mjs
|
||||||
|
a259dd523625a83237ebc98d0361ff6de1732729a661f9e0f37492f99e3b3f60 packages/webui/src/serve.mjs
|
||||||
|
6ec0ff037d91ba52cce3361b079131d255df8484bcc6e924956731d2b2f86de1 packages/webui/src/public/app.js
|
||||||
|
643178391c4e5dc2ac05a9c1d2972dd374bf6f692a5f28717ec0289d74a64a91 packages/webui/src/public/bus.js
|
||||||
|
7dfceeaf485bc9b636fb12c7bc5a0f12ca74d6b43c351feaf2d335f527f31ee9 packages/webui/tests/send.test.mjs
|
||||||
|
fdf0d30c097ab425fe8426f940892b7422a00ab5d6b7d30cefa0c4c5feafaa35 packages/webui/tests/send-browser.test.mjs
|
||||||
|
17cb39097414bd685d68305dd5e402fd645020695f3dee86cd6bb567dd410eff packages/webui/tests/shell.test.mjs
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// Row 76 (#1570): `serve --send` end to end against a real broker, driven over HTTP on loopback
|
||||||
|
// with the headers the sign-in proxy and the browser would send.
|
||||||
|
// Run: TMPDIR=<scratch> node demo.mjs <worktree with the candidate>.
|
||||||
|
// The broker is in this process, as in send-browser.test.mjs; the human transport between the
|
||||||
|
// Console and a bus host is send.test.mjs's. `send` maps the broker's refusal the way humanCall does.
|
||||||
|
import { request } from 'node:http';
|
||||||
|
import { mkdtempSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join, resolve } from 'node:path';
|
||||||
|
import { pathToFileURL } from 'node:url';
|
||||||
|
|
||||||
|
const tree = resolve(process.argv[2]);
|
||||||
|
const load = p => import(pathToFileURL(join(tree, p)).href);
|
||||||
|
const { Store } = await load('packages/bus/src/store.mjs');
|
||||||
|
const { Broker } = await load('packages/bus/src/broker.mjs');
|
||||||
|
const { views } = await load('packages/bus/src/views.mjs');
|
||||||
|
const { startServer } = await load('packages/webui/src/serve.mjs');
|
||||||
|
const { BusReadError } = await load('packages/webui/src/bus.mjs');
|
||||||
|
|
||||||
|
const NAME = 'mosaic-preview.example.com', HEADER = 'x-authentik-uid', ID = 'jason';
|
||||||
|
const preview = { host: NAME, header: HEADER, user: ID };
|
||||||
|
const role = { authority: { withinRole: ['message.send'], crossRole: [] } };
|
||||||
|
const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'cto', delivery: 'pm' }, roles: { pm: role, cto: role, coder: role } } };
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'r76-demo-'));
|
||||||
|
const store = new Store(root), b = new Broker({ store, businesses });
|
||||||
|
const coder = b.bindLaunch({ business: 'demo', role: 'coder', run: 'coder-run', harness: 'pi', address: 'coder-run' });
|
||||||
|
b.request(coder, { verb: 'role.claim' });
|
||||||
|
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
||||||
|
const reader = b.bindReader({ business: 'demo' });
|
||||||
|
const bus = views({ call: async (verb, args = {}) => structuredClone(b.request(reader, { verb, args })) });
|
||||||
|
const toBus = fn => async args => { try { return await fn(args); } catch (e) { throw new BusReadError(e.code ?? 'outcome-unknown'); } };
|
||||||
|
const send = toBus(async args => structuredClone(b.request(human, { verb: 'message.send', args })));
|
||||||
|
// A transport that loses the answer after the broker wrote: the outcome is unknown.
|
||||||
|
const lost = toBus(async args => { b.request(human, { verb: 'message.send', args }); throw Object.assign(new Error('lost'), { code: 'outcome-unknown' }); });
|
||||||
|
const lines = [];
|
||||||
|
const log = l => lines.push(l);
|
||||||
|
const servers = {
|
||||||
|
send: await startServer({ port: 0, board: 'http://127.0.0.1:9', bus, preview, send, log }),
|
||||||
|
lost: await startServer({ port: 0, board: 'http://127.0.0.1:9', bus, preview, send: lost, log }),
|
||||||
|
readonly: await startServer({ port: 0, board: 'http://127.0.0.1:9', bus, preview, log }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const signed = { [HEADER]: ID, origin: `https://${NAME}`, 'sec-fetch-site': 'same-origin', 'content-type': 'application/json' };
|
||||||
|
function call(server, { method = 'POST', headers = signed, body, host = NAME } = {}) {
|
||||||
|
return new Promise((done, fail) => {
|
||||||
|
const length = body === undefined ? {} : { 'content-length': Buffer.byteLength(body) };
|
||||||
|
const req = request({ host: '127.0.0.1', port: servers[server].address().port, method, path: '/api/bus/message', headers: { host, ...length, ...headers } }, res => {
|
||||||
|
let text = '';
|
||||||
|
res.on('data', c => (text += c));
|
||||||
|
res.on('end', () => done(`${res.statusCode} ${text}`));
|
||||||
|
});
|
||||||
|
req.on('error', fail);
|
||||||
|
req.end(body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const omit = (k, extra = {}) => ({ ...Object.fromEntries(Object.entries(signed).filter(([n]) => n !== k)), ...extra });
|
||||||
|
const show = async (what, server, options) => console.log(`${what.padEnd(46)} ${await call(server, options)}`);
|
||||||
|
const BODY = 'Please look at row 76 before the cutover.';
|
||||||
|
const sent = [];
|
||||||
|
|
||||||
|
console.log('== a send, and a reply to the coder');
|
||||||
|
const first = await call('send', { body: JSON.stringify({ to: 'coder', body: BODY }) });
|
||||||
|
console.log(`${'to coder'.padEnd(46)} ${first}`);
|
||||||
|
sent.push(JSON.parse(first.slice(5)).id);
|
||||||
|
const asked = b.request(coder, { verb: 'message.send', args: { to: 'human', body: 'Which branch?' } });
|
||||||
|
const reply = await call('send', { body: JSON.stringify({ to: 'coder', body: 'refactor', in_reply_to: asked.id }) });
|
||||||
|
console.log(`${'to coder, in_reply_to the coder\'s message'.padEnd(46)} ${reply}`);
|
||||||
|
sent.push(JSON.parse(reply.slice(5)).id);
|
||||||
|
|
||||||
|
console.log('== refused before the bus');
|
||||||
|
await show('no sign-in header (401, no log line)', 'send', { headers: omit(HEADER), body: JSON.stringify({ to: 'coder', body: BODY }) });
|
||||||
|
await show('loopback Host with the header', 'send', { host: '127.0.0.1', body: JSON.stringify({ to: 'coder', body: BODY }) });
|
||||||
|
await show('another Origin (shared check, no log line)', 'send', { headers: omit('origin', { origin: 'https://evil.example' }), body: JSON.stringify({ to: 'coder', body: BODY }) });
|
||||||
|
await show('no Origin', 'send', { headers: omit('origin'), body: JSON.stringify({ to: 'coder', body: BODY }) });
|
||||||
|
await show('Sec-Fetch-Site: same-site', 'send', { headers: omit('sec-fetch-site', { 'sec-fetch-site': 'same-site' }), body: JSON.stringify({ to: 'coder', body: BODY }) });
|
||||||
|
await show('Content-Type: text/plain', 'send', { headers: omit('content-type', { 'content-type': 'text/plain' }), body: JSON.stringify({ to: 'coder', body: BODY }) });
|
||||||
|
await show('16385 bytes', 'send', { body: JSON.stringify({ to: 'coder', body: 'x'.repeat(16385 - 23) }) });
|
||||||
|
await show('an extra key', 'send', { body: JSON.stringify({ to: 'coder', body: BODY, class: 'BLOCKING' }) });
|
||||||
|
await show('GET', 'send', { method: 'GET', body: undefined });
|
||||||
|
console.log('== refused by the broker');
|
||||||
|
await show('a role the business lacks', 'send', { body: JSON.stringify({ to: 'auditor', body: BODY }) });
|
||||||
|
await show('an empty body', 'send', { body: JSON.stringify({ to: 'coder', body: '' }) });
|
||||||
|
await show('in_reply_to a missing message', 'send', { body: JSON.stringify({ to: 'coder', body: BODY, in_reply_to: 'msg-none' }) });
|
||||||
|
console.log('== the answer lost after the broker wrote');
|
||||||
|
await show('unknown, not retried', 'lost', { body: JSON.stringify({ to: 'pm', body: 'Lost answer test' }) });
|
||||||
|
console.log('== without --send');
|
||||||
|
await show('POST is read-only', 'readonly', { body: JSON.stringify({ to: 'coder', body: BODY }) });
|
||||||
|
|
||||||
|
console.log('== what the broker holds');
|
||||||
|
for (const id of sent) {
|
||||||
|
const [m] = b.request(human, { verb: 'trail', args: { subject: id } }).filter(r => r.table === 'messages' && r.id === id);
|
||||||
|
console.log(`${id.slice(0, 8)} from=${m.from_role}/${m.from_run} to=${m.to_role} class=${m.class} in_reply_to=${m.in_reply_to ?? '-'} body=${JSON.stringify(m.body)}`);
|
||||||
|
}
|
||||||
|
const pm = b.bindLaunch({ business: 'demo', role: 'pm', run: 'pm-run', harness: 'pi', address: 'pm-run' });
|
||||||
|
b.request(pm, { verb: 'role.claim' });
|
||||||
|
const lostRows = b.request(pm, { verb: 'message.receive', args: {} });
|
||||||
|
console.log(`pm receives ${lostRows.length} message(s) from the lost-answer send: ${JSON.stringify(lostRows.map(m => m.body))}`);
|
||||||
|
|
||||||
|
console.log('== the Console log, one line per send past sign-in');
|
||||||
|
for (const l of lines) console.log(l);
|
||||||
|
console.log(`a body in the log: ${lines.some(l => l.includes(BODY) || l.includes('refactor') || l.includes('Lost answer'))}`);
|
||||||
|
for (const s of Object.values(servers)) await new Promise(r => s.close(r));
|
||||||
|
store.close();
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
== a send, and a reply to the coder
|
||||||
|
to coder 200 {"id":"8f66630d-cb38-4879-8119-fb3defd532c8","at":"2026-10-12T00:40:48.392Z"}
|
||||||
|
to coder, in_reply_to the coder's message 200 {"id":"3a35d7b4-2020-464e-9bc3-5ee870c8b53f","at":"2026-10-12T00:40:49.041Z"}
|
||||||
|
== refused before the bus
|
||||||
|
no sign-in header (401, no log line) 401 {"error":"sign-in required"}
|
||||||
|
loopback Host with the header 403 {"error":"proxy Host refused","message":"the proxy must forward Host unchanged"}
|
||||||
|
another Origin (shared check, no log line) 403 {"error":"cross-origin request refused"}
|
||||||
|
no Origin 403 {"error":"cross-origin request refused","message":"a send needs the Console's own Origin","sent":false}
|
||||||
|
Sec-Fetch-Site: same-site 403 {"error":"cross-site request refused","message":"a send needs Sec-Fetch-Site: same-origin","sent":false}
|
||||||
|
Content-Type: text/plain 400 {"error":"invalid-request","message":"Content-Type must be application/json","sent":false}
|
||||||
|
16385 bytes 400 {"error":"invalid-request","message":"body larger than 16384 bytes","sent":false}
|
||||||
|
an extra key 400 {"error":"invalid-request","message":"a send takes to, body and an optional in_reply_to, nothing else","sent":false}
|
||||||
|
GET 405 {"error":"method not allowed"}
|
||||||
|
== refused by the broker
|
||||||
|
a role the business lacks 400 {"error":"unknown-role","message":"not sent: the bus refused it (unknown-role)","sent":false}
|
||||||
|
an empty body 400 {"error":"invalid-request","message":"not sent: the bus refused it (invalid-request)","sent":false}
|
||||||
|
in_reply_to a missing message 400 {"error":"message-not-found","message":"not sent: the bus refused it (message-not-found)","sent":false}
|
||||||
|
== the answer lost after the broker wrote
|
||||||
|
unknown, not retried 503 {"error":"outcome-unknown","message":"the message may have been sent; nothing was retried. Check the trail before sending again","sent":"unknown"}
|
||||||
|
== without --send
|
||||||
|
POST is read-only 403 {"error":"read-only","message":"the Console preview is read-only"}
|
||||||
|
== what the broker holds
|
||||||
|
8f66630d from=human/human-cli to=coder class=INFO in_reply_to=- body="Please look at row 76 before the cutover."
|
||||||
|
3a35d7b4 from=human/human-cli to=coder class=INFO in_reply_to=0536126f-c54a-40a5-999d-c7fe1c01d9a2 body="refactor"
|
||||||
|
pm receives 1 message(s) from the lost-answer send: ["Lost answer test"]
|
||||||
|
== the Console log, one line per send past sign-in
|
||||||
|
2026-10-12T00:40:48.392Z send user="jason" to="coder" id=8f66630d-cb38-4879-8119-fb3defd532c8
|
||||||
|
2026-10-12T00:40:49.041Z send user="jason" to="coder" id=3a35d7b4-2020-464e-9bc3-5ee870c8b53f
|
||||||
|
2026-10-12T00:40:49.057Z send user="jason" to=none error=cross-origin request refused
|
||||||
|
2026-10-12T00:40:49.058Z send user="jason" to=none error=cross-site request refused
|
||||||
|
2026-10-12T00:40:49.061Z send user="jason" to=none error=invalid-request
|
||||||
|
2026-10-12T00:40:49.066Z send user="jason" to=none error=invalid-request
|
||||||
|
2026-10-12T00:40:49.072Z send user="jason" to=none error=invalid-request
|
||||||
|
2026-10-12T00:40:49.281Z send user="jason" to="auditor" error=unknown-role
|
||||||
|
2026-10-12T00:40:49.535Z send user="jason" to="coder" error=invalid-request
|
||||||
|
2026-10-12T00:40:49.802Z send user="jason" to="coder" error=message-not-found
|
||||||
|
2026-10-12T00:40:50.173Z send user="jason" to="pm" error=outcome-unknown
|
||||||
|
a body in the log: false
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Dewey row 76 mutants: single-site edits in a mutant worktree (never the build tree), each run
|
||||||
|
# against the whole webui suite. The candidate is uncommitted there, so each file is restored
|
||||||
|
# from the copy read before the edit, not from git.
|
||||||
|
# Usage: mutants.py [mutant ...]. A site that doesn't match exactly once is BAD-SITE.
|
||||||
|
import os, subprocess, sys
|
||||||
|
|
||||||
|
R = os.path.expanduser('~/dewey-scratch/r76')
|
||||||
|
T = R + '/mut'
|
||||||
|
OUT = R + '/out/mut'
|
||||||
|
SERVE = 'packages/webui/src/serve.mjs'
|
||||||
|
BUS = 'packages/webui/src/bus.mjs'
|
||||||
|
PAGE = 'packages/webui/src/public/bus.js'
|
||||||
|
TESTS = ['node', '--test', 'packages/webui/tests/*.test.mjs']
|
||||||
|
|
||||||
|
MUTANTS = [
|
||||||
|
# name, file, edits [(old, new)]
|
||||||
|
('send-header-unchecked', SERVE, [(" if (one(req, preview.header) !== preview.user) return json(res, 401, { error: 'sign-in required' });\n", "")]),
|
||||||
|
('send-origin-unchecked', SERVE, [(" if (one(req, 'origin') !== `https://${preview.host}`) return refuse(", " if (false) return refuse(")]),
|
||||||
|
('send-sfs-unchecked', SERVE, [(" if (one(req, 'sec-fetch-site') !== 'same-origin') return refuse(", " if (false) return refuse(")]),
|
||||||
|
('send-sfs-same-site', SERVE, [(" if (one(req, 'sec-fetch-site') !== 'same-origin') return", " if (!['same-origin', 'same-site'].includes(one(req, 'sec-fetch-site'))) return")]),
|
||||||
|
('send-limit-64k', SERVE, [("await body(req, 16384)", "await body(req, 65536)")]),
|
||||||
|
('send-extra-keys', SERVE, [(" || Object.keys(value).some(k => !SEND_KEYS.includes(k))", "")]),
|
||||||
|
('send-drops-reply-to', SERVE, [(" if ('in_reply_to' in value) args.in_reply_to = value.in_reply_to;\n", "")]),
|
||||||
|
('send-no-id-check', SERVE, [(" if (!subjectOk(result?.id)) return refuse(", " if (false) return refuse(")]),
|
||||||
|
('send-log-unbounded', SERVE, [("JSON.stringify(to.slice(0, 64))", "to")]),
|
||||||
|
('config-send-loopback', SERVE, [("send: Boolean(send) && named", "send: Boolean(send)")]),
|
||||||
|
('route-open-without-send', SERVE, [(" if (send && path === '/api/bus/message') {", " if (path === '/api/bus/message') {")]),
|
||||||
|
('send-opens-writes', SERVE, [(" if (preview !== null && req.method !== 'GET' && req.method !== 'HEAD') return json(res, 403, { error: 'read-only'", " if (preview !== null && !send && req.method !== 'GET' && req.method !== 'HEAD') return json(res, 403, { error: 'read-only'")]),
|
||||||
|
('outcome-all-definite', BUS, [("const OPEN = ['outcome-unknown', 'invalid-response', 'response-too-large'];", "const OPEN = ['outcome-unknown'];")]),
|
||||||
|
('status-unknown-role-502', BUS, [("'unknown-role': 400, ", "")]),
|
||||||
|
('page-no-pending-guard', PAGE, [(" if (sending) return;\n", "")]),
|
||||||
|
('page-clears-edited-text', PAGE, [(" if (draft.body === text) draft.body = '';", " draft.body = '';")]),
|
||||||
|
('page-5xx-refusal-unknown', PAGE, [(" || body?.sent === false)", ")")]),
|
||||||
|
('page-retries-unknown', PAGE, [(" } else receipt = { kind: 'unknown', to };", " } else { receipt = { kind: 'unknown', to }; await fetch('/api/bus/message', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ to, body: text }), cache: 'no-store', redirect: 'manual' }).catch(() => {}); }")]),
|
||||||
|
('page-redirect-unknown', PAGE, [(" if (res?.type === 'opaqueredirect') {", " if (false) {")]),
|
||||||
|
('page-follows-redirect', PAGE, [(", cache: 'no-store', redirect: 'manual' });", ", cache: 'no-store' });")]),
|
||||||
|
('page-trail-any-id', PAGE, [(" if (!of(tr, 'messages').some(m => m.id === ref)) return notFound(", " if (false) return notFound(")]),
|
||||||
|
]
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
try:
|
||||||
|
p = subprocess.run(cmd, cwd=T, capture_output=True, text=True, env={**os.environ, 'TMPDIR': R + '/tmp'}, timeout=600)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return 'TIMEOUT', 'timed out after 600 s'
|
||||||
|
return p.returncode, p.stdout + p.stderr
|
||||||
|
|
||||||
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
os.makedirs(R + '/tmp', exist_ok=True)
|
||||||
|
before = run(['git', 'diff'])[1]
|
||||||
|
rc, out = run(TESTS)
|
||||||
|
open(f'{OUT}/baseline.txt', 'w').write(out)
|
||||||
|
print('baseline', 'pass' if rc == 0 else f'FAIL rc={rc}', flush=True)
|
||||||
|
want = sys.argv[1:]
|
||||||
|
results = []
|
||||||
|
for name, f, edits in MUTANTS:
|
||||||
|
if want and name not in want:
|
||||||
|
continue
|
||||||
|
path = os.path.join(T, f)
|
||||||
|
original = open(path).read()
|
||||||
|
bad = [old for old, _ in edits if original.count(old) != 1]
|
||||||
|
if bad:
|
||||||
|
results.append(f'{name} BAD-SITE ({[original.count(o) for o in bad]} matches)')
|
||||||
|
print(results[-1], flush=True)
|
||||||
|
continue
|
||||||
|
src = original
|
||||||
|
for old, new in edits:
|
||||||
|
src = src.replace(old, new)
|
||||||
|
open(path, 'w').write(src)
|
||||||
|
try:
|
||||||
|
rc, out = run(TESTS)
|
||||||
|
finally:
|
||||||
|
open(path, 'w').write(original)
|
||||||
|
open(f'{OUT}/mut-{name}.txt', 'w').write(out)
|
||||||
|
failed = [l[2:].split(' (')[0] for l in out.splitlines() if l.startswith('✖ ') and 'failing tests' not in l]
|
||||||
|
verdict = 'TIMEOUT' if rc == 'TIMEOUT' else 'killed' if rc else 'SURVIVED'
|
||||||
|
results.append(f"{name} {verdict}" + (f": {sorted(set(failed))}" if failed else ''))
|
||||||
|
print(results[-1], flush=True)
|
||||||
|
print('worktree unchanged after:', run(['git', 'diff'])[1] == before)
|
||||||
|
ran = [r for r in results if 'BAD-SITE' not in r]
|
||||||
|
print(f'summary {sum(1 for r in ran if " killed" in r)} of {len(ran)} killed')
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
baseline pass
|
||||||
|
send-header-unchecked killed: ['a send without the signed-in user is 401 and is neither sent nor logged, loopback included']
|
||||||
|
send-origin-unchecked killed: ['each request check refuses before anything is sent, and logs the refusal without the body']
|
||||||
|
send-sfs-unchecked killed: ['each request check refuses before anything is sent, and logs the refusal without the body']
|
||||||
|
send-sfs-same-site killed: ['each request check refuses before anything is sent, and logs the refusal without the body']
|
||||||
|
send-limit-64k killed: ['each request check refuses before anything is sent, and logs the refusal without the body']
|
||||||
|
send-extra-keys killed: ['each request check refuses before anything is sent, and logs the refusal without the body']
|
||||||
|
send-drops-reply-to killed: ['a send passes exactly to, body and in_reply_to, answers the id, and logs it', 'against a real broker: the message is written once as a human send, and its refusals map']
|
||||||
|
send-no-id-check killed: ['a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried']
|
||||||
|
send-log-unbounded killed: ['a crafted role name cannot forge a log line', 'a send passes exactly to, body and in_reply_to, answers the id, and logs it', 'the CLI with --send and a configured bus sends through the human transport and logs one line']
|
||||||
|
config-send-loopback killed: ['config says send only on the public name with --send on']
|
||||||
|
route-open-without-send killed: ['without --send the route is read-only like every other write, and config says no send']
|
||||||
|
send-opens-writes killed: ['with --send every other write is still 403 read-only, and nothing reaches the board or bus']
|
||||||
|
outcome-all-definite killed: ['a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried', 'through the human transport the request is exactly the business, message.send and the keys']
|
||||||
|
status-unknown-role-502 killed: ['a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried', 'against a real broker: the message is written once as a human send, and its refusals map', 'through the human transport the request is exactly the business, message.send and the keys']
|
||||||
|
page-no-pending-guard killed: ['the send box: held roles, the draft across a refresh, each receipt, one POST per Send, and the trail link']
|
||||||
|
page-clears-edited-text killed: ['the send box: held roles, the draft across a refresh, each receipt, one POST per Send, and the trail link']
|
||||||
|
page-5xx-refusal-unknown killed: ['the send box: held roles, the draft across a refresh, each receipt, one POST per Send, and the trail link']
|
||||||
|
page-retries-unknown killed: ['shipped strings are unchanged, and the web keeps its write boundary', 'the send box: held roles, the draft across a refresh, each receipt, one POST per Send, and the trail link']
|
||||||
|
page-redirect-unknown killed: ['the send box: held roles, the draft across a refresh, each receipt, one POST per Send, and the trail link']
|
||||||
|
page-follows-redirect killed: ['shipped strings are unchanged, and the web keeps its write boundary', 'the send box: held roles, the draft across a refresh, each receipt, one POST per Send, and the trail link']
|
||||||
|
page-trail-any-id killed: ['the send box: held roles, the draft across a refresh, each receipt, one POST per Send, and the trail link']
|
||||||
|
worktree unchanged after: True
|
||||||
|
summary 21 of 21 killed
|
||||||
Reference in New Issue
Block a user