Refactor Reconciler tests and update path handling logic

- Updated test descriptions in `diff.spec.lua` and `diff.stress.spec.lua` to reflect changes in behavior regarding duplicate-named children in both real and virtual DOM contexts.
- Adjusted assertions to ensure that duplicate-named children are correctly removed or added based on the new matching logic.
- Refactored `hydrate.lua` and `matching.lua` to streamline the matching process by removing unnecessary parameters, enhancing clarity and maintainability.
- Updated metadata references in various test files to include the `.model.json5` extension for consistency in path handling.

These changes improve the accuracy of reconciliation tests and enhance the overall robustness of the Reconciler's path handling logic.
This commit is contained in:
Joe 2026-02-17 04:46:54 -06:00
parent 3ad3c21c01
commit bb9d241253
18 changed files with 129 additions and 94 deletions

View file

@ -276,7 +276,7 @@ return function()
end) end)
describe("duplicate-named siblings", function() describe("duplicate-named siblings", function()
it("should skip real DOM children with duplicate names", function() it("should remove real DOM children with duplicate names when not in virtual DOM", function()
local knownInstances = InstanceMap.new() local knownInstances = InstanceMap.new()
local virtualInstances = { local virtualInstances = {
ROOT = { ROOT = {
@ -305,11 +305,11 @@ return function()
assert(ok, tostring(patch)) assert(ok, tostring(patch))
-- Duplicate-named children should be skipped (not removed) -- Duplicate-named children should be removed (matching handles pairing)
assert(isEmpty(patch.removed)) expect(#patch.removed).to.equal(2)
end) end)
it("should skip virtual children with duplicate names among siblings", function() it("should add virtual children with duplicate names among siblings", function()
local knownInstances = InstanceMap.new() local knownInstances = InstanceMap.new()
local virtualInstances = { local virtualInstances = {
ROOT = { ROOT = {
@ -340,8 +340,8 @@ return function()
assert(ok, tostring(patch)) assert(ok, tostring(patch))
-- Duplicate-named virtual children should not be added -- Duplicate-named virtual children should be added (matching handles pairing)
assert(isEmpty(patch.added)) expect(size(patch.added)).to.equal(2)
end) end)
end) end)

View file

@ -239,9 +239,8 @@ return function()
local ok, patch = diff(instanceMap, virtualInstances, rootId) local ok, patch = diff(instanceMap, virtualInstances, rootId)
expect(ok).to.equal(true) expect(ok).to.equal(true)
-- Duplicates should NOT be marked for removal -- Duplicates should be marked for removal (matching handles pairing)
-- Only if they had unique names would they be candidates expect(#patch.removed).to.equal(2)
expect(#patch.removed).to.equal(0)
instanceMap:stop() instanceMap:stop()
end) end)
@ -290,9 +289,9 @@ return function()
local ok, patch = diff(instanceMap, virtualInstances, rootId) local ok, patch = diff(instanceMap, virtualInstances, rootId)
expect(ok).to.equal(true) expect(ok).to.equal(true)
-- Neither duplicate should be added since the path is ambiguous -- Both duplicates should be added (matching handles pairing)
expect(patch.added[dup1Id]).to.equal(nil) expect(patch.added[dup1Id]).to.be.ok()
expect(patch.added[dup2Id]).to.equal(nil) expect(patch.added[dup2Id]).to.be.ok()
instanceMap:stop() instanceMap:stop()
end) end)
@ -350,8 +349,8 @@ return function()
local ok, patch = diff(instanceMap, virtualInstances, rootId) local ok, patch = diff(instanceMap, virtualInstances, rootId)
expect(ok).to.equal(true) expect(ok).to.equal(true)
-- Grandchild should also NOT be added since its parent is ambiguous -- Grandchild should be added along with its parent (matching handles pairing)
expect(patch.added[grandchildId]).to.equal(nil) expect(patch.added[grandchildId]).to.be.ok()
instanceMap:stop() instanceMap:stop()
end) end)

View file

@ -41,7 +41,7 @@ local function hydrate(instanceMap, virtualInstances, rootId, rootInstance)
end end
-- Use the 3-pass matching algorithm to pair virtual ↔ studio children -- Use the 3-pass matching algorithm to pair virtual ↔ studio children
local result = Matching.matchChildren(validVirtualIds, existingChildren, virtualInstances, instanceMap) local result = Matching.matchChildren(validVirtualIds, existingChildren, virtualInstances)
-- Recursively hydrate matched pairs -- Recursively hydrate matched pairs
for _, pair in result.matched do for _, pair in result.matched do

View file

@ -114,8 +114,7 @@ local Matching = {}
function Matching.matchChildren( function Matching.matchChildren(
virtualChildren: { string }, virtualChildren: { string },
studioChildren: { Instance }, studioChildren: { Instance },
virtualInstances: { [string]: any }, virtualInstances: { [string]: any }
instanceMap: any
) )
local matched: { { virtualId: string, studioInstance: Instance } } = {} local matched: { { virtualId: string, studioInstance: Instance } } = {}
local remainingVirtual: { string } = table.clone(virtualChildren) local remainingVirtual: { string } = table.clone(virtualChildren)
@ -344,7 +343,7 @@ function Matching._computeChangeCount(
end end
-- Recursive: match children and sum costs -- Recursive: match children and sum costs
local childResult = Matching.matchChildren(validVChildren, studioKids, virtualInstances, nil) local childResult = Matching.matchChildren(validVChildren, studioKids, virtualInstances)
-- Matched children: recursively score each pair -- Matched children: recursively score each pair
for _, pair in childResult.matched do for _, pair in childResult.matched do

View file

@ -1,6 +1,6 @@
{ {
"className": "Model", "className": "Model",
"attributes": { "attributes": {
"Rojo_Ref_PrimaryPart": "Workspace/TestModel/Part1" "Rojo_Ref_PrimaryPart": "Workspace/TestModel/Part1.model.json5"
} }
} }

View file

@ -3,6 +3,8 @@ source: tests/rojo_test/syncback_util.rs
expression: normalized_stdout expression: normalized_stdout
--- ---
Writing OnlyOneCopy/child_of_one.luau Writing OnlyOneCopy/child_of_one.luau
Writing ReplicatedStorage/OnlyOneCopy/child_of_one.luau
Writing ReplicatedStorage/child_replicated_storage.luau Writing ReplicatedStorage/child_replicated_storage.luau
Writing ReplicatedStorage/OnlyOneCopy
Removing OnlyOneCopy/.gitkeep Removing OnlyOneCopy/.gitkeep
Removing ReplicatedStorage/.gitkeep Removing ReplicatedStorage/.gitkeep

View file

@ -2,8 +2,12 @@
source: tests/rojo_test/syncback_util.rs source: tests/rojo_test/syncback_util.rs
expression: normalized_stdout expression: normalized_stdout
--- ---
Writing src/ChildWithDuplicates/DuplicateChild/.gitkeep
Writing src/ChildWithDuplicates/DuplicateChild~1/init.meta.json5
Writing src/ChildWithoutDuplicates/Child/.gitkeep Writing src/ChildWithoutDuplicates/Child/.gitkeep
Writing src/ChildWithDuplicates Writing src/ChildWithDuplicates
Writing src/ChildWithDuplicates/DuplicateChild
Writing src/ChildWithDuplicates/DuplicateChild~1
Writing src/ChildWithoutDuplicates Writing src/ChildWithoutDuplicates
Writing src/ChildWithoutDuplicates/Child Writing src/ChildWithoutDuplicates/Child
Removing src/ChildWithDuplicates/.gitkeep Removing src/ChildWithDuplicates/.gitkeep

View file

@ -4,5 +4,6 @@ expression: normalized_stdout
--- ---
Writing default.project.json5 Writing default.project.json5
Writing src/model_json.model.json5 Writing src/model_json.model.json5
Writing src/project_node.model.json5
Writing src/rbxm.rbxm Writing src/rbxm.rbxm
Writing src/rbxmx.rbxmx Writing src/rbxmx.rbxmx

View file

@ -7,7 +7,7 @@ expression: container.model.json5
{ {
attributes: { attributes: {
Rojo_Id: "value_1", Rojo_Id: "value_1",
Rojo_Ref_Value: "ReplicatedStorage/container/value_1" Rojo_Ref_Value: "ReplicatedStorage/container/value_1.model.json5"
}, },
className: "ObjectValue", className: "ObjectValue",
name: "value_1" name: "value_1"
@ -15,7 +15,7 @@ expression: container.model.json5
{ {
attributes: { attributes: {
Rojo_Id: "value_1", Rojo_Id: "value_1",
Rojo_Ref_Value: "ReplicatedStorage/container/value_2" Rojo_Ref_Value: "ReplicatedStorage/container/value_2.model.json5"
}, },
className: "ObjectValue", className: "ObjectValue",
name: "value_2" name: "value_2"

View file

@ -4,7 +4,7 @@ expression: src/pointer.model.json5
--- ---
{ {
attributes: { attributes: {
Rojo_Ref_Value: "ReplicatedStorage/target" Rojo_Ref_Value: "ReplicatedStorage/target.txt"
}, },
className: "ObjectValue" className: "ObjectValue"
} }

View file

@ -4,7 +4,7 @@ expression: src/Pointer1.model.json5
--- ---
{ {
attributes: { attributes: {
Rojo_Ref_Value: "ReplicatedStorage/Target1" Rojo_Ref_Value: "ReplicatedStorage/Target1.model.json5"
}, },
className: "ObjectValue" className: "ObjectValue"
} }

View file

@ -4,7 +4,7 @@ expression: src/Pointer2.model.json5
--- ---
{ {
attributes: { attributes: {
Rojo_Ref_Value: "ReplicatedStorage/Target2" Rojo_Ref_Value: "ReplicatedStorage/Target2.model.json5"
}, },
className: "ObjectValue" className: "ObjectValue"
} }

View file

@ -4,7 +4,7 @@ expression: src/Pointer3.model.json5
--- ---
{ {
attributes: { attributes: {
Rojo_Ref_Value: "ReplicatedStorage/Target3" Rojo_Ref_Value: "ReplicatedStorage/Target3.model.json5"
}, },
className: "ObjectValue" className: "ObjectValue"
} }

View file

@ -485,10 +485,8 @@ impl JobThreadContext {
// this function is called. The index still stores old filesystem // this function is called. The index still stores old filesystem
// paths. Remap them: replace the old directory segment with the // paths. Remap them: replace the old directory segment with the
// new one so we can find the actual files. // new one so we can find the actual files.
let old_segments = crate::split_ref_path(old_path); let old_segment = old_path.rsplit('/').next().unwrap_or(old_path);
let old_segment = old_segments.last().map(|s| s.as_str()).unwrap_or(old_path); let new_segment = new_path.rsplit('/').next().unwrap_or(new_path);
let new_segments = crate::split_ref_path(new_path);
let new_segment = new_segments.last().map(|s| s.as_str()).unwrap_or(new_path);
// Pre-compute slugified forms for fallback comparison. // Pre-compute slugified forms for fallback comparison.
let slugified_old = if name_needs_slugify(old_segment) { let slugified_old = if name_needs_slugify(old_segment) {
@ -1202,11 +1200,17 @@ impl JobThreadContext {
// Capture the old path BEFORE rename for Rojo_Ref_* path updates. // Capture the old path BEFORE rename for Rojo_Ref_* path updates.
// Must be computed before the `tree.get_instance(id)` borrow. // Must be computed before the `tree.get_instance(id)` borrow.
let old_ref_path = if update.changed_name.is_some() { let old_ref_path =
Some(crate::ref_target_path_from_tree(&tree, id)) if update.changed_name.is_some() || update.changed_class_name.is_some() {
} else { Some(crate::ref_target_path_from_tree(&tree, id))
None } else {
}; None
};
// The new filesystem name segment after a rename. Set during
// rename handling and used by the ref path update code to build
// the correct filesystem-name-based ref path.
let mut new_ref_segment: Option<String> = None;
if let Some(instance) = tree.get_instance(id) { if let Some(instance) = tree.get_instance(id) {
// Track the current source file path — rename and ClassName // Track the current source file path — rename and ClassName
@ -1278,6 +1282,8 @@ impl JobThreadContext {
// The init file moved with the directory. // The init file moved with the directory.
overridden_source_path = overridden_source_path =
Some(new_dir_path.join(file_name)); Some(new_dir_path.join(file_name));
new_ref_segment =
Some(slugified_new_name.clone());
let old_meta = grandparent.join(format!( let old_meta = grandparent.join(format!(
"{}.meta.json5", "{}.meta.json5",
dir_name dir_name
@ -1438,6 +1444,8 @@ impl JobThreadContext {
effective_meta_base = old_base; effective_meta_base = old_base;
} else { } else {
overridden_source_path = Some(new_path.clone()); overridden_source_path = Some(new_path.clone());
new_ref_segment =
Some(new_file_name.clone());
let old_meta = parent let old_meta = parent
.join(format!("{}.meta.json5", old_base)); .join(format!("{}.meta.json5", old_base));
let new_meta = parent.join(format!( let new_meta = parent.join(format!(
@ -1616,6 +1624,14 @@ impl JobThreadContext {
); );
} else { } else {
overridden_source_path = Some(new_path.clone()); overridden_source_path = Some(new_path.clone());
// For standalone files, the ref path
// segment changes with the extension.
// For init files, the directory name
// (ref segment) is unchanged.
if !is_init {
new_ref_segment =
Some(new_file_name.clone());
}
} }
} }
} else { } else {
@ -1717,31 +1733,32 @@ impl JobThreadContext {
// //
// NOTE: The tree name hasn't been updated yet (apply_patch_set // NOTE: The tree name hasn't been updated yet (apply_patch_set
// runs after this loop), so we can't use full_path_of for the // runs after this loop), so we can't use full_path_of for the
// new path. Construct it by replacing the last path segment. // new path. Construct it by replacing the last path segment
// Use split_ref_path for escape-aware splitting (handles // with the NEW filesystem name (set during rename handling).
// instance names containing "/").
if let Some(ref old_ref_path) = old_ref_path { if let Some(ref old_ref_path) = old_ref_path {
if let Some(ref new_name) = update.changed_name { if let Some(ref segment) = new_ref_segment {
let segments = crate::split_ref_path(old_ref_path); // Use the filesystem name computed during rename handling
let segments: Vec<&str> = old_ref_path.split('/').collect();
let new_ref_path = if segments.len() > 1 { let new_ref_path = if segments.len() > 1 {
// Rebuild parent path from all segments except the last, let parent = segments[..segments.len() - 1].join("/");
// re-escaping each segment to preserve the original format. format!("{}/{}", parent, segment)
let parent_parts: Vec<_> = segments[..segments.len() - 1]
.iter()
.map(|s| crate::escape_ref_path_segment(s).into_owned())
.collect();
format!(
"{}/{}",
parent_parts.join("/"),
crate::escape_ref_path_segment(new_name)
)
} else { } else {
// Root-level instance (no parent in path) segment.clone()
crate::escape_ref_path_segment(new_name).into_owned()
}; };
if *old_ref_path != new_ref_path { if *old_ref_path != new_ref_path {
self.update_ref_paths_after_rename(old_ref_path, &new_ref_path); self.update_ref_paths_after_rename(old_ref_path, &new_ref_path);
} }
} else if update.changed_name.is_some()
|| update.changed_class_name.is_some()
{
// Rename or class change was requested but no filesystem
// rename happened (e.g., ProjectNode, init-file class
// change where directory name stays the same). No ref
// path update needed.
log::trace!(
"Skipping ref path update for {:?}: no filesystem rename",
id
);
} }
} }
} }

View file

@ -10,7 +10,7 @@ use rbx_dom_weak::{
use crate::{multimap::MultiMap, RojoRef}; use crate::{multimap::MultiMap, RojoRef};
use super::{InstanceMetadata, InstanceSnapshot}; use super::{InstigatingSource, InstanceMetadata, InstanceSnapshot};
/// An expanded variant of rbx_dom_weak's `WeakDom` that tracks additional /// An expanded variant of rbx_dom_weak's `WeakDom` that tracks additional
/// metadata per instance that's Rojo-specific. /// metadata per instance that's Rojo-specific.
@ -291,8 +291,19 @@ impl RojoTree {
pub fn filesystem_name_for(&self, id: Ref) -> String { pub fn filesystem_name_for(&self, id: Ref) -> String {
if let Some(meta) = self.metadata_map.get(&id) { if let Some(meta) = self.metadata_map.get(&id) {
if let Some(source) = &meta.instigating_source { if let Some(source) = &meta.instigating_source {
if let Some(name) = source.path().file_name().and_then(|f| f.to_str()) { match source {
return name.to_string(); InstigatingSource::Path(_) => {
if let Some(name) = source.path().file_name().and_then(|f| f.to_str()) {
return name.to_string();
}
}
InstigatingSource::ProjectNode { .. } => {
// Project-sourced instance: use instance name, not the
// project file path (which would be e.g. "default.project.json5").
if let Some(inst) = self.inner.get_by_ref(id) {
return inst.name.clone();
}
}
} }
} }
} }

View file

@ -628,6 +628,17 @@ pub fn syncback_project<'sync>(
} }
} }
// Project children with $path are expected in the old DOM (loaded
// from their own path) AND may also appear as filesystem children of
// the parent's scanned directory. Exclude them from duplicate
// detection so the project child loop below can match them.
let project_path_child_names: HashSet<String> = node
.children
.iter()
.filter(|(_, child_node)| child_node.path.is_some())
.map(|(name, _)| name.to_lowercase())
.collect();
let mut old_duplicate_names: HashSet<String> = HashSet::new(); let mut old_duplicate_names: HashSet<String> = HashSet::new();
{ {
let mut seen: HashSet<String> = HashSet::new(); let mut seen: HashSet<String> = HashSet::new();
@ -636,6 +647,9 @@ pub fn syncback_project<'sync>(
.get_old_instance(*child_ref) .get_old_instance(*child_ref)
.expect("all children of Instances should be in old DOM"); .expect("all children of Instances should be in old DOM");
let lower = child.name().to_lowercase(); let lower = child.name().to_lowercase();
if project_path_child_names.contains(&lower) {
continue;
}
if !seen.insert(lower.clone()) { if !seen.insert(lower.clone()) {
old_duplicate_names.insert(lower); old_duplicate_names.insert(lower);
} }

View file

@ -58,11 +58,6 @@ pub fn parse_dedup_suffix(stem: &str) -> Option<(&str, u32)> {
None None
} }
/// Checks whether a filename stem has a dedup suffix.
pub fn has_dedup_suffix(stem: &str) -> bool {
parse_dedup_suffix(stem).is_some()
}
/// Builds a dedup'd filename from a base stem, optional suffix number, and /// Builds a dedup'd filename from a base stem, optional suffix number, and
/// extension. /// extension.
/// ///
@ -180,13 +175,6 @@ mod tests {
assert_eq!(parse_dedup_suffix("My Script~1"), Some(("My Script", 1))); assert_eq!(parse_dedup_suffix("My Script~1"), Some(("My Script", 1)));
} }
#[test]
fn has_suffix() {
assert!(has_dedup_suffix("Foo~1"));
assert!(!has_dedup_suffix("Foo"));
assert!(!has_dedup_suffix("Foo~abc"));
}
#[test] #[test]
fn build_name_file() { fn build_name_file() {
assert_eq!( assert_eq!(

View file

@ -6127,7 +6127,7 @@ fn ref_set_primary_part() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
}); });
} }
@ -6156,7 +6156,7 @@ fn ref_set_object_value() {
let model_path = session let model_path = session
.path() .path()
.join("src/Workspace/TestObjectValue.model.json5"); .join("src/Workspace/TestObjectValue.model.json5");
poll_meta_has_ref_attr(&model_path, "Rojo_Ref_Value", "Workspace/TestModel/Part1"); poll_meta_has_ref_attr(&model_path, "Rojo_Ref_Value", "Workspace/TestModel/Part1.model.json5");
}); });
} }
@ -6190,7 +6190,7 @@ fn ref_set_primary_part_to_nil() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
// Now set PrimaryPart to nil // Now set PrimaryPart to nil
@ -6273,7 +6273,7 @@ fn ref_change_target() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
// Change PrimaryPart to Part2 // Change PrimaryPart to Part2
@ -6294,7 +6294,7 @@ fn ref_change_target() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part2", "Workspace/TestModel/Part2.model.json5",
); );
}); });
} }
@ -6324,7 +6324,7 @@ fn ref_set_nil_then_set_again() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
let mut props2 = UstrMap::default(); let mut props2 = UstrMap::default();
@ -6359,7 +6359,7 @@ fn ref_set_nil_then_set_again() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
}); });
} }
@ -6402,7 +6402,7 @@ fn ref_with_name_change() {
assert_meta_has_ref_attr( assert_meta_has_ref_attr(
&new_meta, &new_meta,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/RenamedModel/Part1", "Workspace/RenamedModel/Part1.model.json5",
); );
}); });
} }
@ -6433,7 +6433,7 @@ fn ref_only_change_creates_meta() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
}); });
} }
@ -6467,7 +6467,7 @@ fn ref_on_model_json5_instance() {
let model_path = session let model_path = session
.path() .path()
.join("src/Workspace/TestObjectValue.model.json5"); .join("src/Workspace/TestObjectValue.model.json5");
poll_meta_has_ref_attr(&model_path, "Rojo_Ref_Value", "Workspace/TestModel/Part1"); poll_meta_has_ref_attr(&model_path, "Rojo_Ref_Value", "Workspace/TestModel/Part1.model.json5");
}); });
} }
@ -6498,7 +6498,7 @@ fn ref_existing_meta_preserved() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
// Verify existing content wasn't clobbered // Verify existing content wasn't clobbered
@ -6571,7 +6571,7 @@ fn ref_mixed_valid_and_invalid() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
}); });
} }
@ -6614,7 +6614,7 @@ fn ref_stale_path_after_target_rename() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
// Step 2: Rename Part1 to RenamedPart // Step 2: Rename Part1 to RenamedPart
@ -6637,7 +6637,7 @@ fn ref_stale_path_after_target_rename() {
assert_meta_has_ref_attr( assert_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/RenamedPart", "Workspace/TestModel/RenamedPart.model.json5",
); );
}); });
} }
@ -6671,7 +6671,7 @@ fn ref_stale_path_after_parent_rename() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
// Step 2: Rename the MODEL (parent) from TestModel to RenamedModel // Step 2: Rename the MODEL (parent) from TestModel to RenamedModel
@ -6700,7 +6700,7 @@ fn ref_stale_path_after_parent_rename() {
assert_meta_has_ref_attr( assert_meta_has_ref_attr(
&new_meta_path, &new_meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/RenamedModel/Part1", "Workspace/RenamedModel/Part1.model.json5",
); );
}); });
} }
@ -6829,7 +6829,7 @@ fn ref_ambiguous_path_no_crash() {
let meta_path = session let meta_path = session
.path() .path()
.join("src/Workspace/DupParent/init.meta.json5"); .join("src/Workspace/DupParent/init.meta.json5");
poll_meta_has_ref_attr(&meta_path, "Rojo_Ref_PrimaryPart", "Workspace/Target"); poll_meta_has_ref_attr(&meta_path, "Rojo_Ref_PrimaryPart", "Workspace/Target.model.json5");
}); });
} }
@ -6926,7 +6926,7 @@ fn ref_startup_index_rename_updates_preexisting_attr() {
assert_meta_has_ref_attr( assert_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
// Rename Part1 to IndexTestPart WITHOUT any prior /api/write for Refs. // Rename Part1 to IndexTestPart WITHOUT any prior /api/write for Refs.
@ -6951,7 +6951,7 @@ fn ref_startup_index_rename_updates_preexisting_attr() {
assert_meta_has_ref_attr( assert_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/IndexTestPart", "Workspace/TestModel/IndexTestPart.model.json5",
); );
}); });
} }
@ -6987,10 +6987,10 @@ fn ref_partial_removal_preserves_remaining_index_entry() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
// Verify second ref is there too // Verify second ref is there too
assert_meta_has_ref_attr(&meta_path, "Rojo_Ref_Adornee", "Workspace/TestModel/Part2"); assert_meta_has_ref_attr(&meta_path, "Rojo_Ref_Adornee", "Workspace/TestModel/Part2.model.json5");
// Step 2: Remove PrimaryPart (set to nil), keep Adornee // Step 2: Remove PrimaryPart (set to nil), keep Adornee
let mut props2 = UstrMap::default(); let mut props2 = UstrMap::default();
@ -7010,7 +7010,7 @@ fn ref_partial_removal_preserves_remaining_index_entry() {
thread::sleep(Duration::from_millis(300)); thread::sleep(Duration::from_millis(300));
assert_meta_no_ref_attr(&meta_path, "Rojo_Ref_PrimaryPart"); assert_meta_no_ref_attr(&meta_path, "Rojo_Ref_PrimaryPart");
// Adornee should still be there // Adornee should still be there
assert_meta_has_ref_attr(&meta_path, "Rojo_Ref_Adornee", "Workspace/TestModel/Part2"); assert_meta_has_ref_attr(&meta_path, "Rojo_Ref_Adornee", "Workspace/TestModel/Part2.model.json5");
// Step 3: Rename Part2. The RefPathIndex should still have the entry // Step 3: Rename Part2. The RefPathIndex should still have the entry
// for Adornee (not overbroad-removed when PrimaryPart was deleted). // for Adornee (not overbroad-removed when PrimaryPart was deleted).
@ -7032,7 +7032,7 @@ fn ref_partial_removal_preserves_remaining_index_entry() {
assert_meta_has_ref_attr( assert_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_Adornee", "Rojo_Ref_Adornee",
"Workspace/TestModel/RenamedPart2", "Workspace/TestModel/RenamedPart2.model.json5",
); );
}); });
} }
@ -7066,7 +7066,7 @@ fn ref_rename_to_slugified_name_updates_ref_paths() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
// Rename Model to a name with forbidden characters. // Rename Model to a name with forbidden characters.
@ -7097,7 +7097,7 @@ fn ref_rename_to_slugified_name_updates_ref_paths() {
assert_meta_has_ref_attr( assert_meta_has_ref_attr(
&new_meta_path, &new_meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/Slug:Model/Part1", "Workspace/Slug_Model/Part1.model.json5",
); );
}); });
} }
@ -7145,7 +7145,7 @@ fn ref_on_standalone_script() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
// Verify the wrong path does NOT have the attribute // Verify the wrong path does NOT have the attribute
@ -7191,7 +7191,7 @@ fn ref_on_txt_file_instance() {
poll_meta_has_ref_attr( poll_meta_has_ref_attr(
&meta_path, &meta_path,
"Rojo_Ref_PrimaryPart", "Rojo_Ref_PrimaryPart",
"Workspace/TestModel/Part1", "Workspace/TestModel/Part1.model.json5",
); );
}); });
} }