- 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%
95 lines
3.8 KiB
Python
95 lines
3.8 KiB
Python
"""前端审计修复验证(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):
|
||
"""无结果文档时预览返回 404(RESULT_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
|