cover the new paths, especially the ones that fail silently

خطاهای این پروژه خاموش‌اند: نامِ اشتباهِ collection یا فیلترِ نخورده نتیجه‌ی
خالی می‌دهد نه استثنا، و دستیار فقط بدتر جواب می‌دهد. این تست‌ها نگهبانِ
همان‌جاهایند.

TextbookLayoutTests بررسی می‌کند رشته‌ی هر فصل بینِ ingest و retrieval یکی
باشد و بازه‌ی صفحه‌ها پیوسته بماند. IngestBookResumeTests مطمئن می‌شود
صفحه‌ی بی‌مسئله دوبار پول نمی‌گیرد، صفحه‌ی خطادار در اجرای بعد دوباره
سراغش می‌روند، و upsertِ شکست‌خورده rollback می‌شود.
LessonContextResolutionTests نگهبانِ نرمال‌سازیِ «پایه نهم» → «ریاضی نهم»
است و اینکه مسیرِ gradeId و مسیرِ lessonId به یک collection برسند.
GradeAwarePromptTests جلوی برگشتنِ لحنِ دبستانی برای پایه‌های بالا را
می‌گیرد. FollowUpStaysGroundedTests پیامِ دومِ یک گفتگو را هم مستند نگه
می‌دارد.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 19:01:24 +03:30
parent ac1b1af398
commit 5f3aae0756
4 changed files with 859 additions and 0 deletions

View File

@@ -712,3 +712,70 @@ class ReadThrottleTests(TestCase):
f'/api/home?grade={self.grade.id}',
HTTP_AUTHORIZATION=f'Bearer {self.token}').status_code)
self.assertIn(429, codes)
class HomeGradeFallbackTests(TestCase):
"""
/api/home باید در نبودِ ?grade= از پایه‌ی ذخیره‌شده‌ی خودِ کاربر استفاده کند.
پیش‌تر این‌طور نبود و نتیجه‌اش در اپ این می‌شد: کاربر در صفحه‌ی آشنایی پایه
را انتخاب می‌کرد و بعد صفحه‌ی خانه خالی می‌آمد.
"""
def setUp(self):
_django_cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='dev-home-fallback', is_guest=True, name='')
self.token = issue_access_token(self.user.id)
self.grade = Grade.objects.create(title='پایه نهم', icon='grade-9', color='#FF9800')
unit = Unit.objects.create(grade=self.grade, title='فصل ۱', order=1)
node = Node.objects.create(unit=unit, title='درس ۱', order=1)
Lesson.objects.create(node=node, title='توان صحیح', intro_text='.')
def tearDown(self):
_django_cache.clear()
def _home(self, query=''):
return self.client.get(f'/api/home{query}', HTTP_AUTHORIZATION=f'Bearer {self.token}')
def test_uses_the_saved_grade_when_no_param_is_given(self):
self.user.grade_id = self.grade.id
self.user.save(update_fields=['grade_id'])
response = self._home()
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['units']), 1)
self.assertEqual(response.data['units'][0]['title'], 'فصل ۱')
def test_explicit_param_wins_over_the_saved_grade(self):
other = Grade.objects.create(title='پایه دهم', icon='grade-10', color='#E91E63')
unit = Unit.objects.create(grade=other, title='فصل مثلثات', order=1)
node = Node.objects.create(unit=unit, title='درس ۱', order=1)
Lesson.objects.create(node=node, title='سینوس', intro_text='.')
self.user.grade_id = self.grade.id
self.user.save(update_fields=['grade_id'])
response = self._home(f'?grade={other.id}')
self.assertEqual(response.data['units'][0]['title'], 'فصل مثلثات')
def test_user_without_a_grade_gets_an_empty_map_not_an_error(self):
response = self._home()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['units'], [])
def test_unknown_explicit_grade_is_still_404(self):
self.assertEqual(self._home('?grade=999999').status_code, 404)
def test_a_deleted_saved_grade_degrades_to_an_empty_map(self):
# پایه‌ای که کاربر ذخیره کرده حذف شده — این خطای کاربر نیست.
self.user.grade_id = 999999
self.user.save(update_fields=['grade_id'])
response = self._home()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['units'], [])