use oa_core::{language, persistence, semantic::*, source_project::*, *}; use std::collections::BTreeMap; fn id(n: u128) -> Id { Id::from_u128(0x40000000_0000_4000_8000_000000000000 + n) } fn reviewed(oa: T, sa: T) -> Reviewed { Reviewed { oa: Observed::Value(oa), sa: Observed::Value(sa), } } fn project(facets: Vec) -> Project { let mut project = persistence::decode(include_bytes!("fixtures/v4-reviewed-prose.json")).unwrap(); project .traces .get_mut(&id(4)) .unwrap() .baseline .as_mut() .unwrap() .semantic = Some(SemanticBaseline { facets }); project } fn facets(project: &mut Project) -> &mut Vec { &mut project .traces .get_mut(&id(4)) .unwrap() .baseline .as_mut() .unwrap() .semantic .as_mut() .unwrap() .facets } fn compile(source: String) -> CompiledSourceProject { compile_bundle( &SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([("project.arcadia".into(), source)]), }, ProjectLimits::default(), ) .unwrap() } #[test] fn typed_historical_facets_roundtrip_deleted_references_and_all_leaf_categories() { let row = MessageValue { path: vec![id(91), id(92)], field: id(93), expression: "x + 1".into(), }; let model = project(vec![ FacetReview::Data(DataReview::Constructor(reviewed( DataConstructor::Integer, DataConstructor::Real, ))), FacetReview::Data(DataReview::IntegerMinimum(Reviewed { oa: Observed::Value(None), sa: Observed::NotApplicable, })), FacetReview::Data(DataReview::RealMaximum(Reviewed { oa: Observed::NotApplicable, sa: Observed::Value(Some(1.5)), })), FacetReview::Field(FieldReview::Type(reviewed( TypeRef::Named(id(90)), TypeRef::Boolean, ))), FacetReview::Field(FieldReview::Default(reviewed( Some(vec![DataValue::String(id(90).to_string())]), None, ))), FacetReview::Member { collection: ReferenceCollection::Steps, keys: KeyPair { oa: Some(id(91)), sa: None, }, review: reviewed(true, false), }, FacetReview::Order { collection: OrderedCollection::Steps, review: reviewed(vec![id(91), id(92)], vec![]), }, FacetReview::Annotation { keys: KeyPair { oa: Some(AnnotationKey::from(&row)), sa: None, }, review: reviewed(Some(row), None), }, FacetReview::Binding { keys: KeyPair { oa: Some(id(94)), sa: None, }, review: reviewed( Some(LifelineBinding { referenced: id(94), local: id(95), }), None, ), }, FacetReview::Literal { keys: KeyPair { oa: Some(id(90).to_string()), sa: None, }, review: reviewed(true, false), }, FacetReview::Artifact { keys: KeyPair { oa: Some(id(96)), sa: None, }, review: ArtifactReview::Relation(reviewed( ArtifactRelation::References, ArtifactRelation::References, )), }, ]); assert!(!model.contains(id(90))); let source = language::format(&model); assert!(source.starts_with("arcadia 5;")); assert!( source.contains(&format!("Named uuid \"{}\"", id(90))), "{source}" ); assert!(source.contains("sa NotApplicable;")); assert!(source.contains("Value none;")); assert!(source.contains(&format!("String \"{}\"", id(90)))); assert_eq!(language::parse(&source).unwrap(), model); assert_eq!(compile(source).decoded.project, model); assert_eq!( persistence::decode(&serde_json::to_vec(&model).unwrap()).unwrap(), model ); } #[test] fn historical_uuid_and_enum_tokens_are_contextual_and_text_remains_quoted() { let model = project(vec![ FacetReview::Field(FieldReview::Type(reviewed( TypeRef::Named(id(2)), TypeRef::Boolean, ))), FacetReview::Data(DataReview::Unit(reviewed(id(2).to_string(), "m".into()))), ]); let source = language::format(&model); let reference = format!("Named uuid \"{}\"", id(2)); for replacement in [format!("Named \"{}\"", id(2)), "Named @oa_dimension".into()] { let invalid = source.replace(&reference, &replacement); assert!(language::parse(&invalid).is_err(), "{invalid}"); assert!( compile_bundle( &SourceBundle { entry: "p.arcadia".into(), files: BTreeMap::from([("p.arcadia".into(), invalid)]) }, ProjectLimits::default() ) .is_err() ); } let invalid_text = source.replace("Value \"m\";", &format!("Value uuid \"{}\";", id(2))); assert!(language::parse(&invalid_text).is_err()); let invalid_enum = source.replace("Value Boolean;", "Value \"Boolean\";"); assert!(language::parse(&invalid_enum).is_err()); let invalid_live = source.replace("oa @oa_dimension;", &format!("oa uuid \"{}\";", id(2))); assert!(language::parse(&invalid_live).is_err()); let invalid_prose = source.replace( "name \"Approved OA name\";", &format!("name uuid \"{}\";", id(2)), ); assert!(language::parse(&invalid_prose).is_err()); // `uuid` remains an ordinary legal declaration alias after @. assert!(language::parse(&source.replace("oa_dimension", "uuid")).is_ok()); } #[test] fn semantic_values_remain_closed_typed_records_in_both_formats() { let model = project(vec![FacetReview::Data(DataReview::IntegerMinimum( reviewed(Some(0), None), ))]); let source = language::format(&model); for invalid in [ source.replace("IntegerMinimum", "UnknownFacet"), source.replace("Value 0;", "Value \"0\";"), source.replace("facets [", "unexpected true; facets ["), ] { assert!(language::parse(&invalid).is_err(), "{invalid}"); } let mut json = serde_json::to_value(&model).unwrap(); json["traces"][id(4).to_string()]["baseline"]["semantic"] = serde_json::json!({"facets": [{"Data": {"IntegerMinimum": {"oa": {"Value": "0"}, "sa": {"Value": null}}}}]}); assert!(persistence::decode(&serde_json::to_vec(&json).unwrap()).is_err()); } #[test] fn facet_reordering_and_reidentification_keep_comments_with_typed_row_identity() { let row = MessageValue { path: vec![id(2)], field: id(90), expression: id(2).to_string(), }; let model = project(vec![ FacetReview::Annotation { keys: KeyPair { oa: Some(AnnotationKey::from(&row)), sa: None, }, review: reviewed(Some(row), None), }, FacetReview::Literal { keys: KeyPair { oa: Some(id(2).to_string()), sa: None, }, review: reviewed(true, false), }, FacetReview::Field(FieldReview::Type(reviewed( TypeRef::Named(id(2)), TypeRef::Boolean, ))), ]); let source = language::format(&model) .replace("Annotation {", "Annotation { // annotation rationale") .replace("Literal {", "Literal { // literal rationale") .replace("Named uuid ", "Named uuid // historical type rationale\n"); let current = compile(source); let mut prepared = prepare_command( ¤t.decoded.project, &Command::Reidentify(id(2), id(20)), ) .unwrap(); facets(&mut prepared.project).reverse(); let updated = update_bundle_with_identity_map( ¤t, &prepared.project, &prepared.identity_map, ProjectLimits::default(), ) .unwrap(); let text = &updated.bundle.files["project.arcadia"]; assert_eq!(updated.decoded.project, prepared.project); assert!( text.contains(&format!( "Named uuid // historical type rationale\n\"{}\"", id(20) )), "{text}" ); let type_note = text.find("// historical type rationale").unwrap(); let literal_note = text.find("// literal rationale").unwrap(); let annotation_note = text.find("// annotation rationale").unwrap(); assert!( type_note < literal_note && literal_note < annotation_note, "{text}" ); assert!(text.contains(&format!("expression \"{}\"", id(2)))); let FacetReview::Literal { keys, .. } = &facets(&mut prepared.project)[1] else { panic!() }; assert_eq!( keys.oa, Some(id(2).to_string()), "UUID-shaped literal keys are text" ); assert_eq!( compile_bundle(&updated.bundle, ProjectLimits::default()) .unwrap() .bundle, updated.bundle ); } #[test] fn historical_reference_deletion_does_not_remove_the_review_or_create_a_live_alias() { let mut model = project(vec![FacetReview::Field(FieldReview::Type(reviewed( TypeRef::Named(id(90)), TypeRef::Boolean, )))]); let mut former_type = model.elements[&id(2)].clone(); former_type.id = id(90); former_type.alias = "former_type".into(); model.elements.insert(id(90), former_type); let current = compile(language::format(&model)); let prepared = prepare_command(¤t.decoded.project, &Command::Delete(id(90))).unwrap(); let updated = update_bundle_with_identity_map( ¤t, &prepared.project, &prepared.identity_map, ProjectLimits::default(), ) .unwrap(); let source = &updated.bundle.files["project.arcadia"]; assert!(!updated.decoded.project.contains(id(90))); assert!(!source.contains("@former_type")); assert!(source.contains(&format!("Named uuid \"{}\"", id(90)))); assert_eq!(updated.decoded.project.traces, model.traces); assert_eq!(language::parse(source).unwrap(), prepared.project); } #[test] fn included_historical_review_edits_save_and_reopen_without_rewriting_other_modules() { use oa_core::source_files::{load_project, save_project}; let model = project(vec![FacetReview::Field(FieldReview::Type(reviewed( TypeRef::Named(id(90)), TypeRef::Boolean, )))]); let source = language::format(&model); let trace_at = source.find(" trace ").unwrap(); let end = source.rfind('}').unwrap(); let entry = format!( "{} include \"reviews.arcadia\";\n}}\n", &source[..trace_at] ); let review = source[trace_at..end].replace("Named uuid ", "Named uuid // deleted type retained\n"); let directory = tempfile::tempdir().unwrap(); let entry_path = directory.path().join("project.arcadia"); std::fs::write(&entry_path, &entry).unwrap(); std::fs::write(directory.path().join("reviews.arcadia"), &review).unwrap(); let snapshot = load_project(&entry_path, ProjectLimits::default()).unwrap(); let compiled = compile_bundle(&snapshot.reachable_bundle(), ProjectLimits::default()).unwrap(); assert_eq!(compiled.index.declarations[&id(4)].path, "reviews.arcadia"); let mut changed = compiled.decoded.project.clone(); let FacetReview::Field(FieldReview::Type(value)) = &mut facets(&mut changed)[0] else { panic!() }; value.sa = Observed::Value(TypeRef::String); let updated = update_bundle(&compiled, &changed, ProjectLimits::default()).unwrap(); assert_eq!(updated.bundle.files["project.arcadia"], entry); let saved = save_project(&snapshot, &updated, ProjectLimits::default()).unwrap(); let reopened = load_project(&entry_path, ProjectLimits::default()).unwrap(); assert_eq!(saved.bundle, reopened.bundle); assert_eq!( compile_bundle(&reopened.reachable_bundle(), ProjectLimits::default()) .unwrap() .decoded .project, changed ); assert_eq!(std::fs::read_to_string(&entry_path).unwrap(), entry); assert!(reopened.bundle.files["reviews.arcadia"].contains("uuid // deleted type retained")); assert!(!changed.contains(id(90))); } #[test] fn prepared_semantic_review_writes_back_and_preserves_the_accepted_target_difference() { use oa_core::transitions::{TransitionDirection, TransitionMapping, TransitionRequest}; let model = project(vec![]); let current = compile(language::format(&model).replace( "baseline {", "baseline { // prior prose review is independent", )); let mut transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![id(2)]); transition.mappings.push(TransitionMapping { source: id(2), target: Some(id(3)), target_kind: None, }); let mut request = SemanticTransitionRequest::new(transition); request.reviews.push(SemanticReviewRequest { source: id(2), resolutions: vec![ FacetResolution { facet: FacetSelector::Data(DataFacet::Unit), decision: SemanticDecision::KeepTarget, }, FacetResolution { facet: FacetSelector::Header(HeaderFacet::Name), decision: SemanticDecision::TakeSource, }, ], reset: vec![], }); let plan = plan_semantic_transition(¤t.decoded.project, request).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let prepared = prepare_semantic_transition(¤t.decoded.project, &plan).unwrap(); let updated = update_bundle_with_identity_map( ¤t, &prepared.project, &prepared.identity_map, ProjectLimits::default(), ) .unwrap(); assert_eq!( updated.decoded.project.elements[&id(3)].name, "OA dimension now" ); assert!( matches!(updated.decoded.project.elements[&id(3)].shared(), Some(SharedBody::DataType { unit, .. }) if unit == "ft") ); assert!( updated.bundle.files["project.arcadia"].contains("// prior prose review is independent") ); let baseline = updated.decoded.project.traces[&id(4)] .baseline .as_ref() .unwrap(); assert_eq!(baseline.oa.name, "Approved OA name"); assert!( baseline .semantic .as_ref() .unwrap() .facets .contains(&FacetReview::Data(DataReview::Unit(reviewed( "m".into(), "ft".into() )))) ); assert_eq!( language::parse(&language::format(&updated.decoded.project)).unwrap(), prepared.project ); assert_eq!( persistence::decode(&serde_json::to_vec(&updated.decoded.project).unwrap()).unwrap(), prepared.project ); }