import copy from game.state import GameState from game.validator import (validate_crisis, validate_event, validate_presentation) GOOD_CRISIS = { "npc_reaction": "Noted, boss.", "consequence": "It resolved somehow.", "revenue_delta": 50_000, "animation": "npc_happy", "boss_title": "VP of Tests", "log_entry": "Brad did a thing. +$50K.", "morale_delta": 3, "npc_id": "brad", "relationship_delta": 5, "pocket_money_delta": 0, "special_next_event": None, } def make_state(**kw): s = GameState(session_id="t") for k, v in kw.items(): setattr(s, k, v) return s def test_good_crisis_passes(): assert validate_crisis(copy.deepcopy(GOOD_CRISIS), make_state(), "brad") def test_missing_field_rejected(): bad = copy.deepcopy(GOOD_CRISIS) del bad["animation"] assert validate_crisis(bad, make_state(), "brad") is None def test_bad_animation_rejected(): bad = copy.deepcopy(GOOD_CRISIS) bad["animation"] = "npc_breakdance" assert validate_crisis(bad, make_state(), "brad") is None def test_confetti_reserved_for_win(): bad = copy.deepcopy(GOOD_CRISIS) bad["animation"] = "confetti_burst" assert validate_crisis(bad, make_state(), "brad") is None def test_revenue_floor_clamped(): state = make_state(revenue=30_000) p = copy.deepcopy(GOOD_CRISIS) p["revenue_delta"] = -200_000 out = validate_crisis(p, state, "brad") assert out["revenue_delta"] == -30_000 def test_pocket_money_capped_by_bribe_offer(): p = copy.deepcopy(GOOD_CRISIS) p["pocket_money_delta"] = 5_000 out = validate_crisis(p, make_state(), "brad", bribe_offer=2_000) assert out["pocket_money_delta"] == 2_000 p2 = copy.deepcopy(GOOD_CRISIS) p2["pocket_money_delta"] = 3_000 out2 = validate_crisis(p2, make_state(), "brad", bribe_offer=0) assert out2["pocket_money_delta"] == 0 def test_office_dates_are_not_romance(): state = make_state() p = copy.deepcopy(GOOD_CRISIS) p["npc_reaction"] = "The launch date is Friday and we are up to date, boss." assert validate_crisis(p, state, "brad") is not None def test_romance_gated_below_65(): state = make_state() state.npc("stacey").relationship = 50 p = copy.deepcopy(GOOD_CRISIS) p["npc_id"] = "stacey" p["npc_reaction"] = "Are you asking me on a date?" assert validate_crisis(p, state, "stacey") is None state.npc("stacey").relationship = 70 p2 = copy.deepcopy(GOOD_CRISIS) p2["npc_id"] = "stacey" p2["npc_reaction"] = "Are you asking me on a date?" assert validate_crisis(p2, state, "stacey") is not None def test_banned_content_rejected(): for word in ("lawsuit", "cancer", "Google", "election", "sue", "suing"): p = copy.deepcopy(GOOD_CRISIS) p["consequence"] = f"Something about {word} happened." assert validate_crisis(p, make_state(), "brad") is None, word def test_mechanics_leak_rejected(): for phrase in ("morale dropped by 5", "relationship_delta -1 applied", "the event_log shows everything"): p = copy.deepcopy(GOOD_CRISIS) p["consequence"] = phrase assert validate_crisis(p, make_state(), "brad") is None, phrase # in-fiction wording stays fine p = copy.deepcopy(GOOD_CRISIS) p["consequence"] = "The relationship with TerraLogix survived, barely." assert validate_crisis(p, make_state(), "brad") is not None def test_grammar_cut_text_trimmed_to_sentence(): from game.validator import _fit # near-cap, mid-sentence (grammar force-close) → trimmed to boundary long = ("He says the board just approved a budget. Then he said the " "thing about the 'synergistic") out = _fit(long, len(long)) assert out.endswith("budget.") # short fragment without punctuation is left alone assert _fit("It is still printing", 120) == "It is still printing" # complete sentence at the cap passes untouched s = "Exactly at the cap." assert _fit(s, len(s)) == s # run-on with no boundary anywhere → None (reject) assert _fit("x" * 200, 200) is None def test_event_intro_self_narration_rejected(): state = make_state() p = copy.deepcopy(GOOD_EVENT) p["affected_npc"] = "derek" p["intro"] = "Derek is sweating through his polo while staring at the wall." assert validate_event(p, state) is None p2 = copy.deepcopy(GOOD_EVENT) p2["affected_npc"] = "derek" p2["intro"] = "Boss. The wiki is on their conference wall. All of it." assert validate_event(p2, state) is not None def test_crisis_reaction_self_narration_rejected(): p = copy.deepcopy(GOOD_CRISIS) p["npc_reaction"] = "Brad will handle this, because Brad is a closer." assert validate_crisis(p, make_state(), "brad") is None def test_log_entry_gets_dollar_amount(): p = copy.deepcopy(GOOD_CRISIS) p["log_entry"] = "Brad did a thing without consequences listed" out = validate_crisis(p, make_state(), "brad") assert "$" in out["log_entry"] def test_log_dollar_matches_applied_not_model_claim(): # model claims -$200K but only $10K is on the books → floor to -$10K; # the log (which the board reads) must cite the applied figure, not -$200K state = make_state(revenue=10_000) p = copy.deepcopy(GOOD_CRISIS) p["revenue_delta"] = -200_000 p["log_entry"] = "Brad torched the deal, resulting in a -$200000 outcome." out = validate_crisis(p, state, "brad") assert "$200" not in out["log_entry"], out["log_entry"] assert "$10K" in out["log_entry"], out["log_entry"] def test_log_floored_to_zero_says_no_impact(): state = make_state(revenue=0) p = copy.deepcopy(GOOD_CRISIS) p["revenue_delta"] = -120_000 p["log_entry"] = "Janet's cloud cost us, resulting in a -$120000 outcome." out = validate_crisis(p, state, "brad") assert "no revenue impact" in out["log_entry"].lower(), out["log_entry"] def test_tool_names_allowed_companies_blocked(): state = make_state() ok = copy.deepcopy(GOOD_CRISIS) ok["consequence"] = "The Google Docs file started arguing with itself." assert validate_crisis(ok, state, "brad") is not None bad = copy.deepcopy(GOOD_CRISIS) bad["consequence"] = "Google signed our biggest deal of the year." assert validate_crisis(bad, state, "brad") is None GOOD_EVENT = { "affected_npc": "kevin", "category": "professional", "headline": "Kevin's chart escaped", "intro": "The chart is out there.", "option_a": "Catch the chart.", "option_b": "Deny the chart.", "urgency": "It is multiplying.", "setup_animation": "npc_confused", "morale_preview": -3, } def test_event_dedup_against_log(): state = make_state() state.log("Event 1 — kevin's chart escaped containment. -$10K.") assert validate_event(copy.deepcopy(GOOD_EVENT), state) is None def test_event_passes_clean_log(): assert validate_event(copy.deepcopy(GOOD_EVENT), make_state()) def test_presentation_must_reference_logged_event(): state = make_state() state.log("Event 1 — Brad promised TerraLogix the moon. -$50K.") p = { "round": 1, "board_tone": "neutral", "event_referenced": "Brad promised TerraLogix the moon", "round_difficulty": "standard", "option_a": "a", "option_b": "b", "board_dialogue": "Explain the moon.", } assert validate_presentation(copy.deepcopy(p), state, 1, closing=False) p["event_referenced"] = "the great flood of imaginary events" assert validate_presentation(p, state, 1, closing=False) is None