fix(agent): canary repaired playbook revisions
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
||||
from src.services.awooop_ansible_audit_service import get_ansible_catalog_item
|
||||
from src.services.awooop_ansible_check_mode_service import (
|
||||
_apply_ansible_playbook_trust_gate,
|
||||
_revalidate_claim_playbook_trust,
|
||||
@@ -9,18 +10,27 @@ from src.services.awooop_ansible_check_mode_service import (
|
||||
)
|
||||
|
||||
|
||||
def _candidate(catalog_id: str) -> dict[str, object]:
|
||||
def _candidate(
|
||||
catalog_id: str,
|
||||
*,
|
||||
catalog_revision: str = "test-revision-v1",
|
||||
) -> dict[str, object]:
|
||||
catalog_item = get_ansible_catalog_item(catalog_id) or {}
|
||||
return {
|
||||
"catalog_id": catalog_id,
|
||||
"playbook_path": f"infra/ansible/playbooks/{catalog_id.split(':', 1)[1]}.yml",
|
||||
"inventory_hosts": ["host_188"],
|
||||
"risk_level": "medium",
|
||||
"catalog_revision": (
|
||||
catalog_item.get("catalog_revision") or catalog_revision
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def test_low_trust_failed_playbook_opens_claim_circuit() -> None:
|
||||
candidate = _candidate("ansible:nginx-sync")
|
||||
guarded, gate = _apply_ansible_playbook_trust_gate(
|
||||
{"executor_candidates": [_candidate("ansible:nginx-sync")]},
|
||||
{"executor_candidates": [candidate]},
|
||||
[
|
||||
{
|
||||
"playbook_id": "PB-ANSIBLE-NGINX-SYNC",
|
||||
@@ -29,6 +39,9 @@ def test_low_trust_failed_playbook_opens_claim_circuit() -> None:
|
||||
"success_count": 0,
|
||||
"failure_count": 24,
|
||||
"review_required": False,
|
||||
"symptom_pattern": {
|
||||
"catalog_revision": candidate["catalog_revision"],
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
@@ -45,11 +58,12 @@ def test_low_trust_failed_playbook_opens_claim_circuit() -> None:
|
||||
|
||||
|
||||
def test_low_trust_candidate_is_filtered_without_blocking_healthy_candidate() -> None:
|
||||
failed = _candidate("ansible:nginx-sync")
|
||||
healthy = _candidate("ansible:188-momo-backup-user")
|
||||
guarded, gate = _apply_ansible_playbook_trust_gate(
|
||||
{
|
||||
"executor_candidates": [
|
||||
_candidate("ansible:nginx-sync"),
|
||||
failed,
|
||||
healthy,
|
||||
]
|
||||
},
|
||||
@@ -61,6 +75,9 @@ def test_low_trust_candidate_is_filtered_without_blocking_healthy_candidate() ->
|
||||
"success_count": 0,
|
||||
"failure_count": 15,
|
||||
"review_required": True,
|
||||
"symptom_pattern": {
|
||||
"catalog_revision": failed["catalog_revision"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"playbook_id": "PB-ANSIBLE-188-MOMO-BACKUP-USER",
|
||||
@@ -69,6 +86,9 @@ def test_low_trust_candidate_is_filtered_without_blocking_healthy_candidate() ->
|
||||
"success_count": 7,
|
||||
"failure_count": 1,
|
||||
"review_required": False,
|
||||
"symptom_pattern": {
|
||||
"catalog_revision": healthy["catalog_revision"],
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
@@ -79,6 +99,132 @@ def test_low_trust_candidate_is_filtered_without_blocking_healthy_candidate() ->
|
||||
assert gate["blocked_candidate_count"] == 1
|
||||
|
||||
|
||||
def test_repaired_catalog_revision_gets_one_bounded_canary() -> None:
|
||||
candidate = _candidate(
|
||||
"ansible:110-devops",
|
||||
catalog_revision="host110-route-v3",
|
||||
)
|
||||
current_revision = str(candidate["catalog_revision"])
|
||||
guarded, gate = _apply_ansible_playbook_trust_gate(
|
||||
{"executor_candidates": [candidate]},
|
||||
[
|
||||
{
|
||||
"playbook_id": "PB-ANSIBLE-110-DEVOPS",
|
||||
"status": "approved",
|
||||
"trust_score": 0.024,
|
||||
"success_count": 0,
|
||||
"failure_count": 1,
|
||||
"review_required": False,
|
||||
"symptom_pattern": {
|
||||
"catalog_revision": f"{current_revision}-previous",
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert guarded["executor_candidates"] == [candidate]
|
||||
assert gate["status"] == "closed"
|
||||
decision = gate["decisions"][0]
|
||||
assert decision["circuit_open"] is False
|
||||
assert decision["bounded_repair_canary"] is True
|
||||
assert decision["reason"] == "repaired_catalog_revision_canary_allowed"
|
||||
assert decision["current_catalog_revision"] == current_revision
|
||||
assert decision["catalog_revision_source"] == "allowlisted_catalog"
|
||||
assert decision["observed_catalog_revision"] == (
|
||||
f"{current_revision}-previous"
|
||||
)
|
||||
|
||||
|
||||
def test_failed_repaired_revision_reopens_circuit_for_same_revision() -> None:
|
||||
candidate = _candidate(
|
||||
"ansible:110-devops",
|
||||
catalog_revision="host110-route-v3",
|
||||
)
|
||||
current_revision = str(candidate["catalog_revision"])
|
||||
guarded, gate = _apply_ansible_playbook_trust_gate(
|
||||
{"executor_candidates": [candidate]},
|
||||
[
|
||||
{
|
||||
"playbook_id": "PB-ANSIBLE-110-DEVOPS",
|
||||
"status": "approved",
|
||||
"trust_score": 0.0192,
|
||||
"success_count": 0,
|
||||
"failure_count": 2,
|
||||
"review_required": False,
|
||||
"symptom_pattern": {
|
||||
"catalog_revision": current_revision,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert guarded["executor_candidates"] == []
|
||||
assert gate["status"] == "circuit_open"
|
||||
decision = gate["decisions"][0]
|
||||
assert decision["circuit_open"] is True
|
||||
assert decision["bounded_repair_canary"] is False
|
||||
assert decision["reason"] == "playbook_trust_below_archive_threshold"
|
||||
|
||||
|
||||
def test_legacy_failed_record_without_revision_gets_migration_canary() -> None:
|
||||
candidate = _candidate(
|
||||
"ansible:110-devops",
|
||||
catalog_revision="host110-route-v3",
|
||||
)
|
||||
guarded, gate = _apply_ansible_playbook_trust_gate(
|
||||
{"executor_candidates": [candidate]},
|
||||
[
|
||||
{
|
||||
"playbook_id": "PB-ANSIBLE-110-DEVOPS",
|
||||
"status": "approved",
|
||||
"trust_score": 0.02,
|
||||
"success_count": 0,
|
||||
"failure_count": 3,
|
||||
"review_required": False,
|
||||
"symptom_pattern": {},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert guarded["executor_candidates"] == [candidate]
|
||||
decision = gate["decisions"][0]
|
||||
assert decision["bounded_repair_canary"] is True
|
||||
assert decision["observed_catalog_revision"] is None
|
||||
|
||||
|
||||
def test_candidate_cannot_forge_revision_to_bypass_same_revision_circuit() -> None:
|
||||
catalog_id = "ansible:110-devops"
|
||||
current_revision = str(
|
||||
(get_ansible_catalog_item(catalog_id) or {}).get("catalog_revision")
|
||||
or ""
|
||||
)
|
||||
candidate = _candidate(catalog_id)
|
||||
candidate["catalog_revision"] = "forged-revision-v999"
|
||||
|
||||
guarded, gate = _apply_ansible_playbook_trust_gate(
|
||||
{"executor_candidates": [candidate]},
|
||||
[
|
||||
{
|
||||
"playbook_id": "PB-ANSIBLE-110-DEVOPS",
|
||||
"status": "approved",
|
||||
"trust_score": 0.02,
|
||||
"success_count": 0,
|
||||
"failure_count": 3,
|
||||
"review_required": False,
|
||||
"symptom_pattern": {
|
||||
"catalog_revision": current_revision,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert guarded["executor_candidates"] == []
|
||||
decision = gate["decisions"][0]
|
||||
assert decision["bounded_repair_canary"] is False
|
||||
assert decision["candidate_catalog_revision_mismatch"] is True
|
||||
assert decision["current_catalog_revision"] == current_revision
|
||||
|
||||
|
||||
def test_owner_review_flag_does_not_become_low_medium_high_terminal_gate() -> None:
|
||||
candidate = _candidate("ansible:188-momo-backup-user")
|
||||
guarded, gate = _apply_ansible_playbook_trust_gate(
|
||||
|
||||
@@ -77,6 +77,7 @@ from src.services.awooop_ansible_check_mode_service import (
|
||||
from src.services.awooop_ansible_learning_writeback import (
|
||||
_ensure_rag_chunk_fallback,
|
||||
_playbook_trust_readback,
|
||||
_record_catalog_revision_observation,
|
||||
canonical_ansible_playbook_id,
|
||||
ensure_ansible_rag_writeback,
|
||||
record_ansible_playbook_trust_writeback,
|
||||
@@ -2743,6 +2744,7 @@ def test_no_write_playbook_observation_does_not_claim_trust_mutation() -> None:
|
||||
success_count=7,
|
||||
failure_count=2,
|
||||
trust_score=0.81,
|
||||
symptom_pattern={"catalog_revision": "backup-user-v2"},
|
||||
updated_at=datetime(2026, 7, 14, tzinfo=UTC),
|
||||
)
|
||||
identity = {
|
||||
@@ -2772,6 +2774,24 @@ def test_no_write_playbook_observation_does_not_claim_trust_mutation() -> None:
|
||||
assert receipt["success_count"] == 7
|
||||
assert receipt["failure_count"] == 2
|
||||
assert receipt["trust_score"] == 0.81
|
||||
assert receipt["catalog_revision"] == "backup-user-v2"
|
||||
|
||||
|
||||
def test_playbook_trust_mutation_records_observed_catalog_revision() -> None:
|
||||
playbook = SimpleNamespace(
|
||||
symptom_pattern={"alert_names": ["Host110"]},
|
||||
)
|
||||
|
||||
revision = _record_catalog_revision_observation(
|
||||
playbook,
|
||||
{"catalog_revision": "host110-route-v3"},
|
||||
)
|
||||
|
||||
assert revision == "host110-route-v3"
|
||||
assert playbook.symptom_pattern == {
|
||||
"alert_names": ["Host110"],
|
||||
"catalog_revision": "host110-route-v3",
|
||||
}
|
||||
|
||||
|
||||
def test_ansible_catalog_ids_map_to_stable_playbook_primary_keys() -> None:
|
||||
|
||||
Reference in New Issue
Block a user