lmjtfy.git / apps / lmjtfy / src / view / tests.rs
tests.rsannotatedtests.rssource765 lines · 38.1 KB · raw
1use ask::{Judged, Outcome, Sent};
2use jev_protocol::{ChoiceAnswer, ScoreAnswer, Usage};
3use llm::{Draft, Opt, ToolCall};
4
5use super::*;

An answer as it was kept before its numbers were.

8fn said(headline: &str) -> Answer {
9    Answer { headline: headline.into(), detail: Detail::Unknown }
10}

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}

An answer to one question, and whether it was sent now or kept.

18type Reply = (Judged, Sent);
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}

A question that went to Jev, answered or still waiting.

29struct Asked(&'static str, Option<Reply>);
31fn jev(kind: &'static str, answer: Option<Reply>) -> Asked {
32    Asked(kind, answer)
33}

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}
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("&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}

Jev's score is the mean over the levels (TypeSafe's docs), so it is the answer, and a spread-out one (confidence 0.08) is asked, not stated: the headline is the level nearest the mean with a question mark, the likeliest 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}
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}

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}
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 { 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 { 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}
268
269#[test]
270fn an_overspent_budget_shows_nothing_left_not_a_negative() {
271    let status = budget::Status { neurons: 10_400.0, counted: budget::Counted::Account, jev_dollars: 0.3 };
272    let html = budget(Some(Shared { status, neurons_per_day: 10_000.0, jev_dollars_per_day: 0.25 })).into_string();
273    assert!(html.contains("0 of 10,000"));
274    assert!(html.contains("$0.0000 of $0.25"));
275    assert!(html.contains("left: 0.0%"));
276}
277
278#[test]
279fn numbers_are_grouped_in_thousands() {
280    assert_eq!(grouped(0.4), "0");
281    assert_eq!(grouped(999.0), "999");
282    assert_eq!(grouped(1000.0), "1,000");
283    assert_eq!(grouped(1_234_567.0), "1,234,567");
284}
285
286#[test]
287fn an_unsure_noul_ends_in_a_question_mark() {
288    let draft = || Draft::Noul { instructions: "Is AI a bit stupid?".into(), yes_means: "y".into(), no_means: "n".into() };
289    for (p_yes, word) in [(0.55, "Yes?"), (0.45, "No?"), (0.7, "Yes."), (0.3, "No."), (0.97, "Yes."), (0.02, "No.")] {
290        let shown = view(vec![tool(Ok(draft()), Some(jev("noul", answered(Judged::Noul(p_yes)))))], Some(Ending::Answered));
291        let html = transcript(&shown).into_string();
292        assert!(html.contains(&format!(r#"<p class="answer">{word}</p>"#)), "{p_yes}: {html}");
293    }
294    // The gate's refusal is a Noul too.
295    let refused = view(Vec::new(), Some(Ending::NotAQuestion { p_yes: 0.4 }));
296    assert!(transcript(&refused).into_string().contains(r#"<p class="answer">No?</p>"#));
297}
298
299#[test]
300fn a_kept_response_says_it_was_not_sent_and_when_it_was_answered() {
301    let draft = Draft::Noul { instructions: "Is it?".into(), yes_means: "y".into(), no_means: "n".into() };
302    let kept = answered_and(Judged::Noul(0.9), Sent::Before { at_ms: 1_790_971_860_000.0 });
303    let mut shown = view(vec![tool(Ok(draft), Some(jev("noul", kept)))], Some(Ending::Answered));
304    shown.llm = Some(LlmCall {
305        model: "@cf/x",
306        request: "{}".into(),
307        outcome: Some(LlmOutcome {
308            body: "{}".into(),
309            neurons: 16.0,
310            took: Duration::from_millis(3000),
311            dropped: 0,
312            sent: Sent::Before { at_ms: 1_790_971_860_000.0 },
313        }),
314        kept: Some(Versions { id: "aaaaaaaaaaaa".into(), version: 2, versions: 3 }),
315    });
316    let html = transcript(&shown).into_string();
317    // An old answer of three: a step each way, and ↻.
318    assert!(html.contains("answer 2 of 3"), "{html}");
319    assert!(html.contains("aaaaaaaaaaaa:1'") && html.contains("aaaaaaaaaaaa:3'") && html.contains("aaaaaaaaaaaa:fresh'"), "{html}");
320    assert_eq!(shown.pinned(), "aaaaaaaaaaaa:2");
321    assert_eq!(html.matches("not sent · answered 2026-10-02 20:11 UTC").count(), 2, "{html}");
322    // The answer and the response body are shown as for a call just made.
323    assert!(html.contains(r#"<p class="answer">Yes.</p>"#));
324    assert!(html.contains(">response</span>"));
325    assert!(!html.contains("3000 ms"));
326}
327
328#[test]
329fn the_feed_links_each_question_and_escapes_it() {
330    let entries = [
331        Entry { input: "is <b>this</b> ok?".into(), answers: vec![said("No?")], asked_ms: 1_790_971_860_000.0, times: 1 },
332        Entry { input: "best editor".into(), answers: vec![said("Vim."), said("Yes.")], asked_ms: 0.0, times: 12 },
333    ];
334    let home = Home { lately: entries.to_vec(), most: vec![entries[1].clone()], stats: Stats::default() };
335    let html = feed(Some(&home)).into_string();
336    assert!(html.contains("most asked"));
337    assert_eq!(html.matches("×12").count(), 2);
338    assert!(!html.contains("×1<"));
339    assert!(html.contains(r#"href="/?q=is+%3Cb%3Ethis%3C%2Fb%3E+ok%3F""#), "{html}");
340    assert!(html.contains("is &lt;b&gt;this&lt;/b&gt; ok?"));
341    assert!(!html.contains("<b>"));
342    assert!(html.contains(r#"<span class="said-s">Vim. Yes.<span class="times"> ×12</span></span>"#), "{html}");
343    assert!(html.find("is &lt;b&gt;").unwrap() < html.find("best editor").unwrap());
344}
345
346#[test]
347fn an_empty_or_unread_feed_is_still_a_place_to_put_the_first_line() {
348    for empty in [feed(None), feed(Some(&Home::default()))] {
349        let html = empty.into_string();
350        assert!(html.contains(r#"<ol class="feed" id="lately"><li id="more" hidden></li></ol>"#), "{html}");
351        assert!(!html.contains("class=\"entry\""), "the style sheet hides a feed with no line: {html}");
352    }
353}
354
355fn asked(n: usize) -> Entry {
356    Entry { input: format!("question {n}?"), answers: vec![said("Yes.")], asked_ms: 1_790_000_000_000.0 - n as f64, times: 1 }
357}
358
359#[test]
360fn a_full_page_of_the_feed_asks_for_the_page_after_its_last_line_once() {
361    let lately: Vec<Entry> = (0..::archive::FEED as usize).map(asked).collect();
362    let html = feed(Some(&Home { lately, ..Home::default() })).into_string();
363    let last = ::archive::FEED - 1;
364    assert!(
365        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))),
366        "{html}"
367    );
368}
369
370#[test]
371fn a_short_page_is_the_end_of_the_feed_and_asks_nothing() {
372    let (lines, end) = older(&[asked(1), asked(2)]);
373    assert_eq!(lines.into_string().matches("<li class=\"entry\"").count(), 2);
374    assert_eq!(end.into_string(), r#"<li id="more" hidden></li>"#);
375    assert_eq!(older(&[]).1.into_string(), r#"<li id="more" hidden></li>"#);
376}
377
378#[test]
379fn a_question_has_one_id_in_asked_lately_and_none_in_most_asked() {
380    let entry = asked(7);
381    let home = Home { lately: vec![entry.clone()], most: vec![entry.clone()], stats: Stats::default() };
382    let html = feed(Some(&home)).into_string();
383    let top = lately_top(&entry).into_string();
384    let id = top.split("id=\"").nth(1).and_then(|rest| rest.split('"').next()).expect("an id");
385    assert!(id.starts_with("q-") && id.len() == 18, "{id}");
386    assert_eq!(html.matches(&format!("id=\"{id}\"")).count(), 1, "{html}");
387    assert!(top.starts_with(r#"<ol hidden data-prepend="lately">"#), "{top}");
388    assert_ne!(lately_top(&asked(8)).into_string(), top);
389}
390
391#[test]
392fn the_feed_gets_every_answer_in_a_few_words() {
393    let noul = Draft::Noul { instructions: "Is it?".into(), yes_means: "y".into(), no_means: "n".into() };
394    let pick = ChoiceAnswer { choice: "Vim".into(), confidence: 0.9, probabilities: vec![("Vim".into(), 0.9), ("Emacs".into(), 0.1)] };
395    let shown = view(
396        vec![
397            tool(Ok(noul.clone()), Some(jev("noul", answered(Judged::Noul(0.55))))),
398            tool(Ok(noul.clone()), Some(jev("choice", answered(Judged::Choice(pick))))),
399            tool(Err("not a question".into()), None),
400        ],
401        Some(Ending::Answered),
402    );
403    assert_eq!(headlines(&shown), ["Yes?", "Vim."]);
404}

Marks the input as one answerable question read as kind, with Jev's own answers in hand.

408fn read_as(shown: &mut View, kinds: &[Kind]) {
409    use rules::Value;
410    shown.known.learn(Fact::Answerable, Value::Bool(true));
411    shown.known.learn(Fact::Several, Value::Bool(false));
412    shown.known.learn(Fact::Scale, Value::Bool(false));
413    shown.known.learn(Fact::Yes, Value::Given);
414    shown.known.learn(Fact::Degree, Value::Given);
415    shown.known.learn(Fact::Chance, Value::Given);
416    for kind in Kind::ALL {
417        shown.known.learn(Fact::Reads(kind), Value::Bool(kinds.contains(&kind)));
418    }
419}
421fn facts(answers: Vec<(Fact, Judged)>) -> JevCall {
422    let kind = |judged: &Judged| match judged {
423        Judged::Noul(_) => "noul",
424        Judged::Choice(_) => "choice",
425        Judged::Score(_) => "score",
426    };
427    let parts = answers.iter().map(|(fact, judged)| (ask::question_id(*fact).to_owned(), kind(judged))).collect();
428    call("facts", parts, Some(outcome(answers.into_iter().map(|(_, judged)| judged).collect(), Sent::Now)))
429}
430
431#[test]
432fn a_direct_answer_is_the_input_answered_by_jev_with_no_llm() {
433    let mut shown = View::new("is water wet?".into());
434    shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.95)), (Fact::Yes, Judged::Noul(0.88))]));
435    read_as(&mut shown, &[Kind::Noul]);
436    shown.ending = Some(Ending::Answered);
437    let html = transcript(&shown).into_string();
438    assert!(html.contains(r#"<p class="asked">is water wet?</p>"#), "{html}");
439    assert!(html.contains(r#"<p class="answer">Yes.</p>"#));
440    assert!(html.contains("p(yes) = 0.88"));
441    assert!(html.contains("Was that so hard?"));
442    assert!(html.contains("Send it to them:"));
443    // The diagram names the LLM; no call to it is shown.
444    assert!(!html.contains("neurons") && !html.contains("the LLM's tool call"));
445    assert_eq!(headlines(&shown), ["Yes."]);
446}
447
448#[test]
449fn a_request_says_how_many_questions_it_carries() {
450    let mut shown = View::new("is water wet?".into());
451    shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.95)), (Fact::Yes, Judged::Noul(0.88))]));
452    let html = transcript(&shown).into_string();
453    assert!(html.contains("jev · facts · 2 questions"), "{html}");
454}
455
456#[test]
457fn the_rules_are_drawn_with_what_is_known_marked() {
458    let mut shown = View::new("is water wet?".into());
459    shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.1))]));
460    shown.known.learn(Fact::Answerable, rules::Value::Bool(false));
461    shown.fired = vec![0];
462    shown.ending = Some(Ending::NotAQuestion { p_yes: 0.1 });
463    let html = transcript(&shown).into_string();
464    assert!(html.contains("rules · rete"));
465    assert!(html.contains(r#"<span class="meta">1 refuse</span>"#), "{html}");
466    assert!(!shown.answered());
467    // Jev's own number, not the yes or no it was read as.
468    assert!(html.contains(">0.10</text>"));
469}
470
471#[test]
472fn a_tool_call_says_which_question_it_became() {
473    let noul = Draft::Noul { instructions: "Is it?".into(), yes_means: "y".into(), no_means: "n".into() };
474    let sent = tool(Ok(noul), Some(jev("noul", answered(Judged::Noul(0.9)))));
475    let shown = view(vec![sent, tool(Err("bad".into()), None)], Some(Ending::Answered));
476    let html = transcript(&shown).into_string();
477    assert!(html.contains("sent to Jev as q1"));
478    assert!(html.contains("Not sent to Jev: bad"));
479    assert!(html.contains("jev · answers · 1 question<"), "{html}");
480}
481
482#[test]
483fn a_link_unfurls_as_the_question_and_what_jev_said() {
484    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 };
485    let html = page("is AI a bit stupid?", Some(&said), false, "https://x.test", "https://code.x.test", None, None).into_string();
486    assert!(html.contains(r#"<meta property="og:title" content="Q: is AI a bit stupid?">"#), "{html}");
487    assert!(html.contains(r#"<meta property="og:description" content="Jev: No? · Noul · p(yes) = 0.36">"#), "{html}");
488    assert!(html.contains("<title>Q: is AI a bit stupid?</title>"));
489    // The picture's address carries the question and a mark of the answer.
490    let picture = format!("https://x.test/card.png?q=is+AI+a+bit+stupid%3F&amp;a={}", mark("No? · Noul · p(yes) = 0.36"));
491    assert!(html.contains(&format!(r#"<meta property="og:image" content="{picture}">"#)), "{html}");
492    assert!(html.contains(r#"<meta property="og:image:width" content="600">"#));
493    assert_ne!(mark("No?"), mark("Yes?"));
494    assert_ne!(mark("No?"), mark(""));
495}
496
497#[test]
498fn a_link_to_a_question_never_asked_unfurls_without_an_answer() {
499    let html = page("is <b> ok \"here\"?", None, false, "https://x.test", "https://code.x.test", None, None).into_string();
500    assert!(html.contains(r#"<meta property="og:title" content="Q: is &lt;b&gt; ok &quot;here&quot;?">"#), "{html}");
501    assert!(html.contains(r#"<meta property="og:description" content="Let me Jev that for you.">"#));
502}
503
504#[test]
505fn the_home_page_offers_the_clone_from_the_code_host() {
506    use crate::host::{GitRedirect, Host};
507    // As the Worker builds it, in both modes: the code host only once the
508    // redirect is on; until then the page never names an address that has
509    // not been checked.
510    let on = Host::Home.code_origin("https://lmjtfy.fun", GitRedirect::On);
511    let html = page("", None, false, "https://lmjtfy.fun", &on, None, None).into_string();
512    assert!(html.contains("git clone --recurse-submodules https://code.lmjtfy.fun/lmjtfy.git"), "{html}");
513    let off = Host::Home.code_origin("https://lmjtfy.fun", GitRedirect::Off);
514    let html = page("", None, false, "https://lmjtfy.fun", &off, None, None).into_string();
515    assert!(html.contains("git clone --recurse-submodules https://lmjtfy.fun/lmjtfy.git"), "{html}");
516    assert!(html.contains(r#"<a href="https://lmjtfy.fun/lmjtfy.git">Get the code:</a>"#), "{html}");
517    assert!(!html.contains("code.lmjtfy.fun"), "{html}");
518    let html = page("", None, false, "https://lmjtfy.fun", "https://code.lmjtfy.fun", None, None).into_string();
519    assert!(html.contains("git clone --recurse-submodules https://code.lmjtfy.fun/lmjtfy.git"), "{html}");
520    assert!(html.contains(r#"<a href="https://code.lmjtfy.fun/lmjtfy.git">Get the code:</a>"#), "{html}");
521    assert!(!html.contains("https://lmjtfy.fun/lmjtfy.git"), "{html}");
522}
523
524#[test]
525fn the_home_page_unfurls_as_what_the_site_is() {
526    let html = page("", None, false, "https://x.test", "https://code.x.test", None, None).into_string();
527    assert!(html.contains(r#"<meta property="og:title" content="Let Me Jev That For You">"#));
528    assert!(html.contains("og:description\" content=\"Ask Jev anything."));
529    assert!(html.contains(r##"<meta name="theme-color" content="#f386a1">"##));
530    assert!(html.contains(r#"<meta property="og:image" content="https://x.test/card.png">"#));
531}
532
533#[test]
534fn a_how_much_question_answered_directly_shows_the_fixed_scale() {
535    let answer = ScoreAnswer {
536        score: 2.2,
537        confidence: 0.7,
538        probabilities: vec![0.0, 0.1, 0.6, 0.3, 0.0],
539        legend: ask::DEGREES.iter().map(|level| (*level).to_owned()).collect(),
540    };
541    let mut shown = View::new("how spicy is a jalapeño".into());
542    shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.95)), (Fact::Degree, Judged::Score(answer))]));
543    read_as(&mut shown, &[Kind::Score]);
544    shown.ending = Some(Ending::Answered);
545    let html = transcript(&shown).into_string();
546    assert!(html.contains(r#"<p class="answer pick">Moderately.<span class="of"> mean 2.2 / 4</span></p>"#), "{html}");
547    assert_eq!(headlines(&shown), ["Moderately."]);
548    assert!(!html.contains("neurons"));
549}
550
551#[test]
552fn the_tally_says_what_jev_settled_alone_and_what_was_never_sent() {
553    let stats = Stats { questions: 3, asks: 1200, no_llm: 2, sent: 9, kept: 1, clones: 2, pulls: 0 };
554    let html = tally(Some(&stats)).into_string();
555    assert!(html.contains("<b>3 questions</b> answered, asked 1,200 times."), "{html}");
556    assert!(html.contains("<b>67%</b> of them Jev answered alone"));
557    assert!(html.contains("<b>9 requests</b> sent"));
558    assert_eq!(tally(Some(&Stats::default())).into_string(), r#"<div class="" id="tally"></div>"#);
559    assert_eq!(tally(None).into_string(), r#"<div class="" id="tally"></div>"#);
560}
561
562#[test]
563fn asking_too_fast_says_so_and_offers_no_link() {
564    let mut shown = View::new("is it?".into());
565    shown.ending = Some(Ending::Slow);
566    let html = transcript(&shown).into_string();
567    assert!(html.contains("Slow down."));
568    assert!(!html.contains("Send it to them:"));
569}
570
571#[test]
572fn the_playground_makes_every_fact_and_test_a_link() {
573    let network = Network::lmjtfy();
574    let html = playground(&network, &Known::default(), &[], "Ask Jev.", |_| "/rules?x=y".to_owned()).into_string();
575    assert_eq!(html.matches(r#"<a href="/rules?x=y""#).count(), network.alphas.len() + Fact::ALL.len());
576    assert!(html.contains("Ask Jev."));
577    assert!(html.contains("<title>The rules · LMJTFY</title>"));
578}
579
580#[test]
581fn a_question_jev_is_split_on_is_answered_both_ways_and_says_how_each_was_read() {
582    let degree = ScoreAnswer {
583        score: 3.1,
584        confidence: 0.7,
585        probabilities: vec![0.0, 0.0, 0.2, 0.6, 0.2],
586        legend: ask::DEGREES.iter().map(|level| (*level).to_owned()).collect(),
587    };
588    let mut shown = View::new("how likely is rain in November".into());
589    shown.facts = Some(facts(vec![(Fact::Yes, Judged::Noul(0.8)), (Fact::Degree, Judged::Score(degree))]));
590    read_as(&mut shown, &[Kind::Noul, Kind::Score]);
591    shown.ending = Some(Ending::Answered);
592    let html = transcript(&shown).into_string();
593    assert!(html.contains(r#"<span class="read-as"> · read as a yes or no</span>"#), "{html}");
594    assert!(html.contains(r#"<span class="read-as"> · read as a how much</span>"#));
595    assert_eq!(headlines(&shown), ["Yes.", "Very."]);
596
597    // Read one way, there is nothing to say about how.
598    read_as(&mut shown, &[Kind::Noul]);
599    assert!(!transcript(&shown).into_string().contains("read-as"));
600}
601
602#[test]
603fn the_diagram_shows_jevs_probability_for_each_reading() {
604    let kind = ChoiceAnswer {
605        choice: "yes_or_no".into(),
606        confidence: 0.4,
607        probabilities: vec![("yes_or_no".into(), 0.45), ("pick_one".into(), 0.15), ("how_much".into(), 0.4)],
608    };
609    let mut shown = View::new("how likely is rain".into());
610    shown.facts = Some(facts(vec![(Fact::Reads(Kind::Noul), Judged::Choice(kind))]));
611    read_as(&mut shown, &[Kind::Noul, Kind::Score]);
612    let html = transcript(&shown).into_string();
613    for p in [">0.45</text>", ">0.15</text>", ">0.40</text>"] {
614        assert!(html.contains(p), "{p}: {html}");
615    }
616    assert_eq!(shown.reading(), Some(Kind::Noul));
617}
618
619#[test]
620fn a_how_likely_question_is_answered_with_the_probability_and_says_it_is_a_noul() {
621    let mut shown = View::new("what are the chances of rain?".into());
622    shown.facts = Some(facts(vec![(Fact::Answerable, Judged::Noul(0.9)), (Fact::Chance, Judged::Noul(0.28))]));
623    read_as(&mut shown, &[Kind::Chance]);
624    shown.ending = Some(Ending::Answered);
625    let html = transcript(&shown).into_string();
626    assert!(html.contains(r#"<p class="answer">28%.</p>"#), "{html}");
627    let note = "Noul · p = 0.28 · Jev thinks that's unlikely";
628    assert!(html.contains(note) || html.contains(&note.replace('\'', "&#39;")), "{html}");
629    // Not a yes or a no, and nobody rewrote the question.
630    assert!(!html.contains(">No.<") && !html.contains("neurons"));
631    assert_eq!(shown.said(), [Answer { headline: "28%.".into(), detail: Detail::Chance { p: 0.28 } }]);
632}
633
634#[test]
635fn what_is_kept_of_an_answer_has_its_numbers() {
636    let pick = ChoiceAnswer { choice: "Vim".into(), confidence: 0.9, probabilities: vec![("Emacs".into(), 0.2), ("Vim".into(), 0.8)] };
637    let draft = Draft::Choice { instructions: "Which?".into(), options: Vec::new() };
638    let shown = view(vec![tool(Ok(draft), Some(jev("choice", answered(Judged::Choice(pick)))))], Some(Ending::Answered));
639    let kept = shown.said();
640    // Most likely first, whatever order they were asked in.
641    let options = vec![("Vim".to_owned(), 0.8), ("Emacs".to_owned(), 0.2)];
642    assert_eq!(kept, [Answer { headline: "Vim.".into(), detail: Detail::Choice { confidence: 0.9, options } }]);
643    assert_eq!(kept[0].line(), "Vim. · Choice · confidence 0.90 · Vim 80%, Emacs 20%");
644}
645
646
647#[test]
648fn the_comment_box_has_a_real_placeholder_a_label_and_a_cap() {
649    let html = comment_box(false).into_string();
650    assert!(html.contains(r#"placeholder="Let us know what we can improve""#), "{html}");
651    assert!(html.contains(r#"<label class="label-s" for="comment-text">Comments?</label>"#), "{html}");
652    assert!(html.contains(r#"maxlength="1000""#), "{html}");
653    assert!(html.contains(r#"data-bind:comment"#) && html.contains("@post('/comment')") && html.contains(">Save<"), "{html}");
654    // Closed it is out of the tab order, and it opens by the page's own signal.
655    assert!(html.contains(r#"data-class:open="$open""#) && html.contains(r#"data-attr:inert="!$open""#), "{html}");
656    // The textarea starts empty: what is typed lives in the signal, so no patch holds it.
657    assert!(html.contains("></textarea>"), "{html}");
658}
659
660#[test]
661fn the_comment_box_for_a_declined_question_asks_why_jev_could_have_answered() {
662    let html = comment_box(true).into_string();
663    assert!(html.contains(r#"placeholder="Why could Jev have answered? Or how can we improve?""#), "{html}");
664    // Everything else about it is the same box.
665    assert!(html.contains(r#"maxlength="1000""#) && html.contains("></textarea>") && html.contains("@post('/comment')"), "{html}");
666    assert!(!comment_box(false).into_string().contains("Why could Jev"));
667}
668
669#[test]
670fn the_thumbs_mean_right_to_pass_and_could_have_answered_under_a_declined_question() {
671    let answered = rating(Some(&archive::Rating { up: 2, down: 1, mine: None, declined: false }), true).into_string();
672    assert!(answered.contains("Was Jev right?") && answered.contains("Jev was wrong"), "{answered}");
673    let declined = rating(Some(&archive::Rating { up: 2, down: 1, mine: Some(archive::Vote::Down), declined: true }), true).into_string();
674    assert!(declined.contains("Was Jev right to pass?"), "{declined}");
675    assert!(declined.contains(r#"title="Jev was right to pass""#) && declined.contains(r#"title="Jev could have answered""#), "{declined}");
676    assert!(declined.contains("$vote = 'up'") && declined.contains("$vote = 'down'") && declined.contains("@post('/rate')"), "{declined}");
677}
678
679#[test]
680fn the_rating_and_the_comment_box_are_under_an_answer_and_a_declined_question() {
681    for ending in [Ending::NotAQuestion { p_yes: 0.1 }, Ending::NoQuestion] {
682        let html = transcript(&view(Vec::new(), Some(ending))).into_string();
683        assert!(html.contains(r#"id="rating""#) && html.contains(r#"id="comment""#), "{html}");
684        assert!(html.contains("Why could Jev have answered?"), "{html}");
685    }
686    let answered = transcript(&view(Vec::new(), Some(Ending::Answered))).into_string();
687    assert!(answered.contains(r#"id="rating""#) && !answered.contains("Why could Jev"), "{answered}");
688    for ending in [Ending::Failed { error: "x".into() }, Ending::Offline, Ending::Slow, Ending::Empty] {
689        assert!(!transcript(&view(Vec::new(), Some(ending))).into_string().contains(r#"id="rating""#));
690    }
691}
692
693#[test]
694fn the_comment_box_is_under_an_answer_and_only_there() {
695    let mut answered = View::new("is it?".into());
696    answered.ending = Some(Ending::Answered);
697    assert!(transcript(&answered).into_string().contains(r#"id="comment""#));
698    let mut offline = View::new("is it?".into());
699    offline.ending = Some(Ending::Offline);
700    assert!(!transcript(&offline).into_string().contains(r#"id="comment""#));
701}
702
703#[test]
704fn why_a_comment_was_not_saved_is_said_in_fixed_words() {
705    use archive::Unsaid;
706    assert!(!comment_error(None).into_string().contains("Write"));
707    for (unsaid, words) in [
708        (Unsaid::Empty, "Write something first."),
709        (Unsaid::TooLong, "over 1000 characters"),
710        (Unsaid::NoVote, "Vote first"),
711        (Unsaid::Limit, "Slow down"),
712        (Unsaid::Failed, "Your text is still here"),
713    ] {
714        let html = comment_error(Some(unsaid)).into_string();
715        assert!(html.contains(words) && html.contains(r#"id="comment-error""#), "{html}");
716    }
717}
718
719#[test]
720fn a_question_jev_cannot_take_suggests_an_llm_and_an_answer_does_not() {
721    let links = |ending| transcript(&view(Vec::new(), Some(ending))).into_string().matches("https://lmmptfy.com/#v3/").count();
722    assert_eq!(links(Ending::NotAQuestion { p_yes: 0.1 }), 3);
723    assert_eq!(links(Ending::NoQuestion), 3);
724    assert_eq!(links(Ending::Answered), 0);
725    // An outage is not a reason to send people away.
726    assert_eq!(links(Ending::Failed { error: "down".into() }), 0);
727    let html = transcript(&view(Vec::new(), Some(Ending::NotAQuestion { p_yes: 0.1 }))).into_string();
728    assert!(html.contains("prompt=what%20is%20the%20best%20editor"), "{html}");
729}
730
731#[test]
732fn an_unanswerable_ending_offers_llms_and_a_card_only_if_one_was_read() {
733    let head = crate::meatproxy::Head { title: "Let Me MeatProxy That for You".into(), image: Some("https://lmmptfy.com/a.png".into()), ..Default::default() };
734    for ending in [Ending::NotAQuestion { p_yes: 0.1 }, Ending::NoQuestion] {
735        let mut shown = View::new("why?".into());
736        shown.ending = Some(ending);
737        let bare = transcript(&shown).into_string();
738        assert!(bare.contains("Please choose an LLM instead:") && bare.contains("class=\"llms\""), "{bare}");
739        assert!(!bare.contains("Provided by") && !bare.contains("linkcard"), "{bare}");
740        shown.preview = Some(head.clone());
741        let carded = transcript(&shown).into_string();
742        assert!(carded.contains("Provided by:") && carded.contains(r#"src="/meatproxy/card""#) && carded.contains("Let Me MeatProxy That for You"), "{carded}");
743    }
744    // Other endings never show one, even if a card were set.
745    let mut failed = View::new("why?".into());
746    failed.ending = Some(Ending::Failed { error: "x".into() });
747    failed.preview = Some(head);
748    assert!(!transcript(&failed).into_string().contains("linkcard"));
749}
750
751#[test]
752fn a_declined_questions_link_unfurls_without_jev_saying_anything() {
753    let html = page("asdf qwerty", None, true, "https://x.test", "https://code.x.test", None, None).into_string();
754    assert!(html.contains(r#"<meta property="og:description" content="Please choose an LLM instead: Claude, ChatGPT or Gemini.">"#), "{html}");
755    assert!(html.contains(r#"<meta property="og:title" content="Q: asdf qwerty">"#), "{html}");
756    assert!(!html.contains("Jev:") && !html.contains("No."), "{html}");
757    let declined_picture = format!("https://x.test/card.png?q=asdf+qwerty&amp;a={}", mark("declined"));
758    assert!(html.contains(&declined_picture), "{html}");
759    // Not declined, or answered since: as before, and another picture address.
760    let asked_before = page("asdf qwerty", None, false, "https://x.test", "https://code.x.test", None, None).into_string();
761    assert!(asked_before.contains("Let me Jev that for you.") && !asked_before.contains(&declined_picture), "{asked_before}");
762    let answered = Entry { input: "asdf qwerty".into(), answers: vec![Answer { headline: "Yes.".into(), detail: Detail::Noul { p_yes: 0.9 } }], asked_ms: 0.0, times: 1 };
763    let html = page("asdf qwerty", Some(&answered), true, "https://x.test", "https://code.x.test", None, None).into_string();
764    assert!(html.contains("Jev: Yes.") && !html.contains("Please choose"), "{html}");
765}