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