use oa_core::{language, persistence, source_project::*, *}; use serde_json::Value; use std::collections::BTreeMap; const JSON4: &[u8] = include_bytes!("fixtures/v4-reviewed-prose.json"); const SOURCE4: &str = include_str!("fixtures/v4-reviewed-prose.arcadia"); #[test] fn frozen_v4_json_and_source_preserve_the_old_review_without_fabricating_semantics() { let json = persistence::decode_with_report(JSON4).unwrap(); let source = language::parse_with_report(SOURCE4).unwrap(); assert_eq!(json.project, source.project); for decoded in [json, source] { assert_eq!(decoded.project.schema_version, 5); let report = decoded.migration.unwrap(); assert_eq!((report.from_schema, report.to_schema), (4, 5)); let trace = decoded.project.traces.values().next().unwrap(); let baseline = trace.baseline.as_ref().unwrap(); assert!(baseline.semantic.is_none()); assert_eq!(baseline.oa.name, "Approved OA name"); assert_eq!(baseline.oa.description, "Approved OA\nprose"); assert_eq!(baseline.sa.name, "Approved SA name"); assert_ne!(baseline.oa.name, decoded.project.named(trace.oa)); let mut expected: Value = serde_json::from_slice(JSON4).unwrap(); expected["schema_version"] = 5.into(); assert_eq!(serde_json::to_value(&decoded.project).unwrap(), expected); let reopened = persistence::decode_with_report(&serde_json::to_vec(&decoded.project).unwrap()) .unwrap(); assert!(reopened.migration.is_none()); assert_eq!(reopened.project, decoded.project); } } #[test] fn older_headers_cannot_admit_new_semantic_baselines() { let mut value: Value = serde_json::from_slice(JSON4).unwrap(); let trace = value["traces"] .as_object_mut() .unwrap() .values_mut() .next() .unwrap(); trace["baseline"]["semantic"] = serde_json::json!({"facets": []}); for version in [3, 4] { value["schema_version"] = version.into(); let error = persistence::decode(&serde_json::to_vec(&value).unwrap()) .unwrap_err() .to_string(); assert!(error.contains("semantic"), "{error}"); } let source = SOURCE4.replace("baseline {", "baseline { semantic { facets []; };"); assert!( language::parse(&source) .unwrap_err() .iter() .any(|d| d.message.contains("require schema/source5")) ); let bundle = SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([("project.arcadia".into(), source)]), }; assert!( compile_bundle(&bundle, ProjectLimits::default()) .unwrap_err() .iter() .any(|d| d.diagnostic.message.contains("require schema/source5")) ); } #[test] fn frozen_v4_trace_envelope_keeps_duplicate_and_unknown_field_rejection() { let json = std::str::from_utf8(JSON4).unwrap(); let duplicate = json.replace("\"baseline\": {", "\"baseline\": null, \"baseline\": {"); assert!( persistence::decode(duplicate.as_bytes()) .unwrap_err() .to_string() .contains("duplicate field") ); let unknown = json.replace("\"baseline\": {", "\"baseline\": { \"reviewed\": true,"); assert!( persistence::decode(unknown.as_bytes()) .unwrap_err() .to_string() .contains("unknown field") ); } #[test] fn authored_v4_modules_upgrade_only_the_version_and_preserve_nested_comments() { let start = SOURCE4.find(" oa_data_type").unwrap(); let end = SOURCE4.rfind('}').unwrap(); let entry = format!( "{} include \"contracts.arcadia\";\n}}\n", &SOURCE4[..start] ); let mut module = SOURCE4[start..end].replace("baseline {", "baseline { // original review rationale"); module.push_str( r#" oa_data_type record "Independent record" id "40000000-0000-4000-8000-000000000011" { definition { Record { supertype none; }; }; // This field remains inside its authored record. oa_data_field flag "Flag" id "40000000-0000-4000-8000-000000000012" { type_ref Boolean; } } "#, ); let bundle = SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([ ("project.arcadia".into(), entry.clone()), ("contracts.arcadia".into(), module.clone()), ]), }; let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap(); assert_eq!(compiled.bundle.files["contracts.arcadia"], module); assert_eq!( compiled.bundle.files["project.arcadia"], entry.replacen("arcadia 4;", "arcadia 5;", 1) ); assert_eq!( compiled.index.lexical_owners [&Id::parse_str("40000000-0000-4000-8000-000000000012").unwrap()], Id::parse_str("40000000-0000-4000-8000-000000000011").unwrap() ); let reopened = compile_bundle(&compiled.bundle, ProjectLimits::default()).unwrap(); assert!(reopened.decoded.migration.is_none()); assert_eq!(reopened.bundle, compiled.bundle); assert_eq!(reopened.decoded.project, compiled.decoded.project); assert_eq!( bundle.files["project.arcadia"], entry, "caller draft is unchanged" ); } #[test] fn original_v1_v2_v3_inputs_complete_the_explicit_upgrade_to_five() { for (version, bytes) in [ ( 1, include_bytes!("../../../examples/legacy-v1-coastal-rescue.oa.json").as_slice(), ), ( 2, include_bytes!("fixtures/v2-coastal-rescue.json").as_slice(), ), ( 3, include_bytes!("fixtures/v3-data-contract.json").as_slice(), ), ] { let decoded = persistence::decode_with_report(bytes).unwrap(); let report = decoded.migration.unwrap(); assert_eq!((report.from_schema, report.to_schema), (version, 5)); assert_eq!(decoded.project.schema_version, 5); assert!( decoded .project .traces .values() .all(|t| t.baseline.as_ref().is_none_or(|b| b.semantic.is_none())) ); assert_eq!( language::parse(&language::format(&decoded.project)).unwrap(), decoded.project ); } }