From 5a35802075937a860527e85fa10a1c2e29de7a54 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Fri, 10 Jul 2026 08:31:38 -0500 Subject: [PATCH] =?UTF-8?q?feat(api):=20wire=20/dm/narrate=20=E2=80=94=202?= =?UTF-8?q?00=20prose=20|=20502=20model=5Ferror=20|=20422=20invalid=20log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- api/app/main.py | 7 +++++- api/tests/test_narrate_endpoint.py | 39 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 api/tests/test_narrate_endpoint.py diff --git a/api/app/main.py b/api/app/main.py index d04fa7b..9d9fddb 100644 --- a/api/app/main.py +++ b/api/app/main.py @@ -12,6 +12,8 @@ from fastapi.responses import JSONResponse from pydantic import BaseModel from .canon_log import validate_canon_log +from .narrate import run as narrate_run +from .ollama_client import ModelError app = FastAPI(title="coc-rpg proxy", version="0.0.1") @@ -52,7 +54,10 @@ def health() -> dict: @app.post("/dm/narrate") def narrate(req: TurnRequest = Depends(valid_turn)) -> dict: - return {"detail": "not implemented"} + try: + return {"prose": narrate_run(req.canon_log)} + except ModelError as exc: + raise HTTPException(status_code=502, detail={"model_error": str(exc)}) @app.post("/dm/adjudicate") diff --git a/api/tests/test_narrate_endpoint.py b/api/tests/test_narrate_endpoint.py new file mode 100644 index 0000000..e3bd86f --- /dev/null +++ b/api/tests/test_narrate_endpoint.py @@ -0,0 +1,39 @@ +import json +from pathlib import Path + +from fastapi.testclient import TestClient + +import app.main as main +import app.narrate as narrate +from app.ollama_client import ModelError + +client = TestClient(main.app) +VALID = json.loads((Path(__file__).parent / "fixtures" / "canon_log_valid.json").read_text()) + + +def test_valid_log_returns_prose(monkeypatch): + monkeypatch.setattr(narrate.ollama_client, "chat", lambda *a, **k: "You stand on the wharf.") + monkeypatch.setattr(narrate.call_log, "record", lambda **kw: None) # keep test output pristine + r = client.post("/dm/narrate", json={"canon_log": VALID}) + assert r.status_code == 200 + assert r.json() == {"prose": "You stand on the wharf."} + + +def test_model_error_maps_to_502(monkeypatch): + def boom(*a, **k): + raise ModelError("upstream down") + + monkeypatch.setattr(narrate.ollama_client, "chat", boom) + monkeypatch.setattr(narrate.call_log, "record", lambda **kw: None) + r = client.post("/dm/narrate", json={"canon_log": VALID}) + assert r.status_code == 502 + assert "model_error" in r.json()["detail"] + + +def test_invalid_log_still_422(monkeypatch): + monkeypatch.setattr(narrate.ollama_client, "chat", lambda *a, **k: "x") + bad = json.loads(json.dumps(VALID)) + bad["player"]["luck"] = 5 # §7 leak — schema rejects + r = client.post("/dm/narrate", json={"canon_log": bad}) + assert r.status_code == 422 + assert "canon_log_errors" in r.json()["detail"]