"""Offline tests for the Creighton Anatomy Learning Platform (Gemini, Supabase, and the web are stubbed). Run from the app folder, with the lecture PDFs available: LECTURES_DIR=/path/to/Lectures GEMINI_API_KEY=test python3 -m unittest -v test_app """ from __future__ import annotations import io import json import os import unittest import urllib.error import urllib.request from types import SimpleNamespace from unittest import mock os.environ.setdefault("GEMINI_API_KEY", "test") os.environ.pop("SUPABASE_URL", None) os.environ.pop("SUPABASE_KEY", None) import app # noqa: E402 (loads the lecture PDFs) import supabase_store # noqa: E402 import web_sources # noqa: E402 from app import Outcome, SlideRef, TutorReply, WebReply # noqa: E402 class FakeStore: """Records every call the app makes; accounts and messages live in memory.""" def __init__(self, enabled: bool = True) -> None: self.enabled = enabled self.users: dict[str, dict] = {} self.messages: list[dict] = [] self.calls: list[tuple] = [] def config_hint(self): return "" if self.enabled else "Missing Space secret(s): SUPABASE_URL" def get_user(self, user_id): return self.users.get(user_id), None def create_user(self, user_id, email): if user_id in self.users: return None, supabase_store.EXISTS self.users[user_id] = {"user_id": user_id, "email": email} return self.users[user_id], None def load_messages(self, user_id, limit=200): return [m for m in self.messages if m["user_id"] == user_id][-limit:], None def enqueue(self, method, *args): self.calls.append((method,) + args) if method == "append_messages": user_id, session_id, rows = args self.messages += [dict(row, user_id=user_id) for row in rows] def events(self): return [call[3] for call in self.calls if call[0] == "log_event"] def flush(self, timeout=3.0): self.calls.append(("flush",)) return True def first_text_slide_with_figure() -> tuple[app.Slide, app.Slide]: """A slide with text followed by a diagram or image-only slide (exists in every lecture set).""" for lecture in app.LECTURES.values(): for slide, following in zip(lecture.slides, lecture.slides[1:]): if not slide.diagram and not slide.image_only and not slide.section and (following.diagram or following.image_only): return slide, following raise AssertionError("no text slide followed by a figure slide") def signed_in_session(store: FakeStore, user_id: str = "jdoe") -> app.Session: session = app.Session() session.user_id, session.email, session.session_id = user_id, f"{user_id}@example.com", "sess1" return session def drain(generator) -> list[tuple]: return list(generator) class CitationTests(unittest.TestCase): def test_cited_slides_expands_ranges_and_drops_missing(self): lecture = next(iter(app.LECTURES.values())) answer = f"Text (Lecture {lecture.number}, slides 1-3 and 5). More (Lecture 999, slide 1)." self.assertEqual(app._cited_slides(answer), [(lecture.number, 1), (lecture.number, 2), (lecture.number, 3), (lecture.number, 5)]) def test_relevant_slides_keeps_cited_slides_and_their_figures(self): slide, figure = first_text_slide_with_figure() other = app.LECTURES[slide.lecture].slides[-1] answer = f"Fact (Lecture {slide.lecture}, slide {slide.number})." refs = [SlideRef(lecture=slide.lecture, slide=slide.number), SlideRef(lecture=figure.lecture, slide=figure.number), SlideRef(lecture=other.lecture, slide=other.number)] kept = app._relevant_slides(refs, answer) self.assertEqual([(r.lecture, r.slide) for r in kept], [slide.key, figure.key] if other.key != figure.key else [slide.key, figure.key]) def test_relevant_slides_is_empty_without_citations(self): slide, _ = first_text_slide_with_figure() self.assertEqual(app._relevant_slides([SlideRef(lecture=slide.lecture, slide=slide.number)], "No citations here."), []) def test_slides_note_groups_by_lecture(self): first, second = list(app.LECTURES.values())[:2] answer = f"(Lecture {first.number}, slide 2) and (Lecture {first.number}, slide 1) and (Lecture {second.number}, slide 1)" note = app._slides_note(answer) self.assertIn(f"Lecture {first.number} ({first.title}), slides 1, 2", note) self.assertIn(f"Lecture {second.number} ({second.title}), slide 1", note) self.assertEqual(app._slides_note("nothing cited"), "Course lecture slides.") def test_source_note_round_trip(self): content = app._with_source("Answer text", "Course lecture slides.") self.assertEqual(app._split_source(content), ("Answer text", "Course lecture slides.")) self.assertEqual(app._split_source("plain"), ("plain", "")) def test_none_note_variants(self): page = web_sources.Page("Kenhub", "https://www.kenhub.com/en/library/anatomy/retina", "Retina") self.assertIn("(Kenhub: [Retina](https://www.kenhub.com/en/library/anatomy/retina)) do not answer it either", app._none_note([page])) self.assertIn("no matching article was found", app._none_note([])) self.assertTrue(app._none_note([], "Kenhub could not be reached").endswith("(Kenhub could not be reached.)")) class TurnTests(unittest.TestCase): def setUp(self): self.store = FakeStore() self.patches = [mock.patch.object(app, "STORE", self.store)] for patch in self.patches: patch.start() self.slide, self.figure = first_text_slide_with_figure() def tearDown(self): for patch in self.patches: patch.stop() def slides_reply(self, covered=True, extra_slide=None): answer = f"## Answer\nA fact (Lecture {self.slide.lecture}, slide {self.slide.number})." refs = [SlideRef(lecture=self.slide.lecture, slide=self.slide.number), SlideRef(lecture=self.figure.lecture, slide=self.figure.number)] if extra_slide is not None: refs.append(SlideRef(lecture=extra_slide.lecture, slide=extra_slide.number)) return TutorReply(answer=answer if covered else "The slides do not cover this.", slides=refs, covered_by_slides=covered, search_topics=["retina"]) def test_thinking_message_then_answer_from_slides(self): other = app.LECTURES[self.slide.lecture].slides[-1] session = signed_in_session(self.store) with mock.patch.object(app, "gemini_reply", return_value=self.slides_reply(extra_slide=other)): outputs = drain(app.run_turn("ask", "What is it?", session)) self.assertGreaterEqual(len(outputs), 2) pending = outputs[0][0][-1] self.assertEqual(pending["metadata"], {"title": "Thinking", "status": "pending"}) self.assertEqual(outputs[0][0][-2], {"role": "user", "content": "What is it?"}) self.assertFalse(outputs[0][-3]["interactive"]) # Ask disabled while thinking self.assertEqual(len(outputs[0]), len(app.turn_outputs)) final = outputs[-1][0][-1] self.assertNotIn("metadata", final) self.assertTrue(final["content"].endswith(f"slide {self.slide.number}.")) self.assertIn("**Source:** Course lecture slides:", final["content"]) self.assertEqual(outputs[-1][1], "") # question box cleared self.assertEqual(outputs[-1][-1], session.version) # the page learns the chat version it now shows self.assertEqual(session.state["slides"], [[self.slide.lecture, self.slide.number], [self.figure.lecture, self.figure.number]]) self.assertEqual(session.state["topic"], "What is it?") self.assertFalse(session.busy) saved = [c for c in self.store.calls if c[0] == "append_messages"] self.assertEqual(len(saved), 1) rows = saved[0][3] self.assertEqual([r["role"] for r in rows], ["user", "assistant"]) self.assertEqual(rows[1]["source"], "slides") self.assertEqual(self.store.events(), ["ask"]) def test_falls_back_to_reference_pages(self): session = signed_in_session(self.store) page = web_sources.Page("TeachMeAnatomy", "https://teachmeanatomy.info/head/organs/eye/retina/", "The Retina", "text") web = WebReply(answer="Ten layers (TeachMeAnatomy: The Retina).", covered=True, used_urls=[page.url]) with mock.patch.object(app, "gemini_reply", return_value=self.slides_reply(covered=False)), \ mock.patch.object(web_sources, "find_pages", return_value=([page], "")) as find, \ mock.patch.object(app, "gemini_web_reply", return_value=web) as web_call: outputs = drain(app.run_turn("ask", "What are the layers of the retina?", session)) find.assert_called_once_with(["retina"]) self.assertEqual(web_call.call_args[0][0], "Student question: What are the layers of the retina?") self.assertEqual(web_call.call_args[0][2], [page]) progress = [o[0][-1]["content"] for o in outputs[:-1]] self.assertEqual(progress, ["Searching the lecture slides...", "The lecture slides do not cover this request. Checking TeachMeAnatomy and Kenhub..."]) final = outputs[-1][0][-1]["content"] self.assertTrue(final.startswith("Ten layers (TeachMeAnatomy: The Retina).")) self.assertIn("Answered from TeachMeAnatomy: [The Retina](https://teachmeanatomy.info/head/organs/eye/retina/).", final) self.assertEqual(session.state["slides"], []) self.assertIsNone(session.state["view"]) self.assertEqual(session.state["history"][-1]["text"], web.answer) rows = [c for c in self.store.calls if c[0] == "append_messages"][0][3] self.assertEqual(rows[1]["source"], "web") def test_reports_when_no_reference_page_matches(self): session = signed_in_session(self.store) with mock.patch.object(app, "gemini_reply", return_value=self.slides_reply(covered=False)), \ mock.patch.object(web_sources, "find_pages", return_value=([], "Kenhub could not be reached")), \ mock.patch.object(app, "gemini_web_reply") as web_call: outputs = drain(app.run_turn("ask", "Unknown thing?", session)) web_call.assert_not_called() final = outputs[-1][0][-1]["content"] self.assertIn("The slides do not cover this.", final) self.assertIn("no matching article was found on TeachMeAnatomy or Kenhub. (Kenhub could not be reached.)", final) rows = [c for c in self.store.calls if c[0] == "append_messages"][0][3] self.assertEqual(rows[1]["source"], "none") def test_reports_when_reference_pages_do_not_answer(self): session = signed_in_session(self.store) page = web_sources.Page("Kenhub", "https://www.kenhub.com/en/library/anatomy/retina", "Retina", "text") with mock.patch.object(app, "gemini_reply", return_value=self.slides_reply(covered=False)), \ mock.patch.object(web_sources, "find_pages", return_value=([page], "")), \ mock.patch.object(app, "gemini_web_reply", return_value=WebReply(answer="The pages do not answer this.", covered=False)): outputs = drain(app.run_turn("ask", "Unknown thing?", session)) final = outputs[-1][0][-1]["content"] self.assertTrue(final.startswith("The pages do not answer this.")) self.assertIn("(Kenhub: [Retina](https://www.kenhub.com/en/library/anatomy/retina)) do not answer it either", final) def test_relevant_slides_falls_back_to_cited_slides(self): other = app.LECTURES[self.slide.lecture].slides[-1] answer = f"Fact (Lecture {self.slide.lecture}, slide {self.slide.number})." kept = app._relevant_slides([SlideRef(lecture=other.lecture, slide=other.number)], answer) self.assertEqual([(r.lecture, r.slide) for r in kept], [self.slide.key]) def test_citation_groups_with_repeated_slide_word(self): lecture = self.slide.lecture self.assertEqual(app._cited_slides(f"(Lecture {lecture}, slide 1, slide 2)"), [(lecture, 1), (lecture, 2)]) self.assertEqual(app._verify_citations(f"(Lecture {lecture}, slide 1, slide 999)"), f"(Lecture {lecture}, slide 1, slide 999{app.NOT_FOUND_TAG})") def clinical(self, slide_reply, pages, web_reply): session = signed_in_session(self.store) session.state["topic"] = "knee and ankle" with mock.patch.object(app, "gemini_reply", return_value=slide_reply) as slide_call, \ mock.patch.object(web_sources, "find_pages", return_value=(pages, "")) as find, \ mock.patch.object(app, "gemini_web_reply", return_value=web_reply) as web_call: outputs = drain(app.run_turn("clinical", "", session)) rows = [c for c in self.store.calls if c[0] == "append_messages"][0][3] return session, outputs, rows, slide_call, find, web_call def test_clinical_answer_names_each_source_and_never_the_tutor(self): page = web_sources.Page("TeachMeAnatomy", "https://teachmeanatomy.info/lower-limb/joints/knee-joint/", "The Knee Joint", "text") slide_reply = TutorReply(answer=f"- An unhappy triad point (Lecture {self.slide.lecture}, slide {self.slide.number}).", slides=[], covered_by_slides=True, search_topics=["knee joint", "ankle joint"]) web_reply = WebReply(answer="- Bursitis (TeachMeAnatomy: The Knee Joint).", covered=True, used_urls=[page.url.rstrip("/")]) session, outputs, rows, slide_call, find, web_call = self.clinical(slide_reply, [page], web_reply) find.assert_called_once_with(["knee joint", "ankle joint"]) self.assertIn("List the clinical correlations for this topic that the slides state", slide_call.call_args[0][0]) self.assertIn("that the reference pages state: knee and ankle", web_call.call_args[0][0]) progress = [o[0][-1]["content"] for o in outputs[:-1]] self.assertEqual(progress, ["Searching the lecture slides for clinical notes...", "Checking TeachMeAnatomy and Kenhub for clinical notes..."]) body, note = app._split_source(outputs[-1][0][-1]["content"]) self.assertEqual(body, f"### From the lecture slides\n\n- An unhappy triad point (Lecture {self.slide.lecture}, slide {self.slide.number}).\n\n" "### From TeachMeAnatomy\n\n- Bursitis (TeachMeAnatomy: The Knee Joint).") self.assertTrue(note.startswith(f"Course lecture slides: Lecture {self.slide.lecture} (")) self.assertTrue(note.endswith("; TeachMeAnatomy: [The Knee Joint](https://teachmeanatomy.info/lower-limb/joints/knee-joint/).")) for phrase in ("beyond", "tutor", "standard clinical anatomy"): self.assertNotIn(phrase, outputs[-1][0][-1]["content"].lower()) self.assertEqual(rows[1]["source"], "slides+web") self.assertEqual(session.state["last_source"], "slides+web") def test_clinical_from_the_sites_when_the_slides_have_none(self): page = web_sources.Page("Kenhub", "https://www.kenhub.com/en/library/anatomy/the-ankle-joint", "Ankle joint", "text") slide_reply = TutorReply(answer="The lecture slides do not include clinical notes on this topic.", slides=[], covered_by_slides=False, search_topics=["ankle joint"]) web_reply = WebReply(answer="### Sprains\n- Inversion sprain (Kenhub: Ankle joint).", covered=True, used_urls=[page.url]) session, outputs, rows, *_ = self.clinical(slide_reply, [page], web_reply) body, note = app._split_source(outputs[-1][0][-1]["content"]) self.assertEqual(body, "### From the lecture slides\n\nThe lecture slides do not include clinical notes on this topic.\n\n" "### From Kenhub\n\n#### Sprains\n- Inversion sprain (Kenhub: Ankle joint).") self.assertEqual(note, "The lecture slides do not include clinical notes on this topic. Answered from " "Kenhub: [Ankle joint](https://www.kenhub.com/en/library/anatomy/the-ankle-joint).") self.assertEqual(rows[1]["source"], "web") def test_clinical_with_nothing_found_says_so(self): slide_reply = TutorReply(answer="The lecture slides do not include clinical notes on this topic.", slides=[], covered_by_slides=False, search_topics=["retina"]) session, outputs, rows, _, _, web_call = self.clinical(slide_reply, [], None) web_call.assert_not_called() body, note = app._split_source(outputs[-1][0][-1]["content"]) self.assertEqual(body, "Neither the lecture slides nor the TeachMeAnatomy and Kenhub pages checked include clinical notes on this topic.") self.assertEqual(rows[1]["source"], "none") def test_clinical_reads_the_pages_while_searching_the_slides_when_the_topic_is_known(self): session = signed_in_session(self.store) session.state.update(topic="knee", search_topics=["knee joint"]) page = web_sources.Page("Kenhub", "https://www.kenhub.com/en/library/anatomy/the-knee-joint", "Knee joint", "text") slide_reply = TutorReply(answer=f"- A point (Lecture {self.slide.lecture}, slide {self.slide.number}).", slides=[], covered_by_slides=True, search_topics=["something else"]) with mock.patch.object(app, "gemini_reply", return_value=slide_reply), \ mock.patch.object(web_sources, "find_pages", return_value=([page], "")) as find, \ mock.patch.object(app, "gemini_web_reply", side_effect=app.TutorError("busy")): outputs = drain(app.run_turn("clinical", "", session)) find.assert_called_once_with(["knee joint"]) # the structures of the answer that set the topic self.assertEqual([o[0][-1]["content"] for o in outputs[:-1]], ["Searching the lecture slides, TeachMeAnatomy, and Kenhub for clinical notes..."]) body, note = app._split_source(outputs[-1][0][-1]["content"]) # the page step failed: the slide part still shows self.assertIn("### From the lecture slides\n\n- A point", body) self.assertIn("The TeachMeAnatomy and Kenhub pages could not be read right now.", body) self.assertIn("could not be read right now", note) self.assertNotIn("add no clinical notes", note) self.assertEqual(session.state["search_topics"], ["knee joint"]) def test_ask_remembers_the_structures_for_clinical_correlation(self): session = signed_in_session(self.store) reply = self.slides_reply() reply.search_topics = ["knee joint", "ankle joint"] with mock.patch.object(app, "gemini_reply", return_value=reply): drain(app.run_turn("ask", "Knee and ankle?", session)) self.assertEqual(session.state["search_topics"], ["knee joint", "ankle joint"]) def test_beyond_guard_catches_heading_variants(self): for heading in ["### **Beyond the slides**", "### 2. Beyond the slides", "Beyond the slides:", "*Beyond the slides*", "### Additional clinical points (beyond the slides)", "**Beyond the lecture slides**"]: text = f"### Ligaments\n- Kept (Lecture 1, slide 2).\n{heading}\n- Tutor point\n#### Detail\n- More tutor\n### Summary\nKept." self.assertEqual(app._drop_beyond_sections(text), "### Ligaments\n- Kept (Lecture 1, slide 2).\n### Summary\nKept.", heading) self.assertEqual(app._drop_beyond_sections("**Beyond doubt, the ACL limits translation** (Lecture 1, slide 2)."), "**Beyond doubt, the ACL limits translation** (Lecture 1, slide 2).") def test_simplify_never_brings_back_old_beyond_sections_or_the_tutor_note(self): session = signed_in_session(self.store) old = "### From the reference pages\n- Point.\n\n### Beyond the reference pages\n- Tutor point." session.state.update(topic="knee", last_answer=old, last_source="web", last_note="Answered from Kenhub: [Knee](u).") rewritten = TutorReply(answer="### From the reference pages\n- Simple point.\n### Beyond the reference pages\n- Tutor.", slides=[], covered_by_slides=False) with mock.patch.object(app, "_generate", return_value=rewritten.model_dump_json()) as generate: outputs = drain(app.run_turn("simplify", "", session)) sent = generate.call_args[0][0][-1].parts[0].text # the request sent to Gemini self.assertIn("Previous answer:", sent) self.assertNotIn("Beyond", sent) # the old section is not sent ... self.assertNotIn("Beyond", outputs[-1][0][-1]["content"]) # ... nor shown if Gemini adds one _, state = app._restore([{"role": "assistant", "mode": "clinical", "topic": "t", "source": "slides", "content": "A" + app.SOURCE_MARK + "Course lecture slides." + app.OLD_TUTOR_NOTE}]) self.assertEqual(state["last_note"], "Course lecture slides.") def test_simplify_of_a_clinical_answer_from_both_sources_checks_slide_citations(self): session = signed_in_session(self.store) session.state.update(topic="knee", last_answer="prev", last_source="slides+web", last_note="note") rewritten = TutorReply(answer="Point (Lecture 1, slide 999) and (Lecture 1, slide 2).", slides=[], covered_by_slides=False) with mock.patch.object(app, "gemini_rewrite", return_value=rewritten): drain(app.run_turn("simplify", "", session)) self.assertIn(app.NOT_FOUND_TAG, session.messages[-1]["content"]) self.assertEqual(session.state["slides"], [[1, 2]]) def test_clinical_says_when_no_article_exists(self): slide_reply = TutorReply(answer=f"- A point (Lecture {self.slide.lecture}, slide {self.slide.number}).", slides=[], covered_by_slides=True, search_topics=["superior thoracic aperture"]) session, outputs, rows, *_ = self.clinical(slide_reply, [], None) body, note = app._split_source(outputs[-1][0][-1]["content"]) self.assertIn("No TeachMeAnatomy or Kenhub article about this topic was found.", body) self.assertTrue(note.endswith("No TeachMeAnatomy or Kenhub article about this topic was found.")) def test_follow_ups_and_quick_actions_search_the_slides_of_the_known_structures(self): state = {"search_topics": ["knee joint"]} self.assertEqual(app.slide_query("ask", "How is it injured?", state), "How is it injured? knee joint") self.assertEqual(app.slide_query("ask", "What muscles form the rotator cuff?", state), "What muscles form the rotator cuff?") self.assertEqual(app.slide_query("quiz", "How is it injured?", state), "How is it injured? knee joint") self.assertEqual(app.slide_query("ask", "How is it injured?", {}), "How is it injured?") upper_limb = {n for n, lec in app.LECTURES.items() if any(w in lec.title.lower() for w in ("shoulder", "brachial", "arm"))} lectures = {s.lecture for s in app.retrieve_slides(app.slide_query("ask", "How is it injured?", state))} self.assertFalse(lectures & upper_limb, lectures) def test_beyond_sections_are_removed(self): text = ("### From the reference pages\n- Point (TeachMeAnatomy: The Knee Joint).\n\n" "### Beyond the reference pages\nThe following points are standard clinical anatomy.\n- Own knowledge\n" "#### Sub point\n- more\n### Summary\nKept.\n\n**Beyond the slides**\n- tutor point\n### Next\nKept too.") self.assertEqual(app._drop_beyond_sections(text), "### From the reference pages\n- Point (TeachMeAnatomy: The Knee Joint).\n\n### Summary\nKept.\n\n### Next\nKept too.") self.assertEqual(app._drop_beyond_sections("The rotator cuff lies beyond the glenoid."), "The rotator cuff lies beyond the glenoid.") def test_prompts_never_ask_for_outside_knowledge(self): for text in [app.TASK_PROMPTS["clinical"], app.WEB_TASK_PROMPTS["clinical"], app.WEB_SYSTEM_PROMPT, app.TASK_PROMPTS["simplify"]]: self.assertNotIn("Beyond the slides'", text) self.assertNotIn("standard clinical anatomy", text) self.assertNotIn('write "the reference pages" instead of "the slides"', app.WEB_SYSTEM_PROMPT) self.assertIn("Never answer from outside knowledge", app.SYSTEM_PROMPT) def test_ignored_clicks_are_still_counted(self): session = signed_in_session(self.store) with mock.patch.object(app, "gemini_reply") as call: drain(app.run_turn("ask", " ", session)) call.assert_not_called() self.assertEqual(self.store.events(), ["ask"]) self.assertEqual(session.messages, []) def test_show_image_keeps_only_cited_slides(self): session = signed_in_session(self.store) session.state["topic"] = "topic" other = app.LECTURES[self.slide.lecture].slides[-1] reply = TutorReply(answer=f"Slide (Lecture {self.slide.lecture}, slide {self.slide.number}) shows it.", slides=[SlideRef(lecture=other.lecture, slide=other.number), SlideRef(lecture=self.slide.lecture, slide=self.slide.number), SlideRef(lecture=self.figure.lecture, slide=self.figure.number)], covered_by_slides=True) with mock.patch.object(app, "gemini_reply", return_value=reply): self._show(session) self.assertEqual(session.state["slides"], [[self.slide.lecture, self.slide.number], [self.figure.lecture, self.figure.number]]) def test_simplify_and_show_do_not_fall_back(self): session = signed_in_session(self.store) session.state.update(topic="retina", last_answer="prev", last_source="web", last_note="The lecture slides do not cover this request. Answered from Kenhub: [Retina](u).") with mock.patch.object(app, "gemini_rewrite", return_value=TutorReply(answer="Simpler.", slides=[], covered_by_slides=False)) as rewrite, \ mock.patch.object(app, "gemini_reply") as slides_call, \ mock.patch.object(web_sources, "find_pages") as find: outputs = drain(app.run_turn("simplify", "", session)) find.assert_not_called() slides_call.assert_not_called() # a web answer is rewritten without the slide prompt self.assertIn("Previous answer:\nprev", rewrite.call_args[0][0]) self.assertEqual(outputs[0][0][-1]["content"], "Rewriting the previous answer...") self.assertTrue(outputs[-1][0][-1]["content"].endswith("Answered from Kenhub: [Retina](u).")) rows = [c for c in self.store.calls if c[0] == "append_messages"][0][3] self.assertEqual(rows[1]["source"], "web") def test_quiz_answer_key_is_collapsed_before_the_source_note(self): session = signed_in_session(self.store) session.state["topic"] = "brachial plexus" reply = TutorReply(answer=f"1. Q?\n\n### Answer key\n1. A (Lecture {self.slide.lecture}, slide {self.slide.number})", slides=[], covered_by_slides=True) with mock.patch.object(app, "gemini_reply", return_value=reply): outputs = drain(app.run_turn("quiz", "", session)) content = outputs[-1][0][-1]["content"] self.assertLess(content.index(""), content.index("**Source:**")) self.assertEqual(session.state["last_answer"], "") # quizzes never become the answer to simplify def test_error_is_shown_without_source_note(self): session = signed_in_session(self.store) with mock.patch.object(app, "gemini_reply", side_effect=app.TutorError("The tutor is busy right now.")): outputs = drain(app.run_turn("ask", "Q?", session)) final = outputs[-1][0][-1] self.assertEqual(final, {"role": "assistant", "content": "The tutor is busy right now."}) self.assertEqual(session.state["history"], []) self.assertFalse(session.busy) rows = [c for c in self.store.calls if c[0] == "append_messages"][0][3] self.assertEqual(rows[1]["source"], "error") def test_turns_need_a_signed_in_user(self): session = app.Session() with mock.patch.object(app, "gemini_reply") as call, mock.patch.object(app.gr, "Warning") as warning: outputs = drain(app.run_turn("ask", "Q?", session)) call.assert_not_called() self.assertEqual(outputs, [app.NOOP_TURN]) # the visible chat is left as it is warning.assert_called_once_with(app.SIGNED_OUT_TEXT) self.assertEqual(session.messages, []) with mock.patch.object(app, "get_session", return_value=session), mock.patch.object(app.gr, "Warning"): shown = list(app.on_show_image("", "", 0, SimpleNamespace(session_hash="x"))) self.assertEqual(shown, [app.NOOP_SHOW]) def test_turn_signs_the_student_back_in_after_a_restart(self): self.store.users["jdoe"] = {"user_id": "jdoe", "email": "jdoe@example.com"} self.store.messages = [{"user_id": "jdoe", "role": "user", "content": "Q1", "mode": "ask", "topic": "t1", "source": None}, {"user_id": "jdoe", "role": "assistant", "content": "A1", "mode": "ask", "topic": "t1", "source": "slides"}] session = app.Session() # what a restarted server knows about the tab with mock.patch.object(app, "gemini_reply", return_value=self.slides_reply()): outputs = drain(app.run_turn("ask", "Q2?", session, ("jdoe", "JDoe@example.com", 1791000000000))) self.assertEqual(session.user_id, "jdoe") self.assertEqual([m["content"] for m in outputs[-1][0]][:3], ["Q1", "A1", "Q2?"]) self.assertIn("resume", self.store.events()) self.assertEqual([c[0] for c in self.store.calls][0], "start_session") # no other tab: no waiting def test_no_automatic_sign_in_from_the_sign_in_form_or_a_wrong_email(self): self.store.users["jdoe"] = {"user_id": "jdoe", "email": "jdoe@example.com"} for creds in [("jdoe", "jdoe@example.com", 0), ("jdoe", "other@example.com", 5), ("nobody", "jdoe@example.com", 5)]: session = app.Session() with mock.patch.object(app, "gemini_reply") as call, mock.patch.object(app.gr, "Warning"): drain(app.run_turn("ask", "Q?", session, creds)) call.assert_not_called() self.assertIsNone(session.user_id, creds) def test_show_image_shows_thinking_while_choosing(self): session = signed_in_session(self.store) session.state["topic"] = "topic" reply = TutorReply(answer=f"Slide (Lecture {self.slide.lecture}, slide {self.slide.number}) shows it.", slides=[SlideRef(lecture=self.slide.lecture, slide=self.slide.number)], covered_by_slides=True) with mock.patch.object(app, "gemini_reply", return_value=reply): outputs = self._show(session) self.assertEqual(outputs[0][0][-1]["metadata"]["status"], "pending") self.assertEqual(session.state["view"], [self.slide.lecture, self.slide.number]) self.assertIn("**Source:** Course lecture slides:", outputs[-1][0][-1]["content"]) self.assertEqual(self.store.events(), ["show_image"]) def _show(self, session): request = SimpleNamespace(session_hash="tab-show") with mock.patch.object(app, "get_session", return_value=session): return list(app.on_show_image("", "", 0, request)) class RestoreTests(unittest.TestCase): def test_restore_rebuilds_chat_and_state(self): rows = [ {"role": "user", "content": "Q1", "mode": "ask", "topic": "t1", "source": None}, {"role": "assistant", "content": "A1" + app.SOURCE_MARK + "Course lecture slides.", "mode": "ask", "topic": "t1", "source": "slides"}, {"role": "user", "content": "Quiz me on: t1", "mode": "quiz", "topic": "t1", "source": None}, {"role": "assistant", "content": "Quiz" + app.SOURCE_MARK + "Course lecture slides.", "mode": "quiz", "topic": "t1", "source": "slides"}, ] messages, state = app._restore(rows) self.assertEqual(messages[1], {"role": "assistant", "content": rows[1]["content"]}) self.assertEqual([h["role"] for h in state["history"]], ["user", "model", "user", "model"]) self.assertEqual(state["history"][1]["text"], "A1") self.assertEqual(state["topic"], "t1") self.assertEqual(state["last_answer"], "A1") # the quiz is skipped self.assertEqual(state["last_note"], "Course lecture slides.") self.assertEqual(state["last_source"], "slides") self.assertIsNone(state["view"]) def test_restore_skips_failed_turns_and_show_notes(self): rows = [ {"role": "user", "content": "Q1", "mode": "ask", "topic": "t1", "source": None}, {"role": "assistant", "content": "A1" + app.SOURCE_MARK + "Not found in the lecture slides, and no matching article was found on TeachMeAnatomy or Kenhub.", "mode": "ask", "topic": "t1", "source": "none"}, {"role": "assistant", "content": "No slide in the lecture files illustrates this topic.", "mode": "show", "topic": "t1", "source": "none"}, {"role": "user", "content": "Q2", "mode": "ask", "topic": "t2", "source": None}, {"role": "assistant", "content": "The tutor is busy right now.", "mode": "ask", "topic": "t2", "source": "error"}, ] messages, state = app._restore(rows) self.assertEqual(len(messages), 5) # everything is shown again self.assertEqual([h["text"] for h in state["history"]], ["Q1", "A1"]) self.assertEqual(state["topic"], "t1") self.assertEqual((state["last_answer"], state["last_source"]), ("A1", "none")) class SignInTests(unittest.TestCase): def setUp(self): self.store = FakeStore() self.patch = mock.patch.object(app, "STORE", self.store) self.patch.start() self.request = SimpleNamespace(session_hash="tab-1") app.SESSIONS.clear() def tearDown(self): self.patch.stop() app.SESSIONS.clear() def test_create_account_then_sign_in_restores_history(self): out = app.on_create_account("JDoe", "JDoe@Example.com", self.request) self.assertFalse(out[0]["visible"]) # login panel hidden self.assertTrue(out[1]["visible"]) # app shown self.assertIn("Signed in as **jdoe** (jdoe@example.com).", out[3]) self.assertEqual(self.store.users["jdoe"]["email"], "jdoe@example.com") self.assertEqual(self.store.events(), ["create_account"]) self.assertEqual([c[0] for c in self.store.calls][:2], ["start_session", "log_event"]) again = app.on_create_account("jdoe", "other@example.com", self.request) self.assertTrue(again[0]["visible"]) self.assertIn("already taken", again[2]) session = app.get_session(self.request) session.messages = [{"role": "user", "content": "Q"}, {"role": "assistant", "content": "A" + app.SOURCE_MARK + "Course lecture slides."}] app._save(session, [{"role": "user", "content": "Q", "mode": "ask", "topic": "Q", "source": None}, {"role": "assistant", "content": session.messages[1]["content"], "mode": "ask", "topic": "Q", "source": "slides"}]) stored = session.messages[1]["content"] out = app.on_sign_out(self.request) self.assertTrue(out[0]["visible"]) self.assertEqual(out[5], []) self.assertEqual(out[-3:], ("", "", 0)) # user ID and email fields cleared, page shows sign-in self.assertEqual(len(out), len(app.login_outputs)) self.assertIsNone(session.user_id) self.assertEqual([c[0] for c in self.store.calls][-2:], ["log_event", "end_session"]) self.assertEqual(self.store.events()[-1], "sign_out") wrong = app.on_sign_in("jdoe", "wrong@example.com", self.request) self.assertIn("does not match", wrong[2]) unknown = app.on_sign_in("nobody", "a@b.co", self.request) self.assertIn("No account has this user ID", unknown[2]) out = app.on_sign_in("jdoe", "jdoe@example.com", self.request) self.assertFalse(out[0]["visible"]) self.assertIn("2 earlier messages restored", out[3]) self.assertEqual([m["content"] for m in out[5]], ["Q", stored]) self.assertEqual(session.state["topic"], "Q") self.assertTrue(out[-5]["interactive"]) # Ask enabled self.assertTrue(out[-9]["interactive"]) # quick actions enabled: a topic exists self.assertEqual(out[-1], session.version) def test_validation_messages(self): self.assertIn("user ID", app.on_sign_in("x", "a@b.co", self.request)[2]) self.assertIn("valid email", app.on_sign_in("jdoe", "not-an-email", self.request)[2]) with mock.patch.object(app, "STORE", FakeStore(enabled=False)): self.assertIn("not configured", app.on_sign_in("jdoe", "a@b.co", self.request)[2]) def sync(self, version, user="jdoe", email="jdoe@example.com"): return app.on_sync(version, user, email, self.request) def test_sync_does_nothing_for_the_sign_in_form_or_an_up_to_date_page(self): self.store.users["jdoe"] = {"user_id": "jdoe", "email": "jdoe@example.com"} self.assertEqual(self.sync(0), app.NOOP_SYNC) # page shows the sign-in form: never signs in self.assertIsNone(app.get_session(self.request).user_id) out = app.on_sign_in("jdoe", "jdoe@example.com", self.request) self.assertEqual(self.sync(out[-1]), app.NOOP_SYNC) def test_sync_delivers_an_answer_the_page_missed(self): import threading app.on_create_account("jdoe", "jdoe@example.com", self.request) session = app.get_session(self.request) shown = session.version # what the page has before it asks slide, figure = first_text_slide_with_figure() reply = TutorReply(answer=f"Fact (Lecture {slide.lecture}, slide {slide.number}).", slides=[], covered_by_slides=True) release = threading.Event() def slow_reply(*args): release.wait(5) return reply with mock.patch.object(app, "gemini_reply", side_effect=slow_reply): turn = app.run_turn("ask", "Q?", session, ("jdoe", "jdoe@example.com", shown)) pending = next(turn) # the page receives the Thinking message ... self.assertFalse(pending[-3]["interactive"]) self.assertEqual(self.sync(shown), app.NOOP_SYNC) # ... the answer is being written: nothing to send turn.close() # ... then its connection drops: Gradio drops the event release.set() for _ in range(100): # the answer is still finished and saved if not session.busy: break app.time.sleep(0.02) self.assertFalse(session.busy) self.assertIn("append_messages", [c[0] for c in self.store.calls]) out = self.sync(shown) self.assertEqual(len(out), len(app.sync_outputs)) self.assertEqual([m["content"].split(app.SOURCE_MARK)[0] for m in out[0]], ["Q?", reply.answer]) self.assertEqual(out[2], session.version) self.assertTrue(all(update["interactive"] for update in out[-6:])) # buttons usable again self.assertEqual(self.sync(out[2]), app.NOOP_SYNC) # nothing more to send def test_a_multi_step_answer_finishes_after_the_page_is_gone(self): session = app.get_session(self.request) app.on_create_account("jdoe", "jdoe@example.com", self.request) page = web_sources.Page("Kenhub", "https://www.kenhub.com/en/library/anatomy/retina", "Retina", "text") slides_reply = TutorReply(answer="The slides do not cover this.", slides=[], covered_by_slides=False, search_topics=["retina"]) with mock.patch.object(app, "gemini_reply", return_value=slides_reply), \ mock.patch.object(web_sources, "find_pages", return_value=([page], "")), \ mock.patch.object(app, "gemini_web_reply", return_value=WebReply(answer="Ten layers (Kenhub: Retina).", covered=True, used_urls=[page.url])): turn = app.run_turn("ask", "Layers of the retina?", session, ()) next(turn) # first progress step reaches the page turn.close() # the page is gone before the second step for _ in range(100): if not session.busy: break app.time.sleep(0.02) saved = [c for c in self.store.calls if c[0] == "append_messages"] self.assertEqual(len(saved), 1) self.assertEqual(saved[0][3][1]["source"], "web") self.assertTrue(session.messages[-1]["content"].startswith("Ten layers (Kenhub: Retina).")) def test_sync_restores_the_student_after_a_server_restart(self): self.store.users["jdoe"] = {"user_id": "jdoe", "email": "jdoe@example.com"} self.store.messages = [{"user_id": "jdoe", "role": "user", "content": "Q1", "mode": "ask", "topic": "t1", "source": None}, {"user_id": "jdoe", "role": "assistant", "content": "A1", "mode": "ask", "topic": "t1", "source": "slides"}] self.assertEqual(self.sync(1791000000000, email="wrong@example.com"), app.NOOP_SYNC) out = self.sync(1791000000000) session = app.get_session(self.request) self.assertEqual(session.user_id, "jdoe") self.assertEqual([m["content"] for m in out[0]], ["Q1", "A1"]) self.assertIn("The site restarted; your chat was restored.", out[3]) self.assertEqual(self.store.events(), ["resume"]) def test_reloaded_tab_signs_itself_back_in_and_keeps_the_draft(self): self.store.users["jdoe"] = {"user_id": "jdoe", "email": "jdoe@example.com"} self.store.messages = [{"user_id": "jdoe", "role": "user", "content": "Q1", "mode": "ask", "topic": "t1", "source": None}, {"user_id": "jdoe", "role": "assistant", "content": "A1", "mode": "ask", "topic": "t1", "source": "slides"}] steps = list(app.on_page_load("JDoe", "jdoe@example.com", "half-typed question", self.request)) self.assertEqual(steps[0][2], "Reconnecting and restoring your chat...") final = steps[-1] self.assertEqual(len(final), len(app.login_outputs)) self.assertFalse(final[0]["visible"]) # sign-in form hidden self.assertTrue(final[1]["visible"]) # chat shown self.assertIn("Reconnected; 2 messages restored.", final[3]) self.assertEqual([m["content"] for m in final[5]], ["Q1", "A1"]) self.assertEqual(final[6], "half-typed question") self.assertEqual(final[-3:], ("jdoe", "jdoe@example.com", app.get_session(self.request).version)) self.assertEqual(self.store.events(), ["resume"]) def test_reloaded_tab_drops_a_draft_that_was_already_answered(self): self.store.users["jdoe"] = {"user_id": "jdoe", "email": "jdoe@example.com"} self.store.messages = [{"user_id": "jdoe", "role": "user", "content": "Q1", "mode": "ask", "topic": "t1", "source": None}, {"user_id": "jdoe", "role": "assistant", "content": "A1", "mode": "ask", "topic": "t1", "source": "slides"}] final = list(app.on_page_load("jdoe", "jdoe@example.com", "Q1", self.request))[-1] self.assertEqual(final[6], "") def test_page_load_without_a_remembered_sign_in_shows_the_form(self): self.assertEqual(list(app.on_page_load("", "", "", self.request)), [app.NOOP_LOGIN]) steps = list(app.on_page_load("jdoe", "wrong@example.com", "", self.request)) self.assertTrue(steps[-1][0]["visible"]) self.assertIn("could not be restored automatically", steps[-1][2]) self.assertIsNone(app.get_session(self.request).user_id) def test_reloaded_page_waits_for_the_old_tab_and_ordinary_sign_ins_do_not(self): import threading self.store.users["jdoe"] = {"user_id": "jdoe", "email": "jdoe@example.com"} old = app.get_session(SimpleNamespace(session_hash="old-tab")) old.user_id, old.busy = "jdoe", True old.messages = [{"role": "user", "content": "Pending question?"}] def finish(): app.time.sleep(1.5) old.saved_at, old.busy = app.time.monotonic(), False threading.Thread(target=finish).start() started = app.time.monotonic() steps = list(app.on_page_load("jdoe", "jdoe@example.com", "Pending question?", self.request)) self.assertGreaterEqual(app.time.monotonic() - started, 1.0) # waited for the old tab self.assertGreater(len(steps), 2) # by polling, one step per second names = [c[0] for c in self.store.calls] self.assertLess(names.index("flush"), names.index("start_session")) # its answer reaches Supabase first # A plain sign-in on another tab does not wait for the shared write queue. self.store.calls.clear() app.on_sign_in("jdoe", "jdoe@example.com", SimpleNamespace(session_hash="tab-2")) self.assertIn("flush", [c[0] for c in self.store.calls]) # the old tab saved just now old.saved_at = 0.0 self.store.calls.clear() app.on_sign_in("jdoe", "jdoe@example.com", SimpleNamespace(session_hash="tab-3")) self.assertNotIn("flush", [c[0] for c in self.store.calls]) def test_an_answer_still_being_written_reaches_the_reloaded_page_later(self): self.store.users["jdoe"] = {"user_id": "jdoe", "email": "jdoe@example.com"} old = app.get_session(SimpleNamespace(session_hash="old-tab")) old.user_id, old.busy = "jdoe", True old.messages = [{"role": "user", "content": "Slow question?"}] with mock.patch.object(app, "ANSWER_WAIT_SECONDS", 0.2): final = list(app.on_page_load("jdoe", "jdoe@example.com", "Slow question?", self.request))[-1] self.assertEqual(final[6], "") # not put back: still being answered session = app.get_session(self.request) self.assertTrue(session.awaiting) self.assertEqual(self.sync(final[-1]), app.NOOP_SYNC) # still being written self.store.messages = [{"user_id": "jdoe", "role": "user", "content": "Slow question?", "mode": "ask", "topic": "s", "source": None}, {"user_id": "jdoe", "role": "assistant", "content": "Slow answer.", "mode": "ask", "topic": "s", "source": "slides"}] old.busy, old.saved_at = False, app.time.monotonic() out = self.sync(final[-1]) self.assertEqual([m["content"] for m in out[0]], ["Slow question?", "Slow answer."]) self.assertFalse(session.awaiting) def test_a_sign_in_made_while_reconnecting_is_not_overwritten(self): self.store.users["jdoe"] = {"user_id": "jdoe", "email": "jdoe@example.com"} self.store.users["bob"] = {"user_id": "bob", "email": "bob@example.com"} old = app.get_session(SimpleNamespace(session_hash="old-tab")) old.user_id, old.busy = "jdoe", True load = app.on_page_load("jdoe", "jdoe@example.com", "", self.request) next(load) # "Reconnecting..." on the sign-in form app.on_sign_in("bob", "bob@example.com", self.request) # bob signs in on this tab meanwhile old.busy = False steps = list(load) self.assertEqual(steps[-1], app.NOOP_LOGIN) self.assertEqual(app.get_session(self.request).user_id, "bob") def test_sign_out_is_final_for_requests_already_in_flight(self): out = app.on_sign_in("jdoe", "jdoe@example.com", self.request) if "jdoe" in self.store.users else None self.store.users["jdoe"] = {"user_id": "jdoe", "email": "jdoe@example.com"} out = app.on_sign_in("jdoe", "jdoe@example.com", self.request) version = out[-1] app.on_sign_out(self.request) self.assertEqual(self.sync(version), app.NOOP_SYNC) # a tick sent before Sign out with mock.patch.object(app, "gemini_reply") as call, mock.patch.object(app.gr, "Warning"): drain(app.run_turn("ask", "Q?", app.get_session(self.request), ("jdoe", "jdoe@example.com", version))) call.assert_not_called() self.assertIsNone(app.get_session(self.request).user_id) app.on_sign_in("jdoe", "jdoe@example.com", self.request) # signing in again works self.assertEqual(app.get_session(self.request).user_id, "jdoe") def test_sign_out_while_an_answer_is_written_says_so(self): app.on_create_account("jdoe", "jdoe@example.com", self.request) session = app.get_session(self.request) session.busy = True with mock.patch.object(app.gr, "Warning") as warning: self.assertEqual(app.on_sign_out(self.request), app.NOOP_LOGIN) warning.assert_called_once() self.assertEqual(session.user_id, "jdoe") def test_evicting_a_closed_visit_does_not_close_it_again(self): app.on_create_account("jdoe", "jdoe@example.com", self.request) app.forget_session(self.request) with mock.patch.object(app, "MAX_SESSIONS", 1): app.get_session(SimpleNamespace(session_hash="someone-else")) self.assertEqual([c[0] for c in self.store.calls].count("end_session"), 1) def test_time_on_site_and_new_visit_after_the_connection_drops(self): app.on_create_account("jdoe", "jdoe@example.com", self.request) session = app.get_session(self.request) first = session.session_id self.sync(session.version) self.assertNotIn("heartbeat", [c[0] for c in self.store.calls]) # written at most every HEARTBEAT_SECONDS session.last_beat -= app.HEARTBEAT_SECONDS self.sync(session.version) self.assertEqual(self.store.calls[-1], ("heartbeat", first)) app.forget_session(self.request) # laptop asleep: the visit is closed self.assertEqual(self.store.calls[-1], ("end_session", first)) app.forget_session(self.request) # closed once only self.assertEqual([c[0] for c in self.store.calls].count("end_session"), 1) self.assertEqual(session.user_id, "jdoe") # still signed in on the server self.sync(session.version) # back: a new visit starts self.assertNotEqual(session.session_id, first) self.assertEqual(self.store.calls[-1], ("start_session", session.session_id, "jdoe")) class WebSourceTests(unittest.TestCase): def test_article_filters(self): tma, kenhub = web_sources.SITES self.assertTrue(tma.article.match("https://teachmeanatomy.info/upper-limb/nerves/brachial-plexus/")) self.assertFalse(tma.article.match("https://teachmeanatomy.info/upper-limb/nerves/")) # category index self.assertFalse(tma.article.match("https://teachmeanatomy.info/encyclopaedia/d/")) self.assertFalse(tma.article.match("https://teachmeanatomy.info/shop/")) self.assertFalse(tma.article.match("https://teachmeanatomy.info/es/miembro-superior/nervios/plexo-braquial/")) self.assertFalse(tma.article.match("https://teachmeanatomy.info/3d-model/upper-limb/")) self.assertTrue(kenhub.article.match("https://www.kenhub.com/en/library/anatomy/brachial-plexus")) self.assertTrue(kenhub.article.match("https://www.kenhub.com/en/library/education/the-human-anatomy")) self.assertFalse(kenhub.article.match("https://www.kenhub.com/en/library/learning-strategies/how-to-study")) self.assertFalse(kenhub.article.match("https://www.kenhub.com/de/library/anatomy/plexus-brachialis")) def test_headings_stay_on_one_line_and_clinical_sections_survive_the_cut(self): self.assertEqual(web_sources.article_text("

