Files
writer-work-flow/packages/agents/tests/test_project_plan_spec.py
Yaojia Wang 02d19019f6 feat(web): AI 立项方案生成+预填(种子门控 + 按字段接受)
向导阶段(project 未建)一键推演结构化书级蓝图预填立项向导。

- 新 SPEC project_plan_spec(analyst 档,纯预览 reads/writes=())+ schema
  ProjectPlanResult(书名候选 + 时空背景/叙事结构/故事核心/结局设计/基调,
  全字段默认值守解析韧性)+ 注册 SCHEMA_CATALOG;计数三处 21→22 + regen golden。
- 端点 POST /skills/project-plan/generate(不带 project 前缀,绕开 toolbox 404):
  请求体序列化向导草稿为 project_context,复用 build_brief_context + run_generator;
  种子门控(缺 genre/logline→422);请求 schema 加 max_length 上界。
- run_generator/_build_request 的 project_id 放宽 uuid.UUID|None(向导无 project)。
- 前端 wizard.ts 加 mapPlanResultToWizardForm(错配字段折进 premise 不静默丢)+
  applyPlanPatch(仅回填空字段、保护已编辑)+ useProjectPlan hook + PlanAssistant。
2026-07-06 16:26:08 +02:00

98 lines
3.7 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.

"""立项方案生成器project-plan契约测试schema + spec灵感⑤
契约测试——构造符合 schema 的 mock 响应校验字段默认值解析韧性、tier、只读权限。
project-plan`{title_candidates:[str], setting, narrative_structure, story_core,
ending_design, tone}`分析档reads=()writes=() 纯预览)。不联网、无 DB。
"""
from __future__ import annotations
import pytest
from pydantic import ValidationError
from ww_agents import (
SPECS,
AgentSpec,
ProjectPlanResult,
project_plan_spec,
)
# ---- ProjectPlanResult schema ----
def test_project_plan_parses_mock_response() -> None:
# Arrange模拟网关 instructor 校验后的结构化立项方案产出
mock = {
"title_candidates": ["逐光而行", "剑试九霄", "凡骨录"],
"setting": "上古仙门倾轧的洞天世界",
"narrative_structure": "黄金三章立钩 → 拜师入门 → 宗门大比爆发",
"story_core": "废柴少年觉醒禁忌血脉,在仙门倾轧中逆势封神,代价是逐渐失去人性",
"ending_design": "主角登顶却选择归隐,核心冲突以自我救赎收束(正剧)",
"tone": "热血 + 悲怆",
}
# Act
result = ProjectPlanResult.model_validate(mock)
# Assert
assert result.title_candidates == ["逐光而行", "剑试九霄", "凡骨录"]
assert result.setting == "上古仙门倾轧的洞天世界"
assert result.narrative_structure.startswith("黄金三章")
assert "逆势封神" in result.story_core
assert result.ending_design.endswith("(正剧)")
assert result.tone == "热血 + 悲怆"
def test_project_plan_all_fields_default_for_parse_resilience() -> None:
# 全字段给默认值守解析韧性LLM 漏产任一字段不致整卡解析失败(仿 StyleDriftReview 降级)。
result = ProjectPlanResult()
assert result.title_candidates == []
assert result.setting == ""
assert result.narrative_structure == ""
assert result.story_core == ""
assert result.ending_design == ""
assert result.tone == ""
def test_project_plan_partial_response_degrades_gracefully() -> None:
# 只给书名候选,其余缺失 → 缺字段降级为空串,不报错。
result = ProjectPlanResult.model_validate({"title_candidates": ["书名甲"]})
assert result.title_candidates == ["书名甲"]
assert result.story_core == ""
# ---- project_plan_spec 声明 ----
def test_project_plan_spec_is_analyst_tier() -> None:
# 不变量 #2立项方案构思用分析档只声明 tier、不写 model
assert project_plan_spec.tier == "analyst"
assert project_plan_spec.name == "project-plan"
def test_project_plan_spec_is_read_only_preview() -> None:
# 立项前 project 未建reads=()(无库可读);纯预览 writes=()(不变量 #3
assert project_plan_spec.reads == ()
assert project_plan_spec.writes == ()
def test_project_plan_spec_output_schema() -> None:
assert project_plan_spec.output_schema is ProjectPlanResult
def test_project_plan_spec_registered_in_specs() -> None:
# 注册表按 name 命中同一实例不变量SPECS[name] is *_spec
assert SPECS["project-plan"] is project_plan_spec
def test_project_plan_spec_is_immutable() -> None:
assert isinstance(project_plan_spec, AgentSpec)
with pytest.raises(ValidationError):
project_plan_spec.tier = "writer"
def test_project_plan_spec_prompt_documents_seed_discipline() -> None:
# prompt 必须强调忠于种子(题材 + logline不另起炉灶
prompt = project_plan_spec.system_prompt
assert "logline" in prompt
assert "种子" in prompt