Skip to content

Conversation

@dsarno
Copy link
Collaborator

@dsarno dsarno commented Jan 26, 2026

Human speaking: The manage_prefabs tool has no reason to open the stage -- it can operate on prefabs headlessly without the stage (a very human affordance). So getting rid of the stage also helps with issues like the save dialog popping up, and worrying about marking stage files as dirty, which were interfering with clean prefab ops. So let's try this and see how it works.

Summary

  • Removes stage-based prefab editing (open_stage, close_stage, save_open_stage) which had issues with dirty flag tracking and potential save dialogs
  • Adds modify_contents action that uses PrefabUtility.LoadPrefabContents() for headless prefab editing - ideal for automated/agentic workflows
  • Supports targeting objects by name or path (e.g., "Turret/Barrel")
  • Supports transform, tag, layer, setActive, name, and component operations
  • Deletes PrefabStage.cs resource (no longer needed)
  • Consolidates tests from 29 to 11 (includes complex multi-component prefab test)

Test plan

  • All 11 ManagePrefabsCrudTests pass
  • All 272 EditMode tests pass (286 passed, 4 explicitly skipped)
  • All 248 pytest tests pass
  • Tested complex prefab operations (Vehicle with wheels, turret, barrel - multiple components per object)

🤖 Generated with Claude Code

Summary by Sourcery

Replace stage-based prefab editing with a new headless modify_contents flow and update bindings/tests accordingly.

New Features:

  • Introduce a modify_contents action for headless prefab editing that can target prefab objects by name or path and apply transform, tagging, layering, activation, naming, parenting, and component changes.

Enhancements:

  • Simplify ManagePrefabs tool API by removing open_stage/save_open_stage/close_stage actions and consolidating supported actions to create_from_gameobject, get_info, get_hierarchy, and modify_contents.
  • Refine ManagePrefabs CRUD tests to focus on core behaviors (create_from_gameobject, get_info, get_hierarchy, modify_contents) and add coverage for complex multi-component prefabs and error handling.
  • Extend Python manage_prefabs tool wrapper to expose modify_contents parameters and update action surface/required parameter validation.
  • Tighten path validation and error messaging around prefab paths, targets, and invalid operations during prefab management.

Documentation:

  • Update tool descriptions and annotations to emphasize headless prefab editing workflows and the current set of supported actions.

Tests:

  • Consolidate and rewrite prefab CRUD/editing tests to exercise headless modify_contents behavior, complex prefab hierarchies, and parameter validation while reducing overall test count.

Chores:

  • Remove the unused PrefabStage editor resource now that stage-based editing is no longer supported.

Summary by CodeRabbit

  • New Features

    • Added headless prefab content modification: edit prefab transforms, components, names, tags, layers, active state and parenting without opening a prefab stage.
  • Refactor

    • Replaced staged open/close/save prefab workflow with a streamlined direct-modification flow; tooling/UI now use modify-first semantics.
  • Tests

    • Updated and consolidated tests to validate headless modifications, nested prefab hierarchies, and parameter validation.

✏️ Tip: You can customize this high-level summary in your review settings.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 26, 2026

Reviewer's Guide

Replaces stage-based prefab editing with a new headless modify_contents workflow and updates the Python tool and tests accordingly, while removing the now-unused PrefabStage resource.

Sequence diagram for headless modify_contents prefab editing workflow

