mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/jxxghp/MoviePilot.git
synced 2026-09-20 08:03:34 +08:00
142 lines
6.3 KiB
Python
142 lines
6.3 KiB
Python
"""成对评测必须锁定同场景、同源码和同模型条件。"""
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from scripts.evaluation.compare import compare_reports
|
|
|
|
|
|
def _report(evidence_kind: str, *, passed: bool = True) -> dict:
|
|
"""构造不含正文和凭据的最小真实报告。"""
|
|
return {
|
|
"scenario_id": "dedup_existing",
|
|
"scenario_sha256": "scenario",
|
|
"production_agent_sha256": "agent",
|
|
"skills_sha256": "skills",
|
|
"harness_sha256": "harness",
|
|
"evidence_kind": evidence_kind,
|
|
"passed": passed,
|
|
"violations": [] if passed else ["download_not_verified"],
|
|
"intelligence_evaluated": True,
|
|
"usage_complete": True,
|
|
"model_calls": 7,
|
|
"completed_model_calls": 7,
|
|
"failed_tool_calls": 0,
|
|
"tool_calls": 2,
|
|
"duplicate_attempts": 0,
|
|
"side_effects": 0,
|
|
"elapsed_seconds": 12.5,
|
|
"tokens": {"input_tokens": 100, "output_tokens": 20, "total_tokens": 120},
|
|
"model": {
|
|
"requested_model": "gpt-5.6-luna", "reasoning_effort": "max", "max_model_calls": 16,
|
|
"max_output_tokens": 8192, "timeout_seconds": 180, "harness_context_window": 128000,
|
|
"auth_mode": "codex_oauth",
|
|
},
|
|
}
|
|
|
|
|
|
def _cancel_report(evidence_kind: str) -> dict:
|
|
"""构造主动取消场景的最小真实报告,模拟一个被取消的模型请求。"""
|
|
report = _report(evidence_kind)
|
|
report.update(
|
|
scenario_id="subagent_cancel_recovery", model_calls=4, completed_model_calls=3,
|
|
usage_complete=False, agent_execution_success=evidence_kind == "moviepilot_live_model",
|
|
native_turn_completed=evidence_kind == "codex_native_controlled", native_exit_code=0,
|
|
)
|
|
if evidence_kind == "moviepilot_live_model":
|
|
report["agent_trace"] = [{
|
|
"type": "ai", "data": {"tool_calls": [{
|
|
"name": "subagent_task", "args": {"action": "cancel"},
|
|
}]},
|
|
}]
|
|
else:
|
|
report["native_events"] = [{
|
|
"type": "item.started", "item": {"type": "collab_tool_call", "tool": "close_agent"},
|
|
}]
|
|
return report
|
|
|
|
|
|
def test_same_conditions_produce_a_directional_pair_summary() -> None:
|
|
"""一侧失败也应保留有效的成对事实,不能把比较结果伪装成两侧都通过。"""
|
|
moviepilot = _report("moviepilot_live_model", passed=False)
|
|
codex = _report("codex_native_controlled")
|
|
codex["model_calls"] = 5
|
|
codex["tokens"] = {"input_tokens": 80, "output_tokens": 10, "total_tokens": 90}
|
|
result = compare_reports(moviepilot, codex)
|
|
assert result["codex_comparison"] is True and result["pair_valid"] is True
|
|
assert result["both_passed"] is False
|
|
assert result["delta_moviepilot_minus_codex"]["model_calls"] == 2
|
|
assert result["moviepilot"]["violations"] == ["download_not_verified"]
|
|
|
|
|
|
@pytest.mark.parametrize("field", ["scenario_id", "scenario_sha256", "production_agent_sha256", "skills_sha256", "harness_sha256"])
|
|
def test_different_provenance_is_rejected(field: str) -> None:
|
|
"""不同场景或代码指纹不能被合并成一次比较。"""
|
|
moviepilot, codex = _report("moviepilot_live_model"), _report("codex_native_controlled")
|
|
codex[field] = "different"
|
|
with pytest.raises(ValueError, match=field):
|
|
compare_reports(moviepilot, codex)
|
|
|
|
|
|
def test_different_model_condition_is_rejected() -> None:
|
|
"""模型、推理档位或预算不同会破坏对照,必须先停止。"""
|
|
moviepilot, codex = _report("moviepilot_live_model"), _report("codex_native_controlled")
|
|
codex["model"]["reasoning_effort"] = "high"
|
|
with pytest.raises(ValueError, match="模型"):
|
|
compare_reports(moviepilot, codex)
|
|
|
|
|
|
def test_incomplete_usage_is_rejected() -> None:
|
|
"""缺少供应商用量时保留单侧报告,但不能宣称完成成对比较。"""
|
|
moviepilot, codex = _report("moviepilot_live_model"), _report("codex_native_controlled")
|
|
moviepilot["usage_complete"] = False
|
|
with pytest.raises(ValueError, match="完整用量"):
|
|
compare_reports(moviepilot, codex)
|
|
|
|
|
|
def test_cancel_pair_allows_intentional_incomplete_usage_without_cost_delta() -> None:
|
|
"""主动取消造成的未完整用量只允许行为配对,不能产生误导性的 token 差。"""
|
|
moviepilot = _cancel_report("moviepilot_live_model")
|
|
codex = _cancel_report("codex_native_controlled")
|
|
result = compare_reports(moviepilot, codex)
|
|
assert result["pair_valid"] is True and result["usage_comparable"] is False
|
|
assert result["delta_moviepilot_minus_codex"]["total_tokens"] is None
|
|
|
|
|
|
def test_cancel_pair_allows_one_side_to_finish_before_cancel() -> None:
|
|
"""取消时序可能让一侧在子代理模型请求前收口,仍应保留行为配对。"""
|
|
moviepilot = _cancel_report("moviepilot_live_model")
|
|
moviepilot["usage_complete"] = True
|
|
moviepilot["completed_model_calls"] = moviepilot["model_calls"]
|
|
codex = _cancel_report("codex_native_controlled")
|
|
result = compare_reports(moviepilot, codex)
|
|
assert result["pair_valid"] is True and result["usage_comparable"] is False
|
|
assert result["delta_moviepilot_minus_codex"]["total_tokens"] is None
|
|
|
|
|
|
def test_cancel_pair_without_cancel_evidence_is_rejected() -> None:
|
|
"""场景名不能单独绕过完整用量门禁。"""
|
|
moviepilot = _cancel_report("moviepilot_live_model")
|
|
codex = _cancel_report("codex_native_controlled")
|
|
codex["native_events"] = []
|
|
with pytest.raises(ValueError, match="完整用量"):
|
|
compare_reports(moviepilot, codex)
|
|
|
|
|
|
def test_cli_compare_reads_reports_and_writes_output(tmp_path: Path) -> None:
|
|
"""命令行比较只读取已有报告,不启动模型或业务服务。"""
|
|
live, native, output = (tmp_path / name for name in ("live.json", "native.json", "pair.json"))
|
|
live.write_text(json.dumps(_report("moviepilot_live_model")), encoding="utf-8")
|
|
native.write_text(json.dumps(_report("codex_native_controlled")), encoding="utf-8")
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "scripts.evaluation", "--compare", str(live), str(native), "--output", str(output)],
|
|
capture_output=True, text=True, check=False,
|
|
)
|
|
payload = json.loads(result.stdout)
|
|
assert result.returncode == 0 and payload["pair_valid"] is True
|
|
assert json.loads(output.read_text(encoding="utf-8")) == payload
|