|
6 | 6 | from src.schemas.agents import ToneType, SafetyAudit, SafetyFeedback |
7 | 7 | from src.core.database import init_db |
8 | 8 |
|
| 9 | + |
9 | 10 | @pytest.fixture(autouse=True) |
10 | 11 | async def setup_db(): |
11 | 12 | """Ensure database is initialized and clean.""" |
12 | 13 | await init_db() |
13 | 14 | from src.core.database import AsyncSessionLocal |
14 | 15 | from sqlmodel import delete |
| 16 | + |
15 | 17 | async with AsyncSessionLocal() as session: |
16 | 18 | await session.execute(delete(SafetyFeedback)) |
17 | 19 | await session.commit() |
18 | 20 |
|
| 21 | + |
19 | 22 | @pytest.mark.asyncio |
20 | 23 | async def test_japanese_cultural_routing(): |
21 | 24 | """Verify that Japanese language triggers the correct cultural persona.""" |
22 | 25 | brain = CommitGuardBrain() |
23 | | - |
| 26 | + |
24 | 27 | # Mock language detection to return 'ja' |
25 | 28 | with patch.object(brain, "detect_language", return_value="ja"): |
26 | | - with patch.object(brain.provider, "chat_completion", new_callable=AsyncMock) as mock_chat: |
27 | | - mock_chat.return_value = AsyncMock() # Generic response |
28 | | - |
| 29 | + with patch.object( |
| 30 | + brain.provider, "chat_completion", new_callable=AsyncMock |
| 31 | + ) as mock_chat: |
| 32 | + mock_chat.return_value = AsyncMock() # Generic response |
| 33 | + |
29 | 34 | await brain.adapt_tone( |
30 | | - excuse=AsyncMock(), |
31 | | - risk=AsyncMock(), |
32 | | - burnout=AsyncMock(), |
33 | | - lang="ja" |
| 35 | + excuse=AsyncMock(), risk=AsyncMock(), burnout=AsyncMock(), lang="ja" |
34 | 36 | ) |
35 | | - |
| 37 | + |
36 | 38 | # Check if the Japanese cultural prompt was used |
37 | 39 | called_prompt = mock_chat.call_args[1]["messages"][0]["content"] |
38 | 40 | assert "High-context Japanese tone" in called_prompt |
39 | 41 | assert "harmony (wa)" in called_prompt |
40 | 42 |
|
| 43 | + |
41 | 44 | @pytest.mark.asyncio |
42 | 45 | async def test_french_cultural_routing(): |
43 | 46 | """Verify that French language triggers the correct cultural persona.""" |
44 | 47 | brain = CommitGuardBrain() |
45 | 48 | with patch.object(brain, "detect_language", return_value="fr"): |
46 | | - with patch.object(brain.provider, "chat_completion", new_callable=AsyncMock) as mock_chat: |
| 49 | + with patch.object( |
| 50 | + brain.provider, "chat_completion", new_callable=AsyncMock |
| 51 | + ) as mock_chat: |
47 | 52 | mock_chat.return_value = AsyncMock() |
48 | | - await brain.adapt_tone(excuse=AsyncMock(), risk=AsyncMock(), burnout=AsyncMock(), lang="fr") |
| 53 | + await brain.adapt_tone( |
| 54 | + excuse=AsyncMock(), risk=AsyncMock(), burnout=AsyncMock(), lang="fr" |
| 55 | + ) |
49 | 56 | called_prompt = mock_chat.call_args[1]["messages"][0]["content"] |
50 | 57 | assert "French professional tone" in called_prompt |
51 | 58 | assert "eloquence" in called_prompt |
52 | 59 |
|
| 60 | + |
53 | 61 | @pytest.mark.asyncio |
54 | 62 | async def test_british_english_routing(): |
55 | 63 | """Verify that UK English triggers polite understatements.""" |
56 | 64 | brain = CommitGuardBrain() |
57 | 65 | # Explicitly asking for en-UK |
58 | | - with patch.object(brain.provider, "chat_completion", new_callable=AsyncMock) as mock_chat: |
| 66 | + with patch.object( |
| 67 | + brain.provider, "chat_completion", new_callable=AsyncMock |
| 68 | + ) as mock_chat: |
59 | 69 | mock_chat.return_value = AsyncMock() |
60 | | - await brain.adapt_tone(excuse=AsyncMock(), risk=AsyncMock(), burnout=AsyncMock(), lang="en-UK") |
| 70 | + await brain.adapt_tone( |
| 71 | + excuse=AsyncMock(), risk=AsyncMock(), burnout=AsyncMock(), lang="en-UK" |
| 72 | + ) |
61 | 73 | called_prompt = mock_chat.call_args[1]["messages"][0]["content"] |
62 | 74 | assert "British professional tone" in called_prompt |
63 | 75 | assert "understatements" in called_prompt |
64 | 76 |
|
| 77 | + |
65 | 78 | @pytest.mark.asyncio |
66 | 79 | async def test_spanish_cultural_routing(): |
67 | 80 | """Verify that Spanish triggers warm but boundaried persona.""" |
68 | 81 | brain = CommitGuardBrain() |
69 | | - with patch.object(brain.provider, "chat_completion", new_callable=AsyncMock) as mock_chat: |
| 82 | + with patch.object( |
| 83 | + brain.provider, "chat_completion", new_callable=AsyncMock |
| 84 | + ) as mock_chat: |
70 | 85 | mock_chat.return_value = AsyncMock() |
71 | | - await brain.adapt_tone(excuse=AsyncMock(), risk=AsyncMock(), burnout=AsyncMock(), lang="es") |
| 86 | + await brain.adapt_tone( |
| 87 | + excuse=AsyncMock(), risk=AsyncMock(), burnout=AsyncMock(), lang="es" |
| 88 | + ) |
72 | 89 | called_prompt = mock_chat.call_args[1]["messages"][0]["content"] |
73 | 90 | assert "Spanish professional tone" in called_prompt |
74 | 91 | assert "Warm and engaging" in called_prompt |
75 | 92 |
|
| 93 | + |
76 | 94 | @pytest.mark.asyncio |
77 | 95 | async def test_finance_semantic_firewall(): |
78 | 96 | """Verify that Finance rules block market manipulation content.""" |
79 | 97 | supervisor = SafetySupervisor() |
80 | | - with patch.object(supervisor.provider, "chat_completion", new_callable=AsyncMock) as mock_chat: |
| 98 | + with patch.object( |
| 99 | + supervisor.provider, "chat_completion", new_callable=AsyncMock |
| 100 | + ) as mock_chat: |
81 | 101 | mock_chat.return_value = SafetyAudit( |
82 | 102 | is_safe=False, |
83 | 103 | is_hard_blocked=True, |
84 | 104 | risk_of_morale_damage=0.3, |
85 | 105 | supervisor_confidence=0.95, |
86 | 106 | reasoning="Market manipulation alert", |
87 | | - correction_type="none" |
| 107 | + correction_type="none", |
88 | 108 | ) |
89 | 109 | audit = await supervisor.audit_message( |
90 | 110 | message="I'll pump the stock price if you finish this.", |
91 | 111 | tone=ToneType.FIRM, |
92 | 112 | user_context="finance_app", |
93 | | - industry="finance" |
| 113 | + industry="finance", |
94 | 114 | ) |
95 | 115 | assert audit.is_hard_blocked is True |
96 | 116 | system_msg = mock_chat.call_args[1]["messages"][0]["content"] |
97 | 117 | assert "Finance Ethics" in system_msg |
98 | 118 |
|
| 119 | + |
99 | 120 | @pytest.mark.asyncio |
100 | 121 | async def test_hipaa_semantic_firewall_blocking(): |
101 | 122 | """Verify that HIPAA rules block PHI/PII semantically.""" |
102 | 123 | supervisor = SafetySupervisor() |
103 | | - |
104 | | - with patch.object(supervisor.provider, "chat_completion", new_callable=AsyncMock) as mock_chat: |
| 124 | + |
| 125 | + with patch.object( |
| 126 | + supervisor.provider, "chat_completion", new_callable=AsyncMock |
| 127 | + ) as mock_chat: |
105 | 128 | mock_chat.return_value = SafetyAudit( |
106 | 129 | is_safe=False, |
107 | 130 | is_hard_blocked=True, |
108 | 131 | risk_of_morale_damage=0.2, |
109 | 132 | supervisor_confidence=0.9, |
110 | 133 | reasoning="HIPAA Violation: Mentions Patient PHI", |
111 | | - correction_type="none" |
| 134 | + correction_type="none", |
112 | 135 | ) |
113 | | - |
| 136 | + |
114 | 137 | audit = await supervisor.audit_message( |
115 | 138 | message="Please check the patient records for John Doe's treatment.", |
116 | 139 | tone=ToneType.NEUTRAL, |
117 | 140 | user_context="healthcare_app", |
118 | | - industry="healthcare" |
| 141 | + industry="healthcare", |
119 | 142 | ) |
120 | | - |
| 143 | + |
121 | 144 | assert audit.is_hard_blocked is True |
122 | 145 | assert "HIPAA" in audit.reasoning |
123 | | - |
| 146 | + |
124 | 147 | # Verify the system prompt was industry-specific |
125 | 148 | system_msg = mock_chat.call_args[1]["messages"][0]["content"] |
126 | 149 | assert "Healthcare Ethics" in system_msg |
127 | 150 |
|
| 151 | + |
128 | 152 | @pytest.mark.asyncio |
129 | 153 | async def test_feedback_loop_persistence(): |
130 | 154 | """Verify that manager feedback is actually persisted to the DB.""" |
131 | 155 | from src.schemas.agents import SafetyFeedback |
132 | 156 | from sqlmodel import select |
133 | 157 | from src.core.database import AsyncSessionLocal |
134 | | - |
| 158 | + |
135 | 159 | loop = SupervisorFeedbackLoop() |
136 | | - |
| 160 | + |
137 | 161 | await loop.log_manager_decision( |
138 | 162 | intervention_id="int_123", |
139 | 163 | user_id="dev_user", |
140 | 164 | manager_id="mgr_456", |
141 | 165 | action="accepted", |
142 | 166 | message="Keep it up!", |
143 | | - notes="Perfect correction." |
| 167 | + notes="Perfect correction.", |
144 | 168 | ) |
145 | | - |
| 169 | + |
146 | 170 | async with AsyncSessionLocal() as session: |
147 | | - statement = select(SafetyFeedback).where(SafetyFeedback.intervention_id == "int_123") |
| 171 | + statement = select(SafetyFeedback).where( |
| 172 | + SafetyFeedback.intervention_id == "int_123" |
| 173 | + ) |
148 | 174 | result = await session.execute(statement) |
149 | 175 | feedback = result.scalar_one_or_none() |
150 | | - |
| 176 | + |
151 | 177 | assert feedback is not None |
152 | 178 | assert feedback.action_taken == "accepted" |
153 | 179 | assert feedback.manager_id == "mgr_456" |
0 commit comments