"""Web 面板测试:HTTP/SSE 端点的集成路径(confirm approve/reject 经端点走通)。 - confirm approve / reject 流程经 HTTP 端点裁决; - undo_session / 快照管理端点; - SSE 事件流(用真实 uvicorn 起服务走 TCP,TestClient 会缓冲无限流); - EventBroker 单元测试。 本模块依赖可选 extra ``core[webui]``,未安装时整模块 skip。 """ import http.client import json import socket import threading import time import pytest fastapi = pytest.importorskip("fastapi") import uvicorn # noqa: E402 from fastapi.testclient import TestClient # noqa: E402 from mock_host import MockHostAdapter # noqa: E402 from mock_llm import MockProvider # noqa: E402 from quercus_core.providers.base import AssistantTurn, ToolCall # noqa: E402 from quercus_core.types import Action # noqa: E402 from quercus_core.webui.app import create_app # noqa: E402 from quercus_core.webui.broker import EventBroker # noqa: E402 def _wait_pending(client, timeout=5.0): deadline = time.time() + timeout while time.time() < deadline: data = client.get("/api/pending").json() if data["pending"]: return data["pending"][0] time.sleep(0.02) return None # ---- 基础端点 ---- def test_index_served(): app = create_app(adapter=MockHostAdapter(), provider=MockProvider(script=[])) client = TestClient(app) r = client.get("/") assert r.status_code == 200 assert "= 1 assert adapter.list_footage() == [] # 时间线/媒体恢复原状 def test_snapshot_endpoints(): adapter = MockHostAdapter() app = create_app(adapter=adapter, provider=MockProvider(script=[])) client = TestClient(app) app.state.session.executor.execute(Action(tool="import_footage", params={"paths": ["/media/a.mp4"]})) snaps = client.get("/api/snapshots").json()["snapshots"] assert len(snaps) >= 1 sid = snaps[0]["id"] assert client.post(f"/api/snapshots/{sid}/drop").json()["ok"] is True assert client.get("/api/snapshots").json()["snapshots"] == [] # ---- EventBroker 单元 ---- def test_event_broker(): broker = EventBroker() q1 = broker.subscribe() q2 = broker.subscribe() broker.publish({"kind": "system", "text": "hi"}) assert q1.get(timeout=1)["text"] == "hi" assert q2.get(timeout=1)["text"] == "hi" broker.unsubscribe(q1) assert broker.subscriber_count == 1 # ---- SSE 事件流(真实 uvicorn 走 TCP) ---- @pytest.fixture def live_server(): adapter = MockHostAdapter() app = create_app(adapter=adapter, provider=MockProvider(script=[])) with socket.socket() as s: s.bind(("127.0.0.1", 0)) port = s.getsockname()[1] config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="error") server = uvicorn.Server(config) thread = threading.Thread(target=server.run, daemon=True) thread.start() deadline = time.time() + 5 while time.time() < deadline: try: with socket.create_connection(("127.0.0.1", port), timeout=0.2): break except OSError: time.sleep(0.05) else: pytest.fail("uvicorn 未在超时内就绪") yield {"app": app, "port": port} server.should_exit = True thread.join(timeout=5) def test_sse_stream_delivers_events(live_server): port = live_server["port"] conn = http.client.HTTPConnection("127.0.0.1", port, timeout=10) try: conn.request("GET", "/api/events") resp = conn.getresponse() assert resp.status == 200 assert resp.getheader("content-type", "").startswith("text/event-stream") first = resp.readline() assert b"connected" in first # 首条事件确认流已就绪 # 发布一条真实事件(undo_session 会发 system 消息) live_server["app"].state.session.undo_session() got = None deadline = time.time() + 5 while time.time() < deadline: line = resp.readline() if not line: break if line.startswith(b"data: "): got = json.loads(line[6:]) break assert got is not None assert got["kind"] == "system" finally: conn.close()