158 lines
5.9 KiB
Python
158 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
APP=ROOT/"active-runtime/app"
|
|
sys.path.insert(0,str(APP))
|
|
|
|
import aios_autonomous_control_plane as acp
|
|
import aios_capability_contract as contract
|
|
import aios_release_online_agent_worker as worker
|
|
|
|
|
|
def _ready_probes():
|
|
return {
|
|
"credential_projection": lambda: True,
|
|
"trusted_builder": lambda: True,
|
|
"authenticated_probe": lambda: True,
|
|
"release_api_health": lambda: True,
|
|
"worker_liveness": lambda: True,
|
|
}
|
|
|
|
|
|
def _task():
|
|
return {
|
|
"task_id":"r42-autonomous-release-admission-test",
|
|
"required_capabilities":["release:candidate"],
|
|
"constraints":{
|
|
"release_candidate":{
|
|
"candidate_id":"r42-release-candidate-v2r2c9-test",
|
|
"source_commit":"1"*40,
|
|
"version":"V2R2C9",
|
|
"projection_id":"projection-"+"a"*32,
|
|
}
|
|
},
|
|
}
|
|
|
|
|
|
def test_capability_contract_registry_is_closed_and_release_is_versioned():
|
|
status=contract.validate_capability_contract_registry()
|
|
assert status["valid"] is True
|
|
release=contract.capability_contract("release:candidate")
|
|
assert release is not None
|
|
assert release.min_version==1
|
|
assert release.max_version==1
|
|
assert release.permissions==("prepare_release",)
|
|
assert "credential_projection" in release.dependencies
|
|
assert "trusted_builder" in release.dependencies
|
|
assert "authenticated_probe" in release.health_requirements
|
|
assert "production_deploy" in release.forbidden
|
|
assert "runtime_switch" in release.forbidden
|
|
|
|
|
|
def test_unknown_agent_capability_cannot_register_or_match():
|
|
assert contract.registered_capability("release:future",worker=True) is False
|
|
assert contract.registered_capability("release:*",worker=True) is False
|
|
assert contract.has_capabilities(["release:future"],["release:future"]) is False
|
|
|
|
|
|
def test_runtime_admission_requires_all_release_execution_readiness():
|
|
decision=acp.runtime_admission(
|
|
worker_capabilities=["release:candidate"],
|
|
required_capabilities=["release:candidate"],
|
|
requested_operations=["prepare_release"],
|
|
probes=_ready_probes(),
|
|
)
|
|
assert decision.admitted is True
|
|
assert decision.state=="EXECUTION_READY"
|
|
|
|
|
|
def test_runtime_admission_blocks_credential_failure_before_execution():
|
|
probes=_ready_probes()
|
|
probes["credential_projection"]=lambda:False
|
|
probes["authenticated_probe"]=lambda:False
|
|
decision=acp.runtime_admission(
|
|
worker_capabilities=["release:candidate"],
|
|
required_capabilities=["release:candidate"],
|
|
requested_operations=["prepare_release"],
|
|
probes=probes,
|
|
)
|
|
assert decision.admitted is False
|
|
assert decision.state=="EXECUTION_READY_FAILED"
|
|
assert decision.failure_class==acp.FailureClass.CREDENTIAL_PROJECTION_ERROR
|
|
assert decision.repair_level==acp.RepairLevel.AUTO_REPAIR_TASK
|
|
|
|
|
|
def test_forbidden_release_operation_requires_owner_boundary():
|
|
decision=acp.runtime_admission(
|
|
worker_capabilities=["release:candidate"],
|
|
required_capabilities=["release:candidate"],
|
|
requested_operations=["production_deploy"],
|
|
probes=_ready_probes(),
|
|
)
|
|
assert decision.admitted is False
|
|
assert decision.failure_class==acp.FailureClass.POLICY_DENIED
|
|
assert decision.repair_level==acp.RepairLevel.OWNER_APPROVAL
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"error,phase,expected,level,consumes",
|
|
[
|
|
("Permission denied: gitops.tokens","release",acp.FailureClass.CREDENTIAL_PROJECTION_ERROR,acp.RepairLevel.AUTO_REPAIR_TASK,False),
|
|
("HTTP_TRANSPORT_UNAVAILABLE","network",acp.FailureClass.DEPENDENCY_UNAVAILABLE,acp.RepairLevel.AUTO_LOCAL,False),
|
|
("TASK_CAPABILITY_SCOPE_INVALID","validation",acp.FailureClass.CAPABILITY_CONTRACT_ERROR,acp.RepairLevel.AUTO_REPAIR_TASK,False),
|
|
("lease lost","heartbeat",acp.FailureClass.LEASE_LOST,acp.RepairLevel.AUTO_LOCAL,False),
|
|
("business permanent invalid release","release",acp.FailureClass.BUSINESS_PERMANENT,acp.RepairLevel.MANUAL,True),
|
|
],
|
|
)
|
|
def test_failure_classification_drives_repair_policy(error,phase,expected,level,consumes):
|
|
failure=acp.classify_failure(error,phase=phase)
|
|
assert failure==expected
|
|
repair=acp.repair_for_failure(failure)
|
|
assert repair.level==level
|
|
assert repair.consumes_business_attempt is consumes
|
|
|
|
|
|
def test_release_worker_does_not_claim_when_admission_is_not_ready(monkeypatch):
|
|
calls=[]
|
|
def fake_http(base,method,route,token,body=None,timeout=30):
|
|
calls.append((base,method,route,body))
|
|
if route=="/health":
|
|
return 200,{"status":"ok"}
|
|
if route=="/createReleaseCandidate" and body=={}:
|
|
return 500,{"error":"Permission denied: gitops.tokens"}
|
|
if route.endswith("/claim"):
|
|
raise AssertionError("claim must not happen before EXECUTION_READY")
|
|
raise AssertionError(route)
|
|
|
|
events=[]
|
|
monkeypatch.setattr(worker,"http_json",fake_http)
|
|
monkeypatch.setattr(worker,"audit",lambda event,**fields:events.append((event,fields)))
|
|
monkeypatch.setattr(worker,"write_liveness",lambda *args,**kwargs:None)
|
|
|
|
assert worker.execute_task(
|
|
_task(),
|
|
agent_token="a"*32,
|
|
release_token="r"*32,
|
|
) is False
|
|
assert not any(route.endswith("/claim") for _,_,route,_ in calls)
|
|
blocked=[item for item in events if item[0]=="TASK_ADMISSION_BLOCKED"]
|
|
assert len(blocked)==1
|
|
assert blocked[0][1]["admission"]["state"]=="EXECUTION_READY_FAILED"
|
|
|
|
|
|
def test_system_invariant_snapshot_is_degraded_when_dependency_is_missing():
|
|
probes=_ready_probes()
|
|
probes["trusted_builder"]=lambda:False
|
|
snapshot=acp.system_invariant_snapshot(
|
|
worker_capabilities=["release:candidate"],
|
|
required_capabilities=["release:candidate"],
|
|
probes=probes,
|
|
)
|
|
assert snapshot["status"]=="DEGRADED"
|
|
assert snapshot["execution_ready"] is False
|