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:
+113
-3
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user