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

902 lines
30 KiB
Python

from __future__ import annotations
import asyncio
import json
import pytest
from gfa_core.mas.agents.base import AgentMessage, AgentState
from gfa_core.mas.agents.intent_v2 import IntentAgentV2
from gfa_core.mas.agents.planning_v2 import PlanningAgentV2, StageStatus
from gfa_core.mas.stage_catalog import complete_dependencies
from gfa_core.mas.utils.intent_parser import parse_intent_from_query
WGS_STAGES = [
"wgs_qc", "wgs_index", "wgs_align", "wgs_markdup", "wgs_bam_qc",
"wgs_haplotype_calling", "wgs_gvcf_combine", "wgs_joint_genotyping",
"wgs_variant_select", "wgs_variant_filter", "wgs_impute",
"wgs_maf_missing_filter",
]
EXPLICIT_STAGE_PHRASES = [
("WGS质控", "wgs_qc"), ("WGS quality control", "wgs_qc"),
("WGS参考索引", "wgs_index"), ("WGS reference index", "wgs_index"),
("WGS比对", "wgs_align"), ("WGS alignment", "wgs_align"),
("WGS去重复", "wgs_markdup"), ("WGS mark duplicates", "wgs_markdup"),
("WGS 去重复", "wgs_markdup"),
("WGS BAM质控", "wgs_bam_qc"), ("WGS BAM QC", "wgs_bam_qc"),
("WGS单样本变异检测", "wgs_haplotype_calling"), ("WGS haplotype calling", "wgs_haplotype_calling"),
("WGS GVCF合并", "wgs_gvcf_combine"), ("WGS GVCF combine", "wgs_gvcf_combine"),
("WGS联合分型", "wgs_joint_genotyping"), ("WGS joint genotyping", "wgs_joint_genotyping"),
("WGS变异选择", "wgs_variant_select"), ("WGS variant selection", "wgs_variant_select"),
("WGS变异过滤", "wgs_variant_filter"), ("WGS variant filtering", "wgs_variant_filter"),
("WGS基因型填充", "wgs_impute"), ("WGS genotype imputation", "wgs_impute"),
("WGS MAF缺失率过滤", "wgs_maf_missing_filter"), ("WGS MAF missingness filter", "wgs_maf_missing_filter"),
]
@pytest.mark.parametrize(("phrase", "stage"), EXPLICIT_STAGE_PHRASES)
def test_explicit_wgs_stage_phrase_is_deterministic(phrase: str, stage: str) -> None:
intent = parse_intent_from_query(f"请执行{phrase}")
assert intent["stages"] == [stage]
assert intent["primary_stage"] == stage
@pytest.mark.parametrize("phrase", ["WGS全流程", "VCF全流程"])
def test_bare_wgs_vcf_full_chain_targets_stage12_with_complete_closure(phrase: str) -> None:
intent = parse_intent_from_query(phrase)
assert intent["primary_stage"] == "wgs_maf_missing_filter"
assert complete_dependencies(intent["stages"]) == WGS_STAGES
def test_wgs_deterministic_routing_precedes_llm_fallback(monkeypatch) -> None:
agent = IntentAgentV2(None)
async def forbidden_fallback(*_args, **_kwargs):
raise AssertionError("LLM fallback must not run for deterministic WGS intent")
monkeypatch.setattr(agent, "_recognize_intent_with_llm", forbidden_fallback)
state = AgentState(session_id="session")
message = AgentMessage(
sender="user", receiver="intent_v2", message_type="query",
payload={"query": "VCF全流程"},
)
result = asyncio.run(agent.execute(state, message))
intent = result.results["results"]["intent"]
assert intent["primary_stage"] == "wgs_maf_missing_filter"
assert intent["stages"] == WGS_STAGES
def test_non_wgs_intent_is_unchanged() -> None:
intent = parse_intent_from_query("构建系统发育树")
assert intent["primary_stage"] == "phylogeny"
assert "wgs_qc" not in intent["stages"]
def test_wgs_full_workflow_is_not_gene_family_full_workflow() -> None:
from apps.web.vue3.main import _looks_like_gene_family_workflow_query
assert _looks_like_gene_family_workflow_query("执行 WGS 全流程") is False
assert _looks_like_gene_family_workflow_query("执行基因家族全流程") is True
def test_wgs_plan_recovers_identity_from_success_manifest(tmp_path, monkeypatch) -> None:
from apps.web.vue3 import main as web_main
final_dir = tmp_path / "job" / "final"
final_dir.mkdir(parents=True)
(final_dir / "cohort.wgs_qc.manifest.json").write_text(
json.dumps({
"stage": "wgs_qc",
"status": "success",
"prefix": "cohort",
"wgs_sample_set_id": "samples-1",
"wgs_analysis_id": "analysis-1",
}),
encoding="utf-8",
)
monkeypatch.setattr(web_main, "WORKSPACE_ROOT", tmp_path)
assert web_main._wgs_manifest_identity_for_plan("job", "wgs_qc", "cohort") == {
"wgsSampleSetId": "samples-1",
"wgsReferenceId": "",
"wgsAnalysisId": "analysis-1",
}
def test_wgs_plan_drops_bindings_for_upstreams_that_will_rerun() -> None:
from apps.web.vue3 import main as web_main
params = {
"wgsQcManifest": "/old/cohort.wgs_qc.manifest.json",
"wgsQcAnalysisId": "old-qc",
"wgsIndexManifest": "/old/cohort.wgs_index.manifest.json",
"wgsReferenceId": "old-reference",
"wgsAlignManifest": "/old/cohort.wgs_align.manifest.json",
"wgsMethod": "gatk",
}
details = [{"id": "wgs_maf_missing_filter", "params": dict(params)}]
web_main._clear_planned_wgs_upstream_bindings(
["wgs_qc", "wgs_index", "wgs_align", "wgs_maf_missing_filter"], params, details
)
assert params == {"wgsMethod": "gatk"}
assert details[0]["params"] == {"wgsMethod": "gatk"}
def test_agent_plan_uses_current_wgs_message_over_stale_intent(monkeypatch) -> None:
from apps.web.vue3 import main as web_main
from gfa_core.mas.tools import stage_status
class Status:
def __init__(self, completed: bool):
self.status = "success" if completed else "not_found"
self.completed = completed
self.manifest_path = None
self.missing_artifacts = []
self.errors = []
monkeypatch.setattr(
web_main,
"_load_methodology_plan_for_agent_request",
lambda _request: (None, None, None),
)
monkeypatch.setattr(
web_main,
"_wgs_manifest_identity_for_plan",
lambda *_args: {
"wgsSampleSetId": "samples",
"wgsReferenceId": "reference",
"wgsAnalysisId": "analysis",
},
)
monkeypatch.setattr(
stage_status,
"get_stage_status",
lambda **kwargs: Status(kwargs["stage"] != "wgs_maf_missing_filter"),
)
response = asyncio.run(
web_main.agent_plan(
web_main.AgentPlanRequest(
message="执行 WGS MAF缺失率过滤",
job_id="job-1",
intent={"stages": ["wgs_variant_select"]},
)
)
)
assert response["success"] is True
assert response["mode"] == "deterministic_wgs_plan"
assert response["plan"]["stages"] == ["wgs_maf_missing_filter"]
def test_agent_plan_keeps_only_missing_wgs_stages(monkeypatch) -> None:
from apps.web.vue3 import main as web_main
from gfa_core.mas.tools import stage_status
class Status:
def __init__(self, completed: bool):
self.status = "success" if completed else "not_found"
self.completed = completed
self.manifest_path = None
self.missing_artifacts = []
self.errors = []
monkeypatch.setattr(
web_main,
"_load_methodology_plan_for_agent_request",
lambda _request: (None, None, None),
)
monkeypatch.setattr(
web_main,
"_wgs_manifest_identity_for_plan",
lambda *_args: {
"wgsSampleSetId": "samples",
"wgsReferenceId": "reference",
"wgsAnalysisId": "analysis",
},
)
missing = {"wgs_qc", "wgs_index", "wgs_align", "wgs_markdup"}
monkeypatch.setattr(
stage_status,
"get_stage_status",
lambda **kwargs: Status(kwargs["stage"] not in missing),
)
response = asyncio.run(
web_main.agent_plan(
web_main.AgentPlanRequest(
message="执行 WGS 全流程分析",
job_id="job-1",
intent={"stages": ["wgs_variant_select"]},
)
)
)
assert response["success"] is True
assert response["plan"]["stages"] == [
"wgs_qc", "wgs_index", "wgs_align", "wgs_markdup"
]
def test_agent_plan_seeds_completed_qc_only_into_pending_align(monkeypatch) -> None:
from apps.web.vue3 import main as web_main
from gfa_core import wgs_upstream_manifest
from gfa_core.mas.tools import stage_status
class Status:
def __init__(self, stage: str):
self.status = "success" if stage == "wgs_qc" else "not_found"
self.completed = stage == "wgs_qc"
self.manifest_path = (
"/workspace/job-1/final/Os.wgs_qc.manifest.json"
if self.completed else None
)
self.missing_artifacts = []
self.errors = []
monkeypatch.setattr(
web_main,
"_load_methodology_plan_for_agent_request",
lambda _request: (None, None, None),
)
monkeypatch.setattr(
web_main,
"_wgs_manifest_identity_for_plan",
lambda *_args: {
"wgsSampleSetId": "samples",
"wgsReferenceId": "reference",
"wgsAnalysisId": "wgs-analysis:" + "c" * 64,
},
)
monkeypatch.setattr(
stage_status, "get_stage_status", lambda **kwargs: Status(kwargs["stage"])
)
monkeypatch.setattr(
wgs_upstream_manifest,
"materialize_completed_wgs_upstream_params",
lambda **kwargs: (
{
"wgsQcManifest": "/workspace/job-1/final/Os.wgs_qc.manifest.json",
"wgsSampleSetId": "wgs-samples:" + "a" * 64,
}
if kwargs["stage"] == "wgs_align"
and set(kwargs["completed_stages"]) == {"wgs_qc"}
else {}
),
)
response = asyncio.run(
web_main.agent_plan(
web_main.AgentPlanRequest(
message="执行 WGS比对",
job_id="job-1",
prefix="Os",
)
)
)
assert response["plan"]["stages"] == ["wgs_index", "wgs_align"]
details = {item["id"]: item["params"] for item in response["plan"]["stages_detail"]}
assert "wgsQcManifest" not in details["wgs_index"]
assert details["wgs_align"]["wgsQcManifest"].endswith(
"/Os.wgs_qc.manifest.json"
)
assert details["wgs_align"]["wgsSampleSetId"].startswith("wgs-samples:")
assert "wgsIndexManifest" not in details["wgs_align"]
assert "wgsQcManifest" not in response["plan"]["params"]
def test_agent_plan_keeps_completed_downstream_stages_when_upstreams_were_deleted(
monkeypatch,
) -> None:
from apps.web.vue3 import main as web_main
from gfa_core.mas.tools import stage_status
class Status:
def __init__(self, completed: bool):
self.status = "success" if completed else "not_found"
self.completed = completed
self.manifest_path = None
self.missing_artifacts = []
self.errors = []
monkeypatch.setattr(
web_main,
"_load_methodology_plan_for_agent_request",
lambda _request: (None, None, None),
)
monkeypatch.setattr(
web_main,
"_wgs_manifest_identity_for_plan",
lambda *_args: {
"wgsSampleSetId": "samples",
"wgsReferenceId": "reference",
"wgsAnalysisId": "analysis",
},
)
missing = {
"wgs_qc", "wgs_index", "wgs_align", "wgs_markdup",
"wgs_maf_missing_filter",
}
def fake_status(**kwargs):
completed = (
kwargs["stage"] not in missing
and kwargs.get("validate_wgs_upstream_authority") is False
)
return Status(completed)
monkeypatch.setattr(stage_status, "get_stage_status", fake_status)
response = asyncio.run(
web_main.agent_plan(
web_main.AgentPlanRequest(
message="执行 WGS 全流程分析",
job_id="job-1",
prefix="Os",
)
)
)
assert response["success"] is True
assert response["plan"]["stages"] == [
"wgs_qc", "wgs_index", "wgs_align", "wgs_markdup",
"wgs_maf_missing_filter",
]
@pytest.mark.parametrize(
("message", "target"),
[
("执行 WGS BAM质控", "wgs_bam_qc"),
("执行 WGS变异过滤", "wgs_variant_filter"),
],
)
def test_agent_plan_limits_every_status_set_to_requested_wgs_closure(
monkeypatch, message: str, target: str
) -> None:
from apps.web.vue3 import main as web_main
from gfa_core.mas.tools import stage_status
class Status:
def __init__(self, stage: str):
self.status = "success" if stage == "wgs_markdup" else "not_found"
self.completed = stage == "wgs_markdup"
self.manifest_path = "/final/Os.wgs_markdup.manifest.json" if self.completed else None
self.missing_artifacts = []
self.errors = []
monkeypatch.setattr(
web_main, "_load_methodology_plan_for_agent_request", lambda _request: (None, None, None)
)
monkeypatch.setattr(
web_main,
"_wgs_manifest_identity_for_plan",
lambda *_args: {
"wgsSampleSetId": "samples",
"wgsReferenceId": "reference",
"wgsAnalysisId": "analysis",
},
)
monkeypatch.setattr(
stage_status, "get_stage_status", lambda **kwargs: Status(kwargs["stage"])
)
response = asyncio.run(
web_main.agent_plan(
web_main.AgentPlanRequest(message=message, job_id="job-1", prefix="Os")
)
)
closure = complete_dependencies([target])
assert response["plan"]["stages"] == [
stage for stage in closure if stage != "wgs_markdup"
]
assert [item["id"] for item in response["plan"]["stages_detail"]] == response["plan"]["stages"]
assert response["completed_stages"] == ["wgs_markdup"]
assert list(response["plan"]["stage_status_summary"]) == closure
def test_agent_plan_rejects_multiple_manifest_prefixes_instead_of_using_cohort(
tmp_path, monkeypatch
) -> None:
from apps.web.vue3 import main as web_main
from gfa_core.mas.tools import stage_status
final_dir = tmp_path / "job-1" / "final"
final_dir.mkdir(parents=True)
for prefix in ("Os", "Rice"):
(final_dir / f"{prefix}.wgs_qc.manifest.json").write_text(
json.dumps({"stage": "wgs_qc", "status": "success", "prefix": prefix}),
encoding="utf-8",
)
class Status:
status = "not_found"
completed = False
manifest_path = None
missing_artifacts = []
errors = []
monkeypatch.setattr(web_main, "WORKSPACE_ROOT", tmp_path)
monkeypatch.setattr(
web_main, "_load_methodology_plan_for_agent_request", lambda _request: (None, None, None)
)
monkeypatch.setattr(stage_status, "get_stage_status", lambda **_kwargs: Status())
response = asyncio.run(
web_main.agent_plan(web_main.AgentPlanRequest(message="执行 WGS质控", job_id="job-1"))
)
assert response["success"] is False
assert "prefix" in response.get("message", "").lower()
def test_wgs_plan_recovers_unique_manifest_prefix_when_requested_prefix_is_stale(
tmp_path, monkeypatch
) -> None:
from apps.web.vue3 import main as web_main
final_dir = tmp_path / "job-1" / "final"
final_dir.mkdir(parents=True)
(final_dir / "Os.wgs_qc.manifest.json").write_text(
json.dumps({"stage": "wgs_qc", "status": "success", "prefix": "Os"}),
encoding="utf-8",
)
monkeypatch.setattr(web_main, "WORKSPACE_ROOT", tmp_path)
assert web_main._wgs_prefix_for_plan(
"job-1", ["wgs_qc", "wgs_index", "wgs_align"], "At"
) == "Os"
def test_agent_plan_rejects_multiple_scientific_identities_for_one_prefix(
tmp_path, monkeypatch
) -> None:
from apps.web.vue3 import main as web_main
final_dir = tmp_path / "job-1" / "final"
final_dir.mkdir(parents=True)
for suffix, identity in (("", "analysis-1"), (".copy", "analysis-2")):
(final_dir / f"Os.wgs_qc{suffix}.manifest.json").write_text(
json.dumps({
"stage": "wgs_qc",
"status": "success",
"prefix": "Os",
"wgs_sample_set_id": "samples",
"wgs_analysis_id": identity,
}),
encoding="utf-8",
)
monkeypatch.setattr(web_main, "WORKSPACE_ROOT", tmp_path)
monkeypatch.setattr(
web_main, "_load_methodology_plan_for_agent_request", lambda _request: (None, None, None)
)
response = asyncio.run(
web_main.agent_plan(
web_main.AgentPlanRequest(message="执行 WGS质控", job_id="job-1", prefix="Os")
)
)
assert response["success"] is False
assert "identity" in response.get("message", "").lower()
@pytest.mark.parametrize(
("producer", "message", "target"),
[
("wgs_variant_select", "执行 WGS变异过滤", "wgs_variant_filter"),
("wgs_variant_filter", "执行 WGS基因型填充", "wgs_impute"),
],
)
def test_agent_plan_propagates_bcftools_method_from_completed_direct_authority(
tmp_path, monkeypatch, producer: str, message: str, target: str
) -> None:
from apps.web.vue3 import main as web_main
from gfa_core.mas.tools import stage_status
manifest = tmp_path / "job-1" / "final" / f"Cohort.{producer}.bcftools.manifest.json"
manifest.parent.mkdir(parents=True)
manifest.write_text(
json.dumps({
"stage": producer,
"status": "success",
"prefix": "Cohort",
"method": "bcftools",
"parameters": {"method": "bcftools"},
}),
encoding="utf-8",
)
class Status:
def __init__(self, stage: str):
self.status = "not_found" if stage == target else "success"
self.completed = stage != target
self.manifest_path = manifest if stage == producer else None
self.missing_artifacts = []
self.errors = []
monkeypatch.setattr(web_main, "WORKSPACE_ROOT", tmp_path)
monkeypatch.setattr(
web_main, "_load_methodology_plan_for_agent_request", lambda _request: (None, None, None)
)
monkeypatch.setattr(
web_main,
"_wgs_manifest_identity_for_plan",
lambda *_args: {
"wgsSampleSetId": "samples",
"wgsReferenceId": "reference",
"wgsAnalysisId": "analysis",
},
)
monkeypatch.setattr(
stage_status, "get_stage_status", lambda **kwargs: Status(kwargs["stage"])
)
response = asyncio.run(
web_main.agent_plan(
web_main.AgentPlanRequest(message=message, job_id="job-1", prefix="Cohort")
)
)
assert response["plan"]["stages"] == [target]
assert response["plan"]["params"]["wgsMethod"] == "bcftools"
assert response["plan"]["stages_detail"][0]["params"]["wgsMethod"] == "bcftools"
def test_agent_plan_rejects_explicit_method_conflicting_with_completed_authority(
tmp_path, monkeypatch
) -> None:
from apps.web.vue3 import main as web_main
from gfa_core.mas.tools import stage_status
manifest = tmp_path / "job-1" / "final" / "Cohort.wgs_variant_select.bcftools.manifest.json"
manifest.parent.mkdir(parents=True)
manifest.write_text(
json.dumps({
"stage": "wgs_variant_select",
"status": "success",
"prefix": "Cohort",
"method": "bcftools",
}),
encoding="utf-8",
)
class Status:
def __init__(self, stage: str):
self.status = "not_found" if stage == "wgs_variant_filter" else "success"
self.completed = stage != "wgs_variant_filter"
self.manifest_path = manifest if stage == "wgs_variant_select" else None
self.missing_artifacts = []
self.errors = []
monkeypatch.setattr(web_main, "WORKSPACE_ROOT", tmp_path)
monkeypatch.setattr(
web_main, "_load_methodology_plan_for_agent_request", lambda _request: (None, None, None)
)
monkeypatch.setattr(
web_main,
"_wgs_manifest_identity_for_plan",
lambda *_args: {
"wgsSampleSetId": "samples",
"wgsReferenceId": "reference",
"wgsAnalysisId": "analysis",
},
)
monkeypatch.setattr(
stage_status, "get_stage_status", lambda **kwargs: Status(kwargs["stage"])
)
response = asyncio.run(
web_main.agent_plan(
web_main.AgentPlanRequest(
message="执行 WGS变异过滤",
job_id="job-1",
prefix="Cohort",
intent={"params": {"wgsMethod": "gatk"}},
)
)
)
assert response["success"] is False
assert "method conflict" in response["message"].lower()
def test_agent_plan_does_not_inherit_stale_authority_params(monkeypatch) -> None:
from apps.web.vue3 import main as web_main
from gfa_core.mas.tools import stage_status
class Status:
def __init__(self, stage: str):
self.status = "not_found" if stage == "wgs_variant_filter" else "success"
self.completed = stage != "wgs_variant_filter"
self.manifest_path = None
self.missing_artifacts = []
self.errors = []
monkeypatch.setattr(
web_main, "_load_methodology_plan_for_agent_request", lambda _request: (None, None, None)
)
monkeypatch.setattr(
web_main,
"_wgs_manifest_identity_for_plan",
lambda *_args: {
"wgsSampleSetId": "samples",
"wgsReferenceId": "reference",
"wgsAnalysisId": "analysis",
},
)
monkeypatch.setattr(
stage_status, "get_stage_status", lambda **kwargs: Status(kwargs["stage"])
)
response = asyncio.run(
web_main.agent_plan(
web_main.AgentPlanRequest(
message="执行 WGS变异过滤",
job_id="job-1",
prefix="Os",
intent={
"params": {
"wgsQcManifest": "/stale/Os.wgs_qc.manifest.json",
"wgsQcAnalysisId": "stale-analysis",
}
},
)
)
)
assert "wgsQcManifest" not in response["plan"]["params"]
assert "wgsQcAnalysisId" not in response["plan"]["params"]
def test_agent_plan_uses_selected_job_and_manifest_prefix_when_request_omits_both(monkeypatch) -> None:
from apps.web.vue3 import main as web_main
from gfa_core.mas.tools import stage_status
class Status:
def __init__(self, completed: bool):
self.status = "success" if completed else "not_found"
self.completed = completed
self.manifest_path = None
self.missing_artifacts = []
self.errors = []
monkeypatch.setattr(web_main, "current_job_id", "job-selected")
monkeypatch.setattr(
web_main,
"_load_methodology_plan_for_agent_request",
lambda _request: (None, None, None),
)
monkeypatch.setattr(
web_main,
"_wgs_prefix_for_plan",
lambda job_id, _stages, _prefix: "Os" if job_id == "job-selected" else "",
)
monkeypatch.setattr(
web_main,
"_wgs_manifest_identity_for_plan",
lambda *_args: {
"wgsSampleSetId": "samples",
"wgsReferenceId": "reference",
"wgsAnalysisId": "analysis",
},
)
seen = []
def fake_status(**kwargs):
seen.append(kwargs)
return Status(kwargs["stage"] != "wgs_maf_missing_filter")
monkeypatch.setattr(stage_status, "get_stage_status", fake_status)
response = asyncio.run(
web_main.agent_plan(
web_main.AgentPlanRequest(message="执行 WGS MAF缺失率过滤")
)
)
assert response["mode"] == "deterministic_wgs_plan"
assert response["plan"]["prefix"] == "Os"
assert response["plan"]["stages"] == ["wgs_maf_missing_filter"]
assert seen and all(item["job_id"] == "job-selected" for item in seen)
assert all(item["prefix"] == "Os" for item in seen)
def test_agent_plan_prefers_current_request_prefix_over_stale_chat_intent(monkeypatch) -> None:
from apps.web.vue3 import main as web_main
from gfa_core.mas.tools import stage_status
class Status:
status = "not_found"
completed = False
manifest_path = None
missing_artifacts = []
errors = []
monkeypatch.setattr(
web_main,
"_load_methodology_plan_for_agent_request",
lambda _request: (None, None, None),
)
seen_prefixes = []
def capture_prefix(_job_id, _stages, requested_prefix):
seen_prefixes.append(requested_prefix)
return requested_prefix
monkeypatch.setattr(web_main, "_wgs_prefix_for_plan", capture_prefix)
monkeypatch.setattr(web_main, "_wgs_manifest_identity_for_plan", lambda *_args: {})
monkeypatch.setattr(stage_status, "get_stage_status", lambda **_kwargs: Status())
response = asyncio.run(
web_main.agent_plan(
web_main.AgentPlanRequest(
message="执行 WGS比对",
job_id="job-selected",
prefix="Os",
intent={
"stages": ["wgs_align"],
"taskType": "pipeline",
"prefix": "At",
"params": {"prefix": "At"},
},
)
)
)
assert response["mode"] == "deterministic_wgs_plan"
assert response["plan"]["prefix"] == "Os"
assert seen_prefixes == ["Os"]
def test_agent_plan_recovers_wgs_job_id_from_chat_intent(monkeypatch) -> None:
from apps.web.vue3 import main as web_main
from gfa_core.mas.tools import stage_status
class Status:
def __init__(self, completed: bool):
self.status = "success" if completed else "not_found"
self.completed = completed
self.manifest_path = None
self.missing_artifacts = []
self.errors = []
monkeypatch.setattr(web_main, "current_job_id", "default_job")
monkeypatch.setattr(
web_main,
"_load_methodology_plan_for_agent_request",
lambda _request: (None, None, None),
)
monkeypatch.setattr(
web_main,
"_wgs_prefix_for_plan",
lambda job_id, _stages, _prefix: "Os" if job_id == "job-selected" else "",
)
monkeypatch.setattr(
web_main,
"_wgs_manifest_identity_for_plan",
lambda *_args: {
"wgsSampleSetId": "samples",
"wgsReferenceId": "reference",
"wgsAnalysisId": "analysis",
},
)
missing = {
"wgs_qc", "wgs_index", "wgs_align", "wgs_markdup",
"wgs_maf_missing_filter",
}
seen = []
def fake_status(**kwargs):
seen.append(kwargs)
return Status(kwargs["stage"] not in missing)
monkeypatch.setattr(stage_status, "get_stage_status", fake_status)
response = asyncio.run(
web_main.agent_plan(
web_main.AgentPlanRequest(
message="执行 WGS全流程",
intent={
"job_id": "job-selected",
"stages": WGS_STAGES,
"taskType": "pipeline",
},
)
)
)
assert response["mode"] == "deterministic_wgs_plan"
assert response["plan"]["prefix"] == "Os"
assert response["plan"]["stages"] == [
"wgs_qc", "wgs_index", "wgs_align", "wgs_markdup",
"wgs_maf_missing_filter",
]
assert seen and all(item["job_id"] == "job-selected" for item in seen)
@pytest.mark.parametrize(
("status", "must_plan"),
[
("success", False),
("failed", True),
("partial_success", True),
("not_found", True),
("stale", True),
("hash_mismatch", True),
],
)
def test_planner_skips_only_revalidated_exact_wgs_success(
tmp_path, monkeypatch, status: str, must_plan: bool
) -> None:
agent = PlanningAgentV2(None, project_root=str(tmp_path))
monkeypatch.setattr(
agent,
"_check_stage_status_preflight",
lambda stage, prefix, workspace, stage_params=None: StageStatus(stage=stage, status=status),
)
plan = asyncio.run(
agent._generate_plan_with_llm(
query="WGS质控",
intent={"stages": ["wgs_qc"], "prefix": "Cohort"},
completed_stages=[],
job_id="job",
workspace=str(tmp_path / "job"),
preflight_result={
"stage_status": {
"wgs_qc": {
"status": "success",
"manifest_path": "final/Cohort.wgs_qc.manifest.json",
}
}
},
)
)
assert bool(plan["stages"]) is must_plan
@pytest.mark.parametrize("stage", WGS_STAGES)
def test_planner_passes_stage_specific_exact_wgs_identity_to_status(
tmp_path, monkeypatch, stage: str
) -> None:
agent = PlanningAgentV2(None, project_root=str(tmp_path))
captured = {}
def capture_status(
stage_name, prefix, workspace, stage_params=None
) -> StageStatus:
captured[stage_name] = dict(stage_params or {})
return StageStatus(stage=stage_name, status="failed")
monkeypatch.setattr(agent, "_check_stage_status_preflight", capture_status)
asyncio.run(
agent._generate_plan_with_llm(
query=f"execute {stage}",
intent={
"stages": [stage],
"prefix": "Cohort",
"params": {
"wgsSampleSetId": "sample-set",
"wgsReferenceId": "reference",
"wgsAnalysisId": "analysis",
},
},
completed_stages=[],
job_id="job",
workspace=str(tmp_path / "job"),
)
)
target_params = captured[stage]
expected = {"wgsAnalysisId": "analysis"}
if stage != "wgs_index":
expected["wgsSampleSetId"] = "sample-set"
if stage != "wgs_qc":
expected["wgsReferenceId"] = "reference"
assert {key: target_params.get(key) for key in expected} == expected