from __future__ import annotations import json import sys from pathlib import Path import pytest REPO_ROOT = Path(__file__).resolve().parents[1] if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) from scripts.lib.model_config import resolve_model_profile from scripts.runtime.roles import resolve_runtime_profile from scripts.runtime.skills import SkillRegistry from scripts.runtime.tasks import ( TaskCard, detect_dependency_cycles, generate_task_cards, validate_packet, validate_task_cards, ) def test_skill_registry_uses_agents_skills_as_canonical() -> None: registry = SkillRegistry() names = registry.list_names() assert "search-strategy" in names assert "search-gateway" in names assert "source-quality" in names assert "document-ingest" in names assert "deep-research" in names assert registry.validate()["ok"] is True def test_model_profile_exposes_task_types_and_role_defaults() -> None: resolved = resolve_model_profile(profile="medium") assert resolved["roles"]["dr_pm"] assert resolved["task_types"]["source_discovery"] == "dr_searcher" assert resolved["task_types"]["chapter_assembly"] == "dr_analyst" def test_runtime_profile_resolves_task_model_and_skills() -> None: runtime = resolve_runtime_profile(profile="medium") worker = runtime.role_for_task("evidence_packet") assert worker.name == "dr_analyst" assert worker.model assert "evidence-table" in worker.skills assert "search-gateway" in worker.skills assert worker.max_concurrency >= 1 def test_generate_task_cards_from_chinese_framework() -> None: framework = """ # 研究框架 ## 第1章 GLP-1 产业链的增量来自适应症扩张 研究思路:围绕临床、监管、竞争格局和生产供应链展开。 ## 第2章 供应链瓶颈决定国产替代窗口 研究思路:围绕专利、上游原料、产能和中国市场展开。 """ cards = generate_task_cards("glp1-test", framework, axes=["clinical", "regulatory"]) assert [card.task_id for card in cards] == [ "ch01-clinical", "ch01-regulatory", "ch02-clinical", "ch02-regulatory", ] assert cards[0].output_packet == "phase2/packets/ch01-clinical.json" def test_task_card_validation_rejects_duplicates_and_cycles() -> None: cards = [ TaskCard(task_id="a", chapter_ids=["ch01"], topic_axis="clinical", questions=["q"], search_routes=["scholar"], output_packet="phase2/packets/a.json", dependencies=["b"]), TaskCard(task_id="b", chapter_ids=["ch01"], topic_axis="regulatory", questions=["q"], search_routes=["general"], output_packet="phase2/packets/b.json", dependencies=["a"]), ] with pytest.raises(ValueError, match="dependency cycle"): detect_dependency_cycles(cards) with pytest.raises(ValueError, match="duplicate task_id"): validate_task_cards([cards[0], cards[0]]) def test_packet_validation_requires_sources_and_counter_evidence() -> None: packet = { "task_id": "ch01-clinical", "claims": [{"claim": "结论", "source_ids": ["src_001"]}], "evidence_items": [{"source_id": "src_001", "summary": "证据"}], "counter_evidence": [], "source_ids": ["src_001"], "source_quality_notes": ["Tier 1"], "open_questions": [], "raw_quotes_or_notes": ["Original English excerpt allowed."], } with pytest.raises(ValueError, match="counter_evidence"): validate_packet(packet) packet["counter_evidence"] = [{"claim": "限制", "source_ids": ["src_002"]}] with pytest.raises(ValueError, match="not declared"): validate_packet(packet) packet["source_ids"].append("src_002") validate_packet(packet) def test_skill_sync_copies_to_adapter_dirs(tmp_path: Path) -> None: canonical = tmp_path / "skills" target = tmp_path / "adapter" / "skills" source_skill = canonical / "demo" source_skill.mkdir(parents=True) (source_skill / "SKILL.md").write_text("---\nname: demo\n---\n\nBody\n", encoding="utf-8") registry = SkillRegistry(canonical_dir=canonical) copied = registry.sync_to([target]) assert copied == 1 assert (target / "demo" / "SKILL.md").read_text(encoding="utf-8").endswith("Body\n") def test_skill_sync_skips_canonical_dir_to_avoid_deleting_source(tmp_path: Path) -> None: canonical = tmp_path / "skills" source_skill = canonical / "demo" source_skill.mkdir(parents=True) (source_skill / "SKILL.md").write_text("---\nname: demo\n---\n\nBody\n", encoding="utf-8") registry = SkillRegistry(canonical_dir=canonical) copied = registry.sync_to([canonical]) assert copied == 0 assert (source_skill / "SKILL.md").exists()