fix(agent): harden apply closure and telegram ownership
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m36s
CD Pipeline / build-and-deploy (push) Successful in 7m2s
CD Pipeline / post-deploy-checks (push) Successful in 1m53s
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m36s
CD Pipeline / build-and-deploy (push) Successful in 7m2s
CD Pipeline / post-deploy-checks (push) Successful in 1m53s
This commit is contained in:
110
apps/api/tests/test_awooop_ansible_trust_claim_gate.py
Normal file
110
apps/api/tests/test_awooop_ansible_trust_claim_gate.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
||||
from src.services.awooop_ansible_check_mode_service import (
|
||||
_apply_ansible_playbook_trust_gate,
|
||||
_revalidate_claim_playbook_trust,
|
||||
run_controlled_apply_for_claim,
|
||||
)
|
||||
|
||||
|
||||
def _candidate(catalog_id: str) -> dict[str, object]:
|
||||
return {
|
||||
"catalog_id": catalog_id,
|
||||
"playbook_path": f"infra/ansible/playbooks/{catalog_id.split(':', 1)[1]}.yml",
|
||||
"inventory_hosts": ["host_188"],
|
||||
"risk_level": "medium",
|
||||
}
|
||||
|
||||
|
||||
def test_low_trust_failed_playbook_opens_claim_circuit() -> None:
|
||||
guarded, gate = _apply_ansible_playbook_trust_gate(
|
||||
{"executor_candidates": [_candidate("ansible:nginx-sync")]},
|
||||
[
|
||||
{
|
||||
"playbook_id": "PB-ANSIBLE-NGINX-SYNC",
|
||||
"status": "approved",
|
||||
"trust_score": 0.001417,
|
||||
"success_count": 0,
|
||||
"failure_count": 24,
|
||||
"review_required": False,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert guarded["executor_candidates"] == []
|
||||
assert gate["status"] == "circuit_open"
|
||||
assert gate["blocked_candidate_count"] == 1
|
||||
assert gate["decisions"][0]["reason"] == (
|
||||
"playbook_trust_below_archive_threshold"
|
||||
)
|
||||
assert gate["next_controlled_action"] == (
|
||||
"generate_or_verify_playbook_repair_candidate"
|
||||
)
|
||||
|
||||
|
||||
def test_low_trust_candidate_is_filtered_without_blocking_healthy_candidate() -> None:
|
||||
healthy = _candidate("ansible:188-momo-backup-user")
|
||||
guarded, gate = _apply_ansible_playbook_trust_gate(
|
||||
{
|
||||
"executor_candidates": [
|
||||
_candidate("ansible:nginx-sync"),
|
||||
healthy,
|
||||
]
|
||||
},
|
||||
[
|
||||
{
|
||||
"playbook_id": "PB-ANSIBLE-NGINX-SYNC",
|
||||
"status": "approved",
|
||||
"trust_score": 0.01,
|
||||
"success_count": 0,
|
||||
"failure_count": 15,
|
||||
"review_required": True,
|
||||
},
|
||||
{
|
||||
"playbook_id": "PB-ANSIBLE-188-MOMO-BACKUP-USER",
|
||||
"status": "approved",
|
||||
"trust_score": 0.65,
|
||||
"success_count": 7,
|
||||
"failure_count": 1,
|
||||
"review_required": False,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
assert guarded["executor_candidates"] == [healthy]
|
||||
assert gate["status"] == "degraded_candidates_filtered"
|
||||
assert gate["eligible_candidate_count"] == 1
|
||||
assert gate["blocked_candidate_count"] == 1
|
||||
|
||||
|
||||
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(
|
||||
{"executor_candidates": [candidate]},
|
||||
[
|
||||
{
|
||||
"playbook_id": "PB-ANSIBLE-188-MOMO-BACKUP-USER",
|
||||
"status": "approved",
|
||||
"trust_score": 0.4,
|
||||
"success_count": 2,
|
||||
"failure_count": 1,
|
||||
"review_required": True,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert guarded["executor_candidates"] == [candidate]
|
||||
assert gate["status"] == "closed"
|
||||
assert gate["owner_review_used_as_terminal"] is False
|
||||
|
||||
|
||||
def test_controlled_apply_revalidates_trust_before_runtime_write() -> None:
|
||||
apply_source = inspect.getsource(run_controlled_apply_for_claim)
|
||||
revalidate_source = inspect.getsource(_revalidate_claim_playbook_trust)
|
||||
|
||||
assert "_revalidate_claim_playbook_trust" in apply_source
|
||||
assert "ansible_controlled_apply_blocked_by_playbook_trust" in apply_source
|
||||
assert "playbook_trust_circuit_open" in revalidate_source
|
||||
assert "UPDATE automation_operation_log" in revalidate_source
|
||||
@@ -21,6 +21,7 @@ from src.services.awooop_ansible_check_mode_service import (
|
||||
_EXECUTION_CAPABILITY_FALLBACK_OPERATION_TYPE,
|
||||
AnsibleCheckModeClaim,
|
||||
AnsibleRunResult,
|
||||
_append_runtime_stage_receipts_to_apply,
|
||||
_automation_operation_log_incident_id,
|
||||
_build_auto_repair_execution_receipt,
|
||||
_claim_from_apply_operation_row,
|
||||
@@ -2310,9 +2311,12 @@ def test_failed_apply_retry_replay_is_no_write_and_idempotent() -> None:
|
||||
assert "run_controlled_apply_for_claim" not in source
|
||||
|
||||
receipt_source = inspect.getsource(_record_retry_runtime_stage_receipt)
|
||||
append_source = inspect.getsource(_append_runtime_stage_receipts_to_apply)
|
||||
assert "_runtime_stage_receipt" in receipt_source
|
||||
assert 'stage_id="retry_or_rollback"' in receipt_source
|
||||
assert "jsonb_array_elements" in receipt_source
|
||||
assert "jsonb_array_elements" in append_source
|
||||
assert "ansible_controlled_retry_terminal_v2" in receipt_source
|
||||
assert "ansible_retry_terminal_receipt_not_written_unverified" in receipt_source
|
||||
assert "runtime_apply_executed" in receipt_source
|
||||
|
||||
|
||||
|
||||
58
apps/api/tests/test_telegram_polling_owner_contract.py
Normal file
58
apps/api/tests/test_telegram_polling_owner_contract.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
|
||||
|
||||
def _load_deployment(path: str) -> dict:
|
||||
deployment_documents = list(
|
||||
yaml.safe_load_all(
|
||||
(ROOT / path).read_text(encoding="utf-8")
|
||||
)
|
||||
)
|
||||
return next(
|
||||
document
|
||||
for document in deployment_documents
|
||||
if document.get("kind") == "Deployment"
|
||||
and document.get("metadata", {}).get("name") == "awoooi-api"
|
||||
)
|
||||
|
||||
|
||||
def _api_environment(deployment: dict) -> dict[str, str | None]:
|
||||
api_container = next(
|
||||
container
|
||||
for container in deployment["spec"]["template"]["spec"]["containers"]
|
||||
if container["name"] == "api"
|
||||
)
|
||||
return {item["name"]: item.get("value") for item in api_container["env"]}
|
||||
|
||||
|
||||
def test_production_api_does_not_compete_with_openclaw_polling_owner() -> None:
|
||||
config = yaml.safe_load(
|
||||
(ROOT / "k8s/awoooi-prod/04-configmap.yaml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
deployment = _load_deployment("k8s/awoooi-prod/06-deployment-api.yaml")
|
||||
main = (ROOT / "apps/api/src/main.py").read_text(encoding="utf-8")
|
||||
api_env = _api_environment(deployment)
|
||||
|
||||
assert config["data"]["TELEGRAM_ENABLE_POLLING"] == "false"
|
||||
assert api_env["TELEGRAM_ENABLE_POLLING"] == "false"
|
||||
assert "if settings.TELEGRAM_ENABLE_POLLING" in main
|
||||
assert 'reason="OpenClaw' in main
|
||||
|
||||
|
||||
def test_development_api_cannot_emit_legacy_telegram_heartbeats() -> None:
|
||||
config = yaml.safe_load(
|
||||
(ROOT / "k8s/awoooi-dev/02-configmap.yaml").read_text(encoding="utf-8")
|
||||
)
|
||||
deployment = _load_deployment("k8s/awoooi-dev/04-deployment-api.yaml")
|
||||
api_env = _api_environment(deployment)
|
||||
|
||||
assert config["data"]["TELEGRAM_ENABLE_POLLING"] == "false"
|
||||
assert api_env["TELEGRAM_ENABLE_POLLING"] == "false"
|
||||
# An explicit env value overrides the same key inherited from envFrom.
|
||||
# This also stops old images whose heartbeat gate only checks this token.
|
||||
assert api_env["OPENCLAW_TG_BOT_TOKEN"] == ""
|
||||
Reference in New Issue
Block a user