feat: M3 — 伏笔账本 + 节奏引擎 + 大纲(含并发记账 bugfix)
- 伏笔账本:纯函数状态机(OPEN/PARTIAL/CLOSED/OVERDUE) + ForeshadowLedger repo;验收后到期扫描(BackgroundTask 自建 session 置 OVERDUE);登记/状态变更端点 - 节奏 + 三审齐:foreshadow-analyst + pace-checker 并入 LangGraph 并行审(REVIEW_SPECS),collect 分列落 chapter_reviews(conflicts/foreshadow_sug/pace),review SSE 加 foreshadow/pace 事件 - 大纲:outliner Agent 产 OutlineResult(含 foreshadow_windows),POST /outline 逐章 upsert outline 表;GET /foreshadow?status= 看板 - 前端:伏笔四泳道看板(OVERDUE 琥珀) + 大纲编辑器(窗口徽标) + 节奏节拍图(▁▃▅) + 审稿页消费 foreshadow/pace SSE - bugfix(T3.8):并行三审共用请求 session 记账触发 'Session is already flushing' → foreshadow/pace 静默丢失;SqlAlchemyLedgerSink.record 改 add-only(靠端点/事务 commit),加并发回归测试 - M3 E2E:真实 DB + mock 网关零 token 走通 埋设→进展→验收后扫描 OVERDUE→看板 + 大纲含窗口 + 三审齐 SSE/留痕;E2E 暴露并钉住上述 bug - 门禁绿:mypy 111 / pytest 228(0 xfailed) / alembic 无漂移;前端 gen:api/lint/tsc/vitest 69/build
This commit is contained in:
117
packages/agents/tests/test_outliner_spec.py
Normal file
117
packages/agents/tests/test_outliner_spec.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""T3.4 大纲 Agent 契约测试:OutlineResult schema + outliner_spec 声明(C6 扩)。
|
||||
|
||||
契约测试——构造符合 schema 的 mock 响应,校验伏笔窗口字段、章节结构、
|
||||
声明式 writes(验收/T3.5 才真写库,不变量 #3)。不联网、无 DB。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
from ww_agents import (
|
||||
AgentSpec,
|
||||
ForeshadowWindow,
|
||||
OutlineChapter,
|
||||
OutlineResult,
|
||||
outliner_spec,
|
||||
)
|
||||
|
||||
|
||||
def test_outline_result_parses_mock_response() -> None:
|
||||
# Arrange:模拟网关 instructor 校验后的结构化大纲产出
|
||||
mock = {
|
||||
"chapters": [
|
||||
{
|
||||
"no": 1,
|
||||
"beats": ["主角登场,立下目标", "埋下身世伏笔"],
|
||||
"foreshadow_windows": [
|
||||
{
|
||||
"code": "FS-BIRTH",
|
||||
"plant_chapter": 1,
|
||||
"expected_close_from": 20,
|
||||
"expected_close_to": 30,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"no": 2,
|
||||
"beats": ["遭遇对手"],
|
||||
"foreshadow_windows": [],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
# Act
|
||||
outline = OutlineResult.model_validate(mock)
|
||||
|
||||
# Assert
|
||||
assert len(outline.chapters) == 2
|
||||
assert outline.chapters[0].no == 1
|
||||
assert outline.chapters[0].beats == ["主角登场,立下目标", "埋下身世伏笔"]
|
||||
window = outline.chapters[0].foreshadow_windows[0]
|
||||
assert window.code == "FS-BIRTH"
|
||||
assert window.plant_chapter == 1
|
||||
assert window.expected_close_from == 20
|
||||
assert window.expected_close_to == 30
|
||||
|
||||
|
||||
def test_outline_chapter_defaults_empty_windows() -> None:
|
||||
chapter = OutlineChapter.model_validate({"no": 3, "beats": ["收束支线"]})
|
||||
assert chapter.foreshadow_windows == []
|
||||
|
||||
|
||||
def test_outline_result_defaults_to_empty_chapters() -> None:
|
||||
assert OutlineResult().chapters == []
|
||||
|
||||
|
||||
def test_foreshadow_window_optional_bounds_default_none() -> None:
|
||||
window = ForeshadowWindow.model_validate({"code": "FS-X"})
|
||||
assert window.plant_chapter is None
|
||||
assert window.expected_close_from is None
|
||||
assert window.expected_close_to is None
|
||||
|
||||
|
||||
def test_foreshadow_window_requires_code() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
ForeshadowWindow.model_validate({"plant_chapter": 1})
|
||||
|
||||
|
||||
def test_outline_chapter_requires_no() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
OutlineChapter.model_validate({"beats": ["x"]})
|
||||
|
||||
|
||||
def test_outliner_spec_is_analyst_tier() -> None:
|
||||
assert outliner_spec.tier == "analyst"
|
||||
assert outliner_spec.name == "outliner"
|
||||
|
||||
|
||||
def test_outliner_spec_declares_expected_reads() -> None:
|
||||
assert outliner_spec.reads == [
|
||||
"projects",
|
||||
"foreshadow",
|
||||
"characters",
|
||||
"world_entities",
|
||||
]
|
||||
|
||||
|
||||
def test_outliner_spec_declares_outline_write() -> None:
|
||||
# 声明式 writes(经验收/T3.5 才真写库,不变量 #3)
|
||||
assert outliner_spec.writes == ["outline"]
|
||||
|
||||
|
||||
def test_outliner_spec_output_schema_is_outline_result() -> None:
|
||||
assert outliner_spec.output_schema is OutlineResult
|
||||
|
||||
|
||||
def test_outliner_spec_has_nonempty_system_prompt() -> None:
|
||||
assert outliner_spec.system_prompt.strip()
|
||||
|
||||
|
||||
def test_outliner_spec_is_immutable() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
outliner_spec.tier = "writer"
|
||||
|
||||
|
||||
def test_outliner_spec_is_agent_spec() -> None:
|
||||
assert isinstance(outliner_spec, AgentSpec)
|
||||
Reference in New Issue
Block a user