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>
214 lines
7.2 KiB
Rust
214 lines
7.2 KiB
Rust
//! `gatewayd` end to end, against a fake Mattermost and a fake `loopd`: who is answered, where,
|
|
//! and how (M4a spec, sections 7 and 8). Do not edit.
|
|
|
|
#[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 fake_loop::{done, serve_loop};
|
|
use fake_mm::{BOT, BOT_NAME, DM, EVE, EVE_DM, FakeMm, KYLE, id, post};
|
|
use gateway::{OTHER, SHARED, WAIT, config, loop_dir, read_state, start, up};
|
|
use gatewayd::serve::Stop;
|
|
use gatewayd::sessions::{BUSY, M4B_COMMAND};
|
|
use tmp::TempDir;
|
|
|
|
#[test]
|
|
fn a_direct_message_is_answered_in_its_thread_while_typing() {
|
|
let home = TempDir::new("serve-dm");
|
|
let (mm, running, turns, mut ws) = up(&home, Duration::from_millis(600));
|
|
let p1 = id('p', 1);
|
|
ws.posted(&post(&p1, KYLE, DM, "", "hello there", 5), "D");
|
|
let turn = turns.recv_timeout(WAIT).unwrap();
|
|
assert_eq!(
|
|
(turn.session.as_str(), turn.content.as_str(), turn.resume),
|
|
(format!("mm-{p1}").as_str(), "hello there", false)
|
|
);
|
|
let typing = ws.typing_within(Duration::from_millis(500));
|
|
assert!(typing.len() >= 2, "{typing:?}");
|
|
assert!(
|
|
typing.iter().all(|(c, p)| c == DM && *p == p1),
|
|
"{typing:?}"
|
|
);
|
|
let posts = mm.wait_posts(1, WAIT);
|
|
assert_eq!(
|
|
posts,
|
|
[(
|
|
DM.to_string(),
|
|
p1.clone(),
|
|
"answer to hello there".to_string()
|
|
)]
|
|
);
|
|
// Typing sent just before the answer may still be on its way; after that, it stops.
|
|
ws.typing_within(Duration::from_millis(300));
|
|
assert!(
|
|
ws.typing_within(Duration::from_millis(400)).is_empty(),
|
|
"typing stops after the answer"
|
|
);
|
|
let log = running.log();
|
|
assert!(
|
|
log.iter()
|
|
.any(|l| l == &format!("gatewayd: connected to {} as {BOT_NAME}", mm.url())),
|
|
"{log:?}"
|
|
);
|
|
assert!(matches!(running.finish(), Stop::Asked));
|
|
}
|
|
|
|
#[test]
|
|
fn anyone_else_gets_nothing_at_all() {
|
|
let home = TempDir::new("serve-stranger");
|
|
let (mm, running, turns, mut ws) = up(&home, Duration::ZERO);
|
|
let p1 = id('p', 1);
|
|
ws.posted(&post(&p1, EVE, EVE_DM, "", "secret words", 5), "D");
|
|
let mut own = post(&id('p', 2), BOT, DM, "", "my own post", 6);
|
|
own["user_id"] = serde_json::json!(BOT);
|
|
ws.posted(&own, "D");
|
|
let log = running.wait_log("not allowed");
|
|
assert!(turns.recv_timeout(Duration::from_millis(300)).is_err());
|
|
assert!(ws.typing_within(Duration::from_millis(200)).is_empty());
|
|
assert!(mm.posts().is_empty());
|
|
assert!(
|
|
log.contains(&format!(
|
|
"gatewayd: ignored post {p1} from {EVE}: not allowed"
|
|
)),
|
|
"{log:?}"
|
|
);
|
|
assert!(!log.iter().any(|l| l.contains("secret words")), "{log:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn messages_during_a_turn_go_together_in_the_next() {
|
|
let home = TempDir::new("serve-burst");
|
|
loop_dir(&home);
|
|
let (release_tx, release_rx) = mpsc::channel::<()>();
|
|
let release = Mutex::new(release_rx);
|
|
let turns = serve_loop(&home.path().join("run/loop/loop.sock"), move |n, turn| {
|
|
if n == 0 {
|
|
let _ = release.lock().unwrap().recv_timeout(WAIT);
|
|
}
|
|
vec![done(&format!("answer to {}", turn.content))]
|
|
});
|
|
let mm = FakeMm::start();
|
|
let _running = start(config(&home, &mm.url(), ""));
|
|
let mut ws = mm.next_ws(WAIT);
|
|
let p1 = id('p', 1);
|
|
ws.posted(&post(&p1, KYLE, DM, "", "one", 5), "D");
|
|
assert_eq!(turns.recv_timeout(WAIT).unwrap().content, "one");
|
|
let saved = read_state(&home);
|
|
assert_eq!(
|
|
saved["in_flight"],
|
|
serde_json::json!([{"session": format!("mm-{p1}"), "channel": DM, "root": p1}])
|
|
);
|
|
ws.posted(&post(&id('p', 2), KYLE, DM, &p1, "two", 6), "D");
|
|
ws.posted(&post(&id('p', 3), KYLE, DM, &p1, "three", 7), "D");
|
|
std::thread::sleep(Duration::from_millis(200));
|
|
release_tx.send(()).unwrap();
|
|
let second = turns.recv_timeout(WAIT).unwrap();
|
|
assert_eq!(
|
|
(
|
|
second.session.as_str(),
|
|
second.content.as_str(),
|
|
second.resume
|
|
),
|
|
(format!("mm-{p1}").as_str(), "two\n\nthree", true)
|
|
);
|
|
let posts = mm.wait_posts(2, WAIT);
|
|
let texts: Vec<&str> = posts.iter().map(|(_, _, t)| t.as_str()).collect();
|
|
assert_eq!(texts, ["answer to one", "answer to two\n\nthree"]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_full_queue_says_busy() {
|
|
let home = TempDir::new("serve-busy");
|
|
loop_dir(&home);
|
|
let (release_tx, release_rx) = mpsc::channel::<()>();
|
|
let release = Mutex::new(release_rx);
|
|
let _turns = serve_loop(&home.path().join("run/loop/loop.sock"), move |_, _| {
|
|
let _ = release.lock().unwrap().recv_timeout(WAIT);
|
|
vec![done("ok")]
|
|
});
|
|
let mm = FakeMm::start();
|
|
let _running = start(config(&home, &mm.url(), "queue = 1"));
|
|
let mut ws = mm.next_ws(WAIT);
|
|
let p1 = id('p', 1);
|
|
for (n, text) in ["run", "waits", "too many"].iter().enumerate() {
|
|
let n = u32::try_from(n).unwrap();
|
|
let root = if n == 0 { String::new() } else { p1.clone() };
|
|
ws.posted(
|
|
&post(&id('p', n + 1), KYLE, DM, &root, text, i64::from(n) + 5),
|
|
"D",
|
|
);
|
|
}
|
|
let posts = mm.wait_posts(1, WAIT);
|
|
assert_eq!(posts, [(DM.to_string(), p1, BUSY.to_string())]);
|
|
release_tx.send(()).unwrap();
|
|
release_tx.send(()).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn channels_are_answered_only_when_named_or_in_our_thread() {
|
|
let home = TempDir::new("serve-channel");
|
|
let (mm, _running, turns, mut ws) = up(&home, Duration::ZERO);
|
|
let p1 = id('p', 1);
|
|
ws.posted(
|
|
&post(&id('p', 9), KYLE, SHARED, "", "hello everyone", 4),
|
|
"O",
|
|
);
|
|
ws.posted(
|
|
&post(
|
|
&id('p', 8),
|
|
KYLE,
|
|
OTHER,
|
|
"",
|
|
"@boxmaker-straylight elsewhere",
|
|
4,
|
|
),
|
|
"O",
|
|
);
|
|
ws.posted(
|
|
&post(&p1, KYLE, SHARED, "", "@boxmaker-straylight start", 5),
|
|
"O",
|
|
);
|
|
assert_eq!(
|
|
turns.recv_timeout(WAIT).unwrap().content,
|
|
"@boxmaker-straylight start"
|
|
);
|
|
mm.wait_posts(1, WAIT);
|
|
ws.posted(
|
|
&post(&id('p', 2), KYLE, SHARED, &p1, "@hermes your turn", 6),
|
|
"O",
|
|
);
|
|
ws.posted(&post(&id('p', 3), KYLE, SHARED, &p1, "and more", 7), "O");
|
|
let next = turns.recv_timeout(WAIT).unwrap();
|
|
assert_eq!((next.content.as_str(), next.resume), ("and more", true));
|
|
let posts = mm.wait_posts(2, WAIT);
|
|
assert!(
|
|
posts.iter().all(|(c, r, _)| c == SHARED && *r == p1),
|
|
"{posts:?}"
|
|
);
|
|
assert!(turns.recv_timeout(Duration::from_millis(200)).is_err());
|
|
let saved = read_state(&home);
|
|
assert!(saved["channels"].get(OTHER).is_none(), "{saved}");
|
|
assert_eq!(saved["threads"], serde_json::json!([p1]));
|
|
}
|
|
|
|
#[test]
|
|
fn commands_are_answered_without_a_turn() {
|
|
let home = TempDir::new("serve-command");
|
|
let (mm, _running, turns, mut ws) = up(&home, Duration::ZERO);
|
|
let p1 = id('p', 1);
|
|
ws.posted(&post(&p1, KYLE, DM, "", "!approve 3", 5), "D");
|
|
assert_eq!(
|
|
mm.wait_posts(1, WAIT),
|
|
[(DM.to_string(), p1, M4B_COMMAND.to_string())]
|
|
);
|
|
assert!(turns.recv_timeout(Duration::from_millis(200)).is_err());
|
|
}
|