diff --git a/.cursor/rules/atlas-dedup.mdc b/.cursor/rules/atlas-dedup.mdc index 17f1d90e..b160f952 100644 --- a/.cursor/rules/atlas-dedup.mdc +++ b/.cursor/rules/atlas-dedup.mdc @@ -92,10 +92,10 @@ pub fn deduplicate_name_with_ext( taken_names: &HashSet, ) -> (String, String) // Returns: (deduped_slug, full_fs_name) -// "Foo" + ".server.luau" with collision → ("Foo~1", "Foo~1.server.luau") +// "Foo" + ".server.luau" with collision → ("Foo~2", "Foo~2.server.luau") ``` -**Key invariant:** Suffix goes on the stem BEFORE the compound extension: `Foo~1.server.luau`, NOT `Foo.server~1.luau`. +**Key invariant:** Suffix goes on the stem BEFORE the compound extension: `Foo~2.server.luau`, NOT `Foo.server~2.luau`. ### Dedup Key Semantics @@ -134,7 +134,7 @@ When the filesystem name differs from the instance name (due to slugification or ``` Instance "Hey/Bro" → Hey_Bro.luau + meta: {"name": "Hey/Bro"} Instance "Folder" → Folder/ (no meta needed, name == slug) -Instance "Folder" → Folder~1/ + meta: {"name": "Folder"} +Instance "Folder" → Folder~2/ + meta: {"name": "Folder"} ``` --- @@ -155,11 +155,11 @@ pub enum DedupCleanupAction { ### Three Cleanup Rules -1. **Gap-tolerant deletion:** Deleting `~1` from `{Foo, Foo~1, Foo~2}` leaves `{Foo, Foo~2}`. No renumbering. Gaps are tolerated. +1. **Gap-tolerant deletion:** Deleting `~2` from `{Foo, Foo~2, Foo~3}` leaves `{Foo, Foo~3}`. No renumbering. Gaps are tolerated. -2. **Base-name promotion:** Deleting `Foo` (base) from `{Foo, Foo~1, Foo~2}` promotes `Foo~1` to `Foo`. `Foo~2` stays. The lowest-numbered suffix becomes the new base. +2. **Base-name promotion:** Deleting `Foo` (base) from `{Foo, Foo~2, Foo~3}` promotes `Foo~2` to `Foo`. `Foo~3` stays. The lowest-numbered suffix becomes the new base. -3. **Group-to-1 cleanup:** Reducing the group to 1 member removes the suffix entirely. Deleting from `{Foo, Foo~1}` leaves one instance → renamed to clean `Foo`. +3. **Group-to-1 cleanup:** Reducing the group to 1 member removes the suffix entirely. Deleting from `{Foo, Foo~2}` leaves one instance → renamed to clean `Foo`. ### compute_cleanup_action @@ -287,7 +287,7 @@ Ref paths use **full filesystem names** including extensions and dedup suffixes: Folder "Foo" → segment "Foo" ModuleScript "Foo" → segment "Foo.luau" Script "Foo" (server) → segment "Foo.server.luau" -Dedup'd ModuleScript → segment "Foo~1.luau" +Dedup'd ModuleScript → segment "Foo~2.luau" Init-style Script → segment "Foo" (directory) ``` @@ -395,7 +395,7 @@ Dedup renames must suppress VFS events to prevent feedback loops: ### Ref Path Updates After Dedup Rename -After successful dedup rename (e.g., `Foo~1` → `Foo`): +After successful dedup rename (e.g., `Foo~2` → `Foo`): 1. Compute old and new ref path segments 2. Call `update_ref_paths_after_rename(&old_prefix, &new_prefix)` 3. This function uses `RefPathIndex` to find affected files @@ -475,8 +475,8 @@ When adding instances, dedup is handled via `sibling_slugs: &mut HashSet 2. **CLI syncback parity:** two-way sync produces byte-identical filesystem output to CLI syncback 3. **Determinism:** same input always produces same filesystem output (sorted child ordering, deterministic suffix assignment) 4. **InstigatingSource consistency:** dedup'd paths reflected in `InstigatingSource::Path` -5. **Compound extensions:** suffix before compound extension (`Foo~1.server.luau`, NOT `Foo.server~1.luau`) -6. **Meta pairing:** `Foo~1.server.luau` pairs with `Foo~1.meta.json5` (strip script suffix, keep dedup suffix) +5. **Compound extensions:** suffix before compound extension (`Foo~2.server.luau`, NOT `Foo.server~2.luau`) +6. **Meta pairing:** `Foo~2.server.luau` pairs with `Foo~2.meta.json5` (strip script suffix, keep dedup suffix) 7. **Dedup key = full FS name:** collisions checked using complete filesystem name (slug + extension), lowercased 8. **Co-removed exclusion:** batch removal skips co-removed siblings when computing cleanup actions diff --git a/.cursor/rules/atlas-project.mdc b/.cursor/rules/atlas-project.mdc index bfcb536c..f55bc43e 100644 --- a/.cursor/rules/atlas-project.mdc +++ b/.cursor/rules/atlas-project.mdc @@ -247,7 +247,7 @@ Add metadata to file-based instances. Meta files use **camelCase keys** (no `$` **Key differences from ProjectNode:** Meta uses `className`, `properties`, `attributes` (camelCase, no `$` prefix). Adjacent meta does NOT support `className`. -**`name` field (slugify support):** When an instance name contains forbidden filesystem characters (`: / ? * < > | " \`), syncback slugifies the filename (forbidden chars → `_`) and stores the real name in the `name` field. On forward sync, `name` overrides the filename-derived instance name. On collisions (e.g. `A/B` and `A_B` both slugify to `A_B`), `~1`, `~2` suffixes are appended and meta `name` fields distinguish them. +**`name` field (slugify support):** When an instance name contains forbidden filesystem characters (`: / ? * < > | " \`), syncback slugifies the filename (forbidden chars → `_`) and stores the real name in the `name` field. On forward sync, `name` overrides the filename-derived instance name. On collisions (e.g. `A/B` and `A_B` both slugify to `A_B`), `~2`, `~3` suffixes are appended and meta `name` fields distinguish them. **`Rojo_Ref_*` attributes:** Meta files may also contain `Rojo_Ref_PropertyName` attributes written by syncback/two-way sync for Ref property linking. These are managed automatically and should not be hand-edited. diff --git a/.cursor/rules/atlas-src.mdc b/.cursor/rules/atlas-src.mdc index d354ca46..5310808f 100644 --- a/.cursor/rules/atlas-src.mdc +++ b/.cursor/rules/atlas-src.mdc @@ -74,7 +74,7 @@ pub struct InstanceContext { Instance names with forbidden filesystem chars (`<>:"/\|?*~`) are handled by: 1. `slugify_name()` -- replaces forbidden chars with `_` (pure, stateless) -2. `deduplicate_name_with_ext()` -- dedup key = full filesystem name (slug + extension), case-insensitive; appends `~1`, `~2`, etc. on collision (stateful) +2. `deduplicate_name_with_ext()` -- dedup key = full filesystem name (slug + extension), case-insensitive; appends `~2`, `~3`, etc. on collision (stateful) 3. Metadata `name` field -- stores the real instance name in `.meta.json5`/`.model.json5` when the slug differs ### 3. Commands (`src/cli/`) diff --git a/.cursor/rules/atlas-syncback.mdc b/.cursor/rules/atlas-syncback.mdc index 08aa6f0b..726d938a 100644 --- a/.cursor/rules/atlas-syncback.mdc +++ b/.cursor/rules/atlas-syncback.mdc @@ -145,8 +145,8 @@ Manages `~N` suffix cleanup after instance deletions during two-way sync. - `compute_cleanup_action(removed_stem, remaining_stems) -> DedupCleanupAction` - Determine what to do after a deletion **Cleanup rules:** -- **Gap-tolerant:** Deleting `~1` from `{Foo, Foo~1, Foo~2}` leaves `{Foo, Foo~2}`. No renumbering. -- **Base-name promotion:** Deleting `Foo` (base) from `{Foo, Foo~1, Foo~2}` promotes `Foo~1` to `Foo`. `Foo~2` stays. +- **Gap-tolerant:** Deleting `~2` from `{Foo, Foo~2, Foo~3}` leaves `{Foo, Foo~3}`. No renumbering. +- **Base-name promotion:** Deleting `Foo` (base) from `{Foo, Foo~2, Foo~3}` promotes `Foo~2` to `Foo`. `Foo~3` stays. - **Group-to-1 cleanup:** If group shrinks to 1 member, remove its suffix entirely. ## Syncback Rules @@ -285,12 +285,12 @@ snapshot.with_new_path(path, new_ref, old_ref) **Filename generation** (`file_names.rs`): - `slugify_name()`: forbidden chars (`<>:"/\|?*~`) → `_`. Also strips leading/trailing spaces/dots, neutralizes dangerous suffixes (`.server`, `.meta`, etc.), handles Windows reserved names (`CON`, `PRN`, etc.) -- `deduplicate_name_with_ext()`: Dedup key = full filesystem name (slug + extension), case-insensitive. `Folder "Foo"` (key `"foo"`) and `Script "Foo"` (key `"foo.server.luau"`) do NOT collide. Appends `~1`, `~2`, etc. on collision, with suffix inserted before compound extension: `Foo~1.server.luau` +- `deduplicate_name_with_ext()`: Dedup key = full filesystem name (slug + extension), case-insensitive. `Folder "Foo"` (key `"foo"`) and `Script "Foo"` (key `"foo.server.luau"`) do NOT collide. Appends `~2`, `~3`, etc. on collision, with suffix inserted before compound extension: `Foo~2.server.luau` - `name_for_inst()` returns `(filename, needs_meta_name, dedup_key)`. `needs_meta_name` is true when the filesystem name differs from the real instance name. - `~` is in `SLUGIFY_CHARS` so natural tilde names (e.g., `"Foo~1"`) get slugified to `"Foo_1"`, avoiding confusion with dedup suffixes. - Example: `Script` named "My Script" → `My Script.server.luau` - Example: `Script` named "Hey/Bro" → `Hey_Bro.server.luau` + meta with `name: "Hey/Bro"` -- Example: Two `Folder "Data"` → `Data/` + `Data~1/` (meta: `name: "Data"`) +- Example: Two `Folder "Data"` → `Data/` + `Data~2/` (meta: `name: "Data"`) ### Property Filtering diff --git a/plugin/src/App/Components/PatchVisualizer/DomLabel.lua b/plugin/src/App/Components/PatchVisualizer/DomLabel.lua index f804cad7..212aed0e 100644 --- a/plugin/src/App/Components/PatchVisualizer/DomLabel.lua +++ b/plugin/src/App/Components/PatchVisualizer/DomLabel.lua @@ -491,7 +491,7 @@ function DomLabel:render() end end - if depth ~= 1 then + if depth > 1 then lineGuides["Connector"] = e("Frame", { Size = UDim2.new(0, 8, 0, 2), Position = UDim2.new(0, 2 + (12 * props.depth), 0, 12), diff --git a/plugin/src/App/Components/PatchVisualizer/init.lua b/plugin/src/App/Components/PatchVisualizer/init.lua index 6c90f328..7d7b8c70 100644 --- a/plugin/src/App/Components/PatchVisualizer/init.lua +++ b/plugin/src/App/Components/PatchVisualizer/init.lua @@ -137,6 +137,11 @@ function PatchVisualizer:render() end end + -- Add ROOT as the first visible node (project summary) + if patchTree.ROOT.changeInfo then + table.insert(visibleNodes, { node = patchTree.ROOT, depth = 0 }) + end + -- Collect from root children in alphabetical order local rootChildren = {} for _, child in patchTree.ROOT.children do diff --git a/plugin/src/PatchTree.lua b/plugin/src/PatchTree.lua index 465cec26..681e5e32 100644 --- a/plugin/src/PatchTree.lua +++ b/plugin/src/PatchTree.lua @@ -79,8 +79,9 @@ function Tree.new() local tree = { idToNode = {}, ROOT = { + id = "ROOT", className = "DataModel", - name = "ROOT", + name = "game", children = {}, }, } @@ -209,6 +210,41 @@ function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, p end end +local function computeRootChangeInfo(tree) + local totalLinesAdded = 0 + local totalLinesRemoved = 0 + local totalPropChanges = 0 + local hasLineData = false + + tree:forEach(function(node) + if not node.changeInfo then + return + end + if node.changeInfo.linesAdded then + totalLinesAdded += node.changeInfo.linesAdded + hasLineData = true + end + if node.changeInfo.linesRemoved then + totalLinesRemoved += node.changeInfo.linesRemoved + hasLineData = true + end + if node.changeInfo.propChanges then + totalPropChanges += node.changeInfo.propChanges + end + end) + + if not hasLineData and totalPropChanges == 0 then + return nil + end + + return { + linesAdded = if hasLineData then totalLinesAdded else nil, + linesRemoved = if hasLineData then totalLinesRemoved else nil, + isWhitespaceOnly = false, + propChanges = if totalPropChanges > 0 then totalPropChanges else nil, + } +end + local PatchTree = {} -- Builds a new tree from a patch and instanceMap @@ -482,6 +518,8 @@ function PatchTree.build(patch, instanceMap, changeListHeaders, gitMetadata) end Timer.stop() + tree.ROOT.changeInfo = computeRootChangeInfo(tree) + Timer.stop() return tree end @@ -653,6 +691,8 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch) end) Timer.stop() + tree.ROOT.changeInfo = computeRootChangeInfo(tree) + Timer.stop() return tree end diff --git a/plugin/src/Reconciler/stats.json b/plugin/src/Reconciler/stats.json new file mode 100644 index 00000000..142ce60a --- /dev/null +++ b/plugin/src/Reconciler/stats.json @@ -0,0 +1,28 @@ +{ + "lines": 629, + "bytecode": 6261, + "bytecodeInstructionCount": 986, + "codegen": 0, + "readTime": 0.000094, + "miscTime": 0.000053, + "parseTime": 0.000251, + "compileTime": 0.000709, + "codegenTime": 0.000000, + "lowerStats": { + "totalFunctions": 0, + "skippedFunctions": 0, + "spillsToSlot": 0, + "spillsToRestore": 0, + "maxSpillSlotsUsed": 0, + "blocksPreOpt": 0, + "blocksPostOpt": 0, + "maxBlockInstructions": 0, + "regAllocErrors": 0, + "loweringErrors": 0, + "blockLinearizationStats": { + "constPropInstructionCount": 0, + "timeSeconds": 0.000000 + }, + "functions": [] + } + } \ No newline at end of file diff --git a/rojo-test/serve-tests/connected_slugify/src/Hey_Bro~1.luau b/rojo-test/serve-tests/connected_slugify/src/Hey_Bro~2.luau similarity index 100% rename from rojo-test/serve-tests/connected_slugify/src/Hey_Bro~1.luau rename to rojo-test/serve-tests/connected_slugify/src/Hey_Bro~2.luau diff --git a/rojo-test/serve-tests/connected_slugify/src/Hey_Bro~1.meta.json5 b/rojo-test/serve-tests/connected_slugify/src/Hey_Bro~2.meta.json5 similarity index 100% rename from rojo-test/serve-tests/connected_slugify/src/Hey_Bro~1.meta.json5 rename to rojo-test/serve-tests/connected_slugify/src/Hey_Bro~2.meta.json5 diff --git a/rojo-test/serve-tests/ref_ambiguous_path/src/Workspace/DupParent/Child~1.model.json5 b/rojo-test/serve-tests/ref_ambiguous_path/src/Workspace/DupParent/Child~2.model.json5 similarity index 100% rename from rojo-test/serve-tests/ref_ambiguous_path/src/Workspace/DupParent/Child~1.model.json5 rename to rojo-test/serve-tests/ref_ambiguous_path/src/Workspace/DupParent/Child~2.model.json5 diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-stdout.snap index 6856e896..d57c1961 100644 --- a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-stdout.snap +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-stdout.snap @@ -3,11 +3,11 @@ source: tests/rojo_test/syncback_util.rs expression: normalized_stdout --- Writing src/ChildWithDuplicates/DuplicateChild/.gitkeep -Writing src/ChildWithDuplicates/DuplicateChild~1/init.meta.json5 +Writing src/ChildWithDuplicates/DuplicateChild~2/init.meta.json5 Writing src/ChildWithoutDuplicates/Child/.gitkeep Writing src/ChildWithDuplicates Writing src/ChildWithDuplicates/DuplicateChild -Writing src/ChildWithDuplicates/DuplicateChild~1 +Writing src/ChildWithDuplicates/DuplicateChild~2 Writing src/ChildWithoutDuplicates Writing src/ChildWithoutDuplicates/Child Removing src/ChildWithDuplicates/.gitkeep diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~10.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~10.model.json5 index dc4b910d..470357ae 100644 --- a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~10.model.json5 +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~10.model.json5 @@ -7,7 +7,7 @@ 0, 1 ], - Face: "Right", + Face: "Left", StudsPerTileU: 200, StudsPerTileV: 200, TextureContent: "rbxassetid://10023635072", diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~11.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~11.model.json5 index df4f761f..dc4b910d 100644 --- a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~11.model.json5 +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~11.model.json5 @@ -7,7 +7,7 @@ 0, 1 ], - Face: "Top", + Face: "Right", StudsPerTileU: 200, StudsPerTileV: 200, TextureContent: "rbxassetid://10023635072", diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~1.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~12.model.json5 similarity index 53% rename from rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~1.model.json5 rename to rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~12.model.json5 index 11e28cd8..df4f761f 100644 --- a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~1.model.json5 +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~12.model.json5 @@ -3,14 +3,14 @@ name: "Texture", properties: { Color3: [ - 0.41568630933761597, + 0.40000003576278687, 0, 1 ], - Face: "Right", + Face: "Top", StudsPerTileU: 200, StudsPerTileV: 200, - TextureContent: "rbxassetid://10025668350", - Transparency: 0.6000000238418579 + TextureContent: "rbxassetid://10023635072", + Transparency: 0.6499999761581421 } } diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~2.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~2.model.json5 index 9f5fd1c3..11e28cd8 100644 --- a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~2.model.json5 +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~2.model.json5 @@ -7,7 +7,7 @@ 0, 1 ], - Face: "Left", + Face: "Right", StudsPerTileU: 200, StudsPerTileV: 200, TextureContent: "rbxassetid://10025668350", diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~3.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~3.model.json5 index cc60c294..9f5fd1c3 100644 --- a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~3.model.json5 +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~3.model.json5 @@ -7,6 +7,7 @@ 0, 1 ], + Face: "Left", StudsPerTileU: 200, StudsPerTileV: 200, TextureContent: "rbxassetid://10025668350", diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~4.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~4.model.json5 index cec50fee..cc60c294 100644 --- a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~4.model.json5 +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~4.model.json5 @@ -7,7 +7,6 @@ 0, 1 ], - Face: "Bottom", StudsPerTileU: 200, StudsPerTileV: 200, TextureContent: "rbxassetid://10025668350", diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~5.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~5.model.json5 index 30761828..cec50fee 100644 --- a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~5.model.json5 +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~5.model.json5 @@ -7,7 +7,7 @@ 0, 1 ], - Face: "Back", + Face: "Bottom", StudsPerTileU: 200, StudsPerTileV: 200, TextureContent: "rbxassetid://10025668350", diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~7.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~7.model.json5 index c9f537bd..015bfdce 100644 --- a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~7.model.json5 +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~7.model.json5 @@ -7,7 +7,7 @@ 0, 1 ], - Face: "Bottom", + Face: "Back", StudsPerTileU: 200, StudsPerTileV: 200, TextureContent: "rbxassetid://10023635072", diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~8.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~8.model.json5 index 869ecc79..c9f537bd 100644 --- a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~8.model.json5 +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~8.model.json5 @@ -7,6 +7,7 @@ 0, 1 ], + Face: "Bottom", StudsPerTileU: 200, StudsPerTileV: 200, TextureContent: "rbxassetid://10023635072", diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~9.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~9.model.json5 index 470357ae..869ecc79 100644 --- a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~9.model.json5 +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~9.model.json5 @@ -7,7 +7,6 @@ 0, 1 ], - Face: "Left", StudsPerTileU: 200, StudsPerTileV: 200, TextureContent: "rbxassetid://10023635072", diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~2.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~2.model.json5 index 25ec3941..943be0f1 100644 --- a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~2.model.json5 +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~2.model.json5 @@ -15,16 +15,16 @@ keypoints: [ { color: [ - 0.6666666865348816, - 0, + 0.364705890417099, + 0.11372549086809158, 1 ], time: 0 }, { color: [ - 0.6666666865348816, - 0, + 0.364705890417099, + 0.11372549086809158, 1 ], time: 1 diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~1.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~3.model.json5 similarity index 96% rename from rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~1.model.json5 rename to rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~3.model.json5 index 943be0f1..25ec3941 100644 --- a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~1.model.json5 +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~3.model.json5 @@ -15,16 +15,16 @@ keypoints: [ { color: [ - 0.364705890417099, - 0.11372549086809158, + 0.6666666865348816, + 0, 1 ], time: 0 }, { color: [ - 0.364705890417099, - 0.11372549086809158, + 0.6666666865348816, + 0, 1 ], time: 1 diff --git a/rokit.toml b/rokit.toml index 97af0c92..e365f787 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,6 +1,5 @@ [tools] selene = "Kampfkarren/selene@0.30.0" stylua = "JohnnyMorganz/stylua@2.3.1" -luau = "luau-lang/luau@0.708" luau-lsp = "JohnnyMorganz/luau-lsp@1.62.0" run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0" diff --git a/src/rojo_ref.rs b/src/rojo_ref.rs index 315f4b84..c9872e81 100644 --- a/src/rojo_ref.rs +++ b/src/rojo_ref.rs @@ -134,7 +134,7 @@ pub fn ref_target_path(dom: &WeakDom, target_ref: Ref) -> String { /// - `Folder "Foo"` → `"Foo"` (directory name) /// - `ModuleScript "Foo"` → `"Foo.luau"` (slug + extension) /// - `Script "Foo"` (server) → `"Foo.server.luau"` -/// - Dedup'd `ModuleScript "Foo"` → `"Foo~1.luau"` +/// - Dedup'd `ModuleScript "Foo"` → `"Foo~2.luau"` /// /// Filesystem names are derived from `instigating_source` paths (the actual /// file/dir on disk). For instances without filesystem backing (inside .rbxm diff --git a/src/snapshot/tree.rs b/src/snapshot/tree.rs index a8085cf1..c66ecd30 100644 --- a/src/snapshot/tree.rs +++ b/src/snapshot/tree.rs @@ -237,7 +237,7 @@ impl RojoTree { /// instances (derived from `instigating_source` path), falling back to /// the instance name for instances without filesystem backing. /// - /// Path format: `"Workspace/Model.model.json5/Attachment~1.luau"` + /// Path format: `"Workspace/Model.model.json5/Attachment~2.luau"` /// Segments are separated by `/` (no escaping needed since filesystem /// names can't contain `/`). pub fn get_instance_by_path(&self, path: &str) -> Option { diff --git a/src/snapshot_middleware/project.rs b/src/snapshot_middleware/project.rs index 22919405..4454dac6 100644 --- a/src/snapshot_middleware/project.rs +++ b/src/snapshot_middleware/project.rs @@ -763,7 +763,7 @@ pub fn syncback_project<'sync>( // Sort remaining children by name for deterministic dedup ordering. // Without this, HashMap iteration order determines which sibling gets - // the base slug vs ~1, causing non-deterministic output across runs. + // the base slug vs ~2, causing non-deterministic output across runs. let mut remaining_children: Vec<_> = new_child_map.drain().collect(); remaining_children.sort_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b)); for (name, new_child) in remaining_children { diff --git a/src/syncback/dedup_suffix.rs b/src/syncback/dedup_suffix.rs index f5edfe25..307e6192 100644 --- a/src/syncback/dedup_suffix.rs +++ b/src/syncback/dedup_suffix.rs @@ -1,12 +1,12 @@ //! Dedup suffix lifecycle management. //! //! When instances have duplicate names, they receive `~N` suffixes on the -//! filesystem (e.g., `Foo`, `Foo~1`, `Foo~2`). This module provides shared +//! filesystem (e.g., `Foo`, `Foo~2`, `Foo~3`). This module provides shared //! helpers for managing suffix cleanup when instances are deleted. //! //! Rules: //! - **Gap-tolerant:** deleting a suffixed instance does NOT renumber remaining -//! siblings. Gaps (`Foo`, `Foo~2` without `~1`) are harmless. +//! siblings. Gaps (`Foo`, `Foo~3` without `~2`) are harmless. //! - **Base-name promotion:** when the base-name holder is deleted and multiple //! siblings remain, the lowest-numbered suffix is promoted to base name. //! - **Group-to-1 cleanup:** when a deletion reduces the dedup group to exactly diff --git a/src/syncback/file_names.rs b/src/syncback/file_names.rs index 0dacda52..7636c049 100644 --- a/src/syncback/file_names.rs +++ b/src/syncback/file_names.rs @@ -267,7 +267,7 @@ pub fn slugify_name(name: &str) -> String { result } -/// Appends ~1, ~2, etc. to the slug to avoid filesystem-level collisions. +/// Appends ~2, ~3, etc. to the slug to avoid filesystem-level collisions. /// Returns `(deduped_slug, full_fs_name)`. /// /// `extension` is `None` for directory middleware, `Some("server.luau")` etc. @@ -294,7 +294,7 @@ pub fn deduplicate_name_with_ext( if !taken_names.contains(&full_name.to_lowercase()) { return (base.to_string(), full_name); } - for i in 1.. { + for i in 2.. { let candidate_slug = format!("{base}~{i}"); let candidate_full = build_full(&candidate_slug); if !taken_names.contains(&candidate_full.to_lowercase()) { @@ -304,7 +304,7 @@ pub fn deduplicate_name_with_ext( unreachable!() } -/// Legacy wrapper: appends ~1, ~2, etc. to avoid collisions using bare slug +/// Legacy wrapper: appends ~2, ~3, etc. to avoid collisions using bare slug /// as the collision key. Used by callers that don't yet use full filesystem /// name dedup (e.g., change_processor.rs renames). pub fn deduplicate_name(base: &str, taken_names: &std::collections::HashSet) -> String { @@ -312,7 +312,7 @@ pub fn deduplicate_name(base: &str, taken_names: &std::collections::HashSet = ["foo".to_string()].into_iter().collect(); - assert_eq!(deduplicate_name("Foo", &taken), "Foo~1"); + assert_eq!(deduplicate_name("Foo", &taken), "Foo~2"); } #[test] @@ -589,14 +589,14 @@ mod tests { #[test] fn dedup_gap_in_suffixes() { - let taken: HashSet = ["foo", "foo~2"].into_iter().map(String::from).collect(); - assert_eq!(deduplicate_name("Foo", &taken), "Foo~1"); + let taken: HashSet = ["foo", "foo~3"].into_iter().map(String::from).collect(); + assert_eq!(deduplicate_name("Foo", &taken), "Foo~2"); } #[test] fn dedup_natural_vs_slug_collision() { let taken: HashSet = ["hey_bro".to_string()].into_iter().collect(); - assert_eq!(deduplicate_name("Hey_Bro", &taken), "Hey_Bro~1"); + assert_eq!(deduplicate_name("Hey_Bro", &taken), "Hey_Bro~2"); } #[test] @@ -609,16 +609,16 @@ mod tests { fn dedup_case_insensitive() { // "foo" is taken, "Foo" should collide (case-insensitive filesystem) let taken: HashSet = ["foo".to_string()].into_iter().collect(); - assert_eq!(deduplicate_name("Foo", &taken), "Foo~1"); - assert_eq!(deduplicate_name("FOO", &taken), "FOO~1"); - assert_eq!(deduplicate_name("foo", &taken), "foo~1"); + assert_eq!(deduplicate_name("Foo", &taken), "Foo~2"); + assert_eq!(deduplicate_name("FOO", &taken), "FOO~2"); + assert_eq!(deduplicate_name("foo", &taken), "foo~2"); } #[test] fn dedup_case_only_difference() { - // Two instances: "MyScript" and "myscript" - second must get ~1 + // Two instances: "MyScript" and "myscript" - second must get ~2 let taken: HashSet = ["myscript".to_string()].into_iter().collect(); - assert_eq!(deduplicate_name("MyScript", &taken), "MyScript~1"); + assert_eq!(deduplicate_name("MyScript", &taken), "MyScript~2"); } // ── name_needs_slugify ────────────────────────────────────────────── @@ -831,7 +831,7 @@ mod tests { let (filename, needs_meta, _dk) = name_for_inst(Middleware::ModuleScript, child, None, &taken).unwrap(); - assert_eq!(filename.as_ref(), "Foo~1.luau"); + assert_eq!(filename.as_ref(), "Foo~2.luau"); assert!(needs_meta, "deduped name differs from original"); } @@ -844,7 +844,7 @@ mod tests { let (filename, needs_meta, _dk) = name_for_inst(Middleware::Dir, child, None, &taken).unwrap(); - assert_eq!(filename.as_ref(), "Stuff~1"); + assert_eq!(filename.as_ref(), "Stuff~2"); assert!(needs_meta); } @@ -858,7 +858,7 @@ mod tests { let (filename, needs_meta, _dk) = name_for_inst(Middleware::ModuleScript, child, None, &taken).unwrap(); - assert_eq!(filename.as_ref(), "Hey_Bro~1.luau"); + assert_eq!(filename.as_ref(), "Hey_Bro~2.luau"); assert!(needs_meta); } @@ -1145,7 +1145,7 @@ mod tests { fn nfi_slug_collides_with_existing_dir() { // "Hey/Bro" slugifies to "Hey_Bro", which is already taken as a dir name let (f, m) = nfi("Hey/Bro", Middleware::Dir, &["hey_bro"]); - assert_eq!(f, "Hey_Bro~1"); + assert_eq!(f, "Hey_Bro~2"); assert!(m); } @@ -1153,15 +1153,15 @@ mod tests { fn nfi_slug_collides_with_natural_dir() { // Dir "Hey_Bro" exists naturally, then "Hey:Bro" slugifies to same let (f, m) = nfi("Hey:Bro", Middleware::Dir, &["hey_bro"]); - assert_eq!(f, "Hey_Bro~1"); + assert_eq!(f, "Hey_Bro~2"); assert!(m); } #[test] fn nfi_two_slugs_collide_dir() { - // Both "A/B" and "A:B" slugify to "A_B". First taken, second gets ~1 + // Both "A/B" and "A:B" slugify to "A_B". First taken, second gets ~2 let (f, m) = nfi("A:B", Middleware::Dir, &["a_b"]); - assert_eq!(f, "A_B~1"); + assert_eq!(f, "A_B~2"); assert!(m); } @@ -1173,9 +1173,9 @@ mod tests { #[test] fn nfi_clean_name_dedup_still_needs_meta_dir() { - // Clean name "Foo", but "foo" is already taken → dedup adds ~1 → needs meta + // Clean name "Foo", but "foo" is already taken → dedup adds ~2 → needs meta let (f, m) = nfi("Foo", Middleware::Dir, &["foo"]); - assert_eq!(f, "Foo~1"); + assert_eq!(f, "Foo~2"); assert!( m, "dedup suffix means filesystem name differs from instance name" @@ -1185,7 +1185,7 @@ mod tests { #[test] fn nfi_case_collision_dir() { let (f, m) = nfi("Assets", Middleware::Dir, &["assets"]); - assert_eq!(f, "Assets~1"); + assert_eq!(f, "Assets~2"); assert!(m); } @@ -1193,7 +1193,7 @@ mod tests { fn nfi_slug_collision_file_middleware() { // For file middleware, dedup compares full filesystem name against taken. let (f, m) = nfi("Hey/Bro", Middleware::ModuleScript, &["hey_bro.luau"]); - assert_eq!(f, "Hey_Bro~1.luau"); + assert_eq!(f, "Hey_Bro~2.luau"); assert!(m); } @@ -1222,7 +1222,7 @@ mod tests { let r = process_siblings_dir(&["Foo", "Foo"]); assert_eq!(r[0].1, "Foo"); assert!(!r[0].2); - assert_eq!(r[1].1, "Foo~1"); + assert_eq!(r[1].1, "Foo~2"); assert!(r[1].2); } @@ -1230,8 +1230,8 @@ mod tests { fn ordering_three_identical_names() { let r = process_siblings_dir(&["X", "X", "X"]); assert_eq!(r[0].1, "X"); - assert_eq!(r[1].1, "X~1"); - assert_eq!(r[2].1, "X~2"); + assert_eq!(r[1].1, "X~2"); + assert_eq!(r[2].1, "X~3"); } #[test] @@ -1240,7 +1240,7 @@ mod tests { let r = process_siblings_dir(&["Hey_Bro", "Hey/Bro"]); assert_eq!(r[0].1, "Hey_Bro"); assert!(!r[0].2); - assert_eq!(r[1].1, "Hey_Bro~1"); + assert_eq!(r[1].1, "Hey_Bro~2"); assert!(r[1].2); } @@ -1250,7 +1250,7 @@ mod tests { let r = process_siblings_dir(&["Hey/Bro", "Hey_Bro"]); assert_eq!(r[0].1, "Hey_Bro"); assert!(r[0].2); - assert_eq!(r[1].1, "Hey_Bro~1"); + assert_eq!(r[1].1, "Hey_Bro~2"); assert!(r[1].2, "dedup suffix → needs meta even for natural name"); } @@ -1259,9 +1259,9 @@ mod tests { let r = process_siblings_dir(&["Script", "script", "SCRIPT"]); assert_eq!(r[0].1, "Script"); assert!(!r[0].2); - assert_eq!(r[1].1, "script~1"); + assert_eq!(r[1].1, "script~2"); assert!(r[1].2); - assert_eq!(r[2].1, "SCRIPT~2"); + assert_eq!(r[2].1, "SCRIPT~3"); assert!(r[2].2); } @@ -1270,8 +1270,8 @@ mod tests { // All three slugify to "A_B" let r = process_siblings_dir(&["A/B", "A:B", "A*B"]); assert_eq!(r[0].1, "A_B"); - assert_eq!(r[1].1, "A_B~1"); - assert_eq!(r[2].1, "A_B~2"); + assert_eq!(r[1].1, "A_B~2"); + assert_eq!(r[2].1, "A_B~3"); assert!(r[0].2); assert!(r[1].2); assert!(r[2].2); @@ -1293,7 +1293,7 @@ mod tests { let r = process_siblings_dir(&["CON", "CON_"]); assert_eq!(r[0].1, "CON_"); assert!(r[0].2); - assert_eq!(r[1].1, "CON_~1"); + assert_eq!(r[1].1, "CON_~2"); assert!(r[1].2); } @@ -1302,17 +1302,17 @@ mod tests { let r = process_siblings_dir(&[ "Utils", // first claim "utils", // case collision with Utils - "Utils", // exact collision (already taken + ~1 taken) + "Utils", // exact collision (already taken + ~2 taken) "UTILS", // case collision "Uti/ls", // different slug "Uti_ls", no collision ]); assert_eq!(r[0].1, "Utils"); assert!(!r[0].2); - assert_eq!(r[1].1, "utils~1"); + assert_eq!(r[1].1, "utils~2"); assert!(r[1].2); - assert_eq!(r[2].1, "Utils~2"); + assert_eq!(r[2].1, "Utils~3"); assert!(r[2].2); - assert_eq!(r[3].1, "UTILS~3"); + assert_eq!(r[3].1, "UTILS~4"); assert!(r[3].2); assert_eq!(r[4].1, "Uti_ls"); assert!(r[4].2); @@ -1323,20 +1323,20 @@ mod tests { // All six slugify to "X_Y" as dirs let r = process_siblings_dir(&["X/Y", "X:Y", "X*Y", "X?Y", "XY"]); assert_eq!(r[0].1, "X_Y"); - assert_eq!(r[1].1, "X_Y~1"); - assert_eq!(r[2].1, "X_Y~2"); - assert_eq!(r[3].1, "X_Y~3"); - assert_eq!(r[4].1, "X_Y~4"); - assert_eq!(r[5].1, "X_Y~5"); + assert_eq!(r[1].1, "X_Y~2"); + assert_eq!(r[2].1, "X_Y~3"); + assert_eq!(r[3].1, "X_Y~4"); + assert_eq!(r[4].1, "X_Y~5"); + assert_eq!(r[5].1, "X_Y~6"); } #[test] fn ordering_tilde_in_name_vs_dedup_suffix() { - // "Foo~1" (tilde slugified to "Foo_1") vs dedup suffix "Foo~1" - // The slugified "Foo~1" → "Foo_1" which is different from dedup "Foo~1" + // "Foo~1" (tilde slugified to "Foo_1") vs dedup suffix "Foo~2" + // The slugified "Foo~1" → "Foo_1" which is different from dedup "Foo~2" let r = process_siblings_dir(&["Foo", "Foo", "Foo~1"]); assert_eq!(r[0].1, "Foo"); - assert_eq!(r[1].1, "Foo~1"); // dedup suffix + assert_eq!(r[1].1, "Foo~2"); // dedup suffix assert_eq!(r[2].1, "Foo_1"); // tilde was slugified to _ assert!(r[2].2); } @@ -1350,9 +1350,9 @@ mod tests { ]); assert_eq!(r[0].1, "Leading"); assert!(r[0].2, "leading space was stripped"); - assert_eq!(r[1].1, "Leading~1"); + assert_eq!(r[1].1, "Leading~2"); assert!(r[1].2); - assert_eq!(r[2].1, "Leading~2"); + assert_eq!(r[2].1, "Leading~3"); assert!(r[2].2); } @@ -1556,9 +1556,9 @@ mod tests { #[test] fn disk_seed_prevents_overwrite_of_existing_dir() { // "Utils" directory exists on disk. New instance "Utils" (dir) - // must get "Utils~1". + // must get "Utils~2". let r = process_new_children_with_disk_seed(&["utils"], &[("Utils", Middleware::Dir)]); - assert_eq!(r[0].1, "Utils~1"); + assert_eq!(r[0].1, "Utils~2"); assert!(r[0].2); } @@ -1566,16 +1566,16 @@ mod tests { fn disk_seed_prevents_overwrite_of_existing_dir_case_insensitive() { // "SCRIPTS" directory on disk, new instance "Scripts" (dir) must dedup. let r = process_new_children_with_disk_seed(&["scripts"], &[("Scripts", Middleware::Dir)]); - assert_eq!(r[0].1, "Scripts~1"); + assert_eq!(r[0].1, "Scripts~2"); assert!(r[0].2); } #[test] fn disk_seed_slug_collision_with_existing_dir() { // "Hey_Bro" directory on disk. New instance "Hey/Bro" slugifies - // to "Hey_Bro" — must dedup to "Hey_Bro~1". + // to "Hey_Bro" — must dedup to "Hey_Bro~2". let r = process_new_children_with_disk_seed(&["hey_bro"], &[("Hey/Bro", Middleware::Dir)]); - assert_eq!(r[0].1, "Hey_Bro~1"); + assert_eq!(r[0].1, "Hey_Bro~2"); assert!(r[0].2); } @@ -1591,9 +1591,9 @@ mod tests { ("NewThing", Middleware::Dir), ], ); - assert_eq!(r[0].1, "Utils~1", "Utils collides with utils/ on disk"); + assert_eq!(r[0].1, "Utils~2", "Utils collides with utils/ on disk"); assert!(r[0].2); - assert_eq!(r[1].1, "Config~1", "Config collides with config/ on disk"); + assert_eq!(r[1].1, "Config~2", "Config collides with config/ on disk"); assert!(r[1].2); assert_eq!(r[2].1, "NewThing", "NewThing is clean"); assert!(!r[2].2); @@ -1607,7 +1607,7 @@ mod tests { &["stuff", "stuff.meta.json5", "other.luau"], &[("Stuff", Middleware::Dir)], ); - assert_eq!(r[0].1, "Stuff~1"); + assert_eq!(r[0].1, "Stuff~2"); assert!(r[0].2); } @@ -1620,7 +1620,7 @@ mod tests { &["mymodule.luau"], &[("MyModule", Middleware::ModuleScript)], ); - assert_eq!(r[0].1, "MyModule~1.luau"); + assert_eq!(r[0].1, "MyModule~2.luau"); assert!(r[0].2, "dedup suffix means needs meta"); } @@ -1628,7 +1628,7 @@ mod tests { fn disk_seed_file_middleware_sibling_slug_collision_deduped() { // Two file-middleware children whose names slugify to the same base. // Since taken_names accumulates dedup_keys (bare slugs), the second - // child correctly detects the collision and gets ~1. + // child correctly detects the collision and gets ~2. let r = process_new_children_with_disk_seed( &[], &[ @@ -1638,7 +1638,7 @@ mod tests { ); assert_eq!(r[0].1, "A_B.luau"); assert!(r[0].2); - assert_eq!(r[1].1, "A_B~1.luau"); + assert_eq!(r[1].1, "A_B~2.luau"); assert!(r[1].2); } @@ -1727,12 +1727,12 @@ mod tests { fn project_siblings_same_slug_different_names() { // Two new project children: "A/B" and "A:B" both slug to "A_B". // Before fix: both got "A_B" path (collision!). - // After fix: second gets "A_B~1". + // After fix: second gets "A_B~2". let r = process_project_siblings(&[], &[("A/B", Middleware::Dir), ("A:B", Middleware::Dir)]); assert_eq!(r[0].1, "A_B"); assert!(r[0].2); - assert_eq!(r[1].1, "A_B~1"); + assert_eq!(r[1].1, "A_B~2"); assert!(r[1].2); } @@ -1746,13 +1746,13 @@ mod tests { ); assert_eq!(r[0].1, "Hey_Bro"); assert!(!r[0].2); - assert_eq!(r[1].1, "Hey_Bro~1"); + assert_eq!(r[1].1, "Hey_Bro~2"); assert!(r[1].2); } #[test] fn project_siblings_three_way_slug_collision() { - // Three siblings all slug to "X_Y" — must get X_Y, X_Y~1, X_Y~2. + // Three siblings all slug to "X_Y" — must get X_Y, X_Y~2, X_Y~3. let r = process_project_siblings( &[], &[ @@ -1762,22 +1762,22 @@ mod tests { ], ); assert_eq!(r[0].1, "X_Y"); - assert_eq!(r[1].1, "X_Y~1"); - assert_eq!(r[2].1, "X_Y~2"); + assert_eq!(r[1].1, "X_Y~2"); + assert_eq!(r[2].1, "X_Y~3"); } #[test] fn project_siblings_disk_plus_siblings_collision_dir() { // "Helper" directory already on disk. Two new dir siblings: "Helper" x2. - // First → "Helper~1" (disk has "helper"). - // Second → "Helper~2" (both disk and first sibling taken). + // First → "Helper~2" (disk has "helper"). + // Second → "Helper~3" (both disk and first sibling taken). let r = process_project_siblings( &["helper"], &[("Helper", Middleware::Dir), ("Helper", Middleware::Dir)], ); - assert_eq!(r[0].1, "Helper~1"); + assert_eq!(r[0].1, "Helper~2"); assert!(r[0].2); - assert_eq!(r[1].1, "Helper~2"); + assert_eq!(r[1].1, "Helper~3"); assert!(r[1].2); } @@ -1795,11 +1795,11 @@ mod tests { ); assert_eq!(r[0].1, "Test"); assert!(!r[0].2); - assert_eq!(r[1].1, "test~1"); + assert_eq!(r[1].1, "test~2"); assert!(r[1].2); - assert_eq!(r[2].1, "TEST~2"); + assert_eq!(r[2].1, "TEST~3"); assert!(r[2].2); - assert_eq!(r[3].1, "tEsT~3"); + assert_eq!(r[3].1, "tEsT~4"); assert!(r[3].2); } @@ -1819,7 +1819,7 @@ mod tests { let r = process_siblings_dir(&["CON", "CON/"]); assert_eq!(r[0].1, "CON_"); assert!(r[0].2); - assert_eq!(r[1].1, "CON_~1"); + assert_eq!(r[1].1, "CON_~2"); assert!(r[1].2); } @@ -1829,29 +1829,29 @@ mod tests { let r = process_siblings_dir(&["foo.server", "foo/server"]); assert_eq!(r[0].1, "foo_server"); assert!(r[0].2); - assert_eq!(r[1].1, "foo_server~1"); + assert_eq!(r[1].1, "foo_server~2"); assert!(r[1].2); } #[test] fn nightmare_tilde_looks_like_dedup_but_gets_slugified() { // "Foo~1" has tilde slugified to "Foo_1". Then real "Foo" gets - // dedup "Foo~1". These should NOT collide because tilde-in-name + // dedup "Foo~2". These should NOT collide because tilde-in-name // becomes underscore, but dedup adds actual tilde. let r = process_siblings_dir(&["Foo~1", "Foo", "Foo"]); assert_eq!(r[0].1, "Foo_1", "tilde slugified"); assert_eq!(r[1].1, "Foo", "clean, no collision"); - assert_eq!(r[2].1, "Foo~1", "dedup suffix, different from Foo_1"); + assert_eq!(r[2].1, "Foo~2", "dedup suffix, different from Foo_1"); } #[test] fn nightmare_dedup_suffix_avoids_slugified_tilde_name() { - // Process "Foo", then "Foo" again (dedup to "Foo~1"), then "Foo~1" - // (tilde slugified to "Foo_1"). The dedup "Foo~1" and slug "Foo_1" + // Process "Foo", then "Foo" again (dedup to "Foo~2"), then "Foo~1" + // (tilde slugified to "Foo_1"). The dedup "Foo~2" and slug "Foo_1" // should coexist. let r = process_siblings_dir(&["Foo", "Foo", "Foo~1"]); assert_eq!(r[0].1, "Foo"); - assert_eq!(r[1].1, "Foo~1"); + assert_eq!(r[1].1, "Foo~2"); assert_eq!(r[2].1, "Foo_1"); // All three have distinct lowercased names let names: HashSet = r.iter().map(|(_, f, _)| f.to_lowercase()).collect(); @@ -1863,7 +1863,7 @@ mod tests { // "カフェ/Bar" → "カフェ_Bar", "カフェ:Bar" → "カフェ_Bar" — collision let r = process_siblings_dir(&["カフェ/Bar", "カフェ:Bar"]); assert_eq!(r[0].1, "カフェ_Bar"); - assert_eq!(r[1].1, "カフェ_Bar~1"); + assert_eq!(r[1].1, "カフェ_Bar~2"); } #[test] @@ -1872,7 +1872,7 @@ mod tests { let r = process_siblings_dir(&["🎮 Games", "🎮 Games"]); assert_eq!(r[0].1, "🎮 Games"); assert!(!r[0].2, "emoji name is clean"); - assert_eq!(r[1].1, "🎮 Games~1"); + assert_eq!(r[1].1, "🎮 Games~2"); assert!(r[1].2); } @@ -1883,9 +1883,9 @@ mod tests { let r = process_siblings_dir(&["CON", "CON_", "con"]); assert_eq!(r[0].1, "CON_"); assert!(r[0].2); - assert_eq!(r[1].1, "CON_~1"); + assert_eq!(r[1].1, "CON_~2"); assert!(r[1].2, "natural CON_ collides with reserved CON's slug"); - assert_eq!(r[2].1, "con_~2"); + assert_eq!(r[2].1, "con_~3"); assert!(r[2].2); } @@ -1895,7 +1895,7 @@ mod tests { let r = process_siblings_dir(&["test.", "test"]); assert_eq!(r[0].1, "test"); assert!(r[0].2, "trailing dot stripped → slug differs"); - assert_eq!(r[1].1, "test~1"); + assert_eq!(r[1].1, "test~2"); assert!(r[1].2); } @@ -1905,9 +1905,9 @@ mod tests { let r = process_siblings_dir(&[" A", "A", " A"]); assert_eq!(r[0].1, "A"); assert!(r[0].2, "leading space stripped"); - assert_eq!(r[1].1, "A~1"); + assert_eq!(r[1].1, "A~2"); assert!(r[1].2); - assert_eq!(r[2].1, "A~2"); + assert_eq!(r[2].1, "A~3"); assert!(r[2].2); } @@ -2004,11 +2004,7 @@ mod tests { ]; let r = process_new_children_with_disk_seed(&["a_b"], &new_children); for (i, (_, filename, needs_meta)) in r.iter().enumerate() { - let expected = if i == 0 { - "A_B~1".to_string() - } else { - format!("A_B~{}", i + 1) - }; + let expected = format!("A_B~{}", i + 2); assert_eq!(filename, &expected, "child {i}"); assert!(needs_meta, "child {i} needs meta"); } @@ -2019,8 +2015,8 @@ mod tests { // Multiple instances with empty names. All slugify to "instance". let r = process_siblings_dir(&["", "", ""]); assert_eq!(r[0].1, "instance"); - assert_eq!(r[1].1, "instance~1"); - assert_eq!(r[2].1, "instance~2"); + assert_eq!(r[1].1, "instance~2"); + assert_eq!(r[2].1, "instance~3"); } #[test] @@ -2028,10 +2024,10 @@ mod tests { // Names that are entirely forbidden chars → all become "instance". let r = process_siblings_dir(&["<>", ":/", "?*", "|\\", "\"~"]); assert_eq!(r[0].1, "instance"); - assert_eq!(r[1].1, "instance~1"); - assert_eq!(r[2].1, "instance~2"); - assert_eq!(r[3].1, "instance~3"); - assert_eq!(r[4].1, "instance~4"); + assert_eq!(r[1].1, "instance~2"); + assert_eq!(r[2].1, "instance~3"); + assert_eq!(r[3].1, "instance~4"); + assert_eq!(r[4].1, "instance~5"); } #[test] @@ -2039,7 +2035,7 @@ mod tests { // "Hello\x00World" → "Hello_World", "Hello/World" → "Hello_World" let r = process_siblings_dir(&["Hello\x00World", "Hello/World"]); assert_eq!(r[0].1, "Hello_World"); - assert_eq!(r[1].1, "Hello_World~1"); + assert_eq!(r[1].1, "Hello_World~2"); } #[test] @@ -2059,7 +2055,7 @@ mod tests { // "init/meta" → slug "init_meta" → collision! let r = process_siblings_dir(&["init.meta", "init/meta"]); assert_eq!(r[0].1, "init_meta"); - assert_eq!(r[1].1, "init_meta~1"); + assert_eq!(r[1].1, "init_meta~2"); } #[test] @@ -2068,9 +2064,9 @@ mod tests { let r = process_siblings_dir(&["a.project", "a/project", "a_project"]); assert_eq!(r[0].1, "a_project"); assert!(r[0].2); - assert_eq!(r[1].1, "a_project~1"); + assert_eq!(r[1].1, "a_project~2"); assert!(r[1].2); - assert_eq!(r[2].1, "a_project~2"); + assert_eq!(r[2].1, "a_project~3"); assert!(r[2].2, "natural name collides with slugified siblings"); } @@ -2082,7 +2078,7 @@ mod tests { assert_eq!(r[0].1, "Script"); assert!(!r[0].2); for (i, entry) in r.iter().enumerate().skip(1) { - assert_eq!(entry.1, format!("Script~{i}")); + assert_eq!(entry.1, format!("Script~{}", i + 1)); assert!(entry.2); } // All filenames must be unique @@ -2097,22 +2093,22 @@ mod tests { let r = process_siblings_dir(&[ "A_B", // clean, claims "A_B" "C_D", // clean, claims "C_D" - "A/B", // slug "A_B" → collision → "A_B~1" + "A/B", // slug "A_B" → collision → "A_B~2" "E_F", // clean, claims "E_F" - "C:D", // slug "C_D" → collision → "C_D~1" - "A:B", // slug "A_B" → collision with A_B and A_B~1 → "A_B~2" + "C:D", // slug "C_D" → collision → "C_D~2" + "A:B", // slug "A_B" → collision with A_B and A_B~2 → "A_B~3" ]); assert_eq!(r[0].1, "A_B"); assert!(!r[0].2); assert_eq!(r[1].1, "C_D"); assert!(!r[1].2); - assert_eq!(r[2].1, "A_B~1"); + assert_eq!(r[2].1, "A_B~2"); assert!(r[2].2); assert_eq!(r[3].1, "E_F"); assert!(!r[3].2); - assert_eq!(r[4].1, "C_D~1"); + assert_eq!(r[4].1, "C_D~2"); assert!(r[4].2); - assert_eq!(r[5].1, "A_B~2"); + assert_eq!(r[5].1, "A_B~3"); assert!(r[5].2); } @@ -2317,7 +2313,7 @@ mod tests { #[test] fn tilde_dedup_collision_roundtrip() { // Two instances "A/B" and "A_B" both slugify to "A_B". - // First gets "A_B", second gets "A_B~1". + // First gets "A_B", second gets "A_B~2". let mut taken = HashSet::new(); let dom1 = make_inst("A/B", "ModuleScript"); @@ -2336,8 +2332,8 @@ mod tests { assert_eq!(name1.as_ref(), "A_B.luau"); assert!(meta1, "A/B was slugified, needs meta name"); - assert_eq!(name2.as_ref(), "A_B~1.luau"); - assert!(meta2, "A_B was deduped to ~1, needs meta name"); + assert_eq!(name2.as_ref(), "A_B~2.luau"); + assert!(meta2, "A_B was deduped to ~2, needs meta name"); } #[test] @@ -2486,7 +2482,7 @@ mod tests { assert_eq!(f1.as_ref(), "A_B.luau"); assert!(m1); - assert_eq!(f2.as_ref(), "A_B~1.luau"); + assert_eq!(f2.as_ref(), "A_B~2.luau"); assert!(m2); assert_ne!(dk1.to_lowercase(), dk2.to_lowercase()); } diff --git a/src/syncback/fs_snapshot.rs b/src/syncback/fs_snapshot.rs index bf97290a..ee9e5326 100644 --- a/src/syncback/fs_snapshot.rs +++ b/src/syncback/fs_snapshot.rs @@ -535,7 +535,7 @@ mod tests { meta_content.as_bytes().to_vec(), ); - let substitutions = vec![("Workspace/Foo".to_string(), "Workspace/Foo~1".to_string())]; + let substitutions = vec![("Workspace/Foo".to_string(), "Workspace/Foo~2".to_string())]; snap.fix_ref_paths(&substitutions); let result = std::str::from_utf8( @@ -546,7 +546,7 @@ mod tests { .unwrap(); assert!( - result.contains(r#""Rojo_Ref_PrimaryPart": "Workspace/Foo~1""#), + result.contains(r#""Rojo_Ref_PrimaryPart": "Workspace/Foo~2""#), "Rojo_Ref line should be updated. Got:\n{}", result ); @@ -566,7 +566,7 @@ mod tests { b"-- Rojo_Ref_PrimaryPart Workspace/Foo".to_vec(), ); - let substitutions = vec![("Workspace/Foo".to_string(), "Workspace/Foo~1".to_string())]; + let substitutions = vec![("Workspace/Foo".to_string(), "Workspace/Foo~2".to_string())]; snap.fix_ref_paths(&substitutions); let result = std::str::from_utf8( @@ -581,7 +581,7 @@ mod tests { result ); assert!( - !result.contains("Workspace/Foo~1"), + !result.contains("Workspace/Foo~2"), "Non-meta file should not have substitution applied" ); } diff --git a/src/syncback/ref_properties.rs b/src/syncback/ref_properties.rs index a9301c84..be9069bd 100644 --- a/src/syncback/ref_properties.rs +++ b/src/syncback/ref_properties.rs @@ -181,7 +181,7 @@ pub fn collect_all_paths(dom: &WeakDom) -> HashMap { /// /// The `final_paths` parameter, when provided, contains the definitive /// filesystem-name-based paths assigned during the syncback walk (including -/// dedup suffixes like `~1`). These take priority over `tentative_fs_path()`. +/// dedup suffixes like `~2`). These take priority over `tentative_fs_path()`. pub fn collect_referents( dom: &WeakDom, pre_prune_paths: &HashMap, diff --git a/src/web/api.rs b/src/web/api.rs index 380873f5..31ceb1ec 100644 --- a/src/web/api.rs +++ b/src/web/api.rs @@ -553,7 +553,7 @@ impl ApiService { // Pre-seed sibling_slugs from the tree's existing children // of this parent so new instances dedup against existing ones. // We derive slugs from actual filesystem paths (via instigating_source) - // to correctly account for dedup suffixes (e.g., Hey_Bro~1). + // to correctly account for dedup suffixes (e.g., Hey_Bro~2). let mut sibling_slugs: HashSet = if *parent_ref != Ref::none() { if let Some(parent_inst) = tree.get_instance(*parent_ref) { use crate::snapshot::InstigatingSource; @@ -6436,7 +6436,7 @@ mod tests { #[test] fn dir_clean_name_collides_with_existing() { let (f, m) = simulate_dir_dedup(&["NewFolder"], "NewFolder"); - assert_eq!(f, "NewFolder~1"); + assert_eq!(f, "NewFolder~2"); assert_eq!(m.unwrap(), "NewFolder"); } @@ -6450,21 +6450,21 @@ mod tests { #[test] fn dir_forbidden_collides_with_existing() { let (f, m) = simulate_dir_dedup(&["Hey_Bro"], "Hey/Bro"); - assert_eq!(f, "Hey_Bro~1"); + assert_eq!(f, "Hey_Bro~2"); assert_eq!(m.unwrap(), "Hey/Bro"); } #[test] fn dir_natural_collides_with_slug() { let (f, m) = simulate_dir_dedup(&["A_B"], "A_B"); - assert_eq!(f, "A_B~1"); + assert_eq!(f, "A_B~2"); assert_eq!(m.unwrap(), "A_B"); } #[test] fn dir_case_insensitive_collision() { let (f, m) = simulate_dir_dedup(&["MyFolder"], "myfolder"); - assert_eq!(f, "myfolder~1"); + assert_eq!(f, "myfolder~2"); assert_eq!(m.unwrap(), "myfolder"); } @@ -6490,7 +6490,7 @@ mod tests { #[test] fn dir_dangerous_suffix_collides() { let (f, m) = simulate_dir_dedup(&["config_meta"], "config.meta"); - assert_eq!(f, "config_meta~1"); + assert_eq!(f, "config_meta~2"); assert_eq!(m.unwrap(), "config.meta"); } @@ -6504,7 +6504,7 @@ mod tests { #[test] fn dir_nul_collides_with_existing() { let (f, _) = simulate_dir_dedup(&["NUL_"], "NUL"); - assert_eq!(f, "NUL_~1"); + assert_eq!(f, "NUL_~2"); } // ── Batch add: directory siblings ───────────────────────────── @@ -6523,24 +6523,24 @@ mod tests { let r = simulate_dir_batch(&[], &["Script", "Script", "Script"]); assert_eq!(r[0].0, "Script"); assert!(r[0].1.is_none()); - assert_eq!(r[1].0, "Script~1"); + assert_eq!(r[1].0, "Script~2"); assert_eq!(r[1].1.as_deref(), Some("Script")); - assert_eq!(r[2].0, "Script~2"); + assert_eq!(r[2].0, "Script~3"); } #[test] fn batch_dir_slug_collision_siblings() { let r = simulate_dir_batch(&[], &["X/Y", "X:Y", "X*Y"]); assert_eq!(r[0].0, "X_Y"); - assert_eq!(r[1].0, "X_Y~1"); - assert_eq!(r[2].0, "X_Y~2"); + assert_eq!(r[1].0, "X_Y~2"); + assert_eq!(r[2].0, "X_Y~3"); } #[test] fn batch_dir_with_existing_and_collisions() { let r = simulate_dir_batch(&["Utils", "Config"], &["Utils", "Config", "NewThing"]); - assert_eq!(r[0].0, "Utils~1"); - assert_eq!(r[1].0, "Config~1"); + assert_eq!(r[0].0, "Utils~2"); + assert_eq!(r[1].0, "Config~2"); assert_eq!(r[2].0, "NewThing"); assert!(r[2].1.is_none()); } @@ -6549,8 +6549,8 @@ mod tests { fn batch_dir_dangerous_then_natural_then_slug() { let r = simulate_dir_batch(&[], &["foo.server", "foo_server", "foo/server"]); assert_eq!(r[0].0, "foo_server"); - assert_eq!(r[1].0, "foo_server~1"); - assert_eq!(r[2].0, "foo_server~2"); + assert_eq!(r[1].0, "foo_server~2"); + assert_eq!(r[2].0, "foo_server~3"); } #[test] @@ -6558,9 +6558,9 @@ mod tests { let r = simulate_dir_batch(&[], &["CON", "CON_", "con"]); assert_eq!(r[0].0, "CON_"); assert_eq!(r[0].1.as_deref(), Some("CON")); - assert_eq!(r[1].0, "CON_~1"); + assert_eq!(r[1].0, "CON_~2"); assert_eq!(r[1].1.as_deref(), Some("CON_")); - assert_eq!(r[2].0, "con_~2"); + assert_eq!(r[2].0, "con_~3"); assert_eq!(r[2].1.as_deref(), Some("con")); } @@ -6568,8 +6568,8 @@ mod tests { fn batch_dir_empty_names() { let r = simulate_dir_batch(&[], &["", "", ""]); assert_eq!(r[0].0, "instance"); - assert_eq!(r[1].0, "instance~1"); - assert_eq!(r[2].0, "instance~2"); + assert_eq!(r[1].0, "instance~2"); + assert_eq!(r[2].0, "instance~3"); } #[test] @@ -6578,7 +6578,7 @@ mod tests { let r = simulate_dir_batch(&[], &children); assert_eq!(r[0].0, "Spam"); for (i, entry) in r.iter().enumerate().skip(1) { - assert_eq!(entry.0, format!("Spam~{i}")); + assert_eq!(entry.0, format!("Spam~{}", i + 1)); } let unique: HashSet = r.iter().map(|(f, _)| f.to_lowercase()).collect(); assert_eq!(unique.len(), 20); @@ -6594,7 +6594,7 @@ mod tests { let taken: HashSet = ["foo".to_string()].into_iter().collect(); // Bare slug "Foo" matches "foo" in taken let deduped = deduplicate_name("Foo", &taken); - assert_eq!(deduped, "Foo~1", "bare slug collides with existing sibling"); + assert_eq!(deduped, "Foo~2", "bare slug collides with existing sibling"); } #[test] @@ -6605,7 +6605,7 @@ mod tests { let slug = slugify_name("Hey/Bro"); assert_eq!(slug, "Hey_Bro"); let deduped = deduplicate_name(&slug, &taken); - assert_eq!(deduped, "Hey_Bro~1", "slug collision correctly detected"); + assert_eq!(deduped, "Hey_Bro~2", "slug collision correctly detected"); } // ── Rename → re-add cycle (the critical slug safety test) ──── @@ -6656,7 +6656,7 @@ mod tests { #[test] fn dir_add_to_populated_no_overwrite() { // Dir has: Helper/ and Helper.meta.json5 - // Add new "Helper" dir → must get Helper~1 + // Add new "Helper" dir → must get Helper~2 let temp = tempdir().expect("Failed to create temp dir"); fs::create_dir(temp.path().join("Helper")).unwrap(); File::create(temp.path().join("Helper.meta.json5")) @@ -6672,7 +6672,7 @@ mod tests { .collect(); let deduped = deduplicate_name("Helper", &taken); - assert_eq!(deduped, "Helper~1"); + assert_eq!(deduped, "Helper~2"); } // ── Nightmare batch scenarios ──────────────────────────────── @@ -6685,15 +6685,15 @@ mod tests { "A/B", "A:B", "A*B", "A?B", "AB", "A|B", "A\\B", "A\"B", ], ); - assert_eq!(r[0].0, "A_B~1"); - assert_eq!(r[1].0, "A_B~2"); - assert_eq!(r[2].0, "A_B~3"); - assert_eq!(r[3].0, "A_B~4"); - assert_eq!(r[4].0, "A_B~5"); - assert_eq!(r[5].0, "A_B~6"); - assert_eq!(r[6].0, "A_B~7"); - assert_eq!(r[7].0, "A_B~8"); - assert_eq!(r[8].0, "A_B~9"); + assert_eq!(r[0].0, "A_B~2"); + assert_eq!(r[1].0, "A_B~3"); + assert_eq!(r[2].0, "A_B~4"); + assert_eq!(r[3].0, "A_B~5"); + assert_eq!(r[4].0, "A_B~6"); + assert_eq!(r[5].0, "A_B~7"); + assert_eq!(r[6].0, "A_B~8"); + assert_eq!(r[7].0, "A_B~9"); + assert_eq!(r[8].0, "A_B~10"); } #[test] @@ -6701,16 +6701,16 @@ mod tests { let r = simulate_dir_batch(&[], &["CON", "PRN", "CON/", "PRN/"]); assert_eq!(r[0].0, "CON_"); assert_eq!(r[1].0, "PRN_"); - assert_eq!(r[2].0, "CON_~1"); // "CON/" slug "CON_" collides - assert_eq!(r[3].0, "PRN_~1"); + assert_eq!(r[2].0, "CON_~2"); // "CON/" slug "CON_" collides + assert_eq!(r[3].0, "PRN_~2"); } #[test] fn batch_dir_unicode_plus_forbidden() { let r = simulate_dir_batch(&[], &["カフェ/Bar", "カフェ:Bar", "カフェ_Bar"]); assert_eq!(r[0].0, "カフェ_Bar"); - assert_eq!(r[1].0, "カフェ_Bar~1"); - assert_eq!(r[2].0, "カフェ_Bar~2"); + assert_eq!(r[1].0, "カフェ_Bar~2"); + assert_eq!(r[2].0, "カフェ_Bar~3"); } #[test] diff --git a/tests/tests/connected_mode.rs b/tests/tests/connected_mode.rs index 3493780e..9d9a6177 100644 --- a/tests/tests/connected_mode.rs +++ b/tests/tests/connected_mode.rs @@ -1044,14 +1044,14 @@ fn delete_meta_name_reverts_to_stem() { // --------------------------------------------------------------------------- /// Test 28: Edit dedup file with meta. Patch should show instance named -/// "Hey/Bro" (from meta), not "Hey_Bro~1". +/// "Hey/Bro" (from meta), not "Hey_Bro~2". #[test] fn edit_dedup_file_with_meta() { run_serve_test("connected_slugify", |session, mut redactions| { let info = session.get_api_rojo().unwrap(); let root_id = info.root_instance_id; - let file_path = session.path().join("src/Hey_Bro~1.luau"); + let file_path = session.path().join("src/Hey_Bro~2.luau"); let socket_packet = session .recv_socket_packet(SocketPacketType::Messages, 0, || { @@ -1110,8 +1110,8 @@ fn remove_one_of_two_colliding_files() { let socket_packet = session .recv_socket_packet(SocketPacketType::Messages, 0, || { - fs::remove_file(src.join("Hey_Bro~1.luau")).unwrap(); - fs::remove_file(src.join("Hey_Bro~1.meta.json5")).unwrap(); + fs::remove_file(src.join("Hey_Bro~2.luau")).unwrap(); + fs::remove_file(src.join("Hey_Bro~2.meta.json5")).unwrap(); }) .unwrap(); assert_yaml_snapshot!( @@ -1138,9 +1138,9 @@ fn add_third_collision() { let socket_packet = session .recv_socket_packet(SocketPacketType::Messages, 0, || { - fs::write(src.join("Hey_Bro~2.meta.json5"), r#"{ "name": "Hey*Bro" }"#).unwrap(); + fs::write(src.join("Hey_Bro~3.meta.json5"), r#"{ "name": "Hey*Bro" }"#).unwrap(); fs::write( - src.join("Hey_Bro~2.luau"), + src.join("Hey_Bro~3.luau"), "-- third collision\nreturn 'Hey*Bro'", ) .unwrap(); @@ -1429,7 +1429,7 @@ fn slugify_rename_chain() { } /// Test 41: Collision evolution. Add third collision, remove one, reuse -/// the ~1 slot with a different name. Round-trip at each step. +/// the ~2 slot with a different name. Round-trip at each step. #[test] fn collision_evolution() { run_serve_test("connected_slugify", |session, mut redactions| { @@ -1442,11 +1442,11 @@ fn collision_evolution() { let src = session.path().join("src"); - // Step 1: Add third collision Hey_Bro~2 -> "Hey*Bro" + // Step 1: Add third collision Hey_Bro~3 -> "Hey*Bro" let packet = session .recv_socket_packet(SocketPacketType::Messages, 0, || { - fs::write(src.join("Hey_Bro~2.meta.json5"), r#"{ "name": "Hey*Bro" }"#).unwrap(); - fs::write(src.join("Hey_Bro~2.luau"), "return 'Hey*Bro'").unwrap(); + fs::write(src.join("Hey_Bro~3.meta.json5"), r#"{ "name": "Hey*Bro" }"#).unwrap(); + fs::write(src.join("Hey_Bro~3.luau"), "return 'Hey*Bro'").unwrap(); }) .unwrap(); assert_yaml_snapshot!( @@ -1455,12 +1455,12 @@ fn collision_evolution() { ); assert_round_trip(&session, root_id); - // Step 2: Remove Hey_Bro~1 (Hey/Bro) + // Step 2: Remove Hey_Bro~2 (Hey/Bro) let cursor = get_message_cursor(&packet); let packet = session .recv_socket_packet(SocketPacketType::Messages, cursor, || { - fs::remove_file(src.join("Hey_Bro~1.luau")).unwrap(); - fs::remove_file(src.join("Hey_Bro~1.meta.json5")).unwrap(); + fs::remove_file(src.join("Hey_Bro~2.luau")).unwrap(); + fs::remove_file(src.join("Hey_Bro~2.meta.json5")).unwrap(); }) .unwrap(); assert_yaml_snapshot!( @@ -1469,12 +1469,12 @@ fn collision_evolution() { ); assert_round_trip(&session, root_id); - // Step 3: Reuse ~1 slot with different name "Hey|Bro" + // Step 3: Reuse ~2 slot with different name "Hey|Bro" let cursor = get_message_cursor(&packet); let packet = session .recv_socket_packet(SocketPacketType::Messages, cursor, || { - fs::write(src.join("Hey_Bro~1.meta.json5"), r#"{ "name": "Hey|Bro" }"#).unwrap(); - fs::write(src.join("Hey_Bro~1.luau"), "return 'Hey|Bro'").unwrap(); + fs::write(src.join("Hey_Bro~2.meta.json5"), r#"{ "name": "Hey|Bro" }"#).unwrap(); + fs::write(src.join("Hey_Bro~2.luau"), "return 'Hey|Bro'").unwrap(); }) .unwrap(); assert_yaml_snapshot!( diff --git a/tests/tests/two_way_sync.rs b/tests/tests/two_way_sync.rs index cc712d96..14b0a136 100644 --- a/tests/tests/two_way_sync.rs +++ b/tests/tests/two_way_sync.rs @@ -5164,7 +5164,7 @@ fn add_instance_with_forbidden_chars_creates_slug_and_meta() { } /// Adding two instances whose names slugify to the same string should produce -/// dedup suffixes (~1) and both should have correct meta `name` fields. +/// dedup suffixes (~2) and both should have correct meta `name` fields. #[test] fn add_two_colliding_instances_deduplicates() { run_serve_test("syncback_encoded_names", |session, _redactions| { @@ -5215,12 +5215,12 @@ fn add_two_colliding_instances_deduplicates() { let src = session.path().join("src"); let base = src.join("X_Y.luau"); - let deduped = src.join("X_Y~1.luau"); + let deduped = src.join("X_Y~2.luau"); - // One should get X_Y.luau, the other X_Y~1.luau + // One should get X_Y.luau, the other X_Y~2.luau assert!( base.exists() && deduped.exists(), - "Both X_Y.luau and X_Y~1.luau should exist. \ + "Both X_Y.luau and X_Y~2.luau should exist. \ base exists: {}, deduped exists: {}", base.exists(), deduped.exists() @@ -5228,9 +5228,9 @@ fn add_two_colliding_instances_deduplicates() { // Both should have meta files with correct names let base_meta = src.join("X_Y.meta.json5"); - let deduped_meta = src.join("X_Y~1.meta.json5"); + let deduped_meta = src.join("X_Y~2.meta.json5"); assert_file_exists(&base_meta, "Meta for base slug instance"); - assert_file_exists(&deduped_meta, "Meta for deduped ~1 instance"); + assert_file_exists(&deduped_meta, "Meta for deduped ~2 instance"); // Read both meta files and verify they contain the correct names let meta1 = fs::read_to_string(&base_meta).unwrap(); @@ -5456,7 +5456,7 @@ fn rename_model_json_to_clean_name() { // --------------------------------------------------------------------------- /// Adding 3 instances that all slug to the same name (X/Y, X:Y, X|Y → X_Y) -/// should produce X_Y.luau, X_Y~1.luau, X_Y~2.luau with correct meta files. +/// should produce X_Y.luau, X_Y~2.luau, X_Y~3.luau with correct meta files. #[test] fn add_three_colliding_instances_deduplicates() { run_serve_test("syncback_encoded_names", |session, _redactions| { @@ -5495,13 +5495,13 @@ fn add_three_colliding_instances_deduplicates() { let src = session.path().join("src"); let base = src.join("X_Y.luau"); - let dedup1 = src.join("X_Y~1.luau"); - let dedup2 = src.join("X_Y~2.luau"); + let dedup1 = src.join("X_Y~2.luau"); + let dedup2 = src.join("X_Y~3.luau"); assert!( base.exists() && dedup1.exists() && dedup2.exists(), "All three slugified files should exist.\n\ - X_Y.luau: {}, X_Y~1.luau: {}, X_Y~2.luau: {}", + X_Y.luau: {}, X_Y~2.luau: {}, X_Y~3.luau: {}", base.exists(), dedup1.exists(), dedup2.exists() @@ -5509,11 +5509,11 @@ fn add_three_colliding_instances_deduplicates() { // All three should have meta files with the original names let base_meta = src.join("X_Y.meta.json5"); - let dedup1_meta = src.join("X_Y~1.meta.json5"); - let dedup2_meta = src.join("X_Y~2.meta.json5"); + let dedup1_meta = src.join("X_Y~2.meta.json5"); + let dedup2_meta = src.join("X_Y~3.meta.json5"); assert_file_exists(&base_meta, "Meta for base slug"); - assert_file_exists(&dedup1_meta, "Meta for ~1 dedup"); - assert_file_exists(&dedup2_meta, "Meta for ~2 dedup"); + assert_file_exists(&dedup1_meta, "Meta for ~2 dedup"); + assert_file_exists(&dedup2_meta, "Meta for ~3 dedup"); // Each meta should contain one of the original names let all_metas = [ @@ -5662,13 +5662,13 @@ fn add_case_insensitive_colliding_instances() { let src = session.path().join("src"); - // One should get the base name, the other should get ~1 + // One should get the base name, the other should get ~2 // The exact casing depends on processing order, but both files // must exist with distinct names. let base = src.join("CaseTest.luau"); let base_lower = src.join("casetest.luau"); - let dedup = src.join("CaseTest~1.luau"); - let dedup_lower = src.join("casetest~1.luau"); + let dedup = src.join("CaseTest~2.luau"); + let dedup_lower = src.join("casetest~2.luau"); let base_exists = base.exists() || base_lower.exists(); let dedup_exists = dedup.exists() || dedup_lower.exists(); @@ -5676,7 +5676,7 @@ fn add_case_insensitive_colliding_instances() { assert!( base_exists && dedup_exists, "Both case-insensitive colliding instances should exist as separate files.\n\ - CaseTest.luau: {}, casetest.luau: {}, CaseTest~1.luau: {}, casetest~1.luau: {}", + CaseTest.luau: {}, casetest.luau: {}, CaseTest~2.luau: {}, casetest~2.luau: {}", base.exists(), base_lower.exists(), dedup.exists(), @@ -7406,7 +7406,7 @@ fn ref_same_batch_add_folder_target_no_extension() { // Tests: Dedup suffix cleanup // --------------------------------------------------------------------------- -/// Add two colliding ModuleScripts (creates X_Y.luau + X_Y~1.luau), then +/// Add two colliding ModuleScripts (creates X_Y.luau + X_Y~2.luau), then /// delete one. The remaining instance should lose its dedup suffix (group-to-1 /// cleanup rule). #[test] @@ -7460,10 +7460,10 @@ fn delete_deduped_instance_group_to_1_cleanup() { let src = session.path().join("src"); let base = src.join("X_Y.luau"); - let deduped = src.join("X_Y~1.luau"); + let deduped = src.join("X_Y~2.luau"); poll_file_exists(&base, "X_Y.luau after add"); - poll_file_exists(&deduped, "X_Y~1.luau after add"); + poll_file_exists(&deduped, "X_Y~2.luau after add"); // Wait for VFS to settle thread::sleep(Duration::from_millis(500)); @@ -7485,7 +7485,7 @@ fn delete_deduped_instance_group_to_1_cleanup() { } } else { // Base file has the survivor source -- the other one is doomed - let dedup_meta = src.join("X_Y~1.meta.json5"); + let dedup_meta = src.join("X_Y~2.meta.json5"); let meta_content = fs::read_to_string(&dedup_meta).unwrap(); if meta_content.contains("\"X/Y\"") { "X/Y" @@ -7497,8 +7497,8 @@ fn delete_deduped_instance_group_to_1_cleanup() { let (doomed_id, _) = find_by_name(&rs_read.instances, doomed_name); send_removal(&session, &info.session_id, vec![doomed_id]); - // After group-to-1 cleanup: only X_Y.luau should remain, no ~1 suffix - poll_not_exists(&deduped, "X_Y~1.luau after group-to-1 cleanup"); + // After group-to-1 cleanup: only X_Y.luau should remain, no ~2 suffix + poll_not_exists(&deduped, "X_Y~2.luau after group-to-1 cleanup"); assert_file_exists(&base, "X_Y.luau should survive"); // The survivor's meta should still have a name field (since it's slugified) @@ -7508,7 +7508,7 @@ fn delete_deduped_instance_group_to_1_cleanup() { } /// Add three colliding ModuleScripts, then delete the base instance. -/// The ~1 instance should be promoted to base (base-name promotion rule). +/// The ~2 instance should be promoted to base (base-name promotion rule). #[test] fn delete_base_deduped_instance_promotes_lowest() { run_serve_test("syncback_encoded_names", |session, _redactions| { @@ -7546,12 +7546,12 @@ fn delete_base_deduped_instance_promotes_lowest() { let src = session.path().join("src"); let base = src.join("X_Y.luau"); - let dedup1 = src.join("X_Y~1.luau"); - let dedup2 = src.join("X_Y~2.luau"); + let dedup1 = src.join("X_Y~2.luau"); + let dedup2 = src.join("X_Y~3.luau"); poll_file_exists(&base, "X_Y.luau after add"); - poll_file_exists(&dedup1, "X_Y~1.luau after add"); - poll_file_exists(&dedup2, "X_Y~2.luau after add"); + poll_file_exists(&dedup1, "X_Y~2.luau after add"); + poll_file_exists(&dedup2, "X_Y~3.luau after add"); // Wait for VFS to settle thread::sleep(Duration::from_millis(500)); @@ -7575,19 +7575,19 @@ fn delete_base_deduped_instance_promotes_lowest() { // Delete the base instance send_removal(&session, &info.session_id, vec![base_id]); - // After base-name promotion: ~1 should be promoted to base - // X_Y.luau should exist (promoted from ~1) - // X_Y~2.luau should still exist (unchanged) - // X_Y~1.luau should NOT exist (promoted away) - poll_not_exists(&dedup1, "X_Y~1.luau after promotion"); + // After base-name promotion: ~2 should be promoted to base + // X_Y.luau should exist (promoted from ~2) + // X_Y~3.luau should still exist (unchanged) + // X_Y~2.luau should NOT exist (promoted away) + poll_not_exists(&dedup1, "X_Y~2.luau after promotion"); thread::sleep(Duration::from_millis(300)); assert_file_exists(&base, "X_Y.luau should exist (promoted)"); - assert_file_exists(&dedup2, "X_Y~2.luau should remain"); + assert_file_exists(&dedup2, "X_Y~3.luau should remain"); }); } -/// Add three colliding ModuleScripts, then delete the ~1 instance. -/// Gap should be tolerated: X_Y.luau and X_Y~2.luau remain unchanged. +/// Add three colliding ModuleScripts, then delete the ~2 instance. +/// Gap should be tolerated: X_Y.luau and X_Y~3.luau remain unchanged. #[test] fn delete_middle_deduped_instance_tolerates_gap() { run_serve_test("syncback_encoded_names", |session, _redactions| { @@ -7625,18 +7625,18 @@ fn delete_middle_deduped_instance_tolerates_gap() { let src = session.path().join("src"); let base = src.join("X_Y.luau"); - let dedup1 = src.join("X_Y~1.luau"); - let dedup2 = src.join("X_Y~2.luau"); + let dedup1 = src.join("X_Y~2.luau"); + let dedup2 = src.join("X_Y~3.luau"); poll_file_exists(&base, "X_Y.luau after add"); - poll_file_exists(&dedup1, "X_Y~1.luau after add"); - poll_file_exists(&dedup2, "X_Y~2.luau after add"); + poll_file_exists(&dedup1, "X_Y~2.luau after add"); + poll_file_exists(&dedup2, "X_Y~3.luau after add"); thread::sleep(Duration::from_millis(500)); - // Find which instance maps to ~1 + // Find which instance maps to ~2 let rs_read = session.get_api_read(rs_id).unwrap(); - let dedup1_meta = src.join("X_Y~1.meta.json5"); + let dedup1_meta = src.join("X_Y~2.meta.json5"); let dedup1_meta_content = fs::read_to_string(&dedup1_meta).unwrap(); let middle_name = if dedup1_meta_content.contains("\"X/Y\"") { "X/Y" @@ -7648,16 +7648,16 @@ fn delete_middle_deduped_instance_tolerates_gap() { let (middle_id, _) = find_by_name(&rs_read.instances, middle_name); - // Delete the ~1 instance + // Delete the ~2 instance send_removal(&session, &info.session_id, vec![middle_id]); - // Gap tolerance: base and ~2 remain, ~1 is gone - poll_not_exists(&dedup1, "X_Y~1.luau after deletion"); - // Also verify the ~1 meta is gone - poll_not_exists(&dedup1_meta, "X_Y~1.meta.json5 after deletion"); + // Gap tolerance: base and ~3 remain, ~2 is gone + poll_not_exists(&dedup1, "X_Y~2.luau after deletion"); + // Also verify the ~2 meta is gone + poll_not_exists(&dedup1_meta, "X_Y~2.meta.json5 after deletion"); thread::sleep(Duration::from_millis(300)); assert_file_exists(&base, "X_Y.luau should remain"); - assert_file_exists(&dedup2, "X_Y~2.luau should remain (gap tolerated)"); + assert_file_exists(&dedup2, "X_Y~3.luau should remain (gap tolerated)"); }); } @@ -7738,7 +7738,7 @@ fn add_same_named_folders_deduplicates() { let src = session.path().join("src"); let data_dir = src.join("Data"); - let data_dedup = src.join("Data~1"); + let data_dedup = src.join("Data~2"); // Both directories should exist assert!( @@ -7748,7 +7748,7 @@ fn add_same_named_folders_deduplicates() { ); assert!( data_dedup.is_dir(), - "Data~1/ directory should exist at {}", + "Data~2/ directory should exist at {}", data_dedup.display() ); @@ -7756,7 +7756,7 @@ fn add_same_named_folders_deduplicates() { let dedup_meta = data_dedup.join("init.meta.json5"); assert!( dedup_meta.exists(), - "Data~1/init.meta.json5 should exist for name override" + "Data~2/init.meta.json5 should exist for name override" ); let meta_content = fs::read_to_string(&dedup_meta).unwrap(); assert!( @@ -7907,7 +7907,7 @@ fn ref_through_deduped_instance() { ); // The Container's init.meta.json5 should have a Rojo_Ref_PrimaryPart - // attribute with a path that includes X_Y (base or ~1 suffix) + // attribute with a path that includes X_Y (base or ~2 suffix) let init_meta = container.join("init.meta.json5"); poll_file_exists(&init_meta, "Container init.meta.json5 after ref set"); thread::sleep(Duration::from_millis(300)); @@ -7998,15 +7998,15 @@ fn ref_path_updated_after_dedup_cleanup() { let src = session.path().join("src"); let base = src.join("X_Y.luau"); - let deduped = src.join("X_Y~1.luau"); + let deduped = src.join("X_Y~2.luau"); poll_file_exists(&base, "X_Y.luau after add"); - poll_file_exists(&deduped, "X_Y~1.luau after add"); + poll_file_exists(&deduped, "X_Y~2.luau after add"); thread::sleep(Duration::from_millis(500)); - // Step 2: Find the ~1 instance and point Pointer's child ObjectValue at it + // Step 2: Find the ~2 instance and point Pointer's child ObjectValue at it let rs_read = session.get_api_read(rs_id).unwrap(); - let dedup_meta_path = src.join("X_Y~1.meta.json5"); + let dedup_meta_path = src.join("X_Y~2.meta.json5"); let dedup_meta_content = fs::read_to_string(&dedup_meta_path).unwrap(); let target_name = if dedup_meta_content.contains("\"X/Y\"") { "X/Y" @@ -8023,7 +8023,7 @@ fn ref_path_updated_after_dedup_cleanup() { let pointer_read = session.get_api_read(pointer_id).unwrap(); let (ptr_child_id, _) = find_by_name(&pointer_read.instances, "Ptr"); - // Set ObjectValue.Value to the ~1 target + // Set ObjectValue.Value to the ~2 target let mut props = UstrMap::default(); props.insert(ustr("Value"), Some(Variant::Ref(target_id))); send_update( @@ -8038,32 +8038,32 @@ fn ref_path_updated_after_dedup_cleanup() { }, ); - // Verify the ref was written with the ~1 path + // Verify the ref was written with the ~2 path let pointer_dir = src.join("Pointer"); let ptr_model_path = pointer_dir.join("Ptr.model.json5"); poll_file_exists(&ptr_model_path, "Ptr.model.json5 after ref set"); thread::sleep(Duration::from_millis(300)); let ref_content = fs::read_to_string(&ptr_model_path).unwrap(); assert!( - ref_content.contains("X_Y~1"), - "Before cleanup: ref should point to X_Y~1, got: {}", + ref_content.contains("X_Y~2"), + "Before cleanup: ref should point to X_Y~2, got: {}", ref_content ); // Step 3: Delete the OTHER instance (not the target) to trigger - // group-to-1 cleanup, which renames X_Y~1 → X_Y + // group-to-1 cleanup, which renames X_Y~2 → X_Y send_removal(&session, &info.session_id, vec![other_id]); - // After cleanup: X_Y~1.luau renamed to X_Y.luau - poll_not_exists(&deduped, "X_Y~1.luau after cleanup"); + // After cleanup: X_Y~2.luau renamed to X_Y.luau + poll_not_exists(&deduped, "X_Y~2.luau after cleanup"); thread::sleep(Duration::from_millis(300)); assert_file_exists(&base, "X_Y.luau after cleanup"); // The ref in Ptr.model.json5 should now point to the non-suffixed path let updated_ref = fs::read_to_string(&ptr_model_path).unwrap(); assert!( - !updated_ref.contains("X_Y~1"), - "After cleanup: ref should NOT contain X_Y~1 (old path), got: {}", + !updated_ref.contains("X_Y~2"), + "After cleanup: ref should NOT contain X_Y~2 (old path), got: {}", updated_ref ); assert!(