An answer as it was kept before its numbers were.
The few words of each answer, as the feed prints them.
An answer to one question, and whether it was sent now or kept.
18type Reply = (Judged, Sent);
A question that went to Jev, answered or still waiting.
29struct Asked(&'static str, Option<Reply>);
A tool call the LLM made, and the question it became, if it was sent.
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}
A view after the facts were asked for: the tools, and the one request that carries every question that was sent. It is answered only when 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}
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}
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}
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}
Marks the input as one answerable question read as kind, with Jev's
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}
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}