165 lines
6.4 KiB
Python
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)]
|