Files
PlantAI-rag-new/bioflow_service/tests/test_manifests.py
T
admin 0282cfeeeb feat(bioflow): 阶段进度条+滚动日志+点击定位全链;图片注入三修;QA启动就绪等待
- 生信进度:后端新增 stage_progress 事件帧+进度解析白名单(rnaseq_qc/sample_qc),
  kernel 日志流式落盘(20行/1s flush+fsync),logs 分页端点(offset/limit/eof),
  事件 seq 跨重启扫描续增(2026-09-17 断流修复);前端 FlowProgressCard 确定性/
  不确定两态进度条+点击定位到生信任务 TAB,BioJobsPane DAG 行展开滚动日志区
  (3s 轮询),log_tail 4000 截断,重跑 stage_started 清零进度
- 图片注入:figures 三源重构(标准DOI图库主路径/遗留体系兜底/PMC兜底),
  [[FIG:]] 宽松归一化识别 PMC/PMID/DOI 三种形态,引文兜底扩展至
  [PMCID:]/[DOI:] 且标记未满配额继续注入,注入预算 90s/10篇(175 实测校准),
  PMC graphic 无 content-type 宽松正则,空结果不缓存(176 冷连接三连败修复)
- QA 启动自愈:start.sh 启动前等 Qdrant readyz(30min)+Neo4j TCP(10min);
  backend_guard 标签校验改子集判定
- 样本配对:同 inode 去重优先真实文件,R1/R2 归一配对一对一认领
- gitignore 新增 *.bak-*/*.before-*/bioflow_service/bioflow.db

测试:bioflow_service pytest 358/358,前端 bioflow vitest 205/205
2026-09-24 16:48:52 +08:00

