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:
@@ -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.
|
||||
Reference in New Issue
Block a user