"""整张表的问题 T1–T5 与配置错误:整次构建停下,两个库下不留完成的构建目录。""" from datetime import datetime from pathlib import Path import pytest from pydantic import ValidationError from cadx.build import load_build_config, load_code_maps, run_build from cadx.delivery import DeliveryError from fake_delivery import ( CLINICAL_PATH, MR_PATH, REPO_ROOT, SAMPLE_CODE_MAPS, ULTRASOUND_PATH, make_delivery, write_config, ) NOW = datetime(2026, 10, 16, 9, 0, 0).astimezone() def _assert_no_finished_build(tmp_path: Path) -> None: for root in (tmp_path / "library", tmp_path / "answers"): if root.exists(): assert all(p.name.endswith(".partial") for p in root.iterdir()) def _run(tmp_path: Path, root: Path, code_maps: dict[str, dict[int, str]] | None) -> None: run_build(write_config(tmp_path, root, 7, code_maps), REPO_ROOT, NOW) def test_t1_missing_summary_table(tmp_path: Path) -> None: root = make_delivery(3).write(tmp_path / "delivery") (root / MR_PATH).unlink() with pytest.raises(DeliveryError, match=r"^T1"): _run(tmp_path, root, None) _assert_no_finished_build(tmp_path) def test_t2_wrong_sheet_name(tmp_path: Path) -> None: delivery = make_delivery(3) delivery.sheet_names[ULTRASOUND_PATH] = "Sheet1" root = delivery.write(tmp_path / "delivery") with pytest.raises(DeliveryError, match=r"^T2"): _run(tmp_path, root, None) _assert_no_finished_build(tmp_path) def test_t3_header_differs(tmp_path: Path) -> None: delivery = make_delivery(3) delivery.headers[MR_PATH][3] = "病理类型" # 诊断列名里写的编码含义丢了 root = delivery.write(tmp_path / "delivery") with pytest.raises(DeliveryError, match=r"^T3"): _run(tmp_path, root, None) _assert_no_finished_build(tmp_path) def test_t4_extra_file_in_clinical_folder(tmp_path: Path) -> None: root = make_delivery(3).write(tmp_path / "delivery") (root / CLINICAL_PATH).with_name("备注.txt").write_text("x", encoding="utf-8") with pytest.raises(DeliveryError, match=r"^T4"): _run(tmp_path, root, None) _assert_no_finished_build(tmp_path) def test_t4_unexpected_entry_in_ceus_folder(tmp_path: Path) -> None: root = make_delivery(3).write(tmp_path / "delivery") (root / "CEUS_agent" / "41").mkdir() # 不是三位数字 with pytest.raises(DeliveryError, match=r"^T4"): _run(tmp_path, root, None) _assert_no_finished_build(tmp_path) def test_t5_code_maps_missing_a_column(tmp_path: Path) -> None: root = make_delivery(3).write(tmp_path / "delivery") maps = {k: v for k, v in SAMPLE_CODE_MAPS.items() if k != "TBil_Level"} with pytest.raises(ValueError, match=r"^T5"): _run(tmp_path, root, maps) _assert_no_finished_build(tmp_path) def test_t5_code_maps_extra_column(tmp_path: Path) -> None: root = make_delivery(3).write(tmp_path / "delivery") maps = {**SAMPLE_CODE_MAPS, "Age": {1: "一"}} with pytest.raises(ValueError, match=r"^T5"): _run(tmp_path, root, maps) _assert_no_finished_build(tmp_path) def test_t5_sex_must_map_to_chinese_characters(tmp_path: Path) -> None: root = make_delivery(3).write(tmp_path / "delivery") maps = {**SAMPLE_CODE_MAPS, "Sex": {1: "M", 2: "F"}} with pytest.raises(ValueError, match=r"^T5"): _run(tmp_path, root, maps) _assert_no_finished_build(tmp_path) def test_t5_diagnosis_must_map_to_four_subtypes(tmp_path: Path) -> None: root = make_delivery(3).write(tmp_path / "delivery") maps = {**SAMPLE_CODE_MAPS, "Diagnosis": {1: "IPMN", 2: "SPN", 3: "MCN", 4: "MCN"}} with pytest.raises(ValueError, match=r"^T5"): _run(tmp_path, root, maps) _assert_no_finished_build(tmp_path) def _write_yaml(tmp_path: Path, body: str) -> Path: path = tmp_path / "build.yaml" path.write_text(body, encoding="utf-8") return path _BASE = "delivery_dir: d\nlibrary_root: l\nanswers_root: a\nalias_seed: 1\n" def test_config_decode_true_needs_code_maps(tmp_path: Path) -> None: path = _write_yaml(tmp_path, _BASE + "decode_codes: true\ncode_maps: null\n") with pytest.raises(ValidationError): load_build_config(path, tmp_path) def test_config_decode_false_needs_null_code_maps(tmp_path: Path) -> None: path = _write_yaml(tmp_path, _BASE + "decode_codes: false\ncode_maps: maps.yaml\n") with pytest.raises(ValidationError): load_build_config(path, tmp_path) def test_config_field_missing(tmp_path: Path) -> None: path = _write_yaml(tmp_path, _BASE + "decode_codes: false\n") with pytest.raises(ValidationError): load_build_config(path, tmp_path) def test_config_extra_field(tmp_path: Path) -> None: path = _write_yaml(tmp_path, _BASE + "decode_codes: false\ncode_maps: null\nseed: 2\n") with pytest.raises(ValidationError): load_build_config(path, tmp_path) def test_config_seed_must_be_int_not_string(tmp_path: Path) -> None: body = _BASE.replace("alias_seed: 1", "alias_seed: '1'") path = _write_yaml(tmp_path, body + "decode_codes: false\ncode_maps: null\n") with pytest.raises(ValidationError): load_build_config(path, tmp_path) def test_config_relative_paths_resolve_against_repo_root(tmp_path: Path) -> None: path = _write_yaml(tmp_path, _BASE + "decode_codes: false\ncode_maps: null\n") config = load_build_config(path, Path("/repo")) assert config.delivery_dir == Path("/repo/d") assert config.library_root == Path("/repo/l") assert config.answers_root == Path("/repo/a") assert config.code_maps is None def test_committed_config_is_valid() -> None: config = load_build_config(REPO_ROOT / "config" / "build.yaml", REPO_ROOT) assert config.decode_codes is False assert config.code_maps is None def test_config_duplicate_key(tmp_path: Path) -> None: body = _BASE + "decode_codes: false\ncode_maps: null\nalias_seed: 2\n" path = _write_yaml(tmp_path, body) with pytest.raises(ValueError, match="重复的键"): load_build_config(path, tmp_path) def test_config_empty_path(tmp_path: Path) -> None: body = _BASE.replace("delivery_dir: d", "delivery_dir: ''") path = _write_yaml(tmp_path, body + "decode_codes: false\ncode_maps: null\n") with pytest.raises(ValidationError, match="空串"): load_build_config(path, tmp_path) def test_config_empty_code_maps_path(tmp_path: Path) -> None: path = _write_yaml(tmp_path, _BASE + "decode_codes: true\ncode_maps: ''\n") with pytest.raises(ValidationError, match="空串"): load_build_config(path, tmp_path) def test_code_maps_duplicate_key(tmp_path: Path) -> None: path = tmp_path / "code_maps.yaml" path.write_text("Sex: {1: 男, 1: 女}\n", encoding="utf-8") with pytest.raises(ValueError, match="重复的键"): load_code_maps(path)