fix(#20): heid-bug-hunt fixups — probe ConnectFailed + adapter finite-PAD (slice-3)
The slice-3 heid-bug-hunt panel (3/3) caught a real regression the cutover introduced, plus a chokepoint-invariant gap: - ConnectFailed escaped both rewired CLI probes. When --set-persona-pad and --seed-first-message moved off raw httpx onto the wt adapter, transport failures changed class: the SDK normalizes any pre-response transport error to worldtree_sdk.ConnectFailed (request.py), a WorldtreeError (not ApiError), so it passed the adapter unmapped AND the probes' httpx-only except tuples → an uncaught traceback instead of the graceful [network_error] exit 21. _amain (slice-2) already handled it; the probes lagged. Fix: add ConnectFailed to both probe except tuples (mirrors _amain). Live-verified at a refused host → [network_error] exit 21. - Finite-PAD enforced only at the CLI, not the adapter chokepoint. wt.set_persona_state delegated finiteness to the caller (documented), so a direct/non-CLI caller passing nan/inf got a raw SDK ConfigurationError. Fix: assert finiteness in the adapter precondition (consistent with its other precondition asserts) so the invariant holds at the chokepoint in ratatoskr's own terms; the CLI pre-check stays for the friendly usage error. Triaged-and-declined (all correct per the panel + Heid's source-check): the deleted sessions.py exports (intended no-shim cutover, zero un-migrated importers), the session["session_id"] index (accept-known-risk, matches --new), and Regin's "web indefinite block" (refuted — the seed is asyncio.wait_for-bounded). The concurrent heid-code-review panel returned zero drift, no code change. TDD: 3 RED tests (both probes' ConnectFailed → exit 21; adapter nan/inf/-inf → AssertionError, never reaches the SDK) → GREEN. Suite 469; ruff clean; mypy no new errors.
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "ratatoskr"
|
||||
version = "0.21.11"
|
||||
version = "0.21.12"
|
||||
description = "Worldtree Conversation API debug console (web + headless CLI) — multi-pane observability"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
+12
-2
@@ -901,7 +901,12 @@ async def _set_persona_probe(args: ParsedArgs) -> int:
|
||||
f"error_code={exc.error_code!r} body={exc.body!r}\n"
|
||||
)
|
||||
return 20
|
||||
except (httpx.ConnectError, httpx.ReadTimeout, httpx.TransportError) as exc:
|
||||
except (
|
||||
httpx.ConnectError,
|
||||
httpx.ReadTimeout,
|
||||
httpx.TransportError,
|
||||
ConnectFailed, # SDK normalizes a pre-response transport failure here
|
||||
) as exc:
|
||||
sys.stderr.write(f"[network_error] {type(exc).__name__}: {exc}\n")
|
||||
return 21
|
||||
sys.stdout.write(
|
||||
@@ -968,7 +973,12 @@ async def _seed_first_message_probe(args: ParsedArgs) -> int:
|
||||
f"error_code={exc.error_code!r} body={exc.body!r}\n"
|
||||
)
|
||||
return 20
|
||||
except (httpx.ConnectError, httpx.ReadTimeout, httpx.TransportError) as exc:
|
||||
except (
|
||||
httpx.ConnectError,
|
||||
httpx.ReadTimeout,
|
||||
httpx.TransportError,
|
||||
ConnectFailed, # SDK normalizes a pre-response transport failure here
|
||||
) as exc:
|
||||
sys.stderr.write(f"[network_error] {type(exc).__name__}: {exc}\n")
|
||||
return 21
|
||||
return 0
|
||||
|
||||
+9
-3
@@ -28,6 +28,7 @@ carries the SDK's parsed `error_code`).
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
from collections.abc import AsyncGenerator, Mapping
|
||||
from typing import Any
|
||||
|
||||
@@ -367,11 +368,16 @@ async def set_persona_state(
|
||||
The adapter builds the canonical `PadState`; the SDK owns the wire wrapper
|
||||
(`{"pad": {pleasure, arousal, dominance}}`, prose-pinned #317) — ratatoskr no
|
||||
longer hand-assembles it. Resolves on 204 (→ None). Error map (INV-CUT-2): no
|
||||
route-specific row → the `SessionApiFailed` default. (A non-finite axis is the
|
||||
caller's to reject; the SDK raises `ConfigurationError` pre-HTTP and the CLI
|
||||
surface pre-validates finiteness before calling.)
|
||||
route-specific row → the `SessionApiFailed` default.
|
||||
|
||||
Finiteness is enforced HERE at the chokepoint (not only at the CLI): a
|
||||
non-finite axis would serialize to `null` and corrupt the injection, and the SDK
|
||||
raises `ConfigurationError` pre-HTTP — the precondition asserts it so any caller
|
||||
gets a clean ratatoskr-side rejection, never a leaked SDK error. (The CLI surface
|
||||
additionally pre-validates for a friendly usage error.)
|
||||
"""
|
||||
assert session_id and isinstance(session_id, str)
|
||||
assert all(math.isfinite(v) for v in (pleasure, arousal, dominance))
|
||||
try:
|
||||
await client.sessions.set_persona_state(
|
||||
session_id, PadState(pleasure=pleasure, arousal=arousal, dominance=dominance)
|
||||
|
||||
@@ -1950,6 +1950,20 @@ class TestTier2Probes:
|
||||
)
|
||||
assert rc == 10
|
||||
|
||||
@respx.mock
|
||||
def test_set_persona_probe_connect_failed(self, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""connect_failed [error-path]: a transport failure the SDK normalizes to
|
||||
ConnectFailed → graceful [network_error], exit 21 (not an uncaught crash)."""
|
||||
respx.post("https://w.example/sessions/s1/persona_state").mock(
|
||||
side_effect=httpx.ConnectError("refused")
|
||||
)
|
||||
rc = main(
|
||||
["--set-persona-pad", "0.4,0.1,-0.2", "--session", "s1",
|
||||
"--api-key", "k", "--server", "https://w.example"]
|
||||
)
|
||||
assert rc == 21
|
||||
assert "[network_error]" in capsys.readouterr().err
|
||||
|
||||
|
||||
class TestSeedFirstMessageProbe:
|
||||
"""--seed-first-message one-shot (#347 authored-history-write reference-consumer probe)."""
|
||||
@@ -2066,3 +2080,17 @@ class TestSeedFirstMessageProbe:
|
||||
assert rc == 0
|
||||
assert "feature-absent" in capsys.readouterr().out
|
||||
assert msgs_route.call_count == 0 # never capability-probes past the 404
|
||||
|
||||
@respx.mock
|
||||
def test_seed_probe_connect_failed(self, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""connect_failed [error-path]: a transport failure on create that the SDK
|
||||
normalizes to ConnectFailed → graceful [network_error], exit 21."""
|
||||
respx.post("https://w.example/sessions").mock(
|
||||
side_effect=httpx.ConnectError("refused")
|
||||
)
|
||||
rc = main(
|
||||
["--seed-first-message", "hello", "--agent", "mimir",
|
||||
"--api-key", "k", "--server", "https://w.example"]
|
||||
)
|
||||
assert rc == 21
|
||||
assert "[network_error]" in capsys.readouterr().err
|
||||
|
||||
@@ -482,6 +482,16 @@ class TestSetPersonaState:
|
||||
await set_persona_state(_wt(fake), "s", pleasure=0.0, arousal=0.0, dominance=0.0)
|
||||
assert ei.value.status == 500
|
||||
|
||||
async def test_non_finite_pad_rejected_at_the_chokepoint(self) -> None:
|
||||
# The finite-PAD invariant is enforced at the adapter (not only the CLI): a
|
||||
# non-finite axis would serialize to null and corrupt the injection, so a
|
||||
# direct caller is rejected pre-SDK — never a leaked SDK ConfigurationError.
|
||||
fake = _FakeSessions(result=None)
|
||||
for bad in (float("nan"), float("inf"), float("-inf")):
|
||||
with pytest.raises(AssertionError):
|
||||
await set_persona_state(_wt(fake), "s", pleasure=bad, arousal=0.0, dominance=0.0)
|
||||
assert fake.calls == [] # never reached the SDK
|
||||
|
||||
|
||||
class TestWriteAuthoredHistory:
|
||||
"""slice-3: write_authored_history → SDK sessions.write_history. Builds the
|
||||
|
||||
Reference in New Issue
Block a user