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
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:
@@ -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"] == {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
49
apps/api/tests/test_evidence_snapshot_tenant.py
Normal file
49
apps/api/tests/test_evidence_snapshot_tenant.py
Normal 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
|
||||
@@ -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
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user