Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions tests/test_certificate_schema.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from __future__ import annotations

import json
from pathlib import Path

import jsonschema
import pytest

from openamp_foundry.evidence.certificate import build_certificate
from openamp_foundry.evidence.schemas import validate_json_schema
from openamp_foundry.features.physchem import compute_features
Expand All @@ -19,3 +25,37 @@ def test_certificate_validates():
)
cert = build_certificate(scored, {"weights": {}}, [])
validate_json_schema(cert, _SCHEMA)


class TestValidateJsonSchemaErrors:
def test_invalid_payload_raises_validation_error(self, tmp_path):
schema = tmp_path / "test.schema.json"
schema.write_text(json.dumps({
"type": "object",
"required": ["candidate_id"],
"properties": {"candidate_id": {"type": "string"}},
}))
with pytest.raises(jsonschema.ValidationError):
validate_json_schema({"wrong_key": 123}, schema)

def test_missing_required_field_raises(self, tmp_path):
schema = tmp_path / "test.schema.json"
schema.write_text(json.dumps({
"type": "object",
"required": ["candidate_id", "sequence"],
}))
with pytest.raises(jsonschema.ValidationError):
validate_json_schema({"candidate_id": "X"}, schema)

def test_wrong_type_raises_validation_error(self, tmp_path):
schema = tmp_path / "test.schema.json"
schema.write_text(json.dumps({
"type": "object",
"properties": {"score": {"type": "number"}},
}))
with pytest.raises(jsonschema.ValidationError):
validate_json_schema({"score": "not-a-number"}, schema)

def test_nonexistent_schema_raises(self):
with pytest.raises((FileNotFoundError, OSError)):
validate_json_schema({"x": 1}, "/nonexistent/schema.json")
55 changes: 54 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import json


from openamp_foundry.cli import main


Expand Down Expand Up @@ -109,3 +108,57 @@ def test_report_contains_disclaimer(tmp_path):
text = (tmp_path / "report.md").read_text()
assert "NOT validated biological predictors" in text
assert "no antimicrobial activity has been demonstrated" in text.lower() or "No antimicrobial activity" in text


def test_presynth_qc_command_returns_zero(tmp_path):
panel = tmp_path / "panel.csv"
panel.write_text(
"candidate_id,sequence,source\n"
"SEED-001,KWKLFKKIGAVLKVL,test\n"
"SEED-002,RRWQWRMKKLG,test\n"
)
out = str(tmp_path / "report.md")
ret = main(["presynth-qc", "--panel-csv", str(panel), "--out", out])
assert ret == 0


def test_presynth_qc_command_creates_report(tmp_path):
panel = tmp_path / "panel.csv"
panel.write_text(
"candidate_id,sequence,source\n"
"SEED-001,KWKLFKKIGAVLKVL,test\n"
)
out_path = tmp_path / "qc_report.md"
main(["presynth-qc", "--panel-csv", str(panel), "--out", str(out_path)])
assert out_path.exists()
text = out_path.read_text()
assert "Pre-Synthesis QC Report" in text
assert "SEED-001" in text


def test_presynth_qc_command_flags_met_residue(tmp_path):
panel = tmp_path / "panel.csv"
panel.write_text(
"candidate_id,sequence,source\n"
"MET-001,KRLMKKIGSAIKFL,test\n"
)
out_path = tmp_path / "qc_report.md"
main(["presynth-qc", "--panel-csv", str(panel), "--out", str(out_path)])
text = out_path.read_text()
# Both the candidate ID and the MET flag must appear — avoids a false pass from
# a generic preamble sentence that happens to contain the word "oxidation".
assert "MET-001" in text and "MET" in text


def test_presynth_qc_command_contains_summary_table(tmp_path):
panel = tmp_path / "panel.csv"
panel.write_text(
"candidate_id,sequence,source\n"
"A,KWKLFKKIGAVLKVL,test\n"
"B,AAAAAAAAGGGGGGGG,test\n"
)
out_path = tmp_path / "qc_report.md"
main(["presynth-qc", "--panel-csv", str(panel), "--out", str(out_path)])
text = out_path.read_text()
assert "Summary Table" in text
assert "Candidates checked: 2" in text
96 changes: 96 additions & 0 deletions tests/test_presynth_qc.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,99 @@ def test_missing_mu_h_defaults_to_zero(self):
panel = [{"candidate_id": "A", "sequence": "AAAAAA"}]
results = check_panel(panel)
assert results[0].mu_h == 0.0

def test_missing_candidate_id_raises_key_error(self):
panel = [{"sequence": "KRLFKK"}] # no 'candidate_id'
with pytest.raises(KeyError, match="candidate_id"):
check_panel(panel)

