diff --git a/tests/test_batch_pack.py b/tests/test_batch_pack.py index 9dd21ab2..632fc07e 100644 --- a/tests/test_batch_pack.py +++ b/tests/test_batch_pack.py @@ -213,6 +213,37 @@ def test_risk_thresholds_present(self): assert "risk_thresholds" in report assert "hydrophobic_fraction_high" in report["risk_thresholds"] + def test_high_charge_density_flagged(self): + cand = [_make_candidate("HIGH-CHG", "KKKKKKKKK")] + cand[0]["features"]["charge_density"] = 0.70 + report = toxicity_hemolysis_risk_report(cand) + assert "high_charge_density" in report["candidates"][0]["risk_flags"] + + def test_high_cysteine_fraction_flagged(self): + cand = [_make_candidate("HIGH-CYS", "CCCCCKKKK")] + cand[0]["features"]["cysteine_fraction"] = 0.55 + report = toxicity_hemolysis_risk_report(cand) + assert "high_cysteine_fraction" in report["candidates"][0]["risk_flags"] + + def test_long_repeat_run_flagged(self): + cand = [_make_candidate("REPEAT", "KKKKKKRRL")] + cand[0]["features"]["longest_repeat_run"] = 6 + report = toxicity_hemolysis_risk_report(cand) + assert "long_repeat_run" in report["candidates"][0]["risk_flags"] + + def test_length_exceeds_35aa_flagged(self): + long_seq = "KWKLFKKIGAVLKVLKWKLFKKIGAVLKVLKWKLFK" # 36 AA (> 35 threshold) + cand = [_make_candidate("LONG", long_seq)] + report = toxicity_hemolysis_risk_report(cand) + assert "length_exceeds_35aa" in report["candidates"][0]["risk_flags"] + + def test_n_with_risk_flags_counts_correctly(self): + clean = _make_candidate("CLEAN", "KWKLFKKIGAVLKVL") + risky = _make_candidate("RISKY", "LLLLLLLLLLL") + risky["features"]["hydrophobic_fraction"] = 0.90 + report = toxicity_hemolysis_risk_report([clean, risky]) + assert report["n_with_risk_flags"] == 1 + class TestSynthesisFeasibilityReport: def test_report_type_field(self): @@ -382,3 +413,19 @@ def test_markdown_requires_human_review(self, ranked_jsonl, tmp_path): write_batch_pack_markdown(pack, md_path) content = md_path.read_text() assert "Human Review" in content or "human expert" in content.lower() + + def test_markdown_truncates_clusters_beyond_20(self, tmp_path): + _CANONICAL = list("ACDEFGHIKLMNPQRSTVWY") + rows = [] + for i, aa in enumerate(_CANONICAL): + rows.append(_make_candidate(f"CAND-{i:03d}", aa * 10, novelty=0.5 + i * 0.001)) + rows.append(_make_candidate("CAND-020", "KWKLFKKIGAVLKVLKWKLFK", novelty=0.6)) + jsonl_path = tmp_path / "big.jsonl" + with open(jsonl_path, "w") as f: + for r in rows: + f.write(json.dumps(r) + "\n") + pack = generate_batch_pack(jsonl_path) + md_path = tmp_path / "big.md" + write_batch_pack_markdown(pack, md_path) + content = md_path.read_text() + assert "more clusters" in content diff --git a/tests/test_lab_results.py b/tests/test_lab_results.py index eb8f95d3..3fd2d885 100644 --- a/tests/test_lab_results.py +++ b/tests/test_lab_results.py @@ -5,16 +5,19 @@ - Invalid lab results are rejected with useful errors - summarise_lab_results produces correct aggregates - candidate_result_map groups correctly + - load_lab_results_dir loads all valid files, skips invalid, returns sorted """ from __future__ import annotations import json +import warnings import pytest from openamp_foundry.data.lab_results import ( candidate_result_map, load_lab_result, + load_lab_results_dir, summarise_lab_results, ) @@ -172,3 +175,71 @@ def test_unknown_candidate_not_in_map(self): def test_empty_results_gives_empty_map(self): assert candidate_result_map([]) == {} + + +class TestLoadLabResultsDir: + def test_empty_directory_returns_empty_list(self, tmp_path): + results = load_lab_results_dir(tmp_path) + assert results == [] + + def test_loads_all_valid_files(self, results_dir): + results = load_lab_results_dir(results_dir) + assert len(results) == 3 + + def test_sorted_by_assay_date(self, tmp_path): + date_map = {0: "2026-07-03", 1: "2026-07-01", 2: "2026-07-02"} + for i in range(3): + result = _valid_result( + result_id=f"RES-{i:03d}", + candidate_id=f"CAND-{i:03d}", + assay_date=date_map[i], + ) + (tmp_path / f"RES-{i:03d}.json").write_text(json.dumps(result)) + results = load_lab_results_dir(tmp_path) + dates = [r["assay_date"] for r in results] + assert dates == sorted(dates) + assert dates[0] == "2026-07-01" + + def test_sorted_by_result_id_within_same_date(self, tmp_path): + for i in [3, 1, 2]: + result = _valid_result( + result_id=f"RES-{i:03d}", + candidate_id=f"CAND-{i:03d}", + assay_date="2026-07-10", + ) + (tmp_path / f"RES-{i:03d}.json").write_text(json.dumps(result)) + results = load_lab_results_dir(tmp_path) + ids = [r["result_id"] for r in results] + assert ids == sorted(ids) + + def test_skips_invalid_json_file_with_warning(self, tmp_path): + valid = _valid_result(result_id="GOOD-001", candidate_id="CAND-A", assay_date="2026-07-01") + (tmp_path / "good.json").write_text(json.dumps(valid)) + (tmp_path / "bad.json").write_text("this is not valid json {{{") + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + results = load_lab_results_dir(tmp_path) + assert len(results) == 1 + assert results[0]["result_id"] == "GOOD-001" + assert any("bad.json" in str(w.message) or "Skipped" in str(w.message) for w in caught) + + def test_skips_schema_invalid_file_with_warning(self, tmp_path): + valid = _valid_result(result_id="GOOD-001", candidate_id="CAND-A", assay_date="2026-07-01") + invalid = _valid_result() + del invalid["candidate_id"] + (tmp_path / "good.json").write_text(json.dumps(valid)) + (tmp_path / "invalid_schema.json").write_text(json.dumps(invalid)) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + results = load_lab_results_dir(tmp_path) + assert len(results) == 1 + assert results[0]["result_id"] == "GOOD-001" + assert any("Skipped" in str(w.message) for w in caught) + + def test_non_json_files_ignored(self, tmp_path): + valid = _valid_result(result_id="R001", candidate_id="C001", assay_date="2026-07-01") + (tmp_path / "result.json").write_text(json.dumps(valid)) + (tmp_path / "notes.txt").write_text("not a json file") + (tmp_path / "data.csv").write_text("col1,col2\n1,2") + results = load_lab_results_dir(tmp_path) + assert len(results) == 1