227 lines
7.9 KiB
Python
227 lines
7.9 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
APP=ROOT/"active-runtime/app"
|
|
sys.path.insert(0,str(APP))
|
|
|
|
import aios_release_online_agent_worker as worker
|
|
|
|
|
|
def task(**overrides):
|
|
value={
|
|
"task_id":"r42-release-candidate-live-acceptance-20260819",
|
|
"required_capabilities":["release:candidate"],
|
|
"constraints":{
|
|
"release_candidate":{
|
|
"candidate_id":"r42-release-candidate-v2r2c2-20260819",
|
|
"source_commit":"5"*40,
|
|
"version":"V2R2C2",
|
|
"projection_id":"projection-"+"a"*32,
|
|
}
|
|
}
|
|
}
|
|
value.update(overrides)
|
|
return value
|
|
|
|
|
|
def test_release_request_accepts_only_exact_release_capability():
|
|
req=worker.ReleaseRequest.from_task(task())
|
|
assert req.candidate_id=="r42-release-candidate-v2r2c2-20260819"
|
|
assert req.source_commit=="5"*40
|
|
assert req.version=="V2R2C2"
|
|
assert req.projection_id=="projection-"+"a"*32
|
|
|
|
with pytest.raises(worker.ReleaseWorkerError,match="TASK_CAPABILITY_SCOPE_INVALID"):
|
|
worker.ReleaseRequest.from_task(task(required_capabilities=["release:candidate","git.read"]))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"payload",
|
|
[
|
|
{
|
|
"candidate_id":"r42-release-candidate-v2r2c2-20260819",
|
|
"source_commit":"short",
|
|
"version":"V2R2C2",
|
|
"projection_id":"projection-"+"a"*32,
|
|
},
|
|
{
|
|
"candidate_id":"bad",
|
|
"source_commit":"5"*40,
|
|
"version":"V2R2C2",
|
|
"projection_id":"projection-"+"a"*32,
|
|
},
|
|
{
|
|
"candidate_id":"r42-release-candidate-v2r2c2-20260819",
|
|
"source_commit":"5"*40,
|
|
"version":"bad",
|
|
"projection_id":"projection-"+"a"*32,
|
|
},
|
|
{
|
|
"candidate_id":"r42-release-candidate-v2r2c2-20260819",
|
|
"source_commit":"5"*40,
|
|
"version":"V2R2C2",
|
|
"projection_id":"bad",
|
|
},
|
|
{
|
|
"candidate_id":"r42-release-candidate-v2r2c2-20260819",
|
|
"source_commit":"5"*40,
|
|
"version":"V2R2C2",
|
|
"projection_id":"projection-"+"a"*32,
|
|
"execute":True,
|
|
},
|
|
],
|
|
)
|
|
def test_release_request_rejects_invalid_or_extra_fields(payload):
|
|
value=task()
|
|
value["constraints"]={"release_candidate":payload}
|
|
with pytest.raises(worker.ReleaseWorkerError):
|
|
worker.ReleaseRequest.from_task(value)
|
|
|
|
|
|
def test_result_payload_preserves_high_risk_denials():
|
|
release=worker.ReleaseRequest.from_task(task())
|
|
result=worker.result_payload(
|
|
"r42-release-candidate-live-acceptance-20260819",
|
|
release,
|
|
{"candidate_id":release.candidate_id,"status":"BUILT"},
|
|
)
|
|
assert result["status"]=="SUCCEEDED"
|
|
assert result["classification"]=="RELEASE_CANDIDATE_CREATED"
|
|
assert result["writer_lease"]=="NONE"
|
|
assert result["changed_files"]==[]
|
|
assert result["direct_main_write"] is False
|
|
assert result["production_write"] is False
|
|
assert result["tag_write"] is False
|
|
assert result["baseline_write"] is False
|
|
assert result["runtime_switch"] is False
|
|
assert result["owner_approval_submitted"] is False
|
|
assert result["credential_material_exposed"] is False
|
|
|
|
|
|
def test_result_payload_rejects_builder_candidate_mismatch():
|
|
release=worker.ReleaseRequest.from_task(task())
|
|
with pytest.raises(worker.ReleaseWorkerError,match="BUILDER_CANDIDATE_BINDING_INVALID"):
|
|
worker.result_payload(
|
|
"r42-release-candidate-live-acceptance-20260819",
|
|
release,
|
|
{"candidate_id":"other-release-candidate"},
|
|
)
|
|
|
|
|
|
def test_worker_calls_only_fixed_release_candidate_route(monkeypatch):
|
|
release=worker.ReleaseRequest.from_task(task())
|
|
calls=[]
|
|
|
|
def fake_http(base,method,route,token,body=None,timeout=30):
|
|
calls.append((base,method,route,token,body))
|
|
if route=="/health":
|
|
return 200,{"status":"ok"}
|
|
if route=="/createReleaseCandidate" and body=={}:
|
|
return 400,{"error":"request_invalid"}
|
|
if route.endswith("/claim"):
|
|
return 200,{"lease_token":"l"*32,"attempt":1}
|
|
if route.endswith("/heartbeat"):
|
|
return 200,{}
|
|
if route=="/createReleaseCandidate":
|
|
return 200,{
|
|
"candidate_id":release.candidate_id,
|
|
"artifact_sha256":"f"*64,
|
|
}
|
|
if route.endswith("/result"):
|
|
return 200,{"status":"VERIFYING"}
|
|
raise AssertionError(route)
|
|
|
|
monkeypatch.setattr(worker,"http_json",fake_http)
|
|
monkeypatch.setattr(worker,"audit",lambda *args,**kwargs:None)
|
|
monkeypatch.setattr(worker,"write_liveness",lambda *args,**kwargs:None)
|
|
monkeypatch.setattr(worker.Heartbeat,"first",lambda self:None)
|
|
monkeypatch.setattr(worker.Heartbeat,"close",lambda self:None)
|
|
monkeypatch.setattr(worker.Heartbeat,"ensure",lambda self:None)
|
|
monkeypatch.setattr(worker.threading.Thread,"start",lambda self:None)
|
|
|
|
assert worker.execute_task(
|
|
task(),
|
|
agent_token="a"*32,
|
|
release_token="r"*32,
|
|
) is True
|
|
|
|
release_calls=[
|
|
item for item in calls
|
|
if item[0]==worker.RELEASE_API
|
|
and item[2]=="/createReleaseCandidate"
|
|
and item[4]!={}
|
|
]
|
|
assert len(release_calls)==1
|
|
assert release_calls[0][1]=="POST"
|
|
assert release_calls[0][2]=="/createReleaseCandidate"
|
|
assert release_calls[0][4]=={
|
|
"candidate_id":release.candidate_id,
|
|
"source_commit":release.source_commit,
|
|
"version":release.version,
|
|
}
|
|
|
|
|
|
|
|
def test_write_liveness_records_release_capability(tmp_path,monkeypatch):
|
|
path=tmp_path/"worker-liveness.json"
|
|
monkeypatch.setattr(worker,"STATE_ROOT",tmp_path)
|
|
monkeypatch.setattr(worker,"LIVENESS_PATH",path)
|
|
worker.write_liveness(
|
|
"IDLE",
|
|
stage="poll",
|
|
visible_count=2,
|
|
)
|
|
value=json.loads(path.read_text())
|
|
assert value["schema"]=="aios.online-agent.worker-liveness.v1"
|
|
assert value["agent_type"]=="release-preparation-worker"
|
|
assert value["capabilities"]==["release:candidate"]
|
|
assert value["state"]=="IDLE"
|
|
assert value["visible_count"]==2
|
|
|
|
|
|
def test_sd_notify_is_noop_without_systemd_socket(monkeypatch):
|
|
monkeypatch.delenv("NOTIFY_SOCKET",raising=False)
|
|
worker.sd_notify("WATCHDOG=1")
|
|
|
|
def test_systemd_unit_uses_two_encrypted_credentials_and_loopback_release_api():
|
|
unit=(ROOT/"deploy/systemd/aios-release-online-agent-worker.service").read_text()
|
|
assert "LoadCredentialEncrypted=agent_token:" in unit
|
|
assert "LoadCredentialEncrypted=release_candidate_token:" in unit
|
|
assert "AIOS_RELEASE_API_URL=http://127.0.0.1:18890" in unit
|
|
assert "AIOS_RELEASE_WORKER_AGENT_ID=online-release-worker-cn-network-mkt-20260819" in unit
|
|
assert "Type=notify" in unit
|
|
assert "WatchdogSec=90" in unit
|
|
assert "Restart=always" in unit
|
|
assert "IPAddressDeny=any" in unit
|
|
assert "IPAddressAllow=127.0.0.0/8" in unit
|
|
assert "InaccessiblePaths=/data/aios-gitops /home" in unit
|
|
assert "submitOwnerApproval" not in unit
|
|
assert "startDeployment" not in unit
|
|
|
|
|
|
def test_source_contains_no_high_risk_release_routes():
|
|
source=(ROOT/"active-runtime/app/aios_release_online_agent_worker.py").read_text()
|
|
assert '"/createReleaseCandidate"' in source
|
|
for forbidden in (
|
|
"/submitOwnerApproval",
|
|
"/startDeployment",
|
|
"/requestOrCreateTag",
|
|
"/buildProductionArtifact",
|
|
"/executeApprovedProductionRelease",
|
|
):
|
|
assert forbidden not in source
|
|
|
|
|
|
def test_docs_keep_release_candidate_distinct_from_production():
|
|
doc=(ROOT/"docs/superpowers/specs/2026-08-19-release-preparation-online-agent.md").read_text()
|
|
assert "release:candidate" in doc
|
|
assert "Owner signature" in doc
|
|
assert "never\nstarts deployment" in doc or "never starts deployment" in doc
|
|
assert "LoadCredentialEncrypted" in doc
|