435 lines
18 KiB
Python
435 lines
18 KiB
Python
import json
|
|||
|
|
from pathlib import Path
|
||
|
|
import subprocess
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from pydantic import ValidationError
|
||
|
|
|
||
|
|
from apps.web.vue3.main import ExecuteRequest
|
||
|
|
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parents[2]
|
||
|
|
|
||
|
|
|
||
|
|
def test_wgs_qc_api_uses_exact_wgs_sample_config_field_name() -> None:
|
||
|
|
request = ExecuteRequest(stage="wgs_qc", wgsSampleConfig="input/wgs_samples.yaml")
|
||
|
|
assert request.wgsSampleConfig == "input/wgs_samples.yaml"
|
||
|
|
assert "wgsSampleConfig" in ExecuteRequest.model_fields
|
||
|
|
|
||
|
|
|
||
|
|
def test_confirmed_wgs_prefix_becomes_prefix_for_the_next_plan_request() -> None:
|
||
|
|
text = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||
|
|
start = text.index("const confirmedStageParamsForExecution =")
|
||
|
|
end = text.index("const shouldPauseForExpressionDataSource =", start)
|
||
|
|
helper = text[start:end]
|
||
|
|
script = f"""
|
||
|
|
const normalizedPrefix = value => String(value || '').trim();
|
||
|
|
const confirmParams = {{prefix: 'Os', cpu: 8}};
|
||
|
|
const params = {{prefix: 'At'}};
|
||
|
|
const batchExecuteParams = {{value: {{prefix: 'At', params: {{prefix: 'At'}}}}}};
|
||
|
|
const confirmCpuForStage = (_stage, value) => value;
|
||
|
|
const usesRnaseqSampleParallel = () => false;
|
||
|
|
const sampleParallelJobsForStage = (_stage, value) => value;
|
||
|
|
{helper}
|
||
|
|
const merged = confirmedStageParamsForExecution('wgs_qc', {{prefix: 'At'}});
|
||
|
|
console.log(JSON.stringify({{merged, params, batch: batchExecuteParams.value}}));
|
||
|
|
"""
|
||
|
|
result = subprocess.run(
|
||
|
|
["/usr/bin/node", "-e", script],
|
||
|
|
text=True,
|
||
|
|
capture_output=True,
|
||
|
|
check=True,
|
||
|
|
)
|
||
|
|
payload = json.loads(result.stdout)
|
||
|
|
assert payload["merged"]["prefix"] == "Os"
|
||
|
|
assert payload["params"]["prefix"] == "Os"
|
||
|
|
assert payload["batch"]["prefix"] == "Os"
|
||
|
|
assert payload["batch"]["params"]["prefix"] == "Os"
|
||
|
|
|
||
|
|
|
||
|
|
def test_wgs_index_frontend_uses_reference_and_tool_contract_fields() -> None:
|
||
|
|
request = ExecuteRequest(stage="wgs_index", wgsReferenceFasta="input/reference.fasta", wgsSamtools="samtools", wgsBwaMem2="bwa-mem2", wgsGatk="gatk")
|
||
|
|
assert request.wgsReferenceFasta == "input/reference.fasta"
|
||
|
|
text = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||
|
|
assert "wgs_index" in text and "wgsReferenceFasta" in text
|
||
|
|
|
||
|
|
|
||
|
|
def test_frontend_binds_wgs_qc_config_and_fastp_parameters_to_execute_request() -> None:
|
||
|
|
text = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||
|
|
assert "applyWgsExecutionParams" in text
|
||
|
|
assert "wgsSampleConfig" in text
|
||
|
|
assert "wgsFastp" in text
|
||
|
|
assert "wgsMaxJobs" in text
|
||
|
|
assert text.count("applyWgsExecutionParams(requestBody, stageName, activeStageParams);") == 1
|
||
|
|
assert text.count("applyWgsExecutionParams(requestBody, stageId, stageParams);") == 1
|
||
|
|
active_call = text.index("applyWgsExecutionParams(requestBody, stageName, activeStageParams);")
|
||
|
|
batch_call = text.index("applyWgsExecutionParams(requestBody, stageId, stageParams);")
|
||
|
|
rnaseq_helper = text.index("const applyRnaseqExecutionParams")
|
||
|
|
assert active_call > rnaseq_helper and batch_call > rnaseq_helper
|
||
|
|
assert "applyWgsExecutionParams(requestBody, stageName, activeStageParams);" in text[active_call - 250:active_call + 100]
|
||
|
|
assert "applyWgsExecutionParams(requestBody, stageId, stageParams);" in text[batch_call - 250:batch_call + 100]
|
||
|
|
|
||
|
|
|
||
|
|
def test_wgs_execution_resource_params_fall_back_to_positive_integers() -> None:
|
||
|
|
text = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||
|
|
apply_start = text.index("const applyWgsExecutionParams =")
|
||
|
|
apply_end = text.index("const applyRnaseqExecutionParams =", apply_start)
|
||
|
|
script = f"""
|
||
|
|
const firstDefinedParam = (...values) => {{
|
||
|
|
for (const value of values) {{
|
||
|
|
if (value !== undefined && value !== null && value !== '') return value;
|
||
|
|
}}
|
||
|
|
return '';
|
||
|
|
}};
|
||
|
|
const numberParam = (value, defaultValue) => {{
|
||
|
|
const num = Number(value);
|
||
|
|
return Number.isFinite(num) ? num : defaultValue;
|
||
|
|
}};
|
||
|
|
const confirmParams = {{}};
|
||
|
|
{text[apply_start:apply_end]}
|
||
|
|
const empty = applyWgsExecutionParams({{}}, 'wgs_qc', {{}});
|
||
|
|
const invalid = applyWgsExecutionParams({{}}, 'wgs_qc', {{
|
||
|
|
wgsThreads: 0, wgsMaxJobs: -2
|
||
|
|
}});
|
||
|
|
const explicit = applyWgsExecutionParams({{}}, 'wgs_qc', {{
|
||
|
|
wgsThreads: 4, wgsMaxJobs: 2
|
||
|
|
}});
|
||
|
|
console.log(JSON.stringify({{empty, invalid, explicit}}));
|
||
|
|
"""
|
||
|
|
result = subprocess.run(
|
||
|
|
["/usr/bin/node", "-e", script],
|
||
|
|
text=True,
|
||
|
|
capture_output=True,
|
||
|
|
check=True,
|
||
|
|
)
|
||
|
|
payload = json.loads(result.stdout)
|
||
|
|
assert payload["empty"]["wgsThreads"] == 1
|
||
|
|
assert payload["empty"]["wgsMaxJobs"] == 1
|
||
|
|
assert payload["invalid"]["wgsThreads"] == 1
|
||
|
|
assert payload["invalid"]["wgsMaxJobs"] == 1
|
||
|
|
assert payload["explicit"]["wgsThreads"] == 4
|
||
|
|
assert payload["explicit"]["wgsMaxJobs"] == 2
|
||
|
|
|
||
|
|
|
||
|
|
def test_wgs_bam_qc_frontend_has_both_authorities_and_fixed_metric_name() -> None:
|
||
|
|
request = ExecuteRequest(
|
||
|
|
stage="wgs_bam_qc",
|
||
|
|
wgsMarkdupManifest="final/Cohort.wgs_markdup.manifest.json",
|
||
|
|
wgsIndexManifest="final/Cohort.wgs_index.manifest.json",
|
||
|
|
wgsIndexAnalysisId="wgs-analysis:" + "a" * 64,
|
||
|
|
wgsCoverageThreshold=1,
|
||
|
|
)
|
||
|
|
assert request.wgsMarkdupManifest.endswith(".wgs_markdup.manifest.json")
|
||
|
|
assert request.wgsCoverageThreshold == 1
|
||
|
|
text = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||
|
|
for token in (
|
||
|
|
"wgs_bam_qc", "wgsMarkdupManifest", "wgsIndexManifest",
|
||
|
|
"wgsIndexAnalysisId", "wgsCoverageThreshold",
|
||
|
|
"wgsBamQcHandoffUpdates",
|
||
|
|
):
|
||
|
|
assert token in text
|
||
|
|
|
||
|
|
|
||
|
|
def test_wgs_bam_qc_coverage_threshold_is_fixed_to_one() -> None:
|
||
|
|
text = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||
|
|
apply_start = text.index("const applyWgsExecutionParams =")
|
||
|
|
apply_end = text.index("const applyRnaseqExecutionParams =", apply_start)
|
||
|
|
script = f"""
|
||
|
|
const firstDefinedParam = (...values) => {{
|
||
|
|
for (const value of values) {{
|
||
|
|
if (value !== undefined && value !== null && value !== '') return value;
|
||
|
|
}}
|
||
|
|
return '';
|
||
|
|
}};
|
||
|
|
const numberParam = (value, defaultValue) => {{
|
||
|
|
const num = Number(value);
|
||
|
|
return Number.isFinite(num) ? num : defaultValue;
|
||
|
|
}};
|
||
|
|
const confirmParams = {{}};
|
||
|
|
{text[apply_start:apply_end]}
|
||
|
|
const empty = applyWgsExecutionParams({{}}, 'wgs_bam_qc', {{}});
|
||
|
|
console.log(JSON.stringify(empty));
|
||
|
|
"""
|
||
|
|
result = subprocess.run(
|
||
|
|
["/usr/bin/node", "-e", script],
|
||
|
|
text=True,
|
||
|
|
capture_output=True,
|
||
|
|
check=True,
|
||
|
|
)
|
||
|
|
assert json.loads(result.stdout)["wgsCoverageThreshold"] == 1
|
||
|
|
|
||
|
|
with pytest.raises(ValidationError):
|
||
|
|
ExecuteRequest(stage="wgs_bam_qc", wgsCoverageThreshold=0)
|
||
|
|
|
||
|
|
|
||
|
|
def test_wgs_downstream_empty_numeric_params_keep_contract_defaults() -> None:
|
||
|
|
text = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||
|
|
apply_start = text.index("const applyWgsExecutionParams =")
|
||
|
|
apply_end = text.index("const applyRnaseqExecutionParams =", apply_start)
|
||
|
|
script = f"""
|
||
|
|
const firstDefinedParam = (...values) => {{
|
||
|
|
for (const value of values) {{
|
||
|
|
if (value !== undefined && value !== null && value !== '') return value;
|
||
|
|
}}
|
||
|
|
return '';
|
||
|
|
}};
|
||
|
|
const numberParam = (value, defaultValue) => {{
|
||
|
|
const num = Number(value);
|
||
|
|
return Number.isFinite(num) ? num : defaultValue;
|
||
|
|
}};
|
||
|
|
const booleanParam = (_value, defaultValue) => defaultValue;
|
||
|
|
const confirmParams = {{}};
|
||
|
|
{text[apply_start:apply_end]}
|
||
|
|
const haplotype = applyWgsExecutionParams(
|
||
|
|
{{}}, 'wgs_haplotype_calling', {{}}
|
||
|
|
);
|
||
|
|
const joint = applyWgsExecutionParams(
|
||
|
|
{{}}, 'wgs_joint_genotyping', {{}}
|
||
|
|
);
|
||
|
|
const impute = applyWgsExecutionParams({{}}, 'wgs_impute', {{}});
|
||
|
|
const finalFilter = applyWgsExecutionParams(
|
||
|
|
{{}}, 'wgs_maf_missing_filter', {{}}
|
||
|
|
);
|
||
|
|
console.log(JSON.stringify({{haplotype, joint, impute, finalFilter}}));
|
||
|
|
"""
|
||
|
|
result = subprocess.run(
|
||
|
|
["/usr/bin/node", "-e", script],
|
||
|
|
text=True,
|
||
|
|
capture_output=True,
|
||
|
|
check=True,
|
||
|
|
)
|
||
|
|
payload = json.loads(result.stdout)
|
||
|
|
assert payload["haplotype"]["wgsNativePairHmmThreads"] == 1
|
||
|
|
assert payload["joint"]["wgsMaxAlternateAlleles"] == 6
|
||
|
|
assert payload["joint"]["wgsCallingThreshold"] == 10
|
||
|
|
assert payload["impute"]["wgsWindow"] == 40
|
||
|
|
assert payload["impute"]["wgsOverlap"] == 2
|
||
|
|
assert payload["impute"]["wgsNe"] == 100000
|
||
|
|
assert payload["impute"]["wgsIterations"] == 12
|
||
|
|
assert payload["impute"]["wgsPhaseStates"] == 280
|
||
|
|
assert payload["impute"]["wgsImpStates"] == 1600
|
||
|
|
assert payload["impute"]["wgsCluster"] == 0.005
|
||
|
|
assert payload["finalFilter"]["wgsMaf"] == 0.05
|
||
|
|
assert payload["finalFilter"]["wgsMaxMissing"] == 0.5
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize(
|
||
|
|
("stage", "field"),
|
||
|
|
(
|
||
|
|
("wgs_haplotype_calling", "wgsNativePairHmmThreads"),
|
||
|
|
("wgs_joint_genotyping", "wgsMaxAlternateAlleles"),
|
||
|
|
("wgs_joint_genotyping", "wgsCallingThreshold"),
|
||
|
|
("wgs_impute", "wgsWindow"),
|
||
|
|
("wgs_impute", "wgsOverlap"),
|
||
|
|
("wgs_impute", "wgsNe"),
|
||
|
|
("wgs_impute", "wgsIterations"),
|
||
|
|
("wgs_impute", "wgsPhaseStates"),
|
||
|
|
("wgs_impute", "wgsImpStates"),
|
||
|
|
("wgs_impute", "wgsCluster"),
|
||
|
|
("wgs_maf_missing_filter", "wgsMaf"),
|
||
|
|
("wgs_maf_missing_filter", "wgsMaxMissing"),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
def test_wgs_downstream_api_rejects_zero_scientific_params(
|
||
|
|
stage: str, field: str
|
||
|
|
) -> None:
|
||
|
|
with pytest.raises(ValidationError):
|
||
|
|
ExecuteRequest(stage=stage, **{field: 0})
|
||
|
|
|
||
|
|
|
||
|
|
def test_wgs_haplotype_frontend_uses_three_authorities_and_bam_qc_handoff() -> None:
|
||
|
|
text = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||
|
|
apply_start = text.index("const applyWgsExecutionParams =")
|
||
|
|
apply_end = text.index("const applyRnaseqExecutionParams =", apply_start)
|
||
|
|
handoff_start = text.index("const wgsHaplotypeCallingHandoffUpdates =")
|
||
|
|
handoff_end = text.index(
|
||
|
|
"const handoffWgsHaplotypeCallingProducerAuthority =", handoff_start
|
||
|
|
)
|
||
|
|
samples = "wgs-samples:" + "a" * 64
|
||
|
|
reference = "wgs-reference:" + "b" * 64
|
||
|
|
analysis = "wgs-analysis:" + "c" * 64
|
||
|
|
script = f"""
|
||
|
|
const firstDefinedParam = (...values) => values.find(v => v !== undefined && v !== null && v !== '');
|
||
|
|
const numberParam = (value, fallback) => Number(value || fallback);
|
||
|
|
const confirmParams = {{}};
|
||
|
|
{text[apply_start:apply_end]}
|
||
|
|
{text[handoff_start:handoff_end]}
|
||
|
|
const body = applyWgsExecutionParams({{}}, 'wgs_haplotype_calling', {{
|
||
|
|
wgsMarkdupManifest: 'markdup.json', wgsBamQcManifest: 'bam-qc.json',
|
||
|
|
wgsIndexManifest: 'index.json', wgsNativePairHmmThreads: 3
|
||
|
|
}});
|
||
|
|
const handoff = wgsHaplotypeCallingHandoffUpdates('wgs_bam_qc', {{
|
||
|
|
result: {{manifest_path: '/workspace/job/final/Cohort.wgs_bam_qc.manifest.json',
|
||
|
|
stage_status: {{completed: true, stage: 'wgs_bam_qc', prefix: 'Cohort',
|
||
|
|
wgs_sample_set_id: '{samples}', wgs_reference_id: '{reference}',
|
||
|
|
wgs_analysis_id: '{analysis}'}}}}
|
||
|
|
}}, 'Cohort');
|
||
|
|
console.log(JSON.stringify({{body, handoff}}));
|
||
|
|
"""
|
||
|
|
result = subprocess.run(
|
||
|
|
["/usr/bin/node", "-e", script],
|
||
|
|
text=True,
|
||
|
|
capture_output=True,
|
||
|
|
check=True,
|
||
|
|
)
|
||
|
|
payload = json.loads(result.stdout)
|
||
|
|
assert payload["body"]["wgsBamQcManifest"] == "bam-qc.json"
|
||
|
|
assert payload["body"]["wgsNativePairHmmThreads"] == 3
|
||
|
|
assert payload["handoff"]["wgsBamQcAnalysisId"] == analysis
|
||
|
|
|
||
|
|
|
||
|
|
def test_wgs_gvcf_combine_frontend_uses_two_authorities_and_handoff() -> None:
|
||
|
|
text = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||
|
|
apply_start = text.index("const applyWgsExecutionParams =")
|
||
|
|
apply_end = text.index("const applyRnaseqExecutionParams =", apply_start)
|
||
|
|
handoff_start = text.index("const wgsGvcfCombineHandoffUpdates =")
|
||
|
|
handoff_end = text.index(
|
||
|
|
"const handoffWgsGvcfCombineProducerAuthority =", handoff_start
|
||
|
|
)
|
||
|
|
samples = "wgs-samples:" + "a" * 64
|
||
|
|
reference = "wgs-reference:" + "b" * 64
|
||
|
|
analysis = "wgs-analysis:" + "c" * 64
|
||
|
|
script = f"""
|
||
|
|
const firstDefinedParam = (...values) => values.find(v => v !== undefined && v !== null && v !== '');
|
||
|
|
const numberParam = (value, fallback) => Number(value || fallback);
|
||
|
|
const confirmParams = {{}};
|
||
|
|
{text[apply_start:apply_end]}
|
||
|
|
{text[handoff_start:handoff_end]}
|
||
|
|
const body = applyWgsExecutionParams({{}}, 'wgs_gvcf_combine', {{
|
||
|
|
wgsHaplotypeManifest: 'haplotype.json', wgsIndexManifest: 'index.json',
|
||
|
|
wgsGatk: '/opt/gatk'
|
||
|
|
}});
|
||
|
|
const handoff = wgsGvcfCombineHandoffUpdates('wgs_haplotype_calling', {{
|
||
|
|
result: {{manifest_path: '/workspace/job/final/Cohort.wgs_haplotype_calling.manifest.json',
|
||
|
|
stage_status: {{completed: true, stage: 'wgs_haplotype_calling', prefix: 'Cohort',
|
||
|
|
wgs_sample_set_id: '{samples}', wgs_reference_id: '{reference}',
|
||
|
|
wgs_analysis_id: '{analysis}'}}}}
|
||
|
|
}}, 'Cohort');
|
||
|
|
console.log(JSON.stringify({{body, handoff}}));
|
||
|
|
"""
|
||
|
|
result = subprocess.run(
|
||
|
|
["/usr/bin/node", "-e", script],
|
||
|
|
text=True,
|
||
|
|
capture_output=True,
|
||
|
|
check=True,
|
||
|
|
)
|
||
|
|
payload = json.loads(result.stdout)
|
||
|
|
assert payload["body"]["wgsHaplotypeManifest"] == "haplotype.json"
|
||
|
|
assert payload["body"]["wgsIndexManifest"] == "index.json"
|
||
|
|
assert payload["body"]["wgsGatk"] == "/opt/gatk"
|
||
|
|
assert payload["handoff"]["wgsHaplotypeAnalysisId"] == analysis
|
||
|
|
|
||
|
|
|
||
|
|
def test_wgs_joint_genotyping_frontend_uses_two_authorities_parameters_and_handoff() -> None:
|
||
|
|
text = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||
|
|
apply_start = text.index("const applyWgsExecutionParams =")
|
||
|
|
apply_end = text.index("const applyRnaseqExecutionParams =", apply_start)
|
||
|
|
handoff_start = text.index("const wgsJointGenotypingHandoffUpdates =")
|
||
|
|
handoff_end = text.index(
|
||
|
|
"const handoffWgsJointGenotypingProducerAuthority =", handoff_start
|
||
|
|
)
|
||
|
|
samples = "wgs-samples:" + "a" * 64
|
||
|
|
reference = "wgs-reference:" + "b" * 64
|
||
|
|
analysis = "wgs-analysis:" + "c" * 64
|
||
|
|
script = f"""
|
||
|
|
const firstDefinedParam = (...values) => values.find(v => v !== undefined && v !== null && v !== '');
|
||
|
|
const numberParam = (value, fallback) => Number(value || fallback);
|
||
|
|
const booleanParam = (value, fallback) => value === undefined || value === null || value === '' ? fallback : Boolean(value);
|
||
|
|
const confirmParams = {{}};
|
||
|
|
{text[apply_start:apply_end]}
|
||
|
|
{text[handoff_start:handoff_end]}
|
||
|
|
const body = applyWgsExecutionParams({{}}, 'wgs_joint_genotyping', {{
|
||
|
|
wgsCombineManifest: 'combine.json', wgsIndexManifest: 'index.json',
|
||
|
|
wgsGatk: '/opt/gatk', wgsMaxAlternateAlleles: 6,
|
||
|
|
wgsCallingThreshold: 10, wgsIncludeNonVariantSites: false
|
||
|
|
}});
|
||
|
|
const handoff = wgsJointGenotypingHandoffUpdates('wgs_gvcf_combine', {{
|
||
|
|
result: {{manifest_path: '/workspace/job/final/Cohort.wgs_gvcf_combine.manifest.json',
|
||
|
|
stage_status: {{completed: true, stage: 'wgs_gvcf_combine', prefix: 'Cohort',
|
||
|
|
wgs_sample_set_id: '{samples}', wgs_reference_id: '{reference}',
|
||
|
|
wgs_analysis_id: '{analysis}'}}}}
|
||
|
|
}}, 'Cohort');
|
||
|
|
console.log(JSON.stringify({{body, handoff}}));
|
||
|
|
"""
|
||
|
|
result = subprocess.run(
|
||
|
|
["/usr/bin/node", "-e", script],
|
||
|
|
text=True,
|
||
|
|
capture_output=True,
|
||
|
|
check=True,
|
||
|
|
)
|
||
|
|
payload = json.loads(result.stdout)
|
||
|
|
assert payload["body"]["wgsCombineManifest"] == "combine.json"
|
||
|
|
assert payload["body"]["wgsIndexManifest"] == "index.json"
|
||
|
|
assert payload["body"]["wgsMaxAlternateAlleles"] == 6
|
||
|
|
assert payload["body"]["wgsCallingThreshold"] == 10
|
||
|
|
assert payload["body"]["wgsIncludeNonVariantSites"] is False
|
||
|
|
assert payload["handoff"]["wgsCombineAnalysisId"] == analysis
|
||
|
|
|
||
|
|
|
||
|
|
def test_wgs_variant_select_frontend_has_confirmed_fallback_contract() -> None:
|
||
|
|
text = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||
|
|
for token in (
|
||
|
|
"wgs_variant_select",
|
||
|
|
"wgsJointManifest",
|
||
|
|
"wgsMethod",
|
||
|
|
"wgsBcftools",
|
||
|
|
"wgsJointAnalysisId",
|
||
|
|
"confirmWgsFallback",
|
||
|
|
"cancelWgsFallback",
|
||
|
|
):
|
||
|
|
assert token in text
|
||
|
|
assert "wgsMethod: pendingWgsFallback.fallback_method" in text
|
||
|
|
|
||
|
|
|
||
|
|
def test_wgs_variant_select_modal_uses_real_reason_cancel_zero_confirm_once() -> None:
|
||
|
|
text = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||
|
|
start = text.index("const wgsFallbackDetails =")
|
||
|
|
end = text.index("const waitForDialogTransition =", start)
|
||
|
|
script = f"""
|
||
|
|
{text[start:end]}
|
||
|
|
let requests = [];
|
||
|
|
let shown = [];
|
||
|
|
const error = {{responseData: {{result: {{
|
||
|
|
error: 'GATK SelectVariants SNP failed with exit code 23',
|
||
|
|
fallback_method: 'bcftools'
|
||
|
|
}}}}}};
|
||
|
|
const executeFn = async (stage, params) => {{
|
||
|
|
requests.push({{stage, params}});
|
||
|
|
return {{ok: true}};
|
||
|
|
}};
|
||
|
|
(async () => {{
|
||
|
|
await confirmWgsFallback(error, 'wgs_variant_select', {{wgsMethod: 'gatk'}},
|
||
|
|
executeFn, async (message) => {{ shown.push(message); throw new Error('cancel'); }});
|
||
|
|
const afterCancel = requests.length;
|
||
|
|
await confirmWgsFallback(error, 'wgs_variant_select', {{wgsMethod: 'gatk'}},
|
||
|
|
executeFn, async (message) => {{ shown.push(message); }});
|
||
|
|
console.log(JSON.stringify({{afterCancel, requests, shown}}));
|
||
|
|
}})();
|
||
|
|
"""
|
||
|
|
result = subprocess.run(
|
||
|
|
["/usr/bin/node", "-e", script],
|
||
|
|
text=True,
|
||
|
|
capture_output=True,
|
||
|
|
check=True,
|
||
|
|
)
|
||
|
|
payload = json.loads(result.stdout)
|
||
|
|
assert payload["afterCancel"] == 0
|
||
|
|
assert len(payload["requests"]) == 1
|
||
|
|
assert payload["requests"][0]["stage"] == "wgs_variant_select"
|
||
|
|
assert payload["requests"][0]["params"]["wgsMethod"] == "bcftools"
|
||
|
|
assert all(
|
||
|
|
"GATK SelectVariants SNP failed with exit code 23" in message
|
||
|
|
for message in payload["shown"]
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_active_foreground_batch_guards_wgs_out_of_background_executor() -> None:
|
||
|
|
text = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||
|
|
start = text.index("const plannedExecutionStages =")
|
||
|
|
end = text.index("// 存储批量执行信息", start)
|
||
|
|
scheduling_branch = text[start:end]
|
||
|
|
background_call = scheduling_branch.index("startBackgroundBatchExecution(")
|
||
|
|
|
||
|
|
assert "wgs_" in scheduling_branch[:background_call].lower()
|
||
|
|
assert "startBackgroundBatchExecution(" in scheduling_branch
|