Files
2026Technology-Competition/tests/test_verify_ws_real_flow.py
T
lhl 80daadcd31 fix(websocket): 修复 app.py 不可导入并新增真实链路验证
- 将 /api/sessions/{sid}/ws 端点移入 create_app(此前置于模块级导致整模块 import NameError,回归被验证拦截)
- register_loop + subscribe 调整至 accept 之前,缩小连接已开但未订阅期间的进度丢失窗口
- 新增 tests/test_verify_ws_real_flow.py:驱动真实 HTTP 聊天流程断言 WS 收到 agent 实际发射的 parse/impact 进度
- 同步 WebSocket 计划文档 Task 3 代码片段(标注端点必须位于 create_app 内)
- 全量 pytest 实测 583 passed / 99.03% 达标
2026-08-29 14:36:12 +08:00

103 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""真实链路验证:确认 agent 实际发射的进度经 hub 送达 /ws。
不走手动 hub.emit,而是驱动真实 HTTP 流程(创建会话→上传真实样本→发送「生成概要设计书」),
断言 WS 收到 agent 在 _parse_and_confirm 中真正发射的进度事件。
"""
import io
import os
import threading
import time
import zipfile
from pathlib import Path
from fastapi.testclient import TestClient
from genesis.server.app import create_app
from genesis.server.service import SessionStore
SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample"
def _zip_dir_bytes(src: Path) -> bytes:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
for root, _dirs, files in os.walk(src):
for f in files:
p = Path(root) / f
zf.write(p, p.relative_to(src))
return buf.getvalue()
def test_ws_receives_agent_emitted_progress(tmp_path):
store = SessionStore(db_path=str(tmp_path / "s.db"))
client = TestClient(create_app(
store=store, data_root=str(tmp_path / "data"), engine="fake"))
# 1) 创建会话(真实流程:sid 由服务端生成)
created = client.post("/api/sessions", json={"user_id": "default", "project": "测试项目"})
assert created.status_code == 200, created.text
sid = created.json()["session_id"]
# 2) 上传真实样本(端点 /api/sessions/{sid}/filesfile_type 走表单字段)
samples = [
("requirements", "requirements.xlsx",
(SAMPLE_DIR / "requirements_newdev.xlsx").read_bytes(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
("template", "template.docx",
(SAMPLE_DIR / "template_design_zh.docx").read_bytes(),
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
("existing_system", "existing_system.zip",
_zip_dir_bytes(SAMPLE_DIR / "existing-system"),
"application/zip"),
]
for ftype, fname, content, mime in samples:
r = client.post(
f"/api/sessions/{sid}/files",
data={"file_type": ftype},
files={"file": (fname, content, mime)},
)
assert r.status_code == 200, f"上传 {ftype} 失败: {r.text}"
# 3) 打开 WS 订阅该会话进度,再驱动聊天(POST 在子线程同步执行,期间 agent 会真实发射进度)
received: list = []
def trigger():
resp = client.post(
f"/api/chat/{sid}/messages",
json={"content": "请生成概要设计书"},
)
received.append(("post_status", resp.status_code))
def receiver(ws):
# 阻塞读取,直到连接关闭(连接关闭由主线程在 POST 完成后触发)
while True:
try:
received.append(ws.receive_json())
except Exception:
return
with client.websocket_connect(f"/api/sessions/{sid}/ws") as ws:
time.sleep(0.1) # 确保服务端已完成订阅(register_loop + subscribe 在 accept 之前)
t = threading.Thread(target=trigger)
t.start()
rth = threading.Thread(target=receiver, args=(ws,))
rth.start()
t.join(timeout=30) # 等待聊天请求(含解析/影响调查)完成
time.sleep(1.0) # 收尾缓冲:让服务端已发射的事件送达
try:
ws.close()
except Exception:
pass
rth.join(timeout=5)
post_status = None
for item in received:
if isinstance(item, tuple) and item[0] == "post_status":
post_status = item[1]
progress_events = [e for e in received if isinstance(e, dict) and e.get("type") == "progress"]
assert progress_events, (
f"WS 未收到任何 agent 实际发射的进度事件;post_status={post_status}; 收到={received}"
)
steps = [e.get("step") for e in progress_events]
assert "parse" in steps, f"至少应收到 parse 进度;实际 steps={steps}"