"""Integration test: a council policy driven by stub clients (no GPU/network). Verifies the multi-engine grouping reassembles raw/messages/decisions keyed by CREATURE name, thoughts populate through the Game, and a 5-turn run never crashes. Run: python -m pytest tests/ -q """ import json import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from ttw.agents import make_council_policy from ttw.council import CREATURE_ENGINE from ttw.game import Game class StubClient: """Fake engine handle. chat_batch returns one valid JSON decision per convo. It tags each decision with the engine id so the test can prove decisions are routed back to the right creature. The convo's system prompt names the creature, so the stub echoes that name into its thought. """ def __init__(self, engine_id: str): self.engine_id = engine_id def chat_batch(self, conversations, max_tokens=256, temperature=0.7): out = [] for convo in conversations: system = convo[0]["content"] # "You are , ..." -> pull the creature name. name = system.split("You are ", 1)[1].split(",", 1)[0] out.append( json.dumps( { "thought": f"{name} via {self.engine_id}", "offers": [{"side": "sell", "good": "honey", "price": 9, "qty": 1}], "gossip": "", } ) ) return out def _clients(): return {eid: StubClient(eid) for eid in set(CREATURE_ENGINE.values())} def test_council_policy_routes_decisions_to_correct_creature(): from ttw.world import seed_world policy = make_council_policy(_clients(), temperature=0.7) world = seed_world() world.turn = 1 # policy decides per turn; force a fresh batch for name in world.creatures: policy(world, name) state = policy.state for name, eid in CREATURE_ENGINE.items(): assert state["raw"][name].count(name) >= 1 assert eid in state["raw"][name] # routed to its assigned engine offers, _ = state["decisions"][name] assert offers and offers[0].creature == name def test_council_game_runs_five_turns_and_populates_thoughts(): g = Game(make_council_policy(_clients(), temperature=0.7), deck_seed=1) for _ in range(5): g.step() assert g.world.turn == 5 # Blank traces would be a bug: every alive creature must have a thought. for c in g.world.alive(): assert c.name in g.thoughts assert g.thoughts[c.name].strip() class RaisingClient(StubClient): """An engine handle whose chat_batch always raises (a flaky/cold engine).""" def chat_batch(self, conversations, max_tokens=256, temperature=0.7): raise RuntimeError("engine unavailable") def test_council_survives_one_failing_engine(): """A single engine raising must not crash the turn: its creatures sit out (no offers) while the rest decide normally. Protects the live Space.""" clients = _clients() # Knock out exactly the engine driving Oona (gptoss) and prove the turn holds. dead = CREATURE_ENGINE["Oona"] clients[dead] = RaisingClient(dead) g = Game(make_council_policy(clients, temperature=0.7), deck_seed=1) for _ in range(3): g.step() # must not raise assert g.world.turn == 3 # A creature on a healthy engine still thinks; the dead engine's sits out. healthy = next(n for n, e in CREATURE_ENGINE.items() if e != dead) assert g.thoughts.get(healthy, "").strip() assert g.policy.state["raw"].get("Oona", "") == ""