All checks were successful
CI / test (push) Successful in 3m9s
架构深化两则(架构审查候选①②): ① 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。
177 lines
5.8 KiB
Python
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
|