Files
Video-Tree-TRM5/tests/unit/test_harness_store.py
T
iomgaa b052c1f3ee feat(harness): store.py — Store 版本操作 + Seed 管理
从 TRM4 core/workspace.py 拆出 Store + Seed 相关函数:
- _parse_version / list_versions / next_version / advance_version
- _write_meta / init_store
- init_seed / list_seeds / read_seed
- extract_run_db(保留原始 CREATE 语句重建主键约束)
- promote_to_seed(强校验版本一致 + 非 NULL + finally 清理)

26 个测试全部通过,radon 复杂度 A (2.83)。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-07 12:36:36 -04:00

461 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Store 版本操作 + Seed 管理的单元测试。"""
from __future__ import annotations
import json
import sqlite3
import pytest
from app.harness.store import (
_parse_version,
_write_meta,
advance_version,
extract_run_db,
init_seed,
init_store,
list_seeds,
list_versions,
next_version,
promote_to_seed,
read_seed,
)
# ---------------------------------------------------------------------------
# _parse_version
# ---------------------------------------------------------------------------
class TestParseVersion:
"""_parse_version 解析 v\\d+ 格式版本号。"""
def test_parse_version_normal(self) -> None:
assert _parse_version("v1") == 1
assert _parse_version("v10") == 10
assert _parse_version("v999") == 999
def test_parse_version_invalid(self) -> None:
with pytest.raises(ValueError, match="无效版本号"):
_parse_version("abc")
with pytest.raises(ValueError, match="无效版本号"):
_parse_version("v")
with pytest.raises(ValueError, match="无效版本号"):
_parse_version("v1.0")
# ---------------------------------------------------------------------------
# list_versions — 数字排序
# ---------------------------------------------------------------------------
class TestListVersions:
"""list_versions 按数字排序,v10 排在 v2 后。"""
def test_list_versions_numeric_sort(self, tmp_path: "Path") -> None:
"""v10 必须排在 v2 后面(非字典序)。"""
store = tmp_path / "store"
resource = store / "skills"
resource.mkdir(parents=True)
for v in ("v1", "v10", "v2", "v20", "v3"):
(resource / v).mkdir()
result = list_versions(store, "skills")
assert result == ["v1", "v2", "v3", "v10", "v20"]
def test_list_versions_empty(self, tmp_path: "Path") -> None:
store = tmp_path / "store"
assert list_versions(store, "skills") == []
def test_list_versions_ignores_non_version_dirs(self, tmp_path: "Path") -> None:
"""非 v\\d+ 格式的目录被忽略。"""
store = tmp_path / "store"
resource = store / "skills"
resource.mkdir(parents=True)
(resource / "v1").mkdir()
(resource / "backup").mkdir()
(resource / ".hidden").mkdir()
assert list_versions(store, "skills") == ["v1"]
# ---------------------------------------------------------------------------
# next_version
# ---------------------------------------------------------------------------
class TestNextVersion:
"""next_version 返回下一个可用版本号。"""
def test_next_version_empty(self, tmp_path: "Path") -> None:
store = tmp_path / "store"
assert next_version(store, "skills") == "v1"
def test_next_version_after_existing(self, tmp_path: "Path") -> None:
store = tmp_path / "store"
resource = store / "skills"
resource.mkdir(parents=True)
(resource / "v1").mkdir()
(resource / "v2").mkdir()
assert next_version(store, "skills") == "v3"
def test_next_version_with_gap(self, tmp_path: "Path") -> None:
"""v1 和 v10 之间有 gapnext 应为 v11。"""
store = tmp_path / "store"
resource = store / "skills"
resource.mkdir(parents=True)
(resource / "v1").mkdir()
(resource / "v10").mkdir()
assert next_version(store, "skills") == "v11"
# ---------------------------------------------------------------------------
# advance_version
# ---------------------------------------------------------------------------
class TestAdvanceVersion:
"""advance_version copytree + _write_meta。"""
def test_advance_version(self, tmp_path: "Path") -> None:
store = tmp_path / "store"
resource = store / "skills"
resource.mkdir(parents=True)
(resource / "v1").mkdir()
source = tmp_path / "new_skills"
source.mkdir()
(source / "skill_a.md").write_text("内容A")
version = advance_version(
store,
"skills",
source,
{"source": "evolution", "description": "进化产出"},
)
assert version == "v2"
assert (resource / "v2" / "skill_a.md").read_text() == "内容A"
meta = json.loads((resource / "v2" / "meta.json").read_text())
assert meta["version"] == "v2"
assert meta["source"] == "evolution"
assert meta["description"] == "进化产出"
assert "created_at" in meta
# ---------------------------------------------------------------------------
# init_store
# ---------------------------------------------------------------------------
class TestInitStore:
"""init_store 初始化 Store 目录结构。"""
def test_init_store(self, tmp_path: "Path") -> None:
videos = tmp_path / "videos_src"
videos.mkdir()
(videos / "v001").mkdir()
(videos / "v001" / "tree.json").write_text("{}")
skills = tmp_path / "skills_src"
skills.mkdir()
(skills / "search.md").write_text("skill")
prompts = tmp_path / "prompts_src"
prompts.mkdir()
(prompts / "system.md").write_text("prompt")
store = tmp_path / "store"
init_store(store, videos, skills, prompts)
assert (store / "videos" / "v001" / "tree.json").exists()
assert (store / "questions" / "benchmarks").is_dir()
assert (store / "questions" / "generated").is_dir()
assert (store / "skills" / "v1" / "search.md").read_text() == "skill"
assert (store / "prompts" / "v1" / "system.md").read_text() == "prompt"
skills_meta = json.loads(
(store / "skills" / "v1" / "meta.json").read_text()
)
assert skills_meta["version"] == "v1"
assert skills_meta["source"] == "manual"
def test_init_store_exists_raises(self, tmp_path: "Path") -> None:
store = tmp_path / "store"
store.mkdir()
with pytest.raises(FileExistsError, match="Store 已存在"):
init_store(store, tmp_path, tmp_path, tmp_path)
# ---------------------------------------------------------------------------
# Seed 相关
# ---------------------------------------------------------------------------
def _make_seed_fixtures(tmp_path):
"""创建 seed 测试所需的公共 fixture。"""
store = tmp_path / "store"
store.mkdir(parents=True)
skills_dir = tmp_path / "sk"
skills_dir.mkdir()
(skills_dir / "search.md").write_text("skill")
prompts_dir = tmp_path / "pr"
prompts_dir.mkdir()
(prompts_dir / "system.md").write_text("prompt")
baseline_db = tmp_path / "base.db"
conn = sqlite3.connect(baseline_db)
conn.execute(
"CREATE TABLE _runs (run_id TEXT PRIMARY KEY, status TEXT)"
)
conn.execute("INSERT INTO _runs VALUES ('r1', 'done')")
conn.execute(
"CREATE TABLE predictions (run_id TEXT, question_id TEXT, answer TEXT)"
)
conn.execute("INSERT INTO predictions VALUES ('r1', 'q1', 'A')")
conn.commit()
conn.close()
return store, skills_dir, prompts_dir, baseline_db
class TestInitSeed:
"""init_seed 创建种子目录。"""
def test_init_seed(self, tmp_path: "Path") -> None:
store, skills_dir, prompts_dir, baseline_db = _make_seed_fixtures(tmp_path)
seed_dir = init_seed(
store,
"initial",
skills_dir,
prompts_dir,
baseline_db,
"r1",
None,
"初始种子",
)
assert seed_dir == store / "seeds" / "initial"
assert (seed_dir / "skills" / "search.md").exists()
assert (seed_dir / "prompts" / "system.md").exists()
assert (seed_dir / "baseline.db").exists()
meta = json.loads((seed_dir / "seed.json").read_text())
assert meta["baseline_run_id"] == "r1"
assert meta["parent"] is None
assert meta["description"] == "初始种子"
assert "created_at" in meta
def test_init_seed_exists_raises(self, tmp_path: "Path") -> None:
store, skills_dir, prompts_dir, baseline_db = _make_seed_fixtures(tmp_path)
init_seed(store, "dup", skills_dir, prompts_dir, baseline_db, "r1", None, "first")
with pytest.raises(FileExistsError, match="种子已存在"):
init_seed(
store, "dup", skills_dir, prompts_dir, baseline_db, "r1", None, "second"
)
class TestListSeeds:
"""list_seeds 列出所有种子。"""
def test_list_seeds(self, tmp_path: "Path") -> None:
store, skills_dir, prompts_dir, baseline_db = _make_seed_fixtures(tmp_path)
init_seed(store, "beta", skills_dir, prompts_dir, baseline_db, "r1", None, "b")
init_seed(store, "alpha", skills_dir, prompts_dir, baseline_db, "r1", None, "a")
assert list_seeds(store) == ["alpha", "beta"]
def test_list_seeds_empty(self, tmp_path: "Path") -> None:
store = tmp_path / "store"
assert list_seeds(store) == []
class TestReadSeed:
"""read_seed 读取 seed.json。"""
def test_read_seed(self, tmp_path: "Path") -> None:
store, skills_dir, prompts_dir, baseline_db = _make_seed_fixtures(tmp_path)
init_seed(store, "s1", skills_dir, prompts_dir, baseline_db, "r1", None, "desc")
meta = read_seed(store, "s1")
assert meta["baseline_run_id"] == "r1"
assert meta["description"] == "desc"
def test_read_seed_not_found(self, tmp_path: "Path") -> None:
store = tmp_path / "store"
store.mkdir()
with pytest.raises(FileNotFoundError, match="种子不存在"):
read_seed(store, "no_such")
# ---------------------------------------------------------------------------
# extract_run_db
# ---------------------------------------------------------------------------
class TestExtractRunDb:
"""extract_run_db 抽取指定 run 的行并保留 PK。"""
def _make_src_db(self, path):
"""创建带 _runs + predictions 表的源 db。"""
conn = sqlite3.connect(path)
conn.execute(
"CREATE TABLE _runs (run_id TEXT PRIMARY KEY, status TEXT)"
)
conn.execute("INSERT INTO _runs VALUES ('r1', 'done')")
conn.execute("INSERT INTO _runs VALUES ('r2', 'done')")
conn.execute(
"CREATE TABLE predictions "
"(run_id TEXT, question_id TEXT, answer TEXT)"
)
conn.execute("INSERT INTO predictions VALUES ('r1', 'q1', 'A')")
conn.execute("INSERT INTO predictions VALUES ('r1', 'q2', 'B')")
conn.execute("INSERT INTO predictions VALUES ('r2', 'q1', 'C')")
conn.commit()
conn.close()
def test_extract_run_db_preserves_pk(self, tmp_path: "Path") -> None:
"""原始 CREATE 保留主键约束。"""
src = tmp_path / "src.db"
dst = tmp_path / "dst.db"
self._make_src_db(src)
extract_run_db(src, dst, "r1")
conn = sqlite3.connect(dst)
# 验证 _runs 表有 PK
create_sql = conn.execute(
"SELECT sql FROM sqlite_master WHERE type='table' AND name='_runs'"
).fetchone()[0]
assert "PRIMARY KEY" in create_sql
# 验证只有 r1 的行
runs = conn.execute("SELECT * FROM _runs").fetchall()
assert len(runs) == 1
assert runs[0][0] == "r1"
preds = conn.execute("SELECT * FROM predictions").fetchall()
assert len(preds) == 2
conn.close()
def test_extract_run_db_missing_table(self, tmp_path: "Path") -> None:
"""源 db 无目标表时报错。"""
src = tmp_path / "src.db"
dst = tmp_path / "dst.db"
conn = sqlite3.connect(src)
conn.execute("CREATE TABLE other (id TEXT)")
conn.commit()
conn.close()
with pytest.raises(RuntimeError, match="源 db 无表"):
extract_run_db(src, dst, "r1")
def test_extract_run_db_no_rows(self, tmp_path: "Path") -> None:
"""目标 run_id 不存在时报错。"""
src = tmp_path / "src.db"
dst = tmp_path / "dst.db"
self._make_src_db(src)
with pytest.raises(RuntimeError, match="无 run_id="):
extract_run_db(src, dst, "nonexistent")
# ---------------------------------------------------------------------------
# promote_to_seed
# ---------------------------------------------------------------------------
def _make_promote_fixtures(tmp_path):
"""创建 promote_to_seed 测试所需的 workspace + store。"""
ws = tmp_path / "ws"
ws.mkdir()
store = tmp_path / "store"
store.mkdir()
# workspace 内的 skills/prompts 版本目录
(ws / "skills" / "v2").mkdir(parents=True)
(ws / "skills" / "v2" / "skill.md").write_text("evolved")
(ws / "prompts" / "v2").mkdir(parents=True)
(ws / "prompts" / "v2" / "system.md").write_text("prompt v2")
# workspace harness.db
db_path = ws / "harness.db"
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE _runs (
run_id TEXT PRIMARY KEY,
skills_version TEXT,
prompts_version TEXT
)
""")
conn.execute(
"INSERT INTO _runs VALUES ('eval_001', 'v2', 'v2')"
)
conn.execute("""
CREATE TABLE predictions (
run_id TEXT, question_id TEXT, answer TEXT
)
""")
conn.execute("INSERT INTO predictions VALUES ('eval_001', 'q1', 'A')")
conn.commit()
conn.close()
return ws, store
class TestPromoteToSeed:
"""promote_to_seed 固化 workspace 版本为种子。"""
def test_promote_to_seed_success(self, tmp_path: "Path") -> None:
ws, store = _make_promote_fixtures(tmp_path)
seed_dir = promote_to_seed(ws, store, "v2", "eval_001", "evolved-seed", "good")
assert seed_dir == store / "seeds" / "evolved-seed"
assert (seed_dir / "skills" / "skill.md").exists()
assert (seed_dir / "prompts" / "system.md").exists()
assert (seed_dir / "baseline.db").exists()
meta = json.loads((seed_dir / "seed.json").read_text())
assert meta["baseline_run_id"] == "eval_001"
assert meta["parent"] == "ws:v2"
def test_promote_to_seed_version_mismatch(self, tmp_path: "Path") -> None:
"""eval run 的 skills_version 与 --version 不符时报错。"""
ws, store = _make_promote_fixtures(tmp_path)
with pytest.raises(ValueError, match="版本.*不符"):
promote_to_seed(ws, store, "v3", "eval_001", "bad", "mismatch")
def test_promote_to_seed_null_version(self, tmp_path: "Path") -> None:
"""eval run 的版本为 NULL 时报错。"""
ws = tmp_path / "ws2"
ws.mkdir()
store = tmp_path / "store2"
store.mkdir()
db_path = ws / "harness.db"
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE _runs (
run_id TEXT PRIMARY KEY,
skills_version TEXT,
prompts_version TEXT
)
""")
conn.execute("INSERT INTO _runs VALUES ('eval_null', NULL, NULL)")
conn.commit()
conn.close()
with pytest.raises(ValueError, match="NULL"):
promote_to_seed(ws, store, "v1", "eval_null", "bad", "null ver")
def test_promote_to_seed_run_not_found(self, tmp_path: "Path") -> None:
"""eval run 不存在时报错。"""
ws, store = _make_promote_fixtures(tmp_path)
with pytest.raises(ValueError, match="eval run 不存在"):
promote_to_seed(ws, store, "v1", "nonexistent", "bad", "no run")
def test_promote_cleanup_tmp_db(self, tmp_path: "Path") -> None:
"""finally 清理临时 db 文件。"""
ws, store = _make_promote_fixtures(tmp_path)
promote_to_seed(ws, store, "v2", "eval_001", "clean-test", "cleanup")
# 临时 db 应已清理
assert not (ws / "_promote_tmp.db").exists()
def test_promote_cleanup_tmp_db_on_error(self, tmp_path: "Path") -> None:
"""即使 init_seed 失败(同名种子),临时 db 也应被清理。"""
ws, store = _make_promote_fixtures(tmp_path)
promote_to_seed(ws, store, "v2", "eval_001", "first", "first time")
with pytest.raises(FileExistsError):
promote_to_seed(ws, store, "v2", "eval_001", "first", "second time")
assert not (ws / "_promote_tmp.db").exists()