"""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)