wsi-annotation-tests 0.1.3

DICOM Annotation Test Suite for WSI ANN, SEG, SR, and Parametric Map
Documentation
use serde_json::Value;
use wsi_annotation_tests::schema::{
    RunManifestVersion, validate_compatible_run_manifest, validate_conversion_report,
    validate_matrix_observation, validate_pathology_mapping, validate_probe_report,
    validate_raster_profile, validate_run_manifest, validate_tiled_manifest,
};

fn reference_report() -> Value {
    serde_json::from_str(include_str!("data/ann-report.json")).unwrap()
}

#[test]
fn committed_v1_manifest_remains_readable_without_fabricated_provenance() {
    let legacy: Value =
        serde_json::from_str(include_str!("data/legacy-run-manifest-v1.json")).unwrap();

    assert_eq!(
        validate_compatible_run_manifest(&legacy).unwrap(),
        RunManifestVersion::V1Legacy
    );
    assert!(legacy.get("provenance").is_none());
}

#[test]
fn published_run_manifest_v2_validates_and_rejects_v1() {
    let directory = tempfile::tempdir().unwrap();
    let mut writer = wsi_annotation_tests::results::RunWriter::new(
        directory.path(),
        "schema-v2",
        serde_json::json!({"profile": "core"}),
    )
    .unwrap();
    writer
        .write_observations(&[serde_json::json!({"status": "passed"})])
        .unwrap();
    let path = writer.finalize().unwrap();
    let mut manifest: Value = serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
    validate_run_manifest(&manifest).unwrap();

    manifest["schema_version"] = serde_json::json!(1);
    assert!(validate_run_manifest(&manifest).is_err());
}

#[test]
fn probe_schema_accepts_the_reference_report() {
    validate_probe_report(&reference_report()).unwrap();
}

#[test]
fn matrix_observation_v2_schema_preserves_phase_evidence() {
    let observation: Value =
        serde_json::from_str(include_str!("../examples/matrix-observation-v2.json")).unwrap();
    validate_matrix_observation(&observation).unwrap();

    let mut missing_phase_status = observation.clone();
    missing_phase_status["phases"][0]
        .as_object_mut()
        .unwrap()
        .remove("status");
    assert!(validate_matrix_observation(&missing_phase_status).is_err());

    let mut old_version = observation;
    old_version["schema_version"] = serde_json::json!(1);
    assert!(validate_matrix_observation(&old_version).is_err());
}

#[test]
fn probe_schema_rejects_runtime_inside_semantic_data() {
    let mut report = reference_report();
    report["semantic"]["runtime"] = serde_json::json!({"parse_ms": 1.0});

    let error = validate_probe_report(&report).unwrap_err();

    assert!(error.contains("probe report schema validation failed"));
    assert!(error.contains("semantic"));
}

#[test]
fn shipped_conversion_contract_examples_validate() {
    let cases = [
        (
            include_str!("../examples/conversion-report-v1.json"),
            validate_conversion_report as fn(&Value) -> Result<(), String>,
        ),
        (
            include_str!("../examples/pathology-mapping-v1.json"),
            validate_pathology_mapping,
        ),
        (
            include_str!("../examples/raster-profile-v1.json"),
            validate_raster_profile,
        ),
        (
            include_str!("../examples/raster-profile-concatenation-v1.json"),
            validate_raster_profile,
        ),
        (
            include_str!("../examples/tiled-manifest-v1.json"),
            validate_tiled_manifest,
        ),
    ];
    for (document, validate) in cases {
        let value = serde_json::from_str(document).unwrap();
        validate(&value).unwrap();
    }
}

#[test]
fn conversion_schema_rejects_untracked_output_fields() {
    let mut report: Value =
        serde_json::from_str(include_str!("../examples/conversion-report-v1.json")).unwrap();
    report["outputs"][0]["untracked"] = serde_json::json!(true);

    let error = validate_conversion_report(&report).unwrap_err();

    assert!(error.contains("conversion report schema validation failed"));
    assert!(error.contains("untracked"));
}

