Files
PlantAI-dev/tests/unit/test_ppi_owner_scheduling_task5.py
T
2026-08-03 18:42:47 +08:00

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"],
}