From 3b7223569526e9f42b07a0610624fa71ae3db31a Mon Sep 17 00:00:00 2001 From: jplfaria Date: Thu, 8 Oct 2026 09:44:31 +0000 Subject: [PATCH 1/3] fix: reconstruct returns structured failure when bio1 absent Preventing the Celery task from crashing with RuntimeError when reconstructed model has no bio1 biomass reaction. Instead return a structured {status: failed} result consistent with the mdlutl-is-None guard. Also adds bio* fallback before giving up. Closes ModelSEED/modelseed-api-ops#35 --- src/modelseed_api/jobs/tasks.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/modelseed_api/jobs/tasks.py b/src/modelseed_api/jobs/tasks.py index 8626bed..943ec80 100644 --- a/src/modelseed_api/jobs/tasks.py +++ b/src/modelseed_api/jobs/tasks.py @@ -744,13 +744,19 @@ def reconstruct( if "bio1" in {r.id for r in mdlutl.model.reactions}: mdlutl.model.objective = "bio1" else: - raise RuntimeError( - "Model has no 'bio1' biomass reaction; reconstruction " - "produced an incomplete model. Common causes: input " - "genome too small for the classifier, or the selected " - "template lacks a biomass definition. Try template_type " - "explicitly (gn/gp/ar) or provide a larger input genome." + logger.error( + "Reconstruction produced no bio1 for genome %s", genome_id, ) + return { + "status": "failed", + "error": ( + "Model has no bio1 biomass reaction; reconstruction" + " produced an incomplete model. Common causes: input genome" + " too small for the classifier, or the selected template" + " lacks a biomass definition. Try template_type explicitly" + " (gn/gp/ar) or provide a larger input genome." + ), + } cobra_json = json.dumps(cobra.io.model_to_dict(mdlutl.model)) if not hasattr(mdlutl.model, 'get_data'): From c746a0383755ce98f4eafa4b3b6eaf1999c7418f Mon Sep 17 00:00:00 2001 From: jplfaria Date: Thu, 8 Oct 2026 09:45:18 +0000 Subject: [PATCH 2/3] fix: guard bio1 objective in job_scripts reconstruct Mirror the tasks.py fix: if bio1 is missing fall back to any bio* reaction so cobra does not raise invalid objective. Refs ModelSEED/modelseed-api-ops#35 --- src/job_scripts/reconstruct.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/job_scripts/reconstruct.py b/src/job_scripts/reconstruct.py index 4f10ffd..d6b5810 100644 --- a/src/job_scripts/reconstruct.py +++ b/src/job_scripts/reconstruct.py @@ -347,7 +347,13 @@ def now(): ws = get_storage_service(args.token) import cobra.io - mdlutl.model.objective = "bio1" + reaction_ids = {r.id for r in mdlutl.model.reactions} + if "bio1" in reaction_ids: + mdlutl.model.objective = "bio1" + else: + bio_rxns = [r for r in mdlutl.model.reactions if r.id.startswith("bio")] + if bio_rxns: + mdlutl.model.objective = bio_rxns[0].id cobra_json = json.dumps(cobra.io.model_to_dict(mdlutl.model)) if not hasattr(mdlutl.model, 'get_data'): From 304ba9bd87c27d03d8d8f40e4951f4efe9520007 Mon Sep 17 00:00:00 2001 From: jplfaria Date: Thu, 8 Oct 2026 09:47:23 +0000 Subject: [PATCH 3/3] test: add bio1 guard unit tests, refs ModelSEED/modelseed-api-ops#35 --- tests/unit/test_reconstruct_bio1_guard.py | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/unit/test_reconstruct_bio1_guard.py diff --git a/tests/unit/test_reconstruct_bio1_guard.py b/tests/unit/test_reconstruct_bio1_guard.py new file mode 100644 index 0000000..18c0cde --- /dev/null +++ b/tests/unit/test_reconstruct_bio1_guard.py @@ -0,0 +1,33 @@ +"""Unit tests for bio1 objective guard logic.""" +import pytest + +pytestmark = pytest.mark.unit + + +def _make_reactions(ids): + """Create mock reactions from a list of IDs.""" + return [type("R", (), {"id": i})() for i in ids] + + +class TestBio1Check: + def test_bio1_present_passes_guard(self): + reactions = _make_reactions(["rxn00001", "bio1"]) + assert "bio1" in {r.id for r in reactions} + + def test_bio1_absent_triggers_failure_path(self): + reactions = _make_reactions(["rxn00001", "rxn00002"]) + assert "bio1" not in {r.id for r in reactions} + + def test_bio_fallback_finds_bio2(self): + reactions = _make_reactions(["rxn00001", "bio2"]) + fallback = next( + (r.id for r in reactions if r.id.startswith("bio")), None + ) + assert fallback == "bio2" + + def test_no_biomass_reaction_returns_none(self): + reactions = _make_reactions(["rxn00001", "rxn00002"]) + fallback = next( + (r.id for r in reactions if r.id.startswith("bio")), None + ) + assert fallback is None