266 lines
12 KiB
Python
266 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))
|
|
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)
|
|
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=0)
|
|
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"})
|