324 lines
13 KiB
Python
324 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
import json
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
if str(REPO_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
import scripts.dr as dr
|
|
|
|
|
|
def test_parser_exposes_python_core_commands() -> None:
|
|
parser = dr.build_parser()
|
|
|
|
assert parser.parse_args(["init", "ADC 市场研究"]).cmd == "init"
|
|
assert parser.parse_args(["approve", "demo"]).cmd == "approve"
|
|
assert parser.parse_args(["frame", "demo"]).cmd == "frame"
|
|
assert parser.parse_args(["review", "demo"]).cmd == "review"
|
|
assert parser.parse_args(["skills", "list"]).cmd == "skills"
|
|
assert parser.parse_args(["methods", "list"]).cmd == "methods"
|
|
assert parser.parse_args(["research", "demo", "--workers", "6", "--dry-run"]).cmd == "research"
|
|
assert parser.parse_args(["research", "demo", "--execute-packets"]).execute_packets is True
|
|
assert parser.parse_args(["research", "demo", "--allow-search-fallback"]).allow_search_fallback is True
|
|
assert parser.parse_args(["research", "demo", "--build-briefs"]).build_briefs is True
|
|
assert parser.parse_args(["research", "demo", "--assemble-chapters"]).assemble_chapters is True
|
|
assert parser.parse_args(["run", "demo", "--dry-run"]).cmd == "run"
|
|
assert parser.parse_args(["finalize", "demo", "--legacy-translate", "--dry-run"]).legacy_translate is True
|
|
|
|
|
|
def test_models_json_includes_task_types(capsys) -> None:
|
|
args = dr.build_parser().parse_args(["models", "--profile", "medium", "--json"])
|
|
|
|
assert dr.cmd_models(args) == 0
|
|
|
|
out = capsys.readouterr().out
|
|
assert '"task_types"' in out
|
|
assert '"evidence_packet"' in out
|
|
|
|
|
|
def test_prompt_uses_canonical_codex_template(capsys) -> None:
|
|
args = dr.build_parser().parse_args(["prompt", "dr-run", "demo"])
|
|
|
|
assert dr.cmd_prompt(args) == 0
|
|
|
|
out = capsys.readouterr().out
|
|
assert "surface adapter for v0.20" in out
|
|
assert "Do not spawn Codex subagents" in out
|
|
|
|
|
|
def test_research_build_briefs_does_not_overwrite_existing_packets(tmp_path: Path) -> None:
|
|
project = tmp_path / "project"
|
|
(project / "phase1").mkdir(parents=True)
|
|
(project / "phase2/packets").mkdir(parents=True)
|
|
(project / "manifest.json").write_text(
|
|
'{"research_method": "mckinsey_market", "phase1": {"approved": true}, "phase2": {}}\n',
|
|
encoding="utf-8",
|
|
)
|
|
(project / "phase1/framework.md").write_text(
|
|
"## 第1章 临床证据正在重塑需求判断\n\n研究思路。",
|
|
encoding="utf-8",
|
|
)
|
|
packet_path = project / "phase2/packets/ch01-literature.json"
|
|
packet = {
|
|
"task_id": "ch01-literature",
|
|
"claims": [{"claim": "真实证据不能被 skeleton 覆盖", "source_ids": ["src_001"]}],
|
|
"evidence_items": [{"source_id": "src_001", "summary": "证据"}],
|
|
"counter_evidence": [{"claim": "限制", "source_ids": ["src_002"]}],
|
|
"source_ids": ["src_001", "src_002"],
|
|
"sources": [
|
|
{"id": "src_001", "title": "来源1", "url": "https://example.com/1"},
|
|
{"id": "src_002", "title": "来源2", "url": "https://example.com/2"},
|
|
],
|
|
"source_quality_notes": ["src_001 Tier 1"],
|
|
"open_questions": [],
|
|
"raw_quotes_or_notes": [],
|
|
}
|
|
packet_path.write_text(json.dumps(packet, ensure_ascii=False), encoding="utf-8")
|
|
|
|
args = dr.build_parser().parse_args(["research", str(project), "--build-briefs"])
|
|
assert dr.cmd_research(args) == 0
|
|
|
|
assert "真实证据不能被 skeleton 覆盖" in packet_path.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_codex_native_profile_does_not_claim_python_core_model_execution(tmp_path: Path) -> None:
|
|
project = tmp_path / "project"
|
|
(project / "phase1").mkdir(parents=True)
|
|
(project / "manifest.json").write_text(
|
|
'{"research_method": "mckinsey_market", "phase1": {"approved": true}, "phase2": {}}\n',
|
|
encoding="utf-8",
|
|
)
|
|
(project / "phase1/framework.md").write_text(
|
|
"## 第1章 临床证据正在重塑需求判断\n\n研究思路。",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
args = dr.build_parser().parse_args(["research", str(project), "--profile", "codex_native", "--execute-packets"])
|
|
|
|
try:
|
|
dr.cmd_research(args)
|
|
except SystemExit as exc:
|
|
assert "not Codex App built-in models" in str(exc)
|
|
else:
|
|
raise AssertionError("codex_native must not execute through Python external clients")
|
|
|
|
|
|
def test_packet_state_counts_ignores_stale_errors_for_ready_packets(tmp_path: Path) -> None:
|
|
project = tmp_path / "project"
|
|
(project / "phase2/packets").mkdir(parents=True)
|
|
(project / "phase2/packet_errors").mkdir(parents=True)
|
|
packet = {
|
|
"task_id": "ch01-literature",
|
|
"claims": [{"claim": "已补齐", "source_ids": ["src_001"]}],
|
|
"evidence_items": [{"source_id": "src_001", "summary": "证据"}],
|
|
"counter_evidence": [{"claim": "限制", "source_ids": ["src_001"]}],
|
|
"source_ids": ["src_001"],
|
|
"source_quality_notes": ["Tier 1"],
|
|
"open_questions": [],
|
|
"raw_quotes_or_notes": [],
|
|
"sources": [{"id": "src_001", "title": "来源", "url": "https://example.com"}],
|
|
}
|
|
(project / "phase2/packets/ch01-literature.json").write_text(
|
|
json.dumps(packet, ensure_ascii=False),
|
|
encoding="utf-8",
|
|
)
|
|
(project / "phase2/packet_errors/ch01-literature.json").write_text(
|
|
'{"status":"failed"}\n',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
counts = dr.packet_state_counts(project)
|
|
|
|
assert counts["ready"] == 1
|
|
assert counts["errors"] == 0
|
|
assert counts["stale_errors"] == 1
|
|
|
|
|
|
def test_run_existing_project_delegates_to_research_without_missing_args(tmp_path: Path) -> None:
|
|
project = tmp_path / "project"
|
|
(project / "phase1").mkdir(parents=True)
|
|
(project / "manifest.json").write_text(
|
|
'{"research_method": "mckinsey_market", "phase1": {"approved": true}, "phase2": {}}\n',
|
|
encoding="utf-8",
|
|
)
|
|
(project / "phase1/framework.md").write_text(
|
|
"## 第1章 临床证据正在重塑需求判断\n\n研究思路。",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
args = dr.build_parser().parse_args(["run", str(project), "--workers", "2"])
|
|
|
|
assert dr.cmd_run(args) == 0
|
|
assert (project / "phase2/task_cards.json").exists()
|
|
|
|
|
|
def test_research_requires_phase1_approval_unless_overridden(tmp_path: Path) -> None:
|
|
project = tmp_path / "project"
|
|
(project / "phase1").mkdir(parents=True)
|
|
(project / "manifest.json").write_text(
|
|
'{"research_method": "mckinsey_market", "phase1": {"approved": false}, "phase2": {}}\n',
|
|
encoding="utf-8",
|
|
)
|
|
(project / "phase1/framework.md").write_text(
|
|
"## 第1章 临床证据正在重塑需求判断\n\n研究思路。",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
args = dr.build_parser().parse_args(["research", str(project)])
|
|
|
|
try:
|
|
dr.cmd_research(args)
|
|
except SystemExit as exc:
|
|
assert "Phase 1 is not approved" in str(exc)
|
|
else:
|
|
raise AssertionError("research should require phase1 approval by default")
|
|
|
|
override = dr.build_parser().parse_args(["research", str(project), "--force"])
|
|
assert dr.cmd_research(override) == 0
|
|
|
|
|
|
def test_init_and_frame_create_executable_python_core_project(tmp_path: Path) -> None:
|
|
args = dr.build_parser().parse_args(
|
|
[
|
|
"init",
|
|
"ADC 全球竞争格局",
|
|
"--slug",
|
|
"adc-global-landscape",
|
|
"--method",
|
|
"mckinsey_market",
|
|
"--projects-dir",
|
|
str(tmp_path),
|
|
]
|
|
)
|
|
|
|
assert dr.cmd_init(args) == 0
|
|
project = tmp_path / "adc-global-landscape"
|
|
manifest = json.loads((project / "manifest.json").read_text(encoding="utf-8"))
|
|
assert manifest["topic"] == "ADC 全球竞争格局"
|
|
assert manifest["research_method"] == "mckinsey_market"
|
|
assert manifest["work_language"] == "zh"
|
|
|
|
frame_args = dr.build_parser().parse_args(["frame", str(project)])
|
|
assert dr.cmd_frame(frame_args) == 0
|
|
|
|
framework = (project / "phase1/framework.md").read_text(encoding="utf-8")
|
|
assert "research_method: mckinsey_market" in framework
|
|
assert "## 第1章" in framework
|
|
assert "中文" in framework
|
|
|
|
|
|
def test_frame_can_preserve_existing_outline(tmp_path: Path) -> None:
|
|
project = tmp_path / "custom-outline"
|
|
(project / "phase1").mkdir(parents=True)
|
|
(project / "manifest.json").write_text(
|
|
'{"topic": "自定义研究", "research_method": "mckinsey_market", "target_words": 12000, "phase1": {}}\n',
|
|
encoding="utf-8",
|
|
)
|
|
(project / "phase1/framework.md").write_text(
|
|
"## 第1章 第一条自定义主线\n\n## 第2章 第二条自定义主线\n\n## 第3章 第三条自定义主线\n\n"
|
|
"## 第4章 第四条自定义主线\n\n## 第5章 第五条自定义主线\n\n## 第6章 第六条自定义主线\n\n"
|
|
"## 第7章 第七条自定义主线\n\n## 第8章 第八条自定义主线\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
args = dr.build_parser().parse_args(["frame", str(project), "--preserve-existing-outline"])
|
|
|
|
assert dr.cmd_frame(args) == 0
|
|
framework = (project / "phase1/framework.md").read_text(encoding="utf-8")
|
|
brief = json.loads((project / "phase1/research_brief.json").read_text(encoding="utf-8"))
|
|
assert "第一条自定义主线" in framework
|
|
assert "本章要解决的问题" in framework
|
|
assert brief["chapter_planning"][0]["title"] == "第一条自定义主线"
|
|
|
|
|
|
def test_review_writes_phase3_critique(tmp_path: Path) -> None:
|
|
project = tmp_path / "project"
|
|
(project / "phase1").mkdir(parents=True)
|
|
(project / "phase2/drafts").mkdir(parents=True)
|
|
(project / "manifest.json").write_text(
|
|
'{"topic": "测试项目", "research_method": "mckinsey_market", "phase3": {}}\n',
|
|
encoding="utf-8",
|
|
)
|
|
(project / "phase2/drafts/ch01.md").write_text("## 观点标题\n\n正文。[src_001]\n", encoding="utf-8")
|
|
(project / "phase2/sources.jsonl").write_text('{"id":"src_001","title":"来源"}\n', encoding="utf-8")
|
|
|
|
args = dr.build_parser().parse_args(["review", str(project)])
|
|
|
|
assert dr.cmd_review(args) == 0
|
|
critique = project / "phase3/critique.md"
|
|
assert critique.exists()
|
|
text = critique.read_text(encoding="utf-8")
|
|
assert "Phase 3 审校" in text
|
|
assert "src_001" in text
|
|
|
|
|
|
def test_review_model_dry_run_exposes_opus_context_plan(tmp_path: Path, capsys) -> None:
|
|
project = tmp_path / "project"
|
|
project.mkdir()
|
|
(project / "manifest.json").write_text('{"topic": "测试项目"}\n', encoding="utf-8")
|
|
|
|
args = dr.build_parser().parse_args(["review", str(project), "--model-review", "--dry-run"])
|
|
|
|
assert dr.cmd_review(args) == 0
|
|
out = capsys.readouterr().out
|
|
assert "zenmux-anthropic/claude-opus-4-7" in out
|
|
assert "review_context_opus_4_7.md" in out
|
|
|
|
|
|
def test_finalize_polish_dry_run_uses_polish_source_argument(tmp_path: Path, capsys) -> None:
|
|
project = tmp_path / "project"
|
|
(project / "phase4").mkdir(parents=True)
|
|
(project / "manifest.json").write_text(
|
|
'{"model_profile": "medium", "phase4": {}}\n',
|
|
encoding="utf-8",
|
|
)
|
|
(project / "phase4/final_zh.md").write_text("# 中文终稿\n", encoding="utf-8")
|
|
|
|
args = dr.build_parser().parse_args(["finalize", str(project), "--polish", "--dry-run"])
|
|
|
|
assert dr.cmd_finalize(args) == 0
|
|
out = capsys.readouterr().out
|
|
assert "scripts/polish.py" in out
|
|
assert "--source phase4/final_zh.md" in out
|
|
assert "--input phase4/final_zh.md" not in out.split("scripts/polish.py", 1)[1]
|
|
|
|
|
|
def test_finalize_number_citations_dry_run_builds_numbered_markdown(tmp_path: Path, capsys) -> None:
|
|
project = tmp_path / "project"
|
|
(project / "phase4").mkdir(parents=True)
|
|
(project / "manifest.json").write_text(
|
|
'{"model_profile": "medium", "phase4": {}}\n',
|
|
encoding="utf-8",
|
|
)
|
|
(project / "phase4/final_zh.md").write_text("# 中文终稿\n\n正文。[src_001]\n", encoding="utf-8")
|
|
|
|
args = dr.build_parser().parse_args(["finalize", str(project), "--number-citations", "--dry-run"])
|
|
|
|
assert dr.cmd_finalize(args) == 0
|
|
out = capsys.readouterr().out
|
|
assert "scripts/number_citations.py" in out
|
|
assert "--input phase4/final_zh_numbered.md" in out
|
|
|
|
|
|
def test_run_new_topic_initializes_and_frames_project(tmp_path: Path) -> None:
|
|
args = dr.build_parser().parse_args(
|
|
[
|
|
"run",
|
|
"GMP 整改咨询",
|
|
"--slug",
|
|
"gmp-remediation",
|
|
"--method",
|
|
"gmp_gap_assessment",
|
|
"--projects-dir",
|
|
str(tmp_path),
|
|
]
|
|
)
|
|
|
|
assert dr.cmd_run(args) == 0
|
|
project = tmp_path / "gmp-remediation"
|
|
assert (project / "manifest.json").exists()
|
|
assert (project / "phase1/framework.md").exists()
|