#!/usr/bin/env python3 """Run an end-to-end ProofFrame API smoke test against a running server.""" from __future__ import annotations import argparse from io import BytesIO import json from pathlib import Path import re from typing import Any from urllib.error import HTTPError from urllib.request import Request, urlopen from zipfile import ZipFile FORBIDDEN_EVIDENCE_KEYS = re.compile( r"(?i)(api[_-]?key|application[_-]?key|authorization|cookie|password|secret|token)" ) FORBIDDEN_EVIDENCE_VALUES = [ re.compile(r"(?i)authorization:\s*bearer\s+[A-Za-z0-9._\-]{20,}"), re.compile(r"(?i)(api[_-]?key|application[_-]?key|secret|token|cookie)=[^&\s]{8,}"), re.compile(r"(?i)x-amz-(credential|security-token|signature)=[^&\s]{8,}"), re.compile(r"(?i)gmi-[A-Za-z0-9_\-]{16,}"), ] FINAL_CLOSEOUT_REQUIRED_GATES = { "credential_handoff", "b2_live_proof", "genblaze_live_proof", "public_video", "devpost_receipt", "final_control", } FINAL_CLOSEOUT_PREFINAL_MODES = {"waiting_for_credentials", "closeout_blocked"} FINAL_CLOSEOUT_FINAL_MODE = "final_closeout_ready" FINAL_CLOSEOUT_SECRET_POLICY_TERMS = { "never stores", "backblaze keys", "genblaze provider keys", "devpost cookies", "signed urls", } def request_json(method: str, url: str, payload: dict[str, Any] | None = None) -> Any: body = None if payload is None else json.dumps(payload).encode("utf-8") request = Request( url, data=body, method=method, headers={"Content-Type": "application/json"}, ) try: with urlopen(request, timeout=60) as response: return json.loads(response.read().decode("utf-8")) except HTTPError as exc: detail = exc.read().decode("utf-8", errors="replace") raise SystemExit(f"{method} {url} failed: {exc.code} {detail}") from exc def request_bytes(method: str, url: str) -> tuple[bytes, str]: request = Request(url, method=method) try: with urlopen(request, timeout=60) as response: return response.read(), response.headers.get_content_type() except HTTPError as exc: detail = exc.read().decode("utf-8", errors="replace") raise SystemExit(f"{method} {url} failed: {exc.code} {detail}") from exc def require_backend(field: str, actual: str, expected: str | None) -> None: if expected and actual != expected: raise SystemExit(f"Expected {field}={expected}, got {actual}") def evidence_findings(value: Any, path: str = "$") -> list[str]: findings: list[str] = [] if isinstance(value, dict): for key, item in value.items(): key_path = f"{path}.{key}" if FORBIDDEN_EVIDENCE_KEYS.search(str(key)): findings.append(f"{key_path}: forbidden evidence key") findings.extend(evidence_findings(item, key_path)) elif isinstance(value, list): for index, item in enumerate(value): findings.extend(evidence_findings(item, f"{path}[{index}]")) elif isinstance(value, str): for pattern in FORBIDDEN_EVIDENCE_VALUES: if pattern.search(value): findings.append(f"{path}: forbidden evidence value") break return findings def assert_safe_evidence(evidence: dict[str, Any]) -> None: findings = evidence_findings(evidence) if findings: raise SystemExit("Unsafe evidence JSON refused:\n" + "\n".join(findings)) def write_evidence(path: Path, evidence: dict[str, Any]) -> None: assert_safe_evidence(evidence) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(evidence, indent=2) + "\n", encoding="utf-8") def run_smoke( base_url: str, *, require_storage_backend: str | None = None, require_generation_backend: str | None = None, ) -> dict[str, Any]: base = base_url.rstrip("/") health = request_json("GET", f"{base}/api/health") if not health.get("ready"): raise SystemExit(f"App is not ready: {health}") require_backend("storage_backend", health["storage_backend"], require_storage_backend) require_backend("generation_backend", health["generation_backend"], require_generation_backend) judge_brief = request_json("GET", f"{base}/api/judge/brief") if judge_brief.get("schema") != "proofframe.judge_brief.v1": raise SystemExit("Judge brief endpoint did not return the expected schema.") safe_to_submit = judge_brief.get("status", {}).get("safe_to_submit") if not isinstance(safe_to_submit, bool): raise SystemExit("Judge brief endpoint did not include a boolean safe_to_submit flag.") judge_crosswalk = request_json("GET", f"{base}/api/judge/crosswalk") if judge_crosswalk.get("schema") != "proofframe.judge_crosswalk.v1": raise SystemExit("Judge crosswalk endpoint did not return the expected schema.") crosswalk_safe_to_submit = judge_crosswalk.get("safe_to_submit") if not isinstance(crosswalk_safe_to_submit, bool): raise SystemExit("Judge crosswalk endpoint did not include a boolean safe_to_submit flag.") crosswalk_rows = judge_crosswalk.get("rows") if not isinstance(crosswalk_rows, list) or len(crosswalk_rows) < 4: raise SystemExit("Judge crosswalk endpoint did not include the expected criteria rows.") decision_brief = request_json("GET", f"{base}/api/judge/decision-brief") if decision_brief.get("schema") != "proofframe.judge_decision_brief.v1": raise SystemExit("Judge decision brief endpoint did not return the expected schema.") decision_brief_safe_to_submit = decision_brief.get("safe_to_submit") if not isinstance(decision_brief_safe_to_submit, bool): raise SystemExit("Judge decision brief endpoint did not include a boolean safe_to_submit flag.") decision_checks = decision_brief.get("decision_checks") if not isinstance(decision_checks, list) or len(decision_checks) < 6: raise SystemExit("Judge decision brief endpoint did not include the expected decision checks.") if health["storage_backend"] == "local" and health["generation_backend"] == "mock": if decision_brief_safe_to_submit: raise SystemExit("Mock/local judge decision brief must remain fail closed.") evidence_index = request_json("GET", f"{base}/api/judge/evidence-index") if evidence_index.get("schema") != "proofframe.judge_evidence_index.v1": raise SystemExit("Judge evidence index endpoint did not return the expected schema.") evidence_links = evidence_index.get("links") if not isinstance(evidence_links, list) or len(evidence_links) < 10: raise SystemExit("Judge evidence index endpoint did not include the expected evidence links.") evidence_safe_to_submit = evidence_index.get("safe_to_submit") if not isinstance(evidence_safe_to_submit, bool): raise SystemExit("Judge evidence index endpoint did not include a boolean safe_to_submit flag.") evidence_index_status = evidence_index.get("status") if not isinstance(evidence_index_status, dict) or "final_blockers" not in evidence_index_status: raise SystemExit("Judge evidence index endpoint did not include final blocker status.") final_closeout = request_json("GET", f"{base}/api/judge/final-closeout") if final_closeout.get("schema") != "proofframe.final_closeout_status.v1": raise SystemExit("Judge final closeout endpoint did not return the expected schema.") closeout_health_ok = final_closeout.get("closeout_health_ok") if closeout_health_ok is not True: raise SystemExit("Judge final closeout endpoint did not report closeout_health_ok=true.") closeout_safe_to_submit = final_closeout.get("safe_to_submit") if not isinstance(closeout_safe_to_submit, bool): raise SystemExit("Judge final closeout endpoint did not include a boolean safe_to_submit flag.") closeout_gates = final_closeout.get("gates") if not isinstance(closeout_gates, list) or len(closeout_gates) < 6: raise SystemExit("Judge final closeout endpoint did not include the expected gate ledger.") closeout_gate_ids = { str(gate.get("id")) for gate in closeout_gates if isinstance(gate, dict) and isinstance(gate.get("id"), str) } if not FINAL_CLOSEOUT_REQUIRED_GATES <= closeout_gate_ids: missing = sorted(FINAL_CLOSEOUT_REQUIRED_GATES - closeout_gate_ids) raise SystemExit("Judge final closeout endpoint is missing required gates: " + ", ".join(missing)) closeout_secret_policy = str(final_closeout.get("secret_policy") or "").lower() if not all(term in closeout_secret_policy for term in FINAL_CLOSEOUT_SECRET_POLICY_TERMS): raise SystemExit("Judge final closeout endpoint did not include the expected no-secret policy.") closeout_mode = final_closeout.get("mode") closeout_all_gates_ok = all(isinstance(gate, dict) and gate.get("ok") is True for gate in closeout_gates) closeout_has_blocker = any(isinstance(gate, dict) and gate.get("ok") is False for gate in closeout_gates) if closeout_safe_to_submit: if closeout_mode != FINAL_CLOSEOUT_FINAL_MODE or not closeout_all_gates_ok: raise SystemExit("Final closeout safe_to_submit=true requires final_closeout_ready and all gates green.") elif closeout_mode not in FINAL_CLOSEOUT_PREFINAL_MODES or not closeout_has_blocker: raise SystemExit("Pre-final closeout must remain blocked with a visible blocker gate.") if health["storage_backend"] == "local" and health["generation_backend"] == "mock": if closeout_safe_to_submit: raise SystemExit("Mock/local final closeout must remain fail closed.") video_publish_kit = request_json("GET", f"{base}/api/judge/video-publish-kit") if video_publish_kit.get("schema") != "proofframe.final_video_publish_kit.v1": raise SystemExit("Judge video publish kit endpoint did not return the expected schema.") video_publish_safe_to_submit = video_publish_kit.get("safe_to_submit") if not isinstance(video_publish_safe_to_submit, bool): raise SystemExit("Judge video publish kit endpoint did not include a boolean safe_to_submit flag.") video_publish_final_ready = video_publish_kit.get("final_video_ready") if not isinstance(video_publish_final_ready, bool): raise SystemExit("Judge video publish kit endpoint did not include a boolean final_video_ready flag.") if health["storage_backend"] == "local" and health["generation_backend"] == "mock": if video_publish_safe_to_submit or video_publish_final_ready: raise SystemExit("Mock/local judge video publish kit must remain pre-final and fail closed.") upload_checklist = video_publish_kit.get("upload_checklist") if not isinstance(upload_checklist, list) or len(upload_checklist) < 4: raise SystemExit("Judge video publish kit endpoint did not include the expected upload checklist.") judge_recording = request_json("GET", f"{base}/api/judge/recording") if judge_recording.get("schema") != "proofframe.recording_assets.v1": raise SystemExit("Judge recording endpoint did not return the expected schema.") recording_shots = judge_recording.get("shot_plan") if not isinstance(recording_shots, list) or len(recording_shots) < 3: raise SystemExit("Judge recording endpoint did not include the expected shot plan.") recording_final_ready = judge_recording.get("final_video_ready") if not isinstance(recording_final_ready, bool): raise SystemExit("Judge recording endpoint did not include a boolean final_video_ready flag.") judge_devpost = request_json("GET", f"{base}/api/judge/devpost") if judge_devpost.get("schema") != "proofframe.devpost_form_kit.v1": raise SystemExit("Judge Devpost endpoint did not return the expected schema.") devpost_fields = judge_devpost.get("fields") if not isinstance(devpost_fields, list) or len(devpost_fields) < 10: raise SystemExit("Judge Devpost endpoint did not include the expected form fields.") devpost_final_ready = judge_devpost.get("final_form_ready") if not isinstance(devpost_final_ready, bool): raise SystemExit("Judge Devpost endpoint did not include a boolean final_form_ready flag.") submission_checklist = request_json("GET", f"{base}/api/judge/submission-checklist") if submission_checklist.get("schema") != "proofframe.devpost_submission_checklist.v1": raise SystemExit("Judge submission checklist endpoint did not return the expected schema.") checklist_preflight = submission_checklist.get("preflight") if not isinstance(checklist_preflight, list) or len(checklist_preflight) < 3: raise SystemExit("Judge submission checklist endpoint did not include the expected preflight rows.") checklist_safe_to_submit = submission_checklist.get("safe_to_submit") if not isinstance(checklist_safe_to_submit, bool): raise SystemExit("Judge submission checklist endpoint did not include a boolean safe_to_submit flag.") campaign = request_json( "POST", f"{base}/api/campaigns", { "title": "Judge packet smoke", "brief": "Create one safe campaign visual for a product launch evidence packet.", "audience": "hackathon judges", "tone": "clear and production-ready", }, ) assets = request_json("POST", f"{base}/api/campaigns/{campaign['id']}/generate?count=1") if len(assets) != 1: raise SystemExit(f"Expected one generated asset, received {len(assets)}") asset = assets[0] approved = request_json( "POST", f"{base}/api/assets/{asset['id']}/status", {"status": "approved", "risk_note": "Approved by API smoke test."}, ) exported = request_json("POST", f"{base}/api/campaigns/{campaign['id']}/export") manifest_assets = exported["manifest"]["assets"] if len(manifest_assets) != 1: raise SystemExit("Manifest did not include the generated asset.") packet_bytes, packet_content_type = request_bytes( "GET", f"{base}/api/campaigns/{campaign['id']}/packet.zip", ) if packet_content_type != "application/zip": raise SystemExit(f"Packet endpoint returned {packet_content_type}, not application/zip.") with ZipFile(BytesIO(packet_bytes)) as packet: packet_names = set(packet.namelist()) if "manifest.json" not in packet_names or "README.txt" not in packet_names: raise SystemExit(f"Packet is missing required files: {packet_names}") demo = request_json("POST", f"{base}/api/demo/judge-packet") demo_assets = demo["assets"] if len(demo_assets) != 3 or demo_assets[0]["status"] != "approved": raise SystemExit("Judge demo packet did not return three assets with one approved asset.") return { "ok": True, "base_url": base, "generation_backend": health["generation_backend"], "storage_backend": health["storage_backend"], "judge_brief_schema": judge_brief["schema"], "judge_safe_to_submit": safe_to_submit, "judge_crosswalk_schema": judge_crosswalk["schema"], "judge_crosswalk_mode": judge_crosswalk.get("mode"), "judge_crosswalk_safe_to_submit": crosswalk_safe_to_submit, "judge_crosswalk_rows": len(crosswalk_rows), "judge_decision_brief_schema": decision_brief["schema"], "judge_decision_brief_mode": decision_brief.get("mode"), "judge_decision_brief_safe_to_submit": decision_brief_safe_to_submit, "judge_decision_brief_checks": len(decision_checks), "judge_evidence_index_schema": evidence_index["schema"], "judge_evidence_index_mode": evidence_index.get("mode"), "judge_evidence_index_safe_to_submit": evidence_safe_to_submit, "judge_evidence_index_links": len(evidence_links), "judge_final_closeout_schema": final_closeout["schema"], "judge_final_closeout_mode": final_closeout.get("mode"), "judge_final_closeout_health_ok": closeout_health_ok, "judge_final_closeout_safe_to_submit": closeout_safe_to_submit, "judge_final_closeout_gates": len(closeout_gates), "judge_video_publish_kit_schema": video_publish_kit["schema"], "judge_video_publish_kit_mode": video_publish_kit.get("mode"), "judge_video_publish_kit_safe_to_submit": video_publish_safe_to_submit, "judge_video_publish_kit_final_video_ready": video_publish_final_ready, "judge_video_publish_kit_upload_checks": len(upload_checklist), "judge_recording_schema": judge_recording["schema"], "judge_recording_mode": judge_recording.get("mode"), "judge_recording_final_video_ready": recording_final_ready, "judge_recording_shots": len(recording_shots), "judge_devpost_schema": judge_devpost["schema"], "judge_devpost_mode": judge_devpost.get("mode"), "judge_devpost_final_form_ready": devpost_final_ready, "judge_devpost_fields": len(devpost_fields), "judge_submit_checklist_schema": submission_checklist["schema"], "judge_submit_checklist_mode": submission_checklist.get("mode"), "judge_submit_checklist_safe_to_submit": checklist_safe_to_submit, "judge_submit_checklist_preflight": len(checklist_preflight), "campaign_id": campaign["id"], "asset_id": asset["id"], "asset_provider": asset["provider"], "asset_model": asset["model"], "asset_storage_backend": asset["storage_backend"], "asset_storage_key": asset["storage_key"], "asset_public_url_present": bool(asset.get("public_url")), "asset_status": approved["status"], "asset_sha256": asset["sha256"], "manifest_storage_backend": exported["stored_manifest"]["storage_backend"], "manifest_key": exported["stored_manifest"]["storage_key"], "manifest_sha256": exported["stored_manifest"]["sha256"], "packet_bytes": len(packet_bytes), "judge_demo_campaign_id": demo["campaign"]["id"], } def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Smoke test a running ProofFrame API.") parser.add_argument("--base-url", default="http://127.0.0.1:8088") parser.add_argument( "--require-storage-backend", choices=["local", "b2"], help="Fail unless /api/health reports this storage backend.", ) parser.add_argument( "--require-generation-backend", choices=["mock", "genblaze"], help="Fail unless /api/health reports this generation backend.", ) parser.add_argument( "--evidence-out", type=Path, help="Write the safe smoke-test evidence JSON to this path.", ) return parser def main() -> None: args = build_parser().parse_args() evidence = run_smoke( args.base_url, require_storage_backend=args.require_storage_backend, require_generation_backend=args.require_generation_backend, ) if args.evidence_out: write_evidence(args.evidence_out, evidence) print(json.dumps(evidence, indent=2)) if __name__ == "__main__": main()