Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/uloop-execute-dynamic-code/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ return x;

Prefer terminal commands for file operations and keep snippets focused on Unity Editor state that existing uloop tools cannot inspect or change.

## Known transpiler constraints

- Literals inside recognized static local function bodies are kept inline automatically. Unsupported header shapes (generic `where` clauses, tuple return types, statement lambdas inside expression bodies) may still hoist literals and surface CS8421; remove `static` or rewrite the helper.
- Static lambdas (`static x => ...`) cannot reference hoisted literals and surface CS8820; remove `static` from the lambda or use a non-static local function.
- Integer literals are hoisted as `int` values. APIs that require `byte` components (for example `new Color32(255, 0, 0, 255)`) need explicit casts such as `(byte)255` even when plain Unity scripts accept uncast numeric literals.

## Shell Quoting

- zsh/bash: single-quote the whole snippet so C# double quotes pass through unchanged: `--code 'return "hi";'`. For a single quote inside the snippet, close and reopen the shell string with `'\''`.
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/uloop-simulate-keyboard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ uloop simulate-keyboard --action <action> --key <key> [options]
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `--action` | enum | `Press` | `Press`, `KeyDown`, `KeyUp` |
| `--key` | string | (required) | Key name matching Input System Key enum (e.g. `W`, `Space`, `LeftShift`, `A`, `Enter`). Case-insensitive. |
| `--key` | string | (required) | Key name matching Input System Key enum (e.g. `W`, `Space`, `LeftShift`, `A`, `Enter`). Case-insensitive. Digit keys use `Digit0`-`Digit9` or `Numpad0`-`Numpad9`, not bare `0`-`9`. |
| `--duration` | number | `0` | Hold duration in seconds for Press action (0 = one-shot tap). Ignored by KeyDown/KeyUp. |

### Actions
Expand Down
6 changes: 6 additions & 0 deletions .claude/skills/uloop-execute-dynamic-code/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ return x;

Prefer terminal commands for file operations and keep snippets focused on Unity Editor state that existing uloop tools cannot inspect or change.

## Known transpiler constraints

- Literals inside recognized static local function bodies are kept inline automatically. Unsupported header shapes (generic `where` clauses, tuple return types, statement lambdas inside expression bodies) may still hoist literals and surface CS8421; remove `static` or rewrite the helper.
- Static lambdas (`static x => ...`) cannot reference hoisted literals and surface CS8820; remove `static` from the lambda or use a non-static local function.
- Integer literals are hoisted as `int` values. APIs that require `byte` components (for example `new Color32(255, 0, 0, 255)`) need explicit casts such as `(byte)255` even when plain Unity scripts accept uncast numeric literals.

## Shell Quoting

- zsh/bash: single-quote the whole snippet so C# double quotes pass through unchanged: `--code 'return "hi";'`. For a single quote inside the snippet, close and reopen the shell string with `'\''`.
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/uloop-simulate-keyboard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ uloop simulate-keyboard --action <action> --key <key> [options]
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `--action` | enum | `Press` | `Press`, `KeyDown`, `KeyUp` |
| `--key` | string | (required) | Key name matching Input System Key enum (e.g. `W`, `Space`, `LeftShift`, `A`, `Enter`). Case-insensitive. |
| `--key` | string | (required) | Key name matching Input System Key enum (e.g. `W`, `Space`, `LeftShift`, `A`, `Enter`). Case-insensitive. Digit keys use `Digit0`-`Digit9` or `Numpad0`-`Numpad9`, not bare `0`-`9`. |
| `--duration` | number | `0` | Hold duration in seconds for Press action (0 = one-shot tap). Ignored by KeyDown/KeyUp. |

