Skip to content

Conversation

@dsarno
Copy link

@dsarno dsarno commented Jan 25, 2026

Summary

This PR improves the prefab stage save functionality for automated/CI workflows, addressing the dialog concern raised in PR CoplayDev#611.

Changes:

  • Add force parameter to save_open_stage: For automated workflows where Unity's isDirty flag may not be correctly set when changes are made programmatically
  • Use PrefabUtility.SaveAsPrefabAsset: Saves prefabs without displaying any dialogs, suitable for unattended automation
  • Mark prefab stage scene dirty: When modifying GameObjects in prefab mode via manage_gameobject, the prefab stage scene is now properly marked dirty
  • Skip unnecessary saves: When force=false (default) and no changes detected, returns early without falsely marking the prefab dirty

Usage

For automated workflows:

manage_prefabs(action="save_open_stage", force=true)

Testing

Tested the complete workflow:

  1. Open prefab stage ✅
  2. Modify GameObject position via manage_gameobject
  3. Save with force=true
  4. Close and reopen - changes persist ✅
  5. No dialogs appeared during any step

🤖 Generated with Claude Code

Summary by Sourcery

Add a new cross-platform texture management toolchain, enhance screenshot and prefab save behavior for automation, and improve CLI and docs coverage.

New Features:

  • Introduce a manage_texture editor tool and corresponding server/CLI support for procedural texture creation, modification, deletion, and sprite generation.
  • Add manage_texture to the Unity MCP tool registry and CLI optional commands list.
  • Support a force flag when saving prefab stages via manage_prefabs for automated workflows without dialogs.
  • Provide a manage_scene screenshot action with async-aware capture across Unity versions.

Bug Fixes:

  • Ensure GameObject modifications correctly mark the appropriate scene or prefab stage as dirty so changes are saved.
  • Stabilize screenshot capture by always using ScreenshotUtility, repainting Game View in the editor, and importing assets only after files exist.
  • Normalize action strings using ToLowerInvariant in several editor tools to avoid locale-dependent behavior.

Enhancements:

  • Refine ScreenshotUtility to support async ScreenCapture on newer Unity versions, camera-based fallback on older versions, reusable capture path logic, and memory-safe texture handling.
  • Improve editor window styling by programmatically applying a section-last class to the last section in each stack.
  • Use newer FindObjectsByType API when available for component searches, with conditional fallback for older Unity versions.
  • Log detailed errors when component enumeration fails while building GameObject summaries.

Documentation:

  • Document the manage_scene screenshot behavior across Unity versions in both English and Chinese development docs.
  • Update the main README to advertise the new manage_texture tool.

Tests:

  • Add CLI tests for new texture commands covering creation, sprite generation, modification, deletion, invalid JSON handling, and option precedence.
  • Add integration tests for manage_texture to validate parameter normalization, import settings mapping, and error handling.

toxifly and others added 5 commits January 24, 2026 17:07
* fix: improve manage_scene screenshot capture

* fix: address PR review feedback for screenshot capture

- Gate pre-2022 ScreenCapture fallback warning to log only once
- Downgrade warning to Debug.Log to reduce log noise
- Refactor path-building into shared PrepareCaptureResult() helper
- Add conditional logging to catch blocks in BestEffortPrepareGameViewForScreenshot
- Add timeout/failure logging to ScheduleAssetImportWhenFileExists
- Fix grammar in README-DEV.md

* fix(unity): resolve screenshot import callback type + FindObjectsOfType deprecation

* chore: bump version to 9.2.0

* Update ManageScene.cs

* Update ScreenshotUtility.cs

* Update error logging in ManageScene.cs

---------

Co-authored-by: Marcus Sanatan <msanatan@gmail.com>
Co-authored-by: GitHub Actions <actions@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>
* Update for Texture2D/Sprite Generation

Given the choice to generate Texture2D based on patterns and color, also introduce pipeline to turn Texture2D direct to Sprite.
Update CLI command to include this too.

* Texture Size Set

Set texture size to 1024X1024 to avoid too large texture set

* Add image input

* Update to release direct error with large tex2d

* Fix for AI advice

* Update on action fetch line
Unity UI Toolkit does not support the :last-child pseudo-class. Replace
it with a .section-last class that is applied programmatically to the
last section in each .section-stack container.

Also moves the Configure All Detected Clients button to the bottom
of the Client Configuration section and makes it auto-width.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add force parameter to save_open_stage for automated workflows
  where isDirty may not be correctly set
- Use PrefabUtility.SaveAsPrefabAsset for dialog-free saving
- Mark prefab stage scene dirty when modifying GameObjects in prefab mode
- Skip save when no changes and force=false (prevents false dirty flag)

The force parameter ensures reliable saving in CI/automation scenarios
where Unity dirty tracking may be inconsistent with programmatic changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@sourcery-ai
Copy link

sourcery-ai bot commented Jan 25, 2026

Reviewer's Guide

Extends Unity MCP tooling with a new manage_texture pipeline (editor tool, server tool, CLI, docs, and tests), improves prefab save semantics for automated workflows, and enhances screenshot capture to be async-aware and more reliable across Unity versions, along with a few robustness and UX tweaks.

Sequence diagram for manage_texture end-to-end pipeline

