"""Turning a chat turn into a session. The chat is for finding out what you do not know; the point of finding out is to go and practise it. What is under test is which questions that lands on, and that a chat cannot reach a question the learner could not otherwise see. """ import unittest from datetime import datetime from unittest.mock import patch import test_quiz_builder as fixtures from app.models.conversation import Conversation, ConversationMessage from app.routers import ai_mode class PracticeFromChatTests(unittest.TestCase): def setUp(self): self.bank = fixtures.BuilderTests() self.bank.setUp() self.client = self.bank.client self.db = self.bank.db self.client.app.include_router(ai_mode.router, prefix='/ai') def tearDown(self): self.bank.tearDown() def thread(self, citations, asked='What causes stridor?'): conversation = Conversation(user_id=1, title='Stridor') self.db.add(conversation) self.db.flush() self.db.add(ConversationMessage(conversation_id=conversation.id, role='user', content=asked, citations=[])) answer = ConversationMessage(conversation_id=conversation.id, role='assistant', content='Croup, mostly.', citations=citations) self.db.add(answer) self.db.commit() return conversation.id, answer.id def practise(self, conversation_id, **body): with patch('app.services.search_service.hybrid_ids', return_value=([], {})): return self.client.post(f'/ai/conversations/{conversation_id}/practice', json=body) def test_a_cited_question_is_the_session(self): cid, _ = self.thread([{'marker': '[[question:1]]', 'kind': 'question', 'id': 1, 'title': 'Question #1'}]) response = self.practise(cid) self.assertEqual(response.status_code, 200, response.text) self.assertEqual(response.json()['count'], 1) quiz = self.client.get(f"/quizzes/{response.json()['quiz_id']}").json() self.assertEqual([q['id'] for q in quiz['questions']], [1]) # Study mode, never exam: this is reading followed by practice. self.assertEqual(quiz['mode'], 'learning') def test_a_cited_article_brings_the_questions_filed_under_its_topic(self): from app.models.article import Article # The fixture files questions 1 and 2 under categories 1 and 2. self.db.add(Article(id=5, title='Croup', slug='croup', content='…', status='published', user_id=3, category_id=2)) self.db.commit() cid, _ = self.thread([{'marker': '[[article:5]]', 'kind': 'article', 'id': 5, 'title': 'Croup'}]) response = self.practise(cid) self.assertEqual(response.status_code, 200, response.text) quiz = self.client.get(f"/quizzes/{response.json()['quiz_id']}").json() self.assertEqual(sorted(q['id'] for q in quiz['questions']), [2, 4]) def test_a_chat_cannot_reach_a_question_the_learner_may_not_see(self): # Question 4 has been removed from the bank. A citation could only name # it by mistake; the bank's own rules answer anyway. self.bank.db.get(fixtures.Question, 4).deleted_at = datetime(2026, 1, 1) self.bank.db.commit() cid, _ = self.thread([{'marker': '[[question:4]]', 'kind': 'question', 'id': 4, 'title': 'Question #4'}]) self.assertEqual(self.practise(cid).status_code, 404) def test_nothing_to_practise_says_so(self): cid, _ = self.thread([]) response = self.practise(cid) self.assertEqual(response.status_code, 404) self.assertIn('No questions', response.json()['detail']) def test_one_turn_can_be_named_and_another_learner_cannot_ask(self): cid, answer_id = self.thread([{'marker': '[[question:1]]', 'kind': 'question', 'id': 1, 'title': 'Question #1'}]) self.assertEqual(self.practise(cid, message_id=answer_id).status_code, 200) self.assertEqual(self.practise(cid, message_id=99999).status_code, 404) self.bank.user = self.bank.peer self.assertEqual(self.practise(cid).status_code, 404)