feat(#2): Tier-2 — transient characters + persona-state write; audit converges

v1 coverage-audit: the last in-scope client I/O points. The audit now
CONVERGES — REST 17/40 covered with zero in-scope gaps (23 excluded-by-
design), SSE 11/11, Bifrost planes 8/8.

- sessions.py: list_character_models / create_character / get_character_state
  / delete_character (#161, character.read/write) + set_persona_state
  (POST /sessions/{id}/persona_state — freeform body, unpinned in the
  frozen surface). 200/201 -> dict (or None on 204), off-status ->
  SessionApiFailed.
- cli.py: two one-shot probes (mirror --whoami): --characters (CRUD
  lifecycle report) + --set-persona-pad "p,a,d" (requires --session).
  New ParsedArgs.characters/set_persona_pad + probe mutual-exclusion.
- Contract #2 amended (5 FNs) + validated. TDD: 7 wrapper + 5 cli tests.
  Suite 573 green; touched code ruff-clean.
- Char read side live-proven (GET /models/available-for-characters -> 200).

Coverage-map: convergence frontier CLOSED — scope-A "done" (every frozen
I/O point classified) is met; ratatoskr cuts v1 when Worldtree tags 1.0.
This commit is contained in:
vh
2026-06-30 23:57:09 -07:00
parent 5fbe353836
commit af07a2329a
9 changed files with 427 additions and 21 deletions
+60
View File
@@ -1633,3 +1633,63 @@ class TestWhoami:
rc = main(["--whoami", "--api-key", "k", "--server", "https://w.example"])
assert rc == 20
assert "[session_api_failed]" in capsys.readouterr().err
class TestTier2Probes:
"""--characters + --set-persona-pad one-shot probes (Tier-2: #161 + persona_state-write)."""
def test_characters_standalone_accepted(self) -> None:
"""characters_standalone: --characters alone → valid."""
args = _parse_args(["--characters", "--api-key", "k"])
assert args.characters is True
assert args.session_id is None
def test_set_persona_requires_session(self) -> None:
"""set_persona_requires_session [adversarial]: --set-persona-pad needs --session."""
with pytest.raises(UsageError, match="requires --session"):
_parse_args(["--set-persona-pad", "0.4,0.1,-0.2", "--api-key", "k"])
def test_probes_mutually_exclusive(self) -> None:
"""probes_mutually_exclusive [adversarial]: --whoami + --characters → UsageError."""
with pytest.raises(UsageError, match="mutually exclusive"):
_parse_args(["--whoami", "--characters", "--api-key", "k"])
@respx.mock
def test_characters_probe_lifecycle(self, capsys: pytest.CaptureFixture[str]) -> None:
"""characters_probe [happy,tracer]: models → create → state → delete; report to stdout."""
respx.get("https://w.example/models/available-for-characters").mock(
return_value=httpx.Response(200, json={"items": [{"name": "fast"}]})
)
respx.post("https://w.example/characters").mock(
return_value=httpx.Response(201, json={"character_id": "char_z", "ttl_expires_at": "t"})
)
respx.get("https://w.example/characters/char_z/state").mock(
return_value=httpx.Response(200, json={"schema_version": "1", "pad": [0.1, 0.2, 0.3]})
)
del_route = respx.delete("https://w.example/characters/char_z").mock(
return_value=httpx.Response(204)
)
rc = main(["--characters", "--api-key", "k", "--server", "https://w.example"])
assert rc == 0
out = capsys.readouterr().out
assert "character models: fast" in out
assert "created: char_z" in out
assert "pad=[0.1, 0.2, 0.3]" in out
assert "deleted: char_z" in out
assert del_route.call_count == 1 # lifecycle cleaned up
@respx.mock
def test_set_persona_probe(self, capsys: pytest.CaptureFixture[str]) -> None:
"""set_persona_probe [happy,tracer]: POST pad to /sessions/{id}/persona_state; 204."""
import json as _json
route = respx.post("https://w.example/sessions/s1/persona_state").mock(
return_value=httpx.Response(204)
)
rc = main(
["--set-persona-pad", "0.4,0.1,-0.2", "--session", "s1",
"--api-key", "k", "--server", "https://w.example"]
)
assert rc == 0
assert "persona_state set" in capsys.readouterr().out
assert _json.loads(route.calls[0].request.content) == {"pad": [0.4, 0.1, -0.2]}