import json from pathlib import Path from apps.web import pipeline_backend def _valid_manifest() -> dict: return { "stage": "wgs_qc", "status": "success", "prefix": "Cohort", "job_id": "qc-job", "wgs_sample_set_id": "wgs-samples:" + "a" * 64, "wgs_analysis_id": "wgs-analysis:" + "b" * 64, "artifacts": { "wgs_sample_config": {"role": "wgs_sample_config"}, **{f"{role}.S1": {"role": role, "sample_id": "S1"} for role in ("clean_reads_r1", "clean_reads_r2", "fastp_html", "fastp_json")}, }, } def test_wgs_manifest_validator_rejects_wrong_config_role_and_incomplete_sample_roles() -> None: wrong_role = _valid_manifest() wrong_role["artifacts"]["wgs_sample_config"]["role"] = "input_config" assert not pipeline_backend._valid_wgs_qc_manifest(wrong_role, job_id="qc-job", prefix="Cohort", sample_ids={"S1"}) incomplete = _valid_manifest() del incomplete["artifacts"]["fastp_json.S1"] assert not pipeline_backend._valid_wgs_qc_manifest(incomplete, job_id="qc-job", prefix="Cohort", sample_ids={"S1"}) def test_wgs_qc_backend_maps_api_config_to_canonical_cli_and_requires_fresh_manifest(monkeypatch, tmp_path: Path) -> None: input_dir = tmp_path / "input" workspace = tmp_path / "workspace" input_dir.mkdir() (input_dir / "S1.R1.fastq").write_text("@r\nA\n+\nI\n", encoding="utf-8") (input_dir / "S1.R2.fastq").write_text("@r\nA\n+\nI\n", encoding="utf-8") config = input_dir / "wgs_samples.yaml" config.write_text("samples:\n S1:\n r1: S1.R1.fastq\n r2: S1.R2.fastq\n", encoding="utf-8") final_dir = workspace / "final" final_dir.mkdir(parents=True) manifest_path = final_dir / "Cohort.wgs_qc.manifest.json" manifest_path.write_text(json.dumps({"stage": "wgs_qc", "status": "success", "prefix": "Cohort", "artifacts": {}}), encoding="utf-8") captured = {} def fake_run(cmd, log_file, cwd): captured["cmd"] = list(cmd) manifest_path.write_text(json.dumps({ "stage": "wgs_qc", "status": "success", "prefix": "Cohort", "artifacts": {"wgs_sample_config": {"role": "wgs_sample_config", "path": "final/config.yaml"}, **{f"{role}.S1": {"role": role, "sample_id": "S1"} for role in ("clean_reads_r1", "clean_reads_r2", "fastp_html", "fastp_json")}}, "job_id": "qc-job", "wgs_sample_set_id": "wgs-samples:" + "a" * 64, "wgs_analysis_id": "wgs-analysis:" + "a" * 64, }), encoding="utf-8") return 0 monkeypatch.setattr(pipeline_backend, "run_cmd_logged", fake_run) message, returned_manifest = pipeline_backend.run_stage( stage="wgs_qc", job=pipeline_backend.JobInfo("qc-job", str(input_dir), str(workspace), str(workspace / "logs"), "now"), prefix="Cohort", wgs_sample_config=str(config), wgs_fastp="/opt/fastp", wgs_threads=3, wgs_max_jobs=2, ) assert "completed" in message.lower() assert returned_manifest == manifest_path assert captured["cmd"][:3] == [pipeline_backend.sys.executable, "-m", "apps.cli.run_wgs_qc"] assert captured["cmd"][captured["cmd"].index("--wgs-samples") + 1] == str(config) assert captured["cmd"][captured["cmd"].index("--fastp") + 1] == "/opt/fastp" def test_wgs_qc_backend_rejects_missing_or_multiple_sample_configs(tmp_path: Path) -> None: job = pipeline_backend.JobInfo("qc-job", str(tmp_path / "input"), str(tmp_path / "workspace"), str(tmp_path / "logs"), "now") message, _ = pipeline_backend.run_stage(stage="wgs_qc", job=job, prefix="Cohort") assert "wgs_samples.yaml" in message input_dir = Path(job.input_dir); workspace = Path(job.workspace_dir) input_dir.mkdir(); workspace.mkdir() (input_dir / "wgs_samples.yaml").write_text("samples: {}\n", encoding="utf-8") (workspace / "wgs_samples.yaml").write_text("samples: {}\n", encoding="utf-8") message, _ = pipeline_backend.run_stage(stage="wgs_qc", job=job, prefix="Cohort") assert "exactly one" in message def test_wgs_qc_backend_rejects_fresh_but_identityless_manifest(monkeypatch, tmp_path: Path) -> None: input_dir = tmp_path / "input"; workspace = tmp_path / "workspace"; input_dir.mkdir() for mate in ("R1", "R2"): (input_dir / f"S1.{mate}.fastq").write_text("@r\nA\n+\nI\n", encoding="utf-8") config = input_dir / "wgs_samples.yaml" config.write_text("samples:\n S1:\n r1: S1.R1.fastq\n r2: S1.R2.fastq\n", encoding="utf-8") manifest = workspace / "final" / "Cohort.wgs_qc.manifest.json"; manifest.parent.mkdir(parents=True) def fake_run(*_args, **_kwargs): manifest.write_text(json.dumps({"stage": "wgs_qc", "status": "success", "prefix": "Cohort", "job_id": "qc-job", "artifacts": {}}), encoding="utf-8") return 0 monkeypatch.setattr(pipeline_backend, "run_cmd_logged", fake_run) message, _ = pipeline_backend.run_stage(stage="wgs_qc", job=pipeline_backend.JobInfo("qc-job", str(input_dir), str(workspace), str(workspace / "logs"), "now"), prefix="Cohort", wgs_sample_config=str(config)) assert "failed" in message.lower() def test_wgs_qc_backend_rejects_stale_success_manifest(monkeypatch, tmp_path: Path) -> None: input_dir = tmp_path / "input"; workspace = tmp_path / "workspace"; input_dir.mkdir() for mate in ("R1", "R2"): (input_dir / f"S1.{mate}.fastq").write_text("@r\nA\n+\nI\n", encoding="utf-8") config = input_dir / "wgs_samples.yaml" config.write_text("samples:\n S1:\n r1: S1.R1.fastq\n r2: S1.R2.fastq\n", encoding="utf-8") manifest = workspace / "final" / "Cohort.wgs_qc.manifest.json"; manifest.parent.mkdir(parents=True) manifest.write_text(json.dumps(_valid_manifest()), encoding="utf-8") monkeypatch.setattr(pipeline_backend, "run_cmd_logged", lambda *_args, **_kwargs: 0) message, returned = pipeline_backend.run_stage(stage="wgs_qc", job=pipeline_backend.JobInfo("qc-job", str(input_dir), str(workspace), str(workspace / "logs"), "now"), prefix="Cohort", wgs_sample_config=str(config)) assert "failed" in message.lower() assert returned == manifest