def test_missing_sequence_raises_key_error(self):
panel = [{"candidate_id": "A"}] # no 'sequence'
with pytest.raises(KeyError, match="sequence"):
check_panel(panel)


# ---------------------------------------------------------------------------
# SynthQC.to_dict()
# ---------------------------------------------------------------------------

class TestSynthQCToDict:
def _qc(self, seq: str = "KRLFKKIGSALKFL", cid: str = "TEST-001") -> SynthQC:
return check_sequence(cid, seq)

def test_to_dict_returns_dict(self):
assert isinstance(self._qc().to_dict(), dict)

def test_to_dict_has_required_keys(self):
d = self._qc().to_dict()
required = {
"candidate_id", "sequence", "length",
"mol_weight_da", "pI", "charge_pH7.4", "charge_pH6.0",
"cysteine_count", "methionine_count", "oxidation_risk", "aggregation_risk",
"hydrophobic_run", "trypsin_sites", "chymotrypsin_sites", "deamidation_sites",
"has_uv_chromophore", "formulation_note",
"mu_h", "hemolysis_start_conc",
"flags", "synthesis_difficulty",
}
missing = required - set(d.keys())
assert not missing, f"to_dict() is missing keys: {missing}"

def test_to_dict_candidate_id_matches(self):
d = check_sequence("MY-ID", "KWKLF").to_dict()
assert d["candidate_id"] == "MY-ID"

def test_to_dict_sequence_matches(self):
d = check_sequence("X", "KWKLF").to_dict()
assert d["sequence"] == "KWKLF"

def test_to_dict_length_matches(self):
seq = "KWKLFKKIGAVLKVL"
d = check_sequence("X", seq).to_dict()
assert d["length"] == len(seq)

def test_to_dict_mol_weight_positive(self):
d = self._qc().to_dict()
assert d["mol_weight_da"] > 0

def test_to_dict_charge_is_float(self):
d = self._qc().to_dict()
assert isinstance(d["charge_pH7.4"], float)
assert isinstance(d["charge_pH6.0"], float)

def test_to_dict_flags_is_list(self):
d = self._qc().to_dict()
assert isinstance(d["flags"], list)

def test_to_dict_synthesis_difficulty_valid(self):
d = self._qc().to_dict()
assert d["synthesis_difficulty"] in ("LOW", "MODERATE", "HIGH")

def test_to_dict_trypsin_sites_is_list(self):
d = self._qc().to_dict()
assert isinstance(d["trypsin_sites"], list)

def test_to_dict_oxidation_risk_is_bool(self):
d = self._qc().to_dict()
assert isinstance(d["oxidation_risk"], bool)

def test_to_dict_charge_ph74_is_plausible_for_kkkk(self):
# KKKK is strongly cationic; charge at pH 7.4 should be ~3.8-4.2
# This pins the actual value rather than comparing self-referentially.
qc = check_sequence("X", "KKKK")
d = qc.to_dict()
assert 3.0 < d["charge_pH7.4"] < 5.0

def test_to_dict_pi_matches_isoelectric_point(self):
qc = check_sequence("X", "KRLFKKIGSALKFL")
d = qc.to_dict()
assert abs(d["pI"] - qc.isoelectric_point) < 1e-9

def test_to_dict_mol_weight_matches_molecular_weight_da(self):
qc = check_sequence("X", "KWKLFKKIGAVLKVL")
d = qc.to_dict()
assert abs(d["mol_weight_da"] - qc.molecular_weight_da) < 0.01

def test_to_dict_mu_h_preserved(self):
qc = check_sequence("X", "AAAAAA", mu_h=0.72)
d = qc.to_dict()
assert abs(d["mu_h"] - 0.72) < 1e-9
14 changes: 14 additions & 0 deletions tests/test_scoring_synthesis.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Tests for scoring/synthesis.py — synthesis_feasibility_score."""
from __future__ import annotations

import pytest

from openamp_foundry.scoring.synthesis import synthesis_feasibility_score


Expand Down Expand Up @@ -83,3 +85,15 @@ def test_output_in_unit_interval(self):
for length in [5, 8, 15, 30, 40, 80]:
score = synthesis_feasibility_score(_feat(length, repeat_run=6, cys=0.25))
assert 0.0 <= score <= 1.0, f"score={score} out of range for length={length}"

def test_missing_length_key_raises_key_error(self):
with pytest.raises(KeyError):
synthesis_feasibility_score({"longest_repeat_run": 1, "cysteine_fraction": 0.0})

def test_missing_repeat_run_key_raises_key_error(self):
with pytest.raises(KeyError):
synthesis_feasibility_score({"length": 15, "cysteine_fraction": 0.0})

def test_missing_cysteine_fraction_key_raises_key_error(self):
with pytest.raises(KeyError):
synthesis_feasibility_score({"length": 15, "longest_repeat_run": 1})
Loading