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 2m24s
CD Pipeline / build-and-deploy (push) Successful in 7m55s
CD Pipeline / post-deploy-checks (push) Successful in 1m45s
182 lines
5.7 KiB
Python
182 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import inspect
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from src.services.awooop_ansible_audit_service import list_ansible_catalog
|
|
from src.services.awooop_ansible_check_mode_service import (
|
|
AnsibleRunResult,
|
|
_post_apply_verification_result,
|
|
_record_apply_post_verifier_terminal,
|
|
run_pending_check_modes_once,
|
|
)
|
|
from src.services.awooop_ansible_post_verifier import (
|
|
AssetPostcondition,
|
|
ReadOnlyProbeResult,
|
|
build_read_only_probe_command,
|
|
registered_catalog_ids,
|
|
run_ansible_asset_post_verifier,
|
|
)
|
|
|
|
|
|
def _verifier_root(tmp_path: Path) -> tuple[Path, Path, Path]:
|
|
root = tmp_path / "infra" / "ansible"
|
|
inventory = root / "inventory" / "hosts.yml"
|
|
inventory.parent.mkdir(parents=True)
|
|
inventory.write_text(
|
|
"""
|
|
all:
|
|
children:
|
|
ai_web:
|
|
hosts:
|
|
host_188:
|
|
ansible_host: 192.168.0.188
|
|
ansible_user: ollama
|
|
""".strip()
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
key = tmp_path / "id_ed25519"
|
|
known_hosts = tmp_path / "known_hosts"
|
|
key.write_text("test-key-metadata-only\n", encoding="utf-8")
|
|
known_hosts.write_text("host-key-metadata-only\n", encoding="utf-8")
|
|
return root, key, known_hosts
|
|
|
|
|
|
def test_asset_postcondition_registry_covers_every_ansible_catalog() -> None:
|
|
catalog_ids = {row["catalog_id"] for row in list_ansible_catalog()}
|
|
|
|
assert registered_catalog_ids() == catalog_ids
|
|
|
|
|
|
def test_read_only_probe_command_uses_structured_inventory(tmp_path: Path) -> None:
|
|
root, key, known_hosts = _verifier_root(tmp_path)
|
|
condition = AssetPostcondition(
|
|
"momo_script",
|
|
"service",
|
|
"host_188",
|
|
"test -x /home/ollama/bin/momo-pg-backup.sh",
|
|
)
|
|
|
|
command = build_read_only_probe_command(
|
|
condition,
|
|
inventory_path=root / "inventory" / "hosts.yml",
|
|
ssh_key_path=key,
|
|
known_hosts_path=known_hosts,
|
|
)
|
|
|
|
assert command[0] == "ssh"
|
|
assert "ollama@192.168.0.188" in command
|
|
assert "ansible-playbook" not in command
|
|
assert command[-1] == condition.probe
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_independent_post_verifier_requires_every_asset_probe(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
root, key, known_hosts = _verifier_root(tmp_path)
|
|
monkeypatch.setenv("AWOOOI_BUILD_COMMIT_SHA", "source-sha-123")
|
|
calls = 0
|
|
|
|
async def probe_runner(
|
|
_command: list[str],
|
|
*,
|
|
timeout_seconds: int,
|
|
) -> ReadOnlyProbeResult:
|
|
nonlocal calls
|
|
assert timeout_seconds == 7
|
|
calls += 1
|
|
return ReadOnlyProbeResult(
|
|
returncode=0 if calls < 5 else 1,
|
|
stdout="runtime probe output must not be persisted",
|
|
stderr="",
|
|
duration_ms=2,
|
|
)
|
|
|
|
receipt = await run_ansible_asset_post_verifier(
|
|
catalog_id="ansible:188-momo-backup-user",
|
|
automation_run_id="run-1",
|
|
apply_op_id="apply-1",
|
|
inventory_hosts=("host_188",),
|
|
executor_returncode=0,
|
|
playbook_root=root,
|
|
ssh_key_path=key,
|
|
known_hosts_path=known_hosts,
|
|
timeout_seconds=7,
|
|
probe_runner=probe_runner,
|
|
)
|
|
|
|
assert calls == 5
|
|
assert receipt["verification_result"] == "failed"
|
|
assert receipt["all_postconditions_passed"] is False
|
|
assert receipt["passed_postcondition_count"] == 4
|
|
assert receipt["required_postcondition_count"] == 5
|
|
assert receipt["executor_returncode_trusted"] is False
|
|
assert receipt["raw_output_stored"] is False
|
|
assert "postcondition_failed:host_188_momo_backup_metric" in receipt[
|
|
"active_blockers"
|
|
]
|
|
assert all("stdout" not in item and "stderr" not in item for item in receipt["postconditions"])
|
|
assert all(len(item["output_sha256"]) == 64 for item in receipt["postconditions"])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_executor_failure_never_runs_or_passes_independent_verifier(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
root, key, known_hosts = _verifier_root(tmp_path)
|
|
called = False
|
|
|
|
async def probe_runner(*_args, **_kwargs) -> ReadOnlyProbeResult:
|
|
nonlocal called
|
|
called = True
|
|
return ReadOnlyProbeResult(0, "", "", 1)
|
|
|
|
receipt = await run_ansible_asset_post_verifier(
|
|
catalog_id="ansible:188-momo-backup-user",
|
|
automation_run_id="run-1",
|
|
apply_op_id="apply-1",
|
|
inventory_hosts=("host_188",),
|
|
executor_returncode=2,
|
|
playbook_root=root,
|
|
ssh_key_path=key,
|
|
known_hosts_path=known_hosts,
|
|
probe_runner=probe_runner,
|
|
)
|
|
|
|
assert called is False
|
|
assert receipt["verification_result"] == "failed"
|
|
assert "executor_apply_failed_before_post_verifier" in receipt[
|
|
"active_blockers"
|
|
]
|
|
|
|
|
|
def test_executor_returncode_alone_cannot_claim_post_verifier_success() -> None:
|
|
result = AnsibleRunResult(returncode=0, stdout="", stderr="", duration_ms=1)
|
|
verified = {
|
|
"verification_result": "success",
|
|
"all_postconditions_passed": True,
|
|
"executor_returncode_trusted": False,
|
|
}
|
|
|
|
assert _post_apply_verification_result(result) == "failed"
|
|
assert _post_apply_verification_result(result, verified) == "success"
|
|
assert _post_apply_verification_result(
|
|
result,
|
|
{**verified, "all_postconditions_passed": False},
|
|
) == "failed"
|
|
|
|
|
|
def test_verifier_failure_enters_durable_failed_apply_and_retry_first() -> None:
|
|
terminal_source = inspect.getsource(_record_apply_post_verifier_terminal)
|
|
broker_source = inspect.getsource(run_pending_check_modes_once)
|
|
|
|
assert "ELSE 'failed'" in terminal_source
|
|
assert "independent_post_verifier_failed" in terminal_source
|
|
assert "run_failed_apply_check_mode_replay_once" in broker_source
|
|
assert "controlled_apply_verifier_failed" in broker_source
|