gatewayd: sessions, routing posts to sessions, commands and the queue
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
//! Which posts become turns, in which session, and how messages wait for a running turn (M4a spec,
|
||||
//! section 7, including its table of examples). Do not edit.
|
||||
|
||||
use gatewayd::mm::Post;
|
||||
use gatewayd::sessions::{
|
||||
BUSY, Batch, Ignored, M4B_COMMAND, Message, Pushed, Queues, Route, Router, Thread,
|
||||
UNKNOWN_COMMAND, named,
|
||||
};
|
||||
use proto::SessionId;
|
||||
|
||||
const BOT: &str = "b0000000000000000000000000";
|
||||
const KYLE: &str = "k0000000000000000000000000";
|
||||
const EVE: &str = "e0000000000000000000000000";
|
||||
const DM: &str = "d0000000000000000000000000";
|
||||
const SHARED: &str = "c0000000000000000000000000";
|
||||
const OTHER: &str = "o0000000000000000000000000";
|
||||
const ROOT: &str = "r0000000000000000000000000";
|
||||
const POST: &str = "p0000000000000000000000000";
|
||||
|
||||
fn router() -> Router {
|
||||
Router::new(
|
||||
BOT,
|
||||
"boxmaker-straylight",
|
||||
&[KYLE.to_string()],
|
||||
&[SHARED.to_string()],
|
||||
)
|
||||
}
|
||||
|
||||
fn post(channel: &str, root: &str, message: &str) -> Post {
|
||||
Post {
|
||||
id: POST.to_string(),
|
||||
user_id: KYLE.to_string(),
|
||||
channel_id: channel.to_string(),
|
||||
root_id: root.to_string(),
|
||||
message: message.to_string(),
|
||||
create_at: 5,
|
||||
delete_at: 0,
|
||||
kind: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn known(root: &str) -> bool {
|
||||
root == ROOT
|
||||
}
|
||||
|
||||
fn unknown(_: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn queued(route: Route) -> Message {
|
||||
match route {
|
||||
Route::Queue(m) => m,
|
||||
other => panic!("not queued: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_examples_in_the_spec() {
|
||||
let r = router();
|
||||
let cases = [
|
||||
("", "@boxmaker-straylight summarise the audit log", true),
|
||||
(ROOT, "and the older files?", true),
|
||||
(ROOT, "@hermes what do you think?", false),
|
||||
("", "@boxmaker-straylight @hermes compare notes", true),
|
||||
("", "@boxmaker-straylightx hello", false),
|
||||
("", "@channel standup in five", false),
|
||||
];
|
||||
for (root, message, yes) in cases {
|
||||
let got = r.route(&post(SHARED, root, message), "O", &known);
|
||||
assert_eq!(matches!(got, Route::Queue(_)), yes, "{message}: {got:?}");
|
||||
if !yes {
|
||||
assert_eq!(got, Route::Ignore(Ignored::NotForUs), "{message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn naming() {
|
||||
assert_eq!(named("@Boxmaker-Straylight, look"), ["boxmaker-straylight"]);
|
||||
assert_eq!(named("ask @boxmaker-straylight."), ["boxmaker-straylight"]);
|
||||
assert_eq!(named("@a.b_c-d... and @e"), ["a.b_c-d", "e"]);
|
||||
assert_eq!(named("@ alone, @@x, trailing @"), ["x"]);
|
||||
assert_eq!(named("ünïcødé @ʙob @bob"), ["bob"]);
|
||||
assert!(named("no names here").is_empty());
|
||||
let r = router();
|
||||
for message in [
|
||||
"hi @BOXMAKER-STRAYLIGHT",
|
||||
"@boxmaker-straylight.",
|
||||
"(@boxmaker-straylight)",
|
||||
] {
|
||||
assert!(
|
||||
matches!(
|
||||
r.route(&post(SHARED, "", message), "P", &unknown),
|
||||
Route::Queue(_)
|
||||
),
|
||||
"{message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replies_in_our_thread_that_name_everyone_are_still_ours() {
|
||||
let r = router();
|
||||
for message in ["@here any news?", "@all done", "thanks @channel"] {
|
||||
assert!(
|
||||
matches!(
|
||||
r.route(&post(SHARED, ROOT, message), "O", &known),
|
||||
Route::Queue(_)
|
||||
),
|
||||
"{message}"
|
||||
);
|
||||
}
|
||||
let got = r.route(
|
||||
&post(
|
||||
SHARED,
|
||||
"q0000000000000000000000000",
|
||||
"a reply in someone else's thread",
|
||||
),
|
||||
"O",
|
||||
&known,
|
||||
);
|
||||
assert_eq!(got, Route::Ignore(Ignored::NotForUs));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn who_and_where() {
|
||||
let r = router();
|
||||
let mut own = post(DM, "", "hello");
|
||||
own.user_id = BOT.to_string();
|
||||
assert_eq!(r.route(&own, "D", &unknown), Route::Ignore(Ignored::Own));
|
||||
let mut system = post(DM, "", "joined");
|
||||
system.kind = "system_join_channel".to_string();
|
||||
assert_eq!(
|
||||
r.route(&system, "D", &unknown),
|
||||
Route::Ignore(Ignored::System)
|
||||
);
|
||||
let mut stranger = post(DM, "", "@boxmaker-straylight hello");
|
||||
stranger.user_id = EVE.to_string();
|
||||
assert_eq!(
|
||||
r.route(&stranger, "D", &unknown),
|
||||
Route::Ignore(Ignored::NotAllowed)
|
||||
);
|
||||
let mut stranger_cmd = post(DM, "", "!approve 1");
|
||||
stranger_cmd.user_id = EVE.to_string();
|
||||
assert_eq!(
|
||||
r.route(&stranger_cmd, "D", &unknown),
|
||||
Route::Ignore(Ignored::NotAllowed)
|
||||
);
|
||||
let naming = "@boxmaker-straylight hello";
|
||||
assert_eq!(
|
||||
r.route(&post(OTHER, "", naming), "O", &unknown),
|
||||
Route::Ignore(Ignored::NotForUs)
|
||||
);
|
||||
assert_eq!(
|
||||
r.route(&post(SHARED, "", naming), "X", &unknown),
|
||||
Route::Ignore(Ignored::NotForUs)
|
||||
);
|
||||
assert!(matches!(
|
||||
r.route(&post(SHARED, "", naming), "G", &unknown),
|
||||
Route::Queue(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
r.route(&post(DM, "", "no name needed"), "D", &unknown),
|
||||
Route::Queue(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sessions_and_threads() {
|
||||
let r = router();
|
||||
let top = queued(r.route(&post(DM, "", "hello"), "D", &unknown));
|
||||
assert_eq!(top.session.as_str(), format!("mm-{POST}"));
|
||||
assert_eq!(
|
||||
top.thread,
|
||||
Thread {
|
||||
channel: DM.to_string(),
|
||||
root: POST.to_string()
|
||||
}
|
||||
);
|
||||
assert!(!top.resume && !top.joins_thread);
|
||||
assert_eq!(top.text, "hello");
|
||||
let reply = queued(r.route(&post(DM, ROOT, "more"), "D", &unknown));
|
||||
assert_eq!(reply.session.as_str(), format!("mm-{ROOT}"));
|
||||
assert_eq!(reply.thread.root, ROOT);
|
||||
assert!(reply.resume);
|
||||
let channel = queued(r.route(&post(SHARED, "", "@boxmaker-straylight hi"), "O", &unknown));
|
||||
assert!(channel.joins_thread && !channel.resume);
|
||||
assert_eq!(channel.text, "@boxmaker-straylight hi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commands() {
|
||||
let r = router();
|
||||
let thread = Thread {
|
||||
channel: DM.to_string(),
|
||||
root: ROOT.to_string(),
|
||||
};
|
||||
let reply = |text: &str| Route::Reply {
|
||||
thread: thread.clone(),
|
||||
text: text.to_string(),
|
||||
};
|
||||
for (message, answer) in [
|
||||
("!approve 42", M4B_COMMAND),
|
||||
("!deny 42 not now", M4B_COMMAND),
|
||||
("!deny", M4B_COMMAND),
|
||||
("!approved", UNKNOWN_COMMAND),
|
||||
("!help", UNKNOWN_COMMAND),
|
||||
("!", UNKNOWN_COMMAND),
|
||||
] {
|
||||
assert_eq!(
|
||||
r.route(&post(DM, ROOT, message), "D", &unknown),
|
||||
reply(answer),
|
||||
"{message}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
queued(r.route(&post(DM, ROOT, "!!approve is a word"), "D", &unknown)).text,
|
||||
"!approve is a word"
|
||||
);
|
||||
assert_eq!(
|
||||
queued(r.route(&post(DM, ROOT, "!!"), "D", &unknown)).text,
|
||||
"!"
|
||||
);
|
||||
assert_eq!(
|
||||
queued(r.route(&post(DM, ROOT, " !help"), "D", &unknown)).text,
|
||||
" !help"
|
||||
);
|
||||
}
|
||||
|
||||
fn message(root: &str, resume: bool, text: &str) -> Message {
|
||||
Message {
|
||||
session: SessionId::new(&format!("mm-{root}")).unwrap(),
|
||||
thread: Thread {
|
||||
channel: DM.to_string(),
|
||||
root: root.to_string(),
|
||||
},
|
||||
resume,
|
||||
text: text.to_string(),
|
||||
joins_thread: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn messages_wait_for_the_running_turn_and_go_together() {
|
||||
let mut q = Queues::new(3);
|
||||
let first = message(ROOT, false, "one");
|
||||
let Pushed::Start(batch) = q.push(first.clone()) else {
|
||||
panic!("not started")
|
||||
};
|
||||
assert_eq!(
|
||||
batch,
|
||||
Batch {
|
||||
session: first.session.clone(),
|
||||
thread: first.thread.clone(),
|
||||
resume: false,
|
||||
text: "one".to_string()
|
||||
}
|
||||
);
|
||||
assert_eq!(q.push(message(ROOT, true, "two")), Pushed::Waiting);
|
||||
assert_eq!(q.push(message(ROOT, true, "three\nlines")), Pushed::Waiting);
|
||||
let other = message(POST, false, "elsewhere");
|
||||
assert!(
|
||||
matches!(q.push(other.clone()), Pushed::Start(_)),
|
||||
"another session starts at once"
|
||||
);
|
||||
assert_eq!(q.running(), 2);
|
||||
let mut roots: Vec<String> = q.threads().into_iter().map(|t| t.root).collect();
|
||||
roots.sort();
|
||||
assert_eq!(roots, [POST, ROOT]);
|
||||
let next = q.finish(&first.session).unwrap();
|
||||
assert_eq!(
|
||||
(next.text.as_str(), next.resume),
|
||||
("two\n\nthree\nlines", true)
|
||||
);
|
||||
assert_eq!(q.finish(&first.session), None);
|
||||
assert_eq!(q.finish(&other.session), None);
|
||||
assert_eq!(q.running(), 0);
|
||||
assert!(
|
||||
matches!(q.push(message(ROOT, true, "later")), Pushed::Start(_)),
|
||||
"idle again"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_full_queue_drops_the_message() {
|
||||
let mut q = Queues::new(2);
|
||||
let m = message(ROOT, false, "run");
|
||||
assert!(matches!(q.push(m.clone()), Pushed::Start(_)));
|
||||
assert_eq!(q.push(message(ROOT, true, "a")), Pushed::Waiting);
|
||||
assert_eq!(q.push(message(ROOT, true, "b")), Pushed::Waiting);
|
||||
assert_eq!(
|
||||
q.push(message(ROOT, true, "c")),
|
||||
Pushed::Full(m.thread.clone())
|
||||
);
|
||||
assert_eq!(q.finish(&m.session).unwrap().text, "a\n\nb");
|
||||
assert!(!BUSY.is_empty());
|
||||
assert_eq!(q.finish(&SessionId::new("mm-never").unwrap()), None);
|
||||
}
|
||||
Reference in New Issue
Block a user