Files
boxmaker/docs/plans/M4b/12-gatewayd-state-approvals.md
kyleandClaude Opus 5.5 2d415fc1be Plan M4b: approvals over Mattermost in 15 tasks
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>
2026-09-24 03:34:21 -07:00

3.1 KiB

M4b task 12: approval posts in the state file

Branch: m4b (run git switch m4b; git status --short must be empty, otherwise stop) Commit subject: gatewayd: state, the posts of open approvals, and the turn of a session

Goal

Two things the serve loop needs from state.json (M4b spec, sections 6 and 8):

  • Approval posts. The posts of each approval shown and not yet ended, so that at the next start the ones left behind can be marked dead. The field is optional: an M4a state file loads unchanged. Like every other field, its ids must be valid, or the file is refused.
  • The turn of a session: where an approval of that session goes (its thread).

Files

  • Copy: crates/gatewayd/tests/state_approvals.rs
  • Modify: crates/gatewayd/src/state.rs, docs/implementer-log.md

The changes, exactly

In crates/gatewayd/src/state.rs:

1. In struct StateFile, after in_flight: Vec<InFlight>,:

    /// Optional, so an M4a state file loads unchanged.
    #[serde(default)]
    approvals: Vec<ApprovalPosts>,

and after struct StateFile:

/// The posts of an approval shown in Mattermost and not yet ended (M4b spec, section 8).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApprovalPosts {
    pub approval: u64,
    pub channel: String,
    /// First part first; the last is the one that is edited when it ends.
    pub posts: Vec<String>,
}

2. In StateFile::problem, after the in_flight loop and before None: each entry of approvals must have a valid_id channel and at least one post, all valid_id; otherwise Some(format!("approval {}'s posts are not valid", shown.approval)).

3. Four methods at the end of impl State, each saving (self.save()) when it changes something, like the methods already there:

  • pub fn turn_of(&self, session: &str) -> Option<InFlight>: the entry of in_flight with that session, cloned.
  • pub fn add_approval(&mut self, shown: ApprovalPosts) -> Result<(), StateError>: remove any entry with the same approval, push this one, save.
  • pub fn remove_approval(&mut self, approval: u64) -> Result<(), StateError>: remove the entries with that approval; save only if one was removed.
  • pub fn take_approvals(&mut self) -> Result<Vec<ApprovalPosts>, StateError>: take them all out (std::mem::take); save only when there were some; return them. (As take_in_flight does.)

Steps

  • 1. Copy. git switch m4b, then cp docs/plans/M4b/files/crates/gatewayd/tests/state_approvals.rs crates/gatewayd/tests/
  • 2. See it fail. cargo test -p gatewayd --test state_approvals. Expected: it does not compile.
  • 3. Make the three changes. Run cargo fmt --all.
  • 4. See it pass. cargo test -p gatewayd --test state_approvals --test state --test serve_restart. Expected: 4, 4 and 7 passed.
  • 5. Run the gate. 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

  • The three suites pass; make gate prints gate: ok.