| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- import json
- import sys
- from pathlib import Path
- def test_extract_state_summary_accepts_dominant_key(tmp_path):
- scripts_dir = Path(__file__).resolve().parents[2]
- if str(scripts_dir) not in sys.path:
- sys.path.insert(0, str(scripts_dir))
- from extract_chapter_context import extract_state_summary
- state = {
- "progress": {"current_chapter": 12, "total_words": 12345},
- "protagonist_state": {
- "power": {"realm": "筑基", "layer": 2},
- "location": "宗门",
- "golden_finger": {"name": "系统", "level": 1},
- },
- "strand_tracker": {
- "history": [
- {"chapter": 10, "dominant": "quest"},
- {"chapter": 11, "dominant": "fire"},
- ]
- },
- }
- webnovel_dir = tmp_path / ".webnovel"
- webnovel_dir.mkdir(parents=True, exist_ok=True)
- (webnovel_dir / "state.json").write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
- text = extract_state_summary(tmp_path)
- assert "Ch10:quest" in text
- assert "Ch11:fire" in text
- def test_extract_chapter_outline_supports_hyphen_filename(tmp_path):
- scripts_dir = Path(__file__).resolve().parents[2]
- if str(scripts_dir) not in sys.path:
- sys.path.insert(0, str(scripts_dir))
- from extract_chapter_context import extract_chapter_outline
- outline_dir = tmp_path / "大纲"
- outline_dir.mkdir(parents=True, exist_ok=True)
- (outline_dir / "第1卷-详细大纲.md").write_text("### 第1章:测试标题\n测试大纲", encoding="utf-8")
- outline = extract_chapter_outline(tmp_path, 1)
- assert "### 第1章:测试标题" in outline
- assert "测试大纲" in outline
- def test_extract_chapter_outline_prefers_state_volume_mapping(tmp_path):
- scripts_dir = Path(__file__).resolve().parents[2]
- if str(scripts_dir) not in sys.path:
- sys.path.insert(0, str(scripts_dir))
- from extract_chapter_context import extract_chapter_outline
- webnovel_dir = tmp_path / ".webnovel"
- webnovel_dir.mkdir(parents=True, exist_ok=True)
- state = {
- "progress": {
- "volumes_planned": [
- {"volume": 1, "chapters_range": "1-10"},
- {"volume": 2, "chapters_range": "11-20"},
- ]
- }
- }
- (webnovel_dir / "state.json").write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
- outline_dir = tmp_path / "大纲"
- outline_dir.mkdir(parents=True, exist_ok=True)
- (outline_dir / "第2卷-详细大纲.md").write_text("### 第12章:V2标题\nV2大纲", encoding="utf-8")
- outline = extract_chapter_outline(tmp_path, 12)
- assert "### 第12章:V2标题" in outline
- assert "V2大纲" in outline
- def test_extract_chapter_outline_falls_back_when_state_has_no_match(tmp_path):
- scripts_dir = Path(__file__).resolve().parents[2]
- if str(scripts_dir) not in sys.path:
- sys.path.insert(0, str(scripts_dir))
- from extract_chapter_context import extract_chapter_outline
- webnovel_dir = tmp_path / ".webnovel"
- webnovel_dir.mkdir(parents=True, exist_ok=True)
- state = {"progress": {"volumes_planned": [{"volume": 1, "chapters_range": "1-10"}]}}
- (webnovel_dir / "state.json").write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
- outline_dir = tmp_path / "大纲"
- outline_dir.mkdir(parents=True, exist_ok=True)
- (outline_dir / "第2卷-详细大纲.md").write_text("### 第60章:V2标题\nV2大纲", encoding="utf-8")
- outline = extract_chapter_outline(tmp_path, 60)
- assert "### 第60章:V2标题" in outline
- assert "V2大纲" in outline
- def test_build_chapter_context_payload_includes_contract_sections(tmp_path):
- scripts_dir = Path(__file__).resolve().parents[2]
- if str(scripts_dir) not in sys.path:
- sys.path.insert(0, str(scripts_dir))
- from extract_chapter_context import build_chapter_context_payload
- from data_modules.config import DataModulesConfig
- from data_modules.index_manager import IndexManager, ChapterReadingPowerMeta, ReviewMetrics
- cfg = DataModulesConfig.from_project_root(tmp_path)
- cfg.ensure_dirs()
- state = {
- "project": {"genre": "xuanhuan"},
- "progress": {"current_chapter": 3, "total_words": 9000},
- "protagonist_state": {
- "power": {"realm": "筑基", "layer": 2},
- "location": "宗门",
- "golden_finger": {"name": "系统", "level": 1},
- },
- "strand_tracker": {"history": [{"chapter": 2, "dominant": "quest"}]},
- "chapter_meta": {},
- "disambiguation_warnings": [],
- "disambiguation_pending": [],
- }
- (cfg.webnovel_dir / "state.json").write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
- summaries_dir = cfg.webnovel_dir / "summaries"
- summaries_dir.mkdir(parents=True, exist_ok=True)
- (summaries_dir / "ch0002.md").write_text("## 剧情摘要\n上一章总结", encoding="utf-8")
- outline_dir = tmp_path / "大纲"
- outline_dir.mkdir(parents=True, exist_ok=True)
- (outline_dir / "第1卷 详细大纲.md").write_text("### 第3章:测试标题\n测试大纲", encoding="utf-8")
- refs_dir = tmp_path / ".claude" / "references"
- refs_dir.mkdir(parents=True, exist_ok=True)
- (refs_dir / "genre-profiles.md").write_text("## xuanhuan\n- 升级线清晰", encoding="utf-8")
- (refs_dir / "reading-power-taxonomy.md").write_text("## xuanhuan\n- 悬念钩优先", encoding="utf-8")
- idx = IndexManager(cfg)
- idx.save_chapter_reading_power(
- ChapterReadingPowerMeta(chapter=2, hook_type="悬念钩", hook_strength="strong", coolpoint_patterns=["身份掉马"])
- )
- idx.save_review_metrics(
- ReviewMetrics(start_chapter=1, end_chapter=2, overall_score=71, dimension_scores={"plot": 71})
- )
- payload = build_chapter_context_payload(tmp_path, 3)
- assert payload["context_contract_version"] == "v2"
- assert payload.get("context_weight_stage") in {"early", "mid", "late"}
- assert "writing_guidance" in payload
- assert isinstance(payload["writing_guidance"].get("guidance_items"), list)
- assert isinstance(payload["writing_guidance"].get("checklist"), list)
- assert isinstance(payload["writing_guidance"].get("checklist_score"), dict)
- assert payload["genre_profile"].get("genre") == "xuanhuan"
- def test_render_text_contains_writing_guidance_section(tmp_path):
- scripts_dir = Path(__file__).resolve().parents[2]
- if str(scripts_dir) not in sys.path:
- sys.path.insert(0, str(scripts_dir))
- from extract_chapter_context import _render_text
- payload = {
- "chapter": 10,
- "outline": "测试大纲",
- "previous_summaries": ["### 第9章摘要\n上一章"],
- "state_summary": "状态",
- "context_contract_version": "v2",
- "context_weight_stage": "early",
- "reader_signal": {"review_trend": {"overall_avg": 72}, "low_score_ranges": [{"start_chapter": 8, "end_chapter": 9}]},
- "genre_profile": {
- "genre": "xuanhuan",
- "genres": ["xuanhuan", "realistic"],
- "composite_hints": ["以玄幻主线推进,同时保留现实议题表达"],
- "reference_hints": ["升级线清晰"],
- },
- "writing_guidance": {
- "guidance_items": ["先修低分", "钩子差异化"],
- "checklist": [
- {
- "id": "fix_low_score_range",
- "label": "修复低分区间问题",
- "weight": 1.4,
- "required": True,
- "source": "reader_signal.low_score_ranges",
- "verify_hint": "至少完成1处冲突升级",
- }
- ],
- "checklist_score": {
- "score": 81.5,
- "completion_rate": 0.66,
- "required_completion_rate": 0.75,
- },
- },
- }
- text = _render_text(payload)
- assert "## 写作执行建议" in text
- assert "先修低分" in text
- assert "## Contract (v2)" in text
- assert "- 上下文阶段权重: early" in text
- assert "### 执行检查清单(可评分)" in text
- assert "- 总权重: 1.40" in text
- assert "[必做][w=1.4] 修复低分区间问题" in text
- assert "### 执行评分" in text
- assert "- 评分: 81.5" in text
- assert "- 复合题材: xuanhuan + realistic" in text
|