Files
Video-Tree-TRM5/tests/unit/test_harness_observation.py
T
iomgaa e8b66f85ab feat: continuous concurrent gate orchestrator (algo #6)
- validate_skills_concurrent: 多题型全部臂共享题槽并发编排,发射序 =
  题型 round-robin × 阶梯序(base 先 cand 后),终态统一组装 outcome,
  verdict None(全 INFRA)保留 RuntimeError 语义
- gate_evidence 列 block_idx → ladder_rank(阶梯序号,0-based);旧块路径
  _build_evidence_rows 仅键名同步(值仍为块号)保持落库兼容
- 新增 3 项编排测试:乱序到达前缀有序性/双题型隔离/全 INFRA raise

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 00:36:01 -04:00

386 lines
12 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.
"""五张观测表 + step/epoch 报告的单元测试。"""
from __future__ import annotations
import json
import sqlite3
from typing import TYPE_CHECKING
import pytest
if TYPE_CHECKING:
from pathlib import Path
from app.harness.observation import (
read_dual_metric,
read_gate_evidence,
read_holdout_eval,
read_quadrant_pairs,
read_shadow_gate,
write_dual_metric,
write_epoch_report,
write_gate_evidence,
write_holdout_eval,
write_quadrant_pairs,
write_shadow_gate,
write_step_report,
)
@pytest.fixture()
def db_path(tmp_path: Path) -> str:
"""返回临时 SQLite 路径。"""
return str(tmp_path / "test_obs.db")
@pytest.fixture()
def run_id() -> str:
return "run-obs-001"
# ---------------------------------------------------------------------------
# dual_metric_eval
# ---------------------------------------------------------------------------
def test_write_read_dual_metric(db_path: str, run_id: str) -> None:
"""写入 dual_metric_eval 后回读应与输入一致。"""
write_dual_metric(
db_path,
run_id=run_id,
epoch=1,
version_kind="baseline",
skills_version="v0",
prompts_version="v0",
pool="val",
hard_acc=0.75,
soft_score=0.80,
mixed_score=0.775,
)
rows = read_dual_metric(db_path, run_id=run_id)
assert len(rows) == 1
row = rows[0]
assert row["epoch"] == 1
assert row["version_kind"] == "baseline"
assert row["skills_version"] == "v0"
assert row["prompts_version"] == "v0"
assert row["pool"] == "val"
assert row["hard_acc"] == pytest.approx(0.75)
assert row["soft_score"] == pytest.approx(0.80)
assert row["mixed_score"] == pytest.approx(0.775)
assert row["run_id"] == run_id
def test_dual_metric_kinds_distinguishable(db_path: str, run_id: str) -> None:
"""observation 层接受任意 version_kindfinal 与 slow_candidate 可区分共存。"""
write_dual_metric(
db_path,
run_id=run_id,
epoch=1,
version_kind="final",
skills_version="v1",
prompts_version="v1",
pool="val",
hard_acc=0.7,
soft_score=None,
mixed_score=None,
)
write_dual_metric(
db_path,
run_id=run_id,
epoch=1,
version_kind="slow_candidate",
skills_version="v2",
prompts_version="v2",
pool="val",
hard_acc=0.6,
soft_score=None,
mixed_score=None,
)
rows = read_dual_metric(db_path, run_id=run_id)
kinds = {row["version_kind"] for row in rows if row["epoch"] == 1}
assert kinds == {"final", "slow_candidate"}
# ---------------------------------------------------------------------------
# shadow_gate
# ---------------------------------------------------------------------------
def test_write_read_shadow_gate(db_path: str, run_id: str) -> None:
"""写入 shadow_gate 后回读应与输入一致,is_mixed_best 布尔转 int。"""
write_shadow_gate(
db_path,
run_id=run_id,
epoch=2,
candidate_version="skills/v1+prompts/v1",
hard_acc=0.82,
soft_score=0.78,
mixed_score=0.80,
is_mixed_best=True,
)
rows = read_shadow_gate(db_path, run_id=run_id)
assert len(rows) == 1
row = rows[0]
assert row["epoch"] == 2
assert row["candidate_version"] == "skills/v1+prompts/v1"
assert row["hard_acc"] == pytest.approx(0.82)
assert row["is_mixed_best"] == 1
# ---------------------------------------------------------------------------
# holdout_eval
# ---------------------------------------------------------------------------
def test_write_read_holdout_eval(db_path: str, run_id: str) -> None:
"""写入 holdout_eval 后回读应与输入一致。"""
per_task = json.dumps({"temporal": {"accuracy": 0.9, "total": 10, "correct": 9}})
write_holdout_eval(
db_path,
run_id=run_id,
epoch=1,
version_kind="best_hard",
hard_acc=0.85,
soft_score=0.70,
mixed_score=0.775,
per_task_type_json=per_task,
)
rows = read_holdout_eval(db_path, run_id=run_id)
assert len(rows) == 1
row = rows[0]
assert row["version_kind"] == "best_hard"
assert row["hard_acc"] == pytest.approx(0.85)
parsed = json.loads(row["per_task_type_json"])
assert parsed["temporal"]["correct"] == 9
# ---------------------------------------------------------------------------
# gate_evidence
# ---------------------------------------------------------------------------
def test_write_read_gate_evidence(db_path: str, run_id: str) -> None:
"""写入 gate_evidence 后回读应与输入一致,逐行插入。"""
evidence_rows = [
{
"task_type": "temporal",
"question_id": "q1",
"ladder_rank": 0,
"baseline_correct": 1,
"candidate_correct": 1,
"e_value": 1.0,
"stop_reason": "",
},
{
"task_type": "temporal",
"question_id": "q2",
"ladder_rank": 0,
"baseline_correct": 0,
"candidate_correct": 1,
"e_value": 2.0,
"stop_reason": "confirmed",
},
]
write_gate_evidence(db_path, run_id=run_id, epoch=1, step=0, rows=evidence_rows)
rows = read_gate_evidence(db_path, run_id=run_id)
assert len(rows) == 2
assert rows[0]["question_id"] == "q1"
assert rows[1]["stop_reason"] == "confirmed"
assert rows[1]["e_value"] == pytest.approx(2.0)
# ---------------------------------------------------------------------------
# quadrant_pair
# ---------------------------------------------------------------------------
def test_write_read_quadrant_pairs(db_path: str, run_id: str) -> None:
"""写入 quadrant_pair 后回读,bool -> int 转换正确。"""
pairs = [
{
"question_id": "q1",
"task_type": "causal",
"prev_correct": True,
"curr_correct": False,
"category": "regression",
},
{
"question_id": "q2",
"task_type": "causal",
"prev_correct": False,
"curr_correct": True,
"category": "improvement",
},
]
write_quadrant_pairs(db_path, run_id=run_id, epoch=1, step=0, pairs=pairs)
rows = read_quadrant_pairs(db_path, run_id=run_id)
assert len(rows) == 2
# bool -> int 转换
assert rows[0]["prev_correct"] == 1
assert rows[0]["curr_correct"] == 0
assert rows[1]["prev_correct"] == 0
assert rows[1]["curr_correct"] == 1
# ---------------------------------------------------------------------------
# NULL vs 0 语义
# ---------------------------------------------------------------------------
def test_null_not_zero_for_soft(db_path: str, run_id: str) -> None:
"""soft_score/mixed_score 为 None 时存 NULL(非 0),回读也是 None。"""
write_dual_metric(
db_path,
run_id=run_id,
epoch=1,
version_kind="baseline",
skills_version="v0",
prompts_version="v0",
pool="val",
hard_acc=0.75,
soft_score=None,
mixed_score=None,
)
rows = read_dual_metric(db_path, run_id=run_id)
assert len(rows) == 1
row = rows[0]
assert row["soft_score"] is None
assert row["mixed_score"] is None
# 用原生 SQL 确认存的是 NULL 而非 0
conn = sqlite3.connect(db_path)
cursor = conn.execute(
"SELECT soft_score, mixed_score FROM dual_metric_eval WHERE run_id=?",
(run_id,),
)
raw = cursor.fetchone()
conn.close()
assert raw[0] is None
assert raw[1] is None
# ---------------------------------------------------------------------------
# 只读连接隔离
# ---------------------------------------------------------------------------
def test_read_only_connection(db_path: str, run_id: str) -> None:
"""read_* 使用独立只读连接,不向 _runs 表插入新行。"""
write_dual_metric(
db_path,
run_id=run_id,
epoch=1,
version_kind="baseline",
skills_version="v0",
prompts_version="v0",
pool="val",
hard_acc=0.5,
soft_score=None,
mixed_score=None,
)
# 用另一个 run_id 回读——不应在 _runs 表中创建新行
other_run = "run-obs-ghost"
rows = read_dual_metric(db_path, run_id=other_run)
assert rows == []
conn = sqlite3.connect(db_path)
cursor = conn.execute("SELECT run_id FROM _runs")
run_ids = [r[0] for r in cursor.fetchall()]
conn.close()
assert other_run not in run_ids
# ---------------------------------------------------------------------------
# step_report
# ---------------------------------------------------------------------------
def test_write_step_report(tmp_path: Path) -> None:
"""step_report 写入 JSON 文件,内容字段完整。"""
workspace = tmp_path / "ws"
workspace.mkdir()
path = write_step_report(
workspace_dir=workspace,
epoch=1,
step=2,
global_step=12,
task_type="Temporal Order",
gate_action="accept_confirmed",
candidate_acc=0.85,
class_baseline_acc=0.70,
edit_budget=5,
rank_clip_triggered=False,
gate_w=3,
gate_l=1,
gate_e_value=4.2,
gate_n_used=8,
gate_stop_reason="confirmed",
)
assert path.exists()
assert path.name == "step_report_e1_s2_temporal-order.json"
data = json.loads(path.read_text(encoding="utf-8"))
assert data["epoch"] == 1
assert data["step"] == 2
assert data["global_step"] == 12
assert data["task_type"] == "Temporal Order"
assert data["gate_action"] == "accept_confirmed"
assert data["gate_w"] == 3
assert data["gate_stop_reason"] == "confirmed"
assert data["rank_clip_triggered"] is False
def test_write_step_report_skipped_null_fields(tmp_path: Path) -> None:
"""skipped/cooldown 路径的 gate 字段应为 null。"""
workspace = tmp_path / "ws"
workspace.mkdir()
path = write_step_report(
workspace_dir=workspace,
epoch=1,
step=0,
global_step=0,
task_type="causal",
gate_action="skipped",
candidate_acc=0.0,
class_baseline_acc=0.0,
edit_budget=10,
rank_clip_triggered=False,
gate_w=None,
gate_l=None,
gate_e_value=None,
gate_n_used=None,
gate_stop_reason=None,
)
data = json.loads(path.read_text(encoding="utf-8"))
assert data["gate_w"] is None
assert data["gate_l"] is None
assert data["gate_e_value"] is None
assert data["gate_n_used"] is None
assert data["gate_stop_reason"] is None
# ---------------------------------------------------------------------------
# epoch_report
# ---------------------------------------------------------------------------
def test_write_epoch_report(tmp_path: Path) -> None:
"""epoch_report 写入 JSON 文件,内容字段完整。"""
workspace = tmp_path / "ws"
workspace.mkdir()
path = write_epoch_report(
workspace_dir=workspace,
epoch=3,
system_tool_action="updated",
momentum_updated_task_types=["temporal", "causal"],
best_val_acc=0.88,
)
assert path.exists()
assert path.name == "epoch_report_3.json"
data = json.loads(path.read_text(encoding="utf-8"))
assert data["epoch"] == 3
assert data["system_tool_action"] == "updated"
assert data["momentum_updated_task_types"] == ["temporal", "causal"]
assert data["best_val_acc"] == pytest.approx(0.88)