Compare commits

...

2 Commits

Author SHA1 Message Date
f833bd5c5e test: add regression tests for duplicate indicator patterns (#123)
All checks were successful
Sanity Checks / sanity-test (pull_request) Successful in 6s
Smoke Test / smoke (pull_request) Successful in 33s
2026-04-16 01:58:33 +00:00
41819292e3 fix: remove 6 duplicate crisis indicator patterns from MEDIUM tier (#123) 2026-04-16 01:56:24 +00:00
2 changed files with 170 additions and 6 deletions

View File

@@ -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 = [

View 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()