Files
aios/tests/autonomous_release/test_http_service.py
T

157 lines
5.9 KiB
Python

from contextlib import contextmanager
import hashlib
import json
from pathlib import Path
from threading import Thread
import urllib.error
import urllib.request
from aios_autonomous_release_candidate.service import CandidateApplication, CandidateHTTPServer
@contextmanager
def running_server(tmp_path: Path):
application = CandidateApplication(tmp_path / "state.sqlite3")
server = CandidateHTTPServer(("127.0.0.1", 0), application)
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield application, f"http://127.0.0.1:{server.server_port}"
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
def get_json(url: str):
with urllib.request.urlopen(url, timeout=5) as response:
return response.status, json.load(response)
def post_json(url: str, payload: dict):
request = urllib.request.Request(
url,
data=json.dumps(payload).encode("utf-8"),
method="POST",
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(request, timeout=5) as response:
return response.status, json.load(response)
def test_health_and_readiness_are_loopback_safe(tmp_path: Path) -> None:
with running_server(tmp_path) as (_, base):
status, health = get_json(f"{base}/health")
assert status == 200
assert health["production_side_effects"] is False
status, ready = get_json(f"{base}/ready")
assert status == 200
assert ready == {"status": "ready", "secrets_included": False}
def test_json_content_type_is_required(tmp_path: Path) -> None:
with running_server(tmp_path) as (_, base):
request = urllib.request.Request(f"{base}/v1/tasks", data=b"{}", method="POST")
try:
urllib.request.urlopen(request, timeout=5)
except urllib.error.HTTPError as error:
assert error.code == 415
assert json.load(error)["error"] == "content_type_required"
def test_internal_exception_text_is_not_disclosed(tmp_path: Path) -> None:
with running_server(tmp_path) as (application, base):
def fail(_payload):
raise RuntimeError("sensitive internal diagnostic")
application.api.create_task = fail
request = urllib.request.Request(
f"{base}/v1/tasks",
data=b"{}",
method="POST",
headers={"Content-Type": "application/json"},
)
try:
urllib.request.urlopen(request, timeout=5)
except urllib.error.HTTPError as error:
assert error.code == 500
body = error.read().decode("utf-8")
assert "candidate_request_failed" in body
assert "sensitive internal diagnostic" not in body
def test_task_lifecycle_evidence_and_release_gate_endpoints(tmp_path: Path) -> None:
task_id = "api-lifecycle-task"
task = {
"task_id": task_id,
"intent": "exercise candidate API",
"binding": {
"repository": "aios/aios",
"branch": "patch/api-lifecycle",
"base_oid": "a" * 40,
"allowed_paths": ["services/test"],
},
"risk": "medium",
"steps": [
{
"step_id": "test-step",
"worker_role": "tester",
"required_capabilities": ["test.execute"],
}
],
"evidence_requirements": ["STEP_RESULT"],
"production_side_effects_allowed": False,
}
with running_server(tmp_path) as (_, base):
status, created = post_json(f"{base}/v1/tasks", task)
assert status == 201
assert created["production_side_effects"] is False
status, steps = get_json(f"{base}/v1/tasks/{task_id}/steps")
assert status == 200
assert steps["steps"]["test-step"]["state"] == "READY"
binding = {
"step_id": "test-step",
"worker_id": "worker-1",
"session_id": "session-1",
}
_, claimed = post_json(f"{base}/v1/tasks/{task_id}/claim", binding)
assert claimed["state"] == "RUNNING"
_, heartbeat = post_json(f"{base}/v1/tasks/{task_id}/heartbeat", binding)
assert heartbeat["state"] == "RUNNING"
_, result = post_json(
f"{base}/v1/tasks/{task_id}/result",
{"step_id": "test-step", "result_digest": hashlib.sha256(b"ok").hexdigest()},
)
assert result["state"] == "COMPLETED"
_, evidence = get_json(f"{base}/v1/tasks/{task_id}/evidence")
assert evidence["verified"] is True
assert any(item["event_type"] == "STEP_RESULT" for item in evidence["events"])
_, closure = post_json(f"{base}/v1/tasks/{task_id}/evidence/close", {})
assert len(closure["evidence_closure"]) == 64
release_plan = {
"task_id": task_id,
"repository": "aios/aios",
"branch": "integration/api-lifecycle",
"version": "SHADOW-0.1.0",
"target_oid": "b" * 40,
"tree_oid": "c" * 40,
"artifact_sha256": "d" * 64,
"manifest_sha256": "e" * 64,
"package_closure_sha256": "f" * 64,
"evidence_closure_oid": "1" * 40,
"approval_plan_id": "external-owner-release",
"rollback_approval_plan_id": "external-owner-rollback",
"rollback_artifact_sha256": "2" * 64,
"expected_tag_oid": "3" * 40,
"expected_target_oid": "b" * 40,
"production_config_sha256": "4" * 64,
}
_, release = post_json(f"{base}/v1/release/plan", release_plan)
assert release["release_state"] == "BUILT"
_, verified = post_json(
f"{base}/v1/release/verify", {"artifact_ok": True, "evidence_ok": True}
)
assert verified["release_state"] == "WAITING_OWNER"
assert verified["production_release"] == "BLOCKED_EXTERNAL_OWNER_SIGNATURE"