thread_conformance/
rendezvous.rs1use serde_json::Value;
12
13use crate::{Clause, Severity};
14
15pub fn check_welcome(msg: &Value) -> Vec<Clause> {
19 let is_welcome = msg.get("t").and_then(Value::as_str) == Some("welcome");
20 vec![
21 Clause {
22 name: "welcome tagged t=welcome",
23 severity: Severity::Error,
24 pass: is_welcome,
25 notes: if is_welcome {
26 vec![]
27 } else {
28 vec![format!("t = {:?}", msg.get("t"))]
29 },
30 },
31 Clause {
32 name: "welcome assigns a peer id",
33 severity: Severity::Error,
34 pass: msg.get("id").and_then(Value::as_u64).is_some(),
35 notes: vec!["clients MUST adopt the assigned id before negotiating".into()],
36 },
37 ]
38}
39
40pub fn check_peers(msg: &Value) -> Vec<Clause> {
42 let is_peers = msg.get("t").and_then(Value::as_str) == Some("peers");
43 let all_ids = msg
44 .get("peers")
45 .and_then(Value::as_array)
46 .map(|a| a.iter().all(|p| p.as_u64().is_some()))
47 .unwrap_or(false);
48 vec![
49 Clause {
50 name: "peers tagged t=peers",
51 severity: Severity::Error,
52 pass: is_peers,
53 notes: if is_peers {
54 vec![]
55 } else {
56 vec![format!("t = {:?}", msg.get("t"))]
57 },
58 },
59 Clause {
60 name: "peers is an array of peer ids",
61 severity: Severity::Error,
62 pass: all_ids,
63 notes: vec![],
64 },
65 ]
66}
67
68pub struct RendezvousOutcome {
70 pub connected: bool,
72 pub clauses: Vec<Clause>,
73 pub notes: Vec<String>,
75}
76
77pub async fn probe(url: &str, timeout_ms: u64) -> RendezvousOutcome {
82 use futures_util::{SinkExt, StreamExt};
83 use tokio_tungstenite::tungstenite::Message;
84
85 let mut clauses = Vec::new();
86 let mut notes = Vec::new();
87
88 let ws = match tokio_tungstenite::connect_async(url).await {
89 Ok((ws, _)) => ws,
90 Err(e) => {
91 clauses.push(Clause {
92 name: "rendezvous reachable (WebSocket)",
93 severity: Severity::Error,
94 pass: false,
95 notes: vec![e.to_string()],
96 });
97 return RendezvousOutcome {
98 connected: false,
99 clauses,
100 notes,
101 };
102 }
103 };
104 clauses.push(Clause {
105 name: "rendezvous reachable (WebSocket)",
106 severity: Severity::Error,
107 pass: true,
108 notes: vec![],
109 });
110
111 let (mut write, mut read) = ws.split();
112 let announce =
113 serde_json::json!({ "t": "announce", "peer": 4242, "world": "conformance-probe" });
114 let _ = write.send(Message::Text(announce.to_string())).await;
115
116 let deadline = tokio::time::Duration::from_millis(timeout_ms);
117 let mut saw_welcome = false;
118 let mut saw_peers = false;
119
120 let _ = tokio::time::timeout(deadline, async {
121 while let Some(Ok(msg)) = read.next().await {
122 let Ok(text) = msg.into_text() else { continue };
123 let Ok(v) = serde_json::from_str::<Value>(&text) else {
124 continue;
125 };
126 match v.get("t").and_then(Value::as_str) {
127 Some("welcome") if !saw_welcome => {
128 saw_welcome = true;
129 clauses.extend(check_welcome(&v));
130 clauses.push(Clause {
132 name: "welcome arrives before peers",
133 severity: Severity::Error,
134 pass: !saw_peers,
135 notes: vec![],
136 });
137 }
138 Some("peers") if !saw_peers => {
139 saw_peers = true;
140 clauses.extend(check_peers(&v));
141 if let Some(p) = v.get("peers").and_then(Value::as_array) {
142 notes.push(format!("room reports {} peer(s) present", p.len()));
143 }
144 }
145 _ => {}
146 }
147 if saw_peers {
148 break; }
150 }
151 })
152 .await;
153
154 if !saw_peers {
155 clauses.push(Clause {
156 name: "rendezvous replies peers on announce",
157 severity: Severity::Error,
158 pass: false,
159 notes: vec!["no peers within timeout (wrong path? not a rendezvous?)".into()],
160 });
161 }
162 if !saw_welcome {
163 notes.push(
164 "no welcome{id} — minimal rendezvous, clients keep provisional ids (allowed)".into(),
165 );
166 }
167
168 RendezvousOutcome {
169 connected: true,
170 clauses,
171 notes,
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::clauses_pass;
179 use serde_json::json;
180
181 #[test]
182 fn a_spec_welcome_passes() {
183 let w = json!({ "t": "welcome", "id": 3 });
184 let clauses = check_welcome(&w);
185 assert!(clauses_pass(&clauses), "{clauses:?}");
186 assert!(clauses.iter().all(|c| c.pass));
187 }
188
189 #[test]
190 fn a_welcome_without_an_id_fails() {
191 let clauses = check_welcome(&json!({ "t": "welcome" }));
192 assert!(!clauses_pass(&clauses));
193 }
194
195 #[test]
196 fn spec_peers_messages_pass_including_the_empty_room_and_the_join_delta() {
197 for p in [
198 json!({ "t": "peers", "peers": [] }),
199 json!({ "t": "peers", "peers": [1, 2] }),
200 json!({ "t": "peers", "peers": [7] }),
201 ] {
202 let clauses = check_peers(&p);
203 assert!(clauses_pass(&clauses), "{p} → {clauses:?}");
204 }
205 }
206
207 #[test]
208 fn malformed_peers_fail() {
209 assert!(!clauses_pass(&check_peers(
211 &json!({ "t": "peers", "peers": ["a"] })
212 )));
213 assert!(!clauses_pass(&check_peers(&json!({ "t": "peers" }))));
214 }
215}