Rebased on row 59 (a8512007). Darkwing's N1-N5 and the Q1 proxy line (lead decision 99), Filbert's N1-N4 (lead decision 100): a 408 is unknown on the send box and the board Reply, the receipt shows the code once, focus and caret kept, a role no longer held kept, Inbox current on a message trail, README notes. 35 of 35 mutants killed; gate green on729da72b. Manifest sha256 04ecf1b57c594a35d34d53277b2df915a571f4fa9f7cc29146dbedfa612f77d4. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -1,13 +1,23 @@
|
||||
# Row 76 (#1570): Console sends a human message to a role, round 1
|
||||
# Row 76 (#1570): Console sends a human message to a role, round 2
|
||||
|
||||
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.
|
||||
the reply view question is lead decision 97, option (a). Base `729da72b`
|
||||
(row 59 landed in `a8512007`). None of the candidate's paths changed
|
||||
between `729da72b` and `refactor` HEAD (`729da72b`) when the packet was
|
||||
built.
|
||||
|
||||
`build.patch` is `git diff --binary 3d298920 -- packages/webui`, the two
|
||||
Round 2 is the rebase on row 59, Darkwing's round 1 notes N1 to N5 with
|
||||
a Q1 line in the README (lead decision 99), and Filbert's round 1 notes
|
||||
(lead decision 100). They are listed separately below: "Round 2: rebase
|
||||
on row 59" for the merged hunks, then "Round 2: Darkwing's notes (lead
|
||||
decision 99)" and "Round 2: Filbert's notes (lead decision 100)" for the
|
||||
content changes.
|
||||
The rest of this file is round 1's, updated where those changes touch it,
|
||||
with the mutants, gate and demonstration rerun on the round 2 tree.
|
||||
|
||||
`build.patch` is `git diff --binary 729da72b -- 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.
|
||||
|
||||
@@ -87,11 +97,11 @@ role": a picker of held roles from the agents read, a text box and Send.
|
||||
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
|
||||
- Not sent: a 4xx other than 408, 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
|
||||
- Anything else, a lost answer and a 408 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
|
||||
@@ -99,9 +109,9 @@ role": a picker of held roles from the agents read, a text box and Send.
|
||||
(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.
|
||||
may be unknown; check the seat before sending again" for a 403. A 4xx other
|
||||
than 408 now says "Not sent."; no answer, an unreadable one, a 5xx or a
|
||||
408 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
|
||||
@@ -135,7 +145,8 @@ pointer to `scripts/webui-service.sh`, which takes no `--send`.
|
||||
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.
|
||||
Console got it, so it stays unknown, and so does a 408 (round 2,
|
||||
Filbert's N1).
|
||||
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.
|
||||
@@ -154,7 +165,141 @@ pointer to `scripts/webui-service.sh`, which takes no `--send`.
|
||||
it has spaces (`error=cross-origin request refused`). It is always last
|
||||
on the line.
|
||||
|
||||
## Rebase on row 60
|
||||
## Round 2: rebase on row 59
|
||||
|
||||
Round 1's candidate was on `3d298920` (candidate manifest `ca870314`,
|
||||
request comment 27594). Row 59 landed first in `a8512007`, so I applied
|
||||
round 1's `build.patch` to `729da72b` with `git apply --3way --index`.
|
||||
Every file applied cleanly, with no conflict.
|
||||
|
||||
Compared by added and removed lines, file by file, round 2's patch is
|
||||
identical to round 1's in all nine files. Only the context around the
|
||||
hunks moved. Two manifest digests change, `packages/webui/README.md` and
|
||||
`packages/webui/src/public/bus.js`, because row 59 changed those files
|
||||
too. The other seven digests are the same as round 1's.
|
||||
|
||||
Row 59's hunks in this row's files, and how each merged:
|
||||
|
||||
- `packages/webui/README.md`: row 59 adds the "Agent state (row 59,
|
||||
#1548)" section, lines 270-280 in the candidate, between "Inbox, tasks,
|
||||
agents and trails" and "Shell". My nearest hunks are in "Inbox, tasks,
|
||||
agents and trails" above it (the route list gains
|
||||
`#/trail/message/<id>`, and "Nothing writes" names the send box) and in
|
||||
"Data and boundaries" below it (without `--send` no route writes).
|
||||
Neither touches row 59's section. It merged cleanly.
|
||||
- `packages/webui/src/public/bus.js`: row 59 adds `stateMark` and
|
||||
`attention` and changes `agentsView`, lines 367-390 in the candidate
|
||||
(State and Attention columns). My hunks sit above it (the header
|
||||
comment, and the send form's `draft` and `composerHtml`, lines 246-287)
|
||||
and below it (the message trail, the router, focus restore, and the
|
||||
submit handler). Neither touches the agents view. It merged cleanly.
|
||||
`stateMark`, `attention`, `plural`, `blockChip`, `first` and `short`
|
||||
are each defined once.
|
||||
|
||||
Row 59's other files (`packages/bus/README.md`, `broker.mjs`,
|
||||
`agents.test.mjs`, the webui `bus.css`, `bus-browser.test.mjs` and
|
||||
`bus.test.mjs`) aren't files this row touches. The mutants, the suites and the
|
||||
demonstration below ran on the round 2 tree.
|
||||
|
||||
## Round 2: Darkwing's notes (lead decision 99)
|
||||
|
||||
Content changes made after the rebase, against Darkwing's round 1 review
|
||||
(comment 27612, `agents/darkwing/work/queue-76-review/review-r1.md`). They
|
||||
change three files: `packages/webui/README.md`, `src/public/bus.js` and
|
||||
`tests/send-browser.test.mjs`. Each was a small change; none is left for a
|
||||
follow-up row.
|
||||
|
||||
- **N1, the board Reply's "Not sent." (`app.js:378`) held by a browser
|
||||
test.** `send-browser.test.mjs`'s stub now answers `/api/reply` from a
|
||||
queue when one is set. The test opens an inspection, types a reply, and
|
||||
answers a 403 and then a 502. The 403 must show `failed: refused here.
|
||||
Not sent.` and the 502 `failed: board unreachable. Delivery may be
|
||||
unknown; check the seat before sending again.`, the reply text kept both
|
||||
times, and the board's capture list stays empty. `app.js` itself is
|
||||
unchanged from round 1. The mutants `reply-4xx-unknown` and
|
||||
`reply-5xx-refused` check that the test catches both directions.
|
||||
- **N2, the caret across a refresh.** The refresh case now selects
|
||||
characters 3 to 7 in the text box, presses Refresh, and asserts focus
|
||||
and the selection `{start: 3, end: 7}`, after the pattern at
|
||||
`browser.test.mjs:55`. Mutants `page-no-caret` (not restored) and
|
||||
`page-caret-not-kept` (not saved).
|
||||
- **N3, README "Message a role".** "It is recorded exactly like one sent
|
||||
from the CLI" is now "It goes the same way as one sent from the CLI,
|
||||
with class INFO where `mosaic talk` uses REQUEST". The class stays INFO.
|
||||
The box's note in `bus.js` said the same thing ("recorded by the bus
|
||||
exactly like one sent from the CLI"); it now says "the same way as one
|
||||
sent from the CLI, with class INFO". No test pins that sentence.
|
||||
- **N4, README reply path.** "talk prints each one and records it
|
||||
delivered" is now: the broker records each one delivered when
|
||||
`message.receive` returns it, so the next receive doesn't return it
|
||||
again; talk prints each one and marks it read (`message.read`,
|
||||
`cli.mjs:208`).
|
||||
- **N5, the code shown twice in a refusal receipt.** The broker's
|
||||
refusals arrive as `not sent: the bus refused it (unknown-role)`, and
|
||||
the receipt added ` (unknown-role)` again, after its own "Not sent to
|
||||
coder:". The receipt now drops the server's leading `not sent: ` and
|
||||
adds the code only when the message doesn't already carry `(code)`. It
|
||||
reads `Not sent to coder: the bus refused it (unknown-role). The draft is
|
||||
kept.` A refusal whose message lacks the code (a 4xx from a check before
|
||||
the bus, or a proxy's) still gets it. Dropping the prefix goes past N5's
|
||||
letter; without it the receipt said "Not sent to coder: not sent: ...".
|
||||
The browser test now asserts the two receipts as exact strings (the 400
|
||||
`unknown-role` and the 503 `no-bus-host`). Mutants `page-code-twice` and
|
||||
`page-keeps-not-sent-prefix`.
|
||||
- **Q1, the proxy contract.** A new bullet in "What the proxy must do":
|
||||
the proxy passes the Console's status and body through and doesn't
|
||||
rewrite upstream errors (nginx's `proxy_intercept_errors` with an
|
||||
`error_page`, for example), because the page reads a 4xx as "not sent"
|
||||
and a rewritten 503 `outcome-unknown` would then report a message that
|
||||
may exist as not sent.
|
||||
- **Q2** stays as built (Darkwing: yes, refuse at start).
|
||||
|
||||
## Round 2: Filbert's notes (lead decision 100)
|
||||
|
||||
Filbert's round 1 review (comment 27637, approve). These change
|
||||
`packages/webui/README.md`, `src/public/bus.js`, `src/public/app.js`,
|
||||
`tests/send.test.mjs` and `tests/send-browser.test.mjs`. N1 is the one
|
||||
behaviour change; N3 is tests only. Each was a small change; none is left
|
||||
for a follow-up row. Filbert's N2 is Darkwing's N1 and their N4 is
|
||||
Darkwing's N5, both above.
|
||||
|
||||
- **N1, a 408 is unknown, not "not sent".** A proxy can answer 408 after
|
||||
it has forwarded the request (Envoy's `stream_idle_timeout`, when no
|
||||
upstream headers arrived in time). The send box's check
|
||||
(`bus.js`, the `refused` branch) and the board Reply's (`app.js:378`) now
|
||||
both leave 408 out of the 4xx they read as not sent; a 408 shows the
|
||||
unknown receipt. A 408 that carries the Console's own `sent: false` would
|
||||
still read as not sent, but the Console never answers 408. Tests: the
|
||||
send box's unknown loop gains a 408, and the board Reply case a third
|
||||
answer, 408, which must say delivery may be unknown with the text kept.
|
||||
README: the "Outcome unknown" bullet says a 408 counts as unknown, here
|
||||
and on the Reply, and the Q1 proxy bullet says "a 4xx other than 408".
|
||||
Mutants `page-408-refused` and `reply-408-refused`.
|
||||
- **N3, Filbert's surviving mutants, each now held by a test:**
|
||||
- `p-no-field-focus` and `p-no-caret`: the refresh case leaves focus in
|
||||
the text box (the button is clicked from script, which doesn't move
|
||||
focus), then asserts focus and the selection (Darkwing's N2 above).
|
||||
Mine: `page-no-field-focus`, `page-no-caret`, `page-caret-not-kept`.
|
||||
- `p-not-held-dropped`: with `pm` selected, the test ends `pm`'s run in
|
||||
the broker (`endLaunch`, which releases its claim) and refreshes. The
|
||||
picker must still hold `pm`, selected, as `pm (not held now)`. Mine:
|
||||
`page-not-held-dropped`.
|
||||
- `p-inbox-not-current`: on `#/trail/message/<id>` the Inbox is the
|
||||
current section (`aria-current="page"`). Mine:
|
||||
`page-inbox-not-current`.
|
||||
- `s-reply-truthy`: the passthrough test sends `in_reply_to: ""` and
|
||||
asserts the key reaches `send` with the empty value. Mine:
|
||||
`send-reply-truthy`.
|
||||
- `s-plain-error-code`: the plain `Error` in the outcomes table
|
||||
(`send.test.mjs`, the `ENOENT` case) now carries `code: 'ENOENT'`, as a
|
||||
socket error does, and must still be 503 `outcome-unknown`. Mine:
|
||||
`outcome-plain-error-code`.
|
||||
- `s-log-cut-65`: the forged-line test asserts the logged `to` is the
|
||||
first 64 characters exactly, and a 65-character role logs without its
|
||||
last one. Mine: `send-log-cut-65`.
|
||||
- `s-array-ok` is equivalent, as Filbert says; no test.
|
||||
|
||||
## Rebase on row 60 (round 1)
|
||||
|
||||
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
|
||||
@@ -180,7 +325,8 @@ this row's files, and how each merged:
|
||||
```
|
||||
|
||||
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.
|
||||
mutants, the suites and the demonstration below ran on round 2's tree (the
|
||||
rebase on `729da72b` with every round 2 change).
|
||||
|
||||
## Files
|
||||
|
||||
@@ -189,11 +335,11 @@ mutants, the suites and the demonstration below ran on the rebased tree.
|
||||
| `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/src/public/bus.js` | the "Message a role" box, the message trail kind, focus and caret kept across a refresh; round 2: the refusal receipt shows the code once, the box's note, a 408 stays unknown |
|
||||
| `packages/webui/src/public/app.js` | the Reply 4xx text; round 2: a 408 stays unknown |
|
||||
| `packages/webui/README.md` | the "Message a role" section and the folded-in row 74 notes; round 2: Darkwing's N3 and N4, the Q1 proxy bullet, the 408 line |
|
||||
| `packages/webui/tests/send.test.mjs` | new, 13 tests; round 2: `in_reply_to: ""`, a coded plain error, the 64-character log cut |
|
||||
| `packages/webui/tests/send-browser.test.mjs` | new, 1 browser test; round 2: the board Reply 403, 502 and 408, focus and caret, a role no longer held, exact refusal receipts, a 408 send, Inbox current on a message trail |
|
||||
| `packages/webui/tests/shell.test.mjs` | the pinned write boundary: bus.js has exactly one write, this one |
|
||||
|
||||
## Tests
|
||||
@@ -215,9 +361,10 @@ 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
|
||||
PUT 405; exactly `to`, `body` and `in_reply_to` passed on, an empty
|
||||
`in_reply_to` included; a crafted `to` can't forge a log line, and `to` is
|
||||
cut at 64 characters; each bus refusal with its status and `sent:
|
||||
false`, each open outcome 503 `unknown` (a plain error with a code too), 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)
|
||||
@@ -227,30 +374,37 @@ and `unknown-role`, `invalid-request` and `message-not-found` map to 400.
|
||||
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
|
||||
request; the draft, focus and caret across a refresh; a role no longer
|
||||
held kept selected as "(not held now)"; a 400 and a 503
|
||||
`sent: false` refused with the draft kept, each receipt asserted exactly
|
||||
with its code once; a redirect refused as `sign-in`; an unknown
|
||||
outcome, a network failure, a 200 without an id and a 408 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.
|
||||
the message's trail, with the Inbox the current section; an unknown
|
||||
message id is not found; the board's Reply answered 403 says "Not sent."
|
||||
and 502 and 408 say delivery may be unknown, the reply text kept each time
|
||||
and nothing captured.
|
||||
|
||||
## 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.
|
||||
`mutants.py` makes 35 single-site edits, one at a time, in a separate
|
||||
worktree of round 2's tree (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
|
||||
**35 of 35 were killed** (2026-10-12T01:54:19Z to 02:29:20Z). `mutants.txt`
|
||||
lists each mutant with the tests that killed it.
|
||||
|
||||
Round 1's 21:
|
||||
|
||||
- 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
|
||||
opened by `--send` (`send-opens-writes`, killed by the "every other
|
||||
write is still 403" test).
|
||||
- Outcomes (`bus.mjs`): only `outcome-unknown` left open; `unknown-role`
|
||||
dropped from the status map.
|
||||
@@ -259,9 +413,27 @@ and **21 of 21 were killed** (2026-10-12T00:40:39Z to 01:02:24Z).
|
||||
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.
|
||||
Round 2's 14, one or more for each reviewer note that changed behaviour or
|
||||
asked for a test:
|
||||
|
||||
- Board Reply (`app.js`): a 4xx read as unknown (`reply-4xx-unknown`); a
|
||||
5xx read as not sent (`reply-5xx-refused`); a 408 read as not sent
|
||||
(`reply-408-refused`). Darkwing's N1, Filbert's N2 and N1.
|
||||
- Send box (`bus.js`): the caret not restored (`page-no-caret`), restored
|
||||
to the end (`page-caret-not-kept`), focus not restored
|
||||
(`page-no-field-focus`); the code shown twice (`page-code-twice`); the
|
||||
"not sent: " prefix kept (`page-keeps-not-sent-prefix`); a 408 read as
|
||||
not sent (`page-408-refused`); a role no longer held dropped from the
|
||||
picker (`page-not-held-dropped`); the Inbox not current on a message
|
||||
trail (`page-inbox-not-current`). Darkwing's N2 and N5, Filbert's N1 and
|
||||
N3.
|
||||
- Route and outcomes: an empty `in_reply_to` dropped (`send-reply-truthy`);
|
||||
a plain error with a `code` read as definite
|
||||
(`outcome-plain-error-code`); the log's `to` cut at 65
|
||||
(`send-log-cut-65`). Filbert's N3.
|
||||
|
||||
Round 1's run on the `bbd22f1b` build and its 21-of-21 run on the row 60
|
||||
rebase are not repeated here; this run covers all 35 on the final tree.
|
||||
|
||||
## Demonstration
|
||||
|
||||
@@ -272,12 +444,12 @@ 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:
|
||||
worktree of round 2's tree:
|
||||
|
||||
```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"}
|
||||
to coder 200 {"id":"9db1d4eb-2abd-4c13-a388-2ed3d9503c07","at":"2026-10-12T02:37:11.862Z"}
|
||||
to coder, in_reply_to the coder's message 200 {"id":"c4b38d2a-34df-4dea-b31b-897f7de01777","at":"2026-10-12T02:37:11.877Z"}
|
||||
== 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"}
|
||||
@@ -297,34 +469,35 @@ unknown, not retried 503 {"error":"outcome-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"
|
||||
9db1d4eb from=human/human-cli to=coder class=INFO in_reply_to=- body="Please look at row 76 before the cutover."
|
||||
c4b38d2a from=human/human-cli to=coder class=INFO in_reply_to=8c236a3d-c415-4252-818c-015cdd81d185 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
|
||||
2026-10-12T02:37:11.862Z send user="jason" to="coder" id=9db1d4eb-2abd-4c13-a388-2ed3d9503c07
|
||||
2026-10-12T02:37:11.877Z send user="jason" to="coder" id=c4b38d2a-34df-4dea-b31b-897f7de01777
|
||||
2026-10-12T02:37:11.881Z send user="jason" to=none error=cross-origin request refused
|
||||
2026-10-12T02:37:11.882Z send user="jason" to=none error=cross-site request refused
|
||||
2026-10-12T02:37:11.883Z send user="jason" to=none error=invalid-request
|
||||
2026-10-12T02:37:11.885Z send user="jason" to=none error=invalid-request
|
||||
2026-10-12T02:37:11.886Z send user="jason" to=none error=invalid-request
|
||||
2026-10-12T02:37:11.892Z send user="jason" to="auditor" error=unknown-role
|
||||
2026-10-12T02:37:11.899Z send user="jason" to="coder" error=invalid-request
|
||||
2026-10-12T02:37:11.905Z send user="jason" to="coder" error=message-not-found
|
||||
2026-10-12T02:37:11.913Z 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:
|
||||
Detached worktree of `729da72b`, `build.patch` applied with `git apply
|
||||
--index`, the manifest (sha256 `04ecf1b5…`) 9/9 OK before and after.
|
||||
Suites one after another, `TMPDIR` in my scratch, Docker on,
|
||||
2026-10-12T02:29:20Z to 02:37:11Z:
|
||||
|
||||
| Suite | Result |
|
||||
|---|---|
|
||||
| `packages/webui` | 76 pass, 0 fail, 0 skipped, the browser tests included (14 new) |
|
||||
| `packages/bus` | 88 pass, 0 fail |
|
||||
| `packages/bus` | 94 pass, 0 fail (88 in round 1; row 59 added 6) |
|
||||
| `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) |
|
||||
@@ -339,8 +512,8 @@ Detached worktree of `3d298920`, `build.patch` applied with `git apply
|
||||
|
||||
## 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.
|
||||
Round 1's two questions are answered. Q1 (decision 5, a proxy 4xx shown
|
||||
as "not sent"): Darkwing said yes, with a proxy-contract line, now added;
|
||||
Filbert said yes for nginx, Caddy and Traefik except 408, now unknown.
|
||||
Q2 (decision 7, refusing `--send` at start without the bus config): both
|
||||
said yes; kept as built. Round 2 has no new question.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/packages/webui/README.md b/packages/webui/README.md
|
||||
index e2ffcb3b..6242cd34 100644
|
||||
index c866804c..5ac12d95 100644
|
||||
--- a/packages/webui/README.md
|
||||
+++ b/packages/webui/README.md
|
||||
@@ -23,7 +23,7 @@ Only HTTP loopback board origins are accepted. The WebUI binds to IPv4 loopback
|
||||
@@ -30,7 +30,7 @@ index e2ffcb3b..6242cd34 100644
|
||||
- The GET routes are unchanged: the bus views, the Queue, Business and
|
||||
Settings reads, the board panel and the assets. If the board is down, its
|
||||
panel shows the existing error and the bus views still answer.
|
||||
@@ -80,7 +82,8 @@ controls it:
|
||||
@@ -80,14 +82,117 @@ controls it:
|
||||
operator's path and skips the header check, so a proxy that forwarded a
|
||||
client's `Host: 127.0.0.1:7332` would let anyone through without signing
|
||||
in. A tunnel whose local end uses the same port number sends such a Host
|
||||
@@ -39,8 +39,13 @@ index e2ffcb3b..6242cd34 100644
|
||||
+ end can send a loopback Host without HEADER, so bind that end to loopback.
|
||||
- Every hop on this host that reaches the loopback port is bound by this
|
||||
contract too, not only the edge.
|
||||
+- It passes the Console's status and body through and doesn't rewrite
|
||||
+ upstream errors (nginx's `proxy_intercept_errors` with an `error_page`,
|
||||
+ for example). Under `--send` the page reads a 4xx other than 408 as "not
|
||||
+ sent", so a proxy that turned the Console's 503 `outcome-unknown` into a
|
||||
+ 4xx or a redirect would report a message that may exist as not sent.
|
||||
|
||||
@@ -88,6 +91,99 @@ The local exposure doesn't change ("Data and boundaries"): any process on
|
||||
The local exposure doesn't change ("Data and boundaries"): any process on
|
||||
the host can still connect to the port and send `Host: NAME` with HEADER and
|
||||
ID, or just a loopback Host, and read what a reader capability reads. The
|
||||
user id is not a secret; the sign-in at the edge is what keeps others out.
|
||||
@@ -64,8 +69,9 @@ index e2ffcb3b..6242cd34 100644
|
||||
+Console would still serve the board. With `--send` the Inbox has a "Message
|
||||
+a role" box: a role picker filled from the agents read, a text box and
|
||||
+Send. The message reaches the role as a message from the human, class INFO,
|
||||
+through `message.send` on the human transport. It is recorded exactly like
|
||||
+one sent from the CLI (lead decision 95). Decisions are still answered in a
|
||||
+through `message.send` on the human transport. It goes the same way as one
|
||||
+sent from the CLI, with class INFO where `mosaic talk` uses REQUEST (lead
|
||||
+decision 95). Decisions are still answered in a
|
||||
+terminal. There are no other message classes, no corrections and no seat
|
||||
+conversations here.
|
||||
+
|
||||
@@ -126,7 +132,9 @@ index e2ffcb3b..6242cd34 100644
|
||||
+ sign in again.
|
||||
+- Outcome unknown says the message may have been sent, keeps the draft, and
|
||||
+ has no trail to link, since no id came back. Check the role's side, or
|
||||
+ the bus trail from a terminal, before sending again.
|
||||
+ the bus trail from a terminal, before sending again. A 408 counts as
|
||||
+ unknown, here and on the board's Reply, since a proxy can answer it
|
||||
+ after forwarding the request (Envoy's `stream_idle_timeout`).
|
||||
+
|
||||
+The role's reply comes back as a message to the human. The Console doesn't
|
||||
+show it yet; that view is row 81 (#1575). Jason's next `mosaic talk` in a
|
||||
@@ -135,12 +143,13 @@ index e2ffcb3b..6242cd34 100644
|
||||
+- talk receives only after it sends its own message;
|
||||
+- `--wait 0` returns straight after the send and receives nothing;
|
||||
+- otherwise each receive returns every undelivered message to the human, up
|
||||
+ to 100 a call. talk prints each one and records it delivered, so the next
|
||||
+ receive doesn't return it again.
|
||||
+ to 100 a call. The broker records each one delivered when
|
||||
+ `message.receive` returns it, so the next receive doesn't return it
|
||||
+ again; talk prints each one and marks it read.
|
||||
|
||||
## Use
|
||||
|
||||
@@ -133,14 +229,14 @@ delivery may be unknown: inspect the seat before sending again.
|
||||
@@ -133,14 +238,14 @@ delivery may be unknown: inspect the seat before sending again.
|
||||
|
||||
The Console sidebar adds Inbox, Tasks and Agents next to the control board,
|
||||
which stays at `#/`. Routes: `#/inbox`, `#/inbox/<decision>`, `#/tasks`,
|
||||
@@ -158,7 +167,7 @@ index e2ffcb3b..6242cd34 100644
|
||||
with Copy, which only puts the command on the clipboard. It is answered in a
|
||||
terminal (REQ-DEC-3, Q4). Seen is not set from the browser either. The
|
||||
command has no `--business`, so `mosaic decide` uses the running bus host's
|
||||
@@ -282,8 +378,8 @@ capability reads ("Human and reader paths" in `packages/bus/README.md`):
|
||||
@@ -293,8 +398,8 @@ capability reads ("Human and reader paths" in `packages/bus/README.md`):
|
||||
the inbox, tasks, agents, trails, the two decision lists and the credential
|
||||
status. That includes
|
||||
a T3 seat using `curl` and a managed S6 session, since S6 doesn't confine
|
||||
@@ -292,25 +301,26 @@ index 5c6a5931..618a2cf1 100644
|
||||
for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => server.close());
|
||||
} catch (err) {
|
||||
diff --git a/packages/webui/src/public/app.js b/packages/webui/src/public/app.js
|
||||
index d321b483..2f04be1b 100644
|
||||
index d321b483..9b50235c 100644
|
||||
--- a/packages/webui/src/public/app.js
|
||||
+++ b/packages/webui/src/public/app.js
|
||||
@@ -372,7 +372,12 @@
|
||||
@@ -372,7 +372,13 @@
|
||||
// Do not discard text typed while the request was in flight.
|
||||
if (result.delivered && drafts.get(id) === text) { drafts.delete(id); if (inView && box.value === text) box.value = ''; }
|
||||
if (result.delivered && !paused) await refresh();
|
||||
- } catch (err) { receipts.set(id, { error: err.message + ' Delivery may be unknown; check the seat before sending again.' }); }
|
||||
+ } catch (err) {
|
||||
+ // A 4xx is a refusal before anything ran (the Console's, or the board's: see replyToRow), so
|
||||
+ // nothing was sent. No answer, an unreadable one or a 5xx leaves delivery unknown.
|
||||
+ const refused = err.status >= 400 && err.status < 500;
|
||||
+ // nothing was sent. No answer, an unreadable one, a 5xx or a 408 (a proxy can answer it
|
||||
+ // after forwarding) leaves delivery unknown.
|
||||
+ const refused = err.status >= 400 && err.status < 500 && err.status !== 408;
|
||||
+ receipts.set(id, { error: err.message + (refused ? '. Not sent.' : '. Delivery may be unknown; check the seat before sending again.') });
|
||||
+ }
|
||||
finally { sending.delete(id); render(); announce(receipts.get(id)?.delivered ? 'Reply delivered' : 'Reply failed; draft kept'); }
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
diff --git a/packages/webui/src/public/bus.js b/packages/webui/src/public/bus.js
|
||||
index 3be22af5..29764898 100644
|
||||
index b22bcf00..ffcc7cf5 100644
|
||||
--- a/packages/webui/src/public/bus.js
|
||||
+++ b/packages/webui/src/public/bus.js
|
||||
@@ -1,7 +1,8 @@
|
||||
@@ -350,10 +360,10 @@ index 3be22af5..29764898 100644
|
||||
+ const said = !r ? '' : r.kind === 'sent'
|
||||
+ ? `<p class="receipt">Sent to ${txt(r.to)} ${when(r.at)}, message <code>${txt(short(r.id))}</code>. <a href="#/trail/message/${enc(r.id)}">Its trail</a>.</p>`
|
||||
+ : r.kind === 'refused'
|
||||
+ ? `<p class="receipt failed">Not sent to ${txt(r.to)}: ${txt(r.message)} (<code>${txt(r.code)}</code>). The draft is kept.</p>`
|
||||
+ ? `<p class="receipt failed">Not sent to ${txt(r.to)}: ${txt(r.message)}${r.message.includes(`(${r.code})`) ? '' : ` (<code>${txt(r.code)}</code>)`}. The draft is kept.</p>`
|
||||
+ : `<p class="receipt failed">Outcome unknown: the message to ${txt(r.to)} may have been sent. Nothing was sent again, and the draft is kept. The bus gave no message id, so there is no trail to link: before sending again, look for it on the role's side or in the bus trail from a terminal.</p>`;
|
||||
+ return `<section class="panel s1-send" id="send-section" aria-labelledby="b-send"><h2 id="b-send">Message a role</h2>
|
||||
+ <p class="small muted">Goes to the role as a message from the human, recorded by the bus exactly like one sent from the CLI. A reply comes back to you as a message, which this Console does not show yet. The next <code>mosaic talk</code> in a terminal prints it, after sending its own message.</p>
|
||||
+ <p class="small muted">Goes to the role as a message from the human, the same way as one sent from the CLI, with class INFO. A reply comes back to you as a message, which this Console does not show yet. The next <code>mosaic talk</code> in a terminal prints it, after sending its own message.</p>
|
||||
+ ${roles === null ? '<p class="small warn">The held roles could not be read, so there is nothing to pick. Read again to retry.</p>' : !roles.length ? '<p class="small muted">No role is held right now, so there is nothing to pick.</p>' : ''}
|
||||
+ <form id="send-form" class="reply-form" novalidate><label for="send-to">To</label><select id="send-to" data-field="to"><option value="">Choose a held role</option>${options}</select>
|
||||
+ <label for="send-body">Message</label><textarea id="send-body" data-field="body" rows="4" placeholder="What should the role do?">${esc(draft.body)}</textarea>
|
||||
@@ -382,7 +392,7 @@ index 3be22af5..29764898 100644
|
||||
<h2 id="b-for-you">For you <span class="count">${rows.length}</span></h2>${body}
|
||||
<p class="small muted">Not shown here yet: ${gap('Decisions routed to roles')} ${gap('Closed decisions')} ${gap('Seen and DM state per row')} Open a decision for its seen, DM and closing history.</p>`;
|
||||
}
|
||||
@@ -444,6 +481,11 @@
|
||||
@@ -455,6 +492,11 @@
|
||||
const row = (await get('tasks')).find(r => r.task_ref === ref);
|
||||
if (!row) return notFound(`No task ${txt(ref)} in this business.`);
|
||||
rows = lines(await get('trail', ref), row.seq);
|
||||
@@ -394,7 +404,7 @@ index 3be22af5..29764898 100644
|
||||
} else {
|
||||
const d = await decision(get, ref);
|
||||
if (!d) return notFound(`No decision with id ${txt(ref)}.`);
|
||||
@@ -454,8 +496,8 @@
|
||||
@@ -465,8 +507,8 @@
|
||||
}
|
||||
const filter = GROUPS.some(([g]) => g === q.get('kind')) ? q.get('kind') : 'all';
|
||||
const base = `#/trail/${kind}/${enc(ref)}`;
|
||||
@@ -405,7 +415,7 @@ index 3be22af5..29764898 100644
|
||||
return `${head(name, `${readNote()} Oldest first. ${back}`)}
|
||||
<nav aria-label="Filter the trail" class="chips">${GROUPS.map(([g, l]) => `<a class="chip" href="${base}${g === 'all' ? '' : `?kind=${g}`}"${filter === g ? ' aria-current="true"' : ''}>${l} <span class="count">${g === 'all' ? rows.length : rows.filter(r => r.group === g).length}</span></a>`).join('')}</nav>
|
||||
${trailList(rows, filter)}`;
|
||||
@@ -527,24 +569,30 @@
|
||||
@@ -538,24 +580,30 @@
|
||||
if (a === 'settings' && n === 1) return settingsView(get);
|
||||
if (a === 'trail' && n === 3 && b === 'task') return TASK.test(c) ? trailView(get, b, c, q) : notFound(`${txt(c)} is not a task reference.`);
|
||||
if (a === 'trail' && n === 3 && b === 'decision') return SAFE.test(c) ? trailView(get, b, c, q) : notFound(`${txt(c)} is not a decision id.`);
|
||||
@@ -438,7 +448,7 @@ index 3be22af5..29764898 100644
|
||||
else if (k.h1) $('bus-view').querySelector('h1')?.focus({ preventScroll: true });
|
||||
}
|
||||
async function render(navigated) {
|
||||
@@ -677,6 +725,36 @@
|
||||
@@ -688,6 +736,37 @@
|
||||
if (m) { const src = $(m.dataset.mirror); src.value = m.value; src.dispatchEvent(new Event('change')); return; }
|
||||
if (e.target.id === 'palette' || e.target.id === 'mode') for (const s of document.querySelectorAll(`#bus-view select[data-mirror="${e.target.id}"]`)) s.value = e.target.value;
|
||||
});
|
||||
@@ -458,14 +468,15 @@ index 3be22af5..29764898 100644
|
||||
+ } catch { res = null; }
|
||||
+ // Sent: an id came back. Not sent: a 4xx (refused before the bus wrote), the server's
|
||||
+ // sent:false, or a redirect, which only the sign-in proxy gives (an expired session), so the
|
||||
+ // request never reached the Console. Anything else, no answer included, may have been sent.
|
||||
+ // request never reached the Console. Anything else, no answer included, may have been sent,
|
||||
+ // and so may a 408: a proxy can answer it after forwarding (Envoy's stream_idle_timeout).
|
||||
+ if (res?.type === 'opaqueredirect') {
|
||||
+ receipt = { kind: 'refused', to, code: 'sign-in', message: 'the sign-in proxy redirected it, so it never reached the Console; reload the page to sign in again' };
|
||||
+ } else if (res?.ok && SAFE.test(body?.id ?? '')) {
|
||||
+ receipt = { kind: 'sent', to, id: body.id, at: body.at };
|
||||
+ if (draft.body === text) draft.body = ''; // keep text typed while the send was in flight
|
||||
+ } else if (res && !res.ok && ((res.status >= 400 && res.status < 500) || body?.sent === false)) {
|
||||
+ receipt = { kind: 'refused', to, code: typeof body?.error === 'string' ? body.error : `HTTP ${res.status}`, message: typeof body?.message === 'string' ? body.message : 'refused' };
|
||||
+ } else if (res && !res.ok && ((res.status >= 400 && res.status < 500 && res.status !== 408) || body?.sent === false)) {
|
||||
+ receipt = { kind: 'refused', to, code: typeof body?.error === 'string' ? body.error : `HTTP ${res.status}`, message: typeof body?.message === 'string' ? body.message.replace(/^not sent: /, '') : 'refused' };
|
||||
+ } else receipt = { kind: 'unknown', to };
|
||||
+ sending = false; paintSend();
|
||||
+ announce(receipt.kind === 'sent' ? `Sent to ${to}` : receipt.kind === 'refused' ? 'Not sent; draft kept' : 'Outcome unknown; it may have been sent; draft kept');
|
||||
@@ -588,15 +599,16 @@ index 89327c07..6209f9b9 100644
|
||||
const file = files.get(path);
|
||||
diff --git a/packages/webui/tests/send-browser.test.mjs b/packages/webui/tests/send-browser.test.mjs
|
||||
new file mode 100644
|
||||
index 00000000..9b57c57c
|
||||
index 00000000..dd7c5668
|
||||
--- /dev/null
|
||||
+++ b/packages/webui/tests/send-browser.test.mjs
|
||||
@@ -0,0 +1,164 @@
|
||||
@@ -0,0 +1,195 @@
|
||||
+// Row 76 (#1570): the Inbox's "Message a role" box in a browser. A send passes only with the
|
||||
+// proxy's https Origin, which a loopback page can't have, so a script added before the page
|
||||
+// loads answers /api/config with send on and answers each POST /api/bus/message from a queue,
|
||||
+// recording what the page sent. Server-side checks are send.test.mjs's. The roles and the
|
||||
+// message trail come from an in-process broker through its reader session.
|
||||
+// recording what the page sent. A role's run is ended in the broker to show a role no longer held. Server-side checks are send.test.mjs's. The roles and the
|
||||
+// message trail come from an in-process broker through its reader session. The same script
|
||||
+// answers the board's /api/reply from its own queue while one is queued, for the Reply receipt.
|
||||
+import { test } from 'node:test';
|
||||
+import assert from 'node:assert/strict';
|
||||
+import { mkdtempSync, rmSync } from 'node:fs';
|
||||
@@ -624,9 +636,13 @@ index 00000000..9b57c57c
|
||||
+// what fetch gives for the sign-in proxy's redirect under redirect: 'manual' (type opaqueredirect, status 0).
|
||||
+const STUB = `(() => {
|
||||
+ const real = window.fetch.bind(window);
|
||||
+ window.__sends = []; window.__answers = [];
|
||||
+ window.__sends = []; window.__answers = []; window.__replies = [];
|
||||
+ window.fetch = async (url, init = {}) => {
|
||||
+ const u = String(url);
|
||||
+ if (u === '/api/reply' && window.__replies.length) {
|
||||
+ const a = window.__replies.shift();
|
||||
+ return new Response(JSON.stringify(a.body), { status: a.status, headers: { 'content-type': 'application/json' } });
|
||||
+ }
|
||||
+ if (u === '/api/config' && !location.search.includes('nosend')) {
|
||||
+ const c = await (await real(url, init)).json();
|
||||
+ return new Response(JSON.stringify({ ...c, send: true }), { headers: { 'content-type': 'application/json' } });
|
||||
@@ -642,7 +658,7 @@ index 00000000..9b57c57c
|
||||
+ };
|
||||
+})();`;
|
||||
+
|
||||
+test('the send box: held roles, the draft across a refresh, each receipt, one POST per Send, and the trail link', { timeout: 120000 }, async () => {
|
||||
+test('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', { timeout: 120000 }, async () => {
|
||||
+ const root = mkdtempSync(join(tmpdir(), 'webui-send-browser-'));
|
||||
+ const store = new Store(root), b0 = new Broker({ store, businesses });
|
||||
+ for (const role of ['pm', 'coder']) b0.request(b0.bindLaunch({ business: 'demo', role, run: `${role}-run`, harness: 'pi', address: `${role}-run` }), { verb: 'role.claim' });
|
||||
@@ -665,6 +681,22 @@ index 00000000..9b57c57c
|
||||
+ await b.call('Page.addScriptToEvaluateOnNewDocument', { source: STUB });
|
||||
+ await b.viewport(1280);
|
||||
+
|
||||
+ // The board's Reply: a 4xx was refused before anything ran, so "Not sent."; a 5xx or a 408
|
||||
+ // leaves delivery unknown. None reaches the board, and the draft stays.
|
||||
+ await b.navigate(base);
|
||||
+ await wait('document.querySelector("table.sessions [data-open=\'proj/agent1\']")');
|
||||
+ await b.evaluate('document.querySelector("table.sessions [data-open=\'proj/agent1\']").click(), true');
|
||||
+ await wait('document.querySelector("#reply")');
|
||||
+ await b.evaluate('document.querySelector("#reply").focus(), true');
|
||||
+ await b.call('Input.insertText', { text: 'reply draft' });
|
||||
+ for (const [status, error, said] of [[403, 'refused here', 'failed: refused here. Not sent.'], [502, 'board unreachable', 'failed: board unreachable. Delivery may be unknown; check the seat before sending again.'], [408, 'request timeout', 'failed: request timeout. Delivery may be unknown; check the seat before sending again.']]) {
|
||||
+ await b.evaluate(`window.__replies.push(${JSON.stringify({ status, body: { error } })}), true`);
|
||||
+ await b.evaluate('document.querySelector(".reply-form").requestSubmit(), true');
|
||||
+ await wait(`document.querySelector("#inspection .receipt")?.textContent === ${JSON.stringify(said)}`);
|
||||
+ assert.equal(await value('#reply'), 'reply draft');
|
||||
+ }
|
||||
+ assert.equal(f.captures.length, 0);
|
||||
+
|
||||
+ // Without send in /api/config there is no box.
|
||||
+ await b.navigate(`${base}?nosend#/inbox`);
|
||||
+ await wait('document.querySelector("#bus-view h1")?.textContent.startsWith("Inbox") && !document.querySelector("#bus-view [aria-busy]")');
|
||||
@@ -684,17 +716,25 @@ index 00000000..9b57c57c
|
||||
+ assert.match(await receipt(), /^Not sent to coder: the message is empty/);
|
||||
+ assert.deepEqual(await sends(), []);
|
||||
+
|
||||
+ // The draft survives a refresh, which redraws the form.
|
||||
+ // The draft, focus and caret survive a refresh, which redraws the form.
|
||||
+ await set('#send-body', msg);
|
||||
+ await b.evaluate('document.querySelector("#send-body").dataset.before = "1", true');
|
||||
+ await b.evaluate('document.querySelector("#refresh").click(), true');
|
||||
+ await b.evaluate('(()=>{const e=document.querySelector("#send-body");e.dataset.before="1";e.focus();e.setSelectionRange(3,7);document.querySelector("#refresh").click();return true})()');
|
||||
+ await wait('!document.querySelector("#send-body")?.dataset.before && !document.querySelector("#bus-view [aria-busy]")');
|
||||
+ assert.deepEqual([await value('#send-to'), await value('#send-body')], ['coder', msg]);
|
||||
+ assert.deepEqual(await b.evaluate('(()=>{const e=document.querySelector("#send-body");return {focus:document.activeElement===e,start:e.selectionStart,end:e.selectionEnd}})()'), { focus: true, start: 3, end: 7 });
|
||||
+
|
||||
+ // A role that stops being held stays selected, marked, so the draft isn't moved to another role.
|
||||
+ await set('#send-to', 'pm');
|
||||
+ b0.endLaunch({ business: 'demo', run: 'pm-run', reason: 'exited' });
|
||||
+ await b.evaluate('(()=>{document.querySelector("#send-body").dataset.before="1";document.querySelector("#refresh").click();return true})()');
|
||||
+ await wait('!document.querySelector("#send-body")?.dataset.before && !document.querySelector("#bus-view [aria-busy]")');
|
||||
+ assert.deepEqual(await b.evaluate('[...document.querySelectorAll("#send-to option")].map(o => [o.value, o.textContent, o.selected])'), [['', 'Choose a held role', false], ['coder', 'coder', false], ['pm', 'pm (not held now)', true]]);
|
||||
+ await set('#send-to', 'coder');
|
||||
+
|
||||
+ // A refusal keeps the draft and says why.
|
||||
+ await answer({ status: 400, body: { error: 'unknown-role', message: 'not sent: the bus refused it (unknown-role)', sent: false } });
|
||||
+ await send();
|
||||
+ assert.equal(await receipt(), 'Not sent to coder: not sent: the bus refused it (unknown-role) (unknown-role). The draft is kept.');
|
||||
+ assert.equal(await receipt(), 'Not sent to coder: the bus refused it (unknown-role). The draft is kept.');
|
||||
+ assert.equal(await value('#send-body'), msg);
|
||||
+ const [first] = await sends();
|
||||
+ assert.deepEqual(first, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ to: 'coder', body: msg }), redirect: 'manual' });
|
||||
@@ -702,13 +742,14 @@ index 00000000..9b57c57c
|
||||
+ // A 503 the server marked not sent is a refusal too; an unknown outcome and a lost answer are unknown.
|
||||
+ await answer({ status: 503, body: { error: 'no-bus-host', message: 'not sent: the bus refused it (no-bus-host)', sent: false } });
|
||||
+ await send();
|
||||
+ assert.match(await receipt(), /^Not sent to coder: .*\(no-bus-host\)\. The draft is kept\.$/);
|
||||
+ assert.equal(await receipt(), 'Not sent to coder: the bus refused it (no-bus-host). The draft is kept.');
|
||||
+ // A redirect is the sign-in proxy's (the Console never redirects this route): not sent.
|
||||
+ await answer({ redirect: true });
|
||||
+ await send();
|
||||
+ assert.equal(await receipt(), 'Not sent to coder: the sign-in proxy redirected it, so it never reached the Console; reload the page to sign in again (sign-in). The draft is kept.');
|
||||
+ assert.equal(await value('#send-body'), msg);
|
||||
+ for (const a of [{ status: 503, body: { error: 'outcome-unknown', message: 'the message may have been sent', sent: 'unknown' } }, { reject: true }, { status: 200, body: { at: 'x' } }]) {
|
||||
+ // A 408 too: a proxy can answer it after forwarding.
|
||||
+ for (const a of [{ status: 503, body: { error: 'outcome-unknown', message: 'the message may have been sent', sent: 'unknown' } }, { reject: true }, { status: 200, body: { at: 'x' } }, { status: 408, body: { error: 'request timeout' } }]) {
|
||||
+ await answer(a);
|
||||
+ await send();
|
||||
+ assert.match(await receipt(), /^Outcome unknown: the message to coder may have been sent\. Nothing was sent again, and the draft is kept\./, JSON.stringify(a));
|
||||
@@ -717,7 +758,7 @@ index 00000000..9b57c57c
|
||||
+ }
|
||||
+ // Each Send was one POST; none was repeated after the unknown ones.
|
||||
+ await new Promise(r => setTimeout(r, 500));
|
||||
+ assert.equal((await sends()).length, 6);
|
||||
+ assert.equal((await sends()).length, 7);
|
||||
+
|
||||
+ // While a send is pending, Send is disabled and a second press sends nothing; text typed
|
||||
+ // meanwhile is kept when the first one succeeds.
|
||||
@@ -727,18 +768,18 @@ index 00000000..9b57c57c
|
||||
+ assert.equal(await text('#send-form [data-send]'), 'Sending…');
|
||||
+ await b.evaluate('document.querySelector("#send-form [data-send]").click(), true');
|
||||
+ await set('#send-body', 'A second thought');
|
||||
+ assert.equal((await sends()).length, 7);
|
||||
+ assert.equal((await sends()).length, 8);
|
||||
+ await b.evaluate('window.__release(), true');
|
||||
+ await wait('document.querySelector("#send-section .receipt") && document.querySelector("#send-form [data-send]").getAttribute("aria-disabled") === "false"');
|
||||
+ assert.match(await receipt(), new RegExp(`^Sent to coder .*, message ${seeded.id.slice(0, 8)}\\. Its trail\\.$`));
|
||||
+ assert.equal(await value('#send-body'), 'A second thought');
|
||||
+ assert.equal((await sends()).length, 7);
|
||||
+ assert.equal((await sends()).length, 8);
|
||||
+
|
||||
+ // A plain success clears the text and keeps the role.
|
||||
+ await answer({ status: 200, body: { id: seeded.id, at: new Date().toISOString() } });
|
||||
+ await send();
|
||||
+ assert.deepEqual([await value('#send-to'), await value('#send-body')], ['coder', '']);
|
||||
+ assert.equal(JSON.parse((await sends())[7].body).body, 'A second thought');
|
||||
+ assert.equal(JSON.parse((await sends())[8].body).body, 'A second thought');
|
||||
+
|
||||
+ // The receipt links the message's trail, which the bus reads.
|
||||
+ assert.equal(await b.evaluate('document.querySelector("#send-section .receipt a").getAttribute("href")'), `#/trail/message/${seeded.id}`);
|
||||
@@ -747,10 +788,11 @@ index 00000000..9b57c57c
|
||||
+ assert.equal(await text('#bus-view h1'), `Trail for message ${seeded.id.slice(0, 8)}`);
|
||||
+ assert.match(await text('#bus-view'), /Seeded message from the human/);
|
||||
+ assert.equal(await b.evaluate('document.querySelector("#bus-view a[href=\'#/inbox\']")?.textContent'), 'Back to the Inbox');
|
||||
+ assert.equal(await b.evaluate('document.querySelector("#sections a[aria-current=page]").getAttribute("href")'), '#/inbox');
|
||||
+ await b.evaluate(`location.hash = '#/trail/message/no-such-message', true`);
|
||||
+ await wait('document.querySelector("#bus-view h1")?.textContent === "Not found"');
|
||||
+ assert.match(await text('#bus-view'), /No message with id no-such-message\./);
|
||||
+ assert.equal((await sends()).length, 8);
|
||||
+ assert.equal((await sends()).length, 9);
|
||||
+ } finally {
|
||||
+ await b.close(); await close(web); await f.close(); store.close();
|
||||
+ rmSync(root, { recursive: true, force: true });
|
||||
@@ -758,10 +800,10 @@ index 00000000..9b57c57c
|
||||
+});
|
||||
diff --git a/packages/webui/tests/send.test.mjs b/packages/webui/tests/send.test.mjs
|
||||
new file mode 100644
|
||||
index 00000000..706e8f43
|
||||
index 00000000..11cdb631
|
||||
--- /dev/null
|
||||
+++ b/packages/webui/tests/send.test.mjs
|
||||
@@ -0,0 +1,301 @@
|
||||
@@ -0,0 +1,305 @@
|
||||
+// Row 76 (#1570, lead decision 95): `serve --send`, a human message to a role from the Console.
|
||||
+// Only with the preview flags; POST /api/bus/message needs the header on this request, its own
|
||||
+// Origin, Sec-Fetch-Site same-origin, JSON, 16 KiB and only to, body and in_reply_to. The broker
|
||||
@@ -957,9 +999,10 @@ index 00000000..706e8f43
|
||||
+ assert.equal(reply.status, 200);
|
||||
+ // Values go to the broker as given, whatever their type: it is the one that refuses them.
|
||||
+ assert.equal((await post(port, { body: JSON.stringify({ to: 7, body: ['x'] }) })).status, 200);
|
||||
+ assert.deepEqual(sends, [{ to: 'coder', body: 'secret-body-text' }, { to: 'pm', body: 'yes', in_reply_to: 'msg-0000' }, { to: 7, body: ['x'] }]);
|
||||
+ assert.deepEqual(sends.map(Object.keys), [['to', 'body'], ['to', 'body', 'in_reply_to'], ['to', 'body']]);
|
||||
+ assert.equal(lines.length, 3);
|
||||
+ assert.equal((await post(port, { body: JSON.stringify({ to: 'pm', body: 'yes', in_reply_to: '' }) })).status, 200);
|
||||
+ assert.deepEqual(sends, [{ to: 'coder', body: 'secret-body-text' }, { to: 'pm', body: 'yes', in_reply_to: 'msg-0000' }, { to: 7, body: ['x'] }, { to: 'pm', body: 'yes', in_reply_to: '' }]);
|
||||
+ assert.deepEqual(sends.map(Object.keys), [['to', 'body'], ['to', 'body', 'in_reply_to'], ['to', 'body'], ['to', 'body', 'in_reply_to']]);
|
||||
+ assert.equal(lines.length, 4);
|
||||
+ assert.match(lines[0], /^\d{4}-\d\d-\d\dT[\d:.]+Z send user="jason" to="coder" id=msg-0001$/);
|
||||
+ assert.match(lines[2], / to=none id=msg-0001$/);
|
||||
+ assert.equal(lines.some(l => l.includes('secret-body-text')), false);
|
||||
@@ -974,7 +1017,10 @@ index 00000000..706e8f43
|
||||
+ assert.equal(lines.length, 1);
|
||||
+ assert.equal(lines[0].includes('\n'), false);
|
||||
+ assert.match(lines[0], / error=unknown-role$/);
|
||||
+ assert.ok(lines[0].length < 200);
|
||||
+ assert.ok(lines[0].includes(` to=${JSON.stringify(to.slice(0, 64))} error=`));
|
||||
+ // The cut is at 64 characters: a 65-character role loses its last one.
|
||||
+ await post(port, { body: JSON.stringify({ to: 'r'.repeat(64) + 's', body: 'hi' }) });
|
||||
+ assert.match(lines[1], new RegExp(` to="${'r'.repeat(64)}" error=unknown-role$`));
|
||||
+});
|
||||
+
|
||||
+test('a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried', async t => {
|
||||
@@ -991,7 +1037,7 @@ index 00000000..706e8f43
|
||||
+ [new BusReadError('outcome-unknown'), 503, 'outcome-unknown', 'unknown'],
|
||||
+ [new BusReadError('invalid-response'), 503, 'outcome-unknown', 'unknown'],
|
||||
+ [new BusReadError('response-too-large'), 503, 'outcome-unknown', 'unknown'],
|
||||
+ [new Error('ENOENT: /home/someone/.mosaic-dev/bus/broker.sock'), 503, 'outcome-unknown', 'unknown'],
|
||||
+ [Object.assign(new Error('ENOENT: /home/someone/.mosaic-dev/bus/broker.sock'), { code: 'ENOENT' }), 503, 'outcome-unknown', 'unknown'],
|
||||
+ [{ id: '' }, 503, 'outcome-unknown', 'unknown'],
|
||||
+ [null, 503, 'outcome-unknown', 'unknown'],
|
||||
+ [{ id: '../x' }, 503, 'outcome-unknown', 'unknown'],
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
c6cf1714407803d21a5757f39333a396c76abf582da0df51aeee4294e00cff06 packages/webui/README.md
|
||||
911feb19e9b16fa4f163d77ee10c33270049ea60d16cabcd4197cd8a8847eaf7 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
|
||||
8f627ccde5775281aa7676b8495d55bcb65a5f32f6a2bb53f1a313afb28ccd8f packages/webui/src/public/app.js
|
||||
78d75b88a89a2bbd83a264e8b8a5da739edb0b19dd7a8cfbbc0f18fd4895fceb packages/webui/src/public/bus.js
|
||||
8401bd8f650dadf07fadf4f1304d8f509f706441763d870e48fd14215c9a7a22 packages/webui/tests/send.test.mjs
|
||||
97304808bab8eb27fde756081863ff56e932f7021fc151165db72ac2a7a83c81 packages/webui/tests/send-browser.test.mjs
|
||||
17cb39097414bd685d68305dd5e402fd645020695f3dee86cd6bb567dd410eff packages/webui/tests/shell.test.mjs
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
== 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"}
|
||||
to coder 200 {"id":"9db1d4eb-2abd-4c13-a388-2ed3d9503c07","at":"2026-10-12T02:37:11.862Z"}
|
||||
to coder, in_reply_to the coder's message 200 {"id":"c4b38d2a-34df-4dea-b31b-897f7de01777","at":"2026-10-12T02:37:11.877Z"}
|
||||
== 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"}
|
||||
@@ -20,19 +20,19 @@ unknown, not retried 503 {"error":"outcome-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"
|
||||
9db1d4eb from=human/human-cli to=coder class=INFO in_reply_to=- body="Please look at row 76 before the cutover."
|
||||
c4b38d2a from=human/human-cli to=coder class=INFO in_reply_to=8c236a3d-c415-4252-818c-015cdd81d185 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
|
||||
2026-10-12T02:37:11.862Z send user="jason" to="coder" id=9db1d4eb-2abd-4c13-a388-2ed3d9503c07
|
||||
2026-10-12T02:37:11.877Z send user="jason" to="coder" id=c4b38d2a-34df-4dea-b31b-897f7de01777
|
||||
2026-10-12T02:37:11.881Z send user="jason" to=none error=cross-origin request refused
|
||||
2026-10-12T02:37:11.882Z send user="jason" to=none error=cross-site request refused
|
||||
2026-10-12T02:37:11.883Z send user="jason" to=none error=invalid-request
|
||||
2026-10-12T02:37:11.885Z send user="jason" to=none error=invalid-request
|
||||
2026-10-12T02:37:11.886Z send user="jason" to=none error=invalid-request
|
||||
2026-10-12T02:37:11.892Z send user="jason" to="auditor" error=unknown-role
|
||||
2026-10-12T02:37:11.899Z send user="jason" to="coder" error=invalid-request
|
||||
2026-10-12T02:37:11.905Z send user="jason" to="coder" error=message-not-found
|
||||
2026-10-12T02:37:11.913Z send user="jason" to="pm" error=outcome-unknown
|
||||
a body in the log: false
|
||||
|
||||
@@ -11,6 +11,7 @@ OUT = R + '/out/mut'
|
||||
SERVE = 'packages/webui/src/serve.mjs'
|
||||
BUS = 'packages/webui/src/bus.mjs'
|
||||
PAGE = 'packages/webui/src/public/bus.js'
|
||||
APP = 'packages/webui/src/public/app.js'
|
||||
TESTS = ['node', '--test', 'packages/webui/tests/*.test.mjs']
|
||||
|
||||
MUTANTS = [
|
||||
@@ -36,6 +37,20 @@ MUTANTS = [
|
||||
('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(")]),
|
||||
('reply-4xx-unknown', APP, [("const refused = err.status >= 400 && err.status < 500 && err.status !== 408;", "const refused = false;")]),
|
||||
('reply-5xx-refused', APP, [("const refused = err.status >= 400 && err.status < 500 && err.status !== 408;", "const refused = err.status >= 400;")]),
|
||||
('page-no-caret', PAGE, [("el.setSelectionRange(k.caret[0], k.caret[1]); ", "")]),
|
||||
('page-caret-not-kept', PAGE, [("caret: view.contains(a) && a.tagName === 'TEXTAREA' ? [a.selectionStart, a.selectionEnd, a.scrollTop] : null", "caret: null")]),
|
||||
('page-code-twice', PAGE, [("${r.message.includes(`(${r.code})`) ? '' : ` (<code>", "${false ? '' : ` (<code>")]),
|
||||
('page-keeps-not-sent-prefix', PAGE, [(".replace(/^not sent: /, '')", "")]),
|
||||
('page-408-refused', PAGE, [(" && res.status !== 408)", ")")]),
|
||||
('reply-408-refused', APP, [(" && err.status !== 408;", ";")]),
|
||||
('page-no-field-focus', PAGE, [(" el.focus({ preventScroll: true });\n if (k.caret", " if (k.caret")]),
|
||||
('page-not-held-dropped', PAGE, [("const roles = sendRoles ? [...new Set([...sendRoles, ...(draft.to && !sendRoles.includes(draft.to) ? [draft.to] : [])])] : null;", "const roles = sendRoles ? [...sendRoles] : null;")]),
|
||||
('page-inbox-not-current', PAGE, [(" || route.startsWith('/trail/message')", "")]),
|
||||
('send-reply-truthy', SERVE, [(" if ('in_reply_to' in value) args.in_reply_to", " if (value.in_reply_to) args.in_reply_to")]),
|
||||
('outcome-plain-error-code', BUS, [("const code = err instanceof BusReadError ? err.code : 'outcome-unknown';", "const code = err?.code ?? 'outcome-unknown';")]),
|
||||
('send-log-cut-65', SERVE, [("JSON.stringify(to.slice(0, 64))", "JSON.stringify(to.slice(0, 65))")]),
|
||||
]
|
||||
|
||||
def run(cmd):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
2026-10-12T01:54:19Z
|
||||
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']
|
||||
@@ -13,12 +14,26 @@ route-open-without-send killed: ['without --send the route is read-only like eve
|
||||
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']
|
||||
page-no-pending-guard killed: ['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']
|
||||
page-clears-edited-text killed: ['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']
|
||||
page-5xx-refusal-unknown killed: ['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']
|
||||
page-retries-unknown killed: ['shipped strings are unchanged, and the web keeps its write boundary', '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']
|
||||
page-redirect-unknown killed: ['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']
|
||||
page-follows-redirect killed: ['shipped strings are unchanged, and the web keeps its write boundary', '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']
|
||||
page-trail-any-id killed: ['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']
|
||||
reply-4xx-unknown killed: ['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']
|
||||
reply-5xx-refused killed: ['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']
|
||||
page-no-caret killed: ['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']
|
||||
page-caret-not-kept killed: ['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']
|
||||
page-code-twice killed: ['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']
|
||||
page-keeps-not-sent-prefix killed: ['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']
|
||||
page-408-refused killed: ['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']
|
||||
reply-408-refused killed: ['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']
|
||||
page-no-field-focus killed: ['Queue, Business and Settings in their five states, with no move button', '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']
|
||||
page-not-held-dropped killed: ['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']
|
||||
page-inbox-not-current killed: ['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']
|
||||
send-reply-truthy killed: ['a send passes exactly to, body and in_reply_to, answers the id, and logs it']
|
||||
outcome-plain-error-code killed: ['a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried']
|
||||
send-log-cut-65 killed: ['a crafted role name cannot forge a log line']
|
||||
worktree unchanged after: True
|
||||
summary 21 of 21 killed
|
||||
summary 35 of 35 killed
|
||||
|
||||
Reference in New Issue
Block a user