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,192 @@
|
||||
//! `gatewayd` end to end across gaps: a restart, a lost connection, an unreachable server, a
|
||||
//! refused token and a damaged state file (M4a spec, section 9). 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::{DM, FakeMm, KYLE, id, post};
|
||||
use gateway::{SHARED, WAIT, answering, config, loop_dir, start, up};
|
||||
use gatewayd::serve::{INTERRUPTED, Stop};
|
||||
use tmp::TempDir;
|
||||
|
||||
#[test]
|
||||
fn a_restart_reports_the_cut_off_turn_and_catches_up() {
|
||||
let home = TempDir::new("serve-restart");
|
||||
let (cut, seen, new1, new2) = (id('r', 1), id('p', 2), id('p', 3), id('p', 4));
|
||||
let state = serde_json::json!({
|
||||
"channels": {DM: 1000}, "recent": [seen], "threads": [],
|
||||
"in_flight": [{"session": format!("mm-{cut}"), "channel": DM, "root": cut}]
|
||||
});
|
||||
home.write("gateway/state.json", &state.to_string());
|
||||
loop_dir(&home);
|
||||
let turns = answering(&home, Duration::ZERO);
|
||||
let mm = FakeMm::start();
|
||||
mm.set_since(
|
||||
DM,
|
||||
&[
|
||||
post(&new2, KYLE, DM, "", "second", 2000),
|
||||
post(&seen, KYLE, DM, "", "already answered", 1500),
|
||||
post(&new1, KYLE, DM, "", "first", 1800),
|
||||
],
|
||||
);
|
||||
let _running = start(config(&home, &mm.url(), ""));
|
||||
let _ws = mm.next_ws(WAIT);
|
||||
let first = turns.recv_timeout(WAIT).unwrap();
|
||||
let second = turns.recv_timeout(WAIT).unwrap();
|
||||
assert_eq!(
|
||||
(first.content.as_str(), second.content.as_str()),
|
||||
("first", "second")
|
||||
);
|
||||
let posts = mm.wait_posts(3, WAIT);
|
||||
assert_eq!(posts[0], (DM.to_string(), cut, INTERRUPTED.to_string()));
|
||||
assert_eq!(posts.len(), 3, "{posts:?}");
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
let saved: serde_json::Value = serde_json::from_str(
|
||||
&std::fs::read_to_string(home.path().join("gateway/state.json")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(saved["in_flight"], serde_json::json!([]));
|
||||
assert_eq!(saved["channels"][DM], 2000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_first_start_answers_no_history() {
|
||||
let home = TempDir::new("serve-first");
|
||||
loop_dir(&home);
|
||||
let turns = answering(&home, Duration::ZERO);
|
||||
let mm = FakeMm::start();
|
||||
mm.set_since(DM, &[post(&id('p', 1), KYLE, DM, "", "old", 5)]);
|
||||
let running = start(config(&home, &mm.url(), ""));
|
||||
let _ws = mm.next_ws(WAIT);
|
||||
running.wait_log("connected to");
|
||||
assert!(turns.recv_timeout(Duration::from_millis(300)).is_err());
|
||||
assert!(
|
||||
!mm.calls().iter().any(|(_, p)| p.contains("since=")),
|
||||
"{:?}",
|
||||
mm.calls()
|
||||
);
|
||||
let saved: serde_json::Value = serde_json::from_str(
|
||||
&std::fs::read_to_string(home.path().join("gateway/state.json")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
saved["channels"][DM].as_i64().unwrap() > 1_700_000_000_000,
|
||||
"marked from now"
|
||||
);
|
||||
assert!(saved["channels"][SHARED].as_i64().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lost_connection_is_made_again() {
|
||||
let home = TempDir::new("serve-reconnect");
|
||||
let (mm, running, turns, ws) = up(&home, Duration::ZERO);
|
||||
ws.drop_connection();
|
||||
let mut again = mm.next_ws(WAIT);
|
||||
let log = running.wait_log("lost the connection");
|
||||
assert!(
|
||||
log.iter()
|
||||
.any(|l| l.ends_with("see docs/runbook.md#mattermost-unreachable")),
|
||||
"{log:?}"
|
||||
);
|
||||
let p1 = id('p', 1);
|
||||
again.posted(&post(&p1, KYLE, DM, "", "still there?", 5), "D");
|
||||
assert_eq!(turns.recv_timeout(WAIT).unwrap().content, "still there?");
|
||||
assert_eq!(mm.wait_posts(1, WAIT).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_refused_token_stops_gatewayd() {
|
||||
let home = TempDir::new("serve-auth");
|
||||
let mm = FakeMm::start();
|
||||
mm.refuse_token();
|
||||
let running = start(config(&home, &mm.url(), ""));
|
||||
let stop = running.join_within();
|
||||
assert!(matches!(stop, Stop::Auth(401)), "{stop}");
|
||||
assert_eq!(
|
||||
stop.to_string(),
|
||||
"gatewayd: Mattermost refused the token (401)\nsee docs/runbook.md#mattermost-auth-failed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unreachable_server_is_tried_again() {
|
||||
let home = TempDir::new("serve-unreachable");
|
||||
let port = std::net::TcpListener::bind("127.0.0.1:0")
|
||||
.unwrap()
|
||||
.local_addr()
|
||||
.unwrap()
|
||||
.port();
|
||||
let url = format!("http://127.0.0.1:{port}");
|
||||
let running = start(config(&home, &url, ""));
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
let log = running.log();
|
||||
let tries: Vec<&String> = log
|
||||
.iter()
|
||||
.filter(|l| l.starts_with(&format!("gatewayd: cannot reach {url}: ")))
|
||||
.collect();
|
||||
assert!(tries.len() >= 2, "{log:?}");
|
||||
assert!(
|
||||
tries
|
||||
.iter()
|
||||
.all(|l| l
|
||||
.ends_with("; trying again in 0 s\nsee docs/runbook.md#mattermost-unreachable")),
|
||||
"{tries:?}"
|
||||
);
|
||||
assert!(matches!(running.finish(), Stop::Asked));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_state_file_stops_at_once() {
|
||||
let home = TempDir::new("serve-damaged");
|
||||
home.write("gateway/state.json", "{not json");
|
||||
let mm = FakeMm::start();
|
||||
let running = start(config(&home, &mm.url(), ""));
|
||||
let stop = running.join_within();
|
||||
assert!(matches!(stop, Stop::State(_)), "{stop}");
|
||||
assert!(
|
||||
stop.to_string()
|
||||
.ends_with("see docs/runbook.md#gateway-state-damaged"),
|
||||
"{stop}"
|
||||
);
|
||||
assert!(mm.calls().is_empty(), "nothing is asked of Mattermost");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reconnect_does_not_interrupt_a_running_turn() {
|
||||
let home = TempDir::new("serve-reconnect-turn");
|
||||
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("the answer")]
|
||||
});
|
||||
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, "", "a long one", 5), "D");
|
||||
turns.recv_timeout(WAIT).unwrap();
|
||||
ws.drop_connection();
|
||||
let _again = mm.next_ws(WAIT);
|
||||
running.wait_log("lost the connection");
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
release_tx.send(()).unwrap();
|
||||
let posts = mm.wait_posts(1, WAIT);
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
assert_eq!(
|
||||
mm.posts(),
|
||||
[(DM.to_string(), p1, "the answer".to_string())],
|
||||
"{posts:?}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user