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),
|
len(new_types),
|
||||||
sorted(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)
|
return load_pools(pools_path)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import pytest
|
|||||||
from app.harness.pools import (
|
from app.harness.pools import (
|
||||||
GlobalPoolStrategy,
|
GlobalPoolStrategy,
|
||||||
PerCategoryPoolStrategy,
|
PerCategoryPoolStrategy,
|
||||||
|
build_or_load_pools,
|
||||||
build_pools,
|
build_pools,
|
||||||
load_pools,
|
load_pools,
|
||||||
save_pools,
|
save_pools,
|
||||||
@@ -291,6 +292,76 @@ class TestBuildOrLoadPoolsFrozen:
|
|||||||
load_ids = [q.question_id for q in getattr(loaded, pool_name)]
|
load_ids = [q.question_id for q in getattr(loaded, pool_name)]
|
||||||
assert orig_ids == load_ids, f"{pool_name} 冻结后 ID 顺序不一致"
|
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:
|
class TestGlobalPoolStrategy:
|
||||||
"""GlobalPoolStrategy 封装现有全局三分逻辑。"""
|
"""GlobalPoolStrategy 封装现有全局三分逻辑。"""
|
||||||
|
|||||||
Reference in New Issue
Block a user