"""真实链路验证:确认 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}/files,file_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}"