from __future__ import annotations import asyncio import hashlib import json from pathlib import Path from gfa_core.mas.tools.stage_identity import StageIdentity, get_stage_status_for_identity def _trait_manifest(final: Path, *, phenotype_scope: str, digest12: str) -> Path: trait = final / f"cohort.gwas_trait.{digest12}.trait.txt" report = final / f"cohort.gwas_trait.{digest12}.report.md" environment = final / f"cohort.gwas_trait.{digest12}.environment.txt" for path in (trait, report, environment): path.write_text(path.name + "\n", encoding="utf-8") def artifact(path: Path, role: str) -> dict: return { "role": role, "path": f"final/{path.name}", "full_path": str(path.resolve()), "type": "text/plain", "size": path.stat().st_size, "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), } manifest = { "manifest_version": "1.0", "stage": "gwas_trait", "prefix": "cohort", "job_id": "job", "run_id": f"gwas_trait_{digest12}", "status": "success", "phenotype_scope_id": phenotype_scope, "analysis_scope_id": "gwas-analysis:" + digest12.ljust(64, "0"), "elapsed_sec": 1.2, "artifacts": { "trait_table": artifact(trait, "trait_table"), "report": artifact(report, "report"), "environment": artifact(environment, "environment"), }, } path = final / f"cohort.gwas_trait.{digest12}.manifest.json" path.write_text(json.dumps(manifest), encoding="utf-8") return path def test_status_and_results_select_exact_phenotype_scope(monkeypatch, tmp_path: Path) -> None: from apps.web.vue3 import main as web workspace_root = tmp_path / "workspace" final = workspace_root / "job" / "final" final.mkdir(parents=True) wanted = "phenotype:" + "a" * 64 _trait_manifest(final, phenotype_scope=wanted, digest12="aaaaaaaaaaaa") _trait_manifest(final, phenotype_scope="phenotype:" + "b" * 64, digest12="bbbbbbbbbbbb") monkeypatch.setattr(web, "WORKSPACE_ROOT", workspace_root) report = get_stage_status_for_identity( job_id="job", identity=StageIdentity(stage="gwas_trait", prefix="cohort", phenotype_scope_id=wanted), workspace_root=workspace_root, ) assert report.completed is True assert report.manifest["phenotype_scope_id"] == wanted payload = asyncio.run(web.get_results( job_id="job", stage="gwas_trait", rnaseq_scope_id="", scope_id="", genotype_scope_id="", phenotype_scope_id=wanted, covariate_scope_id="", analysis_scope_id="", )) assert payload["gwas_trait"]["status"] == "success" assert {item["role"] for item in payload["gwas_trait"]["files"]} == { "manifest", "trait_table", "report", "environment", } assert all("aaaaaaaaaaaa" in item["name"] for item in payload["gwas_trait"]["files"]) without_scope = asyncio.run(web.get_results( job_id="job", stage="gwas_trait", rnaseq_scope_id="", scope_id="", genotype_scope_id="", phenotype_scope_id="", covariate_scope_id="", analysis_scope_id="", )) assert without_scope["gwas_trait"]["files"] == [] def test_results_rehydration_does_not_replay_success_after_failed_attempt( monkeypatch, tmp_path: Path, ) -> None: from apps.web.vue3 import main as web workspace_root = tmp_path / "workspace" final = workspace_root / "job" / "final" final.mkdir(parents=True) success_path = _trait_manifest( final, phenotype_scope="phenotype:" + "a" * 64, digest12="aaaaaaaaaaaa" ) failed = json.loads(success_path.read_text(encoding="utf-8")) failed.update({ "run_id": "gwas_trait_failed", "status": "failed", "start_time": "2026-07-24T00:00:00+00:00", "end_time": "2026-07-24T00:00:01+00:00", "artifacts": {}, "error": "forced failure", }) (final / "cohort.gwas_trait.failed.manifest.json").write_text( json.dumps(failed), encoding="utf-8" ) monkeypatch.setattr(web, "WORKSPACE_ROOT", workspace_root) payload = asyncio.run(web.get_results( job_id="job", stage="gwas_trait", rnaseq_scope_id="", scope_id="", genotype_scope_id="", phenotype_scope_id="", covariate_scope_id="", analysis_scope_id="", )) assert payload["gwas_trait"]["files"] == []