### Actions
Expand Down
54 changes: 54 additions & 0 deletions Assets/Tests/Editor/CaseInsensitiveStringEnumConverterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using io.github.hatayama.UnityCliLoop.FirstPartyTools;
using io.github.hatayama.UnityCliLoop.ToolContracts;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
[TestFixture]
public class CaseInsensitiveStringEnumConverterTests
{
// Locks lowercase enum strings that already worked before the converter was added.
[Test]
public void Deserialize_PlayModeAction_AcceptsLowercaseAction()
{
JObject token = JObject.Parse("{\"action\":\"play\"}");

ControlPlayModeSchema schema = token.ToObject<ControlPlayModeSchema>(
UnityCliLoopToolParameterSerializer.CamelCaseSerializer);

Assert.That(schema.Action, Is.EqualTo(PlayModeAction.Play));
}

// Verifies invalid enum values surface the allowed action names in the error.
[Test]
public void Deserialize_PlayModeAction_InvalidValue_ListsValidValues()
{
JObject token = JObject.Parse("{\"action\":\"jump\"}");

JsonSerializationException exception = Assert.Throws<JsonSerializationException>(() =>
token.ToObject<ControlPlayModeSchema>(UnityCliLoopToolParameterSerializer.CamelCaseSerializer));

Assert.That(exception.Message, Does.Contain("Valid values:"));
Assert.That(exception.Message, Does.Contain("Play"));
Assert.That(exception.Message, Does.Contain("Stop"));
Assert.That(exception.Message, Does.Contain("Pause"));
Assert.That(exception.Message, Does.Contain("Step"));
}

// Verifies integer enum tokens are rejected with an explicit string-only message.
[Test]
public void Deserialize_PlayModeAction_IntegerToken_RejectedWithExplicitMessage()
{
JObject token = JObject.Parse("{\"action\":1}");

JsonSerializationException exception = Assert.Throws<JsonSerializationException>(() =>
token.ToObject<ControlPlayModeSchema>(UnityCliLoopToolParameterSerializer.CamelCaseSerializer));

Assert.That(exception.Message, Does.Contain("Enum parameter values must be JSON strings"));
Assert.That(exception.Message, Does.Contain("Integer"));
Assert.That(exception.Message, Does.Contain("PlayModeAction"));
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 92 additions & 0 deletions Assets/Tests/Editor/CompileResponseFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,98 @@ public void CreateResponse_WhenForceCompileHasPreservedFailure_MapsDetailedIssue
Assert.That(response.Message, Does.Contain("externally"));
}

[Test]
public void CreateResponse_WhenExternalSceneCannotBeResolved_AddsNextActions()
{
// Verifies unresolved external Scene changes include execute-dynamic-code reload guidance.
CompilerMessage error = new CompilerMessage
{
type = CompilerMessageType.Error,
message = "Compilation cannot resolve externally changed Scene files before compile.",
file = "Assets/Scenes/SampleScene.unity",
line = 0
};
CompileResult result = new CompileResult(
success: false,
errorCount: 1,
warningCount: 0,
completedAt: DateTime.Now,
messages: new[] { error },
errors: new[] { error },
warnings: Array.Empty<CompilerMessage>(),
message: error.message);

CompileResponse response =
CompileResponseFactory.CreateResponse(result, forceRecompile: false);

Assert.That(response.NextActions, Is.Not.Null);
Assert.That(response.NextActions, Has.Length.EqualTo(2));
Assert.That(response.NextActions[0], Does.Contain("execute-dynamic-code"));
Assert.That(response.NextActions[0], Does.Contain("EditorSceneManager.OpenScene"));
Assert.That(response.NextActions[0], Does.Contain("OpenSceneMode.Single discards unsaved"));
Assert.That(response.NextActions[1], Does.Contain("unsaved in-editor changes"));
Assert.That(response.NextActions[1], Does.Not.Contain("stop-on-external-scene-changes"));
}

[Test]
public void CreateResponse_WhenExternalSceneCannotBeReloaded_AddsNextActions()
{
// Verifies reload-failure messages from ExternalSceneChangeResolver also surface reload guidance.
CompilerMessage error = new CompilerMessage
{
type = CompilerMessageType.Error,
message =
"Compilation cannot reload externally changed Scene files before compile. " +
"Scenes that could not be reloaded: Assets/Scenes/SampleScene.unity.",
file = "Assets/Scenes/SampleScene.unity",
line = 0
};
CompileResult result = new CompileResult(
success: false,
errorCount: 1,
warningCount: 0,
completedAt: DateTime.Now,
messages: new[] { error },
errors: new[] { error },
warnings: Array.Empty<CompilerMessage>(),
message: error.message);

CompileResponse response =
CompileResponseFactory.CreateResponse(result, forceRecompile: false);

Assert.That(response.NextActions, Is.Not.Null);
Assert.That(response.NextActions, Has.Length.EqualTo(2));
}

[Test]
public void CreateResponse_WhenExternalSceneStopMessageUsesDifferentWording_DoesNotAddNextActions()
{
// Verifies the stopped variant ("changed externally") does not match the NextActions substring gate.
CompilerMessage error = new CompilerMessage
{
type = CompilerMessageType.Error,
message =
"Compilation stopped because open Scene files changed externally. " +
"External Scene changes: Assets/Scenes/SampleScene.unity.",
file = "Assets/Scenes/SampleScene.unity",
line = 0
};
CompileResult result = new CompileResult(
success: false,
errorCount: 1,
warningCount: 0,
completedAt: DateTime.Now,
messages: new[] { error },
errors: new[] { error },
warnings: Array.Empty<CompilerMessage>(),
message: error.message);

CompileResponse response =
CompileResponseFactory.CreateResponse(result, forceRecompile: false);

Assert.That(response.NextActions, Is.Null);
}

[Test]
public void CreateResponse_WhenUnityTestFrameworkSymbolIsMissing_AddsTestAsmdefHint()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests
{
[TestFixture]
public sealed class DynamicCodeDiagnosticColumnMapperTests
{
[Test]
public void MapWrappedColumnToUserColumn_SubtractsWrapperIndentAndClampsToOne()
{
// Verifies wrapped physical columns are rebased to user-snippet columns.
Assert.That(DynamicCodeDiagnosticColumnMapper.MapWrappedColumnToUserColumn(20), Is.EqualTo(8));
Assert.That(DynamicCodeDiagnosticColumnMapper.MapWrappedColumnToUserColumn(5), Is.EqualTo(1));
Assert.That(DynamicCodeDiagnosticColumnMapper.MapWrappedColumnToUserColumn(0), Is.EqualTo(0));
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests
{
[TestFixture]
public sealed class DynamicCodeDiagnosticContextBuilderTests
{
[Test]
public void BuildContext_WhenUserLineFiveHasSyntaxError_ShowsUserSnippetNotWrapperBoilerplate()
{
// Verifies #line-mapped user line 5 renders against the user snippet instead of wrapper usings.
string[] userSnippetLines =
{
"int a=1;",
"int b=2;",
"int c=3;",
"int d=4;",
"int e= ;",
"return a;"
};

string context = DynamicCodeDiagnosticContextBuilder.BuildContext(userSnippetLines, 5, 8);

Assert.That(context, Does.Contain("L5:int e= ;"));
Assert.That(context, Does.Not.Contain("using System.Collections.Generic"));
Assert.That(context, Does.Not.Contain("L7:"));
}

[Test]
public void BuildContext_WhenUserColumnProvided_PlacesCaretUnderErrorCharacter()
{
// Verifies the caret line uses the same 1-based column basis as the rendered user line.
string[] userSnippetLines = { "int e= ;" };
string context = DynamicCodeDiagnosticContextBuilder.BuildContext(userSnippetLines, 1, 8);
string[] contextLines = context.Split('\n');

Assert.That(contextLines[1].IndexOf('^'), Is.EqualTo("L1:".Length + 7));
}

[Test]
public void TryExtract_WhenWrappedSourceContainsLineDirectives_ReturnsIndentedUserSnippet()
{
// Verifies wrapped sources expose only the user region between #line markers.
string wrappedSource = WrapperTemplate.Build(
System.Array.Empty<string>(),
System.Array.Empty<string>(),
"TestNs",
"TestClass",
"int a=1;\nint e= ;\nreturn a;");

bool extracted = WrappedDynamicCodeUserSnippetExtractor.TryExtract(wrappedSource, out string userSnippet);

Assert.That(extracted, Is.True);
Assert.That(userSnippet, Does.Contain("int e= ;"));
Assert.That(userSnippet, Does.Not.Contain("using UnityEditor;"));
}

[Test]
public void Split_WhenOriginalSnippetHasTrailingNewline_DropsTrailingEmptyLine()
{
// Verifies trailing newline does not create an extra empty context line.
string[] lines = DynamicCodeUserSnippetLines.Split("int a=1;\nreturn a;\n");

Assert.That(lines, Has.Length.EqualTo(2));
Assert.That(lines[0], Is.EqualTo("int a=1;"));
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading