Compare commits
2 Commits
fix/94-saf
...
burn/123-1
| Author | SHA1 | Date | |
|---|---|---|---|
| f833bd5c5e | |||
| 41819292e3 |
@@ -104,13 +104,10 @@ MEDIUM_INDICATORS = [
|
||||
r"\blost\s+all\s+hope\b",
|
||||
r"\bno\s+tomorrow\b",
|
||||
# Contextual versions (from crisis_detector.py legacy)
|
||||
# NOTE: feel(s/ing)? hopeless, trapped, desperate, no future, nothing left,
|
||||
# and give(n)? up on myself are already in HIGH_INDICATORS — do not
|
||||
# duplicate here. See issue #123.
|
||||
r"\bfeel(?:s|ing)?\s+(?:so\s+)?worthless\b",
|
||||
r"\bfeel(?:s|ing)?\s+(?:so\s+)?hopeless\b",
|
||||
r"\bfeel(?:s|ing)?\s+trapped\b",
|
||||
r"\bfeel(?:s|ing)?\s+desperate\b",
|
||||
r"\bno\s+future\s+(?:for\s+me|ahead|left)\b",
|
||||
r"\bnothing\s+left\s+(?:to\s+(?:live|hope)\s+for|inside)\b",
|
||||
r"\bgive(?:n)?\s*up\s+on\s+myself\b",
|
||||
]
|
||||
|
||||
LOW_INDICATORS = [
|
||||
|
||||
167
tests/test_crisis_indicator_dedup.py
Normal file
167
tests/test_crisis_indicator_dedup.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Regression test for issue #123: duplicate crisis indicator patterns across tiers.
|
||||
|
||||
Ensures that no pattern appears in more than one indicator tier.
|
||||
Duplicate patterns waste regex matching cycles and create ambiguity
|
||||
about which tier a message should trigger.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from crisis.detect import (
|
||||
CRITICAL_INDICATORS,
|
||||
HIGH_INDICATORS,
|
||||
MEDIUM_INDICATORS,
|
||||
LOW_INDICATORS,
|
||||
)
|
||||
from crisis_detector import CrisisDetector, detect_crisis
|
||||
|
||||
|
||||
class TestNoDuplicatePatterns(unittest.TestCase):
|
||||
"""Ensure no pattern appears in more than one tier."""
|
||||
|
||||
def test_no_duplicates_between_critical_and_high(self):
|
||||
"""CRITICAL and HIGH should not share patterns."""
|
||||
critical_set = set(CRITICAL_INDICATORS)
|
||||
dupes = [p for p in HIGH_INDICATORS if p in critical_set]
|
||||
self.assertEqual(dupes, [], f"Duplicates between CRITICAL and HIGH: {dupes}")
|
||||
|
||||
def test_no_duplicates_between_critical_and_medium(self):
|
||||
"""CRITICAL and MEDIUM should not share patterns."""
|
||||
critical_set = set(CRITICAL_INDICATORS)
|
||||
dupes = [p for p in MEDIUM_INDICATORS if p in critical_set]
|
||||
self.assertEqual(dupes, [], f"Duplicates between CRITICAL and MEDIUM: {dupes}")
|
||||
|
||||
def test_no_duplicates_between_high_and_medium(self):
|
||||
"""HIGH and MEDIUM should not share patterns (issue #123)."""
|
||||
high_set = set(HIGH_INDICATORS)
|
||||
dupes = [p for p in MEDIUM_INDICATORS if p in high_set]
|
||||
self.assertEqual(dupes, [], f"Duplicates between HIGH and MEDIUM: {dupes}")
|
||||
|
||||
def test_no_duplicates_between_high_and_low(self):
|
||||
"""HIGH and LOW should not share patterns."""
|
||||
high_set = set(HIGH_INDICATORS)
|
||||
dupes = [p for p in LOW_INDICATORS if p in high_set]
|
||||
self.assertEqual(dupes, [], f"Duplicates between HIGH and LOW: {dupes}")
|
||||
|
||||
def test_no_duplicates_between_medium_and_low(self):
|
||||
"""MEDIUM and LOW should not share patterns."""
|
||||
medium_set = set(MEDIUM_INDICATORS)
|
||||
dupes = [p for p in LOW_INDICATORS if p in medium_set]
|
||||
self.assertEqual(dupes, [], f"Duplicates between MEDIUM and LOW: {dupes}")
|
||||
|
||||
def test_no_duplicates_between_critical_and_low(self):
|
||||
"""CRITICAL and LOW should not share patterns."""
|
||||
critical_set = set(CRITICAL_INDICATORS)
|
||||
dupes = [p for p in LOW_INDICATORS if p in critical_set]
|
||||
self.assertEqual(dupes, [], f"Duplicates between CRITICAL and LOW: {dupes}")
|
||||
|
||||
def test_no_internal_duplicates(self):
|
||||
"""Each tier should not contain duplicate patterns internally."""
|
||||
for name, indicators in [
|
||||
("CRITICAL", CRITICAL_INDICATORS),
|
||||
("HIGH", HIGH_INDICATORS),
|
||||
("MEDIUM", MEDIUM_INDICATORS),
|
||||
("LOW", LOW_INDICATORS),
|
||||
]:
|
||||
seen = set()
|
||||
dupes = []
|
||||
for p in indicators:
|
||||
if p in seen:
|
||||
dupes.append(p)
|
||||
seen.add(p)
|
||||
self.assertEqual(dupes, [], f"Internal duplicates in {name}: {dupes}")
|
||||
|
||||
|
||||
class TestSpecificDuplicatesFromIssue123(unittest.TestCase):
|
||||
"""Verify the 6 specific duplicates from issue #123 are fixed."""
|
||||
|
||||
def test_feel_hopeless_not_in_medium(self):
|
||||
"""feel(s/ing)? (so)? hopeless should only be in HIGH."""
|
||||
pattern = r"\bfeel(?:s|ing)?\s+(?:so\s+)?hopeless\b"
|
||||
self.assertIn(pattern, HIGH_INDICATORS)
|
||||
self.assertNotIn(pattern, MEDIUM_INDICATORS)
|
||||
|
||||
def test_feel_trapped_not_in_medium(self):
|
||||
"""feel(s/ing)? trapped should only be in HIGH."""
|
||||
pattern = r"\bfeel(?:s|ing)?\s+trapped\b"
|
||||
self.assertIn(pattern, HIGH_INDICATORS)
|
||||
self.assertNotIn(pattern, MEDIUM_INDICATORS)
|
||||
|
||||
def test_feel_desperate_not_in_medium(self):
|
||||
"""feel(s/ing)? desperate should only be in HIGH."""
|
||||
pattern = r"\bfeel(?:s|ing)?\s+desperate\b"
|
||||
self.assertIn(pattern, HIGH_INDICATORS)
|
||||
self.assertNotIn(pattern, MEDIUM_INDICATORS)
|
||||
|
||||
def test_no_future_not_in_medium(self):
|
||||
"""no future (for me|ahead|left) should only be in HIGH."""
|
||||
pattern = r"\bno\s+future\s+(?:for\s+me|ahead|left)\b"
|
||||
self.assertIn(pattern, HIGH_INDICATORS)
|
||||
self.assertNotIn(pattern, MEDIUM_INDICATORS)
|
||||
|
||||
def test_nothing_left_not_in_medium(self):
|
||||
"""nothing left (to (live|hope) for|inside) should only be in HIGH."""
|
||||
pattern = r"\bnothing\s+left\s+(?:to\s+(?:live|hope)\s+for|inside)\b"
|
||||
self.assertIn(pattern, HIGH_INDICATORS)
|
||||
self.assertNotIn(pattern, MEDIUM_INDICATORS)
|
||||
|
||||
def test_give_up_on_myself_not_in_medium(self):
|
||||
"""give(n)? up on myself should only be in HIGH."""
|
||||
pattern = r"\bgive(?:n)?\s*up\s+on\s+myself\b"
|
||||
self.assertIn(pattern, HIGH_INDICATORS)
|
||||
self.assertNotIn(pattern, MEDIUM_INDICATORS)
|
||||
|
||||
|
||||
class TestDetectionStillWorks(unittest.TestCase):
|
||||
"""Verify that removing duplicates didn't break detection."""
|
||||
|
||||
def setUp(self):
|
||||
self.detector = CrisisDetector()
|
||||
|
||||
def test_high_patterns_still_detected(self):
|
||||
"""Messages matching removed MEDIUM duplicates should still hit HIGH."""
|
||||
high_messages = [
|
||||
"I feel so hopeless about everything",
|
||||
"I feel trapped and can't escape",
|
||||
"I feel desperate for help",
|
||||
"There's no future for me",
|
||||
"I have nothing left to live for",
|
||||
"I've given up on myself",
|
||||
]
|
||||
for msg in high_messages:
|
||||
result = detect_crisis(msg)
|
||||
self.assertIn(result.level, ("HIGH", "CRITICAL"),
|
||||
f"'{msg}' should trigger HIGH/CRITICAL, got {result.level}")
|
||||
|
||||
def test_medium_patterns_still_detected(self):
|
||||
"""Original MEDIUM patterns should still work."""
|
||||
medium_messages = [
|
||||
"I feel so worthless",
|
||||
"I'm exhausted and broken",
|
||||
"Everything feels dark",
|
||||
"I'm drowning in this",
|
||||
]
|
||||
for msg in medium_messages:
|
||||
result = detect_crisis(msg)
|
||||
self.assertIn(result.level, ("MEDIUM", "HIGH", "CRITICAL"),
|
||||
f"'{msg}' should trigger MEDIUM+, got {result.level}")
|
||||
|
||||
def test_low_patterns_still_detected(self):
|
||||
"""LOW patterns should still work."""
|
||||
result = detect_crisis("I'm having a tough day")
|
||||
self.assertIn(result.level, ("LOW", "MEDIUM", "HIGH", "CRITICAL"))
|
||||
|
||||
def test_none_still_clean(self):
|
||||
"""Innocent messages should not trigger."""
|
||||
result = detect_crisis("I had a great lunch with friends")
|
||||
self.assertEqual(result.level, "NONE")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user