477d98f52e
Panel (Gróa+Hulda+Regin, 5/5/5, no false positives) confirmed two 3/3 crash
sites where open-world dict reads violate the declared "degrade, never crash the
presenter" invariant — the wt adapter tests + the live smoke used full server
dicts, so partial/drifted wire responses were never exercised:
- FIX (tier3.py _run_define/_run_patch): the CLI hard-indexed the open-world
define/patch dicts (`info["agent_id"]` / `["role"]` / `["agent_name"]`), so a
partial 2xx → KeyError escaping main()'s exit matrix as a raw traceback (exit 1);
and `make_description(info.get("system_prompt", ""))` fed None to .splitlines()
on a present-but-null field → AttributeError. Now reads via `_str_field` (absent/
null/non-str → default), degrades role to '?', indexes only a well-formed identity,
and maps a no-usable-agent_id 2xx to [api_failed] exit 20 (controlled, not a crash).
- FIX (web/server.py _agents_endpoint): the upstream dedup hard-indexed each item
(`{a["agent_id"] for a in upstream}` + `_as_dict`), so a malformed item (`[{}]`,
`["str"]`, `{"name":…}`, non-str agent_id) or a non-list envelope → 500 before the
local fallback merged. Now filters to well-formed mappings first; a non-list
upstream degrades to the local-only list.
- FIX (wt.py _error_field_from_body): type-check the parsed `field` is a str (the
exception surface is `field: str | None`, the CLI prints it) — restores the retired
hand-rolled `_extract_error_field` isinstance guard.
Held (triaged, no change): the 429→Tier3QuotaExceeded / bare-404→Tier3AgentNotFound
maps are ungated-by-error_code BY CONTRACT DESIGN (§ Error map route+status rows; the
SDK's ApiError floor drops Retry-After, so retry_after=0 is canonical) — the arms
flagged them spec-free; Heid's source-check confirmed intended. Dual-keying define's
429 for full row consistency is an available tightening (contract amendment), surfaced
not applied. The persona-endpoint SessionApiFailed gap the arms also caught was
already closed in the prior code-review fixup (aed9429).
Suite 475 green (+5).
287 lines
11 KiB
Python
287 lines
11 KiB
Python
"""CLI integration tests for `python -m ratatoskr.tier3`.
|
|
|
|
The define/patch/delete wire calls route through the worldtree-sdk adapter
|
|
(`ratatoskr.wt`); the adapter's body-building + error-mapping are unit-tested in
|
|
`test_wt.py` (against a fake `client.agents`). These tests exercise the CLI
|
|
end-to-end — argv → the real SDK over a respx-mocked HTTP layer → exit code +
|
|
stdout + the local tier3 index side effects.
|
|
|
|
The define/patch response echoes `role` (Worldtree spec 1.2 / b128), read off the
|
|
SDK's open-world dict; `LocalAgentEntry.role` is the v2-schema field.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
import pytest
|
|
import respx
|
|
|
|
from ratatoskr.tier3 import main
|
|
|
|
# The consumer-agent response echoes `role` (b128), not the former `model`.
|
|
_FULL_AGENT_RESP = {
|
|
"agent_id": "ratatoskr:wizard",
|
|
"user_id": "ratatoskr",
|
|
"agent_name": "wizard",
|
|
"system_prompt": "You are a wizard.",
|
|
"role": "qwen3.6-35-a3b",
|
|
"created_at": "2026-05-25T03:20:09.703601+00:00",
|
|
"updated_at": "2026-05-25T03:20:09.703601+00:00",
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def _isolated_local_agents(
|
|
tmp_path: "Path", monkeypatch: pytest.MonkeyPatch
|
|
) -> "Path":
|
|
"""Isolate the v0.8.0 local-tier-3 index from the operator's real file."""
|
|
path = tmp_path / "local_agents.json"
|
|
monkeypatch.setenv("RATATOSKR_LOCAL_AGENTS", str(path))
|
|
return path
|
|
|
|
|
|
class TestCli:
|
|
@respx.mock
|
|
def test_cli_define_happy(
|
|
self,
|
|
capsys: pytest.CaptureFixture[str],
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
_isolated_local_agents: "Path",
|
|
) -> None:
|
|
"""cli_define_happy [happy,tracer]: argv → 201 mock → stdout confirmation;
|
|
local index updated with the new entry (role echoed).
|
|
"""
|
|
from ratatoskr.local_agents import load_local_agents
|
|
|
|
monkeypatch.setenv("WORLDTREE_API_URL", "https://w.example")
|
|
monkeypatch.setenv("WORLDTREE_API_KEY", "k")
|
|
respx.post("https://w.example/agents/define").mock(
|
|
return_value=httpx.Response(201, json=_FULL_AGENT_RESP)
|
|
)
|
|
rc = main([
|
|
"define",
|
|
"--name", "wizard",
|
|
"--system-prompt", "You are a wizard.",
|
|
"--role", "qwen3.6-35-a3b",
|
|
])
|
|
out = capsys.readouterr()
|
|
assert rc == 0
|
|
assert out.out.strip() == "defined ratatoskr:wizard (qwen3.6-35-a3b)"
|
|
entries = load_local_agents()
|
|
assert len(entries) == 1
|
|
assert entries[0].agent_id == "ratatoskr:wizard"
|
|
assert entries[0].role == "qwen3.6-35-a3b"
|
|
|
|
@respx.mock
|
|
def test_cli_define_body_shape(
|
|
self, monkeypatch: pytest.MonkeyPatch, _isolated_local_agents: "Path"
|
|
) -> None:
|
|
"""cli_define_body_shape [trace]: outbound JSON is exactly the three keys
|
|
(the adapter sends AgentDefineInput, no layer fields)."""
|
|
import json as _json
|
|
|
|
monkeypatch.setenv("WORLDTREE_API_URL", "https://w.example")
|
|
monkeypatch.setenv("WORLDTREE_API_KEY", "k")
|
|
route = respx.post("https://w.example/agents/define").mock(
|
|
return_value=httpx.Response(201, json=_FULL_AGENT_RESP)
|
|
)
|
|
main([
|
|
"define", "--name", "wizard",
|
|
"--system-prompt", "You are a wizard.", "--role", "qwen3.6-35-a3b",
|
|
])
|
|
body = _json.loads(route.calls[0].request.content)
|
|
assert body == {
|
|
"agent_name": "wizard",
|
|
"role": "qwen3.6-35-a3b",
|
|
"system_prompt": "You are a wizard.",
|
|
}
|
|
|
|
@respx.mock
|
|
def test_cli_patch_happy(
|
|
self,
|
|
capsys: pytest.CaptureFixture[str],
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
_isolated_local_agents: "Path",
|
|
) -> None:
|
|
"""cli_patch_happy [happy]: argv → 200 mock → stdout confirmation;
|
|
local index refreshed with the post-patch state.
|
|
"""
|
|
from ratatoskr.local_agents import load_local_agents
|
|
|
|
monkeypatch.setenv("WORLDTREE_API_URL", "https://w.example")
|
|
monkeypatch.setenv("WORLDTREE_API_KEY", "k")
|
|
respx.patch("https://w.example/agents/ratatoskr:wizard").mock(
|
|
return_value=httpx.Response(200, json=_FULL_AGENT_RESP)
|
|
)
|
|
rc = main(["patch", "ratatoskr:wizard", "--system-prompt", "new"])
|
|
out = capsys.readouterr()
|
|
assert rc == 0
|
|
assert out.out.strip() == "patched ratatoskr:wizard"
|
|
entries = load_local_agents()
|
|
assert len(entries) == 1
|
|
assert entries[0].agent_id == "ratatoskr:wizard"
|
|
assert entries[0].role == "qwen3.6-35-a3b"
|
|
|
|
@respx.mock
|
|
def test_cli_delete_happy(
|
|
self,
|
|
capsys: pytest.CaptureFixture[str],
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
_isolated_local_agents: "Path",
|
|
) -> None:
|
|
"""cli_delete_happy [happy]: argv → 204 mock → stdout confirmation;
|
|
local index entry removed (v0.8.0 hook).
|
|
"""
|
|
from ratatoskr.local_agents import (
|
|
LocalAgentEntry,
|
|
add_local_agent,
|
|
load_local_agents,
|
|
)
|
|
|
|
# Pre-populate so we can verify removal (v2 schema: role, not model).
|
|
add_local_agent(LocalAgentEntry(
|
|
agent_id="ratatoskr:wizard",
|
|
agent_name="wizard",
|
|
role="m",
|
|
description="d",
|
|
defined_at="t",
|
|
))
|
|
monkeypatch.setenv("WORLDTREE_API_URL", "https://w.example")
|
|
monkeypatch.setenv("WORLDTREE_API_KEY", "k")
|
|
respx.delete("https://w.example/agents/ratatoskr:wizard").mock(
|
|
return_value=httpx.Response(204)
|
|
)
|
|
rc = main(["delete", "ratatoskr:wizard"])
|
|
out = capsys.readouterr()
|
|
assert rc == 0
|
|
assert out.out.strip() == "deleted ratatoskr:wizard"
|
|
assert load_local_agents() == []
|
|
|
|
def test_cli_missing_auth(
|
|
self, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""cli_missing_auth [error]: no api-key → stderr [auth_error] + exit 11."""
|
|
monkeypatch.delenv("WORLDTREE_API_KEY", raising=False)
|
|
rc = main([
|
|
"define", "--name", "wizard", "--system-prompt", "x", "--role", "m",
|
|
])
|
|
err = capsys.readouterr().err
|
|
assert rc == 11
|
|
assert "[auth_error]" in err
|
|
|
|
@respx.mock
|
|
def test_cli_api_failed(
|
|
self, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""cli_api_failed [error]: 500 → stderr [api_failed] + exit 20."""
|
|
monkeypatch.setenv("WORLDTREE_API_URL", "https://w.example")
|
|
monkeypatch.setenv("WORLDTREE_API_KEY", "k")
|
|
respx.post("https://w.example/agents/define").mock(
|
|
return_value=httpx.Response(500, content=b"upstream out")
|
|
)
|
|
rc = main([
|
|
"define", "--name", "wizard", "--system-prompt", "x", "--role", "m",
|
|
])
|
|
err = capsys.readouterr().err
|
|
assert rc == 20
|
|
assert "[api_failed]" in err
|
|
|
|
@respx.mock
|
|
def test_cli_quota_exceeded(
|
|
self, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""cli_quota_exceeded [error]: 429 → stderr [quota_exceeded] + exit 20."""
|
|
monkeypatch.setenv("WORLDTREE_API_URL", "https://w.example")
|
|
monkeypatch.setenv("WORLDTREE_API_KEY", "k")
|
|
respx.post("https://w.example/agents/define").mock(
|
|
return_value=httpx.Response(
|
|
429,
|
|
headers={"Retry-After": "0"},
|
|
json={"detail": {"error_code": "agent_quota_exceeded"}},
|
|
)
|
|
)
|
|
rc = main([
|
|
"define", "--name", "wizard", "--system-prompt", "x", "--role", "m",
|
|
])
|
|
err = capsys.readouterr().err
|
|
assert rc == 20
|
|
assert "[quota_exceeded]" in err
|
|
|
|
@respx.mock
|
|
def test_cli_network_error(
|
|
self, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""cli_network_error [error]: a transport failure surfaces as the SDK's
|
|
ConnectFailed (not a raw httpx error) → stderr [network_error] + exit 21."""
|
|
monkeypatch.setenv("WORLDTREE_API_URL", "https://w.example")
|
|
monkeypatch.setenv("WORLDTREE_API_KEY", "k")
|
|
respx.post("https://w.example/agents/define").mock(
|
|
side_effect=httpx.ConnectError("refused")
|
|
)
|
|
rc = main([
|
|
"define", "--name", "wizard", "--system-prompt", "x", "--role", "m",
|
|
])
|
|
err = capsys.readouterr().err
|
|
assert rc == 21
|
|
assert "[network_error]" in err
|
|
|
|
def test_cli_patch_no_fields(
|
|
self, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""cli_patch_no_fields [error]: patch with no flags → [usage_error] + exit 10."""
|
|
monkeypatch.setenv("WORLDTREE_API_URL", "https://w.example")
|
|
monkeypatch.setenv("WORLDTREE_API_KEY", "k")
|
|
rc = main(["patch", "ratatoskr:wizard"])
|
|
err = capsys.readouterr().err
|
|
assert rc == 10
|
|
assert "[usage_error]" in err
|
|
|
|
@respx.mock
|
|
def test_cli_define_partial_response_degrades(
|
|
self,
|
|
capsys: pytest.CaptureFixture[str],
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
_isolated_local_agents: "Path",
|
|
) -> None:
|
|
"""cli_define_partial [error]: a 201 missing `role` + a NULL system_prompt
|
|
degrades (role '?', description-safe) and exits 0 — never a KeyError/
|
|
AttributeError traceback (heid-bug-hunt open-world invariant)."""
|
|
from ratatoskr.local_agents import load_local_agents
|
|
|
|
monkeypatch.setenv("WORLDTREE_API_URL", "https://w.example")
|
|
monkeypatch.setenv("WORLDTREE_API_KEY", "k")
|
|
respx.post("https://w.example/agents/define").mock(
|
|
return_value=httpx.Response(201, json={
|
|
"agent_id": "ratatoskr:wizard", "agent_name": "wizard",
|
|
"system_prompt": None, # present-but-null: .get(...,'') would NOT default
|
|
})
|
|
)
|
|
rc = main([
|
|
"define", "--name", "wizard", "--system-prompt", "x", "--role", "m",
|
|
])
|
|
out = capsys.readouterr()
|
|
assert rc == 0
|
|
assert out.out.strip() == "defined ratatoskr:wizard (?)"
|
|
# Well-formed identity → still indexed (role degraded to '?').
|
|
entries = load_local_agents()
|
|
assert len(entries) == 1
|
|
assert entries[0].role == "?"
|
|
|
|
@respx.mock
|
|
def test_cli_define_no_agent_id_is_api_failure(
|
|
self, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""cli_define_no_agent_id [error]: a 2xx with no usable agent_id → [api_failed]
|
|
+ exit 20 (controlled), not an uncaught traceback."""
|
|
monkeypatch.setenv("WORLDTREE_API_URL", "https://w.example")
|
|
monkeypatch.setenv("WORLDTREE_API_KEY", "k")
|
|
respx.post("https://w.example/agents/define").mock(
|
|
return_value=httpx.Response(201, json={"role": "m"}) # no agent_id
|
|
)
|
|
rc = main([
|
|
"define", "--name", "wizard", "--system-prompt", "x", "--role", "m",
|
|
])
|
|
err = capsys.readouterr().err
|
|
assert rc == 20
|
|
assert "[api_failed]" in err
|