fix(agent): require durable tenant evidence before apply
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m34s
CD Pipeline / build-and-deploy (push) Successful in 5m24s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 1s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 33s
CD Pipeline / post-deploy-checks (push) Successful in 3m1s

This commit is contained in:
ogt
2026-07-10 21:04:20 +08:00
parent 4c572aa57c
commit d26bf5515a
12 changed files with 402 additions and 59 deletions

View File

@@ -962,12 +962,23 @@ def test_ai_automation_program_ledger_is_authoritative_and_runtime_strict():
)
assert program["professional_review"]["finding_count"] == 9
assert program["professional_review"]["critical_finding_count"] == 3
assert program["codebase_review"]["scope"]["reviewed_file_count"] == 977
assert program["codebase_review"]["scope"]["reviewed_file_count"] == 1971
assert program["codebase_review"]["risk_metrics"][
"broad_exception_handler_count"
] == 715
assert program_summary["codebase_review_finding_count"] == 6
assert program_summary["codebase_review_critical_finding_count"] == 2
] == 1371
assert program_summary["codebase_review_finding_count"] == 8
assert program_summary["codebase_review_critical_finding_count"] == 3
tenant_finding = next(
row
for row in program["codebase_review"]["findings"]
if row["id"] == "ACR-P0-005"
)
assert tenant_finding["source_remediation_state"] == (
"fixed_tests_passed_runtime_pending"
)
assert work_item_by_id["AIA-P0-001"]["runtime_progress"][
"runtime_closed"
] is False
assert work_item_by_id["AIA-P1-002"]["status"] == "in_progress"
assert "CPU" in work_item_by_id["AIA-P1-002"]["title"]
assert work_item_by_id["AIA-P1-004"]["schedule"] == {

View File

@@ -123,6 +123,43 @@ async def test_backfill_enqueues_catalog_matched_incident(monkeypatch: pytest.Mo
assert recorded[0]["automation_run_id"]
@pytest.mark.asyncio
async def test_backfill_retries_automatically_when_predecision_evidence_is_incomplete(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.jobs import awooop_ansible_candidate_backfill_job as job
fake_db = AsyncMock()
fake_db.execute = AsyncMock(return_value=_FakeResult([_candidate_incident()]))
@asynccontextmanager
async def fake_db_context(project_id: str = "awoooi"):
yield fake_db
async def fake_evidence_collector(**_kwargs):
return None
async def fake_evidence_verifier(**kwargs):
assert kwargs["snapshot"] is None
return False
recorder = AsyncMock(return_value=True)
monkeypatch.setattr(job, "get_db_context", fake_db_context)
monkeypatch.setattr(job.settings, "ENABLE_AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WORKER", True)
result = await job.enqueue_missing_ansible_candidates_once(
recorder=recorder,
evidence_collector=fake_evidence_collector,
evidence_verifier=fake_evidence_verifier,
receipt_backfiller=AsyncMock(return_value={"written": 0, "error": None}),
retry_replayer=AsyncMock(return_value={"error": None}),
)
assert result["queued"] == 0
assert result["pre_decision_evidence_blocked"] == 1
recorder.assert_not_awaited()
@pytest.mark.asyncio
async def test_backfill_skips_when_worker_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
from src.jobs import awooop_ansible_candidate_backfill_job as job

View File

@@ -1969,6 +1969,33 @@ def test_ansible_context_receipts_reference_pre_decision_evidence_without_conten
assert "evidence.post_execution_state IS NULL" in loader_source
def test_ansible_context_receipt_rejects_zero_success_mcp_attempts() -> None:
claim = AnsibleCheckModeClaim(
op_id="00000000-0000-0000-0000-000000000132",
source_candidate_op_id="00000000-0000-0000-0000-000000000131",
incident_id="INC-RUNTIME-CONTEXT-FAILED",
catalog_id="ansible:188-ai-web",
playbook_path="infra/ansible/playbooks/188-ai-web-readonly.yml",
apply_playbook_path="infra/ansible/playbooks/188-ai-web.yml",
inventory_hosts=("host_188",),
risk_level="medium",
input_payload={},
)
receipts = build_ansible_context_runtime_stage_receipts(
claim,
{
"id": "evidence-failed",
"recent_logs": None,
"mcp_health": {"prometheus": False, "kubernetes": False},
"sensors_attempted": 2,
"sensors_succeeded": 0,
},
)
assert receipts == []
def test_ansible_timeline_projection_receipt_is_same_run_and_idempotent() -> None:
claim = AnsibleCheckModeClaim(
op_id="00000000-0000-0000-0000-000000000122",

View File

@@ -0,0 +1,49 @@
from __future__ import annotations
import pytest
from src.services import evidence_snapshot as evidence_module
from src.services.evidence_snapshot import EvidenceSnapshot
class _FakeDb:
def __init__(self) -> None:
self.records: list[object] = []
def add(self, record: object) -> None:
self.records.append(record)
async def flush(self) -> None:
return None
class _FakeDbContext:
def __init__(self, db: _FakeDb) -> None:
self.db = db
async def __aenter__(self) -> _FakeDb:
return self.db
async def __aexit__(self, *_args: object) -> None:
return None
@pytest.mark.asyncio
async def test_save_uses_snapshot_project_context(
monkeypatch: pytest.MonkeyPatch,
) -> None:
projects: list[str] = []
db = _FakeDb()
def fake_get_db_context(project_id: str) -> _FakeDbContext:
projects.append(project_id)
return _FakeDbContext(db)
monkeypatch.setattr(evidence_module, "get_db_context", fake_get_db_context)
snapshot = EvidenceSnapshot(incident_id="INC-TENANT", project_id="tenant-a")
await snapshot.save()
assert projects == ["tenant-a"]
assert len(db.records) == 1
assert snapshot.persisted is True

View File

@@ -224,6 +224,46 @@ class TestComputeFingerprint:
assert all(c in "0123456789abcdef" for c in fp)
class TestInvestigateCacheIdentity:
@pytest.mark.asyncio
async def test_cache_hit_rebinds_identity_and_persists_current_incident(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
cached = EvidenceSnapshot(
incident_id="INC-OLD",
snapshot_id="snapshot-old",
recent_logs="shared log evidence",
sensors_attempted=2,
sensors_succeeded=2,
mcp_health={"kubectl_logs": True},
)
saved: list[EvidenceSnapshot] = []
async def fake_get_cache(
_fingerprint: str,
**_kwargs: object,
) -> EvidenceSnapshot:
return cached
async def fake_save(snapshot: EvidenceSnapshot) -> str:
saved.append(snapshot)
return snapshot.snapshot_id
monkeypatch.setattr(pdi_module, "_get_cache", fake_get_cache)
monkeypatch.setattr(EvidenceSnapshot, "save", fake_save)
incident = _stub_incident(alertname="CacheIdentity")
snapshot = await PreDecisionInvestigator().investigate(incident)
assert snapshot.incident_id == incident.incident_id
assert snapshot.snapshot_id != cached.snapshot_id
assert snapshot.project_id == "awoooi"
assert snapshot.recent_logs == "shared log evidence"
assert snapshot.alert_info["incident_id"] == incident.incident_id
assert saved == [snapshot]
# ─────────────────────────────────────────────────────────────────────────────
# _build_tool_params
# ─────────────────────────────────────────────────────────────────────────────