Plan M4a: gatewayd in 15 tasks, with skeletons and given tests

Each task's tests were run against a reference at its end state; the end states were replayed
from master in order with the gate at each step (650 to 762 tests); each skeleton compiles
against its tests and fails them. The reference is kept off this machine. Lessons T27 (every
wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 19:05:44 -07:00
co-authored by Claude Opus 5.5
parent f38dc8d474
commit 0339dc13b2
69 changed files with 7790 additions and 3 deletions
+61
View File
@@ -0,0 +1,61 @@
# M4a task 13: a turn on `loop.sock`, and its answer in the thread
**Branch:** `m4a` (run `git switch m4a`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `gatewayd: deliver, turns on loop.sock and answers posted in their thread`
## Goal
A batch of messages (task 11) becomes one `turn` on `loopd`'s socket, exactly as `bxctl chat` sends
one (read `crates/bxctl/src/chat.rs`, `run_turn`): one frame `{"kind": "turn", "body": {"session",
"content", "resume"}}` with id 1, then events until a final `turn_done` or `error`. What comes back
is posted in the thread (spec section 8):
- No thinking and no status reach Mattermost. The one event that is shown: `approval_pending`,
posted once as `waiting for approval <id>: approve or deny it with `bxctl` (Mattermost approvals
arrive in M4b)` (the skeleton's `approval_text`).
- `turn_done` → its content, split into posts of at most 16,000 **characters** (a Mattermost post
holds 16,383), at the last newline before the limit, or at the limit when there is none, in
order. An empty answer is posted as `(the answer was empty)`: Mattermost refuses an empty post.
- An `error` frame → `Error: <code>: <detail>`, where the code is the snake_case name
(`no_such_session`), and the detail carries `loopd`'s runbook pointer when it has one.
- `loop.sock` cannot be reached, or closes early, or sends a frame that does not belong →
`LOOP_DOWN`, and the messages are not sent again: `loopd` may have run them.
- A reply in a thread `loopd` does not know (`no_such_session` with `resume: true`) is sent once
more with `resume: false`, creating the session, as `bxctl chat --session` does. Only then.
## Files
- Copy: `crates/gatewayd/tests/deliver.rs`, `crates/gatewayd/tests/support/fake_loop.rs`, and the
skeleton `crates/gatewayd/src/deliver.rs`
- Modify: `crates/gatewayd/src/lib.rs` (`pub mod deliver;`), `docs/implementer-log.md`
## The skeleton
Written: `MAX_POST` (16,000), `LOOP_DOWN`, `EMPTY_ANSWER`, the trait `Poster` (somewhere to post:
Mattermost's `Client`, or a test's record) and its `impl` for `Client`, `Outcome` (`Answer`,
`Refused`, `LoopDown(why)`), `approval_text`.
To fill: `error_text`, `split_answer`, `one_turn`, `run_turn`, `deliver`. `split_answer` counts
**characters**, not bytes, and never cuts inside one: find the byte index of the 16,000th character
with `char_indices().nth(MAX_POST)`.
## Steps
- [ ] **1. Copy.** `git switch m4a`, then
`cp docs/plans/M4a/files/crates/gatewayd/tests/deliver.rs crates/gatewayd/tests/ && cp docs/plans/M4a/files/crates/gatewayd/tests/support/fake_loop.rs crates/gatewayd/tests/support/ && cp docs/plans/M4a/files/crates/gatewayd/src/deliver.rs crates/gatewayd/src/`.
Add `pub mod deliver;` to `lib.rs`.
- [ ] **2. See it fail.** `cargo test -p gatewayd --test deliver`. Expected: it compiles and 9
tests fail.
- [ ] **3. Fill `error_text`, `split_answer`, `one_turn`, `run_turn`, `deliver`**,
`cargo check -p gatewayd` after each.
- [ ] **4. See it pass.** `cargo test -p gatewayd --test deliver`. Expected: 9 passed.
- [ ] **5. Run the gate.** `cargo fmt --all`, then `make gate`. Expected last line: `gate: ok`.
- [ ] **6. Log and commit.** `git add crates/gatewayd docs/implementer-log.md Cargo.lock && git commit`
## Done when
- `cargo test -p gatewayd --test deliver` passes; `make gate` prints `gate: ok`.
## Stop and report if
- A test seems to need a turn sent twice in any case other than the one above.