feat: M4 文风 + M5 生成/多provider/Skill + Kimi Code 订阅接入 + 本地联调修复
M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。 M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变); 网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则; 前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。 K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径; coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。 本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400); GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化; 字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。 门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
87
apps/api/tests/test_rules.py
Normal file
87
apps/api/tests/test_rules.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""T5.5 POST /projects/:id/rules 端点(内存替身,无 DB/无网络)。
|
||||
|
||||
覆盖:
|
||||
- 201 + 回显 level/content + 端点 commit;
|
||||
- 非法 level → 422(Pydantic Literal 校验,FastAPI 422);
|
||||
- 空 content → 422。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from fakes_projects import FakeSession
|
||||
from ww_core.domain.rule_repo import RuleWriteView
|
||||
|
||||
|
||||
class _FakeRuleWriteRepo:
|
||||
def __init__(self) -> None:
|
||||
self.rows: list[RuleWriteView] = []
|
||||
|
||||
async def create(self, project_id: uuid.UUID, *, level: str, content: str) -> RuleWriteView:
|
||||
view = RuleWriteView(project_id=project_id, level=level, content=content)
|
||||
self.rows.append(view)
|
||||
return view
|
||||
|
||||
|
||||
def _make_client() -> tuple[httpx.AsyncClient, _FakeRuleWriteRepo, FakeSession]:
|
||||
import os
|
||||
|
||||
os.environ.setdefault("CREDENTIAL_ENC_KEY", "x" * 44)
|
||||
from ww_api.main import create_app
|
||||
from ww_api.services.project_deps import get_rule_write_repo
|
||||
from ww_db import get_session
|
||||
|
||||
repo = _FakeRuleWriteRepo()
|
||||
session = FakeSession()
|
||||
|
||||
app = create_app()
|
||||
app.dependency_overrides[get_rule_write_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_rule_returns_201_and_commits() -> None:
|
||||
client, repo, session = _make_client()
|
||||
pid = uuid.uuid4()
|
||||
async with client:
|
||||
resp = await client.post(
|
||||
f"/projects/{pid}/rules",
|
||||
json={"level": "project", "content": "主角不许中途复活"},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
assert body["level"] == "project"
|
||||
assert body["content"] == "主角不许中途复活"
|
||||
assert session.commits == 1
|
||||
assert len(repo.rows) == 1
|
||||
assert repo.rows[0].project_id == pid
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_rule_invalid_level_returns_422() -> None:
|
||||
client, _repo, _session = _make_client()
|
||||
pid = uuid.uuid4()
|
||||
async with client:
|
||||
resp = await client.post(
|
||||
f"/projects/{pid}/rules",
|
||||
json={"level": "nonsense", "content": "x"},
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_rule_empty_content_returns_422() -> None:
|
||||
client, _repo, _session = _make_client()
|
||||
pid = uuid.uuid4()
|
||||
async with client:
|
||||
resp = await client.post(
|
||||
f"/projects/{pid}/rules",
|
||||
json={"level": "global", "content": ""},
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
Reference in New Issue
Block a user