fix: typed ChatNotFoundError replaces string-prefix sniff in skip routes (T81)

This commit is contained in:
Joseph Doherty
2026-04-26 21:55:53 -04:00
parent 6f0716201f
commit f816d44438
4 changed files with 82 additions and 19 deletions
+37
View File
@@ -273,6 +273,43 @@ def test_post_skip_elision_advances_clock_and_emits_narration(client, tmp_path):
assert tp["speaker_id"] == "bot_a"
def test_skip_route_404_via_typed_exception_class(client, tmp_path):
"""T81: drawer skip routes 404 via :class:`ChatNotFoundError`.
Pre-T81, the route caught ``ValueError`` and recovered the 404 case
by sniffing ``str(exc).startswith("chat not found")`` — fragile if
the message ever changed wording. The controller now raises a typed
exception so the route dispatches on type. Asserting the 404 from
the unseeded chat exercises the typed branch end-to-end; importing
the class confirms it's a real subclass of ``Exception`` and not a
re-export of ``ValueError`` (which would defeat the type split).
"""
# Don't seed any chat — the controller hits ``get_chat`` returning
# ``None`` and raises ``ChatNotFoundError``. The drawer route then
# maps that to ``404`` via the typed handler (no string sniff).
_override_llm([])
try:
response = client.post(
"/chats/nonexistent/drawer/skip/elision",
data={
"landing_state_hint": "x",
"new_time": "2026-04-26T20:30:00+00:00",
},
)
assert response.status_code == 404
finally:
app.dependency_overrides.clear()
# The exception class itself is importable, distinct from ValueError,
# and a proper Exception subclass — pinning the type-based dispatch
# so future refactors can't quietly collapse it back to a string sniff.
from chat.web.skip import ChatNotFoundError
assert ChatNotFoundError is not None
assert issubclass(ChatNotFoundError, Exception)
assert not issubclass(ChatNotFoundError, ValueError)
def test_post_skip_elision_invalid_time_returns_400(client, tmp_path):
_seed_chat(tmp_path / "test.db")
_override_llm([])