gatewayd: approvals asked by non-approvers in direct messages go to an approver
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -112,28 +112,41 @@ fn without_a_turn_in_flight_it_goes_to_the_first_approvers_direct_messages_in_pa
|
||||
let long = format!("{{\"content\":\"{}\"}}", "x".repeat(20_000));
|
||||
let asking = ask(&socket(&home), pending(7, "chat-1789700000-1", &long)).unwrap();
|
||||
let posts = mm.posts();
|
||||
assert_eq!(posts.len(), 2, "two parts");
|
||||
assert_eq!(posts.len(), 3, "a first post, then two parts");
|
||||
assert_eq!(
|
||||
(posts[0].0.as_str(), posts[0].1.as_str()),
|
||||
(DM, ""),
|
||||
"a new thread in the direct channel"
|
||||
);
|
||||
assert_eq!(
|
||||
(posts[1].0.as_str(), posts[1].1.clone()),
|
||||
(DM, FakeMm::post_id(0)),
|
||||
"the next part in its thread"
|
||||
);
|
||||
assert!(
|
||||
posts[1]
|
||||
posts[0]
|
||||
.2
|
||||
.starts_with("**Approval 7** was asked in session `chat-1789700000-1`"),
|
||||
"{}",
|
||||
posts[0].2
|
||||
);
|
||||
for (n, part) in posts[1..].iter().enumerate() {
|
||||
assert_eq!(
|
||||
(part.0.as_str(), part.1.clone()),
|
||||
(DM, FakeMm::post_id(0)),
|
||||
"part {n} in its thread"
|
||||
);
|
||||
}
|
||||
assert!(posts[1].2.starts_with("**Approval 7**"), "{}", posts[1].2);
|
||||
assert!(
|
||||
posts[2]
|
||||
.2
|
||||
.contains("Reply in this thread with `!approve 7`")
|
||||
);
|
||||
assert_eq!(asking.posts, [FakeMm::post_id(0), FakeMm::post_id(1)]);
|
||||
assert_eq!(
|
||||
asking.posts,
|
||||
[FakeMm::post_id(0), FakeMm::post_id(1), FakeMm::post_id(2)]
|
||||
);
|
||||
asking.close();
|
||||
let edits = mm.wait_edits(1, WAIT);
|
||||
assert_eq!(
|
||||
edits[0].0,
|
||||
FakeMm::post_id(1),
|
||||
FakeMm::post_id(2),
|
||||
"only the last part is edited"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
//! An approval asked for by someone who may talk but not approve, in a direct or group message,
|
||||
//! goes to the first approver's direct messages: first who asked and what, then the approval, and
|
||||
//! the asking thread is told where it went. In a channel it stays in its thread (M4b spec,
|
||||
//! section 6). Do not edit.
|
||||
|
||||
#[path = "support/brokerd_client.rs"]
|
||||
mod brokerd_client;
|
||||
#[path = "support/fake_loop.rs"]
|
||||
mod fake_loop;
|
||||
#[path = "support/fake_mm.rs"]
|
||||
mod fake_mm;
|
||||
#[path = "support/gateway.rs"]
|
||||
mod gateway;
|
||||
#[path = "support/tmp.rs"]
|
||||
mod tmp;
|
||||
|
||||
use std::sync::{Mutex, mpsc};
|
||||
use std::time::Duration;
|
||||
|
||||
use brokerd_client::{Asking, ask, pending};
|
||||
use fake_loop::{done, serve_loop};
|
||||
use fake_mm::{DM, EVE, EVE_DM, FakeMm, KYLE, WsPeer, id, post};
|
||||
use gateway::{Running, WAIT, loop_dir, start};
|
||||
use gatewayd::config::Config;
|
||||
use proto::{GatewayOutcome, Turn};
|
||||
use tmp::TempDir;
|
||||
|
||||
const GROUP: &str = "g0000000000000000000000000";
|
||||
const CHANNEL: &str = "o0000000000000000000000000";
|
||||
|
||||
/// KYLE and EVE may talk, in direct messages, GROUP and CHANNEL; only KYLE approves.
|
||||
fn config(home: &TempDir, url: &str) -> Config {
|
||||
let text = format!(
|
||||
"[mattermost]\nurl = \"{url}\"\n[secrets.mattermost_token]\nenv = \"NOT_READ_BY_RUN\"\n[allow]\nusers = [\"{KYLE}\", \"{EVE}\"]\nchannels = [\"{GROUP}\", \"{CHANNEL}\"]\n[approvals]\nusers = [\"{KYLE}\"]\n[paths]\nhome = \"{}\"\n[limits]\ntyping_every_ms = 100\n",
|
||||
home.path().display()
|
||||
);
|
||||
Config::parse(&text).unwrap()
|
||||
}
|
||||
|
||||
/// A loop whose turns wait for `go`, then answer "done".
|
||||
fn held(home: &TempDir) -> (mpsc::Receiver<Turn>, mpsc::Sender<()>) {
|
||||
loop_dir(home);
|
||||
let (go_tx, go_rx) = mpsc::channel::<()>();
|
||||
let go = Mutex::new(go_rx);
|
||||
let turns = serve_loop(&home.path().join("run/loop/loop.sock"), move |_, _| {
|
||||
let _ = go.lock().unwrap().recv_timeout(Duration::from_secs(20));
|
||||
vec![done("done")]
|
||||
});
|
||||
(turns, go_tx)
|
||||
}
|
||||
|
||||
struct Stage {
|
||||
_home: TempDir,
|
||||
mm: FakeMm,
|
||||
_running: Running,
|
||||
ws: WsPeer,
|
||||
go: mpsc::Sender<()>,
|
||||
asking: Asking,
|
||||
root: String,
|
||||
}
|
||||
|
||||
/// gatewayd up, EVE's message `text` in `channel` (of `channel_type`), its turn held, and
|
||||
/// approval 42 asked for its session.
|
||||
fn stage(tag: &str, channel: &str, channel_type: &str, text: &str) -> Stage {
|
||||
let home = TempDir::new(tag);
|
||||
let (turns, go) = held(&home);
|
||||
let mm = FakeMm::start();
|
||||
let running = start(config(&home, &mm.url()));
|
||||
let mut ws = mm.next_ws(WAIT);
|
||||
running.wait_log("gatewayd: connected to ");
|
||||
let root = id('p', 1);
|
||||
ws.posted(&post(&root, EVE, channel, "", text, 5), channel_type);
|
||||
turns.recv_timeout(WAIT).expect("the turn started");
|
||||
let socket = home.path().join("run/gateway/gateway.sock");
|
||||
let session = format!("mm-{root}");
|
||||
let asking = ask(&socket, pending(42, &session, r#"{"path":"/n/a"}"#)).unwrap();
|
||||
Stage {
|
||||
_home: home,
|
||||
mm,
|
||||
_running: running,
|
||||
ws,
|
||||
go,
|
||||
asking,
|
||||
root,
|
||||
}
|
||||
}
|
||||
|
||||
/// EVE's posts in `channel`, redirected: who asked, the approval, and the note, in that order.
|
||||
fn redirected(s: &Stage, channel: &str, place: &str) {
|
||||
let posts = s.mm.wait_posts(3, WAIT);
|
||||
assert_eq!(posts.len(), 3, "{posts:?}");
|
||||
assert_eq!(
|
||||
(posts[0].0.as_str(), posts[0].1.as_str()),
|
||||
(DM, ""),
|
||||
"a new thread in the approver's direct messages"
|
||||
);
|
||||
assert!(
|
||||
posts[0].2.starts_with(&format!(
|
||||
"**Approval 42** was asked by @user-e000 in {place} with the bot. Their message:\n"
|
||||
)),
|
||||
"{}",
|
||||
posts[0].2
|
||||
);
|
||||
assert_eq!(
|
||||
(posts[1].0.as_str(), posts[1].1.clone()),
|
||||
(DM, FakeMm::post_id(0)),
|
||||
"the approval in that thread"
|
||||
);
|
||||
assert!(posts[1].2.starts_with("**Approval 42**"), "{}", posts[1].2);
|
||||
assert!(
|
||||
posts[1]
|
||||
.2
|
||||
.contains("Reply in this thread with `!approve 42`")
|
||||
);
|
||||
assert_eq!(
|
||||
posts[2],
|
||||
(
|
||||
channel.to_string(),
|
||||
s.root.clone(),
|
||||
"waiting for approval 42: it was sent to an approver".to_string()
|
||||
),
|
||||
"the note, in the asking thread"
|
||||
);
|
||||
assert_eq!(s.asking.posts, [FakeMm::post_id(0), FakeMm::post_id(1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_direct_message_from_a_non_approver_goes_to_the_approver_who_answers_it_there() {
|
||||
let mut s = stage("rd-dm", EVE_DM, "D", "read my notes");
|
||||
redirected(&s, EVE_DM, "a direct message");
|
||||
assert!(
|
||||
s.mm.posts()[0].2.contains("\n```\nread my notes\n```"),
|
||||
"the prompt, fenced: {}",
|
||||
s.mm.posts()[0].2
|
||||
);
|
||||
let thread = FakeMm::post_id(0);
|
||||
s.ws.posted(&post(&id('c', 1), KYLE, DM, &thread, "!approve 42", 6), "D");
|
||||
let reply = s.asking.answer(WAIT).expect("the answer reaches brokerd");
|
||||
assert_eq!(
|
||||
(reply.approval, reply.approved, reply.by.as_str()),
|
||||
(42, true, KYLE)
|
||||
);
|
||||
s.asking.outcome(GatewayOutcome::Runs {});
|
||||
let edits = s.mm.wait_edits(1, WAIT);
|
||||
assert_eq!(
|
||||
edits[0].0,
|
||||
FakeMm::post_id(1),
|
||||
"the approval's post is marked"
|
||||
);
|
||||
let approval_text = s.mm.posts()[1].2.clone();
|
||||
assert!(
|
||||
edits[0].1.starts_with(&approval_text) && edits[0].1.len() > approval_text.len(),
|
||||
"its own text, with the ending: {}",
|
||||
edits[0].1
|
||||
);
|
||||
s.go.send(()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_prompt_is_cut() {
|
||||
let long = "y".repeat(2_500);
|
||||
let s = stage("rd-long", EVE_DM, "D", &long);
|
||||
redirected(&s, EVE_DM, "a direct message");
|
||||
let first = &s.mm.posts()[0].2;
|
||||
assert!(first.contains(&"y".repeat(2_000)), "{first}");
|
||||
assert!(!first.contains(&"y".repeat(2_001)), "{first}");
|
||||
assert!(first.ends_with("\n(… 500 more characters)"), "{first}");
|
||||
s.go.send(()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_group_message_goes_to_the_approver_with_the_mention_stripped() {
|
||||
let s = stage("rd-group", GROUP, "G", "@boxmaker-straylight read my notes");
|
||||
redirected(&s, GROUP, "a group message");
|
||||
let first = &s.mm.posts()[0].2;
|
||||
assert!(first.contains("\n```\nread my notes\n```"), "{first}");
|
||||
s.go.send(()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_a_channel_it_stays_in_the_thread() {
|
||||
let s = stage(
|
||||
"rd-channel",
|
||||
CHANNEL,
|
||||
"O",
|
||||
"@boxmaker-straylight read my notes",
|
||||
);
|
||||
s.mm.wait_posts(1, WAIT);
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
let posts = s.mm.posts();
|
||||
assert_eq!(posts.len(), 1, "{posts:?}");
|
||||
assert_eq!(
|
||||
(posts[0].0.as_str(), posts[0].1.as_str()),
|
||||
(CHANNEL, s.root.as_str())
|
||||
);
|
||||
assert!(posts[0].2.starts_with("**Approval 42**"), "{}", posts[0].2);
|
||||
s.go.send(()).unwrap();
|
||||
}
|
||||
Reference in New Issue
Block a user