aps-agent/tests/golden/test_trace_run.py

161 lines
7.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.

# ============================================================
# round-15 黄金测试:trace_chain 接入 schedule.run(矩阵 114 行)
# 覆盖:ALGO_RUN 审计携带 traceChainHash(格式正确)、
# 同输入/同种子/同版本可复算、元素/顺序/输入变化断链。
# ============================================================
from __future__ import annotations
import asyncio
import re
from server.agent_core.evidence import EvidenceItem, trace_chain
from server.aps_domain.workflow import handle_intent
from server.contracts import IntentResult
from server.state.seed import seed_world
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
class _MemStore:
"""最小内存 store:满足 _run_schedule 对 data/next_id/save 的依赖。"""
def __init__(self, data):
self.data = data
def next_id(self, kind: str) -> int:
key = f"_c_{kind}"
self.data[key] = self.data.get(key, 0) + 1
return self.data[key]
def save(self):
pass
def _run_schedule_once(*, strategy: str = "COMPREHENSIVE") -> tuple[dict, _MemStore]:
"""在全新种子世界上跑一次 schedule.run,返回 (rationale, store)。"""
store = _MemStore(seed_world())
async def _run():
await handle_intent(
store, "t",
IntentResult(intent="schedule.run",
params={"strategy": strategy, "engine": "RULE"},
confidence=1.0, source="RULE_FAST"))
asyncio.run(_run())
runs = [e for e in store.data["auditEvents"]
if e["category"] == "ALGO_RUN" and e["action"] == "schedule.run"]
assert runs, "应产生一条 ALGO_RUN schedule.run 审计"
return runs[-1]["rationale"], store
def test_schedule_run_audit_carries_trace_chain_hash():
"""1) schedule.run 审计含 traceChainHash/traceCount/traceSummary(非空、格式正确)。"""
rationale, _ = _run_schedule_once()
chain_hash = rationale["traceChainHash"]
assert isinstance(chain_hash, str) and chain_hash, "traceChainHash 非空"
assert _HEX64.match(chain_hash), "traceChainHash 应为 64 位小写十六进制 SHA-256"
assert rationale["traceCount"] == 3, "链应含 schedule-version/run/algorithm 三条"
summary = rationale["traceSummary"]
assert isinstance(summary, list) and len(summary) == 3
assert [it["kind"] for it in summary] == ["schedule-version", "run", "algorithm"]
assert summary[0]["ref"] and summary[1]["ref"].startswith("run-")
def test_schedule_run_trace_recomputable_same_input():
"""2) 可复算:同输入/同种子/同版本再次求解得到相同 chainHash。"""
h1, _ = _run_schedule_once()
h2, _ = _run_schedule_once()
assert h1["traceChainHash"] == h2["traceChainHash"]
assert h1["traceCount"] == h2["traceCount"] == 3
assert h1["traceSummary"] == h2["traceSummary"]
def test_trace_chain_breaks_on_element_or_order_change():
"""3) 断链:EvidenceItem 元素或顺序变化后 chainHash 不同(纯函数直接验证)。"""
items = [
EvidenceItem(kind="schedule-version", ref="1", version="1"),
EvidenceItem(kind="run", ref="run-1", runId="1", engine="RULE",
meta={"strategy": "COMPREHENSIVE"}),
EvidenceItem(kind="algorithm", ref="RULE", version="RULE", inputsHash="abc"),
]
base = trace_chain(items)["chainHash"]
# 元素内容变化(version 不同 → 断链)
changed = [
EvidenceItem(kind="schedule-version", ref="2", version="2"),
EvidenceItem(kind="run", ref="run-1", runId="1", engine="RULE",
meta={"strategy": "COMPREHENSIVE"}),
EvidenceItem(kind="algorithm", ref="RULE", version="RULE", inputsHash="abc"),
]
assert trace_chain(changed)["chainHash"] != base
# 顺序变化 → 断链
assert trace_chain(list(reversed(items)))["chainHash"] != base
# 数量变化(删元素)→ 断链
assert trace_chain(items[:-1])["chainHash"] != base
def test_schedule_run_trace_breaks_on_input_change():
"""3+) 端到端断链:输入(策略)变化后审计 chainHash 不同。"""
h_comp, _ = _run_schedule_once(strategy="COMPREHENSIVE")
h_deliv, _ = _run_schedule_once(strategy="DELIVERY_FIRST")
assert h_comp["traceChainHash"] != h_deliv["traceChainHash"]
assert h_comp["traceCount"] == h_deliv["traceCount"] == 3
def test_all_engines_recomputable_and_evidence_has_schedule_version():
"""round-16:RULE/CP/HYBRID/GA 四引擎 schedule.run 均可复算,
evidence_refs 含 schedule-version 引用(对齐 round-15 计划假设 2)。"""
from server.aps_domain.workflow import _run_schedule
from server.contracts import IntentResult
from server.state.seed import seed_world
for engine in ("RULE", "CP", "HYBRID", "GA"):
r1, _ = _run_schedule_once_engine(engine)
r2, _ = _run_schedule_once_engine(engine)
assert r1["traceChainHash"] == r2["traceChainHash"], f"{engine} 应可复算"
assert any(ref.startswith("schedule-version:") for ref in r1["evidence"]), \
f"{engine} evidence 应含 schedule-version 引用"
assert r1["traceCount"] == r2["traceCount"] == 3
def _run_schedule_once_engine(engine: str) -> tuple[dict, _MemStore]:
"""在全新种子世界上以指定引擎跑一次 schedule.run,返回 (rationale, store)。"""
store = _MemStore(seed_world())
async def _run():
await handle_intent(
store, "t",
IntentResult(intent="schedule.run",
params={"strategy": "COMPREHENSIVE", "engine": engine},
confidence=1.0, source="RULE_FAST"))
asyncio.run(_run())
runs = [e for e in store.data["auditEvents"]
if e["category"] == "ALGO_RUN" and e["action"] == "schedule.run"]
assert runs, f"应生成一条 ALGO_RUN schedule.run 审计({engine})"
return runs[-1]["rationale"], store
def test_trace_chain_hash_ignores_wallclock():
"""round-16:墙钟 solveTimeSec 不进入链哈希(同输入跨引擎可复算的根基)。"""
from server.aps_domain.workflow import _build_run_trace
from server.contracts import ScheduleResult, IntentResult
from server.engines.base import EngineParams
store = _MemStore(seed_world())
params = EngineParams(engineType="CP", strategyTemplate="COMPREHENSIVE",
planningHorizonDays=14, timeLimitSeconds=8.0)
intent = IntentResult(intent="schedule.run", params={}, confidence=1.0, source="RULE_FAST")
def mk(tsec: float):
return ScheduleResult(
versionId=1, versionNo="V1", engineType="CP", strategy="COMPREHENSIVE",
status="DRAFT", orderCount=7, poCount=7, woCount=30, conflictCount=0,
totalTardiness=1.0, avgUtilization=0.8, totalCost=0, evidenceRefs=["run:V1"],
solveStatus="OPTIMAL", solveTimeSec=tsec, optimalityGap=0.0)
t1 = _build_run_trace(mk(1.234), params, intent, store)
t2 = _build_run_trace(mk(5.678), params, intent, store)
assert t1["chainHash"] == t2["chainHash"], "墙钟不应影响链哈希"
assert t1["versionRef"] == "schedule-version:1"