Issue #6 (TUI startup error visibility): restructure run_tui lifecycle so pre-App.run() failures land on real stderr instead of getting eaten by the alt-screen teardown. New _resolve_then_run async helper opens the AsyncClient via async-with, does pre-flight session resolution, routes AgentNotFound / SessionApiFailed / network errors to sys.stderr (verbatim same labels + exit codes as cli._amain), then constructs RatatoskrApp with pre-resolved state and awaits app.run_async(). RatatoskrApp.__init__ signature widens to (args, *, session_id, agent_id, client) — all three required. on_mount narrows to identity-widget population; on_unmount becomes a no-op (client lifetime owned by run_tui's async-with). Issue #5 (--end-user-id for per-end-user agents): sessions.create_session gains keyword-only end_user_id kwarg with PRE-003 non-empty assertion; ParsedArgs.end_user_id field added (default None); --end-user-id flag with non-empty validation; _amain + _resolve_then_run thread it to their create_session calls. RATATOSKR_END_USER_ID env-var fallback (flag > env > None) per the post-2026-05-23 amendment; env.sh (gitignored) ships "ratatoskr-tui" as project-stable partition default. Worldtree-dev consumer-API follow-up (althing 01KSBARG2B8M): User-Agent header added (ratatoskr/<version> (vh@phasefinal.com), version pulled via importlib.metadata) to both AsyncClient constructions so server logs can distinguish ratatoskr traffic from other consumers. Volva code-review (2 rounds on #6) found 8 test-precision gaps + 1 PRE assertion drift, all Category 1 fixed: missing PRE-001 at _resolve_then_run entry; Rule separator assertions on markdown render; RichLog-write spy on empty submit; input-cleared + no-new-worker on cancelling busy; worker.cancel observation on three force-exit paths; on_unmount-no-close focused test (the prior client-lifetime test patched run_async so on_unmount was never exercised); happy --new resolve test verifying POST count + identity propagation. Issues #2/#3/#4/#5 contracts amended in-place to reflect: - create_session widened (PRE-003, body construction step, body shape POST) - ParsedArgs description + _parse_args STEPS + _amain create_session call + new TESTS for end_user_id + env-var fallback - _resolve_then_run STEPS + new TEST entries; on_mount narrowed; INV-007 amended for new client ownership - Post-#6 adjustment note on issue #5 (_resolve_then_run replaces on_mount as the threading site since #6 moved session resolution out of the alt-screen) 188 tests GREEN; ruff clean. Bumps to v0.1.0 — first minor release, the load-bearing reason is RatatoskrApp.__init__'s breaking signature change (additive end_user_id alone wouldn't have triggered a minor pre-v1.x). Files Gitea issues #9 (spec-pin refresh v0.19.0 → v0.22.1), #10 (track Worldtree #196 subject:{type,id} migration), #11 (AdminEvents pane auth prerequisite admin.events.read). Infra-ops pinged via althing for agents.call:lofn scope add (broker pattern; they forwarded to worldtree-dev because personal Worldtree exposes no public scope-mutation endpoint).
This commit is contained in:
@@ -94,11 +94,12 @@ Convention-aligned with issue #1: caller owns the `httpx.AsyncClient` and Author
|
||||
---
|
||||
|
||||
```contract
|
||||
FN create_session(client: httpx.AsyncClient, agent_id: str) -> SessionInfo
|
||||
BRIEF: POST /sessions with {"agent_id": agent_id} to create a new conversation session. Returns SessionInfo populated from the 201 response.
|
||||
FN create_session(client: httpx.AsyncClient, agent_id: str, *, end_user_id: str | None = None) -> SessionInfo
|
||||
BRIEF: POST /sessions with {"agent_id": agent_id} (and {"end_user_id": end_user_id} when non-None) to create a new conversation session. Returns SessionInfo populated from the 201 response. Per issue #5: keyword-only `end_user_id` for per-end-user agents (lofn etc.); default-None preserves the pre-#5 baseline.
|
||||
PRE: [PRE-001 hard] client is not None -- assert client is not None
|
||||
PRE: [PRE-002 hard] agent_id is a non-empty string -- assert agent_id and isinstance(agent_id, str)
|
||||
POST: [POST-001 side_effect] exactly one POST to /sessions was issued with body {"agent_id": agent_id} -- assert mock_router.calls.call_count == 1 and json.loads(req.content) == {"agent_id": agent_id}
|
||||
PRE: [PRE-003 hard, issue #5] end_user_id is None OR a non-empty string -- assert end_user_id is None or (isinstance(end_user_id, str) and end_user_id)
|
||||
POST: [POST-001 side_effect] exactly one POST to /sessions was issued; body is {"agent_id": agent_id} when end_user_id is None, OR {"agent_id": agent_id, "end_user_id": end_user_id} when non-None (issue #5 INV-002: omitting the field when None is NOT the same as sending empty)
|
||||
POST: [POST-002 return_value] returns SessionInfo with session_id, agent_id, created_at, last_active, metadata populated from response -- assert all 5 fields non-None
|
||||
POST: [POST-003 return_value] returns SessionInfo where message_count == response["message_count"] (typically 0 for a fresh session) and list-only fields carry the create-origin fixed defaults per INV-001 -- assert info.message_count is not None and info.name is None and info.archived is False and info.tags == []
|
||||
ERROR_ROUTING:
|
||||
@@ -109,19 +110,20 @@ ERROR_ROUTING:
|
||||
HTTP 422 validation_failed:
|
||||
local_handling: raise SessionApiFailed(status=422, body=resp.content[:1024])
|
||||
flow_control: abort
|
||||
state_recovery: none (typically client bug; surface for debugging)
|
||||
state_recovery: none (typically client bug; surface for debugging. Issue #5: a `end_user_id_required` 422 indicates the agent requires --end-user-id; raw label is honest, hint translation deferred.)
|
||||
httpx.HTTPStatusError (other status):
|
||||
local_handling: raise SessionApiFailed(status=resp.status_code, body=resp.content[:1024])
|
||||
flow_control: abort
|
||||
state_recovery: none
|
||||
STEPS:
|
||||
1. [setup, flexibility=prescriptive] Validate inputs per PRE-001, PRE-002
|
||||
2. [sequential, flexibility=prescriptive] CALL client.post("/sessions", json={"agent_id": agent_id})
|
||||
1. [setup, flexibility=prescriptive] Validate inputs per PRE-001, PRE-002, PRE-003
|
||||
2. [sequential, flexibility=prescriptive] Build body = {"agent_id": agent_id}; IF end_user_id is not None: body["end_user_id"] = end_user_id
|
||||
3. [sequential, flexibility=prescriptive] CALL client.post("/sessions", json=body)
|
||||
tool: { destructive: false, idempotent: false, read_only: false, open_world: false }
|
||||
3. [branch, flexibility=prescriptive] IF resp.status_code == 404: RAISE AgentNotFound
|
||||
4. [branch, flexibility=prescriptive] IF resp.status_code == 404: RAISE AgentNotFound
|
||||
ELIF resp.status_code != 201: RAISE SessionApiFailed
|
||||
4. [sequential] Parse resp.json() → body
|
||||
5. [cleanup] RETURN SessionInfo(
|
||||
5. [sequential] Parse resp.json() → body
|
||||
6. [cleanup] RETURN SessionInfo(
|
||||
session_id=body["session_id"],
|
||||
agent_id=body["agent_id"],
|
||||
created_at=body["created_at"],
|
||||
@@ -135,11 +137,14 @@ STEPS:
|
||||
TESTS:
|
||||
happy_create [happy,tracer]: mock returns 201 with full body → returns SessionInfo with all create-side fields populated; list-only fields are at create-origin defaults (name=None, archived=False, tags=[])
|
||||
happy_create_with_metadata [happy]: response includes metadata={"model": "glm5-turbo"} → SessionInfo.metadata == {"model": "glm5-turbo"}
|
||||
request_body_shape [trace]: outbound JSON body is exactly {"agent_id": <arg>} — no Bifrost field, no extra keys
|
||||
request_body_shape [trace]: outbound JSON body is exactly {"agent_id": <arg>} when end_user_id omitted — no Bifrost field, no extra keys
|
||||
unknown_agent_id [error]: mock returns 404 → raises AgentNotFound(agent_id="mimir")
|
||||
validation_failed [error]: mock returns 422 → raises SessionApiFailed(status=422); body truncated to ≤1024 bytes
|
||||
unexpected_status_truncates [error]: mock returns 500 with 5000-byte body → SessionApiFailed; .body is exactly the first 1024 bytes
|
||||
empty_agent_id [adversarial]: agent_id="" → AssertionError; no HTTP issued
|
||||
happy_create_with_end_user_id [happy, issue #5]: end_user_id="alice" → outbound JSON body == {"agent_id": "mimir", "end_user_id": "alice"} byte-for-byte; SessionInfo populated as today
|
||||
default_omits_end_user_id [trace, issue #5]: omit end_user_id kwarg → outbound JSON body == {"agent_id": "mimir"} (no end_user_id key); preserves the pre-#5 baseline
|
||||
empty_end_user_id [adversarial, issue #5]: end_user_id="" → AssertionError before HTTP (PRE-003)
|
||||
```
|
||||
|
||||
```contract
|
||||
|
||||
Reference in New Issue
Block a user