Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d86d6df147 |
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "ratatoskr"
|
||||
version = "0.21.16"
|
||||
version = "0.21.17"
|
||||
description = "Worldtree Conversation API debug console (web + headless CLI) — multi-pane observability"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
+15
-5
@@ -750,7 +750,11 @@ def _format_whoami(me: Mapping[str, Any], caps: Mapping[str, Any]) -> str:
|
||||
lines = ["identity:"]
|
||||
lines.append(f" user_id: {me.get('user_id', '?')}")
|
||||
lines.append(f" tier: {me.get('tier', '?')}")
|
||||
lines.append(f" scopes: {', '.join(me.get('scopes', [])) or '(none)'}")
|
||||
# Open-world read: `scopes` may be absent, present-null, or carry non-strings —
|
||||
# `or []` + str() degrades all three (a present-null `.get('scopes', [])` returns
|
||||
# None, not the default), matching the `allowed_roles` hardening below (heid-code-
|
||||
# review slice-5: the contract names this function the degrade-not-crash exemplar).
|
||||
lines.append(f" scopes: {', '.join(str(s) for s in (me.get('scopes') or [])) or '(none)'}")
|
||||
for k in ("display_name", "key_id", "key_label"):
|
||||
if k in me:
|
||||
lines.append(f" {k}: {me[k]}")
|
||||
@@ -828,10 +832,16 @@ async def _characters_probe(args: ParsedArgs) -> int:
|
||||
client = wt.build_client(args.server_url, api_key=args.api_key, transport=transport)
|
||||
try:
|
||||
models = await wt.list_character_models(client)
|
||||
# Open-world read: `items` may be absent OR explicit-null — `or []` degrades
|
||||
# both to empty (mirrors the slice-4 `_format_whoami` hardening), never a
|
||||
# `join(None)` crash.
|
||||
names = ", ".join(m.get("name", "?") for m in (models.get("items") or []))
|
||||
# Open-world read: `items` may be absent/null (`or []`) AND each entry may be
|
||||
# a non-mapping (`[None]` / `["x"]`) or carry a non-string `name` — guard the
|
||||
# item is a dict and coerce `name` to str so a malformed catalog degrades
|
||||
# rather than crashing (heid-code-review slice-5; element-level completion of
|
||||
# the list-level `or []` guard).
|
||||
names = ", ".join(
|
||||
str(m.get("name", "?"))
|
||||
for m in (models.get("items") or [])
|
||||
if isinstance(m, dict)
|
||||
)
|
||||
sys.stdout.write(f"character models: {names or '(none)'}\n")
|
||||
created = await wt.create_character(
|
||||
client,
|
||||
|
||||
@@ -1879,6 +1879,25 @@ class TestWhoami:
|
||||
assert rc == 20
|
||||
assert "[session_api_failed]" in capsys.readouterr().err
|
||||
|
||||
@respx.mock
|
||||
def test_whoami_tolerates_null_and_nonstring_scopes(
|
||||
self, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""scopes present-null / non-string → renders '(none)' or str-coerced, never a
|
||||
`join(None)` TypeError (heid-code-review slice-5: `_format_whoami` is the
|
||||
contract's degrade-not-crash exemplar; `allowed_roles` was hardened, `scopes`
|
||||
was not)."""
|
||||
# scopes: null (present, not absent) → `.get('scopes', [])` would return None.
|
||||
respx.get("https://w.example/me").mock(
|
||||
return_value=httpx.Response(200, json={"user_id": "u", "scopes": None, "tier": "user"})
|
||||
)
|
||||
respx.get("https://w.example/capabilities").mock(
|
||||
return_value=httpx.Response(200, json={"ephemeral_templates": {}})
|
||||
)
|
||||
rc = main(["--whoami", "--api-key", "k", "--server", "https://w.example"])
|
||||
assert rc == 0
|
||||
assert "scopes: (none)" in capsys.readouterr().out
|
||||
|
||||
|
||||
class TestTier2Probes:
|
||||
"""--characters + --set-persona-pad one-shot probes (Tier-2: #161 + persona_state-write)."""
|
||||
@@ -1923,6 +1942,32 @@ class TestTier2Probes:
|
||||
assert "deleted: char_z" in out
|
||||
assert del_route.call_count == 1 # lifecycle cleaned up
|
||||
|
||||
@respx.mock
|
||||
def test_characters_probe_tolerates_malformed_models(
|
||||
self, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""models catalog with non-mapping / non-string-name items → degrades (no
|
||||
AttributeError/TypeError), lifecycle still proceeds (heid-code-review slice-5:
|
||||
element-level completion of the list-level `or []` guard)."""
|
||||
respx.get("https://w.example/models/available-for-characters").mock(
|
||||
return_value=httpx.Response(
|
||||
200, json={"items": [None, "x", {"name": 123}, {"name": "ok"}]}
|
||||
)
|
||||
)
|
||||
respx.post("https://w.example/characters").mock(
|
||||
return_value=httpx.Response(201, json={"character_id": "c1", "ttl_expires_at": "t"})
|
||||
)
|
||||
respx.get("https://w.example/characters/c1/state").mock(
|
||||
return_value=httpx.Response(200, json={"pad": [0.0, 0.0, 0.0]})
|
||||
)
|
||||
respx.delete("https://w.example/characters/c1").mock(return_value=httpx.Response(204))
|
||||
rc = main(["--characters", "--api-key", "k", "--server", "https://w.example"])
|
||||
assert rc == 0
|
||||
out = capsys.readouterr().out
|
||||
# non-mappings dropped; {"name":123}→"123", {"name":"ok"}→"ok" — no crash.
|
||||
assert "character models: 123, ok" in out
|
||||
assert "created: c1" in out
|
||||
|
||||
@respx.mock
|
||||
def test_characters_probe_create_missing_id_aborts(
|
||||
self, capsys: pytest.CaptureFixture[str]
|
||||
|
||||
Reference in New Issue
Block a user