#[test]
fn profile_schemas_enforce_scheme_designator_conditions_for_urn_codes() {
    let mut raster: Value =
        serde_json::from_str(include_str!("../examples/raster-profile-v1.json")).unwrap();
    raster["channels"][0]["quantity"] = serde_json::json!({
        "urn_code_value": "urn:example:pathology:probability",
        "code_meaning": "Tumor probability"
    });
    validate_raster_profile(&raster).unwrap();

    raster["channels"][0]["quantity"]["coding_scheme_version"] = serde_json::json!("2026");
    assert!(validate_raster_profile(&raster).is_err());

    let mut mapping: Value =
        serde_json::from_str(include_str!("../examples/pathology-mapping-v1.json")).unwrap();
    mapping["labels"]["viable_tumor"]["category"] = serde_json::json!({
        "code_value": "MORPH",
        "code_meaning": "Morphologically abnormal structure"
    });
    assert!(validate_pathology_mapping(&mapping).is_err());
}

#[test]
fn profile_schemas_reject_incomplete_enhanced_code_qualifiers() {
    for (document, code_pointer, validate) in [
        (
            serde_json::from_str::<Value>(include_str!("../examples/pathology-mapping-v1.json"))
                .unwrap(),
            "/labels/viable_tumor/category",
            validate_pathology_mapping as fn(&Value) -> Result<(), String>,
        ),
        (
            serde_json::from_str::<Value>(include_str!("../examples/raster-profile-v1.json"))
                .unwrap(),
            "/channels/0/quantity",
            validate_raster_profile as fn(&Value) -> Result<(), String>,
        ),
    ] {
        let mut incomplete_context = document.clone();
        incomplete_context.pointer_mut(code_pointer).unwrap()["context_identifier"] =
            serde_json::json!("1234");
        assert!(validate(&incomplete_context).is_err());

        let mut invalid_uid = document.clone();
        invalid_uid.pointer_mut(code_pointer).unwrap()["context_uid"] =
            serde_json::json!("not-a-uid");
        assert!(validate(&invalid_uid).is_err());

        let mut incomplete_extension = document;
        incomplete_extension.pointer_mut(code_pointer).unwrap()["context_group_extension"] =
            serde_json::json!(true);
        assert!(validate(&incomplete_extension).is_err());
    }
}

#[test]
fn conversion_schema_keeps_success_fields_out_of_error_reports() {
    let report = serde_json::json!({
        "schema": "conversion-report-v1",
        "schema_version": 1,
        "status": "error",
        "operation": "convert-raster",
        "implementation": {"name": "dicom-viewer-rust", "version": "0.1.0"},
        "losses": [],
        "error": {"code": "CONVERSION_FAILED", "message": "invalid raster"}
    });

    assert!(validate_conversion_report(&report).is_err());
}

#[test]
fn conversion_schema_requires_complete_concatenation_identity() {
    let mut report: Value =
        serde_json::from_str(include_str!("../examples/conversion-report-v1.json")).unwrap();
    report["operation"] = serde_json::json!("convert-raster");
    report["outputs"][0]["target"] = serde_json::json!("pm");
    report["outputs"][0]["frame_offset"] = serde_json::json!(0);
    report["outputs"][0]["frame_count"] = serde_json::json!(1);
    report["outputs"][0]["pixel_value_bytes"] = serde_json::json!(64);
    report["outputs"][0]["pixel_sha256"] =
        serde_json::json!("5555555555555555555555555555555555555555555555555555555555555555");
    report["outputs"][0]["concatenation_uid"] = serde_json::json!("2.25.200");
    report["target_coverage"] = serde_json::json!([{
        "target": "pm",
        "frame_count": 1,
        "channel_count": 1
    }]);

    assert!(validate_conversion_report(&report).is_err());
}

#[test]
fn tiled_manifest_schema_rejects_escaping_paths_and_incomplete_valid_region_crop() {
    let mut manifest: Value =
        serde_json::from_str(include_str!("../examples/tiled-manifest-v1.json")).unwrap();
    manifest["tiles"][0]["path"] = serde_json::json!("../outside.npy");
    assert!(validate_tiled_manifest(&manifest).is_err());

    manifest = serde_json::from_str(include_str!("../examples/tiled-manifest-v1.json")).unwrap();
    manifest["overlap_policy"] = serde_json::json!("valid-region-crop");
    assert!(validate_tiled_manifest(&manifest).is_err());
}