sequenceDiagram
    actor Agent
    participant ManagePrefabsTool
    participant UnityBridge
    participant ManagePrefabs
    participant PrefabUtility
    participant AssetDatabase

    Agent->>ManagePrefabsTool: manage_prefabs(action=modify_contents, prefab_path, target, ...)
    ManagePrefabsTool->>ManagePrefabsTool: Build params dict (position, rotation, scale, name, tag, layer, setActive, parent, componentsToAdd, componentsToRemove)
    ManagePrefabsTool->>UnityBridge: send_with_unity_instance(tool=manage_prefabs, action=modify_contents, params)
    UnityBridge->>ManagePrefabs: HandleCommand(params)
    ManagePrefabs->>ManagePrefabs: Validate prefabPath via AssetPathUtility.SanitizeAssetPath
    ManagePrefabs->>PrefabUtility: LoadPrefabContents(sanitizedPath)
    PrefabUtility-->>ManagePrefabs: prefabContents GameObject

    ManagePrefabs->>ManagePrefabs: FindInPrefabContents(prefabContents, target)
    ManagePrefabs->>ManagePrefabs: ApplyModificationsToPrefabObject(targetGo, params, prefabContents)
    ManagePrefabs-->>ManagePrefabs: (modified, error)
    alt error is not null
        ManagePrefabs-->>UnityBridge: ErrorResponse
        UnityBridge-->>ManagePrefabsTool: error payload
        ManagePrefabsTool-->>Agent: error result
    else no error
        ManagePrefabs->>PrefabUtility: SaveAsPrefabAsset(prefabContents, sanitizedPath, out success)
        PrefabUtility-->>ManagePrefabs: success flag
        alt success is false
            ManagePrefabs-->>UnityBridge: ErrorResponse
            UnityBridge-->>ManagePrefabsTool: error payload
            ManagePrefabsTool-->>Agent: error result
        else success is true
            ManagePrefabs->>AssetDatabase: Refresh
            ManagePrefabs-->>UnityBridge: SuccessResponse(prefabPath, targetName, modified, transform, componentTypes)
            UnityBridge-->>ManagePrefabsTool: success payload
            ManagePrefabsTool-->>Agent: success result
        end
    end
    ManagePrefabs->>PrefabUtility: UnloadPrefabContents(prefabContents)
Loading

Class diagram for updated ManagePrefabs headless editing APIs

classDiagram
    class ManagePrefabs {
        <<static>>
        -ACTION_CREATE_FROM_GAMEOBJECT string
        -ACTION_GET_INFO string
        -ACTION_GET_HIERARCHY string
        -ACTION_MODIFY_CONTENTS string
        -SupportedActions string
        +HandleCommand(@params JObject) object
        -CreatePrefabFromGameObject(@params JObject) object
        -GetInfo(@params JObject) object
        -GetHierarchy(@params JObject) object
        -ModifyContents(@params JObject) object
        -FindInPrefabContents(prefabContents GameObject, target string) GameObject
        -ApplyModificationsToPrefabObject(targetGo GameObject, @params JObject, prefabRoot GameObject) (bool, ErrorResponse)
        -BuildHierarchyItemsRecursive(transform Transform, parentPath string, items List~HierarchyItem~) void
    }

    class ErrorResponse {
        +ErrorResponse(message string)
        +message string
        +success bool
    }

    class SuccessResponse {
        +SuccessResponse(message string, data object)
        +message string
        +data object
        +success bool
    }

    class VectorParsing {
        +ParseVector3(token JToken) Vector3?
    }

    class ComponentResolver {
        +TryResolve(typeName string, componentType out Type, error out string) bool
    }

    class PrefabUtilityHelper {
        +GetComponentTypeNames(targetGo GameObject) List~string~
    }

    class AssetPathUtility {
        +SanitizeAssetPath(path string) string
    }

    class McpLog {
        +Info(message string) void
        +Warn(message string) void
        +Error(message string) void
    }

    class PrefabUtility {
        +LoadPrefabContents(assetPath string) GameObject
        +SaveAsPrefabAsset(prefabContents GameObject, assetPath string, success out bool) void
        +UnloadPrefabContents(prefabContents GameObject) void
    }

    class GameObject {
        +name string
        +tag string
        +layer int
        +activeSelf bool
        +transform Transform
        +SetActive(value bool) void
        +AddComponent(componentType Type) Component
        +GetComponent(componentType Type) Component
    }

    class Transform {
        +localPosition Vector3
        +localEulerAngles Vector3
        +localScale Vector3
        +parent Transform
        +SetParent(parent Transform, worldPositionStays bool) void
        +IsChildOf(parent Transform) bool
        +Find(path string) Transform
        +GetComponentsInChildren(includeInactive bool) List~Transform~
    }

    class LayerMask {
        +NameToLayer(layerName string) int
    }

    class UnityEngineObject {
    }

    class Component {
    }

    class Vector3 {
        +x float
        +y float
        +z float
    }

    ManagePrefabs --> ErrorResponse : uses
    ManagePrefabs --> SuccessResponse : uses
    ManagePrefabs --> VectorParsing : uses
    ManagePrefabs --> ComponentResolver : uses
    ManagePrefabs --> PrefabUtilityHelper : uses
    ManagePrefabs --> AssetPathUtility : uses
    ManagePrefabs --> McpLog : uses
    ManagePrefabs --> PrefabUtility : uses
    ManagePrefabs --> GameObject : manipulates
    GameObject --> Transform : has
    Component --> UnityEngineObject : inherits
    GameObject --> UnityEngineObject : inherits
