capscore-agent / tests /test_proof_pack.py
mathurinacheisoft's picture
Deploy CAPScore (clean recreate)
2a7f679 verified
Raw
History Blame Contribute Delete
7.68 kB
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)
# Extract and compare result.json hashes
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