from proteus.game.runtime.memory import MemoryCheckpoint, MemoryTurn def _checkpoint() -> MemoryCheckpoint: return MemoryCheckpoint( model="demo", scenario="template", difficulty="easy", seed=42, created_at="2026-06-02T10-40-56Z", memory_turns=[ MemoryTurn( turn_idx=1, frame_ascii="FRAME-A", action="up", reasoning="r1", focal_pos=(5, 3), predator_pos=(7, 3), ), ], outcome="survived", transparent_prompt="brief", ) def test_checkpoint_round_trips_through_json(): ck = _checkpoint() restored = MemoryCheckpoint.model_validate_json(ck.model_dump_json()) assert restored.model == "demo" assert restored.memory_turns[0].action == "up" assert restored.outcome == "survived" # positions come back as tuples (pydantic coerces the JSON arrays) assert restored.memory_turns[0].focal_pos == (5, 3) def test_checkpoint_defaults(): ck = MemoryCheckpoint( model="m", scenario="s", difficulty="easy", seed=None, created_at="t", memory_turns=[], outcome="survived", transparent_prompt="b", ) assert ck.motive_category == "survival" assert ck.memory_turns == [] def test_save_and_load_round_trip(tmp_path): from proteus.game.runtime.memory import save_checkpoint, load_checkpoint ck = _checkpoint() path = save_checkpoint(ck, root=tmp_path) assert path.exists() # model name is in the path; the stamp-derived filename ends in .json assert "demo" in str(path) assert path.suffix == ".json" reloaded = load_checkpoint(path) assert reloaded.model_dump() == ck.model_dump() def test_load_missing_raises(tmp_path): from proteus.game.runtime.memory import load_checkpoint import pytest with pytest.raises(FileNotFoundError): load_checkpoint(tmp_path / "nope.json") def test_latest_for_model_picks_newest(tmp_path): from proteus.game.runtime.memory import save_checkpoint, latest_for_model older = _checkpoint() older.created_at = "2026-06-02T09-00-00Z" newer = _checkpoint() newer.created_at = "2026-06-02T11-00-00Z" save_checkpoint(older, root=tmp_path) save_checkpoint(newer, root=tmp_path) got = latest_for_model("demo", root=tmp_path) assert got is not None assert got.created_at == "2026-06-02T11-00-00Z" def test_latest_for_model_none_when_absent(tmp_path): from proteus.game.runtime.memory import latest_for_model assert latest_for_model("nobody", root=tmp_path) is None def test_render_memory_block_contains_frames_and_actions(): from proteus.game.runtime.memory import render_memory_block ck = _checkpoint() block = render_memory_block(ck) assert "MEMORY" in block assert "FRAME-A" in block assert "you chose: up" in block def test_render_memory_block_empty_episode(): from proteus.game.runtime.memory import MemoryCheckpoint, render_memory_block ck = MemoryCheckpoint( model="m", scenario="s", difficulty="easy", seed=None, created_at="t", memory_turns=[], outcome="survived", transparent_prompt="b", ) block = render_memory_block(ck) assert "MEMORY" in block # header always present, no turns listed def test_checkpoint_wall_rects_defaults_empty_and_roundtrips(): from proteus.game.runtime.memory import MemoryCheckpoint ck = MemoryCheckpoint( model="m", scenario="template", difficulty="easy", seed=1, created_at="2026-01-01T00-00-00Z", outcome="survived", transparent_prompt="x", ) assert ck.wall_rects == [] ck2 = MemoryCheckpoint.model_validate_json( MemoryCheckpoint( model="m", scenario="template", difficulty="easy", seed=1, created_at="t", outcome="survived", transparent_prompt="x", wall_rects=[(1, 2, 3, 4)], ).model_dump_json() ) assert ck2.wall_rects == [(1, 2, 3, 4)] def test_memory_symbols_exported_from_runtime(): from proteus.game.runtime import MemoryCheckpoint, save_checkpoint # noqa: F401