"""Comic-panel overlay backend: image_prompt soft-validation + FLUX client.""" import copy import os from game import comic from game.state import GameState from game.validator import validate_crisis, validate_event 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, } GOOD_EVENT = { "affected_npc": "brad", "category": "professional", "headline": "The deck went to the wrong client", "intro": "> A laptop is shoved toward you. 'I think I clicked send,' " "comes the panicked whisper.", "option_a": "Call the client and own it.", "option_b": "Pretend it was a teaser.", "urgency": "The client replies in five minutes.", "setup_animation": "npc_confused", "morale_preview": -3, } CLEAN_PROMPT = ("flat 2D comic panel, bold ink outlines, chibi office workers. " "Panel 1: Brad sweating at a laptop, bubble 'I clicked send'.") def _state(): return GameState(session_id="t") def test_clean_image_prompt_kept_on_crisis(): p = copy.deepcopy(GOOD_CRISIS) p["image_prompt"] = CLEAN_PROMPT out = validate_crisis(p, _state(), "brad") assert out is not None and out["image_prompt"] == CLEAN_PROMPT def test_clean_image_prompt_kept_on_event(): p = copy.deepcopy(GOOD_EVENT) p["image_prompt"] = CLEAN_PROMPT out = validate_event(p, _state()) assert out is not None and out["image_prompt"] == CLEAN_PROMPT def test_absent_image_prompt_is_fine(): out = validate_crisis(copy.deepcopy(GOOD_CRISIS), _state(), "brad") assert out is not None and "image_prompt" not in out def test_mechanics_leak_drops_image_prompt_without_rejecting(): p = copy.deepcopy(GOOD_CRISIS) p["image_prompt"] = "comic panel showing morale dropping and revenue_delta" out = validate_crisis(p, _state(), "brad") # the event itself survives — only the dirty prompt is dropped assert out is not None and "image_prompt" not in out def test_blank_image_prompt_dropped(): p = copy.deepcopy(GOOD_EVENT) p["image_prompt"] = " " out = validate_event(p, _state()) assert out is not None and "image_prompt" not in out def test_overlong_image_prompt_clamped(): p = copy.deepcopy(GOOD_CRISIS) p["image_prompt"] = "comic panel of Brad " + "x" * 500 out = validate_crisis(p, _state(), "brad") assert out is not None and len(out["image_prompt"]) <= 400 def test_generate_comic_none_without_flux_url(): saved = os.environ.pop("FLUX_URL", None) try: assert comic.flux_available() is False assert comic.generate_comic(CLEAN_PROMPT) is None finally: if saved is not None: os.environ["FLUX_URL"] = saved def test_generate_comic_none_on_empty_prompt(): assert comic.generate_comic("") is None def test_comic_caption_kept(): p = copy.deepcopy(GOOD_CRISIS) p["comic_caption"] = "Brad discovers the send button is load-bearing." out = validate_crisis(p, _state(), "brad") assert out is not None and out["comic_caption"].startswith("Brad discovers") def test_comic_caption_mechanics_dropped_event_survives(): p = copy.deepcopy(GOOD_EVENT) p["comic_caption"] = "This will tank morale and your relationship_delta." out = validate_event(p, _state()) assert out is not None and "comic_caption" not in out def test_banned_in_image_prompt_drops_not_rejects(): # a banned real-company name in the decorative prompt must NOT fail the # whole event — only the prompt is dropped p = copy.deepcopy(GOOD_CRISIS) p["image_prompt"] = CLEAN_PROMPT + " logo of Microsoft acquiring us" out = validate_crisis(p, _state(), "brad") assert out is not None and "image_prompt" not in out