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"], "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_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_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_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()