from __future__ import annotations import gzip import hashlib import json from pathlib import Path from threading import Barrier, Thread import pytest SAMPLES = "wgs-samples:" + "a" * 64 REFERENCE = "wgs-reference:" + "b" * 64 HAPLOTYPE_ANALYSIS = "wgs-analysis:" + "c" * 64 INDEX_ANALYSIS = "wgs-analysis:" + "d" * 64 def _record( workspace: Path, path: Path, role: str, sample_id: str = "" ) -> dict: item = { "artifact": role, "role": role, "path": str(path.relative_to(workspace)), "full_path": str(path.resolve()), "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 _write_gvcf(path: Path, samples: list[str]) -> None: with gzip.open(path, "wt", encoding="utf-8") as handle: handle.write("##fileformat=VCFv4.2\n") handle.write( '##ALT=\n' ) handle.write( "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\t" + "\t".join(samples) + "\n" ) handle.write( "chr1\t1\t.\tA\t\t.\t.\tEND=10\tGT\t" + "\t".join("0/0" for _ in samples) + "\n" ) def _authorities(workspace: Path) -> tuple[Path, Path]: final = workspace / "final" haplotype_bundle = ( final / ".wgs_haplotype_calling_runs" / "Cohort" / ("6" * 32) ) haplotype_bundle.mkdir(parents=True) artifacts = {} for sample in ("S1", "S2", "S3"): gvcf = haplotype_bundle / ( f"Cohort.wgs_haplotype_calling.{sample}.g.vcf.gz" ) tbi = Path(f"{gvcf}.tbi") log = haplotype_bundle / ( f"Cohort.wgs_haplotype_calling.{sample}.haplotype.log" ) _write_gvcf(gvcf, [sample]) tbi.write_bytes(f"TBI:{sample}".encode()) log.write_text(f"sample={sample}\n", encoding="utf-8") artifacts[f"gvcf.{sample}"] = _record( workspace, gvcf, "gvcf", sample ) artifacts[f"gvcf_index.{sample}"] = _record( workspace, tbi, "gvcf_index", sample ) artifacts[f"haplotype_log.{sample}"] = _record( workspace, log, "haplotype_log", sample ) haplotype = final / "Cohort.wgs_haplotype_calling.manifest.json" haplotype.write_text( json.dumps( { "manifest_version": "1.0", "stage": "wgs_haplotype_calling", "status": "success", "prefix": "Cohort", "job_id": "job-1", "run_id": "6" * 32, "workspace": str(workspace.resolve()), "wgs_sample_set_id": SAMPLES, "wgs_reference_id": REFERENCE, "wgs_analysis_id": HAPLOTYPE_ANALYSIS, "upstream_wgs_analysis_ids": { "wgs_markdup": "wgs-analysis:" + "e" * 64, "wgs_bam_qc": "wgs-analysis:" + "f" * 64, "wgs_index": INDEX_ANALYSIS, }, "metrics": { "input_sample_ids": ["S1", "S2", "S3"], "output_sample_ids": ["S1", "S2", "S3"], }, "artifacts": artifacts, } ), encoding="utf-8", ) index_bundle = final / ".wgs_index_runs" / "Cohort" / ("2" * 32) index_bundle.mkdir(parents=True) reference = index_bundle / "Cohort.wgs_index.reference.fasta" fai = Path(f"{reference}.fai") dictionary = reference.with_suffix(".dict") reference.write_text(">chr1\nAAAAAAAAAA\n", encoding="utf-8") fai.write_text("chr1\t10\t6\t10\t11\n", encoding="utf-8") dictionary.write_text("@HD\tVN:1.6\n@SQ\tSN:chr1\tLN:10\n", encoding="utf-8") index = final / "Cohort.wgs_index.manifest.json" index.write_text( json.dumps( { "manifest_version": "1.0", "stage": "wgs_index", "status": "success", "prefix": "Cohort", "job_id": "job-1", "run_id": "2" * 32, "workspace": str(workspace.resolve()), "wgs_reference_id": REFERENCE, "wgs_analysis_id": INDEX_ANALYSIS, "artifacts": { "reference_fasta": _record( workspace, reference, "reference_fasta" ), "reference_fai": _record( workspace, fai, "reference_fai" ), "reference_dict": _record( workspace, dictionary, "reference_dict" ), }, } ), encoding="utf-8", ) return haplotype, index def test_stage07_rejects_producer_from_different_index_generation_at_all_gates( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: from apps.cli.run_wgs_gvcf_combine import main from apps.web import pipeline_backend from gfa_core.mas.tools.stage_status import get_stage_status workspace_root = tmp_path / "workspaces" workspace = workspace_root / "job-1" haplotype, index = _authorities(workspace) gatk = _gatk(tmp_path) assert main(_argv(workspace, haplotype, index, gatk)) == 0 output = workspace / "final" / "Cohort.wgs_gvcf_combine.manifest.json" output_analysis = json.loads(output.read_text())["wgs_analysis_id"] producer = json.loads(haplotype.read_text()) producer["upstream_wgs_analysis_ids"]["wgs_index"] = ( "wgs-analysis:" + "0" * 64 ) haplotype.write_text(json.dumps(producer)) assert main(_argv(workspace, haplotype, index, gatk)) == 1 called = {"value": False} def forbidden_run(*_args, **_kwargs): called["value"] = True return 0 monkeypatch.setattr(pipeline_backend, "run_cmd_logged", forbidden_run) message, _ = pipeline_backend.run_stage( stage="wgs_gvcf_combine", job=pipeline_backend.JobInfo( "job-1", "", str(workspace), str(workspace / "logs"), "now" ), prefix="Cohort", wgs_haplotype_manifest=str(haplotype), wgs_index_manifest=str(index), wgs_sample_set_id=SAMPLES, wgs_reference_id=REFERENCE, wgs_haplotype_analysis_id=HAPLOTYPE_ANALYSIS, wgs_index_analysis_id=INDEX_ANALYSIS, wgs_gatk=str(gatk), ) assert "lineage" in message.lower() assert not called["value"] assert not get_stage_status( "job-1", "Cohort", "wgs_gvcf_combine", workspace_root, wgs_sample_set_id=SAMPLES, wgs_reference_id=REFERENCE, wgs_analysis_id=output_analysis, ).completed def _gatk(tmp_path: Path) -> Path: tool = tmp_path / "gatk" tool.write_text( """#!/usr/bin/env python3 import gzip, json, os, pathlib, sys args = sys.argv[1:] if args == ['--version']: print('The Genome Analysis Toolkit (GATK) v4.6.0.0') raise SystemExit(0) command = args[0] def value(flag): return args[args.index(flag) + 1] calls = os.environ.get('GATK_CALLS') if calls: with open(calls, 'a', encoding='utf-8') as handle: handle.write(json.dumps(args) + '\\n') if os.environ.get('FAIL_COMMAND') == command: raise SystemExit(23) if command == 'ValidateVariants': path = pathlib.Path(value('-V')) index = pathlib.Path(str(path) + '.tbi') if not path.is_file() or not index.is_file(): raise SystemExit(2) if index.read_bytes().startswith(b'CORRUPT'): raise SystemExit(24) raise SystemExit(0) if command == 'CombineGVCFs': inputs = [ pathlib.Path(args[pos + 1]) for pos, item in enumerate(args) if item == '-V' ] if len(inputs) != 3: raise SystemExit(25) samples = [] for source in inputs: with gzip.open(source, 'rt') as handle: header = next( line for line in handle if line.startswith('#CHROM') ) samples.extend(header.rstrip().split('\\t')[9:]) if os.environ.get('BAD_OUTPUT_HEADER'): samples[-1] = 'WRONG' output = pathlib.Path(value('-O')) with gzip.open(output, 'wt') as handle: handle.write('##fileformat=VCFv4.2\\n') handle.write('##ALT=\\n') handle.write('#CHROM\\tPOS\\tID\\tREF\\tALT\\tQUAL\\tFILTER\\tINFO' '\\tFORMAT\\t' + '\\t'.join(samples) + '\\n') handle.write('chr1\\t1\\t.\\tA\\t\\t.\\t.\\tEND=10' '\\tGT\\t' + '\\t'.join('0/0' for _ in samples) + '\\n') raise SystemExit(0) if command == 'IndexFeatureFile': path = pathlib.Path(value('-I')) content = ( b'CORRUPT:output' if os.environ.get('CORRUPT_OUTPUT_INDEX') else b'TBI:combined' ) pathlib.Path(str(path) + '.tbi').write_bytes(content) raise SystemExit(0) raise SystemExit(2) """, encoding="utf-8", ) tool.chmod(0o755) return tool def _argv( workspace: Path, haplotype: Path, index: Path, gatk: Path ) -> list[str]: return [ "--workspace", str(workspace), "--job-id", "job-1", "--prefix", "Cohort", "--haplotype-manifest", str(haplotype), "--index-manifest", str(index), "--gatk", str(gatk), ] def test_wgs_gvcf_combine_exposes_import_safe_cli() -> None: from apps.cli.run_wgs_gvcf_combine import build_parser, main assert callable(build_parser) assert callable(main) def test_three_samples_produce_singleton_canonical_outputs_and_exact_header( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: from apps.cli.run_wgs_gvcf_combine import main workspace = tmp_path / "workspace" haplotype, index = _authorities(workspace) calls = tmp_path / "gatk.calls" monkeypatch.setenv("GATK_CALLS", str(calls)) assert main(_argv(workspace, haplotype, index, _gatk(tmp_path))) == 0 authority = workspace / "final" / "Cohort.wgs_gvcf_combine.manifest.json" manifest = json.loads(authority.read_text()) assert manifest["metrics"]["input_sample_ids"] == ["S1", "S2", "S3"] assert manifest["metrics"]["output_sample_ids"] == ["S1", "S2", "S3"] assert manifest["upstream_wgs_analysis_ids"] == { "wgs_haplotype_calling": HAPLOTYPE_ANALYSIS, "wgs_index": INDEX_ANALYSIS, } assert set(manifest["artifacts"]) == { "combined_gvcf", "combined_gvcf_index", "combine_log", } records = { item["role"]: Path(item["full_path"]) for item in manifest["artifacts"].values() } assert records["combined_gvcf"].name == ( "Cohort.wgs_gvcf_combine.cohort.combined.g.vcf.gz" ) assert records["combined_gvcf_index"] == Path( f"{records['combined_gvcf']}.tbi" ) with gzip.open(records["combined_gvcf"], "rt") as handle: header = next(line for line in handle if line.startswith("#CHROM")) assert header.rstrip().split("\t")[9:] == ["S1", "S2", "S3"] assert records["combined_gvcf_index"].read_bytes() == b"TBI:combined" log = records["combine_log"].read_text() assert "CombineGVCFs exit=0" in log assert "IndexFeatureFile exit=0" in log assert "ValidateVariants exit=0" in log invocations = [json.loads(line) for line in calls.read_text().splitlines()] validate_inputs = [ call[call.index("-V") + 1] for call in invocations if call[0] == "ValidateVariants" and "-L" in call and ".wgs_haplotype_calling_runs" in call[call.index("-V") + 1] ] assert len(validate_inputs) == 3 combine = next(call for call in invocations if call[0] == "CombineGVCFs") assert combine.count("-V") == 3 @pytest.mark.parametrize("mutation", ("missing", "duplicate")) def test_missing_or_duplicate_declared_gvcf_is_rejected( tmp_path: Path, mutation: str ) -> None: from apps.cli.run_wgs_gvcf_combine import main workspace = tmp_path / "workspace" haplotype, index = _authorities(workspace) payload = json.loads(haplotype.read_text()) if mutation == "missing": payload["artifacts"].pop("gvcf.S2") else: payload["artifacts"]["gvcf.duplicate"] = dict( payload["artifacts"]["gvcf.S2"] ) haplotype.write_text(json.dumps(payload)) assert main(_argv(workspace, haplotype, index, _gatk(tmp_path))) == 1 def test_corrupt_nonempty_input_tbi_is_rejected_by_real_query( tmp_path: Path ) -> None: from apps.cli.run_wgs_gvcf_combine import main workspace = tmp_path / "workspace" haplotype, index = _authorities(workspace) payload = json.loads(haplotype.read_text()) record = payload["artifacts"]["gvcf_index.S2"] tbi = Path(record["full_path"]) tbi.write_bytes(b"CORRUPT:input") record["size"] = record["size_bytes"] = tbi.stat().st_size record["sha256"] = hashlib.sha256(tbi.read_bytes()).hexdigest() haplotype.write_text(json.dumps(payload)) assert main(_argv(workspace, haplotype, index, _gatk(tmp_path))) == 1 @pytest.mark.parametrize( "failure", ("CombineGVCFs", "IndexFeatureFile", "ValidateVariants") ) def test_each_gatk_return_code_fails_and_preserves_old_authority( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, failure: str, ) -> None: from apps.cli.run_wgs_gvcf_combine import main workspace = tmp_path / "workspace" haplotype, index = _authorities(workspace) authority = workspace / "final" / "Cohort.wgs_gvcf_combine.manifest.json" old = workspace / "final" / "old.combined.g.vcf.gz" old.write_bytes(b"old-success") authority.write_text( json.dumps( { "stage": "wgs_gvcf_combine", "status": "success", "sentinel": True, } ) ) before = ( hashlib.sha256(authority.read_bytes()).hexdigest(), hashlib.sha256(old.read_bytes()).hexdigest(), ) monkeypatch.setenv("FAIL_COMMAND", failure) assert main(_argv(workspace, haplotype, index, _gatk(tmp_path))) == 1 after = ( hashlib.sha256(authority.read_bytes()).hexdigest(), hashlib.sha256(old.read_bytes()).hexdigest(), ) assert after == before sidecars = list( (workspace / "final").glob( "Cohort.wgs_gvcf_combine.failed.*.manifest.json" ) ) assert len(sidecars) == 1 assert json.loads(sidecars[0].read_text())["artifacts"] == {} @pytest.mark.parametrize( "environment", ("BAD_OUTPUT_HEADER", "CORRUPT_OUTPUT_INDEX") ) def test_output_header_or_index_postflight_failure_is_not_published( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, environment: str, ) -> None: from apps.cli.run_wgs_gvcf_combine import main workspace = tmp_path / "workspace" haplotype, index = _authorities(workspace) monkeypatch.setenv(environment, "1") assert main(_argv(workspace, haplotype, index, _gatk(tmp_path))) == 1 failure = json.loads( ( workspace / "final" / "Cohort.wgs_gvcf_combine.manifest.json" ).read_text() ) assert failure["status"] == "failed" assert failure["artifacts"] == {} def test_dual_upstream_identity_is_mandatory(tmp_path: Path) -> None: from apps.cli.run_wgs_gvcf_combine import main workspace = tmp_path / "workspace" haplotype, index = _authorities(workspace) payload = json.loads(index.read_text()) payload["wgs_reference_id"] = "wgs-reference:" + "f" * 64 index.write_text(json.dumps(payload)) assert main(_argv(workspace, haplotype, index, _gatk(tmp_path))) == 1 def test_status_and_results_require_exact_identity_and_singleton_roles( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: import asyncio from apps.cli.run_wgs_gvcf_combine import main from apps.web.vue3 import main as web_main from gfa_core.mas.tools.stage_status import get_stage_status workspace_root = tmp_path / "workspaces" workspace = workspace_root / "job-1" haplotype, index = _authorities(workspace) assert main(_argv(workspace, haplotype, index, _gatk(tmp_path))) == 0 manifest_path = ( workspace / "final" / "Cohort.wgs_gvcf_combine.manifest.json" ) manifest = json.loads(manifest_path.read_text()) analysis_id = manifest["wgs_analysis_id"] status = get_stage_status( "job-1", "Cohort", "wgs_gvcf_combine", workspace_root, wgs_sample_set_id=SAMPLES, wgs_reference_id=REFERENCE, wgs_analysis_id=analysis_id, ) assert status.completed monkeypatch.setattr(web_main, "WORKSPACE_ROOT", workspace_root) results = asyncio.run( web_main.get_results( job_id="job-1", stage="wgs_gvcf_combine", wgs_sample_set_id=SAMPLES, wgs_reference_id=REFERENCE, wgs_analysis_id=analysis_id, ) ) serialized = json.dumps(results) assert "combined_gvcf" in serialized assert "combined_gvcf_index" in serialized manifest["artifacts"].pop("combined_gvcf_index") manifest_path.write_text(json.dumps(manifest)) broken = get_stage_status( "job-1", "Cohort", "wgs_gvcf_combine", workspace_root, wgs_sample_set_id=SAMPLES, wgs_reference_id=REFERENCE, wgs_analysis_id=analysis_id, ) assert not broken.completed def test_backend_builds_stage07_command_and_accepts_only_fresh_manifest( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: from apps.cli.run_wgs_gvcf_combine import main from apps.web import pipeline_backend workspace = tmp_path / "workspace" haplotype, index = _authorities(workspace) gatk = _gatk(tmp_path) calls: list[list[str]] = [] def fake_run(command, *_args, **_kwargs): calls.append(command) return main(command[3:]) monkeypatch.setattr(pipeline_backend, "run_cmd_logged", fake_run) message, output = pipeline_backend.run_stage( stage="wgs_gvcf_combine", job=pipeline_backend.JobInfo( "job-1", "", str(workspace), str(workspace / "logs"), "now" ), prefix="Cohort", wgs_haplotype_manifest=str(haplotype), wgs_index_manifest=str(index), wgs_sample_set_id=SAMPLES, wgs_reference_id=REFERENCE, wgs_haplotype_analysis_id=HAPLOTYPE_ANALYSIS, wgs_index_analysis_id=INDEX_ANALYSIS, wgs_gatk=str(gatk), ) assert "completed" in message.lower() assert output == ( workspace / "final" / "Cohort.wgs_gvcf_combine.manifest.json" ) command = calls[0] assert command[:3] == [ pipeline_backend.sys.executable, "-m", "apps.cli.run_wgs_gvcf_combine", ] assert command[command.index("--haplotype-manifest") + 1] == str( haplotype ) before = output.read_bytes() monkeypatch.setattr( pipeline_backend, "run_cmd_logged", lambda *_args, **_kwargs: 0 ) stale_message, stale_output = pipeline_backend.run_stage( stage="wgs_gvcf_combine", job=pipeline_backend.JobInfo( "job-1", "", str(workspace), str(workspace / "logs"), "now" ), prefix="Cohort", wgs_haplotype_manifest=str(haplotype), wgs_index_manifest=str(index), wgs_sample_set_id=SAMPLES, wgs_reference_id=REFERENCE, wgs_haplotype_analysis_id=HAPLOTYPE_ANALYSIS, wgs_index_analysis_id=INDEX_ANALYSIS, wgs_gatk=str(gatk), ) assert "failed" in stale_message.lower() assert stale_output == output assert output.read_bytes() == before def test_publish_lock_prevents_older_attempt_replacing_newer( tmp_path: Path ) -> None: from apps.cli.run_wgs_gvcf_combine import _publish workspace = tmp_path / "workspace" authority = workspace / "final" / "Cohort.wgs_gvcf_combine.manifest.json" destination = ( workspace / "final" / ".wgs_gvcf_combine_runs" / "Cohort" / ("9" * 32) ) authority.parent.mkdir(parents=True) authority.write_text( json.dumps({"status": "success", "start_time": "2026-07-23T12:00:00Z"}) ) bundle = tmp_path / "bundle" bundle.mkdir() (bundle / "artifact").write_text("older") manifest_work = tmp_path / "manifest.json" manifest_work.write_text("{}") assert not _publish( bundle, destination, manifest_work, authority, "2026-07-23T11:00:00Z", ) assert json.loads(authority.read_text())["start_time"].startswith( "2026-07-23T12" ) def test_concurrent_publish_has_one_complete_authority(tmp_path: Path) -> None: from apps.cli.run_wgs_gvcf_combine import _publish workspace = tmp_path / "workspace" authority = workspace / "final" / "Cohort.wgs_gvcf_combine.manifest.json" authority.parent.mkdir(parents=True) barrier = Barrier(2) outcomes: list[bool] = [] def worker(name: str, started_at: str) -> None: bundle = tmp_path / f"bundle-{name}" bundle.mkdir() (bundle / "artifact").write_text(name) manifest = tmp_path / f"manifest-{name}.json" manifest.write_text( json.dumps( { "status": "success", "start_time": started_at, "winner": name, } ) ) destination = ( workspace / "final" / ".wgs_gvcf_combine_runs" / "Cohort" / name ) barrier.wait() outcomes.append( _publish( bundle, destination, manifest, authority, started_at ) ) threads = [ Thread(target=worker, args=("a" * 32, "2026-07-23T11:00:00Z")), Thread(target=worker, args=("b" * 32, "2026-07-23T12:00:00Z")), ] for thread in threads: thread.start() for thread in threads: thread.join() manifest = json.loads(authority.read_text()) assert manifest["winner"] == "b" * 32 assert outcomes.count(True) in {1, 2}