Files
writer-work-flow/apps/api/tests/test_templates.py
Yaojia Wang 1921b037a0 fix(backlog): 修评审 3 HIGH — resume chain_key 真值 / 模板空白校验 / 续写链 resume E2E
1. resume_chain 回报真 chain_key:从 awaiting job.result 读回(run 时 _chain_result
   持久化),不再按 job.kind 推断(kind 恒为通用常量 "chain",从不在 SUPPORTED_CHAINS
   → 总错回退 draft_volume)。continue_volume 的 resume 回执现报真值。
2. 模板 title/body 拒纯空白:StringConstraints(strip_whitespace, min_length=1),
   "   " strip 后为空 → 422。
3. 续写链 resume 路径 E2E:continue_volume 第 1 章冲突 → interrupt → awaiting →
   resume 带裁决 → done,断言 resume 回执 / job result chain_key="continue_volume"
   (回归守卫 #1)。

测试:+resume continue_volume 单测(chain_key 真值)+2 模板空白 422 单测 +1 续写链
resume E2E。重生成 TS 客户端(仅 description 文案变化)。
2026-06-23 20:45:06 +02:00

145 lines
4.8 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.

"""F3b GET/POST/DELETE /templates 端点(内存替身,无 DB/无网络)。
覆盖:
- POST 201 + 回显 + 端点 commitGET 列出DELETE 204 + commit
- 空 title → 422、空 body → 422schema `min_length=1`FastAPI 422
- 删不存在 → 404repo.delete 返 False → AppError NOT_FOUND
"""
from __future__ import annotations
import os
import uuid
import httpx
import pytest
from cryptography.fernet import Fernet
from fakes_projects import FakeSession
from ww_core.domain.template_repo import TemplateCreate, TemplateView
class _FakeTemplateRepo:
def __init__(self) -> None:
self.rows: dict[uuid.UUID, TemplateView] = {}
async def create(self, owner_id: uuid.UUID, data: TemplateCreate) -> TemplateView:
view = TemplateView(
id=uuid.uuid4(),
title=data.title,
body=data.body,
category=data.category,
tool_key=data.tool_key,
)
self.rows[view.id] = view
return view
async def list_for_owner(self, owner_id: uuid.UUID) -> list[TemplateView]:
return list(self.rows.values())
async def delete(self, owner_id: uuid.UUID, template_id: uuid.UUID) -> bool:
if template_id in self.rows:
del self.rows[template_id]
return True
return False
def _make_client() -> tuple[httpx.AsyncClient, _FakeTemplateRepo, FakeSession]:
os.environ.setdefault("CREDENTIAL_ENC_KEY", Fernet.generate_key().decode())
from ww_api.main import create_app
from ww_api.services.project_deps import get_template_repo
from ww_db import get_session
repo = _FakeTemplateRepo()
session = FakeSession()
app = create_app()
app.dependency_overrides[get_template_repo] = lambda: repo
app.dependency_overrides[get_session] = lambda: session
transport = httpx.ASGITransport(app=app)
client = httpx.AsyncClient(transport=transport, base_url="http://test")
return client, repo, session
@pytest.mark.asyncio
async def test_create_template_returns_201_and_commits() -> None:
client, repo, session = _make_client()
async with client:
resp = await client.post(
"/templates",
json={"title": "爽文开局", "body": "主角穿越...", "tool_key": "expand"},
)
assert resp.status_code == 201
body = resp.json()
assert body["title"] == "爽文开局"
assert body["body"] == "主角穿越..."
assert body["tool_key"] == "expand"
assert session.commits == 1
assert len(repo.rows) == 1
@pytest.mark.asyncio
async def test_list_templates_returns_created() -> None:
client, repo, _session = _make_client()
async with client:
await client.post("/templates", json={"title": "a", "body": "b"})
resp = await client.get("/templates")
assert resp.status_code == 200
items = resp.json()
assert len(items) == 1
assert items[0]["title"] == "a"
@pytest.mark.asyncio
async def test_create_template_empty_title_returns_422() -> None:
client, _repo, _session = _make_client()
async with client:
resp = await client.post("/templates", json={"title": "", "body": "b"})
assert resp.status_code == 422
@pytest.mark.asyncio
async def test_create_template_empty_body_returns_422() -> None:
client, _repo, _session = _make_client()
async with client:
resp = await client.post("/templates", json={"title": "a", "body": ""})
assert resp.status_code == 422
@pytest.mark.asyncio
async def test_create_template_whitespace_only_title_returns_422() -> None:
# strip 后为空 → min_length=1 不满足 → 422防纯空白标题审评 #2
client, _repo, _session = _make_client()
async with client:
resp = await client.post("/templates", json={"title": " ", "body": "b"})
assert resp.status_code == 422
@pytest.mark.asyncio
async def test_create_template_whitespace_only_body_returns_422() -> None:
client, _repo, _session = _make_client()
async with client:
resp = await client.post("/templates", json={"title": "a", "body": " "})
assert resp.status_code == 422
@pytest.mark.asyncio
async def test_delete_template_returns_204_and_commits() -> None:
client, repo, session = _make_client()
async with client:
created = await client.post("/templates", json={"title": "a", "body": "b"})
tid = created.json()["id"]
resp = await client.delete(f"/templates/{tid}")
assert resp.status_code == 204
assert session.commits == 2
assert repo.rows == {}
@pytest.mark.asyncio
async def test_delete_missing_template_returns_404() -> None:
client, _repo, session = _make_client()
async with client:
resp = await client.delete(f"/templates/{uuid.uuid4()}")
assert resp.status_code == 404
assert resp.json()["error"]["code"] == "NOT_FOUND"
assert session.commits == 0