fix(qa): 修 QA C1/H1/H2——写章/规则缺项目校验 + 立项向导字段覆盖
C1 (CRITICAL) stream_draft:对不存在 project 流式写章先 fail-fast 404, 否则非法 id 静默烧一次付费/限流 LLM 调用并返 200。在触网关前查 project_repo.get。 H2 (HIGH) create_rule:给不存在 project 加规则原 FK 违例逃逸成 500 → 改为入库前 校验项目存在返 404(仿 chain/_require_project)。 H1 (HIGH) ProjectWizard:第3步「立意」与第4步「主角/金手指」原共用 form.premise 互相覆盖丢数据 → 新增独立 form.protagonist,toCreateRequest 合并两段进 premise (M1 projects 表仍只有 premise,不编造 API)。 回归测试: - test_projects.py:stream_draft 不存在 project → 404 且网关零调用;已有 draft 用例改 seed 真项目。 - test_rules.py:create_rule 不存在 project → 404 不写库;已有用例 seed 真项目。 - wizard.test.ts:premise+protagonist 合并不互相覆盖(2 例)。 门禁绿:ruff/format clean · mypy 210 · pytest 749 · 前端 tsc/lint/vitest 干净。
This commit is contained in:
@@ -11,6 +11,8 @@ import httpx
|
||||
import pytest
|
||||
from cryptography.fernet import Fernet
|
||||
from fakes_projects import FakeChapterRepo, FakeProjectRepo, FakeWriterGateway
|
||||
from ww_api.services.credentials import STUB_OWNER_ID
|
||||
from ww_core.domain.project_repo import ProjectView
|
||||
from ww_core.domain.repositories import (
|
||||
CharacterView,
|
||||
DigestView,
|
||||
@@ -122,6 +124,16 @@ def _make_client(
|
||||
return client, project_repo, chapter_repo, gateway
|
||||
|
||||
|
||||
def _seed_project(project_repo: FakeProjectRepo) -> uuid.UUID:
|
||||
"""Seed 一个属于 STUB_OWNER_ID 的项目,返回其 pid。
|
||||
|
||||
stream_draft 现先校验项目存在(QA C1),流式用例须用已存在项目,否则 404。
|
||||
"""
|
||||
pid = uuid.uuid4()
|
||||
project_repo.rows[pid] = (STUB_OWNER_ID, ProjectView(id=pid, title="测试"))
|
||||
return pid
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_project_returns_201() -> None:
|
||||
client, _, _, _ = _make_client()
|
||||
@@ -180,8 +192,8 @@ async def test_create_project_rejects_blank_title() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_draft_stream_yields_sse_tokens_and_done() -> None:
|
||||
gateway = FakeWriterGateway(chunks=["阿福", "走进门。"])
|
||||
client, _, _, _ = _make_client(gateway=gateway)
|
||||
pid = uuid.uuid4()
|
||||
client, project_repo, _, _ = _make_client(gateway=gateway)
|
||||
pid = _seed_project(project_repo)
|
||||
async with client:
|
||||
resp = await client.post(f"/projects/{pid}/chapters/1/draft")
|
||||
assert resp.status_code == 200
|
||||
@@ -200,8 +212,8 @@ async def test_draft_stream_yields_sse_tokens_and_done() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_draft_stream_threads_directive_into_volatile() -> None:
|
||||
gateway = FakeWriterGateway(chunks=["正文"])
|
||||
client, _, _, _ = _make_client(gateway=gateway)
|
||||
pid = uuid.uuid4()
|
||||
client, project_repo, _, _ = _make_client(gateway=gateway)
|
||||
pid = _seed_project(project_repo)
|
||||
async with client:
|
||||
resp = await client.post(
|
||||
f"/projects/{pid}/chapters/1/draft", json={"directive": "多写战斗"}
|
||||
@@ -215,8 +227,8 @@ async def test_draft_stream_threads_directive_into_volatile() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_draft_stream_backward_compatible_without_body() -> None:
|
||||
gateway = FakeWriterGateway(chunks=["正文"])
|
||||
client, _, _, _ = _make_client(gateway=gateway)
|
||||
pid = uuid.uuid4()
|
||||
client, project_repo, _, _ = _make_client(gateway=gateway)
|
||||
pid = _seed_project(project_repo)
|
||||
async with client:
|
||||
resp = await client.post(f"/projects/{pid}/chapters/1/draft")
|
||||
assert resp.status_code == 200
|
||||
@@ -229,8 +241,8 @@ async def test_draft_stream_maps_error_to_sse_error_event() -> None:
|
||||
from ww_shared import AppError
|
||||
|
||||
gateway = FakeWriterGateway(chunks=["半段"], error=AppError(ErrorCode.LLM_UNAVAILABLE, "boom"))
|
||||
client, _, _, _ = _make_client(gateway=gateway)
|
||||
pid = uuid.uuid4()
|
||||
client, project_repo, _, _ = _make_client(gateway=gateway)
|
||||
pid = _seed_project(project_repo)
|
||||
async with client:
|
||||
resp = await client.post(f"/projects/{pid}/chapters/1/draft")
|
||||
assert resp.status_code == 200
|
||||
@@ -240,6 +252,18 @@ async def test_draft_stream_maps_error_to_sse_error_event() -> None:
|
||||
assert ErrorCode.LLM_UNAVAILABLE in text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_draft_stream_unknown_project_returns_404_without_calling_gateway() -> None:
|
||||
# QA C1 回归:对不存在的 project 流式写章必须 404,且绝不触网关(不烧 LLM 调用)。
|
||||
gateway = FakeWriterGateway(chunks=["不该被生成"])
|
||||
client, _project_repo, _, _ = _make_client(gateway=gateway)
|
||||
async with client:
|
||||
resp = await client.post(f"/projects/{uuid.uuid4()}/chapters/1/draft")
|
||||
assert resp.status_code == 404
|
||||
assert resp.json()["error"]["code"] == ErrorCode.NOT_FOUND
|
||||
assert len(gateway.requests) == 0 # 未触达网关
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_draft_is_idempotent() -> None:
|
||||
chapter_repo = FakeChapterRepo()
|
||||
|
||||
Reference in New Issue
Block a user