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

123 lines
4.4 KiB
Python

from __future__ import annotations
from dataclasses import fields
from pathlib import Path
from gfa_core.mas.stage_capabilities import CAPABILITIES
from gfa_core.mas.stage_catalog import (
EXECUTABLE_STAGES,
PIPELINE_STAGES,
STAGE_DEPENDENCIES,
STAGE_INFO,
STAGE_RESOURCE_PROFILES,
complete_dependencies,
)
from gfa_core.mas.tools.stage_identity import StageIdentity, params_from_identity
from gfa_core.mas.tools.stage_tools import StageToolRegistry
PROJECT_ROOT = Path(__file__).resolve().parents[2]
GWAS_STAGES = {
"gwas_kinship": "run_gwas_kinship.py",
"gwas_trait": "run_gwas_trait.py",
"gwas_cov": "run_gwas_covariate.py",
"gwas_mlm": "run_gwas_tassel_mlm.py",
"gwas_plot": "run_gwas_plot.py",
"gwas_emmax": "run_gwas_emmax.py",
"gwas_emplot": "run_gwas_emmax_plot.py",
"gwas_glm": "run_gwas_tassel_glm.py",
}
EXPECTED_DEPENDENCIES = {
"gwas_kinship": ["gwas_fmt"],
"gwas_trait": [],
"gwas_cov": [],
"gwas_mlm": ["gwas_fmt", "gwas_kinship", "gwas_trait", "gwas_cov"],
"gwas_plot": ["gwas_mlm"],
"gwas_emmax": ["gwas_fmt", "gwas_trait"],
"gwas_emplot": ["gwas_emmax"],
"gwas_glm": ["gwas_fmt", "gwas_trait", "gwas_cov"],
}
def test_all_gwas_stages_have_one_stable_registration_and_runner() -> None:
definitions = {item["id"]: item for item in StageToolRegistry.STAGE_DEFINITIONS}
for stage, runner in GWAS_STAGES.items():
assert stage in PIPELINE_STAGES
assert stage in EXECUTABLE_STAGES
assert stage in STAGE_INFO
assert stage in CAPABILITIES
assert definitions[stage]["script"] == runner
assert CAPABILITIES[stage].cli == f"apps/cli/{runner}"
def test_gwas_dependencies_are_exact_and_acyclic() -> None:
for stage, dependencies in EXPECTED_DEPENDENCIES.items():
assert STAGE_DEPENDENCIES[stage] == dependencies
full_chain = complete_dependencies(["gwas_plot", "gwas_emplot", "gwas_glm"])
assert len(full_chain) == len(set(full_chain))
for stage in full_chain:
for dependency in STAGE_DEPENDENCIES.get(stage, []):
assert full_chain.index(dependency) < full_chain.index(stage)
def test_model_stages_share_heavy_capacity_group() -> None:
for stage in ("gwas_mlm", "gwas_emmax", "gwas_glm"):
profile = STAGE_RESOURCE_PROFILES[stage]
assert profile["queue"] == "heavy"
assert profile["resource_class"] == "heavy"
for stage in ("gwas_trait", "gwas_cov", "gwas_plot", "gwas_emplot"):
assert STAGE_RESOURCE_PROFILES[stage]["resource_class"] == "light"
def test_gwas_identity_fields_round_trip_to_api_params() -> None:
names = {field.name for field in fields(StageIdentity)}
required = {
"genotype_scope_id",
"phenotype_scope_id",
"covariate_scope_id",
"model_scope_id",
"source_model_scope_id",
"analysis_scope_id",
"run_id",
}
assert required <= names
identity = StageIdentity(
stage="gwas_mlm",
prefix="cohort",
genotype_scope_id="genotype:g",
phenotype_scope_id="phenotype:p",
covariate_scope_id="covariate:c",
model_scope_id="gwas-model:m",
source_model_scope_id="gwas-model:s",
analysis_scope_id="gwas-analysis:a",
run_id="gwas_mlm_run",
)
params = params_from_identity(identity)
assert params["genotypeScopeId"] == "genotype:g"
assert params["phenotypeScopeId"] == "phenotype:p"
assert params["covariateScopeId"] == "covariate:c"
assert params["modelScopeId"] == "gwas-model:m"
assert params["sourceModelScopeId"] == "gwas-model:s"
assert params["analysisScopeId"] == "gwas-analysis:a"
assert params["runId"] == "gwas_mlm_run"
def test_each_gwas_stage_has_contract_and_skill_boundary() -> None:
skill_names = {
"gwas_kinship": "gwas-kinship",
"gwas_trait": "gwas-trait",
"gwas_cov": "gwas-covariate",
"gwas_mlm": "gwas-tassel-mlm",
"gwas_plot": "gwas-plot",
"gwas_emmax": "gwas-emmax",
"gwas_emplot": "gwas-emmax-plot",
"gwas_glm": "gwas-tassel-glm",
}
for stage, skill in skill_names.items():
assert (PROJECT_ROOT / "gfa_core" / "schemas" / "stages" / f"{stage}_contract_v1.md").is_file()
assert (PROJECT_ROOT / "skills" / skill / "SKILL.md").is_file()
assert (PROJECT_ROOT / "skills" / skill / "config.yaml").is_file()
assert (PROJECT_ROOT / "skills" / skill / "run.py").is_file()