sequenceDiagram
    actor Developer
    participant CLI_texture as CLI_texture_command
    participant Server_tool as Server_manage_texture_tool
    participant UnityTransport as Unity_transport
    participant UnityTool as ManageTexture
    participant TextureOps as TextureOps
    participant AssetDB as AssetDatabase

    Developer->>CLI_texture: unity_mcp texture create path width height color pattern palette
    CLI_texture->>CLI_texture: normalize_color normalize_palette normalize_import_settings
    CLI_texture->>Server_tool: manage_texture action=create path width height fill_color pattern palette import_settings

    Server_tool->>Server_tool: preflight
    Server_tool->>Server_tool: _normalize_color _normalize_palette _normalize_import_settings
    Server_tool->>UnityTransport: async_send_command_with_retry("manage_texture", params)

    UnityTransport->>UnityTool: HandleCommand(params JObject)
    UnityTool->>UnityTool: action dispatch create
    UnityTool->>UnityTool: AssetPathUtility.SanitizeAssetPath
    UnityTool->>UnityTool: EnsureDirectoryExists

    alt imagePath provided
        UnityTool->>UnityTool: File.ReadAllBytes imagePath
        UnityTool->>UnityTool: new Texture2D LoadImage
    else generated texture
        UnityTool->>TextureOps: FillTexture or ApplyPixelData or ApplyPatternToTexture
        TextureOps-->>UnityTool: populated Texture2D
    end

    UnityTool->>TextureOps: EncodeTexture(texture assetPath)
    TextureOps-->>UnityTool: byte[] imageData
    UnityTool->>UnityTool: File.WriteAllBytes
    UnityTool->>AssetDB: ImportAsset path ForceUpdate

    opt importSettings or spriteSettings
        UnityTool->>UnityTool: ConfigureTextureImporter or ConfigureAsSprite
        UnityTool->>AssetDB: SaveAndReimport via TextureImporter
    end

    UnityTool-->>UnityTransport: SuccessResponse path width height warnings
    UnityTransport-->>Server_tool: response dict
    Server_tool-->>CLI_texture: response dict
    CLI_texture-->>Developer: formatted output
Loading

Sequence diagram for forced prefab stage save workflow

sequenceDiagram
    actor CI
    participant Server_manage_gameobject as Server_manage_gameobject_tool
    participant Unity_Modify as GameObjectModify
    participant PrefabStageUtil as PrefabStageUtility
    participant SceneMgr as EditorSceneManager
    participant Server_manage_prefabs as Server_manage_prefabs_tool
    participant Unity_Prefabs as ManagePrefabs
    participant PrefabUtil as PrefabUtility
    participant AssetDB as AssetDatabase

    CI->>Server_manage_gameobject: manage_gameobject action=modify target params
    Server_manage_gameobject->>Unity_Modify: Handle params targetToken
    Unity_Modify->>Unity_Modify: apply changes to GameObject
    Unity_Modify->>Unity_Modify: EditorUtility.SetDirty(targetGo)
    Unity_Modify->>PrefabStageUtil: GetCurrentPrefabStage
    alt prefab stage open
        Unity_Modify->>SceneMgr: MarkSceneDirty(prefabStage.scene)
    else regular scene
        Unity_Modify->>SceneMgr: MarkSceneDirty(GetActiveScene)
    end
    Unity_Modify-->>Server_manage_gameobject: SuccessResponse

    CI->>Server_manage_prefabs: manage_prefabs action=save_open_stage force=true
    Server_manage_prefabs->>Unity_Prefabs: SaveOpenStage JObject{force=true}
    Unity_Prefabs->>PrefabStageUtil: GetCurrentPrefabStage
    PrefabStageUtil-->>Unity_Prefabs: PrefabStage stage
    Unity_Prefabs->>Unity_Prefabs: ValidateStage(stage)
    Unity_Prefabs->>Unity_Prefabs: wasDirty = stage.scene.isDirty
    Unity_Prefabs->>Unity_Prefabs: force = true
    Unity_Prefabs->>Unity_Prefabs: SaveAndRefreshStage(stage force=true)

    Unity_Prefabs->>Unity_Prefabs: validate prefabContentsRoot and assetPath
    Unity_Prefabs->>Unity_Prefabs: EditorUtility.SetDirty(stage.prefabContentsRoot)
    Unity_Prefabs->>SceneMgr: MarkSceneDirty(stage.scene)
    Unity_Prefabs->>PrefabUtil: SaveAsPrefabAsset(stage.prefabContentsRoot stage.assetPath out success)
    PrefabUtil-->>Unity_Prefabs: success=true
    Unity_Prefabs->>AssetDB: SaveAssets

    Unity_Prefabs-->>Server_manage_prefabs: SuccessResponse SerializeStage(stage)
    Server_manage_prefabs-->>CI: success message
Loading

Sequence diagram for async screenshot capture workflow

sequenceDiagram
    actor User
    participant ManageScene as ManageScene_CaptureScreenshot
    participant ScreenshotUtil as ScreenshotUtility
    participant GameView as EnsureGameView
    participant ScreenCap as ScreenCapture_or_Camera
    participant ImportScheduler as ScheduleAssetImportWhenFileExists
    participant EditorApp as EditorApplication
    participant AssetDB as AssetDatabase

    User->>ManageScene: manage_scene action=screenshot fileName superSize
    ManageScene->>ManageScene: resolvedSuperSize
    alt not Application.isBatchMode
        ManageScene->>GameView: EnsureGameView
        GameView->>EditorApp: ExecuteMenuItem Window.General.Game
        GameView->>GameView: EditorWindow.GetWindow GameView.Repaint
        GameView->>SceneView: RepaintAll
        GameView->>EditorApp: QueuePlayerLoopUpdate
    end

    ManageScene->>ScreenshotUtil: CaptureToAssetsFolder(fileName superSize ensureUniqueFileName=true)

    alt UNITY_2022_1_OR_NEWER
        ScreenshotUtil->>ScreenshotUtil: PrepareCaptureResult isAsync=true
        ScreenshotUtil->>ScreenCap: ScreenCapture.CaptureScreenshot(assetsRelativePath superSize)
        ScreenCap-->>ScreenshotUtil: async capture
        ScreenshotUtil-->>ManageScene: ScreenshotCaptureResult IsAsync=true
    else legacy Unity
        ScreenshotUtil->>ScreenshotUtil: FindAvailableCamera
        ScreenshotUtil->>ScreenshotUtil: PrepareCaptureResult isAsync=false
        ScreenshotUtil->>ScreenCap: CaptureFromCameraToAssetsFolder(camera fileName superSize ensureUniqueFileName)
        ScreenCap-->>ScreenshotUtil: sync file write
        ScreenshotUtil-->>ManageScene: ScreenshotCaptureResult IsAsync=false
    end

    alt result.IsAsync
        ManageScene->>ImportScheduler: ScheduleAssetImportWhenFileExists(assetsRelativePath fullPath timeoutSeconds)
        ImportScheduler->>EditorApp: register update callback
        loop until file exists or timeout
            ImportScheduler->>ImportScheduler: File.Exists(fullPath)
            alt file exists
                ImportScheduler->>AssetDB: ImportAsset(assetsRelativePath ForceSynchronousImport)
                ImportScheduler->>EditorApp: unregister callback
            else timeout
                ImportScheduler->>ImportScheduler: log warning
                ImportScheduler->>EditorApp: unregister callback
            end
        end
    else sync capture
        ManageScene->>AssetDB: ImportAsset(assetsRelativePath ForceSynchronousImport)
    end

    ManageScene-->>User: SuccessResponse message path fullPath superSize isAsync
