Implement fixes and enhancements from audit pass 2

- Introduced a new plan addressing critical bugs, correctness issues, and refactors identified during the audit, including improvements to metadata handling and slugification logic.
- Replaced inline removal logic in `handle_api_write` with a new `remove_instance_at_path` method to ensure correct deletion of instances.
- Updated pre-seeding logic in `dir.rs` and `project.rs` to derive deduplication keys from actual filesystem paths instead of slugified names.
- Enhanced rename handling in `change_processor.rs` to prevent collisions by deduplicating names against existing siblings.
- Added tests for various scenarios, including handling of forbidden characters and ensuring correct deduplication behavior.
- Refactored child iteration in `dir.rs` and `project.rs` to sort children alphabetically for deterministic deduplication ordering.
This commit is contained in:
Joe 2026-02-10 21:35:26 -06:00
parent baf05dcb3d
commit 2ff5c3936c
30 changed files with 871 additions and 229 deletions

View file

@ -2400,4 +2400,77 @@ mod tests {
let dk_from_filename = strip_middleware_extension(&name1, Middleware::ModuleScript);
assert_eq!(dk_from_filename.to_lowercase(), dk1.to_lowercase());
}
#[test]
fn syncback_forbidden_chars_produce_slugified_filenames() {
// Verify that instances with forbidden chars produce correct
// (filename, needs_meta, dedup_key) via name_for_inst
let mut dom = rbx_dom_weak::WeakDom::new(rbx_dom_weak::InstanceBuilder::new("Folder"));
let root_ref = dom.root_ref();
let child = dom.insert(root_ref, rbx_dom_weak::InstanceBuilder::new("ModuleScript").with_name("Hey/Bro"));
let inst = dom.get_by_ref(child).unwrap();
let taken = std::collections::HashSet::new();
let (filename, needs_meta, dedup_key) = name_for_inst(
crate::snapshot_middleware::Middleware::ModuleScript,
inst, None, &taken,
).unwrap();
assert_eq!(filename.as_ref(), "Hey_Bro.luau");
assert!(needs_meta);
assert_eq!(dedup_key, "Hey_Bro");
}
#[test]
fn syncback_collision_deduplicates() {
let mut dom = rbx_dom_weak::WeakDom::new(rbx_dom_weak::InstanceBuilder::new("Folder"));
let root_ref = dom.root_ref();
let child1 = dom.insert(root_ref, rbx_dom_weak::InstanceBuilder::new("ModuleScript").with_name("A/B"));
let child2 = dom.insert(root_ref, rbx_dom_weak::InstanceBuilder::new("ModuleScript").with_name("A:B"));
let inst1 = dom.get_by_ref(child1).unwrap();
let inst2 = dom.get_by_ref(child2).unwrap();
let mut taken = std::collections::HashSet::new();
let (f1, m1, dk1) = name_for_inst(
crate::snapshot_middleware::Middleware::ModuleScript,
inst1, None, &taken,
).unwrap();
taken.insert(dk1.to_lowercase());
let (f2, m2, dk2) = name_for_inst(
crate::snapshot_middleware::Middleware::ModuleScript,
inst2, None, &taken,
).unwrap();
assert_eq!(f1.as_ref(), "A_B.luau");
assert!(m1);
assert_eq!(f2.as_ref(), "A_B~1.luau");
assert!(m2);
assert_ne!(dk1.to_lowercase(), dk2.to_lowercase());
}
#[test]
fn syncback_idempotency_same_input_same_output() {
// Run the same set of instances through name_for_inst twice
// and verify identical output (determinism).
let names = vec!["Alpha", "Beta/Gamma", "Beta:Gamma", "Delta", "CON"];
let mw = crate::snapshot_middleware::Middleware::ModuleScript;
let mut dom = rbx_dom_weak::WeakDom::new(rbx_dom_weak::InstanceBuilder::new("Folder"));
let root = dom.root_ref();
let refs: Vec<_> = names.iter().map(|n|
dom.insert(root, rbx_dom_weak::InstanceBuilder::new("ModuleScript").with_name(*n))
).collect();
let run = |dom: &rbx_dom_weak::WeakDom, refs: &[rbx_dom_weak::types::Ref]| -> Vec<(String, bool, String)> {
let mut taken = std::collections::HashSet::new();
refs.iter().map(|r| {
let inst = dom.get_by_ref(*r).unwrap();
let (f, m, dk) = name_for_inst(mw, inst, None, &taken).unwrap();
taken.insert(dk.to_lowercase());
(f.into_owned(), m, dk)
}).collect()
};
let result1 = run(&dom, &refs);
let result2 = run(&dom, &refs);
assert_eq!(result1, result2, "Two runs of name_for_inst must produce identical output");
}
}

View file

@ -32,7 +32,8 @@ use crate::{
pub use file_names::{
adjacent_meta_path, deduplicate_name, extension_for_middleware, name_for_inst,
name_needs_slugify, slugify_name, strip_middleware_extension, validate_file_name,
name_needs_slugify, slugify_name, strip_middleware_extension, strip_script_suffix,
validate_file_name,
};
pub use fs_snapshot::FsSnapshot;
pub use hash::*;