"""Coverage-focused tests for ``animetix.api.cognition`` DRF views. Exercises success, validation (400) and error (500) paths of every endpoint via the real URL routing + APIClient, with the constructor-injected services mocked through real-container provider overrides and the throttle cache forced to local memory (CI sets REDIS_URL which would otherwise make DRF throttling hit a real redis). """ from contextlib import contextmanager from unittest.mock import MagicMock import pytest from animetix.containers import get_container from django.contrib.auth.models import User from django.test import override_settings from django.urls import reverse from rest_framework.test import APIClient real_container = get_container() LOCMEM_CACHE = { "default": { "BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "cognition-coverage", } } pytestmark = pytest.mark.django_db @pytest.fixture(autouse=True) def _locmem_cache(): """Force LocMem cache so DRF throttling does not hit redis (CI sets REDIS_URL).""" with override_settings(CACHES=LOCMEM_CACHE): yield @pytest.fixture def api_client(): return APIClient() @pytest.fixture def user(db): return User.objects.create_user(username="cog_user", password="password") @pytest.fixture def auth_client(api_client, user): api_client.force_authenticate(user=user) return api_client def _patch_container(): """Override the cognition providers on the real container with mocks. Returns a context manager plus a container-shaped MagicMock whose ``.core.X.return_value`` is the very service instance injected into the views — the historical call sites keep configuring their mocks the same way, but the wiring now goes through constructor injection. """ fake = MagicMock() overrides = [ ( real_container.core.archetype_drift_service, fake.core.archetype_drift_service.return_value, ), ( real_container.core.neuro_symbolic_user_profiler, fake.core.neuro_symbolic_user_profiler.return_value, ), ( real_container.persistence.feedback_adapter, fake.persistence.feedback_adapter.return_value, ), ( real_container.core.self_play_debate_service, fake.core.self_play_debate_service.return_value, ), ( real_container.core.counterfactual_simulator, fake.core.counterfactual_simulator.return_value, ), ( real_container.core.cove_oracle_service, fake.core.cove_oracle_service.return_value, ), ( real_container.core.cfr_game_solver, fake.core.cfr_game_solver.return_value, ), ] @contextmanager def _cm(): try: for provider, mock in overrides: provider.override(mock) yield finally: for provider, _ in overrides: provider.reset_last_overriding() return _cm(), fake # -------------------------------------------------------------------------- # ArchetypeNexusView (GET) # -------------------------------------------------------------------------- def test_archetype_nexus_get_success_creates_snapshot(auth_client, user): from animetix.models import ArchetypeDriftSnapshot cm, container = _patch_container() with cm: container.core.archetype_drift_service.return_value.calculate_drift.return_value = MagicMock( archetype_id="The Sage", primary_accent="blue", aura_type="calm", aura_intensity=0.8, font_vibe="serif", ) container.core.neuro_symbolic_user_profiler.return_value.deduce_preference_rules.return_value = [ "Likes action" ] container.persistence.feedback_adapter.return_value.get_user_feedback.return_value = [ {"input_context": "ctx", "is_positive": True, "created_at": "now"} ] resp = auth_client.get(reverse("api_archetype_nexus")) assert resp.status_code == 200 assert resp.data["archetype"]["id"] == "The Sage" assert resp.data["logical_rules"] == ["Likes action"] assert resp.data["cognitive_stats"]["memory_depth"] == 1 assert len(resp.data["recent_signals"]) == 1 # A snapshot row was written to the real ORM assert ArchetypeDriftSnapshot.objects.filter(user=user).count() == 1 def test_archetype_nexus_skips_snapshot_when_recent_exists(auth_client, user): """If a snapshot was created within the last hour, no new one is added.""" from animetix.models import ArchetypeDriftSnapshot ArchetypeDriftSnapshot.objects.create( user=user, archetype_id="Existing", intensity=0.5, shonen_affinity=0.1, seinen_affinity=0.1, logic_consistency=0.9, ) cm, container = _patch_container() with cm: container.core.archetype_drift_service.return_value.calculate_drift.return_value = MagicMock( archetype_id="The Sage", primary_accent="blue", aura_type="calm", aura_intensity=0.8, font_vibe="serif", ) container.core.neuro_symbolic_user_profiler.return_value.deduce_preference_rules.return_value = ( [] ) container.persistence.feedback_adapter.return_value.get_user_feedback.return_value = ( [] ) resp = auth_client.get(reverse("api_archetype_nexus")) assert resp.status_code == 200 # Still only the pre-existing snapshot; history is returned assert ArchetypeDriftSnapshot.objects.filter(user=user).count() == 1 assert len(resp.data["drift_history"]) == 1 def test_archetype_nexus_unauthenticated(api_client): resp = api_client.get(reverse("api_archetype_nexus")) assert resp.status_code in (401, 403) # -------------------------------------------------------------------------- # AIDebateArenaView (POST) # -------------------------------------------------------------------------- def test_ai_debate_arena_success(auth_client): cm, container = _patch_container() with cm: container.core.self_play_debate_service.return_value.run_debate.return_value = { "status": "success", "debate": "Naruto vs Sasuke", } resp = auth_client.post( reverse("api_debate_arena"), {"media_title": "Naruto", "topic": "Best Ninja"}, format="json", ) assert resp.status_code == 200 assert resp.data["debate"] == "Naruto vs Sasuke" def test_ai_debate_arena_validation_error(auth_client): resp = auth_client.post( reverse("api_debate_arena"), {"topic": "no title"}, format="json" ) assert resp.status_code == 400 assert "media_title" in resp.data def test_ai_debate_arena_service_exception(auth_client): cm, container = _patch_container() with cm: container.core.self_play_debate_service.return_value.run_debate.side_effect = ( RuntimeError("boom") ) resp = auth_client.post( reverse("api_debate_arena"), {"media_title": "Naruto", "topic": "X"}, format="json", ) assert resp.status_code == 500 assert resp.data["error"] == "Internal server error" # -------------------------------------------------------------------------- # NeuroMemoryManagementView (GET + POST actions) # -------------------------------------------------------------------------- def test_neuro_memory_get(auth_client): cm, container = _patch_container() with cm: container.core.neuro_symbolic_user_profiler.return_value.deduce_preference_rules.return_value = [ "Rule 1", "Rule 2", ] container.persistence.feedback_adapter.return_value.get_user_feedback.return_value = [ {"id": 1}, {"id": 2}, ] resp = auth_client.get(reverse("api_neuro_memory")) assert resp.status_code == 200 assert resp.data["total_signals"] == 2 assert len(resp.data["deduced_rules"]) == 2 def test_neuro_memory_post_reset(auth_client, user): from animetix.models import AIFeedback fb = AIFeedback.objects.create(user=user, feedback_type="like", is_positive=True) resp = auth_client.post( reverse("api_neuro_memory"), {"action": "reset"}, format="json" ) assert resp.status_code == 200 fb.refresh_from_db() assert fb.is_ignored is True def test_neuro_memory_post_revoke(auth_client, user): from animetix.models import AIFeedback fb = AIFeedback.objects.create(user=user, feedback_type="like", is_positive=True) resp = auth_client.post( reverse("api_neuro_memory"), {"action": "revoke", "feedback_id": fb.id}, format="json", ) assert resp.status_code == 200 fb.refresh_from_db() assert fb.is_ignored is True def test_neuro_memory_post_revoke_missing_id(auth_client): resp = auth_client.post( reverse("api_neuro_memory"), {"action": "revoke"}, format="json" ) assert resp.status_code == 400 assert "feedback_id" in resp.data["error"] def test_neuro_memory_post_restore(auth_client, user): from animetix.models import AIFeedback fb = AIFeedback.objects.create( user=user, feedback_type="like", is_positive=True, is_ignored=True ) resp = auth_client.post( reverse("api_neuro_memory"), {"action": "restore", "feedback_id": fb.id}, format="json", ) assert resp.status_code == 200 fb.refresh_from_db() assert fb.is_ignored is False def test_neuro_memory_post_restore_missing_id(auth_client): resp = auth_client.post( reverse("api_neuro_memory"), {"action": "restore"}, format="json" ) assert resp.status_code == 400 def test_neuro_memory_post_update_weight(auth_client, user): from animetix.models import AIFeedback fb = AIFeedback.objects.create(user=user, feedback_type="like", is_positive=True) resp = auth_client.post( reverse("api_neuro_memory"), {"action": "update_weight", "feedback_id": fb.id, "weight": 2.5}, format="json", ) assert resp.status_code == 200 fb.refresh_from_db() assert fb.weight == 2.5 def test_neuro_memory_post_update_weight_missing_params(auth_client): resp = auth_client.post( reverse("api_neuro_memory"), {"action": "update_weight"}, format="json" ) assert resp.status_code == 400 def test_neuro_memory_post_invalid_action(auth_client): resp = auth_client.post( reverse("api_neuro_memory"), {"action": "nope"}, format="json" ) assert resp.status_code == 400 assert resp.data["error"] == "Invalid action" # -------------------------------------------------------------------------- # CounterfactualSimulatorView (POST) # -------------------------------------------------------------------------- def test_counterfactual_success(auth_client): cm, container = _patch_container() with cm: container.core.counterfactual_simulator.return_value.simulate_counterfactual_path.return_value = { "timeline": "alt" } resp = auth_client.post( reverse("api_counterfactual"), { "what_if": "What if X?", "actual_context": [{"speaker": "a", "text": "hi"}], }, format="json", ) assert resp.status_code == 200 assert resp.data["timeline"] == "alt" def test_counterfactual_validation_error(auth_client): resp = auth_client.post(reverse("api_counterfactual"), {}, format="json") assert resp.status_code == 400 assert "what_if" in resp.data def test_counterfactual_service_exception(auth_client): cm, container = _patch_container() with cm: container.core.counterfactual_simulator.return_value.simulate_counterfactual_path.side_effect = ValueError( "kaboom" ) resp = auth_client.post( reverse("api_counterfactual"), {"what_if": "X"}, format="json" ) assert resp.status_code == 500 assert resp.data["error"] == "Internal server error" # -------------------------------------------------------------------------- # CoveOracleView (POST) # -------------------------------------------------------------------------- def test_cove_oracle_success(auth_client): cm, container = _patch_container() with cm: container.core.cove_oracle_service.return_value.trace_verification.return_value = { "verified": True } resp = auth_client.post( reverse("api_cove_oracle"), {"question": "Is X real?", "media_type": "manga"}, format="json", ) assert resp.status_code == 200 assert resp.data["verified"] is True def test_cove_oracle_validation_error(auth_client): resp = auth_client.post(reverse("api_cove_oracle"), {}, format="json") assert resp.status_code == 400 assert "question" in resp.data def test_cove_oracle_service_exception(auth_client): cm, container = _patch_container() with cm: container.core.cove_oracle_service.return_value.trace_verification.side_effect = RuntimeError( "cove down" ) resp = auth_client.post( reverse("api_cove_oracle"), {"question": "Q?"}, format="json" ) assert resp.status_code == 500 assert resp.data["error"] == "Internal server error" # -------------------------------------------------------------------------- # CFRStrategyLabView (POST) # -------------------------------------------------------------------------- def test_cfr_strategy_success(auth_client): cm, container = _patch_container() with cm: container.core.cfr_game_solver.return_value.solve_with_history.return_value = { "converged": True } resp = auth_client.post( reverse("api_cfr_strategy_lab"), {"questions": ["q1", "q2"], "iterations": 50}, format="json", ) assert resp.status_code == 200 assert resp.data["converged"] is True def test_cfr_strategy_validation_error(auth_client): # iterations out of allowed range -> serializer invalid resp = auth_client.post( reverse("api_cfr_strategy_lab"), {"iterations": 99999}, format="json" ) assert resp.status_code == 400 assert "iterations" in resp.data def test_cfr_strategy_service_exception(auth_client): cm, container = _patch_container() with cm: container.core.cfr_game_solver.return_value.solve_with_history.side_effect = ( RuntimeError("cfr fail") ) resp = auth_client.post( reverse("api_cfr_strategy_lab"), {"iterations": 10}, format="json" ) assert resp.status_code == 500 assert resp.data["error"] == "Internal server error"