"""app/harness/workspace 单元测试。 覆盖 Workspace 生命周期管理、manifest 读写、 VersionedSkillStore / VersionedPromptStore Protocol 合规。 """ from __future__ import annotations import json import shutil from pathlib import Path import pytest from app.harness.workspace import ( ResolvedPaths, VersionedPromptStore, VersionedSkillStore, archive_workspace, init_workspace, init_workspace_from_seed, list_video_ids, load_manifest, read_best, record_run, resolve_paths, update_best, update_manifest, ) from core.evolution.protocols import PromptStore, SkillStore # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture() def store_dir(tmp_path: Path) -> Path: """构建一个最小 Store 目录结构供测试使用。""" sd = tmp_path / "store" sd.mkdir() # videos:两个含 tree.json 的视频目录 for vid in ("vid_A", "vid_B"): vdir = sd / "videos" / vid vdir.mkdir(parents=True) (vdir / "tree.json").write_text("{}") # skills/v1 + prompts/v1 s1 = sd / "skills" / "v1" s1.mkdir(parents=True) (s1 / "temporal-reasoning.md").write_text("skill content A") (s1 / "spatial-analysis.md").write_text("skill content B") p1 = sd / "prompts" / "v1" p1.mkdir(parents=True) (p1 / "system.md").write_text("system prompt v1") (p1 / "extract.md").write_text("extract prompt v1") # questions/benchmarks/Video-MME q = sd / "questions" / "benchmarks" / "Video-MME" q.mkdir(parents=True) (q / "q1.json").write_text('{"id": "q1"}') # seed "initial" seed_dir = sd / "seeds" / "initial" seed_dir.mkdir(parents=True) shutil.copytree(s1, seed_dir / "skills") shutil.copytree(p1, seed_dir / "prompts") baseline_db = seed_dir / "baseline.db" baseline_db.write_bytes(b"") (seed_dir / "seed.json").write_text( json.dumps({"baseline_run_id": "baseline-001", "parent": None}) ) return sd @pytest.fixture() def workspace_dir(tmp_path: Path) -> Path: """返回一个不存在的 workspace 路径。""" return tmp_path / "ws_test" # --------------------------------------------------------------------------- # ResolvedPaths # --------------------------------------------------------------------------- def test_resolved_paths_frozen() -> None: """ResolvedPaths 实例应不可变。""" rp = ResolvedPaths( store_dir=Path("/s"), videos_dir=Path("/v"), questions_dir=Path("/q"), skills_dir=Path("/sk"), prompts_dir=Path("/p"), workspace_dir=Path("/w"), db_path=Path("/d"), analyses_dir=Path("/a"), runs_dir=Path("/r"), ) with pytest.raises(AttributeError): rp.store_dir = Path("/other") # type: ignore[misc] # --------------------------------------------------------------------------- # init_workspace # --------------------------------------------------------------------------- def test_init_workspace(store_dir: Path, workspace_dir: Path) -> None: """init_workspace 应创建 manifest、拷贝权重。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) # manifest 存在 manifest = json.loads((workspace_dir / "manifest.json").read_text()) assert manifest["current"]["skills"] == "skills/v1" assert manifest["current"]["prompts"] == "prompts/v1" assert manifest["current"]["questions"] == "questions/benchmarks/Video-MME" # 权重已拷入 workspace assert (workspace_dir / "skills" / "v1" / "temporal-reasoning.md").exists() assert (workspace_dir / "prompts" / "v1" / "system.md").exists() # analyses, runs 目录已创建 assert (workspace_dir / "analyses").is_dir() assert (workspace_dir / "runs").is_dir() # 重复创建应报错 with pytest.raises(FileExistsError): init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) # --------------------------------------------------------------------------- # init_workspace_from_seed # --------------------------------------------------------------------------- def test_init_workspace_from_seed(store_dir: Path, workspace_dir: Path) -> None: """从种子初始化应拷权重 + baseline.db + 返回 baseline_run_id。""" run_id = init_workspace_from_seed( workspace_dir, store_dir, seed_name="initial", questions="benchmarks/Video-MME", ) assert run_id == "baseline-001" # 权重在 workspace assert (workspace_dir / "skills" / "v1" / "temporal-reasoning.md").exists() assert (workspace_dir / "prompts" / "v1" / "system.md").exists() # baseline.db -> harness.db assert (workspace_dir / "harness.db").exists() # manifest 正确 manifest = json.loads((workspace_dir / "manifest.json").read_text()) assert manifest["current"]["skills"] == "skills/v1" def test_init_workspace_from_seed_missing_questions(store_dir: Path, workspace_dir: Path) -> None: """questions ref 不存在应 fail-fast(FileNotFoundError),不创建 workspace。""" with pytest.raises(FileNotFoundError, match="questions ref"): init_workspace_from_seed( workspace_dir, store_dir, seed_name="initial", questions="benchmarks/NONEXISTENT", ) # workspace 不应被创建 assert not workspace_dir.exists() # --------------------------------------------------------------------------- # load_manifest # --------------------------------------------------------------------------- def test_load_manifest(store_dir: Path, workspace_dir: Path) -> None: """正常加载 manifest。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) manifest = load_manifest(workspace_dir) assert "current" in manifest assert "history" in manifest assert manifest["current"]["skills"] == "skills/v1" def test_load_manifest_missing(workspace_dir: Path) -> None: """manifest 不存在应 FileNotFoundError。""" workspace_dir.mkdir(parents=True) with pytest.raises(FileNotFoundError): load_manifest(workspace_dir) # --------------------------------------------------------------------------- # update_manifest # --------------------------------------------------------------------------- def test_update_manifest_invalid_key(store_dir: Path, workspace_dir: Path) -> None: """非法 key 应 KeyError。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) with pytest.raises(KeyError, match="无效"): update_manifest(workspace_dir, bad_key="skills/v2") def test_update_manifest_valid(store_dir: Path, workspace_dir: Path) -> None: """合法 key 应更新 manifest current。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) update_manifest(workspace_dir, skills="skills/v2") manifest = load_manifest(workspace_dir) assert manifest["current"]["skills"] == "skills/v2" # --------------------------------------------------------------------------- # record_run # --------------------------------------------------------------------------- def test_record_run_idempotent(store_dir: Path, workspace_dir: Path) -> None: """同 run_id 调用两次不应重复追加 history。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) run_dir1 = record_run(workspace_dir, "run_001") run_dir2 = record_run(workspace_dir, "run_001") # 返回路径一致 assert run_dir1 == run_dir2 # history 只有一条 manifest = load_manifest(workspace_dir) matched = [h for h in manifest["history"] if h["run_id"] == "run_001"] assert len(matched) == 1 # run 目录存在 assert run_dir1.is_dir() # per-video wiki 目录存在 assert (run_dir1 / "vid_A" / "wiki").is_dir() assert (run_dir1 / "vid_B" / "wiki").is_dir() # --------------------------------------------------------------------------- # update_best / read_best # --------------------------------------------------------------------------- def test_update_best_independent_of_current(store_dir: Path, workspace_dir: Path) -> None: """best 应独立于 current——更新 best 不影响 current。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) # 初始无 best assert read_best(workspace_dir) is None # 设置 best update_best( workspace_dir, skills="skills/v3", prompts="prompts/v3", val_acc=0.85, run_id="run_005", epoch=3, ) best = read_best(workspace_dir) assert best is not None assert best["skills"] == "skills/v3" assert best["val_acc"] == 0.85 assert best["epoch"] == 3 # current 不受影响 manifest = load_manifest(workspace_dir) assert manifest["current"]["skills"] == "skills/v1" # --------------------------------------------------------------------------- # archive_workspace # --------------------------------------------------------------------------- def test_archive_workspace(store_dir: Path, workspace_dir: Path) -> None: """归档应把 workspace 移到 .archive/ 下。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) archived = archive_workspace(workspace_dir) # 原目录不存在 assert not workspace_dir.exists() # 归档目录存在并包含 manifest assert archived.is_dir() assert (archived / "manifest.json").exists() # 归档路径在 .archive 下 assert archived.parent.name == ".archive" def test_archive_workspace_missing(workspace_dir: Path) -> None: """归档不存在的 workspace 应 FileNotFoundError。""" with pytest.raises(FileNotFoundError): archive_workspace(workspace_dir) # --------------------------------------------------------------------------- # list_video_ids # --------------------------------------------------------------------------- def test_list_video_ids(store_dir: Path, workspace_dir: Path) -> None: """列出 workspace 引用的所有视频 ID。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) ids = list_video_ids(workspace_dir) assert ids == ["vid_A", "vid_B"] # --------------------------------------------------------------------------- # resolve_paths # --------------------------------------------------------------------------- def test_resolve_paths(store_dir: Path, workspace_dir: Path) -> None: """resolve_paths 应正确解析绝对路径。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) rp = resolve_paths(workspace_dir) # skills_dir/prompts_dir 解析到 workspace(非 store) ws_abs = workspace_dir.resolve() assert rp.skills_dir == ws_abs / "skills" / "v1" assert rp.prompts_dir == ws_abs / "prompts" / "v1" # videos/questions 解析到 store store_abs = store_dir.resolve() assert rp.videos_dir == store_abs / "videos" assert rp.questions_dir == store_abs / "questions" / "benchmarks" / "Video-MME" # db_path, analyses, runs assert rp.db_path == ws_abs / "harness.db" assert rp.analyses_dir == ws_abs / "analyses" assert rp.runs_dir == ws_abs / "runs" # --------------------------------------------------------------------------- # VersionedSkillStore # --------------------------------------------------------------------------- def test_versioned_skill_store_read(store_dir: Path, workspace_dir: Path) -> None: """read_skill 应返回文件全文。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) skills_dir = workspace_dir / "skills" / "v1" store = VersionedSkillStore(skills_dir) content = store.read_skill("temporal-reasoning.md") assert content == "skill content A" def test_versioned_skill_store_read_missing(store_dir: Path, workspace_dir: Path) -> None: """读取不存在的 skill 文件应 FileNotFoundError。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) skills_dir = workspace_dir / "skills" / "v1" store = VersionedSkillStore(skills_dir) with pytest.raises(FileNotFoundError): store.read_skill("nonexistent.md") def test_versioned_skill_store_list(store_dir: Path, workspace_dir: Path) -> None: """list_skill_files 应列出所有 skill 文件名(排序)。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) skills_dir = workspace_dir / "skills" / "v1" store = VersionedSkillStore(skills_dir) files = store.list_skill_files() assert sorted(files) == ["spatial-analysis.md", "temporal-reasoning.md"] # --------------------------------------------------------------------------- # VersionedPromptStore # --------------------------------------------------------------------------- def test_versioned_prompt_store(store_dir: Path, workspace_dir: Path) -> None: """VersionedPromptStore 读写功能验证。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) prompts_dir = workspace_dir / "prompts" / "v1" store = VersionedPromptStore(prompts_dir) content = store.read_prompt("system.md") assert content == "system prompt v1" files = store.list_prompt_files() assert sorted(files) == ["extract.md", "system.md"] def test_versioned_prompt_store_read_missing(store_dir: Path, workspace_dir: Path) -> None: """读取不存在的 prompt 文件应 FileNotFoundError。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) prompts_dir = workspace_dir / "prompts" / "v1" store = VersionedPromptStore(prompts_dir) with pytest.raises(FileNotFoundError): store.read_prompt("nonexistent.md") # --------------------------------------------------------------------------- # Protocol 合规 # --------------------------------------------------------------------------- def test_skill_store_protocol_compliance(store_dir: Path, workspace_dir: Path) -> None: """VersionedSkillStore 应满足 core/evolution/protocols.py::SkillStore Protocol。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) skills_dir = workspace_dir / "skills" / "v1" store = VersionedSkillStore(skills_dir) assert isinstance(store, SkillStore) def test_prompt_store_protocol_compliance(store_dir: Path, workspace_dir: Path) -> None: """VersionedPromptStore 应满足 core/evolution/protocols.py::PromptStore Protocol。""" init_workspace( workspace_dir, store_dir, questions="benchmarks/Video-MME", skills_version="v1", prompts_version="v1", ) prompts_dir = workspace_dir / "prompts" / "v1" store = VersionedPromptStore(prompts_dir) assert isinstance(store, PromptStore)