767 lines
28 KiB
Python
767 lines
28 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from openpyxl import Workbook
|
|
|
|
from apps.web import pipeline_backend as backend
|
|
from apps.web.vue3 import main as web_main
|
|
|
|
|
|
def _source_block(source: str, start: str, end: str) -> str:
|
|
start_index = source.index(start)
|
|
return source[start_index:source.index(end, start_index)]
|
|
|
|
|
|
def _run_node(source: str) -> dict:
|
|
completed = subprocess.run(
|
|
["/usr/bin/node", "-e", source],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
return json.loads(completed.stdout)
|
|
|
|
|
|
def test_confirmed_numeric_reference_uses_storage_key_for_local_id_map(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
input_dir = tmp_path / "input"
|
|
workspace = tmp_path / "workspace"
|
|
final_dir = workspace / "final"
|
|
logs_dir = workspace / "logs"
|
|
input_dir.mkdir()
|
|
final_dir.mkdir(parents=True)
|
|
logs_dir.mkdir(parents=True)
|
|
workbook = Workbook()
|
|
sheet = workbook.active
|
|
sheet.title = "config"
|
|
sheet.append(["Species_Code", "Species_Name", "TaxID"])
|
|
sheet.append(["Sv", "Setaria viridis", "4556"])
|
|
workbook.save(input_dir / "config.xlsx")
|
|
|
|
storage_dir = input_dir / "string_taxon_4555"
|
|
storage_dir.mkdir()
|
|
expected_id_map = storage_dir / "string_taxon_4555.pep_cds_idmap.tsv"
|
|
expected_id_map.write_text(
|
|
"species_code\tprotein_id\nstring_taxon_4555\tSeita.1\n",
|
|
encoding="utf-8",
|
|
)
|
|
(storage_dir / "string_taxon_4555.pep.fasta").write_text(
|
|
">Seita.1\nMA\n",
|
|
encoding="utf-8",
|
|
)
|
|
(final_dir / "Sv.HKL.final_members.id").write_text("SvGene1\n", encoding="utf-8")
|
|
(final_dir / "Sv.HKL.orthologs.txt").write_text(
|
|
"SvGene1\tSeita.1\t91\tSv\tstring_taxon_4555\n",
|
|
encoding="utf-8",
|
|
)
|
|
scope_file = final_dir / "Sv.HKL.target-ref.scope.json"
|
|
scope_file.write_text(
|
|
json.dumps(
|
|
{
|
|
"family": "HKL",
|
|
"prefix": "Sv.HKL",
|
|
"scope_id": "HKL.target-Sv.ref-string_taxon_4555.target_plus_reference",
|
|
"scope_mode": "target_plus_reference",
|
|
"target_species": ["Sv"],
|
|
"reference_species": ["string_taxon_4555"],
|
|
"selected_species": ["Sv", "string_taxon_4555"],
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
job = backend.JobInfo(
|
|
job_id="job_task5_storage",
|
|
input_dir=str(input_dir),
|
|
workspace_dir=str(workspace),
|
|
logs_dir=str(logs_dir),
|
|
created_at="",
|
|
)
|
|
launched: list[list[str]] = []
|
|
monkeypatch.setattr(backend, "can_start_new_task", lambda: (True, "ok"))
|
|
monkeypatch.setattr(
|
|
backend,
|
|
"run_cmd_logged",
|
|
lambda cmd, *_args, **_kwargs: launched.append(list(cmd)) or 1,
|
|
)
|
|
|
|
message, _manifest = backend.run_stage(
|
|
"ppi",
|
|
job,
|
|
"Sv.HKL",
|
|
scope_file=str(scope_file),
|
|
ppi_species="4555",
|
|
normalization_species_label="sit",
|
|
)
|
|
|
|
wrong_id_map = input_dir / "4555" / "4555.pep_cds_idmap.tsv"
|
|
assert str(wrong_id_map) not in message
|
|
assert launched
|
|
command = launched[0]
|
|
assert command[command.index("--species") + 1] == "4555"
|
|
assert command[command.index("--id-map") + 1] == str(expected_id_map)
|
|
|
|
|
|
def test_ppi_owner_fields_survive_execute_and_worker_transport() -> None:
|
|
from apps.web.vue3.main import ExecuteRequest
|
|
from tasks import normalize_stage_params
|
|
|
|
request = ExecuteRequest(
|
|
stage="ppi",
|
|
job_id="job_task5_transport",
|
|
prefix="Sv.HKL",
|
|
ppiReferenceTaxonId="4555",
|
|
ppiReferenceStorageKey="string_taxon_4555",
|
|
ppiReferenceSelectionMode="tree_nearest",
|
|
ppiReferenceUserConfirmed=True,
|
|
dependencyOwner="ppi",
|
|
)
|
|
params, _profile = normalize_stage_params("ppi", request.dict())
|
|
|
|
assert params["ppiReferenceTaxonId"] == "4555"
|
|
assert params["ppiReferenceStorageKey"] == "string_taxon_4555"
|
|
assert params["ppiReferenceSelectionMode"] == "tree_nearest"
|
|
assert params["ppiReferenceUserConfirmed"] is True
|
|
assert params["dependencyOwner"] == "ppi"
|
|
|
|
|
|
def test_execute_scope_uses_confirmed_ppi_owner_storage_without_biological_scope_pollution() -> None:
|
|
request = web_main.ExecuteRequest(
|
|
stage="ortholog",
|
|
job_id="job_owner_scope",
|
|
prefix="Sv.HKL",
|
|
scopeMode="target_only",
|
|
targetSpecies=["Sv"],
|
|
ppiReferenceTaxonId="4555",
|
|
ppiReferenceStorageKey="string_taxon_4555",
|
|
ppiReferenceUserConfirmed=True,
|
|
dependencyOwner="ppi",
|
|
)
|
|
scope_params = {
|
|
"scopeMode": "target_only", "targetSpecies": ["Sv"],
|
|
"referenceSpecies": [], "refSpeciesList": [],
|
|
}
|
|
|
|
technical_refs, ppi_owned_ortholog, needs_fallback = (
|
|
web_main._prepare_execute_technical_reference_scope(request, scope_params)
|
|
)
|
|
|
|
assert technical_refs == ["string_taxon_4555"]
|
|
assert ppi_owned_ortholog is True
|
|
assert needs_fallback is False
|
|
assert scope_params == {
|
|
"scopeMode": "target_only", "targetSpecies": ["Sv"],
|
|
"referenceSpecies": [], "refSpeciesList": [],
|
|
"comparativeSpecies": [], "scopeFile": "",
|
|
}
|
|
|
|
|
|
def test_execute_scope_keeps_ordinary_ortholog_reference_fallback_enabled() -> None:
|
|
request = web_main.ExecuteRequest(
|
|
stage="ortholog", job_id="job_ordinary_scope", prefix="Sv.HKL",
|
|
scopeMode="target_only", targetSpecies=["Sv"],
|
|
)
|
|
|
|
scope_params = {
|
|
"scopeMode": "target_only", "targetSpecies": ["Sv"],
|
|
"referenceSpecies": [], "refSpeciesList": [],
|
|
}
|
|
technical_refs, ppi_owned_ortholog, needs_fallback = (
|
|
web_main._prepare_execute_technical_reference_scope(request, scope_params)
|
|
)
|
|
|
|
assert technical_refs == []
|
|
assert ppi_owned_ortholog is False
|
|
assert needs_fallback is True
|
|
assert scope_params["scopeMode"] == "target_only"
|
|
|
|
|
|
def test_execute_scope_kaks_ignores_ppi_owner_only_reference_fields() -> None:
|
|
request = web_main.ExecuteRequest(
|
|
stage="kaks",
|
|
job_id="job_kaks_scope",
|
|
prefix="Sv.HKL",
|
|
ppiReferenceTaxonId="4555",
|
|
ppiReferenceStorageKey="string_taxon_4555",
|
|
ppiReferenceUserConfirmed=True,
|
|
dependencyOwner="ppi",
|
|
)
|
|
|
|
scope_params = {
|
|
"scopeMode": "target_plus_reference", "targetSpecies": ["Sv"],
|
|
"referenceSpecies": [], "refSpeciesList": [],
|
|
}
|
|
technical_refs, ppi_owned_ortholog, needs_fallback = (
|
|
web_main._prepare_execute_technical_reference_scope(request, scope_params)
|
|
)
|
|
|
|
assert technical_refs == []
|
|
assert ppi_owned_ortholog is False
|
|
assert needs_fallback is True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("confirmed", "storage_key"),
|
|
[(False, "string_taxon_4555"), (True, "")],
|
|
)
|
|
def test_execute_rejects_incomplete_ppi_owned_ortholog_before_scope_or_enqueue(
|
|
monkeypatch, confirmed: bool, storage_key: str,
|
|
) -> None:
|
|
scope_called = False
|
|
|
|
def fail_if_scope_is_built(**_kwargs):
|
|
nonlocal scope_called
|
|
scope_called = True
|
|
raise ValueError("scope builder must not run")
|
|
|
|
monkeypatch.setattr(web_main, "_ensure_gene_family_scope_file", fail_if_scope_is_built)
|
|
request = web_main.ExecuteRequest(
|
|
stage="ortholog", job_id="job_incomplete_owner", prefix="Sv.HKL",
|
|
scopeMode="target_plus_reference", targetSpecies=["Sv"],
|
|
referenceSpecies=["At"], refSpeciesList=["At"],
|
|
ppiKaksReferenceSpecies=["At"], ppiReferenceSpecies="At",
|
|
referenceBundleSpecies="At", ppiReferenceTaxonId="4555",
|
|
ppiReferenceStorageKey=storage_key,
|
|
ppiReferenceUserConfirmed=confirmed, dependencyOwner="ppi",
|
|
)
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
asyncio.run(web_main.execute_stage(request))
|
|
|
|
assert exc_info.value.status_code == 400
|
|
assert exc_info.value.detail["code"] == "ppi_owner_reference_incomplete"
|
|
assert scope_called is False
|
|
|
|
|
|
def test_execute_normalizes_polluted_confirmed_ppi_owner_before_scope_creation(monkeypatch) -> None:
|
|
captured = {}
|
|
|
|
def capture_scope(**kwargs):
|
|
captured.update(kwargs["params"])
|
|
raise ValueError("captured normalized scope")
|
|
|
|
monkeypatch.setattr(web_main, "_ensure_gene_family_scope_file", capture_scope)
|
|
request = web_main.ExecuteRequest(
|
|
stage="ortholog", job_id="job_confirmed_owner", prefix="Sv.HKL",
|
|
scopeMode="target_plus_reference", targetSpecies=["Sv"],
|
|
referenceSpecies=["At"], refSpeciesList=["At"],
|
|
ppiKaksReferenceSpecies=["At"], ppiReferenceSpecies="At",
|
|
referenceBundleSpecies="At", ppiReferenceTaxonId="4555",
|
|
ppiReferenceStorageKey="string_taxon_4555",
|
|
ppiReferenceUserConfirmed=True, dependencyOwner="ppi",
|
|
)
|
|
|
|
with pytest.raises(HTTPException):
|
|
asyncio.run(web_main.execute_stage(request))
|
|
|
|
assert captured["scopeMode"] == "target_only"
|
|
assert captured["targetSpecies"] == ["Sv"]
|
|
assert captured["referenceSpecies"] == []
|
|
assert captured["refSpeciesList"] == []
|
|
assert [*captured["targetSpecies"], *captured["referenceSpecies"]] == ["Sv"]
|
|
|
|
|
|
def test_execute_preserves_valid_target_only_scope_identity_for_confirmed_ppi_owner(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
from gfa_core.mas import resource_governor
|
|
from tasks import execute_single_stage_task
|
|
|
|
scope_file = tmp_path / "Sv.HKL.biological.scope.json"
|
|
scope_file.write_text(
|
|
json.dumps(
|
|
{
|
|
"family": "HKL",
|
|
"prefix": "Sv.HKL",
|
|
"scope_id": "fixed-biological-target-only-scope",
|
|
"scope_mode": "target_only",
|
|
"target_species": ["Sv"],
|
|
"reference_species": [],
|
|
"comparative_species": ["Os"],
|
|
"selected_species": ["Sv"],
|
|
"analysis": {"Family": "HKL", "context_marker": "preserve-me"},
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
scope_called = False
|
|
|
|
def fail_if_scope_is_rebuilt(**_kwargs):
|
|
nonlocal scope_called
|
|
scope_called = True
|
|
raise AssertionError("a validated existing owner scope must be reused")
|
|
|
|
monkeypatch.setattr(web_main, "_ensure_gene_family_scope_file", fail_if_scope_is_rebuilt)
|
|
isolated_apply_async = Mock()
|
|
isolated_apply_async.return_value.id = "isolated-valid-owner-scope"
|
|
monkeypatch.setattr(execute_single_stage_task, "apply_async", isolated_apply_async)
|
|
monkeypatch.setattr(web_main, "_ensure_celery_queue_health", lambda *_args, **_kwargs: ({"available": True}, None))
|
|
monkeypatch.setattr(resource_governor, "record_task", lambda **_kwargs: {})
|
|
request = web_main.ExecuteRequest(
|
|
stage="ortholog", job_id="job_owner_existing_scope", prefix="Sv.HKL",
|
|
scopeFile=str(scope_file), scopeMode="target_plus_reference",
|
|
targetSpecies=["WrongTarget"], referenceSpecies=["At"],
|
|
refSpeciesList=["At"], comparativeSpecies=["Zm"],
|
|
ppiKaksReferenceSpecies=["At"], ppiReferenceSpecies="At",
|
|
referenceBundleSpecies="At", ppiReferenceTaxonId="4555",
|
|
ppiReferenceStorageKey="string_taxon_4555",
|
|
ppiReferenceSelectionMode="tree_nearest",
|
|
ppiReferenceUserConfirmed=True, dependencyOwner="ppi",
|
|
)
|
|
|
|
asyncio.run(web_main.execute_stage(request))
|
|
|
|
assert scope_called is False
|
|
submitted_params = isolated_apply_async.call_args.kwargs["kwargs"]["params"]
|
|
assert submitted_params["scopeFile"] == str(scope_file)
|
|
assert submitted_params["stageContext"]["scope_id"] == "fixed-biological-target-only-scope"
|
|
assert submitted_params["scopeMode"] == "target_only"
|
|
assert submitted_params["targetSpecies"] == ["Sv"]
|
|
assert submitted_params["referenceSpecies"] == []
|
|
assert submitted_params["refSpeciesList"] == []
|
|
assert submitted_params["comparativeSpecies"] == ["Os"]
|
|
assert submitted_params["ppiReferenceTaxonId"] == "4555"
|
|
assert submitted_params["ppiReferenceStorageKey"] == "string_taxon_4555"
|
|
assert submitted_params["ppiReferenceSelectionMode"] == "tree_nearest"
|
|
assert submitted_params["ppiReferenceUserConfirmed"] is True
|
|
assert submitted_params["dependencyOwner"] == "ppi"
|
|
assert submitted_params["ppiKaksReferenceSpecies"] == []
|
|
assert submitted_params["ppiReferenceSpecies"] == ""
|
|
assert submitted_params["referenceBundleSpecies"] == ""
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("scope_payload", "expected_code"),
|
|
[
|
|
("{not-json", "ppi_owner_scope_invalid"),
|
|
(
|
|
json.dumps(
|
|
{
|
|
"family": "HKL",
|
|
"scope_id": "wrong-mode-scope",
|
|
"scope_mode": "target_plus_reference",
|
|
"target_species": ["Sv"],
|
|
"reference_species": ["At"],
|
|
"selected_species": ["Sv", "At"],
|
|
}
|
|
),
|
|
"ppi_owner_scope_mode_incompatible",
|
|
),
|
|
],
|
|
)
|
|
def test_execute_fails_closed_for_invalid_ppi_owner_scope_file(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
scope_payload: str,
|
|
expected_code: str,
|
|
) -> None:
|
|
from tasks import execute_single_stage_task
|
|
|
|
scope_file = tmp_path / "owner.scope.json"
|
|
scope_file.write_text(scope_payload, encoding="utf-8")
|
|
scope_called = False
|
|
|
|
def fail_if_scope_is_built(**_kwargs):
|
|
nonlocal scope_called
|
|
scope_called = True
|
|
raise AssertionError("invalid owner scope must fail before scope construction")
|
|
|
|
monkeypatch.setattr(web_main, "_ensure_gene_family_scope_file", fail_if_scope_is_built)
|
|
isolated_apply_async = Mock()
|
|
isolated_apply_async.return_value.id = "must-not-submit-invalid-owner-scope"
|
|
monkeypatch.setattr(execute_single_stage_task, "apply_async", isolated_apply_async)
|
|
request = web_main.ExecuteRequest(
|
|
stage="ortholog", job_id="job_owner_invalid_scope", prefix="Sv.HKL",
|
|
scopeFile=str(scope_file), targetSpecies=["Sv"],
|
|
ppiReferenceTaxonId="4555", ppiReferenceStorageKey="string_taxon_4555",
|
|
ppiReferenceUserConfirmed=True, dependencyOwner="ppi",
|
|
)
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
asyncio.run(web_main.execute_stage(request))
|
|
|
|
assert exc_info.value.status_code == 400
|
|
assert exc_info.value.detail["code"] == expected_code
|
|
assert scope_called is False
|
|
isolated_apply_async.assert_not_called()
|
|
|
|
|
|
def test_ppi_owner_fields_are_forwarded_without_becoming_global_reference_scope() -> None:
|
|
main_source = Path("apps/web/vue3/main.py").read_text(encoding="utf-8")
|
|
stage_tools_source = Path("gfa_core/mas/tools/stage_tools.py").read_text(encoding="utf-8")
|
|
technical_start = main_source.index("technical_reference_species: list[str] = []")
|
|
technical_end = main_source.index("gene_family_identity_stages =", technical_start)
|
|
technical_block = main_source[technical_start:technical_end]
|
|
|
|
for field in (
|
|
"ppiReferenceTaxonId",
|
|
"ppiReferenceStorageKey",
|
|
"ppiReferenceSelectionMode",
|
|
"ppiReferenceUserConfirmed",
|
|
"dependencyOwner",
|
|
):
|
|
assert f'"{field}": getattr(req' in main_source or f'"{field}": bool(getattr(req' in main_source
|
|
assert field in stage_tools_source
|
|
assert field not in technical_block
|
|
|
|
|
|
def test_frontend_requires_confirmation_and_keeps_ppi_owner_state_isolated() -> None:
|
|
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
|
assert "const confirmNearestPpiReferenceCandidate = async" in html
|
|
assert "resolution.candidates" in html
|
|
assert ".map((candidate, index) =>" in html
|
|
assert "ppiReferenceTaxonId" in html
|
|
assert "ppiReferenceStorageKey" in html
|
|
assert "ppiReferenceSelectionMode" in html
|
|
assert "ppiReferenceUserConfirmed" in html
|
|
assert "dependencyOwner" in html
|
|
assert "preflight.resolution?.status === 'confirmation_required'" in html
|
|
assert "preflight = await runPpiSpeciesResolutionPreflight" in html
|
|
assert "const applyPpiOwnerSelectionToBatch =" in html
|
|
|
|
|
|
def test_confirmation_applies_owner_then_repeats_preflight() -> None:
|
|
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
|
owner_block = _source_block(
|
|
html,
|
|
"const ensurePpiKaksDependencyOwner = async",
|
|
"const ensureOrthologProducerForPpiKaks = async",
|
|
)
|
|
|
|
assert "preflight.resolution?.status === 'confirmation_required'" in owner_block
|
|
assert "await confirmNearestPpiReferenceCandidate(preflight.resolution)" in owner_block
|
|
assert "applyPpiOwnerSelectionToBatch(ownerSelection" in owner_block
|
|
assert owner_block.count("preflight = await runPpiSpeciesResolutionPreflight") >= 2
|
|
|
|
|
|
def test_ppi_owner_selection_never_mutates_shared_reference_scope() -> None:
|
|
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
|
selection_block = _source_block(
|
|
html,
|
|
"const applyPpiOwnerSelectionToBatch =",
|
|
"const releaseDirectTargetPpiFromOrtholog =",
|
|
)
|
|
|
|
for field in (
|
|
"ppiReferenceTaxonId",
|
|
"ppiReferenceStorageKey",
|
|
"ppiReferenceSelectionMode",
|
|
"ppiReferenceUserConfirmed",
|
|
"dependencyOwner",
|
|
):
|
|
assert field in selection_block
|
|
assert "getStageId(stage) !== 'ppi'" in selection_block
|
|
assert "ppiKaksReferenceSpecies" not in selection_block
|
|
assert "referenceSpecies:" not in selection_block
|
|
assert "refSpeciesList:" not in selection_block
|
|
assert "scopeMode:" not in selection_block
|
|
|
|
|
|
def test_owner_request_reads_selection_and_keeps_kaks_independent() -> None:
|
|
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
|
request_block = _source_block(
|
|
html,
|
|
"const buildPpiKaksDependencyRequest =",
|
|
"const attachOrthologDependencyToOwners =",
|
|
)
|
|
owner_block = _source_block(
|
|
html,
|
|
"const ensurePpiKaksDependencyOwner = async",
|
|
"const ensureOrthologProducerForPpiKaks = async",
|
|
)
|
|
|
|
for field in (
|
|
"ppiReferenceTaxonId",
|
|
"ppiReferenceStorageKey",
|
|
"ppiReferenceSelectionMode",
|
|
"ppiReferenceUserConfirmed",
|
|
"dependencyOwner",
|
|
):
|
|
assert field in request_block
|
|
assert "const requestedStages = [ownerStageId]" in owner_block
|
|
assert "dependencyOwner: ownerStageId" in request_block
|
|
assert "ownerStageId === 'ppi'" in request_block
|
|
|
|
|
|
def test_download_completion_reapplies_owner_without_shared_merge() -> None:
|
|
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
|
monitor_block = _source_block(
|
|
html,
|
|
"const monitorPpiReferenceBundleDownload =",
|
|
"const ppiHasNearReferenceBeforeArabidopsis =",
|
|
)
|
|
owner_block = _source_block(
|
|
html,
|
|
"const ensurePpiKaksDependencyOwner = async",
|
|
"const ensureOrthologProducerForPpiKaks = async",
|
|
)
|
|
|
|
assert "applyPpiOwnerSelectionToBatch(ownerSelection" in monitor_block
|
|
assert "requestedStages.includes('kaks')" in monitor_block
|
|
assert "mergePpiReferenceSpeciesIntoBatch(refCode, ['kaks'])" in monitor_block
|
|
assert "mergePpiReferenceSpeciesIntoBatch(refCode, requestedStages)" not in monitor_block
|
|
assert "applyPpiOwnerSelectionToBatch(ownerSelection" in owner_block
|
|
|
|
|
|
def test_arabidopsis_fallback_is_unselected_until_explicit_confirmation() -> None:
|
|
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
|
fallback_request = _source_block(
|
|
html,
|
|
"const requestPpiDownloadFallbackDecision =",
|
|
"const clearPpiKaksReferencePause =",
|
|
)
|
|
|
|
assert "defaultDecision: 'skip'" in fallback_request
|
|
assert "confirmParams.ppiKaksFallbackDecision: 'skip'" not in html
|
|
assert "ppiKaksFallbackDecision: 'skip'" in html
|
|
assert "confirmParams.ppiKaksFallbackDecision = 'skip'" in html
|
|
|
|
|
|
def test_final_ppi_request_dynamically_preserves_owner_and_project_scope() -> None:
|
|
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
|
helper = _source_block(
|
|
html,
|
|
"const applyPpiOwnerFieldsToRequest =",
|
|
"const applyGeneFamilyContextToRequest =",
|
|
)
|
|
result = _run_node(
|
|
helper
|
|
+ """
|
|
const request = {
|
|
referenceSpecies: ['ProjectRef'],
|
|
refSpeciesList: ['ProjectRef'],
|
|
scopeMode: 'target_plus_reference',
|
|
ppiKaksReferenceSpecies: ['KaKsRef'],
|
|
ppiReferenceSpecies: 'KaKsRef',
|
|
referenceBundleSpecies: 'KaKsRef'
|
|
};
|
|
const batch = {
|
|
ppiReferenceTaxonId: '4555',
|
|
ppiReferenceStorageKey: 'string_taxon_4555',
|
|
ppiReferenceSelectionMode: 'tree_nearest',
|
|
ppiReferenceUserConfirmed: true,
|
|
dependencyOwner: 'ppi'
|
|
};
|
|
console.log(JSON.stringify(applyPpiOwnerFieldsToRequest(request, 'ppi', {}, batch, {})));
|
|
"""
|
|
)
|
|
|
|
assert result["ppiReferenceTaxonId"] == "4555"
|
|
assert result["ppiReferenceStorageKey"] == "string_taxon_4555"
|
|
assert result["ppiReferenceSelectionMode"] == "tree_nearest"
|
|
assert result["ppiReferenceUserConfirmed"] is True
|
|
assert result["dependencyOwner"] == "ppi"
|
|
assert result["referenceSpecies"] == ["ProjectRef"]
|
|
assert result["refSpeciesList"] == ["ProjectRef"]
|
|
assert result["scopeMode"] == "target_plus_reference"
|
|
for forbidden in ("ppiKaksReferenceSpecies", "ppiReferenceSpecies", "referenceBundleSpecies"):
|
|
assert forbidden not in result
|
|
|
|
|
|
def test_joint_batch_ppi_request_dynamically_ignores_kaks_reference() -> None:
|
|
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
|
helper = _source_block(
|
|
html,
|
|
"const applyPpiOwnerFieldsToRequest =",
|
|
"const applyGeneFamilyContextToRequest =",
|
|
)
|
|
result = _run_node(
|
|
helper
|
|
+ """
|
|
const request = { referenceSpecies: ['ProjectRef'], refSpeciesList: ['ProjectRef'] };
|
|
const batch = {
|
|
ppiKaksReferenceSpecies: ['KaKsRef'],
|
|
ppiReferenceSpecies: 'KaKsRef',
|
|
referenceBundleSpecies: 'KaKsRef',
|
|
ppiReferenceTaxonId: '4555',
|
|
ppiReferenceStorageKey: 'string_taxon_4555',
|
|
ppiReferenceSelectionMode: 'tree_nearest',
|
|
ppiReferenceUserConfirmed: true,
|
|
dependencyOwner: 'ppi'
|
|
};
|
|
console.log(JSON.stringify(applyPpiOwnerFieldsToRequest(request, 'ppi', {}, batch, {})));
|
|
"""
|
|
)
|
|
|
|
assert result["referenceSpecies"] == ["ProjectRef"]
|
|
assert "KaKsRef" not in json.dumps(result)
|
|
assert result["ppiReferenceTaxonId"] == "4555"
|
|
|
|
|
|
def test_reference_ready_orchestration_dynamically_inserts_ortholog_before_resume() -> None:
|
|
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
|
helper = _source_block(
|
|
html,
|
|
"const orchestratePpiOwnerAfterReferenceReady =",
|
|
"const monitorPpiReferenceBundleDownload =",
|
|
)
|
|
result = _run_node(
|
|
helper
|
|
+ """
|
|
(async () => {
|
|
const calls = [];
|
|
const ok = await orchestratePpiOwnerAfterReferenceReady({
|
|
requestBody: { stage: 'ppi' },
|
|
requestedStages: ['ppi'],
|
|
runPreflight: async () => ({ allowed: true, resolution: { status: 'needs_ortholog' } }),
|
|
ensureOrtholog: async () => { calls.push('ortholog'); return true; },
|
|
resume: async () => { calls.push('resume'); }
|
|
});
|
|
console.log(JSON.stringify({ ok, calls }));
|
|
})();
|
|
"""
|
|
)
|
|
|
|
assert result == {"ok": True, "calls": ["ortholog", "resume"]}
|
|
|
|
|
|
def test_ppi_owned_ortholog_uses_owner_storage_without_polluting_final_ppi_request() -> None:
|
|
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
|
producer_helper = _source_block(
|
|
html,
|
|
"const buildPpiOwnedOrthologStage =",
|
|
"const attachOrthologDependencyToOwners =",
|
|
)
|
|
request_helper = _source_block(
|
|
html,
|
|
"const applyPpiOwnerFieldsToRequest =",
|
|
"const applyGeneFamilyContextToRequest =",
|
|
)
|
|
ortholog_request_helper = _source_block(
|
|
html,
|
|
"const applyPpiOwnedOrthologFieldsToRequest =",
|
|
"const applyPpiOwnerFieldsToRequest =",
|
|
)
|
|
direct_builder = _source_block(
|
|
html,
|
|
"const finalizeExecuteStageDirectRequest =",
|
|
"const executeStageDirect = async",
|
|
)
|
|
result = _run_node(
|
|
producer_helper
|
|
+ ortholog_request_helper
|
|
+ request_helper
|
|
+ direct_builder
|
|
+ """
|
|
const request = {
|
|
referenceSpecies: ['At'], refSpeciesList: ['At'], scopeMode: 'target_plus_reference',
|
|
ppiKaksReferenceSpecies: ['KaKsRef'], ppiReferenceSpecies: 'KaKsRef', referenceBundleSpecies: 'KaKsRef'
|
|
};
|
|
const owner = {
|
|
ppiReferenceTaxonId: '4555', ppiReferenceStorageKey: 'string_taxon_4555',
|
|
ppiReferenceSelectionMode: 'tree_nearest', ppiReferenceUserConfirmed: true, dependencyOwner: 'ppi'
|
|
};
|
|
const ortholog = buildPpiOwnedOrthologStage(request, owner, ['ppi']);
|
|
const finalOrtholog = applyPpiOwnedOrthologFieldsToRequest(
|
|
{...ortholog.params, ppiKaksReferenceSpecies: ['KaKsRef'], referenceSpecies: ['KaKsRef']},
|
|
'ortholog', ortholog.params
|
|
);
|
|
const directOrtholog = finalizeExecuteStageDirectRequest(
|
|
{stage: 'ortholog', referenceSpecies: ['KaKsRef'], ppiKaksReferenceSpecies: ['KaKsRef']},
|
|
'ortholog', ortholog.params
|
|
);
|
|
const ppi = applyPpiOwnerFieldsToRequest({...request}, 'ppi', {}, owner, {});
|
|
console.log(JSON.stringify({ortholog, finalOrtholog, directOrtholog, ppi}));
|
|
"""
|
|
)
|
|
|
|
assert result["ortholog"]["params"]["ppiReferenceTaxonId"] == "4555"
|
|
expected_owner = {
|
|
"ppiReferenceTaxonId": "4555",
|
|
"ppiReferenceStorageKey": "string_taxon_4555",
|
|
"ppiReferenceSelectionMode": "tree_nearest",
|
|
"ppiReferenceUserConfirmed": True,
|
|
"dependencyOwner": "ppi",
|
|
}
|
|
for payload in (result["ortholog"]["params"], result["finalOrtholog"], result["directOrtholog"]):
|
|
for field, expected in expected_owner.items():
|
|
assert payload[field] == expected
|
|
for forbidden in (
|
|
"referenceSpecies", "refSpeciesList", "scopeMode",
|
|
"ppiKaksReferenceSpecies", "ppiReferenceSpecies", "referenceBundleSpecies",
|
|
):
|
|
assert forbidden not in payload
|
|
assert result["ppi"]["referenceSpecies"] == ["At"]
|
|
assert result["ppi"]["refSpeciesList"] == ["At"]
|
|
for forbidden in ("ppiKaksReferenceSpecies", "ppiReferenceSpecies", "referenceBundleSpecies"):
|
|
assert forbidden not in result["ppi"]
|
|
|
|
|
|
def test_active_scheduler_executes_inserted_ppi_owned_ortholog_before_resume() -> None:
|
|
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
|
helper = _source_block(
|
|
html,
|
|
"const orchestratePpiOwnerAfterReferenceReady =",
|
|
"const monitorPpiReferenceBundleDownload =",
|
|
)
|
|
result = _run_node(
|
|
helper
|
|
+ """
|
|
(async () => {
|
|
const calls = [];
|
|
const producer = { id: 'ortholog', params: { ppiReferenceStorageKey: 'string_taxon_4555' } };
|
|
const ok = await orchestratePpiOwnerAfterReferenceReady({
|
|
requestBody: { stage: 'ppi' }, requestedStages: ['ppi'], isBatchExecuting: true,
|
|
runPreflight: async () => { calls.push('preflight'); return {allowed: true, resolution: {status: 'needs_ortholog'}}; },
|
|
ensureOrtholog: async () => { calls.push('ensure'); return {ready: true, producerStage: producer}; },
|
|
executeProducer: async stage => { calls.push(`execute:${stage.id}`); return true; },
|
|
resume: async () => { calls.push('resume'); }
|
|
});
|
|
console.log(JSON.stringify({ok, calls}));
|
|
})();
|
|
"""
|
|
)
|
|
|
|
assert result == {
|
|
"ok": True,
|
|
"calls": ["preflight", "ensure", "execute:ortholog", "resume"],
|
|
}
|
|
|
|
|
|
def test_orchestrator_waits_for_identify_and_does_not_resume_on_producer_failure() -> None:
|
|
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
|
helper = _source_block(
|
|
html,
|
|
"const orchestratePpiOwnerAfterReferenceReady =",
|
|
"const monitorPpiReferenceBundleDownload =",
|
|
)
|
|
result = _run_node(
|
|
helper
|
|
+ """
|
|
(async () => {
|
|
const calls = [];
|
|
let identify = 'running';
|
|
const base = {
|
|
requestBody: {stage: 'ppi'}, requestedStages: ['ppi'],
|
|
runPreflight: async () => ({allowed: true, resolution: {status: 'needs_ortholog'}}),
|
|
dependencyStatus: () => identify,
|
|
waitForDependency: async () => { calls.push('wait:identify'); identify = 'completed'; return true; },
|
|
ensureOrtholog: async () => ({ready: true, producerStage: {id: 'ortholog'}}),
|
|
resume: async () => { calls.push('resume'); }
|
|
};
|
|
const failed = await orchestratePpiOwnerAfterReferenceReady({
|
|
...base, executeProducer: async () => { calls.push('execute:failed'); return false; }
|
|
});
|
|
const succeeded = await orchestratePpiOwnerAfterReferenceReady({
|
|
...base, executeProducer: async () => { calls.push('execute:success'); return true; }
|
|
});
|
|
console.log(JSON.stringify({failed, succeeded, calls}));
|
|
})();
|
|
"""
|
|
)
|
|
|
|
assert result == {
|
|
"failed": False,
|
|
"succeeded": True,
|
|
"calls": ["wait:identify", "execute:failed", "execute:success", "resume"],
|
|
}
|