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

137 lines
7.6 KiB
Python

import hashlib
import asyncio
import json
from pathlib import Path
from gfa_core.mas.tools.stage_identity import resolve_stage_identity
from gfa_core.mas.tools.stage_status import get_stage_status
def _artifact(workspace: Path, path: Path, role: str, sample_id: str = "") -> dict:
path.write_bytes(b"proof")
item = {"role": role, "path": str(path.relative_to(workspace)), "size": path.stat().st_size,
"size_bytes": path.stat().st_size,
"sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
if sample_id:
item["sample_id"] = sample_id
return item
def test_wgs_qc_status_is_unscoped_and_rejects_partial_sample_roles(tmp_path: Path) -> None:
workspace = tmp_path / "workspace" / "job"
final = workspace / "final"
final.mkdir(parents=True)
artifacts = {"wgs_sample_config": _artifact(workspace, final / "config.yaml", "wgs_sample_config")}
for role in ("clean_reads_r1", "clean_reads_r2", "fastp_html", "fastp_json"):
artifacts[f"{role}.S1"] = _artifact(workspace, final / f"S1.{role}", role, "S1")
manifest = {"stage": "wgs_qc", "prefix": "Cohort", "status": "success", "wgs_sample_set_id": "samples", "wgs_analysis_id": "analysis", "artifacts": artifacts,
"metrics": {"input_sample_ids": ["S1"], "output_sample_ids": ["S1"]}}
(final / "Cohort.wgs_qc.manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
identity = resolve_stage_identity("wgs_qc", "Cohort", {"scopeId": "wrong.scope"}, "job", tmp_path / "workspace")
assert identity.scope_id == ""
assert get_stage_status("job", "Cohort", "wgs_qc", tmp_path / "workspace", wgs_sample_set_id="samples", wgs_analysis_id="analysis").completed
del artifacts["fastp_json.S1"]
(final / "Cohort.wgs_qc.manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
assert not get_stage_status("job", "Cohort", "wgs_qc", tmp_path / "workspace", wgs_sample_set_id="samples", wgs_analysis_id="analysis").completed
def test_wgs_results_fail_closed_to_exact_sample_and_analysis_identity(tmp_path: Path, monkeypatch) -> None:
from apps.web.vue3 import main as web_main
workspace_root = tmp_path / "workspace"
final = workspace_root / "job" / "final"
final.mkdir(parents=True)
matched = final / "matched.fastp.html"
sibling = final / "sibling.fastp.html"
matched.write_text("matched", encoding="utf-8")
sibling.write_text("sibling", encoding="utf-8")
for name, sample_set_id, analysis_id, artifact in (
("matched.wgs_qc.manifest.json", "sample-set-a", "analysis-a", matched),
("sibling.wgs_qc.manifest.json", "sample-set-b", "analysis-b", sibling),
):
(final / name).write_text(json.dumps({
"stage": "wgs_qc", "status": "success", "prefix": "Cohort",
"wgs_sample_set_id": sample_set_id, "wgs_analysis_id": analysis_id,
"artifacts": {"fastp_html.S1": {"role": "fastp_html", "sample_id": "S1", "path": f"final/{artifact.name}"}},
}), encoding="utf-8")
monkeypatch.setattr(web_main, "WORKSPACE_ROOT", workspace_root)
result = asyncio.run(web_main.get_results(
job_id="job", stage="wgs_qc", wgs_sample_set_id="sample-set-a", wgs_analysis_id="analysis-a",
))
rendered = json.dumps(result, sort_keys=True)
assert "matched.fastp.html" in rendered
assert "sibling.fastp.html" not in rendered
assert result["wgs_qc"]["wgs_sample_set_id"] == "sample-set-a"
assert result["wgs_qc"]["wgs_analysis_id"] == "analysis-a"
missing_identity = asyncio.run(web_main.get_results(job_id="job", stage="wgs_qc"))
assert "matched.fastp.html" not in json.dumps(missing_identity, sort_keys=True)
assert "sibling.fastp.html" not in json.dumps(missing_identity, sort_keys=True)
def test_wgs_align_status_and_results_require_all_three_identities_and_complete_sample_roles(tmp_path: Path, monkeypatch) -> None:
from apps.web.vue3 import main as web_main
workspace_root = tmp_path / "workspace"
workspace = workspace_root / "job"
final = workspace / "final"; final.mkdir(parents=True)
artifacts = {}
for sample in ("S1", "S2", "S3"):
for role, suffix in (("sorted_bam", "bam"), ("sorted_bam_index", "bam.bai"), ("align_log", "log")):
artifacts[f"{role}.{sample}"] = _artifact(workspace, final / f"{sample}.{suffix}", role, sample)
manifest = {"stage": "wgs_align", "prefix": "Cohort", "status": "success",
"wgs_sample_set_id": "samples", "wgs_reference_id": "reference", "wgs_analysis_id": "analysis",
"metrics": {"input_sample_ids": ["S1", "S2", "S3"], "output_sample_ids": ["S1", "S2", "S3"]},
"artifacts": artifacts}
authority = final / "Cohort.wgs_align.manifest.json"; authority.write_text(json.dumps(manifest))
complete_status = get_stage_status("job", "Cohort", "wgs_align", workspace_root,
wgs_sample_set_id="samples", wgs_reference_id="reference", wgs_analysis_id="analysis")
assert complete_status.completed
status_payload = complete_status.to_dict()
assert status_payload["wgs_sample_set_id"] == "samples"
assert status_payload["wgs_reference_id"] == "reference"
assert status_payload["wgs_analysis_id"] == "analysis"
monkeypatch.setattr(web_main, "WORKSPACE_ROOT", workspace_root)
result = asyncio.run(web_main.get_results(job_id="job", stage="wgs_align", wgs_sample_set_id="samples",
wgs_reference_id="reference", wgs_analysis_id="analysis"))
assert "S1.bam" in json.dumps(result)
shadow = final / "shadow.wgs_align.manifest.json"
shadow_artifact = final / "shadow.bam"; shadow_artifact.write_bytes(b"shadow")
shadow_manifest = dict(manifest)
shadow_manifest["artifacts"] = {
"sorted_bam.shadow": _artifact(workspace, shadow_artifact, "sorted_bam", "shadow")
}
shadow.write_text(json.dumps(shadow_manifest), encoding="utf-8")
authoritative = asyncio.run(web_main.get_results(
job_id="job", stage="wgs_align", wgs_sample_set_id="samples",
wgs_reference_id="reference", wgs_analysis_id="analysis",
))
assert "shadow.bam" not in json.dumps(authoritative)
assert authoritative["wgs_align"]["status"] == "success"
assert not authoritative["wgs_align"].get("error")
(final / "S1.bam").write_bytes(b"other")
tampered_status = get_stage_status("job", "Cohort", "wgs_align", workspace_root,
wgs_sample_set_id="samples", wgs_reference_id="reference", wgs_analysis_id="analysis")
assert not tampered_status.completed
tampered = asyncio.run(web_main.get_results(
job_id="job", stage="wgs_align", wgs_sample_set_id="samples",
wgs_reference_id="reference", wgs_analysis_id="analysis",
))
assert "S1.bam" not in json.dumps(tampered)
(final / "S1.bam").write_bytes(b"proof")
missing = asyncio.run(web_main.get_results(job_id="job", stage="wgs_align", wgs_sample_set_id="samples",
wgs_reference_id="reference"))
assert "S1.bam" not in json.dumps(missing)
manifest["artifacts"].pop("sorted_bam_index.S2")
authority.write_text(json.dumps(manifest))
assert not get_stage_status("job", "Cohort", "wgs_align", workspace_root,
wgs_sample_set_id="samples", wgs_reference_id="reference", wgs_analysis_id="analysis").completed
invalid = asyncio.run(web_main.get_results(
job_id="job", stage="wgs_align", wgs_sample_set_id="samples",
wgs_reference_id="reference", wgs_analysis_id="analysis",
))
assert "S1.bam" not in json.dumps(invalid)