http service with health, extension listing, profile validation, run planning, async run jobs, job inspection, and report retrieval

This commit is contained in:
2026-05-07 22:19:10 +02:00
parent 3ae6fd4140
commit a3ea11139c
12 changed files with 1028 additions and 13 deletions

View File

@@ -1,13 +1,16 @@
from __future__ import annotations
import unittest
import http.client
import json
import time
import unittest
from tempfile import TemporaryDirectory
from pathlib import Path
from guide_board.discovery import discover_extensions
from guide_board.execution import run_assessment
from guide_board.gates import evaluate_trend_gates
from guide_board.io import load_json
from guide_board.planning import (
build_run_plan,
validate_assessment_profile,
@@ -15,6 +18,7 @@ from guide_board.planning import (
)
from guide_board.retention import build_trend_summary, list_retained_runs
from guide_board.schema import assert_valid
from guide_board.service import ServiceHandle, start_service
ROOT = Path(__file__).resolve().parents[1]
@@ -35,6 +39,15 @@ class CoreArchitectureTests(unittest.TestCase):
self.assertEqual(target["id"], "sample-repository")
self.assertEqual(assessment["target_profile_ref"], "sample-repository")
def test_validates_evidence_request_template(self) -> None:
request_set = load_json(ROOT / "extensions" / "_template" / "evidence-request-set.json")
assert_valid(request_set, "evidence-request-set")
self.assertEqual(
request_set["source_boundary"]["certification_boundary"],
"This evidence request set supports assessment preparation only.",
)
def test_builds_sample_run_plan(self) -> None:
plan = build_run_plan(
ROOT,
@@ -164,6 +177,70 @@ class CoreArchitectureTests(unittest.TestCase):
self.assertEqual(len(mappings), 1)
self.assertEqual(mappings[0]["target_id"], "profile-readiness")
def test_serves_local_api_run_lifecycle(self) -> None:
with TemporaryDirectory() as temporary_directory:
service = start_service(ROOT, host="127.0.0.1", port=0)
try:
health = _request_json(service, "GET", "/health")
self.assertEqual(health["status"], "ok")
extensions = _request_json(service, "GET", "/extensions")
self.assertIn(
"sample-noop",
[extension["id"] for extension in extensions["extensions"]],
)
target_validation = _request_json(
service,
"POST",
"/profiles/validate",
{
"kind": "target",
"path": "profiles/targets/sample-repository.json",
},
)
self.assertEqual(target_validation["profile_id"], "sample-repository")
plan = _request_json(
service,
"POST",
"/assessments/plan",
{
"target": "profiles/targets/sample-repository.json",
"assessment": "profiles/assessments/sample-noop.json",
},
)
self.assertEqual(plan["target_profile_snapshot"]["id"], "sample-repository")
job = _request_json(
service,
"POST",
"/runs",
{
"target": "profiles/targets/sample-repository.json",
"assessment": "profiles/assessments/sample-noop.json",
"output_dir": str(Path(temporary_directory) / "api-run"),
},
expected_status=202,
)
status = _wait_for_job(service, job["job_id"])
self.assertEqual(status["status"], "succeeded")
self.assertEqual(status["result"]["status"], "completed")
reports = _request_json(
service,
"GET",
f"/runs/{job['job_id']}/reports",
)
self.assertIn("Guide Board Assessment Report", reports["report"]["markdown"])
self.assertEqual(
reports["assessment_package"]["json"]["run_id"],
status["result"]["run_id"],
)
finally:
service.stop()
def test_builds_retained_run_trends(self) -> None:
with TemporaryDirectory() as temporary_directory:
runs_dir = Path(temporary_directory)
@@ -293,6 +370,42 @@ def _write_retention_summary(
)
def _request_json(
service: ServiceHandle,
method: str,
path: str,
payload: dict[str, object] | None = None,
expected_status: int = 200,
) -> dict[str, object]:
connection = http.client.HTTPConnection(service.host, service.port, timeout=5)
body = None
headers = {}
if payload is not None:
body = json.dumps(payload).encode("utf-8")
headers["Content-Type"] = "application/json"
try:
connection.request(method, path, body=body, headers=headers)
response = connection.getresponse()
data = response.read().decode("utf-8")
finally:
connection.close()
if response.status != expected_status:
raise AssertionError(f"expected HTTP {expected_status}, got {response.status}: {data}")
value = json.loads(data)
if not isinstance(value, dict):
raise AssertionError(f"expected JSON object response, got {type(value).__name__}")
return value
def _wait_for_job(service: ServiceHandle, job_id: str) -> dict[str, object]:
for _ in range(50):
status = _request_json(service, "GET", f"/runs/{job_id}")
if status["status"] in {"succeeded", "failed"}:
return status
time.sleep(0.05)
raise AssertionError(f"job did not finish: {job_id}")
def _write_external_extension(extension_dir: Path) -> None:
extension_dir.mkdir(parents=True, exist_ok=True)
(extension_dir / "extension.json").write_text(