Update logging levels in Rojo plugin and enhance test plans

- Promoted sync-related logging from Debug/Trace to Info level across multiple files, ensuring visibility of changes in the Studio output console.
- Added a new plan for expanding the connected mode test suite to cover multi-step scenarios and round-trip identity verification.
- Introduced a plan to fix stale tree desynchronization by implementing a validation mechanism on plugin connection.
- Created a plan to bump sync logging to Info level, detailing todos for various components in the plugin.

This update improves the clarity of logs and enhances testing coverage for connected mode scenarios.
This commit is contained in:
Joe 2026-02-11 17:10:35 -06:00
parent 2cc1726975
commit 316d7ea70e
17 changed files with 2443 additions and 1553 deletions

View file

@ -13,12 +13,12 @@ local function getLogLevel()
end
local function addTags(tag, message)
return tag .. message:gsub("\n", "\n" .. tag)
return tag .. message
end
local TRACE_TAG = (" "):rep(15) .. "[Rojo-Trace] "
local INFO_TAG = (" "):rep(15) .. "[Rojo-Info] "
local DEBUG_TAG = (" "):rep(15) .. "[Rojo-Debug] "
local TRACE_TAG = "[Rojo-Trace] "
local INFO_TAG = "[Rojo-Info] "
local DEBUG_TAG = "[Rojo-Debug] "
local WARN_TAG = "[Rojo-Warn] "
local Log = {}

View file

@ -197,9 +197,8 @@ function ApiContext:write(patch)
addedCount += 1
end
-- Only log summary at debug level to reduce verbosity
if #patch.removed > 0 or addedCount > 0 or #patch.updated > 0 then
Log.debug(
Log.info(
"Sending to server: {} removals, {} additions, {} updates",
#patch.removed,
addedCount,
@ -207,15 +206,14 @@ function ApiContext:write(patch)
)
end
-- Log individual operations at trace level (very detailed)
for _, removed in ipairs(patch.removed) do
Log.trace("[Syncback] Remove ID: {}", tostring(removed))
Log.info("[Syncback] Remove ID: {}", tostring(removed))
end
for _, addedInstance in pairs(patch.added) do
local instanceName = addedInstance.name or addedInstance.Name or "unknown"
local instanceClass = addedInstance.className or addedInstance.ClassName or "unknown"
Log.trace("[Syncback] Add {} ({})", instanceName, instanceClass)
Log.info("[Syncback] Add {} ({})", instanceName, instanceClass)
end
for _, update in ipairs(patch.updated) do
@ -225,7 +223,7 @@ function ApiContext:write(patch)
propCount += 1
end
end
Log.trace("[Syncback] Update ID {} ({} properties)", update.id, propCount)
Log.info("[Syncback] Update ID {} ({} properties)", update.id, propCount)
end
local updated = {}

View file

@ -21,6 +21,7 @@ local Log = require(Packages.Log)
local RbxDom = require(Packages.RbxDom)
local encodeProperty = require(script.Parent.encodeProperty)
local UNENCODABLE_DATA_TYPES = require(script.Parent.propertyFilter)
-- Script class names that need Source property
local SCRIPT_CLASS_NAMES = {
@ -234,6 +235,10 @@ encodeInstance = function(instance, parentId, _skipPathCheck)
-- Get the full PropertyDescriptor for encoding
local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
if descriptor then
if UNENCODABLE_DATA_TYPES[descriptor.dataType] then
continue
end
local encodeSuccess, encodeResult = encodeProperty(instance, propertyName, descriptor)
if encodeSuccess and encodeResult ~= nil then
properties[propertyName] = encodeResult

View file

@ -3,6 +3,7 @@ local Log = require(Packages.Log)
local RbxDom = require(Packages.RbxDom)
local encodeProperty = require(script.Parent.encodeProperty)
local UNENCODABLE_DATA_TYPES = require(script.Parent.propertyFilter)
return function(instance, instanceId, properties)
local update = {
@ -21,6 +22,10 @@ return function(instance, instanceId, properties)
continue
end
if UNENCODABLE_DATA_TYPES[descriptor.dataType] then
continue
end
local encodeSuccess, encodeResult = encodeProperty(instance, propertyName, descriptor)
if not encodeSuccess then

View file

@ -11,7 +11,13 @@ return function(instance, propertyName, propertyDescriptor)
end
local dataType = propertyDescriptor.dataType
local encodeSuccess, encodeResult = RbxDom.EncodedValue.encode(readResult, dataType)
local pcallOk, encodeSuccess, encodeResult = pcall(RbxDom.EncodedValue.encode, readResult, dataType)
if not pcallOk then
-- Encoder threw an error (e.g., Ref, Region3, SharedString)
Log.warn("Could not sync back property {:?}.{}: {}", instance, propertyName, encodeSuccess)
return false, nil
end
if not encodeSuccess then
Log.warn("Could not sync back property {:?}.{}: {}", instance, propertyName, encodeResult)

View file

@ -107,12 +107,11 @@ function ChangeBatcher:__flush()
return nil
end
-- Log summary at debug level
local addedCount = 0
for _ in pairs(patch.added) do
addedCount += 1
end
Log.debug("Two-way sync: {} updates, {} additions, {} removals", #patch.updated, addedCount, #patch.removed)
Log.info("Two-way sync: {} updates, {} additions, {} removals", #patch.updated, addedCount, #patch.removed)
return patch
end

View file

@ -0,0 +1,16 @@
--[[
Property type blacklist for encoding, matching the server-side
filter_properties_for_meta behavior in src/web/api.rs.
Types listed here are skipped during property encoding because they
cannot be serialized to JSON or are meaningless for syncback.
]]
-- Property data types that cannot be encoded standalone.
-- Matches the Variant::Ref / Variant::UniqueId filter in api.rs.
local UNENCODABLE_DATA_TYPES = {
Ref = true, -- Instance references; need the InstanceMap, not encodable standalone
UniqueId = true, -- Internal Roblox identifiers; not meaningful for syncback
}
return UNENCODABLE_DATA_TYPES

View file

@ -27,13 +27,12 @@ local function applyPatch(instanceMap, patch)
-- to ensure that referents can be mapped to instances correctly.
local deferredRefs = {}
-- Log summary of patch being applied (debug level for summary)
local addCount = 0
for _ in pairs(patch.added) do
addCount += 1
end
if #patch.removed > 0 or addCount > 0 or #patch.updated > 0 then
Log.debug(
Log.info(
"Applying patch to Studio: {} removals, {} additions, {} updates",
#patch.removed,
addCount,
@ -124,7 +123,6 @@ local function applyPatch(instanceMap, patch)
continue
end
-- Log the update (debug level for property details to reduce verbosity)
local instancePath = instance:GetFullName()
local propList = {}
if update.changedName then
@ -138,7 +136,7 @@ local function applyPatch(instanceMap, patch)
table.insert(propList, propName)
end
end
Log.debug("[Studio] Update: {} - {}", instancePath, table.concat(propList, ", "))
Log.info("[Studio] Update: {} - {}", instancePath, table.concat(propList, ", "))
-- Pause updates on this instance to avoid picking up our changes when
-- two-way sync is enabled.

View file

@ -219,7 +219,7 @@ function ServeSession:__onWebSocketMessage(messagesPacket)
return
end
Log.debug("Received {} messages from Rojo server", #messagesPacket.messages)
Log.info("Received {} messages from Rojo server", #messagesPacket.messages)
-- Combine all messages into a single patch
local combinedPatch = PatchSet.newEmpty()
@ -227,6 +227,17 @@ function ServeSession:__onWebSocketMessage(messagesPacket)
PatchSet.assign(combinedPatch, message)
end
local addedCount = 0
for _ in pairs(combinedPatch.added) do
addedCount += 1
end
Log.info(
"Combined patch: {} removals, {} additions, {} updates",
#combinedPatch.removed,
addedCount,
#combinedPatch.updated
)
-- If we're already waiting for confirmation, merge into the patch being confirmed
-- Changes flow into the dialogue even in one-shot mode so user can see them
if self.__confirmingPatch ~= nil then
@ -580,7 +591,7 @@ function ServeSession:__applyPatch(patch)
end
if not PatchSet.isEmpty(unappliedPatch) then
Log.debug(
Log.info(
"Could not apply all changes requested by the Rojo server:\n{}",
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)
)
@ -696,7 +707,7 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
ignoreCount += 1
end
end
Log.debug("User selections: {} push, {} pull, {} ignored", pushCount, pullCount, ignoreCount)
Log.info("User selections: {} push, {} pull, {} ignored", pushCount, pullCount, ignoreCount)
-- Build partial patches based on selections
local pushPatch = PatchSet.newEmpty() -- Items to apply to Studio
@ -710,9 +721,9 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
if selection == "push" then
-- Apply Rojo changes to Studio
Log.trace("[Push] Update: {}", instancePath)
table.insert(pushPatch.updated, change)
elseif selection == "pull" and self.__twoWaySync then
Log.info("[Push] Update: {}", instancePath)
table.insert(pushPatch.updated, change)
elseif selection == "pull" and self.__twoWaySync then
-- Send Studio state back to Rojo
if instance then
local propertiesToSync = change.changedProperties
@ -723,7 +734,7 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
continue
end
end
Log.trace("[Pull] Update: {}", instancePath)
Log.info("[Pull] Update: {}", instancePath)
local update = encodePatchUpdate(instance, change.id, propertiesToSync)
if update then
table.insert(pullPatch.updated, update)
@ -754,7 +765,7 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
if selection == "push" then
-- Apply Rojo removal to Studio (delete the instance)
Log.trace("[Push] Delete: {}", instancePath)
Log.info("[Push] Delete: {}", instancePath)
table.insert(pushPatch.removed, idOrInstance)
elseif selection == "pull" and self.__twoWaySync then
-- Syncback: Create file in Rojo from Studio instance
@ -789,7 +800,7 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
if selection == "push" then
-- Apply Rojo addition to Studio
Log.trace("[Push] Add: {}.{}", parentPath, instanceName)
Log.info("[Push] Add: {}.{}", parentPath, instanceName)
pushPatch.added[id] = change
elseif selection == "pull" and self.__twoWaySync then
-- Don't add in Studio, remove from Rojo
@ -810,12 +821,12 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
for _ in pairs(pushPatch.added) do
addCount += 1
end
Log.debug(
"Applying to Studio: {} additions, {} removals, {} updates",
addCount,
#pushPatch.removed,
#pushPatch.updated
)
Log.info(
"Applying to Studio: {} additions, {} removals, {} updates",
addCount,
#pushPatch.removed,
#pushPatch.updated
)
self:__applyPatch(pushPatch)
end