"""
Tests for Stage 68 — bundle export with manifest.
bundle_pilot_output zips a pilot output dir + writes a manifest.json
(sha256 per file). verify_bundle re-hashes and compares. The headline
test is a round-trip: bundle → unzip → verify checksums match.
"""
import hashlib
import json
import zipfile
import pytest
from delivery.bundle import bundle_pilot_output, verify_bundle
def _populate(tmp_path):
"""Build a tiny pilot-shaped dir with predictable content."""
src = tmp_path / "pilot_out"
src.mkdir()
(src / "customer_report.html").write_text("
Report
",
encoding="utf-8")
(src / "customer_report.pdf").write_bytes(b"%PDF-stub\nfake")
(src / "decision_queue.html").write_text("Queue
",
encoding="utf-8")
(src / "evidence").mkdir()
(src / "evidence" / "run_xyz.html").write_text("Evidence
",
encoding="utf-8")
return src
def test_bundle_creates_zip_next_to_source(tmp_path):
src = _populate(tmp_path)
out = bundle_pilot_output(src)
assert out.exists()
assert out.suffix == ".zip"
assert out.parent == src.parent
assert out.stem == src.name
def test_bundle_explicit_path(tmp_path):
src = _populate(tmp_path)
target = tmp_path / "custom_name.zip"
out = bundle_pilot_output(src, bundle_path=target)
assert out == target.resolve()
assert out.exists()
def test_bundle_includes_every_source_file(tmp_path):
src = _populate(tmp_path)
out = bundle_pilot_output(src)
with zipfile.ZipFile(out) as zf:
names = set(zf.namelist())
# manifest + the four files we wrote, with relative paths
assert "manifest.json" in names
assert "customer_report.html" in names
assert "customer_report.pdf" in names
assert "decision_queue.html" in names
assert "evidence/run_xyz.html" in names
def test_bundle_manifest_carries_sha256_per_file(tmp_path):
src = _populate(tmp_path)
out = bundle_pilot_output(src)
with zipfile.ZipFile(out) as zf:
manifest = json.loads(zf.read("manifest.json"))
assert manifest["source_dir_name"] == src.name
assert manifest["n_files"] == 4
assert manifest["total_bytes"] > 0
# spot-check one file's hash
by_path = {f["path"]: f for f in manifest["files"]}
expected = hashlib.sha256(b"Report
").hexdigest()
assert by_path["customer_report.html"]["sha256"] == expected
assert by_path["customer_report.html"]["bytes"] == len(b"Report
")
def test_bundle_manifest_files_sorted(tmp_path):
"""Deterministic manifest — same input gives same file order, so
diffing two bundles is meaningful."""
src = _populate(tmp_path)
out = bundle_pilot_output(src)
with zipfile.ZipFile(out) as zf:
manifest = json.loads(zf.read("manifest.json"))
paths = [f["path"] for f in manifest["files"]]
assert paths == sorted(paths)
def test_bundle_missing_source_dir_raises(tmp_path):
with pytest.raises(FileNotFoundError):
bundle_pilot_output(tmp_path / "nope")
# --- verify_bundle ----------------------------------------------------
def test_verify_bundle_returns_ok_on_intact_zip(tmp_path):
src = _populate(tmp_path)
out = bundle_pilot_output(src)
report = verify_bundle(out)
assert report["ok"] is True
assert report["n_files"] == 4
assert report["n_mismatches"] == 0
assert report["mismatches"] == []
def test_verify_bundle_detects_tampered_file(tmp_path):
"""The whole point of the manifest — if a file in the bundle was
modified, verify must spot it."""
src = _populate(tmp_path)
out = bundle_pilot_output(src)
# rewrite the zip, replacing one of the files with different bytes
tampered = tmp_path / "tampered.zip"
with zipfile.ZipFile(out, "r") as zin, \
zipfile.ZipFile(tampered, "w", zipfile.ZIP_DEFLATED) as zout:
for name in zin.namelist():
data = zin.read(name)
if name == "customer_report.html":
data = b"MALICIOUS
"
zout.writestr(name, data)
report = verify_bundle(tampered)
assert report["ok"] is False
assert report["n_mismatches"] == 1
assert (report["mismatches"][0]["path"]
== "customer_report.html")
# --- end-to-end through the pilot CLI -------------------------------
def test_pilot_cli_bundle_flag_creates_zip(tmp_path):
"""The pilot's --bundle flag end-to-end: real pilot run + bundle
write. Doesn't exhaustively check the bundle (the helper tests
do that) — just confirms the CLI wiring works."""
from delivery.pilot import _cli
out_dir = tmp_path / "pilot_out"
rc = _cli([
"--seed", "7",
"--out", str(out_dir),
"--bundle",
])
assert rc == 0
zip_path = out_dir.with_suffix(".zip")
assert zip_path.exists()
# the zip is verifiable
assert verify_bundle(zip_path)["ok"]
def test_pilot_cli_no_bundle_flag_skips_zip(tmp_path):
"""Backward compat — pre-Stage-68 cron lines that don't know
about --bundle still work as before."""
from delivery.pilot import _cli
out_dir = tmp_path / "pilot_out"
rc = _cli([
"--seed", "7",
"--out", str(out_dir),
])
assert rc == 0
zip_path = out_dir.with_suffix(".zip")
assert not zip_path.exists()