Loading

File-Level Changes

Change Details Files
Replace stage-based prefab editing APIs with a headless modify_contents action in ManagePrefabs.
  • Remove open_stage, close_stage, and save_open_stage actions and their supporting save/validation helpers from ManagePrefabs.
  • Introduce a modify_contents action that uses PrefabUtility.LoadPrefabContents/SaveAsPrefabAsset for headless editing without UI or prefab stages.
  • Implement FindInPrefabContents and ApplyModificationsToPrefabObject helpers to support targeting by name/path and applying transform, hierarchy, tag, layer, active state, and component changes.
  • Return detailed success metadata (transform, component types, target name) from modify_contents and ensure prefab contents are always unloaded.
MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
Expand the server-side manage_prefabs tool API to support modify_contents and drop stage-related actions.
  • Update REQUIRED_PARAMS and action Literal to remove open_stage/save_open_stage/close_stage and add modify_contents.
  • Add parameters for modify_contents (position, rotation, scale, name, tag, layer, set_active, parent, components_to_add, components_to_remove) and map them into the JSON payload sent to Unity.
  • Adjust docstring/description to describe the new action set and recommend modify_contents for headless editing.
Server/src/services/tools/manage_prefabs.py
Refactor and consolidate prefab CRUD/edit tests around create_from_gameobject, get_info, get_hierarchy, and modify_contents.
  • Simplify and merge multiple create_from_gameobject tests into a single CreateFromGameObject_HandlesExistingPrefabsAndLinks covering unlinkIfInstance, allowOverwrite, and unique-path behavior.
  • Trim GetInfo and GetHierarchy tests to validate key metadata and nesting information while adding coverage for nested prefabs using the new helper factories.
  • Add a new suite of ModifyContents tests covering transform edits without opening stages, name/path targeting, component add/remove, property changes, complex multi-component prefabs, and error handling for invalid parameters and paths.
  • Add CreateNestedTestPrefab and CreateComplexTestPrefab helpers and minor cleanups like concise prefab creation error handling.
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsCrudTests.cs
Remove obsolete PrefabStage resource assets that are no longer used by the tool.
  • Delete PrefabStage.cs and its corresponding .meta file from Editor resources.
MCPForUnity/Editor/Resources/Editor/PrefabStage.cs
MCPForUnity/Editor/Resources/Editor/PrefabStage.cs.meta

Possibly linked issues

  • #0: Issue reports manage_prefabs action mismatch; PR changes SupportedActions and Python tool schema to align and restore editing.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 26, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Removed the PrefabStage editor resource and its "get_prefab_stage" command; replaced stage-based prefab workflows with a headless modify_contents action for direct prefab content edits (transforms, components, properties) via ManagePrefabs and updated server tooling and tests accordingly.

Changes

Cohort / File(s) Summary
PrefabStage Removal
MCPForUnity/Editor/Resources/Editor/PrefabStage.cs, MCPForUnity/Editor/Resources/Editor/PrefabStage.cs.meta
Deleted the PrefabStage class, its command handler (get_prefab_stage), and the associated .meta file.
Editor: Headless Prefab Modification
MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
Removed open_stage/close_stage/save_open_stage actions; added modify_contents action and handler plus helpers to load, find, apply modifications, save, and unload prefab contents using headless PrefabUtility APIs. Updated supported actions list and removed stage-serialization exposure.
Server: Manage Prefabs API
Server/src/services/tools/manage_prefabs.py
Replaced stage actions with modify_contents in action literals, parameter validation, and payload mapping. Added modify payload fields (position, rotation, scale, name, tag, layer, set_active, parent, components_to_add/remove) and back-compat mapping for nametarget for create_from_gameobject. Removed save_before_close/force.
Tests: EditMode Prefab Tests
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsCrudTests.cs
Consolidated and renamed tests to reflect headless modify flow; removed stage lifecycle tests; added tests and helpers for ModifyContents scenarios, nested/complex prefabs, and component add/remove cases.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ManagePrefabs as ManagePrefabs (Editor)
    participant PrefabUtility
    participant AssetDatabase

    Client->>ManagePrefabs: modify_contents(prefab_path, target, modifications)
    ManagePrefabs->>PrefabUtility: LoadPrefabContents(prefab_path)
    PrefabUtility-->>ManagePrefabs: prefab_contents
    ManagePrefabs->>ManagePrefabs: Find target object in prefab_contents
    ManagePrefabs->>ManagePrefabs: Apply modifications (transform, components, properties)
    ManagePrefabs->>PrefabUtility: SaveAsPrefabAsset(prefab_contents, prefab_path)
    PrefabUtility->>AssetDatabase: Register/Update assets
    AssetDatabase-->>PrefabUtility: saved
    ManagePrefabs->>PrefabUtility: UnloadPrefabContents(prefab_contents)
    ManagePrefabs-->>Client: success_response(modified_state, object_info)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • msanatan

