fix(#20): heid-bug-hunt fixups — cutover edge-path robustness (slice-2)
Triaged the heid-bug-hunt panel (Gróa 8 / Hulda 6 / Regin 6; Heid source-checked + refuted 2 Regin FPs). The lens pulled real weight — confirmed bugs the conformance review structurally could not see. Confirmed bugs fixed: - SessionRetired (410) stream-open maps to wt.SessionApiFailed, but neither cli _run_turn nor web gen() caught it → crash / dropped SSE stream. Both presenters now catch it (cli → exit 20; web → labeled `event: error`). (Gróa#2) + cli regression test. - cli forwarded consumer_key unconditionally; an UNBOUND create with the env key set would auth as the Bifrost consumer, not the default bearer. Guarded in the adapter (consumer_key only when bifrost is set). (Gróa#4 + Regin#4) + test. - cli _turn_id_from_sse_id crashed on a None/non-str sse_id (web guarded, cli didn't) → now tolerant. (Gróa#1 + Hulda#2) + test. - _cancel_and_log broadened to `except Exception` — after the code-review's ApiError default, a cancel could raise SessionApiFailed it didn't catch, breaking INV-009 (never-raise). (Gróa#3, Heid-endorsed over Regin's refuted mechanism). Open-world degrade-not-crash (contract posture): render hardened — float duration_ms (_format_duration_safe), non-mapping usage/snapshot guards, unknown event type degrades instead of asserting (Gróa#5/#6 + Hulda#3); web _event_to_browser_payload guards a non-mapping `raw` (Hulda#4); web _wt_client bearer extraction is now case-insensitive + whitespace-robust (Hulda#5 + Regin#5). + render-degrade test. Rejected (verified): Regin#1 (httpx IS caught), Regin#2 (wtsdk IS worldtree_sdk), Regin#3 (sse_client.AgentNotAvailable IS caught by SseConnectFailed) — all FPs; Hulda#1 (deleted funcs "break callers") — grep-verified zero callers pre-deletion. Accepted-known-risk: lenient sse_id parse, CancelFailed status=0, async-gen aclose (pre-existing pattern, not a cutover regression). Suite 497 green; wt/cli/web ruff + wt mypy clean. Patch.
This commit is contained in:
@@ -647,6 +647,39 @@ class TestCliPresenterState:
|
||||
state.render(_make_done(duration_ms=72000), stdout=io.StringIO(), stderr=stderr)
|
||||
assert "duration=1.2m" in stderr.getvalue()
|
||||
|
||||
def test_render_degrades_on_malformed_open_world_fields(self) -> None:
|
||||
"""Open-world hardening (heid-bug-hunt Gróa#5 / Hulda#3): a DoneEvent with a
|
||||
float duration_ms + a non-mapping usage, and an AffectUpdate with a non-mapping
|
||||
snapshot, DEGRADE rather than crash the presenter."""
|
||||
from ratatoskr.cli import CliPresenterState
|
||||
|
||||
stderr = io.StringIO()
|
||||
state = CliPresenterState()
|
||||
done = build_event(
|
||||
"done", "42:9", 42,
|
||||
{"type": "done", "duration_ms": 1234.0, "usage": 5, "model": "m"},
|
||||
)
|
||||
state.render(done, stdout=io.StringIO(), stderr=stderr) # must not raise
|
||||
out = stderr.getvalue()
|
||||
# float duration floored to int (1234ms → "1.2s"); non-mapping usage → "(n/a)".
|
||||
assert "[done]" in out and "duration=1.2s" in out and "usage (n/a)" in out
|
||||
# AffectUpdate with a list snapshot → no AttributeError on .get.
|
||||
affect = build_event(
|
||||
"affect_update", "42:1", 42,
|
||||
{"type": "affect_update", "status": "current", "snapshot": []},
|
||||
)
|
||||
CliPresenterState().render(affect, stdout=io.StringIO(), stderr=io.StringIO())
|
||||
|
||||
def test_turn_id_from_sse_id_tolerates_non_str(self) -> None:
|
||||
"""Open-world hardening (heid-bug-hunt Gróa#1 / Hulda#2): a None/non-str sse_id
|
||||
yields None instead of crashing on .partition."""
|
||||
from ratatoskr.cli import _turn_id_from_sse_id
|
||||
|
||||
assert _turn_id_from_sse_id(None) is None
|
||||
assert _turn_id_from_sse_id(42) is None
|
||||
assert _turn_id_from_sse_id("42:1") == 42
|
||||
assert _turn_id_from_sse_id("0:1") is None
|
||||
|
||||
def test_usage_format_ascii_arrow(self) -> None:
|
||||
"""usage_format_ascii_arrow [trace]: stderr label contains the natural-language
|
||||
usage shape with ASCII arrow (-> not →) for CLI scriptability.
|
||||
@@ -940,6 +973,21 @@ class TestRunTurn:
|
||||
assert "[sse_connect_failed]" in out
|
||||
assert "status=404" in out
|
||||
|
||||
@respx.mock
|
||||
async def test_session_retired_410_maps_to_session_api_failed(self) -> None:
|
||||
"""session_retired [error]: 410 stream-open → SessionRetired → SessionApiFailed
|
||||
→ exit 20. Without the presenter catch this crashed _run_turn (heid-bug-hunt Gróa#2)."""
|
||||
respx.post("https://w.example/sessions/s-1/messages").mock(
|
||||
return_value=httpx.Response(410, json={"error_code": "session_retired"})
|
||||
)
|
||||
sigint = asyncio.Event()
|
||||
stdout, stderr = io.StringIO(), io.StringIO()
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as _tp:
|
||||
client = _wtc(_tp)
|
||||
exit_code = await _run_turn(client, "s-1", "hi", sigint, stdout=stdout, stderr=stderr)
|
||||
assert exit_code == 20
|
||||
assert "[session_api_failed]" in stderr.getvalue()
|
||||
|
||||
@respx.mock
|
||||
async def test_connection_dropped(self) -> None:
|
||||
"""connection_dropped [error]: RemoteProtocolError mid-stream → exit 21."""
|
||||
|
||||
@@ -218,6 +218,14 @@ class TestCreateSession:
|
||||
# INV-CUT: the consumer key rides the SDK's per-request auth, NOT a header.
|
||||
assert kwargs["consumer_key"] == "ck-real"
|
||||
|
||||
async def test_unbound_create_drops_consumer_key(self) -> None:
|
||||
# A consumer_key must NOT reach the SDK on an UNBOUND create — the SDK's
|
||||
# credential precedence would otherwise auth as the Bifrost consumer instead
|
||||
# of the default bearer (heid-bug-hunt Gróa#4 / Regin#4).
|
||||
fake = _FakeSessions(result={"session_id": "s"})
|
||||
await create_session(_wt(fake), "mimir", consumer_key="ck-should-be-dropped")
|
||||
assert fake.calls[-1][2]["consumer_key"] is None
|
||||
|
||||
async def test_bifrost_without_consumer_key_rejected_pre_http(self) -> None:
|
||||
fake = _FakeSessions(result={"session_id": "s"})
|
||||
binding = BifrostBinding(endpoint_url="http://h:8391", scope=None)
|
||||
|
||||
Reference in New Issue
Block a user