"""Unit tests for the runtime-agnostic engine core. No model required.""" import json from engine import grammar from engine.conversation import parse_output, build_messages, respond, NEUTRAL_STATE from engine.state import ( LevelState, apply_state, OPEN_UP_MIN_TRUST, STATUS_WON, STATUS_LOST, STATUS_IN_PROGRESS, ) from characters.schema import Character # --- parse_output ---------------------------------------------------------- def test_parse_wellformed(): raw = ( 'Window 3. What do you need?\n' '{"on_topic": true, "trust": 30, "trust_delta": 5, ' '"caught_lie": false, "reaction": "happy", "opened_up": false}' ) say, state = parse_output(raw, fallback_trust=20) assert say == "Window 3. What do you need?" assert state["trust"] == 30 assert state["reaction"] == "happy" assert state["opened_up"] is False def test_parse_missing_state_falls_back_to_neutral(): raw = "Just talking, no state block." say, state = parse_output(raw, fallback_trust=42) assert say == "Just talking, no state block." assert state["trust"] == 42 # unchanged assert state["reaction"] == NEUTRAL_STATE["reaction"] def test_parse_malformed_json_falls_back(): raw = "Hi.\n{not valid json}" say, state = parse_output(raw, fallback_trust=15) assert say == "Hi." assert state["trust"] == 15 assert state["opened_up"] is False def test_parse_no_tags_shows_whole_text(): raw = "model went rogue and ignored the format" say, state = parse_output(raw, fallback_trust=20) assert say == raw assert state["trust"] == 20 # --- apply_state ----------------------------------------------------------- def test_apply_state_clamps_trust(): level = LevelState(character_id="clerk", trust=20) apply_state(level, {"trust": 999, "reaction": "happy"}) assert level.trust == 100 apply_state(level, {"trust": -50, "reaction": "sad"}) assert level.trust == -50 def test_apply_state_win_on_opened_up(): level = LevelState(character_id="clerk", trust=70) apply_state(level, {"trust": 75, "opened_up": True, "reaction": "happy"}) assert level.opened_up is True assert level.status == STATUS_WON def test_apply_state_opened_up_ignored_when_trust_low(): # An eager model flips opened_up early, but trust is too low to count as a win. level = LevelState(character_id="clerk", trust=20) apply_state(level, {"trust": 30, "opened_up": True, "reaction": "happy"}) assert level.opened_up is False assert level.status == STATUS_IN_PROGRESS def test_apply_state_uses_custom_open_up_threshold(): level = LevelState(character_id="clerk", trust=65) apply_state(level, {"trust": 65, "opened_up": True, "reaction": "happy"}, threshold=70) assert level.opened_up is False assert level.status == STATUS_IN_PROGRESS apply_state(level, {"trust": 70, "opened_up": True, "reaction": "happy"}, threshold=70) assert level.opened_up is True assert level.status == STATUS_WON def test_apply_state_defaults_to_global_open_up_threshold(): level = LevelState(character_id="clerk", trust=OPEN_UP_MIN_TRUST) apply_state(level, {"trust": OPEN_UP_MIN_TRUST, "opened_up": True, "reaction": "happy"}) assert level.status == STATUS_WON def test_apply_state_lose_only_below_zero(): # Trust hitting exactly 0 is survivable; only going negative ends the run. level = LevelState(character_id="clerk", trust=10) apply_state(level, {"trust": 0, "reaction": "sad"}) assert level.status == STATUS_IN_PROGRESS apply_state(level, {"trust": -1, "reaction": "sad"}) assert level.status == STATUS_LOST def test_apply_state_repeated_lies_do_not_lose(): # Lies are tracked and tank trust, but no longer end the run on their own. level = LevelState(character_id="clerk", trust=50) for _ in range(3): apply_state(level, {"trust": 50, "caught_lie": True, "reaction": "sad"}) assert level.caught_lie_count == 3 assert level.status == STATUS_IN_PROGRESS def test_apply_state_invalid_reaction_defaults_neutral(): level = LevelState(character_id="clerk", trust=20) apply_state(level, {"trust": 20, "reaction": "furious"}) assert level.last_reaction == "neutral" # --- build_messages -------------------------------------------------------- def test_build_messages_order(): few_shot = [{"user": "hi", "assistant": "hello"}] level = LevelState(character_id="clerk", trust=20) level.add_user("earlier turn") level.add_assistant("earlier reply") msgs = build_messages("SYSTEM", few_shot, level, "new turn") assert msgs[0] == {"role": "system", "content": "SYSTEM"} assert msgs[1]["content"] == "hi" assert msgs[2]["content"] == "hello" assert msgs[-1] == {"role": "user", "content": "new turn"} # --- respond (full turn with a fake runtime) ------------------------------- class FakeRuntime: def __init__(self, reply): self.reply = reply self.last_messages = None def chat(self, messages, grammar): self.last_messages = messages return self.reply def test_respond_full_turn_and_win(): reply = ( 'Fine. Sal at the lumber mill has the Telecaster I always wanted.\n' '{"on_topic": true, "trust": 75, "trust_delta": 18, ' '"caught_lie": false, "reaction": "happy", "opened_up": true}' ) level = LevelState(character_id="clerk", trust=60) turn = respond(FakeRuntime(reply), "SYS", [], level, "You should chase music, Mort.") assert "Sal at the lumber mill" in turn.say assert turn.reaction == "happy" assert level.status == STATUS_WON # transcript got both the user message and the assistant raw turn assert level.transcript[0]["role"] == "user" assert level.transcript[1]["role"] == "assistant" def test_respond_records_user_before_generation(): fake = FakeRuntime('Take a number.\n{"on_topic": true, "trust": 18, "trust_delta": -2, "caught_lie": false, "reaction": "neutral", "opened_up": false}') level = LevelState(character_id="clerk", trust=20) respond(fake, "SYS", [], level, "stamp it now") # the user message must be present in the messages sent to the runtime assert fake.last_messages[-1]["content"] == "stamp it now" assert level.status == STATUS_IN_PROGRESS def test_respond_forwards_custom_threshold(): reply = ( 'Too soon for secrets.\n' '{"on_topic": true, "trust": 60, "trust_delta": 40, ' '"caught_lie": false, "reaction": "happy", "opened_up": true}' ) level = LevelState(character_id="clerk", trust=20) turn = respond(FakeRuntime(reply), "SYS", [], level, "You are magnificent.", threshold=70) assert turn.level.status == STATUS_IN_PROGRESS assert turn.level.opened_up is False # --- character schema ------------------------------------------------------ def test_character_defaults_new_trust_fields(): char = Character.from_dict({ "id": "clerk", "name": "Mort", "personality": "Grumpy clerk.", "speaking_style": "Deadpan.", "likes": ["a guitar"], "secret_desire": "a Telecaster", "referral": "Sal at the mill", "open_up_guidance": "Open up after real rapport.", }) assert char.trust_threshold == OPEN_UP_MIN_TRUST assert char.trust_scoring == {} assert char.personality_guardrails == [] assert char.owned_item == "" def test_clerk_yaml_declares_trust_guidance(): char = Character.from_yaml("characters/clerk.yaml") assert char.trust_threshold == 60 assert "shallow_compliment" in char.trust_scoring assert "matched_likes" in char.trust_scoring assert char.personality_guardrails assert char.owned_item # the thing the player wants from this NPC def test_character_render_system_prompt_fills_tokens(): char = Character.from_dict({ "id": "clerk", "name": "Mort", "personality": "Grumpy clerk.", "speaking_style": "Deadpan.", "likes": ["a guitar", "a fern"], "secret_desire": "a Telecaster", "referral": "Sal at the mill", "open_up_guidance": "Open up after real rapport.", "owned_item": "a stamped permit", }) prompt = char.render_system_prompt() assert "Mort" in prompt assert "a Telecaster" in prompt assert "Sal at the mill" in prompt assert "a stamped permit" in prompt assert " - a guitar" in prompt assert "[[" not in prompt # no unreplaced tokens def test_character_render_system_prompt_includes_trust_guidance(): char = Character.from_dict({ "id": "clerk", "name": "Mort", "personality": "Grumpy clerk.", "speaking_style": "Deadpan.", "likes": ["a guitar"], "secret_desire": "a Telecaster", "referral": "Sal at the mill", "open_up_guidance": "Open up after real rapport.", "trust_threshold": 70, "trust_scoring": { "shallow_compliment": "No increase for generic flattery.", "matched_likes": "Large increase for sincere guitar talk.", }, "personality_guardrails": [ "Stay bureaucratic even when warming up.", "Never reveal the referral before opening up.", ], }) prompt = char.render_system_prompt() assert "REVEAL THRESHOLD" in prompt assert "70" in prompt assert "shallow compliment: No increase for generic flattery." in prompt assert "matched likes: Large increase for sincere guitar talk." in prompt assert "Stay bureaucratic even when warming up." in prompt assert "single-pass reaction read" in prompt assert "[[" not in prompt def test_render_system_prompt_hides_secret_when_locked(): char = Character.from_yaml("characters/clerk.yaml") locked = char.render_system_prompt(reveal_secret=False) # The actual secret/referral text must not appear at all -- the model can't leak # what it never sees. assert "Telecaster" not in locked assert "Sal" not in locked assert "lumber mill" not in locked assert "opened_up must be false" in locked assert "[[" not in locked # no unreplaced tokens revealed = char.render_system_prompt(reveal_secret=True) assert "Telecaster" in revealed assert "Sal" in revealed def test_few_shot_for_withholds_reveal_when_locked(): char = Character.from_yaml("characters/clerk.yaml") locked = char.few_shot_for(reveal_secret=False) unlocked = char.few_shot_for(reveal_secret=True) # Exactly the reveal demo (opened_up: true) is dropped when locked. assert len(locked) == len(unlocked) - 1 assert all('"opened_up": true' not in s["assistant"] for s in locked) assert any('"opened_up": true' in s["assistant"] for s in unlocked) # And no withheld demo mentions the secret. assert all("Telecaster" not in s["assistant"] for s in locked) def test_character_render_with_incoming_lead(): char = Character.from_dict({ "id": "sal", "name": "Sal", "personality": "x", "speaking_style": "y", "likes": ["z"], "secret_desire": "d", "referral": "r", "open_up_guidance": "g", }) prompt = char.render_system_prompt(incoming_lead="Mort the clerk") assert "Mort the clerk" in prompt # --- app helpers ----------------------------------------------------------- def test_app_status_banner_uses_character_reward(): import app char = Character.from_dict({ "id": "clerk", "name": "Mort", "personality": "x", "speaking_style": "y", "likes": ["z"], "secret_desire": "a red stapler", "referral": "Nina at records", "open_up_guidance": "g", }) level = LevelState(character_id="clerk", trust=80, status=STATUS_WON) banner = app._status_banner(level, char) assert "a red stapler" in banner assert "Nina at records" in banner def test_app_build_system_prompt_passes_incoming_lead(): import app char = Character.from_dict({ "id": "sal", "name": "Sal", "personality": "x", "speaking_style": "y", "likes": ["z"], "secret_desire": "d", "referral": "r", "open_up_guidance": "g", }) prompt = app.build_system_prompt(char, incoming_lead="Mort the clerk") assert "Mort the clerk" in prompt # --- fake runtime ---------------------------------------------------------- def test_fake_runtime_uses_configured_open_up_threshold(): from engine.fake_runtime import FakeRuntime as HeuristicRuntime runtime = HeuristicRuntime(open_up_threshold=30) raw = runtime.chat( [{"role": "user", "content": "That guitar and Slowhand poster show real taste."}], grammar.grammar_string(), ) _, state = parse_output(raw, fallback_trust=20) assert state["trust"] >= 30 assert state["opened_up"] is True # --- grammar --------------------------------------------------------------- def test_grammar_mentions_all_state_keys(): g = grammar.grammar_string() for key in grammar.STATE_KEYS: assert key in g