fix(api): best-effort call logging + non-JSON 200 → ModelError (final-review)
- call_log._default_write: swallow any exception from the default file/ stdout sink and report to stderr, so a log-write failure (disk full, bad permissions, misconfigured CALL_LOG_PATH) never turns a successful narration into a 500 (charter §13). Injected write= sinks (used by tests) are left to surface their own errors. - ollama_client.chat: catch ValueError alongside httpx.HTTPError in the retry loop so a 200 response with a non-JSON body (JSONDecodeError is a ValueError subclass) counts as a failed attempt and falls through to ModelError after the one retry, instead of escaping chat() uncaught (charter §12 — one retry, then ModelError, nothing else escapes). - main.py: update stale docstrings — /dm/narrate is now fully wired (routing + model call + logging); the other four roles remain stubs. Regression tests added for both fixes (TDD: watched RED, then GREEN). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,3 +32,25 @@ def test_failure_record_shape():
|
||||
assert parsed["ok"] is False
|
||||
assert parsed["error"] == "ModelError: down"
|
||||
assert "response" not in parsed
|
||||
|
||||
|
||||
def test_default_write_failure_does_not_propagate(monkeypatch, capsys):
|
||||
"""§13 — a good narration must never become a 500 because the log write
|
||||
failed. Point CALL_LOG_PATH at a path whose parent directory does not
|
||||
exist, so the default sink's `open()` raises. record() must swallow it,
|
||||
report to stderr (never stdout — stdout may be the log sink itself), and
|
||||
still return the record.
|
||||
"""
|
||||
monkeypatch.setenv(
|
||||
"CALL_LOG_PATH", "/nonexistent-dir-for-test/does/not/exist.log"
|
||||
)
|
||||
rec = record(
|
||||
role="narrator", model="qwen3.5:latest", options={"seed": 7},
|
||||
messages=[{"role": "user", "content": "x"}], canon_log={"a": 1},
|
||||
ok=True, latency_ms=120, response="prose",
|
||||
)
|
||||
assert rec["response"] == "prose"
|
||||
assert rec["ok"] is True
|
||||
out, err = capsys.readouterr()
|
||||
assert out == ""
|
||||
assert err != ""
|
||||
|
||||
@@ -81,3 +81,15 @@ def test_empty_content_retried_then_modelerror():
|
||||
|
||||
with pytest.raises(ModelError):
|
||||
chat("m", [{"role": "user", "content": "x"}], {}, client=_client(handler))
|
||||
|
||||
|
||||
def test_non_json_200_retried_then_modelerror():
|
||||
"""A 200 response with a non-JSON body (e.g. a proxy error page) must
|
||||
degrade to ModelError after the one retry, not escape as a bare
|
||||
JSONDecodeError/ValueError (charter §12/§13).
|
||||
"""
|
||||
def handler(request):
|
||||
return httpx.Response(200, text="<html>not json</html>")
|
||||
|
||||
with pytest.raises(ModelError):
|
||||
chat("m", [{"role": "user", "content": "x"}], {}, client=_client(handler))
|
||||
|
||||
Reference in New Issue
Block a user