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
+70
View File
@@ -0,0 +1,70 @@
# M4a task 10: Mattermost's events and REST calls
**Branch:** `m4a` (run `git switch m4a`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `gatewayd: mm, Mattermost's events and the four REST calls, typed`
## Goal
Mattermost's JSON, typed, and the REST calls `gatewayd` makes. This is **another program's
format**: unknown fields are ignored. But every id we keep ends up in a session id or a URL path,
so every id must be a valid Mattermost id (26 characters of `a-z0-9`), or the whole post is refused.
Facts from Mattermost's source at the owner's server version, v11.11.0 (checked by the design
model; the tests use these shapes):
- A WebSocket event is `{"event", "data", "broadcast", "seq"}`. The first is `hello`. `posted`
carries the post as a **JSON string** in `data.post` (a string holding JSON, not an object), and
the channel's type in `data.channel_type`: `D` direct, `G` group, `O` open, `P` private. A reply
to one of our requests has no `event`.
- `user_typing` is a request we send: `{"action": "user_typing", "seq": <n>, "data":
{"channel_id", "parent_id"}}`; it shows the bot as typing in that thread.
- `GET /api/v4/channels/{id}/posts?since=<ms>` returns `{"order": [ids], "posts": {id: post}}`
with the posts **changed** after that time. Only the ids in `order` changed; `posts` also holds
the roots of their threads, which may be old. Edited and deleted posts come back too. The server
takes at most 1000 (`SINCE_LIMIT`); a full answer may have left some out.
- REST errors: 401 or 403 is a refused token, 429 is a rate limit (wait for `X-Ratelimit-Reset`),
5xx is worth trying again.
## Files
- Copy: `crates/gatewayd/tests/mm_json.rs`, `crates/gatewayd/tests/mm_rest.rs`,
`crates/gatewayd/tests/support/http_server.rs`, and the skeletons
`crates/gatewayd/src/mm/mod.rs` and `crates/gatewayd/src/mm/rest.rs`
- Modify: `crates/gatewayd/src/lib.rs` (`pub mod mm;`), `docs/implementer-log.md`
## The skeletons
`mm/mod.rs`, written: `SINCE_LIMIT`; `MmError` (`Net`, `Auth(u16)`, `RateLimited(Duration)`,
`Status(u16, String)`, `Json`) and its `Display`, which **quotes** a server's body (`{:?}`) so that
a newline in it cannot forge a log line; `Me { id, username }`; `Post { id, user_id, channel_id,
root_id, message, create_at, delete_at, kind }` (`kind` is the JSON `type`); `Event` (`Hello`,
`Posted { post, channel_type }`, `Other(String)`); `Since { posts, full }`; and the private serde
shapes `RawEvent` and `PostList`. To fill: `Post::check`, `json`, `parse_event`, `typing`,
`since_list`.
`mm/rest.rs`, written: `RETRY_5XX` (500 ms), `RETRIES` (2), `BODY_KEPT` (200), `Client`. To fill:
`new`, `connector`, `token`, `once`, `call`, `me`, `create_post`, `posts_since`, `direct_channel`.
The token reaches the wire only in `once`, as `Authorization: Bearer <token>`; nowhere else calls
`expose()`.
## Steps
- [ ] **1. Copy.** `git switch m4a`, then
`mkdir -p crates/gatewayd/src/mm && cp docs/plans/M4a/files/crates/gatewayd/src/mm/mod.rs docs/plans/M4a/files/crates/gatewayd/src/mm/rest.rs crates/gatewayd/src/mm/ && cp docs/plans/M4a/files/crates/gatewayd/tests/mm_json.rs docs/plans/M4a/files/crates/gatewayd/tests/mm_rest.rs crates/gatewayd/tests/ && cp docs/plans/M4a/files/crates/gatewayd/tests/support/http_server.rs crates/gatewayd/tests/support/`.
Add `pub mod mm;` to `lib.rs`.
- [ ] **2. See it fail.** `cargo test -p gatewayd --no-fail-fast --test mm_json --test mm_rest`.
Expected: it compiles; `mm_json` 6 fail and 1 passes (the `Display` test), `mm_rest` 10 fail.
- [ ] **3. Fill `mm/mod.rs`, then `mm/rest.rs`**, one function at a time, `cargo check -p gatewayd`
after each.
- [ ] **4. See it pass.** `cargo test -p gatewayd --test mm_json --test mm_rest`. Expected: 7 and
10 passed. `mm_rest` takes about 3 s: two tests wait out a rate limit on purpose.
- [ ] **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
- Both suites pass; `make gate` prints `gate: ok`.
## Stop and report if
- A test needs a post with an invalid id to be accepted.