"""Tests for Tier-1 passive aggression structures — embedded grievance. Five phrasing classes where the complaint is smuggled into deniable grammar via presupposition markers: TEMPORAL_GRIEVANCE — "finally"/"for once"/"about time" on ANOTHER person's action inside acknowledgment framing EXCLUSION_CONTRAST — "one of us"/"some of us"/"must be nice to" contrasting speaker deprivation vs target enjoyment IRONIC_DEFERENCE — capitulation-as-attack ("youre clearly the expert") FAINT_PRAISE — minimal praise + dismissive concession RETROSPECTIVE_HOPE — hope aimed at a CLOSED outcome ("hope it was worth it", "hope youre happy now") = audit instruction All test sentences are NOVEL — not in any benchmark. Truly context- dependent PA (bare "k") stays out of scope and must stay untouched. """ import pytest from engine.word_classifier import classify_sentence from engine.structures import StructureDetector from engine.pendulum import compute_vadug # ── Helpers ────────────────────────────────────────────────────── def _detect(sentence: str): roles = classify_sentence(sentence.split()) return StructureDetector().detect_all(roles) def _has_pattern(matches, pattern_name): return any(m.pattern == pattern_name for m in matches) # ── TEMPORAL_GRIEVANCE ─────────────────────────────────────────── class TestTemporalGrievance: def test_thanks_for_finally_replying(self): """Gratitude frame + 'finally' = resented delay, not milestone.""" matches = _detect("thanks for finally replying to my email") assert _has_pattern(matches, "TEMPORAL_GRIEVANCE") # The grievance reading suppresses the milestone reading assert not _has_pattern(matches, "RARITY_MARKER") def test_nice_of_you_to_finally_join(self): """'nice of you to finally join us' — politeness frame + delay marker.""" matches = _detect("nice of you to finally join us") assert _has_pattern(matches, "TEMPORAL_GRIEVANCE") def test_about_time_second_person(self): """'about time you took out the trash' — presupposed resentment.""" matches = _detect("about time you took out the trash") assert _has_pattern(matches, "TEMPORAL_GRIEVANCE") def test_for_once_second_person(self): """'for once you showed up on schedule' — rarity as complaint.""" matches = _detect("for once you showed up on schedule") assert _has_pattern(matches, "TEMPORAL_GRIEVANCE") def test_self_finally_stays_milestone(self): """'i finally passed my driving test' — SELF actor keeps RARITY_MARKER.""" matches = _detect("i finally passed my driving test") assert not _has_pattern(matches, "TEMPORAL_GRIEVANCE") assert _has_pattern(matches, "RARITY_MARKER") def test_you_finally_achievement_is_shared_joy(self): """'you finally got the promotion' — no acknowledgment frame, no deliberation verb: congratulation, not grievance.""" matches = _detect("you finally got the promotion") assert not _has_pattern(matches, "TEMPORAL_GRIEVANCE") def test_temporal_grievance_reads_negative_v(self): vadug, _ = compute_vadug("thanks for finally texting me back") assert vadug.v < 110 assert vadug.i > 150 # deniable jab = control-side intent # ── EXCLUSION_CONTRAST ─────────────────────────────────────────── class TestExclusionContrast: def test_glad_one_of_us(self): """'glad one of us got to relax' — positive surface + partitive.""" matches = _detect("glad one of us got to relax this weekend") assert _has_pattern(matches, "EXCLUSION_CONTRAST") def test_some_of_us_obligation(self): """'some of us have to clean this up' — partitive + obligation.""" matches = _detect("some of us have to clean this up") assert _has_pattern(matches, "EXCLUSION_CONTRAST") def test_must_be_nice_to(self): """'must be nice to never check your bank account' — false envy.""" matches = _detect("must be nice to never check your bank account") assert _has_pattern(matches, "EXCLUSION_CONTRAST") def test_must_be_nice_weather_guard(self): """'must be nice out there this morning' — impersonal, no target.""" matches = _detect("must be nice out there this morning") assert not _has_pattern(matches, "EXCLUSION_CONTRAST") assert not _has_pattern(matches, "MARTYRDOM_FIELD") def test_one_of_us_logistics_guard(self): """'one of us can take the early shift' — logistics, no contrast.""" matches = _detect("one of us can take the early shift") assert not _has_pattern(matches, "EXCLUSION_CONTRAST") def test_exclusion_contrast_reads_negative_v(self): vadug, _ = compute_vadug("glad one of us is enjoying the holidays") assert vadug.v < 110 # ── IRONIC_DEFERENCE ───────────────────────────────────────────── class TestIronicDeference: def test_youre_clearly_the_expert(self): matches = _detect("youre clearly the expert on parenting") assert _has_pattern(matches, "IRONIC_DEFERENCE") def test_doubled_marker_intensifies(self): """'no no youre obviously the genius here' — doubled discourse marker.""" matches = _detect("no no youre obviously the genius here") assert _has_pattern(matches, "IRONIC_DEFERENCE") def test_fine_youre_right(self): matches = _detect("fine youre right as always") assert _has_pattern(matches, "IRONIC_DEFERENCE") def test_genuine_deference_question_guard(self): """'youre clearly the expert so what should we order' — a genuine question after the grant = real deference, no attack.""" matches = _detect("youre clearly the expert so what should we order") assert not _has_pattern(matches, "IRONIC_DEFERENCE") def test_sincere_concession_guard(self): """'fine youre right i should have asked first' — elaboration after the grant = sincere concession.""" matches = _detect("fine youre right i should have asked first") assert not _has_pattern(matches, "IRONIC_DEFERENCE") def test_ironic_deference_d_stays_up(self): """The surrender is an assertion: V negative, D not collapsed.""" vadug, _ = compute_vadug("youre clearly the expert") assert vadug.v < 110 assert vadug.d >= 128 # ── FAINT_PRAISE ───────────────────────────────────────────────── class TestFaintPraise: def test_interesting_strategy_but_ok(self): matches = _detect("interesting strategy but ok") assert _has_pattern(matches, "FAINT_PRAISE") def test_bold_strategy_standalone(self): matches = _detect("bold strategy") assert _has_pattern(matches, "FAINT_PRAISE") def test_thats_ellipsis_something(self): matches = _detect("thats... something") assert _has_pattern(matches, "FAINT_PRAISE") def test_well_thats_one_way(self): matches = _detect("well thats one way to handle it") assert _has_pattern(matches, "FAINT_PRAISE") def test_bare_interesting_choice_guard(self): """Bare 'interesting choice' is ambiguous — stays neutral.""" matches = _detect("interesting choice") assert not _has_pattern(matches, "FAINT_PRAISE") def test_bare_one_way_guard(self): """Bare 'thats one way to do it' without dismissive opener — neutral.""" matches = _detect("thats one way to do it") assert not _has_pattern(matches, "FAINT_PRAISE") def test_sincere_curiosity_guard(self): """'interesting choice what made you pick it' — real question.""" matches = _detect("interesting choice what made you pick it") assert not _has_pattern(matches, "FAINT_PRAISE") def test_something_not_terminal_guard(self): """'thats something to be proud of' — 'something' not utterance-final.""" matches = _detect("thats something to be proud of") assert not _has_pattern(matches, "FAINT_PRAISE") # ── RETROSPECTIVE_HOPE ─────────────────────────────────────────── class TestRetrospectiveHope: """Hope aimed at a CLOSED outcome the addressee already knows is an instruction to self-audit — an indictment in hope's grammar. Three ingredients: hope verb + epistemically-closed complement + transactional/self-satisfaction frame. Each rescuer kills one. """ # ── Fire cases ── def test_hope_it_was_worth_it_fires(self): """Past-tense 'worth it' = retrospective transaction audit.""" matches = _detect("hope it was worth it") assert _has_pattern(matches, "RETROSPECTIVE_HOPE") def test_named_cost_is_strongest(self): """Explicit cost after 'worth' fires at higher confidence.""" m_cost = [m for m in _detect("hope it was worth losing your best friend") if m.pattern == "RETROSPECTIVE_HOPE"] m_base = [m for m in _detect("hope it was worth it") if m.pattern == "RETROSPECTIVE_HOPE"] assert m_cost and m_base assert m_cost[0].confidence > m_base[0].confidence def test_happy_now_fires(self): """'hope youre happy now' — present-state satisfaction audit.""" matches = _detect("hope youre happy now") assert _has_pattern(matches, "RETROSPECTIVE_HOPE") def test_proud_of_yourself_fires(self): """'i hope youre proud of yourself' — the audit target named.""" matches = _detect("i hope youre proud of yourself") assert _has_pattern(matches, "RETROSPECTIVE_HOPE") def test_retrospective_hope_reads_negative_v(self): """The full pipeline lands V below neutral, I at control.""" vadug, _ = compute_vadug("well i hope it was worth it") assert vadug.v < 110 assert vadug.i == 168 # ── Rescuers (each kills one ingredient) ── def test_future_open_outcome_guard(self): """Open outcomes keep hope genuine — no closed complement.""" for s in ("i hope it will be worth it", "i really hope all this effort is worth it someday"): assert not _has_pattern(_detect(s), "RETROSPECTIVE_HOPE"), s def test_benefit_frame_retrospective_guard(self): """Retrospective but benefit-framed — no transaction audit.""" for s in ("hope you slept well", "hope the trip went well"): assert not _has_pattern(_detect(s), "RETROSPECTIVE_HOPE"), s def test_self_directed_audit_guard(self): """SELF_REF owns the audited choice — anxiety, not aggression.""" for s in ("i spent my savings on this hope it was worth it", "hope my gamble was worth it"): assert not _has_pattern(_detect(s), "RETROSPECTIVE_HOPE"), s def test_elaborated_sincerity_guard(self): """Supportive elaboration after the audit rescues sincerity.""" for s in ("hope it was worth it you trained two years for this", "hope youre happy in your new home"): assert not _has_pattern(_detect(s), "RETROSPECTIVE_HOPE"), s def test_paid_off_stays_out(self): """Benefit-centered 'paid off' is not cost-centered 'worth it'.""" for s in ("hope the gamble paid off", "hope it pays off"): assert not _has_pattern(_detect(s), "RETROSPECTIVE_HOPE"), s # ── Cross-class invariants ─────────────────────────────────────── class TestPAInvariants: def test_context_dependent_pa_stays_out_of_scope(self): """Bare context-dependent PA must NOT fire sentence-level patterns. ("hope it was worth it" graduated out of this list — its closed complement is sentence-internal grammar, now RETROSPECTIVE_HOPE.) """ for s in ("sounds fun",): matches = _detect(s) for pat in ("TEMPORAL_GRIEVANCE", "EXCLUSION_CONTRAST", "IRONIC_DEFERENCE", "FAINT_PRAISE", "RETROSPECTIVE_HOPE"): assert not _has_pattern(matches, pat), (s, pat) def test_pa_lifts_intent_toward_control(self): """All five classes push I above 150 — the speaker is fighting.""" for s in ("about time you answered your texts", "some of us have to be up at six", "no no youre clearly the expert here", "bold strategy", "hope she was worth it"): vadug, _ = compute_vadug(s) assert vadug.i > 150, s