| import json |
| import zipfile |
| import io |
| import pytest |
| from datetime import datetime, timezone |
| from app.proof_pack import build_proof_pack, _sha256, _canonical_json, render_result_md |
| from app.models import JobResult, JobStatus, ScorecardResult, ScoreDimension, WorkerResults |
|
|
|
|
| def _make_job(score: float = 75.0) -> JobResult: |
| sc = ScorecardResult( |
| overall_score=score, |
| technical_execution=ScoreDimension(score=80.0, notes="Test"), |
| a2a_composability=ScoreDimension(score=70.0, notes="Test"), |
| innovation=ScoreDimension(score=75.0, notes="Test"), |
| adoption_readiness=ScoreDimension(score=65.0, notes="Test"), |
| presentation_readiness=ScoreDimension(score=70.0, notes="Test"), |
| critical_issues=["Missing .env.example"], |
| top_fixes=["Add .env.example"], |
| ) |
| return JobResult( |
| job_id="test_job_001", |
| capability="audit_repository", |
| status=JobStatus.DONE, |
| created_at=datetime(2026, 6, 12, 10, 0, 0, tzinfo=timezone.utc), |
| completed_at=datetime(2026, 6, 12, 10, 4, 0, tzinfo=timezone.utc), |
| scorecard=sc, |
| result_hash="sha256:placeholder", |
| ) |
|
|
|
|
| def test_proof_pack_deterministic(): |
| """Same input produces same SHA-256 hash.""" |
| job = _make_job(75.0) |
| log_lines = ['{"ts": "2026-06-12T10:00:00Z", "event": "job_start"}'] |
| zip1 = build_proof_pack(job, log_lines) |
| zip2 = build_proof_pack(job, log_lines) |
| |
| with zipfile.ZipFile(io.BytesIO(zip1)) as z1, zipfile.ZipFile(io.BytesIO(zip2)) as z2: |
| hash1 = z1.read("result_hash.sha256").decode() |
| hash2 = z2.read("result_hash.sha256").decode() |
| assert hash1 == hash2 |
|
|
|
|
| def test_proof_pack_contains_required_files(): |
| """ZIP must contain all required proof files.""" |
| job = _make_job() |
| zip_bytes = build_proof_pack(job, []) |
| with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: |
| names = set(zf.namelist()) |
| required = { |
| "manifest.json", |
| "result.json", |
| "result.md", |
| "result_hash.sha256", |
| "execution_log.jsonl", |
| "attestation.json", |
| "evidence/sources.json", |
| } |
| assert required.issubset(names), f"Missing files: {required - names}" |
|
|
|
|
| def test_result_hash_format(): |
| """Result hash must start with 'sha256:'.""" |
| job = _make_job() |
| zip_bytes = build_proof_pack(job, []) |
| with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: |
| hash_content = zf.read("result_hash.sha256").decode() |
| assert hash_content.startswith("sha256:") |
|
|
|
|
| def test_result_hash_is_hex(): |
| """The hex part of the SHA-256 hash is a valid 64-char hex string.""" |
| job = _make_job() |
| zip_bytes = build_proof_pack(job, []) |
| with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: |
| hash_content = zf.read("result_hash.sha256").decode() |
| hex_part = hash_content.removeprefix("sha256:") |
| assert len(hex_part) == 64 |
| assert all(c in "0123456789abcdef" for c in hex_part) |
|
|
|
|
| def test_manifest_valid_json(): |
| """manifest.json must be valid JSON.""" |
| job = _make_job() |
| zip_bytes = build_proof_pack(job, []) |
| with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: |
| manifest = json.loads(zf.read("manifest.json")) |
| assert "job_id" in manifest |
| assert "result_hash" in manifest |
| assert "proof_files" in manifest |
|
|
|
|
| def test_manifest_job_id_matches(): |
| """manifest.json job_id matches the job.""" |
| job = _make_job() |
| zip_bytes = build_proof_pack(job, []) |
| with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: |
| manifest = json.loads(zf.read("manifest.json")) |
| assert manifest["job_id"] == "test_job_001" |
|
|
|
|
| def test_attestation_valid_json(): |
| """attestation.json must be valid JSON with required fields.""" |
| job = _make_job() |
| zip_bytes = build_proof_pack(job, []) |
| with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: |
| attestation = json.loads(zf.read("attestation.json")) |
| assert "statement" in attestation |
| assert "result_hash" in attestation |
| assert "provider_agent" in attestation |
|
|
|
|
| def test_attestation_hash_matches_manifest(): |
| """attestation.json result_hash matches manifest.json result_hash.""" |
| job = _make_job() |
| zip_bytes = build_proof_pack(job, []) |
| with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: |
| manifest = json.loads(zf.read("manifest.json")) |
| attestation = json.loads(zf.read("attestation.json")) |
| assert manifest["result_hash"] == attestation["result_hash"] |
|
|
|
|
| def test_canonical_json_deterministic(): |
| """_canonical_json produces identical output for same input.""" |
| obj = {"b": 2, "a": 1, "c": {"z": 26, "x": 24}} |
| assert _canonical_json(obj) == _canonical_json(obj) |
| assert _canonical_json(obj) == _canonical_json({"a": 1, "b": 2, "c": {"x": 24, "z": 26}}) |
|
|
|
|
| def test_canonical_json_sorted_keys(): |
| """_canonical_json sorts keys alphabetically.""" |
| obj = {"z": 1, "a": 2, "m": 3} |
| result = _canonical_json(obj) |
| parsed = json.loads(result) |
| assert list(parsed.keys()) == ["a", "m", "z"] |
|
|
|
|
| def test_sha256_returns_hex(): |
| """_sha256 returns a 64-char lowercase hex string.""" |
| result = _sha256("hello world") |
| assert len(result) == 64 |
| assert all(c in "0123456789abcdef" for c in result) |
|
|
|
|
| def test_sha256_known_value(): |
| """_sha256 returns the correct SHA-256 for 'hello world'.""" |
| import hashlib |
| expected = hashlib.sha256(b"hello world").hexdigest() |
| assert _sha256("hello world") == expected |
|
|
|
|
| def test_result_md_contains_score(): |
| """result.md must contain the overall score.""" |
| job = _make_job(84.0) |
| md = render_result_md(job) |
| assert "84" in md |
| assert "CAPScore" in md |
|
|
|
|
| def test_result_md_contains_dimensions(): |
| """result.md must contain all 5 scoring dimensions.""" |
| job = _make_job(75.0) |
| md = render_result_md(job) |
| assert "Technical Execution" in md |
| assert "A2A Composability" in md |
| assert "Innovation" in md |
| assert "Adoption Readiness" in md |
| assert "Presentation Readiness" in md |
|
|
|
|
| def test_result_md_contains_critical_issues(): |
| """result.md includes Critical Issues section when issues exist.""" |
| job = _make_job(75.0) |
| md = render_result_md(job) |
| assert "Critical Issues" in md |
| assert "Missing .env.example" in md |
|
|
|
|
| def test_result_md_no_scorecard(): |
| """render_result_md handles a job with no scorecard gracefully.""" |
| job = JobResult( |
| job_id="no_scorecard_job", |
| capability="audit_repository", |
| status=JobStatus.DONE, |
| created_at=datetime(2026, 6, 12, 10, 0, 0, tzinfo=timezone.utc), |
| ) |
| md = render_result_md(job) |
| assert "CAPScore" in md |
| assert "no_scorecard_job" in md |
|
|
|
|
| def test_proof_pack_different_scores_different_hashes(): |
| """Two jobs with different scores produce different hashes.""" |
| job1 = _make_job(75.0) |
| job2 = _make_job(90.0) |
| zip1 = build_proof_pack(job1, []) |
| zip2 = build_proof_pack(job2, []) |
| with zipfile.ZipFile(io.BytesIO(zip1)) as z1, zipfile.ZipFile(io.BytesIO(zip2)) as z2: |
| hash1 = z1.read("result_hash.sha256").decode() |
| hash2 = z2.read("result_hash.sha256").decode() |
| assert hash1 != hash2 |
|
|
|
|
| def test_execution_log_written(): |
| """execution_log.jsonl contains the provided log lines.""" |
| job = _make_job() |
| log_lines = [ |
| '{"ts": "2026-06-12T10:00:00Z", "event": "job_start"}', |
| '{"ts": "2026-06-12T10:01:00Z", "event": "worker_done", "worker": "repo"}', |
| ] |
| zip_bytes = build_proof_pack(job, log_lines) |
| with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: |
| log_content = zf.read("execution_log.jsonl").decode() |
| for line in log_lines: |
| assert line in log_content |
|
|