1//! The recorded request script: every request is fixed text, so every host is asked the same thing and 2//! its answers can be compared byte for byte. 3//! 4//! The script is restricted to the hub routes (`/memory/sync`, `/household/sync`, `/chat/sync`) plus the 5//! refusals every route shares (wrong method, unknown route, a body that is not a document). It 6//! starts from an empty household and each step's answer depends on the steps before it. 7 8use whiskers_conformance::suite::build::household; 9 10/// One request in the script. 11pub struct Step { 12 pub name: &'static str, 13 pub method: &'static str, 14 pub path: &'static str, 15 pub body: String, 16 /// The status every host must answer (checked in-process by this crate's tests against the reference service). 17 pub expect: u16, 18} 19 20fn post(name: &'static str, path: &'static str, body: impl Into<String>) -> Step { 21 let name_says_refused = name.contains("not JSON") || name.contains("empty body") || name.contains("wrong shape") || name.contains("limit"); 22 let expect = if name.starts_with("refusal: an unknown") || name.starts_with("refusal: the root") { 404 } else if name_says_refused { 400 } else { 200 }; 23 Step { name, method: "POST", path, body: body.into(), expect } 24} 25 26/// A fact as a device writes it, by hand, so the keys are in the order a device sends them. 27fn fact(gid: &str, text: &str, embedding: &str) -> String { 28 format!( 29 r#"{{"id":0,"text":{},"learned_at_ms":5,"kind":"Other","who":[],"place":null,"when":null,"pictures":[],"embedding":{embedding},"gid":"{gid}"}}"#, 30 serde_json::to_string(text).expect("text") 31 ) 32} 33 34fn memory(facts: &[String], forgotten: &[&str]) -> String { 35 let forgotten: Vec<String> = forgotten.iter().map(|g| format!("\"{g}\"")).collect(); 36 format!(r#"{{"facts":[{}],"forgotten":[{}]}}"#, facts.join(","), forgotten.join(",")) 37} 38 39fn chat(version: u64, active: u64, summary: &str) -> String { 40 format!(r#"{{"summary":"{summary}","turns":[{{"speaker":"Child","text":"hi"}},{{"speaker":"Whiskers","text":"hello"}}],"last_active_ms":{active},"version":{version}}}"#) 41} 42 43pub fn script() -> Vec<Step> { 44 let a = fact("g1", "Biscuit is a bunny", "[]"); 45 let b = fact("g2", "she loves purple", "[]"); 46 let dup = fact("g3", "BISCUIT is a bunny", "[]"); 47 let embedded = fact("g1", "Biscuit is a bunny", "[0.1,1e-7,16777216.0,-3.4028235e38,0.30000001192092896]"); 48 let accents = fact("g4", "Nana lives by the sea \u{1F30A} caf\u{e9} \u{65e5}\u{672c}", "[]"); 49 let long = fact("g5", &"a long thing she said ".repeat(5000), "[]"); 50 let mut steps = vec![ 51 post("memory: an empty household is empty", "/memory/sync", memory(&[], &[])), 52 post("memory: device A teaches two facts", "/memory/sync", memory(&[a.clone(), b.clone()], &[])), 53 post("memory: device B learns the same words twice and forgets g2", "/memory/sync", memory(&[dup], &["g2"])), 54 post("memory: device A repeats itself (idempotent)", "/memory/sync", memory(&[a.clone(), b.clone()], &[])), 55 post("memory: a forgotten fact does not come back from a device that has it", "/memory/sync", memory(&[b], &[])), 56 post("memory: an embedding computed on one device serves all", "/memory/sync", memory(&[embedded], &[])), 57 post("memory: a second embedding is ignored", "/memory/sync", memory(&[fact("g1", "Biscuit is a bunny", "[9.5]")], &[])), 58 post("memory: a fact with no identity is ignored", "/memory/sync", memory(&[fact("", "no identity yet", "[]")], &[])), 59 post("memory: accents, emoji and kana are kept byte for byte", "/memory/sync", memory(&[accents], &[])), 60 post("memory: a long fact (110 KB)", "/memory/sync", memory(&[long], &[])), 61 post("memory: a forgotten identity never held", "/memory/sync", memory(&[], &["g99"])), 62 post("memory: read it all back", "/memory/sync", memory(&[], &[])), 63 post("memory: unknown fields are ignored", "/memory/sync", r#"{"facts":[],"forgotten":[],"extra":1}"#), 64 post("memory: not JSON", "/memory/sync", "this is not json"), 65 post("memory: empty body", "/memory/sync", ""), 66 post("memory: JSON of the wrong shape", "/memory/sync", r#"{"facts":"nope"}"#), 67 post("household: an empty household's choices", "/household/sync", serde_json::to_string(&household(0, 0, 0, 0, &[])).expect("json")), 68 post("household: the grown-ups choose (config at 10 ms)", "/household/sync", serde_json::to_string(&household(10, 150, 3, 1, &[("tablet", 60_000)])).expect("json")), 69 post("household: an older choice loses", "/household/sync", serde_json::to_string(&household(5, 999, 9, 1, &[("phone", 30_000)])).expect("json")), 70 post("household: a later day wins", "/household/sync", serde_json::to_string(&household(10, 150, 3, 2, &[("tablet", 1_000)])).expect("json")), 71 post("household: repeat", "/household/sync", serde_json::to_string(&household(10, 150, 3, 2, &[("tablet", 1_000)])).expect("json")), 72 post("chat: the first copy", "/chat/sync", chat(2, 5, "kept")), 73 post("chat: fewer changes loses even if later", "/chat/sync", chat(1, 99, "old")), 74 post("chat: a tie goes to the more recently active", "/chat/sync", chat(2, 9, "later")), 75 post("chat: repeat", "/chat/sync", chat(2, 9, "later")), 76 post("chat: not JSON", "/chat/sync", "{"), 77 ]; 78 steps.push(Step { name: "refusal: GET is not served", method: "GET", path: "/memory/sync", body: String::new(), expect: 405 }); 79 steps.push(Step { name: "refusal: PUT is not served", method: "PUT", path: "/memory/sync", body: String::new(), expect: 405 }); 80 steps.push(post("refusal: an unknown route", "/memory/nope", "{}")); 81 steps.push(post("refusal: the root", "/", "{}")); 82 steps.push(post("refusal: a body over the limit (8 MiB + 1)", "/memory/sync", "x".repeat(8 * 1024 * 1024 + 1))); 83 steps 84} 85 86#[cfg(test)] 87mod tests { 88 use super::*; 89 90 #[test] 91 fn the_script_is_fixed_text_and_every_step_is_named_once() { 92 let steps = script(); 93 assert!(steps.len() >= 30); 94 let mut names: Vec<_> = steps.iter().map(|s| s.name).collect(); 95 names.sort_unstable(); 96 names.dedup(); 97 assert_eq!(names.len(), steps.len()); 98 // Fixed: asking twice builds the same bytes. 99 let again = script(); 100 assert!(steps.iter().zip(&again).all(|(a, b)| a.body == b.body && a.path == b.path)); 101 } 102 103 #[test] 104 fn the_reference_service_answers_every_step_as_the_script_says() { 105 use whiskers_conformance::MemBackend; 106 use whiskers_ports::run_ready; 107 use whiskers_service::routes::{ChatSync, HouseholdSync, MemorySync}; 108 use whiskers_service::{Method, Request, Service}; 109 // The routes the Worker serves, and no others: the script is the same on every host. 110 let service = Service::new(MemBackend::default()).serve(MemorySync).serve(HouseholdSync).serve(ChatSync); 111 for step in script() { 112 let method = if step.method == "POST" { Method::Post } else { Method::Other(step.method.into()) }; 113 let r = run_ready(service.handle(Request { method, ..Request::post(step.path, step.body.clone()) })); 114 assert_eq!(r.status, step.expect, "{}", step.name); 115 } 116 } 117 118 #[test] 119 fn the_documents_in_the_script_are_documents() { 120 for s in script().iter().filter(|s| s.name.starts_with("memory: ") && !s.name.contains("not JSON") && !s.name.contains("empty body") && !s.name.contains("wrong shape") && !s.name.contains("limit")) { 121 let v: serde_json::Value = serde_json::from_str(&s.body).unwrap_or_else(|e| panic!("{}: {e}", s.name)); 122 assert!(v.get("facts").is_some(), "{}", s.name); 123 } 124 } 125}