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

215 lines
10 KiB
Python
Executable File

from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
def _index_html() -> str:
return (REPO_ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
def test_ppi_reset_dialog_opens_gene_list_without_instruction_textarea():
html = _index_html()
assert 'title="PPI Reset 输入基因列表"' in html
assert 'width="92%"' in html
assert 'class="ppi-reset-dialog-body"' in html
assert 'class="ppi-reset-table-wrap"' in html
assert 'max-height:70vh' in html
assert 'overflow:auto' in html
assert 'v-model="ppiResetSearch"' in html
assert 'class="ppi-reset-table"' in html
assert "v-for=\"row in ppiResetFilteredRows\"" in html
assert "{{ row.gene_id }}" in html
assert "{{ row.source }}" in html
assert "当前保留 {{ ppiResetKeptCount }} / {{ ppiResetRows.length }} 个 ID" in html
assert 'v-model="ppiResetInstruction"' not in html
def test_ppi_alphafold_links_remain_in_file_list_and_use_table_preview():
html = _index_html()
assert ".ppi_alphafold2_links.tsv')) return false" not in html
assert "AlphaFold DB/Fold2 单蛋白结构链接" not in html
assert "if (stage?.name === 'ppi') return !isImageFile(name);" in html
assert 'class="table-preview-table"' in html
assert "v-for=\"(row, rowIdx) in tablePreviewFilteredRows\"" in html
assert "当前显示 {{ tablePreviewFilteredRows.length }} / {{ tablePreviewRows.length }} 行" in html
assert "openTablePreview(file)" in html
assert "tablePreviewFilteredRows" in html
def test_natural_language_ppi_reset_intent_opens_reset_dialog():
html = _index_html()
assert "isPpiResetIntent" in html
assert "raw.includes('基因id')" in html
assert "openPpiResetDialog()" in html
assert "PPI Reset 基因列表编辑弹窗" in html
def test_ppi_reset_uses_current_stage_prefix_before_default_execution_prefix():
html = _index_html()
assert "const inferPpiPrefixFromStage = (stage)" in html
assert "const ppiResetPrefix = ref('')" in html
assert "inferPpiPrefixFromStage(stage) || params.prefix || executionPrefix('ppi')" in html
assert "const prefixValue = ppiResetPrefix.value || params.prefix || executionPrefix('ppi')" in html
assert "prefix: prefixValue" in html
def test_ppi_species_resolution_api_contract_exists():
main_py = (REPO_ROOT / "apps/web/vue3/main.py").read_text(encoding="utf-8")
assert "class PpiSpeciesResolutionRequest(BaseModel)" in main_py
assert '@app.post("/api/ppi/species-resolution")' in main_py
assert "resolve_ppi_species_resolution(" in main_py
assert "targetSpecies: List[str]" in main_py
assert "referenceSpecies: List[str]" in main_py
assert "ignoreExistingOrtholog: bool = False" in main_py
assert "ignore_existing_ortholog=bool(req.ignoreExistingOrtholog)" in main_py
assert "requestedStages: List[str]" in main_py
assert "requested_stages=req.requestedStages or []" in main_py
assert '"reference_species": "referenceSpecies"' in main_py
assert '"requested_stages": "requestedStages"' in main_py
assert '"jobId": "job_id"' in main_py
def test_reference_bundle_download_api_contract_exists():
main_py = (REPO_ROOT / "apps/web/vue3/main.py").read_text(encoding="utf-8")
assert "class ReferenceBundleDownloadRequest(BaseModel)" in main_py
assert '@app.post("/api/reference-bundle/download")' in main_py
assert "download_reference_annotation_bundle(" in main_py
assert '"species_code": "speciesCode"' in main_py
assert '"taxon_id": "taxonId"' in main_py
assert '"requested_stages": "requestedStages"' in main_py
assert 'include_recommended=bool(payload.get("include_recommended"))' in main_py
def test_ppi_execute_path_runs_species_resolution_preflight_before_execute():
html = _index_html()
assert "const runPpiSpeciesResolutionPreflight = async" in html
assert "fetch('/api/ppi/species-resolution'" in html
assert "diagnostics.reference_recommendations" in html
assert "requestedStages" in html
assert "PPI/KaKs 前置数据检查" in html
assert "Spinacia oleracea" not in html
assert "const shouldRunPpiKaksPreflight =" in html
assert "const currentIsDependencyOwner = ppiKaksDependencyStages.has(currentStageId) || ppiKaksDependencyStages.has(requestBody.stage)" in html
assert "PPI/KaKs 需要先处理参考物种/Ortholog" in html
def test_ortholog_scope_dialog_and_ppi_queue_preflight_are_wired():
html = _index_html()
assert "'ortholog', 'phylogeny'" in html
assert "currentStageId === 'ortholog' && currentBatchIncludesPpiKaks" in html
assert "ignoreExistingOrtholog: currentStageId === 'ortholog'" in html
assert "allowNeedsOrtholog: currentStageId === 'ortholog'" in html
assert "ppiHasNearReferenceBeforeArabidopsis" in html
def test_ppi_kaks_dependency_owners_are_preflighted_independently():
html = _index_html()
assert "const ppiKaksDependencyStages = new Set(['ppi', 'kaks'])" in html
assert "const ensurePpiKaksDependencyOwner = async (ownerStageId) =>" in html
assert "const ensureOrthologProducerForPpiKaks = async" in html
assert "for (const ownerStageId of requestedStages)" in html
assert "await ensurePpiKaksDependencyOwner(ownerStageId)" in html
assert "const requestedStages = [ownerStageId];" in html
assert "autoInsertedFor: requestedStages" in html
assert "referencePolicySource: 'ppi_kaks_dependency_preflight'" in html
assert "await ensureOrthologProducerForPpiKaks()" in html
def test_ppi_preflight_uses_current_stage_and_releases_direct_target_from_ortholog():
html = _index_html()
assert "const releaseDirectTargetPpiFromOrtholog = (resolution = {}) =>" in html
assert "resolution?.decision !== 'ready_target_string'" in html
assert "const requestedStages = currentIsDependencyOwner" in html
assert "? [currentStageId]" in html
assert "releaseDirectTargetPpiFromOrtholog(preflight.resolution);" in html
def test_ppi_needing_mapping_attaches_to_an_existing_ortholog_stage():
html = _index_html()
start = html.index("const ensureOrthologStageForPpiKaks =")
end = html.index("const ensurePpiKaksDependencyOwner =", start)
producer = html[start:end]
attach = "attachOrthologDependencyToOwners(requestedStages);"
existing = "if (stageIds.includes('ortholog'))"
assert attach in producer
assert producer.index(attach) < producer.index(existing)
def test_ppi_static_dependencies_exclude_ortholog_while_kaks_keeps_it():
html = _index_html()
catalog = (REPO_ROOT / "gfa_core/mas/stage_catalog.py").read_text(encoding="utf-8")
main_py = (REPO_ROOT / "apps/web/vue3/main.py").read_text(encoding="utf-8")
assert "'ppi': ['identify']," in html
assert '"ppi": ["identify"],' in catalog
assert '"ppi": ["identify"],' in main_py
assert "'kaks': ['identify', 'ortholog']," in html
assert '"kaks": ["identify", "ortholog"],' in catalog
assert '"kaks": ["identify", "ortholog"],' in main_py
def test_ppi_plan_generation_never_inserts_static_ortholog_dependency():
main_py = (REPO_ROOT / "apps/web/vue3/main.py").read_text(encoding="utf-8")
catalog = (REPO_ROOT / "gfa_core/mas/stage_catalog.py").read_text(encoding="utf-8")
assert '"ppi": ["identify", "ortholog"]' not in main_py
assert '"ppi": ["identify", "ortholog"]' not in catalog
context_start = main_py.index("def _downstream_gene_family_stages_for_context")
context_end = main_py.index("def _resolve_contextual_gene_family_request", context_start)
context = main_py[context_start:context_end]
ppi_start = context.index('if any(token in text for token in ["ppi", "互作"]):')
ppi_end = context.index('if any(token in text for token in ["表达", "expression", "rna"]):', ppi_start)
ppi_block = context[ppi_start:ppi_end]
assert 'stages.append("ppi")' in ppi_block
assert 'stages.append("ortholog")' not in ppi_block
assert 'elif stage_id in {"kaks", "ppi"} and "ortholog" in node_by_stage:' not in main_py
def test_ppi_preflight_recovered_reference_is_written_to_ppi_owner_only():
html = _index_html()
assert "const applyResolvedPpiReferenceToBatch = (resolution = {}, requestedStages = [], requestBody = {})" in html
assert "const applyPpiOwnerSelectionToBatch = (ownerSelection = {}, requestBody = {})" in html
assert "ppiReferenceTaxonId: taxonId" in html
assert "ppiReferenceStorageKey: storageKey" in html
assert "dependencyOwner: 'ppi'" in html
assert "requestBody.ppiKaksReferenceSpecies = [resolvedRef]" not in html
assert "const recoveredReferenceApplied = applyResolvedPpiReferenceToBatch(resolution, requestedStages, requestBody)" in html
def test_ready_preflight_propagates_recovered_reference_before_early_return():
html = _index_html()
start = html.index("const ensurePpiKaksDependencyOwner = async (ownerStageId) =>")
end = html.index("const ensureOrthologProducerForPpiKaks = async () =>", start)
producer = html[start:end]
propagation = "applyResolvedPpiReferenceToBatch(preflight.resolution, requestedStages, requestBody);"
ready_branch = "if (preflight.resolution?.status !== 'needs_ortholog')"
assert propagation in producer
assert producer.index(propagation) < producer.index(ready_branch)
def test_ppi_kaks_plan_marks_reference_as_preliminary():
html = _index_html()
assert "参考物种:${listText(refs)}(仅作为初步推荐,执行前将确定技术参考物种)" in html
def test_initial_ppi_kaks_reference_dialog_offers_project_reference():
html = _index_html()
assert 'label="project_reference"' in html
assert "confirmParams.ppiKaksProjectReferenceSpecies" in html
assert "decision === 'project_reference'" in html
assert "ppi_kaks_user_selected_project_reference" in html
def test_batch_confirmation_marks_both_ppi_kaks_reference_labels_as_preliminary():
html = _index_html()
start = html.index('v-if="!isBatchExecuting && batchExecuteParams && batchExecuteParams.geneFamilyScopeOptions"')
end = html.index('v-if="(batchExecuteStages || []).some', start)
batch_confirmation = html[start:end]
assert "const batchIncludesPpiKaks = () =>" in html
setup_return = html[html.index(" return {", html.index("onUnmounted(() =>")):]
assert "batchIncludesPpiKaks," in setup_return
assert "reference 参考物种{{ batchIncludesPpiKaks() ? '(仅作为初步推荐,执行前将确定技术参考物种)' : '' }}" in batch_confirmation
assert "batchIncludesPpiKaks() ? 'reference 参考物种(仅作为初步推荐,执行前将确定技术参考物种)' : 'reference 参考物种(仅用于标签迁移)'" in batch_confirmation