Files
iomgaa 6bdb802f01 chore: track claude skills, tools, templates, reference code and research-wiki
- Add all claude skills (brainstorming, commit, debugging, TDD, etc.)
- Add claude hooks (pre-commit-guard, post-edit-quality)
- Add research templates (experiment plan, research brief, etc.)
- Add claude tools (arxiv/semantic_scholar/openalex fetch, wiki, exa)
- Add TRM4 reference implementation as algorithm fidelity baseline
- Add research-wiki content (plans, index, graph, query_pack)
- Update .gitignore to exclude .graphify_version runtime state
2026-07-06 20:59:03 -04:00

277 lines
9.0 KiB
Python
Raw Permalink 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.
"""
配置管理模块单元测试
====================
覆盖: YAML 加载、.env 覆盖、CLI 覆盖、缺字段报错、优先级验证、embed_dim 一致性。
"""
from __future__ import annotations
from pathlib import Path
import pytest
import yaml
from video_tree_trm.config import Config, TreeConfig
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
_FULL_YAML = {
"tree": {
"max_paragraphs_per_l2": 5,
"l1_segment_duration": 600.0,
"l2_clip_duration": 20.0,
"l3_fps": 1.0,
"l2_representative_frames": 3,
"cache_dir": "cache/trees",
},
"embed": {
"backend": "local",
"model_name": "test-model",
"embed_dim": 2560,
"device": "cpu",
"api_key": "",
"api_url": "",
},
"llm": {
"backend": "qwen",
"api_key": "yaml-llm-key",
"model": "qwen-plus",
"api_url": "https://example.com/llm",
"max_tokens": 256,
"temperature": 0.1,
},
"vlm": {
"backend": "qwen",
"api_key": "yaml-vlm-key",
"model": "qwen-vl-plus",
"api_url": "https://example.com/vlm",
"max_tokens": 256,
"temperature": 0.1,
},
"retriever": {
"embed_dim": 2560,
"num_heads": 4,
"L_layers": 2,
"L_cycles": 4,
"max_rounds": 5,
"ffn_expansion": 2.0,
"checkpoint": None,
},
"train": {
"lr": 1e-4,
"weight_decay": 1e-5,
"batch_size": 1,
"max_epochs_phase1": 30,
"max_epochs_phase2": 20,
"nav_loss_weight": 1.0,
"act_loss_weight": 0.1,
"act_lambda_step": 0.1,
"act_gamma": 0.9,
"eval_interval": 5,
"save_dir": "checkpoints",
"dataset": "longbench",
"dataset_path": "data/longbench",
},
}
@pytest.fixture()
def yaml_path(tmp_path: Path) -> Path:
"""创建完整配置的临时 YAML 文件。"""
p = tmp_path / "config" / "default.yaml"
p.parent.mkdir(parents=True, exist_ok=True)
with open(p, "w", encoding="utf-8") as f:
yaml.dump(_FULL_YAML, f, allow_unicode=True)
return p
@pytest.fixture()
def env_path(tmp_path: Path) -> Path:
"""创建临时 .env 文件。"""
p = tmp_path / ".env"
p.write_text(
"LLM_API_KEY=env-llm-key\n"
"LLM_MODEL=env-llm-model\n"
"LLM_API_URL=https://env.example.com/llm\n"
"VLM_API_KEY=env-vlm-key\n"
"VLM_MODEL=env-vlm-model\n"
"VLM_API_URL=https://env.example.com/vlm\n"
"EMBED_BACKEND=remote\n"
"EMBED_MODEL=env-embed-model\n"
"EMBED_API_KEY=env-embed-key\n"
"EMBED_API_URL=https://env.example.com/embed\n"
)
return p
# ---------------------------------------------------------------------------
# 测试: YAML 加载
# ---------------------------------------------------------------------------
class TestYAMLLoad:
"""YAML 基础加载测试。"""
def test_load_full_yaml(self, yaml_path: Path) -> None:
"""完整 YAML 应成功加载所有字段。"""
cfg = Config.load(
str(yaml_path), env_path=str(yaml_path.parent / ".env.nonexist")
)
assert isinstance(cfg.tree, TreeConfig)
assert cfg.tree.max_paragraphs_per_l2 == 5
assert cfg.tree.l1_segment_duration == 600.0
assert cfg.embed.embed_dim == 2560
assert cfg.retriever.checkpoint is None
assert cfg.train.dataset == "longbench"
def test_file_not_found(self, tmp_path: Path) -> None:
"""不存在的 YAML 应抛出 FileNotFoundError。"""
with pytest.raises(FileNotFoundError, match="配置文件不存在"):
Config.load(str(tmp_path / "nonexist.yaml"))
# ---------------------------------------------------------------------------
# 测试: 缺字段报错
# ---------------------------------------------------------------------------
class TestMissingField:
"""缺少必需字段时应抛出 TypeError。"""
def test_missing_tree_field(self, tmp_path: Path) -> None:
"""tree 节缺少字段应报 TypeError。"""
bad_yaml = _FULL_YAML.copy()
bad_yaml = {
k: (v.copy() if isinstance(v, dict) else v) for k, v in _FULL_YAML.items()
}
del bad_yaml["tree"]["cache_dir"]
p = tmp_path / "bad.yaml"
with open(p, "w") as f:
yaml.dump(bad_yaml, f)
with pytest.raises(TypeError):
Config.load(str(p), env_path=str(tmp_path / ".env.nonexist"))
def test_missing_section(self, tmp_path: Path) -> None:
"""缺少整个配置节应报 TypeError。"""
bad_yaml = {k: v for k, v in _FULL_YAML.items() if k != "train"}
p = tmp_path / "bad2.yaml"
with open(p, "w") as f:
yaml.dump(bad_yaml, f)
with pytest.raises(TypeError):
Config.load(str(p), env_path=str(tmp_path / ".env.nonexist"))
# ---------------------------------------------------------------------------
# 测试: .env 覆盖
# ---------------------------------------------------------------------------
class TestEnvOverride:
""".env 文件应覆盖 YAML 中的 api_key。"""
def test_env_overrides_api_keys(self, yaml_path: Path, env_path: Path) -> None:
"""api_key/model/api_url 应优先使用 .env 中的值。"""
cfg = Config.load(str(yaml_path), env_path=str(env_path))
assert cfg.llm.api_key == "env-llm-key"
assert cfg.llm.model == "env-llm-model"
assert cfg.llm.api_url == "https://env.example.com/llm"
assert cfg.vlm.api_key == "env-vlm-key"
assert cfg.vlm.model == "env-vlm-model"
assert cfg.vlm.api_url == "https://env.example.com/vlm"
def test_env_overrides_embed(self, yaml_path: Path, env_path: Path) -> None:
"""embed 相关字段应优先使用 .env 中的值。"""
cfg = Config.load(str(yaml_path), env_path=str(env_path))
assert cfg.embed.backend == "remote"
assert cfg.embed.model_name == "env-embed-model"
assert cfg.embed.api_key == "env-embed-key"
assert cfg.embed.api_url == "https://env.example.com/embed"
def test_yaml_fallback_when_no_env(self, yaml_path: Path) -> None:
"""无 .env 时应使用 YAML 中的值。"""
cfg = Config.load(
str(yaml_path), env_path=str(yaml_path.parent / ".env.nonexist")
)
assert cfg.llm.api_key == "yaml-llm-key"
assert cfg.vlm.api_key == "yaml-vlm-key"
# ---------------------------------------------------------------------------
# 测试: CLI 覆盖
# ---------------------------------------------------------------------------
class TestCLIOverride:
"""CLI args 应覆盖 YAML 和 .env 的值。"""
def test_cli_overrides_yaml(self, yaml_path: Path) -> None:
"""CLI 点路径覆盖应生效。"""
cfg = Config.load(
str(yaml_path),
cli_args={"retriever.num_heads": 8, "train.lr": 0.001},
env_path=str(yaml_path.parent / ".env.nonexist"),
)
assert cfg.retriever.num_heads == 8
assert cfg.train.lr == 0.001
def test_cli_overrides_env(self, yaml_path: Path, env_path: Path) -> None:
"""CLI 应覆盖 .env 中的 api_key。"""
cfg = Config.load(
str(yaml_path),
cli_args={"llm.api_key": "cli-key"},
env_path=str(env_path),
)
assert cfg.llm.api_key == "cli-key"
# ---------------------------------------------------------------------------
# 测试: 优先级
# ---------------------------------------------------------------------------
class TestPriority:
"""三层优先级: CLI > .env > YAML。"""
def test_full_priority_chain(self, yaml_path: Path, env_path: Path) -> None:
"""CLI > .env > YAML 的完整优先级链。"""
cfg = Config.load(
str(yaml_path),
cli_args={"llm.api_key": "cli-key"},
env_path=str(env_path),
)
# CLI 覆盖 .env
assert cfg.llm.api_key == "cli-key"
# .env 覆盖 YAMLvlm 未被 CLI 覆盖)
assert cfg.vlm.api_key == "env-vlm-key"
# ---------------------------------------------------------------------------
# 测试: embed_dim 一致性校验
# ---------------------------------------------------------------------------
class TestEmbedDimConsistency:
"""embed.embed_dim 与 retriever.embed_dim 必须一致。"""
def test_inconsistent_embed_dim(self, tmp_path: Path) -> None:
"""embed_dim 不一致应抛出 ValueError。"""
bad_yaml = {
k: (v.copy() if isinstance(v, dict) else v) for k, v in _FULL_YAML.items()
}
bad_yaml["retriever"] = bad_yaml["retriever"].copy()
bad_yaml["retriever"]["embed_dim"] = 512 # 与 embed.embed_dim=768 不一致
p = tmp_path / "bad_dim.yaml"
with open(p, "w") as f:
yaml.dump(bad_yaml, f)
with pytest.raises(ValueError, match="不一致"):
Config.load(str(p), env_path=str(tmp_path / ".env.nonexist"))