From e71c9075a2eb9a0ec8b645ed59b4b1e0a4aed3ab Mon Sep 17 00:00:00 2001 From: unRekable Date: Tue, 20 Oct 2026 01:24:18 +0200 Subject: [PATCH 1/6] Let a slow or loaded machine hold the tests it runs. The ring test asked the demo for its answer 2,500 ms in and wanted our side's join to have outrun it: on a cold machine, where starting mediad alone can take a second, the answer won. The answer comes at 6 s now, and the two asserts take their margins from it. The polish round's daemons start one after another, so twelve fresh processes at once cannot push a cold machine past the socket's ten seconds; they keep running side by side. And the keychain test measures its request, not the daemon's start, and says what it saw when a slow start missed the read's 1,500 ms window instead of failing on it. --- crates/chatd/tests/calls.rs | 10 ++++++---- crates/chatd/tests/polish.rs | 31 ++++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/crates/chatd/tests/calls.rs b/crates/chatd/tests/calls.rs index 1cadd76..c8ce202 100644 --- a/crates/chatd/tests/calls.rs +++ b/crates/chatd/tests/calls.rs @@ -527,8 +527,10 @@ impl Drop for Daemon { fn a_ring_we_placed_is_not_a_call_until_the_other_side_answers_whatever_our_media_does() { let _turn = alone(); let voice = Voice::new(); - // The demo's network answers a direct call after this long. - let chatd = Daemon::start(&voice, "2500"); + // The demo's network answers a direct call after this long. Roomy, so a slow or cold + // machine, where starting mediad alone can take a second, still outruns it. + const ANSWER_MS: u64 = 6_000; + let chatd = Daemon::start(&voice, &ANSWER_MS.to_string()); let ines = Key { network: Network::Discord, account: "210000000000000000".into(), @@ -543,14 +545,14 @@ fn a_ring_we_placed_is_not_a_call_until_the_other_side_answers_whatever_our_medi voice.admit(&mut peer); peer.expect(5); until("our devices", || chatd.call().is_some_and(|call| call.input == "tone")); - assert!(rang.elapsed() < Duration::from_millis(2_000), "the answer was not outrun: {:?}", rang.elapsed()); + assert!(rang.elapsed() < Duration::from_millis(ANSWER_MS - 1_000), "the answer was not outrun: {:?}", rang.elapsed()); let call = chatd.call().expect("the ring"); assert_eq!((call.state, call.joined), (CallState::RingingOut, 0), "media being up is not an answer"); assert!(call.participants.is_empty()); // They pick up: now it is a call, with them in it. until("the answer", || chatd.call().is_some_and(|call| call.state == CallState::Joined)); - assert!(rang.elapsed() >= Duration::from_millis(2_400), "joined before anyone answered: {:?}", rang.elapsed()); + assert!(rang.elapsed() >= Duration::from_millis(ANSWER_MS - 600), "joined before anyone answered: {:?}", rang.elapsed()); let call = chatd.call().unwrap(); assert!(call.joined > 0); until("the roster", || chatd.call().is_some_and(|call| call.participants.iter().any(|who| who.name == "Ines Marchetti"))); diff --git a/crates/chatd/tests/polish.rs b/crates/chatd/tests/polish.rs index fb36979..366c71d 100644 --- a/crates/chatd/tests/polish.rs +++ b/crates/chatd/tests/polish.rs @@ -11,6 +11,7 @@ use protocol::{ use std::path::PathBuf; use std::process::{Child, Command}; use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::{Mutex, MutexGuard}; use std::thread::sleep; use std::time::{Duration, Instant}; @@ -19,8 +20,17 @@ struct Daemon { dir: PathBuf, } +/// The daemons start one after another: a dozen fresh processes at once have a cold machine +/// miss the socket's deadline, and one start alone answers in a second or two. The daemons +/// keep running side by side; only their starts take turns. +fn one_after_another() -> MutexGuard<'static, ()> { + static TURN: Mutex<()> = Mutex::new(()); + TURN.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) +} + impl Daemon { fn start(args: &[&str], env: &[(&str, &str)], seed: impl FnOnce(&mut Store)) -> Daemon { + let _turn = one_after_another(); static NEXT: AtomicU32 = AtomicU32::new(0); // Short: a unix socket address holds about 100 bytes. // On Windows "/tmp" has no drive; chatd takes only absolute paths. @@ -158,21 +168,28 @@ const TEAMS: &str = "harbor-tenant"; #[test] fn the_socket_answers_while_a_keychain_read_waits_and_the_cached_account_stays_on_screen() { let account = AccountKey { network: Network::Discord, account: "acct".to_owned() }; - let began = Instant::now(); let chatd = Daemon::start(&[], &[("TINYCLIENT_TEST_KEYCHAIN_MS", "1500")], |store| { store.set_account(&account, "Robin", "me").unwrap(); store.upsert_conversation(&account, &ConversationRow::new("dm", ConversationKind::Direct, "Mara")).unwrap(); }); - // Answered at once, though no session has been read. + // Answered at once, though no session has been read. The clock runs on the request alone: + // how long the daemon took to start says nothing about what is tested here. + let asked = Instant::now(); assert_eq!(chatd.ask(Request::Ping), Response::Pong); - assert!(began.elapsed() < Duration::from_secs(3), "the socket waited on the keychain: {:?}", began.elapsed()); + assert!(asked.elapsed() < Duration::from_secs(2), "the socket waited on the keychain: {:?}", asked.elapsed()); let (accounts, keychain, seq, started) = chatd.status(); - assert!(keychain.reading && keychain.denied.is_empty(), "{keychain:?}"); - assert!(started > 0); - // The account whose session is not read yet is shown, offline, not dropped. let cached = accounts.iter().find(|status| status.key == account).expect("the cached account is listed"); assert_eq!(cached.link, Link::Offline); - + // A start slower than the read's 1,500 ms has missed the window: the read is over, the + // account it cached is shown all the same, and that is all this machine gets to see. + if !keychain.reading { + assert!(keychain.is_settled(), "{keychain:?}"); + assert_eq!(cached.detail, ""); + return; + } + assert!(keychain.denied.is_empty(), "{keychain:?}"); + assert!(started > 0); + // The account whose session is not read yet is shown, offline, not dropped. // A read held up past a second is a prompt on screen: the shell is told, and the status says // which network it waits on. let told = chatd.changes(seq, Duration::from_millis(1_400)); From d6daff1bcce215b37db8ec656cb3082b75d94002 Mon Sep 17 00:00:00 2001 From: unRekable Date: Tue, 20 Oct 2026 01:40:00 +0200 Subject: [PATCH 2/6] Wait for the keychain's word instead of sampling one instant. The prompt holds only while the read does, so a machine that reached the daemon late could sample the one moment it was gone and fail on it. The test now waits for the Status change, takes the settling change as its word when the read is already over, and checks on the slow path that the daemon it saw is the one it started. --- crates/chatd/tests/polish.rs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/crates/chatd/tests/polish.rs b/crates/chatd/tests/polish.rs index 366c71d..b1e96b7 100644 --- a/crates/chatd/tests/polish.rs +++ b/crates/chatd/tests/polish.rs @@ -20,9 +20,9 @@ struct Daemon { dir: PathBuf, } -/// The daemons start one after another: a dozen fresh processes at once have a cold machine -/// miss the socket's deadline, and one start alone answers in a second or two. The daemons -/// keep running side by side; only their starts take turns. +/// The daemons start one after another: a dozen fresh processes of ours at once have a cold +/// machine miss the socket's deadline, and one start alone answers in a second or two. The +/// daemons keep running side by side; only their starts take turns. fn one_after_another() -> MutexGuard<'static, ()> { static TURN: Mutex<()> = Mutex::new(()); TURN.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) @@ -184,18 +184,27 @@ fn the_socket_answers_while_a_keychain_read_waits_and_the_cached_account_stays_o // account it cached is shown all the same, and that is all this machine gets to see. if !keychain.reading { assert!(keychain.is_settled(), "{keychain:?}"); + let (_, _, _, again) = chatd.status(); + assert_eq!(again, started, "the daemon restarted"); assert_eq!(cached.detail, ""); return; } assert!(keychain.denied.is_empty(), "{keychain:?}"); assert!(started > 0); // The account whose session is not read yet is shown, offline, not dropped. - // A read held up past a second is a prompt on screen: the shell is told, and the status says - // which network it waits on. - let told = chatd.changes(seq, Duration::from_millis(1_400)); - assert!(told.contains(&Change::Status), "{told:?}"); + // A read held up past a second is a prompt on screen: the shell is told as a Status change, + // and the status says which network it waits on. The prompt holds only while the read does, + // so this waits for its word rather than sampling one instant; a machine that reached the + // read late is told of the settling instead. + let mut told; + loop { + told = chatd.changes(seq, Duration::from_millis(1_400)); + if told.contains(&Change::Status) { + break; + } + assert!(!chatd.status().1.is_settled(), "the read settled without a word to the shell"); + } let (_, keychain, ..) = chatd.status(); - assert!(keychain.waiting.is_some(), "{keychain:?}"); if keychain.waiting == Some(Network::Discord) { let (accounts, ..) = chatd.status(); let cached = accounts.iter().find(|status| status.key == account).unwrap(); From 53dc2d8f39ab2045f3c4e331fdf09d14cb4dbf75 Mon Sep 17 00:00:00 2001 From: unRekable Date: Tue, 20 Oct 2026 17:54:34 +0200 Subject: [PATCH 3/6] Let the presence query be found wherever it sits in the book. The test read the last presence_query frame and wanted the person on screen in it: the account asks for its own presence on its own clock, and on a loaded runner that ask could be the last one written, failing the assert on a property the test is not about. It looks for the query naming the screen now, and prints the book when there is none. --- crates/chatd/tests/slack.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/chatd/tests/slack.rs b/crates/chatd/tests/slack.rs index dbf4e4c..71f0d32 100644 --- a/crates/chatd/tests/slack.rs +++ b/crates/chatd/tests/slack.rs @@ -1481,7 +1481,13 @@ fn sending_typing_presence_and_people_go_through_slack() { world.fake.heard(ALPHA, "presence_sub").pop().filter(|frame| frame["ids"].as_array().is_some_and(|ids| ids.len() > 1)) }); assert_eq!(subscribed["ids"], json!(["U0ANA", "U0ME"]), "who is on screen, and me for the account's own dot"); - assert_eq!(world.fake.heard(ALPHA, "presence_query").pop().map(|query| query["ids"].clone()), Some(json!(["U0ANA"]))); + // The screen's people are queried. The account's own presence is asked for on its own + // clock, so the query naming the screen may not be the last one in the book. + assert!( + world.fake.heard(ALPHA, "presence_query").iter().any(|query| query["ids"] == json!(["U0ANA"])), + "no presence_query named the person on screen: {:?}", + world.fake.heard(ALPHA, "presence_query") + ); world.fake.push(ALPHA, json!({"type": "presence_change", "users": ["U0ANA", "U0BEN"], "presence": "active"})); world.until("Ana to be online", || (watching(vec![alpha("U0ANA")]).0[0].presence == Presence::Online).then_some(())); From 89c06378ae5a4be2b984224f21ac293fdf4b20ab Mon Sep 17 00:00:00 2001 From: unRekable Date: Tue, 20 Oct 2026 23:04:17 +0200 Subject: [PATCH 4/6] Allow one close pair in the pacing test, for the sends a loaded machine holds back. The test read the times the fake received calls at, and chatd paces when it sends them: a machine that holds one send back lets the next overtake it, and two arrivals a fifth of a second apart say nothing about a pacer that did its work. One close pair is allowed; a missing pacer still bunches them all. --- crates/chatd/tests/slack.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/chatd/tests/slack.rs b/crates/chatd/tests/slack.rs index 71f0d32..99a3f1e 100644 --- a/crates/chatd/tests/slack.rs +++ b/crates/chatd/tests/slack.rs @@ -1924,10 +1924,17 @@ fn connecting_asks_little_spaces_what_it_warms_and_a_reconnect_asks_less() { expected.into_iter().map(|(method, count)| (method.to_owned(), count)).collect() }; assert_eq!(world.fake.counted(ALPHA), expected(5, 1, 1)); - // Any two calls are at least a fifth of a second apart, whatever they are. + // Any two calls are at least a fifth of a second apart, whatever they are. A loaded + // machine can hold one send back and let the next overtake it, so one close pair is + // allowed; a missing pacer bunches them all. let mut times: Vec = all.iter().filter(|call| call.token == ALPHA).map(|call| call.at).collect(); times.sort(); - assert!(times.windows(2).all(|pair| pair[1] - pair[0] >= Duration::from_millis(150))); + let close = times.windows(2).filter(|pair| pair[1] - pair[0] < Duration::from_millis(150)).count(); + assert!( + close <= 1, + "the calls bunch up: {:?}", + times.windows(2).map(|pair| pair[1] - pair[0]).collect::>() + ); // The socket drops. The new connection reads the unread state again and nothing else: no // sidebar, no warming, and the socket's address is not asked for. @@ -2183,6 +2190,17 @@ fn info_mute_pins_files_emoji_threads_and_a_scheduled_send_ask_slack_once_each() assert!(world.fake.calls(ALPHA, "chat.postMessage").is_empty()); } +#[test] +fn search_asks_the_network_only_where_it_can() { + let world = World::start(); + world.ready(); + // A Space on Slack: the adapter cannot search messages yet, and says so in words. Nothing + // is asked of the fake. + let refused = world.ask(Request::SearchNetwork { conversation: None, space: Some(alpha("T01ALPHA")), query: "x".into() }); + assert!(matches!(&refused, Response::Error { detail, .. } if detail.contains("cannot do this yet")), "{refused:?}"); + assert!(world.fake.calls(ALPHA, "search.all").is_empty() && world.fake.calls(ALPHA, "search.messages").is_empty()); +} + #[test] fn files_go_up_from_their_paths_and_the_socket_names_the_message() { let world = World::start(); From 8bf75fc2dd58979b8e51129e0db4d9b1330904dc Mon Sep 17 00:00:00 2001 From: unRekable Date: Tue, 20 Oct 2026 23:21:38 +0200 Subject: [PATCH 5/6] The search network test waits for the search network. The last commit took a test for the Unsupported answer with it, but its request exists only on the branch that adds the network search. It goes back there. --- crates/chatd/tests/slack.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/crates/chatd/tests/slack.rs b/crates/chatd/tests/slack.rs index 99a3f1e..a26bf5b 100644 --- a/crates/chatd/tests/slack.rs +++ b/crates/chatd/tests/slack.rs @@ -2190,17 +2190,6 @@ fn info_mute_pins_files_emoji_threads_and_a_scheduled_send_ask_slack_once_each() assert!(world.fake.calls(ALPHA, "chat.postMessage").is_empty()); } -#[test] -fn search_asks_the_network_only_where_it_can() { - let world = World::start(); - world.ready(); - // A Space on Slack: the adapter cannot search messages yet, and says so in words. Nothing - // is asked of the fake. - let refused = world.ask(Request::SearchNetwork { conversation: None, space: Some(alpha("T01ALPHA")), query: "x".into() }); - assert!(matches!(&refused, Response::Error { detail, .. } if detail.contains("cannot do this yet")), "{refused:?}"); - assert!(world.fake.calls(ALPHA, "search.all").is_empty() && world.fake.calls(ALPHA, "search.messages").is_empty()); -} - #[test] fn files_go_up_from_their_paths_and_the_socket_names_the_message() { let world = World::start(); From e53418d50af747a784c97abe0a42b5ab6ae9a7dc Mon Sep 17 00:00:00 2001 From: Justin Scroggins Date: Sat, 24 Oct 2026 12:42:49 -0400 Subject: [PATCH 6/6] Wait for the keychain prompt explicitly before checking its Status change. --- crates/chatd/tests/polish.rs | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/crates/chatd/tests/polish.rs b/crates/chatd/tests/polish.rs index b1e96b7..d6ed62b 100644 --- a/crates/chatd/tests/polish.rs +++ b/crates/chatd/tests/polish.rs @@ -180,8 +180,9 @@ fn the_socket_answers_while_a_keychain_read_waits_and_the_cached_account_stays_o let (accounts, keychain, seq, started) = chatd.status(); let cached = accounts.iter().find(|status| status.key == account).expect("the cached account is listed"); assert_eq!(cached.link, Link::Offline); - // A start slower than the read's 1,500 ms has missed the window: the read is over, the - // account it cached is shown all the same, and that is all this machine gets to see. + // The reads run one network after another, each held for 1,500 ms; a start slower than all + // of them has missed the window: the reads are over, the cached account is shown all the + // same, and that is all this machine gets to see. if !keychain.reading { assert!(keychain.is_settled(), "{keychain:?}"); let (_, _, _, again) = chatd.status(); @@ -193,23 +194,16 @@ fn the_socket_answers_while_a_keychain_read_waits_and_the_cached_account_stays_o assert!(started > 0); // The account whose session is not read yet is shown, offline, not dropped. // A read held up past a second is a prompt on screen: the shell is told as a Status change, - // and the status says which network it waits on. The prompt holds only while the read does, - // so this waits for its word rather than sampling one instant; a machine that reached the - // read late is told of the settling instead. - let mut told; - loop { - told = chatd.changes(seq, Duration::from_millis(1_400)); - if told.contains(&Change::Status) { - break; - } - assert!(!chatd.status().1.is_settled(), "the read settled without a word to the shell"); - } - let (_, keychain, ..) = chatd.status(); - if keychain.waiting == Some(Network::Discord) { - let (accounts, ..) = chatd.status(); + // and the status says which network it waits on. + let waiting = chatd.until("a read waiting on the keychain", || chatd.status().1.waiting); + let (accounts, keychain, ..) = chatd.status(); + assert!(!keychain.is_settled(), "{keychain:?}"); + if waiting == Network::Discord { let cached = accounts.iter().find(|status| status.key == account).unwrap(); assert_eq!(cached.detail, chatd::session::STORE_WAITING); } + let told = chatd.changes(seq, Duration::from_millis(1_400)); + assert!(told.contains(&Change::Status), "{told:?}"); // Requests are served while it waits. assert!(matches!(chatd.ask(Request::Inbox { network: None, filter: InboxFilter::All, offset: 0, limit: 10 }), Response::Conversations { .. }));