\r\n Clinical\r\n significance

x

"), "## Clinical significance\n\nx") text = "## Anatomy\n" + "a" * 20000 + "\n## Clinical significance\nACL injury.\n### Terrible triad\nThree.\n## Sources\nRefs." cut = web_sources.excerpt(text, 1000) self.assertLessEqual(len(cut), 1000) self.assertTrue(cut.startswith("## Anatomy\naaa")) self.assertTrue(cut.endswith("[...]\n\n## Clinical significance\nACL injury.\n### Terrible triad\nThree.")) self.assertEqual(web_sources.excerpt("short", 1000), "short") def test_article_text_keeps_the_article_only(self): page = ("Retina | Kenhub" "

Retina

The retina has ten layers.

" "
caption
") text = web_sources.article_text(page) self.assertEqual(text, "# Retina\n\nThe retina has ten layers.\n\n- Layer one\n- Layer two") self.assertEqual(web_sources.page_title(page, "Kenhub"), "Retina") self.assertEqual(web_sources.page_title("Eye | Kenhub", "Kenhub"), "Eye") @staticmethod def catalog(urls): entries = [("TeachMeAnatomy" if "teachmeanatomy" in u else "Kenhub", u) for u in urls] return web_sources.Catalog( data=(entries, web_sources.BM25([web_sources.url_tokens(u) for _, u in entries]), [frozenset(web_sources.words(web_sources.url_path(u))) for _, u in entries]), next_load=float("inf")) def test_find_pages_matches_each_structure_and_never_a_generic_word(self): tma, kh = "https://teachmeanatomy.info/", "https://www.kenhub.com/en/library/anatomy/" catalog = self.catalog([ tma + "lower-limb/joints/knee-joint/", tma + "lower-limb/joints/ankle-joint/", tma + "upper-limb/joints/shoulder/", tma + "lower-limb/areas/tarsal-tunnel/", tma + "upper-limb/areas/carpal-tunnel/", kh + "the-knee-joint", kh + "the-ankle-joint", kh + "upper-and-lower-brachial-plexus-injury", kh + "colles-fracture", kh + "carpal-tunnel", kh + "superior-thoracic-artery", kh + "retina", kh + "central-retinal-artery", kh + "medial-meniscus", ]) def find(topics): def load(page): # the page text names the structure in its URL return web_sources.Page(page.site, page.url, "T", web_sources.url_path(page.url)) with mock.patch.object(web_sources, "CATALOG", catalog), mock.patch.object(web_sources, "load_page", side_effect=load): pages, error = web_sources.find_pages(topics) self.assertEqual(error, "") return [p.url.replace(tma, "TMA:").replace(kh, "KH:") for p in pages] self.assertEqual(find(["knee joint", "ankle joint"]), ["TMA:lower-limb/joints/knee-joint/", "TMA:lower-limb/joints/ankle-joint/", "KH:the-knee-joint", "KH:the-ankle-joint"]) self.assertEqual(find(["knee joint injuries", "ankle fracture"]), # injury/fracture never select a page ["TMA:lower-limb/joints/knee-joint/", "TMA:lower-limb/joints/ankle-joint/", "KH:the-knee-joint", "KH:the-ankle-joint"]) self.assertEqual(find(["injury"]), []) self.assertEqual(find(["tarsal tunnel"]), ["TMA:lower-limb/areas/tarsal-tunnel/"]) # not the carpal tunnel self.assertEqual(find(["superior thoracic aperture"]), []) # not the superior thoracic artery self.assertEqual(find(["retinal layers"]), ["KH:retina"]) # the retina before the artery self.assertEqual(find(["menisci"]), ["KH:medial-meniscus"]) self.assertEqual(find("knee"), ["TMA:lower-limb/joints/knee-joint/", "KH:the-knee-joint"]) self.assertLessEqual(len(find(["knee", "ankle", "shoulder", "retina"])), web_sources.MAX_PAGES) with mock.patch.object(web_sources, "CATALOG", catalog), mock.patch.object(web_sources, "load_page", return_value=None): pages, error = web_sources.find_pages(["retina"]) self.assertEqual(pages, []) self.assertEqual(error, "the matching TeachMeAnatomy and Kenhub pages could not be downloaded") class WordTests(unittest.TestCase): def test_singular_and_joint_names(self): self.assertEqual(web_sources.words("arches sinuses menisci vertebrae trapezius trapezium pelvis processes talocrural"), ["arch", "sinus", "meniscus", "vertebra", "trapezius", "trapezium", "pelvis", "process", "ankle"]) self.assertFalse(web_sources.same_word("trapezius", "trapezium")) self.assertTrue(web_sources.same_word("retina", "retinal")) def test_type_word_must_agree_and_be_mentioned(self): kh = "https://www.kenhub.com/en/library/anatomy/" catalog = WebSourceTests.catalog([kh + "the-deltoid-muscle", kh + "the-ankle-joint"]) entries, index, slug_words = catalog.data self.assertEqual(web_sources.topic_matches("deltoid ligament", entries, index, slug_words), []) self.assertEqual(web_sources.topic_matches("deltoid muscle", entries, index, slug_words), [0]) page = web_sources.Page("Kenhub", kh + "x", "Deltoid", "The deltoid is a muscle of the shoulder.") self.assertFalse(web_sources.names_structure("deltoid ligament", page)) page.text = "The medial (deltoid) ligament of the ankle." self.assertTrue(web_sources.names_structure("deltoid ligament", page)) self.assertTrue(web_sources.names_structure("brachial plexus", page)) # no type word: always accepted def test_slide_names_reach_the_article_names(self): kh = "https://www.kenhub.com/en/library/anatomy/" catalog = WebSourceTests.catalog([kh + "achilles-tendon", kh + "calcaneal-tuberosity"]) load = lambda page: web_sources.Page(page.site, page.url, "Achilles tendon", "The achilles tendon (calcaneal tendon) ...") with mock.patch.object(web_sources, "CATALOG", catalog), mock.patch.object(web_sources, "load_page", side_effect=load): pages, _ = web_sources.find_pages(["calcaneal tendon"]) self.assertEqual([p.url for p in pages], [kh + "achilles-tendon"]) def test_question_fallback_splits_structures(self): kh = "https://www.kenhub.com/en/library/anatomy/" catalog = WebSourceTests.catalog([kh + "the-knee-joint", kh + "the-ankle-joint", kh + "carpal-tunnel"]) with mock.patch.object(web_sources, "CATALOG", catalog): self.assertEqual(web_sources.question_topics("What are the ligaments of the knee and ankle?"), ["knee", "ankle"]) self.assertEqual(web_sources.question_topics("Tell me about the carpal tunnel"), ["Tell me about the carpal tunnel"]) class RetrievalTests(unittest.TestCase): def test_question_words_do_not_match_slides(self): self.assertEqual(app.tokenize("What are the ligaments of the knees?"), ["ligament", "knee"]) def test_knee_and_ankle_questions_attach_no_upper_limb_slides(self): knee = next(n for n, lec in app.LECTURES.items() if "knee" in lec.title.lower()) ankle = next(n for n, lec in app.LECTURES.items() if "ankle" in lec.title.lower()) upper_limb = {n for n, lec in app.LECTURES.items() if any(w in lec.title.lower() for w in ("shoulder", "brachial", "arm", "forearm", "hand", "axillary"))} self.assertTrue(upper_limb) for question in ["What are the ligaments of the knee and ankle?", "What about the knees and ankles?", "What are the bones of the knee joint?", "What is the Ottawa ankle rule?", "How is the ankle joint stabilized?", "What causes knee and ankle injuries?"]: lectures = [slide.lecture for slide in app.retrieve_slides(question)] self.assertFalse(set(lectures) & upper_limb, (question, lectures)) self.assertGreaterEqual(sum(n in (knee, ankle) for n in lectures), len(lectures) / 2, (question, lectures)) class StoreTests(unittest.TestCase): def test_validators(self): self.assertTrue(supabase_store.is_valid_user_id(" JDoe_01 ")) self.assertFalse(supabase_store.is_valid_user_id("j")) self.assertFalse(supabase_store.is_valid_user_id("j doe")) self.assertTrue(supabase_store.is_valid_email("A.B@Example.co")) self.assertFalse(supabase_store.is_valid_email("a@b")) self.assertEqual(supabase_store.normalize_user_id(" JDoe "), "jdoe") def test_requests_use_the_supabase_rest_layout(self): store = supabase_store.SupabaseStore(url="https://abc.supabase.co/", key="k") seen = [] def fake_urlopen(request, timeout=None): seen.append(request) return io.BytesIO(b'[{"user_id": "jdoe", "email": "j@d.co"}]') with mock.patch.object(urllib.request, "urlopen", fake_urlopen): row, error = store.get_user("JDoe") self.assertEqual((row["email"], error), ("j@d.co", None)) self.assertEqual(seen[0].full_url, "https://abc.supabase.co/rest/v1/anatomy_users?select=user_id%2Cemail%2Ccreated_at%2Clast_login_at&user_id=eq.jdoe&limit=1") self.assertEqual(seen[0].get_header("Apikey"), "k") self.assertEqual(seen[0].get_header("Authorization"), "Bearer k") with mock.patch.object(urllib.request, "urlopen", fake_urlopen): ok, error = store.append_messages("jdoe", "s1", [{"role": "user", "content": "Q", "mode": "ask", "topic": "Q"}, {"role": "bad"}]) self.assertTrue(ok) body = json.loads(seen[-1].data) self.assertEqual(len(body), 1) self.assertEqual(set(body[0]), {"user_id", "session_id", "role", "content", "mode", "topic", "source", "created_at"}) self.assertEqual(seen[-1].get_header("Prefer"), "return=minimal") def test_duplicate_user_id_is_reported_as_exists(self): store = supabase_store.SupabaseStore(url="https://abc.supabase.co", key="k") error = urllib.error.HTTPError("u", 409, "Conflict", {}, io.BytesIO(b'{"code":"23505"}')) with mock.patch.object(urllib.request, "urlopen", side_effect=error): row, err = store.create_user("jdoe", "j@d.co") self.assertEqual((row, err), (None, supabase_store.EXISTS)) def test_inserts_are_not_retried_but_reads_are(self): store = supabase_store.SupabaseStore(url="https://abc.supabase.co", key="k", timeout=1) calls = [] def flaky(request, timeout=None): calls.append(request.get_method()) raise TimeoutError("timed out") with mock.patch.object(urllib.request, "urlopen", side_effect=flaky), mock.patch.object(supabase_store.time, "sleep"): ok, error = store.log_event("jdoe", "s1", "ask", {}) self.assertFalse(ok) self.assertIn("TimeoutError", error) row, error = store.get_user("jdoe") self.assertEqual(calls, ["POST"] + ["GET"] * supabase_store.RETRIES) def test_disabled_store_is_harmless(self): store = supabase_store.SupabaseStore(url="", key="") self.assertFalse(store.enabled) self.assertEqual(store.get_user("x"), (None, "Missing Space secret(s): SUPABASE_URL, SUPABASE_KEY")) store.enqueue("log_event", "x", "s", "ask", {}) self.assertTrue(store.flush(0.1)) if __name__ == "__main__": unittest.main()