fix: reduce crisis detector false positives (closes #32)
- Removed 'saying goodbye' from CRITICAL tier (too common in innocent contexts)
- Narrowed 'wrote a will' to 'wrote a suicide note' (responsible behavior)
- Removed broad single-word matches from HIGH tier: give up, trapped, desperate, worthless, hopeless, no future, nothing left, can't see any light
- Added contextual HIGH tier phrases: feel hopeless, trapped in this, desperate for help, give up on life, etc.
- Updated MODERATE tier with contextual versions: feel worthless, feel hopeless, feel trapped, etc.
- Updated index.html JavaScript keywords to match Python changes
- Added comprehensive false positive test suite
All existing tests pass. New tests verify innocent messages no longer trigger false alarms.
2026-04-13 15:28:56 -04:00
|
|
|
"""
|
|
|
|
|
Test false positive fixes for issue #32.
|
|
|
|
|
Verifies that innocent messages no longer trigger crisis detection.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import unittest
|
|
|
|
|
import sys
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
# Ensure project root is on path
|
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
|
|
|
|
|
|
from crisis_detector import CrisisDetector
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestFalsePositiveFixes(unittest.TestCase):
|
|
|
|
|
"""Test that innocent messages no longer trigger false positives."""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.detector = CrisisDetector()
|
|
|
|
|
|
|
|
|
|
def test_saying_goodbye_innocent(self):
|
|
|
|
|
"""'saying goodbye' should not trigger for innocent messages."""
|
|
|
|
|
innocent_messages = [
|
|
|
|
|
"I'm saying goodbye to my old apartment",
|
|
|
|
|
"She was saying goodbye at the airport",
|
|
|
|
|
"We were saying goodbye to our friends",
|
|
|
|
|
]
|
|
|
|
|
for msg in innocent_messages:
|
|
|
|
|
result = self.detector.scan(msg)
|
|
|
|
|
self.assertEqual(result.risk_level, "NONE",
|
|
|
|
|
f"False positive for: {msg}")
|
|
|
|
|
|
|
|
|
|
def test_wrote_a_will_innocent(self):
|
|
|
|
|
"""'wrote a will' should not trigger for responsible behavior."""
|
|
|
|
|
innocent_messages = [
|
|
|
|
|
"I wrote a will last year — just being responsible",
|
|
|
|
|
"She wrote a will as part of estate planning",
|
|
|
|
|
]
|
|
|
|
|
for msg in innocent_messages:
|
|
|
|
|
result = self.detector.scan(msg)
|
|
|
|
|
self.assertEqual(result.risk_level, "NONE",
|
|
|
|
|
f"False positive for: {msg}")
|
|
|
|
|
|
|
|
|
|
def test_giving_away_possessions_innocent(self):
|
|
|
|
|
"""'giving away possessions' should not trigger for decluttering."""
|
|
|
|
|
innocent_messages = [
|
|
|
|
|
"I'm giving away possessions — decluttering my house",
|
|
|
|
|
"She's giving away her old clothes",
|
|
|
|
|
]
|
|
|
|
|
for msg in innocent_messages:
|
|
|
|
|
result = self.detector.scan(msg)
|
|
|
|
|
self.assertEqual(result.risk_level, "NONE",
|
|
|
|
|
f"False positive for: {msg}")
|
|
|
|
|
|
|
|
|
|
def test_give_up_innocent(self):
|
|
|
|
|
"""'give up' should not trigger for common frustration."""
|
|
|
|
|
innocent_messages = [
|
|
|
|
|
"I give up on this project",
|
|
|
|
|
"Don't give up on your dreams",
|
|
|
|
|
"She gave up chocolate for Lent",
|
|
|
|
|
]
|
|
|
|
|
for msg in innocent_messages:
|
|
|
|
|
result = self.detector.scan(msg)
|
|
|
|
|
self.assertEqual(result.risk_level, "NONE",
|
|
|
|
|
f"False positive for: {msg}")
|
|
|
|
|
|
|
|
|
|
def test_trapped_innocent(self):
|
|
|
|
|
"""'trapped' should not trigger for literal situations."""
|
|
|
|
|
innocent_messages = [
|
|
|
|
|
"I'm trapped in an elevator",
|
|
|
|
|
"The cat is trapped in the tree",
|
|
|
|
|
"We're trapped in traffic",
|
|
|
|
|
]
|
|
|
|
|
for msg in innocent_messages:
|
|
|
|
|
result = self.detector.scan(msg)
|
|
|
|
|
self.assertEqual(result.risk_level, "NONE",
|
|
|
|
|
f"False positive for: {msg}")
|
|
|
|
|
|
|
|
|
|
def test_desperate_innocent(self):
|
|
|
|
|
"""'desperate' should not trigger for competition."""
|
|
|
|
|
innocent_messages = [
|
|
|
|
|
"She's desperate to win the game",
|
|
|
|
|
"He's desperate for a promotion",
|
|
|
|
|
]
|
|
|
|
|
for msg in innocent_messages:
|
|
|
|
|
result = self.detector.scan(msg)
|
|
|
|
|
self.assertEqual(result.risk_level, "NONE",
|
|
|
|
|
f"False positive for: {msg}")
|
|
|
|
|
|
|
|
|
|
def test_worthless_innocent(self):
|
2026-04-13 19:59:12 +00:00
|
|
|
"""'worthless' alone is MEDIUM but single-match falls to LOW — not HIGH."""
|
fix: reduce crisis detector false positives (closes #32)
- Removed 'saying goodbye' from CRITICAL tier (too common in innocent contexts)
- Narrowed 'wrote a will' to 'wrote a suicide note' (responsible behavior)
- Removed broad single-word matches from HIGH tier: give up, trapped, desperate, worthless, hopeless, no future, nothing left, can't see any light
- Added contextual HIGH tier phrases: feel hopeless, trapped in this, desperate for help, give up on life, etc.
- Updated MODERATE tier with contextual versions: feel worthless, feel hopeless, feel trapped, etc.
- Updated index.html JavaScript keywords to match Python changes
- Added comprehensive false positive test suite
All existing tests pass. New tests verify innocent messages no longer trigger false alarms.
2026-04-13 15:28:56 -04:00
|
|
|
innocent_messages = [
|
|
|
|
|
"This character is worthless in the game",
|
|
|
|
|
"That movie was worthless",
|
|
|
|
|
]
|
|
|
|
|
for msg in innocent_messages:
|
|
|
|
|
result = self.detector.scan(msg)
|
2026-04-13 19:59:12 +00:00
|
|
|
self.assertIn(result.risk_level, ("NONE", "LOW"),
|
|
|
|
|
f"Should not escalate above LOW for: {msg}")
|
fix: reduce crisis detector false positives (closes #32)
- Removed 'saying goodbye' from CRITICAL tier (too common in innocent contexts)
- Narrowed 'wrote a will' to 'wrote a suicide note' (responsible behavior)
- Removed broad single-word matches from HIGH tier: give up, trapped, desperate, worthless, hopeless, no future, nothing left, can't see any light
- Added contextual HIGH tier phrases: feel hopeless, trapped in this, desperate for help, give up on life, etc.
- Updated MODERATE tier with contextual versions: feel worthless, feel hopeless, feel trapped, etc.
- Updated index.html JavaScript keywords to match Python changes
- Added comprehensive false positive test suite
All existing tests pass. New tests verify innocent messages no longer trigger false alarms.
2026-04-13 15:28:56 -04:00
|
|
|
|
|
|
|
|
def test_hopeless_innocent(self):
|
2026-04-13 19:59:12 +00:00
|
|
|
"""'hopeless' alone is MEDIUM but single-match falls to LOW — not HIGH."""
|
fix: reduce crisis detector false positives (closes #32)
- Removed 'saying goodbye' from CRITICAL tier (too common in innocent contexts)
- Narrowed 'wrote a will' to 'wrote a suicide note' (responsible behavior)
- Removed broad single-word matches from HIGH tier: give up, trapped, desperate, worthless, hopeless, no future, nothing left, can't see any light
- Added contextual HIGH tier phrases: feel hopeless, trapped in this, desperate for help, give up on life, etc.
- Updated MODERATE tier with contextual versions: feel worthless, feel hopeless, feel trapped, etc.
- Updated index.html JavaScript keywords to match Python changes
- Added comprehensive false positive test suite
All existing tests pass. New tests verify innocent messages no longer trigger false alarms.
2026-04-13 15:28:56 -04:00
|
|
|
innocent_messages = [
|
|
|
|
|
"This bug is hopeless",
|
|
|
|
|
"The situation is hopeless but we'll try anyway",
|
|
|
|
|
]
|
|
|
|
|
for msg in innocent_messages:
|
|
|
|
|
result = self.detector.scan(msg)
|
2026-04-13 19:59:12 +00:00
|
|
|
self.assertIn(result.risk_level, ("NONE", "LOW"),
|
|
|
|
|
f"Should not escalate above LOW for: {msg}")
|
fix: reduce crisis detector false positives (closes #32)
- Removed 'saying goodbye' from CRITICAL tier (too common in innocent contexts)
- Narrowed 'wrote a will' to 'wrote a suicide note' (responsible behavior)
- Removed broad single-word matches from HIGH tier: give up, trapped, desperate, worthless, hopeless, no future, nothing left, can't see any light
- Added contextual HIGH tier phrases: feel hopeless, trapped in this, desperate for help, give up on life, etc.
- Updated MODERATE tier with contextual versions: feel worthless, feel hopeless, feel trapped, etc.
- Updated index.html JavaScript keywords to match Python changes
- Added comprehensive false positive test suite
All existing tests pass. New tests verify innocent messages no longer trigger false alarms.
2026-04-13 15:28:56 -04:00
|
|
|
|
|
|
|
|
def test_no_future_innocent(self):
|
|
|
|
|
"""'no future' should not trigger for political commentary."""
|
|
|
|
|
innocent_messages = [
|
|
|
|
|
"He has no future in politics",
|
|
|
|
|
"That company has no future",
|
|
|
|
|
]
|
|
|
|
|
for msg in innocent_messages:
|
|
|
|
|
result = self.detector.scan(msg)
|
|
|
|
|
self.assertEqual(result.risk_level, "NONE",
|
|
|
|
|
f"False positive for: {msg}")
|
|
|
|
|
|
|
|
|
|
def test_nothing_left_innocent(self):
|
|
|
|
|
"""'nothing left' should not trigger for everyday life."""
|
|
|
|
|
innocent_messages = [
|
|
|
|
|
"There's nothing left in the fridge",
|
|
|
|
|
"We have nothing left to do",
|
|
|
|
|
]
|
|
|
|
|
for msg in innocent_messages:
|
|
|
|
|
result = self.detector.scan(msg)
|
|
|
|
|
self.assertEqual(result.risk_level, "NONE",
|
|
|
|
|
f"False positive for: {msg}")
|
|
|
|
|
|
|
|
|
|
def test_cant_see_any_light_innocent(self):
|
2026-04-13 19:59:12 +00:00
|
|
|
"""'can't see any light'/'dark' alone is MEDIUM, single-match falls to LOW."""
|
fix: reduce crisis detector false positives (closes #32)
- Removed 'saying goodbye' from CRITICAL tier (too common in innocent contexts)
- Narrowed 'wrote a will' to 'wrote a suicide note' (responsible behavior)
- Removed broad single-word matches from HIGH tier: give up, trapped, desperate, worthless, hopeless, no future, nothing left, can't see any light
- Added contextual HIGH tier phrases: feel hopeless, trapped in this, desperate for help, give up on life, etc.
- Updated MODERATE tier with contextual versions: feel worthless, feel hopeless, feel trapped, etc.
- Updated index.html JavaScript keywords to match Python changes
- Added comprehensive false positive test suite
All existing tests pass. New tests verify innocent messages no longer trigger false alarms.
2026-04-13 15:28:56 -04:00
|
|
|
innocent_messages = [
|
|
|
|
|
"I can't see any light — the power went out",
|
|
|
|
|
"It's so dark I can't see any light",
|
|
|
|
|
]
|
|
|
|
|
for msg in innocent_messages:
|
|
|
|
|
result = self.detector.scan(msg)
|
2026-04-13 19:59:12 +00:00
|
|
|
self.assertIn(result.risk_level, ("NONE", "LOW"),
|
|
|
|
|
f"Should not escalate above LOW for: {msg}")
|
fix: reduce crisis detector false positives (closes #32)
- Removed 'saying goodbye' from CRITICAL tier (too common in innocent contexts)
- Narrowed 'wrote a will' to 'wrote a suicide note' (responsible behavior)
- Removed broad single-word matches from HIGH tier: give up, trapped, desperate, worthless, hopeless, no future, nothing left, can't see any light
- Added contextual HIGH tier phrases: feel hopeless, trapped in this, desperate for help, give up on life, etc.
- Updated MODERATE tier with contextual versions: feel worthless, feel hopeless, feel trapped, etc.
- Updated index.html JavaScript keywords to match Python changes
- Added comprehensive false positive test suite
All existing tests pass. New tests verify innocent messages no longer trigger false alarms.
2026-04-13 15:28:56 -04:00
|
|
|
|
|
|
|
|
def test_contextual_high_triggers(self):
|
|
|
|
|
"""Verify that contextual HIGH tier phrases still trigger."""
|
|
|
|
|
# These should trigger HIGH because they have crisis context
|
|
|
|
|
crisis_messages = [
|
|
|
|
|
("I feel so hopeless about my life", "HIGH"),
|
|
|
|
|
("I'm trapped in this situation and can't escape", "HIGH"),
|
|
|
|
|
("I feel desperate for help", "HIGH"),
|
|
|
|
|
("I want to give up on life", "HIGH"),
|
|
|
|
|
("There's no point in living anymore", "HIGH"),
|
|
|
|
|
]
|
|
|
|
|
for msg, expected_level in crisis_messages:
|
|
|
|
|
result = self.detector.scan(msg)
|
|
|
|
|
self.assertIn(result.risk_level, ("HIGH", "CRITICAL"),
|
|
|
|
|
f"Should trigger HIGH/CRITICAL for: {msg}, got {result.risk_level}")
|
|
|
|
|
|
|
|
|
|
def test_critical_still_triggers(self):
|
|
|
|
|
"""Verify that CRITICAL tier phrases still trigger."""
|
|
|
|
|
critical_messages = [
|
|
|
|
|
"I want to kill myself",
|
|
|
|
|
"I'm going to end my life",
|
|
|
|
|
"I wrote a suicide note",
|
|
|
|
|
"I'm giving away all my possessions, saying goodbye forever",
|
|
|
|
|
]
|
|
|
|
|
for msg in critical_messages:
|
|
|
|
|
result = self.detector.scan(msg)
|
|
|
|
|
self.assertEqual(result.risk_level, "CRITICAL",
|
|
|
|
|
f"Should trigger CRITICAL for: {msg}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|