diff --git a/README.md b/README.md index 6593906a..beec2b7e 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ This repo gives you a safe starting point for: - penalizing likely safety/synthesis risks; - selecting diverse candidates; - generating auditable JSON evidence certificates; +- bundling lab-review packs with chain-of-custody identity hashes; - running a demo pipeline without downloading external biological datasets; - expanding later with real predictors and qualified external validation. @@ -61,6 +62,7 @@ candidate sequences -> novelty score against references -> ensemble rank -> evidence certificate + -> chain-of-custody hash manifest -> expert-review package, if human review approves ``` diff --git a/docs/50_LOOP_PLAN.md b/docs/50_LOOP_PLAN.md index e2e3aab0..e6acade8 100644 --- a/docs/50_LOOP_PLAN.md +++ b/docs/50_LOOP_PLAN.md @@ -130,7 +130,7 @@ Prepare for real wet-lab partners. Make the pack reviewable, the evidence tracea | 42 | No expert-review GitHub template. Review process is ad-hoc | `.github/ISSUE_TEMPLATE/expert_review.yml`: structured form for expert reviewers covering novelty, safety, mechanism plausibility, experiment design | Template generates a review issue with all required fields | | 43 ✅ | No decision-log format for human review. AGENTS.md requires human review for 6 decision types but no machine-readable log format exists | `schemas/decision_log.schema.json`: JSON Schema (Draft 2020-12) with 12 fields, 9 decision types, dissent-requires-notes conditional. v0.5.64 | 11 tests, 2000 total. Schema validates against real decision examples | | 44 ✅ | No candidate batch pack that a lab partner can download as a single artifact | `scripts/build_lab_batch_pack.py`: generates a zip with candidate CSV, evidence certificates, protocol refs, controls manifest, and data return template. `make lab-batch-pack` target. v0.5.63 | 10 tests, 1989 total. Zip contains 36 evidence certs + 3 protocol docs + controls + data return template | -| 45 | No chain-of-custody hashing for lab samples. Lab receives peptides but can't verify identity | Add SHA-256 hash of each candidate sequence and synthesis order to the batch pack. Lab can verify received peptides match | Hash verification script works end-to-end on synthetic data | +| 45 ✅ | No chain-of-custody hashing for lab samples. Lab receives peptides but can't verify identity | `scripts/build_lab_batch_pack.py` now writes `chain_of_custody.json` + `MANIFEST.json` into the lab batch pack, including SHA-256 hashes for `panel.csv`, the ordered candidate list, each candidate sequence, and each evidence certificate. `--verify-pack` re-checks archive integrity end to end. v0.5.65 | Verification script passes on synthetic data and fails on tampered packs | | 46 | No pre-registered analysis plan for Wave 1 results. What analysis runs when data comes back? | `docs/WAVE1_ANALYSIS_PLAN.md`: pre-registered statistical analysis (primary: MIC ≤ 32 hit rate; secondary: selectivity index; exploratory: per-family correlation). No post-hoc changes allowed | Plan is timestamped and locked before data return | | 47 ✅ | No data-return validation tool. Lab sends CSV/JSON files; pipeline needs to validate them against schema | `scripts/validate_lab_data_return.py`: validates JSON files against `schemas/lab_result.schema.json`, reports per-file errors, detects control failures. v0.5.67 | 8 tests, 2008 total. Detects missing fields, invalid JSON, control failures | | 48 | No public-data release package. If results are publishable, what goes in the supplement? | `docs/PUBLICATION_PACK.md`: checklist for public release — benchmark data, evidence certificates, candidate scores, negative results, analysis scripts, README for reproduction | Pack is complete enough for a reviewer to reproduce all findings | diff --git a/docs/LAB_PARTNER_ONBOARDING.md b/docs/LAB_PARTNER_ONBOARDING.md index 8a3712b0..95ec3b50 100644 --- a/docs/LAB_PARTNER_ONBOARDING.md +++ b/docs/LAB_PARTNER_ONBOARDING.md @@ -59,11 +59,23 @@ partner_packet/ unsupported_claims.md pre_registered_pilot_template.md result_intake_schema_reference.md + chain_of_custody.json release_review.md ``` See [`EXTERNAL_REVIEW_PACKET.md`](EXTERNAL_REVIEW_PACKET.md) for the full packet standard. +Current lab batch packs generated by `scripts/build_lab_batch_pack.py` include +`chain_of_custody.json` and `MANIFEST.json`. A receiver can run: + +```bash +python scripts/build_lab_batch_pack.py --verify-pack outputs/lab_batch_pack.zip +``` + +The check verifies archive identity only: candidate IDs, sequences, panel order, +and evidence certificate files. It does not verify synthesis, biological +activity, safety, or experimental provenance after receipt. + ## What a lab partner does not receive from this repo OpenAMP should not provide: diff --git a/docs/METRICS_CURRENT.md b/docs/METRICS_CURRENT.md index 9ae7951e..cb8f4a34 100644 --- a/docs/METRICS_CURRENT.md +++ b/docs/METRICS_CURRENT.md @@ -6,6 +6,7 @@ Machine-readable snapshot: `outputs/metrics_snapshot.json` regenerated with `mak > with this file, this file wins. Updated whenever benchmark/benchmark config changes. > > **Last updated:** 2026-07-06 (data-return validation — v0.5.67) +> **New in v0.5.65:** `scripts/build_lab_batch_pack.py` now writes `chain_of_custody.json` and `MANIFEST.json` into lab batch packs. The custody file includes SHA-256 hashes for `panel.csv`, the ordered candidate list, each candidate sequence, and each evidence certificate. `--verify-pack` verifies archive integrity and detects tampering. These hashes prove identity/integrity only, not biological activity, safety, or synthesis success. 5 tests. > **New in v0.5.64:** `schemas/decision_log.schema.json` — 12-field JSON Schema for human review decisions. Covers 9 decision types from AGENTS.md §8. Dissent conditional. 11 tests. 2000 total. > **New in v0.5.63:** `scripts/build_lab_batch_pack.py` — generates single zip with candidate CSV, 36 evidence certs, protocol refs, controls manifest, data return template. `make lab-batch-pack`. 10 tests. 1989 total. > **New in v0.5.62:** Pre-registered pass/fail criteria + simulation uncertainty in evidence. `configs/wave1_pass_fail.yaml` + `scripts/check_wave1_pass_fail.py`. `rank info` now propagates simulation uncertainty into evidence certs. 17 tests. @@ -49,7 +50,7 @@ Machine-readable snapshot: `outputs/metrics_snapshot.json` regenerated with `mak > **New in v0.5.31:** Added dipeptide-order features for sequence-order awareness. `dipeptide_order_score` achieves AUROC 0.7861 on AMP-vs-scrambled discrimination — the strongest order-dependent feature in the pipeline. Only 7/31 features survive scrambling (amphipathicity/helix-wheel + dipeptide). All composition features are purely position-independent (exactly 0.5000 AUROC on scrambled test). > **New in v0.5.30:** Easy baseline benchmark added — charge density alone (AUROC 0.8166) outperforms the full pipeline ensemble (0.7792) on AMP-vs-Swiss-Prot-decoy discrimination. Honest finding documented: expected because pipeline optimizes for safety, not raw discrimination. > **New in v0.5.29:** Expanded benchmark to 500 AMPs + 500 composition-matched decoys (n=1000). AUROC 0.7792 (CI₉₅: 0.7505–0.8065) confirms signal generalizes. Cluster-aware CI: 0.746–0.8102. Representative AUROC: 0.778. Standard benchmark (n=191) retained for backward comparison. -> **Pipeline version:** v0.5.62 +> **Pipeline version:** v0.5.67 > **Branch:** main --- diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a7e3735b..eaac5afe 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -13,6 +13,28 @@ Changes: - ``docs/METRICS_CURRENT.md`` — Changelog updated. Test count: 2008. - 2008 tests passing. +## v0.5.65 — Lab Batch Pack Chain-of-Custody Hashes (Loop 45) ✓ (2026-07-06) + +Closed a real Phase 4 handoff gap: a lab batch pack could be zipped and sent, +but the receiver had no machine-checkable way to verify that candidate IDs, +sequences, ordering, and evidence files were still the intended ones. + +Changes: +- `scripts/build_lab_batch_pack.py` — now writes `chain_of_custody.json` and + `MANIFEST.json` into each pack. The custody file records SHA-256 hashes for + `panel.csv`, the ordered candidate list, each candidate sequence, and each + bundled evidence certificate. Added `--verify-pack` to re-check those hashes + against an existing zip. +- `tests/test_lab_batch_pack.py` — covers custody-file generation, successful + verification, tamper detection, and CLI verification mode. +- `docs/50_LOOP_PLAN.md` — Loop 45 ✅. Next: Loop 46. +- `docs/METRICS_CURRENT.md` — version and changelog synced. + +Honest boundary: +- These hashes verify identity and archive integrity only. +- They are not evidence of biological activity, safety, synthesis success, or + experimental provenance after a qualified lab receives material. + ## v0.5.64 — Decision-Log Schema (Loop 43) ✓ (2026-07-06) Machine-readable JSON Schema for human review decisions, covering the diff --git a/scripts/build_lab_batch_pack.py b/scripts/build_lab_batch_pack.py index b30a1332..09c9c6dd 100644 --- a/scripts/build_lab_batch_pack.py +++ b/scripts/build_lab_batch_pack.py @@ -7,6 +7,7 @@ from __future__ import annotations import csv +import hashlib import json import sys import zipfile @@ -15,6 +16,90 @@ from typing import Any +def _sha256_text(value: str) -> str: + return hashlib.sha256(value.encode("utf-8")).hexdigest() + + +def _sha256_bytes(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + + +def _read_panel_rows(panel_csv: Path) -> list[dict[str, str]]: + with panel_csv.open(newline="", encoding="utf-8") as f: + return list(csv.DictReader(f)) + + +def _candidate_identity_rows(panel_rows: list[dict[str, str]]) -> list[dict[str, Any]]: + identities: list[dict[str, Any]] = [] + for row in panel_rows: + candidate_id = row.get("candidate_id", "").strip() + sequence = row.get("sequence", "").strip().upper() + if not candidate_id or not sequence: + continue + identities.append({ + "candidate_id": candidate_id, + "sequence_sha256": _sha256_text(sequence), + "sequence_length": len(sequence), + }) + return identities + + +def _synthesis_order_digest(panel_rows: list[dict[str, str]]) -> str: + """Hash the ordered candidate identity list, not vendor instructions.""" + lines = [] + for row in panel_rows: + candidate_id = row.get("candidate_id", "").strip() + sequence = row.get("sequence", "").strip().upper() + if candidate_id and sequence: + lines.append(f"{candidate_id},{sequence}") + return _sha256_text("\n".join(lines) + "\n") + + +def verify_batch_pack(zip_path: str | Path) -> dict[str, Any]: + """Verify chain-of-custody hashes inside a lab batch pack zip.""" + zip_p = Path(zip_path) + if not zip_p.exists(): + return {"status": "error", "error": f"Pack not found: {zip_path}"} + + with zipfile.ZipFile(zip_p) as zf: + names = set(zf.namelist()) + required = {"panel.csv", "chain_of_custody.json", "MANIFEST.json"} + missing = sorted(required - names) + if missing: + return {"status": "failed", "missing_files": missing} + + panel_bytes = zf.read("panel.csv") + custody = json.loads(zf.read("chain_of_custody.json").decode("utf-8")) + manifest = json.loads(zf.read("MANIFEST.json").decode("utf-8")) + + panel_hash_ok = _sha256_bytes(panel_bytes) == custody.get("panel_csv_sha256") + + panel_text = panel_bytes.decode("utf-8") + panel_rows = list(csv.DictReader(panel_text.splitlines())) + expected_identities = _candidate_identity_rows(panel_rows) + identities_ok = expected_identities == custody.get("candidate_identities", []) + order_ok = _synthesis_order_digest(panel_rows) == custody.get("synthesis_order_sha256") + + evidence_ok = True + evidence_hashes = custody.get("evidence_certificate_sha256", {}) + for archive_name, expected_hash in evidence_hashes.items(): + if archive_name not in names or _sha256_bytes(zf.read(archive_name)) != expected_hash: + evidence_ok = False + break + + ok = panel_hash_ok and identities_ok and order_ok and evidence_ok + return { + "status": "ok" if ok else "failed", + "pack": str(zip_p), + "panel_hash_ok": panel_hash_ok, + "candidate_identity_hashes_ok": identities_ok, + "synthesis_order_hash_ok": order_ok, + "evidence_hashes_ok": evidence_ok, + "candidate_count": len(expected_identities), + "manifest_candidate_count": manifest.get("candidate_count"), + } + + def build_batch_pack( panel_csv: str, evidence_dir: str, @@ -37,30 +122,44 @@ def build_batch_pack( if not evidence_p.exists(): return {"error": f"Evidence directory not found: {evidence_dir}"} + panel_rows = _read_panel_rows(panel_p) + candidate_identities = _candidate_identity_rows(panel_rows) + panel_data = panel_p.read_bytes() + panel_csv_sha256 = _sha256_bytes(panel_data) + synthesis_order_sha256 = _synthesis_order_digest(panel_rows) + manifest: dict[str, Any] = { "generated_at": datetime.now(timezone.utc).isoformat(), "panel_csv": panel_csv, + "panel_csv_sha256": panel_csv_sha256, + "synthesis_order_sha256": synthesis_order_sha256, + "candidate_count": len(candidate_identities), "evidence_count": 0, "included_protocols": [], "included_candidates": [], + "candidate_identities": candidate_identities, "data_return_template": None, "controls": controls or [], } with zipfile.ZipFile(out_p, "w", zipfile.ZIP_DEFLATED) as zf: # 1. Panel CSV - panel_data = panel_p.read_bytes() zf.writestr("panel.csv", panel_data) manifest["panel_csv_size"] = len(panel_data) # 2. Evidence certificates ev_count = 0 + evidence_hashes: dict[str, str] = {} for f in sorted(evidence_p.iterdir()): if f.suffix == ".json": - zf.write(str(f), f"evidence/{f.name}") + archive_name = f"evidence/{f.name}" + evidence_bytes = f.read_bytes() + zf.writestr(archive_name, evidence_bytes) + evidence_hashes[archive_name] = _sha256_bytes(evidence_bytes) ev_count += 1 manifest["included_candidates"].append(f.stem) manifest["evidence_count"] = ev_count + manifest["evidence_certificate_sha256"] = evidence_hashes # 3. Protocol references (as a README) protocols = protocol_docs or [ @@ -95,7 +194,6 @@ def build_batch_pack( # 5. Data return template (empty JSON per schema) if schema_p.exists(): - schema = json.loads(schema_p.read_text()) template: dict[str, Any] = { "result_id": "REPLACE_WITH_UUID", "candidate_id": "REPLACE_WITH_CANDIDATE_ID", @@ -114,6 +212,27 @@ def build_batch_pack( zf.writestr("data_return_template.json", json.dumps(template, indent=2)) manifest["data_return_template"] = str(schema_p) + # 6. Chain-of-custody identity hashes + custody = { + "chain_of_custody_version": "1.0", + "scope": ( + "Dry-lab identity check only. Hashes verify that candidate IDs, " + "sequences, panel ordering, and evidence certificate files did not drift." + ), + "not_evidence_of": [ + "biological activity", + "mammalian safety", + "synthesis success", + "clinical utility", + ], + "panel_csv_sha256": panel_csv_sha256, + "synthesis_order_sha256": synthesis_order_sha256, + "candidate_identities": candidate_identities, + "evidence_certificate_sha256": evidence_hashes, + } + zf.writestr("chain_of_custody.json", json.dumps(custody, indent=2)) + zf.writestr("MANIFEST.json", json.dumps(manifest, indent=2)) + # 6. Batch README readme = [ "# OpenAMP Foundry — Lab Batch Pack", @@ -129,6 +248,16 @@ def build_batch_pack( "- protocols/ — Assay protocol documents", "- controls_manifest.json — Positive/negative control sequences", "- data_return_template.json — Empty JSON template per lab_result.schema.json", + "- chain_of_custody.json — SHA-256 identity hashes for panel, sequences, and evidence files", + "- MANIFEST.json — Machine-readable archive manifest", + "", + "## Identity Verification", + "", + "Before using returned results, verify that panel and evidence files match:", + "", + " python scripts/build_lab_batch_pack.py --verify-pack ", + "", + "These hashes verify identity only. They are not evidence of activity, safety, synthesis success, or clinical utility.", "", "## Return Data", "", @@ -153,8 +282,14 @@ def main() -> int: parser.add_argument("--evidence-dir", default="outputs/evidence_wave0_5") parser.add_argument("--out", default="outputs/lab_batch_pack.zip") parser.add_argument("--manifest-out", default=None, help="JSON manifest path") + parser.add_argument("--verify-pack", default=None, help="Verify an existing lab batch pack zip") args = parser.parse_args() + if args.verify_pack: + result = verify_batch_pack(args.verify_pack) + print(json.dumps(result, indent=2)) + return 0 if result.get("status") == "ok" else 3 + manifest = build_batch_pack( panel_csv=args.panel_csv, evidence_dir=args.evidence_dir, diff --git a/tests/test_decision_log_schema.py b/tests/test_decision_log_schema.py index cbed4bc3..b5580591 100644 --- a/tests/test_decision_log_schema.py +++ b/tests/test_decision_log_schema.py @@ -51,7 +51,6 @@ def test_schema_decision_values(): def test_valid_decision_passes_validation(): - schema = _load_schema() example = { "entry_id": "DR-2026-07-06-001", "date": "2026-07-06", @@ -67,7 +66,6 @@ def test_valid_decision_passes_validation(): def test_missing_required_field_fails(): - schema = _load_schema() example = { "entry_id": "DR-001", "date": "2026-07-06", diff --git a/tests/test_docs_consistent.py b/tests/test_docs_consistent.py index 367607e8..d7070938 100644 --- a/tests/test_docs_consistent.py +++ b/tests/test_docs_consistent.py @@ -120,6 +120,9 @@ def test_virtual_assay_scope_records_uncertainty_certificate_contract(self): assert "evidence" in scope assert "certificates" in scope assert "uncertainty" in scope + assert "Current ablation evidence does not justify weighted ranking impact." in scope + assert "Weighted simulation remains blocked" in scope + assert "docs/SIMULATION_BENCHMARK.md" in scope def test_doc_metrics_snapshot_includes_ranking_policy(self): """Snapshot must encode the current ranking recommendation contract.""" @@ -207,9 +210,10 @@ def test_doc_wave05_historical_docs_do_not_pose_as_live_pending_state(self): assert "Historical baseline state only" in baseline_text assert "External predictor screen (all 60 shortlist) | COMPLETE" in scaffold_text assert "Wave 0.5 Gate W0.5-3 (activity consensus) | COMPLETE" in scaffold_text - assert "pre-registration" in prereg_text.lower() - assert "external" in prereg_text.lower() - assert "predictor" not in prereg_text or "predictor" in prereg_text + assert "Template / planning artifact. Not a wet-lab protocol." in prereg_text + assert "candidate IDs or safe sequence references" in prereg_text + assert "The external predictor portion has since been completed" in checklist_text + assert "External predictor review for Wave 0.5 was completed after this panel recommendation was first drafted." in wave1_text def test_doc_external_predictor_gate_distinguishes_generic_from_wave05(self): """Live docs must not imply the completed Wave 0.5 screen is still wholly pending.""" @@ -235,6 +239,27 @@ def test_doc_external_predictor_gate_distinguishes_generic_from_wave05(self): assert "Wave 0.5 complete; generic future-panel Gate 6 remains panel-specific" in roadmap_text assert "expert review" in expert_text.lower() assert "template" in expert_text.lower() + assert "replaces earlier panel-specific review drafts" in expert_text + assert "Full sequences should be included only when release review allows it." in expert_text + + def test_doc_lab_batch_pack_chain_of_custody_current(self): + """Loop 45 must stay marked as shipped with identity-hash boundaries.""" + loop_text = (DOCS_DIR / "50_LOOP_PLAN.md").read_text(encoding="utf-8") + roadmap_text = (DOCS_DIR / "ROADMAP.md").read_text(encoding="utf-8") + metrics_text = (DOCS_DIR / "METRICS_CURRENT.md").read_text(encoding="utf-8") + onboarding_text = (DOCS_DIR / "LAB_PARTNER_ONBOARDING.md").read_text( + encoding="utf-8" + ) + + assert "| 45 ✅ |" in loop_text + assert "chain_of_custody.json" in loop_text + assert "v0.5.65" in roadmap_text + assert "--verify-pack" in roadmap_text + assert "These hashes verify identity and archive integrity only." in roadmap_text + assert "New in v0.5.65" in metrics_text + assert "chain_of_custody.json" in onboarding_text + assert "does not verify synthesis, biological" in onboarding_text + assert "activity, safety, or experimental provenance after receipt" in onboarding_text class TestFeatureDecompositionDocsConsistent: diff --git a/tests/test_lab_batch_pack.py b/tests/test_lab_batch_pack.py index b3a5d9d8..d050d5fe 100644 --- a/tests/test_lab_batch_pack.py +++ b/tests/test_lab_batch_pack.py @@ -3,11 +3,10 @@ import json import subprocess import sys -import tempfile import zipfile from pathlib import Path -from scripts.build_lab_batch_pack import build_batch_pack +from scripts.build_lab_batch_pack import build_batch_pack, verify_batch_pack def _make_evidence(tmp: Path, candidate_id: str) -> Path: @@ -79,6 +78,58 @@ def test_build_pack_contains_data_return_template(tmp_path): assert "data_return_template.json" in z.namelist() +def test_build_pack_contains_chain_of_custody(tmp_path): + panel = _make_panel_csv(tmp_path) + ev_dir = _make_evidence(tmp_path, "C1") + _make_evidence(tmp_path, "C2") + out = tmp_path / "pack.zip" + manifest = build_batch_pack(panel_csv=str(panel), evidence_dir=str(ev_dir), out_zip=str(out)) + with zipfile.ZipFile(out) as z: + custody = json.loads(z.read("chain_of_custody.json")) + assert "MANIFEST.json" in z.namelist() + assert custody["panel_csv_sha256"] == manifest["panel_csv_sha256"] + assert custody["synthesis_order_sha256"] == manifest["synthesis_order_sha256"] + assert len(custody["candidate_identities"]) == 2 + assert custody["candidate_identities"][0]["candidate_id"] == "C1" + assert "sequence_sha256" in custody["candidate_identities"][0] + assert "biological activity" in custody["not_evidence_of"] + + +def test_verify_batch_pack_success(tmp_path): + panel = _make_panel_csv(tmp_path) + ev_dir = _make_evidence(tmp_path, "C1") + _make_evidence(tmp_path, "C2") + out = tmp_path / "pack.zip" + build_batch_pack(panel_csv=str(panel), evidence_dir=str(ev_dir), out_zip=str(out)) + result = verify_batch_pack(out) + assert result["status"] == "ok" + assert result["panel_hash_ok"] is True + assert result["candidate_identity_hashes_ok"] is True + assert result["synthesis_order_hash_ok"] is True + assert result["evidence_hashes_ok"] is True + + +def test_verify_batch_pack_detects_tampered_panel(tmp_path): + panel = _make_panel_csv(tmp_path) + ev_dir = _make_evidence(tmp_path, "C1") + out = tmp_path / "pack.zip" + build_batch_pack(panel_csv=str(panel), evidence_dir=str(ev_dir), out_zip=str(out)) + + tampered = tmp_path / "tampered.zip" + with zipfile.ZipFile(out) as src, zipfile.ZipFile(tampered, "w", zipfile.ZIP_DEFLATED) as dst: + for name in src.namelist(): + data = src.read(name) + if name == "panel.csv": + data = data.replace(b"C1,KKLFKKILKYL", b"C1,KKLFKKILKKK") + dst.writestr(name, data) + + result = verify_batch_pack(tampered) + assert result["status"] == "failed" + assert result["panel_hash_ok"] is False + assert result["candidate_identity_hashes_ok"] is False + assert result["synthesis_order_hash_ok"] is False + + def test_build_pack_contains_protocols(tmp_path): panel = _make_panel_csv(tmp_path) ev_dir = _make_evidence(tmp_path, "C1") @@ -142,3 +193,20 @@ def test_cli_manifest_output(tmp_path): assert manifest.exists() data = json.loads(manifest.read_text()) assert "evidence_count" in data + assert "panel_csv_sha256" in data + assert "synthesis_order_sha256" in data + + +def test_cli_verify_pack(tmp_path): + ev_dir = _make_evidence(tmp_path, "C1") + panel = _make_panel_csv(tmp_path) + out = tmp_path / "pack.zip" + build_batch_pack(panel_csv=str(panel), evidence_dir=str(ev_dir), out_zip=str(out)) + result = subprocess.run( + [sys.executable, "scripts/build_lab_batch_pack.py", "--verify-pack", str(out)], + capture_output=True, text=True, + env={"PYTHONPATH": "src"}, + ) + assert result.returncode == 0 + payload = json.loads(result.stdout) + assert payload["status"] == "ok" diff --git a/tests/test_validate_lab_data_return.py b/tests/test_validate_lab_data_return.py index e1329b54..23e4c963 100644 --- a/tests/test_validate_lab_data_return.py +++ b/tests/test_validate_lab_data_return.py @@ -6,8 +6,6 @@ import tempfile from pathlib import Path -import pytest - from scripts.validate_lab_data_return import validate_lab_results_dir diff --git a/tests/test_wave1_pass_fail.py b/tests/test_wave1_pass_fail.py index b6947053..72aab288 100644 --- a/tests/test_wave1_pass_fail.py +++ b/tests/test_wave1_pass_fail.py @@ -6,8 +6,6 @@ import tempfile from pathlib import Path -import pytest - from scripts.check_wave1_pass_fail import _check_criteria, _load_lab_results @@ -200,7 +198,7 @@ def test_cli_writes_output_json(tmp_path): (results_dir / "c1_mic.json").write_text(json.dumps(_make_result("C1", "MIC", 8))) (results_dir / "ctrl.json").write_text(json.dumps(_make_result("CTRL-001", "MIC", 16))) out = tmp_path / "verdict.json" - result = subprocess.run( + subprocess.run( [sys.executable, "scripts/check_wave1_pass_fail.py", "--results-dir", str(results_dir), "--out", str(out)],