# M4b task 17: the texts of a redirected approval **Branch:** `m4b` (run `git switch m4b`; `git status --short` must be empty, otherwise stop) **Commit subject:** `gatewayd: the texts that say who asked for a redirected approval` ## Goal Texts only; task 18 posts them. When someone who may talk to the bot but not approve asks for something in a direct or group message, no approver can answer the approval there, so it goes to the first approver's direct messages (M4b spec, section 6, as changed after the review of 2026-09-24). The approver must see who asked, where, and what they asked. Three texts: 1. `asked_post`: the first post of a redirected approval. The asker's message is escaped with `proto::escape::escape_model_text` and fenced (`fence`, already in this file). It is context, not what is approved, so only its first `PROMPT_SHOWN` (2,000) characters are shown, then a line saying how many more there were. For example: ````text **Approval 3** was asked by @alex in a direct message with the bot. Their message: ``` Read the file /srv/notes/q3.txt and summarise it. ``` ```` 2. `unfollowed_post`: the first post of an approval whose session `gatewayd` does not follow (`bxctl chat`, or a turn from before a restart). 3. `sent_to_approver`: the line left in the asking thread. ## Files - Copy: `crates/gatewayd/tests/approvals_asked.rs` - Modify: `crates/gatewayd/src/approvals/text.rs` (append only), `docs/implementer-log.md` ## The change, exactly Append this to the end of `crates/gatewayd/src/approvals/text.rs`, after `with_ending`, exactly as it is. Nothing else in the file changes, and there are no new imports: `proto` is already a dependency, and `code` and `fence` are in this file. ```rust /// The most characters of an asker's message shown to the approver: it is context, not what is /// approved, so it may be cut. pub const PROMPT_SHOWN: usize = 2_000; /// Who asked for an approval that is shown elsewhere (M4b spec, section 6). #[derive(Debug, Clone, PartialEq, Eq)] pub struct Asked { /// `@username`, or the user id when the name could not be found. pub by: String, /// The channel's type: `D` a direct message, `G` a group message. pub channel_type: String, /// The text of the turn that asked. pub prompt: String, } /// The first post of a redirected approval: who asked, where, and their message. pub fn asked_post(id: u64, asked: &Asked) -> String { let place = if asked.channel_type == "G" { "a group message" } else { "a direct message" }; let escaped = proto::escape::escape_model_text(&asked.prompt); let total = escaped.chars().count(); let shown: String = escaped.chars().take(PROMPT_SHOWN).collect(); let mut post = format!( "**Approval {id}** was asked by {} in {place} with the bot. Their message:\n{}", asked.by, fence(&shown) ); if total > PROMPT_SHOWN { post.push_str(&format!("\n(… {} more characters)", total - PROMPT_SHOWN)); } post } /// The first post of an approval whose session `gatewayd` does not follow. pub fn unfollowed_post(id: u64, session: &str) -> String { format!( "**Approval {id}** was asked in session {}, which gatewayd does not follow now (a restart, or a session from outside Mattermost).", code(session) ) } /// The line left in the asking thread when its approval goes to an approver. pub fn sent_to_approver(id: u64) -> String { format!("waiting for approval {id}: it was sent to an approver") } ``` Characters are counted with `chars()`, never `len()`: `len()` counts bytes, and a message in any language but English would be cut short. ## Steps - [ ] **1. Copy.** `git switch m4b`, then `cp docs/plans/M4b/files/crates/gatewayd/tests/approvals_asked.rs crates/gatewayd/tests/` - [ ] **2. See it fail.** `cargo test -p gatewayd --test approvals_asked`. Expected: it does not compile (`Asked` and the three functions do not exist yet). - [ ] **3. Make the change.** Run `cargo fmt --all`. - [ ] **4. See it pass.** `cargo test -p gatewayd --test approvals_asked`: 4 passed. Then `cargo test -p gatewayd`: every suite passes. - [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`, with 4 more tests than before this task (about 850). - [ ] **6. Log and commit.** `git add crates/gatewayd docs/implementer-log.md Cargo.lock && git commit` ## Done when - `cargo test -p gatewayd` passes; `make gate` prints `gate: ok`. ## Stop and report if - A test in `approvals_asked.rs` still fails with the code above appended exactly. Do not change the test or the texts to make it pass.