Add git metadata handling and integration tests for sync direction defaults

- Introduced new tests for git-based sync direction defaults, verifying the presence of git metadata and correct mappings of changed files to instance references.
- Enhanced the `compute_git_metadata` function to include canonical paths for repository roots.
- Added multiple test cases to ensure accurate behavior of `changedIds` and `scriptCommittedHashes` based on git status.
- Created a new project structure for testing, including various script and model files to facilitate comprehensive testing of git integration.

These changes improve the robustness of the git synchronization features and ensure reliable behavior in various scenarios.
This commit is contained in:
Joe 2026-02-17 20:46:14 -06:00
parent 7ba04f7f6a
commit 6c0098d839
12 changed files with 1707 additions and 1 deletions

View file

@ -256,6 +256,440 @@ return function()
end)
end)
describe("gitMetadata default selections", function()
local SHA1 = require(script.Parent.SHA1)
local function computeBlobSha1(content)
local gitBlob = "blob " .. tostring(#content) .. "\0" .. content
return SHA1(buffer.fromstring(gitBlob))
end
it("should default to pull for unchanged files (not in changedIds)", function()
local patch = PatchSet.newEmpty()
local instanceMap = InstanceMap.new()
local workspace = game:GetService("Workspace")
local folder = Instance.new("Folder")
folder.Name = "UnchangedFolder"
folder.Parent = workspace
local id = HttpService:GenerateGUID(false)
instanceMap:insert(id, folder)
table.insert(patch.updated, {
id = id,
changedName = "Renamed",
changedProperties = {},
})
local gitMetadata = {
changedIds = {}, -- empty = no files changed in git
scriptCommittedHashes = {},
}
local tree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, gitMetadata)
local selections = PatchTree.buildInitialSelections(tree)
expect(selections[id]).to.equal("pull")
folder:Destroy()
instanceMap:stop()
end)
it("should default to nil when no gitMetadata provided", function()
local patch = PatchSet.newEmpty()
local instanceMap = InstanceMap.new()
local workspace = game:GetService("Workspace")
local folder = Instance.new("Folder")
folder.Name = "NoGitFolder"
folder.Parent = workspace
local id = HttpService:GenerateGUID(false)
instanceMap:insert(id, folder)
table.insert(patch.updated, {
id = id,
changedName = "Renamed",
changedProperties = {},
})
local tree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, nil)
local selections = PatchTree.buildInitialSelections(tree)
expect(selections[id]).to.equal(nil)
folder:Destroy()
instanceMap:stop()
end)
it("should default to nil for non-script changed files", function()
local patch = PatchSet.newEmpty()
local instanceMap = InstanceMap.new()
local workspace = game:GetService("Workspace")
local folder = Instance.new("Folder")
folder.Name = "ChangedFolder"
folder.Parent = workspace
local id = HttpService:GenerateGUID(false)
instanceMap:insert(id, folder)
table.insert(patch.updated, {
id = id,
changedName = "Renamed",
changedProperties = {},
})
local gitMetadata = {
changedIds = { id },
scriptCommittedHashes = {},
}
local tree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, gitMetadata)
local selections = PatchTree.buildInitialSelections(tree)
expect(selections[id]).to.equal(nil)
folder:Destroy()
instanceMap:stop()
end)
it("should default to push when script Source matches committed hash", function()
local patch = PatchSet.newEmpty()
local instanceMap = InstanceMap.new()
local workspace = game:GetService("Workspace")
local script = Instance.new("ModuleScript")
script.Name = "MatchingScript"
script.Source = "local x = 1\nreturn x"
script.Parent = workspace
local id = HttpService:GenerateGUID(false)
instanceMap:insert(id, script)
table.insert(patch.updated, {
id = id,
changedProperties = { Source = { String = "-- different from server" } },
})
local committedHash = computeBlobSha1(script.Source)
local gitMetadata = {
changedIds = { id },
scriptCommittedHashes = {
[id] = { committedHash },
},
}
local tree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, gitMetadata)
local selections = PatchTree.buildInitialSelections(tree)
expect(selections[id]).to.equal("push")
script:Destroy()
instanceMap:stop()
end)
it("should default to nil when script Source does not match any committed hash", function()
local patch = PatchSet.newEmpty()
local instanceMap = InstanceMap.new()
local workspace = game:GetService("Workspace")
local script = Instance.new("ModuleScript")
script.Name = "MismatchScript"
script.Source = "-- studio has different content"
script.Parent = workspace
local id = HttpService:GenerateGUID(false)
instanceMap:insert(id, script)
table.insert(patch.updated, {
id = id,
changedProperties = { Source = { String = "-- server version" } },
})
local gitMetadata = {
changedIds = { id },
scriptCommittedHashes = {
[id] = { computeBlobSha1("-- committed version, different from studio") },
},
}
local tree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, gitMetadata)
local selections = PatchTree.buildInitialSelections(tree)
expect(selections[id]).to.equal(nil)
script:Destroy()
instanceMap:stop()
end)
it("should match against staged hash when HEAD hash doesn't match", function()
local patch = PatchSet.newEmpty()
local instanceMap = InstanceMap.new()
local workspace = game:GetService("Workspace")
local script = Instance.new("ModuleScript")
script.Name = "StagedMatchScript"
script.Source = "-- staged version content"
script.Parent = workspace
local id = HttpService:GenerateGUID(false)
instanceMap:insert(id, script)
table.insert(patch.updated, {
id = id,
changedProperties = { Source = { String = "-- server" } },
})
local headHash = computeBlobSha1("-- old committed version")
local stagedHash = computeBlobSha1(script.Source)
local gitMetadata = {
changedIds = { id },
scriptCommittedHashes = {
[id] = { headHash, stagedHash },
},
}
local tree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, gitMetadata)
local selections = PatchTree.buildInitialSelections(tree)
expect(selections[id]).to.equal("push")
script:Destroy()
instanceMap:stop()
end)
it("should not auto-select Added items regardless of git status", function()
local patch = PatchSet.newEmpty()
local instanceMap = InstanceMap.new()
local parentId = HttpService:GenerateGUID(false)
local workspace = game:GetService("Workspace")
instanceMap:insert(parentId, workspace)
local addId = HttpService:GenerateGUID(false)
patch.added[addId] = {
Id = addId,
Parent = parentId,
ClassName = "ModuleScript",
Name = "AddedScript",
Properties = {},
}
local gitMetadata = {
changedIds = { addId },
scriptCommittedHashes = {},
}
local tree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, gitMetadata)
local selections = PatchTree.buildInitialSelections(tree)
expect(selections[addId]).to.equal(nil)
instanceMap:stop()
end)
it("should not auto-select Removed items regardless of git status", function()
local patch = PatchSet.newEmpty()
local instanceMap = InstanceMap.new()
local workspace = game:GetService("Workspace")
local folder = Instance.new("Folder")
folder.Name = "ToRemove"
folder.Parent = workspace
local id = HttpService:GenerateGUID(false)
instanceMap:insert(id, folder)
table.insert(patch.removed, id)
local gitMetadata = {
changedIds = {},
scriptCommittedHashes = {},
}
local tree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, gitMetadata)
local selections = PatchTree.buildInitialSelections(tree)
expect(selections[id]).to.equal(nil)
folder:Destroy()
instanceMap:stop()
end)
it("should handle Script and LocalScript as LuaSourceContainer", function()
local patch = PatchSet.newEmpty()
local instanceMap = InstanceMap.new()
local workspace = game:GetService("Workspace")
local serverScript = Instance.new("Script")
serverScript.Name = "ServerTest"
serverScript.Source = "print('server')"
serverScript.Parent = workspace
local localScript = Instance.new("LocalScript")
localScript.Name = "ClientTest"
localScript.Source = "print('client')"
localScript.Parent = workspace
local serverId = HttpService:GenerateGUID(false)
local localId = HttpService:GenerateGUID(false)
instanceMap:insert(serverId, serverScript)
instanceMap:insert(localId, localScript)
table.insert(patch.updated, {
id = serverId,
changedProperties = { Source = { String = "-- new" } },
})
table.insert(patch.updated, {
id = localId,
changedProperties = { Source = { String = "-- new" } },
})
local gitMetadata = {
changedIds = { serverId, localId },
scriptCommittedHashes = {
[serverId] = { computeBlobSha1(serverScript.Source) },
[localId] = { computeBlobSha1(localScript.Source) },
},
}
local tree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, gitMetadata)
local selections = PatchTree.buildInitialSelections(tree)
expect(selections[serverId]).to.equal("push")
expect(selections[localId]).to.equal("push")
serverScript:Destroy()
localScript:Destroy()
instanceMap:stop()
end)
it("should handle mixed changed and unchanged files", function()
local patch = PatchSet.newEmpty()
local instanceMap = InstanceMap.new()
local workspace = game:GetService("Workspace")
local changedScript = Instance.new("ModuleScript")
changedScript.Name = "Changed"
changedScript.Source = "-- matches committed"
changedScript.Parent = workspace
local unchangedFolder = Instance.new("Folder")
unchangedFolder.Name = "Unchanged"
unchangedFolder.Parent = workspace
local changedId = HttpService:GenerateGUID(false)
local unchangedId = HttpService:GenerateGUID(false)
instanceMap:insert(changedId, changedScript)
instanceMap:insert(unchangedId, unchangedFolder)
table.insert(patch.updated, {
id = changedId,
changedProperties = { Source = { String = "-- server" } },
})
table.insert(patch.updated, {
id = unchangedId,
changedName = "StudioRenamed",
changedProperties = {},
})
local gitMetadata = {
changedIds = { changedId }, -- only the script is git-changed
scriptCommittedHashes = {
[changedId] = { computeBlobSha1(changedScript.Source) },
},
}
local tree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, gitMetadata)
local selections = PatchTree.buildInitialSelections(tree)
expect(selections[changedId]).to.equal("push")
expect(selections[unchangedId]).to.equal("pull")
changedScript:Destroy()
unchangedFolder:Destroy()
instanceMap:stop()
end)
it("should handle changed script with no committed hash (new file)", function()
local patch = PatchSet.newEmpty()
local instanceMap = InstanceMap.new()
local workspace = game:GetService("Workspace")
local script = Instance.new("ModuleScript")
script.Name = "NewScript"
script.Source = "return 'new'"
script.Parent = workspace
local id = HttpService:GenerateGUID(false)
instanceMap:insert(id, script)
table.insert(patch.updated, {
id = id,
changedProperties = { Source = { String = "-- different" } },
})
local gitMetadata = {
changedIds = { id },
scriptCommittedHashes = {}, -- no hash for this script (new file)
}
local tree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, gitMetadata)
local selections = PatchTree.buildInitialSelections(tree)
expect(selections[id]).to.equal(nil)
script:Destroy()
instanceMap:stop()
end)
it("should handle empty changedIds with gitMetadata present", function()
local patch = PatchSet.newEmpty()
local instanceMap = InstanceMap.new()
local workspace = game:GetService("Workspace")
local folder = Instance.new("Folder")
folder.Name = "AllUnchanged"
folder.Parent = workspace
local script = Instance.new("ModuleScript")
script.Name = "AlsoUnchanged"
script.Source = "return true"
script.Parent = workspace
local folderId = HttpService:GenerateGUID(false)
local scriptId = HttpService:GenerateGUID(false)
instanceMap:insert(folderId, folder)
instanceMap:insert(scriptId, script)
table.insert(patch.updated, {
id = folderId,
changedName = "StudioRenamed",
changedProperties = {},
})
table.insert(patch.updated, {
id = scriptId,
changedProperties = { Source = { String = "-- studio version" } },
})
local gitMetadata = {
changedIds = {}, -- nothing changed in git
scriptCommittedHashes = {},
}
local tree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, gitMetadata)
local selections = PatchTree.buildInitialSelections(tree)
expect(selections[folderId]).to.equal("pull")
expect(selections[scriptId]).to.equal("pull")
folder:Destroy()
script:Destroy()
instanceMap:stop()
end)
end)
describe("getSelectableNodeIds", function()
it("should return empty array for empty tree", function()
local patch = PatchSet.newEmpty()