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
+95
View File
@@ -0,0 +1,95 @@
# M4a task 03: `gatewayd.toml`
**Branch:** `m4a` (run `git switch m4a`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `gatewayd: config, gatewayd.toml into a checked Config`
## Goal
`gatewayd.toml` says where Mattermost is, where the token comes from, who may talk to Boxmaker and
in which channels, and a few limits. It is **our** format: an unknown key anywhere is an error, and
a value that is wrong is refused at start with a message that names it. Spec section 3.
```toml
[mattermost]
url = "https://straylight.scylla-hammerhead.ts.net" # https or http; no path
ca_file = "/etc/boxmaker/extra-ca.pem" # optional; added to the system's roots
[secrets.mattermost_token]
credential = "mattermost-token" # exactly one of: credential, env, file
[allow]
users = ["abcdefghijklmnopqrstuvwxyz"] # Mattermost user ids (26 characters)
channels = [] # channels and group messages allowed, by id; default none
[loop]
socket = "" # empty: <home>/run/loop/loop.sock
[paths]
home = "/var/lib/boxmaker" # default: BOXMAKER_HOME, then /var/lib/boxmaker
[limits]
queue = 20
typing_every_ms = 3000
ping_every_ms = 30000
dead_after_ms = 60000
```
## Files
- Copy: `crates/gatewayd/tests/config.rs`, `crates/gatewayd/tests/support/tmp.rs`, and the
skeleton `crates/gatewayd/src/config.rs`
- Modify: `crates/gatewayd/src/lib.rs` (`pub mod config;`), `docs/implementer-log.md`
## The skeleton
Every type is written, with its serde attributes: `Config`, `MattermostConfig`, `SecretSpec`,
`SecretSource`, `AllowConfig`, `LoopConfig`, `Paths`, `Limits` (with their defaults), `ServerUrl`,
`ConfigError`, and the constant `MATTERMOST_TOKEN = "mattermost_token"`. Do not change them. The
functions are `todo!()`, each with its steps above it.
## The messages, exactly
`Config::problem` returns the first problem, in this order:
| Problem | Message |
|---|---|
| `url` fails `parse_url` | `parse_url`'s message: `[mattermost] url "<url>" must be http:// or https://, a host, an optional port, and nothing else` (the url with `{:?}`) |
| `ca_file` not absolute | `[mattermost] ca_file "<path>" must be an absolute path` (`{:?}`) |
| no `[secrets.mattermost_token]` | `[secrets.mattermost_token] is missing` |
| a secret whose `source()` fails | `[secrets.<name>] <source's message>` |
| `allow.users` empty | `[allow] users is empty: a gateway that answers nobody is a mistake` |
| an id in `allow.users` or `allow.channels` that is not `valid_id` | `[allow] "<id>" is not a Mattermost id (26 characters of a-z and 0-9)` (`{:?}`) |
| a limit that is 0 (checked in the order queue, typing_every_ms, ping_every_ms, dead_after_ms) | `[limits] <name> must be at least 1` |
`SecretSpec::source`'s messages:
| Case | Message |
|---|---|
| not exactly one of the three | `needs exactly one of credential, env and file` |
| `credential` empty or with a byte that is not an ASCII letter, digit, `_`, `.` or `-` | `credential "<name>" is not a credential name (letters, digits, _ . -)` |
| `env` empty or with a byte that is not `A-Z`, `0-9` or `_` | `env "<var>" is not a variable name (A-Z, 0-9, _)` |
| `file` not absolute | `file "<path>" must be an absolute path` |
`Config::token_source` gives `source()` of `mattermost_token`, or `[secrets.mattermost_token] is
missing`. `ConfigError`'s `Display` is `<path>: <error or message>`.
## Steps
- [ ] **1. Copy.** `git switch m4a`, then
`mkdir -p crates/gatewayd/tests/support && cp docs/plans/M4a/files/crates/gatewayd/tests/config.rs crates/gatewayd/tests/ && cp docs/plans/M4a/files/crates/gatewayd/tests/support/tmp.rs crates/gatewayd/tests/support/ && cp docs/plans/M4a/files/crates/gatewayd/src/config.rs crates/gatewayd/src/`.
Add `pub mod config;` to `crates/gatewayd/src/lib.rs`.
- [ ] **2. See it fail.** `cargo test -p gatewayd --test config`. Expected: it compiles and 7 tests
fail with `not yet implemented`.
- [ ] **3. Fill the functions one at a time**: `valid_id`, `parse_url`, `SecretSpec::source`,
`ConfigError`'s `fmt`, then the `Config` methods. `cargo check -p gatewayd` after each.
- [ ] **4. See it pass.** `cargo test -p gatewayd --test config`. Expected: 7 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 config` passes; `make gate` prints `gate: ok`.
## Stop and report if
- A test wants a type changed. The types are the format; report instead.