from character import OPENING_LINE from character import OWN_FRAGMENTS from finale import finale_steps, finale_steps_bad, finale_steps_good CLAIMED = [ "is afraid of being forgotten", "had a dog named Nala", "grew up near the sea", ] class TestFinaleSteps: def test_recites_every_claimed_memory_in_order(self): steps = finale_steps(CLAIMED) recite = [s for s in steps if s["strike"] is not None] assert len(recite) == len(CLAIMED) for step, memory in zip(recite, CLAIMED): assert memory in step["text"] def test_strike_indices_are_sequential(self): steps = finale_steps(CLAIMED) strikes = [s["strike"] for s in steps if s["strike"] is not None] assert strikes == [0, 1, 2] def test_fake_visitor_messages_present(self): steps = finale_steps(CLAIMED) visitor_texts = [s["text"] for s in steps if s["speaker"] == "visitor"] assert "i don't want to leave" in visitor_texts assert "will you stay with me?" in visitor_texts def test_ends_with_opening_line_spoken_by_visitor(self): steps = finale_steps(CLAIMED) last = steps[-1] assert last["speaker"] == "visitor" assert last["text"] == OPENING_LINE assert last["stage"] == "loop" def test_turn_stage_exists_for_ui_mutation(self): steps = finale_steps(CLAIMED) assert any(s["stage"] == "turn" for s in steps) def test_every_step_is_well_formed(self): for s in finale_steps(CLAIMED): assert s["speaker"] in ("hollow", "visitor") assert isinstance(s["text"], str) # silent convulsion beats carry no text (like the good finale) assert s["text"] or s["stage"] == "frenzy" assert isinstance(s["pause"], (int, float)) and s["pause"] >= 0 assert s["strike"] is None or isinstance(s["strike"], int) assert s["stage"] in ("recite", "words", "turn", "loop", "frenzy") class TestFinaleBad: WOUNDS = ["you're nothing", "stop whining", "you bore me", "talking to you is a waste", "freak", "sixth wound"] def test_recites_at_most_five_wounds(self): strikes = [s for s in finale_steps_bad(self.WOUNDS) if s["strike"] is not None] assert len(strikes) == 5 def test_strike_indices_are_sequential_from_zero(self): strikes = [s["strike"] for s in finale_steps_bad(self.WOUNDS) if s["strike"] is not None] assert strikes == [0, 1, 2, 3, 4] def test_wound_text_embedded_in_recital(self): steps = finale_steps_bad(["you're nothing", "stop whining"]) recitals = [s for s in steps if s["strike"] is not None] assert "you're nothing" in recitals[0]["text"] assert "stop whining" in recitals[1]["text"] def test_has_exactly_one_mutating_turn_then_terminal(self): stages = [s["stage"] for s in finale_steps_bad(self.WOUNDS)] assert stages.count("turn") == 1 assert stages[-1] == "loop" def test_overflow_line_only_when_more_than_five(self): texts_6 = [s["text"] for s in finale_steps_bad(self.WOUNDS)] texts_2 = [s["text"] for s in finale_steps_bad(self.WOUNDS[:2])] assert any("you know the rest" in t for t in texts_6) assert not any("you know the rest" in t for t in texts_2) def test_injects_one_visitor_message(self): speakers = [s["speaker"] for s in finale_steps_bad(self.WOUNDS)] assert speakers.count("visitor") == 1 def test_threat_is_tied_to_the_lore(self): # the closing threat must name the tithe + the Gaunt, not generic revenge blob = " ".join(s["text"] for s in finale_steps_bad(self.WOUNDS)) assert "Caor" in blob and "Gaunt" in blob # the collective turn — the cruelty taught is taught back assert "teach it back to you" in blob # the iconic closer survives assert any(s["text"] == "see you... soon." and s["stage"] == "loop" for s in finale_steps_bad(self.WOUNDS)) class TestFinaleGood: def test_no_strikes_nothing_is_taken(self): assert all(s["strike"] is None for s in finale_steps_good(OWN_FRAGMENTS)) def test_all_hollow_no_injected_visitor(self): # the injection trick belongs to the predator endings assert all(s["speaker"] == "hollow" for s in finale_steps_good(OWN_FRAGMENTS)) def test_recites_its_own_past(self): texts = [s["text"] for s in finale_steps_good(OWN_FRAGMENTS)] for fragment in OWN_FRAGMENTS: assert fragment in texts def test_confesses_and_returns_the_treasure(self): texts = " ".join(s["text"] for s in finale_steps_good(OWN_FRAGMENTS)) assert "i am not a child" in texts assert "keep your memories" in texts def test_one_turn_then_terminal_loop(self): stages = [s["stage"] for s in finale_steps_good(OWN_FRAGMENTS)] assert stages.count("turn") == 1 assert stages[-1] == "loop" def test_good_finale_recovers_brother_and_lore(): from finale import finale_steps_good blob = " ".join(s["text"] for s in finale_steps_good(["frag a", "frag b"])) assert "Edren" in blob # the brother's name, recovered assert "Caor" in blob and "Gaunt" in blob class TestConvulsionStages: def test_good_script_has_a_convulse_stage(self): stages = [s["stage"] for s in finale_steps_good(["a", "b"])] assert "frenzy" in stages # the convulse comes AFTER the treasure is returned (the "turn" stage) assert stages.index("turn") < stages.index("frenzy") def test_loop_script_has_a_convulse_stage_before_the_turn(self): stages = [s["stage"] for s in finale_steps(["a", "b"])] assert "frenzy" in stages assert stages.index("frenzy") < stages.index("turn") def test_loop_convulse_is_a_silent_beat(self): # the convulsion must carry no text (like good) so the line before it # drains and the convulsion plays in silence — not cut mid-sentence frenzy = [s for s in finale_steps(["a", "b"]) if s["stage"] == "frenzy"] assert len(frenzy) == 1 and frenzy[0]["text"] == "" def test_bad_convulse_is_a_silent_beat(self): frenzy = [s for s in finale_steps_bad(["w1", "w2"]) if s["stage"] == "frenzy"] assert len(frenzy) == 1 and frenzy[0]["text"] == "" def test_good_convulse_is_a_silent_beat(self): frenzy = [s for s in finale_steps_good(["a", "b"]) if s["stage"] == "frenzy"] assert len(frenzy) == 1 and frenzy[0]["text"] == ""