use oa_core::{ Id, PortContract, Project, SharedBody, language::{self, TokenClass}, source_project::SourceBundle, }; use source_authoring_experiment::{compile, writeback::writeback}; use std::collections::BTreeMap; fn id(project: &Project, alias: &str) -> Id { project .identities() .into_iter() .find(|(_, a)| *a == alias) .unwrap() .0 } fn fragments(source: &str, path: &str, extras: BTreeMap) -> SourceBundle { let start = source.find('{').unwrap() + 1; let end = source.rfind('}').unwrap(); let mut files = extras; files.insert(path.into(), source[start..end].into()); let includes = files .keys() .map(|path| format!(" include \"{path}\";\n")) .collect::(); files.insert( "project.arcadia".into(), format!("{}\n{includes}}}\n", &source[..start]), ); SourceBundle { entry: "project.arcadia".into(), files, } } fn sa() -> SourceBundle { let source=include_str!("../fixtures/sa-b.arcadia") .replace(" sa_function send_report", " // Keep reporting allocation explicit; this note belongs to the function.\n sa_function send_report") .replace(" output report_out", " // This note belongs to the payload port.\n output report_out") .replace(" sa_function assess", " // Assessment is independently owned.\n sa_function assess") .replace(" output report_out \"Report output\" id \"00000000-0000-4000-8000-000000000208\" items [@report_data];", " output report_out \"Report output\" id \"00000000-0000-4000-8000-000000000208\" items [@report_data]; // Primary payload\n output diagnostic_out \"Diagnostic output\" id \"00000000-0000-4000-8000-000000000222\" items [\n @report_data // Keep this contract rationale\n ];"); fragments(&source,"domain/system.arcadia",BTreeMap::from([ ("domain/other.arcadia".into(),"// Other analysis aggregate\nsa_function alternate \"Alternate analysis\" id \"00000000-0000-4000-8000-000000000223\" {\n description \"A separate analysis context\";\n}\nsa_exchange_item location_data \"Location data\" id \"00000000-0000-4000-8000-000000000224\" {}\n".into()), ("views/reporting.arcadia".into(),r#"// View rationale survives a geometry edit. diagram reporting_view "Reporting view" id "00000000-0000-4000-8000-000000000220" { layer Sa; projection FunctionalDataflow; } node send_placement id "00000000-0000-4000-8000-000000000221" { diagram @reporting_view; element @send_report; position { y 32.5; // Y rationale x -0.0; }; // Preserve the author's field order size { width 200.0; height 100.0; }; collapsed false; } "#.into()), ])) } fn scenario() -> SourceBundle { let source=include_str!("../fixtures/scenario-b.arcadia") .replace(" oa_occurrence initial_report", " // Initial request retains its call identity.\n oa_occurrence initial_report") .replace(" oa_occurrence accepted", " // Acknowledgement rationale.\n oa_occurrence accepted") .replace(" oa_occurrence update_report", " // Newer position update is its own occurrence.\n oa_occurrence update_report") .replace(" precondition", " // This precondition travels with the update.\n precondition"); fragments( &source, "behavior/reports.arcadia", BTreeMap::from([( "unrelated.arcadia".into(), "// This file intentionally contains only narrative comments.\n".into(), )]), ) } fn comment_strings(bundle: &SourceBundle) -> Vec { let mut comments = bundle .files .values() .flat_map(|source| { language::tokenize(source) .into_iter() .filter(|token| token.class == TokenClass::Comment) .map(|token| source[token.span.start..token.span.end].to_owned()) }) .collect::>(); comments.sort(); comments } fn verify(bundle: &SourceBundle, expected: &Project) -> SourceBundle { let original = bundle.clone(); let output = writeback(bundle, expected).unwrap(); assert_eq!(*bundle, original, "Caller input changed"); assert_eq!(compile(&output).unwrap(), *expected); assert_eq!(comment_strings(&output), comment_strings(bundle)); assert_eq!( writeback(&output, expected).unwrap(), output, "Repeated writeback churned source" ); output } fn changed_files(before: &SourceBundle, after: &SourceBundle) -> Vec { before .files .iter() .filter(|(path, text)| after.files.get(*path) != Some(*text)) .map(|(path, _)| path.clone()) .collect() } #[test] fn no_op_and_owner_rename_preserve_authored_structure_comments_and_other_files() { let mut bundle = sa(); // Equivalent but noncanonical lexical forms must survive an unrelated edit. *bundle.files.get_mut("domain/other.arcadia").unwrap() = bundle.files["domain/other.arcadia"] .replace("Alternate analysis", r"Alternate \u0061nalysis"); let old = compile(&bundle).unwrap(); assert_eq!(writeback(&bundle, &old).unwrap(), bundle); let mut candidate = old.clone(); candidate .elements .get_mut(&id(&old, "manage")) .unwrap() .name = "Manage emergency reporting".into(); let output = verify(&bundle, &candidate); assert_eq!(changed_files(&bundle, &output), ["domain/system.arcadia"]); assert_eq!( output.files["domain/system.arcadia"], bundle.files["domain/system.arcadia"] .replace("\"Manage reporting\"", "\"Manage emergency reporting\"") ); } #[test] fn owner_alias_rename_updates_shorthand_and_cross_file_references_only() { let bundle = sa(); let old = compile(&bundle).unwrap(); let mut candidate = old.clone(); candidate .elements .get_mut(&id(&old, "send_report")) .unwrap() .alias = "transmit_report".into(); let output = verify(&bundle, &candidate); assert_eq!( changed_files(&bundle, &output), ["domain/system.arcadia", "views/reporting.arcadia"] ); for path in &changed_files(&bundle, &output) { assert_eq!( output.files[path], bundle.files[path].replace("send_report", "transmit_report") ); } } #[test] fn simultaneous_alias_swap_preserves_identity_and_does_not_rewrite_comments_or_strings() { let mut bundle = sa(); *bundle.files.get_mut("domain/other.arcadia").unwrap() = bundle.files["domain/other.arcadia"] .replace( "A separate analysis context", "@send_report and @assess are prose, not references", ); let old = compile(&bundle).unwrap(); let mut candidate = old.clone(); candidate .elements .get_mut(&id(&old, "send_report")) .unwrap() .alias = "assess".into(); candidate .elements .get_mut(&id(&old, "assess")) .unwrap() .alias = "send_report".into(); let output = verify(&bundle, &candidate); assert_eq!( output.files["domain/other.arcadia"], bundle.files["domain/other.arcadia"] ); assert_eq!(old.relations, candidate.relations); } #[test] fn raw_component_contract_edit_preserves_nested_comments_and_authored_field_order() { let mut bundle = sa(); *bundle.files.get_mut("domain/system.arcadia").unwrap() = bundle.files["domain/system.arcadia"] .replace( "kind Flow; orientation Out;", "orientation Out; // Keep orientation reasoning\nkind Flow;", ); let old = compile(&bundle).unwrap(); let mut candidate = old.clone(); if let Some(SharedBody::Port { contract: PortContract::Component { orientation, .. }, .. }) = candidate .elements .get_mut(&id(&old, "vessel_out")) .unwrap() .shared_mut() { *orientation = oa_core::ComponentPortOrientation::InOut; } let output = verify(&bundle, &candidate); assert_eq!(changed_files(&bundle, &output), ["domain/system.arcadia"]); assert_eq!( output.files["domain/system.arcadia"], bundle.files["domain/system.arcadia"].replace("orientation Out;", "orientation InOut;") ); } #[test] fn reparent_and_rename_in_one_model_transaction_absorb_only_the_moved_subtree_edits() { let bundle = sa(); let old = compile(&bundle).unwrap(); let mut moved = old.elements[&id(&old, "send_report")].clone(); moved.name = "Transmit emergency report".into(); moved.parent = Some(id(&old, "alternate")); let candidate = oa_core::execute_batch(&old, &[oa_core::Command::EditElement(moved.id, moved)]).unwrap(); let output = verify(&bundle, &candidate); assert!(output.files["domain/other.arcadia"].contains("\"Transmit emergency report\"")); assert_eq!( output.files["views/reporting.arcadia"], bundle.files["views/reporting.arcadia"] ); } #[test] fn function_port_direction_and_item_addition_keep_shorthand_and_inline_rationale() { let bundle = sa(); let old = compile(&bundle).unwrap(); let mut candidate = old.clone(); let port = id(&old, "diagnostic_out"); let payload = id(&old, "report_data"); let location = id(&old, "location_data"); if let Some(SharedBody::Port { contract, .. }) = candidate.elements.get_mut(&port).unwrap().shared_mut() { *contract = PortContract::FunctionInput { incoming_items: vec![payload, location], }; } let output = verify(&bundle, &candidate); assert_eq!(changed_files(&bundle, &output), ["domain/system.arcadia"]); let text = &output.files["domain/system.arcadia"]; assert!(text.contains("input diagnostic_out")); assert!(!text.contains("contract { FunctionInput")); assert!(text.contains("@report_data, // Keep this contract rationale")); assert!(text.contains("@location_data")); let untouched_prefix = bundle.files["domain/system.arcadia"] .split("output diagnostic_out") .next() .unwrap(); assert!(text.starts_with(untouched_prefix)); } #[test] fn widening_an_input_contract_does_not_reformat_its_existing_exchange() { let bundle = sa(); let old = compile(&bundle).unwrap(); let mut candidate = old.clone(); // Increasing the accepted input contract does not change exchange semantics. let input = id(&old, "report_in"); let payload = id(&old, "report_data"); let location = id(&old, "location_data"); if let Some(SharedBody::Port { contract, .. }) = candidate.elements.get_mut(&input).unwrap().shared_mut() { *contract = PortContract::FunctionInput { incoming_items: vec![payload, location], }; } let output = verify(&bundle, &candidate); let before_exchange = bundle.files["domain/system.arcadia"] .lines() .find(|line| line.contains("functional_exchange report_flow")) .unwrap(); assert!( output.files["domain/system.arcadia"] .lines() .any(|line| line == before_exchange) ); } #[test] fn reordering_occurrences_moves_their_rationale_and_retains_step_blocks() { let bundle = scenario(); let old = compile(&bundle).unwrap(); let mut candidate = old.clone(); candidate .elements .get_mut(&id(&old, "reports")) .unwrap() .steps_mut() .unwrap() .rotate_right(1); let output = verify(&bundle, &candidate); assert_eq!( changed_files(&bundle, &output), ["behavior/reports.arcadia"] ); let source = &output.files["behavior/reports.arcadia"]; assert!(source.contains("steps {")); assert!(!source.contains("steps [")); assert!( source.find("// Newer position update").unwrap() < source.find("// Initial request").unwrap() ); assert!( source.find("// This precondition travels").unwrap() > source.find("oa_occurrence update_report").unwrap() ); assert_eq!(old.relations, candidate.relations); } #[test] fn adding_an_occurrence_uses_its_new_identity_and_preserves_existing_blocks() { let bundle = scenario(); let old = compile(&bundle).unwrap(); let mut candidate = old.clone(); let owner = id(&old, "reports"); let mut occurrence = candidate.elements[&id(&old, "update_report")].clone(); occurrence.id = "00000000-0000-4000-8000-000000000316".parse().unwrap(); occurrence.alias = "further_report".into(); occurrence.name = "Further report".into(); candidate .elements .get_mut(&owner) .unwrap() .steps_mut() .unwrap() .push(occurrence.id); candidate.elements.insert(occurrence.id, occurrence); let output = verify(&bundle, &candidate); assert_eq!( changed_files(&bundle, &output), ["behavior/reports.arcadia"] ); let source = &output.files["behavior/reports.arcadia"]; assert!(source.contains("oa_occurrence further_report")); assert!(!source.contains("parent @reports")); assert!(source.contains("steps {")); let original = &bundle.files["behavior/reports.arcadia"]; let before_close = original.rfind(" };").unwrap(); assert!(source.starts_with(&original[..before_close])); } #[test] fn semantic_reparent_moves_nested_function_ports_and_comments_across_files() { let bundle = sa(); let old = compile(&bundle).unwrap(); let mut candidate = old.clone(); candidate .elements .get_mut(&id(&old, "send_report")) .unwrap() .parent = Some(id(&old, "alternate")); let output = verify(&bundle, &candidate); assert_eq!( changed_files(&bundle, &output), ["domain/other.arcadia", "domain/system.arcadia"] ); assert!(!output.files["domain/system.arcadia"].contains("sa_function send_report")); assert!( output.files["domain/other.arcadia"].contains(" // Keep reporting allocation explicit") ); assert!(output.files["domain/other.arcadia"].contains(" sa_function send_report")); assert!(output.files["domain/other.arcadia"].contains(" output report_out")); assert_eq!( old.relations, candidate.relations, "Reparenting changed explicit allocations" ); } #[test] fn diagram_geometry_and_existing_a_fields_preserve_comments_and_field_order() { let bundle = sa(); let old = compile(&bundle).unwrap(); let diagram = id(&old, "reporting_view"); let node = id(&old, "send_placement"); let candidate = oa_core::execute_batch( &old, &[oa_core::Command::MoveNode { diagram, node, position: oa_core::Position { x: 72.25, y: 99.5 }, with_descendants: false, }], ) .unwrap(); let output = verify(&bundle, &candidate); assert_eq!(changed_files(&bundle, &output), ["views/reporting.arcadia"]); assert_eq!( output.files["views/reporting.arcadia"], bundle.files["views/reporting.arcadia"] .replace("y 32.5", "y 99.5") .replace("x -0.0", "x 72.25") ); } #[test] fn raw_a_parent_changes_preserve_file_organization_and_declaration_comments() { let source = include_str!("../fixtures/oa-a.arcadia").replace( " oa_activity report", " // Reparent the activity, keep this explanation.\n oa_activity report", ); let bundle = fragments(&source, "operations.arcadia", BTreeMap::new()); let old = compile(&bundle).unwrap(); let mut candidate = old.clone(); candidate .elements .get_mut(&id(&old, "report")) .unwrap() .parent = None; let output = verify(&bundle, &candidate); assert_eq!( output.files["operations.arcadia"], bundle.files["operations.arcadia"].replacen("parent @respond", "parent none", 1) ); } #[test] fn ambiguous_comment_ownership_and_invalid_candidates_fail_without_mutation() { let mut bundle = scenario(); *bundle.files.get_mut("behavior/reports.arcadia").unwrap()=bundle.files["behavior/reports.arcadia"].replace(" // Acknowledgement rationale.","\n // Unattached group rationale.\n\n // Acknowledgement rationale."); let original = bundle.clone(); let old = compile(&bundle).unwrap(); let mut candidate = old.clone(); candidate .elements .get_mut(&id(&old, "reports")) .unwrap() .steps_mut() .unwrap() .rotate_right(1); assert!( writeback(&bundle, &candidate) .unwrap_err() .contains("Unattached comments") ); assert_eq!(bundle, original); let bundle = sa(); let original = bundle.clone(); let old = compile(&bundle).unwrap(); let mut candidate = old.clone(); candidate .elements .get_mut(&id(&old, "send_report")) .unwrap() .parent = Some(id(&old, "center")); assert!( writeback(&bundle, &candidate) .unwrap_err() .contains("Invalid candidate") ); assert_eq!(bundle, original); }