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"\blost\s+all\s+hope\b",
|
||||||
r"\bno\s+tomorrow\b",
|
r"\bno\s+tomorrow\b",
|
||||||
# Contextual versions (from crisis_detector.py legacy)
|
# 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+)?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 = [
|
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