Loading

Updated class diagram for texture management, prefab saving, and screenshots

classDiagram
    class ManageTexture {
        <<static>>
        -int MaxTextureDimension
        -int MaxTexturePixels
        -int MaxNoiseWork
        -List~string~ ValidActions
        +object HandleCommand(JObject @params)
        -object CreateTexture(JObject @params, bool asSprite)
        -object ModifyTexture(JObject @params)
        -object DeleteTexture(string path)
        -object ApplyPattern(JObject @params)
        -object ApplyGradient(JObject @params)
        -object ApplyNoise(JObject @params)
        -ErrorResponse ValidateDimensions(int width, int height, List~string~ warnings)
        -void ApplyPatternToTexture(Texture2D texture, string pattern, List~Color32~ palette, int patternSize)
        -Color32 GetPatternColor(int x, int y, string pattern, List~Color32~ palette, int size, int width, int height)
        -void ApplyLinearGradient(Texture2D texture, List~Color32~ palette, float angle)
        -void ApplyRadialGradient(Texture2D texture, List~Color32~ palette)
        -Color32 LerpPalette(List~Color32~ palette, float t)
        -void ApplyPerlinNoise(Texture2D texture, List~Color32~ palette, float scale, int octaves)
        -void ConfigureAsSprite(string path, JToken spriteSettings)
        -void ConfigureTextureImporter(string path, JToken importSettings)
        -bool TryParseEnum~T~(string value, out T result)
        -bool AssetExists(string path)
        -void EnsureDirectoryExists(string assetPath)
        -string GetAbsolutePath(string assetPath)
        -string ResolveImagePath(string imagePath)
    }

    class TextureOps {
        <<static>>
        +byte[] EncodeTexture(Texture2D texture, string assetPath)
        +void FillTexture(Texture2D texture, Color32 color)
        +Color32 ParseColor32(JArray colorArray)
        +List~Color32~ ParsePalette(JArray paletteArray)
        +void ApplyPixelData(Texture2D texture, JToken pixelsToken, int width, int height)
        +void ApplyPixelDataToRegion(Texture2D texture, JToken pixelsToken, int offsetX, int offsetY, int regionWidth, int regionHeight)
    }

    class ScreenshotCaptureResult {
        +ScreenshotCaptureResult(string fullPath, string assetsRelativePath, int superSize)
        +ScreenshotCaptureResult(string fullPath, string assetsRelativePath, int superSize, bool isAsync)
        +string FullPath
        +string AssetsRelativePath
        +int SuperSize
        +bool IsAsync
    }

    class ScreenshotUtility {
        <<static>>
        -string ScreenshotsFolderName
        -bool s_loggedLegacyScreenCaptureFallback
        -Camera FindAvailableCamera()
        +ScreenshotCaptureResult CaptureToAssetsFolder(string fileName, int superSize, bool ensureUniqueFileName)
        +ScreenshotCaptureResult CaptureFromCameraToAssetsFolder(Camera camera, string fileName, int superSize, bool ensureUniqueFileName)
        -ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName, bool isAsync)
        -string ToAssetsRelativePath(string normalizedFullPath)
        -string BuildFileName(string fileName)
        -string EnsureUnique(string fullPath)
        -string GetProjectRootPath()
    }

    class ManageScene {
        <<static>>
        -object CaptureScreenshot(string fileName, int? superSize)
        -void EnsureGameView()
        -void ScheduleAssetImportWhenFileExists(string assetsRelativePath, string fullPath, double timeoutSeconds)
        -object GetActiveSceneInfo()
        -object BuildGameObjectSummary(GameObject go, bool includeTransform, int maxChildrenPerNode, int maxComponentsPerNode)
    }

    class ManagePrefabs {
        <<static>>
        +object HandleCommand(JObject @params)
        -object SaveOpenStage(JObject @params)
        -void SaveAndRefreshStage(PrefabStage stage, bool force)
        -bool ValidateStage(PrefabStage stage)
        -object SerializeStage(PrefabStage stage)
    }

    class GameObjectModify {
        <<static>>
        +object Handle(JObject @params, JToken targetToken, string searchContext)
    }

    class MCPForUnityEditorWindow {
        +void CreateGUI()
        -void EnsureToolsLoaded()
        -void ApplySectionLastClasses()
    }

    ManageTexture ..> TextureOps : uses
    ManageTexture ..> AssetDatabase : imports
    ManageTexture ..> TextureImporter : configures
    ManageTexture ..> McpLog : logs

    ScreenshotUtility o--> ScreenshotCaptureResult
    ManageScene ..> ScreenshotUtility : uses
    ManageScene ..> AssetDatabase : imports
    ManageScene ..> EditorApplication : schedules update

    ManagePrefabs ..> PrefabStage : saves
    ManagePrefabs ..> PrefabUtility : SaveAsPrefabAsset
    ManagePrefabs ..> AssetDatabase : SaveAssets

    GameObjectModify ..> PrefabStageUtility : GetCurrentPrefabStage
    GameObjectModify ..> EditorSceneManager : MarkSceneDirty

    MCPForUnityEditorWindow ..> VisualElement : UI Toolkit
    MCPForUnityEditorWindow ..> McpLog : logs
Loading

File-Level Changes

Change Details Files
Make screenshot capture async-aware and more robust across play/edit modes and Unity versions.
  • Always use ScreenshotUtility.CaptureToAssetsFolder and add async handling via ScreenshotCaptureResult.IsAsync.
  • Introduce EnsureGameView to open/repaint the Game View and Scene View before capture in non-batch mode.
  • Add ScheduleAssetImportWhenFileExists to poll for the captured file and import it once it appears instead of using AssetDatabase.Refresh.
  • Refine screenshot success message and include isAsync flag in the response payload.
