| """ |
| 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("<h1>Report</h1>", |
| encoding="utf-8") |
| (src / "customer_report.pdf").write_bytes(b"%PDF-stub\nfake") |
| (src / "decision_queue.html").write_text("<h1>Queue</h1>", |
| encoding="utf-8") |
| (src / "evidence").mkdir() |
| (src / "evidence" / "run_xyz.html").write_text("<h1>Evidence</h1>", |
| 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()) |
| |
| 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 |
| |
| by_path = {f["path"]: f for f in manifest["files"]} |
| expected = hashlib.sha256(b"<h1>Report</h1>").hexdigest() |
| assert by_path["customer_report.html"]["sha256"] == expected |
| assert by_path["customer_report.html"]["bytes"] == len(b"<h1>Report</h1>") |
|
|
|
|
| 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") |
|
|
|
|
| |
|
|
| 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) |
| |
| 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"<h1>MALICIOUS</h1>" |
| 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") |
|
|
|
|
| |
|
|
| 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() |
| |
| 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() |
|
|