lmjtfy.git / apps / lmjtfy / src / view / tests.rs
tests.rsannotatedtests.rssource806 lines · 41.5 KB · raw
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("&lt;script&gt;"));
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">&quot;model&quot;</span>: <span class="s">&quot;jev-1.13.0&quot;</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&#39;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\">&quot;model&quot;</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("&quot;", "\"").replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&");
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 &lt;down&gt;</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&#39;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("&lt;hot dog&gt;") && 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&#39;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 &lt;b&gt;this&lt;/b&gt; 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 &lt;b&gt;").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={}&amp;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&amp;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 &lt;AI&gt; 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 &lt;b&gt; ok &quot;here&quot;?">"#), "{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(&note.replace('\'', "&#39;")), "{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&amp;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}