MCPForUnity/Editor/Tools/ManageScene.cs
MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs
docs/development/README-DEV.md
docs/development/README-DEV-zh.md
Add a full manage_texture toolchain for procedural texture creation/modification and sprite generation, including CLI and tests.
  • Implement ManageTexture editor tool supporting create/modify/delete, patterns, gradients, noise, and sprite/import settings, with validation and warnings.
  • Add TextureOps helper for encoding textures, filling, color/palette parsing, and applying pixel data/regions including base64 support.
  • Implement server-side manage_texture tool adapter with rich normalization (colors, palette, pixels, sprite/import settings) and preflight integration.
  • Add CLI texture command group (create/sprite/modify/delete) with argument normalization and validation, and wire it into CLI registration and README tool list.
  • Add integration tests for manage_texture plus CLI tests for new texture subcommands.
MCPForUnity/Editor/Tools/ManageTexture.cs
MCPForUnity/Editor/Helpers/TextureOps.cs
Server/src/services/tools/manage_texture.py
Server/src/cli/commands/texture.py
Server/src/services/tools/manage_prefabs.py
Server/src/cli/main.py
Server/tests/integration/test_manage_texture.py
Server/tests/test_cli.py
README.md
MCPForUnity/Editor/Helpers/TextureOps.cs.meta
MCPForUnity/Editor/Tools/ManageTexture.cs.meta
Server/uv.lock
Improve prefab-stage saving and dirty tracking to better support automated workflows and prefab-mode GameObject edits.
  • Extend manage_prefabs save_open_stage to accept a force parameter, skipping saves when not dirty unless force=true.
  • Switch prefab stage saving to PrefabUtility.SaveAsPrefabAsset with optional dirty-marking for force saves and stricter validation/errors.
  • Ensure GameObject modifications mark the appropriate scene dirty, including when operating in a prefab stage.
  • Expose the new force flag through the server-side manage_prefabs tool so automated callers can request forced saves.
MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs
Server/src/services/tools/manage_prefabs.py
Minor robustness and editor UX improvements.
  • Use FindObjectsByType on Unity 2023+ when searching components, falling back to FindObjectsOfType for older versions.
  • Harden action string handling in several Manage* tools using ToLowerInvariant and fix minor logging/exception handling in ManageScene and GameObject summary building.
  • Add ApplySectionLastClasses in the main editor window to emulate a CSS :last-child behavior for stacked sections.
  • Update docs to describe manage_scene screenshot behavior and reflect the new manage_texture tool.
MCPForUnity/Editor/Tools/ManageGameObjectCommon.cs
MCPForUnity/Editor/Tools/ManageAsset.cs
MCPForUnity/Editor/Tools/ManageEditor.cs
MCPForUnity/Editor/Tools/ManageMaterial.cs
MCPForUnity/Editor/Tools/ManageShader.cs
MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
MCPForUnity/Editor/Tools/ManageScene.cs
MCPForUnity/Editor/Tools/ManageGameObjects/GameObjectModify.cs
docs/development/README-DEV.md
docs/development/README-DEV-zh.md

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

coderabbitai bot commented Jan 25, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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

Copy link

@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 5 issues, and left some high level feedback:

  • The new ManageTexture.cs is very large and mixes command routing, validation, IO, and multiple rendering algorithms in a single static class; consider extracting pattern/gradient/noise generation and importer configuration into smaller helper types or partial classes to keep the tool easier to navigate and maintain.
  • TextureOps.ParseColor32 assumes incoming channel values are already in the 0–255 range; if there’s any future path that sends normalized 0.0–1.0 colors directly to Unity, it may be worth adding a safety check/conversion here similar to the Python-side normalization to guard against subtle color bugs.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new ManageTexture.cs is very large and mixes command routing, validation, IO, and multiple rendering algorithms in a single static class; consider extracting pattern/gradient/noise generation and importer configuration into smaller helper types or partial classes to keep the tool easier to navigate and maintain.
- TextureOps.ParseColor32 assumes incoming channel values are already in the 0–255 range; if there’s any future path that sends normalized 0.0–1.0 colors directly to Unity, it may be worth adding a safety check/conversion here similar to the Python-side normalization to guard against subtle color bugs.

## Individual Comments

### Comment 1
<location> `MCPForUnity/Editor/Tools/ManageTexture.cs:1012-1014` </location>
<code_context>
+            }
+        }
+
+        private static string GetAbsolutePath(string assetPath)
+        {
+            return Path.Combine(Directory.GetCurrentDirectory(), assetPath);
+        }
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Relying on `Directory.GetCurrentDirectory()` for project paths can break in some editor contexts.

Methods like `GetAbsolutePath`, `EnsureDirectoryExists`, and the create/modify file writes implicitly treat `Directory.GetCurrentDirectory()` as the Unity project root, which may not hold in all editor or pipeline contexts. Prefer deriving paths from `Application.dataPath` (for example, `Path.GetFullPath(Path.Combine(Application.dataPath, "..", assetPath))`) so paths are anchored to the actual Unity project root instead of the process working directory.

Suggested implementation:

```csharp
        private static string GetAbsolutePath(string assetPath)
        {
            // Resolve paths relative to the Unity project root (parent of Assets)
            // This is more robust than relying on Directory.GetCurrentDirectory()
            string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));

            // Normalize assetPath to avoid leading separators breaking Path.Combine
            string normalizedAssetPath = assetPath.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);

            return Path.GetFullPath(Path.Combine(projectRoot, normalizedAssetPath));
        }

```

To compile successfully and use `Application.dataPath`, ensure this file has `using UnityEngine;` at the top. If it's missing, add:
- `using UnityEngine;` alongside the other using directives.
No other call sites of `GetAbsolutePath` need changes; they will now automatically resolve against the Unity project root.
</issue_to_address>

### Comment 2
<location> `Server/src/services/tools/manage_texture.py:557-558` </location>
<code_context>
+    if image_path is not None and action_lower not in ("create", "create_sprite"):
+        return {"success": False, "message": "image_path is only supported for create/create_sprite."}
+
+    if image_path is not None and (fill_color is not None or pattern is not None or pixels is not None):
+        return {"success": False, "message": "image_path cannot be combined with fill_color, pattern, or pixels."}
+
+    # Default to white for create action if nothing else specified
</code_context>

<issue_to_address>
**question (bug_risk):** Server-side defaults for `create` differ from Unity-side defaults, which can make behavior surprising.

