from __future__ import annotations from unittest.mock import AsyncMock, patch from asgiref.sync import async_to_sync from django.test import TransactionTestCase from core.commands.base import CommandContext from core.commands.handlers.bp import BPCommandHandler from core.models import ( AI, BusinessPlanDocument, ChatSession, CommandAction, CommandChannelBinding, CommandProfile, CommandRun, Message, Person, PersonIdentifier, User, ) class BPFallbackTests(TransactionTestCase): def setUp(self): self.user = User.objects.create_user( username="bp-fallback-user", email="bp-fallback@example.com", password="x", ) self.person = Person.objects.create(user=self.user, name="Fallback Person") self.identifier = PersonIdentifier.objects.create( user=self.user, person=self.person, service="whatsapp", identifier="120363402761690215", ) self.session = ChatSession.objects.create( user=self.user, identifier=self.identifier, ) self.profile = CommandProfile.objects.create( user=self.user, slug="bp", name="Business Plan", enabled=True, trigger_token="#bp#", reply_required=True, exact_match_only=True, ) CommandChannelBinding.objects.create( profile=self.profile, direction="ingress", service="whatsapp", channel_identifier="120363402761690215", enabled=True, ) CommandChannelBinding.objects.create( profile=self.profile, direction="egress", service="web", channel_identifier="120363402761690215", enabled=True, ) for action_type, position in ( ("extract_bp", 0), ("save_document", 1), ("post_result", 2), ): CommandAction.objects.create( profile=self.profile, action_type=action_type, enabled=True, position=position, ) AI.objects.create( user=self.user, base_url="https://example.invalid", api_key="test-key", model="gpt-4o-mini", ) def test_bp_fails_fast_when_ai_fails(self): anchor = Message.objects.create( user=self.user, session=self.session, sender_uuid="peer", text="Anchor content", ts=1000, source_service="whatsapp", source_message_id="wa-anchor-1", ) trigger = Message.objects.create( user=self.user, session=self.session, sender_uuid="peer", text="#bp#", ts=2000, source_service="whatsapp", source_message_id="wa-trigger-1", reply_to=anchor, reply_source_service="whatsapp", reply_source_message_id="wa-anchor-1", ) with patch( "core.commands.handlers.bp.ai_runner.run_prompt", new=AsyncMock(side_effect=RuntimeError("quota")), ): result = async_to_sync(BPCommandHandler().execute)( CommandContext( service="whatsapp", channel_identifier="120363402761690215", message_id=str(trigger.id), user_id=self.user.id, message_text="#bp#", payload={}, ) ) self.assertFalse(result.ok) run = CommandRun.objects.get(trigger_message=trigger, profile=self.profile) self.assertEqual("failed", run.status) self.assertIn("bp_ai_failed", str(run.error)) self.assertFalse(BusinessPlanDocument.objects.filter(trigger_message=trigger).exists()) def test_bp_uses_same_ai_selection_order_as_compose(self): AI.objects.create( user=self.user, base_url="https://example.invalid", api_key="another-key", model="gpt-4o", ) selected = AI.objects.filter(user=self.user).first() # Compose uses QuerySet.first() without explicit ordering; BP should match. self.assertIsNotNone(selected) self.assertEqual(self.profile.user_id, selected.user_id)