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:
2026-10-03 10:38:42 +03:30
parent 7e4b8b54a6
commit 7307bfcadb
10 changed files with 98 additions and 24 deletions

View File

@@ -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())