For `action == "create"`, this code defaults to a white fill when no `fill_color`, `pattern`, `pixels`, or `image_path` are provided, but `ManageTexture.CreateTexture` defaults to a fully transparent texture in the same case. Unless this difference is intentional, consider aligning these defaults so the same call shape behaves consistently between the MCP tool and Unity.
</issue_to_address>

### Comment 3
<location> `Server/tests/test_cli.py:1230` </location>
<code_context>
+# Texture Command Tests
+# =============================================================================
+
+class TestTextureCommands:
+    """Tests for Texture CLI commands."""
+
</code_context>

<issue_to_address>
**suggestion (testing):** Texture CLI tests only cover happy paths; add failure-mode tests for argument validation (e.g. `image-path` conflicts and bad colors).

Current tests only cover success flows plus one invalid JSON case, but they miss key validation paths in `cli.commands.texture`, such as:

- Rejecting `--image-path` when combined with `--color`/`--pattern`/`--palette` for `create` and `sprite`.
- Failing `create` when dimensions are 0 or negative and exposing the validation message.
- Returning non‑zero and printing a clear error when `--color` or `--palette` are malformed for `sprite`/`create`.

Please add tests for these cases (asserting `exit_code == 1` and checking the error text) so changes to CLI parsing and normalization are safer.

Suggested implementation:

```python
class TestTextureCommands:
    """Tests for Texture CLI commands."""

    def test_texture_create_rejects_image_path_with_color(self, cli_runner, cli):
        """--image-path must not be combined with --color on `texture create`."""
        result = cli_runner.invoke(
            cli,
            [
                "texture",
                "create",
                "--image-path",
                "tests/data/texture.png",
                "--color",
                "#ff0000",
            ],
        )

        assert result.exit_code == 1
        # Keep assertions loose to be resilient to wording changes
        assert "image-path" in result.output
        assert "color" in result.output

    def test_texture_create_rejects_image_path_with_pattern(self, cli_runner, cli):
        """--image-path must not be combined with --pattern on `texture create`."""
        result = cli_runner.invoke(
            cli,
            [
                "texture",
                "create",
                "--image-path",
                "tests/data/texture.png",
                "--pattern",
                "stripes",
            ],
        )

        assert result.exit_code == 1
        assert "image-path" in result.output
        assert "pattern" in result.output

    def test_texture_create_rejects_image_path_with_palette(self, cli_runner, cli):
        """--image-path must not be combined with --palette on `texture create`."""
        result = cli_runner.invoke(
            cli,
            [
                "texture",
                "create",
                "--image-path",
                "tests/data/texture.png",
                "--palette",
                '[ "#ff0000", "#00ff00" ]',
            ],
        )

        assert result.exit_code == 1
        assert "image-path" in result.output
        assert "palette" in result.output

    def test_texture_create_rejects_zero_or_negative_dimensions(self, cli_runner, cli):
        """`texture create` should fail fast on zero/negative dimensions."""

        # Width is zero
        result_zero = cli_runner.invoke(
            cli,
            [
                "texture",
                "create",
                "--width",
                "0",
                "--height",
                "32",
                "--color",
                "#ff0000",
            ],
        )

        assert result_zero.exit_code == 1
        assert "width" in result_zero.output

        # Height is negative
        result_negative = cli_runner.invoke(
            cli,
            [
                "texture",
                "create",
                "--width",
                "32",
                "--height",
                "-1",
                "--color",
                "#ff0000",
            ],
        )

        assert result_negative.exit_code == 1
        assert "height" in result_negative.output

    def test_texture_create_rejects_malformed_color(self, cli_runner, cli):
        """`texture create` should fail on malformed color values."""
        result = cli_runner.invoke(
            cli,
            [
                "texture",
                "create",
                "--width",
                "32",
                "--height",
                "32",
                "--color",
                "not-a-color",
            ],
        )

        assert result.exit_code == 1
        assert "color" in result.output
        assert "invalid" in result.output.lower()

    def test_texture_sprite_rejects_malformed_palette_colors(self, cli_runner, cli):
        """
        `texture sprite` should fail when palette JSON is valid but contains
        invalid color entries.
        """
        result = cli_runner.invoke(
            cli,
            [
                "texture",
                "sprite",
                "--palette",
                '["#ff0000", "not-a-color"]',
            ],
        )

        assert result.exit_code == 1
        assert "palette" in result.output
        assert "color" in result.output
        assert "invalid" in result.output.lower()

```

These edits assume:
1. There is a `cli_runner` fixture (likely wrapping `click.testing.CliRunner`) and a `cli` object used in other CLI tests. If your fixtures/names differ (e.g. `runner` instead of `cli_runner`, or `main` instead of `cli`), update the test signatures and `invoke` calls accordingly.
2. The root command exposes `texture` with subcommands `create` and `sprite`. If the command path is different (e.g. `["textures", "create"]` or no root `texture` group), adjust the argument lists.
3. The error messages include the option names (`image-path`, `color`, `pattern`, `palette`, `width`, `height`) and the word `invalid` for malformed values. If your error text differs, tweak the asserted substrings to match the actual messages while keeping them reasonably loose to avoid brittle tests.
</issue_to_address>

### Comment 4
<location> `Server/tests/integration/test_manage_texture.py:21` </location>
<code_context>
+async def noop_preflight(*args, **kwargs):
+    return None
+
+class TestManageTextureIntegration:
+    """Integration tests for texture management tool logic."""
+
</code_context>

<issue_to_address>
**suggestion (testing):** Integration tests cover create/modify/delete well, but `apply_pattern`, `apply_gradient`, and `apply_noise` paths are untested.

These actions introduce additional parameters (`gradient_type`, `gradient_angle`, `noise_scale`, `octaves`, palette handling, etc.). Please add at least one integration test for each that checks parameter normalization and the structure of the outgoing `params` dict to ensure ongoing compatibility with the Unity integration.

Suggested implementation:

```python
async def noop_preflight(*args, **kwargs):
    return None


import pytest
import asyncio
from .test_helpers import DummyContext
import services.tools.manage_texture as manage_texture_mod

def run_async(coro):

```

