import json from pathlib import Path import pytest from fastapi.testclient import TestClient import app.narrate as narrate from app.main import app client = TestClient(app) VALID_LOG = json.load(open(Path(__file__).parent / "fixtures" / "canon_log_valid.json")) ROLE_ENDPOINTS = [ "/dm/narrate", "/dm/adjudicate", "/dm/improvise", "/npc/speak", "/party/banter", ] @pytest.fixture(autouse=True) def _stub_narrate_model_call(monkeypatch): # /dm/narrate is wired to a real Ollama call (Task 7). These tests exercise # canon-log validation across ALL role endpoints, not model behavior, so # stub the model boundary — keeps the default suite hermetic (no network). monkeypatch.setattr(narrate.ollama_client, "chat", lambda *a, **k: "stubbed prose") monkeypatch.setattr(narrate.call_log, "record", lambda **kw: None) def test_health_ok(): assert client.get("/health").json() == {"status": "ok"} def test_every_role_endpoint_accepts_a_valid_canon_log(): for path in ROLE_ENDPOINTS: if path == "/npc/speak": # /npc/speak has additional required fields body = { "canon_log": VALID_LOG, "npc_id": "brannoc_thane", "disposition": 0, "available_moves": [], "utterance": "test" } else: body = {"canon_log": VALID_LOG} r = client.post(path, json=body) assert r.status_code == 200, f"{path} rejected a valid log: {r.text}" def test_every_role_endpoint_rejects_an_invalid_canon_log(): bad = json.loads(json.dumps(VALID_LOG)) bad["player"]["luck"] = 5 # §7 leak for path in ROLE_ENDPOINTS: if path == "/npc/speak": # /npc/speak has additional required fields body = { "canon_log": bad, "npc_id": "brannoc_thane", "disposition": 0, "available_moves": [], "utterance": "test" } else: body = {"canon_log": bad} r = client.post(path, json=body) assert r.status_code == 422, f"{path} accepted an invalid log" assert "canon_log_errors" in r.json()["detail"] def test_missing_canon_log_is_a_422(): r = client.post("/dm/narrate", json={}) assert r.status_code == 422 assert "canon_log_errors" in r.json()["detail"] def test_malformed_body_uses_the_unified_error_shape(): # A pydantic body failure returns the SAME envelope as a schema failure, # so the client only ever parses one 422 shape. r = client.post("/dm/narrate", json={"canon_log": "not-a-dict"}) assert r.status_code == 422 assert "canon_log_errors" in r.json()["detail"]