137 lines
7.6 KiB
Python
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)
|