From 9023809e23e7039ca4ce06e5bd93800cc479b295 Mon Sep 17 00:00:00 2001 From: j Date: Mon, 29 Jun 2026 00:50:17 +0700 Subject: [PATCH 1/2] =?UTF-8?q?test:=2012=20new=20CLI=20tests=20=E2=80=94?= =?UTF-8?q?=201304=20tests,=2099%=20coverage;=20docs=20sync=20PRs=20#93?= =?UTF-8?q?=E2=80=93#98=20(#99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test coverage: - generate-batch: missing seeds file error + successful CSV generation - batch-pack: full success path with JSON and markdown output - pilot-panel: ranked file not found + blank-line skipping + success path (lines 521–571) - novelty-check-broad: empty panel, missing refs, empty refs, empty sequence skip, CLOSE_RELATIVE classification and report output - diversity-check: family structural warnings (TRYPSIN_STABILITY, ≥4 sites × 3 members), redundancy warning section, optimal-cluster-representatives section (optimal ≠ minimal) CLI coverage: 86% → 99% (576 lines, 6 remaining structurally unreachable) Total coverage: 96% → 99% (2171 lines, 14 remaining) Test count: 1292 → 1304 Docs: - ROADMAP.md v0.7.x extended to include PRs #93–#99 - DISCOVERY_PREDICTION.md tracking line extended through PR #99, bottom-line tag updated --- docs/DISCOVERY_PREDICTION.md | 4 +- docs/ROADMAP.md | 6 + tests/test_cli.py | 245 +++++++++++++++++++++++++++++++++++ 3 files changed, 253 insertions(+), 2 deletions(-) diff --git a/docs/DISCOVERY_PREDICTION.md b/docs/DISCOVERY_PREDICTION.md index c7b38e28..465e6e6d 100644 --- a/docs/DISCOVERY_PREDICTION.md +++ b/docs/DISCOVERY_PREDICTION.md @@ -3,7 +3,7 @@ **Pipeline:** OpenAMP-Foundry v0.5.x **Date:** 2026-06-28 (updated 2026-06-28) **Status:** Pre-synthesis scientific assessment — for expert review before ordering -**Completed improvements:** Serum stability scoring (PR #31/#32), Family diversity cap (PR #31), Reference set expansion 44→73 sequences (PR #33), Net charge pH 7.4 (PR #34), Helix propensity (PR #35), C-amidation flag (PR #36), Novelty bonus in pilot priority (PR #37), SEED-006 Mastoparan-X (PR #38), Charge×amphipathicity cross-term (PR #39), Amphipathicity weight + helix_bonus (PR #40), SEED-007 Bombolitin II + SEED-008 Puroindoline-a (PR #41), N-terminal acetylation flag + D-amino Wave 2 guidance (PR #42), Full synthesis risk QC — QG/QS deamidation, DG/DS isomerization, Trp photolability (PR #44), Vendor-ready synthesis order generator (PR #45), Diversity-aware pilot panel selection similarity-threshold=0.75 (PR #46), Selectivity proxy + HIGH_CYTOTOX_RISK flag — charge/GRAVY-based mammalian cytotoxicity risk detector (PR #47), selectivity_proxy routed into pilot_priority formula — low-cytotox-risk candidates gently demoted (PR #48), Elastase resistance (HNE 3-protease stability model) + aggregation propensity scoring (synthesis feasibility penalty) (PR #49), Aggregation-safe mutation generation + balanced K/R charge variants + SynthQC continuous aggregation score (PR #50), Proline synthesis penalty + helix bonus enhancement — AUROC 0.8138 → 0.8164 (PR #51), Safety pH74 charge consistency + stronger cytotoxicity penalty — SEED-004 net selectivity swing increased 0.035→0.055 (PR #53), AUPRC metric + SEED-009 (Bac2A proline-rich) + SEED-010 (KR-12 LL-37 fragment) (PR #58), ATCC strain corrections + preregistration assay protocol + negative result schema (PR #60), **CRITICAL FIX: SEED-008 Trp-rich scaffold reinstated in synthesis pool (was excluded by Boman W scale artifact); all 10 seeds generated; SEED-007/009 newly included; SEED-004 correctly excluded by safety gate; 6 mechanism-diverse scaffolds confirmed in synthesis pool** (PR #61), Phase3.yaml AUROC documented (0.7936→0.7890 after PR #65) (PR #63), Stale threshold/reference-count docs corrected (PR #64), **Trp-weighted aromatic bonus (1.5× Trp vs Phe/Tyr; Wimley-White interfacial mechanism); safety abs() bug fix (negative charge does not cause hemolysis); Eisenberg scale comment fix; pool SEED-006: 11→10, SEED-009: 18→19; AUROC 0.8164→0.8086** (PR #65), **Duplicate validation entry removed (REF-GIG-001 = magainin-2 = REF-MAG-001 counted twice); corrected AUROC 0.8086→0.8047 (pipeline), 0.7890→0.7846 (phase3); 43-AMP + 44-background set (n=87)** (PR #66), **Windowed hydrophobic moment (window=11, Eisenberg standard) replaces full-sequence mu_h for activity scoring; anionic guard (charge_density_ph74 < 0.0 → score 0.0); Python eager-eval bug fix in feature dict fallback; 23 new tests; AUROC 0.8047→0.8348 (pipeline), 0.7846→0.8126 (phase3)** (PR #70), Rotation-invariant helix-wheel face analysis — identifies non-helical mechanism class pre-synthesis (PR #71), **Face segregation bonus (helix_wheel_amphipathic_score × 0.05) in activity_likeness_score; max_disagreement raised 0.40→0.45 for SEED-008 Trp-rich class; AUROC 0.8348→0.8420, bootstrap CI₉₅: 0.76–0.91** (PR #72), Lab_results dir loading + toxicity flag branch coverage (PR #80), **Wet-lab probability analysis (WET_LAB_PROBABILITY.md) + gold-standard calibration + ASSAY_PREREGISTRATION.md with MRSA USA300 and serum stability gate** (PR #81/#83), SEED-007 helix-wheel length fix 18 AA→11 AA (PR #84), CLI integration tests — gold-standard, external-predict, pilot-confident, diversity-check; 7-seed pool documentation sync (PRs #74–#79/#82/#85), **novelty-check-broad CLI + 72-AMP broad novelty check: 16/20 NOVEL, 3 KNOWN_VARIANT (SEED-003 variants at 81.8% similarity to RRWQWRMKKLG = tachyplesin-like Tam 2002 J Biol Chem), 1 CLOSE_RELATIVE; docs/NOVELTY_BROAD_CHECK.md generated** (PR #86), **WET_LAB_PROBABILITY.md updated: SEED-003 mechanism corrected to RRWQWRMKKLG-class, P(MIC≤16) raised 50–65%→60–75%, novelty for publication lowered 25–40%→10–20%; composite P(≥1 active MIC≤16) = 99.95% (P(zero) = 0.000508)** (PR #87), **EXPERT_REVIEW_PACK.md overhauled: v0.5.x state, 7-seed mechanism table, full 20-candidate novelty table, SEED-008/009 mechanism notes, Melittin safety blind spot** (PR #88), WET_LAB_HANDOFF SEED-003 Special Notes: KNOWN_VARIANT classification (wet-lab SAR value retained), Met oxidation risk, aggregation caution, C-terminal amidation recommendation (PR #89), PROLINE_RICH_INTRACELLULAR flag in presynth QC — RPMI-1640 parallel assay recommendation for ≥25% Pro sequences; all four SEED-009 pilot variants flagged (Krizsan et al. 2014 Angew Chem Int Ed 53:12236) (PR #90), amphipathic_score (helix_wheel_amphipathic_score) and charge_ph74 (net_charge_ph74 at pH 7.4) added to pilot panel CSV output for wet-lab within-family prioritization (PR #91), Doc sync: Krizsan citation 53:14546→53:12236, RPMI-1640 note added to WET_LAB_HANDOFF SEED-009 section, RISK_REVIEW.md PROLINE_RICH_INTRACELLULAR note, make help target (PR #92), ROADMAP v0.7.x section documents PRs #73–#92 wet-lab readiness package (PR #93), 5 tests covering LONG_PEPTIDE flag, no-flags checklist branch, and length-filter failure mode — 1292 tests passing 96% coverage (PR #94), His pKa unified 6.0→6.5 (Bjellqvist 1993) across presynth_check.py and physchem.py, METHODS.md safety abs() correction, ensemble.py docstring with config weight references (PR #95) +**Completed improvements:** Serum stability scoring (PR #31/#32), Family diversity cap (PR #31), Reference set expansion 44→73 sequences (PR #33), Net charge pH 7.4 (PR #34), Helix propensity (PR #35), C-amidation flag (PR #36), Novelty bonus in pilot priority (PR #37), SEED-006 Mastoparan-X (PR #38), Charge×amphipathicity cross-term (PR #39), Amphipathicity weight + helix_bonus (PR #40), SEED-007 Bombolitin II + SEED-008 Puroindoline-a (PR #41), N-terminal acetylation flag + D-amino Wave 2 guidance (PR #42), Full synthesis risk QC — QG/QS deamidation, DG/DS isomerization, Trp photolability (PR #44), Vendor-ready synthesis order generator (PR #45), Diversity-aware pilot panel selection similarity-threshold=0.75 (PR #46), Selectivity proxy + HIGH_CYTOTOX_RISK flag — charge/GRAVY-based mammalian cytotoxicity risk detector (PR #47), selectivity_proxy routed into pilot_priority formula — low-cytotox-risk candidates gently demoted (PR #48), Elastase resistance (HNE 3-protease stability model) + aggregation propensity scoring (synthesis feasibility penalty) (PR #49), Aggregation-safe mutation generation + balanced K/R charge variants + SynthQC continuous aggregation score (PR #50), Proline synthesis penalty + helix bonus enhancement — AUROC 0.8138 → 0.8164 (PR #51), Safety pH74 charge consistency + stronger cytotoxicity penalty — SEED-004 net selectivity swing increased 0.035→0.055 (PR #53), AUPRC metric + SEED-009 (Bac2A proline-rich) + SEED-010 (KR-12 LL-37 fragment) (PR #58), ATCC strain corrections + preregistration assay protocol + negative result schema (PR #60), **CRITICAL FIX: SEED-008 Trp-rich scaffold reinstated in synthesis pool (was excluded by Boman W scale artifact); all 10 seeds generated; SEED-007/009 newly included; SEED-004 correctly excluded by safety gate; 6 mechanism-diverse scaffolds confirmed in synthesis pool** (PR #61), Phase3.yaml AUROC documented (0.7936→0.7890 after PR #65) (PR #63), Stale threshold/reference-count docs corrected (PR #64), **Trp-weighted aromatic bonus (1.5× Trp vs Phe/Tyr; Wimley-White interfacial mechanism); safety abs() bug fix (negative charge does not cause hemolysis); Eisenberg scale comment fix; pool SEED-006: 11→10, SEED-009: 18→19; AUROC 0.8164→0.8086** (PR #65), **Duplicate validation entry removed (REF-GIG-001 = magainin-2 = REF-MAG-001 counted twice); corrected AUROC 0.8086→0.8047 (pipeline), 0.7890→0.7846 (phase3); 43-AMP + 44-background set (n=87)** (PR #66), **Windowed hydrophobic moment (window=11, Eisenberg standard) replaces full-sequence mu_h for activity scoring; anionic guard (charge_density_ph74 < 0.0 → score 0.0); Python eager-eval bug fix in feature dict fallback; 23 new tests; AUROC 0.8047→0.8348 (pipeline), 0.7846→0.8126 (phase3)** (PR #70), Rotation-invariant helix-wheel face analysis — identifies non-helical mechanism class pre-synthesis (PR #71), **Face segregation bonus (helix_wheel_amphipathic_score × 0.05) in activity_likeness_score; max_disagreement raised 0.40→0.45 for SEED-008 Trp-rich class; AUROC 0.8348→0.8420, bootstrap CI₉₅: 0.76–0.91** (PR #72), Lab_results dir loading + toxicity flag branch coverage (PR #80), **Wet-lab probability analysis (WET_LAB_PROBABILITY.md) + gold-standard calibration + ASSAY_PREREGISTRATION.md with MRSA USA300 and serum stability gate** (PR #81/#83), SEED-007 helix-wheel length fix 18 AA→11 AA (PR #84), CLI integration tests — gold-standard, external-predict, pilot-confident, diversity-check; 7-seed pool documentation sync (PRs #74–#79/#82/#85), **novelty-check-broad CLI + 72-AMP broad novelty check: 16/20 NOVEL, 3 KNOWN_VARIANT (SEED-003 variants at 81.8% similarity to RRWQWRMKKLG = tachyplesin-like Tam 2002 J Biol Chem), 1 CLOSE_RELATIVE; docs/NOVELTY_BROAD_CHECK.md generated** (PR #86), **WET_LAB_PROBABILITY.md updated: SEED-003 mechanism corrected to RRWQWRMKKLG-class, P(MIC≤16) raised 50–65%→60–75%, novelty for publication lowered 25–40%→10–20%; composite P(≥1 active MIC≤16) = 99.95% (P(zero) = 0.000508)** (PR #87), **EXPERT_REVIEW_PACK.md overhauled: v0.5.x state, 7-seed mechanism table, full 20-candidate novelty table, SEED-008/009 mechanism notes, Melittin safety blind spot** (PR #88), WET_LAB_HANDOFF SEED-003 Special Notes: KNOWN_VARIANT classification (wet-lab SAR value retained), Met oxidation risk, aggregation caution, C-terminal amidation recommendation (PR #89), PROLINE_RICH_INTRACELLULAR flag in presynth QC — RPMI-1640 parallel assay recommendation for ≥25% Pro sequences; all four SEED-009 pilot variants flagged (Krizsan et al. 2014 Angew Chem Int Ed 53:12236) (PR #90), amphipathic_score (helix_wheel_amphipathic_score) and charge_ph74 (net_charge_ph74 at pH 7.4) added to pilot panel CSV output for wet-lab within-family prioritization (PR #91), Doc sync: Krizsan citation 53:14546→53:12236, RPMI-1640 note added to WET_LAB_HANDOFF SEED-009 section, RISK_REVIEW.md PROLINE_RICH_INTRACELLULAR note, make help target (PR #92), ROADMAP v0.7.x section documents PRs #73–#92 wet-lab readiness package (PR #93), 5 tests covering LONG_PEPTIDE flag, no-flags checklist branch, and length-filter failure mode — 1292 tests passing 96% coverage (PR #94), His pKa unified 6.0→6.5 (Bjellqvist 1993) across presynth_check.py and physchem.py, METHODS.md safety abs() correction, ensemble.py docstring with config weight references (PR #95), Discovery prediction tracked through PRs #92–#95 (PR #96), WET_LAB_HANDOFF version tag + PROLINE_RICH_INTRACELLULAR QC flag table row + amphipathic_score/charge_ph74 score reference rows (PR #97), EXPERT_REVIEW_PACK new CSV columns note (PR #98), 12 new CLI tests covering generate-batch, batch-pack, pilot-panel success path, batch-pack markdown, redundancy + optimal-cluster-rep sections, novelty error paths, family structural warnings — 1304 tests, 99% coverage; ROADMAP extended through PRs #93–#98 (PR #99) --- @@ -14,7 +14,7 @@ panel nominated by the computational pipeline. It covers the likelihood of wet-l each stage, identifies the key risk factors in the current nominee set, and lists concrete improvements already implemented or recommended. -**Bottom line (post-PR #95, 7 scaffold families, AUROC 0.8420):** The pilot panel has a +**Bottom line (post-PR #99, 7 scaffold families, AUROC 0.8420):** The pilot panel has a ~99.95% probability of yielding ≥1 candidate with MIC ≤ 16 µg/mL (P(zero active) = 0.000508 across 7 families; per-family breakdown in [`docs/WET_LAB_PROBABILITY.md`](WET_LAB_PROBABILITY.md)) and **~10–20%** probability of generating "breaking news" publication material (up from 5–12% diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index d3f37f2b..8231b862 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -79,6 +79,12 @@ Implemented during the pre-wet-lab improvement loop (PRs #31–#54): - `PROLINE_RICH_INTRACELLULAR` flag in presynth QC — RPMI-1640 parallel assay recommendation for ≥25% Pro; all 4 SEED-009 pilot variants flagged (Krizsan et al. 2014 Angew Chem Int Ed 53:12236) (PR #90) - `amphipathic_score` + `charge_ph74` added to pilot panel CSV for wet-lab within-family prioritization (PR #91) - Doc sync: Krizsan citation corrected (53:14546 → 53:12236) across ASSAY_PREREGISTRATION + EXPERT_REVIEW_PACK; `make help` target added (PR #92) +- ROADMAP v0.7.x section added documenting all PRs #73–#92 (PR #93) +- Test coverage: 5 new tests covering LONG_PEPTIDE flag, no-flags checklist, and length-filter failure mode (PR #94); 12 new CLI tests for generate-batch, batch-pack, pilot-panel success, batch-pack markdown, redundancy + optimal-cluster-rep sections, novelty error paths, family structural warnings — 1304 tests, 99% coverage (PR #99) +- His pKa unified 6.0 → 6.5 (Bjellqvist 1993) across presynth_check.py and physchem.py; METHODS.md abs() correction; ensemble.py weight docstring (PR #95) +- Discovery prediction tracked through PRs #92–#95 in DISCOVERY_PREDICTION.md (PR #96) +- WET_LAB_HANDOFF.md: version tag corrected, PROLINE_RICH_INTRACELLULAR QC flag table row, `amphipathic_score`/`charge_ph74` score reference table rows added (PR #97) +- EXPERT_REVIEW_PACK.md: note added on new CSV columns for within-family prioritization (PR #98) ## v1.0 — Validated dry-lab-to-wet-lab loop diff --git a/tests/test_cli.py b/tests/test_cli.py index b2f90f0f..a9da9a70 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -391,6 +391,168 @@ def test_diversity_check_creates_report(self, tmp_path): assert "SEED-009_VAR_033" in text assert "SEED-007_VAR_009" in text + def test_diversity_check_family_structural_warning(self, tmp_path): + # Three variants from the same seed, all with ≥4 interior trypsin sites → TRYPSIN_STABILITY family warning + panel = tmp_path / "fam_panel.csv" + panel.write_text( + PANEL_CSV_HEADER + + "1,SEED-001_VAR_001,KWKLFKKIGAVLKVL,15,SEED-001,0.75,0.70,0.60,0.10,0.90,0.85,0.70,0.60,1.0,0.80\n" + + "2,SEED-001_VAR_002,KWKLFKRIGAVLKVL,15,SEED-001,0.74,0.70,0.60,0.10,0.90,0.85,0.70,0.60,1.0,0.79\n" + + "3,SEED-001_VAR_003,KWKLFRRIGAVLKVL,15,SEED-001,0.73,0.70,0.60,0.10,0.90,0.85,0.70,0.60,1.0,0.78\n", + encoding="utf-8", + ) + out = tmp_path / "diversity.md" + rc = main(["diversity-check", "--panel-csv", str(panel), "--out", str(out)]) + assert rc == 0 + text = out.read_text(encoding="utf-8") + assert "Family-Level Structural" in text or "SEED-001" in text + + def test_diversity_check_redundancy_and_optimal_diff_sections(self, tmp_path): + # Two candidates nearly identical (sim=0.889>0.60) → same cluster. + # Lower pilot_rank wins in minimal but lower ensemble wins in optimal. + # This triggers both the redundancy warning section (n_redundant>0) + # and the optimal-cluster-representatives section (optimal_ids != minimal_ids). + panel = tmp_path / "similar_panel.csv" + panel.write_text( + PANEL_CSV_HEADER + + "1,CAND-A,KWKLFKKIG,9,SEED-001,0.70,0.60,0.55,0.10,0.90,0.85,0.70,0.60,1.0,0.80\n" + + "2,CAND-B,KWKLFKKIA,9,SEED-001,0.90,0.75,0.60,0.10,0.90,0.85,0.70,0.60,1.0,0.82\n", + encoding="utf-8", + ) + out = tmp_path / "diversity.md" + rc = main(["diversity-check", "--panel-csv", str(panel), "--out", str(out)]) + assert rc == 0 + text = out.read_text(encoding="utf-8") + # Redundancy section appears when n_redundant > 0 + assert "Redundancy Warning" in text or "redundant" in text.lower() + # Optimal section appears when optimal picks differ from minimal + assert "Optimal Cluster" in text + + +def _write_ranked_jsonl(tmp_path, n: int = 3) -> str: + """Minimal ranked JSONL with n selected candidates from distinct seeds.""" + rows = [] + for i in range(n): + seed = f"SEED-00{i + 1}" + rows.append({ + "candidate_id": f"{seed}_VAR_001", + "sequence": f"KWKLFK{'A' * i}LG", + "source": f"template_mutation_from_{seed}", + "selected": True, + "valid": True, + "scores": { + "ensemble": round(0.80 - i * 0.05, 3), + "activity": 0.70, + "safety": 0.90, + "synthesis": 0.85, + "novelty": 0.70, + "disagreement": 0.10, + "serum_stability": 0.60, + "boman_activity": 0.60, + "selectivity_proxy": 1.0, + }, + "features": {}, + "selection_reason": [], + "known_failure_modes": [], + "nearest_reference": { + "candidate_id": "REF-001", + "sequence": "KWKLFK", + "similarity": 0.5, + "source": "demo", + }, + }) + p = tmp_path / "ranked.jsonl" + p.write_text("\n".join(json.dumps(r) for r in rows) + "\n", encoding="utf-8") + return str(p) + + +def test_pilot_panel_ranked_file_not_found_returns_error(tmp_path, capsys): + rc = main([ + "pilot-panel", + "--ranked", str(tmp_path / "nonexistent.jsonl"), + "--out-csv", str(tmp_path / "panel.csv"), + ]) + assert rc == 1 + data = json.loads(capsys.readouterr().out) + assert data["status"] == "error" + assert "not found" in data["message"] + + +def test_pilot_panel_blank_lines_skipped_and_output_produced(tmp_path, capsys): + ranked = _write_ranked_jsonl(tmp_path) + # Prepend/append blank lines to trigger the blank-line-skip branch + content = open(ranked, encoding="utf-8").read() + open(ranked, "w", encoding="utf-8").write("\n" + content + "\n\n") + + out_csv = str(tmp_path / "panel.csv") + out_md = str(tmp_path / "panel.md") + rc = main(["pilot-panel", "--ranked", ranked, "--out-csv", out_csv, "--out-md", out_md, "--n", "2"]) + assert rc == 0 + data = json.loads(capsys.readouterr().out) + assert data["status"] == "ok" + assert data["n_panel"] <= 2 + assert "No antimicrobial activity" in data["disclaimer"] + assert (tmp_path / "panel.md").exists() + + +def test_generate_batch_missing_seeds_file_returns_error(tmp_path, capsys): + rc = main([ + "generate-batch", + "--seeds", str(tmp_path / "nonexistent.csv"), + "--out", str(tmp_path / "pool.csv"), + ]) + assert rc == 1 + data = json.loads(capsys.readouterr().out) + assert data["status"] == "error" + assert "Seeds file not found" in data["message"] + + +def test_generate_batch_creates_candidate_pool(tmp_path, capsys): + seeds = tmp_path / "seeds.csv" + seeds.write_text( + "id,sequence,source\nSEED-001,GIGKFLHSAGKFGKAFVGEIMKS,test\n", + encoding="utf-8", + ) + out = str(tmp_path / "pool.csv") + rc = main([ + "generate-batch", + "--seeds", str(seeds), + "--out", out, + "--n-double", "3", + "--n-charge", "2", + ]) + assert rc == 0 + data = json.loads(capsys.readouterr().out) + assert data["status"] == "ok" + assert data["n_seeds"] == 1 + assert data["n_candidates_generated"] > 0 + import csv as _csv + rows = list(_csv.DictReader(open(out, encoding="utf-8"))) + assert len(rows) == data["n_candidates_generated"] + + +def test_batch_pack_creates_json_output(tmp_path, capsys): + ranked = str(tmp_path / "ranked.jsonl") + main([ + "rank", + "--candidates", "examples/sequences/demo_candidates.csv", + "--references", "examples/known_reference/demo_known_amps.csv", + "--out", ranked, + ]) + capsys.readouterr() # discard rank stdout + + out_json = str(tmp_path / "batch.json") + out_md = str(tmp_path / "batch.md") + rc = main(["batch-pack", "--ranked", ranked, "--out-json", out_json, "--out-md", out_md]) + assert rc == 0 + data = json.loads(capsys.readouterr().out) + assert data["status"] == "ok" + assert "n_selected" in data + import json as _json + pack = _json.loads(open(out_json, encoding="utf-8").read()) + assert "summary" in pack + assert (tmp_path / "batch.md").exists() + class TestNoveltyCheckBroad: def test_novelty_check_broad_returns_zero(self, tmp_path, capsys): @@ -478,3 +640,86 @@ def test_novelty_check_broad_inverted_thresholds_returns_error(self, tmp_path, c data = json.loads(captured.out) assert data["status"] == "error" assert "threshold-close" in data["message"] + + def test_novelty_check_broad_empty_panel_csv_returns_error(self, tmp_path, capsys): + empty_panel = tmp_path / "empty.csv" + empty_panel.write_text(PANEL_CSV_HEADER, encoding="utf-8") # header only, no rows + rc = main([ + "novelty-check-broad", + "--panel-csv", str(empty_panel), + "--out", str(tmp_path / "out.md"), + ]) + assert rc == 1 + data = json.loads(capsys.readouterr().out) + assert data["status"] == "error" + assert "empty" in data["message"] + + def test_novelty_check_broad_missing_references_csv_returns_error(self, tmp_path, capsys): + panel_csv = _write_panel(tmp_path) + rc = main([ + "novelty-check-broad", + "--panel-csv", panel_csv, + "--references-csv", str(tmp_path / "nonexistent_refs.csv"), + "--out", str(tmp_path / "out.md"), + ]) + assert rc == 1 + data = json.loads(capsys.readouterr().out) + assert data["status"] == "error" + assert "not found" in data["message"] + + def test_novelty_check_broad_empty_references_csv_returns_error(self, tmp_path, capsys): + panel_csv = _write_panel(tmp_path) + refs = tmp_path / "refs.csv" + refs.write_text("id,sequence,family,reference\n", encoding="utf-8") # header only + rc = main([ + "novelty-check-broad", + "--panel-csv", panel_csv, + "--references-csv", str(refs), + "--out", str(tmp_path / "out.md"), + ]) + assert rc == 1 + data = json.loads(capsys.readouterr().out) + assert data["status"] == "error" + assert "no valid sequences" in data["message"] + + def test_novelty_check_broad_empty_sequence_row_is_skipped(self, tmp_path, capsys): + panel = tmp_path / "panel.csv" + panel.write_text( + PANEL_CSV_HEADER + + PANEL_CSV_ROW1 + + "3,EMPTY-001,,0,SEED-009,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0\n", + encoding="utf-8", + ) + rc = main([ + "novelty-check-broad", + "--panel-csv", str(panel), + "--out", str(tmp_path / "out.md"), + ]) + assert rc == 0 + data = json.loads(capsys.readouterr().out) + # Only the row with a sequence is scored; the empty row is skipped + assert data["n_candidates"] == 1 + + def test_novelty_check_broad_close_relative_classification_and_report(self, tmp_path, capsys): + # Reference ~66.7% similar to SEED-009_VAR_033 (RRLPRPGYMPRP): 4 substitutions + # sim = 1 - 4/12 = 0.667 → CLOSE_RELATIVE (0.50 ≤ sim < 0.70) + ref_csv = tmp_path / "refs.csv" + ref_csv.write_text( + "id,sequence,family,reference\n" + "REF-CLOSE,RRLPAAGYMAAP,test_family,test_ref\n", + encoding="utf-8", + ) + panel_csv = _write_panel(tmp_path, two_rows=False) # SEED-009_VAR_033 only + rc = main([ + "novelty-check-broad", + "--panel-csv", panel_csv, + "--references-csv", str(ref_csv), + "--out", str(tmp_path / "out.md"), + ]) + assert rc == 0 + data = json.loads(capsys.readouterr().out) + assert data["status"] == "ok" + assert data["n_close_relative"] == 1 + assert data["n_novel"] == 0 + report = (tmp_path / "out.md").read_text(encoding="utf-8") + assert "CLOSE_RELATIVE" in report From 0d2568cf02e2d129c64efd4fb61d327b609d670d Mon Sep 17 00:00:00 2001 From: j Date: Mon, 29 Jun 2026 00:57:57 +0700 Subject: [PATCH 2/2] fix: tighten test assertions, close file handles, remove inline re-imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test_diversity_check_family_structural_warning: remove vacuous OR fallback; assert 'Family-Level Structural' directly (reviewer HIGH finding) - test_pilot_panel_blank_lines: n_panel <= 2 → == 2, deterministic with --n 2 and 3 distinct-seed candidates (reviewer MEDIUM finding) - open() calls → context managers / Path.write_text throughout (reviewer LOW finding: Windows-safe file handling) --- tests/test_cli.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index a9da9a70..eee6fac6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -405,7 +405,7 @@ def test_diversity_check_family_structural_warning(self, tmp_path): rc = main(["diversity-check", "--panel-csv", str(panel), "--out", str(out)]) assert rc == 0 text = out.read_text(encoding="utf-8") - assert "Family-Level Structural" in text or "SEED-001" in text + assert "Family-Level Structural" in text def test_diversity_check_redundancy_and_optimal_diff_sections(self, tmp_path): # Two candidates nearly identical (sim=0.889>0.60) → same cluster. @@ -481,8 +481,9 @@ def test_pilot_panel_ranked_file_not_found_returns_error(tmp_path, capsys): def test_pilot_panel_blank_lines_skipped_and_output_produced(tmp_path, capsys): ranked = _write_ranked_jsonl(tmp_path) # Prepend/append blank lines to trigger the blank-line-skip branch - content = open(ranked, encoding="utf-8").read() - open(ranked, "w", encoding="utf-8").write("\n" + content + "\n\n") + from pathlib import Path as _Path + content = _Path(ranked).read_text(encoding="utf-8") + _Path(ranked).write_text("\n" + content + "\n\n", encoding="utf-8") out_csv = str(tmp_path / "panel.csv") out_md = str(tmp_path / "panel.md") @@ -490,7 +491,7 @@ def test_pilot_panel_blank_lines_skipped_and_output_produced(tmp_path, capsys): assert rc == 0 data = json.loads(capsys.readouterr().out) assert data["status"] == "ok" - assert data["n_panel"] <= 2 + assert data["n_panel"] == 2 assert "No antimicrobial activity" in data["disclaimer"] assert (tmp_path / "panel.md").exists() @@ -527,7 +528,8 @@ def test_generate_batch_creates_candidate_pool(tmp_path, capsys): assert data["n_seeds"] == 1 assert data["n_candidates_generated"] > 0 import csv as _csv - rows = list(_csv.DictReader(open(out, encoding="utf-8"))) + with open(out, encoding="utf-8") as fh: + rows = list(_csv.DictReader(fh)) assert len(rows) == data["n_candidates_generated"] @@ -548,8 +550,8 @@ def test_batch_pack_creates_json_output(tmp_path, capsys): data = json.loads(capsys.readouterr().out) assert data["status"] == "ok" assert "n_selected" in data - import json as _json - pack = _json.loads(open(out_json, encoding="utf-8").read()) + with open(out_json, encoding="utf-8") as fh: + pack = json.loads(fh.read()) assert "summary" in pack assert (tmp_path / "batch.md").exists()