Hackathon-IA-VisualNovel / tests /test_directives.py
WillHbx's picture
test: engine split, llm helpers, and orchestrator guard coverage
1ea8154
Raw
History Blame Contribute Delete
5.38 kB
"""apply_directives is the only mutator — pin its behaviour: scene change, new character,
relationship clamp, exit, and ending."""
from __future__ import annotations
from visualnovel import state
from visualnovel.schemas import (
Character,
Directives,
DirectorOutput,
Ending,
GameState,
NewCharacter,
Scene,
SceneChange,
)
def _state() -> GameState:
return GameState(
seed=1,
vibe="v",
style_guide="s",
scene=Scene(id="a", place="A", present=["moth"]),
characters={"moth": Character(id="moth", name="Moth", relationship=95)},
)
def test_scene_change_and_relationship_clamp():
s = _state()
out = DirectorOutput(
speaker="moth",
dialogue="hi",
emotion="joy",
directives=Directives(
scene_change=SceneChange(place="The Hollow Stair", description="d", mood="eerie"),
relationship_delta=50, # 95 + 50 -> clamped to 100
),
)
effects = state.apply_directives(s, out)
assert "scene_changed" in effects
assert s.scene.place == "The Hollow Stair"
assert s.scene.present == ["moth"] # people carry across a move
assert s.characters["moth"].relationship == 100 # clamped
assert s.characters["moth"].mood == "joy"
def test_newcomer_speaker_gets_mood_applied():
# new_character is processed before the speaker block, so an arriving newcomer
# who speaks this turn gets their emotion applied (drives their first sprite)
s = _state()
out = DirectorOutput(
speaker="fox",
dialogue="Oh! You noticed me?",
emotion="shy",
directives=Directives(
new_character=NewCharacter(
id="fox", name="Fen", one_line="a sly fox", appearance="russet"
)
),
)
effects = state.apply_directives(s, out)
assert "new_character:fox" in effects
assert s.characters["fox"].mood == "shy"
def test_new_character_joins_stage():
s = _state()
out = DirectorOutput(
speaker="moth",
dialogue="look",
directives=Directives(
new_character=NewCharacter(
id="fox", name="Fen", one_line="a sly fox", appearance="russet"
)
),
)
effects = state.apply_directives(s, out)
assert "new_character:fox" in effects
assert "fox" in s.characters and "fox" in s.scene.present
assert s.characters["fox"].sprite_seed != 0 # pinned seed assigned
def test_remember_fact_appends_dedupes_and_caps():
s = _state()
for i in range(9): # 9 distinct facts -> capped at 8, oldest dropped
out = DirectorOutput(
speaker="moth", dialogue="x", directives=Directives(remember_fact=f"fact {i}")
)
effects = state.apply_directives(s, out)
assert "fact:moth" in effects
# duplicate is ignored
out = DirectorOutput(
speaker="moth", dialogue="x", directives=Directives(remember_fact="fact 8")
)
effects = state.apply_directives(s, out)
assert "fact:moth" not in effects
facts = s.characters["moth"].known_facts
assert len(facts) == 8
assert "fact 0" not in facts and "fact 8" in facts
def test_milestone50_lifecycle():
from visualnovel import memory
s = _state()
s.characters["moth"].relationship = 45
# crossing 50 arms the milestone
out = DirectorOutput(speaker="moth", dialogue="x", directives=Directives(relationship_delta=10))
effects = state.apply_directives(s, out)
assert "milestone50:moth" in effects
assert s.flags["milestone50_moth"] == "pending"
# the next context assembly injects the hint exactly while pending
ctx = memory.assemble_context(s, "hello")
assert "GROWING CLOSE" in ctx
# the next apply retires it, and a further delta does not re-arm
out2 = DirectorOutput(speaker="moth", dialogue="y", directives=Directives(relationship_delta=5))
effects2 = state.apply_directives(s, out2)
assert s.flags["milestone50_moth"] == "done"
assert "milestone50:moth" not in effects2
assert "GROWING CLOSE" not in memory.assemble_context(s, "hello")
def test_advance_beat_rate_limited():
s = _state()
# too early: beat_started_turn defaults to 0 and turn_index is 0
out = DirectorOutput(speaker="moth", dialogue="x", directives=Directives(advance_beat=True))
effects = state.apply_directives(s, out)
assert "beat" not in effects and s.beat == "opening"
# after 4 turns in the beat, the advance is honoured
s.turn_index = 4
effects = state.apply_directives(s, out)
assert "beat" in effects and s.beat == "rising"
# and immediately advancing again is blocked
s.turn_index = 5
effects = state.apply_directives(s, out)
assert "beat" not in effects and s.beat == "rising"
def test_exit_and_ending():
s = _state()
s.scene.present.append("fox")
s.characters["fox"] = Character(id="fox", name="Fen")
out = DirectorOutput(
speaker="moth",
dialogue="farewell",
directives=Directives(
exit_character="fox",
ending=Ending(kind="warm", text="And the wood let you go, gently."),
),
)
effects = state.apply_directives(s, out)
assert "exit_character:fox" in effects and "fox" not in s.scene.present
assert s.beat == "ended" and s.flags["ending_kind"] == "warm"