Poem

🐰 I hopped through code with nimble feet,
Removed the stage, kept edits neat.
Headless tweaks to prefab cores,
Transforms, parts, and metadata roars.
A joyful hop — no stage, much cheer!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: replace prefab stage actions with headless modify_contents' clearly and concisely summarizes the main change: replacing stage-based prefab editing with a new headless modify_contents action.
Description check ✅ Passed The description comprehensively covers all key aspects: type of change (new feature), detailed changes made, test results, and related context. It follows the repository template structure well.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs:476-478` </location>
<code_context>
+                    return modifyResult.error;
+                }
+
+                // Save the prefab
+                bool success;
+                PrefabUtility.SaveAsPrefabAsset(prefabContents, sanitizedPath, out success);
+
+                if (!success)
</code_context>

<issue_to_address>
**suggestion (performance):** Skip saving when no modifications were made to avoid unnecessary asset writes.

You already compute `modifyResult.modified`, but still call `SaveAsPrefabAsset` unconditionally. In automated/headless use this creates unnecessary writes and version-control noise. Consider returning success early when `modified == false` (and reflecting that in the message), and only calling `SaveAsPrefabAsset` when changes were actually made.

Suggested implementation:

```csharp
                // Apply modifications
                var modifyResult = ApplyModificationsToPrefabObject(targetGo, @params, prefabContents);
                if (modifyResult.error != null)
                {
                    return modifyResult.error;
                }

                // Skip saving when no modifications were made to avoid unnecessary asset writes
                if (!modifyResult.modified)
                {
                    return new SuccessResponse($"Prefab '{sanitizedPath}' is already up to date; no changes were applied.");
                }

                // Save the prefab
                bool success;
                PrefabUtility.SaveAsPrefabAsset(prefabContents, sanitizedPath, out success);

                if (!success)
                {
                    return new ErrorResponse($"Failed to save prefab '{sanitizedPath}'.");
                }

                return new SuccessResponse($"Prefab '{sanitizedPath}' updated successfully.");