```python
def run_async(coro):
    """Simple wrapper to run a coroutine synchronously."""
    loop = asyncio.new_event_loop()
    try:
        asyncio.set_event_loop(loop)


class TestManageTextureApplyVariantsIntegration:
    """Integration tests for pattern/gradient/noise application parameter handling."""

    def _extract_params(self, result):
        """
        Helper to normalize access to the params dict from the manage_texture tool result.

        The tool may return either:
        - an object with a `.params` attribute, or
        - a plain dict with a `"params"` key.

        This helper keeps the tests resilient to either representation.
        """
        if hasattr(result, "params"):
            return result.params
        if isinstance(result, dict) and "params" in result:
            return result["params"]
        pytest.fail(f"manage_texture result does not expose 'params': {result!r}")

    def test_apply_pattern_params_normalization(self, tmp_path):
        """Ensure apply_pattern parameters are normalized and forwarded correctly."""
        ctx = DummyContext(project_root=tmp_path)

        request = {
            "action": "apply_pattern",
            "texture_path": "Assets/Textures/test_pattern.png",
            "pattern_name": "checkerboard",
            "palette": ["#ff0000", "#00ff00"],
            "scale": 2.5,
        }

        result = run_async(
            manage_texture_mod.manage_texture(
                context=ctx,
                request=request,
                preflight=noop_preflight,
            )
        )

        params = self._extract_params(result)
        assert params["action"] == "apply_pattern"
        assert params["texture_path"].endswith("Assets/Textures/test_pattern.png")
        assert params["pattern_name"] == "checkerboard"
        # scale should be a float and preserved/normalized
        assert isinstance(params["scale"], (int, float))
        assert params["scale"] == pytest.approx(2.5)
        # palette should be a list of strings (Unity expects color strings)
        assert isinstance(params["palette"], list)
        assert all(isinstance(color, str) for color in params["palette"])

    def test_apply_gradient_params_normalization(self, tmp_path):
        """Ensure apply_gradient parameters (type, angle, palette) are handled correctly."""
        ctx = DummyContext(project_root=tmp_path)

        request = {
            "action": "apply_gradient",
            "texture_path": "Assets/Textures/test_gradient.png",
            "gradient_type": "linear",
            "gradient_angle": 45,
            "palette": ["#000000", "#ffffff"],
        }

        result = run_async(
            manage_texture_mod.manage_texture(
                context=ctx,
                request=request,
                preflight=noop_preflight,
            )
        )

        params = self._extract_params(result)
        assert params["action"] == "apply_gradient"
        assert params["texture_path"].endswith("Assets/Textures/test_gradient.png")
        assert params["gradient_type"] == "linear"
        # angle should survive any normalization and be numeric
        assert isinstance(params["gradient_angle"], (int, float))
        assert params["gradient_angle"] == pytest.approx(45)
        assert isinstance(params["palette"], list)
        assert all(isinstance(color, str) for color in params["palette"])

    def test_apply_noise_params_normalization(self, tmp_path):
        """Ensure apply_noise parameters (noise_scale, octaves, seed) are handled correctly."""
        ctx = DummyContext(project_root=tmp_path)

        request = {
            "action": "apply_noise",
            "texture_path": "Assets/Textures/test_noise.png",
            "noise_scale": 0.75,
            "octaves": 4,
            "seed": 1234,
        }

        result = run_async(
            manage_texture_mod.manage_texture(
                context=ctx,
                request=request,
                preflight=noop_preflight,
            )
        )

        params = self._extract_params(result)
        assert params["action"] == "apply_noise"
        assert params["texture_path"].endswith("Assets/Textures/test_noise.png")
        # noise_scale should be a float and preserved/normalized
        assert isinstance(params["noise_scale"], (int, float))
        assert params["noise_scale"] == pytest.approx(0.75)
        # octaves should be an integer
        assert isinstance(params["octaves"], int)
        assert params["octaves"] == 4
        # seed should be present and stable
        assert params["seed"] == 1234

```

These tests assume the following API, consistent with typical patterns in this codebase:

1. `manage_texture_mod.manage_texture` is an async function with signature similar to:
   `async def manage_texture(context: DummyContext, request: dict, preflight: Callable = noop_preflight) -> ResultLike:`
2. The returned object exposes the outgoing Unity call parameters either via:
   - a `.params` attribute, or
   - a `"params"` key on a dict.

If your actual API differs, adjust:
- The call site inside each test (`manage_texture_mod.manage_texture(...)`) to match the real parameter names/order.
- The `_extract_params` helper if the result structure uses a different attribute/key for the Unity parameter dict.
- The request dict keys (`"gradient_type"`, `"gradient_angle"`, `"noise_scale"`, `"octaves"`, `"palette"`) to match the exact names expected by `manage_texture`.

Finally, if the existing integration tests use a shared fixture or factory for `DummyContext` or for building requests, you may want to:
- Replace direct `DummyContext(project_root=tmp_path)` with that fixture.
- Build the `request` dicts via the same helper to keep test style consistent.
</issue_to_address>

### Comment 5
<location> `Server/tests/integration/test_manage_texture.py:257` </location>
<code_context>
+        assert resp["success"] is True
+        assert captured["params"]["path"] == "Assets/Textures/Old.png"
+
+    def test_invalid_dimensions(self, monkeypatch):
+        """Test error handling for invalid dimensions."""
+        async def fake_send(func, instance, cmd, params, **kwargs):
</code_context>

<issue_to_address>
**nitpick (testing):** The fake `send_with_unity_instance` logic in `test_invalid_dimensions` is unused; consider simplifying to keep the test focused on local validation.

In `test_invalid_dimensions`, the `fake_send` branch for `w > 4096` is never reached because normalization fails earlier at `width=0`, so `send_with_unity_instance` is never called. This makes the test’s intent unclear. You could either simplify `fake_send` to just record calls (or skip the patch), or add a separate test for a too‑large positive width that verifies the Unity call and expected warnings.
</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 +1012 to +1014
private static string GetAbsolutePath(string assetPath)
{
return Path.Combine(Directory.GetCurrentDirectory(), assetPath);
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): Relying on Directory.GetCurrentDirectory() for project paths can break in some editor contexts.

