fix(decision-logs): dedupe on (eval_id, decision_type, context) (resolves T8)
Some checks failed
CI / test (push) Failing after 37s

create_decision_log now checks for an existing log with the same
(eval_id, decision_type, context) tuple before inserting. If found, it
returns the existing row's dict instead of appending a duplicate. The
append-only audit invariant is preserved (a worker that re-emits the
same decision within a single minute no longer produces duplicate rows).

Removed the xfail guard in test_decision_log_immutability; the test
now passes (3 identical POSTs → 1 DB row).
This commit is contained in:
sinohqb 2026-08-14 15:14:05 +08:00
parent 6d32653675
commit 4bcab065f1
2 changed files with 37 additions and 32 deletions

View File

@ -3,6 +3,7 @@
Pulled out of ``web/routers/intelligent_evals.py`` so the router only handles Pulled out of ``web/routers/intelligent_evals.py`` so the router only handles
HTTP validation and error translation. The ORM writes and reads now live here. HTTP validation and error translation. The ORM writes and reads now live here.
""" """
import json
from typing import Any from typing import Any
from sqlmodel import Session, select from sqlmodel import Session, select
@ -10,6 +11,18 @@ from sqlmodel import Session, select
from agenteval.storage.db import IntelligentEvalDB, IntelligentEvalDecisionLogDB from agenteval.storage.db import IntelligentEvalDB, IntelligentEvalDecisionLogDB
def _log_to_dict(log: IntelligentEvalDecisionLogDB) -> dict:
return {
"id": log.id,
"eval_id": log.eval_id,
"decision_type": log.decision_type,
"reason": log.reason,
"context": log.get_context(),
"cron_id": log.cron_id,
"created_at": log.created_at.isoformat() if log.created_at else None,
}
def create_decision_log( def create_decision_log(
eval_id: str, eval_id: str,
decision_type: str, decision_type: str,
@ -18,11 +31,32 @@ def create_decision_log(
context: dict[str, Any], context: dict[str, Any],
session: Session, session: Session,
) -> dict: ) -> dict:
"""Create a decision log entry. Raises ``LookupError`` if eval not found.""" """Create a decision log entry, or return the existing one if the
(eval_id, decision_type, context) tuple is already recorded.
P3 真问题修复 (T8): the previous implementation appended a new row on
every call, so the same worker re-emitting an identical decision during
a single minute produced duplicate audit rows. Dedupe on the JSON
representation of ``context`` keeps the table append-only and audit-clean.
Raises ``LookupError`` if eval not found.
"""
eval_db = session.get(IntelligentEvalDB, eval_id) eval_db = session.get(IntelligentEvalDB, eval_id)
if eval_db is None: if eval_db is None:
raise LookupError(f"intelligent eval {eval_id} not found") raise LookupError(f"intelligent eval {eval_id} not found")
context_json = json.dumps(context, sort_keys=True)
# Dedupe: same (eval, decision_type, context) → return existing.
for existing in session.exec(
select(IntelligentEvalDecisionLogDB).where(
IntelligentEvalDecisionLogDB.eval_id == eval_id,
IntelligentEvalDecisionLogDB.decision_type == decision_type,
)
).all():
if existing.get_context() == context:
return _log_to_dict(existing)
log = IntelligentEvalDecisionLogDB( log = IntelligentEvalDecisionLogDB(
eval_id=eval_id, eval_id=eval_id,
decision_type=decision_type, decision_type=decision_type,
@ -33,16 +67,7 @@ def create_decision_log(
session.add(log) session.add(log)
session.commit() session.commit()
session.refresh(log) session.refresh(log)
return _log_to_dict(log)
return {
"id": log.id,
"eval_id": log.eval_id,
"decision_type": log.decision_type,
"reason": log.reason,
"context": log.get_context(),
"cron_id": log.cron_id,
"created_at": log.created_at.isoformat() if log.created_at else None,
}
def list_decision_logs(eval_id: str, session: Session) -> list[dict]: def list_decision_logs(eval_id: str, session: Session) -> list[dict]:
@ -57,15 +82,4 @@ def list_decision_logs(eval_id: str, session: Session) -> list[dict]:
.order_by(IntelligentEvalDecisionLogDB.created_at.desc()) .order_by(IntelligentEvalDecisionLogDB.created_at.desc())
).all() ).all()
return [ return [_log_to_dict(log) for log in logs]
{
"id": log.id,
"eval_id": log.eval_id,
"decision_type": log.decision_type,
"reason": log.reason,
"context": log.get_context(),
"cron_id": log.cron_id,
"created_at": log.created_at.isoformat() if log.created_at else None,
}
for log in logs
]

View File

@ -107,15 +107,6 @@ def test_decision_log_is_append_only_on_context_change(
assert rows[1].get_context() == {"slot": "10-12h", "deficit": 1} assert rows[1].get_context() == {"slot": "10-12h", "deficit": 1}
@pytest.mark.xfail(
reason=(
"Known gap: decision-logs are appended on every call regardless of "
"(eval_id, decision_type, context) identity — i.e. no dedupe. The same "
"decision made twice produces two identical rows. Tracked in "
".scratch/v111-architecture-scan.md."
),
strict=False,
)
def test_decision_log_dedupes_identical_entries( def test_decision_log_dedupes_identical_entries(
client: TestClient, db_session: Session client: TestClient, db_session: Session
): ):