feat(#19): ephemeral-template (Echo) session creation

create_session could only mint foundational sessions; an ephemeral template
(agent_id="echo") returned 422 ephemeral_requires_config because ratatoskr never
sent the required config block — Echo was uncreatable, surfacing as an opaque
session_api_failed at the CLI. Thread an opaque, role/model-agnostic config
passthrough through the create path so Echo sessions are creatable.

- sessions.py: create_session(config=...) verbatim passthrough (PRE-004 Mapping /
  PRE-005 config-xor-bifrost guards); SessionInfo gains kind + config, captured
  defensively (.get) on both create and list.
- cli.py: --system-prompt flag builds config={"system_prompt": ...} (validation:
  non-empty, requires --new+--agent, xor bifrost); _amain surfaces kind=; the
  --whoami renderer now reads allowed_roles/default_role (was reading the dead
  allowed_models/default_model) and tolerates a malformed capabilities shape.
- contract #2 amended (Amendment 2026-07-18); Heid-panel contract-reviewed +
  diff-scoped bug-hunted (one whoami null-join gap found + fixed).

Canonical grounding: config.role, never config.model (worldtree-dev althing
01KXT976NN91DRBZBPXNZ2BVZR; ADR-0012 role cutover). Verified end-to-end against
the live v0.16.2 target. TDD across create + CLI; full suite green (534).

Closes #19.
This commit is contained in:
2026-07-18 12:02:18 -07:00
parent 5d06a274bf
commit c7016f23a6
7 changed files with 481 additions and 10 deletions
+113 -3
View File
@@ -281,6 +281,45 @@ class TestParseArgs:
)
assert args.end_user_id == "from-flag"
def test_system_prompt_sets_field(self) -> None:
"""system_prompt_sets_field [happy, #161]: --system-prompt → ParsedArgs.system_prompt."""
args = _parse_args(
["--send", "hi", "--new", "--agent", "echo",
"--system-prompt", "You are X.", "--api-key", "k"]
)
assert args.system_prompt == "You are X."
def test_system_prompt_default_none(self) -> None:
"""system_prompt_default_none [trace, #161]: omitted → None (foundational baseline)."""
args = _parse_args(["--send", "hi", "--new", "--agent", "mimir", "--api-key", "k"])
assert args.system_prompt is None
def test_system_prompt_empty_rejected(self) -> None:
"""system_prompt_empty_rejected [adversarial, #161]: '' → UsageError."""
with pytest.raises(UsageError, match="--system-prompt"):
_parse_args(
["--send", "hi", "--new", "--agent", "echo",
"--system-prompt", "", "--api-key", "k"]
)
def test_system_prompt_requires_new(self) -> None:
"""system_prompt_requires_new [adversarial, #161]: with --session → UsageError."""
with pytest.raises(UsageError, match="--system-prompt"):
_parse_args(
["--send", "hi", "--session", "s-1",
"--system-prompt", "You are X.", "--api-key", "k"]
)
def test_system_prompt_xor_bifrost(self) -> None:
"""system_prompt_xor_bifrost [adversarial, #161]: config + bifrost → UsageError."""
with pytest.raises(UsageError, match="mutually exclusive"):
_parse_args(
["--send", "hi", "--new", "--agent", "echo",
"--system-prompt", "You are X.",
"--bifrost-plane", "memory", "--bifrost-host", "h.example",
"--api-key", "k"]
)
SID = SseId(42, 5)
@@ -1225,6 +1264,45 @@ class TestAmain:
assert "[done]" in err
assert err.index(". create_session:") < err.index("[done]")
@respx.mock
async def test_ephemeral_create_sends_config(
self, capsys: pytest.CaptureFixture[str]
) -> None:
"""ephemeral_create_sends_config [#161]: --system-prompt → config sent; kind surfaced."""
import json as _json
sessions_route = respx.post("https://w.example/sessions").mock(
return_value=httpx.Response(
201,
json={
**_CREATE_OK_RESP,
"kind": "ephemeral",
"config": {"system_prompt": "You are X.", "role": "echo"},
},
)
)
sse_body = _sse_chunk("42:1", {"type": "text", "content": "hi"}) + _sse_chunk(
"42:2", _DONE_BODY
)
respx.post("https://w.example/sessions/s-new/messages").mock(
return_value=_sse_resp(sse_body)
)
parsed = ParsedArgs(
send_content="hi",
session_id=None,
new=True,
agent_id="echo",
api_key="k",
server_url="https://w.example",
raw=False,
system_prompt="You are X.",
)
exit_code = await _amain(parsed)
assert exit_code == 0
body = _json.loads(sessions_route.calls[0].request.content)
assert body == {"agent_id": "echo", "config": {"system_prompt": "You are X."}}
assert "kind=ephemeral" in capsys.readouterr().err
@respx.mock
async def test_happy_existing_session(self, capsys: pytest.CaptureFixture[str]) -> None:
"""happy_existing_session: --session, no create POST; just SSE stream → exit 0."""
@@ -1601,8 +1679,10 @@ class TestWhoami:
json={
"ephemeral_templates": {
"echo": {
"allowed_models": ["glm5-turbo"],
"default_model": "glm5-turbo",
# Canonical post-cutover shape (worldtree-dev althing
# 2026-07-18; ADR-0012): roles, not models.
"allowed_roles": ["echo"],
"default_role": "echo",
"system_prompt_max_bytes": 32768,
}
}
@@ -1616,7 +1696,37 @@ class TestWhoami:
assert "tier: user" in out
assert "key_id: a1b2c3d4" in out
assert "ephemeral_template echo" in out
assert "glm5-turbo" in out
assert "default=echo" in out
assert "roles=[echo]" in out
@respx.mock
def test_whoami_tolerates_malformed_capabilities(
self, capsys: pytest.CaptureFixture[str]
) -> None:
"""whoami null/malformed caps → no crash; renders defensively (bug-hunt Gróa#1/#2)."""
respx.get("https://w.example/me").mock(
return_value=httpx.Response(
200, json={"user_id": "u", "scopes": [], "tier": "user"}
)
)
# allowed_roles: null (explicit) would crash `", ".join(None)`; a non-mapping
# template value would crash `spec.get(...)`. Both must degrade, not abort.
respx.get("https://w.example/capabilities").mock(
return_value=httpx.Response(
200,
json={
"ephemeral_templates": {
"echo": {"allowed_roles": None, "system_prompt_max_bytes": 32768},
"broken": None,
}
},
)
)
rc = main(["--whoami", "--api-key", "k", "--server", "https://w.example"])
assert rc == 0
out = capsys.readouterr().out
assert "roles=[]" in out
assert "broken: (malformed)" in out
@respx.mock
def test_whoami_me_auth_failure_exits_20(self, capsys: pytest.CaptureFixture[str]) -> None: