fix(chat): 前端审计修复——预览契约改HTML/状态机统一/进度错误持久化/chat_state.js抽取

- app.py: /result/preview 返回渲染 HTML(HTMLResponse) 而非 {html} JSON;新增 /chat_state.js 静态路由;预览缺失返回 404(RESULT_NOT_FOUND)
- chat.html: currentProject 统一为 draftProject;新会话继承已选项目(§4.2);上传区显隐/类型映射/抽屉脏检测改用 GenesisState;loadSession 按 role 渲染 progress/error;加 h1 标题
- chat_state.js(新): 抽取 autoBindProject/shouldHideUploadSelect/resolveUploadType/computeDrawerSnapshot/isDrawerDirty/buildWelcome 纯函数(U+1 分隔符)
- agent.py: 新增 _persist_progress/_store_error,进度与错误以 role 入库(重载可见)
- 测试: test_chat_state.js(11 passed) + test_frontend_audit_fixes.py(5) + 修正 test_server_api 旧 JSON 契约断言;全量 pytest 568 passed / 99.06%
This commit is contained in:
lhl
2026-08-29 11:28:08 +08:00
parent c7897a15c1
commit be79624db5
9 changed files with 638 additions and 43 deletions
+94
View File
@@ -0,0 +1,94 @@
"""前端审计修复验证(docs/frontend_audit_plan.md §7.2/§7.3)。
覆盖:
- 预览契约返回 HTML(非 JSON)
- 进度消息持久化 role='progress'(重载会话可见)
- 错误消息持久化 role='error'
- /chat_state.js 静态路由可用
"""
from __future__ import annotations
import pytest
from pathlib import Path
from fastapi.testclient import TestClient
from genesis.chat.agent import ChatAgent
from genesis.server.app import create_app
from genesis.server.service import GenesisService
from genesis.server.store import ProjectsStore, SessionStore
_SAMPLE = Path(__file__).resolve().parents[1] / "sample"
@pytest.fixture
def client(tmp_path):
app = create_app(
store=SessionStore(db_path=str(tmp_path / "s.db")),
data_root=str(tmp_path / "data"),
engine="fake",
)
return TestClient(app)
def _upload_core(client, sid):
files = [
("requirements", "requirements_newdev.xlsx", _SAMPLE / "requirements_newdev.xlsx"),
("template", "template_design_ja.docx", _SAMPLE / "template_design_ja.docx"),
("write_instruction", "rules_design_ja.docx", _SAMPLE / "rules_design_ja.docx"),
("rules", "rules_entry_ja.docx", _SAMPLE / "rules_entry_ja.docx"),
]
for ft, name, path in files:
r = client.post(f"/api/sessions/{sid}/files",
data={"file_type": ft},
files={"file": (name, path.read_bytes())})
assert r.status_code == 200, r.text
def test_result_preview_returns_html(client):
"""预览链接应返回渲染后的 HTML,而非 {'html': ...} JSON(§4.3)。"""
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
_upload_core(client, sid)
client.post(f"/api/chat/{sid}/messages", json={"content": "请生成概要设计书"})
r = client.get(f"/api/sessions/{sid}/result/preview")
assert r.status_code == 200
assert "text/html" in r.headers["content-type"]
assert r.text.lstrip().lower().startswith("<!doctype html>")
def test_result_preview_missing_returns_404(client):
"""无结果文档时预览返回 404RESULT_NOT_FOUND)。"""
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
r = client.get(f"/api/sessions/{sid}/result/preview")
assert r.status_code == 404
assert r.json()["detail"]["code"] == "RESULT_NOT_FOUND"
def test_progress_messages_persisted(client):
"""生成全流程的进度条目应以 role='progress' 入库,重载会话可见(§4.6)。"""
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
_upload_core(client, sid)
client.post(f"/api/chat/{sid}/messages", json={"content": "请生成概要设计书"})
msgs = client.get(f"/api/chat/{sid}/messages").json()
assert any(m["role"] == "progress" for m in msgs)
def test_error_role_persisted(tmp_path):
"""错误回复以 role='error' 入库(§4.6 后端 _store_error)。"""
store = SessionStore(db_path=str(tmp_path / "s.db"))
projects = ProjectsStore(db_path=str(tmp_path / "s.db"))
service = GenesisService(store=store, data_root=str(tmp_path / "data"),
engine="fake", projects=projects)
agent = ChatAgent(service=service, fake=True, engine="fake")
sid = service.create_session("u1").session_id
agent._store_error(sid, "解析失败:boom", "generate")
msgs = service.store.list_messages(sid)
assert any(m["role"] == "error" for m in msgs)
def test_chat_state_js_route(client):
"""/chat_state.js 静态路由返回 JS(前端模块可加载,§4.5)。"""
r = client.get("/chat_state.js")
assert r.status_code == 200
assert "application/javascript" in r.headers["content-type"]
assert "GenesisState" in r.text or "autoBindProject" in r.text