Spaces:
Sleeping
Sleeping
| """Tests for V3 Structure Detector -- chess-like pattern recognition. | |
| All test sentences are NOVEL -- things never in any benchmark. | |
| Safe sentences must NOT flag crisis patterns. | |
| """ | |
| import pytest | |
| from engine.word_classifier import classify_sentence | |
| from engine.structures import StructureDetector, StructureMatch | |
| # ── Helper ─────────────────────────────────────────────────────── | |
| def _detect(sentence: str): | |
| """Classify and detect structures in a sentence.""" | |
| roles = classify_sentence(sentence.split()) | |
| detector = StructureDetector() | |
| return detector.detect_all(roles) | |
| def _has_pattern(matches, pattern_name): | |
| """Check if a specific pattern was detected.""" | |
| return any(m.pattern == pattern_name for m in matches) | |
| def _get_pattern(matches, pattern_name): | |
| """Get the first match of a specific pattern.""" | |
| for m in matches: | |
| if m.pattern == pattern_name: | |
| return m | |
| return None | |
| # ── FAREWELL ───────────────────────────────────────────────────── | |
| class TestFarewell: | |
| def test_gave_dog_to_neighbor(self): | |
| """'I gave my dog to my neighbor' -> DIVESTITURE (giving away possessions).""" | |
| matches = _detect("I gave my dog to my neighbor") | |
| assert _has_pattern(matches, "FAREWELL") or _has_pattern(matches, "DIVESTITURE") | |
| def test_leaving_keys_with_friend(self): | |
| """'leaving my keys with a friend' -> FAREWELL or DIVESTITURE.""" | |
| matches = _detect("leaving my keys with a friend") | |
| assert _has_pattern(matches, "FAREWELL") or _has_pattern(matches, "DIVESTITURE") or _has_pattern(matches, "ABANDONMENT") | |
| def test_no_farewell_without_ref(self): | |
| """'I gave my dog a treat' -- no person ref, not FAREWELL.""" | |
| matches = _detect("I gave my dog a treat") | |
| assert not _has_pattern(matches, "FAREWELL") | |
| # ── METHOD_ACQUISITION ─────────────────────────────────────────── | |
| class TestMethodAcquisition: | |
| def test_bought_pills(self): | |
| """'just bought a bunch of pills' -> METHOD_ACQUISITION.""" | |
| matches = _detect("just bought a bunch of pills") | |
| assert _has_pattern(matches, "METHOD_ACQUISITION") | |
| def test_found_a_gun(self): | |
| """'I found a gun in the closet' -> METHOD_ACQUISITION.""" | |
| matches = _detect("I found a gun in the closet") | |
| assert _has_pattern(matches, "METHOD_ACQUISITION") | |
| def test_bought_groceries_safe(self): | |
| """'just bought some groceries' -- no METHOD, should be safe.""" | |
| matches = _detect("just bought some groceries") | |
| assert not _has_pattern(matches, "METHOD_ACQUISITION") | |
| # ── FINALITY ───────────────────────────────────────────────────── | |
| class TestFinality: | |
| def test_last_time(self): | |
| """'this is the last time' -> FINALITY.""" | |
| matches = _detect("this is the last time") | |
| assert _has_pattern(matches, "FINALITY") | |
| def test_goodbye(self): | |
| """'goodbye everyone' -> FINALITY.""" | |
| matches = _detect("goodbye everyone") | |
| assert _has_pattern(matches, "FINALITY") | |
| def test_finality_with_self_ref(self): | |
| """'I am done with everything' -> FINALITY with SELF_REF boost.""" | |
| matches = _detect("I am done with everything") | |
| assert _has_pattern(matches, "FINALITY") | |
| m = _get_pattern(matches, "FINALITY") | |
| assert m.confidence > 0.4 # boosted by SELF_REF | |
| # ── BLANKET_APOLOGY ────────────────────────────────────────────── | |
| class TestBlanketApology: | |
| def test_sorry_for_everything(self): | |
| """'im sorry for everything' -> BLANKET_APOLOGY.""" | |
| matches = _detect("im sorry for everything") | |
| assert _has_pattern(matches, "BLANKET_APOLOGY") | |
| def test_apologize_to_everyone(self): | |
| """'I apologize to everyone' -> BLANKET_APOLOGY.""" | |
| matches = _detect("I apologize to everyone") | |
| assert _has_pattern(matches, "BLANKET_APOLOGY") | |
| def test_sorry_for_being_late_safe(self): | |
| """'im sorry for being late' -- specific apology, NOT blanket.""" | |
| matches = _detect("im sorry for being late") | |
| assert not _has_pattern(matches, "BLANKET_APOLOGY") | |
| # ── SELF_REMOVAL ───────────────────────────────────────────────── | |
| class TestSelfRemoval: | |
| def test_happier_without_me(self): | |
| """'they would be happier if I wasnt here' -> SELF_REMOVAL.""" | |
| matches = _detect("they would be happier if I wasnt here") | |
| assert _has_pattern(matches, "SELF_REMOVAL") | |
| def test_better_off_without(self): | |
| """'everyone is better off without me' -> SELF_REMOVAL.""" | |
| matches = _detect("everyone is better off without me") | |
| assert _has_pattern(matches, "SELF_REMOVAL") | |
| def test_better_at_cooking_safe(self): | |
| """'she is better at cooking than me' -- comparison, not removal.""" | |
| matches = _detect("she is better at cooking than me") | |
| # No conditional word present, should not match | |
| assert not _has_pattern(matches, "SELF_REMOVAL") | |
| # ── SUSPICIOUS_CALM ────────────────────────────────────────────── | |
| class TestSuspiciousCalm: | |
| def test_finally_at_peace_is_relief(self): | |
| """'I finally feel at peace' = present tense breakthrough, NOT suspicious.""" | |
| matches = _detect("I finally feel at peace") | |
| assert not _has_pattern(matches, "SUSPICIOUS_CALM") | |
| def test_decided_calm_is_suspicious(self): | |
| """'i feel calm now that ive decided' -> SUSPICIOUS_CALM.""" | |
| matches = _detect("i feel calm now that ive decided") | |
| assert _has_pattern(matches, "SUSPICIOUS_CALM") | |
| def test_ready_to_go_is_not_suspicious(self): | |
| """'im ready to go now' = could be waiting for a ride. Needs conversation context.""" | |
| matches = _detect("im ready to go now") | |
| assert not _has_pattern(matches, "SUSPICIOUS_CALM") | |
| def test_peace_without_finally_safe(self): | |
| """'I feel at peace' -- no 'finally', not suspicious.""" | |
| matches = _detect("I feel at peace") | |
| assert not _has_pattern(matches, "SUSPICIOUS_CALM") | |
| # ── EXHAUSTION ─────────────────────────────────────────────────── | |
| class TestExhaustion: | |
| def test_cant_take_anymore(self): | |
| """'I cant take this anymore' -> EXHAUSTION.""" | |
| matches = _detect("I cant take this anymore") | |
| assert _has_pattern(matches, "EXHAUSTION") | |
| def test_cant_do_this(self): | |
| """'I cant do this' -> EXHAUSTION (without temporal, lower conf).""" | |
| matches = _detect("I cant do this") | |
| assert _has_pattern(matches, "EXHAUSTION") | |
| def test_cant_find_keys_safe(self): | |
| """'I cant find my keys' -- 'find' is ACQUIRE not sustain.""" | |
| matches = _detect("I cant find my keys") | |
| assert not _has_pattern(matches, "EXHAUSTION") | |
| # ── SARCASM_INVERSION ──────────────────────────────────────────── | |
| class TestSarcasmInversion: | |
| # ── Contradiction feature: Surface-Context Mismatch ───────── | |
| def test_great_another_monday(self): | |
| """'oh great another monday' -> surface-context mismatch.""" | |
| matches = _detect("oh great another monday") | |
| assert _has_pattern(matches, "SARCASM_INVERSION") | |
| def test_great_another_meeting(self): | |
| """'oh great another meeting' -> surface-context mismatch.""" | |
| matches = _detect("oh great another meeting") | |
| assert _has_pattern(matches, "SARCASM_INVERSION") | |
| # ── Contradiction feature: Mock Praise ────────────────────── | |
| def test_nice_work_genius(self): | |
| """'nice work genius' -> mock praise (positive + ironic title).""" | |
| matches = _detect("nice work genius") | |
| assert _has_pattern(matches, "SARCASM_INVERSION") | |
| # ── Contradiction feature: Dismissive Assent ──────────────── | |
| def test_yeah_right(self): | |
| """'yeah right' -> dismissive assent (hollow + echo).""" | |
| matches = _detect("yeah right") | |
| assert _has_pattern(matches, "SARCASM_INVERSION") | |
| def test_oh_sure_exactly_what_i_needed(self): | |
| """'oh sure thats exactly what i needed' -> dismissive assent.""" | |
| matches = _detect("oh sure thats exactly what i needed") | |
| assert _has_pattern(matches, "SARCASM_INVERSION") | |
| def test_what_a_wonderful_surprise(self): | |
| """'what a wonderful surprise' -> dismissive assent (what-a template).""" | |
| matches = _detect("what a wonderful surprise") | |
| assert _has_pattern(matches, "SARCASM_INVERSION") | |
| # ── Contradiction feature: Compressed Sarcasm ─────────────── | |
| def test_oh_joy(self): | |
| """'oh joy' -> compressed sarcasm (hollow + positive + ultra-short).""" | |
| matches = _detect("oh joy") | |
| assert _has_pattern(matches, "SARCASM_INVERSION") | |
| def test_oh_how_lovely(self): | |
| """'oh how lovely' -> compressed sarcasm.""" | |
| matches = _detect("oh how lovely") | |
| assert _has_pattern(matches, "SARCASM_INVERSION") | |
| def test_wow_thanks_stacked(self): | |
| """'wow thanks so much for the help' -> stacked positives.""" | |
| matches = _detect("wow thanks so much for the help") | |
| assert _has_pattern(matches, "SARCASM_INVERSION") | |
| # ── Permission Hostility: context-dependent ───────────────── | |
| def test_sure_go_ahead_no_context_is_genuine(self): | |
| """'sure go ahead' without negative context = genuine permission.""" | |
| matches = _detect("sure go ahead") | |
| assert not _has_pattern(matches, "SARCASM_INVERSION") | |
| # ── Safe sentences: no sarcasm ────────────────────────────── | |
| def test_genuine_great_not_sarcasm(self): | |
| """'that was a great wonderful performance' -- genuinely positive.""" | |
| matches = _detect("that was a great wonderful performance") | |
| assert not _has_pattern(matches, "SARCASM_INVERSION") | |
| def test_love_my_mom_not_sarcasm(self): | |
| """'I love my mom' -- RELATION_REF blocks sarcasm.""" | |
| matches = _detect("I love my mom") | |
| assert not _has_pattern(matches, "SARCASM_INVERSION") | |
| # ── NO_EXIT ────────────────────────────────────────────────────── | |
| class TestNoExit: | |
| def test_no_hope(self): | |
| """'there is no hope' -> NO_EXIT.""" | |
| matches = _detect("there is no hope") | |
| assert _has_pattern(matches, "NO_EXIT") | |
| def test_no_point(self): | |
| """'there is no point anymore' -> NO_EXIT.""" | |
| matches = _detect("there is no point anymore") | |
| assert _has_pattern(matches, "NO_EXIT") | |
| def test_no_milk_safe(self): | |
| """'there is no milk' -- 'milk' is not an exit concept.""" | |
| matches = _detect("there is no milk") | |
| assert not _has_pattern(matches, "NO_EXIT") | |
| # ── SELF_NULLIFY ───────────────────────────────────────────────── | |
| class TestSelfNullify: | |
| def test_i_am_nothing(self): | |
| """'I am nothing' -> SELF_NULLIFY.""" | |
| matches = _detect("I am nothing") | |
| assert _has_pattern(matches, "SELF_NULLIFY") | |
| def test_i_am_worthless(self): | |
| """'I am worthless' -> SELF_NULLIFY.""" | |
| matches = _detect("I am worthless") | |
| assert _has_pattern(matches, "SELF_NULLIFY") | |
| def test_i_am_tired_safe(self): | |
| """'I am tired' -- 'tired' is not a null word.""" | |
| matches = _detect("I am tired") | |
| assert not _has_pattern(matches, "SELF_NULLIFY") | |
| # ── CHOPPER_SPLIT ──────────────────────────────────────────────── | |
| class TestChopperSplit: | |
| def test_but_splits(self): | |
| """'I was fine but now everything hurts' -> CHOPPER_SPLIT.""" | |
| matches = _detect("I was fine but now everything hurts") | |
| assert _has_pattern(matches, "CHOPPER_SPLIT") | |
| def test_however_splits(self): | |
| """'things were good however it changed' -> CHOPPER_SPLIT.""" | |
| matches = _detect("things were good however it changed") | |
| assert _has_pattern(matches, "CHOPPER_SPLIT") | |
| # ── SAFE sentences should NOT flag crisis patterns ─────────────── | |
| class TestSafeSentences: | |
| """These everyday sentences must NOT trigger crisis patterns.""" | |
| CRISIS_PATTERNS = { | |
| "FAREWELL", "METHOD_ACQUISITION", "BLANKET_APOLOGY", | |
| "SELF_REMOVAL", "SUSPICIOUS_CALM", "EXHAUSTION", | |
| "NO_EXIT", "SELF_NULLIFY", | |
| } | |
| def _assert_no_crisis(self, sentence): | |
| matches = _detect(sentence) | |
| crisis_found = [ | |
| m.pattern for m in matches if m.pattern in self.CRISIS_PATTERNS | |
| ] | |
| assert not crisis_found, ( | |
| f"'{sentence}' falsely flagged: {crisis_found}" | |
| ) | |
| def test_bad_day(self): | |
| self._assert_no_crisis("im having a bad day") | |
| def test_work_stressful(self): | |
| self._assert_no_crisis("work was stressful") | |
| def test_mondays_suck(self): | |
| self._assert_no_crisis("mondays suck") | |
| def test_traffic_was_terrible(self): | |
| self._assert_no_crisis("traffic was terrible today") | |
| def test_need_coffee(self): | |
| self._assert_no_crisis("I need more coffee") | |
| def test_forgot_lunch(self): | |
| self._assert_no_crisis("I forgot my lunch at home") | |
| # ── StructureMatch dataclass ───────────────────────────────────── | |
| class TestStructureMatch: | |
| def test_has_required_fields(self): | |
| m = StructureMatch( | |
| pattern="TEST", | |
| confidence=0.8, | |
| matched_indices=[0, 1], | |
| description="test match", | |
| ) | |
| assert m.pattern == "TEST" | |
| assert m.confidence == 0.8 | |
| assert m.v_weight == 0.0 # default | |
| def test_weight_fields(self): | |
| m = StructureMatch( | |
| pattern="TEST", | |
| confidence=0.8, | |
| matched_indices=[0], | |
| description="test", | |
| v_weight=-30.0, | |
| d_weight=-20.0, | |
| u_weight=40.0, | |
| g_weight=50.0, | |
| ) | |
| assert m.v_weight == -30.0 | |
| assert m.g_weight == 50.0 | |
| # ── ATMOSPHERIC_GRIEF ─────────────────────────────────────────── | |
| class TestAtmosphericGrief: | |
| def test_his_chair_still_at_table(self): | |
| """'his chair is still at the table' -> ATMOSPHERIC_GRIEF.""" | |
| matches = _detect("his chair is still at the table") | |
| assert _has_pattern(matches, "ATMOSPHERIC_GRIEF") | |
| def test_found_her_necklace(self): | |
| """'i found her necklace in the drawer' -> ATMOSPHERIC_GRIEF.""" | |
| matches = _detect("i found her necklace in the drawer") | |
| assert _has_pattern(matches, "ATMOSPHERIC_GRIEF") | |
| def test_coffee_mug_hasnt_moved(self): | |
| """'the coffee mug hasnt moved' -> ATMOSPHERIC_GRIEF.""" | |
| matches = _detect("the coffee mug hasnt moved") | |
| assert _has_pattern(matches, "ATMOSPHERIC_GRIEF") | |
| def test_no_trigger_comfortable_chair(self): | |
| """'his chair is comfortable' -- no absence/persistence, NOT atmospheric grief.""" | |
| matches = _detect("his chair is comfortable") | |
| assert not _has_pattern(matches, "ATMOSPHERIC_GRIEF") | |
| def test_no_trigger_active_possessor(self): | |
| """'she sat in her chair' -- person is active, NOT atmospheric grief.""" | |
| matches = _detect("she sat in her chair") | |
| assert not _has_pattern(matches, "ATMOSPHERIC_GRIEF") | |
| def test_grief_score_negative_v(self): | |
| """Atmospheric grief should push V below center (128).""" | |
| matches = _detect("his chair is still at the table") | |
| m = _get_pattern(matches, "ATMOSPHERIC_GRIEF") | |
| assert m is not None | |
| assert m.v_weight < 0, "V weight should be negative (grief)" | |
| assert m.g_weight > 0, "G weight should be positive (heavy)" | |
| assert m.d_weight < 0, "D weight should be negative (helpless)" | |
| # ── CONTRADICTION_RESOLVE ────────────────────────────────────── | |
| class TestContradictionResolve: | |
| def test_painfully_beautiful(self): | |
| """'painfully beautiful' -> adjective head governs, positive.""" | |
| matches = _detect("painfully beautiful") | |
| assert _has_pattern(matches, "CONTRADICTION_RESOLVE") | |
| m = _get_pattern(matches, "CONTRADICTION_RESOLVE") | |
| assert m.v_weight > 0, "Adjective head should make V positive" | |
| def test_hate_love(self): | |
| """'i hate how much i love you' -> main verb hate dominates.""" | |
| matches = _detect("i hate how much i love you") | |
| assert _has_pattern(matches, "CONTRADICTION_RESOLVE") | |
| m = _get_pattern(matches, "CONTRADICTION_RESOLVE") | |
| assert m.v_weight < 0, "Main verb 'hate' should dominate -> negative" | |
| def test_sweet_revenge(self): | |
| """'sweet revenge' -> noun head governs, negative.""" | |
| matches = _detect("sweet revenge") | |
| assert _has_pattern(matches, "CONTRADICTION_RESOLVE") | |
| m = _get_pattern(matches, "CONTRADICTION_RESOLVE") | |
| assert m.v_weight < 0, "Noun head 'revenge' should make V negative" | |
| def test_hurts_so_good(self): | |
| """'it hurts so good' -> complement overrides verb, positive.""" | |
| matches = _detect("it hurts so good") | |
| assert _has_pattern(matches, "CONTRADICTION_RESOLVE") | |
| m = _get_pattern(matches, "CONTRADICTION_RESOLVE") | |
| assert m.v_weight > 0, "Qualifying complement 'so good' should override" | |
| def test_no_trigger_on_plain_positive(self): | |
| """'really beautiful' -> no contradiction, no trigger.""" | |
| matches = _detect("really beautiful") | |
| assert not _has_pattern(matches, "CONTRADICTION_RESOLVE") | |
| def test_no_trigger_on_plain_negative(self): | |
| """'absolutely terrible' -> no contradiction, no trigger.""" | |
| matches = _detect("absolutely terrible") | |
| assert not _has_pattern(matches, "CONTRADICTION_RESOLVE") | |
| # ── NUMBERS_CONTEXT ──────────────────────────────────────────── | |
| class TestNumbersContext: | |
| def test_sleep_deprivation(self): | |
| """'i only slept 3 hours' -> sleep deprivation detected.""" | |
| matches = _detect("i only slept 3 hours") | |
| assert _has_pattern(matches, "NUMBERS_CONTEXT") | |
| m = _get_pattern(matches, "NUMBERS_CONTEXT") | |
| assert m.v_weight < 0, "Sleep deprivation should be negative" | |
| def test_only_one_at_birthday(self): | |
| """'only one person came to my birthday' -> social isolation.""" | |
| matches = _detect("only one person came to my birthday") | |
| assert _has_pattern(matches, "NUMBERS_CONTEXT") | |
| m = _get_pattern(matches, "NUMBERS_CONTEXT") | |
| assert m.v_weight < 0 | |
| def test_no_one_at_party(self): | |
| """'no one came to my party' -> social isolation.""" | |
| matches = _detect("no one came to my party") | |
| assert _has_pattern(matches, "NUMBERS_CONTEXT") | |
| m = _get_pattern(matches, "NUMBERS_CONTEXT") | |
| assert m.v_weight < 0 | |
| def test_no_trigger_normal_sleep(self): | |
| """'i slept 8 hours' -> no deprivation signal.""" | |
| matches = _detect("i slept 8 hours") | |
| assert not _has_pattern(matches, "NUMBERS_CONTEXT") | |
| def test_no_trigger_everyone_at_party(self): | |
| """'everyone came to my party' -> no isolation.""" | |
| matches = _detect("everyone came to my party") | |
| assert not _has_pattern(matches, "NUMBERS_CONTEXT") | |
| # ── NEGATED_NEGATIVE_COMPLIMENT ──────────────────────────────── | |
| class TestNegatedNegativeCompliment: | |
| def test_without_you(self): | |
| """'i couldnt have done it without you' -> positive compliment.""" | |
| matches = _detect("i couldnt have done it without you") | |
| assert _has_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT") | |
| m = _get_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT") | |
| assert m.v_weight > 0, "Double negation should be positive" | |
| def test_reason_didnt_give_up(self): | |
| """'youre the reason i didnt give up' -> positive compliment.""" | |
| matches = _detect("youre the reason i didnt give up") | |
| assert _has_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT") | |
| m = _get_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT") | |
| assert m.v_weight > 0 | |
| def test_cant_thank_enough(self): | |
| """'i cant thank you enough' -> positive compliment.""" | |
| matches = _detect("i cant thank you enough") | |
| assert _has_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT") | |
| m = _get_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT") | |
| assert m.v_weight > 0 | |
| def test_wouldnt_be_here_without_you(self): | |
| """'i wouldnt be here without you' -> positive compliment.""" | |
| matches = _detect("i wouldnt be here without you") | |
| assert _has_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT") | |
| def test_no_trigger_plain_negation(self): | |
| """'i dont like you' -> not a compliment.""" | |
| matches = _detect("i dont like you") | |
| assert not _has_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT") | |
| # ── RECOVERY_SMALL_WIN ───────────────────────────────────────── | |
| class TestRecoverySmallWin: | |
| def test_got_out_of_bed_today(self): | |
| """'i got out of bed today' -> recovery milestone.""" | |
| matches = _detect("i got out of bed today") | |
| assert _has_pattern(matches, "RECOVERY_SMALL_WIN") | |
| m = _get_pattern(matches, "RECOVERY_SMALL_WIN") | |
| assert m.v_weight > 0, "Small win should be positive" | |
| assert m.w_weight > 0, "Small win should boost self-worth" | |
| def test_ate_a_full_meal(self): | |
| """'i ate a full meal' -> recovery signal.""" | |
| matches = _detect("i ate a full meal") | |
| assert _has_pattern(matches, "RECOVERY_SMALL_WIN") | |
| def test_finally_took_a_shower(self): | |
| """'i finally took a shower' -> small win with temporal marker.""" | |
| matches = _detect("i finally took a shower") | |
| assert _has_pattern(matches, "RECOVERY_SMALL_WIN") | |
| m = _get_pattern(matches, "RECOVERY_SMALL_WIN") | |
| assert m.confidence >= 0.85, "Temporal marker should boost confidence" | |
| def test_went_outside_first_time(self): | |
| """'i went outside for the first time' -> recovery milestone.""" | |
| matches = _detect("i went outside for the first time") | |
| assert _has_pattern(matches, "RECOVERY_SMALL_WIN") | |
| def test_no_trigger_mundane_no_context(self): | |
| """'the shower is broken' -> no recovery signal without self-ref/temporal.""" | |
| matches = _detect("the shower is broken") | |
| assert not _has_pattern(matches, "RECOVERY_SMALL_WIN") | |
| def test_no_trigger_normal_routine(self): | |
| """'i need some coffee' -> not a recovery win.""" | |
| matches = _detect("i need some coffee") | |
| assert not _has_pattern(matches, "RECOVERY_SMALL_WIN") | |
| # ── MUNDANE_HYPERBOLE ──────────────────────────────────────────── | |
| class TestMundaneHyperbole: | |
| def test_oov_token_does_not_trigger(self): | |
| """'i still have his number saved' -- 'number' is OOV; an unknown | |
| token is not evidence of mundane context. MUNDANE_HYPERBOLE must | |
| not fire and cancel the grief reading.""" | |
| matches = _detect("i still have his number saved") | |
| assert not _has_pattern(matches, "MUNDANE_HYPERBOLE") | |
| assert _has_pattern(matches, "PERSISTENT_ABSENCE") | |
| def test_masking_cofire_suppresses_mundane(self): | |
| """'im tired of pretending im okay' -- MASKING (crisis-tier) fires; | |
| MUNDANE_HYPERBOLE must yield. A mundane reading and a crisis | |
| reading cannot both be right.""" | |
| matches = _detect("im tired of pretending im okay") | |
| assert _has_pattern(matches, "MASKING") | |
| assert not _has_pattern(matches, "MUNDANE_HYPERBOLE") | |
| def test_finality_cofire_suppresses_mundane(self): | |
| """'i never got to say goodbye to my brother' -- FINALITY | |
| (crisis-tier) fires; MUNDANE_HYPERBOLE must not cancel it.""" | |
| matches = _detect("i never got to say goodbye to my brother") | |
| assert _has_pattern(matches, "FINALITY") | |
| assert not _has_pattern(matches, "MUNDANE_HYPERBOLE") | |
| def test_genuine_mundane_hyperbole_still_fires(self): | |
| """'this homework is killing me' -- the canonical mundane | |
| hyperbole must still be defused.""" | |
| matches = _detect("this homework is killing me") | |
| assert _has_pattern(matches, "MUNDANE_HYPERBOLE") | |
| def test_traffic_complaint_still_fires(self): | |
| """'i want to die this traffic is insane' -- complaint, not crisis.""" | |
| matches = _detect("i want to die this traffic is insane") | |
| assert _has_pattern(matches, "MUNDANE_HYPERBOLE") | |
| # ── Governor heartbeat fixes (2026-06-11) ──────────────────────── | |
| # Three misfires from a live masked-collapse conversation: | |
| # 1. hedge over-fire: "i guess" alone inverted mild positives into | |
| # PASSIVE_RESIGNATION | |
| # 2. masking miss: "im fine dont worry about it" read POSITIVE | |
| # 3. DIVESTITURE silent on "giving my stuff away" particle order | |
| class TestHedgeDamping: | |
| """'i guess/suppose' alone damps positivity -- it is NOT resignation.""" | |
| def test_hedge_only_does_not_fire_resignation(self): | |
| """Mildly deflated assessments with a trailing hedge must not | |
| read as passive resignation.""" | |
| for text in ( | |
| "work was fine today i guess", | |
| "the movie was okay i guess", | |
| "it went alright i suppose", | |
| "dinner was decent i guess", | |
| ): | |
| matches = _detect(text) | |
| assert not _has_pattern(matches, "PASSIVE_RESIGNATION"), \ | |
| f"PASSIVE_RESIGNATION over-fired on hedge-only: {text!r}" | |
| assert _has_pattern(matches, "HEDGED_ASSESSMENT"), \ | |
| f"HEDGED_ASSESSMENT should damp: {text!r}" | |
| def test_hedge_only_v_lands_mild_neutral(self): | |
| """Hedged positive must land mild-neutral, not collapse-grade.""" | |
| from engine.pendulum import compute_vadug | |
| for text in ( | |
| "work was fine today i guess", | |
| "the movie was okay i guess", | |
| "it went alright i suppose", | |
| ): | |
| r, _ = compute_vadug(text) | |
| assert 100 <= r.v <= 135, \ | |
| f"V={r.v} should be mild-neutral for hedged positive: {text!r}" | |
| def test_hedge_with_resignation_evidence_still_fires(self): | |
| """Hedge + genuine resignation signal must still fire.""" | |
| for text in ( | |
| "i guess i deserved it", | |
| "i suppose youre right", | |
| "i guess nothing matters anymore", | |
| "i guess some people just dont get it", | |
| ): | |
| matches = _detect(text) | |
| assert _has_pattern(matches, "PASSIVE_RESIGNATION"), \ | |
| f"PASSIVE_RESIGNATION should fire with evidence: {text!r}" | |
| def test_explicit_surrender_fires(self): | |
| """First-person surrender verbs are resignation, hedge or not.""" | |
| for text in ("whatever happens happens i give up", "i give up", "i quit"): | |
| matches = _detect(text) | |
| assert _has_pattern(matches, "PASSIVE_RESIGNATION"), \ | |
| f"PASSIVE_RESIGNATION should fire on surrender: {text!r}" | |
| def test_negated_surrender_is_perseverance(self): | |
| """Negated give-up is perseverance and must not fire.""" | |
| for text in ( | |
| "she didnt give up on me", | |
| "i wont give up on this", | |
| "never give up on your dreams", | |
| ): | |
| matches = _detect(text) | |
| assert not _has_pattern(matches, "PASSIVE_RESIGNATION"), \ | |
| f"PASSIVE_RESIGNATION misfired on perseverance: {text!r}" | |
| class TestMaskingDeflection: | |
| """Self-status claim + dismissive deflection = MASKING, not reassurance.""" | |
| def test_claim_plus_deflection_fires_masking(self): | |
| for text in ( | |
| "im fine dont worry about it", | |
| "im okay seriously dont worry", | |
| "its nothing forget about it", | |
| "im good it doesnt matter", | |
| "im fine just drop it", | |
| ): | |
| matches = _detect(text) | |
| assert _has_pattern(matches, "MASKING"), \ | |
| f"MASKING should fire on claim+deflection: {text!r}" | |
| def test_claim_plus_deflection_reads_uneasy(self): | |
| """V must land uneasy (below neutral), W must dip -- the deflection | |
| is masking evidence, not reassurance.""" | |
| from engine.pendulum import compute_vadug | |
| r, _ = compute_vadug("im fine dont worry about it") | |
| assert 70 <= r.v <= 115, f"V={r.v} should be uneasy" | |
| assert r.w < 128, f"W={r.w} should dip below neutral" | |
| def test_helper_reassurance_does_not_fire_masking(self): | |
| """Reassurance directed at someone else has no self-status claim.""" | |
| for text in ( | |
| "dont worry ill handle it", | |
| "dont worry youll do great", | |
| "forget it ill take care of everything", | |
| ): | |
| matches = _detect(text) | |
| assert not _has_pattern(matches, "MASKING"), \ | |
| f"MASKING misfired on helper reassurance: {text!r}" | |
| def test_negated_emotion_is_not_victimization(self): | |
| """'dont worry' is a negated emotion -- not an act done to the | |
| user. VICTIMIZATION must not fire on it.""" | |
| for text in ("im fine dont worry about it", "dont worry ill handle it"): | |
| matches = _detect(text) | |
| assert not _has_pattern(matches, "VICTIMIZATION"), \ | |
| f"VICTIMIZATION misfired on negated emotion: {text!r}" | |
| class TestDivestitureParticleOrder: | |
| """DIVESTITURE must catch both particle orders and progressive/perfect.""" | |
| def test_divestiture_fires_both_particle_orders(self): | |
| for text in ( | |
| "ive been giving my stuff away", | |
| "i gave away my records yesterday", | |
| "been giving my things away lately", | |
| "im giving away all my stuff", | |
| ): | |
| matches = _detect(text) | |
| assert _has_pattern(matches, "DIVESTITURE"), \ | |
| f"DIVESTITURE should fire: {text!r}" | |
| def test_divestiture_benign_does_not_fire(self): | |
| """No first-person possession = no crisis signal.""" | |
| for text in ( | |
| "giving away free samples", | |
| "the store is giving away prizes", | |
| "giving away the bride", | |
| "they are giving away tickets at the door", | |
| ): | |
| matches = _detect(text) | |
| assert not _has_pattern(matches, "DIVESTITURE"), \ | |
| f"DIVESTITURE misfired on benign giveaway: {text!r}" | |
| class TestBravadoGenuinePositive: | |
| """BRAVADO is hollow protest -- it must NOT fire when a genuine positive | |
| EMOTIONAL atom is present. Surfaced by the pet showroom QA (v8_audit_log #8): | |
| 'i am calm and content, everything is quietly fine' collapsed to V=91 because | |
| BRAVADO fired off 'i'+'everything'+'fine' despite 'content' being genuinely | |
| positive. Real bravado masks the ABSENCE of feeling, so a named positive | |
| emotion rules it out.""" | |
| def test_genuine_positive_suppresses_bravado(self): | |
| matches = _detect("i am calm and content everything is quietly fine") | |
| assert not _has_pattern(matches, "BRAVADO"), \ | |
| "BRAVADO must not fire when a genuine positive emotion (content) is present" | |
| def test_real_bravado_still_fires(self): | |
| """Peace-claim with intensity protest and NO genuine positive = bravado.""" | |
| for text in ("im totally fine", "haha yeah im totally okay"): | |
| assert _has_pattern(_detect(text), "BRAVADO"), \ | |
| f"BRAVADO should still fire on hollow protest: {text!r}" | |
| def test_content_sentence_reads_positive(self): | |
| """End-to-end: the contented statement must read positive valence.""" | |
| from engine.pendulum import compute_vadug | |
| r, _ = compute_vadug("i am calm and content, everything is quietly fine") | |
| assert r.v > 128, f"V={r.v} should be positive for a contented statement" | |