feat(sessions): implement issue #2 contract via TDD
Implements docs/contracts/issues/2.contract.md. Two functions (create_session, list_sessions), two frozen dataclasses (SessionInfo, SessionPage), three exception types (AgentNotFound, InvalidCursor, SessionApiFailed). 19 contract-listed tests cover every TESTS: entry verbatim per the tracer-bullet vertical-slice ordering. SessionInfo uses one shape across both endpoints with origin- conditional defaults per INV-001 (create) and INV-002 (list). create- origin always sets list-only fields to (name=None, archived=False, tags=[]); list-origin reads them from the response item with absent/null treated as those same defaults — keeps the dataclass uniform without forcing callers to handle two types. Spotted an internal-inconsistency in the contract at TDD start — POST-003 and happy_create's test description still said "archived is None, tags is None" while the freshly-applied Volva amendment had moved INV-001 to (archived=False, tags=[]). Fixed in-place before writing any tests so the spec stayed coherent. SessionApiFailed.body truncates to <= 1024 bytes at construction, matching the SseConnectFailed / CancelFailed precedent from issue #1. No code shared with sse_client.py (convention-dependency only per issue #2's dependencies: block). 62 tests GREEN total (42 sse_client + 19 sessions + 1 boundary smoke). Ruff clean. No refactor pass — the two functions are ~25 LOC each with distinct error-routing branches that don't naturally share more than they already do.
This commit is contained in:
@@ -100,7 +100,7 @@ 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}
|
||||
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 (name, archived, tags) are None -- assert info.message_count is not None and info.name is None and info.archived is None and info.tags is 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:
|
||||
HTTP 404 unknown_agent_id:
|
||||
local_handling: raise AgentNotFound(agent_id=agent_id)
|
||||
@@ -133,7 +133,7 @@ STEPS:
|
||||
tags=[], # INV-001 fixed for create-origin
|
||||
)
|
||||
TESTS:
|
||||
happy_create [happy,tracer]: mock returns 201 with full body → returns SessionInfo with all create-side fields populated; list-only fields are None
|
||||
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
|
||||
unknown_agent_id [error]: mock returns 404 → raises AgentNotFound(agent_id="mimir")
|
||||
|
||||
Reference in New Issue
Block a user