rename popup chat to floating chat

This commit is contained in:
2026-03-08 22:53:31 +01:00
parent 0bd46937d3
commit 34f01234c9
8 changed files with 102 additions and 102 deletions

View File

@@ -1,12 +1,12 @@
"""Tests for app.core.output_formatter — HomeFormatter and PopupFormatter."""
"""Tests for app.core.output_formatter — HomeFormatter and FloatingFormatter."""
from __future__ import annotations
import pytest
from app.core.output_formatter import HomeFormatter, PopupFormatter
from app.core.output_formatter import HomeFormatter, FloatingFormatter
from app.schemas import (
WsPopupDomain,
WsFloatingDomain,
WsStreamBlock,
WsStreamEnd,
WsStreamStart,
@@ -134,12 +134,12 @@ async def test_home_formatter_frame_order():
assert isinstance(frames[-1], WsStreamEnd)
# ── PopupFormatter ────────────────────────────────────────────────────────────
# ── FloatingFormatter ────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_popup_formatter_domain_emitted_first():
async def test_floating_formatter_domain_emitted_first():
req_id = "pop-1"
formatter = PopupFormatter(request_id=req_id)
formatter = FloatingFormatter(request_id=req_id)
tokens = [
("task_agent", ""), # domain signal
("task_agent", "Hello"),
@@ -147,19 +147,19 @@ async def test_popup_formatter_domain_emitted_first():
]
frames = await collect(formatter, _stream(*tokens))
assert isinstance(frames[0], WsPopupDomain)
assert isinstance(frames[0], WsFloatingDomain)
assert frames[0].domain == "tasks"
assert frames[0].request_id == req_id
@pytest.mark.asyncio
async def test_popup_formatter_text_only():
async def test_floating_formatter_text_only():
req_id = "pop-2"
formatter = PopupFormatter(request_id=req_id)
formatter = FloatingFormatter(request_id=req_id)
tokens = [("checkpoint_agent", ""), ("checkpoint_agent", "Summary")]
frames = await collect(formatter, _stream(*tokens))
assert isinstance(frames[0], WsPopupDomain)
assert isinstance(frames[0], WsFloatingDomain)
assert frames[0].domain == "checkpoints"
text_frames = [f for f in frames if isinstance(f, WsStreamText)]
assert len(text_frames) == 1
@@ -167,10 +167,10 @@ async def test_popup_formatter_text_only():
@pytest.mark.asyncio
async def test_popup_formatter_no_block_frames():
"""PopupFormatter must never emit WsStreamBlock."""
async def test_floating_formatter_no_block_frames():
"""FloatingFormatter must never emit WsStreamBlock."""
req_id = "pop-3"
formatter = PopupFormatter(request_id=req_id)
formatter = FloatingFormatter(request_id=req_id)
tokens = [
("note_agent", ""),
("note_agent", '{"type": "chart", "chartType": "bar", "data": []}'),
@@ -180,16 +180,16 @@ async def test_popup_formatter_no_block_frames():
@pytest.mark.asyncio
async def test_popup_formatter_end_frame():
async def test_floating_formatter_end_frame():
req_id = "pop-4"
formatter = PopupFormatter(request_id=req_id)
formatter = FloatingFormatter(request_id=req_id)
frames = await collect(formatter, _stream(("project_agent", ""), ("project_agent", "Done")))
assert isinstance(frames[-1], WsStreamEnd)
@pytest.mark.asyncio
async def test_popup_formatter_unknown_agent_defaults_to_tasks():
async def test_floating_formatter_unknown_agent_defaults_to_tasks():
req_id = "pop-5"
formatter = PopupFormatter(request_id=req_id)
formatter = FloatingFormatter(request_id=req_id)
frames = await collect(formatter, _stream(("unknown_agent", ""), ("unknown_agent", "hi")))
assert frames[0].domain == "tasks"

View File

@@ -6,9 +6,9 @@ from pydantic import ValidationError
from app.schemas import (
WsFrameType,
WsHomeRequest,
WsPopupDomain,
WsPopupRequest,
WsPopupScope,
WsFloatingDomain,
WsFloatingRequest,
WsFloatingScope,
WsStreamBlock,
WsStreamEnd,
WsStreamStart,
@@ -22,12 +22,12 @@ from app.schemas import (
def test_v3_frame_types_exist():
v3_types = [
"home_request",
"popup_request",
"floating_request",
"stream_start",
"stream_text",
"stream_block",
"stream_end",
"popup_domain",
"floating_domain",
"data_request",
"data_response",
"mutation",
@@ -90,49 +90,49 @@ def test_home_request_requires_message():
WsHomeRequest.model_validate({"type": "home_request"})
# ── WsPopupRequest ────────────────────────────────────────────────────
# ── WsFloatingRequest ────────────────────────────────────────────────────
def test_popup_request_basic():
frame = WsPopupRequest(
def test_floating_request_basic():
frame = WsFloatingRequest(
message="Summarise",
scope=WsPopupScope(type="task", id="task-123"),
scope=WsFloatingScope(type="task", id="task-123"),
)
assert frame.type == WsFrameType.popup_request
assert frame.type == WsFrameType.floating_request
assert frame.scope.type == "task"
assert frame.scope.id == "task-123"
def test_popup_request_scope_without_id():
frame = WsPopupRequest(
def test_floating_request_scope_without_id():
frame = WsFloatingRequest(
message="Show all",
scope=WsPopupScope(type="project"),
scope=WsFloatingScope(type="project"),
)
assert frame.scope.id is None
def test_popup_request_serializes():
frame = WsPopupRequest(
def test_floating_request_serializes():
frame = WsFloatingRequest(
message="Test",
scope=WsPopupScope(type="note", id="n-1"),
scope=WsFloatingScope(type="note", id="n-1"),
)
data = frame.model_dump()
assert data["type"] == "popup_request"
assert data["type"] == "floating_request"
assert data["scope"]["type"] == "note"
assert data["scope"]["id"] == "n-1"
def test_popup_request_invalid_scope_type():
def test_floating_request_invalid_scope_type():
with pytest.raises(ValidationError):
WsPopupRequest(
WsFloatingRequest(
message="X",
scope=WsPopupScope(type="unknown"), # type: ignore[arg-type]
scope=WsFloatingScope(type="unknown"), # type: ignore[arg-type]
)
def test_popup_request_requires_scope():
def test_floating_request_requires_scope():
with pytest.raises(ValidationError):
WsPopupRequest.model_validate({"type": "popup_request", "message": "X"})
WsFloatingRequest.model_validate({"type": "floating_request", "message": "X"})
# ── WsStreamStart ─────────────────────────────────────────────────────
@@ -261,32 +261,32 @@ def test_stream_end_deserializes():
assert frame.request_id == "r3"
# ── WsPopupDomain ─────────────────────────────────────────────────────
# ── WsFloatingDomain ─────────────────────────────────────────────────────
def test_popup_domain_tasks():
frame = WsPopupDomain(request_id="r1", domain="tasks")
assert frame.type == WsFrameType.popup_domain
def test_floating_domain_tasks():
frame = WsFloatingDomain(request_id="r1", domain="tasks")
assert frame.type == WsFrameType.floating_domain
assert frame.domain == "tasks"
@pytest.mark.parametrize("domain", ["tasks", "checkpoints", "notes", "projects"])
def test_popup_domain_valid_domains(domain: str):
frame = WsPopupDomain(request_id="r1", domain=domain) # type: ignore[arg-type]
def test_floating_domain_valid_domains(domain: str):
frame = WsFloatingDomain(request_id="r1", domain=domain) # type: ignore[arg-type]
assert frame.domain == domain
def test_popup_domain_invalid():
def test_floating_domain_invalid():
with pytest.raises(ValidationError):
WsPopupDomain(request_id="r1", domain="invalid") # type: ignore[arg-type]
WsFloatingDomain(request_id="r1", domain="invalid") # type: ignore[arg-type]
def test_popup_domain_serializes():
d = WsPopupDomain(request_id="r1", domain="notes").model_dump()
assert d == {"type": "popup_domain", "request_id": "r1", "domain": "notes"}
def test_floating_domain_serializes():
d = WsFloatingDomain(request_id="r1", domain="notes").model_dump()
assert d == {"type": "floating_domain", "request_id": "r1", "domain": "notes"}
def test_popup_domain_deserializes():
raw = {"type": "popup_domain", "request_id": "r1", "domain": "projects"}
frame = WsPopupDomain.model_validate(raw)
def test_floating_domain_deserializes():
raw = {"type": "floating_domain", "request_id": "r1", "domain": "projects"}
frame = WsFloatingDomain.model_validate(raw)
assert frame.domain == "projects"

View File

@@ -1,6 +1,6 @@
"""Integration tests for the unified WebSocket handler (Step 5).
Tests the device WS endpoint with home_request and popup_request frames,
Tests the device WS endpoint with home_request and floating_request frames,
verifying that the correct v3 frame sequence is returned.
LLM calls are mocked to avoid network dependency.
@@ -34,7 +34,7 @@ def _override_db(db_session):
def _recv_until_end(ws, max_frames: int = 20) -> list[dict]:
"""Receive frames until stream_end (or stream_end inside popup flow), or max_frames."""
"""Receive frames until stream_end (or stream_end inside floating flow), or max_frames."""
frames = []
for _ in range(max_frames):
raw = ws.receive_text()
@@ -50,7 +50,7 @@ async def _mock_home_stream(user_id, message, context, reg=None):
yield "task_agent", '{"type": "text", "content": "Hello"}'
async def _mock_popup_stream(user_id, message, context, reg=None):
async def _mock_floating_stream(user_id, message, context, reg=None):
yield "task_agent", ""
yield "task_agent", "Here is a summary"
@@ -80,17 +80,17 @@ def test_home_request_produces_stream_frames(client):
assert types.index(WsFrameType.stream_start) < types.index(WsFrameType.stream_end)
def test_popup_request_produces_domain_frame(client):
"""popup_request → popup_domain first, then stream_text*, stream_end."""
def test_floating_request_produces_domain_frame(client):
"""floating_request → floating_domain first, then stream_text*, stream_end."""
token = make_jwt("power", user_id=USER_ID)
with patch("app.api.routes.device_ws.orchestrate_v3_stream", side_effect=_mock_popup_stream):
with patch("app.api.routes.device_ws.orchestrate_v3_stream", side_effect=_mock_floating_stream):
with client.websocket_connect(f"/api/v1/ws/device?token={token}") as ws:
ws.send_text(json.dumps({
"type": "device_hello", "device_id": "dev-2", "agent_ids": []
}))
ws.send_text(json.dumps({
"type": "popup_request",
"type": "floating_request",
"request_id": "p1",
"message": "Summarize this task",
"scope": {"type": "task", "id": "task-123"},
@@ -98,11 +98,11 @@ def test_popup_request_produces_domain_frame(client):
frames = _recv_until_end(ws)
types = [f["type"] for f in frames]
assert WsFrameType.popup_domain in types
assert WsFrameType.floating_domain in types
assert WsFrameType.stream_end in types
assert types.index(WsFrameType.popup_domain) < types.index(WsFrameType.stream_end)
assert types.index(WsFrameType.floating_domain) < types.index(WsFrameType.stream_end)
domain_frame = next(f for f in frames if f["type"] == WsFrameType.popup_domain)
domain_frame = next(f for f in frames if f["type"] == WsFrameType.floating_domain)
assert domain_frame["domain"] == "tasks"
assert domain_frame["request_id"] == "p1"