import csv import os import json import sys import types import zipfile from pathlib import Path from apps.web import pipeline_backend as backend from gfa_core.gene_family_scope import build_scope_id from gfa_core.ortholog_materialize import ( OrthologRunRecord, build_ortholog_run_identity, materialize_ortholog_final_outputs, ) def _materialize_trusted_ppi_mapping( job, *, prefix, target, family, storage_key, taxon_id, query_id, subject_id, ): workspace = Path(job.workspace_dir) final_dir = workspace / "final" query = final_dir / f"{prefix}.final_members.pep.fa" reference = Path(job.input_dir) / storage_key / f"{storage_key}.pep.fasta" query.write_text(f">{query_id}\nMA\n", encoding="utf-8") pairs = workspace / "ortholog" / f"{prefix}.{target}_to_{storage_key}_pairs.txt" pairs.parent.mkdir(parents=True, exist_ok=True) pairs.write_text(f"{subject_id}\t{query_id}\t91\n", encoding="utf-8") scope_id = build_scope_id(family, [target], [storage_key], [], "target_plus_reference") identity = build_ortholog_run_identity( query_species=target, reference_taxon_id=taxon_id, reference_storage_key=storage_key, query_fasta=query, reference_pep=reference, scope_id=scope_id, member_set_id="", mapping_path=pairs, ) materialize_ortholog_final_outputs( final_dir=final_dir, prefix=prefix, run_records=[OrthologRunRecord(target, storage_key, query, reference, pairs)], job_id=job.job_id, workspace=workspace, reference_species=storage_key, reference_taxon_id=taxon_id, reference_storage_key=storage_key, ortholog_identity=identity, scope_fields={ "scope_id": scope_id, "scope_mode": "target_plus_reference", "target_species": [target], "reference_species": [storage_key], "selected_species": [target, storage_key], "family": family, }, ) def test_run_cmd_logged_exports_project_root_to_pythonpath(tmp_path, monkeypatch): captured = {} class FakeProcess: def wait(self): return 0 def fake_popen(cmd, cwd=None, stdout=None, stderr=None, text=None, env=None): captured["env"] = env or {} return FakeProcess() monkeypatch.setenv("PYTHONPATH", "/existing/path") monkeypatch.setattr(backend.subprocess, "Popen", fake_popen) exit_code = backend.run_cmd_logged( ["python", "skills/ppi/run.py"], tmp_path / "ppi.log", cwd=backend.PROJECT_ROOT, ) pythonpath = captured["env"].get("PYTHONPATH", "").split(os.pathsep) assert exit_code == 0 assert str(backend.PROJECT_ROOT) in pythonpath assert "/existing/path" in pythonpath def _write_ppi_scope(path, target_species="Os", reference_species="At"): data = { "family": "CRR", "prefix": "Os.CRR", "scope_id": "CRR.target-Os.ref-At.target_plus_reference", "scope_mode": "target_plus_reference", "target_species": [target_species], "reference_species": [reference_species], "selected_species": [target_species, reference_species], } path.write_text(json.dumps(data), encoding="utf-8") def _write_fake_ppi_outputs(ppi_dir, prefix="Os.CRR"): ppi_dir.mkdir(parents=True, exist_ok=True) (ppi_dir / f"{prefix}_network.tsv").write_text("protein1\tprotein2\tscore\nA\tB\t0.9\n", encoding="utf-8") (ppi_dir / f"{prefix}_report.md").write_text("# PPI\n", encoding="utf-8") (ppi_dir / f"{prefix}_url.txt").write_text("View online: https://string-db.org/cgi/network?species=39947\n", encoding="utf-8") (ppi_dir / f"{prefix}_id_resolution.tsv").write_text("raw_id\tnormalized_id\nOs01t0810800-01\tOs01g0810800\n", encoding="utf-8") (ppi_dir / f"{prefix}.manifest.json").write_text( json.dumps({"manifest_version": "1.0", "stage": "ppi", "status": "success", "prefix": prefix}), encoding="utf-8", ) def test_reference_bundle_id_map_preserves_loc_and_numeric_geneid(tmp_path): pep_path = tmp_path / "soe.pep.fasta" cds_path = tmp_path / "soe.cds.fasta" output_path = tmp_path / "soe.pep_cds_idmap.tsv" pep_path.write_text( ">XP_021867507.1 [protein_id=XP_021867507.1]\nMAAA\n", encoding="utf-8", ) cds_path.write_text( ">XP_021867507.1 [gene=LOC110806180] " "[db_xref=GeneID:110806180] [protein_id=XP_021867507.1]\nATGGCG\n", encoding="utf-8", ) result = backend._build_reference_bundle_id_map( species_code="soe", pep_path=pep_path, cds_path=cds_path, output_path=output_path, ) with open(result["id_map_path"], encoding="utf-8", newline="") as handle: row = next(csv.DictReader(handle, delimiter="\t")) assert row["gene_id"] == "LOC110806180" assert row["ncbi_gene_id"] == "110806180" def test_reference_bundle_download_extracts_ncbi_zip_and_unblocks_ppi_kaks_preflight(tmp_path, monkeypatch): job = backend.JobInfo( job_id="job_test_ref_bundle", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" workspace = tmp_path / "workspace" (workspace / "final").mkdir(parents=True) (workspace / "logs").mkdir(parents=True) input_dir.mkdir(parents=True) fake_zip = tmp_path / "ncbi_dataset.zip" with zipfile.ZipFile(fake_zip, "w") as zf: zf.writestr("ncbi_dataset/data/GCF_FAKE/protein.faa", ">SOE_PEP1\nMAAA\n") zf.writestr( "ncbi_dataset/data/GCF_FAKE/cds_from_genomic.fna", ">lcl|NC_000001.1_cds_XP_000001.1_1 [protein_id=XP_000001.1]\nATGGCG\n", ) monkeypatch.setattr( backend, "_select_ncbi_reference_assembly", lambda **kwargs: { "accession": "GCF_FAKE", "organism_name": "Spinacia oleracea", "taxon_id": "3562", "assembly_name": "fake", "assembly_level": "Chromosome", "annotation_name": "fake annotation", }, ) def fake_download(url, output_path, timeout_sec=300): output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_bytes(fake_zip.read_bytes()) return output_path.stat().st_size monkeypatch.setattr(backend, "_download_url_to_file", fake_download) manifest = backend.download_reference_annotation_bundle( job=job, species_code="soe", species_name="Spinacia oleracea", taxon_id="3562", requested_stages=["ppi", "kaks"], ) assert manifest["status"] == "success" assert (input_dir / "soe" / "soe.pep.fasta").exists() assert (input_dir / "soe" / "soe.cds.fasta").exists() assert (input_dir / "soe" / "soe.cds.fasta").read_text(encoding="utf-8").startswith(">XP_000001.1 ") resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", target_species=["Ft"], reference_species=["Spinacia oleracea"], family="SPL", requested_stages=["ppi", "kaks"], ) assert resolution["status"] == "needs_ortholog" assert resolution["selected_string_species"] == "soe" assert resolution["reference_species"] == "soe" assert resolution["reference_bundle_status"]["kaks_ready"] is True assert resolution["reference_bundle_status"]["missing_required"] == [] def test_ortholog_scope_uses_downloaded_reference_proteome_when_family_pep_missing(tmp_path, monkeypatch): job = backend.JobInfo( job_id="job_test_ortholog_ref_bundle", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" workspace = tmp_path / "workspace" final_dir = workspace / "final" logs_dir = workspace / "logs" (input_dir / "soe").mkdir(parents=True) final_dir.mkdir(parents=True) logs_dir.mkdir(parents=True) (final_dir / "Ft.SPL.identify.manifest.json").write_text( json.dumps({"stage": "identify", "status": "success", "prefix": "Ft.SPL"}), encoding="utf-8", ) (final_dir / "Ft.SPL.final_members.pep.fa").write_text(">FtGene1\nMAAA\n", encoding="utf-8") (input_dir / "soe" / "soe.pep.fasta").write_text(">SoeGene1\nMAAA\n", encoding="utf-8") scope_file = final_dir / "Ft.SPL.SPL.target-Ft.ref-soe.target_plus_reference.scope.json" scope_file.write_text( json.dumps({ "family": "SPL", "prefix": "Ft.SPL", "scope_id": "SPL.target-Ft.ref-soe.target_plus_reference", "scope_mode": "target_plus_reference", "target_species": ["Ft"], "reference_species": ["Spinacia oleracea"], "selected_species": ["Ft", "Spinacia oleracea"], }), encoding="utf-8", ) captured = {} def fake_run_cmd_logged(cmd, log_path, cwd=None): captured["cmd"] = cmd output_dir = tmp_path / "workspace" / "ortholog" run_prefix = "Ft.SPL.Ft_to_soe" (output_dir / f"{run_prefix}_pairs.txt").write_text( "# Ortholog pairs identified by BLASTP\n" "# Format: Subject_ID\tQuery_ID\tIdentity(%)\n" "SoeGene1\tFtGene1\t91.00\n", encoding="utf-8", ) (output_dir / f"{run_prefix}.blastp.txt").write_text("FtGene1\tSoeGene1\t91\n", encoding="utf-8") return 0 monkeypatch.setattr(backend, "run_cmd_logged", fake_run_cmd_logged) message, manifest = backend.run_stage( "ortholog", job, "Ft.SPL", cpu=2, scope_file=str(scope_file), blast_evalue=1e-5, ) assert message.startswith("✅ Ortholog analysis complete") assert manifest is not None assert str(input_dir / "soe" / "soe.pep.fasta") in captured["cmd"] def test_ppi_auto_prefers_target_string_species_when_target_is_supported(tmp_path, monkeypatch): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" workspace = tmp_path / "workspace" final_dir = workspace / "final" logs_dir = workspace / "logs" final_dir.mkdir(parents=True) logs_dir.mkdir(parents=True) input_dir.mkdir(parents=True) (final_dir / "Os.CRR.final_members.id").write_text("Os01t0810800-01\n", encoding="utf-8") stale_image = final_dir / "Os.CRR.ppi_network.string.png" stale_image.write_bytes(b"old-at-image") (final_dir / "Os.CRR.orthologs.txt").write_text( "Os01t0810800-01\tAT1G51790\t90\tOs\tAt\n", encoding="utf-8", ) scope_file = final_dir / "Os.CRR.CRR.target-Os.ref-At.target_plus_reference.scope.json" _write_ppi_scope(scope_file) captured = {} def fake_run_cmd_logged(cmd, log_path, cwd=None): captured["cmd"] = cmd _write_fake_ppi_outputs(workspace / "ppi") return 0 monkeypatch.setattr(backend, "can_start_new_task", lambda *args, **kwargs: (True, "ok")) monkeypatch.setattr(backend, "run_cmd_logged", fake_run_cmd_logged) monkeypatch.setattr(backend, "download_string_network_image", lambda *args, **kwargs: None) message, manifest = backend.run_stage( "ppi", job, "Os.CRR", ppi_species="auto", ppi_score=400, scope_file=str(scope_file), analysis_route="target_plus_reference", ) cmd = captured["cmd"] assert "✅ PPI analysis completed" in message assert manifest and manifest.exists() assert cmd[cmd.index("--species") + 1] == "osa" assert cmd[cmd.index("--gene-ids") + 1] == str(final_dir / "Os.CRR.final_members.id") assert "--mapping" not in cmd assert "--id-map" not in cmd assert not stale_image.exists() def test_ppi_direct_supported_target_forwards_exact_current_job_id_map(tmp_path, monkeypatch): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" workspace = tmp_path / "workspace" final_dir = workspace / "final" (input_dir / "osa").mkdir(parents=True) final_dir.mkdir(parents=True) (workspace / "logs").mkdir(parents=True) id_map_file = input_dir / "osa" / "osa.pep_cds_idmap.tsv" id_map_file.write_text("species_code\tprotein_id\nosa\tOs01g0810800\n", encoding="utf-8") (final_dir / "Os.CRR.final_members.id").write_text("Os01t0810800-01\n", encoding="utf-8") scope_file = final_dir / "Os.CRR.CRR.target-Os.ref-At.target_plus_reference.scope.json" _write_ppi_scope(scope_file) captured = {} def fake_run_cmd_logged(cmd, log_path, cwd=None): captured["cmd"] = cmd _write_fake_ppi_outputs(workspace / "ppi") return 0 monkeypatch.setattr(backend, "can_start_new_task", lambda *args, **kwargs: (True, "ok")) monkeypatch.setattr(backend, "run_cmd_logged", fake_run_cmd_logged) monkeypatch.setattr(backend, "download_string_network_image", lambda *args, **kwargs: None) message, manifest = backend.run_stage( "ppi", job, "Os.CRR", ppi_species="osa", ppi_score=400, scope_file=str(scope_file), analysis_route="target_plus_reference", ) cmd = captured["cmd"] assert "✅ PPI analysis completed" in message assert manifest and manifest.exists() assert cmd[cmd.index("--id-map") + 1] == str(id_map_file) assert "--mapping" not in cmd def test_infer_string_species_from_ortholog_mapping_does_not_default_to_ath(tmp_path): mapping = tmp_path / "Ft.unknown.orthologs.txt" mapping.write_text("FtGene1\tRefGene1\t90\tFt\tUnknownSpecies\n", encoding="utf-8") assert backend.infer_string_species_from_ortholog_mapping(mapping, "") == "" def test_ppi_scope_with_mapping_but_unknown_reference_species_fails_instead_of_ath(tmp_path, monkeypatch): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" workspace = tmp_path / "workspace" final_dir = workspace / "final" logs_dir = workspace / "logs" final_dir.mkdir(parents=True) logs_dir.mkdir(parents=True) input_dir.mkdir(parents=True) (final_dir / "Ft.SPL.final_members.id").write_text("FtGene1\n", encoding="utf-8") (final_dir / "Ft.SPL.orthologs.txt").write_text( "FtGene1\tRefGene1\t90\tFt\tUnknownSpecies\n", encoding="utf-8", ) scope = { "family": "SPL", "prefix": "Ft.SPL", "scope_id": "SPL.target-Ft.ref-UnknownSpecies.target_plus_reference", "scope_mode": "target_plus_reference", "target_species": ["Ft"], "reference_species": ["UnknownSpecies"], "selected_species": ["Ft", "UnknownSpecies"], } scope_file = final_dir / "Ft.SPL.scope.json" scope_file.write_text(json.dumps(scope), encoding="utf-8") called = {"run": False} def fake_run_cmd_logged(*args, **kwargs): called["run"] = True return 0 monkeypatch.setattr(backend, "run_cmd_logged", fake_run_cmd_logged) message, manifest = backend.run_stage( "ppi", job, "Ft.SPL", ppi_species="auto", ppi_score=200, scope_file=str(scope_file), analysis_route="target_plus_reference", ) assert called["run"] is False assert "无法从 Ortholog 映射推断 STRING 参考物种" in message assert manifest and manifest.exists() data = json.loads(manifest.read_text(encoding="utf-8")) assert data["diagnostics"]["issue_code"] == "ppi_invalid_ortholog_mapping" assert data["parameters"]["ppi_species_resolution"]["decision"] == "failed_requires_ortholog" def test_ppi_species_resolution_reports_reference_data_missing(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) (tmp_path / "input").mkdir() (tmp_path / "workspace" / "final").mkdir(parents=True) resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", target_species=["Ft"], reference_species=[], family="SPL", ) assert resolution["status"] == "confirmation_required" assert resolution["issue_code"] == "ppi_reference_confirmation_required" assert resolution["required_next_stage"] == "" assert [row["selected_string_taxon_id"] for row in resolution["candidates"]] == ["3562", "3476", "3775"] def test_os_direct_ppi_uses_complete_string_registry_without_ortholog(tmp_path): job = backend.JobInfo( job_id="job_test_os_direct_ppi", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) (tmp_path / "input").mkdir() (tmp_path / "workspace" / "final").mkdir(parents=True) resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Os.HKL", target_species=["Os"], requested_stages=["ppi"], ) assert resolution["status"] == "ready" assert resolution["selection_mode"] == "exact_string" assert resolution["selected_taxon_id"] == "39947" assert resolution["required_next_stage"] == "" assert resolution["ortholog_mapping_source"] == "" def test_ppi_species_resolution_canonicalizes_fagopyrum_tataricum_alias(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) (tmp_path / "input").mkdir() (tmp_path / "workspace" / "final").mkdir(parents=True) resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", target_species=["Fagopyrum tataricum"], reference_species=[], family="SPL", ) assert resolution["target_species"] == ["Ft"] assert resolution["target_string_supported"] is False assert resolution["status"] == "confirmation_required" def test_ppi_species_resolution_recommends_near_string_species_before_arabidopsis_for_fagopyrum(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) (tmp_path / "input").mkdir() (tmp_path / "workspace" / "final").mkdir(parents=True) resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", target_species=["Fagopyrum tataricum"], reference_species=[], family="SPL", ) candidates = resolution["diagnostics"]["reference_recommendations"] assert resolution["status"] == "confirmation_required" assert [row["selected_string_taxon_id"] for row in candidates] == ["3562", "3476", "3775"] assert candidates[0]["selected_string_name"] == "Spinacia oleracea" assert candidates[0]["lca_taxon_id"] == "3524" assert candidates[0]["tree_distance"] == 10 def test_explicit_project_reference_is_not_overridden_by_downloaded_near_reference(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" (tmp_path / "workspace" / "final").mkdir(parents=True) (input_dir / "At").mkdir(parents=True) (input_dir / "soe").mkdir(parents=True) (input_dir / "At" / "At.pep.fasta").write_text(">AT1\nMAAA\n", encoding="utf-8") (input_dir / "At" / "At.cds.fasta").write_text(">AT1\nATGGCTGCTGCT\n", encoding="utf-8") (input_dir / "soe" / "soe.pep.fasta").write_text(">SOE1\nMAAA\n", encoding="utf-8") (input_dir / "soe" / "soe.cds.fasta").write_text(">SOE1\nATGGCTGCTGCT\n", encoding="utf-8") (input_dir / "soe" / "soe.reference_bundle_download.manifest.json").write_text( json.dumps({"status": "success", "species_code": "soe"}), encoding="utf-8" ) resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="ath", target_species=["Ft"], reference_species=["At"], family="SPL", requested_stages=["kaks"], ) assert resolution["reference_species"] == "At" assert resolution["selected_string_species"] == "ath" assert resolution["reference_bundle_status"]["kaks_ready"] is True def test_ppi_species_resolution_selected_near_string_species_still_needs_reference_data_without_pep(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) (tmp_path / "input").mkdir() (tmp_path / "workspace" / "final").mkdir(parents=True) resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", target_species=["Fagopyrum tataricum"], reference_species=["Spinacia oleracea"], family="SPL", ppi_reference_taxon_id="3562", ppi_reference_storage_key="soe", ppi_reference_selection_mode="tree_nearest", ppi_reference_user_confirmed=True, dependency_owner="ppi", ) assert resolution["status"] == "needs_reference_data" assert resolution["issue_code"] == "ppi_reference_data_missing" assert resolution["selected_string_species"] == "soe" assert resolution["selected_taxon_id"] == "3562" assert resolution["reference_species"] == "soe" assert resolution["reference_data_source"] == "" def test_ppi_species_resolution_selected_near_string_species_with_pep_needs_ortholog(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" final_dir = tmp_path / "workspace" / "final" input_dir.mkdir() final_dir.mkdir(parents=True) (input_dir / "soe").mkdir() (input_dir / "soe" / "soe.pep.fasta").write_text(">SoeGene1\nMA\n", encoding="utf-8") resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", target_species=["Fagopyrum tataricum"], reference_species=["Spinacia oleracea"], family="SPL", ppi_reference_taxon_id="3562", ppi_reference_storage_key="soe", ppi_reference_selection_mode="tree_nearest", ppi_reference_user_confirmed=True, dependency_owner="ppi", ) assert resolution["status"] == "needs_ortholog" assert resolution["issue_code"] == "ppi_requires_ortholog_mapping" assert resolution["selected_string_species"] == "soe" assert resolution["selected_taxon_id"] == "3562" assert resolution["reference_data_source"].endswith("soe/soe.pep.fasta") assert resolution["reference_bundle_status"]["kaks_ready"] is True def test_supported_target_ppi_is_ready_independently_when_kaks_lacks_reference_data(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) (tmp_path / "input").mkdir() (tmp_path / "workspace" / "final").mkdir(parents=True) (tmp_path / "input" / "soe").mkdir() (tmp_path / "input" / "soe" / "soe.pep.fasta").write_text(">SoeGene1\nMA\n", encoding="utf-8") ppi_resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Os.CRR", ppi_species="auto", target_species=["Os"], reference_species=["At"], family="CRR", requested_stages=["ppi"], ) kaks_resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Os.CRR", ppi_species="auto", target_species=["Os"], reference_species=["Spinacia oleracea"], family="CRR", requested_stages=["kaks"], ) assert ppi_resolution["status"] == "ready" assert ppi_resolution["decision"] == "ready_target_string" assert ppi_resolution["selected_string_species"] == "osa" assert ppi_resolution["ortholog_mapping_source"] == "" assert kaks_resolution["status"] == "needs_reference_data" assert kaks_resolution["issue_code"] == "kaks_reference_cds_missing" def test_ppi_kaks_dependency_preflight_requires_reference_cds_before_ortholog(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" final_dir = tmp_path / "workspace" / "final" input_dir.mkdir() final_dir.mkdir(parents=True) (input_dir / "soe").mkdir() (input_dir / "soe" / "soe.pep.fasta").write_text(">SoeGene1\nMA\n", encoding="utf-8") resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", target_species=["Fagopyrum tataricum"], reference_species=["Spinacia oleracea"], family="SPL", requested_stages=["ppi", "kaks"], ) assert resolution["status"] == "needs_reference_data" assert resolution["issue_code"] == "kaks_reference_cds_missing" assert resolution["requested_stages"] == ["ppi", "kaks"] assert resolution["reference_bundle_status"]["missing_required"] == ["cds"] assert resolution["reference_bundle_status"]["kaks_ready"] is False def test_ppi_kaks_dependency_preflight_with_pep_and_cds_needs_ortholog(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" final_dir = tmp_path / "workspace" / "final" input_dir.mkdir() final_dir.mkdir(parents=True) (input_dir / "soe").mkdir() (input_dir / "soe" / "soe.pep.fasta").write_text(">SoeGene1\nMA\n", encoding="utf-8") (input_dir / "soe" / "soe.cds.fasta").write_text(">SoeGene1\nATGGCT\n", encoding="utf-8") resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", target_species=["Fagopyrum tataricum"], reference_species=["Spinacia oleracea"], family="SPL", requested_stages=["ppi", "kaks"], ) assert resolution["status"] == "needs_ortholog" assert resolution["reference_bundle_status"]["kaks_ready"] is True assert resolution["diagnostics"]["ortholog_dependency_role"] == "producer" def test_ppi_kaks_dependency_preflight_rejects_stale_at_mapping_when_spinacia_selected(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" final_dir = tmp_path / "workspace" / "final" input_dir.mkdir() final_dir.mkdir(parents=True) (input_dir / "soe").mkdir() (input_dir / "soe" / "soe.pep.fasta").write_text(">SoeGene1\nMA\n", encoding="utf-8") (input_dir / "soe" / "soe.cds.fasta").write_text(">SoeGene1\nATGGCT\n", encoding="utf-8") (final_dir / "Ft.SPL.orthologs.txt").write_text( "FtGene1\tAT1G01010\t90\tFt\tAt\n", encoding="utf-8", ) resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", target_species=["Fagopyrum tataricum"], reference_species=["Spinacia oleracea"], family="SPL", requested_stages=["ppi", "kaks"], ) assert resolution["status"] == "needs_ortholog" assert resolution["issue_code"] == "stale_ortholog_reference_conflict" assert resolution["mapping_reference_species"] == "At" def test_ppi_species_resolution_selected_near_string_species_with_mapping_is_ready(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" final_dir = tmp_path / "workspace" / "final" input_dir.mkdir() final_dir.mkdir(parents=True) (input_dir / "soe").mkdir() (input_dir / "soe" / "soe.pep.fasta").write_text(">SoeGene1\nMA\n", encoding="utf-8") _materialize_trusted_ppi_mapping( job, prefix="Ft.SPL", target="Ft", family="SPL", storage_key="soe", taxon_id="3562", query_id="FtGene1", subject_id="SoeGene1", ) resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", target_species=["Fagopyrum tataricum"], reference_species=["Spinacia oleracea"], family="SPL", ppi_reference_taxon_id="3562", ppi_reference_storage_key="soe", ppi_reference_selection_mode="tree_nearest", ppi_reference_user_confirmed=True, dependency_owner="ppi", ) assert resolution["status"] == "ready" assert resolution["selected_string_species"] == "soe" assert resolution["selected_taxon_id"] == "3562" assert resolution["ortholog_mapping_source"].endswith("Ft.SPL.orthologs.txt") assert resolution["decision"] == "ready_reference_mapping" def test_ppi_species_resolution_can_ignore_existing_mapping_for_pre_ortholog_check(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" final_dir = tmp_path / "workspace" / "final" input_dir.mkdir() final_dir.mkdir(parents=True) (input_dir / "soe").mkdir() (input_dir / "soe" / "soe.pep.fasta").write_text(">SoeGene1\nMA\n", encoding="utf-8") _materialize_trusted_ppi_mapping( job, prefix="Ft.SPL", target="Ft", family="SPL", storage_key="soe", taxon_id="3562", query_id="FtGene1", subject_id="SoeGene1", ) ready = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", target_species=["Fagopyrum tataricum"], reference_species=["Spinacia oleracea"], family="SPL", ppi_reference_taxon_id="3562", ppi_reference_storage_key="soe", ppi_reference_selection_mode="tree_nearest", ppi_reference_user_confirmed=True, dependency_owner="ppi", ) pre_ortholog = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", target_species=["Fagopyrum tataricum"], reference_species=["Spinacia oleracea"], family="SPL", ignore_existing_ortholog=True, ppi_reference_taxon_id="3562", ppi_reference_storage_key="soe", ppi_reference_selection_mode="tree_nearest", ppi_reference_user_confirmed=True, dependency_owner="ppi", ) assert ready["status"] == "ready" assert ready["ortholog_mapping_source"].endswith("Ft.SPL.orthologs.txt") assert pre_ortholog["status"] == "needs_ortholog" assert pre_ortholog["ortholog_mapping_source"] == "" assert pre_ortholog["selected_string_species"] == "soe" assert pre_ortholog["candidates"][0]["selected_string_taxon_id"] == "3562" def test_ppi_kaks_preflight_recovers_prior_non_ath_reference_before_config_default(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" final_dir = tmp_path / "workspace" / "final" soe_dir = input_dir / "soe" input_dir.mkdir() soe_dir.mkdir() final_dir.mkdir(parents=True) (soe_dir / "soe.pep.fasta").write_text(">SoeGene1\nMA\n", encoding="utf-8") (input_dir / "At.pep.fasta").write_text(">AT1G01010\nMA\n", encoding="utf-8") (final_dir / "Ft.SPL.orthologs.txt").write_text( "FtGene1\tAT1G01010\t90\tFt\tAt\n", encoding="utf-8", ) soe_manifest = final_dir / "Ft.SPL.SPL.target-Ft.ref-soe.target_plus_reference.ortholog.manifest.json" soe_manifest.write_text(json.dumps({ "stage": "ortholog", "status": "success", "reference_species": ["soe"], "scope_id": "SPL.target-Ft.ref-soe.target_plus_reference", }), encoding="utf-8") at_manifest = final_dir / "Ft.SPL.SPL.target-Ft.ref-At.target_plus_reference.ppi.manifest.json" at_manifest.write_text(json.dumps({ "stage": "ppi", "status": "success", "reference_species": ["At"], "scope_id": "SPL.target-Ft.ref-At.target_plus_reference", }), encoding="utf-8") os.utime(soe_manifest, (1_000_000, 1_000_000)) os.utime(at_manifest, (2_000_000, 2_000_000)) resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", target_species=["Fagopyrum tataricum"], reference_species=["At"], requested_stages=["ppi"], family="SPL", ) assert resolution["reference_species"] == "" assert resolution["selected_string_species"] == "" assert resolution["status"] == "confirmation_required" assert resolution["issue_code"] == "ppi_reference_confirmation_required" assert resolution["mapping_reference_species"] == "" def test_kaks_preflight_recovers_non_ath_reference_and_prioritizes_missing_cds(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" final_dir = tmp_path / "workspace" / "final" soe_dir = input_dir / "soe" input_dir.mkdir() soe_dir.mkdir() final_dir.mkdir(parents=True) (soe_dir / "soe.pep.fasta").write_text(">SoeGene1\nMA\n", encoding="utf-8") (input_dir / "At.pep.fasta").write_text(">AT1G01010\nMA\n", encoding="utf-8") (input_dir / "At.cds.fasta").write_text(">AT1G01010\nATG\n", encoding="utf-8") (final_dir / "Ft.SPL.orthologs.txt").write_text( "FtGene1\tAT1G01010\t90\tFt\tAt\n", encoding="utf-8", ) (final_dir / "Ft.SPL.SPL.target-Ft.ref-soe.target_plus_reference.ortholog.manifest.json").write_text(json.dumps({ "stage": "ortholog", "status": "success", "reference_species": ["soe"], "scope_id": "SPL.target-Ft.ref-soe.target_plus_reference", }), encoding="utf-8") resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", target_species=["Fagopyrum tataricum"], reference_species=["At"], requested_stages=["kaks"], family="SPL", ) assert resolution["reference_species"] == "soe" assert resolution["selected_string_species"] == "soe" assert resolution["status"] == "needs_reference_data" assert resolution["issue_code"] == "kaks_reference_cds_missing" assert resolution["reference_bundle_status"]["missing_required"] == ["cds"] def test_ppi_species_resolution_reports_needs_ortholog_when_reference_pep_exists(tmp_path): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" final_dir = tmp_path / "workspace" / "final" input_dir.mkdir() final_dir.mkdir(parents=True) (input_dir / "soe").mkdir() (input_dir / "soe" / "soe.pep.fasta").write_text(">SoeGene1\nMA\n", encoding="utf-8") resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", target_species=["Ft"], reference_species=["Spinacia oleracea"], family="SPL", ppi_reference_taxon_id="3562", ppi_reference_storage_key="soe", ppi_reference_selection_mode="tree_nearest", ppi_reference_user_confirmed=True, dependency_owner="ppi", ) assert resolution["status"] == "needs_ortholog" assert resolution["issue_code"] == "ppi_requires_ortholog_mapping" assert resolution["reference_data_source"].endswith("soe/soe.pep.fasta") assert resolution["selected_string_species"] == "soe" def test_ppi_reference_bundle_download_requests_only_pep(tmp_path, monkeypatch): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) (tmp_path / "input").mkdir() (tmp_path / "input" / "soe").mkdir() (tmp_path / "workspace").mkdir() captured = {} monkeypatch.setattr( backend, "_select_ncbi_reference_assembly", lambda **kwargs: {"accession": "GCF_TEST", "organism_name": "Spinacia oleracea", "taxon_id": "3562"}, ) def fake_download_url(accession, file_types): captured["file_types"] = list(file_types) return "https://example.invalid/reference.zip" def fake_download(_url, zip_path, timeout_sec=300): zip_path.parent.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(zip_path, "w") as zf: zf.writestr("README.txt", "fake") return 4 monkeypatch.setattr(backend, "_ncbi_dataset_download_url", fake_download_url) monkeypatch.setattr(backend, "_download_url_to_file", fake_download) monkeypatch.setattr( backend, "_extract_ncbi_reference_bundle_zip", lambda **kwargs: {"pep": str(tmp_path / "input" / "soe" / "soe.pep.fasta")}, ) monkeypatch.setattr( backend, "_build_reference_bundle_id_map", lambda **kwargs: { "id_map_path": str(tmp_path / "input" / "soe" / "soe.pep_cds_idmap.tsv"), "pep_records": 1, "cds_records": 0, "pep_cds_overlap": 0, }, ) result = backend.download_reference_annotation_bundle( job=job, species_code="soe", species_name="Spinacia oleracea", taxon_id="3562", requested_stages=["ppi"], ) assert captured["file_types"] == ["pep"] assert result["status"] == "success" assert result["missing_required"] == [] assert result["requested_file_types"] == ["pep"] def test_ppi_execution_scope_canonicalizes_full_species_names_before_mapping_filter(tmp_path, monkeypatch): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" workspace = tmp_path / "workspace" final_dir = workspace / "final" logs_dir = workspace / "logs" input_dir.mkdir(parents=True) final_dir.mkdir(parents=True) logs_dir.mkdir(parents=True) ath_dir = input_dir / "ath" ath_dir.mkdir() (ath_dir / "ath.pep_cds_idmap.tsv").write_text( "species_code\tprotein_id\nath\tAT1G01010\n", encoding="utf-8" ) (final_dir / "Ft.SPL.final_members.id").write_text("FtGene1\n", encoding="utf-8") (final_dir / "Ft.SPL.orthologs.txt").write_text( "FtGene1\tAT1G01010\t90\tFt\tAt\n", encoding="utf-8", ) scope_file = final_dir / "Ft.SPL.full_name.scope.json" scope_file.write_text(json.dumps({ "family": "SPL", "prefix": "Ft.SPL", "scope_id": "SPL.target-Fagopyrum_tataricum.ref-Arabidopsis_thaliana.target_plus_reference", "scope_mode": "target_plus_reference", "target_species": ["Fagopyrum tataricum"], "reference_species": ["Arabidopsis thaliana"], }), encoding="utf-8") captured = {} def fake_run_cmd_logged(cmd, log_path, cwd=None): captured["cmd"] = cmd _write_fake_ppi_outputs(workspace / "ppi", prefix="Ft.SPL") return 0 monkeypatch.setattr(backend, "run_cmd_logged", fake_run_cmd_logged) monkeypatch.setattr(backend, "download_string_network_image", lambda *args, **kwargs: None) message, manifest = backend.run_stage( "ppi", job, "Ft.SPL", ppi_species="auto", ppi_score=200, scope_file=str(scope_file), analysis_route="target_plus_reference", ) gene_ids_file = backend.Path(captured["cmd"][captured["cmd"].index("--gene-ids") + 1]) assert "✅ PPI analysis completed" in message assert manifest and manifest.exists() assert captured["cmd"][captured["cmd"].index("--species") + 1] == "ath" assert gene_ids_file.read_text(encoding="utf-8").splitlines() == ["FtGene1"] resolution = backend.resolve_ppi_species_resolution( job=job, prefix="Ft.SPL", ppi_species="auto", scope_file=str(scope_file), family="SPL", ) assert resolution["status"] == "confirmation_required" assert resolution["issue_code"] == "ppi_reference_confirmation_required" assert resolution["required_next_stage"] == "" def test_ppi_auto_prefers_curated_members_when_available(tmp_path, monkeypatch): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" workspace = tmp_path / "workspace" final_dir = workspace / "final" logs_dir = workspace / "logs" final_dir.mkdir(parents=True) logs_dir.mkdir(parents=True) input_dir.mkdir(parents=True) curated_file = final_dir / "Target.Os.CRR.curated_members.id" final_file = final_dir / "Target.Os.CRR.final_members.id" curated_file.write_text("Os01t0810800-01\nOs09t0359500-01\n", encoding="utf-8") final_file.write_text( "Os01t0810800-01\nOs09t0359500-01\nOs05t0393100-01\n", encoding="utf-8", ) scope_file = final_dir / "Os.CRR.CRR.target-Os.ref-At.target_plus_reference.scope.json" _write_ppi_scope(scope_file) captured = {} def fake_run_cmd_logged(cmd, log_path, cwd=None): captured["cmd"] = cmd _write_fake_ppi_outputs(workspace / "ppi") return 0 monkeypatch.setattr(backend, "can_start_new_task", lambda *args, **kwargs: (True, "ok")) monkeypatch.setattr(backend, "run_cmd_logged", fake_run_cmd_logged) monkeypatch.setattr(backend, "download_string_network_image", lambda *args, **kwargs: None) message, manifest = backend.run_stage( "ppi", job, "Os.CRR", ppi_species="auto", ppi_score=400, scope_file=str(scope_file), analysis_route="target_plus_reference", ) cmd = captured["cmd"] assert "✅ PPI analysis completed" in message assert manifest and manifest.exists() assert cmd[cmd.index("--gene-ids") + 1] == str(curated_file) def test_ppi_projected_soe_uses_exact_mapping_and_same_species_id_map(tmp_path, monkeypatch): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" workspace = tmp_path / "workspace" final_dir = workspace / "final" logs_dir = workspace / "logs" final_dir.mkdir(parents=True) logs_dir.mkdir(parents=True) input_dir.mkdir(parents=True) soe_dir = input_dir / "soe" soe_dir.mkdir() id_map_file = soe_dir / "soe.pep_cds_idmap.tsv" id_map_file.write_text("species_code\tprotein_id\nsoe\tSoeGene1\n", encoding="utf-8") (final_dir / "Os.CRR.final_members.id").write_text("Os01t0810800-01\n", encoding="utf-8") mapping_file = final_dir / "Os.CRR.orthologs.txt" mapping_file.write_text("Os01t0810800-01\tSoeGene1\t90\tOs\tsoe\n", encoding="utf-8") scope_file = final_dir / "Os.CRR.CRR.target-Os.ref-soe.target_plus_reference.scope.json" _write_ppi_scope(scope_file, reference_species="soe") captured = {} def fake_run_cmd_logged(cmd, log_path, cwd=None): captured["cmd"] = cmd _write_fake_ppi_outputs(workspace / "ppi") return 0 monkeypatch.setattr(backend, "can_start_new_task", lambda *args, **kwargs: (True, "ok")) monkeypatch.setattr(backend, "run_cmd_logged", fake_run_cmd_logged) monkeypatch.setattr(backend, "download_string_network_image", lambda *args, **kwargs: None) message, manifest = backend.run_stage( "ppi", job, "Os.CRR", ppi_species="soe", ppi_score=400, scope_file=str(scope_file), analysis_route="target_plus_reference", ) cmd = captured["cmd"] assert "✅ PPI analysis completed" in message assert manifest and manifest.exists() assert cmd[cmd.index("--species") + 1] == "soe" assert cmd[cmd.index("--mapping") + 1] == str(mapping_file) assert cmd[cmd.index("--id-map") + 1] == str(id_map_file) def test_ppi_projected_soe_rejects_only_ath_id_map(tmp_path, monkeypatch): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" final_dir = tmp_path / "workspace" / "final" logs_dir = tmp_path / "workspace" / "logs" (input_dir / "ath").mkdir(parents=True) final_dir.mkdir(parents=True) logs_dir.mkdir(parents=True) ath_map = input_dir / "ath" / "ath.pep_cds_idmap.tsv" ath_map.write_text("species_code\tprotein_id\nath\tAT1G51790\n", encoding="utf-8") (final_dir / "Os.CRR.final_members.id").write_text("Os01t0810800-01\n", encoding="utf-8") (final_dir / "Os.CRR.orthologs.txt").write_text( "Os01t0810800-01\tSoeGene1\t90\tOs\tsoe\n", encoding="utf-8" ) scope_file = final_dir / "Os.CRR.CRR.target-Os.ref-soe.target_plus_reference.scope.json" _write_ppi_scope(scope_file, reference_species="soe") launched = [] monkeypatch.setattr(backend, "run_cmd_logged", lambda *args, **kwargs: launched.append(args)) message, _manifest = backend.run_stage( "ppi", job, "Os.CRR", ppi_species="soe", scope_file=str(scope_file), analysis_route="target_plus_reference", ) expected = input_dir / "soe" / "soe.pep_cds_idmap.tsv" assert launched == [] assert str(expected) in message assert "species-matched ID map" in message assert str(ath_map) not in message def test_ppi_projected_soe_rejects_missing_id_map(tmp_path, monkeypatch): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" final_dir = tmp_path / "workspace" / "final" logs_dir = tmp_path / "workspace" / "logs" input_dir.mkdir(parents=True) final_dir.mkdir(parents=True) logs_dir.mkdir(parents=True) (final_dir / "Os.CRR.final_members.id").write_text("Os01t0810800-01\n", encoding="utf-8") (final_dir / "Os.CRR.orthologs.txt").write_text( "Os01t0810800-01\tSoeGene1\t90\tOs\tsoe\n", encoding="utf-8" ) scope_file = final_dir / "Os.CRR.CRR.target-Os.ref-soe.target_plus_reference.scope.json" _write_ppi_scope(scope_file, reference_species="soe") launched = [] monkeypatch.setattr(backend, "run_cmd_logged", lambda *args, **kwargs: launched.append(args)) message, _manifest = backend.run_stage( "ppi", job, "Os.CRR", ppi_species="soe", scope_file=str(scope_file), analysis_route="target_plus_reference", ) expected = input_dir / "soe" / "soe.pep_cds_idmap.tsv" assert launched == [] assert message == f"❌ PPI projected analysis requires species-matched ID map: {expected}" def test_ppi_explicit_absolute_species_rejected_before_outside_id_map_lookup(tmp_path, monkeypatch): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "job" / "input"), workspace_dir=str(tmp_path / "job" / "workspace"), logs_dir=str(tmp_path / "job" / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "job" / "input" workspace = tmp_path / "job" / "workspace" final_dir = workspace / "final" outside_species = tmp_path / "outside" / "soe" outside_id_map = outside_species.with_name("soe.pep_cds_idmap.tsv") input_dir.mkdir(parents=True) final_dir.mkdir(parents=True) (workspace / "logs").mkdir(parents=True) outside_id_map.parent.mkdir(parents=True) outside_id_map.write_text("species_code\tprotein_id\nsoe\tSoeGene1\n", encoding="utf-8") (final_dir / "Os.CRR.final_members.id").write_text("Os01t0810800-01\n", encoding="utf-8") (final_dir / "Os.CRR.orthologs.txt").write_text( "Os01t0810800-01\tSoeGene1\t90\tOs\tsoe\n", encoding="utf-8" ) scope_file = final_dir / "Os.CRR.CRR.target-Os.ref-soe.target_plus_reference.scope.json" _write_ppi_scope(scope_file, reference_species="soe") launched = [] monkeypatch.setattr(backend, "can_start_new_task", lambda *args, **kwargs: (True, "ok")) monkeypatch.setattr(backend, "run_cmd_logged", lambda *args, **kwargs: launched.append(args)) requested_species = str(outside_species) message, _manifest = backend.run_stage( "ppi", job, "Os.CRR", ppi_species=requested_species, scope_file=str(scope_file), analysis_route="target_plus_reference", ) assert launched == [] assert "unsupported STRING species" in message assert requested_species in message assert str(outside_id_map) not in message def test_ppi_missing_authoritative_skill_never_selects_legacy_cli(tmp_path, monkeypatch): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) input_dir = tmp_path / "input" final_dir = tmp_path / "workspace" / "final" logs_dir = tmp_path / "workspace" / "logs" input_dir.mkdir(parents=True) final_dir.mkdir(parents=True) logs_dir.mkdir(parents=True) (final_dir / "Os.CRR.final_members.id").write_text( "Os01g0810800\n", encoding="utf-8" ) original_exists = backend.Path.exists def fake_exists(path): if str(path) == "skills/ppi/run.py": return False if str(path) == "apps/cli/run_ppi_v2.py": return True return original_exists(path) launched = [] def fail_if_launched(*args, **kwargs): launched.append((args, kwargs)) raise AssertionError("no PPI implementation may launch") monkeypatch.setattr(backend.Path, "exists", fake_exists) monkeypatch.setattr(backend, "can_start_new_task", lambda *args, **kwargs: (True, "ok")) monkeypatch.setattr(backend, "run_cmd_logged", fail_if_launched) message, manifest = backend.run_stage( "ppi", job, "Os.CRR", ppi_species="osa", ppi_score=400 ) assert "authoritative ppi skill" in message.lower() assert "legacy fallback disabled" in message assert "run_ppi_v2.py" not in message assert launched == [] expected_manifest = final_dir / "Os.CRR.ppi.manifest.json" assert manifest == expected_manifest payload = json.loads(expected_manifest.read_text(encoding="utf-8")) assert payload["stage"] == "ppi" assert payload["prefix"] == "Os.CRR" assert payload["status"] == "failed" assert "authoritative ppi skill" in payload["error"].lower() assert "legacy fallback disabled" in payload["error"] def test_string_network_image_falls_back_to_standard_png(tmp_path, monkeypatch): calls = [] class FakeResponse: headers = {"content-type": "image/png"} status_code = 200 def __init__(self, content, url): self._content = content self.url = url def raise_for_status(self): return None def iter_content(self, chunk_size=1): yield self._content def fake_post(url, data=None, timeout=None, stream=None, allow_redirects=None): assert allow_redirects is False calls.append(url) if "highres_image" in url: raise TimeoutError("highres timeout") return FakeResponse(b"\x89PNG\r\n\x1a\nstandard-png", url) monkeypatch.setitem(sys.modules, "requests", types.SimpleNamespace(post=fake_post)) output = tmp_path / "Os.CRR.ppi_network.string.png" result = backend.download_string_network_image( "https://string-db.org/cgi/network?identifiers=Os01g0810800&species=39947", output, ) assert result == output assert output.read_bytes() == b"\x89PNG\r\n\x1a\nstandard-png" assert calls == [ "https://string-db.org/api/highres_image/network", "https://string-db.org/api/image/network", ] def test_string_network_image_falls_back_to_svg(tmp_path, monkeypatch): calls = [] class FakeResponse: status_code = 200 def __init__(self, content_type, content, url): self.headers = {"content-type": content_type} self._content = content self.url = url def raise_for_status(self): return None def iter_content(self, chunk_size=1): yield self._content def fake_post(url, data=None, timeout=None, stream=None, allow_redirects=None): assert allow_redirects is False calls.append(url) if "svg" in url: return FakeResponse("image/svg+xml", b"", url) raise TimeoutError("png timeout") monkeypatch.setitem(sys.modules, "requests", types.SimpleNamespace(post=fake_post)) output = tmp_path / "Os.CRR.ppi_network.string.png" result = backend.download_string_network_image( "https://string-db.org/cgi/network?identifiers=Os01g0810800&species=39947", output, ) assert result == tmp_path / "Os.CRR.ppi_network.string.svg" assert result.read_bytes() == b"" assert calls == [ "https://string-db.org/api/highres_image/network", "https://string-db.org/api/image/network", "https://string-db.org/api/svg/network", ] def test_create_alphafold2_links_marks_single_protein_entries(tmp_path): id_resolution = tmp_path / "Os.CRR.ppi_id_resolution.tsv" id_resolution.write_text( "\t".join(["raw_id", "normalized_id", "selected_query_id", "string_id", "mapping_status", "method", "reason"]) + "\n" + "\t".join(["Os01t0810800-01", "Os01g0810800", "Os01g0810800", "", "candidate", "species_rule", ""]) + "\n", encoding="utf-8", ) network = tmp_path / "Os.CRR.ppi_network.tsv" network.write_text("protein1\tprotein2\tscore\nA0A0N7KDY0\tQ60ER5_ORYSJ\t0.234\n", encoding="utf-8") output = tmp_path / "Os.CRR.ppi_alphafold2_links.tsv" result = backend.create_alphafold2_links( id_resolution_file=id_resolution, network_file=network, output_file=output, ) assert result == output rows = output.read_text(encoding="utf-8").splitlines() assert rows[0].split("\t") == [ "source_id", "lookup_id", "link_type", "alphafold_db_url", "structure_type", "note", ] assert "AlphaFold DB/Fold2 single protein" in rows[1] assert "complex" not in output.read_text(encoding="utf-8").lower() assert "https://alphafold.ebi.ac.uk/entry/A0A0N7KDY0" in output.read_text(encoding="utf-8") def test_sync_ppi_manifest_artifacts_points_to_final_files(tmp_path): final_dir = tmp_path / "final" final_dir.mkdir() manifest = final_dir / "Os.CRR.ppi.manifest.json" for name in [ "Os.CRR.ppi_network.tsv", "Os.CRR.ppi_report.md", "Os.CRR.ppi_id_resolution.tsv", "Os.CRR.ppi_url.txt", ]: (final_dir / name).write_text("x\n", encoding="utf-8") manifest.write_text( json.dumps({ "manifest_version": "1.0", "stage": "ppi", "status": "success", "prefix": "Os.CRR", "artifacts": { "network": {"path": "workspace/job/ppi/Os.CRR_network.tsv", "type": "tsv"}, "report": {"path": "workspace/job/ppi/Os.CRR_report.md", "type": "markdown"}, "id_resolution": {"path": "workspace/job/ppi/Os.CRR_id_resolution.tsv", "type": "tsv"}, "string_url": {"path": "workspace/job/ppi/Os.CRR_url.txt", "type": "txt"}, }, }), encoding="utf-8", ) backend.sync_ppi_manifest_final_artifacts(manifest, final_dir, "Os.CRR") data = json.loads(manifest.read_text(encoding="utf-8")) assert data["artifacts"]["network"]["path"] == "final/Os.CRR.ppi_network.tsv" assert data["artifacts"]["report"]["path"] == "final/Os.CRR.ppi_report.md" assert data["artifacts"]["id_resolution"]["path"] == "final/Os.CRR.ppi_id_resolution.tsv" assert data["artifacts"]["string_url"]["path"] == "final/Os.CRR.ppi_url.txt" assert data["artifacts"]["network"]["full_path"] == str((final_dir / "Os.CRR.ppi_network.tsv").absolute()) def test_prepare_ppi_reset_inputs_creates_isolated_diff_and_manifest(tmp_path): final_dir = tmp_path / "final" final_dir.mkdir() source = final_dir / "Target.Os.CRR.curated_members.id" source.write_text("Os01g0810800\nOs09g0359500\nOs05g0393100\n", encoding="utf-8") original_ppi = final_dir / "Os.CRR.ppi_network.tsv" original_ppi.write_text("protein1\tprotein2\tscore\nA\tB\t0.9\n", encoding="utf-8") result = backend.prepare_ppi_reset_inputs( final_dir=final_dir, prefix="Os.CRR", source_gene_id_file=source, submitted_gene_ids=["Os01g0810800", "Os05g0393100", "Os07g0501800"], instruction="remove one member and add one candidate", ppi_species="osa", ppi_score=200, ) reset_dir = final_dir / "ppi_reset" / "reset_001" assert result["reset_id"] == "reset_001" assert result["reset_dir"] == reset_dir assert (reset_dir / "Os.CRR.ppi_reset_input.id").read_text(encoding="utf-8").splitlines() == [ "Os01g0810800", "Os05g0393100", "Os07g0501800", ] diff_lines = (reset_dir / "Os.CRR.ppi_reset_diff.tsv").read_text(encoding="utf-8").splitlines() assert "Os09g0359500\tdeleted" in diff_lines assert "Os07g0501800\tadded" in diff_lines manifest = json.loads((reset_dir / "Os.CRR.ppi_reset.manifest.json").read_text(encoding="utf-8")) assert manifest["reset_id"] == "reset_001" assert manifest["source_gene_id_file"] == str(source) assert manifest["input_gene_count"] == 3 assert manifest["deleted_count"] == 1 assert manifest["added_count"] == 1 assert manifest["parameters"]["ppi_species"] == "osa" assert original_ppi.read_text(encoding="utf-8").startswith("protein1\tprotein2") def test_prepare_ppi_reset_inputs_uses_next_reset_directory(tmp_path): final_dir = tmp_path / "final" source = final_dir / "Target.Os.CRR.curated_members.id" source.parent.mkdir(parents=True) source.write_text("A\nB\n", encoding="utf-8") (final_dir / "ppi_reset" / "reset_001").mkdir(parents=True) result = backend.prepare_ppi_reset_inputs( final_dir=final_dir, prefix="Os.CRR", source_gene_id_file=source, submitted_gene_ids=["A"], instruction="second reset", ppi_species="osa", ppi_score=200, ) assert result["reset_id"] == "reset_002" assert result["reset_dir"] == final_dir / "ppi_reset" / "reset_002" def test_run_ppi_reset_analysis_writes_outputs_under_reset_dir(tmp_path, monkeypatch): job = backend.JobInfo( job_id="job_test", input_dir=str(tmp_path / "input"), workspace_dir=str(tmp_path / "workspace"), logs_dir=str(tmp_path / "workspace" / "logs"), created_at="", ) final_dir = tmp_path / "workspace" / "final" logs_dir = tmp_path / "workspace" / "logs" final_dir.mkdir(parents=True) logs_dir.mkdir(parents=True) source = final_dir / "Target.Os.CRR.curated_members.id" source.write_text("A\nB\nC\n", encoding="utf-8") captured = {} def fake_run_cmd_logged(cmd, log_path, cwd=None): captured["cmd"] = cmd out_dir = cmd[cmd.index("--output") + 1] ppi_dir = backend.Path(out_dir) ppi_dir.mkdir(parents=True, exist_ok=True) _write_fake_ppi_outputs(ppi_dir, prefix="Os.CRR") return 0 monkeypatch.setattr(backend, "run_cmd_logged", fake_run_cmd_logged) monkeypatch.setattr(backend, "download_string_network_image", lambda url, path: None) result = backend.run_ppi_reset_analysis( job=job, prefix="Os.CRR", source_gene_id_file=source, submitted_gene_ids=["A", "C"], instruction="remove B", ppi_species="osa", ppi_score=200, ) reset_dir = final_dir / "ppi_reset" / "reset_001" assert result["success"] is True assert captured["cmd"][captured["cmd"].index("--gene-ids") + 1] == str(reset_dir / "Os.CRR.ppi_reset_input.id") assert (reset_dir / "Os.CRR.ppi_network.tsv").exists() assert (reset_dir / "Os.CRR.ppi_report.md").exists() assert (reset_dir / "Os.CRR.ppi_id_resolution.tsv").exists() assert (reset_dir / "Os.CRR.ppi_url.txt").exists() assert (reset_dir / "Os.CRR.ppi_alphafold2_links.tsv").exists() assert not (final_dir / "Os.CRR.ppi_network.tsv").exists() manifest = json.loads((reset_dir / "Os.CRR.ppi_reset.manifest.json").read_text(encoding="utf-8")) assert manifest["status"] == "success" assert manifest["artifacts"]["network"]["path"] == "final/ppi_reset/reset_001/Os.CRR.ppi_network.tsv"