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"> mean 1.6 / 2</span></p>"#)); 142 assert!(html.contains(r#"<div class="bar won"><span class="label">2 · good <span class="tag">likeliest</span></span>"#)); 143 assert!(html.find("0 · bad").unwrap() < html.find("2 · good").unwrap()); 144} 145 146/// Jev's score is the mean over the levels (TypeSafe's docs), so it is the 147/// answer, and a spread-out one (confidence 0.08) is asked, not stated: the 148/// headline is the level nearest the mean with a question mark, the likeliest 149/// level (55%) is tagged, and everything Jev said is on the page. 150#[test] 151fn a_spread_out_score_is_headed_by_its_mean_with_a_question_and_tags_the_likeliest() { 152 let levels = ["None", "A few", "Several", "Many", "Numerous"]; 153 let draft = Draft::Score { instructions: "How many?".into(), levels: levels.iter().map(|l| l.to_string()).collect() }; 154 let answer = ScoreAnswer { 155 score: 1.1, 156 confidence: 0.08, 157 probabilities: vec![0.55, 0.13, 0.09, 0.14, 0.09], 158 legend: levels.iter().map(|l| l.to_string()).collect(), 159 }; 160 let shown = view(vec![tool(Ok(draft), Some(jev("score", answered(Judged::Score(answer)))))], Some(Ending::Answered)); 161 let html = transcript(&shown).into_string(); 162 assert!(html.contains(r#"<p class="answer pick">A few?<span class="of"> mean 1.1 / 4</span></p>"#), "{html}"); 163 assert!(html.contains(r#"<div class="bar won"><span class="label">1 · A few</span>"#), "the headline's level is inked"); 164 assert!(html.contains(r#"<span class="label">0 · None <span class="tag">likeliest</span></span>"#), "{html}"); 165 assert_eq!(html.matches("likeliest").count(), 1); 166 assert!(html.contains("55%") && html.contains("14%") && html.contains("confidence 0.08 · unsure"), "{html}"); 167} 168 169#[test] 170fn a_sure_score_is_stated_flatly_and_inks_the_level_it_tags() { 171 let draft = Draft::Score { instructions: "How good?".into(), levels: vec!["bad".into(), "fine".into(), "good".into()] }; 172 let answer = ScoreAnswer { 173 score: 1.8, 174 confidence: 0.8, 175 probabilities: vec![0.05, 0.1, 0.85], 176 legend: vec!["bad".into(), "fine".into(), "good".into()], 177 }; 178 let shown = view(vec![tool(Ok(draft), Some(jev("score", answered(Judged::Score(answer)))))], Some(Ending::Answered)); 179 let html = transcript(&shown).into_string(); 180 assert!(html.contains(r#"<p class="answer pick">good.<span class="of"> mean 1.8 / 2</span></p>"#), "{html}"); 181 assert!(html.contains(r#"<div class="bar won"><span class="label">2 · good <span class="tag">likeliest</span></span>"#), "{html}"); 182 assert!(!html.contains("unsure")); 183} 184 185#[test] 186fn a_call_the_llm_got_wrong_is_shown_and_not_sent() { 187 let shown = view(vec![tool(Err("missing field `yes_means`".into()), None)], Some(Ending::NoQuestion)); 188 let html = transcript(&shown).into_string(); 189 assert!(html.contains("Not sent to Jev: missing field `yes_means`")); 190 assert!(html.contains("Jev wasn't asked anything.") || html.contains("Jev wasn't asked anything.")); 191 assert!(!html.contains("Was that so hard?")); 192} 193 194/// The printed JSON, with the markup taken back out, is the same JSON. 195#[test] 196fn pretty_printing_changes_only_whitespace() { 197 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,{},[]]}}"#; 198 let html = json(raw).into_string(); 199 assert!(html.contains("\n <span class=\"k\">"model"</span>")); 200 let mut text = String::new(); 201 let mut in_tag = false; 202 for ch in html.chars() { 203 match ch { 204 '<' => in_tag = true, 205 '>' => in_tag = false, 206 _ if !in_tag => text.push(ch), 207 _ => {} 208 } 209 } 210 let text = text.replace(""", "\"").replace("<", "<").replace(">", ">").replace("&", "&"); 211 let shown: Value = serde_json::from_str(&text).unwrap(); 212 assert_eq!(serde_json::to_string(&shown).unwrap(), raw); 213} 214 215#[test] 216fn text_that_is_not_json_is_shown_as_it_is() { 217 assert_eq!(json("upstream <down>").into_string(), "<pre>upstream <down></pre>"); 218} 219 220#[test] 221fn the_live_line_says_whether_jev_can_judge_it_and_what_kind_it_is() { 222 let line = |answerable, kind| live(Some(Glance { answerable, kind, unfinished: false })).into_string(); 223 assert!(line(0.97, Some(Kind::Choice)).contains("Jev can judge this · a pick")); 224 assert!(line(0.97, None).contains("Jev can judge this")); 225 let no = line(0.05, Some(Kind::Noul)); 226 assert!(no.contains("can't judge this yet") || no.contains("can't judge this yet")); 227 assert!(!no.contains("a yes or no")); 228 assert!(no.contains("press Ask for an LLM that can"), "{no}"); 229 assert!(!line(0.97, None).contains("press Ask"), "only when Jev cannot judge it"); 230 assert_eq!(live(None).into_string(), r#"<p class="live" id="live" aria-live="polite"></p>"#); // Half a question is not judged: the line says to go on, and nothing of what Jev made of it. 231 let half = live(Some(Glance { answerable: 0.97, kind: Some(Kind::Noul), unfinished: true })).into_string(); 232 assert!(half.contains("Jev reads this as unfinished") && !half.contains("can judge") && !half.contains("97") && !half.contains("press Ask"), "{half}"); 233} 234 235#[test] 236fn what_is_typed_is_met_with_questions_already_asked() { 237 let asked = Entry { slug: String::new(), shown: true, input: "Is a <hot dog> a sandwich?".into(), answers: Vec::new(), asked_ms: 0.0, times: 3 }; 238 let html = suggested(&[asked]).into_string(); 239 // A link that asks it again; a stranger's words are escaped. 240 assert!(html.contains(r#"id="suggested""#) && html.contains("href=\"/?q=Is") && html.contains("<hot dog>") && html.contains("×3"), "{html}"); 241 // With none, the list is there and empty, so the last ones are cleared. 242 assert_eq!(suggested(&[]).into_string(), r#"<ul class="feed suggested" id="suggested" aria-label="Already asked"></ul>"#); 243} 244 245 246#[test] 247fn an_icon_draws_one_square_per_inked_cell() { 248 let inked: usize = icons::JEV.iter().map(|row| row.matches('#').count()).sum(); 249 assert_eq!(icon(&icons::JEV, "").into_string().matches("<rect").count(), inked); 250 for rows in [icons::ASK, icons::JEV, icons::LLM, icons::TOOL] { 251 assert!(rows.iter().all(|row| row.len() == 12)); 252 } 253} 254 255#[test] 256fn the_budget_window_shows_what_is_left_and_whose_count_it_is() { 257 let status = budget::Status { day: 20_731, neurons: 1068.2, counted: budget::Counted::Account, jev_dollars: 0.0123 }; 258 let html = budget(Some(Shared { status, neurons_per_day: 10_000.0, jev_dollars_per_day: 0.25 })).into_string(); 259 assert!(html.contains("8,932 of 10,000"), "{html}"); 260 assert!(html.contains("$0.2377 of $0.25")); 261 assert!(html.contains("left: 89.3%")); 262 assert!(html.contains("Cloudflare's own count") || html.contains("Cloudflare's own count")); 263 let own = budget::Status { counted: budget::Counted::Own, ..status }; 264 let html = budget(Some(Shared { status: own, neurons_per_day: 10_000.0, jev_dollars_per_day: 0.25 })).into_string(); 265 assert!(html.contains("this site") && html.contains("own count")); 266 assert!(budget(None).into_string().contains("could not be read")); 267 // The reset is the end of the day the figures are of, as a moment. 268 assert!(html.contains(r#"resets <time data-ago datetime="2026-10-06T00:00:00Z">2026-10-06 00:00:00 UTC</time>"#), "{html}"); 269 assert!(!budget(None).into_string().contains("resets")); 270} 271 272#[test] 273fn an_overspent_budget_shows_nothing_left_not_a_negative() { 274 let status = budget::Status { day: 20_731, neurons: 10_400.0, counted: budget::Counted::Account, jev_dollars: 0.3 }; 275 let html = budget(Some(Shared { status, neurons_per_day: 10_000.0, jev_dollars_per_day: 0.25 })).into_string(); 276 assert!(html.contains("0 of 10,000")); 277 assert!(html.contains("$0.0000 of $0.25")); 278 assert!(html.contains("left: 0.0%")); 279} 280 281#[test] 282fn numbers_are_grouped_in_thousands() { 283 assert_eq!(grouped(0.4), "0"); 284 assert_eq!(grouped(999.0), "999"); 285 assert_eq!(grouped(1000.0), "1,000"); 286 assert_eq!(grouped(1_234_567.0), "1,234,567"); 287} 288 289#[test] 290fn an_unsure_noul_ends_in_a_question_mark() { 291 let draft = || Draft::Noul { instructions: "Is AI a bit stupid?".into(), yes_means: "y".into(), no_means: "n".into() }; 292 for (p_yes, word) in [(0.55, "Yes?"), (0.45, "No?"), (0.7, "Yes."), (0.3, "No."), (0.97, "Yes."), (0.02, "No.")] { 293 let shown = view(vec![tool(Ok(draft()), Some(jev("noul", answered(Judged::Noul(p_yes)))))], Some(Ending::Answered)); 294 let html = transcript(&shown).into_string(); 295 assert!(html.contains(&format!(r#"<p class="answer">{word}</p>"#)), "{p_yes}: {html}"); 296 } 297 // The gate's refusal is a Noul too. 298 let refused = view(Vec::new(), Some(Ending::NotAQuestion { p_yes: 0.4 })); 299 assert!(transcript(&refused).into_string().contains(r#"<p class="answer">No?</p>"#)); 300} 301 302#[test] 303fn a_kept_response_says_it_was_not_sent_and_when_it_was_answered() { 304 let draft = Draft::Noul { instructions: "Is it?".into(), yes_means: "y".into(), no_means: "n".into() }; 305 let kept = answered_and(Judged::Noul(0.9), Sent::Before { at_ms: 1_790_971_860_000.0 }); 306 let mut shown = view(vec![tool(Ok(draft), Some(jev("noul", kept)))], Some(Ending::Answered)); 307 shown.llm = Some(LlmCall { 308 model: "@cf/x", 309 request: "{}".into(), 310 outcome: Some(LlmOutcome { 311 body: "{}".into(), 312 neurons: 16.0, 313 took: Duration::from_millis(3000), 314 dropped: 0, 315 sent: Sent::Before { at_ms: 1_790_971_860_000.0 }, 316 }), 317 kept: Some(Versions { id: "aaaaaaaaaaaa".into(), version: 2, versions: 3 }), 318 }); 319 let html = transcript(&shown).into_string(); 320 // An old answer of three: a step each way, and ↻. 321 assert!(html.contains("answer 2 of 3"), "{html}"); 322 assert!(html.contains("aaaaaaaaaaaa:1'") && html.contains("aaaaaaaaaaaa:3'") && html.contains("aaaaaaaaaaaa:fresh'"), "{html}"); 323 assert_eq!(shown.pinned(), "aaaaaaaaaaaa:2"); 324 // When it was answered is a moment the browser rewrites (page.js): in 325 // UTC as it leaves, for a page where no script runs. 326 let answered = r#"not sent · answered <time data-ago datetime="2026-10-02T20:11:00Z">2026-10-02 20:11:00 UTC</time>"#; 327 assert_eq!(html.matches(answered).count(), 2, "{html}"); 328 // The answer and the response body are shown as for a call just made. 329 assert!(html.contains(r#"<p class="answer">Yes.</p>"#)); 330 assert!(html.contains(">response</span>")); 331 assert!(!html.contains("3000 ms")); 332} 333 334#[test] 335fn the_feed_links_each_question_and_escapes_it() { 336 let entries = [ 337 Entry { slug: String::new(), shown: true, input: "is <b>this</b> ok?".into(), answers: vec![said("No?")], asked_ms: 1_790_971_860_000.0, times: 1 }, 338 Entry { slug: String::new(), shown: true, input: "best editor".into(), answers: vec![said("Vim."), said("Yes.")], asked_ms: 0.0, times: 12 }, 339 ]; 340 let home = Home { lately: entries.to_vec(), most: vec![entries[1].clone()], stats: Stats::default() }; 341 let html = feed(Some(&home)).into_string(); 342 assert!(html.contains("most asked")); 343 assert_eq!(html.matches("×12").count(), 2); 344 assert!(!html.contains("×1<")); 345 assert!(html.contains(r#"href="/?q=is+%3Cb%3Ethis%3C%2Fb%3E+ok%3F""#), "{html}"); 346 assert!(html.contains("is <b>this</b> ok?")); 347 assert!(!html.contains("<b>")); 348 assert!(html.contains(r#"<span class="said-s">Vim. Yes.<span class="times"> ×12</span></span>"#), "{html}"); 349 assert!(html.find("is <b>").unwrap() < html.find("best editor").unwrap()); 350} 351 352#[test] 353fn an_empty_or_unread_feed_is_still_a_place_to_put_the_first_line() { 354 for empty in [feed(None), feed(Some(&Home::default()))] { 355 let html = empty.into_string(); 356 assert!(html.contains(r#"<ol class="feed" id="lately"><li id="more" hidden></li></ol>"#), "{html}"); 357 assert!(!html.contains("class=\"entry\""), "the style sheet hides a feed with no line: {html}"); 358 } 359} 360 361fn asked(n: usize) -> Entry { 362 Entry { slug: String::new(), shown: true, input: format!("question {n}?"), answers: vec![said("Yes.")], asked_ms: 1_790_000_000_000.0 - n as f64, times: 1 } 363} 364 365#[test] 366fn a_full_page_of_the_feed_asks_for_the_page_after_its_last_line_once() { 367 let lately: Vec<Entry> = (0..::archive::FEED as usize).map(asked).collect(); 368 let html = feed(Some(&Home { lately, ..Home::default() })).into_string(); 369 let last = ::archive::FEED - 1; 370 assert!( 371 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))), 372 "{html}" 373 ); 374} 375 376#[test] 377fn a_short_page_is_the_end_of_the_feed_and_asks_nothing() { 378 let (lines, end) = older(&[asked(1), asked(2)]); 379 assert_eq!(lines.into_string().matches("<li class=\"entry\"").count(), 2); 380 assert_eq!(end.into_string(), r#"<li id="more" hidden></li>"#); 381 assert_eq!(older(&[]).1.into_string(), r#"<li id="more" hidden></li>"#); 382} 383 384#[test] 385fn a_question_has_one_id_in_asked_lately_and_none_in_most_asked() { 386 let entry = asked(7); 387 let home = Home { lately: vec![entry.clone()], most: vec![entry.clone()], stats: Stats::default() }; 388 let html = feed(Some(&home)).into_string(); 389 let top = lately_top(&entry).into_string(); 390 let id = top.split("id=\"").nth(1).and_then(|rest| rest.split('"').next()).expect("an id"); 391 assert!(id.starts_with("q-") && id.len() == 18, "{id}"); 392 assert_eq!(html.matches(&format!("id=\"{id}\"")).count(), 1, "{html}"); 393 assert!(top.starts_with(r#"<ol hidden data-prepend="lately">"#), "{top}"); 394 assert_ne!(lately_top(&asked(8)).into_string(), top); 395} 396 397#[test] 398fn the_feed_gets_every_answer_in_a_few_words() { 399 let noul = Draft::Noul { instructions: "Is it?".into(), yes_means: "y".into(), no_means: "n".into() }; 400 let pick = ChoiceAnswer { choice: "Vim".into(), confidence: 0.9, probabilities: vec![("Vim".into(), 0.9), ("Emacs".into(), 0.1)] }; 401 let shown = view( 402 vec![ 403 tool(Ok(noul.clone()), Some(jev("noul", answered(Judged::Noul(0.55))))), 404 tool(Ok(noul.clone()), Some(jev("choice", answered(Judged::Choice(pick))))), 405 tool(Err("not a question".into()), None), 406 ], 407 Some(Ending::Answered), 408 ); 409 assert_eq!(headlines(&shown), ["Yes?", "Vim."]); 410} 411 412/// Marks the input as one answerable question read as `kind`, with Jev's 413/// own answers in hand. 414fn read_as(shown: &mut View, kinds: &[Kind]) { 415 use rules::Value; 416 shown.known.learn(Fact::Answerable, Value::Bool(true)); 417 shown.known.learn(Fact::Several, Value::Bool(false)); 418 shown.known.learn(Fact::Scale, Value::Bool(false)); 419 shown.known.learn(Fact::Yes, Value::Given); 420 shown.known.learn(Fact::Degree, Value::Given); 421 shown.known.learn(Fact::Chance, Value::Given); 422 for kind in Kind::ALL { 423 shown.known.learn(Fact::Reads(kind), Value::Bool(kinds.contains(&kind))); 424 } 425} 426 427fn facts(answers: Vec<(Fact, Judged)>) -> JevCall { 428 let kind = |judged: &Judged| match judged { 429 Judged::Noul(_) => "noul", 430 Judged::Choice(_) => "choice", 431 Judged::Score(_) => "score", 432 }; 433 let parts = answers.iter().map(|(fact, judged)| (ask::question_id(*fact).to_owned(), kind(judged))).collect(); 434 call("facts", parts, Some(outcome(answers.into_iter().map(|(_, judged)| judged).collect(), Sent::Now))) 435} 436 437#[test] 438fn a_direct_answer_is_the_input_answered_by_jev_with_no_llm() { 439 let mut shown = View::new("is water wet?".into()); 440 shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.95)), (Fact::Yes, Judged::Noul(0.88))])); 441 read_as(&mut shown, &[Kind::Noul]); 442 shown.ending = Some(Ending::Answered); 443 let html = transcript(&shown).into_string(); 444 assert!(html.contains(r#"<p class="asked">is water wet?</p>"#), "{html}"); 445 assert!(html.contains(r#"<p class="answer">Yes.</p>"#)); 446 assert!(html.contains("p(yes) = 0.88")); 447 assert!(html.contains("Was that so hard?")); 448 assert!(html.contains("Send it to them:")); 449 // The diagram names the LLM; no call to it is shown. 450 assert!(!html.contains("neurons") && !html.contains("the LLM's tool call")); 451 assert_eq!(headlines(&shown), ["Yes."]); 452} 453 454#[test] 455fn a_request_says_how_many_questions_it_carries() { 456 let mut shown = View::new("is water wet?".into()); 457 shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.95)), (Fact::Yes, Judged::Noul(0.88))])); 458 let html = transcript(&shown).into_string(); 459 assert!(html.contains("jev · facts · 2 questions"), "{html}"); 460} 461 462#[test] 463fn the_rules_are_drawn_with_what_is_known_marked() { 464 let mut shown = View::new("is water wet?".into()); 465 shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.1))])); 466 shown.known.learn(Fact::Answerable, rules::Value::Bool(false)); 467 shown.fired = vec![0]; 468 shown.ending = Some(Ending::NotAQuestion { p_yes: 0.1 }); 469 let html = transcript(&shown).into_string(); 470 assert!(html.contains("rules · rete")); 471 assert!(html.contains(r#"<span class="meta">1 refuse</span>"#), "{html}"); 472 assert!(!shown.answered()); 473 // Jev's own number, not the yes or no it was read as. 474 assert!(html.contains(">0.10</text>")); 475} 476 477#[test] 478fn a_tool_call_says_which_question_it_became() { 479 let noul = Draft::Noul { instructions: "Is it?".into(), yes_means: "y".into(), no_means: "n".into() }; 480 let sent = tool(Ok(noul), Some(jev("noul", answered(Judged::Noul(0.9))))); 481 let shown = view(vec![sent, tool(Err("bad".into()), None)], Some(Ending::Answered)); 482 let html = transcript(&shown).into_string(); 483 assert!(html.contains("sent to Jev as q1")); 484 assert!(html.contains("Not sent to Jev: bad")); 485 assert!(html.contains("jev · answers · 1 question<"), "{html}"); 486} 487 488#[test] 489fn a_link_unfurls_as_the_question_and_what_jev_said() { 490 let said = Entry { slug: String::new(), shown: true, 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 }; 491 let html = page("is AI a bit stupid?", Some(&said), false, "https://x.test", "https://code.x.test", None, None).into_string(); 492 assert!(html.contains(r#"<meta property="og:title" content="Q: is AI a bit stupid?">"#), "{html}"); 493 assert!(html.contains(r#"<meta property="og:description" content="Jev: No? · Noul · p(yes) = 0.36">"#), "{html}"); 494 assert!(html.contains("<title>Q: is AI a bit stupid?</title>")); 495 // The picture's address carries the question and a mark of the answer. 496 let picture = format!("https://x.test/card.png?q=is+AI+a+bit+stupid%3F&a={}", mark("No? · Noul · p(yes) = 0.36")); 497 assert!(html.contains(&format!(r#"<meta property="og:image" content="{picture}">"#)), "{html}"); 498 assert!(html.contains(r#"<meta property="og:image:width" content="600">"#)); 499 assert_ne!(mark("No?"), mark("Yes?")); 500 assert_ne!(mark("No?"), mark("")); 501} 502 503#[test] 504fn a_questions_page_has_its_answer_and_its_one_address() { 505 let answer = Answer { headline: "No?".into(), detail: Detail::Noul { p_yes: 0.36 } }; 506 let said = Entry { slug: "is-ai-a-bit-stupid".into(), shown: true, input: "is <AI> a bit stupid?".into(), answers: vec![answer.clone()], asked_ms: 0.0, times: 1 }; 507 let html = page(&said.input, Some(&said), false, "https://x.test", "https://code.x.test", None, None).into_string(); 508 // One address, whichever link led here, and a search engine may keep it. 509 assert!(html.contains(r#"<link rel="canonical" href="https://x.test/q/is-ai-a-bit-stupid">"#), "{html}"); 510 assert!(!html.contains("noindex"), "{html}"); 511 // The answer is in the page as it leaves, for a reader that runs no script. 512 let resting = &html[html.find(r#"<div id="transcript">"#).unwrap()..]; 513 assert!(resting.contains(r#"<span class="q">is <AI> a bit stupid?</span>"#), "{resting}"); 514 assert!(resting.contains(r#"<p class="answer">No?</p><p class="why">No? · Noul · p(yes) = 0.36</p>"#), "{resting}"); 515 516 // A question Jev judged unfit for the feed is for whoever holds the link. 517 let hidden = Entry { shown: false, ..said.clone() }; 518 let html = page(&hidden.input, Some(&hidden), false, "https://x.test", "https://code.x.test", None, None).into_string(); 519 assert!(html.contains(r#"<meta name="robots" content="noindex">"#), "{html}"); 520 // Text nobody was answered about is a page too, and not one to index. 521 let html = page("anything at all", None, false, "https://x.test", "https://code.x.test", None, None).into_string(); 522 assert!(html.contains(r#"<meta name="robots" content="noindex">"#) && !html.contains(r#"rel="canonical""#), "{html}"); 523 assert!(html.contains(r#"<div id="transcript"></div>"#), "{html}"); 524 // The home page is neither. 525 let html = page("", None, false, "https://x.test", "https://code.x.test", None, None).into_string(); 526 // Its footer links the site for what Jev cannot take, where a crawler reads it. 527 assert!(html.contains(r#"there is <a href="https://lmmptfy.com/">Let Me MeatProxy That For You</a>."#), "{html}"); 528 assert!(!html.contains("noindex") && !html.contains(r#"rel="canonical""#), "{html}"); 529 530 // A question is linked by its slug once it has one. 531 assert_eq!(address("best editor?", "best-editor"), "/q/best-editor"); 532 assert_eq!(address("best editor?", ""), "/?q=best+editor%3F"); 533 let link = share("best editor?", "best-editor").into_string(); 534 assert!(link.contains(r#"<div class="share" id="share">"#) || link.contains(r#"<div id="share" class="share">"#), "{link}"); 535 assert!(link.contains(r#"<a id="link" href="/q/best-editor">/q/best-editor</a>"#), "{link}"); 536} 537 538#[test] 539fn a_link_to_a_question_never_asked_unfurls_without_an_answer() { 540 let html = page("is <b> ok \"here\"?", None, false, "https://x.test", "https://code.x.test", None, None).into_string(); 541 assert!(html.contains(r#"<meta property="og:title" content="Q: is <b> ok "here"?">"#), "{html}"); 542 assert!(html.contains(r#"<meta property="og:description" content="Let me Jev that for you.">"#)); 543} 544 545#[test] 546fn the_home_page_offers_the_clone_from_the_code_host() { 547 use crate::host::{GitRedirect, Host}; 548 // As the Worker builds it, in both modes: the code host only once the 549 // redirect is on; until then the page never names an address that has 550 // not been checked. 551 let on = Host::Home.code_origin("https://lmjtfy.fun", GitRedirect::On); 552 let html = page("", None, false, "https://lmjtfy.fun", &on, None, None).into_string(); 553 assert!(html.contains("git clone --recurse-submodules https://code.lmjtfy.fun/lmjtfy.git"), "{html}"); 554 let off = Host::Home.code_origin("https://lmjtfy.fun", GitRedirect::Off); 555 let html = page("", None, false, "https://lmjtfy.fun", &off, None, None).into_string(); 556 assert!(html.contains("git clone --recurse-submodules https://lmjtfy.fun/lmjtfy.git"), "{html}"); 557 assert!(html.contains(r#"<a href="https://lmjtfy.fun/lmjtfy.git">Get the code:</a>"#), "{html}"); 558 assert!(!html.contains("code.lmjtfy.fun"), "{html}"); 559 let html = page("", None, false, "https://lmjtfy.fun", "https://code.lmjtfy.fun", None, None).into_string(); 560 assert!(html.contains("git clone --recurse-submodules https://code.lmjtfy.fun/lmjtfy.git"), "{html}"); 561 assert!(html.contains(r#"<a href="https://code.lmjtfy.fun/lmjtfy.git">Get the code:</a>"#), "{html}"); 562 assert!(!html.contains("https://lmjtfy.fun/lmjtfy.git"), "{html}"); 563} 564 565#[test] 566fn the_home_page_unfurls_as_what_the_site_is() { 567 let html = page("", None, false, "https://x.test", "https://code.x.test", None, None).into_string(); 568 assert!(html.contains(r#"<meta property="og:title" content="Let Me Jev That For You">"#)); 569 assert!(html.contains("og:description\" content=\"Ask Jev anything.")); 570 assert!(html.contains(r##"<meta name="theme-color" content="#f386a1">"##)); 571 assert!(html.contains(r#"<meta property="og:image" content="https://x.test/card.png">"#)); 572} 573 574#[test] 575fn a_how_much_question_answered_directly_shows_the_fixed_scale() { 576 let answer = ScoreAnswer { 577 score: 2.2, 578 confidence: 0.7, 579 probabilities: vec![0.0, 0.1, 0.6, 0.3, 0.0], 580 legend: ask::DEGREES.iter().map(|level| (*level).to_owned()).collect(), 581 }; 582 let mut shown = View::new("how spicy is a jalapeño".into()); 583 shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.95)), (Fact::Degree, Judged::Score(answer))])); 584 read_as(&mut shown, &[Kind::Score]); 585 shown.ending = Some(Ending::Answered); 586 let html = transcript(&shown).into_string(); 587 assert!(html.contains(r#"<p class="answer pick">Moderately.<span class="of"> mean 2.2 / 4</span></p>"#), "{html}"); 588 assert_eq!(headlines(&shown), ["Moderately."]); 589 assert!(!html.contains("neurons")); 590} 591 592#[test] 593fn the_tally_says_what_jev_settled_alone_and_what_was_never_sent() { 594 let stats = Stats { questions: 3, asks: 1200, no_llm: 2, sent: 9, kept: 1, clones: 2, pulls: 0 }; 595 let html = tally(Some(&stats)).into_string(); 596 assert!(html.contains("<b>3 questions</b> answered, asked 1,200 times."), "{html}"); 597 assert!(html.contains("<b>67%</b> of them Jev answered alone")); 598 assert!(html.contains("<b>9 requests</b> sent")); 599 assert_eq!(tally(Some(&Stats::default())).into_string(), r#"<div class="" id="tally"></div>"#); 600 assert_eq!(tally(None).into_string(), r#"<div class="" id="tally"></div>"#); 601} 602 603#[test] 604fn asking_too_fast_says_so_and_offers_no_link() { 605 let mut shown = View::new("is it?".into()); 606 shown.ending = Some(Ending::Slow); 607 let html = transcript(&shown).into_string(); 608 assert!(html.contains("Slow down.")); 609 assert!(!html.contains("Send it to them:")); 610} 611 612#[test] 613fn the_playground_makes_every_fact_and_test_a_link() { 614 let network = Network::lmjtfy(); 615 let html = playground(&network, &Known::default(), &[], "Ask Jev.", |_| "/rules?x=y".to_owned()).into_string(); 616 assert_eq!(html.matches(r#"<a href="/rules?x=y""#).count(), network.alphas.len() + Fact::ALL.len()); 617 assert!(html.contains("Ask Jev.")); 618 assert!(html.contains("<title>The rules · LMJTFY</title>")); 619} 620 621#[test] 622fn a_question_jev_is_split_on_is_answered_both_ways_and_says_how_each_was_read() { 623 let degree = ScoreAnswer { 624 score: 3.1, 625 confidence: 0.7, 626 probabilities: vec![0.0, 0.0, 0.2, 0.6, 0.2], 627 legend: ask::DEGREES.iter().map(|level| (*level).to_owned()).collect(), 628 }; 629 let mut shown = View::new("how likely is rain in November".into()); 630 shown.facts = Some(facts(vec![(Fact::Yes, Judged::Noul(0.8)), (Fact::Degree, Judged::Score(degree))])); 631 read_as(&mut shown, &[Kind::Noul, Kind::Score]); 632 shown.ending = Some(Ending::Answered); 633 let html = transcript(&shown).into_string(); 634 assert!(html.contains(r#"<span class="read-as"> · read as a yes or no</span>"#), "{html}"); 635 assert!(html.contains(r#"<span class="read-as"> · read as a how much</span>"#)); 636 assert_eq!(headlines(&shown), ["Yes.", "Very."]); 637 638 // Read one way, there is nothing to say about how. 639 read_as(&mut shown, &[Kind::Noul]); 640 assert!(!transcript(&shown).into_string().contains("read-as")); 641} 642 643#[test] 644fn the_diagram_shows_jevs_probability_for_each_reading() { 645 let kind = ChoiceAnswer { 646 choice: "yes_or_no".into(), 647 confidence: 0.4, 648 probabilities: vec![("yes_or_no".into(), 0.45), ("pick_one".into(), 0.15), ("how_much".into(), 0.4)], 649 }; 650 let mut shown = View::new("how likely is rain".into()); 651 shown.facts = Some(facts(vec![(Fact::Reads(Kind::Noul), Judged::Choice(kind))])); 652 read_as(&mut shown, &[Kind::Noul, Kind::Score]); 653 let html = transcript(&shown).into_string(); 654 for p in [">0.45</text>", ">0.15</text>", ">0.40</text>"] { 655 assert!(html.contains(p), "{p}: {html}"); 656 } 657 assert_eq!(shown.reading(), Some(Kind::Noul)); 658} 659 660#[test] 661fn a_how_likely_question_is_answered_with_the_probability_and_says_it_is_a_noul() { 662 let mut shown = View::new("what are the chances of rain?".into()); 663 shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.9)), (Fact::Chance, Judged::Noul(0.28))])); 664 read_as(&mut shown, &[Kind::Chance]); 665 shown.ending = Some(Ending::Answered); 666 let html = transcript(&shown).into_string(); 667 assert!(html.contains(r#"<p class="answer">28%.</p>"#), "{html}"); 668 let note = "Noul · p = 0.28 · Jev thinks that's unlikely"; 669 assert!(html.contains(note) || html.contains(¬e.replace('\'', "'")), "{html}"); 670 // Not a yes or a no, and nobody rewrote the question. 671 assert!(!html.contains(">No.<") && !html.contains("neurons")); 672 assert_eq!(shown.said(), [Answer { headline: "28%.".into(), detail: Detail::Chance { p: 0.28 } }]); 673} 674 675#[test] 676fn what_is_kept_of_an_answer_has_its_numbers() { 677 let pick = ChoiceAnswer { choice: "Vim".into(), confidence: 0.9, probabilities: vec![("Emacs".into(), 0.2), ("Vim".into(), 0.8)] }; 678 let draft = Draft::Choice { instructions: "Which?".into(), options: Vec::new() }; 679 let shown = view(vec![tool(Ok(draft), Some(jev("choice", answered(Judged::Choice(pick)))))], Some(Ending::Answered)); 680 let kept = shown.said(); 681 // Most likely first, whatever order they were asked in. 682 let options = vec![("Vim".to_owned(), 0.8), ("Emacs".to_owned(), 0.2)]; 683 assert_eq!(kept, [Answer { headline: "Vim.".into(), detail: Detail::Choice { confidence: 0.9, options } }]); 684 assert_eq!(kept[0].line(), "Vim. · Choice · confidence 0.90 · Vim 80%, Emacs 20%"); 685} 686 687 688#[test] 689fn the_comment_box_has_a_real_placeholder_a_label_and_a_cap() { 690 let html = comment_box(false).into_string(); 691 assert!(html.contains(r#"placeholder="Let us know what we can improve""#), "{html}"); 692 assert!(html.contains(r#"<label class="label-s" for="comment-text">Comments?</label>"#), "{html}"); 693 assert!(html.contains(r#"maxlength="1000""#), "{html}"); 694 assert!(html.contains(r#"data-bind:comment"#) && html.contains("@post('/comment')") && html.contains(">Save<"), "{html}"); 695 // Closed it is out of the tab order, and it opens by the page's own signal. 696 assert!(html.contains(r#"data-class:open="$open""#) && html.contains(r#"data-attr:inert="!$open""#), "{html}"); 697 // The textarea starts empty: what is typed lives in the signal, so no patch holds it. 698 assert!(html.contains("></textarea>"), "{html}"); 699} 700 701#[test] 702fn the_comment_box_for_a_declined_question_asks_why_jev_could_have_answered() { 703 let html = comment_box(true).into_string(); 704 assert!(html.contains(r#"placeholder="Why could Jev have answered? Or how can we improve?""#), "{html}"); 705 // Everything else about it is the same box. 706 assert!(html.contains(r#"maxlength="1000""#) && html.contains("></textarea>") && html.contains("@post('/comment')"), "{html}"); 707 assert!(!comment_box(false).into_string().contains("Why could Jev")); 708} 709 710#[test] 711fn the_thumbs_mean_right_to_pass_and_could_have_answered_under_a_declined_question() { 712 let answered = rating(Some(&archive::Rating { up: 2, down: 1, mine: None, declined: false }), true).into_string(); 713 assert!(answered.contains("Was Jev right?") && answered.contains("Jev was wrong"), "{answered}"); 714 let declined = rating(Some(&archive::Rating { up: 2, down: 1, mine: Some(archive::Vote::Down), declined: true }), true).into_string(); 715 assert!(declined.contains("Was Jev right to pass?"), "{declined}"); 716 assert!(declined.contains(r#"title="Jev was right to pass""#) && declined.contains(r#"title="Jev could have answered""#), "{declined}"); 717 assert!(declined.contains("$vote = 'up'") && declined.contains("$vote = 'down'") && declined.contains("@post('/rate')"), "{declined}"); 718} 719 720#[test] 721fn the_rating_and_the_comment_box_are_under_an_answer_and_a_declined_question() { 722 for ending in [Ending::NotAQuestion { p_yes: 0.1 }, Ending::NoQuestion] { 723 let html = transcript(&view(Vec::new(), Some(ending))).into_string(); 724 assert!(html.contains(r#"id="rating""#) && html.contains(r#"id="comment""#), "{html}"); 725 assert!(html.contains("Why could Jev have answered?"), "{html}"); 726 } 727 let answered = transcript(&view(Vec::new(), Some(Ending::Answered))).into_string(); 728 assert!(answered.contains(r#"id="rating""#) && !answered.contains("Why could Jev"), "{answered}"); 729 for ending in [Ending::Failed { error: "x".into() }, Ending::Offline, Ending::Slow, Ending::Empty] { 730 assert!(!transcript(&view(Vec::new(), Some(ending))).into_string().contains(r#"id="rating""#)); 731 } 732} 733 734#[test] 735fn the_comment_box_is_under_an_answer_and_only_there() { 736 let mut answered = View::new("is it?".into()); 737 answered.ending = Some(Ending::Answered); 738 assert!(transcript(&answered).into_string().contains(r#"id="comment""#)); 739 let mut offline = View::new("is it?".into()); 740 offline.ending = Some(Ending::Offline); 741 assert!(!transcript(&offline).into_string().contains(r#"id="comment""#)); 742} 743 744#[test] 745fn why_a_comment_was_not_saved_is_said_in_fixed_words() { 746 use archive::Unsaid; 747 assert!(!comment_error(None).into_string().contains("Write")); 748 for (unsaid, words) in [ 749 (Unsaid::Empty, "Write something first."), 750 (Unsaid::TooLong, "over 1000 characters"), 751 (Unsaid::NoVote, "Vote first"), 752 (Unsaid::Limit, "Slow down"), 753 (Unsaid::Failed, "Your text is still here"), 754 ] { 755 let html = comment_error(Some(unsaid)).into_string(); 756 assert!(html.contains(words) && html.contains(r#"id="comment-error""#), "{html}"); 757 } 758} 759 760#[test] 761fn a_question_jev_cannot_take_suggests_an_llm_and_an_answer_does_not() { 762 let links = |ending| transcript(&view(Vec::new(), Some(ending))).into_string().matches("https://lmmptfy.com/#v3/").count(); 763 assert_eq!(links(Ending::NotAQuestion { p_yes: 0.1 }), 3); 764 assert_eq!(links(Ending::NoQuestion), 3); 765 assert_eq!(links(Ending::Answered), 0); 766 // An outage is not a reason to send people away. 767 assert_eq!(links(Ending::Failed { error: "down".into() }), 0); 768 let html = transcript(&view(Vec::new(), Some(Ending::NotAQuestion { p_yes: 0.1 }))).into_string(); 769 assert!(html.contains("prompt=what%20is%20the%20best%20editor"), "{html}"); 770} 771 772#[test] 773fn an_unanswerable_ending_offers_llms_and_a_card_only_if_one_was_read() { 774 let head = crate::meatproxy::Head { title: "Let Me MeatProxy That for You".into(), image: Some("https://lmmptfy.com/a.png".into()), ..Default::default() }; 775 for ending in [Ending::NotAQuestion { p_yes: 0.1 }, Ending::NoQuestion] { 776 let mut shown = View::new("why?".into()); 777 shown.ending = Some(ending); 778 let bare = transcript(&shown).into_string(); 779 assert!(bare.contains("Please choose an LLM instead:") && bare.contains("class=\"llms\""), "{bare}"); 780 assert!(!bare.contains("Provided by") && !bare.contains("linkcard"), "{bare}"); 781 shown.preview = Some(head.clone()); 782 let carded = transcript(&shown).into_string(); 783 assert!(carded.contains("Provided by:") && carded.contains(r#"src="/meatproxy/card""#) && carded.contains("Let Me MeatProxy That for You"), "{carded}"); 784 } 785 // Other endings never show one, even if a card were set. 786 let mut failed = View::new("why?".into()); 787 failed.ending = Some(Ending::Failed { error: "x".into() }); 788 failed.preview = Some(head); 789 assert!(!transcript(&failed).into_string().contains("linkcard")); 790} 791 792#[test] 793fn a_declined_questions_link_unfurls_without_jev_saying_anything() { 794 let html = page("asdf qwerty", None, true, "https://x.test", "https://code.x.test", None, None).into_string(); 795 assert!(html.contains(r#"<meta property="og:description" content="Please choose an LLM instead: Claude, ChatGPT or Gemini.">"#), "{html}"); 796 assert!(html.contains(r#"<meta property="og:title" content="Q: asdf qwerty">"#), "{html}"); 797 assert!(!html.contains("Jev:") && !html.contains("No."), "{html}"); 798 let declined_picture = format!("https://x.test/card.png?q=asdf+qwerty&a={}", mark("declined")); 799 assert!(html.contains(&declined_picture), "{html}"); 800 // Not declined, or answered since: as before, and another picture address. 801 let asked_before = page("asdf qwerty", None, false, "https://x.test", "https://code.x.test", None, None).into_string(); 802 assert!(asked_before.contains("Let me Jev that for you.") && !asked_before.contains(&declined_picture), "{asked_before}"); 803 let answered = Entry { slug: String::new(), shown: true, input: "asdf qwerty".into(), answers: vec![Answer { headline: "Yes.".into(), detail: Detail::Noul { p_yes: 0.9 } }], asked_ms: 0.0, times: 1 }; 804 let html = page("asdf qwerty", Some(&answered), true, "https://x.test", "https://code.x.test", None, None).into_string(); 805 assert!(html.contains("Jev: Yes.") && !html.contains("Please choose"), "{html}"); 806}