Remove silent fallbacks found in the 2026-10-03 audit
- /api/ai/ask: the stub answer is reachable only under DEBUG or RAG_FORCE_MOCK. Without RAG keys on the server it used to return 200 with "this is a stub, set OPENAI_API_KEY" and spend the student's quota; now 503. - /api/home: the `grade` alias is gone and sending it is a 400 instead of a silent fallback to the user's own grade. No client sent it. - activate_subscription logs ERROR when the paid order's plan no longer exists (it silently became 30 days). - resolve_grade_title logs instead of `except: pass`; curriculum_io no longer reads an `intro_text` key no file contains. - CLAUDE.md: "No silent fallbacks" rules for future agents. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, override_settings
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework.throttling import ScopedRateThrottle
|
||||
|
||||
@@ -514,3 +514,18 @@ class AskAIViewGuestThrottleBypassTests(TestCase):
|
||||
response_b = self._ask(issue_access_token(user_b.id), 'user-b-key-1')
|
||||
|
||||
self.assertEqual(response_b.status_code, 200)
|
||||
|
||||
|
||||
class StubNeverReachesProductionTests(TestCase):
|
||||
"""بیکلیدِ RAG و بیرون از DEBUG، دانشآموز ۵۰۳ میگیرد، نه متنِ «پاسخ نمونه (stub)»."""
|
||||
|
||||
@override_settings(DEBUG=False, OPENAI_API_KEY='', QDRANT_API_KEY='', RAG_FORCE_MOCK=False)
|
||||
def test_missing_keys_are_a_503_and_cost_nothing(self):
|
||||
cache.clear()
|
||||
user = User.objects.create(device_id='device-no-keys', is_guest=True, name='')
|
||||
response = APIClient().post(
|
||||
'/api/ai/ask', {'userQuestion': '۲+۲ چند است؟'}, format='json',
|
||||
HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}',
|
||||
)
|
||||
self.assertEqual(response.status_code, 503)
|
||||
self.assertFalse(Message.objects.filter(conversation__user_id=user.id).exists())
|
||||
|
||||
@@ -230,10 +230,14 @@ def generate_tutor_answer(
|
||||
تولید میکنه. مالکیتِ تاریخچهی مکالمه همیشه دستِ دیتابیسِ خودمونه، نه
|
||||
provider.
|
||||
|
||||
اگه RAG پیکربندی نشده باشه (نبودِ OPENAI_API_KEY/QDRANT_API_KEY یا
|
||||
RAG_FORCE_MOCK)، همون پاسخِ stubِ قطعیِ قبلی برمیگرده تا اجرای بدونِ
|
||||
کلید و تستها دستنخورده کار کنن.
|
||||
اگه RAG پیکربندی نشده باشه، پاسخِ stub فقط در DEBUG یا با RAG_FORCE_MOCK
|
||||
(تستها) برمیگرده. بیرون از آن خطا میدهد و AskAIView آن را ۵۰۳ میکند.
|
||||
تا ۲۰۲۶-۱۰-۰۳ stub در پروداکشن هم برمیگشت: اگر کلیدی از .env سرور میافتاد،
|
||||
دانشآموز با ۲۰۰ متنِ «این یک پاسخ نمونه (stub)… OPENAI_API_KEY را تنظیم
|
||||
کنید» میگرفت و سهمیهی روزانهاش هم کم میشد (با تستِ دورریختنی بازتولید شد).
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
from rag.services.config import rag_is_configured
|
||||
|
||||
if rag_is_configured():
|
||||
@@ -247,6 +251,9 @@ def generate_tutor_answer(
|
||||
answer, follow_ups, _meta = generate_answer(user_question, context_chunks)
|
||||
return answer, follow_ups
|
||||
|
||||
if not (settings.DEBUG or settings.RAG_FORCE_MOCK):
|
||||
raise RuntimeError('RAG پیکربندی نشده (OPENAI_API_KEY/QDRANT_API_KEY)؛ stub فقط در DEBUG.')
|
||||
|
||||
context_note = f' (با context: {context})' if context else ''
|
||||
answer = (
|
||||
f'این یک پاسخ نمونه (stub) برای سوال «{user_question}»{context_note} است. '
|
||||
|
||||
Reference in New Issue
Block a user