Methods like GetAbsolutePath, EnsureDirectoryExists, and the create/modify file writes implicitly treat Directory.GetCurrentDirectory() as the Unity project root, which may not hold in all editor or pipeline contexts. Prefer deriving paths from Application.dataPath (for example, Path.GetFullPath(Path.Combine(Application.dataPath, "..", assetPath))) so paths are anchored to the actual Unity project root instead of the process working directory.

Suggested implementation:

        private static string GetAbsolutePath(string assetPath)
        {
            // Resolve paths relative to the Unity project root (parent of Assets)
            // This is more robust than relying on Directory.GetCurrentDirectory()
            string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));

            // Normalize assetPath to avoid leading separators breaking Path.Combine
            string normalizedAssetPath = assetPath.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);

            return Path.GetFullPath(Path.Combine(projectRoot, normalizedAssetPath));
        }

To compile successfully and use Application.dataPath, ensure this file has using UnityEngine; at the top. If it's missing, add:

  • using UnityEngine; alongside the other using directives.
    No other call sites of GetAbsolutePath need changes; they will now automatically resolve against the Unity project root.

Comment on lines +557 to +558
if image_path is not None and (fill_color is not None or pattern is not None or pixels is not None):
return {"success": False, "message": "image_path cannot be combined with fill_color, pattern, or pixels."}
Copy link

Choose a reason for hiding this comment

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

question (bug_risk): Server-side defaults for create differ from Unity-side defaults, which can make behavior surprising.

For action == "create", this code defaults to a white fill when no fill_color, pattern, pixels, or image_path are provided, but ManageTexture.CreateTexture defaults to a fully transparent texture in the same case. Unless this difference is intentional, consider aligning these defaults so the same call shape behaves consistently between the MCP tool and Unity.

# Texture Command Tests
# =============================================================================

class TestTextureCommands:
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Texture CLI tests only cover happy paths; add failure-mode tests for argument validation (e.g. image-path conflicts and bad colors).

Current tests only cover success flows plus one invalid JSON case, but they miss key validation paths in cli.commands.texture, such as:

  • Rejecting --image-path when combined with --color/--pattern/--palette for create and sprite.
  • Failing create when dimensions are 0 or negative and exposing the validation message.
  • Returning non‑zero and printing a clear error when --color or --palette are malformed for sprite/create.

Please add tests for these cases (asserting exit_code == 1 and checking the error text) so changes to CLI parsing and normalization are safer.

Suggested implementation:

class TestTextureCommands:
    """Tests for Texture CLI commands."""

    def test_texture_create_rejects_image_path_with_color(self, cli_runner, cli):
        """--image-path must not be combined with --color on `texture create`."""
        result = cli_runner.invoke(
            cli,
            [
                "texture",
                "create",
                "--image-path",
                "tests/data/texture.png",
                "--color",
                "#ff0000",
            ],
        )

        assert result.exit_code == 1
        # Keep assertions loose to be resilient to wording changes
        assert "image-path" in result.output
        assert "color" in result.output

    def test_texture_create_rejects_image_path_with_pattern(self, cli_runner, cli):
        """--image-path must not be combined with --pattern on `texture create`."""
        result = cli_runner.invoke(
            cli,
            [
                "texture",
                "create",
                "--image-path",
                "tests/data/texture.png",
                "--pattern",
                "stripes",
            ],
        )

        assert result.exit_code == 1
        assert "image-path" in result.output
        assert "pattern" in result.output

    def test_texture_create_rejects_image_path_with_palette(self, cli_runner, cli):
        """--image-path must not be combined with --palette on `texture create`."""
        result = cli_runner.invoke(
            cli,
            [
                "texture",
                "create",
                "--image-path",
                "tests/data/texture.png",
                "--palette",
                '[ "#ff0000", "#00ff00" ]',
            ],
        )

        assert result.exit_code == 1
        assert "image-path" in result.output
        assert "palette" in result.output

    def test_texture_create_rejects_zero_or_negative_dimensions(self, cli_runner, cli):
        """`texture create` should fail fast on zero/negative dimensions."""

        # Width is zero
        result_zero = cli_runner.invoke(
            cli,
            [
                "texture",
                "create",
                "--width",
                "0",
                "--height",
                "32",
                "--color",
                "#ff0000",
            ],
        )

        assert result_zero.exit_code == 1
        assert "width" in result_zero.output

        # Height is negative
        result_negative = cli_runner.invoke(
            cli,
            [
                "texture",
                "create",
                "--width",
                "32",
                "--height",
                "-1",
                "--color",
                "#ff0000",
            ],
        )

        assert result_negative.exit_code == 1
        assert "height" in result_negative.output

    def test_texture_create_rejects_malformed_color(self, cli_runner, cli):
        """`texture create` should fail on malformed color values."""
        result = cli_runner.invoke(
            cli,
            [
                "texture",
                "create",
                "--width",
                "32",
                "--height",
                "32",
                "--color",
                "not-a-color",
            ],
        )

        assert result.exit_code == 1
        assert "color" in result.output
        assert "invalid" in result.output.lower()

    def test_texture_sprite_rejects_malformed_palette_colors(self, cli_runner, cli):
        """
        `texture sprite` should fail when palette JSON is valid but contains
        invalid color entries.
        """
        result = cli_runner.invoke(
            cli,
            [
                "texture",
                "sprite",
                "--palette",
                '["#ff0000", "not-a-color"]',
            ],
        )

        assert result.exit_code == 1
        assert "palette" in result.output
        assert "color" in result.output
        assert "invalid" in result.output.lower()

These edits assume:

  1. There is a cli_runner fixture (likely wrapping click.testing.CliRunner) and a cli object used in other CLI tests. If your fixtures/names differ (e.g. runner instead of cli_runner, or main instead of cli), update the test signatures and invoke calls accordingly.
  2. The root command exposes texture with subcommands create and sprite. If the command path is different (e.g. ["textures", "create"] or no root texture group), adjust the argument lists.
  3. The error messages include the option names (image-path, color, pattern, palette, width, height) and the word invalid for malformed values. If your error text differs, tweak the asserted substrings to match the actual messages while keeping them reasonably loose to avoid brittle tests.

async def noop_preflight(*args, **kwargs):
return None

class TestManageTextureIntegration:
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Integration tests cover create/modify/delete well, but apply_pattern, apply_gradient, and apply_noise paths are untested.

