refactor(#20): delete the orphaned hand-rolled turn-stream paths (slice-2, part 2b-iii)
DEC-4 live smoke PASSED first (personal :8081, b127/b128): create → streamed turn that rendered (worker_phase/text/text_boundary/done with usage) → SIGINT cancel that round-tripped to a cancelled terminal. With both CLI + web on the adapter, the hand-rolled turn-stream family is fully orphaned — deleting it now. - sse_client.py (714 → 224): removed stream_turn / reconnect_turn / stream_turn_resilient / cancel_turn + the Event dataclasses (Text/Done/…/Event union) + CancelResult + the SSE parse helpers (_iter_events / _envelope_for_type / _parse_sse_id / _eager_failure_fields / _INT_RE). KEPT: the caller-semantic exceptions (the adapter raises them, DEC-2), SseId, AdminEvent, stream_admin_events (slice-6 admin surface). - sessions.py (677 → 608): removed list_sessions + get_session_tools (no surface users) + SessionPage. KEPT: create_session / get_session_messages (the --seed-first-message probe still uses them, slice-3) + all exceptions + SessionInfo. - tests: test_sse_client pruned to TestStreamAdminEvents; test_sessions dropped the list_sessions + get_session_tools classes. The deleted turn-stream behavior is now covered by test_wt.py + the CLI/web integration tests + the live smoke. Suite 490 green (570 − 80 deleted turn-stream tests); ruff clean on all touched files; no new mypy errors. Patch (internal cleanup; behavior preserved).
This commit is contained in:
@@ -13,10 +13,8 @@ from ratatoskr.sessions import (
|
||||
BifrostBinding,
|
||||
BifrostConsumerKeyMissing,
|
||||
BifrostHandshakeFailed,
|
||||
InvalidCursor,
|
||||
PersonaNotConfigured,
|
||||
SessionApiFailed,
|
||||
SessionPage,
|
||||
create_character,
|
||||
create_session,
|
||||
delete_character,
|
||||
@@ -27,10 +25,8 @@ from ratatoskr.sessions import (
|
||||
get_persona_state,
|
||||
get_session_bifrost,
|
||||
get_session_messages,
|
||||
get_session_tools,
|
||||
list_agents,
|
||||
list_character_models,
|
||||
list_sessions,
|
||||
set_persona_state,
|
||||
write_authored_history,
|
||||
)
|
||||
@@ -551,223 +547,6 @@ class TestEndpointForPlane:
|
||||
endpoint_for_plane("persona", "10.100.10.50")
|
||||
|
||||
|
||||
def _list_item(
|
||||
*,
|
||||
session_id: str = "s1",
|
||||
agent_id: str = "mimir",
|
||||
created_at: str = "2026-04-15T12:00:00+00:00",
|
||||
last_active: str = "2026-04-15T12:05:00+00:00",
|
||||
metadata: dict[str, object] | None = None,
|
||||
name: str | None = "Research session",
|
||||
archived: bool = False,
|
||||
tags: list[str] | None = None,
|
||||
) -> dict[str, object]:
|
||||
"""Build a GET /sessions list-item body for tests."""
|
||||
item: dict[str, object] = {
|
||||
"session_id": session_id,
|
||||
"agent_id": agent_id,
|
||||
"created_at": created_at,
|
||||
"last_active": last_active,
|
||||
"metadata": metadata if metadata is not None else {},
|
||||
"name": name,
|
||||
"archived": archived,
|
||||
"tags": tags if tags is not None else ["work"],
|
||||
}
|
||||
return item
|
||||
|
||||
|
||||
class TestListSessions:
|
||||
@respx.mock
|
||||
async def test_explicit_null_list_defaults(self) -> None:
|
||||
"""INV-002: explicit-null archived -> False; explicit-null tags -> []."""
|
||||
raw_item = {
|
||||
"session_id": "s1",
|
||||
"agent_id": "mimir",
|
||||
"created_at": "2026-04-15T12:00:00+00:00",
|
||||
"last_active": "2026-04-15T12:05:00+00:00",
|
||||
"metadata": {},
|
||||
"name": None,
|
||||
"archived": None,
|
||||
"tags": None,
|
||||
}
|
||||
respx.get("https://w.example/sessions").mock(
|
||||
return_value=httpx.Response(
|
||||
200, json={"items": [raw_item], "next_cursor": None}
|
||||
)
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
page = await list_sessions(client)
|
||||
info = page.items[0]
|
||||
assert info.archived is False, "explicit-null archived must default to False"
|
||||
assert info.tags == [], "explicit-null tags must default to []"
|
||||
assert info.name is None
|
||||
|
||||
@respx.mock
|
||||
async def test_happy_first_page(self) -> None:
|
||||
"""happy_first_page [happy,tracer]: one item + next_cursor -> SessionPage shape."""
|
||||
respx.get("https://w.example/sessions").mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"items": [_list_item()],
|
||||
"next_cursor": "v1.eyJhYmMifQ",
|
||||
},
|
||||
)
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
page = await list_sessions(client)
|
||||
assert isinstance(page, SessionPage)
|
||||
assert len(page.items) == 1
|
||||
assert page.next_cursor == "v1.eyJhYmMifQ"
|
||||
info = page.items[0]
|
||||
assert info.session_id == "s1"
|
||||
assert info.message_count is None # INV-002: not in list response
|
||||
assert info.name == "Research session"
|
||||
assert info.archived is False
|
||||
assert info.tags == ["work"]
|
||||
|
||||
@respx.mock
|
||||
async def test_happy_last_page(self) -> None:
|
||||
"""happy_last_page: next_cursor=null -> SessionPage(next_cursor=None)."""
|
||||
respx.get("https://w.example/sessions").mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
json={"items": [_list_item()], "next_cursor": None},
|
||||
)
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
page = await list_sessions(client)
|
||||
assert page.next_cursor is None
|
||||
|
||||
@respx.mock
|
||||
async def test_empty_results(self) -> None:
|
||||
"""empty_results: {items: [], next_cursor: null} -> SessionPage([], None)."""
|
||||
respx.get("https://w.example/sessions").mock(
|
||||
return_value=httpx.Response(200, json={"items": [], "next_cursor": None})
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
page = await list_sessions(client)
|
||||
assert page == SessionPage(items=[], next_cursor=None)
|
||||
|
||||
@respx.mock
|
||||
async def test_include_archived_query(self) -> None:
|
||||
"""include_archived_query: True -> has param; default -> NO param at all."""
|
||||
route = respx.get("https://w.example/sessions").mock(
|
||||
return_value=httpx.Response(
|
||||
200, json={"items": [], "next_cursor": None}
|
||||
)
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
await list_sessions(client, include_archived=True)
|
||||
await list_sessions(client) # default
|
||||
url_with = str(route.calls[0].request.url)
|
||||
url_default = str(route.calls[1].request.url)
|
||||
assert "include_archived=true" in url_with
|
||||
assert "include_archived" not in url_default
|
||||
|
||||
@respx.mock
|
||||
async def test_cursor_threaded(self) -> None:
|
||||
"""cursor_threaded: cursor=opaque -> URL has cursor=opaque."""
|
||||
route = respx.get("https://w.example/sessions").mock(
|
||||
return_value=httpx.Response(
|
||||
200, json={"items": [], "next_cursor": None}
|
||||
)
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
await list_sessions(client, cursor="opaque-from-prev-page")
|
||||
assert "cursor=opaque-from-prev-page" in str(route.calls[0].request.url)
|
||||
|
||||
@respx.mock
|
||||
async def test_limit_query(self) -> None:
|
||||
"""limit_query: limit=10 -> URL has limit=10."""
|
||||
route = respx.get("https://w.example/sessions").mock(
|
||||
return_value=httpx.Response(
|
||||
200, json={"items": [], "next_cursor": None}
|
||||
)
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
await list_sessions(client, limit=10)
|
||||
assert "limit=10" in str(route.calls[0].request.url)
|
||||
|
||||
@respx.mock
|
||||
async def test_invalid_cursor_server(self) -> None:
|
||||
"""invalid_cursor_server: 422 cursor_invalid -> InvalidCursor(raw=<passed cursor>)."""
|
||||
respx.get("https://w.example/sessions").mock(
|
||||
return_value=httpx.Response(
|
||||
422,
|
||||
json={"error_code": "cursor_invalid", "message": "bad cursor"},
|
||||
)
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
with pytest.raises(InvalidCursor) as exc_info:
|
||||
await list_sessions(client, cursor="bogus")
|
||||
assert exc_info.value.raw == "bogus"
|
||||
|
||||
@respx.mock
|
||||
async def test_other_validation_failed(self) -> None:
|
||||
"""other_validation_failed: 422 other error_code -> SessionApiFailed(422); truncated."""
|
||||
respx.get("https://w.example/sessions").mock(
|
||||
return_value=httpx.Response(
|
||||
422,
|
||||
json={"error_code": "validation_failed", "message": "limit out of range"},
|
||||
)
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
with pytest.raises(SessionApiFailed) as exc_info:
|
||||
await list_sessions(client)
|
||||
assert exc_info.value.status == 422
|
||||
assert len(exc_info.value.body) <= 1024
|
||||
|
||||
@respx.mock
|
||||
async def test_unexpected_status_truncates(self) -> None:
|
||||
"""unexpected_status_truncates: 500 + 5000-byte body -> SessionApiFailed; body == 1024."""
|
||||
big = b"x" * 5000
|
||||
respx.get("https://w.example/sessions").mock(
|
||||
return_value=httpx.Response(500, content=big)
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
with pytest.raises(SessionApiFailed) as exc_info:
|
||||
await list_sessions(client)
|
||||
assert exc_info.value.status == 500
|
||||
assert exc_info.value.body == big[:1024]
|
||||
|
||||
@respx.mock
|
||||
async def test_limit_below_one(self) -> None:
|
||||
"""limit_below_one [adversarial]: limit=0 -> AssertionError; no HTTP."""
|
||||
route = respx.get("https://w.example/sessions").mock(
|
||||
return_value=httpx.Response(200, json={"items": [], "next_cursor": None})
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
with pytest.raises(AssertionError):
|
||||
await list_sessions(client, limit=0)
|
||||
assert route.call_count == 0
|
||||
|
||||
@respx.mock
|
||||
async def test_limit_above_max(self) -> None:
|
||||
"""limit_above_max [adversarial]: limit=300 -> AssertionError; no HTTP."""
|
||||
route = respx.get("https://w.example/sessions").mock(
|
||||
return_value=httpx.Response(200, json={"items": [], "next_cursor": None})
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
with pytest.raises(AssertionError):
|
||||
await list_sessions(client, limit=300)
|
||||
assert route.call_count == 0
|
||||
|
||||
@respx.mock
|
||||
async def test_empty_cursor(self) -> None:
|
||||
"""empty_cursor [adversarial]: cursor='' -> AssertionError; no HTTP."""
|
||||
route = respx.get("https://w.example/sessions").mock(
|
||||
return_value=httpx.Response(200, json={"items": [], "next_cursor": None})
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
with pytest.raises(AssertionError):
|
||||
await list_sessions(client, cursor="")
|
||||
assert route.call_count == 0
|
||||
|
||||
|
||||
# ---- Issue #8: list_agents + AgentInfo --------------------------------------
|
||||
|
||||
|
||||
class TestListAgents:
|
||||
@respx.mock
|
||||
async def test_happy_full_shape(self) -> None:
|
||||
@@ -1145,53 +924,6 @@ class TestGetCapabilities:
|
||||
assert exc.value.status == 500
|
||||
|
||||
|
||||
class TestGetSessionTools:
|
||||
"""docs/contracts/issues/2.contract.md — get_session_tools (GET /sessions/{id}/tools, #183)."""
|
||||
|
||||
@respx.mock
|
||||
async def test_happy(self) -> None:
|
||||
"""happy [happy,tracer]: 200 → merged tool inventory dict verbatim."""
|
||||
respx.get("https://w.example/sessions/s1/tools").mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"agent_id": "alice:wizard",
|
||||
"builtin_tools": [],
|
||||
"bifrost_tools": [
|
||||
{"name": "bifrost.alice.set_field", "description": "d", "parameters": {}}
|
||||
],
|
||||
},
|
||||
)
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
tools = await get_session_tools(client, "s1")
|
||||
assert tools["agent_id"] == "alice:wizard"
|
||||
assert tools["builtin_tools"] == []
|
||||
assert tools["bifrost_tools"][0]["name"] == "bifrost.alice.set_field"
|
||||
|
||||
@respx.mock
|
||||
async def test_cross_owner_404_raises(self) -> None:
|
||||
"""cross_owner_404 [error]: 404 session_not_found → SessionApiFailed(404)."""
|
||||
respx.get("https://w.example/sessions/s1/tools").mock(
|
||||
return_value=httpx.Response(404, json={"error_code": "session_not_found"})
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
with pytest.raises(SessionApiFailed) as exc:
|
||||
await get_session_tools(client, "s1")
|
||||
assert exc.value.status == 404
|
||||
|
||||
@respx.mock
|
||||
async def test_empty_session_id_asserts(self) -> None:
|
||||
"""empty_session_id [adversarial]: '' → AssertionError; no HTTP issued."""
|
||||
route = respx.get("https://w.example/sessions//tools").mock(
|
||||
return_value=httpx.Response(200, json={})
|
||||
)
|
||||
async with httpx.AsyncClient(base_url="https://w.example") as client:
|
||||
with pytest.raises(AssertionError):
|
||||
await get_session_tools(client, "")
|
||||
assert route.call_count == 0
|
||||
|
||||
|
||||
class TestGetSessionBifrost:
|
||||
"""#2 contract — get_session_bifrost (GET /admin/sessions/{id}/bifrost, #176)."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user