Files
aios/tests/test_live_executor_contract.py
T

165 lines
6.4 KiB
Python

from __future__ import annotations
import tempfile
from pathlib import Path
import pytest
import aios_gitops_job_executor as executor_module
from aios_authority_git_executor import AuthorityGitError, AuthorityGitExecutor
from aios_gitea_machine_transport import GiteaMachineTransport
from aios_gitea_machine_broker import CampaignCapabilityReceipt, GiteaMachineBroker, MachineCredentialProjection
from aios_gitops_job_controller import GitOpsJobController, JobError, JobStore, JobState
from aios_gitops_writer_worker import handle_request
OLD = "bba675fab3b7e0c5fba7066720252b3c91b9f20d"
NEW = "3fbbc18fb2131bbbd9e511004a59ee95cf3e86b7"
class FakeAuthority:
def __init__(self, descendant=True):
self.head = OLD
self.descendant = descendant
self.cas_calls = []
def read_ref(self, ref):
return self.head
def ancestry_check(self, older, newer):
return self.descendant and older == OLD and newer == NEW
def advance_task_branch_CAS(self, branch, expected, new):
if self.head != expected:
raise AuthorityGitError("AUTHORITY_BRANCH_EXPECTATION_MISMATCH")
self.cas_calls.append((branch, expected, new))
self.head = new
return new
class FakeBroker:
def __init__(self):
self.remote = None
self.push_calls = []
def verify_identity(self):
return {"login": "aios-autonomy-agent", "repository": "aios/aios"}
def readback(self):
return self.remote
def push_branch(self, new):
self.push_calls.append(new)
self.remote = new
return new
def create_pr(self, *args):
return {"number": 1}
def status(self, *args):
return {"status": "success"}
def merge(self, *args):
return {"merged": True}
def _request(**extra):
value = {
"campaign_id": executor_module.CAMPAIGN_ID,
"repository": executor_module.REPOSITORY,
"operation": "ADVANCE_EXISTING_CAMPAIGN_BRANCH",
"branch": executor_module.BRANCH,
"expected_branch_oid": OLD,
"payload_digest": "0" * 64,
"task_id": executor_module.TASK_ID,
"new_branch_oid": NEW,
"authorization_recovery_digest": executor_module.AUTHORIZATION_DIGEST,
}
value.update(extra)
return value
def _executor(temp, authority=None, broker=None):
controller = GitOpsJobController(JobStore(temp))
job = controller.submit(_request())
executor = executor_module.DurableJobExecutor(controller, authority or FakeAuthority(), broker or FakeBroker(), Path(temp) / "recovery.json")
return controller, job, executor
def _recovery():
return {
"digest": executor_module.AUTHORIZATION_DIGEST,
"campaign_id": executor_module.CAMPAIGN_ID,
"task_id": executor_module.TASK_ID,
"repository": executor_module.REPOSITORY,
"branch": executor_module.BRANCH,
"expected_branch_oid": OLD,
}
def test_advance_existing_branch_success_and_duplicate_execute(monkeypatch):
monkeypatch.setattr(executor_module, "load_recovered_authorization", lambda _path: _recovery())
with tempfile.TemporaryDirectory() as temp:
controller, job, executor = _executor(temp)
first = executor.execute(job.job_id)
second = executor.execute(job.job_id)
assert first["state"] == JobState.COMPLETED.value
assert second["state"] == JobState.COMPLETED.value
assert executor.authority.cas_calls == [(f"refs/heads/{executor_module.BRANCH}", OLD, NEW)]
assert executor.broker.push_calls == [NEW]
def test_authorization_digest_required(monkeypatch):
monkeypatch.setattr(executor_module, "load_recovered_authorization", lambda _path: _recovery())
with tempfile.TemporaryDirectory() as temp:
controller = GitOpsJobController(JobStore(temp))
job = controller.submit(_request(authorization_recovery_digest="bad"))
executor = executor_module.DurableJobExecutor(controller, FakeAuthority(), FakeBroker(), Path(temp) / "recovery.json")
with pytest.raises(executor_module.JobExecutionError, match="AUTHORIZATION_RECOVERY_DIGEST_MISMATCH"):
executor.dry_run(job.job_id)
def test_wrong_branch_rejected(monkeypatch):
monkeypatch.setattr(executor_module, "load_recovered_authorization", lambda _path: _recovery())
with tempfile.TemporaryDirectory() as temp:
controller = GitOpsJobController(JobStore(temp))
job = controller.submit(_request(branch="agent/not-the-campaign"))
executor = executor_module.DurableJobExecutor(controller, FakeAuthority(), FakeBroker(), Path(temp) / "recovery.json")
with pytest.raises(executor_module.JobExecutionError, match="BRANCH_MISMATCH"):
executor.dry_run(job.job_id)
def test_non_descendant_new_oid_rejected(monkeypatch):
monkeypatch.setattr(executor_module, "load_recovered_authorization", lambda _path: _recovery())
with tempfile.TemporaryDirectory() as temp:
authority = FakeAuthority(descendant=False)
controller, job, executor = _executor(temp, authority=authority)
with pytest.raises(executor_module.JobExecutionError, match="NEW_OID_NOT_DESCENDANT"):
executor.dry_run(job.job_id)
def test_worker_execute_action_consumes_job():
with tempfile.TemporaryDirectory() as temp:
controller = GitOpsJobController(JobStore(temp))
job = controller.submit(_request())
class Stub:
def execute(self, job_id, *, dry_run=False):
assert job_id == job.job_id
assert dry_run is True
return {"decision": "SAFE_TO_ADVANCE"}
result = handle_request(controller, {"action": "execute", "job_id": job.job_id, "dry_run": True}, Stub())
assert result["job"]["decision"] == "SAFE_TO_ADVANCE"
def test_machine_transport_branch_cas_is_not_force_push(monkeypatch):
transport = GiteaMachineTransport("http://127.0.0.1:3000/api/v1", "aios/aios", "https://g.cnnetmkt.top/git/aios/aios.git", Path("/tmp/repo.git"))
calls = []
def read_branch(_self, _branch):
return NEW if calls else OLD
monkeypatch.setattr(GiteaMachineTransport, "read_branch", read_branch)
monkeypatch.setattr(GiteaMachineTransport, "_push_branch_fast_forward", lambda _self, branch, new: calls.append((branch, new)))
assert transport.compare_and_swap_branch(executor_module.BRANCH, OLD, NEW) == NEW
assert calls == [(executor_module.BRANCH, NEW)]