1use ask::{Judged, Outcome, Sent}; 2use jev_protocol::{ChoiceAnswer, ScoreAnswer, Usage}; 3use llm::{Draft, Opt, ToolCall}; 4 5use super::*; 6 7/// An answer as it was kept before its numbers were. 8fn said(headline: &str) -> Answer { 9 Answer { headline: headline.into(), detail: Detail::Unknown } 10} 11 12/// The few words of each answer, as the feed prints them. 13fn headlines(shown: &View) -> Vec<String> { 14 shown.said().into_iter().map(|answer| answer.headline).collect() 15} 16 17/// An answer to one question, and whether it was sent now or kept. 18type Reply = (Judged, Sent); 19 20fn answered(judged: Judged) -> Option<Reply> { 21 answered_and(judged, Sent::Now) 22} 23 24fn answered_and(judged: Judged, sent: Sent) -> Option<Reply> { 25 Some((judged, sent)) 26} 27 28/// A question that went to Jev, answered or still waiting. 29struct Asked(&'static str, Option<Reply>); 30 31fn jev(kind: &'static str, answer: Option<Reply>) -> Asked { 32 Asked(kind, answer) 33} 34 35/// A tool call the LLM made, and the question it became, if it was sent. 36fn tool(draft: Result<Draft, String>, asked: Option<Asked>) -> (ToolCall, Option<Asked>) { 37 let name = draft.as_ref().map_or("jev_noul", Draft::tool).to_owned(); 38 (ToolCall { name, arguments: r#"{"instructions":"q"}"#.into(), draft }, asked) 39} 40 41fn outcome(judged: Vec<Judged>, sent: Sent) -> Outcome { 42 Outcome::Answered { 43 judged, 44 sent, 45 body: r#"{"model":"jev-1.13.0"}"#.into(), 46 request_id: Some("req_1".into()), 47 attempts: 1, 48 usage: Usage { input_tokens: 300, output_tokens: 20 }, 49 took: Duration::from_millis(150), 50 } 51} 52 53fn call(id: &'static str, parts: Vec<(String, &'static str)>, outcome: Option<Outcome>) -> JevCall { 54 JevCall { id, parts, request: r#"{"model":"jev-1.13.0"}"#.into(), outcome, kept: None } 55} 56 57/// A view after the facts were asked for: the tools, and the one request 58/// that carries every question that was sent. It is answered only when 59/// every question in it is. 60fn view(tools: Vec<(ToolCall, Option<Asked>)>, ending: Option<Ending>) -> View { 61 let mut shown = View::new("what is the best editor".into()); 62 shown.facts = Some(call("facts", vec![("answerable".into(), "noul")], None)); 63 let mut parts = Vec::new(); 64 let mut answers = Vec::new(); 65 for (index, (tool, asked)) in tools.into_iter().enumerate() { 66 let id = format!("q{}", index + 1); 67 if let Some(Asked(kind, answer)) = asked { 68 parts.push((id.clone(), kind)); 69 answers.push(answer); 70 } 71 shown.tools.push(Tool { id, call: tool, refused: None }); 72 } 73 if !parts.is_empty() { 74 let sent = answers.iter().flatten().map(|(_, sent)| *sent).next().unwrap_or(Sent::Now); 75 let judged: Option<Vec<Judged>> = answers.into_iter().map(|answer| answer.map(|(judged, _)| judged)).collect(); 76 let answered = judged.is_some(); 77 shown.judging = Some(call("answers", parts, judged.map(|judged| outcome(judged, sent)))); 78 shown.known.learn(Fact::Judged, rules::Value::Bool(answered)); 79 } 80 shown.ending = ending; 81 shown 82} 83 84#[test] 85fn the_input_is_escaped() { 86 let mut shown = View::new("<script>alert(1)</script>".into()); 87 shown.ending = Some(Ending::Offline); 88 let html = transcript(&shown).into_string(); 89 assert!(!html.contains("<script>")); 90 assert!(html.contains("<script>")); 91} 92 93#[test] 94fn the_share_link_round_trips_a_question() { 95 assert_eq!(url_encode("is 2+2 = 4?"), "is+2%2B2+%3D+4%3F"); 96} 97 98#[test] 99fn a_pending_call_shows_its_request_and_no_response() { 100 let html = transcript(&view(Vec::new(), None)).into_string(); 101 assert!(html.contains("waiting for Jev")); 102 assert!(html.contains(r#"<span class="k">"model"</span>: <span class="s">"jev-1.13.0"</span>"#)); 103 assert!(!html.contains(">response</span>")); 104 assert!(html.contains("Step 1")); 105} 106 107#[test] 108fn a_choice_is_drawn_most_likely_first_with_the_winner_marked() { 109 let draft = Draft::Choice { 110 instructions: "Which editor is best?".into(), 111 options: vec![ 112 Opt { label: "Vim".into(), description: "modal".into() }, 113 Opt { label: "Emacs".into(), description: "lisp".into() }, 114 ], 115 }; 116 let answer = ChoiceAnswer { 117 choice: "Emacs".into(), 118 confidence: 0.8, 119 probabilities: vec![("Vim".into(), 0.25), ("Emacs".into(), 0.75)], 120 }; 121 let shown = view(vec![tool(Ok(draft), Some(jev("choice", answered(Judged::Choice(answer)))))], Some(Ending::Answered)); 122 let html = transcript(&shown).into_string(); 123 assert!(html.contains("Which editor is best?")); 124 assert!(html.contains(r#"<p class="answer pick">Emacs.</p>"#)); 125 assert!(html.find("left: 75.0%").unwrap() < html.find("left: 25.0%").unwrap()); 126 assert!(html.contains(r#"<div class="bar won"><span class="label">Emacs</span>"#)); 127 assert!(html.contains("Was that so hard?")); 128} 129 130#[test] 131fn a_score_is_drawn_lowest_first_with_the_nearest_level_marked() { 132 let draft = Draft::Score { instructions: "How good?".into(), levels: vec!["bad".into(), "fine".into(), "good".into()] }; 133 let answer = ScoreAnswer { 134 score: 1.6, 135 confidence: 0.6, 136 probabilities: vec![0.1, 0.2, 0.7], 137 legend: vec!["bad".into(), "fine".into(), "good".into()], 138 }; 139 let shown = view(vec![tool(Ok(draft), Some(jev("score", answered(Judged::Score(answer)))))], Some(Ending::Answered)); 140 let html = transcript(&shown).into_string(); 141 assert!(html.contains(r#"<p class="answer pick">good.<span class="of"> 1.6 / 2</span></p>"#)); 142 assert!(html.contains(r#"<div class="bar won"><span class="label">2 · good</span>"#)); 143 assert!(html.find("0 · bad").unwrap() < html.find("2 · good").unwrap()); 144} 145 146#[test] 147fn a_call_the_llm_got_wrong_is_shown_and_not_sent() { 148 let shown = view(vec![tool(Err("missing field `yes_means`".into()), None)], Some(Ending::NoQuestion)); 149 let html = transcript(&shown).into_string(); 150 assert!(html.contains("Not sent to Jev: missing field `yes_means`")); 151 assert!(html.contains("Jev wasn't asked anything.") || html.contains("Jev wasn't asked anything.")); 152 assert!(!html.contains("Was that so hard?")); 153} 154 155/// The printed JSON, with the markup taken back out, is the same JSON. 156#[test] 157fn pretty_printing_changes_only_whitespace() { 158 let raw = r#"{"model":"jev-1.13.0","questions":{"gate":{"type":"noul","criteria":{"true":"a \"quoted\" <b>","false":""}}},"state":{"input":"é?","n":[0.97,1,true,null,{},[]]}}"#; 159 let html = json(raw).into_string(); 160 assert!(html.contains("\n <span class=\"k\">"model"</span>")); 161 let mut text = String::new(); 162 let mut in_tag = false; 163 for ch in html.chars() { 164 match ch { 165 '<' => in_tag = true, 166 '>' => in_tag = false, 167 _ if !in_tag => text.push(ch), 168 _ => {} 169 } 170 } 171 let text = text.replace(""", "\"").replace("<", "<").replace(">", ">").replace("&", "&"); 172 let shown: Value = serde_json::from_str(&text).unwrap(); 173 assert_eq!(serde_json::to_string(&shown).unwrap(), raw); 174} 175 176#[test] 177fn text_that_is_not_json_is_shown_as_it_is() { 178 assert_eq!(json("upstream <down>").into_string(), "<pre>upstream <down></pre>"); 179} 180 181#[test] 182fn the_live_line_says_whether_jev_can_judge_it_and_what_kind_it_is() { 183 let line = |answerable, kind| live(Some(Glance { answerable, kind })).into_string(); 184 assert!(line(0.97, Some(Kind::Choice)).contains("Jev can judge this · a pick")); 185 assert!(line(0.97, None).contains("Jev can judge this")); 186 let no = line(0.05, Some(Kind::Noul)); 187 assert!(no.contains("can't judge this yet") || no.contains("can't judge this yet")); 188 assert!(!no.contains("a yes or no")); 189 assert_eq!(live(None).into_string(), r#"<p class="live" id="live" aria-live="polite"></p>"#); 190} 191 192#[test] 193fn an_icon_draws_one_square_per_inked_cell() { 194 let inked: usize = icons::JEV.iter().map(|row| row.matches('#').count()).sum(); 195 assert_eq!(icon(&icons::JEV, "").into_string().matches("<rect").count(), inked); 196 for rows in [icons::ASK, icons::JEV, icons::LLM, icons::TOOL] { 197 assert!(rows.iter().all(|row| row.len() == 12)); 198 } 199} 200 201#[test] 202fn the_budget_window_shows_what_is_left_and_whose_count_it_is() { 203 let status = budget::Status { neurons: 1068.2, counted: budget::Counted::Account, jev_dollars: 0.0123 }; 204 let html = budget(Some(Shared { status, neurons_per_day: 10_000.0, jev_dollars_per_day: 0.25 })).into_string(); 205 assert!(html.contains("8,932 of 10,000"), "{html}"); 206 assert!(html.contains("$0.2377 of $0.25")); 207 assert!(html.contains("left: 89.3%")); 208 assert!(html.contains("Cloudflare's own count") || html.contains("Cloudflare's own count")); 209 let own = budget::Status { counted: budget::Counted::Own, ..status }; 210 let html = budget(Some(Shared { status: own, neurons_per_day: 10_000.0, jev_dollars_per_day: 0.25 })).into_string(); 211 assert!(html.contains("this site") && html.contains("own count")); 212 assert!(budget(None).into_string().contains("could not be read")); 213} 214 215#[test] 216fn an_overspent_budget_shows_nothing_left_not_a_negative() { 217 let status = budget::Status { neurons: 10_400.0, counted: budget::Counted::Account, jev_dollars: 0.3 }; 218 let html = budget(Some(Shared { status, neurons_per_day: 10_000.0, jev_dollars_per_day: 0.25 })).into_string(); 219 assert!(html.contains("0 of 10,000")); 220 assert!(html.contains("$0.0000 of $0.25")); 221 assert!(html.contains("left: 0.0%")); 222} 223 224#[test] 225fn numbers_are_grouped_in_thousands() { 226 assert_eq!(grouped(0.4), "0"); 227 assert_eq!(grouped(999.0), "999"); 228 assert_eq!(grouped(1000.0), "1,000"); 229 assert_eq!(grouped(1_234_567.0), "1,234,567"); 230} 231 232#[test] 233fn an_unsure_noul_ends_in_a_question_mark() { 234 let draft = || Draft::Noul { instructions: "Is AI a bit stupid?".into(), yes_means: "y".into(), no_means: "n".into() }; 235 for (p_yes, word) in [(0.55, "Yes?"), (0.45, "No?"), (0.7, "Yes."), (0.3, "No."), (0.97, "Yes."), (0.02, "No.")] { 236 let shown = view(vec![tool(Ok(draft()), Some(jev("noul", answered(Judged::Noul(p_yes)))))], Some(Ending::Answered)); 237 let html = transcript(&shown).into_string(); 238 assert!(html.contains(&format!(r#"<p class="answer">{word}</p>"#)), "{p_yes}: {html}"); 239 } 240 // The gate's refusal is a Noul too. 241 let refused = view(Vec::new(), Some(Ending::NotAQuestion { p_yes: 0.4 })); 242 assert!(transcript(&refused).into_string().contains(r#"<p class="answer">No?</p>"#)); 243} 244 245#[test] 246fn a_kept_response_says_it_was_not_sent_and_when_it_was_answered() { 247 let draft = Draft::Noul { instructions: "Is it?".into(), yes_means: "y".into(), no_means: "n".into() }; 248 let kept = answered_and(Judged::Noul(0.9), Sent::Before { at_ms: 1_790_971_860_000.0 }); 249 let mut shown = view(vec![tool(Ok(draft), Some(jev("noul", kept)))], Some(Ending::Answered)); 250 shown.llm = Some(LlmCall { 251 model: "@cf/x", 252 request: "{}".into(), 253 outcome: Some(LlmOutcome { 254 body: "{}".into(), 255 neurons: 16.0, 256 took: Duration::from_millis(3000), 257 dropped: 0, 258 sent: Sent::Before { at_ms: 1_790_971_860_000.0 }, 259 }), 260 kept: Some(Versions { id: "aaaaaaaaaaaa".into(), version: 2, versions: 3 }), 261 }); 262 let html = transcript(&shown).into_string(); 263 // An old answer of three: a step each way, and ↻. 264 assert!(html.contains("answer 2 of 3"), "{html}"); 265 assert!(html.contains("aaaaaaaaaaaa:1'") && html.contains("aaaaaaaaaaaa:3'") && html.contains("aaaaaaaaaaaa:fresh'"), "{html}"); 266 assert_eq!(shown.pinned(), "aaaaaaaaaaaa:2"); 267 assert_eq!(html.matches("not sent · answered 2026-10-02 20:11 UTC").count(), 2, "{html}"); 268 // The answer and the response body are shown as for a call just made. 269 assert!(html.contains(r#"<p class="answer">Yes.</p>"#)); 270 assert!(html.contains(">response</span>")); 271 assert!(!html.contains("3000 ms")); 272} 273 274#[test] 275fn the_feed_links_each_question_and_escapes_it() { 276 let entries = [ 277 Entry { input: "is <b>this</b> ok?".into(), answers: vec![said("No?")], asked_ms: 1_790_971_860_000.0, times: 1 }, 278 Entry { input: "best editor".into(), answers: vec![said("Vim."), said("Yes.")], asked_ms: 0.0, times: 12 }, 279 ]; 280 let home = Home { lately: entries.to_vec(), most: vec![entries[1].clone()], stats: Stats::default() }; 281 let html = feed(Some(&home)).into_string(); 282 assert!(html.contains("most asked")); 283 assert_eq!(html.matches("×12").count(), 2); 284 assert!(!html.contains("×1<")); 285 assert!(html.contains(r#"href="/?q=is+%3Cb%3Ethis%3C%2Fb%3E+ok%3F""#), "{html}"); 286 assert!(html.contains("is <b>this</b> ok?")); 287 assert!(!html.contains("<b>")); 288 assert!(html.contains(r#"<span class="said-s">Vim. Yes.<span class="times"> ×12</span></span>"#), "{html}"); 289 assert!(html.find("is <b>").unwrap() < html.find("best editor").unwrap()); 290} 291 292#[test] 293fn an_empty_or_unread_feed_is_still_a_place_to_put_the_first_line() { 294 for empty in [feed(None), feed(Some(&Home::default()))] { 295 let html = empty.into_string(); 296 assert!(html.contains(r#"<ol class="feed" id="lately"><li id="more" hidden></li></ol>"#), "{html}"); 297 assert!(!html.contains("class=\"entry\""), "the style sheet hides a feed with no line: {html}"); 298 } 299} 300 301fn asked(n: usize) -> Entry { 302 Entry { input: format!("question {n}?"), answers: vec![said("Yes.")], asked_ms: 1_790_000_000_000.0 - n as f64, times: 1 } 303} 304 305#[test] 306fn a_full_page_of_the_feed_asks_for_the_page_after_its_last_line_once() { 307 let lately: Vec<Entry> = (0..::archive::FEED as usize).map(asked).collect(); 308 let html = feed(Some(&Home { lately, ..Home::default() })).into_string(); 309 let last = ::archive::FEED - 1; 310 assert!( 311 html.contains(&format!(r#"<li id="more" data-on-intersect__once="@get('/feed?ms={}&q=question+{last}%3F')">"#, 1_790_000_000_000.0 - f64::from(last))), 312 "{html}" 313 ); 314} 315 316#[test] 317fn a_short_page_is_the_end_of_the_feed_and_asks_nothing() { 318 let (lines, end) = older(&[asked(1), asked(2)]); 319 assert_eq!(lines.into_string().matches("<li class=\"entry\"").count(), 2); 320 assert_eq!(end.into_string(), r#"<li id="more" hidden></li>"#); 321 assert_eq!(older(&[]).1.into_string(), r#"<li id="more" hidden></li>"#); 322} 323 324#[test] 325fn a_question_has_one_id_in_asked_lately_and_none_in_most_asked() { 326 let entry = asked(7); 327 let home = Home { lately: vec![entry.clone()], most: vec![entry.clone()], stats: Stats::default() }; 328 let html = feed(Some(&home)).into_string(); 329 let top = lately_top(&entry).into_string(); 330 let id = top.split("id=\"").nth(1).and_then(|rest| rest.split('"').next()).expect("an id"); 331 assert!(id.starts_with("q-") && id.len() == 18, "{id}"); 332 assert_eq!(html.matches(&format!("id=\"{id}\"")).count(), 1, "{html}"); 333 assert!(top.starts_with(r#"<ol hidden data-prepend="lately">"#), "{top}"); 334 assert_ne!(lately_top(&asked(8)).into_string(), top); 335} 336 337#[test] 338fn the_feed_gets_every_answer_in_a_few_words() { 339 let noul = Draft::Noul { instructions: "Is it?".into(), yes_means: "y".into(), no_means: "n".into() }; 340 let pick = ChoiceAnswer { choice: "Vim".into(), confidence: 0.9, probabilities: vec![("Vim".into(), 0.9), ("Emacs".into(), 0.1)] }; 341 let shown = view( 342 vec![ 343 tool(Ok(noul.clone()), Some(jev("noul", answered(Judged::Noul(0.55))))), 344 tool(Ok(noul.clone()), Some(jev("choice", answered(Judged::Choice(pick))))), 345 tool(Err("not a question".into()), None), 346 ], 347 Some(Ending::Answered), 348 ); 349 assert_eq!(headlines(&shown), ["Yes?", "Vim."]); 350} 351 352/// Marks the input as one answerable question read as `kind`, with Jev's 353/// own answers in hand. 354fn read_as(shown: &mut View, kinds: &[Kind]) { 355 use rules::Value; 356 shown.known.learn(Fact::Answerable, Value::Bool(true)); 357 shown.known.learn(Fact::Several, Value::Bool(false)); 358 shown.known.learn(Fact::Scale, Value::Bool(false)); 359 shown.known.learn(Fact::Yes, Value::Given); 360 shown.known.learn(Fact::Degree, Value::Given); 361 shown.known.learn(Fact::Chance, Value::Given); 362 for kind in Kind::ALL { 363 shown.known.learn(Fact::Reads(kind), Value::Bool(kinds.contains(&kind))); 364 } 365} 366 367fn facts(answers: Vec<(Fact, Judged)>) -> JevCall { 368 let kind = |judged: &Judged| match judged { 369 Judged::Noul(_) => "noul", 370 Judged::Choice(_) => "choice", 371 Judged::Score(_) => "score", 372 }; 373 let parts = answers.iter().map(|(fact, judged)| (ask::question_id(*fact).to_owned(), kind(judged))).collect(); 374 call("facts", parts, Some(outcome(answers.into_iter().map(|(_, judged)| judged).collect(), Sent::Now))) 375} 376 377#[test] 378fn a_direct_answer_is_the_input_answered_by_jev_with_no_llm() { 379 let mut shown = View::new("is water wet?".into()); 380 shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.95)), (Fact::Yes, Judged::Noul(0.88))])); 381 read_as(&mut shown, &[Kind::Noul]); 382 shown.ending = Some(Ending::Answered); 383 let html = transcript(&shown).into_string(); 384 assert!(html.contains(r#"<p class="asked">is water wet?</p>"#), "{html}"); 385 assert!(html.contains(r#"<p class="answer">Yes.</p>"#)); 386 assert!(html.contains("p(yes) = 0.88")); 387 assert!(html.contains("Was that so hard?")); 388 assert!(html.contains("Send it to them:")); 389 // The diagram names the LLM; no call to it is shown. 390 assert!(!html.contains("neurons") && !html.contains("the LLM's tool call")); 391 assert_eq!(headlines(&shown), ["Yes."]); 392} 393 394#[test] 395fn a_request_says_how_many_questions_it_carries() { 396 let mut shown = View::new("is water wet?".into()); 397 shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.95)), (Fact::Yes, Judged::Noul(0.88))])); 398 let html = transcript(&shown).into_string(); 399 assert!(html.contains("jev · facts · 2 questions"), "{html}"); 400} 401 402#[test] 403fn the_rules_are_drawn_with_what_is_known_marked() { 404 let mut shown = View::new("is water wet?".into()); 405 shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.1))])); 406 shown.known.learn(Fact::Answerable, rules::Value::Bool(false)); 407 shown.fired = vec![0]; 408 shown.ending = Some(Ending::NotAQuestion { p_yes: 0.1 }); 409 let html = transcript(&shown).into_string(); 410 assert!(html.contains("rules · rete")); 411 assert!(html.contains(r#"<span class="meta">1 refuse</span>"#), "{html}"); 412 assert!(!shown.answered()); 413 // Jev's own number, not the yes or no it was read as. 414 assert!(html.contains(">0.10</text>")); 415} 416 417#[test] 418fn a_tool_call_says_which_question_it_became() { 419 let noul = Draft::Noul { instructions: "Is it?".into(), yes_means: "y".into(), no_means: "n".into() }; 420 let sent = tool(Ok(noul), Some(jev("noul", answered(Judged::Noul(0.9))))); 421 let shown = view(vec![sent, tool(Err("bad".into()), None)], Some(Ending::Answered)); 422 let html = transcript(&shown).into_string(); 423 assert!(html.contains("sent to Jev as q1")); 424 assert!(html.contains("Not sent to Jev: bad")); 425 assert!(html.contains("jev · answers · 1 question<"), "{html}"); 426} 427 428#[test] 429fn a_link_unfurls_as_the_question_and_what_jev_said() { 430 let said = Entry { input: "is AI a bit stupid?".into(), answers: vec![Answer { headline: "No?".into(), detail: Detail::Noul { p_yes: 0.36 } }], asked_ms: 0.0, times: 1 }; 431 let html = page("is AI a bit stupid?", Some(&said), "https://x.test", None, None).into_string(); 432 assert!(html.contains(r#"<meta property="og:title" content="Q: is AI a bit stupid?">"#), "{html}"); 433 assert!(html.contains(r#"<meta property="og:description" content="Jev: No? · Noul · p(yes) = 0.36">"#), "{html}"); 434 assert!(html.contains("<title>Q: is AI a bit stupid?</title>")); 435 // The picture's address carries the question and a mark of the answer. 436 let picture = format!("https://x.test/card.png?q=is+AI+a+bit+stupid%3F&a={}", mark("No? · Noul · p(yes) = 0.36")); 437 assert!(html.contains(&format!(r#"<meta property="og:image" content="{picture}">"#)), "{html}"); 438 assert!(html.contains(r#"<meta property="og:image:width" content="600">"#)); 439 assert_ne!(mark("No?"), mark("Yes?")); 440 assert_ne!(mark("No?"), mark("")); 441} 442 443#[test] 444fn a_link_to_a_question_never_asked_unfurls_without_an_answer() { 445 let html = page("is <b> ok \"here\"?", None, "https://x.test", None, None).into_string(); 446 assert!(html.contains(r#"<meta property="og:title" content="Q: is <b> ok "here"?">"#), "{html}"); 447 assert!(html.contains(r#"<meta property="og:description" content="Let me Jev that for you.">"#)); 448} 449 450#[test] 451fn the_home_page_unfurls_as_what_the_site_is() { 452 let html = page("", None, "https://x.test", None, None).into_string(); 453 assert!(html.contains(r#"<meta property="og:title" content="Let Me Jev That For You">"#)); 454 assert!(html.contains("og:description\" content=\"Ask Jev anything.")); 455 assert!(html.contains(r##"<meta name="theme-color" content="#f386a1">"##)); 456 assert!(html.contains(r#"<meta property="og:image" content="https://x.test/card.png">"#)); 457} 458 459#[test] 460fn a_how_much_question_answered_directly_shows_the_fixed_scale() { 461 let answer = ScoreAnswer { 462 score: 2.2, 463 confidence: 0.7, 464 probabilities: vec![0.0, 0.1, 0.6, 0.3, 0.0], 465 legend: ask::DEGREES.iter().map(|level| (*level).to_owned()).collect(), 466 }; 467 let mut shown = View::new("how spicy is a jalapeño".into()); 468 shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.95)), (Fact::Degree, Judged::Score(answer))])); 469 read_as(&mut shown, &[Kind::Score]); 470 shown.ending = Some(Ending::Answered); 471 let html = transcript(&shown).into_string(); 472 assert!(html.contains(r#"<p class="answer pick">Moderately.<span class="of"> 2.2 / 4</span></p>"#), "{html}"); 473 assert_eq!(headlines(&shown), ["Moderately."]); 474 assert!(!html.contains("neurons")); 475} 476 477#[test] 478fn the_tally_says_what_jev_settled_alone_and_what_was_never_sent() { 479 let stats = Stats { questions: 3, asks: 1200, no_llm: 2, sent: 9, kept: 1, clones: 2, pulls: 0 }; 480 let html = tally(Some(&stats)).into_string(); 481 assert!(html.contains("<b>3 questions</b> answered, asked 1,200 times."), "{html}"); 482 assert!(html.contains("<b>67%</b> of them Jev answered alone")); 483 assert!(html.contains("<b>9 requests</b> sent")); 484 assert_eq!(tally(Some(&Stats::default())).into_string(), r#"<div class="" id="tally"></div>"#); 485 assert_eq!(tally(None).into_string(), r#"<div class="" id="tally"></div>"#); 486} 487 488#[test] 489fn asking_too_fast_says_so_and_offers_no_link() { 490 let mut shown = View::new("is it?".into()); 491 shown.ending = Some(Ending::Slow); 492 let html = transcript(&shown).into_string(); 493 assert!(html.contains("Slow down.")); 494 assert!(!html.contains("Send it to them:")); 495} 496 497#[test] 498fn the_playground_makes_every_fact_and_test_a_link() { 499 let network = Network::lmjtfy(); 500 let html = playground(&network, &Known::default(), &[], "Ask Jev.", |_| "/rules?x=y".to_owned()).into_string(); 501 assert_eq!(html.matches(r#"<a href="/rules?x=y""#).count(), network.alphas.len() + Fact::ALL.len()); 502 assert!(html.contains("Ask Jev.")); 503 assert!(html.contains("<title>The rules · LMJTFY</title>")); 504} 505 506#[test] 507fn a_question_jev_is_split_on_is_answered_both_ways_and_says_how_each_was_read() { 508 let degree = ScoreAnswer { 509 score: 3.1, 510 confidence: 0.7, 511 probabilities: vec![0.0, 0.0, 0.2, 0.6, 0.2], 512 legend: ask::DEGREES.iter().map(|level| (*level).to_owned()).collect(), 513 }; 514 let mut shown = View::new("how likely is rain in November".into()); 515 shown.facts = Some(facts(vec![(Fact::Yes, Judged::Noul(0.8)), (Fact::Degree, Judged::Score(degree))])); 516 read_as(&mut shown, &[Kind::Noul, Kind::Score]); 517 shown.ending = Some(Ending::Answered); 518 let html = transcript(&shown).into_string(); 519 assert!(html.contains(r#"<span class="read-as"> · read as a yes or no</span>"#), "{html}"); 520 assert!(html.contains(r#"<span class="read-as"> · read as a how much</span>"#)); 521 assert_eq!(headlines(&shown), ["Yes.", "Very."]); 522 523 // Read one way, there is nothing to say about how. 524 read_as(&mut shown, &[Kind::Noul]); 525 assert!(!transcript(&shown).into_string().contains("read-as")); 526} 527 528#[test] 529fn the_diagram_shows_jevs_probability_for_each_reading() { 530 let kind = ChoiceAnswer { 531 choice: "yes_or_no".into(), 532 confidence: 0.4, 533 probabilities: vec![("yes_or_no".into(), 0.45), ("pick_one".into(), 0.15), ("how_much".into(), 0.4)], 534 }; 535 let mut shown = View::new("how likely is rain".into()); 536 shown.facts = Some(facts(vec![(Fact::Reads(Kind::Noul), Judged::Choice(kind))])); 537 read_as(&mut shown, &[Kind::Noul, Kind::Score]); 538 let html = transcript(&shown).into_string(); 539 for p in [">0.45</text>", ">0.15</text>", ">0.40</text>"] { 540 assert!(html.contains(p), "{p}: {html}"); 541 } 542 assert_eq!(shown.reading(), Some(Kind::Noul)); 543} 544 545#[test] 546fn a_how_likely_question_is_answered_with_the_probability_and_says_it_is_a_noul() { 547 let mut shown = View::new("what are the chances of rain?".into()); 548 shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.9)), (Fact::Chance, Judged::Noul(0.28))])); 549 read_as(&mut shown, &[Kind::Chance]); 550 shown.ending = Some(Ending::Answered); 551 let html = transcript(&shown).into_string(); 552 assert!(html.contains(r#"<p class="answer">28%.</p>"#), "{html}"); 553 let note = "Noul · p = 0.28 · Jev thinks that's unlikely"; 554 assert!(html.contains(note) || html.contains(¬e.replace('\'', "'")), "{html}"); 555 // Not a yes or a no, and nobody rewrote the question. 556 assert!(!html.contains(">No.<") && !html.contains("neurons")); 557 assert_eq!(shown.said(), [Answer { headline: "28%.".into(), detail: Detail::Chance { p: 0.28 } }]); 558} 559 560#[test] 561fn what_is_kept_of_an_answer_has_its_numbers() { 562 let pick = ChoiceAnswer { choice: "Vim".into(), confidence: 0.9, probabilities: vec![("Emacs".into(), 0.2), ("Vim".into(), 0.8)] }; 563 let draft = Draft::Choice { instructions: "Which?".into(), options: Vec::new() }; 564 let shown = view(vec![tool(Ok(draft), Some(jev("choice", answered(Judged::Choice(pick)))))], Some(Ending::Answered)); 565 let kept = shown.said(); 566 // Most likely first, whatever order they were asked in. 567 let options = vec![("Vim".to_owned(), 0.8), ("Emacs".to_owned(), 0.2)]; 568 assert_eq!(kept, [Answer { headline: "Vim.".into(), detail: Detail::Choice { confidence: 0.9, options } }]); 569 assert_eq!(kept[0].line(), "Vim. · Choice · confidence 0.90 · Vim 80%, Emacs 20%"); 570} 571