use oa_core::{source_files::*, source_project::*}; use std::{collections::BTreeMap, fs}; fn setup() -> tempfile::TempDir { let directory = tempfile::tempdir().unwrap(); fs::write( directory.path().join("project.arcadia"), "arcadia 5; project p \"P\" { include \"parts/a.arcadia\"; }\n", ) .unwrap(); fs::create_dir(directory.path().join("parts")).unwrap(); fs::write( directory.path().join("parts/a.arcadia"), "// rationale\noa_activity a \"A\" {}\n", ) .unwrap(); directory } #[test] fn loading_keeps_original_no_uuid_bytes_and_saving_stabilizes_exactly_once() { let directory = setup(); let entry = directory.path().join("project.arcadia"); let snapshot = load_project(&entry, ProjectLimits::default()).unwrap(); assert!( snapshot .diagnostics .iter() .all(|d| d.diagnostic.severity != oa_core::validation::Severity::Error) ); let compiled = compile_bundle(&snapshot.reachable_bundle(), ProjectLimits::default()).unwrap(); assert_ne!(compiled.bundle, snapshot.bundle); for (path, expected) in &snapshot.expectations { assert_eq!( *expected, DiskExpectation::Bytes(fs::read(directory.path().join(path)).unwrap()) ); } let saved = save_project(&snapshot, &compiled, ProjectLimits::default()).unwrap(); assert_eq!(saved.bundle, compiled.bundle); let reopened = load_project(&entry, ProjectLimits::default()).unwrap(); assert_eq!(reopened.bundle, compiled.bundle); assert_eq!( compile_bundle(&reopened.reachable_bundle(), ProjectLimits::default()) .unwrap() .decoded .project, compiled.decoded.project ); assert_eq!( save_project(&saved, &compiled, ProjectLimits::default()) .unwrap() .bundle, compiled.bundle ); } #[test] fn invalid_and_missing_includes_keep_editable_text_and_located_diagnostics() { let directory = setup(); let entry = directory.path().join("project.arcadia"); fs::write(&entry, "arcadia 5; project p \"P\" {\n include \"missing.arcadia\";\n include \"parts/a.arcadia\";\n}\n").unwrap(); fs::write( directory.path().join("parts/a.arcadia"), "oa_activity a \"A\" { invalid", ) .unwrap(); let snapshot = load_project(&entry, ProjectLimits::default()).unwrap(); assert_eq!(snapshot.bundle.files.len(), 2); assert_eq!( snapshot.expectations["missing.arcadia"], DiskExpectation::Missing ); assert!( snapshot .diagnostics .iter() .any(|d| d.path == "project.arcadia" && d.diagnostic.line == 2 && d.diagnostic.code == "missing_include") ); assert!( snapshot .diagnostics .iter() .any(|d| d.path == "parts/a.arcadia") ); assert!(snapshot.bundle.files["parts/a.arcadia"].ends_with("invalid")); } #[test] fn discovery_never_refreshes_drafts_and_preserves_detached_buffers() { let directory = setup(); let entry = directory.path().join("project.arcadia"); let original = load_project(&entry, ProjectLimits::default()).unwrap(); fs::write(directory.path().join("parts/a.arcadia"), "// external edit").unwrap(); fs::write( directory.path().join("new.arcadia"), "oa_activity b \"B\" {}", ) .unwrap(); let overlays = BTreeMap::from([ ( "project.arcadia".into(), "arcadia 5; project p \"P\" { include \"new.arcadia\"; }".into(), ), ( "parts/a.arcadia".into(), "// unfinished detached draft\noa_activity".into(), ), ]); let discovered = discover_missing(&original, &overlays, ProjectLimits::default()).unwrap(); assert_eq!( discovered.bundle.files["parts/a.arcadia"], overlays["parts/a.arcadia"] ); assert_eq!( discovered.expectations["parts/a.arcadia"], original.expectations["parts/a.arcadia"] ); assert!(discovered.bundle.files.contains_key("new.arcadia")); assert!( !discovered .reachable_bundle() .files .contains_key("parts/a.arcadia") ); assert!( discovered .diagnostics .iter() .all(|d| d.diagnostic.severity != oa_core::validation::Severity::Error) ); } #[test] fn unchanged_dependency_edits_and_deletions_block_save_before_any_write() { for deletion in [false, true] { let directory = setup(); let entry = directory.path().join("project.arcadia"); let snapshot = load_project(&entry, ProjectLimits::default()).unwrap(); let compiled = compile_bundle(&snapshot.reachable_bundle(), ProjectLimits::default()).unwrap(); let snapshot = save_project(&snapshot, &compiled, ProjectLimits::default()).unwrap(); let mut bundle = compiled.bundle.clone(); bundle .files .get_mut("project.arcadia") .unwrap() .push_str("// changed\n"); let changed = compile_bundle(&bundle, ProjectLimits::default()).unwrap(); let child = directory.path().join("parts/a.arcadia"); if deletion { fs::remove_file(&child).unwrap(); } else { fs::write(&child, "// another editor").unwrap(); } assert!(matches!( save_project(&snapshot, &changed, ProjectLimits::default()), Err(SourceFileError::Conflict(_)) )); assert_eq!( fs::read_to_string(&entry).unwrap(), compiled.bundle.files["project.arcadia"] ); } } #[test] fn new_destinations_require_explicit_missing_expectations_and_reject_collisions() { let directory = tempfile::tempdir().unwrap(); let entry = directory.path().join("project.arcadia"); let mut snapshot = new_project(&entry, ProjectLimits::default()).unwrap(); let bundle = SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([ ( "project.arcadia".into(), "arcadia 5; project p \"P\" { include \"child.arcadia\"; }".into(), ), ("child.arcadia".into(), "oa_activity a \"A\" {}".into()), ]), }; let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap(); assert!(matches!( save_project(&snapshot, &compiled, ProjectLimits::default()), Err(SourceFileError::UnknownExpectation(_)) )); expect_new_file(&mut snapshot, "child.arcadia", ProjectLimits::default()).unwrap(); fs::write( directory.path().join("child.arcadia"), "// pre-existing work", ) .unwrap(); assert!(matches!( save_project(&snapshot, &compiled, ProjectLimits::default()), Err(SourceFileError::Conflict(_)) )); assert!(!entry.exists()); assert!(expect_new_file(&mut snapshot, "child.arcadia", ProjectLimits::default()).is_err()); } #[test] fn aggregate_budgets_and_portable_paths_are_checked_before_loading_excess_data() { let directory = setup(); let entry = directory.path().join("project.arcadia"); let mut limits = ProjectLimits { max_files: 1, ..ProjectLimits::default() }; let snapshot = load_project(&entry, limits).unwrap(); assert_eq!(snapshot.bundle.files.len(), 1); assert!( snapshot .diagnostics .iter() .any(|d| d.diagnostic.code == "project_limit") ); limits.max_files = 256; limits.model.max_source_bytes = fs::metadata(&entry).unwrap().len() as usize + 5; let snapshot = load_project(&entry, limits).unwrap(); assert_eq!(snapshot.bundle.files.len(), 1); assert!( snapshot .diagnostics .iter() .any(|d| d.diagnostic.code == "source_limit") ); let mut snapshot = new_project( &directory.path().join("new.arcadia"), ProjectLimits::default(), ) .unwrap(); for invalid in [ "../escape.arcadia", "a\\b.arcadia", "CON.arcadia", "/absolute.arcadia", "a/../b.arcadia", ".arcadia-save-project.arcadia.journal/intrusion.arcadia", ] { assert!( expect_new_file(&mut snapshot, invalid, ProjectLimits::default()).is_err(), "{invalid}" ); } } #[test] fn physical_hard_link_aliases_are_rejected() { let directory = setup(); fs::hard_link( directory.path().join("parts/a.arcadia"), directory.path().join("alias.arcadia"), ) .unwrap(); fs::write( directory.path().join("project.arcadia"), "arcadia 5; project p \"P\" { include \"parts/a.arcadia\"; include \"alias.arcadia\"; }", ) .unwrap(); assert!(matches!( load_project( &directory.path().join("project.arcadia"), ProjectLimits::default() ), Err(SourceFileError::Path(_)) )); } #[cfg(unix)] #[test] fn symlinks_to_outside_or_inside_project_are_rejected() { let directory = setup(); let outside = tempfile::tempdir().unwrap(); fs::write( outside.path().join("outside.arcadia"), "oa_activity outside \"Outside\" {}", ) .unwrap(); for destination in [ outside.path().join("outside.arcadia"), directory.path().join("parts/a.arcadia"), ] { let link = directory.path().join("alias.arcadia"); std::os::unix::fs::symlink(&destination, &link).unwrap(); fs::write( directory.path().join("project.arcadia"), "arcadia 5; project p \"P\" { include \"alias.arcadia\"; }", ) .unwrap(); let loaded = load_project( &directory.path().join("project.arcadia"), ProjectLimits::default(), ) .unwrap(); assert!( loaded .diagnostics .iter() .any(|d| d.diagnostic.code == "invalid_path") ); assert!(!loaded.bundle.files.contains_key("alias.arcadia")); fs::remove_file(&link).unwrap(); } } #[test] fn forged_compiled_model_or_index_cannot_be_saved() { let directory = setup(); let entry = directory.path().join("project.arcadia"); let snapshot = load_project(&entry, ProjectLimits::default()).unwrap(); let mut compiled = compile_bundle(&snapshot.reachable_bundle(), ProjectLimits::default()).unwrap(); compiled.decoded.project.name = "forged".into(); assert!(matches!( save_project(&snapshot, &compiled, ProjectLimits::default()), Err(SourceFileError::Invalid(_)) )); assert_eq!( fs::read(&entry).unwrap(), snapshot.bundle.files["project.arcadia"].as_bytes() ); } #[cfg(unix)] #[test] fn named_pipes_are_rejected_before_opening_on_load_discovery_and_save() { let directory = setup(); let entry = directory.path().join("project.arcadia"); let child = directory.path().join("parts/a.arcadia"); let snapshot = load_project(&entry, ProjectLimits::default()).unwrap(); let compiled = compile_bundle(&snapshot.reachable_bundle(), ProjectLimits::default()).unwrap(); fs::remove_file(&child).unwrap(); assert!( std::process::Command::new("mkfifo") .arg(&child) .status() .unwrap() .success() ); let loaded = load_project(&entry, ProjectLimits::default()).unwrap(); assert!( loaded .diagnostics .iter() .any(|d| d.diagnostic.code == "invalid_path" && d.diagnostic.message.contains("not a regular file")) ); assert!(!loaded.bundle.files.contains_key("parts/a.arcadia")); assert!(matches!( discover_missing(&snapshot, &BTreeMap::new(), ProjectLimits::default()), Err(SourceFileError::Path(_)) )); assert!(matches!( save_project(&snapshot, &compiled, ProjectLimits::default()), Err(SourceFileError::Path(_)) )); assert_eq!( fs::read_to_string(&entry).unwrap(), snapshot.bundle.files["project.arcadia"] ); } #[cfg(unix)] #[test] fn atomic_save_preserves_existing_unix_permissions() { use std::os::unix::fs::PermissionsExt; let directory = setup(); let entry = directory.path().join("project.arcadia"); let child = directory.path().join("parts/a.arcadia"); fs::set_permissions(&entry, fs::Permissions::from_mode(0o664)).unwrap(); fs::set_permissions(&child, fs::Permissions::from_mode(0o640)).unwrap(); let snapshot = load_project(&entry, ProjectLimits::default()).unwrap(); let compiled = compile_bundle(&snapshot.reachable_bundle(), ProjectLimits::default()).unwrap(); save_project(&snapshot, &compiled, ProjectLimits::default()).unwrap(); assert_eq!( fs::metadata(&entry).unwrap().permissions().mode() & 0o777, 0o664 ); assert_eq!( fs::metadata(&child).unwrap().permissions().mode() & 0o777, 0o640 ); }