fix: validate global frozen pools baseline_run_id + sha256 on load
This commit is contained in:
@@ -844,6 +844,28 @@ def build_or_load_pools(
|
||||
len(new_types),
|
||||
sorted(new_types),
|
||||
)
|
||||
else:
|
||||
# global:校验 baseline_run_id 与(若有)manifest 内容指纹,
|
||||
# 拒绝静默加载与 seed 错配 / 被篡改的冻结切分(P5 fail loud)。
|
||||
frozen_baseline = raw.get("baseline_run_id")
|
||||
if frozen_baseline != baseline_run_id:
|
||||
raise ValueError(
|
||||
f"冻结 pools.json 的 baseline_run_id={frozen_baseline!r} 与 seed "
|
||||
f"的 {baseline_run_id!r} 不一致,拒绝静默加载错配切分。"
|
||||
)
|
||||
manifest_path = config.workspace_dir / "split_manifest.json"
|
||||
if manifest_path.exists():
|
||||
import hashlib
|
||||
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
actual_sha = hashlib.sha256(
|
||||
pools_path.read_text(encoding="utf-8").encode("utf-8")
|
||||
).hexdigest()
|
||||
if manifest.get("pools_sha256") != actual_sha:
|
||||
raise ValueError(
|
||||
"pools.json 内容指纹与 split_manifest.pools_sha256 不符,"
|
||||
"冻结产物疑被篡改,拒绝加载。"
|
||||
)
|
||||
|
||||
return load_pools(pools_path)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import pytest
|
||||
from app.harness.pools import (
|
||||
GlobalPoolStrategy,
|
||||
PerCategoryPoolStrategy,
|
||||
build_or_load_pools,
|
||||
build_pools,
|
||||
load_pools,
|
||||
save_pools,
|
||||
@@ -291,6 +292,76 @@ class TestBuildOrLoadPoolsFrozen:
|
||||
load_ids = [q.question_id for q in getattr(loaded, pool_name)]
|
||||
assert orig_ids == load_ids, f"{pool_name} 冻结后 ID 顺序不一致"
|
||||
|
||||
def _run_config_for_frozen(self, tmp_path: Path, seed_name: str) -> object:
|
||||
"""构造指向 tmp workspace/store + 指定种子名的最小 train RunConfig。"""
|
||||
from app.harness.config import RunConfig
|
||||
|
||||
return RunConfig(
|
||||
workspace_dir=tmp_path / "ws",
|
||||
store_dir=tmp_path / "store",
|
||||
mode="train",
|
||||
concurrency=4,
|
||||
max_steps=10,
|
||||
skill_mode="auto",
|
||||
n_samples=0,
|
||||
questions="benchmarks/Video-MME",
|
||||
skills_version="v1",
|
||||
prompts_version="v1",
|
||||
epochs=1,
|
||||
diag_size=10,
|
||||
diag_correct_ratio=0.5,
|
||||
val_size=10,
|
||||
val_correct_ratio=0.5,
|
||||
edit_budget_start=5,
|
||||
edit_budget_end=2,
|
||||
batch_size=15,
|
||||
min_class_per_batch=2,
|
||||
eval_min_per_class=2,
|
||||
early_stop_patience=4,
|
||||
test_size=10,
|
||||
use_slow_momentum=True,
|
||||
gate_e_confirm=20.0,
|
||||
gate_e_provisional=3.0,
|
||||
gate_w_net_min=2,
|
||||
gate_delta_min=0.02,
|
||||
gate_lambda_dir=-0.642,
|
||||
gate_e_rollback=10.0,
|
||||
gate_block=8,
|
||||
gate_n_max=40,
|
||||
gate_p_low=0.05,
|
||||
gate_p_high=0.95,
|
||||
gate_probe_quota=0.2,
|
||||
gate_gamma_decay=0.9,
|
||||
gate_cooldown_steps=2,
|
||||
gate_guard_err=0.10,
|
||||
skill_update_mode="patch",
|
||||
appendix_consolidate_threshold=6,
|
||||
fresh=True,
|
||||
seed=seed_name,
|
||||
test_questions="", # 绕过 _to_pool_config 的 resolve_paths(manifest 依赖)
|
||||
)
|
||||
|
||||
def test_global_frozen_rejects_baseline_mismatch(self, tmp_path: Path) -> None:
|
||||
"""global 冻结 pools 的 baseline_run_id 与 seed 不符时 fail-loud。"""
|
||||
seed_name = "myseed"
|
||||
seed_dir = tmp_path / "store" / "seeds" / seed_name
|
||||
seed_dir.mkdir(parents=True)
|
||||
(seed_dir / "seed.json").write_text(
|
||||
json.dumps({"baseline_run_id": "infer_adhoc", "parent": None})
|
||||
)
|
||||
|
||||
ws = tmp_path / "ws"
|
||||
ws.mkdir()
|
||||
(ws / "pools.json").write_text(
|
||||
json.dumps({"split_mode": "global", "baseline_run_id": "other"}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
config = self._run_config_for_frozen(tmp_path, seed_name)
|
||||
strategy = GlobalPoolStrategy()
|
||||
with pytest.raises(ValueError, match="baseline_run_id"):
|
||||
build_or_load_pools(config, strategy, tmp_path / "nonexistent.db")
|
||||
|
||||
|
||||
class TestGlobalPoolStrategy:
|
||||
"""GlobalPoolStrategy 封装现有全局三分逻辑。"""
|
||||
|
||||
Reference in New Issue
Block a user