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,96 @@
|
||||
//! One turn on `loop.sock` and its answer in the thread (M4a spec, section 8). Typing is shown by
|
||||
//! `serve`, which owns the WebSocket; this module only sends the turn and posts what comes back.
|
||||
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::Path;
|
||||
|
||||
use proto::{
|
||||
Envelope, ErrorCode, Message, PROTOCOL_VERSION, Turn, TurnEvent, WireError, read_frame,
|
||||
write_frame,
|
||||
};
|
||||
|
||||
use crate::mm::MmError;
|
||||
use crate::mm::rest::Client;
|
||||
use crate::sessions::Batch;
|
||||
|
||||
/// The longest reply we post, in characters (a post holds at most 16,383).
|
||||
pub const MAX_POST: usize = 16_000;
|
||||
pub const LOOP_DOWN: &str = "Boxmaker's loop is not running (see docs/runbook.md#loop-unavailable)";
|
||||
pub const EMPTY_ANSWER: &str = "(the answer was empty)";
|
||||
|
||||
/// Somewhere to post: Mattermost, or a test's record.
|
||||
pub trait Poster: Send + Sync {
|
||||
fn post(&self, channel: &str, root: &str, text: &str) -> Result<(), MmError>;
|
||||
}
|
||||
|
||||
impl Poster for Client {
|
||||
fn post(&self, channel: &str, root: &str, text: &str) -> Result<(), MmError> {
|
||||
self.create_post(channel, root, text).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
/// How a turn ended.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Outcome {
|
||||
Answer(String),
|
||||
Refused(WireError),
|
||||
/// `loop.sock` could not be reached or closed early; why, for the log.
|
||||
LoopDown(String),
|
||||
}
|
||||
|
||||
pub fn approval_text(approval: u64) -> String {
|
||||
format!(
|
||||
"waiting for approval {approval}: approve or deny it with `bxctl` (Mattermost approvals arrive in M4b)"
|
||||
)
|
||||
}
|
||||
|
||||
/// "Error: <code>: <detail>"; the detail carries `loopd`'s runbook pointer when there is one.
|
||||
pub fn error_text(error: &WireError) -> String {
|
||||
// "Error: <code>: <detail>", where <code> is the snake_case name serde gives the ErrorCode
|
||||
// (`serde_json::to_value(code)` is a JSON string, e.g. "no_such_session").
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// An answer in posts of at most `MAX_POST` characters: each cut at the last newline before the
|
||||
/// limit (the newline is dropped), or at the limit when there is none.
|
||||
pub fn split_answer(text: &str) -> Vec<String> {
|
||||
// Blank (only whitespace) -> [EMPTY_ANSWER]. Otherwise, while the rest is longer than MAX_POST
|
||||
// *characters*: take the first MAX_POST characters; if they hold a newline past position 0, cut
|
||||
// at the last one and drop that newline; else cut at MAX_POST characters. The last part is the
|
||||
// rest.
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Send one turn and read it to its end; `on_event` sees every event.
|
||||
fn one_turn(
|
||||
socket: &Path,
|
||||
batch: &Batch,
|
||||
resume: bool,
|
||||
on_event: &mut dyn FnMut(&TurnEvent),
|
||||
) -> Outcome {
|
||||
// 1. Connect to the socket (else LoopDown("cannot connect to <path>: <e>")).
|
||||
// 2. `write_frame` one Envelope: v PROTOCOL_VERSION, id 1, final true, msg Turn (else
|
||||
// LoopDown).
|
||||
// 3. `read_frame` until the end: (id 1, not final, TurnEvent) -> on_event; (1, final, TurnDone)
|
||||
// -> Answer with its content; (1, final, Error) -> Refused; a read error -> LoopDown("the
|
||||
// turn ended early: <e>"); anything else -> LoopDown("an unexpected frame").
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// A turn for a batch. A reply in a thread `loopd` does not know creates the session, as
|
||||
/// `bxctl chat --session` does.
|
||||
pub fn run_turn(socket: &Path, batch: &Batch, on_event: &mut dyn FnMut(&TurnEvent)) -> Outcome {
|
||||
// `one_turn` with batch.resume. When that is Refused with NoSuchSession and batch.resume was
|
||||
// true, one more `one_turn` with resume false. Otherwise the first outcome.
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Run a batch's turn and post what comes of it in its thread. A post that fails is logged.
|
||||
pub fn deliver(poster: &dyn Poster, socket: &Path, batch: &Batch, log: &dyn Fn(&str)) {
|
||||
// Post in the batch thread. An ApprovalPending event posts `approval_text(approval)` at once.
|
||||
// Then: Answer -> every part of `split_answer`, in order; Refused -> `error_text`;
|
||||
// LoopDown(why) -> log "gatewayd: <session>: <why>" and post LOOP_DOWN. A post that fails is
|
||||
// logged, exactly "gatewayd: cannot post in <channel> (thread <root>): <error>", and the rest
|
||||
// goes on.
|
||||
todo!()
|
||||
}
|
||||
Reference in New Issue
Block a user