fix: preserve gene-family stage intent and completed results
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
FRONTEND_PATH = PROJECT_ROOT / "apps/web/vue3/index_ml.html"
|
||||
I18N_PATH = PROJECT_ROOT / "apps/web/vue3/static/i18n_en.js"
|
||||
|
||||
|
||||
def _run_empty_plan_branch(tmp_path: Path, plan_data: dict) -> dict:
|
||||
frontend = FRONTEND_PATH.read_text(encoding="utf-8")
|
||||
start = frontend.index("const plan = planData.plan;")
|
||||
end = frontend.index("// 显示智能规划结果", start)
|
||||
branch = frontend[start:end]
|
||||
script = tmp_path / "empty-plan-branch.cjs"
|
||||
script.write_text(
|
||||
"const planData=" + json.dumps(plan_data, ensure_ascii=False) + ";"
|
||||
"const events={messages:[],filters:[],refreshes:[]};"
|
||||
"const t=value=>value;"
|
||||
"const getStageLabel=id=>({genome_prepare:'0️⃣ Genome Prepare',ppi:'PPI分析'}[id]||id);"
|
||||
"const setResultStageFilter=id=>events.filters.push(id);"
|
||||
"const refreshResults=async(jobId,id)=>events.refreshes.push([jobId,id]);"
|
||||
"const addAgentMessage=(role,message,meta)=>events.messages.push({role,message,meta});"
|
||||
"const jobInfo={job_id:'job-test'};"
|
||||
"const nlLoading={value:true};const nlInput={value:'request'};"
|
||||
"async function run(){" + branch + "}"
|
||||
"run().then(()=>process.stdout.write(JSON.stringify(events)));",
|
||||
encoding="utf-8",
|
||||
)
|
||||
completed = subprocess.run(
|
||||
["/usr/bin/node", str(script)],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def test_completed_full_workflow_does_not_masquerade_as_genome_prepare(tmp_path: Path) -> None:
|
||||
requested = ["phylogeny", "motif", "domain", "structure", "combo", "promoter"]
|
||||
result = _run_empty_plan_branch(
|
||||
tmp_path,
|
||||
{
|
||||
"success": True,
|
||||
"plan": {
|
||||
"stages": [],
|
||||
"all_stages": requested,
|
||||
"description": "所有阶段已完成",
|
||||
},
|
||||
"completed_stages": [*requested, "genome_prepare"],
|
||||
},
|
||||
)
|
||||
|
||||
assert result["filters"] == []
|
||||
assert result["refreshes"] == []
|
||||
assert [item["message"] for item in result["messages"]] == [
|
||||
"当前请求中的分析阶段均已有同一分析范围的有效结果,无需重复执行。"
|
||||
]
|
||||
|
||||
|
||||
def test_completed_single_stage_keeps_stage_specific_result_handoff(tmp_path: Path) -> None:
|
||||
result = _run_empty_plan_branch(
|
||||
tmp_path,
|
||||
{
|
||||
"success": True,
|
||||
"plan": {
|
||||
"stages": [],
|
||||
"all_stages": ["ppi"],
|
||||
"description": "所有阶段已完成",
|
||||
},
|
||||
"completed_stages": ["ppi"],
|
||||
},
|
||||
)
|
||||
|
||||
assert result["filters"] == ["ppi"]
|
||||
assert result["refreshes"] == [["job-test", "ppi"]]
|
||||
assert [item["message"] for item in result["messages"]] == [
|
||||
"PPI分析 已有同一分析范围的有效结果,无需重复执行。"
|
||||
]
|
||||
|
||||
|
||||
def test_generic_completed_plan_label_has_english_translation() -> None:
|
||||
translations = I18N_PATH.read_text(encoding="utf-8")
|
||||
|
||||
assert (
|
||||
'"当前请求中的分析阶段均已有同一分析范围的有效结果,无需重复执行。": '
|
||||
'"All analysis stages in the current request already have valid results for the same analysis scope. No need to re-execute."'
|
||||
) in translations
|
||||
@@ -0,0 +1,124 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
_ORIGINAL_PATH_MKDIR = Path.mkdir
|
||||
|
||||
|
||||
def _ignore_broken_remote_mount_mkdir(path: Path, *args, **kwargs):
|
||||
if path.is_symlink() and not path.exists():
|
||||
return None
|
||||
return _ORIGINAL_PATH_MKDIR(path, *args, **kwargs)
|
||||
|
||||
|
||||
Path.mkdir = _ignore_broken_remote_mount_mkdir
|
||||
try:
|
||||
from apps.web.vue3 import main as web_main
|
||||
finally:
|
||||
Path.mkdir = _ORIGINAL_PATH_MKDIR
|
||||
|
||||
|
||||
def _full_plan_inputs():
|
||||
stage_ids = ["identify", *web_main.FULL_GENE_FAMILY_WORKFLOW_STAGES]
|
||||
details = [web_main._stage_detail_defaults(stage_id) for stage_id in stage_ids]
|
||||
params = {
|
||||
"targetSpecies": ["Ft"],
|
||||
"referenceSpecies": ["At"],
|
||||
"geneFamily": "SPL",
|
||||
}
|
||||
return stage_ids, details, params
|
||||
|
||||
|
||||
def test_full_workflow_normalization_respects_intent_stage_exclusions():
|
||||
stage_ids, details, params = _full_plan_inputs()
|
||||
|
||||
for excluded_stage in ("kaks", "karyotype"):
|
||||
authorized = [stage for stage in stage_ids if stage != excluded_stage]
|
||||
normalized, _ = web_main._normalize_full_gene_family_workflow_plan(
|
||||
message=f"执行基因家族分析,但是不要执行{excluded_stage}",
|
||||
stage_ids=stage_ids,
|
||||
stages_detail=details,
|
||||
plan_params=dict(params),
|
||||
plan_prefix="Ft.SPL",
|
||||
authorized_stage_ids=authorized,
|
||||
)
|
||||
|
||||
assert excluded_stage not in normalized
|
||||
assert set(normalized) == set(authorized)
|
||||
|
||||
|
||||
def test_full_workflow_normalization_keeps_full_intent_and_required_dependencies():
|
||||
stage_ids, details, params = _full_plan_inputs()
|
||||
normalized, _ = web_main._normalize_full_gene_family_workflow_plan(
|
||||
message="执行基因家族分析",
|
||||
stage_ids=stage_ids,
|
||||
stages_detail=details,
|
||||
plan_params=dict(params),
|
||||
plan_prefix="Ft.SPL",
|
||||
authorized_stage_ids=stage_ids,
|
||||
)
|
||||
|
||||
assert normalized == stage_ids
|
||||
assert normalized.index("ppi") < normalized.index("kaks")
|
||||
|
||||
normalized_without_supplied_intent, _ = web_main._normalize_full_gene_family_workflow_plan(
|
||||
message="执行基因家族分析",
|
||||
stage_ids=stage_ids,
|
||||
stages_detail=details,
|
||||
plan_params=dict(params),
|
||||
plan_prefix="Ft.SPL",
|
||||
authorized_stage_ids=[],
|
||||
)
|
||||
assert normalized_without_supplied_intent == stage_ids
|
||||
|
||||
dependency_stages = ["identify", "synteny", "karyotype"]
|
||||
dependency_details = [web_main._stage_detail_defaults(stage) for stage in dependency_stages]
|
||||
normalized_dependencies, _ = web_main._normalize_full_gene_family_workflow_plan(
|
||||
message="执行核型分析",
|
||||
stage_ids=dependency_stages,
|
||||
stages_detail=dependency_details,
|
||||
plan_params=dict(params),
|
||||
plan_prefix="Ft.SPL",
|
||||
authorized_stage_ids=dependency_stages,
|
||||
)
|
||||
assert normalized_dependencies == dependency_stages
|
||||
|
||||
|
||||
def test_contextual_full_workflow_uses_request_intent_as_stage_authority():
|
||||
stage_ids, _, params = _full_plan_inputs()
|
||||
authorized = [stage for stage in stage_ids if stage != "kaks"]
|
||||
request = web_main.AgentPlanRequest(
|
||||
message="执行基因家族分析,但是不要执行kaks分析",
|
||||
prefix="Ft.SPL",
|
||||
intent={"stages": authorized, "params": params},
|
||||
)
|
||||
contextual_request = {
|
||||
"family": "SPL",
|
||||
"prefix": "Ft.SPL",
|
||||
"stages": stage_ids,
|
||||
"targetSpecies": ["Ft"],
|
||||
"referenceSpecies": ["At"],
|
||||
"comparativeSpecies": [],
|
||||
"scopeMode": "target_plus_reference",
|
||||
"requiresConfirmedSubfamily": True,
|
||||
"skipSubfamilyLabels": False,
|
||||
"subfamilyState": {"by_species": {"Ft": {"status": "confirmed"}}},
|
||||
"contextGate": {"status": "ready", "issues": []},
|
||||
}
|
||||
|
||||
response = web_main._build_contextual_gene_family_plan_response(
|
||||
request,
|
||||
"test_intent_stage_authority",
|
||||
contextual_request,
|
||||
)
|
||||
|
||||
assert "kaks" not in response["plan"]["stages"]
|
||||
|
||||
|
||||
def test_llm_plan_normalization_calls_keep_original_intent_stage_authority():
|
||||
source = (web_main.PROJECT_ROOT / "apps/web/vue3/main.py").read_text(encoding="utf-8")
|
||||
start = source.index("full_workflow_normalized = False")
|
||||
end = source.index("stage_ids, stages_detail = _apply_standard_combo_panel_plan", start)
|
||||
normalization_block = source[start:end]
|
||||
|
||||
assert "plan_authorized_stage_ids = authoritative_intent_stage_ids or" in source
|
||||
assert normalization_block.count("authorized_stage_ids=plan_authorized_stage_ids") == 2
|
||||
@@ -0,0 +1,144 @@
|
||||
import asyncio
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
from gfa_core.mas.agents.base import AgentMessage, AgentState
|
||||
from gfa_core.mas.agents.intent_v2 import IntentAgentV2
|
||||
from gfa_core.mas.core.fast_intent import fast_intent_recognition
|
||||
from gfa_core.mas.utils.intent_parser import CORE_STAGES, parse_intent_from_query
|
||||
|
||||
|
||||
def test_complete_gene_family_workflow_excludes_explicitly_negated_ppi():
|
||||
intent = parse_intent_from_query("执行完整流程,但不执行PPI")
|
||||
|
||||
assert intent["stages"] == [stage for stage in CORE_STAGES if stage != "ppi"]
|
||||
|
||||
|
||||
def test_symbolic_minus_excludes_stage_from_complete_workflow():
|
||||
intent = parse_intent_from_query("完整流程 - PPI")
|
||||
|
||||
assert intent["stages"] == [stage for stage in CORE_STAGES if stage != "ppi"]
|
||||
|
||||
|
||||
def test_mixed_positive_and_negative_stages_keep_only_positive_targets():
|
||||
intent = parse_intent_from_query("做系统发育和Motif,不要做结构域分析")
|
||||
|
||||
assert intent["stages"] == ["phylogeny", "motif"]
|
||||
|
||||
|
||||
def test_positive_ppi_and_kaks_request_is_unchanged():
|
||||
intent = parse_intent_from_query("执行PPI和KaKs分析")
|
||||
|
||||
assert intent["stages"] == ["kaks", "ppi"]
|
||||
|
||||
|
||||
def test_generic_rnaseq_workflow_excludes_wgcna_target():
|
||||
intent = parse_intent_from_query("执行RNA-seq分析,但不要WGCNA")
|
||||
|
||||
assert intent["stages"] == [
|
||||
"rnaseq_de",
|
||||
"rnaseq_go_enrich",
|
||||
"rnaseq_kegg_enrich",
|
||||
"rnaseq_fgsea",
|
||||
"rnaseq_candidate_family",
|
||||
]
|
||||
|
||||
|
||||
def test_pure_negative_request_never_becomes_positive_in_rule_fallbacks():
|
||||
parsed = parse_intent_from_query("不要执行PPI")
|
||||
fallback = IntentAgentV2(None)._fallback_recognition("不要执行PPI")
|
||||
fast = fast_intent_recognition("不要执行PPI")
|
||||
|
||||
assert parsed["stages"] == []
|
||||
assert parsed["need_clarify"] is True
|
||||
assert fallback["stages"] == []
|
||||
assert fallback["needs_clarification"] is True
|
||||
assert fast is None
|
||||
|
||||
|
||||
def test_model_output_cannot_reintroduce_a_negated_stage():
|
||||
class _PpiLLM:
|
||||
async def acall(self, *_args, **_kwargs):
|
||||
return SimpleNamespace(
|
||||
content=json.dumps(
|
||||
{
|
||||
"gene_family": "SPL",
|
||||
"species": "Ft",
|
||||
"stages": ["ppi"],
|
||||
"primary_stage": "ppi",
|
||||
"description": "执行PPI分析",
|
||||
"requirements": [],
|
||||
"confidence": 0.9,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
state = AgentState(session_id="negated-stage", job_id="job-negated-stage")
|
||||
message = AgentMessage(
|
||||
sender="router",
|
||||
receiver="intent_v2",
|
||||
message_type="recognize_intent",
|
||||
payload={"query": "不要执行PPI"},
|
||||
)
|
||||
|
||||
returned = asyncio.run(IntentAgentV2(_PpiLLM()).execute(state, message))
|
||||
intent = returned.results["results"]["intent"]
|
||||
|
||||
assert intent["stages"] == []
|
||||
assert intent["needs_clarification"] is True
|
||||
|
||||
|
||||
def test_dependency_completion_can_restore_required_non_target_stage():
|
||||
class _ForbiddenLLM:
|
||||
async def acall(self, *_args, **_kwargs):
|
||||
raise AssertionError("deterministic intent must not call the LLM")
|
||||
|
||||
state = AgentState(session_id="dependency", job_id="job-dependency")
|
||||
message = AgentMessage(
|
||||
sender="router",
|
||||
receiver="intent_v2",
|
||||
message_type="recognize_intent",
|
||||
payload={"query": "执行核型分析,不单独执行种内共线性"},
|
||||
)
|
||||
|
||||
returned = asyncio.run(IntentAgentV2(_ForbiddenLLM()).execute(state, message))
|
||||
intent = returned.results["results"]["intent"]
|
||||
|
||||
assert intent["primary_stage"] == "karyotype"
|
||||
assert intent["stages"] == ["identify", "synteny", "karyotype"]
|
||||
|
||||
|
||||
def test_completed_subfamily_context_does_not_override_current_stage_negation():
|
||||
class _ForbiddenLLM:
|
||||
async def acall(self, *_args, **_kwargs):
|
||||
raise AssertionError("deterministic intent must not call the LLM")
|
||||
|
||||
state = AgentState(session_id="context-negation", job_id="job-context-negation")
|
||||
message = AgentMessage(
|
||||
sender="router",
|
||||
receiver="intent_v2",
|
||||
message_type="recognize_intent",
|
||||
payload={
|
||||
"query": (
|
||||
"帮我执行Ft的基因家族分析,但是不要执行kaks分析\n"
|
||||
"已确认上下文:基因家族 SPL;目标物种 Ft;参考物种 At;"
|
||||
"当前 job 的 subfamily_confirm 已成功完成,家族成员和亚家族分类表已确认。"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
returned = asyncio.run(IntentAgentV2(_ForbiddenLLM()).execute(state, message))
|
||||
intent = returned.results["results"]["intent"]
|
||||
|
||||
assert intent["gene_family"] == "SPL"
|
||||
assert intent["species"] == "Ft"
|
||||
assert "ppi" in intent["stages"]
|
||||
assert "kaks" not in intent["stages"]
|
||||
assert "reference_classification" not in intent["stages"]
|
||||
|
||||
|
||||
def test_subfamily_classification_bypasses_generic_stage_negation():
|
||||
intent = parse_intent_from_query("执行亚家族分类,不要结构域分析")
|
||||
|
||||
assert "reference_classification" in intent["stages"]
|
||||
assert "domain" in intent["stages"]
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
def _write_ft_technical_reference_manifest(final_dir, stage, *, status="success"):
|
||||
@@ -34,6 +35,50 @@ def _write_ft_technical_reference_manifest(final_dir, stage, *, status="success"
|
||||
return manifest
|
||||
|
||||
|
||||
def _write_regular_gene_family_manifest(
|
||||
final_dir,
|
||||
stage,
|
||||
*,
|
||||
scope_id="SPL.target-Ft.ref-At.target_plus_reference",
|
||||
route="multi_species",
|
||||
references=None,
|
||||
member_set_id="",
|
||||
filename="",
|
||||
with_artifact=True,
|
||||
):
|
||||
references = ["At"] if references is None else list(references)
|
||||
member_set_id = member_set_id or f"{scope_id}:{route}:all_members"
|
||||
artifact = final_dir / f"Ft.SPL.{stage}.result.tsv"
|
||||
if with_artifact:
|
||||
artifact.write_text(f"{stage}\tresult\n", encoding="utf-8")
|
||||
manifest = final_dir / (filename or f"Ft.SPL.scoped.{stage}.manifest.json")
|
||||
manifest.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"manifest_version": "1.0",
|
||||
"stage": stage,
|
||||
"status": "success",
|
||||
"prefix": "Ft.SPL",
|
||||
"scope_id": scope_id,
|
||||
"scope_mode": "target_plus_reference" if references else "target_only",
|
||||
"analysis_route": route,
|
||||
"member_set_id": member_set_id,
|
||||
"family": "SPL",
|
||||
"target_species": ["Ft"],
|
||||
"reference_species": references,
|
||||
"artifacts": (
|
||||
{"result": {"path": f"final/{artifact.name}", "type": "tsv"}}
|
||||
if with_artifact
|
||||
else {"result": {"path": "final/missing.tsv", "type": "tsv"}}
|
||||
),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return manifest
|
||||
|
||||
|
||||
def _confirmed_contextual_ppi_fixture(web_main, workspace_root, job_id, intent_params):
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -416,6 +461,179 @@ def test_identity_completed_stages_force_rerun_keeps_technical_stages_pending(tm
|
||||
assert completed == []
|
||||
|
||||
|
||||
def test_post_confirm_plan_recovers_regular_manifest_identity(tmp_path, monkeypatch):
|
||||
from apps.web.vue3 import main as web_main
|
||||
|
||||
workspace_root = tmp_path / "workspace"
|
||||
final_dir = workspace_root / "job_regular_recovery" / "final"
|
||||
final_dir.mkdir(parents=True)
|
||||
confirm = final_dir / "Ft.SPL.subfamily_confirm.manifest.json"
|
||||
confirm.write_text('{"stage":"subfamily_confirm","status":"success","prefix":"Ft.SPL"}', encoding="utf-8")
|
||||
manifest = _write_regular_gene_family_manifest(final_dir, "phylogeny")
|
||||
os.utime(confirm, (1, 1))
|
||||
os.utime(manifest, (2, 2))
|
||||
monkeypatch.setattr(web_main, "WORKSPACE_ROOT", workspace_root)
|
||||
|
||||
scope_id = "SPL.target-Ft.ref-At.target_plus_reference"
|
||||
plan_params = {
|
||||
"prefix": "Ft.SPL",
|
||||
"geneFamily": "SPL",
|
||||
"targetSpecies": ["Ft"],
|
||||
"referenceSpecies": ["At"],
|
||||
"workflowPhase": "post_subfamily_confirm",
|
||||
"requiresConfirmedSubfamily": True,
|
||||
}
|
||||
stage_params = {
|
||||
**plan_params,
|
||||
"scopeMode": "target_plus_reference",
|
||||
"analysisRoute": "multi_species",
|
||||
"memberSetId": f"{scope_id}:confirmed_subfamily:all_members",
|
||||
}
|
||||
|
||||
completed = web_main._identity_completed_stages_for_plan(
|
||||
"job_regular_recovery",
|
||||
["phylogeny"],
|
||||
plan_params,
|
||||
[{"id": "phylogeny", "params": stage_params}],
|
||||
request_prefix="Ft.SPL",
|
||||
)
|
||||
|
||||
assert completed == ["phylogeny"]
|
||||
|
||||
|
||||
def test_post_confirm_plan_tries_next_manifest_after_incomplete_alias(tmp_path, monkeypatch):
|
||||
from apps.web.vue3 import main as web_main
|
||||
|
||||
workspace_root = tmp_path / "workspace"
|
||||
final_dir = workspace_root / "job_regular_alias" / "final"
|
||||
final_dir.mkdir(parents=True)
|
||||
confirm = final_dir / "Ft.SPL.subfamily_confirm.manifest.json"
|
||||
confirm.write_text('{"stage":"subfamily_confirm","status":"success","prefix":"Ft.SPL"}', encoding="utf-8")
|
||||
canonical = _write_regular_gene_family_manifest(
|
||||
final_dir,
|
||||
"synteny",
|
||||
route="single_species",
|
||||
filename="Ft.SPL.scoped.synteny.manifest.json",
|
||||
)
|
||||
alias = _write_regular_gene_family_manifest(
|
||||
final_dir,
|
||||
"synteny",
|
||||
route="single_species",
|
||||
filename="Ft.SPL.synteny.manifest.json",
|
||||
with_artifact=False,
|
||||
)
|
||||
alias_payload = json.loads(alias.read_text(encoding="utf-8"))
|
||||
for key in ("scope_id", "scope_mode", "analysis_route", "member_set_id"):
|
||||
alias_payload.pop(key, None)
|
||||
alias.write_text(json.dumps(alias_payload), encoding="utf-8")
|
||||
os.utime(confirm, (1, 1))
|
||||
os.utime(canonical, (2, 2))
|
||||
os.utime(alias, (3, 3))
|
||||
monkeypatch.setattr(web_main, "WORKSPACE_ROOT", workspace_root)
|
||||
|
||||
target_scope = "SPL.target-Ft.target_only"
|
||||
plan_params = {
|
||||
"prefix": "Ft.SPL",
|
||||
"geneFamily": "SPL",
|
||||
"targetSpecies": ["Ft"],
|
||||
"referenceSpecies": ["At"],
|
||||
"workflowPhase": "post_subfamily_confirm",
|
||||
"requiresConfirmedSubfamily": True,
|
||||
}
|
||||
stage_params = {
|
||||
**plan_params,
|
||||
"referenceSpecies": [],
|
||||
"scopeMode": "target_only",
|
||||
"analysisRoute": "single_species",
|
||||
"memberSetId": f"{target_scope}:confirmed_subfamily:all_members",
|
||||
}
|
||||
|
||||
completed = web_main._identity_completed_stages_for_plan(
|
||||
"job_regular_alias",
|
||||
["synteny"],
|
||||
plan_params,
|
||||
[{"id": "synteny", "params": stage_params}],
|
||||
request_prefix="Ft.SPL",
|
||||
)
|
||||
|
||||
assert completed == ["synteny"]
|
||||
|
||||
|
||||
def test_post_confirm_plan_rejects_result_older_than_confirmation(tmp_path, monkeypatch):
|
||||
from apps.web.vue3 import main as web_main
|
||||
|
||||
workspace_root = tmp_path / "workspace"
|
||||
final_dir = workspace_root / "job_regular_stale" / "final"
|
||||
final_dir.mkdir(parents=True)
|
||||
manifest = _write_regular_gene_family_manifest(final_dir, "phylogeny")
|
||||
confirm = final_dir / "Ft.SPL.subfamily_confirm.manifest.json"
|
||||
confirm.write_text('{"stage":"subfamily_confirm","status":"success","prefix":"Ft.SPL"}', encoding="utf-8")
|
||||
os.utime(manifest, (1, 1))
|
||||
os.utime(confirm, (2, 2))
|
||||
monkeypatch.setattr(web_main, "WORKSPACE_ROOT", workspace_root)
|
||||
|
||||
scope_id = "SPL.target-Ft.ref-At.target_plus_reference"
|
||||
plan_params = {
|
||||
"prefix": "Ft.SPL",
|
||||
"geneFamily": "SPL",
|
||||
"targetSpecies": ["Ft"],
|
||||
"referenceSpecies": ["At"],
|
||||
"workflowPhase": "post_subfamily_confirm",
|
||||
"requiresConfirmedSubfamily": True,
|
||||
}
|
||||
stage_params = {
|
||||
**plan_params,
|
||||
"scopeMode": "target_plus_reference",
|
||||
"analysisRoute": "multi_species",
|
||||
"memberSetId": f"{scope_id}:confirmed_subfamily:all_members",
|
||||
}
|
||||
|
||||
completed = web_main._identity_completed_stages_for_plan(
|
||||
"job_regular_stale",
|
||||
["phylogeny"],
|
||||
plan_params,
|
||||
[{"id": "phylogeny", "params": stage_params}],
|
||||
request_prefix="Ft.SPL",
|
||||
)
|
||||
|
||||
assert completed == []
|
||||
|
||||
|
||||
def test_post_confirm_kaks_inherits_completed_shared_ppi_reference(tmp_path, monkeypatch):
|
||||
from apps.web.vue3 import main as web_main
|
||||
|
||||
workspace_root = tmp_path / "workspace"
|
||||
final_dir = workspace_root / "job_contextual_kaks" / "final"
|
||||
final_dir.mkdir(parents=True)
|
||||
_write_ft_technical_reference_manifest(final_dir, "kaks")
|
||||
monkeypatch.setattr(web_main, "WORKSPACE_ROOT", workspace_root)
|
||||
|
||||
plan_params = {
|
||||
"prefix": "Ft.SPL",
|
||||
"geneFamily": "SPL",
|
||||
"targetSpecies": ["Ft"],
|
||||
"referenceSpecies": ["At"],
|
||||
"workflowPhase": "post_subfamily_confirm",
|
||||
"requiresConfirmedSubfamily": True,
|
||||
}
|
||||
stage_params = {
|
||||
**plan_params,
|
||||
"scopeMode": "target_plus_reference",
|
||||
"analysisRoute": "multi_species",
|
||||
"memberSetId": "SPL.target-Ft.ref-At.target_plus_reference:confirmed_subfamily:all_members",
|
||||
}
|
||||
|
||||
completed = web_main._identity_completed_stages_for_plan(
|
||||
"job_contextual_kaks",
|
||||
["kaks"],
|
||||
plan_params,
|
||||
[{"id": "kaks", "params": stage_params}],
|
||||
request_prefix="Ft.SPL",
|
||||
)
|
||||
|
||||
assert completed == ["kaks"]
|
||||
|
||||
|
||||
|
||||
|
||||
def test_contextual_plan_returns_only_pending_stages(tmp_path, monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user