179 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""tests/test_manifests.py —— manifest 解析/校验/跨阶段输入定位。"""
from __future__ import annotations
import json
import pytest
from app.services.bioflow.manifests import (
Manifest,
ManifestError,
artifact_paths,
input_resolution,
locate_manifest,
parse_manifest,
)
pytestmark = pytest.mark.bioflow_unit
def _write_manifest(workspace, stage, status="success", prefix="Ft.SPL", extra=None):
final = workspace / "final"
final.mkdir(parents=True, exist_ok=True)
raw = {
"manifest_version": "1", "job_id": "j1", "stage": stage, "status": status,
"prefix": prefix,
"artifacts": {
"final_members": {"artifact": "final_members",
"full_path": str(final / f"{prefix}.final_members.id"),
"type": "table", "size": 100},
},
"parameters": {"cpu": 8}, "metrics": {"final_members": 24},
}
raw.update(extra or {})
path = final / f"{prefix}.{stage}.manifest.json"
path.write_text(json.dumps(raw), encoding="utf-8")
return path
def test_parse_success(tmp_path):
_write_manifest(tmp_path, "identify")
m = parse_manifest(tmp_path, "identify")
assert isinstance(m, Manifest)
assert m.status == "success"
assert m.artifacts["final_members"]["type"] == "table"
assert m.metrics["final_members"] == 24
assert m.prefix == "Ft.SPL"
def test_missing_manifest(tmp_path):
with pytest.raises(ManifestError, match="缺失"):
parse_manifest(tmp_path, "identify")
def test_invalid_json(tmp_path):
final = tmp_path / "final"
final.mkdir(parents=True)
(final / "Ft.SPL.identify.manifest.json").write_text("{broken", encoding="utf-8")
with pytest.raises(ManifestError, match="非法 JSON"):
parse_manifest(tmp_path, "identify")
def test_missing_required_keys(tmp_path):
final = tmp_path / "final"
final.mkdir(parents=True)
(final / "Ft.SPL.identify.manifest.json").write_text('{"stage": "identify"}', encoding="utf-8")
with pytest.raises(ManifestError, match="缺必需字段"):
parse_manifest(tmp_path, "identify")
def test_invalid_status(tmp_path):
_write_manifest(tmp_path, "identify", status="weird")
with pytest.raises(ManifestError, match="status 非法"):
parse_manifest(tmp_path, "identify")
def test_failed_status_valid(tmp_path):
_write_manifest(tmp_path, "identify", status="failed")
m = parse_manifest(tmp_path, "identify")
assert m.status == "failed"
def test_locate_manifest_strict_no_fallback(tmp_path):
# 无 prefix 匹配时绝不兜底其他阶段 manifest(跨阶段误配会导致误判成败)
path = _write_manifest(tmp_path, "identify")
assert locate_manifest(tmp_path, "phylogeny", prefix="OTHER") is None
assert locate_manifest(tmp_path, "identify") == path
def test_locate_manifest_no_infix_name(tmp_path):
"""BUG-FIX 2026-09-18:无中缀名兜底 final/{stage}.manifest.json
(rnaseq 系 CLI 空 prefix 形态,如 rnaseq_qc.manifest.json)。"""
final = tmp_path / "final"
final.mkdir(parents=True, exist_ok=True)
raw = {
"manifest_version": "1.0", "job_id": "unknown", "stage": "rnaseq_qc",
"status": "success", "prefix": "",
"artifacts": {"clean": {"full_path": str(final / "x.clean.fastq.gz")}},
"parameters": {}, "metrics": {},
}
path = final / "rnaseq_qc.manifest.json"
path.write_text(json.dumps(raw), encoding="utf-8")
assert locate_manifest(tmp_path, "rnaseq_qc") == path
assert locate_manifest(tmp_path, "rnaseq_qc", prefix="") == path
m = parse_manifest(tmp_path, "rnaseq_qc")
assert m.status == "success"
assert m.stage == "rnaseq_qc"
def test_locate_manifest_no_cross_stage_misattribution(tmp_path):
"""无中缀兜底不跨阶段误配:stage=rnaseq 不命中 rnaseq_qc.manifest.json;
stage=rnaseq_qc 也不命中他阶段带 prefix 的 manifest。"""
final = tmp_path / "final"
final.mkdir(parents=True, exist_ok=True)
_write_manifest(tmp_path, "rnaseq_qc") # Ft.SPL.rnaseq_qc.manifest.json
# 加一个裸名 rnaseq_qc.manifest.json 模拟 rnaseq 系实际产出
raw = {
"manifest_version": "1.0", "job_id": "j1", "stage": "rnaseq_qc",
"status": "success", "prefix": "",
"artifacts": {}, "parameters": {}, "metrics": {},
}
(final / "rnaseq_qc.manifest.json").write_text(json.dumps(raw), encoding="utf-8")
# stage=rnaseq 绝不命中 rnaseq_qc.manifest.json(精确等名,无前缀混淆)
assert locate_manifest(tmp_path, "rnaseq") is None
# 中缀匹配优先于裸名兜底(有 prefix 中缀文件时先命中)
qc = locate_manifest(tmp_path, "rnaseq_qc")
assert qc is not None
assert qc.name == "Ft.SPL.rnaseq_qc.manifest.json"
def test_locate_manifest_bare_name_priority(tmp_path):
"""只有裸名文件时兜底命中;同目录存在其他阶段 manifest 不干扰。"""
final = tmp_path / "final"
final.mkdir(parents=True, exist_ok=True)
_write_manifest(tmp_path, "identify") # Ft.SPL.identify.manifest.json
raw = {
"manifest_version": "1.0", "job_id": "j1", "stage": "rnaseq_qc",
"status": "success", "prefix": "",
"artifacts": {}, "parameters": {}, "metrics": {},
}
bare = final / "rnaseq_qc.manifest.json"
bare.write_text(json.dumps(raw), encoding="utf-8")
assert locate_manifest(tmp_path, "rnaseq_qc") == bare
# identify 照常中缀命中,不受裸名文件影响
assert locate_manifest(tmp_path, "identify").name == "Ft.SPL.identify.manifest.json"
def test_artifact_paths():
artifacts = {
"a": {"full_path": "/x/a.id", "type": "table"},
"b": "/y/b.fa",
"c": {"path": "/z/c.txt"},
"d": {"relpath_or_hint": "final/d.txt"},
}
paths = artifact_paths(artifacts)
assert "/x/a.id" in paths
assert "/y/b.fa" in paths
assert "/z/c.txt" in paths
assert "final/d.txt" in paths
def test_input_resolution(tmp_path):
final = tmp_path / "final"
final.mkdir(parents=True, exist_ok=True)
(final / "Ft.SPL.final_members.id").write_text("gene1\ngene2\n", encoding="utf-8")
_write_manifest(tmp_path, "identify")
res = input_resolution(tmp_path, "identify")
assert res["resolved"] is True
assert res["manifest_path"]
assert res["inputs"]
def test_input_resolution_missing(tmp_path):
res = input_resolution(tmp_path, "identify")
assert res["resolved"] is False
assert "缺失" in res["error"]