AgentEvalTool/tests/integration/test_decision_log_immutability.py
sinohqb 182b0e59cb
All checks were successful
CI / test (push) Successful in 3m9s
refactor(intelligent-eval): extract scheduler runtime + internalize decision-log dedup
架构深化两则(架构审查候选①②):

① scheduler 抽取:web/app.py 约 400 行触发式执行编排(60s 扫描循环、
docker exec 触发、失败落账)沉入 intelligent_eval/scheduler.py,runtime
单例 start()/stop()/scan_once() 与 campaign_runtime 惯例一致;worker/planner
两处重复触发代码合并为一个触发原语;_supplement_decision_logs 归入
decision_logs.py。测试改为直接驱动 scan_once(interface 即测试面)。

② 决策日志去重内化:create_decision_log 的去重只服务 agent 上报路径;
新增 append_decision_log(平台落账纯追加)与 count_decisions(计数原语),
lifecycle/task_queue 全部平台落账切换,调用方不再需要塞 attempt 骗去重。

零行为变化:提示词、60s 节拍、编排顺序、闸门语义原样保留,866 tests passed。
2026-08-21 03:20:14 +08:00

177 lines
5.8 KiB
Python

"""Decision-log immutability & dedup contract (Gitea issue #6 / T8).
Pins the desired behaviour: a decision log is append-only and not silently
overwritten or duplicated. The current router writes through directly; once
it moves into a service (P3), these guards must continue to pass.
"""
import uuid
import pytest
from fastapi.testclient import TestClient
from sqlmodel import Session, SQLModel, create_engine, select
from agenteval.intelligent_eval.models import IntelligentEvalStatus
from agenteval.storage.db import (
IntelligentEvalDB,
IntelligentEvalDecisionLogDB,
utc_now,
)
from agenteval.web.app import app
from agenteval.web.deps import get_db
@pytest.fixture()
def client(tmp_path):
from agenteval.storage.db import ( # noqa: F401
IntelligentEvalDB,
IntelligentEvalSessionDB,
IntelligentEvalTaskQueueDB,
)
engine = create_engine(
f"sqlite:///{tmp_path / 'test.db'}",
connect_args={"check_same_thread": False},
)
SQLModel.metadata.create_all(engine)
session = Session(engine)
def override_get_db():
try:
yield session
finally:
pass
app.dependency_overrides[get_db] = override_get_db
yield TestClient(app)
app.dependency_overrides.clear()
session.close()
engine.dispose()
@pytest.fixture()
def db_session(client):
return next(app.dependency_overrides[get_db]())
def _make_eval(db_session: Session) -> IntelligentEvalDB:
eval_db = IntelligentEvalDB(
id=str(uuid.uuid4()),
name="eval-dl-immut",
target_id="t1",
status=IntelligentEvalStatus.EXECUTING.value,
)
db_session.add(eval_db)
db_session.commit()
return eval_db
def _post_log(client: TestClient, eval_id: str, **overrides) -> dict:
body = {
"decision_type": "execute_session",
"reason": "slot_due",
"context": {"slot": "8-10h", "deficit": 2},
"cron_id": "cron-1",
}
body.update(overrides)
response = client.post(
f"/api/intelligent-evals/{eval_id}/decision-logs",
json=body,
)
assert response.status_code == 200, response.text
return response.json()
def test_decision_log_is_append_only_on_context_change(
client: TestClient, db_session: Session
):
"""Modifying context must append, never overwrite, an existing log row."""
eval_db = _make_eval(db_session)
first = _post_log(
client, eval_db.id, context={"slot": "8-10h", "deficit": 2}
)
second = _post_log(
client, eval_db.id, context={"slot": "10-12h", "deficit": 1}
)
assert first["id"] != second["id"]
rows = db_session.exec(
select(IntelligentEvalDecisionLogDB)
.where(IntelligentEvalDecisionLogDB.eval_id == eval_db.id)
.order_by(IntelligentEvalDecisionLogDB.created_at)
).all()
assert len(rows) == 2
# The first row's context is preserved (not overwritten by the second).
assert rows[0].get_context() == {"slot": "8-10h", "deficit": 2}
assert rows[1].get_context() == {"slot": "10-12h", "deficit": 1}
def test_decision_log_dedupes_identical_entries(
client: TestClient, db_session: Session
):
"""Identical (decision_type, context) must not insert a second row."""
eval_db = _make_eval(db_session)
body = {
"decision_type": "execute_session",
"reason": "slot_due",
"context": {"slot": "8-10h", "deficit": 2},
"cron_id": "cron-1",
}
for _ in range(3):
r = client.post(
f"/api/intelligent-evals/{eval_db.id}/decision-logs",
json=body,
)
assert r.status_code == 200
rows = db_session.exec(
select(IntelligentEvalDecisionLogDB).where(
IntelligentEvalDecisionLogDB.eval_id == eval_db.id
)
).all()
assert len(rows) == 1, f"expected dedup, got {len(rows)} rows"
def test_append_decision_log_always_appends_identical_context(db_session: Session):
"""平台落账入口不去重:相同 context 也总是追加(每次落账都是新事实)。"""
from agenteval.intelligent_eval.decision_logs import append_decision_log
eval_db = _make_eval(db_session)
ctx = {"platform_supplemented": True, "attempt": 1}
for _ in range(3):
append_decision_log(eval_db.id, "worker_trigger", "落账", "platform", ctx, db_session)
rows = db_session.exec(
select(IntelligentEvalDecisionLogDB).where(
IntelligentEvalDecisionLogDB.eval_id == eval_db.id
)
).all()
assert len(rows) == 3, f"append 不应去重,得到 {len(rows)}"
def test_append_decision_log_raises_for_unknown_eval(db_session: Session):
"""append 与 create 一样校验评估存在性。"""
from agenteval.intelligent_eval.decision_logs import append_decision_log
with pytest.raises(LookupError):
append_decision_log("no-such-eval", "worker_trigger", "r", "platform", {}, db_session)
def test_count_decisions_counts_by_eval_and_type(db_session: Session):
"""count_decisions 按 eval + 类型计数(闸门与 attempt 落账的计数原语)。"""
from agenteval.intelligent_eval.decision_logs import append_decision_log, count_decisions
eval_db = _make_eval(db_session)
other = _make_eval(db_session)
for _ in range(2):
append_decision_log(eval_db.id, "planner_trigger", "r", "platform", {"n": 1}, db_session)
append_decision_log(eval_db.id, "worker_trigger", "r", "platform", {"n": 2}, db_session)
append_decision_log(other.id, "planner_trigger", "r", "platform", {"n": 3}, db_session)
assert count_decisions(eval_db.id, "planner_trigger", db_session) == 2
assert count_decisions(eval_db.id, "worker_trigger", db_session) == 1
assert count_decisions(other.id, "planner_trigger", db_session) == 1
assert count_decisions(eval_db.id, "trigger_failed", db_session) == 0