These actions introduce additional parameters (gradient_type, gradient_angle, noise_scale, octaves, palette handling, etc.). Please add at least one integration test for each that checks parameter normalization and the structure of the outgoing params dict to ensure ongoing compatibility with the Unity integration.

Suggested implementation:

async def noop_preflight(*args, **kwargs):
    return None


import pytest
import asyncio
from .test_helpers import DummyContext
import services.tools.manage_texture as manage_texture_mod

def run_async(coro):
def run_async(coro):
    """Simple wrapper to run a coroutine synchronously."""
    loop = asyncio.new_event_loop()
    try:
        asyncio.set_event_loop(loop)


class TestManageTextureApplyVariantsIntegration:
    """Integration tests for pattern/gradient/noise application parameter handling."""

    def _extract_params(self, result):
        """
        Helper to normalize access to the params dict from the manage_texture tool result.

        The tool may return either:
        - an object with a `.params` attribute, or
        - a plain dict with a `"params"` key.

        This helper keeps the tests resilient to either representation.
        """
        if hasattr(result, "params"):
            return result.params
        if isinstance(result, dict) and "params" in result:
            return result["params"]
        pytest.fail(f"manage_texture result does not expose 'params': {result!r}")

    def test_apply_pattern_params_normalization(self, tmp_path):
        """Ensure apply_pattern parameters are normalized and forwarded correctly."""
        ctx = DummyContext(project_root=tmp_path)

        request = {
            "action": "apply_pattern",
            "texture_path": "Assets/Textures/test_pattern.png",
            "pattern_name": "checkerboard",
            "palette": ["#ff0000", "#00ff00"],
            "scale": 2.5,
        }

        result = run_async(
            manage_texture_mod.manage_texture(
                context=ctx,
                request=request,
                preflight=noop_preflight,
            )
        )

        params = self._extract_params(result)
        assert params["action"] == "apply_pattern"
        assert params["texture_path"].endswith("Assets/Textures/test_pattern.png")
        assert params["pattern_name"] == "checkerboard"
        # scale should be a float and preserved/normalized
        assert isinstance(params["scale"], (int, float))
        assert params["scale"] == pytest.approx(2.5)
        # palette should be a list of strings (Unity expects color strings)
        assert isinstance(params["palette"], list)
        assert all(isinstance(color, str) for color in params["palette"])

    def test_apply_gradient_params_normalization(self, tmp_path):
        """Ensure apply_gradient parameters (type, angle, palette) are handled correctly."""
        ctx = DummyContext(project_root=tmp_path)

        request = {
            "action": "apply_gradient",
            "texture_path": "Assets/Textures/test_gradient.png",
            "gradient_type": "linear",
            "gradient_angle": 45,
            "palette": ["#000000", "#ffffff"],
        }

        result = run_async(
            manage_texture_mod.manage_texture(
                context=ctx,
                request=request,
                preflight=noop_preflight,
            )
        )

        params = self._extract_params(result)
        assert params["action"] == "apply_gradient"
        assert params["texture_path"].endswith("Assets/Textures/test_gradient.png")
        assert params["gradient_type"] == "linear"
        # angle should survive any normalization and be numeric
        assert isinstance(params["gradient_angle"], (int, float))
        assert params["gradient_angle"] == pytest.approx(45)
        assert isinstance(params["palette"], list)
        assert all(isinstance(color, str) for color in params["palette"])

    def test_apply_noise_params_normalization(self, tmp_path):
        """Ensure apply_noise parameters (noise_scale, octaves, seed) are handled correctly."""
        ctx = DummyContext(project_root=tmp_path)

        request = {
            "action": "apply_noise",
            "texture_path": "Assets/Textures/test_noise.png",
            "noise_scale": 0.75,
            "octaves": 4,
            "seed": 1234,
        }

        result = run_async(
            manage_texture_mod.manage_texture(
                context=ctx,
                request=request,
                preflight=noop_preflight,
            )
        )

        params = self._extract_params(result)
        assert params["action"] == "apply_noise"
        assert params["texture_path"].endswith("Assets/Textures/test_noise.png")
        # noise_scale should be a float and preserved/normalized
        assert isinstance(params["noise_scale"], (int, float))
        assert params["noise_scale"] == pytest.approx(0.75)
        # octaves should be an integer
        assert isinstance(params["octaves"], int)
        assert params["octaves"] == 4
        # seed should be present and stable
        assert params["seed"] == 1234

These tests assume the following API, consistent with typical patterns in this codebase:

  1. manage_texture_mod.manage_texture is an async function with signature similar to:
    async def manage_texture(context: DummyContext, request: dict, preflight: Callable = noop_preflight) -> ResultLike:
  2. The returned object exposes the outgoing Unity call parameters either via:
    • a .params attribute, or
    • a "params" key on a dict.

If your actual API differs, adjust:

  • The call site inside each test (manage_texture_mod.manage_texture(...)) to match the real parameter names/order.
  • The _extract_params helper if the result structure uses a different attribute/key for the Unity parameter dict.
  • The request dict keys ("gradient_type", "gradient_angle", "noise_scale", "octaves", "palette") to match the exact names expected by manage_texture.

Finally, if the existing integration tests use a shared fixture or factory for DummyContext or for building requests, you may want to:

  • Replace direct DummyContext(project_root=tmp_path) with that fixture.
  • Build the request dicts via the same helper to keep test style consistent.

assert resp["success"] is True
assert captured["params"]["path"] == "Assets/Textures/Old.png"

def test_invalid_dimensions(self, monkeypatch):
Copy link

Choose a reason for hiding this comment

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

nitpick (testing): The fake send_with_unity_instance logic in test_invalid_dimensions is unused; consider simplifying to keep the test focused on local validation.

In test_invalid_dimensions, the fake_send branch for w > 4096 is never reached because normalization fails earlier at width=0, so send_with_unity_instance is never called. This makes the test’s intent unclear. You could either simplify fake_send to just record calls (or skip the patch), or add a separate test for a too‑large positive width that verifies the Unity call and expected warnings.

@whatevertogo whatevertogo merged commit 56a8d23 into whatevertogo:enhancement/Prefab-management Jan 25, 2026
2 checks passed
@dsarno dsarno deleted the fix/prefab-save-dialogs-and-force-param 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.

4 participants