diff --git a/.agents/skills/uloop-clear-console/SKILL.md b/.agents/skills/uloop-clear-console/SKILL.md index 3cb9e2be3f..8f34706efa 100644 --- a/.agents/skills/uloop-clear-console/SKILL.md +++ b/.agents/skills/uloop-clear-console/SKILL.md @@ -39,10 +39,11 @@ uloop clear-console --add-confirmation-message ## Output Returns JSON with: -- `success` (boolean): Whether the clear operation succeeded -- `clearedLogCount` (number): Total number of log entries that were cleared -- `clearedCounts` (object): Breakdown by log type - - `errorCount` (number): Errors cleared - - `warningCount` (number): Warnings cleared - - `logCount` (number): Info logs cleared -- `message` (string): Description of the result. On failure, this field carries the error summary. + +- `Success` (boolean): Whether the clear operation succeeded +- `ClearedLogCount` (number): Total number of log entries that were cleared +- `ClearedCounts` (object): Breakdown by log type + - `ErrorCount` (number): Errors cleared + - `WarningCount` (number): Warnings cleared + - `LogCount` (number): Info logs cleared +- `Message` (string): Description of the result. On failure, this field carries the error summary. diff --git a/.agents/skills/uloop-compile/SKILL.md b/.agents/skills/uloop-compile/SKILL.md index 9a2780a2a1..3eeb6bfe4e 100644 --- a/.agents/skills/uloop-compile/SKILL.md +++ b/.agents/skills/uloop-compile/SKILL.md @@ -44,7 +44,8 @@ uloop compile --stop-on-external-scene-changes ## Output Returns JSON: -- `success`: boolean or null -- `errorCount`: number or null -- `warningCount`: number or null -- `message`: string + +- `Success`: boolean or null +- `ErrorCount`: number or null +- `WarningCount`: number or null +- `Message`: string diff --git a/.agents/skills/uloop-control-play-mode/SKILL.md b/.agents/skills/uloop-control-play-mode/SKILL.md index bfdb9e9bc9..fc85b7df71 100644 --- a/.agents/skills/uloop-control-play-mode/SKILL.md +++ b/.agents/skills/uloop-control-play-mode/SKILL.md @@ -49,14 +49,15 @@ uloop control-play-mode --action Step ## Output Returns JSON with the current play mode state: -- `isPlaying`: Whether Unity is currently in play mode -- `isPaused`: Whether play mode is paused -- `changed`: Whether the requested action changed the current play mode state -- `wasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped -- `message`: Description of the action performed + +- `IsPlaying`: Whether Unity is currently in play mode +- `IsPaused`: Whether play mode is paused +- `Changed`: Whether the requested action changed the current play mode state +- `WasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped +- `Message`: Description of the action performed ## Notes -- Stop on an already-stopped Editor sets `changed: false`, `wasAlreadyStopped: true` +- Stop on an already-stopped Editor sets `Changed: false`, `WasAlreadyStopped: true` - Step is independent of `Time.timeScale`; requires PlayMode to be running - The command waits for the requested state before returning. Increase `--timeout-seconds` for projects with slow PlayMode entry. diff --git a/.agents/skills/uloop-execute-dynamic-code/SKILL.md b/.agents/skills/uloop-execute-dynamic-code/SKILL.md index 1b2c08ebf9..7fc716edb2 100644 --- a/.agents/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.agents/skills/uloop-execute-dynamic-code/SKILL.md @@ -43,16 +43,17 @@ Prefer terminal commands for file operations and keep snippets focused on Unity ## Output Returns JSON: -- `success`: boolean — overall execution success -- `result`: string — value of the snippet's `return` statement (empty when omitted) -- `logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` -- `compilationErrors`: object[] — Roslyn diagnostics with `message`, `line`, `column`, `errorCode`, optional `hint` and `suggestions` -- `error` / `errorMessage`: string — top-level failure summary (empty on success) -- `updatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) -- `diagnosticsSummary`: string|null — compact summary when diagnostics are available -- `diagnostics`: object[] — structured diagnostics; same shape as `compilationErrors`, usually populated together with it - -On `success: false`, inspect `compilationErrors` first. If empty, read `errorMessage` (and `logs` for extra context) — the failure may be a runtime exception, cancellation, or an "execution in progress" rejection, all of which return empty `compilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. + +- `Success`: boolean — overall execution success +- `Result`: string — value of the snippet's `return` statement (empty when omitted) +- `Logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` +- `CompilationErrors`: object[] — Roslyn diagnostics with `Message`, `Line`, `Column`, `ErrorCode`, optional `Hint` and `Suggestions` +- `Error` / `ErrorMessage`: string — top-level failure summary (empty on success) +- `UpdatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) +- `DiagnosticsSummary`: string|null — compact summary when diagnostics are available +- `Diagnostics`: object[] — structured diagnostics; same shape as `CompilationErrors`, usually populated together with it + +On `Success: false`, inspect `CompilationErrors` first. If empty, read `ErrorMessage` (and `Logs` for extra context) — the failure may be a runtime exception, cancellation, or an "execution in progress" rejection, all of which return empty `CompilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. ## Code Examples by Category diff --git a/.agents/skills/uloop-find-game-objects/SKILL.md b/.agents/skills/uloop-find-game-objects/SKILL.md index 24edd909a3..cd92c9963e 100644 --- a/.agents/skills/uloop-find-game-objects/SKILL.md +++ b/.agents/skills/uloop-find-game-objects/SKILL.md @@ -70,21 +70,23 @@ uloop find-game-objects --search-mode Selected --include-inactive ## Output Returns JSON with: -- `results` (array): Matching GameObjects, each containing: - - `name` (string): GameObject name - - `path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) - - `isActive` (boolean): Active state in hierarchy - - `tag` (string): GameObject tag - - `layer` (number): Layer index - - `components` (array): Each entry has `type` (short name, e.g., `Rigidbody`), `fullTypeName` (e.g., `UnityEngine.Rigidbody`), and `properties` (array of Inspector-visible `{name, type, value}` pairs) -- `totalFound` (number): Results returned (after `--max-results` clipping). For multi-selection file export, this is the number exported. -- `errorMessage` (string): Top-level failure summary (empty on success) -- `processingErrors` (array): Selected-mode per-GameObject serialization failures, each `{gameObjectName, gameObjectPath, error}`. Omitted/null or empty on clean runs. + +- `Results` (array): Matching GameObjects, each containing: + - `Name` (string): GameObject name + - `Path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) + - `IsActive` (boolean): Active state in hierarchy + - `Tag` (string): GameObject tag + - `Layer` (number): Layer index + - `Components` (array): Each entry has `Type` (short name, e.g., `Rigidbody`), `FullTypeName` (e.g., `UnityEngine.Rigidbody`), and `Properties` (array of Inspector-visible `{Name, Type, Value}` pairs) +- `TotalFound` (number): Results returned (after `--max-results` clipping). For multi-selection file export, this is the number exported. +- `ErrorMessage` (string): Top-level failure summary (empty on success) +- `ProcessingErrors` (array): Selected-mode per-GameObject serialization failures, each `{GameObjectName, GameObjectPath, Error}`. Omitted/null or empty on clean runs. ### Multi-selection file export -For `Selected` mode with **multiple** successfully serialized GameObjects, inline `results` is not populated and the data is written to a file instead. Two extra fields appear: -- `resultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` -- `message` (string): Human-readable summary (e.g., "5 GameObjects exported") +For `Selected` mode with **multiple** successfully serialized GameObjects, inline `Results` is not populated and the data is written to a file instead. Two extra fields appear: + +- `ResultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` +- `Message` (string): Human-readable summary (e.g., "5 GameObjects exported") -Single-selection and search-mode calls (`Exact`, `Path`, `Regex`, `Contains`) always return inline. No selection (`Selected` mode with empty selection) returns empty `results` plus a `message`. +Single-selection and search-mode calls (`Exact`, `Path`, `Regex`, `Contains`) always return inline. No selection (`Selected` mode with empty selection) returns empty `Results` plus a `Message`. diff --git a/.agents/skills/uloop-focus-window/SKILL.md b/.agents/skills/uloop-focus-window/SKILL.md index e2ca7a23bb..ead80195ae 100644 --- a/.agents/skills/uloop-focus-window/SKILL.md +++ b/.agents/skills/uloop-focus-window/SKILL.md @@ -29,8 +29,9 @@ uloop focus-window ## Output Returns JSON with: -- `success`: Whether the focus operation succeeded -- `message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) + +- `Success`: Whether the focus operation succeeded +- `Message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) ## Notes diff --git a/.agents/skills/uloop-get-hierarchy/SKILL.md b/.agents/skills/uloop-get-hierarchy/SKILL.md index 4a1ec7cc08..1af6712840 100644 --- a/.agents/skills/uloop-get-hierarchy/SKILL.md +++ b/.agents/skills/uloop-get-hierarchy/SKILL.md @@ -56,7 +56,8 @@ uloop get-hierarchy --use-selection ## Output Returns JSON with: -- `message` (string): Human-readable guidance pointing at the saved file -- `hierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data -The hierarchy itself is **not** in the response — it is written to the file at `hierarchyFilePath`. Open that file to read the `Context` and `Hierarchy` payload (GameObject tree, components, etc.). +- `Message` (string): Human-readable guidance pointing at the saved file +- `HierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data + +The hierarchy itself is **not** in the response — it is written to the file at `HierarchyFilePath`. Open that file to read the `Context` and `Hierarchy` payload (GameObject tree, components, etc.). diff --git a/.agents/skills/uloop-get-logs/SKILL.md b/.agents/skills/uloop-get-logs/SKILL.md index 57d1d97f62..dab6fc1d2f 100644 --- a/.agents/skills/uloop-get-logs/SKILL.md +++ b/.agents/skills/uloop-get-logs/SKILL.md @@ -50,10 +50,11 @@ uloop get-logs --search-text "Missing.*Component" --use-regex ## Output Returns JSON with: -- `totalCount` (number): Total logs available before max-count clipping -- `displayedCount` (number): Logs returned in this response (≤ `--max-count`) -- Input filters (`logType`, `maxCount`, `searchText`, `includeStackTrace`) are echoed back in the response -- `logs` (array): Each entry has: - - `type` (string): `"Error"`, `"Warning"`, or `"Log"` - - `message` (string): Log message body - - `stackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. + +- `TotalCount` (number): Total logs available before max-count clipping +- `DisplayedCount` (number): Logs returned in this response (≤ `--max-count`) +- Input filters (`LogType`, `MaxCount`, `SearchText`, `IncludeStackTrace`) are echoed back in the response +- `Logs` (array): Each entry has: + - `Type` (string): `"Error"`, `"Warning"`, or `"Log"` + - `Message` (string): Log message body + - `StackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. diff --git a/.agents/skills/uloop-hello-world/SKILL.md b/.agents/skills/uloop-hello-world/SKILL.md index 2724067ddd..2c0153fb28 100644 --- a/.agents/skills/uloop-hello-world/SKILL.md +++ b/.agents/skills/uloop-hello-world/SKILL.md @@ -40,13 +40,15 @@ uloop hello-world --name "Carlos" --language spanish --no-include-timestamp ## Output Returns JSON with: -- `message`: The greeting message -- `language`: Language used for greeting -- `timestamp`: Current timestamp (if enabled) + +- `Message`: The greeting message +- `Language`: Language used for greeting +- `Timestamp`: Current timestamp (if enabled) ## Notes This is a sample custom tool demonstrating: + - Type-safe parameter handling with Schema - Enum parameters for language selection - Value-less flag parameters diff --git a/.agents/skills/uloop-launch/SKILL.md b/.agents/skills/uloop-launch/SKILL.md index 7bbee3d3e5..82aadc69d6 100644 --- a/.agents/skills/uloop-launch/SKILL.md +++ b/.agents/skills/uloop-launch/SKILL.md @@ -50,18 +50,19 @@ uloop launch --quit May print status/progress lines before the final JSON payload, such as project path, detected Unity version, or readiness wait messages. The final JSON payload includes: -- `success`: whether the command completed -- `ready`: whether Unity CLI Loop is ready for commands -- `serverReady`: whether the Unity CLI Loop server accepted requests -- `projectIpcReady`: whether the project IPC path accepted tool requests -- `alreadyRunning`: whether an existing Unity process was reused -- `launched`: whether this command launched a Unity process -- `restarted`: whether this command stopped an existing process and launched a new one -- `quit`: whether this command stopped Unity without launching a new process -- `previousProcessId`: process ID stopped by restart or quit, when available -- `currentProcessId`: current Unity process ID, when available -- `projectRoot`: resolved project root -- `message`: readiness summary + +- `Success`: whether the command completed +- `Ready`: whether Unity CLI Loop is ready for commands +- `ServerReady`: whether the Unity CLI Loop server accepted requests +- `ProjectIpcReady`: whether the project IPC path accepted tool requests +- `AlreadyRunning`: whether an existing Unity process was reused +- `Launched`: whether this command launched a Unity process +- `Restarted`: whether this command stopped an existing process and launched a new one +- `Quit`: whether this command stopped Unity without launching a new process +- `PreviousProcessId`: process ID stopped by restart or quit, when available +- `CurrentProcessId`: current Unity process ID, when available +- `ProjectRoot`: resolved project root +- `Message`: readiness summary ## Notes diff --git a/.agents/skills/uloop-record-input/SKILL.md b/.agents/skills/uloop-record-input/SKILL.md index a76d38cb33..43f966dc3d 100644 --- a/.agents/skills/uloop-record-input/SKILL.md +++ b/.agents/skills/uloop-record-input/SKILL.md @@ -44,11 +44,12 @@ Replay injects input frame-by-frame, so the game must also be deterministic to p ## Output The CLI prints JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Start` or `Stop`) -- `outputPath`: Path to saved recording (nullable; populated on `Stop` only) -- `totalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) -- `durationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) - -Frame data lives only in the JSON file at `outputPath`; internal metadata fields are not included in the response. + +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Start` or `Stop`) +- `OutputPath`: Path to saved recording (nullable; populated on `Stop` only) +- `TotalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) +- `DurationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) + +Frame data lives only in the JSON file at `OutputPath`; internal metadata fields are not included in the response. diff --git a/.agents/skills/uloop-replay-input/SKILL.md b/.agents/skills/uloop-replay-input/SKILL.md index 81793dbc8e..051b96759c 100644 --- a/.agents/skills/uloop-replay-input/SKILL.md +++ b/.agents/skills/uloop-replay-input/SKILL.md @@ -48,11 +48,12 @@ Replay injects the exact same input frame-by-frame, but the game must also be de ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) -- `inputPath`: Path to recording file (nullable string; populated on `Start` only) -- `currentFrame`: Current replay frame index (nullable int) -- `totalFrames`: Total frames in the recording (nullable int) -- `progress`: Replay progress (nullable float in 0.0 – 1.0) -- `isReplaying`: Whether replay is currently active (nullable bool) + +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) +- `InputPath`: Path to recording file (nullable string; populated on `Start` only) +- `CurrentFrame`: Current replay frame index (nullable int) +- `TotalFrames`: Total frames in the recording (nullable int) +- `Progress`: Replay progress (nullable float in 0.0 – 1.0) +- `IsReplaying`: Whether replay is currently active (nullable bool) diff --git a/.agents/skills/uloop-run-tests/SKILL.md b/.agents/skills/uloop-run-tests/SKILL.md index 7660586e11..c77e73e7b2 100644 --- a/.agents/skills/uloop-run-tests/SKILL.md +++ b/.agents/skills/uloop-run-tests/SKILL.md @@ -6,13 +6,13 @@ description: "Run Unity Test Runner and report detailed results. Use for EditMod # uloop run-tests -Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `xmlPath` for detailed failure diagnosis. +Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `XmlPath` for detailed failure diagnosis. Before running tests, run `uloop compile` first if you created, deleted, renamed, moved, or edited C# files, test files, `.asmdef`/`.asmref`, or package manifests since the last successful compile. This refreshes the AssetDatabase and surfaces compile errors before test execution. -Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `success: false`, keeps `testCount` at `0`, lists the unsaved items in `message`, and does not start the Unity Test Runner. +Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `Success: false`, keeps `TestCount` at `0`, lists the unsaved items in `Message`, and does not start the Unity Test Runner. -`NoTestsFound` means zero tests matched — not a test failure. Check `noTestsFoundExplanation` and `message` for asmdef hints. +`NoTestsFound` means zero tests matched — not a test failure. Check `NoTestsFoundExplanation` and `Message` for asmdef hints. ## Usage @@ -57,22 +57,24 @@ uloop run-tests --filter-type regex --filter-value ".*Integration.*" ## Output Returns JSON with: -- `success` (boolean): Whether all tests passed -- `status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` -- `hasFailures` (boolean): Whether any discovered test failed -- `message` (string): Summary message -- `noTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests -- `noTestsFoundExplanation` (string): Agent-facing explanation when `noTestsFound` is true; empty otherwise -- `completedAt` (string): ISO timestamp when the run finished -- `testCount` (number): Total tests executed -- `passedCount` (number): Passed tests -- `failedCount` (number): Failed tests -- `skippedCount` (number): Skipped tests -- `xmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `success: true`); populated only when tests failed and the XML file exists on disk. + +- `Success` (boolean): Whether all tests passed +- `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` +- `HasFailures` (boolean): Whether any discovered test failed +- `Message` (string): Summary message +- `NoTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests +- `NoTestsFoundExplanation` (string): Agent-facing explanation when `NoTestsFound` is true; empty otherwise +- `CompletedAt` (string): ISO timestamp when the run finished +- `TestCount` (number): Total tests executed +- `PassedCount` (number): Passed tests +- `FailedCount` (number): Failed tests +- `SkippedCount` (number): Skipped tests +- `XmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `Success: true`); populated only when tests failed and the XML file exists on disk. ### XML Result File When tests fail, NUnit XML results are automatically saved to `{project_root}/.uloop/outputs/TestResults/.xml`. The XML contains per-test-case results including: + - Test name and full name - Pass/fail/skip status and duration - For failed tests: `` (assertion error) and `` diff --git a/.agents/skills/uloop-screenshot/SKILL.md b/.agents/skills/uloop-screenshot/SKILL.md index bc721a5ee7..39e81c686e 100644 --- a/.agents/skills/uloop-screenshot/SKILL.md +++ b/.agents/skills/uloop-screenshot/SKILL.md @@ -72,18 +72,19 @@ uloop screenshot --output-directory /tmp/screenshots ## Output Returns JSON with: -- `screenshotCount`: Number of windows captured -- `screenshots`: Array of screenshot info, each containing: - - `imagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. - - `fileSizeBytes`: Size of the saved file in bytes - - `width`: Captured image width in pixels - - `height`: Captured image height in pixels - - `coordinateSystem`: `"gameView"` or `"window"` - - `resolutionScale`: Resolution scale used for capture - - `yOffset`: Y offset used for gameView coordinate conversion - - `annotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` is used. - -For `annotatedElements` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. + +- `ScreenshotCount`: Number of windows captured +- `Screenshots`: Array of screenshot info, each containing: + - `ImagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. + - `FileSizeBytes`: Size of the saved file in bytes + - `Width`: Captured image width in pixels + - `Height`: Captured image height in pixels + - `CoordinateSystem`: `"gameView"` or `"window"` + - `ResolutionScale`: Resolution scale used for capture + - `YOffset`: Y offset used for gameView coordinate conversion + - `AnnotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` is used. + +For `AnnotatedElements` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. When multiple windows match (e.g., multiple Inspector windows or when using `contains` mode), all matching windows are captured with numbered filenames (e.g., `Inspector_1_*.png`, `Inspector_2_*.png`). diff --git a/.agents/skills/uloop-screenshot/references/annotated-elements.md b/.agents/skills/uloop-screenshot/references/annotated-elements.md index 50b899b096..283109ccc2 100644 --- a/.agents/skills/uloop-screenshot/references/annotated-elements.md +++ b/.agents/skills/uloop-screenshot/references/annotated-elements.md @@ -2,34 +2,34 @@ Read this when using `uloop screenshot --capture-mode rendering --annotate-elements` to find coordinates for `simulate-mouse-ui` or `simulate-mouse-input`. -## annotatedElements Fields +## AnnotatedElements Fields -`annotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: +`AnnotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: -- `label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. -- `name`: Element name -- `path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. -- `type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) -- `interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. -- `simX`, `simY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. -- `boundsMinX`, `boundsMinY`, `boundsMaxX`, `boundsMaxY`: Bounding box in simulate-mouse coordinates -- `sortingOrder`: Canvas sorting order. Higher values are in front. -- `siblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. +- `Label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. +- `Name`: Element name +- `Path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. +- `Type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) +- `Interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. +- `SimX`, `SimY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. +- `BoundsMinX`, `BoundsMinY`, `BoundsMaxX`, `BoundsMaxY`: Bounding box in simulate-mouse coordinates +- `SortingOrder`: Canvas sorting order. Higher values are in front. +- `SiblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. ## Coordinate Conversion -When `coordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: +When `CoordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: ```text -sim_x = image_x / resolutionScale -sim_y = image_y / resolutionScale + yOffset +sim_x = image_x / ResolutionScale +sim_y = image_y / ResolutionScale + YOffset ``` -When `resolutionScale` is `1.0`, this simplifies to: +When `ResolutionScale` is `1.0`, this simplifies to: ```text sim_x = image_x -sim_y = image_y + yOffset +sim_y = image_y + YOffset ``` ## Annotation Readability diff --git a/.agents/skills/uloop-simulate-keyboard/SKILL.md b/.agents/skills/uloop-simulate-keyboard/SKILL.md index 7b65d6b135..e898d1090a 100644 --- a/.agents/skills/uloop-simulate-keyboard/SKILL.md +++ b/.agents/skills/uloop-simulate-keyboard/SKILL.md @@ -42,11 +42,12 @@ uloop simulate-keyboard --action --key [options] Use `Press` for edge-triggered keyboard code such as `Keyboard.current.spaceKey.wasPressedThisFrame`. `KeyDown` emits one initial press edge, then only keeps the key held. It does not keep `wasPressedThisFrame` true while the key remains held. If a successful `Press` or `KeyDown` leaves `Keyboard.current..isPressed` true but runtime state does not change, do not immediately rewrite the user's runtime code to `isPressed`. First verify that the target component is active during the command, that it polls input in the configured Input System update phase, and that a missed `KeyDown` edge is followed by `KeyUp` before retrying. + ### Pause Point Inspection (Standard for E2E) For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the key, not immediately after `simulate-keyboard`. -- If `interruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `pausePointId` and `pausePointHitCount` identify the marker. `pressEdgeObserved` is still reported on pause-point interruptions. +- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. `PressEdgeObserved` is still reported on pause-point interruptions. ### KeyDown/KeyUp Rules @@ -85,15 +86,16 @@ uloop simulate-keyboard --action KeyUp --key LeftShift ## Output Returns JSON with: -- `success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) -- `message` (string): Description of what happened or why it failed -- `action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) -- `keyName` (string, nullable): The key that was acted on; may be `null` when the action could not resolve a key -- `interruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `pausePointId` (string, nullable): The marker id when a `UloopPausePoint.Pause` marker caused the interruption -- `pausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker caused the interruption -- `pausePointHits` (array, nullable): Every marker hit during this input as `{id, hitCount}` entries, in hit order. Read this when one input may trigger several markers; `pausePointId` only names the latest one -- `pressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was actually visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge (e.g. the press was consumed by an editor-only input update) — retry the input or verify with a focused log instead of trusting `success` alone. `null` only for `KeyUp` and for timed-out responses; pause-point interruptions still report the observed value + +- `Success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) +- `Message` (string): Description of what happened or why it failed +- `Action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) +- `KeyName` (string, nullable): The key that was acted on; may be `null` when the action could not resolve a key +- `InterruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released +- `PausePointId` (string, nullable): The marker id when a `UloopPausePoint.Pause` marker caused the interruption +- `PausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker caused the interruption +- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one +- `PressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was actually visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge (e.g. the press was consumed by an editor-only input update) — retry the input or verify with a focused log instead of trusting `Success` alone. `null` only for `KeyUp` and for timed-out responses; pause-point interruptions still report the observed value ## Prerequisites diff --git a/.agents/skills/uloop-simulate-mouse-input/SKILL.md b/.agents/skills/uloop-simulate-mouse-input/SKILL.md index 8bc5b09e69..1df0d2a943 100644 --- a/.agents/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-input/SKILL.md @@ -52,7 +52,7 @@ uloop simulate-mouse-input --action [options] For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the mouse input, not immediately after `simulate-mouse-input`. -- If `interruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `pausePointId` and `pausePointHitCount` identify the marker. +- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. - Remove temporary pause-point/log instrumentation before final validation when it was added only for inspection. ### Global Options (optional) @@ -61,7 +61,6 @@ For standard frame proof when this input drives a state transition, follow the ` |--------|-------------| | `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | - ## When to use this vs simulate-mouse-ui All rows below assume the New Input System is installed. @@ -109,15 +108,16 @@ uloop simulate-mouse-input --action SmoothDelta --delta-x 300 --delta-y 0 --dura ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) -- `button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) -- `positionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) -- `positionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) -- `interruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `pausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption -- `pausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` -- `pausePointHits` (array, nullable): Every marker hit during this input as `{id, hitCount}` entries, in hit order. Read this when one input may trigger several markers; `pausePointId` only names the latest one + +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) +- `Button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) +- `PositionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) +- `PositionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) +- `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released +- `PausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption +- `PausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` +- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one Verify visual outcome with a follow-up screenshot. diff --git a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md index 8a9a797b6b..a8f7d7ecea 100644 --- a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md @@ -13,7 +13,7 @@ Simulate mouse interaction on Unity PlayMode UI. 1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not) 2. Get UI element info: `uloop screenshot --capture-mode rendering --annotate-elements --elements-only` -3. Use the `annotatedElements` array to find the target element by `label`, `name`, or `path` (A=frontmost, B=next, ...). Use `interaction` to distinguish click targets from drag/drop/text targets, then use `simX`/`simY` directly as `--x`/`--y` coordinates. +3. Use the `AnnotatedElements` array to find the target element by `Label`, `Name`, or `Path` (A=frontmost, B=next, ...). Use `Interaction` to distinguish click targets from drag/drop/text targets, then use `SimX`/`SimY` directly as `--x`/`--y` coordinates. 4. Execute the needed `uloop simulate-mouse-ui` commands 5. Inspect the result with the lightest useful evidence: runtime state, logs, or a screenshot 6. When this UI input verifies a state transition, use Pause Point inspection from the section below as the standard frame proof @@ -38,7 +38,7 @@ uloop simulate-mouse-ui --action --x --y [options] | `--duration` | number | `0.5` | Hold duration in seconds for LongPress action. | | `--button` | enum | `Left` | Mouse button. `Click` and `LongPress` support `Left`, `Right`, and `Middle`. Drag actions support `Left` only; other buttons return an error. | | `--bypass-raycast` | flag | - | For `Click`, `LongPress`, `Drag`, and `DragStart`, bypass EventSystem raycast and dispatch pointer events directly to `--target-path`. Use when a raycast-blocking overlay visually covers the intended target. | -| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `annotatedElements[].path` from screenshot JSON. | +| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `AnnotatedElements[].Path` from screenshot JSON. | | `--drop-target-path` | string | `""` | Optional hierarchy path of a drop target for `Drag` or `DragEnd`, for example `Canvas/DropZone`. Use this when the drop zone is also behind a raycast blocker. | ### Actions @@ -64,14 +64,13 @@ uloop simulate-mouse-ui --action --x --y [options] |--------|-------------| | `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | - ## Coordinate System - Origin is **top-left** (0, 0) - All positions are in **screen pixels** -- Get coordinates from `annotatedElements` JSON (`simX`/`simY`) — do NOT look up GameObject positions +- Get coordinates from `AnnotatedElements` JSON (`SimX`/`SimY`) — do NOT look up GameObject positions - Clicking or long-pressing on empty space (no UI element) still succeeds with a message indicating no element was hit -- Dragging on empty space (no draggable UI element) returns `success = false` +- Dragging on empty space (no draggable UI element) returns `Success = false` - `--bypass-raycast` still uses coordinates for pointer event positions, but chooses the clicked, long-pressed, or dragged GameObject by `--target-path` - If `--target-path` or `--drop-target-path` matches multiple active GameObjects, the command fails instead of choosing an arbitrary duplicate @@ -125,15 +124,16 @@ uloop simulate-mouse-ui --action DragEnd --x 600 --y 300 ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") -- `action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) -- `hitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) -- `positionX`: Target X coordinate that was used -- `positionY`: Target Y coordinate that was used -- `endPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) -- `endPositionY`: Drag end Y coordinate (nullable float; populated for drag actions only) + +- `Success`: Whether the operation succeeded +- `Message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") +- `Action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) +- `HitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) +- `PositionX`: Target X coordinate that was used +- `PositionY`: Target Y coordinate that was used +- `EndPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) +- `EndPositionY`: Drag end Y coordinate (nullable float; populated for drag actions only) Verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. -Note: Click and LongPress on empty space (no UI element) still return `success = true` with `hitGameObjectName = null`. Drag actions on empty space return `success = false`. +Note: Click and LongPress on empty space (no UI element) still return `Success = true` with `HitGameObjectName = null`. Drag actions on empty space return `Success = false`. diff --git a/.agents/skills/uloop-wait-for-pause-point/SKILL.md b/.agents/skills/uloop-wait-for-pause-point/SKILL.md index 14699e0deb..a0055d4f47 100644 --- a/.agents/skills/uloop-wait-for-pause-point/SKILL.md +++ b/.agents/skills/uloop-wait-for-pause-point/SKILL.md @@ -3,6 +3,8 @@ name: uloop-wait-for-pause-point description: "Pauses Unity's playback and allows you to inspect specific frames. Use this for bug investigation or PlayMode/E2E testing. It's the most convenient yet rigorous method for verifying variable states at specific frames or confirming whether particular code has been executed." --- +# uloop wait-for-pause-point + ## Quick Check Template Use this small loop for one representative frame you care about: @@ -19,27 +21,27 @@ Debug.Log($"state-transition-applied localValue={localValue} reason={reason}"); UloopPausePoint.Pause("state-transition-applied"); ``` -2. Compile, enter PlayMode, then enable the marker: +1. Compile, enter PlayMode, then enable the marker: ```bash uloop enable-pause-point --id state-transition-applied --timeout-seconds 30 ``` -3. Trigger the action with a `simulate-*` command. -4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `interruptedByPausePoint=true`: +1. Trigger the action with a `simulate-*` command. +2. Wait for the marker and read the focused log in one call, even if the trigger command already returned `InterruptedByPausePoint=true`: ```bash uloop wait-for-pause-point --id state-transition-applied --timeout-seconds 30 ``` -The hit response always embeds the log entries matching the marker id as `matchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `matchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. +The hit response always embeds the log entries matching the marker id as `MatchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `MatchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. -Read `evidenceSummary` first when it is present. It groups `editorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `editorState.capturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. +Read `EvidenceSummary` first when it is present. It groups `EditorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `EditorState.CapturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. -Use `generation`, `enabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `remainingMilliseconds` and `expired` are returned directly so you do not need to infer marker lifetime from elapsed time. +Use `Generation`, `EnabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `RemainingMilliseconds` and `Expired` are returned directly so you do not need to infer marker lifetime from elapsed time. -5. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. -6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `editorState` describes Unity Editor play/pause state, not marker state. +1. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. +2. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `EditorState` describes Unity Editor play/pause state, not marker state. ## When To Use @@ -48,12 +50,12 @@ Use `generation`, `enabledAtUtc`, and the hit sequence fields from the hit or st - Use this before reaching for `Time.timeScale`, sleeps, repeated polling, or after-the-fact `execute-dynamic-code`; those checks can supplement the paused-frame proof, but they are not substitutes. - If the value you need is a method local, an intermediate calculation, or a branch reason that `execute-dynamic-code` cannot reach, add a focused `Debug.Log` immediately before the marker and read it with `get-logs` while paused. Do not count the breakpoint check complete until the matching log has been read. - Treat the pause like a lightweight breakpoint for one important transition: combine nearby debug logs with paused-frame inspection to confirm the variables and component state at that point. -- Do not treat `simulate-* success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. +- Do not treat `simulate-* Success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. - Skip this only for ordinary persistent-state checks when you are not validating simulated input delivery, event ordering, or transition-frame fidelity. ## Timeout Checks -If this command times out, the marker line was not reached while the command waited. Read `error.details.hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `error.details.recommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `id` and `timeoutSeconds`. Then inspect `error.details.status`, `hitCount`, `generation`, `enabledAtUtc`, `editorState`, `elapsedSinceEnabledMilliseconds`, and `remainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `error.details.matchingLogs` and `error.details.evidenceSummary` show whether the marker's focused log ever appeared. `elapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. +If this command times out, the marker line was not reached while the command waited. Read `Error.Details.Hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `Error.Details.RecommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `Id` and `TimeoutSeconds`. Then inspect `Error.Details.Status`, `HitCount`, `Generation`, `EnabledAtUtc`, `EditorState`, `ElapsedSinceEnabledMilliseconds`, and `RemainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `Error.Details.MatchingLogs` and `Error.Details.EvidenceSummary` show whether the marker's focused log ever appeared. `ElapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. Use `uloop pause-point-status --id state-transition-applied` only when you need to confirm the marker is armed or inspect the current hit state. Add focused debug logs before the marker when local variables must be captured. diff --git a/.claude/skills/uloop-clear-console/SKILL.md b/.claude/skills/uloop-clear-console/SKILL.md index 3cb9e2be3f..8f34706efa 100644 --- a/.claude/skills/uloop-clear-console/SKILL.md +++ b/.claude/skills/uloop-clear-console/SKILL.md @@ -39,10 +39,11 @@ uloop clear-console --add-confirmation-message ## Output Returns JSON with: -- `success` (boolean): Whether the clear operation succeeded -- `clearedLogCount` (number): Total number of log entries that were cleared -- `clearedCounts` (object): Breakdown by log type - - `errorCount` (number): Errors cleared - - `warningCount` (number): Warnings cleared - - `logCount` (number): Info logs cleared -- `message` (string): Description of the result. On failure, this field carries the error summary. + +- `Success` (boolean): Whether the clear operation succeeded +- `ClearedLogCount` (number): Total number of log entries that were cleared +- `ClearedCounts` (object): Breakdown by log type + - `ErrorCount` (number): Errors cleared + - `WarningCount` (number): Warnings cleared + - `LogCount` (number): Info logs cleared +- `Message` (string): Description of the result. On failure, this field carries the error summary. diff --git a/.claude/skills/uloop-compile/SKILL.md b/.claude/skills/uloop-compile/SKILL.md index 9a2780a2a1..3eeb6bfe4e 100644 --- a/.claude/skills/uloop-compile/SKILL.md +++ b/.claude/skills/uloop-compile/SKILL.md @@ -44,7 +44,8 @@ uloop compile --stop-on-external-scene-changes ## Output Returns JSON: -- `success`: boolean or null -- `errorCount`: number or null -- `warningCount`: number or null -- `message`: string + +- `Success`: boolean or null +- `ErrorCount`: number or null +- `WarningCount`: number or null +- `Message`: string diff --git a/.claude/skills/uloop-control-play-mode/SKILL.md b/.claude/skills/uloop-control-play-mode/SKILL.md index bfdb9e9bc9..fc85b7df71 100644 --- a/.claude/skills/uloop-control-play-mode/SKILL.md +++ b/.claude/skills/uloop-control-play-mode/SKILL.md @@ -49,14 +49,15 @@ uloop control-play-mode --action Step ## Output Returns JSON with the current play mode state: -- `isPlaying`: Whether Unity is currently in play mode -- `isPaused`: Whether play mode is paused -- `changed`: Whether the requested action changed the current play mode state -- `wasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped -- `message`: Description of the action performed + +- `IsPlaying`: Whether Unity is currently in play mode +- `IsPaused`: Whether play mode is paused +- `Changed`: Whether the requested action changed the current play mode state +- `WasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped +- `Message`: Description of the action performed ## Notes -- Stop on an already-stopped Editor sets `changed: false`, `wasAlreadyStopped: true` +- Stop on an already-stopped Editor sets `Changed: false`, `WasAlreadyStopped: true` - Step is independent of `Time.timeScale`; requires PlayMode to be running - The command waits for the requested state before returning. Increase `--timeout-seconds` for projects with slow PlayMode entry. diff --git a/.claude/skills/uloop-execute-dynamic-code/SKILL.md b/.claude/skills/uloop-execute-dynamic-code/SKILL.md index 1b2c08ebf9..7fc716edb2 100644 --- a/.claude/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.claude/skills/uloop-execute-dynamic-code/SKILL.md @@ -43,16 +43,17 @@ Prefer terminal commands for file operations and keep snippets focused on Unity ## Output Returns JSON: -- `success`: boolean — overall execution success -- `result`: string — value of the snippet's `return` statement (empty when omitted) -- `logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` -- `compilationErrors`: object[] — Roslyn diagnostics with `message`, `line`, `column`, `errorCode`, optional `hint` and `suggestions` -- `error` / `errorMessage`: string — top-level failure summary (empty on success) -- `updatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) -- `diagnosticsSummary`: string|null — compact summary when diagnostics are available -- `diagnostics`: object[] — structured diagnostics; same shape as `compilationErrors`, usually populated together with it - -On `success: false`, inspect `compilationErrors` first. If empty, read `errorMessage` (and `logs` for extra context) — the failure may be a runtime exception, cancellation, or an "execution in progress" rejection, all of which return empty `compilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. + +- `Success`: boolean — overall execution success +- `Result`: string — value of the snippet's `return` statement (empty when omitted) +- `Logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` +- `CompilationErrors`: object[] — Roslyn diagnostics with `Message`, `Line`, `Column`, `ErrorCode`, optional `Hint` and `Suggestions` +- `Error` / `ErrorMessage`: string — top-level failure summary (empty on success) +- `UpdatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) +- `DiagnosticsSummary`: string|null — compact summary when diagnostics are available +- `Diagnostics`: object[] — structured diagnostics; same shape as `CompilationErrors`, usually populated together with it + +On `Success: false`, inspect `CompilationErrors` first. If empty, read `ErrorMessage` (and `Logs` for extra context) — the failure may be a runtime exception, cancellation, or an "execution in progress" rejection, all of which return empty `CompilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. ## Code Examples by Category diff --git a/.claude/skills/uloop-find-game-objects/SKILL.md b/.claude/skills/uloop-find-game-objects/SKILL.md index 24edd909a3..cd92c9963e 100644 --- a/.claude/skills/uloop-find-game-objects/SKILL.md +++ b/.claude/skills/uloop-find-game-objects/SKILL.md @@ -70,21 +70,23 @@ uloop find-game-objects --search-mode Selected --include-inactive ## Output Returns JSON with: -- `results` (array): Matching GameObjects, each containing: - - `name` (string): GameObject name - - `path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) - - `isActive` (boolean): Active state in hierarchy - - `tag` (string): GameObject tag - - `layer` (number): Layer index - - `components` (array): Each entry has `type` (short name, e.g., `Rigidbody`), `fullTypeName` (e.g., `UnityEngine.Rigidbody`), and `properties` (array of Inspector-visible `{name, type, value}` pairs) -- `totalFound` (number): Results returned (after `--max-results` clipping). For multi-selection file export, this is the number exported. -- `errorMessage` (string): Top-level failure summary (empty on success) -- `processingErrors` (array): Selected-mode per-GameObject serialization failures, each `{gameObjectName, gameObjectPath, error}`. Omitted/null or empty on clean runs. + +- `Results` (array): Matching GameObjects, each containing: + - `Name` (string): GameObject name + - `Path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) + - `IsActive` (boolean): Active state in hierarchy + - `Tag` (string): GameObject tag + - `Layer` (number): Layer index + - `Components` (array): Each entry has `Type` (short name, e.g., `Rigidbody`), `FullTypeName` (e.g., `UnityEngine.Rigidbody`), and `Properties` (array of Inspector-visible `{Name, Type, Value}` pairs) +- `TotalFound` (number): Results returned (after `--max-results` clipping). For multi-selection file export, this is the number exported. +- `ErrorMessage` (string): Top-level failure summary (empty on success) +- `ProcessingErrors` (array): Selected-mode per-GameObject serialization failures, each `{GameObjectName, GameObjectPath, Error}`. Omitted/null or empty on clean runs. ### Multi-selection file export -For `Selected` mode with **multiple** successfully serialized GameObjects, inline `results` is not populated and the data is written to a file instead. Two extra fields appear: -- `resultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` -- `message` (string): Human-readable summary (e.g., "5 GameObjects exported") +For `Selected` mode with **multiple** successfully serialized GameObjects, inline `Results` is not populated and the data is written to a file instead. Two extra fields appear: + +- `ResultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` +- `Message` (string): Human-readable summary (e.g., "5 GameObjects exported") -Single-selection and search-mode calls (`Exact`, `Path`, `Regex`, `Contains`) always return inline. No selection (`Selected` mode with empty selection) returns empty `results` plus a `message`. +Single-selection and search-mode calls (`Exact`, `Path`, `Regex`, `Contains`) always return inline. No selection (`Selected` mode with empty selection) returns empty `Results` plus a `Message`. diff --git a/.claude/skills/uloop-focus-window/SKILL.md b/.claude/skills/uloop-focus-window/SKILL.md index e2ca7a23bb..ead80195ae 100644 --- a/.claude/skills/uloop-focus-window/SKILL.md +++ b/.claude/skills/uloop-focus-window/SKILL.md @@ -29,8 +29,9 @@ uloop focus-window ## Output Returns JSON with: -- `success`: Whether the focus operation succeeded -- `message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) + +- `Success`: Whether the focus operation succeeded +- `Message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) ## Notes diff --git a/.claude/skills/uloop-get-hierarchy/SKILL.md b/.claude/skills/uloop-get-hierarchy/SKILL.md index 4a1ec7cc08..1af6712840 100644 --- a/.claude/skills/uloop-get-hierarchy/SKILL.md +++ b/.claude/skills/uloop-get-hierarchy/SKILL.md @@ -56,7 +56,8 @@ uloop get-hierarchy --use-selection ## Output Returns JSON with: -- `message` (string): Human-readable guidance pointing at the saved file -- `hierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data -The hierarchy itself is **not** in the response — it is written to the file at `hierarchyFilePath`. Open that file to read the `Context` and `Hierarchy` payload (GameObject tree, components, etc.). +- `Message` (string): Human-readable guidance pointing at the saved file +- `HierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data + +The hierarchy itself is **not** in the response — it is written to the file at `HierarchyFilePath`. Open that file to read the `Context` and `Hierarchy` payload (GameObject tree, components, etc.). diff --git a/.claude/skills/uloop-get-logs/SKILL.md b/.claude/skills/uloop-get-logs/SKILL.md index 57d1d97f62..dab6fc1d2f 100644 --- a/.claude/skills/uloop-get-logs/SKILL.md +++ b/.claude/skills/uloop-get-logs/SKILL.md @@ -50,10 +50,11 @@ uloop get-logs --search-text "Missing.*Component" --use-regex ## Output Returns JSON with: -- `totalCount` (number): Total logs available before max-count clipping -- `displayedCount` (number): Logs returned in this response (≤ `--max-count`) -- Input filters (`logType`, `maxCount`, `searchText`, `includeStackTrace`) are echoed back in the response -- `logs` (array): Each entry has: - - `type` (string): `"Error"`, `"Warning"`, or `"Log"` - - `message` (string): Log message body - - `stackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. + +- `TotalCount` (number): Total logs available before max-count clipping +- `DisplayedCount` (number): Logs returned in this response (≤ `--max-count`) +- Input filters (`LogType`, `MaxCount`, `SearchText`, `IncludeStackTrace`) are echoed back in the response +- `Logs` (array): Each entry has: + - `Type` (string): `"Error"`, `"Warning"`, or `"Log"` + - `Message` (string): Log message body + - `StackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. diff --git a/.claude/skills/uloop-hello-world/SKILL.md b/.claude/skills/uloop-hello-world/SKILL.md index 2724067ddd..2c0153fb28 100644 --- a/.claude/skills/uloop-hello-world/SKILL.md +++ b/.claude/skills/uloop-hello-world/SKILL.md @@ -40,13 +40,15 @@ uloop hello-world --name "Carlos" --language spanish --no-include-timestamp ## Output Returns JSON with: -- `message`: The greeting message -- `language`: Language used for greeting -- `timestamp`: Current timestamp (if enabled) + +- `Message`: The greeting message +- `Language`: Language used for greeting +- `Timestamp`: Current timestamp (if enabled) ## Notes This is a sample custom tool demonstrating: + - Type-safe parameter handling with Schema - Enum parameters for language selection - Value-less flag parameters diff --git a/.claude/skills/uloop-launch/SKILL.md b/.claude/skills/uloop-launch/SKILL.md index 7bbee3d3e5..82aadc69d6 100644 --- a/.claude/skills/uloop-launch/SKILL.md +++ b/.claude/skills/uloop-launch/SKILL.md @@ -50,18 +50,19 @@ uloop launch --quit May print status/progress lines before the final JSON payload, such as project path, detected Unity version, or readiness wait messages. The final JSON payload includes: -- `success`: whether the command completed -- `ready`: whether Unity CLI Loop is ready for commands -- `serverReady`: whether the Unity CLI Loop server accepted requests -- `projectIpcReady`: whether the project IPC path accepted tool requests -- `alreadyRunning`: whether an existing Unity process was reused -- `launched`: whether this command launched a Unity process -- `restarted`: whether this command stopped an existing process and launched a new one -- `quit`: whether this command stopped Unity without launching a new process -- `previousProcessId`: process ID stopped by restart or quit, when available -- `currentProcessId`: current Unity process ID, when available -- `projectRoot`: resolved project root -- `message`: readiness summary + +- `Success`: whether the command completed +- `Ready`: whether Unity CLI Loop is ready for commands +- `ServerReady`: whether the Unity CLI Loop server accepted requests +- `ProjectIpcReady`: whether the project IPC path accepted tool requests +- `AlreadyRunning`: whether an existing Unity process was reused +- `Launched`: whether this command launched a Unity process +- `Restarted`: whether this command stopped an existing process and launched a new one +- `Quit`: whether this command stopped Unity without launching a new process +- `PreviousProcessId`: process ID stopped by restart or quit, when available +- `CurrentProcessId`: current Unity process ID, when available +- `ProjectRoot`: resolved project root +- `Message`: readiness summary ## Notes diff --git a/.claude/skills/uloop-record-input/SKILL.md b/.claude/skills/uloop-record-input/SKILL.md index a76d38cb33..43f966dc3d 100644 --- a/.claude/skills/uloop-record-input/SKILL.md +++ b/.claude/skills/uloop-record-input/SKILL.md @@ -44,11 +44,12 @@ Replay injects input frame-by-frame, so the game must also be deterministic to p ## Output The CLI prints JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Start` or `Stop`) -- `outputPath`: Path to saved recording (nullable; populated on `Stop` only) -- `totalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) -- `durationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) - -Frame data lives only in the JSON file at `outputPath`; internal metadata fields are not included in the response. + +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Start` or `Stop`) +- `OutputPath`: Path to saved recording (nullable; populated on `Stop` only) +- `TotalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) +- `DurationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) + +Frame data lives only in the JSON file at `OutputPath`; internal metadata fields are not included in the response. diff --git a/.claude/skills/uloop-replay-input/SKILL.md b/.claude/skills/uloop-replay-input/SKILL.md index 81793dbc8e..051b96759c 100644 --- a/.claude/skills/uloop-replay-input/SKILL.md +++ b/.claude/skills/uloop-replay-input/SKILL.md @@ -48,11 +48,12 @@ Replay injects the exact same input frame-by-frame, but the game must also be de ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) -- `inputPath`: Path to recording file (nullable string; populated on `Start` only) -- `currentFrame`: Current replay frame index (nullable int) -- `totalFrames`: Total frames in the recording (nullable int) -- `progress`: Replay progress (nullable float in 0.0 – 1.0) -- `isReplaying`: Whether replay is currently active (nullable bool) + +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) +- `InputPath`: Path to recording file (nullable string; populated on `Start` only) +- `CurrentFrame`: Current replay frame index (nullable int) +- `TotalFrames`: Total frames in the recording (nullable int) +- `Progress`: Replay progress (nullable float in 0.0 – 1.0) +- `IsReplaying`: Whether replay is currently active (nullable bool) diff --git a/.claude/skills/uloop-run-tests/SKILL.md b/.claude/skills/uloop-run-tests/SKILL.md index 7660586e11..c77e73e7b2 100644 --- a/.claude/skills/uloop-run-tests/SKILL.md +++ b/.claude/skills/uloop-run-tests/SKILL.md @@ -6,13 +6,13 @@ description: "Run Unity Test Runner and report detailed results. Use for EditMod # uloop run-tests -Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `xmlPath` for detailed failure diagnosis. +Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `XmlPath` for detailed failure diagnosis. Before running tests, run `uloop compile` first if you created, deleted, renamed, moved, or edited C# files, test files, `.asmdef`/`.asmref`, or package manifests since the last successful compile. This refreshes the AssetDatabase and surfaces compile errors before test execution. -Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `success: false`, keeps `testCount` at `0`, lists the unsaved items in `message`, and does not start the Unity Test Runner. +Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `Success: false`, keeps `TestCount` at `0`, lists the unsaved items in `Message`, and does not start the Unity Test Runner. -`NoTestsFound` means zero tests matched — not a test failure. Check `noTestsFoundExplanation` and `message` for asmdef hints. +`NoTestsFound` means zero tests matched — not a test failure. Check `NoTestsFoundExplanation` and `Message` for asmdef hints. ## Usage @@ -57,22 +57,24 @@ uloop run-tests --filter-type regex --filter-value ".*Integration.*" ## Output Returns JSON with: -- `success` (boolean): Whether all tests passed -- `status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` -- `hasFailures` (boolean): Whether any discovered test failed -- `message` (string): Summary message -- `noTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests -- `noTestsFoundExplanation` (string): Agent-facing explanation when `noTestsFound` is true; empty otherwise -- `completedAt` (string): ISO timestamp when the run finished -- `testCount` (number): Total tests executed -- `passedCount` (number): Passed tests -- `failedCount` (number): Failed tests -- `skippedCount` (number): Skipped tests -- `xmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `success: true`); populated only when tests failed and the XML file exists on disk. + +- `Success` (boolean): Whether all tests passed +- `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` +- `HasFailures` (boolean): Whether any discovered test failed +- `Message` (string): Summary message +- `NoTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests +- `NoTestsFoundExplanation` (string): Agent-facing explanation when `NoTestsFound` is true; empty otherwise +- `CompletedAt` (string): ISO timestamp when the run finished +- `TestCount` (number): Total tests executed +- `PassedCount` (number): Passed tests +- `FailedCount` (number): Failed tests +- `SkippedCount` (number): Skipped tests +- `XmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `Success: true`); populated only when tests failed and the XML file exists on disk. ### XML Result File When tests fail, NUnit XML results are automatically saved to `{project_root}/.uloop/outputs/TestResults/.xml`. The XML contains per-test-case results including: + - Test name and full name - Pass/fail/skip status and duration - For failed tests: `` (assertion error) and `` diff --git a/.claude/skills/uloop-screenshot/SKILL.md b/.claude/skills/uloop-screenshot/SKILL.md index bc721a5ee7..39e81c686e 100644 --- a/.claude/skills/uloop-screenshot/SKILL.md +++ b/.claude/skills/uloop-screenshot/SKILL.md @@ -72,18 +72,19 @@ uloop screenshot --output-directory /tmp/screenshots ## Output Returns JSON with: -- `screenshotCount`: Number of windows captured -- `screenshots`: Array of screenshot info, each containing: - - `imagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. - - `fileSizeBytes`: Size of the saved file in bytes - - `width`: Captured image width in pixels - - `height`: Captured image height in pixels - - `coordinateSystem`: `"gameView"` or `"window"` - - `resolutionScale`: Resolution scale used for capture - - `yOffset`: Y offset used for gameView coordinate conversion - - `annotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` is used. - -For `annotatedElements` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. + +- `ScreenshotCount`: Number of windows captured +- `Screenshots`: Array of screenshot info, each containing: + - `ImagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. + - `FileSizeBytes`: Size of the saved file in bytes + - `Width`: Captured image width in pixels + - `Height`: Captured image height in pixels + - `CoordinateSystem`: `"gameView"` or `"window"` + - `ResolutionScale`: Resolution scale used for capture + - `YOffset`: Y offset used for gameView coordinate conversion + - `AnnotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` is used. + +For `AnnotatedElements` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. When multiple windows match (e.g., multiple Inspector windows or when using `contains` mode), all matching windows are captured with numbered filenames (e.g., `Inspector_1_*.png`, `Inspector_2_*.png`). diff --git a/.claude/skills/uloop-screenshot/references/annotated-elements.md b/.claude/skills/uloop-screenshot/references/annotated-elements.md index 50b899b096..283109ccc2 100644 --- a/.claude/skills/uloop-screenshot/references/annotated-elements.md +++ b/.claude/skills/uloop-screenshot/references/annotated-elements.md @@ -2,34 +2,34 @@ Read this when using `uloop screenshot --capture-mode rendering --annotate-elements` to find coordinates for `simulate-mouse-ui` or `simulate-mouse-input`. -## annotatedElements Fields +## AnnotatedElements Fields -`annotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: +`AnnotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: -- `label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. -- `name`: Element name -- `path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. -- `type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) -- `interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. -- `simX`, `simY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. -- `boundsMinX`, `boundsMinY`, `boundsMaxX`, `boundsMaxY`: Bounding box in simulate-mouse coordinates -- `sortingOrder`: Canvas sorting order. Higher values are in front. -- `siblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. +- `Label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. +- `Name`: Element name +- `Path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. +- `Type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) +- `Interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. +- `SimX`, `SimY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. +- `BoundsMinX`, `BoundsMinY`, `BoundsMaxX`, `BoundsMaxY`: Bounding box in simulate-mouse coordinates +- `SortingOrder`: Canvas sorting order. Higher values are in front. +- `SiblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. ## Coordinate Conversion -When `coordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: +When `CoordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: ```text -sim_x = image_x / resolutionScale -sim_y = image_y / resolutionScale + yOffset +sim_x = image_x / ResolutionScale +sim_y = image_y / ResolutionScale + YOffset ``` -When `resolutionScale` is `1.0`, this simplifies to: +When `ResolutionScale` is `1.0`, this simplifies to: ```text sim_x = image_x -sim_y = image_y + yOffset +sim_y = image_y + YOffset ``` ## Annotation Readability diff --git a/.claude/skills/uloop-simulate-keyboard/SKILL.md b/.claude/skills/uloop-simulate-keyboard/SKILL.md index 7b65d6b135..e898d1090a 100644 --- a/.claude/skills/uloop-simulate-keyboard/SKILL.md +++ b/.claude/skills/uloop-simulate-keyboard/SKILL.md @@ -42,11 +42,12 @@ uloop simulate-keyboard --action --key [options] Use `Press` for edge-triggered keyboard code such as `Keyboard.current.spaceKey.wasPressedThisFrame`. `KeyDown` emits one initial press edge, then only keeps the key held. It does not keep `wasPressedThisFrame` true while the key remains held. If a successful `Press` or `KeyDown` leaves `Keyboard.current..isPressed` true but runtime state does not change, do not immediately rewrite the user's runtime code to `isPressed`. First verify that the target component is active during the command, that it polls input in the configured Input System update phase, and that a missed `KeyDown` edge is followed by `KeyUp` before retrying. + ### Pause Point Inspection (Standard for E2E) For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the key, not immediately after `simulate-keyboard`. -- If `interruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `pausePointId` and `pausePointHitCount` identify the marker. `pressEdgeObserved` is still reported on pause-point interruptions. +- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. `PressEdgeObserved` is still reported on pause-point interruptions. ### KeyDown/KeyUp Rules @@ -85,15 +86,16 @@ uloop simulate-keyboard --action KeyUp --key LeftShift ## Output Returns JSON with: -- `success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) -- `message` (string): Description of what happened or why it failed -- `action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) -- `keyName` (string, nullable): The key that was acted on; may be `null` when the action could not resolve a key -- `interruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `pausePointId` (string, nullable): The marker id when a `UloopPausePoint.Pause` marker caused the interruption -- `pausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker caused the interruption -- `pausePointHits` (array, nullable): Every marker hit during this input as `{id, hitCount}` entries, in hit order. Read this when one input may trigger several markers; `pausePointId` only names the latest one -- `pressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was actually visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge (e.g. the press was consumed by an editor-only input update) — retry the input or verify with a focused log instead of trusting `success` alone. `null` only for `KeyUp` and for timed-out responses; pause-point interruptions still report the observed value + +- `Success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) +- `Message` (string): Description of what happened or why it failed +- `Action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) +- `KeyName` (string, nullable): The key that was acted on; may be `null` when the action could not resolve a key +- `InterruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released +- `PausePointId` (string, nullable): The marker id when a `UloopPausePoint.Pause` marker caused the interruption +- `PausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker caused the interruption +- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one +- `PressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was actually visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge (e.g. the press was consumed by an editor-only input update) — retry the input or verify with a focused log instead of trusting `Success` alone. `null` only for `KeyUp` and for timed-out responses; pause-point interruptions still report the observed value ## Prerequisites diff --git a/.claude/skills/uloop-simulate-mouse-input/SKILL.md b/.claude/skills/uloop-simulate-mouse-input/SKILL.md index 8bc5b09e69..1df0d2a943 100644 --- a/.claude/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-input/SKILL.md @@ -52,7 +52,7 @@ uloop simulate-mouse-input --action [options] For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the mouse input, not immediately after `simulate-mouse-input`. -- If `interruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `pausePointId` and `pausePointHitCount` identify the marker. +- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. - Remove temporary pause-point/log instrumentation before final validation when it was added only for inspection. ### Global Options (optional) @@ -61,7 +61,6 @@ For standard frame proof when this input drives a state transition, follow the ` |--------|-------------| | `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | - ## When to use this vs simulate-mouse-ui All rows below assume the New Input System is installed. @@ -109,15 +108,16 @@ uloop simulate-mouse-input --action SmoothDelta --delta-x 300 --delta-y 0 --dura ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) -- `button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) -- `positionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) -- `positionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) -- `interruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `pausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption -- `pausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` -- `pausePointHits` (array, nullable): Every marker hit during this input as `{id, hitCount}` entries, in hit order. Read this when one input may trigger several markers; `pausePointId` only names the latest one + +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) +- `Button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) +- `PositionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) +- `PositionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) +- `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released +- `PausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption +- `PausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` +- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one Verify visual outcome with a follow-up screenshot. diff --git a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md index 8a9a797b6b..a8f7d7ecea 100644 --- a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md @@ -13,7 +13,7 @@ Simulate mouse interaction on Unity PlayMode UI. 1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not) 2. Get UI element info: `uloop screenshot --capture-mode rendering --annotate-elements --elements-only` -3. Use the `annotatedElements` array to find the target element by `label`, `name`, or `path` (A=frontmost, B=next, ...). Use `interaction` to distinguish click targets from drag/drop/text targets, then use `simX`/`simY` directly as `--x`/`--y` coordinates. +3. Use the `AnnotatedElements` array to find the target element by `Label`, `Name`, or `Path` (A=frontmost, B=next, ...). Use `Interaction` to distinguish click targets from drag/drop/text targets, then use `SimX`/`SimY` directly as `--x`/`--y` coordinates. 4. Execute the needed `uloop simulate-mouse-ui` commands 5. Inspect the result with the lightest useful evidence: runtime state, logs, or a screenshot 6. When this UI input verifies a state transition, use Pause Point inspection from the section below as the standard frame proof @@ -38,7 +38,7 @@ uloop simulate-mouse-ui --action --x --y [options] | `--duration` | number | `0.5` | Hold duration in seconds for LongPress action. | | `--button` | enum | `Left` | Mouse button. `Click` and `LongPress` support `Left`, `Right`, and `Middle`. Drag actions support `Left` only; other buttons return an error. | | `--bypass-raycast` | flag | - | For `Click`, `LongPress`, `Drag`, and `DragStart`, bypass EventSystem raycast and dispatch pointer events directly to `--target-path`. Use when a raycast-blocking overlay visually covers the intended target. | -| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `annotatedElements[].path` from screenshot JSON. | +| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `AnnotatedElements[].Path` from screenshot JSON. | | `--drop-target-path` | string | `""` | Optional hierarchy path of a drop target for `Drag` or `DragEnd`, for example `Canvas/DropZone`. Use this when the drop zone is also behind a raycast blocker. | ### Actions @@ -64,14 +64,13 @@ uloop simulate-mouse-ui --action --x --y [options] |--------|-------------| | `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | - ## Coordinate System - Origin is **top-left** (0, 0) - All positions are in **screen pixels** -- Get coordinates from `annotatedElements` JSON (`simX`/`simY`) — do NOT look up GameObject positions +- Get coordinates from `AnnotatedElements` JSON (`SimX`/`SimY`) — do NOT look up GameObject positions - Clicking or long-pressing on empty space (no UI element) still succeeds with a message indicating no element was hit -- Dragging on empty space (no draggable UI element) returns `success = false` +- Dragging on empty space (no draggable UI element) returns `Success = false` - `--bypass-raycast` still uses coordinates for pointer event positions, but chooses the clicked, long-pressed, or dragged GameObject by `--target-path` - If `--target-path` or `--drop-target-path` matches multiple active GameObjects, the command fails instead of choosing an arbitrary duplicate @@ -125,15 +124,16 @@ uloop simulate-mouse-ui --action DragEnd --x 600 --y 300 ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") -- `action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) -- `hitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) -- `positionX`: Target X coordinate that was used -- `positionY`: Target Y coordinate that was used -- `endPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) -- `endPositionY`: Drag end Y coordinate (nullable float; populated for drag actions only) + +- `Success`: Whether the operation succeeded +- `Message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") +- `Action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) +- `HitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) +- `PositionX`: Target X coordinate that was used +- `PositionY`: Target Y coordinate that was used +- `EndPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) +- `EndPositionY`: Drag end Y coordinate (nullable float; populated for drag actions only) Verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. -Note: Click and LongPress on empty space (no UI element) still return `success = true` with `hitGameObjectName = null`. Drag actions on empty space return `success = false`. +Note: Click and LongPress on empty space (no UI element) still return `Success = true` with `HitGameObjectName = null`. Drag actions on empty space return `Success = false`. diff --git a/.claude/skills/uloop-wait-for-pause-point/SKILL.md b/.claude/skills/uloop-wait-for-pause-point/SKILL.md index 14699e0deb..a0055d4f47 100644 --- a/.claude/skills/uloop-wait-for-pause-point/SKILL.md +++ b/.claude/skills/uloop-wait-for-pause-point/SKILL.md @@ -3,6 +3,8 @@ name: uloop-wait-for-pause-point description: "Pauses Unity's playback and allows you to inspect specific frames. Use this for bug investigation or PlayMode/E2E testing. It's the most convenient yet rigorous method for verifying variable states at specific frames or confirming whether particular code has been executed." --- +# uloop wait-for-pause-point + ## Quick Check Template Use this small loop for one representative frame you care about: @@ -19,27 +21,27 @@ Debug.Log($"state-transition-applied localValue={localValue} reason={reason}"); UloopPausePoint.Pause("state-transition-applied"); ``` -2. Compile, enter PlayMode, then enable the marker: +1. Compile, enter PlayMode, then enable the marker: ```bash uloop enable-pause-point --id state-transition-applied --timeout-seconds 30 ``` -3. Trigger the action with a `simulate-*` command. -4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `interruptedByPausePoint=true`: +1. Trigger the action with a `simulate-*` command. +2. Wait for the marker and read the focused log in one call, even if the trigger command already returned `InterruptedByPausePoint=true`: ```bash uloop wait-for-pause-point --id state-transition-applied --timeout-seconds 30 ``` -The hit response always embeds the log entries matching the marker id as `matchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `matchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. +The hit response always embeds the log entries matching the marker id as `MatchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `MatchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. -Read `evidenceSummary` first when it is present. It groups `editorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `editorState.capturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. +Read `EvidenceSummary` first when it is present. It groups `EditorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `EditorState.CapturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. -Use `generation`, `enabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `remainingMilliseconds` and `expired` are returned directly so you do not need to infer marker lifetime from elapsed time. +Use `Generation`, `EnabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `RemainingMilliseconds` and `Expired` are returned directly so you do not need to infer marker lifetime from elapsed time. -5. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. -6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `editorState` describes Unity Editor play/pause state, not marker state. +1. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. +2. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `EditorState` describes Unity Editor play/pause state, not marker state. ## When To Use @@ -48,12 +50,12 @@ Use `generation`, `enabledAtUtc`, and the hit sequence fields from the hit or st - Use this before reaching for `Time.timeScale`, sleeps, repeated polling, or after-the-fact `execute-dynamic-code`; those checks can supplement the paused-frame proof, but they are not substitutes. - If the value you need is a method local, an intermediate calculation, or a branch reason that `execute-dynamic-code` cannot reach, add a focused `Debug.Log` immediately before the marker and read it with `get-logs` while paused. Do not count the breakpoint check complete until the matching log has been read. - Treat the pause like a lightweight breakpoint for one important transition: combine nearby debug logs with paused-frame inspection to confirm the variables and component state at that point. -- Do not treat `simulate-* success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. +- Do not treat `simulate-* Success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. - Skip this only for ordinary persistent-state checks when you are not validating simulated input delivery, event ordering, or transition-frame fidelity. ## Timeout Checks -If this command times out, the marker line was not reached while the command waited. Read `error.details.hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `error.details.recommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `id` and `timeoutSeconds`. Then inspect `error.details.status`, `hitCount`, `generation`, `enabledAtUtc`, `editorState`, `elapsedSinceEnabledMilliseconds`, and `remainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `error.details.matchingLogs` and `error.details.evidenceSummary` show whether the marker's focused log ever appeared. `elapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. +If this command times out, the marker line was not reached while the command waited. Read `Error.Details.Hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `Error.Details.RecommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `Id` and `TimeoutSeconds`. Then inspect `Error.Details.Status`, `HitCount`, `Generation`, `EnabledAtUtc`, `EditorState`, `ElapsedSinceEnabledMilliseconds`, and `RemainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `Error.Details.MatchingLogs` and `Error.Details.EvidenceSummary` show whether the marker's focused log ever appeared. `ElapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. Use `uloop pause-point-status --id state-transition-applied` only when you need to confirm the marker is armed or inspect the current hit state. Add focused debug logs before the marker when local variables must be captured. diff --git a/Assets/Editor/CustomCommandSamples/GetProjectInfo/Skill/SKILL.md b/Assets/Editor/CustomCommandSamples/GetProjectInfo/Skill/SKILL.md index 6805b43933..729f7a1118 100644 --- a/Assets/Editor/CustomCommandSamples/GetProjectInfo/Skill/SKILL.md +++ b/Assets/Editor/CustomCommandSamples/GetProjectInfo/Skill/SKILL.md @@ -21,6 +21,7 @@ None. ## Output Returns JSON with: + - `ProjectName`: Application product name - `CompanyName`: Company name - `Version`: Application version diff --git a/Assets/Editor/CustomCommandSamples/HelloWorld/Skill/SKILL.md b/Assets/Editor/CustomCommandSamples/HelloWorld/Skill/SKILL.md index 2724067ddd..2c0153fb28 100644 --- a/Assets/Editor/CustomCommandSamples/HelloWorld/Skill/SKILL.md +++ b/Assets/Editor/CustomCommandSamples/HelloWorld/Skill/SKILL.md @@ -40,13 +40,15 @@ uloop hello-world --name "Carlos" --language spanish --no-include-timestamp ## Output Returns JSON with: -- `message`: The greeting message -- `language`: Language used for greeting -- `timestamp`: Current timestamp (if enabled) + +- `Message`: The greeting message +- `Language`: Language used for greeting +- `Timestamp`: Current timestamp (if enabled) ## Notes This is a sample custom tool demonstrating: + - Type-safe parameter handling with Schema - Enum parameters for language selection - Value-less flag parameters diff --git a/Assets/Editor/FindGameObjects/FindGameObjectsTestMenu.cs b/Assets/Editor/FindGameObjects/FindGameObjectsTestMenu.cs index 9604823c97..305d12dbfd 100644 --- a/Assets/Editor/FindGameObjects/FindGameObjectsTestMenu.cs +++ b/Assets/Editor/FindGameObjects/FindGameObjectsTestMenu.cs @@ -30,17 +30,17 @@ public static async void TestFindGameObjectsCamera() if (response is FindGameObjectsResponse findResponse) { - Debug.Log($"Found {findResponse.totalFound} objects with Camera"); + Debug.Log($"Found {findResponse.TotalFound} objects with Camera"); - foreach (FindGameObjectResult result in findResponse.results) + foreach (FindGameObjectResult result in findResponse.Results) { - Debug.Log($"- {result.name}: {result.components.Length} components"); + Debug.Log($"- {result.Name}: {result.Components.Length} components"); - foreach (ComponentInfo component in result.components) + foreach (ComponentInfo component in result.Components) { - if (component.type == "Camera") + if (component.Type == "Camera") { - Debug.Log($" Camera: {component.properties?.Length ?? 0} properties"); + Debug.Log($" Camera: {component.Properties?.Length ?? 0} properties"); } } } @@ -73,16 +73,16 @@ public static async void TestFindMainCameraByPath() if (response is FindGameObjectsResponse findResponse) { - Debug.Log($"[FindGameObjectsTestMenu] Found {findResponse.totalFound} objects"); + Debug.Log($"[FindGameObjectsTestMenu] Found {findResponse.TotalFound} objects"); - foreach (FindGameObjectResult result in findResponse.results) + foreach (FindGameObjectResult result in findResponse.Results) { - Debug.Log($"[FindGameObjectsTestMenu] - {result.name} at {result.path}"); - Debug.Log($"[FindGameObjectsTestMenu] Components: {result.components.Length}"); + Debug.Log($"[FindGameObjectsTestMenu] - {result.Name} at {result.Path}"); + Debug.Log($"[FindGameObjectsTestMenu] Components: {result.Components.Length}"); - foreach (ComponentInfo component in result.components) + foreach (ComponentInfo component in result.Components) { - Debug.Log($"[FindGameObjectsTestMenu] - {component.type}: {component.properties?.Length ?? 0} properties"); + Debug.Log($"[FindGameObjectsTestMenu] - {component.Type}: {component.Properties?.Length ?? 0} properties"); } } } diff --git a/Assets/Tests/Editor/CliInstallationDetectorTests.cs b/Assets/Tests/Editor/CliInstallationDetectorTests.cs index 3800c6d038..5f7c538c98 100644 --- a/Assets/Tests/Editor/CliInstallationDetectorTests.cs +++ b/Assets/Tests/Editor/CliInstallationDetectorTests.cs @@ -181,7 +181,7 @@ public void ParseShellCliInstallationOutput_WhenContractJsonExists_ReturnsProtoc + "/Users/ExampleUser/.npm-global/bin/uloop\n" + "__ULOOP_PATH_END__\n" + "__ULOOP_CONTRACT_START__\n" - + "{\"cliVersion\":\"3.0.0-beta.31\",\"protocolVersion\":1}\n" + + "{\"CliVersion\":\"3.0.0-beta.31\",\"ProtocolVersion\":1}\n" + "__ULOOP_CONTRACT_END__\n" + "__ULOOP_CONTRACT_STATUS_START__\n" + "0\n" @@ -209,7 +209,7 @@ public void ParseShellCliInstallationOutput_WhenProtocolVersionIsOutsideIntRange + "/tmp/uloop\n" + "__ULOOP_PATH_END__\n" + "__ULOOP_CONTRACT_START__\n" - + "{\"cliVersion\":\"3.0.0-beta.31\",\"protocolVersion\":2147483648}\n" + + "{\"CliVersion\":\"3.0.0-beta.31\",\"ProtocolVersion\":2147483648}\n" + "__ULOOP_CONTRACT_END__\n" + "__ULOOP_CONTRACT_STATUS_START__\n" + "0\n" diff --git a/Assets/Tests/Editor/CompileSessionResultServiceTests.cs b/Assets/Tests/Editor/CompileSessionResultServiceTests.cs index 1dcf606034..dc6254d0c5 100644 --- a/Assets/Tests/Editor/CompileSessionResultServiceTests.cs +++ b/Assets/Tests/Editor/CompileSessionResultServiceTests.cs @@ -171,9 +171,9 @@ public void CreateCompileResult_WhenUnityTestFrameworkSymbolIsMissing_AddsTestAs } [Test] - public void StoreCompileResult_WhenResultIsPersisted_UsesCamelCaseJson() + public void StoreCompileResult_WhenResultIsPersisted_UsesPascalCaseJson() { - // Verifies delayed compile polling reads the same camelCase response contract as immediate tool responses. + // Verifies delayed compile polling reads the same PascalCase response contract as immediate tool responses. UnityCliLoopEditorSessionStateService sessionStateService = UnityCliLoopEditorSessionStateTestFactory.CreateService(); UnityCliLoopEditorSessionStateSnapshot originalSnapshot = @@ -202,9 +202,9 @@ public void StoreCompileResult_WhenResultIsPersisted_UsesCamelCaseJson() UnityCliLoopStoredCompileResult storedResult = sessionStateService.GetCompileResult("compile_test_request"); - Assert.That(storedResult.ResultJson, Does.Contain("\"success\":true")); - Assert.That(storedResult.ResultJson, Does.Contain("\"errorCount\":0")); - Assert.That(storedResult.ResultJson, Does.Not.Contain("\"Success\"")); + Assert.That(storedResult.ResultJson, Does.Contain("\"Success\":true")); + Assert.That(storedResult.ResultJson, Does.Contain("\"ErrorCount\":0")); + Assert.That(storedResult.ResultJson, Does.Not.Contain("\"success\"")); } finally { diff --git a/Assets/Tests/Editor/CompileStatusBridgeCommandTests.cs b/Assets/Tests/Editor/CompileStatusBridgeCommandTests.cs index ba15d36ea9..5ad5886515 100644 --- a/Assets/Tests/Editor/CompileStatusBridgeCommandTests.cs +++ b/Assets/Tests/Editor/CompileStatusBridgeCommandTests.cs @@ -37,7 +37,7 @@ public void BuildResponse_WhenUnityIsIdleAndResultMatches_ReturnsReadyResult() _sessionStateService.StoreCompileResult( "compile_test_request", forceRecompile: false, - resultJson: "{\"success\":true}", + resultJson: "{\"Success\":true}", completedAtUtc: System.DateTime.UtcNow); GetCompileStatusResponse response = CompileStatusBridgeCommand.BuildResponse( @@ -50,7 +50,7 @@ public void BuildResponse_WhenUnityIsIdleAndResultMatches_ReturnsReadyResult() Assert.That(response.Ready, Is.True); Assert.That(response.HasResult, Is.True); Assert.That(response.Result, Is.Not.Null); - Assert.That(response.Result["success"]?.Value(), Is.True); + Assert.That(response.Result["Success"]?.Value(), Is.True); } [Test] @@ -60,7 +60,7 @@ public void BuildResponse_WhenUnityIsStillCompiling_ReturnsNotReadyWithStoredRes _sessionStateService.StoreCompileResult( "compile_test_request", forceRecompile: false, - resultJson: "{\"success\":true}", + resultJson: "{\"Success\":true}", completedAtUtc: System.DateTime.UtcNow); GetCompileStatusResponse response = CompileStatusBridgeCommand.BuildResponse( @@ -82,7 +82,7 @@ public void BuildResponse_WhenRequestIdDiffers_DoesNotReturnStaleResult() _sessionStateService.StoreCompileResult( "compile_old_request", forceRecompile: false, - resultJson: "{\"success\":true}", + resultJson: "{\"Success\":true}", completedAtUtc: System.DateTime.UtcNow); GetCompileStatusResponse response = CompileStatusBridgeCommand.BuildResponse( @@ -138,10 +138,10 @@ public void BuildResponse_WhenUnityIsIdleAndPendingRequestObservedReload_Returns Assert.That(response.Ready, Is.True); Assert.That(response.HasResult, Is.True); - Assert.That(response.Result["success"]?.Type, Is.EqualTo(JTokenType.Null)); - Assert.That(response.Result["errorCount"]?.Type, Is.EqualTo(JTokenType.Null)); - Assert.That(response.Result["warnings"]?.Type, Is.EqualTo(JTokenType.Null)); - Assert.That(response.Result["message"]?.ToString(), Does.Contain("reloaded scripts")); + Assert.That(response.Result["Success"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["ErrorCount"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["Warnings"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["Message"]?.ToString(), Does.Contain("reloaded scripts")); Assert.That(_sessionStateService.GetPendingCompileRequest().HasRequest, Is.False); } @@ -186,10 +186,10 @@ public void BuildResponse_WhenForceCompilePendingRequestHasNoResult_ReturnsExpla Assert.That(response.Ready, Is.True); Assert.That(response.HasResult, Is.True); - Assert.That(response.Result["success"]?.Type, Is.EqualTo(JTokenType.Null)); - Assert.That(response.Result["errorCount"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["Success"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["ErrorCount"]?.Type, Is.EqualTo(JTokenType.Null)); Assert.That( - response.Result["message"]?.ToString(), + response.Result["Message"]?.ToString(), Is.EqualTo(ForceCompileUnknownResult.MessageText)); } } diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs index 9ecc3b4384..d3a5ef52b7 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs @@ -40,6 +40,10 @@ public void ExecuteDynamicCodeResponse_WhenSerializedWithoutTimings_DoesNotExpos JObject serializedResponse = JObject.Parse( JsonConvert.SerializeObject(response, JsonRpcResponseSerializer.Settings)); + Assert.That(serializedResponse["Success"], Is.Not.Null); + Assert.That(serializedResponse["Result"], Is.Not.Null); + Assert.That(serializedResponse["success"], Is.Null); + Assert.That(serializedResponse["result"], Is.Null); Assert.That(serializedResponse["timings"], Is.Null); Assert.That(serializedResponse["emitTimingsInJsonResponse"], Is.Null); Assert.That(serializedResponse["emitsTimingsInJsonResponse"], Is.Null); @@ -48,9 +52,9 @@ public void ExecuteDynamicCodeResponse_WhenSerializedWithoutTimings_DoesNotExpos } [Test] - public void ExecuteDynamicCodeResponse_WhenSerializedWithInternalSignals_KeepsLegacyControlFieldNames() + public void ExecuteDynamicCodeResponse_WhenSerializedWithInternalSignals_KeepsControlFieldNames() { - // Tests that protocol-2 CLIs can still strip internal wait and timing fields from stdout. + // Tests that CLIs can still strip internal wait and timing fields from stdout. ExecuteDynamicCodeResponse response = new() { Success = true, @@ -63,10 +67,12 @@ public void ExecuteDynamicCodeResponse_WhenSerializedWithInternalSignals_KeepsLe JObject serializedResponse = JObject.Parse( JsonConvert.SerializeObject(response, JsonRpcResponseSerializer.Settings)); - Assert.That(serializedResponse["success"], Is.Not.Null); - Assert.That(serializedResponse["result"], Is.Not.Null); + Assert.That(serializedResponse["Success"], Is.Not.Null); + Assert.That(serializedResponse["Result"], Is.Not.Null); Assert.That(serializedResponse["DomainReloadWaitRequired"], Is.Not.Null); Assert.That(serializedResponse["Timings"], Is.Not.Null); + Assert.That(serializedResponse["success"], Is.Null); + Assert.That(serializedResponse["result"], Is.Null); Assert.That(serializedResponse["domainReloadWaitRequired"], Is.Null); Assert.That(serializedResponse["timings"], Is.Null); } diff --git a/Assets/Tests/Editor/FindGameObjectsToolTests.cs b/Assets/Tests/Editor/FindGameObjectsToolTests.cs index 19113db3a8..b2f4ea942e 100644 --- a/Assets/Tests/Editor/FindGameObjectsToolTests.cs +++ b/Assets/Tests/Editor/FindGameObjectsToolTests.cs @@ -62,12 +62,12 @@ public async Task ExecuteAsync_WithNamePattern_FindsMatchingObjects() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.results, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(2)); - Assert.That(response.results.Length, Is.EqualTo(2)); + Assert.That(response.Results, Is.Not.Null); + Assert.That(response.TotalFound, Is.EqualTo(2)); + Assert.That(response.Results.Length, Is.EqualTo(2)); // Check that both TestObject1 and TestObject2 are found - string[] foundNames = System.Array.ConvertAll(response.results, r => r.name); + string[] foundNames = System.Array.ConvertAll(response.Results, r => r.Name); Assert.That(foundNames, Does.Contain("TestObject1")); Assert.That(foundNames, Does.Contain("TestObject2")); Assert.That(foundNames, Does.Not.Contain("AnotherObject")); @@ -85,9 +85,9 @@ public async Task ExecuteAsync_WithEmptyParameters_ReturnsError() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(0)); - Assert.That(response.errorMessage, Is.Not.Null); - Assert.That(response.errorMessage, Does.Contain("At least one search criterion")); + Assert.That(response.TotalFound, Is.EqualTo(0)); + Assert.That(response.ErrorMessage, Is.Not.Null); + Assert.That(response.ErrorMessage, Does.Contain("At least one search criterion")); } [Test] @@ -108,9 +108,9 @@ public async Task ExecuteAsync_WithComponentSearch_FindsObjectsWithSpecificCompo // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.GreaterThanOrEqualTo(2)); // Scene might have other objects with BoxCollider + Assert.That(response.TotalFound, Is.GreaterThanOrEqualTo(2)); // Scene might have other objects with BoxCollider - string[] foundNames = System.Array.ConvertAll(response.results, r => r.name); + string[] foundNames = System.Array.ConvertAll(response.Results, r => r.Name); Assert.That(foundNames, Does.Contain("TestObject1")); Assert.That(foundNames, Does.Contain("AnotherObject")); Assert.That(foundNames, Does.Not.Contain("TestObject2")); @@ -135,12 +135,12 @@ public async Task ExecuteAsync_WithMultipleComponentSearch_FindsObjectsWithAllCo // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); - Assert.That(response.results[0].name, Is.EqualTo("TestObject1")); + Assert.That(response.TotalFound, Is.EqualTo(1)); + Assert.That(response.Results[0].Name, Is.EqualTo("TestObject1")); // Verify components are returned - ComponentInfo boxCollider = System.Array.Find(response.results[0].components, c => c.type == "BoxCollider"); - ComponentInfo rigidbody = System.Array.Find(response.results[0].components, c => c.type == "Rigidbody"); + ComponentInfo boxCollider = System.Array.Find(response.Results[0].Components, c => c.Type == "BoxCollider"); + ComponentInfo rigidbody = System.Array.Find(response.Results[0].Components, c => c.Type == "Rigidbody"); Assert.That(boxCollider, Is.Not.Null); Assert.That(rigidbody, Is.Not.Null); } @@ -167,19 +167,19 @@ public async Task ExecuteAsync_WithTagSearch_FindsObjectsWithSpecificTag() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.GreaterThanOrEqualTo(3)); // At least our 3 test objects + Assert.That(response.TotalFound, Is.GreaterThanOrEqualTo(3)); // At least our 3 test objects - string[] foundNames = System.Array.ConvertAll(response.results, r => r.name); + string[] foundNames = System.Array.ConvertAll(response.Results, r => r.Name); Assert.That(foundNames, Does.Contain("TestObject1")); Assert.That(foundNames, Does.Contain("TestObject2")); Assert.That(foundNames, Does.Contain("AnotherObject")); // Verify tag is returned in results - foreach (var result in response.results) + foreach (var result in response.Results) { - if (result.name == "TestObject1" || result.name == "TestObject2" || result.name == "AnotherObject") + if (result.Name == "TestObject1" || result.Name == "TestObject2" || result.Name == "AnotherObject") { - Assert.That(result.tag, Is.EqualTo("Untagged")); + Assert.That(result.Tag, Is.EqualTo("Untagged")); } } } @@ -203,15 +203,15 @@ public async Task ExecuteAsync_WithLayerSearch_FindsObjectsOnSpecificLayer() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(2)); + Assert.That(response.TotalFound, Is.EqualTo(2)); - string[] foundNames = System.Array.ConvertAll(response.results, r => r.name); + string[] foundNames = System.Array.ConvertAll(response.Results, r => r.Name); Assert.That(foundNames, Does.Contain("TestObject2")); Assert.That(foundNames, Does.Contain("AnotherObject")); Assert.That(foundNames, Does.Not.Contain("TestObject1")); // Verify layer is returned in results - Assert.That(response.results[0].layer, Is.EqualTo(enemyLayer)); + Assert.That(response.Results[0].Layer, Is.EqualTo(enemyLayer)); } [Test] @@ -235,9 +235,9 @@ public async Task ExecuteAsync_WithRegexSearch_FindsObjectsMatchingPattern() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.GreaterThanOrEqualTo(2)); + Assert.That(response.TotalFound, Is.GreaterThanOrEqualTo(2)); - string[] foundNames = System.Array.ConvertAll(response.results, r => r.name); + string[] foundNames = System.Array.ConvertAll(response.Results, r => r.Name); Assert.That(foundNames, Does.Contain("Enemy1")); Assert.That(foundNames, Does.Contain("Enemy2")); Assert.That(foundNames, Does.Not.Contain("Player1")); @@ -271,9 +271,9 @@ public async Task ExecuteAsync_WithIncludeInactive_FindsInactiveObjects() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(3)); // Should find all 3 objects including inactive + Assert.That(response.TotalFound, Is.EqualTo(3)); // Should find all 3 objects including inactive - string[] foundNames = System.Array.ConvertAll(response.results, r => r.name); + string[] foundNames = System.Array.ConvertAll(response.Results, r => r.Name); Assert.That(foundNames, Does.Contain("TestObject1")); Assert.That(foundNames, Does.Contain("TestObject2")); Assert.That(foundNames, Does.Contain("AnotherObject")); @@ -299,9 +299,9 @@ public async Task ExecuteAsync_WithoutIncludeInactive_ExcludesInactiveObjects() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); // Should only find active object - Assert.That(response.results[0].name, Is.EqualTo("TestObject1")); - Assert.That(response.results[0].isActive, Is.True); + Assert.That(response.TotalFound, Is.EqualTo(1)); // Should only find active object + Assert.That(response.Results[0].Name, Is.EqualTo("TestObject1")); + Assert.That(response.Results[0].IsActive, Is.True); } [Test] @@ -327,13 +327,13 @@ public async Task ExecuteAsync_WithComplexSearch_CombinesMultipleCriteria() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); // Only TestObject2 matches all criteria - Assert.That(response.results[0].name, Is.EqualTo("TestObject2")); + Assert.That(response.TotalFound, Is.EqualTo(1)); // Only TestObject2 matches all criteria + Assert.That(response.Results[0].Name, Is.EqualTo("TestObject2")); // Verify all criteria are met - ComponentInfo boxCollider = System.Array.Find(response.results[0].components, c => c.type == "BoxCollider"); + ComponentInfo boxCollider = System.Array.Find(response.Results[0].Components, c => c.Type == "BoxCollider"); Assert.That(boxCollider, Is.Not.Null); - Assert.That(response.results[0].layer, Is.EqualTo(8)); + Assert.That(response.Results[0].Layer, Is.EqualTo(8)); } [Test] @@ -361,13 +361,13 @@ public async Task ExecuteAsync_WithMaxResults_LimitsReturnedObjects() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.results.Length, Is.EqualTo(5)); // Should be limited to 5 - Assert.That(response.totalFound, Is.EqualTo(5)); // Total found should also be 5 + Assert.That(response.Results.Length, Is.EqualTo(5)); // Should be limited to 5 + Assert.That(response.TotalFound, Is.EqualTo(5)); // Total found should also be 5 // Verify all results match the pattern - foreach (var result in response.results) + foreach (var result in response.Results) { - Assert.That(result.name, Does.StartWith("ManyObject")); + Assert.That(result.Name, Does.StartWith("ManyObject")); } } finally @@ -403,9 +403,9 @@ public async Task ExecuteAsync_WithPathSearchMode_FindsObjectByHierarchyPath() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); - Assert.That(response.results[0].name, Is.EqualTo("Grandchild")); - Assert.That(response.results[0].path, Is.EqualTo("Parent/Child/Grandchild")); + Assert.That(response.TotalFound, Is.EqualTo(1)); + Assert.That(response.Results[0].Name, Is.EqualTo("Grandchild")); + Assert.That(response.Results[0].Path, Is.EqualTo("Parent/Child/Grandchild")); } finally { @@ -435,8 +435,8 @@ public async Task ExecuteAsync_WithExactSearchMode_FindsExactNameMatch() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); - Assert.That(response.results[0].name, Is.EqualTo("ExactName")); + Assert.That(response.TotalFound, Is.EqualTo(1)); + Assert.That(response.Results[0].Name, Is.EqualTo("ExactName")); } finally { @@ -468,9 +468,9 @@ public async Task ExecuteAsync_WithContainsSearchMode_FindsPartialMatch() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(4)); // Includes SetUp objects (TestObject1, TestObject2) + Assert.That(response.TotalFound, Is.EqualTo(4)); // Includes SetUp objects (TestObject1, TestObject2) - string[] foundNames = System.Array.ConvertAll(response.results, r => r.name); + string[] foundNames = System.Array.ConvertAll(response.Results, r => r.Name); Assert.That(foundNames, Does.Contain("TestObjectOne")); Assert.That(foundNames, Does.Contain("AnotherTestObjectTwo")); Assert.That(foundNames, Does.Not.Contain("DifferentName")); @@ -500,9 +500,9 @@ public async Task ExecuteAsync_WithSelectedMode_NoSelection_ReturnsEmptyResult() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(0)); - Assert.That(response.results, Is.Empty); - Assert.That(response.message, Does.Contain("No GameObjects")); + Assert.That(response.TotalFound, Is.EqualTo(0)); + Assert.That(response.Results, Is.Empty); + Assert.That(response.Message, Does.Contain("No GameObjects")); } [Test] @@ -524,11 +524,11 @@ public async Task ExecuteAsync_WithSelectedMode_SingleSelection_ReturnsJsonDirec // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); - Assert.That(response.results, Is.Not.Null); - Assert.That(response.results.Length, Is.EqualTo(1)); - Assert.That(response.results[0].name, Is.EqualTo("TestObject1")); - Assert.That(response.resultsFilePath, Is.Null); + Assert.That(response.TotalFound, Is.EqualTo(1)); + Assert.That(response.Results, Is.Not.Null); + Assert.That(response.Results.Length, Is.EqualTo(1)); + Assert.That(response.Results[0].Name, Is.EqualTo("TestObject1")); + Assert.That(response.ResultsFilePath, Is.Null); } finally { @@ -554,13 +554,13 @@ public async Task ExecuteAsync_WithSelectedMode_MultipleSelection_ExportsToFile( // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(2)); - Assert.That(response.resultsFilePath, Is.Not.Null); - Assert.That(response.resultsFilePath, Does.Contain("FindGameObjectsResults")); - Assert.That(response.message, Does.Contain("Multiple objects selected")); + Assert.That(response.TotalFound, Is.EqualTo(2)); + Assert.That(response.ResultsFilePath, Is.Not.Null); + Assert.That(response.ResultsFilePath, Does.Contain("FindGameObjectsResults")); + Assert.That(response.Message, Does.Contain("Multiple objects selected")); // Verify file exists - string fullPath = Path.Combine(UnityEngine.Application.dataPath, "..", response.resultsFilePath); + string fullPath = Path.Combine(UnityEngine.Application.dataPath, "..", response.ResultsFilePath); Assert.That(File.Exists(fullPath), Is.True, $"Export file should exist at {fullPath}"); // Cleanup exported file @@ -596,10 +596,10 @@ public async Task ExecuteAsync_WithSelectedMode_IncludeInactiveFalse_ExcludesIna // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); - Assert.That(response.results, Is.Not.Null); - Assert.That(response.results.Length, Is.EqualTo(1)); - Assert.That(response.results[0].name, Is.EqualTo("TestObject1")); + Assert.That(response.TotalFound, Is.EqualTo(1)); + Assert.That(response.Results, Is.Not.Null); + Assert.That(response.Results.Length, Is.EqualTo(1)); + Assert.That(response.Results[0].Name, Is.EqualTo("TestObject1")); } finally { @@ -630,21 +630,21 @@ public async Task ExecuteAsync_ReturnsObjectReferenceProperties() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); + Assert.That(response.TotalFound, Is.EqualTo(1)); ComponentInfo meshRenderer = System.Array.Find( - response.results[0].components, c => c.type == "MeshRenderer"); + response.Results[0].Components, c => c.Type == "MeshRenderer"); Assert.That(meshRenderer, Is.Not.Null); ComponentPropertyInfo probeAnchor = System.Array.Find( - meshRenderer.properties, p => p.name == "Probe Anchor"); + meshRenderer.Properties, p => p.Name == "Probe Anchor"); Assert.That(probeAnchor, Is.Not.Null, "MeshRenderer should have Probe Anchor property"); - Assert.That(probeAnchor.type, Is.EqualTo("ObjectReference")); + Assert.That(probeAnchor.Type, Is.EqualTo("ObjectReference")); string expectedEntityId = GetExpectedObjectId(anchorTarget.transform); // Value should be a structured object with name, type, entityId - JObject valueObj = JObject.FromObject(probeAnchor.value); + JObject valueObj = JObject.FromObject(probeAnchor.Value); Assert.That(valueObj["name"].ToString(), Is.EqualTo("AnchorTarget")); Assert.That(valueObj["type"].ToString(), Is.EqualTo("Transform")); Assert.That(valueObj["entityId"].ToString(), Is.EqualTo(expectedEntityId)); @@ -672,17 +672,17 @@ public async Task ExecuteAsync_ReturnsNoneForUnsetObjectReference() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); + Assert.That(response.TotalFound, Is.EqualTo(1)); ComponentInfo meshRenderer = System.Array.Find( - response.results[0].components, c => c.type == "MeshRenderer"); + response.Results[0].Components, c => c.Type == "MeshRenderer"); Assert.That(meshRenderer, Is.Not.Null); ComponentPropertyInfo probeAnchor = System.Array.Find( - meshRenderer.properties, p => p.name == "Probe Anchor"); + meshRenderer.Properties, p => p.Name == "Probe Anchor"); Assert.That(probeAnchor, Is.Not.Null, "MeshRenderer should have Probe Anchor property"); - JObject valueObj = JObject.FromObject(probeAnchor.value); + JObject valueObj = JObject.FromObject(probeAnchor.Value); Assert.That(valueObj["name"].ToString(), Is.EqualTo("None")); Assert.That(valueObj["type"].ToString(), Is.EqualTo("None")); Assert.That(valueObj["entityId"].ToString(), Is.EqualTo("0")); @@ -721,11 +721,11 @@ public async Task ExecuteAsync_WithSelectedMode_IncludeInactiveTrue_IncludesInac // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(2)); - Assert.That(response.resultsFilePath, Is.Not.Null); // Multiple selection exports to file + Assert.That(response.TotalFound, Is.EqualTo(2)); + Assert.That(response.ResultsFilePath, Is.Not.Null); // Multiple selection exports to file // Cleanup exported file - string fullPath = Path.Combine(UnityEngine.Application.dataPath, "..", response.resultsFilePath); + string fullPath = Path.Combine(UnityEngine.Application.dataPath, "..", response.ResultsFilePath); if (File.Exists(fullPath)) { File.Delete(fullPath); diff --git a/Assets/Tests/Editor/GetHierarchyToolTests.cs b/Assets/Tests/Editor/GetHierarchyToolTests.cs index 1dde4030ed..967420ab0a 100644 --- a/Assets/Tests/Editor/GetHierarchyToolTests.cs +++ b/Assets/Tests/Editor/GetHierarchyToolTests.cs @@ -38,9 +38,9 @@ public async Task ExecuteAsync_WithDefaultParameters_ReturnsHierarchyExport() GetHierarchyResponse response = baseResponse as GetHierarchyResponse; Assert.That(response, Is.Not.Null); - Assert.That(response.hierarchyFilePath, Is.Not.Empty); - Assert.That(response.message, Does.Contain("Hierarchy data saved")); - DeleteExportedFile(response.hierarchyFilePath); + Assert.That(response.HierarchyFilePath, Is.Not.Empty); + Assert.That(response.Message, Does.Contain("Hierarchy data saved")); + DeleteExportedFile(response.HierarchyFilePath); } [Test] @@ -55,7 +55,7 @@ public async Task ExecuteAsync_WithMaxDepthParameter_MapsRequest() GetHierarchyResponse response = baseResponse as GetHierarchyResponse; Assert.That(response, Is.Not.Null); - DeleteExportedFile(response.hierarchyFilePath); + DeleteExportedFile(response.HierarchyFilePath); } [Test] @@ -70,7 +70,7 @@ public async Task ExecuteAsync_WithIncludeComponentsFalse_MapsRequest() GetHierarchyResponse response = baseResponse as GetHierarchyResponse; Assert.That(response, Is.Not.Null); - DeleteExportedFile(response.hierarchyFilePath); + DeleteExportedFile(response.HierarchyFilePath); } [Test] diff --git a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs index a99beb808e..b319b77362 100644 --- a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs +++ b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs @@ -33,8 +33,8 @@ public async Task ProcessRequest_WhenProtocolVersionMatches_AllowsRequest() Assert.That(parsed["error"], Is.Null); Assert.That(parsed["result"], Is.Not.Null); - Assert.That(parsed["result"]?["unityVersion"]?.ToString(), Is.Not.Null.And.Not.Empty); - Assert.That(parsed["result"]?["UnityVersion"], Is.Null); + Assert.That(parsed["result"]?["UnityVersion"]?.ToString(), Is.Not.Null.And.Not.Empty); + Assert.That(parsed["result"]?["unityVersion"], Is.Null); } [Test] @@ -479,7 +479,7 @@ public async Task ProcessRequest_AfterGetHierarchyReturns_AllowsImmediateGetLogs JObject parsedHierarchy = JObject.Parse(hierarchyResponse); Assert.That(parsedHierarchy["error"], Is.Null); - Assert.That(parsedHierarchy["result"]?["hierarchyFilePath"]?.ToString(), Is.Not.Empty); + Assert.That(parsedHierarchy["result"]?["HierarchyFilePath"]?.ToString(), Is.Not.Empty); logsResponseTask = JsonRpcProcessor.ProcessRequestWithEarlyResponseAsync( BuildToolRequestWithParams( @@ -495,7 +495,7 @@ public async Task ProcessRequest_AfterGetHierarchyReturns_AllowsImmediateGetLogs JObject parsedLogs = JObject.Parse(logsResponse); Assert.That(parsedLogs["error"], Is.Null); - Assert.That(parsedLogs["result"]?["displayedCount"]?.ToObject(), Is.EqualTo(0)); + Assert.That(parsedLogs["result"]?["DisplayedCount"]?.ToObject(), Is.EqualTo(0)); } finally { @@ -697,7 +697,7 @@ private static void DeleteHierarchyFileFromResponse(string response) } JObject parsed = JObject.Parse(response); - string relativePath = parsed["result"]?["hierarchyFilePath"]?.ToString(); + string relativePath = parsed["result"]?["HierarchyFilePath"]?.ToString(); if (string.IsNullOrEmpty(relativePath)) { return; diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs index 6e09233f74..4729e438f5 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs @@ -190,6 +190,7 @@ public void GetMigrationSkillPromptText_ReturnsActionablePrompt() Assert.That(prompt, Does.Contain("v3-cli-invocation-migration")); Assert.That(prompt, Does.Contain("SKILL.md, Markdown, POSIX shell scripts, and PowerShell scripts")); Assert.That(prompt, Does.Contain("Do not change C# snippets")); + Assert.That(prompt, Does.Not.Contain("JSON output fields")); Assert.That(prompt, Does.Contain("summarize changed files")); } diff --git a/Assets/Tests/Editor/ToolSkillSynchronizerTests.cs b/Assets/Tests/Editor/ToolSkillSynchronizerTests.cs index acbb2383b0..3089bf6652 100644 --- a/Assets/Tests/Editor/ToolSkillSynchronizerTests.cs +++ b/Assets/Tests/Editor/ToolSkillSynchronizerTests.cs @@ -1712,6 +1712,32 @@ await ToolSkillSynchronizer.InstallV3MigrationSkillFilesAtProjectRoot( Assert.That(installState, Is.EqualTo(SkillInstallState.Installed)); } + [Test] + public void GetV3MigrationSkillInstallStateAtProjectRoot_WhenSkillDirectoryLacksSkillFile_ReturnsMissing() + { + // Tests that a leftover migration skill directory without SKILL.md is not treated as installed. + string temporaryRoot = CreateTemporaryProjectRoot(); + ToolSkillSynchronizer.SkillTargetInfo target = new( + "Codex CLI", + ".codex", + "--codex", + hasSkillsDirectory: true, + hasExistingSkills: false); + string targetRoot = Path.Combine(temporaryRoot, ".codex"); + string migrationSkillDir = SkillInstallLayout.GetInstalledSkillDirectoryPathForLayout( + targetRoot, + CliConstants.V3_CLI_INVOCATION_MIGRATION_SKILL_NAME, + groupSkillsUnderUnityCliLoop: false); + Directory.CreateDirectory(Path.Combine(migrationSkillDir, "references")); + + SkillInstallState installState = ToolSkillSynchronizer.GetV3MigrationSkillInstallStateAtProjectRoot( + temporaryRoot, + target, + groupSkillsUnderUnityCliLoop: false); + + Assert.That(installState, Is.EqualTo(SkillInstallState.Missing)); + } + [Test] public async Task RemoveSpecificSkillFilesAtProjectRoot_WhenSkillExists_RemovesOnlyThatSkill() { diff --git a/Packages/src/Editor/CliOnlyTools~/FocusWindow/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/FocusWindow/Skill/SKILL.md index e2ca7a23bb..ead80195ae 100644 --- a/Packages/src/Editor/CliOnlyTools~/FocusWindow/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/FocusWindow/Skill/SKILL.md @@ -29,8 +29,9 @@ uloop focus-window ## Output Returns JSON with: -- `success`: Whether the focus operation succeeded -- `message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) + +- `Success`: Whether the focus operation succeeded +- `Message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) ## Notes diff --git a/Packages/src/Editor/CliOnlyTools~/GetProjectInfo/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/GetProjectInfo/Skill/SKILL.md index 6f5987df53..aeab55f947 100644 --- a/Packages/src/Editor/CliOnlyTools~/GetProjectInfo/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/GetProjectInfo/Skill/SKILL.md @@ -28,6 +28,7 @@ uloop get-project-info ## Output Returns JSON with project information: + - Unity version - Project name - Platform settings diff --git a/Packages/src/Editor/CliOnlyTools~/GetVersion/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/GetVersion/Skill/SKILL.md index 78e43a6ee8..2ded5514dc 100644 --- a/Packages/src/Editor/CliOnlyTools~/GetVersion/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/GetVersion/Skill/SKILL.md @@ -21,6 +21,7 @@ None. ## Output Returns JSON with: + - `UnityVersion`: Unity Editor version - `Platform`: Current platform - `DataPath`: Assets folder path diff --git a/Packages/src/Editor/CliOnlyTools~/Launch/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/Launch/Skill/SKILL.md index 7bbee3d3e5..82aadc69d6 100644 --- a/Packages/src/Editor/CliOnlyTools~/Launch/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/Launch/Skill/SKILL.md @@ -50,18 +50,19 @@ uloop launch --quit May print status/progress lines before the final JSON payload, such as project path, detected Unity version, or readiness wait messages. The final JSON payload includes: -- `success`: whether the command completed -- `ready`: whether Unity CLI Loop is ready for commands -- `serverReady`: whether the Unity CLI Loop server accepted requests -- `projectIpcReady`: whether the project IPC path accepted tool requests -- `alreadyRunning`: whether an existing Unity process was reused -- `launched`: whether this command launched a Unity process -- `restarted`: whether this command stopped an existing process and launched a new one -- `quit`: whether this command stopped Unity without launching a new process -- `previousProcessId`: process ID stopped by restart or quit, when available -- `currentProcessId`: current Unity process ID, when available -- `projectRoot`: resolved project root -- `message`: readiness summary + +- `Success`: whether the command completed +- `Ready`: whether Unity CLI Loop is ready for commands +- `ServerReady`: whether the Unity CLI Loop server accepted requests +- `ProjectIpcReady`: whether the project IPC path accepted tool requests +- `AlreadyRunning`: whether an existing Unity process was reused +- `Launched`: whether this command launched a Unity process +- `Restarted`: whether this command stopped an existing process and launched a new one +- `Quit`: whether this command stopped Unity without launching a new process +- `PreviousProcessId`: process ID stopped by restart or quit, when available +- `CurrentProcessId`: current Unity process ID, when available +- `ProjectRoot`: resolved project root +- `Message`: readiness summary ## Notes diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md index 14699e0deb..a0055d4f47 100644 --- a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md @@ -3,6 +3,8 @@ name: uloop-wait-for-pause-point description: "Pauses Unity's playback and allows you to inspect specific frames. Use this for bug investigation or PlayMode/E2E testing. It's the most convenient yet rigorous method for verifying variable states at specific frames or confirming whether particular code has been executed." --- +# uloop wait-for-pause-point + ## Quick Check Template Use this small loop for one representative frame you care about: @@ -19,27 +21,27 @@ Debug.Log($"state-transition-applied localValue={localValue} reason={reason}"); UloopPausePoint.Pause("state-transition-applied"); ``` -2. Compile, enter PlayMode, then enable the marker: +1. Compile, enter PlayMode, then enable the marker: ```bash uloop enable-pause-point --id state-transition-applied --timeout-seconds 30 ``` -3. Trigger the action with a `simulate-*` command. -4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `interruptedByPausePoint=true`: +1. Trigger the action with a `simulate-*` command. +2. Wait for the marker and read the focused log in one call, even if the trigger command already returned `InterruptedByPausePoint=true`: ```bash uloop wait-for-pause-point --id state-transition-applied --timeout-seconds 30 ``` -The hit response always embeds the log entries matching the marker id as `matchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `matchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. +The hit response always embeds the log entries matching the marker id as `MatchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `MatchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. -Read `evidenceSummary` first when it is present. It groups `editorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `editorState.capturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. +Read `EvidenceSummary` first when it is present. It groups `EditorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `EditorState.CapturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. -Use `generation`, `enabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `remainingMilliseconds` and `expired` are returned directly so you do not need to infer marker lifetime from elapsed time. +Use `Generation`, `EnabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `RemainingMilliseconds` and `Expired` are returned directly so you do not need to infer marker lifetime from elapsed time. -5. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. -6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `editorState` describes Unity Editor play/pause state, not marker state. +1. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. +2. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `EditorState` describes Unity Editor play/pause state, not marker state. ## When To Use @@ -48,12 +50,12 @@ Use `generation`, `enabledAtUtc`, and the hit sequence fields from the hit or st - Use this before reaching for `Time.timeScale`, sleeps, repeated polling, or after-the-fact `execute-dynamic-code`; those checks can supplement the paused-frame proof, but they are not substitutes. - If the value you need is a method local, an intermediate calculation, or a branch reason that `execute-dynamic-code` cannot reach, add a focused `Debug.Log` immediately before the marker and read it with `get-logs` while paused. Do not count the breakpoint check complete until the matching log has been read. - Treat the pause like a lightweight breakpoint for one important transition: combine nearby debug logs with paused-frame inspection to confirm the variables and component state at that point. -- Do not treat `simulate-* success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. +- Do not treat `simulate-* Success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. - Skip this only for ordinary persistent-state checks when you are not validating simulated input delivery, event ordering, or transition-frame fidelity. ## Timeout Checks -If this command times out, the marker line was not reached while the command waited. Read `error.details.hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `error.details.recommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `id` and `timeoutSeconds`. Then inspect `error.details.status`, `hitCount`, `generation`, `enabledAtUtc`, `editorState`, `elapsedSinceEnabledMilliseconds`, and `remainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `error.details.matchingLogs` and `error.details.evidenceSummary` show whether the marker's focused log ever appeared. `elapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. +If this command times out, the marker line was not reached while the command waited. Read `Error.Details.Hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `Error.Details.RecommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `Id` and `TimeoutSeconds`. Then inspect `Error.Details.Status`, `HitCount`, `Generation`, `EnabledAtUtc`, `EditorState`, `ElapsedSinceEnabledMilliseconds`, and `RemainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `Error.Details.MatchingLogs` and `Error.Details.EvidenceSummary` show whether the marker's focused log ever appeared. `ElapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. Use `uloop pause-point-status --id state-transition-applied` only when you need to confirm the marker is armed or inspect the current hit state. Add focused debug logs before the marker when local variables must be captured. diff --git a/Packages/src/Editor/FirstPartyTools/ClearConsole/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ClearConsole/Skill/SKILL.md index 3cb9e2be3f..8f34706efa 100644 --- a/Packages/src/Editor/FirstPartyTools/ClearConsole/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ClearConsole/Skill/SKILL.md @@ -39,10 +39,11 @@ uloop clear-console --add-confirmation-message ## Output Returns JSON with: -- `success` (boolean): Whether the clear operation succeeded -- `clearedLogCount` (number): Total number of log entries that were cleared -- `clearedCounts` (object): Breakdown by log type - - `errorCount` (number): Errors cleared - - `warningCount` (number): Warnings cleared - - `logCount` (number): Info logs cleared -- `message` (string): Description of the result. On failure, this field carries the error summary. + +- `Success` (boolean): Whether the clear operation succeeded +- `ClearedLogCount` (number): Total number of log entries that were cleared +- `ClearedCounts` (object): Breakdown by log type + - `ErrorCount` (number): Errors cleared + - `WarningCount` (number): Warnings cleared + - `LogCount` (number): Info logs cleared +- `Message` (string): Description of the result. On failure, this field carries the error summary. diff --git a/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md index 9a2780a2a1..3eeb6bfe4e 100644 --- a/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md @@ -44,7 +44,8 @@ uloop compile --stop-on-external-scene-changes ## Output Returns JSON: -- `success`: boolean or null -- `errorCount`: number or null -- `warningCount`: number or null -- `message`: string + +- `Success`: boolean or null +- `ErrorCount`: number or null +- `WarningCount`: number or null +- `Message`: string diff --git a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md index bfdb9e9bc9..fc85b7df71 100644 --- a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md @@ -49,14 +49,15 @@ uloop control-play-mode --action Step ## Output Returns JSON with the current play mode state: -- `isPlaying`: Whether Unity is currently in play mode -- `isPaused`: Whether play mode is paused -- `changed`: Whether the requested action changed the current play mode state -- `wasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped -- `message`: Description of the action performed + +- `IsPlaying`: Whether Unity is currently in play mode +- `IsPaused`: Whether play mode is paused +- `Changed`: Whether the requested action changed the current play mode state +- `WasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped +- `Message`: Description of the action performed ## Notes -- Stop on an already-stopped Editor sets `changed: false`, `wasAlreadyStopped: true` +- Stop on an already-stopped Editor sets `Changed: false`, `WasAlreadyStopped: true` - Step is independent of `Time.timeScale`; requires PlayMode to be running - The command waits for the requested state before returning. Increase `--timeout-seconds` for projects with slow PlayMode entry. diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md index 1b2c08ebf9..7fc716edb2 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md @@ -43,16 +43,17 @@ Prefer terminal commands for file operations and keep snippets focused on Unity ## Output Returns JSON: -- `success`: boolean — overall execution success -- `result`: string — value of the snippet's `return` statement (empty when omitted) -- `logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` -- `compilationErrors`: object[] — Roslyn diagnostics with `message`, `line`, `column`, `errorCode`, optional `hint` and `suggestions` -- `error` / `errorMessage`: string — top-level failure summary (empty on success) -- `updatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) -- `diagnosticsSummary`: string|null — compact summary when diagnostics are available -- `diagnostics`: object[] — structured diagnostics; same shape as `compilationErrors`, usually populated together with it - -On `success: false`, inspect `compilationErrors` first. If empty, read `errorMessage` (and `logs` for extra context) — the failure may be a runtime exception, cancellation, or an "execution in progress" rejection, all of which return empty `compilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. + +- `Success`: boolean — overall execution success +- `Result`: string — value of the snippet's `return` statement (empty when omitted) +- `Logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` +- `CompilationErrors`: object[] — Roslyn diagnostics with `Message`, `Line`, `Column`, `ErrorCode`, optional `Hint` and `Suggestions` +- `Error` / `ErrorMessage`: string — top-level failure summary (empty on success) +- `UpdatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) +- `DiagnosticsSummary`: string|null — compact summary when diagnostics are available +- `Diagnostics`: object[] — structured diagnostics; same shape as `CompilationErrors`, usually populated together with it + +On `Success: false`, inspect `CompilationErrors` first. If empty, read `ErrorMessage` (and `Logs` for extra context) — the failure may be a runtime exception, cancellation, or an "execution in progress" rejection, all of which return empty `CompilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. ## Code Examples by Category diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/ComponentInfo.cs b/Packages/src/Editor/FirstPartyTools/FindGameObjects/ComponentInfo.cs index c92cdfecea..53a9bfbaa0 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/ComponentInfo.cs +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/ComponentInfo.cs @@ -1,4 +1,3 @@ - namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { /// @@ -6,9 +5,11 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// public class ComponentInfo { - public string type { get; set; } - public string fullTypeName { get; set; } - public ComponentPropertyInfo[] properties { get; set; } + public string Type { get; set; } + + public string FullTypeName { get; set; } + + public ComponentPropertyInfo[] Properties { get; set; } } /// @@ -16,8 +17,10 @@ public class ComponentInfo /// public class ComponentPropertyInfo { - public string name { get; set; } - public string type { get; set; } - public object value { get; set; } + public string Name { get; set; } + + public string Type { get; set; } + + public object Value { get; set; } } -} \ No newline at end of file +} diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsResponse.cs b/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsResponse.cs index 71aa8a215e..8598207047 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsResponse.cs +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsResponse.cs @@ -7,16 +7,19 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// public class FindGameObjectsResponse : UnityCliLoopToolResponse { - public FindGameObjectResult[] results { get; set; } - public int totalFound { get; set; } - public string errorMessage { get; set; } + public FindGameObjectResult[] Results { get; set; } + + public int TotalFound { get; set; } + + public string ErrorMessage { get; set; } // For multiple selection file output - public string resultsFilePath { get; set; } - public string message { get; set; } + public string ResultsFilePath { get; set; } + + public string Message { get; set; } // Processing errors for objects that failed to serialize - public ProcessingError[] processingErrors { get; set; } + public ProcessingError[] ProcessingErrors { get; set; } } /// @@ -24,12 +27,17 @@ public class FindGameObjectsResponse : UnityCliLoopToolResponse /// public class FindGameObjectResult { - public string name { get; set; } - public string path { get; set; } - public bool isActive { get; set; } - public string tag { get; set; } - public int layer { get; set; } - public ComponentInfo[] components { get; set; } + public string Name { get; set; } + + public string Path { get; set; } + + public bool IsActive { get; set; } + + public string Tag { get; set; } + + public int Layer { get; set; } + + public ComponentInfo[] Components { get; set; } } /// @@ -37,8 +45,10 @@ public class FindGameObjectResult /// public class ProcessingError { - public string gameObjectName { get; set; } - public string gameObjectPath { get; set; } - public string error { get; set; } + public string GameObjectName { get; set; } + + public string GameObjectPath { get; set; } + + public string Error { get; set; } } -} \ No newline at end of file +} diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsTool.cs b/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsTool.cs index e713710b34..52ba54454a 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsTool.cs +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsTool.cs @@ -49,12 +49,12 @@ private static FindGameObjectsResponse ToResponse(UnityCliLoopGameObjectSearchRe return new FindGameObjectsResponse { - results = ToResults(result.Results), - totalFound = result.TotalFound, - errorMessage = result.ErrorMessage, - resultsFilePath = result.ResultsFilePath, - message = result.Message, - processingErrors = ToProcessingErrors(result.ProcessingErrors), + Results = ToResults(result.Results), + TotalFound = result.TotalFound, + ErrorMessage = result.ErrorMessage, + ResultsFilePath = result.ResultsFilePath, + Message = result.Message, + ProcessingErrors = ToProcessingErrors(result.ProcessingErrors), }; } @@ -71,12 +71,12 @@ private static FindGameObjectResult[] ToResults(UnityCliLoopGameObjectResult[] r UnityCliLoopGameObjectResult result = results[i]; mappedResults[i] = new FindGameObjectResult { - name = result.Name, - path = result.Path, - isActive = result.IsActive, - tag = result.Tag, - layer = result.Layer, - components = result.Components, + Name = result.Name, + Path = result.Path, + IsActive = result.IsActive, + Tag = result.Tag, + Layer = result.Layer, + Components = result.Components, }; } @@ -96,9 +96,9 @@ private static ProcessingError[] ToProcessingErrors(UnityCliLoopGameObjectProces UnityCliLoopGameObjectProcessingError error = errors[i]; mappedErrors[i] = new ProcessingError { - gameObjectName = error.GameObjectName, - gameObjectPath = error.GameObjectPath, - error = error.Error, + GameObjectName = error.GameObjectName, + GameObjectPath = error.GameObjectPath, + Error = error.Error, }; } diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentPropertySerializer.cs b/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentPropertySerializer.cs index 3582964113..a4919e17e7 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentPropertySerializer.cs +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentPropertySerializer.cs @@ -38,9 +38,9 @@ public ComponentPropertyInfo[] SerializeProperties(Component component) if (value != null) { ComponentPropertyInfo info = new() { - name = iterator.displayName, - type = iterator.propertyType.ToString(), - value = SerializeValue(value) + Name = iterator.displayName, + Type = iterator.propertyType.ToString(), + Value = SerializeValue(value) }; propertyInfos.Add(info); diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentSerializer.cs b/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentSerializer.cs index 0e8b0fb7ef..d7c68bf920 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentSerializer.cs +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentSerializer.cs @@ -35,9 +35,9 @@ public ComponentInfo[] SerializeComponents(GameObject gameObject) ComponentPropertyInfo[] properties = _propertySerializer.SerializeProperties(component); ComponentInfo info = new() { - type = componentTypeName, - fullTypeName = component.GetType().FullName, - properties = properties + Type = componentTypeName, + FullTypeName = component.GetType().FullName, + Properties = properties }; componentInfos.Add(info); @@ -46,4 +46,4 @@ public ComponentInfo[] SerializeComponents(GameObject gameObject) return componentInfos.ToArray(); } } -} \ No newline at end of file +} diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/FindGameObjects/Skill/SKILL.md index 24edd909a3..cd92c9963e 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/Skill/SKILL.md @@ -70,21 +70,23 @@ uloop find-game-objects --search-mode Selected --include-inactive ## Output Returns JSON with: -- `results` (array): Matching GameObjects, each containing: - - `name` (string): GameObject name - - `path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) - - `isActive` (boolean): Active state in hierarchy - - `tag` (string): GameObject tag - - `layer` (number): Layer index - - `components` (array): Each entry has `type` (short name, e.g., `Rigidbody`), `fullTypeName` (e.g., `UnityEngine.Rigidbody`), and `properties` (array of Inspector-visible `{name, type, value}` pairs) -- `totalFound` (number): Results returned (after `--max-results` clipping). For multi-selection file export, this is the number exported. -- `errorMessage` (string): Top-level failure summary (empty on success) -- `processingErrors` (array): Selected-mode per-GameObject serialization failures, each `{gameObjectName, gameObjectPath, error}`. Omitted/null or empty on clean runs. + +- `Results` (array): Matching GameObjects, each containing: + - `Name` (string): GameObject name + - `Path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) + - `IsActive` (boolean): Active state in hierarchy + - `Tag` (string): GameObject tag + - `Layer` (number): Layer index + - `Components` (array): Each entry has `Type` (short name, e.g., `Rigidbody`), `FullTypeName` (e.g., `UnityEngine.Rigidbody`), and `Properties` (array of Inspector-visible `{Name, Type, Value}` pairs) +- `TotalFound` (number): Results returned (after `--max-results` clipping). For multi-selection file export, this is the number exported. +- `ErrorMessage` (string): Top-level failure summary (empty on success) +- `ProcessingErrors` (array): Selected-mode per-GameObject serialization failures, each `{GameObjectName, GameObjectPath, Error}`. Omitted/null or empty on clean runs. ### Multi-selection file export -For `Selected` mode with **multiple** successfully serialized GameObjects, inline `results` is not populated and the data is written to a file instead. Two extra fields appear: -- `resultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` -- `message` (string): Human-readable summary (e.g., "5 GameObjects exported") +For `Selected` mode with **multiple** successfully serialized GameObjects, inline `Results` is not populated and the data is written to a file instead. Two extra fields appear: + +- `ResultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` +- `Message` (string): Human-readable summary (e.g., "5 GameObjects exported") -Single-selection and search-mode calls (`Exact`, `Path`, `Regex`, `Contains`) always return inline. No selection (`Selected` mode with empty selection) returns empty `results` plus a `message`. +Single-selection and search-mode calls (`Exact`, `Path`, `Regex`, `Contains`) always return inline. No selection (`Selected` mode with empty selection) returns empty `Results` plus a `Message`. diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/UnityCliLoopGameObjectSearchTypes.cs b/Packages/src/Editor/FirstPartyTools/FindGameObjects/UnityCliLoopGameObjectSearchTypes.cs index 0f84b43ab1..c5e80762c7 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/UnityCliLoopGameObjectSearchTypes.cs +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/UnityCliLoopGameObjectSearchTypes.cs @@ -1,6 +1,5 @@ using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { @@ -56,22 +55,16 @@ public sealed class UnityCliLoopGameObjectSearchResult /// public sealed class UnityCliLoopGameObjectResult { - [JsonProperty("name")] public string Name { get; set; } - [JsonProperty("path")] public string Path { get; set; } - [JsonProperty("isActive")] public bool IsActive { get; set; } - [JsonProperty("tag")] public string Tag { get; set; } - [JsonProperty("layer")] public int Layer { get; set; } - [JsonProperty("components")] public ComponentInfo[] Components { get; set; } } @@ -80,13 +73,10 @@ public sealed class UnityCliLoopGameObjectResult /// public sealed class UnityCliLoopGameObjectProcessingError { - [JsonProperty("gameObjectName")] public string GameObjectName { get; set; } - [JsonProperty("gameObjectPath")] public string GameObjectPath { get; set; } - [JsonProperty("error")] public string Error { get; set; } } } diff --git a/Packages/src/Editor/FirstPartyTools/GetHierarchy/GetHierarchyResponse.cs b/Packages/src/Editor/FirstPartyTools/GetHierarchy/GetHierarchyResponse.cs index c92140ed75..ae17d81c7b 100644 --- a/Packages/src/Editor/FirstPartyTools/GetHierarchy/GetHierarchyResponse.cs +++ b/Packages/src/Editor/FirstPartyTools/GetHierarchy/GetHierarchyResponse.cs @@ -17,20 +17,20 @@ public class GetHierarchyResponse : UnityCliLoopToolResponse /// Human-readable guidance for clients to locate and read the JSON file /// [JsonProperty(Order = -2)] - public string message { get; } + public string Message { get; } /// /// File path where hierarchy data was saved /// [JsonProperty(Order = -1)] - public string hierarchyFilePath { get; } + public string HierarchyFilePath { get; } public GetHierarchyResponse(string filePath, string message = null) { - this.hierarchyFilePath = filePath ?? string.Empty; - this.message = string.IsNullOrEmpty(message) + HierarchyFilePath = filePath ?? string.Empty; + Message = string.IsNullOrEmpty(message) ? "Hierarchy data saved below. Open the JSON to read 'Context' and 'Hierarchy'." : message; } } -} \ No newline at end of file +} diff --git a/Packages/src/Editor/FirstPartyTools/GetHierarchy/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/GetHierarchy/Skill/SKILL.md index 4a1ec7cc08..1af6712840 100644 --- a/Packages/src/Editor/FirstPartyTools/GetHierarchy/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/GetHierarchy/Skill/SKILL.md @@ -56,7 +56,8 @@ uloop get-hierarchy --use-selection ## Output Returns JSON with: -- `message` (string): Human-readable guidance pointing at the saved file -- `hierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data -The hierarchy itself is **not** in the response — it is written to the file at `hierarchyFilePath`. Open that file to read the `Context` and `Hierarchy` payload (GameObject tree, components, etc.). +- `Message` (string): Human-readable guidance pointing at the saved file +- `HierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data + +The hierarchy itself is **not** in the response — it is written to the file at `HierarchyFilePath`. Open that file to read the `Context` and `Hierarchy` payload (GameObject tree, components, etc.). diff --git a/Packages/src/Editor/FirstPartyTools/GetLogs/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/GetLogs/Skill/SKILL.md index 57d1d97f62..dab6fc1d2f 100644 --- a/Packages/src/Editor/FirstPartyTools/GetLogs/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/GetLogs/Skill/SKILL.md @@ -50,10 +50,11 @@ uloop get-logs --search-text "Missing.*Component" --use-regex ## Output Returns JSON with: -- `totalCount` (number): Total logs available before max-count clipping -- `displayedCount` (number): Logs returned in this response (≤ `--max-count`) -- Input filters (`logType`, `maxCount`, `searchText`, `includeStackTrace`) are echoed back in the response -- `logs` (array): Each entry has: - - `type` (string): `"Error"`, `"Warning"`, or `"Log"` - - `message` (string): Log message body - - `stackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. + +- `TotalCount` (number): Total logs available before max-count clipping +- `DisplayedCount` (number): Logs returned in this response (≤ `--max-count`) +- Input filters (`LogType`, `MaxCount`, `SearchText`, `IncludeStackTrace`) are echoed back in the response +- `Logs` (array): Each entry has: + - `Type` (string): `"Error"`, `"Warning"`, or `"Log"` + - `Message` (string): Log message body + - `StackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. diff --git a/Packages/src/Editor/FirstPartyTools/RecordInput/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/RecordInput/Skill/SKILL.md index a76d38cb33..43f966dc3d 100644 --- a/Packages/src/Editor/FirstPartyTools/RecordInput/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/RecordInput/Skill/SKILL.md @@ -44,11 +44,12 @@ Replay injects input frame-by-frame, so the game must also be deterministic to p ## Output The CLI prints JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Start` or `Stop`) -- `outputPath`: Path to saved recording (nullable; populated on `Stop` only) -- `totalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) -- `durationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) - -Frame data lives only in the JSON file at `outputPath`; internal metadata fields are not included in the response. + +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Start` or `Stop`) +- `OutputPath`: Path to saved recording (nullable; populated on `Stop` only) +- `TotalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) +- `DurationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) + +Frame data lives only in the JSON file at `OutputPath`; internal metadata fields are not included in the response. diff --git a/Packages/src/Editor/FirstPartyTools/ReplayInput/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ReplayInput/Skill/SKILL.md index 81793dbc8e..051b96759c 100644 --- a/Packages/src/Editor/FirstPartyTools/ReplayInput/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ReplayInput/Skill/SKILL.md @@ -48,11 +48,12 @@ Replay injects the exact same input frame-by-frame, but the game must also be de ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) -- `inputPath`: Path to recording file (nullable string; populated on `Start` only) -- `currentFrame`: Current replay frame index (nullable int) -- `totalFrames`: Total frames in the recording (nullable int) -- `progress`: Replay progress (nullable float in 0.0 – 1.0) -- `isReplaying`: Whether replay is currently active (nullable bool) + +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) +- `InputPath`: Path to recording file (nullable string; populated on `Start` only) +- `CurrentFrame`: Current replay frame index (nullable int) +- `TotalFrames`: Total frames in the recording (nullable int) +- `Progress`: Replay progress (nullable float in 0.0 – 1.0) +- `IsReplaying`: Whether replay is currently active (nullable bool) diff --git a/Packages/src/Editor/FirstPartyTools/RunTests/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/RunTests/Skill/SKILL.md index 7660586e11..c77e73e7b2 100644 --- a/Packages/src/Editor/FirstPartyTools/RunTests/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/RunTests/Skill/SKILL.md @@ -6,13 +6,13 @@ description: "Run Unity Test Runner and report detailed results. Use for EditMod # uloop run-tests -Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `xmlPath` for detailed failure diagnosis. +Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `XmlPath` for detailed failure diagnosis. Before running tests, run `uloop compile` first if you created, deleted, renamed, moved, or edited C# files, test files, `.asmdef`/`.asmref`, or package manifests since the last successful compile. This refreshes the AssetDatabase and surfaces compile errors before test execution. -Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `success: false`, keeps `testCount` at `0`, lists the unsaved items in `message`, and does not start the Unity Test Runner. +Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `Success: false`, keeps `TestCount` at `0`, lists the unsaved items in `Message`, and does not start the Unity Test Runner. -`NoTestsFound` means zero tests matched — not a test failure. Check `noTestsFoundExplanation` and `message` for asmdef hints. +`NoTestsFound` means zero tests matched — not a test failure. Check `NoTestsFoundExplanation` and `Message` for asmdef hints. ## Usage @@ -57,22 +57,24 @@ uloop run-tests --filter-type regex --filter-value ".*Integration.*" ## Output Returns JSON with: -- `success` (boolean): Whether all tests passed -- `status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` -- `hasFailures` (boolean): Whether any discovered test failed -- `message` (string): Summary message -- `noTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests -- `noTestsFoundExplanation` (string): Agent-facing explanation when `noTestsFound` is true; empty otherwise -- `completedAt` (string): ISO timestamp when the run finished -- `testCount` (number): Total tests executed -- `passedCount` (number): Passed tests -- `failedCount` (number): Failed tests -- `skippedCount` (number): Skipped tests -- `xmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `success: true`); populated only when tests failed and the XML file exists on disk. + +- `Success` (boolean): Whether all tests passed +- `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` +- `HasFailures` (boolean): Whether any discovered test failed +- `Message` (string): Summary message +- `NoTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests +- `NoTestsFoundExplanation` (string): Agent-facing explanation when `NoTestsFound` is true; empty otherwise +- `CompletedAt` (string): ISO timestamp when the run finished +- `TestCount` (number): Total tests executed +- `PassedCount` (number): Passed tests +- `FailedCount` (number): Failed tests +- `SkippedCount` (number): Skipped tests +- `XmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `Success: true`); populated only when tests failed and the XML file exists on disk. ### XML Result File When tests fail, NUnit XML results are automatically saved to `{project_root}/.uloop/outputs/TestResults/.xml`. The XML contains per-test-case results including: + - Test name and full name - Pass/fail/skip status and duration - For failed tests: `` (assertion error) and `` diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md index bc721a5ee7..39e81c686e 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md @@ -72,18 +72,19 @@ uloop screenshot --output-directory /tmp/screenshots ## Output Returns JSON with: -- `screenshotCount`: Number of windows captured -- `screenshots`: Array of screenshot info, each containing: - - `imagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. - - `fileSizeBytes`: Size of the saved file in bytes - - `width`: Captured image width in pixels - - `height`: Captured image height in pixels - - `coordinateSystem`: `"gameView"` or `"window"` - - `resolutionScale`: Resolution scale used for capture - - `yOffset`: Y offset used for gameView coordinate conversion - - `annotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` is used. - -For `annotatedElements` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. + +- `ScreenshotCount`: Number of windows captured +- `Screenshots`: Array of screenshot info, each containing: + - `ImagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. + - `FileSizeBytes`: Size of the saved file in bytes + - `Width`: Captured image width in pixels + - `Height`: Captured image height in pixels + - `CoordinateSystem`: `"gameView"` or `"window"` + - `ResolutionScale`: Resolution scale used for capture + - `YOffset`: Y offset used for gameView coordinate conversion + - `AnnotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` is used. + +For `AnnotatedElements` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. When multiple windows match (e.g., multiple Inspector windows or when using `contains` mode), all matching windows are captured with numbered filenames (e.g., `Inspector_1_*.png`, `Inspector_2_*.png`). diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/references/annotated-elements.md b/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/references/annotated-elements.md index 50b899b096..283109ccc2 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/references/annotated-elements.md +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/references/annotated-elements.md @@ -2,34 +2,34 @@ Read this when using `uloop screenshot --capture-mode rendering --annotate-elements` to find coordinates for `simulate-mouse-ui` or `simulate-mouse-input`. -## annotatedElements Fields +## AnnotatedElements Fields -`annotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: +`AnnotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: -- `label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. -- `name`: Element name -- `path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. -- `type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) -- `interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. -- `simX`, `simY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. -- `boundsMinX`, `boundsMinY`, `boundsMaxX`, `boundsMaxY`: Bounding box in simulate-mouse coordinates -- `sortingOrder`: Canvas sorting order. Higher values are in front. -- `siblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. +- `Label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. +- `Name`: Element name +- `Path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. +- `Type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) +- `Interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. +- `SimX`, `SimY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. +- `BoundsMinX`, `BoundsMinY`, `BoundsMaxX`, `BoundsMaxY`: Bounding box in simulate-mouse coordinates +- `SortingOrder`: Canvas sorting order. Higher values are in front. +- `SiblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. ## Coordinate Conversion -When `coordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: +When `CoordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: ```text -sim_x = image_x / resolutionScale -sim_y = image_y / resolutionScale + yOffset +sim_x = image_x / ResolutionScale +sim_y = image_y / ResolutionScale + YOffset ``` -When `resolutionScale` is `1.0`, this simplifies to: +When `ResolutionScale` is `1.0`, this simplifies to: ```text sim_x = image_x -sim_y = image_y + yOffset +sim_y = image_y + YOffset ``` ## Annotation Readability diff --git a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md index 7b65d6b135..e898d1090a 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md @@ -42,11 +42,12 @@ uloop simulate-keyboard --action --key [options] Use `Press` for edge-triggered keyboard code such as `Keyboard.current.spaceKey.wasPressedThisFrame`. `KeyDown` emits one initial press edge, then only keeps the key held. It does not keep `wasPressedThisFrame` true while the key remains held. If a successful `Press` or `KeyDown` leaves `Keyboard.current..isPressed` true but runtime state does not change, do not immediately rewrite the user's runtime code to `isPressed`. First verify that the target component is active during the command, that it polls input in the configured Input System update phase, and that a missed `KeyDown` edge is followed by `KeyUp` before retrying. + ### Pause Point Inspection (Standard for E2E) For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the key, not immediately after `simulate-keyboard`. -- If `interruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `pausePointId` and `pausePointHitCount` identify the marker. `pressEdgeObserved` is still reported on pause-point interruptions. +- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. `PressEdgeObserved` is still reported on pause-point interruptions. ### KeyDown/KeyUp Rules @@ -85,15 +86,16 @@ uloop simulate-keyboard --action KeyUp --key LeftShift ## Output Returns JSON with: -- `success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) -- `message` (string): Description of what happened or why it failed -- `action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) -- `keyName` (string, nullable): The key that was acted on; may be `null` when the action could not resolve a key -- `interruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `pausePointId` (string, nullable): The marker id when a `UloopPausePoint.Pause` marker caused the interruption -- `pausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker caused the interruption -- `pausePointHits` (array, nullable): Every marker hit during this input as `{id, hitCount}` entries, in hit order. Read this when one input may trigger several markers; `pausePointId` only names the latest one -- `pressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was actually visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge (e.g. the press was consumed by an editor-only input update) — retry the input or verify with a focused log instead of trusting `success` alone. `null` only for `KeyUp` and for timed-out responses; pause-point interruptions still report the observed value + +- `Success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) +- `Message` (string): Description of what happened or why it failed +- `Action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) +- `KeyName` (string, nullable): The key that was acted on; may be `null` when the action could not resolve a key +- `InterruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released +- `PausePointId` (string, nullable): The marker id when a `UloopPausePoint.Pause` marker caused the interruption +- `PausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker caused the interruption +- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one +- `PressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was actually visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge (e.g. the press was consumed by an editor-only input update) — retry the input or verify with a focused log instead of trusting `Success` alone. `null` only for `KeyUp` and for timed-out responses; pause-point interruptions still report the observed value ## Prerequisites diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md index 8bc5b09e69..1df0d2a943 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md @@ -52,7 +52,7 @@ uloop simulate-mouse-input --action [options] For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the mouse input, not immediately after `simulate-mouse-input`. -- If `interruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `pausePointId` and `pausePointHitCount` identify the marker. +- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. - Remove temporary pause-point/log instrumentation before final validation when it was added only for inspection. ### Global Options (optional) @@ -61,7 +61,6 @@ For standard frame proof when this input drives a state transition, follow the ` |--------|-------------| | `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | - ## When to use this vs simulate-mouse-ui All rows below assume the New Input System is installed. @@ -109,15 +108,16 @@ uloop simulate-mouse-input --action SmoothDelta --delta-x 300 --delta-y 0 --dura ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) -- `button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) -- `positionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) -- `positionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) -- `interruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `pausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption -- `pausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` -- `pausePointHits` (array, nullable): Every marker hit during this input as `{id, hitCount}` entries, in hit order. Read this when one input may trigger several markers; `pausePointId` only names the latest one + +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) +- `Button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) +- `PositionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) +- `PositionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) +- `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released +- `PausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption +- `PausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` +- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one Verify visual outcome with a follow-up screenshot. diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md index 8a9a797b6b..a8f7d7ecea 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md @@ -13,7 +13,7 @@ Simulate mouse interaction on Unity PlayMode UI. 1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not) 2. Get UI element info: `uloop screenshot --capture-mode rendering --annotate-elements --elements-only` -3. Use the `annotatedElements` array to find the target element by `label`, `name`, or `path` (A=frontmost, B=next, ...). Use `interaction` to distinguish click targets from drag/drop/text targets, then use `simX`/`simY` directly as `--x`/`--y` coordinates. +3. Use the `AnnotatedElements` array to find the target element by `Label`, `Name`, or `Path` (A=frontmost, B=next, ...). Use `Interaction` to distinguish click targets from drag/drop/text targets, then use `SimX`/`SimY` directly as `--x`/`--y` coordinates. 4. Execute the needed `uloop simulate-mouse-ui` commands 5. Inspect the result with the lightest useful evidence: runtime state, logs, or a screenshot 6. When this UI input verifies a state transition, use Pause Point inspection from the section below as the standard frame proof @@ -38,7 +38,7 @@ uloop simulate-mouse-ui --action --x --y [options] | `--duration` | number | `0.5` | Hold duration in seconds for LongPress action. | | `--button` | enum | `Left` | Mouse button. `Click` and `LongPress` support `Left`, `Right`, and `Middle`. Drag actions support `Left` only; other buttons return an error. | | `--bypass-raycast` | flag | - | For `Click`, `LongPress`, `Drag`, and `DragStart`, bypass EventSystem raycast and dispatch pointer events directly to `--target-path`. Use when a raycast-blocking overlay visually covers the intended target. | -| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `annotatedElements[].path` from screenshot JSON. | +| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `AnnotatedElements[].Path` from screenshot JSON. | | `--drop-target-path` | string | `""` | Optional hierarchy path of a drop target for `Drag` or `DragEnd`, for example `Canvas/DropZone`. Use this when the drop zone is also behind a raycast blocker. | ### Actions @@ -64,14 +64,13 @@ uloop simulate-mouse-ui --action --x --y [options] |--------|-------------| | `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | - ## Coordinate System - Origin is **top-left** (0, 0) - All positions are in **screen pixels** -- Get coordinates from `annotatedElements` JSON (`simX`/`simY`) — do NOT look up GameObject positions +- Get coordinates from `AnnotatedElements` JSON (`SimX`/`SimY`) — do NOT look up GameObject positions - Clicking or long-pressing on empty space (no UI element) still succeeds with a message indicating no element was hit -- Dragging on empty space (no draggable UI element) returns `success = false` +- Dragging on empty space (no draggable UI element) returns `Success = false` - `--bypass-raycast` still uses coordinates for pointer event positions, but chooses the clicked, long-pressed, or dragged GameObject by `--target-path` - If `--target-path` or `--drop-target-path` matches multiple active GameObjects, the command fails instead of choosing an arbitrary duplicate @@ -125,15 +124,16 @@ uloop simulate-mouse-ui --action DragEnd --x 600 --y 300 ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") -- `action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) -- `hitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) -- `positionX`: Target X coordinate that was used -- `positionY`: Target Y coordinate that was used -- `endPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) -- `endPositionY`: Drag end Y coordinate (nullable float; populated for drag actions only) + +- `Success`: Whether the operation succeeded +- `Message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") +- `Action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) +- `HitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) +- `PositionX`: Target X coordinate that was used +- `PositionY`: Target Y coordinate that was used +- `EndPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) +- `EndPositionY`: Drag end Y coordinate (nullable float; populated for drag actions only) Verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. -Note: Click and LongPress on empty space (no UI element) still return `success = true` with `hitGameObjectName = null`. Drag actions on empty space return `success = false`. +Note: Click and LongPress on empty space (no UI element) still return `Success = true` with `HitGameObjectName = null`. Drag actions on empty space return `Success = false`. diff --git a/Packages/src/Editor/Infrastructure/Api/CompileStatusBridgeCommand.cs b/Packages/src/Editor/Infrastructure/Api/CompileStatusBridgeCommand.cs index 5fc5fbdcb7..4cc577ec33 100644 --- a/Packages/src/Editor/Infrastructure/Api/CompileStatusBridgeCommand.cs +++ b/Packages/src/Editor/Infrastructure/Api/CompileStatusBridgeCommand.cs @@ -153,13 +153,13 @@ private static JObject CreateRecoveredCompileResult( return new JObject { - ["success"] = JValue.CreateNull(), - ["errorCount"] = JValue.CreateNull(), - ["warningCount"] = JValue.CreateNull(), - ["errors"] = JValue.CreateNull(), - ["warnings"] = JValue.CreateNull(), - ["message"] = message, - ["projectRoot"] = UnityCliLoopPathResolver.GetProjectRoot() + ["Success"] = JValue.CreateNull(), + ["ErrorCount"] = JValue.CreateNull(), + ["WarningCount"] = JValue.CreateNull(), + ["Errors"] = JValue.CreateNull(), + ["Warnings"] = JValue.CreateNull(), + ["Message"] = message, + ["ProjectRoot"] = UnityCliLoopPathResolver.GetProjectRoot() }; } diff --git a/Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs b/Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs index a3e4f0598c..2c9e704ff8 100644 --- a/Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs +++ b/Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; using io.github.hatayama.UnityCliLoop.ToolContracts; namespace io.github.hatayama.UnityCliLoop.Infrastructure @@ -11,16 +10,6 @@ public static class JsonRpcResponseSerializer { public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings { - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy - { - // Why: response DTO property names are the public contract, but user-supplied - // dictionary keys can be data and must not be rewritten by the transport layer. - ProcessDictionaryKeys = false, - OverrideSpecifiedNames = false - } - }, ReferenceLoopHandling = ReferenceLoopHandling.Ignore, MaxDepth = UnityCliLoopServerConfig.DEFAULT_JSON_MAX_DEPTH }; diff --git a/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs b/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs index a867c85b31..7bd57a3bc6 100644 --- a/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs +++ b/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs @@ -45,6 +45,8 @@ public sealed class CliInstallationDetector : ICliInstallationDetector private const string SHELL_CONTRACT_STATUS_START_MARKER = "__ULOOP_CONTRACT_STATUS_START__"; private const string SHELL_CONTRACT_STATUS_END_MARKER = "__ULOOP_CONTRACT_STATUS_END__"; private const string SHELL_SUCCESS_EXIT_CODE = "0"; + private const string VERSION_JSON_CLI_VERSION_PROPERTY = "CliVersion"; + private const string VERSION_JSON_PROTOCOL_VERSION_PROPERTY = "ProtocolVersion"; private string _cachedCliVersion; private int? _cachedCliProtocolVersion; @@ -362,8 +364,8 @@ private static CliInstallationDetection ParseCliContractOutput(string output, st try { JObject parsed = JObject.Parse(jsonLine); - string version = parsed["cliVersion"]?.ToString(); - JToken protocolVersionToken = parsed["protocolVersion"]; + string version = parsed[VERSION_JSON_CLI_VERSION_PROPERTY]?.ToString(); + JToken protocolVersionToken = parsed[VERSION_JSON_PROTOCOL_VERSION_PROPERTY]; int? protocolVersion = ReadProtocolVersion(protocolVersionToken); return new CliInstallationDetection(version, protocolVersion, executablePath); } diff --git a/Packages/src/Editor/Infrastructure/SkillSetup/SkillInstallLayout.cs b/Packages/src/Editor/Infrastructure/SkillSetup/SkillInstallLayout.cs index 949a448dea..0822db01b1 100644 --- a/Packages/src/Editor/Infrastructure/SkillSetup/SkillInstallLayout.cs +++ b/Packages/src/Editor/Infrastructure/SkillSetup/SkillInstallLayout.cs @@ -306,6 +306,12 @@ internal static SkillInstallState GetInstalledStateForSkillSource( return SkillInstallState.Missing; } + string installedSkillFilePath = Path.Combine(installedSkillDirectory, SkillFileName); + if (!File.Exists(installedSkillFilePath)) + { + return SkillInstallState.Missing; + } + return IsSkillDirectoryOutdated(skill.SkillFiles, installedSkillDirectory) ? SkillInstallState.Outdated : SkillInstallState.Installed; diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs index 36cb526b5b..e60fdf4e63 100644 --- a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs @@ -24,7 +24,7 @@ internal static class ThirdPartyToolMigrationWizardText "for V3.\n\n" + "Scope:\n" + "- Check SKILL.md, Markdown, POSIX shell scripts, and PowerShell scripts.\n" + - "- Migrate V2 boolean arguments, renamed first-party options, removed commands, and first-party JSON output fields.\n" + + "- Migrate V2 boolean arguments, renamed first-party options, and removed commands.\n" + "- Read nearby context before editing. Do not change C# snippets, enum/member references, regex match properties, DTO properties, or non-uloop JSON.\n" + "- After editing, summarize changed files, remaining candidates, and any commands I should verify manually."; internal const string MigrationNotCheckedText = "C# source migration status has not been checked."; diff --git a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md index f26aab2fa1..3dfa7c1980 100644 --- a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md +++ b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md @@ -1,38 +1,68 @@ --- name: v3-cli-invocation-migration -description: Migrate uloop V2 CLI invocations in agent skills, Markdown, POSIX shell scripts, and PowerShell scripts to V3 syntax. Use when updating third-party or first-party uloop command examples, shell automation, SKILL.md files, or JSON output parsing from V2-style boolean arguments, renamed first-party options, removed commands, or PascalCase first-party response fields. +description: Migrate only uloop V2 CLI option syntax in agent skills, Markdown, POSIX shell scripts, and PowerShell scripts to V3 syntax. Use when updating first-party or third-party uloop command examples that contain V2-style boolean arguments or renamed options. --- # V3 CLI Invocation Migration -Use this skill to update V2-era `uloop` CLI invocations to V3 syntax in agent-facing docs and automation. +Use this skill to update V2-era `uloop` CLI option syntax to V3 +syntax in agent-facing docs and automation. ## Workflow 1. Read `references/first-party-v2-to-v3.md` before editing. 2. Search the repository for `uloop` invocations and the V2 names listed in the reference. 3. Inspect nearby context before every edit. Search hits are candidates, not proof. -4. Edit only files that clearly contain V2 `uloop` command syntax or V2 first-party output parsing. -5. Repeat the searches after editing and report any remaining V2 candidates with the reason each one was left unchanged. +4. Edit only files that clearly contain V2 `uloop` option syntax. +5. Repeat the searches after editing and report any remaining V2 + candidates with the reason each one was left unchanged. Prefer `rg` for searches when available. If `rg` is unavailable, use the best available project search tool. Candidate discovery should be repository search plus context inspection, not a generated candidate list. +## Validation Rules + +- Verify this migration with targeted search passes and a focused diff review. +- Do not run Markdownlint as required validation for this migration. +- If Markdownlint runs anyway and fails, ignore that result for this task. + Do not stop, revert, or withhold completion solely because Markdownlint + reports existing or unrelated Markdown formatting violations. +- Do not fix Markdownlint findings as part of this migration unless the + finding is directly caused by changing a `uloop` option token. + ## Editing Rules -- Convert V2 boolean arguments from `--flag true` or `--flag=false` syntax to V3 flag syntax. -- For third-party tools, check the tool's current schema or documentation before deciding whether `false` means removal, a `--no-*` flag, or a renamed option. -- For first-party tools, use the reference table. `compile` and `run-tests` have special renamed negative flags. -- Convert V2 PascalCase output field reads to V3 camelCase only when the surrounding command is a `uloop` first-party command. -- Treat helpers and wrappers as context clues, not proof. Trace them far enough to confirm they execute `uloop` and parse its JSON before rewriting their result fields. -- Do not edit generated installed skills under `.agents`, `.claude`, `.codex`, `.cursor`, `.gemini`, `.windsurf`, `.agent`, or similar target folders unless the user explicitly asks to migrate installed copies. -- Do not edit Markdown C# snippets, enum/member references, ordinary DTO/property access, regex match properties, or non-`uloop` JSON. -- Do not change protocol versions, release versions, package names, assembly names, or public extension identifiers as part of this migration. -- Keep edits local to the command or parser being migrated. +- Only change option tokens inside confirmed `uloop` command invocations. +- Convert V2 boolean arguments from `--flag true` or `--flag=false` + syntax to V3 flag syntax. +- For third-party tools, check the tool's current schema or documentation + before deciding whether `false` means removal, a `--no-*` flag, or a + renamed option. +- For first-party tools, use the reference table. `compile` and `run-tests` + have special renamed negative flags. +- A valid edit is limited to replacing, adding, or removing a `uloop` option + token and the boolean value attached to that option in the same invocation. +- Preserve surrounding Markdown, shell, and PowerShell formatting. Do not + reflow prose, wrap lines, reorder arguments, normalize unrelated whitespace, + trim unrelated trailing spaces, or change indentation. +- If removing an option leaves extra spaces in that same `uloop` invocation, + collapse only the spaces created by that removal. +- Do not rename commands, replace removed commands, or rewrite command + structure unless the user explicitly asks for command migration. +- Do not change non-`uloop` command options, even when they have V2-looking + boolean syntax. +- Do not edit generated installed skills under `.agents`, `.claude`, + `.codex`, `.cursor`, `.gemini`, `.windsurf`, `.agent`, or similar target + folders unless the user explicitly asks to migrate installed copies. +- Do not edit Markdown C# snippets, enum/member references, ordinary + DTO/property access, regex match properties, or non-`uloop` JSON. +- Do not change protocol versions, release versions, package names, assembly + names, or public extension identifiers as part of this migration. ## Required Search Passes - `uloop` command lines and examples. - Boolean-looking CLI options: `--* true`, `--*=true`, `--* false`, `--*=false`. - First-party renamed options from the reference, including bare flags. -- Removed commands: `get-project-info` and `get-version`. -- PascalCase output fields from the reference, but only where surrounding code parses first-party `uloop` JSON. +- Removed commands: `get-project-info` and `get-version`. Report these as + out-of-scope command migration candidates unless the user explicitly asked + to migrate removed commands. diff --git a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md index a535baaae6..21e00d48c5 100644 --- a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md +++ b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md @@ -1,6 +1,18 @@ # First-Party V2 to V3 CLI Migration -Use this reference as the canonical migration map. Search results are only candidates. Edit a match only after the surrounding context proves it is a V2 `uloop` invocation or first-party `uloop` JSON output parser. +Use this reference as the canonical option migration map. Search results are +only candidates. Edit a match only after the surrounding context proves it is +a V2 `uloop` invocation. + +This migration is option-only by default. Do not reformat surrounding text or +scripts, and do not change non-`uloop` commands. A valid edit only replaces, +adds, or removes a `uloop` option token and the boolean value attached to that +option in the same invocation. + +Markdownlint is not required validation for this migration. If Markdownlint +runs and reports existing or unrelated formatting issues, ignore that result, +continue the option migration, and report the lint result as non-blocking. Do +not fix Markdownlint findings unless the migration itself created them. ## Search Checklist @@ -9,8 +21,8 @@ Prefer `rg` when available, but any repository search tool is acceptable. - Search `uloop` first and inspect command examples, shell scripts, PowerShell scripts, and agent skills. - Search boolean-looking CLI syntax: `--` plus nearby `true` or `false`, including `--flag true`, `--flag=false`, and inline Markdown command examples. - Search renamed first-party option names: `wait-for-domain-reload`, `reload-external-scene-changes`, `force-recompile`, `save-before-run`, `show-overlay`, `include-components`, `include-inactive`, and `compile-only`. -- Search removed first-party commands: `get-project-info` and `get-version`. -- Search PascalCase output field names from the table below, then keep only matches that parse JSON produced by first-party `uloop` commands. +- Search removed first-party commands only to report them as out-of-scope + command migration candidates: `get-project-info` and `get-version`. - Skip generated installed skill copies under `.agents`, `.claude`, `.codex`, `.cursor`, `.gemini`, `.windsurf`, `.agent`, or equivalent target folders unless the user explicitly asks to migrate installed copies. ## Boolean Argument Rules @@ -51,65 +63,5 @@ For third-party tools, inspect the current tool schema or docs before choosing t | V2 command | V3 handling | | --- | --- | -| `uloop get-project-info` | Replace manually based on caller intent. Do not guess from the command name alone. | -| `uloop get-version` | Replace manually based on caller intent. Do not guess from the command name alone. | - -## Output JSON Field Candidates - -V3 first-party CLI output generally uses camelCase JSON fields. Only update these when the surrounding parser is reading first-party `uloop` JSON. Examples that usually qualify include `jq '.Success'`, PowerShell objects produced by `ConvertFrom-Json` from `uloop` output, and local helper functions whose implementation clearly executes `uloop` and returns parsed first-party JSON. - -| V2 field | V3 field | -| --- | --- | -| `Success` | `success` | -| `Message` | `message` | -| `ErrorMessage` | `errorMessage` | -| `ErrorCount` | `errorCount` | -| `WarningCount` | `warningCount` | -| `TotalCount` | `totalCount` | -| `DisplayedCount` | `displayedCount` | -| `LogType` | `logType` | -| `StackTrace` | `stackTrace` | -| `XmlPath` | `xmlPath` | -| `TestCount` | `testCount` | -| `PassedCount` | `passedCount` | -| `FailedCount` | `failedCount` | -| `SkippedCount` | `skippedCount` | -| `CompletedAt` | `completedAt` | -| `ScreenshotCount` | `screenshotCount` | -| `Screenshots` | `screenshots` | -| `CompilationErrors` | `compilationErrors` | -| `ErrorCode` | `errorCode` | -| `UpdatedCode` | `updatedCode` | -| `DiagnosticsSummary` | `diagnosticsSummary` | -| `OutputPath` | `outputPath` | -| `InputPath` | `inputPath` | -| `TotalFrames` | `totalFrames` | -| `DurationSeconds` | `durationSeconds` | -| `CurrentFrame` | `currentFrame` | -| `IsReplaying` | `isReplaying` | -| `KeyName` | `keyName` | -| `PositionX` | `positionX` | -| `PositionY` | `positionY` | -| `EndPositionX` | `endPositionX` | -| `EndPositionY` | `endPositionY` | -| `HitGameObjectName` | `hitGameObjectName` | -| `IsPlaying` | `isPlaying` | -| `IsPaused` | `isPaused` | -| `ClearedLogCount` | `clearedLogCount` | -| `ClearedCounts` | `clearedCounts` | - -## Context Rules - -Migrate output fields when context proves all of these: - -- The JSON comes from a first-party `uloop` command. -- The parser reads command output, not a C# object, regex match, unrelated API response, or sample domain model. -- The PascalCase field belongs to the old CLI response shape, not to code that intentionally documents C# property names. - -Do not migrate these common false positives: - -- C# enum or member references such as `ConnectStatus.Success`. -- C# null-conditional method calls such as `_soundController?.IsPlaying()`. -- Regex or framework result properties such as `$baseMatch.Success` or `match.Success`. -- C# DTOs, tests, serialized model definitions, or ordinary properties named `Success`, `ErrorMessage`, `IsPlaying`, or similar. -- Non-`uloop` JSON parsing, even when it uses the same field names. +| `uloop get-project-info` | Report only unless the user explicitly asks for removed command migration. Do not guess from the command name alone. | +| `uloop get-version` | Report only unless the user explicitly asks for removed command migration. Do not guess from the command name alone. | diff --git a/cli/internal/cli/argument_error.go b/cli/internal/cli/argument_error.go index 5907cd62fb..9b0aecf27f 100644 --- a/cli/internal/cli/argument_error.go +++ b/cli/internal/cli/argument_error.go @@ -19,13 +19,13 @@ func (err *argumentError) toCLIError(context errorContext) cliError { command := firstNonEmpty(err.command, context.command) details := map[string]any{} if err.option != "" { - details["option"] = err.option + details["Option"] = err.option } if err.received != "" { - details["received"] = err.received + details["Received"] = err.received } if err.expectedType != "" { - details["expectedType"] = err.expectedType + details["ExpectedType"] = err.expectedType } nextActions := err.nextActions diff --git a/cli/internal/cli/busy_status.go b/cli/internal/cli/busy_status.go index d9e7a18124..8009ff6e29 100644 --- a/cli/internal/cli/busy_status.go +++ b/cli/internal/cli/busy_status.go @@ -9,12 +9,12 @@ import ( const cliStatusBusy = "Busy" type cliStatusEnvelope struct { - Status string `json:"status"` - Message string `json:"message"` - RunningToolName string `json:"runningToolName,omitempty"` - RequestedToolName string `json:"requestedToolName,omitempty"` - IsPlaying *bool `json:"isPlaying,omitempty"` - IsPaused *bool `json:"isPaused,omitempty"` + Status string `json:"Status"` + Message string `json:"Message"` + RunningToolName string `json:"RunningToolName,omitempty"` + RequestedToolName string `json:"RequestedToolName,omitempty"` + IsPlaying *bool `json:"IsPlaying,omitempty"` + IsPaused *bool `json:"IsPaused,omitempty"` } type serverBusyStatusDetails struct { @@ -40,7 +40,7 @@ func writeBusyStatusEnvelope(writer io.Writer, message string, details serverBus } func serverBusyStatusDetailsFromError(err cliError) serverBusyStatusDetails { - data, ok := err.Details["data"].(map[string]any) + data, ok := serverBusyDetailsData(err.Details) if !ok { return serverBusyStatusDetails{ requestedToolName: err.Command, @@ -59,6 +59,15 @@ func serverBusyStatusDetailsFromError(err cliError) serverBusyStatusDetails { } } +func serverBusyDetailsData(details map[string]any) (map[string]any, bool) { + data, ok := details["Data"].(map[string]any) + if ok { + return data, true + } + data, ok = details["data"].(map[string]any) + return data, ok +} + func optionalBool(hasValue bool, value bool) *bool { if !hasValue { return nil diff --git a/cli/internal/cli/compile_wait.go b/cli/internal/cli/compile_wait.go index 41a4a0c089..19d9d7116a 100644 --- a/cli/internal/cli/compile_wait.go +++ b/cli/internal/cli/compile_wait.go @@ -32,13 +32,13 @@ type compileCompletionOptions struct { } type compileStatusResponse struct { - Ready bool `json:"ready"` - HasResult bool `json:"hasResult"` - IsCompiling bool `json:"isCompiling"` - IsUpdating bool `json:"isUpdating"` - IsDomainReloadInProgress bool `json:"isDomainReloadInProgress"` - Result json.RawMessage `json:"result"` - Message string `json:"message"` + Ready bool `json:"Ready"` + HasResult bool `json:"HasResult"` + IsCompiling bool `json:"IsCompiling"` + IsUpdating bool `json:"IsUpdating"` + IsDomainReloadInProgress bool `json:"IsDomainReloadInProgress"` + Result json.RawMessage `json:"Result"` + Message string `json:"Message"` } var queryCompileStatus = queryCompileStatusFromUnity @@ -469,12 +469,20 @@ func compileResultLogSummary(result json.RawMessage) compileResultSummary { return compileResultSummary{} } return compileResultSummary{ - success: payload["success"], - errorCount: payload["errorCount"], - warningCount: payload["warningCount"], + success: firstPresentJSONValue(payload, "Success", "success"), + errorCount: firstPresentJSONValue(payload, "ErrorCount", "errorCount"), + warningCount: firstPresentJSONValue(payload, "WarningCount", "warningCount"), } } +func firstPresentJSONValue(payload map[string]any, primaryKey string, legacyKey string) any { + value, ok := payload[primaryKey] + if ok { + return value + } + return payload[legacyKey] +} + func compileStatusLogContext(status compileStatusResponse) map[string]any { return map[string]any{ "ready": status.Ready, diff --git a/cli/internal/cli/compile_wait_test.go b/cli/internal/cli/compile_wait_test.go index 0a699af8b0..deb4ee9f23 100644 --- a/cli/internal/cli/compile_wait_test.go +++ b/cli/internal/cli/compile_wait_test.go @@ -98,20 +98,20 @@ func TestShouldWaitForExecuteDynamicCodeDomainReloadSkipsCompileOnly(t *testing. // Verifies that execute-dynamic-code waits only when Unity explicitly reports a reload signal. func TestExecuteDynamicCodeDomainReloadWaitRequiredReadsResponseSignal(t *testing.T) { - result := []byte(`{"success":true,"domainReloadWaitRequired":true}`) + result := []byte(`{"Success":true,"DomainReloadWaitRequired":true}`) if !executeDynamicCodeDomainReloadWaitRequired(result) { t.Fatal("dynamic-code response should request a reload wait") } - if executeDynamicCodeDomainReloadWaitRequired([]byte(`{"success":true}`)) { + if executeDynamicCodeDomainReloadWaitRequired([]byte(`{"Success":true}`)) { t.Fatal("dynamic-code response without a reload signal should not request a wait") } } -// Verifies that legacy-cased dynamic-code reload signals remain parseable by the CLI. -func TestExecuteDynamicCodeDomainReloadWaitRequiredReadsLegacyResponseSignal(t *testing.T) { - result := []byte(`{"Success":true,"DomainReloadWaitRequired":true}`) +// Verifies that legacy lower-camel dynamic-code reload signals remain parseable by the CLI. +func TestExecuteDynamicCodeDomainReloadWaitRequiredReadsLegacyLowerCamelResponseSignal(t *testing.T) { + result := []byte(`{"success":true,"domainReloadWaitRequired":true}`) if !executeDynamicCodeDomainReloadWaitRequired(result) { t.Fatal("legacy dynamic-code response should request a reload wait") @@ -133,18 +133,18 @@ func TestShouldWaitForExecuteDynamicCodeDisconnectWaitsAfterDispatchedTransportL // Verifies that the CLI does not expose its internal reload-wait response field to users. func TestStripExecuteDynamicCodeControlResultRemovesReloadSignal(t *testing.T) { - result := stripExecuteDynamicCodeControlResult([]byte(`{"success":true,"domainReloadWaitRequired":true}`)) + result := stripExecuteDynamicCodeControlResult([]byte(`{"Success":true,"DomainReloadWaitRequired":true}`)) - if strings.Contains(string(result), "domainReloadWaitRequired") { + if strings.Contains(string(result), "DomainReloadWaitRequired") { t.Fatalf("control field leaked into user output: %s", result) } } -// Verifies that the CLI hides legacy-cased reload-wait response fields from users. -func TestStripExecuteDynamicCodeControlResultRemovesLegacyReloadSignal(t *testing.T) { - result := stripExecuteDynamicCodeControlResult([]byte(`{"success":true,"DomainReloadWaitRequired":true}`)) +// Verifies that the CLI hides legacy lower-camel reload-wait response fields from users. +func TestStripExecuteDynamicCodeControlResultRemovesLegacyLowerCamelReloadSignal(t *testing.T) { + result := stripExecuteDynamicCodeControlResult([]byte(`{"Success":true,"domainReloadWaitRequired":true}`)) - if strings.Contains(string(result), "DomainReloadWaitRequired") { + if strings.Contains(string(result), "domainReloadWaitRequired") { t.Fatalf("legacy control field leaked into user output: %s", result) } } @@ -174,9 +174,9 @@ func TestWaitForCompileCompletionReturnsReadyStatusResult(t *testing.T) { replaceQueryCompileStatus(t, func(context.Context, unityipc.Connection, string) (compileStatusResponse, error) { callCount++ if callCount == 1 { - return compileStatusResponse{Ready: false, HasResult: true, Result: json.RawMessage(`{"success":true}`)}, nil + return compileStatusResponse{Ready: false, HasResult: true, Result: json.RawMessage(`{"Success":true}`)}, nil } - return compileStatusResponse{Ready: true, HasResult: true, Result: json.RawMessage(`{"success":true}`)}, nil + return compileStatusResponse{Ready: true, HasResult: true, Result: json.RawMessage(`{"Success":true}`)}, nil }) result, completed, err := waitForCompileCompletion(context.Background(), compileCompletionOptions{ @@ -191,7 +191,7 @@ func TestWaitForCompileCompletionReturnsReadyStatusResult(t *testing.T) { if !completed { t.Fatal("compile wait did not complete") } - if string(result) != "{\"success\":true}" { + if string(result) != "{\"Success\":true}" { t.Fatalf("result mismatch: %s", result) } } @@ -215,7 +215,7 @@ func TestWaitForCompileCompletionWritesPollLifecycleVibeLogs(t *testing.T) { return compileStatusResponse{ Ready: true, HasResult: true, - Result: json.RawMessage(`{"success":false,"errorCount":2,"warningCount":1}`), + Result: json.RawMessage(`{"Success":false,"ErrorCount":2,"WarningCount":1}`), Message: "Compile result is available.", }, nil }) @@ -232,7 +232,7 @@ func TestWaitForCompileCompletionWritesPollLifecycleVibeLogs(t *testing.T) { if !completed { t.Fatal("compile wait did not complete") } - if string(result) != `{"success":false,"errorCount":2,"warningCount":1}` { + if string(result) != `{"Success":false,"ErrorCount":2,"WarningCount":1}` { t.Fatalf("result mismatch: %s", result) } @@ -266,7 +266,7 @@ func TestWaitForCompileCompletionForceCompileWaitsForStoredResult(t *testing.T) return compileStatusResponse{ Ready: true, HasResult: true, - Result: json.RawMessage(`{"success":null,"errorCount":null,"message":"Force compilation completed"}`), + Result: json.RawMessage(`{"Success":null,"ErrorCount":null,"Message":"Force compilation completed"}`), }, nil }) @@ -291,15 +291,15 @@ func TestWaitForCompileCompletionForceCompileWaitsForStoredResult(t *testing.T) if err := json.Unmarshal(result, &payload); err != nil { t.Fatalf("force compile result is not JSON: %v", err) } - if payload["success"] != nil { - t.Fatalf("force compile success should be unknown: %#v", payload["success"]) + if payload["Success"] != nil { + t.Fatalf("force compile success should be unknown: %#v", payload["Success"]) } - if payload["errorCount"] != nil || payload["warningCount"] != nil { + if payload["ErrorCount"] != nil || payload["WarningCount"] != nil { t.Fatalf("force compile counts should be unknown: %#v", payload) } - message, ok := payload["message"].(string) + message, ok := payload["Message"].(string) if !ok || !strings.Contains(message, "Force compilation completed") { - t.Fatalf("force compile message mismatch: %#v", payload["message"]) + t.Fatalf("force compile message mismatch: %#v", payload["Message"]) } } @@ -448,7 +448,7 @@ func TestRunCompileWithDomainReloadWaitWritesRequestLifecycleVibeLogs(t *testing return compileStatusResponse{ Ready: true, HasResult: true, - Result: json.RawMessage(`{"success":false,"errorCount":1,"warningCount":0}`), + Result: json.RawMessage(`{"Success":false,"ErrorCount":1,"WarningCount":0}`), }, nil }) @@ -523,10 +523,10 @@ func TestShouldWaitForCompileStatusAllowsAcceptedFinalResponseTimeout(t *testing // Verifies compile readiness warmup only runs after confirmed successful results. func TestCompileResultReadinessWaitMode(t *testing.T) { cases := map[string]compileReadinessWaitMode{ - `{"success":true}`: compileReadinessWaitWarmup, - `{"success":false,"errors":[{"message":"boom"}]}`: compileReadinessWaitNone, - `{"success":null,"message":"indeterminate"}`: compileReadinessWaitNone, - `{"message":"indeterminate"}`: compileReadinessWaitNone, + `{"Success":true}`: compileReadinessWaitWarmup, + `{"Success":false,"Errors":[{"Message":"boom"}]}`: compileReadinessWaitNone, + `{"Success":null,"Message":"indeterminate"}`: compileReadinessWaitNone, + `{"Message":"indeterminate"}`: compileReadinessWaitNone, } for result, expected := range cases { diff --git a/cli/internal/cli/control_play_mode_wait.go b/cli/internal/cli/control_play_mode_wait.go index f62d0ad2b3..f811d3a191 100644 --- a/cli/internal/cli/control_play_mode_wait.go +++ b/cli/internal/cli/control_play_mode_wait.go @@ -23,20 +23,20 @@ const ( var controlPlayModeStatePoll = 50 * time.Millisecond type controlPlayModeResponse struct { - IsPlaying bool `json:"isPlaying"` - IsPaused bool `json:"isPaused"` - Changed bool `json:"changed"` - WasAlreadyStopped bool `json:"wasAlreadyStopped"` - BlockedByCompileErrors bool `json:"blockedByCompileErrors"` - CompileErrorCount int `json:"compileErrorCount"` - CompileErrors []controlPlayModeCompileError `json:"compileErrors"` - Message string `json:"message"` + IsPlaying bool `json:"IsPlaying"` + IsPaused bool `json:"IsPaused"` + Changed bool `json:"Changed"` + WasAlreadyStopped bool `json:"WasAlreadyStopped"` + BlockedByCompileErrors bool `json:"BlockedByCompileErrors"` + CompileErrorCount int `json:"CompileErrorCount"` + CompileErrors []controlPlayModeCompileError `json:"CompileErrors"` + Message string `json:"Message"` } type controlPlayModeCompileError struct { - Message string `json:"message"` - File string `json:"file"` - Line int `json:"line"` + Message string `json:"Message"` + File string `json:"File"` + Line int `json:"Line"` } func shouldWaitForControlPlayModeState(command string, params map[string]any) bool { @@ -331,10 +331,10 @@ func controlPlayModeWaitTimeoutError( "Retry `uloop control-play-mode` after Unity finishes compiling, reloading scripts, or entering PlayMode.", }, Details: map[string]any{ - "requestedAction": action, - "isPlaying": response.IsPlaying, - "isPaused": response.IsPaused, - "timeoutSeconds": timeoutSeconds, + "RequestedAction": action, + "IsPlaying": response.IsPlaying, + "IsPaused": response.IsPaused, + "TimeoutSeconds": timeoutSeconds, }, } } @@ -362,10 +362,10 @@ func controlPlayModeCompileErrorsError( "Run `uloop compile` to verify the project compiles, then retry `uloop control-play-mode --action Play`.", }, Details: map[string]any{ - "requestedAction": action, - "compileErrorCount": compileErrorCount, - "compileErrors": response.CompileErrors, - "message": response.Message, + "RequestedAction": action, + "CompileErrorCount": compileErrorCount, + "CompileErrors": response.CompileErrors, + "Message": response.Message, }, } } diff --git a/cli/internal/cli/control_play_mode_wait_test.go b/cli/internal/cli/control_play_mode_wait_test.go index 30b5ad521b..8cd292c01f 100644 --- a/cli/internal/cli/control_play_mode_wait_test.go +++ b/cli/internal/cli/control_play_mode_wait_test.go @@ -36,8 +36,8 @@ func TestRunControlPlayModeWithStateWaitPollsStatusAfterStaleInitialResponse(t * requests, serverErr, []string{ - `{"isPlaying":false,"isPaused":false,"message":"Play mode started"}`, - `{"isPlaying":true,"isPaused":false,"message":"Play mode status"}`, + `{"IsPlaying":false,"IsPaused":false,"Message":"Play mode started"}`, + `{"IsPlaying":true,"IsPaused":false,"Message":"Play mode status"}`, }) connection := unityipc.Connection{ @@ -113,8 +113,8 @@ func TestRunControlPlayModeWithStateWaitPreservesStopChangeFields(t *testing.T) make(chan map[string]any, 2), serverErr, []string{ - `{"isPlaying":true,"isPaused":false,"changed":true,"wasAlreadyStopped":false,"message":"Play mode stopped"}`, - `{"isPlaying":false,"isPaused":false,"message":"Play mode status"}`, + `{"IsPlaying":true,"IsPaused":false,"Changed":true,"WasAlreadyStopped":false,"Message":"Play mode stopped"}`, + `{"IsPlaying":false,"IsPaused":false,"Message":"Play mode status"}`, }) connection := unityipc.Connection{ @@ -194,7 +194,7 @@ func TestRunControlPlayModeWithStateWaitFailsWhenStateNeverMatches(t *testing.T) go serveRepeatedControlPlayModeResponse( listener, serverErr, - `{"isPlaying":false,"isPaused":false,"message":"Play mode status"}`) + `{"IsPlaying":false,"IsPaused":false,"Message":"Play mode status"}`) connection := unityipc.Connection{ Endpoint: unityipc.Endpoint{ @@ -253,7 +253,7 @@ func TestRunControlPlayModeWithStateWaitFailsImmediatelyWhenCompileErrorsBlockPl requests, serverErr, []string{ - `{"isPlaying":false,"isPaused":false,"blockedByCompileErrors":true,"compileErrorCount":1,"compileErrors":[{"message":"CS1002: ; expected","file":"Assets/Scripts/Sample.cs","line":12}],"message":"Play mode could not start because Unity has compiler errors."}`, + `{"IsPlaying":false,"IsPaused":false,"BlockedByCompileErrors":true,"CompileErrorCount":1,"CompileErrors":[{"Message":"CS1002: ; expected","File":"Assets/Scripts/Sample.cs","Line":12}],"Message":"Play mode could not start because Unity has compiler errors."}`, }) connection := unityipc.Connection{ @@ -287,7 +287,7 @@ func TestRunControlPlayModeWithStateWaitFailsImmediatelyWhenCompileErrorsBlockPl if envelope.Error.ErrorCode != errorCodeControlPlayModeCompileErrors { t.Fatalf("error code mismatch: %#v", envelope.Error) } - if envelope.Error.Details["compileErrorCount"] != float64(1) { + if envelope.Error.Details["CompileErrorCount"] != float64(1) { t.Fatalf("compile error count mismatch: %#v", envelope.Error.Details) } if !bytes.Contains(stderr.Bytes(), []byte("CS1002")) { @@ -334,8 +334,8 @@ func TestRunControlPlayModeWithStateWaitFailsWhenCompileErrorsAppearDuringPollin requests, serverErr, []string{ - `{"isPlaying":false,"isPaused":false,"message":"Play mode started"}`, - `{"isPlaying":false,"isPaused":false,"blockedByCompileErrors":true,"compileErrorCount":1,"compileErrors":[{"message":"CS1525: invalid expression","file":"Assets/Scripts/Sample.cs","line":3}],"message":"Play mode could not start because Unity has compiler errors."}`, + `{"IsPlaying":false,"IsPaused":false,"Message":"Play mode started"}`, + `{"IsPlaying":false,"IsPaused":false,"BlockedByCompileErrors":true,"CompileErrorCount":1,"CompileErrors":[{"Message":"CS1525: invalid expression","File":"Assets/Scripts/Sample.cs","Line":3}],"Message":"Play mode could not start because Unity has compiler errors."}`, }) connection := unityipc.Connection{ diff --git a/cli/internal/cli/debug_timing.go b/cli/internal/cli/debug_timing.go index 7193f8fce7..3f172837b1 100644 --- a/cli/internal/cli/debug_timing.go +++ b/cli/internal/cli/debug_timing.go @@ -71,7 +71,7 @@ func extractUnityDebugTimings(command string, result json.RawMessage) []string { } var payload struct { - Timings []string `json:"timings"` + Timings []string `json:"Timings"` } if err := json.Unmarshal(result, &payload); err != nil { return nil diff --git a/cli/internal/cli/debug_timing_test.go b/cli/internal/cli/debug_timing_test.go index 92eb629447..c9160f81fc 100644 --- a/cli/internal/cli/debug_timing_test.go +++ b/cli/internal/cli/debug_timing_test.go @@ -87,7 +87,7 @@ func TestWriteDebugTiming_WhenUnityTimingsExist_ShouldWriteUnityTimingLines(t *t t.Setenv(debugTimingEnvName, "1") var stderr bytes.Buffer outcome := unityipc.UnitySendOutcome{ - Result: []byte(`{"success":true,"timings":["[Perf] Build: 12.3ms","[Perf] Execution: 4.5ms"]}`), + Result: []byte(`{"Success":true,"Timings":["[Perf] Build: 12.3ms","[Perf] Execution: 4.5ms"]}`), } writeDebugTiming(&stderr, executeDynamicCodeCommandName, time.Millisecond, outcome) @@ -108,31 +108,31 @@ func TestStripDebugTimingResult_WhenUnityTimingsExist_ShouldRemoveTimings(t *tes t.Setenv(debugTimingEnvName, "1") result := stripDebugTimingResult( executeDynamicCodeCommandName, - []byte(`{"success":true,"result":"ok","timings":["[Perf] Build: 12.3ms"]}`), + []byte(`{"Success":true,"Result":"ok","Timings":["[Perf] Build: 12.3ms"]}`), ) output := string(result) - if strings.Contains(output, "timings") { + if strings.Contains(output, "Timings") { t.Fatalf("Timings remained in sanitized result: %s", output) } - if !strings.Contains(output, `"result":"ok"`) { + if !strings.Contains(output, `"Result":"ok"`) { t.Fatalf("sanitized result lost normal fields: %s", output) } } -// Verifies that legacy-cased debug-only Unity timings are also removed from JSON stdout. -func TestStripDebugTimingResult_WhenLegacyUnityTimingsExist_ShouldRemoveTimings(t *testing.T) { +// Verifies that legacy lower-camel debug-only Unity timings are also removed from JSON stdout. +func TestStripDebugTimingResult_WhenLegacyLowerCamelUnityTimingsExist_ShouldRemoveTimings(t *testing.T) { t.Setenv(debugTimingEnvName, "1") result := stripDebugTimingResult( executeDynamicCodeCommandName, - []byte(`{"success":true,"result":"ok","Timings":["[Perf] Build: 12.3ms"]}`), + []byte(`{"Success":true,"Result":"ok","timings":["[Perf] Build: 12.3ms"]}`), ) output := string(result) - if strings.Contains(output, "Timings") { + if strings.Contains(output, "timings") { t.Fatalf("legacy Timings remained in sanitized result: %s", output) } - if !strings.Contains(output, `"result":"ok"`) { + if !strings.Contains(output, `"Result":"ok"`) { t.Fatalf("sanitized result lost normal fields: %s", output) } } diff --git a/cli/internal/cli/dynamic_code_wait.go b/cli/internal/cli/dynamic_code_wait.go index f31d0ce960..38dc8bc1f6 100644 --- a/cli/internal/cli/dynamic_code_wait.go +++ b/cli/internal/cli/dynamic_code_wait.go @@ -9,8 +9,8 @@ import ( const ( domainReloadWaitParam = "WaitForDomainReload" dynamicCodeCompileOnlyParam = "CompileOnly" - dynamicCodeDomainReloadWaitRequiredField = "domainReloadWaitRequired" - legacyDynamicCodeDomainReloadWaitRequiredField = "DomainReloadWaitRequired" + dynamicCodeDomainReloadWaitRequiredField = "DomainReloadWaitRequired" + legacyDynamicCodeDomainReloadWaitRequiredField = "domainReloadWaitRequired" ) func shouldWaitForExecuteDynamicCodeDomainReload(command string, params map[string]any) bool { @@ -34,7 +34,7 @@ func domainReloadWaitEnabled(params map[string]any, defaultValue bool) bool { func executeDynamicCodeDomainReloadWaitRequired(result []byte) bool { var payload struct { - DomainReloadWaitRequired bool `json:"domainReloadWaitRequired"` + DomainReloadWaitRequired bool `json:"DomainReloadWaitRequired"` } if err := json.Unmarshal(result, &payload); err != nil { return false diff --git a/cli/internal/cli/error_editor_unresponsive.go b/cli/internal/cli/error_editor_unresponsive.go index a6f94a96c6..ff8a6d9419 100644 --- a/cli/internal/cli/error_editor_unresponsive.go +++ b/cli/internal/cli/error_editor_unresponsive.go @@ -28,8 +28,8 @@ func unityEditorUnresponsiveError(err *unityipc.EditorUnresponsiveError, context "Close the modal dialog or wait for the Editor operation to finish, then retry the command.", }, Details: map[string]any{ - "stallSeconds": err.StallSeconds, - "cause": "Unity Editor main thread did not tick while the IPC heartbeat stayed alive.", + "StallSeconds": err.StallSeconds, + "Cause": "Unity Editor main thread did not tick while the IPC heartbeat stayed alive.", }, } } diff --git a/cli/internal/cli/error_envelope.go b/cli/internal/cli/error_envelope.go index 1f11c698af..289a268e3d 100644 --- a/cli/internal/cli/error_envelope.go +++ b/cli/internal/cli/error_envelope.go @@ -45,15 +45,15 @@ const ( ) type cliError struct { - ErrorCode string `json:"errorCode"` - Phase string `json:"phase"` - Message string `json:"message"` - Retryable bool `json:"retryable"` - SafeToRetry bool `json:"safeToRetry"` - ProjectRoot string `json:"projectRoot,omitempty"` - Command string `json:"command,omitempty"` - NextActions []string `json:"nextActions"` - Details map[string]any `json:"details,omitempty"` + ErrorCode string `json:"ErrorCode"` + Phase string `json:"Phase"` + Message string `json:"Message"` + Retryable bool `json:"Retryable"` + SafeToRetry bool `json:"SafeToRetry"` + ProjectRoot string `json:"ProjectRoot,omitempty"` + Command string `json:"Command,omitempty"` + NextActions []string `json:"NextActions"` + Details map[string]any `json:"Details,omitempty"` } func (err cliError) Error() string { @@ -61,8 +61,8 @@ func (err cliError) Error() string { } type cliErrorEnvelope struct { - Success bool `json:"success"` - Error cliError `json:"error"` + Success bool `json:"Success"` + Error cliError `json:"Error"` } type errorContext struct { @@ -134,7 +134,7 @@ func responseTimeoutAfterAcceptError(err error, context errorContext) cliError { "Retry after Unity finishes the command, compiling, reloading scripts, or restarting the bridge.", }, Details: map[string]any{ - "cause": err.Error(), + "Cause": err.Error(), }, } } @@ -175,8 +175,8 @@ func classifyError(err error, context errorContext) cliError { "Confirm that the command targets the intended Unity project and the Editor package is installed.", }, Details: map[string]any{ - "endpoint": notRespondingErr.endpoint, - "cause": notRespondingErr.causeText(), + "Endpoint": notRespondingErr.endpoint, + "Cause": notRespondingErr.causeText(), }, } } @@ -202,8 +202,8 @@ func classifyError(err error, context errorContext) cliError { "Confirm that the command targets the intended Unity project.", }, Details: map[string]any{ - "endpoint": connectionErr.Endpoint, - "cause": connectionAttemptCause(connectionErr), + "Endpoint": connectionErr.Endpoint, + "Cause": connectionAttemptCause(connectionErr), }, } } @@ -211,19 +211,19 @@ func classifyError(err error, context errorContext) cliError { var rpcErr *unityipc.RPCError if errors.As(err, &rpcErr) { details := map[string]any{ - "code": rpcErr.Code, - "message": rpcErr.Message, + "Code": rpcErr.Code, + "Message": rpcErr.Message, } var decodedData map[string]any if len(rpcErr.Data) > 0 { var data any if json.Unmarshal(rpcErr.Data, &data) == nil { - details["data"] = data + details["Data"] = data if typedData, ok := data.(map[string]any); ok { decodedData = typedData } } else { - details["data"] = string(rpcErr.Data) + details["Data"] = string(rpcErr.Data) } } if rpcDataType(decodedData) == "cli_update_required" { @@ -399,7 +399,7 @@ func disconnectedAfterAcceptError(err error, context errorContext) cliError { "Retry after Unity finishes compiling, reloading scripts, or restarting the bridge.", }, Details: map[string]any{ - "cause": err.Error(), + "Cause": err.Error(), }, } } @@ -418,7 +418,7 @@ func disconnectedAfterDispatchError(err error, context errorContext) cliError { "Retry after Unity finishes compiling, reloading scripts, or restarting the bridge.", }, Details: map[string]any{ - "cause": err.Error(), + "Cause": err.Error(), }, } } @@ -438,8 +438,8 @@ func unityServerNotRespondingAfterDispatchError(err unityServerNotRespondingErro "Run `uloop focus-window` if Unity appears stalled in the background.", }, Details: map[string]any{ - "endpoint": err.endpoint, - "cause": err.causeText(), + "Endpoint": err.endpoint, + "Cause": err.causeText(), }, } } @@ -458,7 +458,7 @@ func unknownCommandError(command string, cache toolsCache, context errorContext) "Run `uloop sync` if the local tool cache may be stale.", }, Details: map[string]any{ - "availableCommands": availableCommandNames(cache), + "AvailableCommands": availableCommandNames(cache), }, } } diff --git a/cli/internal/cli/error_envelope_test.go b/cli/internal/cli/error_envelope_test.go index 630ec34520..cc6684e5ae 100644 --- a/cli/internal/cli/error_envelope_test.go +++ b/cli/internal/cli/error_envelope_test.go @@ -23,9 +23,9 @@ func TestWriteErrorEnvelopeWritesMachineReadableJSON(t *testing.T) { Command: "sample", NextActions: []string{"Pass a valid boolean value for `--enabled`."}, Details: map[string]any{ - "option": "--enabled", - "received": "maybe", - "expectedType": "boolean", + "Option": "--enabled", + "Received": "maybe", + "ExpectedType": "boolean", }, }) @@ -39,7 +39,7 @@ func TestWriteErrorEnvelopeWritesMachineReadableJSON(t *testing.T) { if envelope.Error.ErrorCode != errorCodeInvalidArgument { t.Fatalf("error code mismatch: %#v", envelope.Error) } - if envelope.Error.Details["option"] != "--enabled" { + if envelope.Error.Details["Option"] != "--enabled" { t.Fatalf("details mismatch: %#v", envelope.Error.Details) } } @@ -68,7 +68,7 @@ func TestBuildToolParamsReturnsStructuredBooleanValueError(t *testing.T) { if cliErr.ErrorCode != errorCodeInvalidArgument { t.Fatalf("error code mismatch: %#v", cliErr) } - if cliErr.Details["expectedType"] != "flag" { + if cliErr.Details["ExpectedType"] != "flag" { t.Fatalf("details mismatch: %#v", cliErr.Details) } } @@ -103,7 +103,7 @@ func TestClassifyConnectionAttemptAllowsNilCause(t *testing.T) { if cliErr.ErrorCode != errorCodeUnityNotReachable { t.Fatalf("error code mismatch: %#v", cliErr) } - if cliErr.Details["cause"] != "" { + if cliErr.Details["Cause"] != "" { t.Fatalf("cause should be empty for nil unwrap: %#v", cliErr.Details) } } @@ -135,7 +135,7 @@ func TestClassifyUnityServerNotRespondingError(t *testing.T) { t.Fatalf("next action should not guide restart: %#v", cliErr.NextActions) } } - if cliErr.Details["endpoint"] != "/tmp/uloop/UnityCliLoop-sample.sock" { + if cliErr.Details["Endpoint"] != "/tmp/uloop/UnityCliLoop-sample.sock" { t.Fatalf("details mismatch: %#v", cliErr.Details) } } @@ -157,10 +157,10 @@ func TestClassifyEditorUnresponsiveError(t *testing.T) { if !cliErr.Retryable || !cliErr.SafeToRetry { t.Fatalf("retry flags mismatch: %#v", cliErr) } - if cliErr.Details["stallSeconds"] != float64(321) { + if cliErr.Details["StallSeconds"] != float64(321) { t.Fatalf("stall seconds details mismatch: %#v", cliErr.Details) } - if strings.Contains(cliErr.Details["cause"].(string), "launch -r") { + if strings.Contains(cliErr.Details["Cause"].(string), "launch -r") { t.Fatalf("cause should not include stale restart advice: %#v", cliErr.Details) } joinedActions := strings.Join(cliErr.NextActions, "\n") @@ -194,7 +194,7 @@ func TestClassifyLaunchStartupTimeoutError(t *testing.T) { t.Fatalf("next action should avoid package guesses and launch retry guidance: %#v", cliErr.NextActions) } } - if cliErr.Details["timeoutSeconds"] != 600 { + if cliErr.Details["TimeoutSeconds"] != 600 { t.Fatalf("timeout details mismatch: %#v", cliErr.Details) } } @@ -216,7 +216,7 @@ func TestClassifyLaunchProcessExitTimeoutError(t *testing.T) { if !cliErr.Retryable || !cliErr.SafeToRetry { t.Fatalf("process exit timeout should be retryable: %#v", cliErr) } - if cliErr.Details["pid"] != 123 { + if cliErr.Details["Pid"] != 123 { t.Fatalf("pid details mismatch: %#v", cliErr.Details) } } @@ -263,7 +263,7 @@ func TestClassifyRPCErrorKeepsData(t *testing.T) { if cliErr.ErrorCode != errorCodeUnityRPCError { t.Fatalf("error code mismatch: %#v", cliErr) } - data, ok := cliErr.Details["data"].(map[string]any) + data, ok := cliErr.Details["Data"].(map[string]any) if !ok { t.Fatalf("rpc data missing: %#v", cliErr.Details) } @@ -334,7 +334,7 @@ func TestClassifyServerBusyRPCError(t *testing.T) { if cliErr.Message != expectedMessage { t.Fatalf("message mismatch: %s", cliErr.Message) } - data, ok := cliErr.Details["data"].(map[string]any) + data, ok := cliErr.Details["Data"].(map[string]any) if !ok { t.Fatalf("busy data missing: %#v", cliErr.Details) } @@ -380,6 +380,40 @@ func TestWriteClassifiedServerBusyRPCErrorWritesBusyStatus(t *testing.T) { } } +func TestWriteErrorEnvelopeServerBusyAcceptsLegacyLowercaseDataDetails(t *testing.T) { + // Verifies legacy lower-camel error details still preserve busy tool names in status output. + cliErr := cliError{ + ErrorCode: errorCodeUnityServerBusy, + Message: "Unity is busy.", + Command: "get-logs", + Details: map[string]any{ + "data": map[string]any{ + "runningToolName": "compile", + "requestedToolName": "get-logs", + "isPlaying": true, + "isPaused": false, + }, + }, + } + var stderr bytes.Buffer + + writeErrorEnvelope(&stderr, cliErr) + + var envelope cliStatusEnvelope + if err := json.Unmarshal(stderr.Bytes(), &envelope); err != nil { + t.Fatalf("stderr is not valid JSON: %v\n%s", err, stderr.String()) + } + if envelope.RunningToolName != "compile" || envelope.RequestedToolName != "get-logs" { + t.Fatalf("tool names mismatch: %#v", envelope) + } + if envelope.IsPlaying == nil || *envelope.IsPlaying != true { + t.Fatalf("isPlaying mismatch: %#v", envelope) + } + if envelope.IsPaused == nil || *envelope.IsPaused != false { + t.Fatalf("isPaused mismatch: %#v", envelope) + } +} + func TestWriteToolFailureClassifiesDispatchedDisconnect(t *testing.T) { var stderr bytes.Buffer @@ -466,7 +500,7 @@ func TestUnknownCommandErrorIncludesAvailableCommands(t *testing.T) { if cliErr.ErrorCode != errorCodeUnknownCommand { t.Fatalf("error code mismatch: %#v", cliErr) } - available, ok := cliErr.Details["availableCommands"].([]string) + available, ok := cliErr.Details["AvailableCommands"].([]string) if !ok { t.Fatalf("available commands missing: %#v", cliErr.Details) } diff --git a/cli/internal/cli/focus.go b/cli/internal/cli/focus.go index 089ffe0fb9..96a02fafbb 100644 --- a/cli/internal/cli/focus.go +++ b/cli/internal/cli/focus.go @@ -31,8 +31,8 @@ type unityProcess struct { } type focusResponse struct { - Success bool `json:"success"` - Message string `json:"message"` + Success bool `json:"Success"` + Message string `json:"Message"` } type restoreFocusFunc func(context.Context) error diff --git a/cli/internal/cli/help_test.go b/cli/internal/cli/help_test.go index 8876556095..1654a82747 100644 --- a/cli/internal/cli/help_test.go +++ b/cli/internal/cli/help_test.go @@ -61,10 +61,10 @@ func TestRunProjectLocalVersionJSONIncludesProtocolVersion(t *testing.T) { if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil { t.Fatalf("version json output is not JSON: %v\n%s", err, stdout.String()) } - if payload["cliVersion"] != version { + if payload["CliVersion"] != version { t.Fatalf("cliVersion mismatch: %#v", payload) } - if payload["protocolVersion"] != float64(protocolVersion) { + if payload["ProtocolVersion"] != float64(protocolVersion) { t.Fatalf("protocolVersion mismatch: %#v", payload) } } diff --git a/cli/internal/cli/install.go b/cli/internal/cli/install.go index 9bcde2e330..bb1fe946d4 100644 --- a/cli/internal/cli/install.go +++ b/cli/internal/cli/install.go @@ -65,10 +65,10 @@ func tryHandleInstallRequest(ctx context.Context, args []string, stdout io.Write func installSetupFailureError(err error, installerStderr string) cliError { details := map[string]any{ - "cause": err.Error(), + "Cause": err.Error(), } if stderrText := strings.TrimSpace(installerStderr); stderrText != "" { - details["installerStderr"] = stderrText + details["InstallerStderr"] = stderrText } return cliError{ diff --git a/cli/internal/cli/install_test.go b/cli/internal/cli/install_test.go index e4ef4aac8c..9cf8e39e01 100644 --- a/cli/internal/cli/install_test.go +++ b/cli/internal/cli/install_test.go @@ -127,10 +127,10 @@ func TestInstallSetupFailureErrorIncludesInstallerStderr(t *testing.T) { if cliErr.ErrorCode != errorCodeInternalError { t.Fatalf("error code mismatch: %#v", cliErr) } - if cliErr.Details["cause"] != context.Canceled.Error() { + if cliErr.Details["Cause"] != context.Canceled.Error() { t.Fatalf("cause detail mismatch: %#v", cliErr.Details) } - if cliErr.Details["installerStderr"] != "warning before failure" { + if cliErr.Details["InstallerStderr"] != "warning before failure" { t.Fatalf("installer stderr detail mismatch: %#v", cliErr.Details) } } diff --git a/cli/internal/cli/launch_process_exit_timeout_error.go b/cli/internal/cli/launch_process_exit_timeout_error.go index 7ffa9a0056..21b05b788a 100644 --- a/cli/internal/cli/launch_process_exit_timeout_error.go +++ b/cli/internal/cli/launch_process_exit_timeout_error.go @@ -30,8 +30,8 @@ func unityProcessExitTimeoutCLIError(err launchProcessExitTimeoutError, context "If Unity remains visible or keeps project files locked, close the Unity process from the OS and retry.", }, Details: map[string]any{ - "pid": err.pid, - "timeoutSeconds": int(err.timeout.Seconds()), + "Pid": err.pid, + "TimeoutSeconds": int(err.timeout.Seconds()), }, } } diff --git a/cli/internal/cli/launch_ready.go b/cli/internal/cli/launch_ready.go index 0f0cfd1046..cb1ecc0d88 100644 --- a/cli/internal/cli/launch_ready.go +++ b/cli/internal/cli/launch_ready.go @@ -15,18 +15,18 @@ const ( ) type launchReadyResponse struct { - Success bool `json:"success"` - Ready bool `json:"ready"` - ServerReady bool `json:"serverReady"` - ProjectIpcReady bool `json:"projectIpcReady"` - AlreadyRunning bool `json:"alreadyRunning"` - Launched bool `json:"launched"` - Restarted bool `json:"restarted"` - Quit bool `json:"quit"` - PreviousProcessId *int `json:"previousProcessId,omitempty"` - CurrentProcessId *int `json:"currentProcessId,omitempty"` - ProjectRoot string `json:"projectRoot"` - Message string `json:"message"` + Success bool `json:"Success"` + Ready bool `json:"Ready"` + ServerReady bool `json:"ServerReady"` + ProjectIpcReady bool `json:"ProjectIpcReady"` + AlreadyRunning bool `json:"AlreadyRunning"` + Launched bool `json:"Launched"` + Restarted bool `json:"Restarted"` + Quit bool `json:"Quit"` + PreviousProcessId *int `json:"PreviousProcessId,omitempty"` + CurrentProcessId *int `json:"CurrentProcessId,omitempty"` + ProjectRoot string `json:"ProjectRoot"` + Message string `json:"Message"` } func writeLaunchReadinessWait(stdout io.Writer, spinner *terminalSpinner) { diff --git a/cli/internal/cli/launch_startup_timeout_error.go b/cli/internal/cli/launch_startup_timeout_error.go index 6c479f3e35..da30583898 100644 --- a/cli/internal/cli/launch_startup_timeout_error.go +++ b/cli/internal/cli/launch_startup_timeout_error.go @@ -43,10 +43,10 @@ func waitForLaunchReadiness(ctx context.Context, projectRoot string) error { func unityStartupTimeoutCLIError(err launchStartupTimeoutError, context errorContext) cliError { projectRoot := firstNonEmpty(context.projectRoot, err.projectRoot) details := map[string]any{ - "timeoutSeconds": int(launchReadinessTimeout.Seconds()), + "TimeoutSeconds": int(launchReadinessTimeout.Seconds()), } if err.cause != nil { - details["cause"] = err.cause.Error() + details["Cause"] = err.cause.Error() } return cliError{ ErrorCode: errorCodeUnityStartupTimeout, diff --git a/cli/internal/cli/launch_test.go b/cli/internal/cli/launch_test.go index c33f9b4d52..1593903eef 100644 --- a/cli/internal/cli/launch_test.go +++ b/cli/internal/cli/launch_test.go @@ -159,11 +159,11 @@ func TestRunLaunchWritesReadyResponseAfterToolReadiness(t *testing.T) { output := stdout.String() for _, expected := range []string{ "Waiting for Unity CLI Loop server readiness...", - `"success": true`, - `"ready": true`, - `"serverReady": true`, - `"projectIpcReady": true`, - `"message": "Unity CLI Loop is ready."`, + `"Success": true`, + `"Ready": true`, + `"ServerReady": true`, + `"ProjectIpcReady": true`, + `"Message": "Unity CLI Loop is ready."`, } { if !strings.Contains(output, expected) { t.Fatalf("launch output missing %q:\n%s", expected, output) diff --git a/cli/internal/cli/list_output.go b/cli/internal/cli/list_output.go index a94a5bc6d9..91c05e6dd4 100644 --- a/cli/internal/cli/list_output.go +++ b/cli/internal/cli/list_output.go @@ -6,24 +6,24 @@ import ( ) type listCatalog struct { - Version string `json:"version,omitempty"` - ServerVersion string `json:"serverVersion,omitempty"` - UpdatedAt string `json:"updatedAt,omitempty"` - Tools []listTool `json:"tools"` + Version string `json:"Version,omitempty"` + ServerVersion string `json:"ServerVersion,omitempty"` + UpdatedAt string `json:"UpdatedAt,omitempty"` + Tools []listTool `json:"Tools"` } type listTool struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - Options []listOption `json:"options"` + Name string `json:"Name"` + Description string `json:"Description,omitempty"` + Options []listOption `json:"Options"` } type listOption struct { - Name string `json:"name"` - Type string `json:"type,omitempty"` - Description string `json:"description,omitempty"` - Default any `json:"default,omitempty"` - Values []string `json:"values,omitempty"` + Name string `json:"Name"` + Type string `json:"Type,omitempty"` + Description string `json:"Description,omitempty"` + Default any `json:"Default,omitempty"` + Values []string `json:"Values,omitempty"` } func formatToolListResult(result json.RawMessage) json.RawMessage { diff --git a/cli/internal/cli/pause_point_errors.go b/cli/internal/cli/pause_point_errors.go index 42769bde25..99017a6d38 100644 --- a/cli/internal/cli/pause_point_errors.go +++ b/cli/internal/cli/pause_point_errors.go @@ -32,7 +32,7 @@ func pausePointWaitError( true) hint := pausePointExpiredHint(response) if hint != "" { - expiredError.Details["hint"] = hint + expiredError.Details["Hint"] = hint } return expiredError case pausePointWaitStateCleared: @@ -53,7 +53,7 @@ func pausePointWaitError( true) hint := pausePointTimeoutHint(response) if hint != "" { - timeoutError.Details["hint"] = hint + timeoutError.Details["Hint"] = hint } return timeoutError } @@ -114,22 +114,22 @@ func pausePointStateError( NextActions: []string{ "Run `uloop enable-pause-point --id ` before waiting.", "Confirm the code path calls `UloopPausePoint.Pause(\"\")` with the same id.", - "Check `details.status`, `details.editorState`, `details.elapsedSinceEnabledMilliseconds`, and `details.remainingMilliseconds` to distinguish a missed code path from an already-paused Editor.", + "Check `Details.Status`, `Details.EditorState`, `Details.ElapsedSinceEnabledMilliseconds`, and `Details.RemainingMilliseconds` to distinguish a missed code path from an already-paused Editor.", "If the marker is inside a custom asmdef, add a reference to `UnityCLILoop.PausePoints.Runtime`.", }, Details: map[string]any{ - "id": options.id, - "status": response.Status, - "expired": response.Expired, - "hitCount": response.HitCount, - "timeoutSeconds": pausePointMarkerTimeoutSeconds(options, response), - "enabledAtUtc": response.EnabledAtUtc, - "elapsedSinceEnabledMilliseconds": response.ElapsedSinceEnabledMilliseconds, - "generation": response.Generation, - "editorState": response.EditorState, - "remainingMilliseconds": pausePointRemainingMilliseconds(options, response), - "markerMessage": response.Message, - "recommendedNextAction": response.RecommendedNextAction, + "Id": options.id, + "Status": response.Status, + "Expired": response.Expired, + "HitCount": response.HitCount, + "TimeoutSeconds": pausePointMarkerTimeoutSeconds(options, response), + "EnabledAtUtc": response.EnabledAtUtc, + "ElapsedSinceEnabledMilliseconds": response.ElapsedSinceEnabledMilliseconds, + "Generation": response.Generation, + "EditorState": response.EditorState, + "RemainingMilliseconds": pausePointRemainingMilliseconds(options, response), + "MarkerMessage": response.Message, + "RecommendedNextAction": response.RecommendedNextAction, }, } } diff --git a/cli/internal/cli/pause_point_logs.go b/cli/internal/cli/pause_point_logs.go index fba9ffd404..8bb57ebc59 100644 --- a/cli/internal/cli/pause_point_logs.go +++ b/cli/internal/cli/pause_point_logs.go @@ -18,60 +18,60 @@ const ( var fetchMatchingLogs = fetchMatchingLogsFromUnity type pausePointMatchingLog struct { - Type string `json:"type"` - Message string `json:"message"` + Type string `json:"Type"` + Message string `json:"Message"` } type pausePointMatchingLogsResult struct { - SearchText string `json:"searchText"` - TotalCount int `json:"totalCount"` - DisplayedCount int `json:"displayedCount"` - MaxCount int `json:"maxCount"` - Logs []pausePointMatchingLog `json:"logs"` + SearchText string `json:"SearchText"` + TotalCount int `json:"TotalCount"` + DisplayedCount int `json:"DisplayedCount"` + MaxCount int `json:"MaxCount"` + Logs []pausePointMatchingLog `json:"Logs"` } type pausePointEvidenceSummary struct { - EditorState pausePointEditorState `json:"editorState"` - PausePoint pausePointEvidencePausePoint `json:"pausePoint"` - MatchingLogs pausePointEvidenceLogs `json:"matchingLogs"` - Warning string `json:"warning"` + EditorState pausePointEditorState `json:"EditorState"` + PausePoint pausePointEvidencePausePoint `json:"PausePoint"` + MatchingLogs pausePointEvidenceLogs `json:"MatchingLogs"` + Warning string `json:"Warning"` } type pausePointEvidencePausePoint struct { - Id string `json:"id"` - Status string `json:"status"` - Generation int `json:"generation"` - HitCount int `json:"hitCount"` - MultipleHitsObserved bool `json:"multipleHitsObserved"` - FirstHitAtUtc string `json:"firstHitAtUtc"` - LastHitAtUtc string `json:"lastHitAtUtc"` - FirstHitSequence int `json:"firstHitSequence"` - LastHitSequence int `json:"lastHitSequence"` + Id string `json:"Id"` + Status string `json:"Status"` + Generation int `json:"Generation"` + HitCount int `json:"HitCount"` + MultipleHitsObserved bool `json:"MultipleHitsObserved"` + FirstHitAtUtc string `json:"FirstHitAtUtc"` + LastHitAtUtc string `json:"LastHitAtUtc"` + FirstHitSequence int `json:"FirstHitSequence"` + LastHitSequence int `json:"LastHitSequence"` } type pausePointEvidenceLogs struct { - SearchText string `json:"searchText"` - MatchingLogCount int `json:"matchingLogCount"` - ReturnedLogCount int `json:"returnedLogCount"` - MaxCount int `json:"maxCount"` - MayBeTruncated bool `json:"mayBeTruncated"` - MultipleMatchingLogsObserved bool `json:"multipleMatchingLogsObserved"` + SearchText string `json:"SearchText"` + MatchingLogCount int `json:"MatchingLogCount"` + ReturnedLogCount int `json:"ReturnedLogCount"` + MaxCount int `json:"MaxCount"` + MayBeTruncated bool `json:"MayBeTruncated"` + MultipleMatchingLogsObserved bool `json:"MultipleMatchingLogsObserved"` } // pausePointWaitResult extends the hit response with marker-matching logs and // evidence summary so agents do not need a separate get-logs call while Unity is paused. type pausePointWaitResult struct { pausePointStatusResponse - MatchingLogs []pausePointMatchingLog `json:"matchingLogs"` - EvidenceSummary pausePointEvidenceSummary `json:"evidenceSummary"` + MatchingLogs []pausePointMatchingLog `json:"MatchingLogs"` + EvidenceSummary pausePointEvidenceSummary `json:"EvidenceSummary"` } type pausePointGetLogsResponse struct { - TotalCount int `json:"totalCount"` - DisplayedCount int `json:"displayedCount"` - MaxCount int `json:"maxCount"` - SearchText string `json:"searchText"` - Logs []pausePointMatchingLog `json:"logs"` + TotalCount int `json:"TotalCount"` + DisplayedCount int `json:"DisplayedCount"` + MaxCount int `json:"MaxCount"` + SearchText string `json:"SearchText"` + Logs []pausePointMatchingLog `json:"Logs"` } func fetchMatchingLogsFromUnity( @@ -173,7 +173,7 @@ func buildPausePointEvidenceWarning(logs pausePointMatchingLogsResult, hitCount if evidenceLogs.MultipleMatchingLogsObserved { warnings = append( warnings, - "Multiple matching logs were observed for this pause point id; inspect matchingLogs before treating the scenario as single-fire evidence.") + "Multiple matching logs were observed for this pause point id; inspect MatchingLogs before treating the scenario as single-fire evidence.") } if hitCount > 1 { warnings = append( diff --git a/cli/internal/cli/pause_point_wait.go b/cli/internal/cli/pause_point_wait.go index c18881b374..487cffad5a 100644 --- a/cli/internal/cli/pause_point_wait.go +++ b/cli/internal/cli/pause_point_wait.go @@ -46,30 +46,30 @@ type pausePointStatusOptions struct { } type pausePointStatusResponse struct { - Id string `json:"id"` - Status string `json:"status"` - IsEnabled bool `json:"isEnabled"` - IsHit bool `json:"isHit"` - HitCount int `json:"hitCount"` - TimeoutSeconds int `json:"timeoutSeconds"` - Expired bool `json:"expired"` - EnabledAtUtc string `json:"enabledAtUtc"` - ElapsedSinceEnabledMilliseconds int64 `json:"elapsedSinceEnabledMilliseconds"` - RemainingMilliseconds int64 `json:"remainingMilliseconds"` - Generation int `json:"generation"` - EditorState pausePointEditorState `json:"editorState"` - FirstHitAtUtc string `json:"firstHitAtUtc"` - LastHitAtUtc string `json:"lastHitAtUtc"` - FirstHitSequence int `json:"firstHitSequence"` - LastHitSequence int `json:"lastHitSequence"` - Message string `json:"message"` - RecommendedNextAction string `json:"recommendedNextAction"` + Id string `json:"Id"` + Status string `json:"Status"` + IsEnabled bool `json:"IsEnabled"` + IsHit bool `json:"IsHit"` + HitCount int `json:"HitCount"` + TimeoutSeconds int `json:"TimeoutSeconds"` + Expired bool `json:"Expired"` + EnabledAtUtc string `json:"EnabledAtUtc"` + ElapsedSinceEnabledMilliseconds int64 `json:"ElapsedSinceEnabledMilliseconds"` + RemainingMilliseconds int64 `json:"RemainingMilliseconds"` + Generation int `json:"Generation"` + EditorState pausePointEditorState `json:"EditorState"` + FirstHitAtUtc string `json:"FirstHitAtUtc"` + LastHitAtUtc string `json:"LastHitAtUtc"` + FirstHitSequence int `json:"FirstHitSequence"` + LastHitSequence int `json:"LastHitSequence"` + Message string `json:"Message"` + RecommendedNextAction string `json:"RecommendedNextAction"` } type pausePointEditorState struct { - IsPlaying bool `json:"isPlaying"` - IsPaused bool `json:"isPaused"` - CapturedAt string `json:"capturedAt"` + IsPlaying bool `json:"IsPlaying"` + IsPaused bool `json:"IsPaused"` + CapturedAt string `json:"CapturedAt"` } type pausePointWaitState string @@ -219,8 +219,8 @@ func runWaitForPausePoint( // Best-effort: the timeout diagnosis must not depend on a second Unity round trip succeeding. logs, logsErr := fetchMatchingLogs(ctx, connection, options.id, options.matchingLogsMaxCount) if logsErr == nil { - waitErr.Details["matchingLogs"] = logs.Logs - waitErr.Details["evidenceSummary"] = buildPausePointEvidenceSummary(response, logs) + waitErr.Details["MatchingLogs"] = logs.Logs + waitErr.Details["EvidenceSummary"] = buildPausePointEvidenceSummary(response, logs) } } writeErrorEnvelope(stderr, waitErr) diff --git a/cli/internal/cli/pause_point_wait_test.go b/cli/internal/cli/pause_point_wait_test.go index 8a349e049e..43eebdad50 100644 --- a/cli/internal/cli/pause_point_wait_test.go +++ b/cli/internal/cli/pause_point_wait_test.go @@ -121,17 +121,17 @@ func TestRunWaitForPausePointClearsEnabledMarkerAfterTimeout(t *testing.T) { t.Fatalf("timeout error missing from stderr: %s", stderr.String()) } envelope := parsePausePointErrorEnvelope(t, stderr.Bytes()) - editorState, ok := envelope.Error.Details["editorState"].(map[string]any) - if !ok || editorState["isPlaying"] != true || editorState["isPaused"] != false || editorState["capturedAt"] != "Current" { + editorState, ok := envelope.Error.Details["EditorState"].(map[string]any) + if !ok || editorState["IsPlaying"] != true || editorState["IsPaused"] != false || editorState["CapturedAt"] != "Current" { t.Fatalf("editorState detail mismatch: %#v", envelope.Error.Details) } - if envelope.Error.Details["markerMessage"] != "Pause point enabled." { + if envelope.Error.Details["MarkerMessage"] != "Pause point enabled." { t.Fatalf("markerMessage detail mismatch: %#v", envelope.Error.Details) } - if envelope.Error.Details["elapsedSinceEnabledMilliseconds"] != float64(100) { + if envelope.Error.Details["ElapsedSinceEnabledMilliseconds"] != float64(100) { t.Fatalf("elapsedSinceEnabledMilliseconds detail mismatch: %#v", envelope.Error.Details) } - if envelope.Error.Details["remainingMilliseconds"] != float64(900) { + if envelope.Error.Details["RemainingMilliseconds"] != float64(900) { t.Fatalf("remainingMilliseconds detail mismatch: %#v", envelope.Error.Details) } } @@ -157,16 +157,16 @@ func TestPausePointExpiredErrorReportsRecoveryFields(t *testing.T) { timeoutSeconds: 1, }, response, pausePointWaitStateExpired) - if cliErr.Details["expired"] != true { + if cliErr.Details["Expired"] != true { t.Fatalf("expired detail mismatch: %#v", cliErr.Details) } - if cliErr.Details["enabledAtUtc"] != "2026-06-03T00:00:00.0000000Z" { + if cliErr.Details["EnabledAtUtc"] != "2026-06-03T00:00:00.0000000Z" { t.Fatalf("enabledAtUtc detail mismatch: %#v", cliErr.Details) } - if cliErr.Details["generation"] != 7 { + if cliErr.Details["Generation"] != 7 { t.Fatalf("generation detail mismatch: %#v", cliErr.Details) } - if cliErr.Details["recommendedNextAction"] != response.RecommendedNextAction { + if cliErr.Details["RecommendedNextAction"] != response.RecommendedNextAction { t.Fatalf("recommendedNextAction detail mismatch: %#v", cliErr.Details) } } @@ -186,7 +186,7 @@ func TestPausePointExpiredErrorReportsMarkerTimeoutSeconds(t *testing.T) { timeoutSeconds: 5, }, response, pausePointWaitStateExpired) - if cliErr.Details["timeoutSeconds"] != 30 { + if cliErr.Details["TimeoutSeconds"] != 30 { t.Fatalf("timeoutSeconds detail mismatch: %#v", cliErr.Details) } } @@ -205,7 +205,7 @@ func TestPausePointExpiredErrorDerivesExpiredFromStatus(t *testing.T) { timeoutSeconds: 1, }, response, pausePointWaitStateExpired) - if cliErr.Details["expired"] != true { + if cliErr.Details["Expired"] != true { t.Fatalf("expired detail mismatch: %#v", cliErr.Details) } } @@ -348,11 +348,11 @@ func TestRunWaitForPausePointReportsNotEnabledError(t *testing.T) { if envelope.Error.ErrorCode != errorCodePausePointNotEnabled { t.Fatalf("error code mismatch: %#v", envelope.Error) } - if envelope.Error.Details["status"] != pausePointStatusNotEnabled { + if envelope.Error.Details["Status"] != pausePointStatusNotEnabled { t.Fatalf("status detail mismatch: %#v", envelope.Error.Details) } - editorState, ok := envelope.Error.Details["editorState"].(map[string]any) - if !ok || editorState["isPlaying"] != true || editorState["isPaused"] != false || editorState["capturedAt"] != "Current" { + editorState, ok := envelope.Error.Details["EditorState"].(map[string]any) + if !ok || editorState["IsPlaying"] != true || editorState["IsPaused"] != false || editorState["CapturedAt"] != "Current" { t.Fatalf("editorState details mismatch: %#v", envelope.Error.Details) } } @@ -579,10 +579,10 @@ func TestRunWaitForPausePointIgnoresLogFetchFailure(t *testing.T) { if code != 0 { t.Fatalf("expected success despite log fetch failure, got %d with stderr %s", code, stderr.String()) } - if strings.Contains(stdout.String(), "matchingLogs") { + if strings.Contains(stdout.String(), "MatchingLogs") { t.Fatalf("MatchingLogs must be omitted when the fetch fails: %s", stdout.String()) } - if strings.Contains(stdout.String(), "evidenceSummary") { + if strings.Contains(stdout.String(), "EvidenceSummary") { t.Fatalf("EvidenceSummary must be omitted when the fetch fails: %s", stdout.String()) } } @@ -649,16 +649,16 @@ func TestRunWaitForPausePointEmbedsMatchingLogsOnTimeout(t *testing.T) { } envelope := parsePausePointErrorEnvelope(t, stderr.Bytes()) // The detail key mirrors the hit-response field name, so one spelling covers both surfaces. - matchingLogs, ok := envelope.Error.Details["matchingLogs"].([]any) + matchingLogs, ok := envelope.Error.Details["MatchingLogs"].([]any) if !ok || len(matchingLogs) != 1 { t.Fatalf("MatchingLogs detail mismatch: %#v", envelope.Error.Details) } - evidenceSummary, ok := envelope.Error.Details["evidenceSummary"].(map[string]any) + evidenceSummary, ok := envelope.Error.Details["EvidenceSummary"].(map[string]any) if !ok { t.Fatalf("EvidenceSummary detail missing: %#v", envelope.Error.Details) } - evidenceLogs, ok := evidenceSummary["matchingLogs"].(map[string]any) - if !ok || evidenceLogs["mayBeTruncated"] != true || evidenceLogs["matchingLogCount"] != float64(3) { + evidenceLogs, ok := evidenceSummary["MatchingLogs"].(map[string]any) + if !ok || evidenceLogs["MayBeTruncated"] != true || evidenceLogs["MatchingLogCount"] != float64(3) { t.Fatalf("EvidenceSummary matching logs mismatch: %#v", evidenceSummary) } } @@ -703,7 +703,7 @@ func TestPausePointTimeoutErrorIncludesDiagnosisHint(t *testing.T) { timeoutSeconds: 1, }, testCase.response, pausePointWaitStateTimeout) - if cliErr.Details["hint"] != testCase.wantHint { + if cliErr.Details["Hint"] != testCase.wantHint { t.Fatalf("hint mismatch: %#v", cliErr.Details) } }) @@ -751,7 +751,7 @@ func TestPausePointExpiredErrorIncludesDiagnosisHint(t *testing.T) { timeoutSeconds: 1, }, testCase.response, pausePointWaitStateExpired) - if cliErr.Details["hint"] != testCase.wantHint { + if cliErr.Details["Hint"] != testCase.wantHint { t.Fatalf("hint mismatch: %#v", cliErr.Details) } }) @@ -770,7 +770,7 @@ func TestPausePointHintIsOmittedOutsideDiagnosableStates(t *testing.T) { id: "jump", timeoutSeconds: 1, }, hitResponse, pausePointWaitStateTimeout) - if _, exists := timeoutErr.Details["hint"]; exists { + if _, exists := timeoutErr.Details["Hint"]; exists { t.Fatalf("hint should be omitted when no diagnosis applies: %#v", timeoutErr.Details) } @@ -783,7 +783,7 @@ func TestPausePointHintIsOmittedOutsideDiagnosableStates(t *testing.T) { id: "jump", timeoutSeconds: 1, }, clearedResponse, pausePointWaitStateCleared) - if _, exists := clearedErr.Details["hint"]; exists { + if _, exists := clearedErr.Details["Hint"]; exists { t.Fatalf("hint should be omitted for cleared markers: %#v", clearedErr.Details) } } @@ -807,7 +807,7 @@ func TestPausePointExpiredErrorReportsNoRemainingTime(t *testing.T) { if cliErr.ErrorCode != errorCodePausePointExpired { t.Fatalf("error code mismatch: %#v", cliErr) } - if cliErr.Details["remainingMilliseconds"] != int64(0) { + if cliErr.Details["RemainingMilliseconds"] != int64(0) { t.Fatalf("remainingMilliseconds detail mismatch: %#v", cliErr.Details) } } diff --git a/cli/internal/cli/run.go b/cli/internal/cli/run.go index daf1f6228f..8d776be47f 100644 --- a/cli/internal/cli/run.go +++ b/cli/internal/cli/run.go @@ -164,8 +164,8 @@ func RunProjectLocal(ctx context.Context, args []string, stdout io.Writer, stder func writeVersionJSON(stdout io.Writer) { content, err := json.Marshal(map[string]any{ - "cliVersion": version, - "protocolVersion": protocolVersion, + "CliVersion": version, + "ProtocolVersion": protocolVersion, }) if err != nil { panic(err) @@ -407,7 +407,7 @@ func writeJSON(stdout io.Writer, result json.RawMessage) { } type compileResultStatus struct { - Success *bool `json:"success"` + Success *bool `json:"Success"` } type compileReadinessWaitMode int diff --git a/cli/internal/cli/tool_readiness.go b/cli/internal/cli/tool_readiness.go index b12493eb8f..7122139011 100644 --- a/cli/internal/cli/tool_readiness.go +++ b/cli/internal/cli/tool_readiness.go @@ -147,8 +147,8 @@ func executeDynamicCodeReadinessProbeParams() map[string]any { } type executeDynamicCodeReadinessResponse struct { - Success bool `json:"success"` - ErrorMessage string `json:"errorMessage"` + Success bool `json:"Success"` + ErrorMessage string `json:"ErrorMessage"` } func isExecuteDynamicCodeAvailable(projectRoot string) bool { diff --git a/cli/internal/cli/uninstall.go b/cli/internal/cli/uninstall.go index 150e7d6e58..a8e0693d28 100644 --- a/cli/internal/cli/uninstall.go +++ b/cli/internal/cli/uninstall.go @@ -65,7 +65,7 @@ func tryHandleUninstallRequest(ctx context.Context, args []string, stdout io.Wri Command: uninstallCommandName, NextActions: []string{"Retry `uloop uninstall` after checking file permissions."}, Details: map[string]any{ - "cause": err.Error(), + "Cause": err.Error(), }, }) return true, 1 diff --git a/cli/internal/cli/update.go b/cli/internal/cli/update.go index 8ce2486cbd..bdb341a5c0 100644 --- a/cli/internal/cli/update.go +++ b/cli/internal/cli/update.go @@ -57,7 +57,7 @@ func tryHandleUpdateRequest(ctx context.Context, args []string, stdout io.Writer Command: updateCommandName, NextActions: []string{"Retry `uloop update` after checking network access to GitHub."}, Details: map[string]any{ - "cause": err.Error(), + "Cause": err.Error(), }, }) return true, 1 diff --git a/scripts/refresh-neighbor-game-skills-macos.sh b/scripts/refresh-neighbor-game-skills-macos.sh index daa8574e5e..e84b8e0f54 100644 --- a/scripts/refresh-neighbor-game-skills-macos.sh +++ b/scripts/refresh-neighbor-game-skills-macos.sh @@ -393,7 +393,7 @@ return SceneManager.GetActiveScene().path; fail "failed to execute sample scene open command: $project" fi - if ! printf '%s\n' "$response" | jq -e --arg scene_path "$sample_scene_path" '.success == true and .result == $scene_path' >/dev/null; then + if ! printf '%s\n' "$response" | jq -e --arg scene_path "$sample_scene_path" '.Success == true and .Result == $scene_path' >/dev/null; then printf '%s\n' "$response" >&2 fail "failed to open sample scene: $project" fi diff --git a/scripts/refresh-neighbor-game-skills-windows.ps1 b/scripts/refresh-neighbor-game-skills-windows.ps1 index 2139eaf610..e341d36dd0 100644 --- a/scripts/refresh-neighbor-game-skills-windows.ps1 +++ b/scripts/refresh-neighbor-game-skills-windows.ps1 @@ -758,7 +758,7 @@ return SceneManager.GetActiveScene().path; } [pscustomobject]$json = $responseText | ConvertFrom-Json - if ($json.success -eq $true -and $json.result -eq $SampleScenePath) { + if ($json.Success -eq $true -and $json.Result -eq $SampleScenePath) { return } diff --git a/scripts/refresh-neighbor-game-skills-windows.sh b/scripts/refresh-neighbor-game-skills-windows.sh index b28940df24..7725bc7a91 100644 --- a/scripts/refresh-neighbor-game-skills-windows.sh +++ b/scripts/refresh-neighbor-game-skills-windows.sh @@ -592,7 +592,7 @@ return SceneManager.GetActiveScene().path; response_matches_scene() { response=$1 if command -v jq >/dev/null 2>&1; then - printf '%s\n' "$response" | jq -e --arg scene_path "$sample_scene_path" '.success == true and .result == $scene_path' >/dev/null + printf '%s\n' "$response" | jq -e --arg scene_path "$sample_scene_path" '.Success == true and .Result == $scene_path' >/dev/null return $? fi @@ -606,7 +606,7 @@ try { } catch { exit 1 } -if (\$Value.success -eq \$true -and \$Value.result -eq \$ScenePath) { +if (\$Value.Success -eq \$true -and \$Value.Result -eq \$ScenePath) { exit 0 } exit 1 diff --git a/scripts/run-windows-e2e.ps1 b/scripts/run-windows-e2e.ps1 index 557970296c..5664c7ac5e 100644 --- a/scripts/run-windows-e2e.ps1 +++ b/scripts/run-windows-e2e.ps1 @@ -108,7 +108,7 @@ function Invoke-UloopJsonChecked { [string]$text = Invoke-UloopChecked -CommandArguments $CommandArguments [pscustomobject]$json = $text | ConvertFrom-Json - if ($json.PSObject.Properties.Name -contains "success" -and $json.success -ne $true) { + if ($json.PSObject.Properties.Name -contains "Success" -and $json.Success -ne $true) { throw "uloop $($CommandArguments -join " ") returned success=false: $text" } @@ -172,11 +172,11 @@ function Wait-PlayMode { [pscustomobject]$result = $probe.Text | ConvertFrom-Json [string]$isPlaying = "" - if ($null -ne $result.result) { - $isPlaying = $result.result.ToString() + if ($null -ne $result.Result) { + $isPlaying = $result.Result.ToString() } - if ($result.success -eq $true -and $isPlaying -eq "True") { + if ($result.Success -eq $true -and $isPlaying -eq "True") { return } @@ -221,7 +221,7 @@ return SceneManager.GetActiveScene().path; "@ [pscustomobject]$result = Invoke-DynamicCode -Code $code - if ($result.result -ne $ScenePath) { + if ($result.Result -ne $ScenePath) { throw "Failed to open scene: $ScenePath" } } @@ -309,7 +309,7 @@ function Invoke-LaunchSmoke { Invoke-UloopChecked -CommandArguments @("launch") | Out-Host Wait-UnityReady [pscustomobject]$result = Invoke-DynamicCode -Code 'return "windows-launch-smoke";' - if ($result.result -ne "windows-launch-smoke") { + if ($result.Result -ne "windows-launch-smoke") { throw "launch smoke dynamic-code readiness check failed" } } @@ -376,7 +376,7 @@ if (cube == null) return -9999f; return cube.transform.position.z; '@ - return [double]$result.result + return [double]$result.Result } try { diff --git a/scripts/smoke-cli-recovery-readiness.go b/scripts/smoke-cli-recovery-readiness.go index ee45235f94..47efd51901 100644 --- a/scripts/smoke-cli-recovery-readiness.go +++ b/scripts/smoke-cli-recovery-readiness.go @@ -327,7 +327,7 @@ func assertJSONSuccess(result commandResult, label string) (map[string]any, erro return nil, err } - if success, ok := payload["success"].(bool); !ok || !success { + if success, ok := payload["Success"].(bool); !ok || !success { printCommandContext(label, result) return nil, fmt.Errorf("%s returned invalid success payload: %v", label, payload) } @@ -336,7 +336,7 @@ func assertJSONSuccess(result commandResult, label string) (map[string]any, erro } func assertDynamicCodeResult(payload map[string]any) error { - result, ok := payload["result"].(string) + result, ok := payload["Result"].(string) if ok && result == expectedDynamicCodeResult { return nil } diff --git a/scripts/test-simulate-mouse-demo.ps1 b/scripts/test-simulate-mouse-demo.ps1 index 718046e68f..c07c537b8d 100644 --- a/scripts/test-simulate-mouse-demo.ps1 +++ b/scripts/test-simulate-mouse-demo.ps1 @@ -109,7 +109,7 @@ function Wait-PlayMode { } [pscustomobject]$result = $probe.Text | ConvertFrom-Json - if ($result.success -eq $true -and $result.result -eq "True") { + if ($result.Success -eq $true -and $result.Result -eq "True") { return } @@ -140,14 +140,14 @@ function Assert-MouseResponse { [string]$ExpectedHit ) - if ($Response.success -ne $true) { - throw "$ExpectedAction failed: $($Response.message)" + if ($Response.Success -ne $true) { + throw "$ExpectedAction failed: $($Response.Message)" } - Assert-EqualText -Actual $Response.action -Expected $ExpectedAction -Context "Action" + Assert-EqualText -Actual $Response.Action -Expected $ExpectedAction -Context "Action" if (-not [string]::IsNullOrWhiteSpace($ExpectedHit)) { - Assert-EqualText -Actual $Response.hitGameObjectName -Expected $ExpectedHit -Context "hitGameObjectName" + Assert-EqualText -Actual $Response.HitGameObjectName -Expected $ExpectedHit -Context "hitGameObjectName" } } @@ -157,7 +157,7 @@ function Get-UiElement { [string]$Name ) - [object[]]$matches = @($Elements | Where-Object { $_.name -eq $Name }) + [object[]]$matches = @($Elements | Where-Object { $_.Name -eq $Name }) if ($matches.Count -eq 1) { return $matches[0] } @@ -240,11 +240,11 @@ return text.text; "@ [pscustomobject]$result = Invoke-UloopJson -CommandArguments @("execute-dynamic-code", "--code", $code) - if ($result.success -ne $true) { - throw "Failed to read text from ${ObjectName}: $($result.errorMessage)" + if ($result.Success -ne $true) { + throw "Failed to read text from ${ObjectName}: $($result.ErrorMessage)" } - return $result.result + return $result.Result } function Get-DropZoneStatus { @@ -261,11 +261,11 @@ return dropZone.StatusMessage; '@ ) - if ($result.success -ne $true) { - throw "Failed to read DropZone status: $($result.errorMessage)" + if ($result.Success -ne $true) { + throw "Failed to read DropZone status: $($result.ErrorMessage)" } - return $result.result + return $result.Result } function Get-LongPressButtonText { @@ -283,11 +283,11 @@ return text.text; '@ ) - if ($result.success -ne $true) { - throw "Failed to read LongPressButton text: $($result.errorMessage)" + if ($result.Success -ne $true) { + throw "Failed to read LongPressButton text: $($result.ErrorMessage)" } - return $result.result + return $result.Result } function Get-VirtualPadState { @@ -305,11 +305,11 @@ return pad.Direction.ToString("F3"); '@ ) - if ($result.success -ne $true) { - throw "Failed to read VirtualPad state: $($result.errorMessage)" + if ($result.Success -ne $true) { + throw "Failed to read VirtualPad state: $($result.ErrorMessage)" } - return $result.result + return $result.Result } function Initialize-DemoScene { @@ -330,11 +330,11 @@ return SceneManager.GetActiveScene().path; "@ [pscustomobject]$result = Invoke-UloopJson -CommandArguments @("execute-dynamic-code", "--code", $code) - if ($result.success -ne $true) { - throw "Failed to load ${ScenePath}: $($result.errorMessage)" + if ($result.Success -ne $true) { + throw "Failed to load ${ScenePath}: $($result.ErrorMessage)" } - Assert-EqualText -Actual $result.result -Expected $ScenePath -Context "Active scene" + Assert-EqualText -Actual $result.Result -Expected $ScenePath -Context "Active scene" } function Get-AnnotatedElements { @@ -346,11 +346,11 @@ function Get-AnnotatedElements { "--elements-only" ) - if ($response.screenshotCount -ne 1) { - throw "Expected one screenshot response, got $($response.screenshotCount)" + if ($response.ScreenshotCount -ne 1) { + throw "Expected one screenshot response, got $($response.ScreenshotCount)" } - [object[]]$elements = @($response.screenshots[0].annotatedElements) + [object[]]$elements = @($response.Screenshots[0].AnnotatedElements) if ($elements.Count -eq 0) { throw "No annotated UI elements were returned" } @@ -386,41 +386,41 @@ try { [pscustomobject]$virtualPad = Get-UiElement -Elements $elements -Name "VirtualPadBackground" Write-Host "[4/7] Clicking counter buttons..." - Invoke-MouseUi -Action "Click" -X $clickButton1.simX -Y $clickButton1.simY -TargetPath $clickButton1.path -ExpectedHit "ClickButton1" | Out-Null - Invoke-MouseUi -Action "Click" -X $clickButton2.simX -Y $clickButton2.simY -TargetPath $clickButton2.path -ExpectedHit "ClickButton2" | Out-Null - Invoke-MouseUi -Action "Click" -X $clickButton1.simX -Y $clickButton1.simY -TargetPath $clickButton1.path -ExpectedHit "ClickButton1" | Out-Null - Invoke-MouseUi -Action "Click" -X $clickButton2.simX -Y $clickButton2.simY -TargetPath $clickButton2.path -ExpectedHit "ClickButton2" | Out-Null + Invoke-MouseUi -Action "Click" -X $clickButton1.SimX -Y $clickButton1.SimY -TargetPath $clickButton1.Path -ExpectedHit "ClickButton1" | Out-Null + Invoke-MouseUi -Action "Click" -X $clickButton2.SimX -Y $clickButton2.SimY -TargetPath $clickButton2.Path -ExpectedHit "ClickButton2" | Out-Null + Invoke-MouseUi -Action "Click" -X $clickButton1.SimX -Y $clickButton1.SimY -TargetPath $clickButton1.Path -ExpectedHit "ClickButton1" | Out-Null + Invoke-MouseUi -Action "Click" -X $clickButton2.SimX -Y $clickButton2.SimY -TargetPath $clickButton2.Path -ExpectedHit "ClickButton2" | Out-Null [string]$counterText = Get-TextFromScene -ObjectName "CounterText" Assert-EqualText -Actual $counterText -Expected "Total Clicks: 4" -Context "CounterText" Write-Host "[5/7] Long-pressing the hold button..." - Invoke-MouseUi -Action "LongPress" -X $longPressButton.simX -Y $longPressButton.simY -Duration 3.2 -TargetPath $longPressButton.path -ExpectedHit "LongPressButton" | Out-Null + Invoke-MouseUi -Action "LongPress" -X $longPressButton.SimX -Y $longPressButton.SimY -Duration 3.2 -TargetPath $longPressButton.Path -ExpectedHit "LongPressButton" | Out-Null [string]$longPressText = Get-LongPressButtonText Assert-EqualText -Actual $longPressText -Expected "Activated!" -Context "LongPressButton label" Write-Host "[6/7] Dragging boxes into the DropZone..." - Invoke-MouseUi -Action "Drag" -FromX $redBox.simX -FromY $redBox.simY -X $dropZone.simX -Y $dropZone.simY -DragSpeed 900 -TargetPath $redBox.path -DropTargetPath $dropZone.path -ExpectedHit "RedBox" | Out-Null + Invoke-MouseUi -Action "Drag" -FromX $redBox.SimX -FromY $redBox.SimY -X $dropZone.SimX -Y $dropZone.SimY -DragSpeed 900 -TargetPath $redBox.Path -DropTargetPath $dropZone.Path -ExpectedHit "RedBox" | Out-Null Assert-EqualText -Actual (Get-DropZoneStatus) -Expected "Dropped: RedBox" -Context "DropZone after RedBox" - Invoke-MouseUi -Action "DragStart" -X $greenBox.simX -Y $greenBox.simY -DragSpeed 700 -TargetPath $greenBox.path -ExpectedHit "GreenBox" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($dropZone.simX + 150) -Y ($greenBox.simY - 50) -DragSpeed 500 -ExpectedHit "GreenBox" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($dropZone.simX - 150) -Y ($dropZone.simY + 50) -DragSpeed 500 -ExpectedHit "GreenBox" | Out-Null - Invoke-MouseUi -Action "DragEnd" -X $dropZone.simX -Y $dropZone.simY -DragSpeed 500 -DropTargetPath $dropZone.path -ExpectedHit "GreenBox" | Out-Null + Invoke-MouseUi -Action "DragStart" -X $greenBox.SimX -Y $greenBox.SimY -DragSpeed 700 -TargetPath $greenBox.Path -ExpectedHit "GreenBox" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($dropZone.SimX + 150) -Y ($greenBox.SimY - 50) -DragSpeed 500 -ExpectedHit "GreenBox" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($dropZone.SimX - 150) -Y ($dropZone.SimY + 50) -DragSpeed 500 -ExpectedHit "GreenBox" | Out-Null + Invoke-MouseUi -Action "DragEnd" -X $dropZone.SimX -Y $dropZone.SimY -DragSpeed 500 -DropTargetPath $dropZone.Path -ExpectedHit "GreenBox" | Out-Null Assert-EqualText -Actual (Get-DropZoneStatus) -Expected "Dropped: GreenBox" -Context "DropZone after GreenBox" - Invoke-MouseUi -Action "Drag" -FromX $blueBox.simX -FromY $blueBox.simY -X $dropZone.simX -Y $dropZone.simY -DragSpeed 900 -TargetPath $blueBox.path -DropTargetPath $dropZone.path -ExpectedHit "BlueBox" | Out-Null + Invoke-MouseUi -Action "Drag" -FromX $blueBox.SimX -FromY $blueBox.SimY -X $dropZone.SimX -Y $dropZone.SimY -DragSpeed 900 -TargetPath $blueBox.Path -DropTargetPath $dropZone.Path -ExpectedHit "BlueBox" | Out-Null Assert-EqualText -Actual (Get-DropZoneStatus) -Expected "Dropped: BlueBox" -Context "DropZone after BlueBox" Write-Host "[7/7] Exercising the virtual pad..." - [double]$padWidth = $virtualPad.boundsMaxX - $virtualPad.boundsMinX - [double]$padHeight = $virtualPad.boundsMaxY - $virtualPad.boundsMinY + [double]$padWidth = $virtualPad.BoundsMaxX - $virtualPad.BoundsMinX + [double]$padHeight = $virtualPad.BoundsMaxY - $virtualPad.BoundsMinY [double]$padOffset = [Math]::Min($padWidth, $padHeight) * 0.28 - Invoke-MouseUi -Action "DragStart" -X $virtualPad.simX -Y $virtualPad.simY -TargetPath $virtualPad.path -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($virtualPad.simX + $padOffset) -Y ($virtualPad.simY - $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($virtualPad.simX - $padOffset) -Y ($virtualPad.simY + $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragMove" -X $virtualPad.simX -Y ($virtualPad.simY - $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($virtualPad.simX + $padOffset) -Y $virtualPad.simY -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragEnd" -X $virtualPad.simX -Y $virtualPad.simY -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragStart" -X $virtualPad.SimX -Y $virtualPad.SimY -TargetPath $virtualPad.Path -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($virtualPad.SimX + $padOffset) -Y ($virtualPad.SimY - $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($virtualPad.SimX - $padOffset) -Y ($virtualPad.SimY + $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragMove" -X $virtualPad.SimX -Y ($virtualPad.SimY - $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($virtualPad.SimX + $padOffset) -Y $virtualPad.SimY -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragEnd" -X $virtualPad.SimX -Y $virtualPad.SimY -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null Assert-EqualText -Actual (Get-VirtualPadState) -Expected "Zero" -Context "VirtualPad state" Write-Host "" diff --git a/scripts/test-simulate-mouse-demo.sh b/scripts/test-simulate-mouse-demo.sh index 2fea736fb5..3e19a4cde0 100755 --- a/scripts/test-simulate-mouse-demo.sh +++ b/scripts/test-simulate-mouse-demo.sh @@ -71,8 +71,8 @@ assert_json_success() { json=$1 context=$2 - printf '%s\n' "$json" | jq -e '.success == true' >/dev/null \ - || fail "$context failed: $(printf '%s\n' "$json" | jq -r '.errorMessage // .message // .error // "unknown error"')" + printf '%s\n' "$json" | jq -e '.Success == true' >/dev/null \ + || fail "$context failed: $(printf '%s\n' "$json" | jq -r '.ErrorMessage // .Message // .Error // "unknown error"')" } assert_text_equals() { @@ -101,7 +101,7 @@ wait_play_mode() { attempt=0 while [ "$attempt" -lt 20 ]; do json=$(run_uloop_json execute-dynamic-code --code 'using UnityEngine; return Application.isPlaying;') - if printf '%s\n' "$json" | jq -e '.success == true and .result == "True"' >/dev/null; then + if printf '%s\n' "$json" | jq -e '.Success == true and .Result == "True"' >/dev/null; then return fi @@ -128,7 +128,7 @@ return SceneManager.GetActiveScene().path; json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Load demo scene" - scene=$(printf '%s\n' "$json" | jq -r '.result') + scene=$(printf '%s\n' "$json" | jq -r '.Result') assert_text_equals "$scene" "$SCENE_PATH" "Active scene" } @@ -214,7 +214,7 @@ return displayTexts[selectedIndex] + " / " + currentGameViewSize.x + "x" + curre json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Select Full HD Game View" - printf ' %s\n' "$(printf '%s\n' "$json" | jq -r '.result')" + printf ' %s\n' "$(printf '%s\n' "$json" | jq -r '.Result')" sleep 1 } @@ -251,7 +251,7 @@ return selectedSizeIndexProperty.GetValue(gameView, null).ToString(); json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Capture Game View size index" - printf '%s\n' "$json" | jq -r '.result' + printf '%s\n' "$json" | jq -r '.Result' } restore_game_view_size_index() { @@ -299,16 +299,16 @@ capture_annotated_elements() { --annotate-elements \ --elements-only >"$ELEMENTS_JSON" - jq -e '.screenshotCount == 1 and (.screenshots[0].annotatedElements | length > 0)' "$ELEMENTS_JSON" >/dev/null \ + jq -e '.ScreenshotCount == 1 and (.Screenshots[0].AnnotatedElements | length > 0)' "$ELEMENTS_JSON" >/dev/null \ || fail "No annotated UI elements were returned" } get_element_json() { name=$1 - count=$(jq --arg name "$name" '[.screenshots[0].annotatedElements[] | select(.name == $name)] | length' "$ELEMENTS_JSON") + count=$(jq --arg name "$name" '[.Screenshots[0].AnnotatedElements[] | select(.Name == $name)] | length' "$ELEMENTS_JSON") [ "$count" = "1" ] || fail "Expected exactly one annotated element named '$name', found $count" - jq -c --arg name "$name" '.screenshots[0].annotatedElements[] | select(.name == $name)' "$ELEMENTS_JSON" + jq -c --arg name "$name" '.Screenshots[0].AnnotatedElements[] | select(.Name == $name)' "$ELEMENTS_JSON" } element_field() { @@ -323,11 +323,11 @@ assert_mouse_response() { expected_hit=$3 printf '%s\n' "$json" | jq -e --arg action "$expected_action" \ - '.success == true and .action == $action' >/dev/null \ - || fail "$expected_action failed: $(printf '%s\n' "$json" | jq -r '.message // "unknown error"')" + '.Success == true and .Action == $action' >/dev/null \ + || fail "$expected_action failed: $(printf '%s\n' "$json" | jq -r '.Message // "unknown error"')" if [ -n "$expected_hit" ]; then - actual_hit=$(printf '%s\n' "$json" | jq -r '.hitGameObjectName // ""') + actual_hit=$(printf '%s\n' "$json" | jq -r '.HitGameObjectName // ""') assert_text_equals "$actual_hit" "$expected_hit" "hitGameObjectName" fi } @@ -335,9 +335,9 @@ assert_mouse_response() { invoke_click() { name=$1 element=$(get_element_json "$name") - x=$(element_field "$element" "simX") - y=$(element_field "$element" "simY") - path=$(element_field "$element" "path") + x=$(element_field "$element" "SimX") + y=$(element_field "$element" "SimY") + path=$(element_field "$element" "Path") json=$(run_uloop_json simulate-mouse-ui \ --action Click \ @@ -352,9 +352,9 @@ invoke_long_press() { name=$1 duration=$2 element=$(get_element_json "$name") - x=$(element_field "$element" "simX") - y=$(element_field "$element" "simY") - path=$(element_field "$element" "path") + x=$(element_field "$element" "SimX") + y=$(element_field "$element" "SimY") + path=$(element_field "$element" "Path") json=$(run_uloop_json simulate-mouse-ui \ --action LongPress \ @@ -371,12 +371,12 @@ invoke_drag_to_drop_zone() { speed=$2 element=$(get_element_json "$name") drop_zone=$(get_element_json "DropZone") - from_x=$(element_field "$element" "simX") - from_y=$(element_field "$element" "simY") - to_x=$(element_field "$drop_zone" "simX") - to_y=$(element_field "$drop_zone" "simY") - target_path=$(element_field "$element" "path") - drop_path=$(element_field "$drop_zone" "path") + from_x=$(element_field "$element" "SimX") + from_y=$(element_field "$element" "SimY") + to_x=$(element_field "$drop_zone" "SimX") + to_y=$(element_field "$drop_zone" "SimY") + target_path=$(element_field "$element" "Path") + drop_path=$(element_field "$drop_zone" "Path") json=$(run_uloop_json simulate-mouse-ui \ --action Drag \ @@ -394,9 +394,9 @@ invoke_drag_to_drop_zone() { invoke_drag_start() { name=$1 element=$(get_element_json "$name") - x=$(element_field "$element" "simX") - y=$(element_field "$element" "simY") - path=$(element_field "$element" "path") + x=$(element_field "$element" "SimX") + y=$(element_field "$element" "SimY") + path=$(element_field "$element" "Path") json=$(run_uloop_json simulate-mouse-ui \ --action DragStart \ @@ -452,7 +452,7 @@ return text.text; json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Read text from $object_name" - printf '%s\n' "$json" | jq -r '.result' + printf '%s\n' "$json" | jq -r '.Result' } get_long_press_button_text() { @@ -468,7 +468,7 @@ return text.text; json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Read LongPressButton text" - printf '%s\n' "$json" | jq -r '.result' + printf '%s\n' "$json" | jq -r '.Result' } get_drop_zone_status() { @@ -483,7 +483,7 @@ return dropZone.StatusMessage; json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Read DropZone status" - printf '%s\n' "$json" | jq -r '.result' + printf '%s\n' "$json" | jq -r '.Result' } get_virtual_pad_state() { @@ -499,17 +499,17 @@ return pad.Direction.ToString("F3"); json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Read VirtualPad state" - printf '%s\n' "$json" | jq -r '.result' + printf '%s\n' "$json" | jq -r '.Result' } exercise_virtual_pad() { virtual_pad=$(get_element_json "VirtualPadBackground") - pad_x=$(element_field "$virtual_pad" "simX") - pad_y=$(element_field "$virtual_pad" "simY") - pad_width=$(printf '%s\n' "$virtual_pad" | jq -r '.boundsMaxX - .boundsMinX') - pad_height=$(printf '%s\n' "$virtual_pad" | jq -r '.boundsMaxY - .boundsMinY') + pad_x=$(element_field "$virtual_pad" "SimX") + pad_y=$(element_field "$virtual_pad" "SimY") + pad_width=$(printf '%s\n' "$virtual_pad" | jq -r '.BoundsMaxX - .BoundsMinX') + pad_height=$(printf '%s\n' "$virtual_pad" | jq -r '.BoundsMaxY - .BoundsMinY') pad_offset=$(jq -nr --argjson width "$pad_width" --argjson height "$pad_height" '([$width, $height] | min) * 0.28') - target_path=$(element_field "$virtual_pad" "path") + target_path=$(element_field "$virtual_pad" "Path") json=$(run_uloop_json simulate-mouse-ui \ --action DragStart \ @@ -582,11 +582,11 @@ assert_text_equals "$(get_drop_zone_status)" "Dropped: RedBox" "DropZone after R green_box=$(get_element_json "GreenBox") drop_zone=$(get_element_json "DropZone") -green_x=$(element_field "$green_box" "simX") -green_y=$(element_field "$green_box" "simY") -drop_x=$(element_field "$drop_zone" "simX") -drop_y=$(element_field "$drop_zone" "simY") -drop_path=$(element_field "$drop_zone" "path") +green_x=$(element_field "$green_box" "SimX") +green_y=$(element_field "$green_box" "SimY") +drop_x=$(element_field "$drop_zone" "SimX") +drop_y=$(element_field "$drop_zone" "SimY") +drop_path=$(element_field "$drop_zone" "Path") invoke_drag_start "GreenBox" move_x=$(jq -nr --argjson center "$drop_x" '$center + 150') diff --git a/scripts/test-smoke-cli-recovery-readiness.sh b/scripts/test-smoke-cli-recovery-readiness.sh index 86f11751cd..d7adab044a 100755 --- a/scripts/test-smoke-cli-recovery-readiness.sh +++ b/scripts/test-smoke-cli-recovery-readiness.sh @@ -48,18 +48,18 @@ func main() { fmt.Printf("Unity is already running for %s (PID: 1234)\n", projectPath) case "get-logs": if os.Getenv("ULOOP_FAKE_CONNECTION_FAILURE") == "1" { - fmt.Fprintln(os.Stderr, "{\n \"success\": false,\n \"error\": {\n \"errorCode\": \"UNITY_NOT_REACHABLE\",\n \"message\": \"The Unity CLI Loop server is not reachable for this project.\",\n \"nextActions\": [\n \"If Unity is closed, run `uloop launch`.\"\n ]\n }\n}") + fmt.Fprintln(os.Stderr, "{\n \"Success\": false,\n \"Error\": {\n \"ErrorCode\": \"UNITY_NOT_REACHABLE\",\n \"Message\": \"The Unity CLI Loop server is not reachable for this project.\",\n \"NextActions\": [\n \"If Unity is closed, run `uloop launch`.\"\n ]\n }\n}") os.Exit(1) } - fmt.Println(`{"displayedCount":0,"logs":[],"maxCount":1,"totalCount":0}`) + fmt.Println(`{"DisplayedCount":0,"Logs":[],"MaxCount":1,"TotalCount":0}`) case "compile": if os.Getenv("ULOOP_FAKE_COMPILE_WITHOUT_SUCCESS") == "1" { - fmt.Println(`{"errorCount":0,"warningCount":0}`) + fmt.Println(`{"ErrorCount":0,"WarningCount":0}`) return } - fmt.Println(`{"success":true,"errorCount":0,"warningCount":0}`) + fmt.Println(`{"Success":true,"ErrorCount":0,"WarningCount":0}`) case "execute-dynamic-code": - fmt.Println(`{"success":true,"result":"cli-recovery-readiness-e2e"}`) + fmt.Println(`{"Success":true,"Result":"cli-recovery-readiness-e2e"}`) default: fmt.Fprintf(os.Stderr, "unexpected command: %s\n", commandName) os.Exit(98)