Files

1945 lines
70 KiB
Python
Raw Permalink Normal View History

2026-08-03 18:42:47 +08:00
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
from gfa_core.mas.tools.stage_tools import StageToolExecutor, StageToolRegistry
2026-08-03 18:42:47 +08:00
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 _ppi_scope_js() -> str:
return """
const uniqueList = value => [...new Set((Array.isArray(value) ? value : []).filter(Boolean))];
const scopeIdForRoles = (family, targets, refs, comps, mode) => `${family}.target-${targets.join('-')}.${mode}`;
const memberSetForScope = (scopeId, route) => `${scopeId}:${route}:all_members`;
"""
@pytest.mark.parametrize(
("stage", "mapping_owner", "confirmed", "expected"),
[
("ppi", "", True, "string_taxon_3562"),
("kaks", "ppi", True, "string_taxon_3562"),
("kaks", "kaks", True, "At"),
("ppi", "", False, "soe"),
],
)
def test_technical_reference_bundle_lookup_uses_storage_identity_only_for_ppi_owned_reference(
stage: str,
mapping_owner: str,
confirmed: bool,
expected: str,
) -> None:
request = web_main.ExecuteRequest(
stage=stage,
ppiReferenceSpecies="soe",
ppiReferenceStorageKey="string_taxon_3562",
ppiReferenceUserConfirmed=confirmed,
orthologMappingOwner=mapping_owner,
)
assert web_main._technical_reference_bundle_lookup_code(request, "soe" if stage == "ppi" else "At") == expected
def test_stage_tool_executor_forwards_kaks_ortholog_mapping_owner(
tmp_path: Path,
monkeypatch,
) -> None:
captured: dict = {}
def fake_run_stage(**kwargs):
captured.update(kwargs)
manifest = Path(kwargs["job"].workspace_dir) / "final" / "Ft.SPL.kaks.manifest.json"
manifest.parent.mkdir(parents=True, exist_ok=True)
manifest.write_text(
json.dumps({"manifest_version": "1.0", "stage": "kaks", "status": "success", "prefix": "Ft.SPL"}),
encoding="utf-8",
)
return "KaKs complete", manifest
monkeypatch.setattr(backend, "run_stage", fake_run_stage)
tool = StageToolRegistry().get("kaks")
result = asyncio.run(
StageToolExecutor(project_root=tmp_path).execute(
tool,
"job_owner_forwarding",
"Ft.SPL",
extra_params={"orthologMappingOwner": "ppi"},
)
)
assert result["status"] == "success"
assert captured["ortholog_mapping_owner"] == "ppi"
2026-08-03 18:42:47 +08:00
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",
analysisRoute="multi_species",
memberSetId="HKL.target-Sv.target_only:multi_species:all_members",
2026-08-03 18:42:47 +08:00
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 request.analysisRoute == "single_species"
assert request.memberSetId == ""
2026-08-03 18:42:47 +08:00
assert scope_params == {
"scopeMode": "target_only", "targetSpecies": ["Sv"],
"referenceSpecies": [], "refSpeciesList": [],
"comparativeSpecies": [], "scopeFile": "",
}
def test_execute_scope_normalizes_ppi_consumer_to_target_only() -> None:
request = web_main.ExecuteRequest(
stage="ppi", job_id="job_ppi_consumer", prefix="Ft.SPL",
scopeFile="/stale/project.scope.json",
scopeMode="target_plus_reference", targetSpecies=["Ft"],
analysisRoute="multi_species",
memberSetId="SPL.target-Ft.target-reference-At:multi_species:all_members",
referenceSpecies=["At"], refSpeciesList=["At"], comparativeSpecies=["Os"],
ppiReferenceTaxonId="3562",
ppiReferenceStorageKey="string_taxon_3562",
ppiReferenceUserConfirmed=True,
dependencyOwner="ppi",
)
scope_params = {
"scopeFile": request.scopeFile,
"scopeMode": request.scopeMode,
"targetSpecies": request.targetSpecies,
"referenceSpecies": request.referenceSpecies,
"refSpeciesList": request.refSpeciesList,
"comparativeSpecies": request.comparativeSpecies,
}
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 False
assert request.analysisRoute == "single_species"
assert request.memberSetId == ""
assert scope_params == {
"scopeFile": "", "scopeMode": "target_only", "targetSpecies": ["Ft"],
"referenceSpecies": [], "refSpeciesList": [], "comparativeSpecies": [],
}
2026-08-03 18:42:47 +08:00
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["analysisRoute"] == "single_species"
assert submitted_params["memberSetId"] == "fixed-biological-target-only-scope:single_species:all_members"
2026-08-03 18:42:47 +08:00
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 "const normalizePpiReferenceCandidates =" in html
assert 'v-for="(candidate, index) in confirmParams.ppiKaksReferenceCandidates"' in html
2026-08-03 18:42:47 +08:00
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_allows_kaks_to_reuse_ppi_owner() -> None:
2026-08-03 18:42:47 +08:00
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 "const mappingOwner = stageParams.orthologMappingOwner || ownerStageId" in request_block
assert "dependencyOwner: mappingOwner" in request_block
2026-08-03 18:42:47 +08:00
assert "ownerStageId === 'ppi'" in request_block
def test_owner_resolution_and_dependency_request_return_live_values() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
selection_block = _source_block(
html,
"const ppiOwnerSelectionFromResolution =",
"const applyPpiOwnerSelectionToBatch =",
)
request_block = _source_block(
html,
"const buildPpiKaksDependencyRequest =",
"const ppiKaksInternalOrthologProducers =",
)
assert "return {" in selection_block
assert "const requestBody = {" in request_block
assert request_block.index("const requestBody = {") < request_block.index(
"return isPpiOwner ? canonicalizePpiOwnerBiologicalScope(requestBody) : requestBody"
)
assert "return {" not in request_block
2026-08-03 18:42:47 +08:00
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 "recoveryStages.includes('kaks')" in monitor_block
assert "mergePpiReferenceSpeciesIntoBatch(storageKey, ['kaks'])" in monitor_block
assert "mergePpiReferenceSpeciesIntoBatch(storageKey, recoveryStages)" not in monitor_block
2026-08-03 18:42:47 +08:00
assert "applyPpiOwnerSelectionToBatch(ownerSelection" in owner_block
def test_kaks_download_success_uses_storage_key_and_recovers_shared_ppi_pause() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
recovery_helper = _source_block(
html,
"const ppiKaksFallbackRecoveryOwners =",
"const handlePpiReferenceDownloadFailure =",
)
monitor = _source_block(
html,
"const monitorPpiReferenceBundleDownload =",
"const ppiHasNearReferenceBeforeArabidopsis =",
)
result = _run_node(
recovery_helper
+ monitor
+ """
const calls = {applied: [], merged: [], orchestrated: [], resumed: []};
const batchExecuteStages = {value: [
{id: 'ortholog'},
{id: 'kaks', dependencies: ['ortholog']},
{id: 'ppi', dependencies: ['ortholog']}
]};
const stageExecutionStatus = {value: {ortholog: 'paused', kaks: 'paused', ppi: 'paused'}};
const stagePauseReasons = {value: {
ppi: {type: 'dependency_paused', reason: 'upstream_stage_paused'}
}};
const getStageId = stage => typeof stage === 'object' ? stage.id : stage;
const updateReferenceBundleDownloadProgress = () => {};
const pollReferenceBundleDownloadTask = async () => ({
success: true,
species_code: 'string_taxon_3562',
species_name: 'Spinacia oleracea',
taxon_id: '3562',
requested_stages: ['kaks'],
files: {
pep: '/job/string_taxon_3562/string_taxon_3562.pep.fasta',
cds: '/job/string_taxon_3562/string_taxon_3562.cds.fasta'
}
});
const ppiOwnerSelectionFromResolution = resolution => ({
taxonId: resolution.selected_taxon_id,
storageKey: resolution.ppi_reference_storage_key,
selectionMode: 'tree_nearest',
userConfirmed: resolution.ppi_reference_user_confirmed
});
const applyPpiOwnerSelectionToBatch = selection => calls.applied.push(selection);
const mergePpiReferenceSpeciesIntoBatch = (code, stages) => calls.merged.push({code, stages});
const clearReferenceBundleDownloadProgress = () => {};
const addAgentMessage = () => {};
const buildPpiKaksDependencyRequest = stages => ({requestedStages: stages});
const runPpiSpeciesResolutionPreflight = async () => ({allowed: true});
const orchestratePpiOwnerAfterReferenceReady = async options => {
calls.orchestrated.push(options.requestedStages);
return true;
};
const resumePpiKaksPausedStages = async stages => calls.resumed.push(stages);
const waitForPpiProducerDependency = async () => true;
const ensureOrthologStageForPpiKaks = () => true;
const executePpiOwnedOrthologProducer = async () => true;
const handlePpiReferenceDownloadFailure = async () => false;
monitorPpiReferenceBundleDownload(
'task-3562',
{species_code: 'soe', species_name: 'Spinacia oleracea', selected_string_taxon_id: '3562'},
['kaks'],
{ppi_reference_storage_key: 'string_taxon_3562'}
);
setTimeout(() => console.log(JSON.stringify(calls)), 20);
"""
)
assert result["merged"] == [{"code": "string_taxon_3562", "stages": ["kaks"]}]
assert result["applied"] == [{
"taxonId": "3562",
"storageKey": "string_taxon_3562",
"selectionMode": "tree_nearest",
"userConfirmed": True,
}]
assert result["orchestrated"] == [["ppi"]]
assert result["resumed"] == [["kaks"]]
def test_immediate_download_success_uses_storage_key_and_shared_pause_recovery() -> 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 "const storageKey = result.species_code || preflight.resolution.ppi_reference_storage_key" in owner_block
assert "const recoveryStages = ppiKaksFallbackRecoveryOwners(" in owner_block
assert "mergePpiReferenceSpeciesIntoBatch(storageKey, ['kaks'])" in owner_block
assert "if (recoveryStages.includes('ppi'))" in owner_block
assert "if (recoveryStages.includes('kaks'))" in owner_block
2026-08-03 18:42:47 +08:00
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_arabidopsis_fallback_does_not_preselect_kaks_reference_route() -> None:
2026-08-03 18:42:47 +08:00
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
fallback = _source_block(
2026-08-03 18:42:47 +08:00
html,
"const applyArabidopsisFallbackForPpiKaks =",
"const resumePpiKaksPausedStages =",
)
assert "kaksReferenceSource: 'project'" not in fallback
assert "referenceSpecies: ['At']" not in fallback
assert "orthologMappingOwner: 'kaks'" not in fallback
def test_kaks_download_failure_recovers_only_ppi_paused_on_same_ortholog() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
helper_marker = "const ppiKaksFallbackRecoveryOwners ="
helper = (
_source_block(html, helper_marker, "const handlePpiReferenceDownloadFailure =")
if helper_marker in html
else ""
)
handler = _source_block(
html,
"const handlePpiReferenceDownloadFailure =",
"const downloadRecommendedPpiReferenceBundle =",
2026-08-03 18:42:47 +08:00
)
result = _run_node(
_ppi_scope_js()
+ helper
+ handler
+ """
const runCase = async ({stages, statuses, pauses}) => {
const calls = {dialog: null, fallback: null, orchestrated: [], resumed: []};
batchExecuteStages.value = stages;
stageExecutionStatus.value = statuses;
stagePauseReasons.value = pauses;
requestPpiDownloadFallbackDecision = async (_err, _resolution, _recommendation, owners) => {
calls.dialog = owners;
return 'ath';
};
applyArabidopsisFallbackForPpiKaks = owners => { calls.fallback = owners; };
orchestratePpiOwnerAfterReferenceReady = async options => {
calls.orchestrated.push(options.requestedStages);
return true;
};
resumePpiKaksPausedStages = async owners => { calls.resumed.push(owners); };
await handlePpiReferenceDownloadFailure(new Error('download failed'), {}, {}, ['kaks']);
return calls;
};
let batchExecuteStages = {value: []};
let stageExecutionStatus = {value: {}};
let stagePauseReasons = {value: {}};
let requestPpiDownloadFallbackDecision;
let applyArabidopsisFallbackForPpiKaks;
let orchestratePpiOwnerAfterReferenceReady;
let resumePpiKaksPausedStages;
const addAgentMessage = () => {};
const buildPpiKaksDependencyRequest = () => ({});
const runPpiSpeciesResolutionPreflight = async () => ({allowed: true});
const waitForPpiProducerDependency = async () => true;
const ensureOrthologStageForPpiKaks = () => true;
const getStageId = stage => typeof stage === 'object' ? stage.id : stage;
const executePpiOwnedOrthologProducer = async () => true;
const storePpiKaksReferencePause = () => {};
(async () => {
const jointPaused = await runCase({
stages: [
{id: 'ortholog'},
{id: 'kaks', dependencies: ['ortholog']},
{id: 'ppi', dependencies: ['ortholog']}
],
statuses: {ortholog: 'paused', kaks: 'paused', ppi: 'paused'},
pauses: {ppi: {type: 'dependency_paused', reason: 'upstream_stage_paused'}}
});
const completedPpi = await runCase({
stages: [{id: 'ortholog'}, {id: 'kaks'}, {id: 'ppi', dependencies: ['ortholog']}],
statuses: {ortholog: 'paused', kaks: 'paused', ppi: 'completed'},
pauses: {ppi: {type: 'dependency_paused', reason: 'upstream_stage_paused'}}
});
const kaksOnly = await runCase({
stages: [{id: 'ortholog'}, {id: 'kaks', dependencies: ['ortholog']}],
statuses: {ortholog: 'paused', kaks: 'paused'},
pauses: {}
});
console.log(JSON.stringify({jointPaused, completedPpi, kaksOnly}));
})();
"""
)
assert result["jointPaused"] == {
"dialog": ["kaks", "ppi"],
"fallback": ["ppi"],
"orchestrated": [["ppi"]],
"resumed": [],
}
for guarded_case in (result["completedPpi"], result["kaksOnly"]):
assert guarded_case == {
"dialog": ["kaks"],
"fallback": None,
"orchestrated": [],
"resumed": [],
}
def test_final_ppi_request_dynamically_uses_target_only_biological_scope() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
helper = _source_block(html, "const canonicalizePpiOwnerBiologicalScope =", "const applyGeneFamilyContextToRequest =")
result = _run_node(
_ppi_scope_js()
+ helper
2026-08-03 18:42:47 +08:00
+ """
const request = {
geneFamily: 'SPL', targetSpecies: ['Ft'], comparativeSpecies: ['Os'],
analysisRoute: 'multi_species', memberSetId: 'stale-member', scopeId: 'stale-scope', scopeFile: '/stale/scope.json',
2026-08-03 18:42:47 +08:00
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["targetSpecies"] == ["Ft"]
assert result["referenceSpecies"] == []
assert result["refSpeciesList"] == []
assert result["comparativeSpecies"] == []
assert result["scopeMode"] == "target_only"
assert result["analysisRoute"] == "single_species"
assert result["scopeId"] == "SPL.target-Ft.target_only"
assert result["memberSetId"] == "SPL.target-Ft.target_only:single_species:all_members"
assert result["scopeFile"] == ""
2026-08-03 18:42:47 +08:00
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 canonicalizePpiOwnerBiologicalScope =", "const applyGeneFamilyContextToRequest =")
2026-08-03 18:42:47 +08:00
result = _run_node(
_ppi_scope_js()
+ helper
2026-08-03 18:42:47 +08:00
+ """
const request = { geneFamily: 'SPL', targetSpecies: ['Ft'], referenceSpecies: ['ProjectRef'], refSpeciesList: ['ProjectRef'] };
2026-08-03 18:42:47 +08:00
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"] == []
2026-08-03 18:42:47 +08:00
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 = [];
let preflightCount = 0;
2026-08-03 18:42:47 +08:00
const ok = await orchestratePpiOwnerAfterReferenceReady({
requestBody: { stage: 'ppi' },
requestedStages: ['ppi'],
runPreflight: async () => {
calls.push('preflight');
preflightCount += 1;
return { allowed: true, resolution: { status: preflightCount === 1 ? 'needs_ortholog' : 'ready' } };
},
2026-08-03 18:42:47 +08:00
ensureOrtholog: async () => { calls.push('ortholog'); return true; },
resume: async () => { calls.push('resume'); }
});
console.log(JSON.stringify({ ok, calls }));
})();
"""
)
assert result == {"ok": True, "calls": ["preflight", "ortholog", "preflight", "resume"]}
2026-08-03 18:42:47 +08:00
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 canonicalizePpiOwnerBiologicalScope =", "const applyGeneFamilyContextToRequest =")
2026-08-03 18:42:47 +08:00
ortholog_request_helper = _source_block(
html,
"const applyPpiOwnedOrthologFieldsToRequest =",
"const canonicalizePpiOwnerBiologicalScope =",
2026-08-03 18:42:47 +08:00
)
direct_builder = _source_block(
html,
"const finalizeExecuteStageDirectRequest =",
"const executeStageDirect = async",
)
result = _run_node(
_ppi_scope_js()
+ producer_helper
2026-08-03 18:42:47 +08:00
+ ortholog_request_helper
+ request_helper
+ direct_builder
+ """
const request = {
geneFamily: 'SPL', targetSpecies: ['Ft'],
2026-08-03 18:42:47 +08:00
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["directOrtholog"]["requestedStages"] == ["ppi"]
assert result["directOrtholog"]["autoInsertedFor"] == ["ppi"]
assert result["ppi"]["referenceSpecies"] == []
assert result["ppi"]["refSpeciesList"] == []
assert result["ppi"]["scopeMode"] == "target_only"
assert result["ppi"]["scopeId"] == "SPL.target-Ft.target_only"
2026-08-03 18:42:47 +08:00
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 = [];
let preflightCount = 0;
2026-08-03 18:42:47 +08:00
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');
preflightCount += 1;
return {allowed: true, resolution: {status: preflightCount === 1 ? 'needs_ortholog' : 'ready'}};
},
2026-08-03 18:42:47 +08:00
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", "preflight", "resume"],
2026-08-03 18:42:47 +08:00
}
def test_kaks_soft_gate_releases_only_after_ppi_settles() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
helper = _source_block(
html,
"const ppiSettledForKaks =",
"const executeStagesWithDependencies = async",
)
result = _run_node(
helper
+ """
const stages = [{id: 'ppi'}, {id: 'kaks'}];
const getStageId = stage => typeof stage === 'object' ? stage.id : stage;
console.log(JSON.stringify({
pending: ppiSettledForKaks(stages, {ppi: 'pending'}),
running: ppiSettledForKaks(stages, {ppi: 'running'}),
paused: ppiSettledForKaks(stages, {ppi: 'paused'}),
completed: ppiSettledForKaks(stages, {ppi: 'completed'}),
failed: ppiSettledForKaks(stages, {ppi: 'failed'}),
noPpi: ppiSettledForKaks([{id: 'kaks'}], {})
}));
"""
)
assert result == {
"pending": False,
"running": False,
"paused": False,
"completed": True,
"failed": True,
"noPpi": True,
}
def test_kaks_reference_choice_keeps_ppi_and_project_namespaces_separate() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
helper = _source_block(
html,
"const kaksReferenceChoiceParams =",
"const confirmedStageParamsForExecution =",
)
result = _run_node(
helper
+ """
console.log(JSON.stringify({
ppi: kaksReferenceChoiceParams('ppi', 'string_taxon_3562', 'At'),
project: kaksReferenceChoiceParams('project', 'string_taxon_3562', 'At')
}));
"""
)
assert result["ppi"] == {
"kaksReferenceSource": "ppi",
"referenceSpecies": ["string_taxon_3562"],
"refSpeciesList": ["string_taxon_3562"],
"ppiKaksReferenceSpecies": ["string_taxon_3562"],
"ppiReferenceSpecies": "string_taxon_3562",
"referenceBundleSpecies": "string_taxon_3562",
"orthologMappingOwner": "ppi",
}
assert result["project"] == {
"kaksReferenceSource": "project",
"referenceSpecies": ["At"],
"refSpeciesList": ["At"],
"ppiKaksReferenceSpecies": [],
"ppiReferenceSpecies": "",
"referenceBundleSpecies": "",
"orthologMappingOwner": "kaks",
}
def test_kaks_preflight_uses_selected_ppi_mapping_owner() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
helper = _source_block(
html,
"const buildPpiKaksDependencyRequest =",
"const ppiKaksInternalOrthologProducers =",
)
result = _run_node(
helper
+ """
const batchExecuteParams = {value: {
prefix: 'Ft.SPL',
ppiReferenceTaxonId: '3562',
ppiReferenceStorageKey: 'string_taxon_3562',
ppiReferenceSelectionMode: 'tree_nearest',
ppiReferenceUserConfirmed: true
}};
const batchExecuteStages = {value: [{
id: 'kaks',
params: {
kaksReferenceSource: 'project',
orthologMappingOwner: 'kaks',
referenceSpecies: ['At'],
refSpeciesList: ['At']
}
}]};
const getStageId = stage => typeof stage === 'object' ? stage.id : stage;
const firstNonEmptyList = (...values) => {
for (const value of values) {
if (Array.isArray(value) && value.length) return [...value];
if (typeof value === 'string' && value.trim()) return [value.trim()];
}
return [];
};
const params = {prefix: 'Ft.SPL', geneFamily: 'SPL'};
const confirmParams = {};
const executionPrefix = () => 'Ft.SPL';
const canonicalizePpiOwnerBiologicalScope = value => value;
const confirmedStageParams = {
kaksReferenceSource: 'ppi',
orthologMappingOwner: 'ppi',
referenceSpecies: ['string_taxon_3562'],
refSpeciesList: ['string_taxon_3562'],
ppiKaksReferenceSpecies: ['string_taxon_3562'],
ppiReferenceSpecies: 'string_taxon_3562',
referenceBundleSpecies: 'string_taxon_3562'
};
console.log(JSON.stringify(
buildPpiKaksDependencyRequest(['kaks'], confirmedStageParams)
));
"""
)
assert {
"dependencyOwner": result["dependencyOwner"],
"requestedStages": result["requestedStages"],
"referenceSpecies": result["referenceSpecies"],
"ppiReferenceTaxonId": result["ppiReferenceTaxonId"],
"ppiReferenceStorageKey": result["ppiReferenceStorageKey"],
"ppiReferenceSelectionMode": result["ppiReferenceSelectionMode"],
"ppiReferenceUserConfirmed": result["ppiReferenceUserConfirmed"],
} == {
"dependencyOwner": "ppi",
"requestedStages": ["kaks"],
"referenceSpecies": ["string_taxon_3562"],
"ppiReferenceTaxonId": "3562",
"ppiReferenceStorageKey": "string_taxon_3562",
"ppiReferenceSelectionMode": "tree_nearest",
"ppiReferenceUserConfirmed": True,
}
def test_kaks_project_at_route_is_not_blocked_by_ppi_near_reference_policy() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
helper = _source_block(
html,
"const ppiHasNearReferenceBeforeArabidopsis =",
"const runPpiSpeciesResolutionPreflight =",
)
result = _run_node(
helper
+ """
const resolution = {
status: 'needs_ortholog',
selected_string_species: 'ath',
diagnostics: {
reference_recommendations: [{species_code: 'string_taxon_3562'}]
}
};
console.log(JSON.stringify({
ppi: ppiNeedsNearReferenceDecision(resolution, ['ppi']),
kaks: ppiNeedsNearReferenceDecision(resolution, ['kaks'])
}));
"""
)
assert result == {"ppi": True, "kaks": False}
def test_prepare_kaks_preflights_the_exact_confirmed_stage_params() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
helper = _source_block(
html,
"const prepareKaksSelectedReference =",
"const submitExecuteRequestWithAutoAnnotation =",
)
result = _run_node(
helper
+ """
const confirmedStageParams = {
kaksReferenceSource: 'ppi',
orthologMappingOwner: 'ppi',
referenceSpecies: ['string_taxon_3562'],
ppiKaksReferenceSpecies: ['string_taxon_3562']
};
let captured = null;
const ensurePpiKaksDependencyOwner = async (...args) => {
captured = args;
return true;
};
const ppiKaksDependencyPauses = () => ({});
(async () => {
const ready = await prepareKaksSelectedReference(confirmedStageParams);
console.log(JSON.stringify({ready, captured}));
})();
"""
)
assert result == {
"ready": True,
"captured": [
"kaks",
{
"kaksReferenceSource": "ppi",
"orthologMappingOwner": "ppi",
"referenceSpecies": ["string_taxon_3562"],
"ppiKaksReferenceSpecies": ["string_taxon_3562"],
},
],
}
def test_submit_preflight_maps_kaks_selected_ortholog_owner_to_dependency_owner() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
retry_helper = _source_block(
html,
"const fetchWithTransientRetry = async",
"const pollReferenceBundleDownloadTask = async",
)
helper = _source_block(
html,
"const runPpiSpeciesResolutionPreflight =",
"const ppiKaksDependencyStages =",
)
result = _run_node(
retry_helper
+ helper
+ """
const jobInfo = {job_id: 'job_1786243816439_04955b'};
const executionPrefix = () => 'Ft.SPL';
let capturedPayload = null;
const fetch = async (_url, options) => {
capturedPayload = JSON.parse(options.body);
return {
ok: true,
json: async () => ({success: true, status: 'ready'})
};
};
(async () => {
const outcome = await runPpiSpeciesResolutionPreflight({
stage: 'kaks',
prefix: 'Ft.SPL',
orthologMappingOwner: 'ppi',
targetSpecies: ['Ft'],
referenceSpecies: ['string_taxon_3562']
}, {requestedStages: ['kaks']});
console.log(JSON.stringify({
allowed: outcome.allowed,
dependencyOwner: capturedPayload.dependencyOwner
}));
})();
"""
)
assert result == {"allowed": True, "dependencyOwner": "ppi"}
def test_at_fallback_owner_orchestration_failure_stays_paused_and_returns_false() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
handler = _source_block(
html,
"const handlePpiReferenceDownloadFailure =",
"const downloadRecommendedPpiReferenceBundle =",
)
result = _run_node(
handler
+ """
const batchExecuteStages = {value: [{id: 'ppi', params: {}}]};
const stageExecutionStatus = {value: {ppi: 'paused'}};
const stagePauseReasons = {value: {}};
const pauses = [];
const ppiKaksFallbackRecoveryOwners = () => ['ppi'];
const requestPpiDownloadFallbackDecision = async () => 'ath';
const applyArabidopsisFallbackForPpiKaks = () => {};
const buildPpiKaksDependencyRequest = () => ({stage: 'ppi'});
const orchestratePpiOwnerAfterReferenceReady = async () => false;
const runPpiSpeciesResolutionPreflight = async () => ({allowed: true});
const waitForPpiProducerDependency = async () => true;
const ensureOrthologStageForPpiKaks = () => true;
const getPpiKaksInternalOrthologProducer = () => null;
const executePpiOwnedOrthologProducer = async () => false;
const resumePpiKaksPausedStages = async () => {};
const addAgentMessage = () => {};
const storePpiKaksReferencePause = (owners, resolution, message) => {
pauses.push({owners, resolution, message});
};
(async () => {
const accepted = await handlePpiReferenceDownloadFailure(
new Error('download failed'),
{},
{},
['ppi']
);
console.log(JSON.stringify({accepted, pauses}));
})();
"""
)
assert result["accepted"] is False
assert result["pauses"] == [
{
"owners": ["ppi"],
"resolution": {},
"message": "拟南芥兜底参考尚未形成可用的 PPI Ortholog 映射;PPI 保持暂停。",
}
]
def test_kaks_missing_reference_data_does_not_open_download_decision() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
helper = _source_block(
html,
"const ensurePpiKaksDependencyOwner = async",
"const preflightPpiKaksDependencyOwners = async",
)
result = _run_node(
helper
+ """
const batchExecuteStages = {value: [{id: 'kaks', params: {orthologMappingOwner: 'ppi'}}]};
const ppiKaksDependencyStages = new Set(['ppi', 'kaks']);
const getStageId = stage => typeof stage === 'object' ? stage.id : stage;
const buildPpiKaksDependencyRequest = () => ({stage: 'kaks'});
const runPpiSpeciesResolutionPreflight = async () => ({
allowed: false,
resolution: {status: 'needs_reference_data', issue_code: 'kaks_reference_cds_missing'}
});
const ppiNeedsNearReferenceDecision = () => false;
let decisionCalls = 0;
const requestPpiReferenceDataDecision = async () => { decisionCalls += 1; return 'skip'; };
const formatPpiSpeciesResolutionBlockMessage = () => 'reference data missing';
const storePpiKaksReferencePause = () => {};
const addAgentMessage = () => {};
(async () => {
const ready = await ensurePpiKaksDependencyOwner('kaks');
console.log(JSON.stringify({ready, decisionCalls}));
})();
"""
)
assert result == {"ready": False, "decisionCalls": 0}
2026-08-03 18:42:47 +08:00
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 () => {
calls.push('preflight');
return {allowed: true, resolution: {status: calls.filter(item => item === 'preflight').length <= 2 ? 'needs_ortholog' : 'ready'}};
},
2026-08-03 18:42:47 +08:00
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": ["preflight", "wait:identify", "execute:failed", "preflight", "execute:success", "preflight", "resume"],
}
def test_ppi_stage_error_path_keeps_confirmed_params_in_catch_scope_and_batch_has_final_cleanup() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
stage_start = html.index("const executeStageWithConfirmation = async")
stage_end = html.index("const NCBI_SPECIES_DEFAULTS =", stage_start)
stage_block = html[stage_start:stage_end]
try_index = stage_block.index("try {")
declaration_index = stage_block.index("let confirmedStageParams")
catch_index = stage_block.index("} catch (err)")
assert declaration_index < try_index < catch_index
assert "confirmedStageParams = confirmedStageParamsForExecution" in stage_block
assert "stageExecutionStatus.value[stageId] = 'failed'" in stage_block[catch_index:]
batch_start = html.index("const startBatchExecute = async")
batch_end = html.index("const ppiSettledForKaks =", batch_start)
batch_block = html[batch_start:batch_end]
execute_index = batch_block.index("await executeStagesWithDependencies")
finally_index = batch_block.index("finally", execute_index)
cleanup_index = batch_block.index("isBatchExecuting.value = false", finally_index)
assert execute_index < finally_index < cleanup_index
def test_hidden_ppi_ortholog_reports_success_to_the_user() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
block = _source_block(
html,
"const executePpiOwnedOrthologProducer = async",
"const waitForPpiProducerDependency = async",
)
assert "专属同源映射生成完成" in block
def test_arabidopsis_fallback_keeps_ppi_paused_until_owner_ortholog_is_ready() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
helper = _source_block(
html,
"const applyArabidopsisFallbackForPpiKaks =",
"const resumePpiKaksPausedStages =",
)
result = _run_node(
helper
+ """
const calls = {selection: null, clear: []};
const batchExecuteStages = {value: [{id: 'ppi', params: {}}]};
const stageExecutionStatus = {value: {ppi: 'paused'}};
const getStageId = stage => typeof stage === 'object' ? stage.id : stage;
const getStageLabel = stage => stage;
const stageDependencies = {};
const applyPpiOwnerSelectionToBatch = selection => { calls.selection = selection; };
const mergePpiReferenceSpeciesIntoBatch = () => {};
const clearPpiKaksReferencePause = stages => { calls.clear.push(stages); };
applyArabidopsisFallbackForPpiKaks(['ppi'], 'download failed');
console.log(JSON.stringify({calls, status: stageExecutionStatus.value.ppi}));
"""
)
assert result == {
"calls": {
"selection": {
"taxonId": "3702",
"storageKey": "At",
"selectionMode": "model_fallback",
"userConfirmed": True,
},
"clear": [],
},
"status": "paused",
2026-08-03 18:42:47 +08:00
}
def test_at_fallback_batch_owner_overrides_stale_spinach_stage_for_ppi_request() -> 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 canonicalizePpiOwnerBiologicalScope = request => request;
const request = {};
applyPpiOwnerFieldsToRequest(request, 'ppi', {
ppiReferenceTaxonId: '3562',
ppiReferenceStorageKey: 'string_taxon_3562',
ppiReferenceSelectionMode: 'tree_nearest',
ppiReferenceUserConfirmed: true
}, {
ppiReferenceTaxonId: '3702',
ppiReferenceStorageKey: 'At',
ppiReferenceSelectionMode: 'model_fallback',
ppiReferenceUserConfirmed: true
}, {});
console.log(JSON.stringify(request));
"""
)
assert result["ppiReferenceTaxonId"] == "3702"
assert result["ppiReferenceStorageKey"] == "At"
assert result["ppiReferenceSelectionMode"] == "model_fallback"
assert result["ppiReferenceUserConfirmed"] is True
def test_normal_spinach_ppi_owner_request_is_unchanged() -> 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 canonicalizePpiOwnerBiologicalScope = request => request;
const request = {};
const spinach = {
ppiReferenceTaxonId: '3562',
ppiReferenceStorageKey: 'string_taxon_3562',
ppiReferenceSelectionMode: 'tree_nearest',
ppiReferenceUserConfirmed: true
};
applyPpiOwnerFieldsToRequest(request, 'ppi', spinach, spinach, {});
console.log(JSON.stringify(request));
"""
)
assert result["ppiReferenceTaxonId"] == "3562"
assert result["ppiReferenceStorageKey"] == "string_taxon_3562"
assert result["ppiReferenceSelectionMode"] == "tree_nearest"
assert result["ppiReferenceUserConfirmed"] is True
def test_ppi_owned_ortholog_producer_is_single_flight_for_same_owner_and_reference_identity() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
helper = _source_block(
html,
"const executePpiOwnedOrthologProducer = async",
"const waitForPpiProducerDependency = async",
)
result = _run_node(
helper
+ """
const ppiKaksInternalOrthologStatus = new Map();
const ppiKaksInternalOrthologPromises = new Map();
const ppiKaksInternalOrthologIdentityKey = producerStage => {
const producerParams = producerStage.params || {};
return [
String(producerParams.dependencyOwner || 'ppi').trim().toLowerCase(),
String(producerParams.ppiReferenceTaxonId || '').trim(),
String(producerParams.ppiReferenceStorageKey || '').trim()
].join('::');
};
const getStageId = stage => stage.id;
const getStageLabel = stage => stage;
const addAgentMessage = () => {};
let executeCalls = 0;
const executeStageDirect = async () => {
executeCalls += 1;
await new Promise(resolve => setTimeout(resolve, 10));
};
const producer = {id: 'ortholog', params: {
dependencyOwner: 'ppi',
ppiReferenceTaxonId: '3562',
ppiReferenceStorageKey: 'string_taxon_3562'
}};
(async () => {
const results = await Promise.all([
executePpiOwnedOrthologProducer(producer),
executePpiOwnedOrthologProducer(producer)
]);
console.log(JSON.stringify({results, executeCalls}));
})();
"""
)
assert result == {"results": [True, True], "executeCalls": 1}
def test_ppi_owned_ortholog_producer_does_not_reuse_in_flight_promise_for_new_reference_identity() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
helper = _source_block(
html,
"const executePpiOwnedOrthologProducer = async",
"const waitForPpiProducerDependency = async",
)
result = _run_node(
helper
+ """
const ppiKaksInternalOrthologStatus = new Map();
const ppiKaksInternalOrthologPromises = new Map();
const ppiKaksInternalOrthologIdentityKey = producerStage => {
const producerParams = producerStage.params || {};
return [
String(producerParams.dependencyOwner || 'ppi').trim().toLowerCase(),
String(producerParams.ppiReferenceTaxonId || '').trim(),
String(producerParams.ppiReferenceStorageKey || '').trim()
].join('::');
};
const getStageId = stage => stage.id;
const getStageLabel = stage => stage;
const addAgentMessage = () => {};
const executeCalls = [];
const executeStageDirect = async (_stage, params) => {
executeCalls.push(params.ppiReferenceStorageKey);
if (params.ppiReferenceStorageKey === 'string_taxon_3562') {
await new Promise(resolve => setTimeout(resolve, 20));
throw new Error('old reference failed');
}
await new Promise(resolve => setTimeout(resolve, 5));
};
const oldProducer = {id: 'ortholog', params: {
dependencyOwner: 'ppi',
ppiReferenceTaxonId: '3562',
ppiReferenceStorageKey: 'string_taxon_3562'
}};
const atProducer = {id: 'ortholog', params: {
dependencyOwner: 'ppi',
ppiReferenceTaxonId: '3702',
ppiReferenceStorageKey: 'At'
}};
(async () => {
const results = await Promise.all([
executePpiOwnedOrthologProducer(oldProducer),
executePpiOwnedOrthologProducer(atProducer)
]);
console.log(JSON.stringify({results, executeCalls}));
})();
"""
)
assert result == {
"results": [False, True],
"executeCalls": ["string_taxon_3562", "At"],
}
def test_resetting_ppi_producer_clears_completed_identity_for_same_reference_rerun() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
execute_helper = _source_block(
html,
"const executePpiOwnedOrthologProducer = async",
"const waitForPpiProducerDependency = async",
)
remember_helper = _source_block(
html,
"const ppiKaksInternalOrthologProducers = new Map();",
"const getPpiKaksInternalOrthologProducer =",
)
result = _run_node(
execute_helper
+ remember_helper
+ """
const ppiKaksInternalOrthologIdentityKey = producerStage => {
const producerParams = producerStage.params || {};
return [
String(producerParams.dependencyOwner || 'ppi').trim().toLowerCase(),
String(producerParams.ppiReferenceTaxonId || '').trim(),
String(producerParams.ppiReferenceStorageKey || '').trim()
].join('::');
};
const getStageId = stage => stage.id;
const getStageLabel = stage => stage;
const addAgentMessage = () => {};
let executeCalls = 0;
const executeStageDirect = async () => { executeCalls += 1; };
const atProducer = {id: 'ortholog', params: {
dependencyOwner: 'ppi',
ppiReferenceTaxonId: '3702',
ppiReferenceStorageKey: 'At'
}};
(async () => {
rememberPpiKaksInternalOrthologProducer('ppi', atProducer);
const first = await executePpiOwnedOrthologProducer(atProducer);
rememberPpiKaksInternalOrthologProducer('ppi', null);
rememberPpiKaksInternalOrthologProducer('ppi', atProducer);
const second = await executePpiOwnedOrthologProducer(atProducer);
console.log(JSON.stringify({first, second, executeCalls}));
})();
"""
)
assert result == {"first": True, "second": True, "executeCalls": 2}
def test_resume_does_not_change_running_ppi_back_to_pending() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
helper = _source_block(
html,
"const resumePpiKaksPausedStages = async",
"const ppiKaksFallbackRecoveryOwners =",
)
result = _run_node(
helper
+ """
const batchExecuteStages = {value: [{id: 'ppi'}]};
const stageExecutionStatus = {value: {ppi: 'running'}};
const isBatchExecuting = {value: true};
const shouldStopBatchExecute = {value: false};
const currentBatchStage = {value: ''};
const getStageId = stage => typeof stage === 'object' ? stage.id : stage;
const ppiKaksPauseStageIds = stages => stages;
const clearPpiKaksReferencePause = () => {};
const addAgentMessage = () => {};
const executeStagesWithDependencies = async () => {};
(async () => {
await resumePpiKaksPausedStages(['ppi'], 'At ready');
console.log(JSON.stringify({status: stageExecutionStatus.value.ppi}));
})();
"""
)
assert result == {"status": "running"}
def test_ppi_manifest_and_background_status_queries_use_canonical_owner_identity() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
manifest_block = _source_block(html, "const checkManifestExists = async", "const pollTaskUntilComplete = async")
background_block = _source_block(html, "const fetchStageCompleted = async", "const finalizeExecuteStageDirectRequest =")
expected = "canonicalizePpiOwnerBiologicalScope({ ...batchPlanParams, ...batchParams, ...stageParams })"
assert expected in manifest_block
assert expected in background_block
def test_ppi_preflight_retries_one_transient_network_error_before_returning_ready() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
retry_helper = _source_block(
html,
"const fetchWithTransientRetry = async",
"const pollReferenceBundleDownloadTask = async",
)
preflight = _source_block(
html,
"const runPpiSpeciesResolutionPreflight = async",
"const ppiKaksDependencyStages =",
)
result = _run_node(
retry_helper
+ preflight
+ """
const jobInfo = {job_id: 'job_network_retry'};
const executionPrefix = () => 'Ft.SPL';
const canonicalizePpiOwnerBiologicalScope = payload => payload;
const ppiNeedsNearReferenceDecision = () => false;
const formatPpiSpeciesResolutionBlockMessage = () => 'not ready';
const showStageConfirmDialog = async () => true;
const addAgentMessage = () => {};
const ElMessageBox = {alert: async () => {}};
const setTimeout = callback => callback();
let attempts = 0;
const fetch = async () => {
attempts += 1;
if (attempts === 1) {
throw new TypeError('NetworkError when attempting to fetch resource.');
}
return {
ok: true,
status: 200,
statusText: 'OK',
json: async () => ({success: true, status: 'ready'})
};
};
(async () => {
const outcome = await runPpiSpeciesResolutionPreflight({
stage: 'ppi', prefix: 'Ft.SPL', targetSpecies: ['Ft']
}, {requestedStages: ['ppi']});
console.log(JSON.stringify({attempts, allowed: outcome.allowed}));
})();
"""
)
assert result == {"attempts": 2, "allowed": True}
def test_ppi_preflight_does_not_retry_a_valid_business_response() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
retry_helper = _source_block(
html,
"const fetchWithTransientRetry = async",
"const pollReferenceBundleDownloadTask = async",
)
preflight = _source_block(
html,
"const runPpiSpeciesResolutionPreflight = async",
"const ppiKaksDependencyStages =",
)
result = _run_node(
retry_helper
+ preflight
+ """
const jobInfo = {job_id: 'job_business_response'};
const executionPrefix = () => 'Ft.SPL';
const canonicalizePpiOwnerBiologicalScope = payload => payload;
const ppiNeedsNearReferenceDecision = () => false;
const formatPpiSpeciesResolutionBlockMessage = () => 'reference data missing';
const showStageConfirmDialog = async () => true;
const addAgentMessage = () => {};
const ElMessageBox = {alert: async () => {}};
const setTimeout = callback => callback();
let attempts = 0;
const fetch = async () => {
attempts += 1;
return {
ok: true,
status: 200,
statusText: 'OK',
json: async () => ({success: true, status: 'needs_reference_data'})
};
};
(async () => {
const outcome = await runPpiSpeciesResolutionPreflight({
stage: 'ppi', prefix: 'Ft.SPL', targetSpecies: ['Ft']
}, {requestedStages: ['ppi']});
console.log(JSON.stringify({attempts, allowed: outcome.allowed}));
})();
"""
)
assert result == {"attempts": 1, "allowed": False}
def test_reference_bundle_status_poll_recovers_from_one_transient_network_error() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
helper = _source_block(
html,
"const fetchWithTransientRetry = async",
"const ppiKaksOwnedOrthologOwners =",
)
result = _run_node(
helper
+ """
const setTimeout = callback => callback();
const referenceBundleErrorMessage = (_data, fallback) => fallback;
const updateReferenceBundleDownloadProgress = () => {};
let attempts = 0;
const fetch = async () => {
attempts += 1;
if (attempts === 1) throw new TypeError('Failed to fetch');
return {
ok: true,
status: 200,
json: async () => ({status: 'success', result: {status: 'success', species_code: 'string_taxon_3562'}})
};
};
(async () => {
const result = await pollReferenceBundleDownloadTask('task-download');
console.log(JSON.stringify({attempts, status: result.status, species: result.species_code}));
})();
"""
)
assert result == {"attempts": 2, "status": "success", "species": "string_taxon_3562"}
def test_transient_retry_is_not_used_for_download_or_analysis_submission() -> None:
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
download_submit = _source_block(
html,
"const downloadRecommendedPpiReferenceBundle = async",
"const updateReferenceBundleDownloadProgress =",
)
execute_submit = _source_block(
html,
"const submitExecuteRequestWithAutoAnnotation = async",
"const waitForDialogTransition = async",
)
assert "fetch('/api/reference-bundle/download'" in download_submit
assert "fetchWithTransientRetry('/api/reference-bundle/download'" not in download_submit
assert "fetch('/api/execute'" in execute_submit
assert "fetchWithTransientRetry('/api/execute'" not in execute_submit