use oa_core::{language, persistence, *}; use serde_json::{Value, json}; const SOURCE3: &str = include_str!("fixtures/v3-data-contract.arcadia"); fn id(n: u128) -> Id { Id::from_u128(n) } // Frozen JSON uses the same authored schema3 contract as the source fixture; // this helper never serializes current defaults under version3. fn original3() -> Value { serde_json::from_str(include_str!("fixtures/v3-data-contract.json")).unwrap() } #[test] fn genuine_v3_preserves_typed_ports_and_allocates_deterministic_relationships() { let old = original3(); let decoded = persistence::decode_with_report(&serde_json::to_vec(&old).unwrap()).unwrap(); let source = language::parse_with_report(SOURCE3).unwrap(); assert_eq!(decoded.project, source.project); assert_eq!(decoded.migration, source.migration); let project = &decoded.project; let report = decoded.migration.as_ref().unwrap(); assert_eq!( (report.from_schema, report.to_schema), (3, oa_core::SCHEMA_VERSION) ); assert_eq!(project.identities().len(), 19); assert!(report.preserved_ports.is_empty()); assert!(report.generated_endpoints.is_empty()); assert_eq!(report.generated_interface_allocations.len(), 2); for allocation in &report.generated_interface_allocations { assert_eq!( allocation.id, Id::new_v5( &id(1), format!( "v2-interface-item:{}:{}", allocation.source, allocation.target ) .as_bytes() ) ); assert_eq!(allocation.target, id(4)); assert_eq!(allocation.order, 0); assert!(matches!( project.relations[&allocation.id].body, RelationBody::InterfaceItemAllocation { transmission: TransmissionProtocol::Unspecified, acquisition: AcquisitionProtocol::Unspecified, .. } )); } let now = serde_json::to_value(project).unwrap(); for port in [id(11), id(12)] { assert_eq!( now["elements"][port.to_string()], old["elements"][port.to_string()] ); } assert!(matches!( project.elements[&id(4)].shared(), Some(SharedBody::ExchangeItem { kind: ExchangeItemKind::Information, mechanism: ExchangeMechanism::Event, supertype: None }) )); assert_eq!(report.unresolved_message_values.len(), 1); assert!( validation::validate(project) .iter() .any(|issue| issue.code == "message_value_ambiguous_path") ); let canonical = language::format(project); assert!(canonical.starts_with(&format!("arcadia {SCHEMA_VERSION};"))); let reopened = language::parse_with_report(&canonical).unwrap(); assert_eq!(&reopened.project, project); assert!(reopened.migration.is_none()); assert_eq!(canonical, language::format(&reopened.project)); } #[test] fn v3_changed_payloads_reject_schema4_fields_and_new_constructor_names() { let mut old = original3(); old["elements"][id(4).to_string()]["body"]["Sa"]["Shared"]["ExchangeItem"]["mechanism"] = json!("Flow"); assert!(persistence::decode(&serde_json::to_vec(&old).unwrap()).is_err()); let mut old = original3(); old["elements"][id(5).to_string()]["body"]["Sa"]["Shared"]["DataType"]["definition"] = json!({"Record":{"supertype":null}}); assert!(persistence::decode(&serde_json::to_vec(&old).unwrap()).is_err()); let mut old = original3(); old["elements"][id(17).to_string()]["body"]["Sa"]["Shared"]["Occurrence"]["body"]["Exchange"] ["values"][0]["path"] = json!([]); assert!(persistence::decode(&serde_json::to_vec(&old).unwrap()).is_err()); let source = SOURCE3.replace("kind Event;", "kind Information; mechanism Flow;"); assert!(language::parse(&source).is_err()); let source = SOURCE3.replace( "functional_exchange position_exchange", "interface_item_allocation position_exchange", ); assert!(language::parse(&source).is_err()); } #[test] fn deterministic_allocation_id_collision_is_located_without_overwriting_any_object() { let mut old = original3(); let collision = Id::new_v5( &id(1), format!("v2-interface-item:{}:{}", id(9), id(4)).as_bytes(), ); old["artifact_links"][collision.to_string()] = json!({"id":collision,"alias":"retained_artifact","name":"Retain","description":"","owner":id(1),"relation":"References","uri":"docs/contract.md","revision":null,"fragment":null}); let error = persistence::decode(&serde_json::to_vec(&old).unwrap()).unwrap_err(); match error { persistence::FileError::Model { issues, .. } => { assert_eq!(issues[0].code, "identity_collision"); assert_eq!(issues[0].element, Some(id(9))); assert_eq!(issues[0].field.as_deref(), Some("items")); } other => panic!("unexpected error {other}"), } } #[test] fn unique_legacy_nested_value_is_qualified_and_physical_nature_is_not_guessed() { let source = SOURCE3.replace("kind Event;", "kind Material;").replace( "type_ref { Named @coordinate; }; }\n }", "type_ref String; }\n }", ); let decoded = language::parse_with_report(&source).unwrap(); assert!(matches!( decoded.project.elements[&id(4)].shared(), Some(SharedBody::ExchangeItem { kind: ExchangeItemKind::Material, mechanism: ExchangeMechanism::Unspecified, .. }) )); let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) = decoded.project.elements[&id(17)].shared() else { panic!() }; assert_eq!(values[0].path, vec![id(7)]); assert_eq!(values[0].expression, "navigation.latitude"); } #[test] fn v1_and_frozen_v2_inputs_reach_four_through_the_shared_data_upgrade() { let v2 = persistence::decode_with_report(include_bytes!("fixtures/v2-coastal-rescue.json")).unwrap(); let text = language::parse_with_report(include_str!("fixtures/v2-coastal-rescue.arcadia")).unwrap(); assert_eq!(v2.project, text.project); assert_eq!(v2.migration, text.migration); assert_eq!(v2.migration.unwrap().to_schema, oa_core::SCHEMA_VERSION); let v1 = language::parse_with_report( "arcadia 1; project \"Old\" id \"00000000-0000-0000-0000-000000000001\" {}", ) .unwrap(); assert_eq!(v1.project.schema_version, oa_core::SCHEMA_VERSION); assert_eq!(v1.migration.unwrap().from_schema, 1); } #[test] fn allocation_alias_collisions_are_stable_and_duplicate_legacy_payload_fields_reject() { let mut old = original3(); let first = persistence::decode_with_report(&serde_json::to_vec(&old).unwrap()).unwrap(); let alias = first.migration.unwrap().generated_interface_allocations[0] .alias .clone(); old["artifact_links"][id(100).to_string()] = json!({"id":id(100),"alias":alias,"name":"Existing alias","description":"","owner":id(1),"relation":"References","uri":"docs/contract.md","revision":null,"fragment":null}); let bytes = serde_json::to_vec(&old).unwrap(); let migrated = persistence::decode_with_report(&bytes).unwrap(); let generated = &migrated .migration .as_ref() .unwrap() .generated_interface_allocations[0]; assert_ne!(generated.alias, alias); assert!(generated.alias.starts_with(&alias)); assert_eq!( migrated.project, persistence::decode_with_report(&bytes).unwrap().project ); let duplicate = include_str!("fixtures/v3-data-contract.json").replace( "\"kind\": \"Event\"", "\"kind\": \"Event\", \"kind\": \"Material\"", ); assert!( persistence::decode(duplicate.as_bytes()) .unwrap_err() .to_string() .contains("Duplicate field 'kind'") ); } #[test] fn recursive_legacy_record_paths_remain_ambiguous_instead_of_becoming_one_argument() { let mut old = original3(); old["elements"][id(8).to_string()]["body"]["Sa"]["Shared"]["DataField"]["type_ref"] = json!("String"); old["elements"][id(100).to_string()] = json!({"id":id(100),"alias":"next_coordinate","name":"Next coordinate","description":"","parent":id(5),"body":{"Sa":{"Shared":{"DataField":{"type_ref":{"Named":id(5)},"minimum":0,"maximum":1,"unit":""}}}}}); let decoded = persistence::decode_with_report(&serde_json::to_vec(&old).unwrap()).unwrap(); assert_eq!( decoded .migration .as_ref() .unwrap() .unresolved_message_values .len(), 1 ); let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) = decoded.project.elements[&id(17)].shared() else { panic!() }; assert!(values[0].path.is_empty()); assert!( validation::validate(&decoded.project) .iter() .any(|issue| issue.code == "message_value_ambiguous_path") ); }