"""立项方案生成器(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