Files
aios/tests/test_control_plane_convergence_contract.py
T

270 lines
12 KiB
Python

from __future__ import annotations
from datetime import datetime, timezone
import tempfile
from concurrent.futures import ThreadPoolExecutor
import ast
from pathlib import Path
import pytest
from aios_continuation_lease import (
CAMPAIGN_HEAD,
LINEAGE_ANCHOR,
ContinuationError,
ContinuationLeaseManager,
WriterLeaseRequestV2,
)
from aios_gitops_job_controller import JobState, JobStore, GitOpsJobController, JobError, PayloadStore
from aios_gitea_machine_broker import MachineBrokerError, MachineCredentialProjection, reconcile_exact_branch, reconcile_fast_forward
from aios_gitops_writer_worker import runtime_policy
from aios_control_plane_runtime_status import RuntimeStatus
from aios_control_plane_sentinel import SentinelInputs, evaluate
from authorized_task_writer_provisioning import validate_writer_lease_request_v2
class Probe:
def __init__(self):
self.head = CAMPAIGN_HEAD
def branch_exists(self, branch):
return branch == "agent/autonomous-release-campaign-v1-20260807"
def branch_head(self, branch):
return self.head
def is_ancestor(self, older, newer):
return (older == LINEAGE_ANCHOR and newer == CAMPAIGN_HEAD) or (
older != newer and older in {CAMPAIGN_HEAD, "c" * 40} and newer in {"c" * 40, "d" * 40}
)
class CAS:
def __init__(self):
self.calls = []
self.stale = False
def compare_and_swap_branch(self, branch, expected, new):
if self.stale:
raise RuntimeError("stale")
self.calls.append((branch, expected, new))
return new
def request(generation=1):
return WriterLeaseRequestV2(
task_id="AIOS_AUTONOMOUS_RELEASE_CAMPAIGN_V1",
repository="aios/aios",
branch_mode="CONTINUE_EXISTING",
base_oid=LINEAGE_ANCHOR,
expected_branch_oid=CAMPAIGN_HEAD,
target_branch="agent/autonomous-release-campaign-v1-20260807",
generation=generation,
)
def test_continue_existing_accepts_exact_head_and_lineage():
manager = ContinuationLeaseManager(
Probe(), CAS(), now=lambda tz: datetime(2026, 8, 8, tzinfo=timezone.utc)
)
lease = manager.open(request())
assert lease.current_head_oid == CAMPAIGN_HEAD
assert lease.multi_commit and lease.append_only and lease.recoverable
def test_continue_existing_rejects_branch_head_conflict():
probe = Probe()
probe.head = "c" * 40
with pytest.raises(ContinuationError, match="EXISTING_BRANCH_OID_CONFLICT"):
request().validate(probe)
def test_provisioning_contract_v2_never_creates_existing_branch():
raw = {
"schema": "aios.task-writer-lease-request.v2",
"task_id": request().task_id,
"repository": request().repository,
"branch_mode": request().branch_mode,
"base_oid": request().base_oid,
"expected_branch_oid": request().expected_branch_oid,
"target_branch": request().target_branch,
"generation": 1,
"lease_seconds": 900,
}
result = validate_writer_lease_request_v2(raw, Probe())
assert result["create_target_branch"] is False
assert result["cas_ref_update_required"] is True
def test_append_uses_compare_and_swap_and_allows_multiple_commits():
cas = CAS()
manager = ContinuationLeaseManager(Probe(), cas)
lease = manager.open(request())
next_lease = manager.append_commit(lease, "c" * 40)
manager.append_commit(next_lease, "d" * 40)
assert cas.calls == [
(request().target_branch, CAMPAIGN_HEAD, "c" * 40),
(request().target_branch, "c" * 40, "d" * 40),
]
def test_stale_cas_fails_closed():
cas = CAS()
cas.stale = True
manager = ContinuationLeaseManager(Probe(), cas)
with pytest.raises(ContinuationError, match="BRANCH_LEASE_STALE"):
manager.append_commit(manager.open(request()), "c" * 40)
def test_job_idempotency_and_recovery():
with tempfile.TemporaryDirectory() as temp:
controller = GitOpsJobController(JobStore(temp))
req = {
"campaign_id": "campaign",
"repository": "aios/aios",
"operation": "commit-file",
"branch": request().target_branch,
"expected_branch_oid": CAMPAIGN_HEAD,
"payload_digest": "b" * 64,
"payload": {"files": ["active-runtime/app/x.py"]},
}
first = controller.submit(req)
duplicate = controller.submit(req)
assert first.job_id == duplicate.job_id
assert duplicate.side_effect_state == "SAME_JOB"
failed = controller.advance(first.job_id, JobState.RECOVERABLE_FAILURE, side_effect_state="UNKNOWN")
assert failed.side_effect_state == "RESUME_SAME_JOB"
resumed = controller.resume(first.job_id)
assert resumed.job_id == first.job_id and resumed.state == JobState.REQUESTED
tables = {"jobs", "idempotency_keys", "branch_leases", "state_transitions", "receipts"}
with JobStore(temp)._connect() as connection:
assert tables.issubset({row["name"] for row in connection.execute("SELECT name FROM sqlite_master WHERE type='table'")})
assert connection.execute("PRAGMA journal_mode").fetchone()[0].lower() == "wal"
def test_job_state_cannot_regress():
with tempfile.TemporaryDirectory() as temp:
controller = GitOpsJobController(JobStore(temp))
req = {
"campaign_id": "campaign",
"repository": "aios/aios",
"operation": "commit-file",
"branch": request().target_branch,
"expected_branch_oid": CAMPAIGN_HEAD,
"payload_digest": "b" * 64,
}
job = controller.submit(req)
controller.advance(job.job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED")
with pytest.raises(JobError):
controller.advance(job.job_id, JobState.REQUESTED, side_effect_state="BAD")
def test_machine_broker_rejects_owner_identity_and_plaintext_reference():
with pytest.raises(MachineBrokerError):
MachineCredentialProjection("owner-token-value", "aiman")
projection = MachineCredentialProjection.from_mapping({"scheme": "systemd-credential", "name": "gitea-machine-token", "account": "aios-machine"})
assert projection.reference == "systemd-credential:gitea-machine-token"
def test_ref_reconciler_is_fail_closed():
assert reconcile_exact_branch("a" * 40, None, "a" * 40) == "CREATE_REQUIRED"
assert reconcile_exact_branch("a" * 40, "b" * 40, "a" * 40) == "MANUAL_OWNER_REVIEW_REQUIRED"
assert reconcile_fast_forward("a" * 40, "a" * 40, "b" * 40, old_is_ancestor_of_new=True) == "FAST_FORWARD_REQUIRED"
def test_payload_store_is_content_addressed_and_fail_closed():
with tempfile.TemporaryDirectory() as temp:
store = PayloadStore(temp)
payload = b"approved-payload"
digest = __import__("hashlib").sha256(payload).hexdigest()
reference, size = store.put(payload, digest)
assert store.read_verified(reference, digest, size) == payload
with pytest.raises(JobError, match="PAYLOAD_DIGEST_MISMATCH"):
store.put(b"changed", digest)
def test_concurrent_duplicate_submit_collapses_to_one_job():
with tempfile.TemporaryDirectory() as temp:
controller = GitOpsJobController(JobStore(temp))
req = {"campaign_id": "campaign", "repository": "aios/aios", "operation": "commit-file", "branch": request().target_branch, "expected_branch_oid": CAMPAIGN_HEAD, "payload_digest": "b" * 64}
with ThreadPoolExecutor(max_workers=4) as pool:
receipts = list(pool.map(lambda _: controller.submit(req), range(8)))
assert len({item.job_id for item in receipts}) == 1
def test_gate_split_keeps_legacy_mutation_false():
assert runtime_policy({"AIOS_GITOPS_WRITES_ENABLED": "false", "AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED": "true"}) == {
"legacy_global_write": False,
"durable_job_execution": True,
"repository_mutation_allowed": True,
}
def test_revision_cas_and_receipt_are_transactional():
with tempfile.TemporaryDirectory() as temp:
controller = GitOpsJobController(JobStore(temp))
req = {"campaign_id": "campaign", "repository": "aios/aios", "operation": "commit-file", "branch": request().target_branch, "expected_branch_oid": CAMPAIGN_HEAD, "payload_digest": "b" * 64}
job = controller.submit(req)
authorized = controller.advance(job.job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED", expected_revision=0)
with pytest.raises(JobError, match="REVISION_CAS_CONFLICT"):
controller.advance(job.job_id, JobState.LEASED, side_effect_state="LEASED", expected_revision=0)
completed = controller.advance(job.job_id, JobState.COMPLETED, side_effect_state="REMOTE_VERIFIED", expected_revision=authorized.revision, receipt={"remote_verified": True})
assert completed.http_status == 200
assert controller.submit(req).http_status == 200
with JobStore(temp)._connect() as connection:
assert connection.execute("SELECT COUNT(*) FROM receipts").fetchone()[0] == 1
assert connection.execute("SELECT COUNT(*) FROM state_transitions").fetchone()[0] == 3
def test_job_lease_mutation_is_transactional_and_revision_bound():
with tempfile.TemporaryDirectory() as temp:
controller = GitOpsJobController(
JobStore(temp),
clock=lambda: datetime(2026, 8, 7, 23, 59, tzinfo=timezone.utc),
)
req = {"campaign_id": "campaign", "repository": "aios/aios", "operation": "commit-file", "branch": request().target_branch, "expected_branch_oid": CAMPAIGN_HEAD, "payload_digest": "b" * 64}
job = controller.submit(req)
authorized = controller.advance(job.job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED")
leased = controller.lease_job(job.job_id, lease_id="lease-1", lineage_anchor_oid=LINEAGE_ANCHOR, current_head_oid=CAMPAIGN_HEAD, generation=1, lease_expires_at="2026-08-08T00:00:00Z", expected_revision=authorized.revision)
assert leased.state == JobState.LEASED
controller.update_lease("lease-1", expected_revision=0, current_head_oid="c" * 40, generation=1)
with pytest.raises(JobError, match="LEASE_REVISION_CAS_CONFLICT"):
controller.update_lease("lease-1", expected_revision=0, current_head_oid="d" * 40, generation=1)
def test_reconcile_failure_is_durable_and_not_directly_advanced():
with tempfile.TemporaryDirectory() as temp:
controller = GitOpsJobController(JobStore(temp))
req = {"campaign_id": "campaign", "repository": "aios/aios", "operation": "commit-file", "branch": request().target_branch, "expected_branch_oid": CAMPAIGN_HEAD, "payload_digest": "b" * 64}
job = controller.submit(req)
controller.advance(job.job_id, JobState.RECOVERABLE_FAILURE, side_effect_state="UNKNOWN")
with pytest.raises(JobError, match="RECONCILIATION_REQUIRED"):
controller.reconcile(job.job_id, lambda _row: (_ for _ in ()).throw(RuntimeError("probe failed")))
assert JobStore(temp).read(job.job_id)["state"] == JobState.RECONCILIATION_REQUIRED.value
def test_worker_gate_closed_and_safe_dry_execution():
assert runtime_policy({"AIOS_GITOPS_WRITES_ENABLED": "false", "AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED": "false"})["durable_job_execution"] is False
def test_runtime_planes_do_not_conflate_readiness():
status = RuntimeStatus({"model_api": True}, {"job_controller": True, "gitea_machine_credential": False}, {"artifact_store": True})
assert status.release_control_plane_ready is False
assert status.as_dict()["WRITE_PLANE"]["gitea_machine_credential"] is False
def test_sentinel_is_workspace_independent_and_fail_closed():
inputs = SentinelInputs(tempfile.gettempdir() + "/missing-authority.git", "gitea", "https://gitea/api", "refs/heads/campaign", "a" * 40)
result = evaluate(inputs, network_reader=lambda *_: "a" * 40, gitea_reader=lambda *_: "a" * 40)
assert result.equal is False and result.error == "AUTHORITY_REF_UNAVAILABLE"
def test_api_file_method_has_no_inline_git_mutation_calls():
source = Path(__file__).parents[1] / "active-runtime" / "app" / "aios_chatgpt_actions_api.py"
if not source.exists():
pytest.skip("candidate source is remote-only in this local test runner")
tree = ast.parse(source.read_text(encoding="utf-8"))
method = next(node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef) and node.name == "file")
calls = {node.func.id for node in ast.walk(method) if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)}
assert not calls.intersection({"checkout", "write_file", "commit", "sh"})