Skeletons for new files only; existing files change by exact edits, so no task overwrites an earlier one's work. Each task's tests were run against a reference built in order from master, with the gate at each step and mutations checked; each skeleton compiles and fails its tests, and every todo!() comment was read beside its signature. The reference is kept off this machine. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
153 lines
6.8 KiB
Markdown
153 lines
6.8 KiB
Markdown
# M4b task 15: commands answer approvals; typing pauses; the notice
|
|
|
|
**Branch:** `m4b` (run `git switch m4b`; `git status --short` must be empty, otherwise stop)
|
|
**Commit subject:** `gatewayd: approvals answered from Mattermost; typing paused; the notice`
|
|
|
|
## Goal
|
|
|
|
The last three pieces (M4b spec, section 7):
|
|
|
|
1. **Commands.** `Route::Approval` (task 08) goes to the registry's `judge` (task 11): an accepted
|
|
answer goes to `brokerd`; anything else is answered in the thread with its reason. M4a's notice
|
|
`M4B_COMMAND` goes away.
|
|
2. **Typing.** A turn waiting for its approval is not working: no `user_typing` for its thread
|
|
while the registry holds an approval of its session.
|
|
3. **The notice.** On `approval_pending`, the turn waits up to 10 s for `brokerd` to bring that
|
|
approval here (`Registry::was_posted`); only if it never does, it posts `waiting for approval
|
|
<id>: brokerd has not sent it here; answer it with bxctl approvals`.
|
|
|
|
## Files
|
|
|
|
- Copy: `crates/gatewayd/tests/serve_answers.rs`, `crates/gatewayd/tests/deliver_notice.rs`, and
|
|
**new versions** of three given files: `crates/gatewayd/tests/deliver.rs` (passes a `Notice`;
|
|
the new notice text), `crates/gatewayd/tests/serve.rs` (`!approve 3` is now "approval 3 is not
|
|
pending here"), `crates/gatewayd/tests/support/gateway.rs` (`Tuning` has `notice`)
|
|
- Modify: `crates/gatewayd/src/approvals/mod.rs`, `crates/gatewayd/src/deliver.rs`,
|
|
`crates/gatewayd/src/serve/mod.rs`, `crates/gatewayd/src/serve/handle.rs`,
|
|
`crates/gatewayd/src/sessions.rs`, `docs/implementer-log.md`
|
|
|
|
## The changes, exactly
|
|
|
|
**`approvals/mod.rs`**: every approval ever posted, for the notice. `Registry` gains a field
|
|
`ever: Mutex<HashSet<u64>>` (doc comment: "Every approval ever posted here, answered or not: for
|
|
the notice."; import `HashSet` beside `HashMap`); `add` also inserts `posted.approval` into it
|
|
(`self.ever.lock().unwrap_or_else(|p| p.into_inner()).insert(…)`, before inserting into `posted`);
|
|
and a new method:
|
|
|
|
```rust
|
|
/// Was approval `id` ever posted here?
|
|
pub fn was_posted(&self, id: u64) -> bool {
|
|
self.ever
|
|
.lock()
|
|
.unwrap_or_else(|p| p.into_inner())
|
|
.contains(&id)
|
|
}
|
|
```
|
|
|
|
**`deliver.rs`**:
|
|
- `approval_text` returns
|
|
`format!("waiting for approval {approval}: brokerd has not sent it here; answer it with bxctl approvals")`.
|
|
- After it, and `use std::time::{Duration, Instant};` at the top:
|
|
|
|
```rust
|
|
/// How long a turn waits for `brokerd` to bring its approval here before posting the notice.
|
|
pub const NOTICE_WAIT: Duration = Duration::from_secs(10);
|
|
|
|
/// Whether an approval was brought here (and so is posted already), and how long to wait for it.
|
|
pub struct Notice<'a> {
|
|
pub posted: &'a dyn Fn(u64) -> bool,
|
|
pub wait: Duration,
|
|
}
|
|
```
|
|
|
|
and write `impl Notice<'_> { fn arrives(&self, id: u64) -> bool }`: loop: `(self.posted)(id)`
|
|
→ true; past `Instant::now() + self.wait` (taken once, before the loop) → false; otherwise sleep
|
|
`Duration::from_millis(50).min(<the time left>)` (`saturating_duration_since`).
|
|
- `deliver` gains a last parameter `notice: &Notice`, and its `ApprovalPending` branch posts the
|
|
notice only when it does not arrive:
|
|
|
|
```rust
|
|
// The approval post itself comes through `gateway.sock`; the notice only when it does not.
|
|
if let TurnEvent::ApprovalPending { approval, .. } = event
|
|
&& !notice.arrives(*approval)
|
|
{
|
|
post(poster, &batch.thread, approval_text(*approval), log);
|
|
}
|
|
```
|
|
|
|
**`serve/mod.rs`**: `Tuning` gains `pub notice: Duration,` (doc: "How long a turn waits for its
|
|
approval to come through `gateway.sock` before the notice."), `crate::deliver::NOTICE_WAIT` in its
|
|
`Default`; `Gateway` gains a field `notice: Duration` (doc "`Tuning::notice`."), set by a new last
|
|
parameter of `Gateway::new`, which `run` passes as `tuning.notice`.
|
|
|
|
**`serve/handle.rs`**:
|
|
- The `Route::Approval` arm becomes:
|
|
|
|
```rust
|
|
Route::Approval {
|
|
thread,
|
|
command,
|
|
user,
|
|
post: command_post,
|
|
at,
|
|
} => {
|
|
let approvers = self.config.approvers();
|
|
let judged = self.registry.judge(
|
|
&command,
|
|
&user,
|
|
&thread.root,
|
|
&command_post,
|
|
at,
|
|
approvers,
|
|
);
|
|
if let Judged::Reply(text) = judged {
|
|
self.post(&thread.channel, &thread.root, &text);
|
|
}
|
|
}
|
|
```
|
|
|
|
- In `start`, the thread gets the registry and the wait, and passes a `Notice`:
|
|
|
|
```rust
|
|
let (registry, wait) = (Arc::clone(&self.registry), self.notice);
|
|
let spawned = std::thread::Builder::new().spawn(move || {
|
|
let posted = |id: u64| registry.was_posted(id);
|
|
let notice = Notice {
|
|
posted: &posted,
|
|
wait,
|
|
};
|
|
deliver(client.as_ref(), &socket, &batch, &|line| log(line), ¬ice);
|
|
let _ = done.send(batch.session);
|
|
});
|
|
```
|
|
|
|
- In `typing`, before the loop: `let waiting = self.registry.waiting_sessions();`, and at the top
|
|
of the loop body: `if waiting.contains(&format!("mm-{}", thread.root)) { continue; }` (with the
|
|
comment "A turn waiting for its approval is not working: no typing in its thread.").
|
|
- Imports: `use crate::approvals::Judged;`; `Notice` beside `deliver`; `M4B_COMMAND` removed.
|
|
|
|
**`sessions.rs`**: delete `pub const M4B_COMMAND`.
|
|
|
|
## Steps
|
|
|
|
- [ ] **1. Copy.** `git switch m4b`, then
|
|
`cp docs/plans/M4b/files/crates/gatewayd/tests/serve_answers.rs docs/plans/M4b/files/crates/gatewayd/tests/deliver_notice.rs docs/plans/M4b/files/crates/gatewayd/tests/deliver.rs docs/plans/M4b/files/crates/gatewayd/tests/serve.rs crates/gatewayd/tests/ && cp docs/plans/M4b/files/crates/gatewayd/tests/support/gateway.rs crates/gatewayd/tests/support/`
|
|
- [ ] **2. See it fail.** `cargo test -p gatewayd --test serve_answers --test deliver_notice`.
|
|
Expected: they do not compile (`deliver::Notice` and `Tuning::notice` do not exist yet).
|
|
- [ ] **3. Make the changes**, one file at a time, `cargo check -p gatewayd` after each. Run
|
|
`cargo fmt --all`.
|
|
- [ ] **4. See it pass.** `cargo test -p gatewayd --test serve_answers --test deliver_notice --test deliver --test serve`,
|
|
three times: 4, 4, 9 and 6 passed. Then `cargo test -p gatewayd`: every suite passes.
|
|
- [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`, with about 845 tests.
|
|
- [ ] **6. Log and commit.** `git add crates/gatewayd docs/implementer-log.md Cargo.lock && git commit`
|
|
|
|
This is the last task of M4b. Stop after the commit.
|
|
|
|
## Done when
|
|
|
|
- `cargo test -p gatewayd` passes; `make gate` prints `gate: ok`.
|
|
|
|
## Stop and report if
|
|
|
|
- A test passes only sometimes, or waits much longer than stated.
|