```

The exact success/response types may differ in your codebase. You will need to:
1. Replace `SuccessResponse` with whatever success response type you actually use (e.g. `OkResponse`, `Success`, etc.), and adjust constructor arguments as appropriate.
2. If you already have standardized success/error message patterns, align the new messages with those conventions.
3. Ensure this method's return type matches the `SuccessResponse`/`ErrorResponse` usage above; if it is a common response base type, both subclasses should be compatible.
4. If save failure handling is implemented elsewhere in this method, integrate the `!modified` early-return in a way that doesn't duplicate or conflict with existing save/error logic.
</issue_to_address>

### Comment 2
<location> `Server/src/services/tools/manage_prefabs.py:23-30` </location>
<code_context>
 }


 @mcp_for_unity_tool(
     description=(
         "Manages Unity Prefab assets and stages. "
</code_context>

<issue_to_address>
**suggestion:** Update the tool description to reflect that stage-based actions are no longer supported.

The decorator description still mentions "stages" even though `open_stage`, `save_open_stage`, and `close_stage` have been removed and only headless operations remain. Please rephrase it to emphasize prefab asset CRUD/headless editing and remove references to stages to avoid confusing tool consumers.

```suggestion
@mcp_for_unity_tool(
    description=(
        "Manages Unity Prefab assets via headless operations (no UI, no prefab stages). "
        "Actions: get_info, get_hierarchy, create_from_gameobject, modify_contents. "
        "Use modify_contents for headless prefab editing (no UI, no dialogs) - ideal for automated workflows. "
        "Use manage_asset action=search filterType=Prefab to list prefabs."
    ),
    annotations=ToolAnnotations(
```
</issue_to_address>

### Comment 3
<location> `TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsCrudTests.cs:45-51` </location>
<code_context>
         #region CREATE Tests

         [Test]
-        public void CreateFromGameObject_CreatesNewPrefab_WithValidParameters()
+        public void CreateFromGameObject_CreatesNewPrefab()
</code_context>

<issue_to_address>
**suggestion (testing):** Reintroduce a test that verifies `create_from_gameobject` can find inactive scene objects when `searchInactive` is true.

The previous `CreateFromGameObject_FindsInactiveObject_WhenSearchInactiveIsTrue` test covered both failure without `searchInactive` and success with it enabled, which the new combined test no longer exercises. Please add a dedicated `searchInactive` test (even a simplified one) so regressions in inactive-object lookup are still caught.

Suggested implementation:

```csharp
        #region CREATE Tests

        [Test]
        public void CreateFromGameObject_CreatesNewPrefab()
        {
            string prefabPath = Path.Combine(TempDirectory, "NewPrefab.prefab").Replace('\\', '/');
            GameObject sceneObject = new GameObject("TestObject");
                    ["prefabPath"] = prefabPath
                }));
        }

        [Test]
        public void CreateFromGameObject_FindsInactiveObject_WhenSearchInactiveIsTrue()
        {
            // Arrange
            string prefabPath = Path.Combine(TempDirectory, "InactivePrefab.prefab").Replace('\\', '/');
            GameObject sceneObject = new GameObject("InactiveTestObject");
            sceneObject.SetActive(false);

            // Act + Assert:
            // 1) Without searchInactive we expect the call to fail / not find the object.
            // 2) With searchInactive = true we expect the call to succeed and create the prefab.

            // 1) Call without searchInactive flag and verify it fails appropriately.
            //    (Use the same invocation pattern as CreateFromGameObject_CreatesNewPrefab but
            //     omit the searchInactive parameter and assert the expected failure.)
            //    Example (adjust to your helper/assert style):
            //
            // Assert.Throws<SomeExpectedException>(() =>
            //     CallTool("create_from_gameobject", new Dictionary<string, object>
            //     {
            //         ["gameObjectName"] = sceneObject.name,
            //         ["prefabPath"]    = prefabPath
            //     }));
            //
            // or if you use result objects:
            //
            // var resultWithoutSearchInactive = CallTool(...);
            // Assert.IsFalse(resultWithoutSearchInactive.success);

            // 2) Call with searchInactive = true and verify it now succeeds and creates the prefab.
            //
            // var resultWithSearchInactive = CallTool("create_from_gameobject", new Dictionary<string, object>
            // {
            //     ["gameObjectName"] = sceneObject.name,
            //     ["prefabPath"]     = prefabPath,
            //     ["searchInactive"] = true
            // });
            //
            // Assert.IsTrue(resultWithSearchInactive.success);
            // Assert.That(File.Exists(prefabPath), Is.True);
        }

```

To fully wire this test to your existing infrastructure you should:
1. Replace the commented `CallTool`/assert snippets with the actual helper you use in `CreateFromGameObject_CreatesNewPrefab` (e.g. your MCP/tool invocation method and its result type).
2. Use the same parameter keys that `create_from_gameobject` currently expects:
   - For the scene object reference, either pass the object itself (if that’s how the existing test does it) or pass `sceneObject.name` and adjust the key accordingly.
   - Add the `"searchInactive"` (or your actual flag name) boolean parameter only in the success case.
3. Use the same assertion style as the other tests:
   - For the failure case: either assert a specific exception is thrown or assert the returned result indicates failure.
   - For the success case: assert success and that the prefab was created at `prefabPath` (e.g. via `File.Exists(prefabPath)` or your existing helper).
4. If your existing test uses async patterns (e.g. `await CallToolAsync(...)`), update this new test to be `async Task` and `await` the calls consistently.
</issue_to_address>

### Comment 4
<location> `TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsCrudTests.cs:260` </location>
<code_context>
-
         [Test]
-        public void SaveOpenStage_WithForce_SavesEvenWhenNotDirty()
+        public void ModifyContents_TargetsChildrenByNameAndPath()
         {
-            string prefabPath = CreateTestPrefab("ForceSaveTest");
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests for `modify_contents` parent re-parenting behavior, including the hierarchy loop guard.

`modify_contents` also supports a `parent` parameter and has an `IsChildOf` guard to prevent hierarchy loops, but those paths aren’t covered by tests. Please add one test that verifies a valid re-parent within the prefab succeeds, and another that verifies attempting to parent a node under one of its descendants fails with the expected error, so we cover both the normal and loop-prevention behavior.

Suggested implementation:

```csharp
        [Test]
        public void ModifyContents_ReparentsChildWithinPrefab()
        {
            string prefabPath = CreateNestedTestPrefab("ReparentTest");

            try
            {
                // Reparent a child under another sibling within the same prefab
                var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
                {
                    ["command"] = "modify_contents",
                    ["asset_path"] = prefabPath,
                    ["targets"] = new JArray
                    {
                        new JObject
                        {
                            // These names/paths assume the nested prefab created by CreateNestedTestPrefab
                            // exposes a root with at least two children; adjust as needed to match
                            ["path"] = "Root/ChildA",
                            ["parent"] = "Root/ChildB"
                        }
                    }
                }));

                // Verify the command succeeded (no hierarchy loop triggered)
                Assert.AreEqual("ok", (string)result["status"], "Expected modify_contents to succeed for a valid reparent within the prefab.");
            }
            finally
            {
                SafeDeleteAsset(prefabPath);
            }
        }

        [Test]
        public void ModifyContents_PreventsHierarchyLoopsWhenReparenting()
        {
            string prefabPath = CreateNestedTestPrefab("HierarchyLoopTest");

            try
            {
                // Attempt to parent an ancestor under one of its descendants, which should be rejected
                var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
                {
                    ["command"] = "modify_contents",
                    ["asset_path"] = prefabPath,
                    ["targets"] = new JArray
                    {
                        new JObject
                        {
                            // Attempt to parent Root under one of its descendants, e.g. Root/ChildA
                            // so that the underlying IsChildOf guard is exercised.
                            ["path"] = "Root",
                            ["parent"] = "Root/ChildA"
                        }
                    }
                }));

                // Verify the command failed with an appropriate error
                Assert.AreEqual("error", (string)result["status"], "Expected modify_contents to fail when creating a hierarchy loop.");
                StringAssert.Contains(
                    "hierarchy", 
                    (string)result["message"], 
                    "Expected error message to mention invalid hierarchy / loop prevention.");
            }
            finally
            {
                SafeDeleteAsset(prefabPath);
            }
        }

        [Test]
        public void ModifyContents_TargetsChildrenByNameAndPath()
        {
            string prefabPath = CreateNestedTestPrefab("TargetTest");

            try
            {
                // Target by name
                var nameResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
                {

```

1. The exact object paths (`"Root/ChildA"`, `"Root/ChildB"`, `"Root"`) must be updated to match the actual hierarchy produced by `CreateNestedTestPrefab`. Replace these strings with the real node paths (for example, `"Parent/Child"` patterns the existing tests already assert on).
2. The response contract from `ManagePrefabs.HandleCommand` may differ (for example, using `"success"` instead of `"status"`, or naming the error field differently). Align the assertions:
   - Change `result["status"]` to the actual field used to indicate success/failure.
   - Change `result["message"]` and the `StringAssert.Contains("hierarchy", ...)` check to match the real error field name and message substring used when the `IsChildOf` guard triggers.
3. If `CreateNestedTestPrefab` does not create a root node named `"Root"` with at least two children, adjust the test to reparent one real child under another within the prefab, and for the loop test, choose an ancestor/descendant pair that will trigger the `IsChildOf` check.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 45 to 51
#region CREATE Tests

[Test]
public void CreateFromGameObject_CreatesNewPrefab_WithValidParameters()
public void CreateFromGameObject_CreatesNewPrefab()
{
string prefabPath = Path.Combine(TempDirectory, "NewPrefab.prefab").Replace('\\', '/');
GameObject sceneObject = new GameObject("TestObject");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Reintroduce a test that verifies create_from_gameobject can find inactive scene objects when searchInactive is true.

The previous CreateFromGameObject_FindsInactiveObject_WhenSearchInactiveIsTrue test covered both failure without searchInactive and success with it enabled, which the new combined test no longer exercises. Please add a dedicated searchInactive test (even a simplified one) so regressions in inactive-object lookup are still caught.

Suggested implementation:

        #region CREATE Tests

        [Test]
        public void CreateFromGameObject_CreatesNewPrefab()
        {
            string prefabPath = Path.Combine(TempDirectory, "NewPrefab.prefab").Replace('\\', '/');
            GameObject sceneObject = new GameObject("TestObject");
                    ["prefabPath"] = prefabPath
                }));
        }

        [Test]
        public void CreateFromGameObject_FindsInactiveObject_WhenSearchInactiveIsTrue()
        {
            // Arrange
            string prefabPath = Path.Combine(TempDirectory, "InactivePrefab.prefab").Replace('\\', '/');
            GameObject sceneObject = new GameObject("InactiveTestObject");
            sceneObject.SetActive(false);

            // Act + Assert:
            // 1) Without searchInactive we expect the call to fail / not find the object.
            // 2) With searchInactive = true we expect the call to succeed and create the prefab.

            // 1) Call without searchInactive flag and verify it fails appropriately.
            //    (Use the same invocation pattern as CreateFromGameObject_CreatesNewPrefab but
            //     omit the searchInactive parameter and assert the expected failure.)
            //    Example (adjust to your helper/assert style):
            //
            // Assert.Throws<SomeExpectedException>(() =>
            //     CallTool("create_from_gameobject", new Dictionary<string, object>
            //     {
            //         ["gameObjectName"] = sceneObject.name,
            //         ["prefabPath"]    = prefabPath
            //     }));
            //
            // or if you use result objects:
            //
            // var resultWithoutSearchInactive = CallTool(...);
            // Assert.IsFalse(resultWithoutSearchInactive.success);

            // 2) Call with searchInactive = true and verify it now succeeds and creates the prefab.
            //
            // var resultWithSearchInactive = CallTool("create_from_gameobject", new Dictionary<string, object>
            // {
            //     ["gameObjectName"] = sceneObject.name,
            //     ["prefabPath"]     = prefabPath,
            //     ["searchInactive"] = true
            // });
            //
            // Assert.IsTrue(resultWithSearchInactive.success);
            // Assert.That(File.Exists(prefabPath), Is.True);
        }

To fully wire this test to your existing infrastructure you should:

  1. Replace the commented CallTool/assert snippets with the actual helper you use in CreateFromGameObject_CreatesNewPrefab (e.g. your MCP/tool invocation method and its result type).
  2. Use the same parameter keys that create_from_gameobject currently expects:
    • For the scene object reference, either pass the object itself (if that’s how the existing test does it) or pass sceneObject.name and adjust the key accordingly.
    • Add the "searchInactive" (or your actual flag name) boolean parameter only in the success case.
  3. Use the same assertion style as the other tests:
    • For the failure case: either assert a specific exception is thrown or assert the returned result indicates failure.
    • For the success case: assert success and that the prefab was created at prefabPath (e.g. via File.Exists(prefabPath) or your existing helper).
  4. If your existing test uses async patterns (e.g. await CallToolAsync(...)), update this new test to be async Task and await the calls consistently.


[Test]
public void SaveOpenStage_WithForce_SavesEvenWhenNotDirty()
public void ModifyContents_TargetsChildrenByNameAndPath()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add tests for modify_contents parent re-parenting behavior, including the hierarchy loop guard.

modify_contents also supports a parent parameter and has an IsChildOf guard to prevent hierarchy loops, but those paths aren’t covered by tests. Please add one test that verifies a valid re-parent within the prefab succeeds, and another that verifies attempting to parent a node under one of its descendants fails with the expected error, so we cover both the normal and loop-prevention behavior.

Suggested implementation:

        [Test]
        public void ModifyContents_ReparentsChildWithinPrefab()
        {
            string prefabPath = CreateNestedTestPrefab("ReparentTest");

            try
            {
                // Reparent a child under another sibling within the same prefab
                var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
                {
                    ["command"] = "modify_contents",
                    ["asset_path"] = prefabPath,
                    ["targets"] = new JArray
                    {
                        new JObject
                        {
                            // These names/paths assume the nested prefab created by CreateNestedTestPrefab
                            // exposes a root with at least two children; adjust as needed to match
                            ["path"] = "Root/ChildA",
                            ["parent"] = "Root/ChildB"
                        }
                    }
                }));

                // Verify the command succeeded (no hierarchy loop triggered)
                Assert.AreEqual("ok", (string)result["status"], "Expected modify_contents to succeed for a valid reparent within the prefab.");
            }
            finally
            {
                SafeDeleteAsset(prefabPath);
            }
        }

        [Test]
        public void ModifyContents_PreventsHierarchyLoopsWhenReparenting()
        {
            string prefabPath = CreateNestedTestPrefab("HierarchyLoopTest");

            try
            {
                // Attempt to parent an ancestor under one of its descendants, which should be rejected
                var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
                {
                    ["command"] = "modify_contents",
                    ["asset_path"] = prefabPath,
                    ["targets"] = new JArray
                    {
                        new JObject
                        {
                            // Attempt to parent Root under one of its descendants, e.g. Root/ChildA
                            // so that the underlying IsChildOf guard is exercised.
                            ["path"] = "Root",
                            ["parent"] = "Root/ChildA"
                        }
                    }
                }));

                // Verify the command failed with an appropriate error
                Assert.AreEqual("error", (string)result["status"], "Expected modify_contents to fail when creating a hierarchy loop.");
                StringAssert.Contains(
                    "hierarchy", 
                    (string)result["message"], 
                    "Expected error message to mention invalid hierarchy / loop prevention.");
            }
            finally
            {
                SafeDeleteAsset(prefabPath);
            }
        }

        [Test]
        public void ModifyContents_TargetsChildrenByNameAndPath()
        {
            string prefabPath = CreateNestedTestPrefab("TargetTest");

            try
            {
                // Target by name
                var nameResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
                {
  1. The exact object paths ("Root/ChildA", "Root/ChildB", "Root") must be updated to match the actual hierarchy produced by CreateNestedTestPrefab. Replace these strings with the real node paths (for example, "Parent/Child" patterns the existing tests already assert on).
  2. The response contract from ManagePrefabs.HandleCommand may differ (for example, using "success" instead of "status", or naming the error field differently). Align the assertions:
    • Change result["status"] to the actual field used to indicate success/failure.
    • Change result["message"] and the StringAssert.Contains("hierarchy", ...) check to match the real error field name and message substring used when the IsChildOf guard triggers.
  3. If CreateNestedTestPrefab does not create a root node named "Root" with at least two children, adjust the test to reparent one real child under another within the prefab, and for the loop test, choose an ancestor/descendant pair that will trigger the IsChildOf check.

@dsarno dsarno force-pushed the feature/prefab-headless-editing branch from f400086 to b0702af Compare January 26, 2026 18:50
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Server/src/services/tools/manage_prefabs.py (1)

47-69: Back‑compat gap: create_from_gameobject now rejects name as a target alias.

The server-side required-param check only accepts target, while the Unity side still accepts name as a fallback. Any existing callers sending name only will now fail before reaching Unity. Consider mapping name → target for this action before validation.

🛠️ Suggested fix
 ) -> dict[str, Any]:
+    # Back-compat: accept "name" as alias for "target" in create_from_gameobject
+    if action == "create_from_gameobject":
+        if (target is None or (isinstance(target, str) and not target.strip())) and isinstance(name, str) and name.strip():
+            target = name
+
     # Validate required parameters
     required = REQUIRED_PARAMS.get(action, [])

Removes stage-based prefab editing (open_stage, close_stage, save_open_stage)
in favor of headless modify_contents action that uses PrefabUtility.LoadPrefabContents
for reliable automated workflows without UI dialogs.

Changes:
- Remove open_stage, close_stage, save_open_stage actions
- Add modify_contents action for headless prefab editing
- Support targeting objects by name or path (e.g., "Turret/Barrel")
- Support transform, tag, layer, setActive, name, parent, components operations
- Skip saving when no modifications made (avoids unnecessary asset writes)
- Delete PrefabStage.cs resource (no longer needed)
- Update Python tool description to remove "stages" reference
- Consolidate tests from 29 to 14 (covers complex prefabs, reparenting, hierarchy loop guard)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@dsarno dsarno force-pushed the feature/prefab-headless-editing branch from b0702af to 4d10cfe Compare January 26, 2026 18:51
@dsarno dsarno merged commit 17eb171 into CoplayDev:beta Jan 26, 2026
1 of 2 checks passed
@dsarno dsarno mentioned this pull request Jan 26, 2026
3 tasks
@dsarno dsarno deleted the feature/prefab-headless-editing branch January 29, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant