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
15 changes: 10 additions & 5 deletions .agents/skills/uloop-run-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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 or end inconclusive, NUnit XML results with failure messages, stack traces, and inconclusive reasons are automatically saved. Read the XML file at `XmlPath` for detailed diagnosis.

`uloop run-tests` automatically compiles pending script changes before running tests. Pass `--skip-compile` only while validating active hot-reload patches, because the compile clears those patches; otherwise let the default compile surface errors and run against current scripts. `--skip-compile` skips only the CLI-side compile: Unity still imports script edits saved since the last compile, and that import reloads the domain as soon as the run releases its assembly lock, discarding active patches and ending the request.

Expand All @@ -18,6 +18,8 @@ A test run can end by discarding active hot-reload changes: script edits importe

`NoTestsFound` means zero tests matched — not a test failure. Check `NoTestsFoundExplanation` and `Message` for asmdef hints. When an unfiltered run finds no tests and the project has no test assembly for the TestMode, `ProposedTestAsmdef` carries a ready-to-write `.asmdef`: save `Content` at `AssetPath`, move the test scripts under that folder, then compile and rerun.

`Status: Inconclusive` means no test failed but at least one could not meet an `Assume`; `Success` is `false`, as Unity's batchmode test run also fails for it. `InconclusiveTests` names them with the assumption's message. A test that cannot run in this environment should call `Assert.Ignore` so it reports as skipped.

## Usage

```bash
Expand All @@ -44,8 +46,8 @@ exact matches the full test name (Namespace.Class.Method). class runs every test

Returns JSON with:

- `Success` (boolean): Whether all tests passed
- `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed`
- `Success` (boolean): Whether every test passed or was skipped; `false` when any failed or was inconclusive
- `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `Inconclusive`, `NoTestsFound`, or `ExecutionFailed`
- `HasFailures` (boolean): Whether any discovered test failed
- `Message` (string): Summary message
- `NoTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests
Expand All @@ -55,17 +57,20 @@ Returns JSON with:
- `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.
- `InconclusiveCount` (number): Inconclusive tests (an `Assume` was not met)
- `XmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `Success: true`); populated only when tests failed or were inconclusive and the XML file exists on disk.
- `ClearedPausePointIds` (string[], optional): IDs of pause points that were cleared before test execution. Omitted from JSON when no pause points were active.
- `FailedTests` (array, optional): Up to 10 failed leaf tests with `FullName`, `Message`, and when the stack trace contains a path:line location, `File` and `Line`. Omitted when no tests failed. When `FailedCount` is greater than 10, `Message` ends with `first 10 of N failures listed; see XmlPath for full results.`
- `SkippedTests` (string[], optional): Up to 10 full names of skipped leaf tests. Omitted when no tests were skipped. When `SkippedCount` is greater than 10, only the first 10 names are listed.
- `InconclusiveTests` (array, optional): Up to 10 inconclusive leaf tests with `FullName` and `Message`. Omitted when no test was inconclusive. When `InconclusiveCount` is greater than 10, only the first 10 are listed; the XML at `XmlPath` has every message.
- `ProposedTestAsmdef` (object, optional): `AssetPath` and `Content` of a ready-to-write test `.asmdef` (test-assembly wiring plus references to the project's assemblies under test). Present only when an unfiltered run found no tests and no test assembly exists for the TestMode.
- `CompileNote` (string, optional): States that the automatic compile ran and succeeded before the tests and names `--skip-compile` as the opt-out. When the compile response carried a Warning (for example active hot-reload changes dropped by the domain reload), the note repeats it. Omitted when `--skip-compile` was passed; a failed compile returns the compile error response instead.

### XML Result File

When tests fail, NUnit XML results are automatically saved to `{project_root}/.uloop/outputs/TestResults/<timestamp>.xml`. The XML contains per-test-case results including:
When tests fail or end inconclusive, NUnit XML results are automatically saved to `{project_root}/.uloop/outputs/TestResults/<timestamp>.xml`. The XML contains per-test-case results including:

- Test name and full name
- Pass/fail/skip status and duration
- For failed tests: `<message>` (assertion error) and `<stack-trace>`
- For inconclusive tests: `<reason><message>` (the assumption that was not met)
15 changes: 10 additions & 5 deletions .claude/skills/uloop-run-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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 or end inconclusive, NUnit XML results with failure messages, stack traces, and inconclusive reasons are automatically saved. Read the XML file at `XmlPath` for detailed diagnosis.

`uloop run-tests` automatically compiles pending script changes before running tests. Pass `--skip-compile` only while validating active hot-reload patches, because the compile clears those patches; otherwise let the default compile surface errors and run against current scripts. `--skip-compile` skips only the CLI-side compile: Unity still imports script edits saved since the last compile, and that import reloads the domain as soon as the run releases its assembly lock, discarding active patches and ending the request.

Expand All @@ -18,6 +18,8 @@ A test run can end by discarding active hot-reload changes: script edits importe

`NoTestsFound` means zero tests matched — not a test failure. Check `NoTestsFoundExplanation` and `Message` for asmdef hints. When an unfiltered run finds no tests and the project has no test assembly for the TestMode, `ProposedTestAsmdef` carries a ready-to-write `.asmdef`: save `Content` at `AssetPath`, move the test scripts under that folder, then compile and rerun.

`Status: Inconclusive` means no test failed but at least one could not meet an `Assume`; `Success` is `false`, as Unity's batchmode test run also fails for it. `InconclusiveTests` names them with the assumption's message. A test that cannot run in this environment should call `Assert.Ignore` so it reports as skipped.

## Usage

```bash
Expand All @@ -44,8 +46,8 @@ exact matches the full test name (Namespace.Class.Method). class runs every test

Returns JSON with:

- `Success` (boolean): Whether all tests passed
- `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed`
- `Success` (boolean): Whether every test passed or was skipped; `false` when any failed or was inconclusive
- `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `Inconclusive`, `NoTestsFound`, or `ExecutionFailed`
- `HasFailures` (boolean): Whether any discovered test failed
- `Message` (string): Summary message
- `NoTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests
Expand All @@ -55,17 +57,20 @@ Returns JSON with:
- `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.
- `InconclusiveCount` (number): Inconclusive tests (an `Assume` was not met)
- `XmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `Success: true`); populated only when tests failed or were inconclusive and the XML file exists on disk.
- `ClearedPausePointIds` (string[], optional): IDs of pause points that were cleared before test execution. Omitted from JSON when no pause points were active.
- `FailedTests` (array, optional): Up to 10 failed leaf tests with `FullName`, `Message`, and when the stack trace contains a path:line location, `File` and `Line`. Omitted when no tests failed. When `FailedCount` is greater than 10, `Message` ends with `first 10 of N failures listed; see XmlPath for full results.`
- `SkippedTests` (string[], optional): Up to 10 full names of skipped leaf tests. Omitted when no tests were skipped. When `SkippedCount` is greater than 10, only the first 10 names are listed.
- `InconclusiveTests` (array, optional): Up to 10 inconclusive leaf tests with `FullName` and `Message`. Omitted when no test was inconclusive. When `InconclusiveCount` is greater than 10, only the first 10 are listed; the XML at `XmlPath` has every message.
- `ProposedTestAsmdef` (object, optional): `AssetPath` and `Content` of a ready-to-write test `.asmdef` (test-assembly wiring plus references to the project's assemblies under test). Present only when an unfiltered run found no tests and no test assembly exists for the TestMode.
- `CompileNote` (string, optional): States that the automatic compile ran and succeeded before the tests and names `--skip-compile` as the opt-out. When the compile response carried a Warning (for example active hot-reload changes dropped by the domain reload), the note repeats it. Omitted when `--skip-compile` was passed; a failed compile returns the compile error response instead.

### XML Result File

When tests fail, NUnit XML results are automatically saved to `{project_root}/.uloop/outputs/TestResults/<timestamp>.xml`. The XML contains per-test-case results including:
When tests fail or end inconclusive, NUnit XML results are automatically saved to `{project_root}/.uloop/outputs/TestResults/<timestamp>.xml`. The XML contains per-test-case results including:

- Test name and full name
- Pass/fail/skip status and duration
- For failed tests: `<message>` (assertion error) and `<stack-trace>`
- For inconclusive tests: `<reason><message>` (the assumption that was not met)
128 changes: 128 additions & 0 deletions Assets/Tests/Editor/RunTestsResponseContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public void RunTestsResponse_WhenSerialized_OmitsNullFailedTestsAndNullFileLineK
passedCount: 1,
failedCount: 0,
skippedCount: 0,
inconclusiveCount: 0,
xmlPath: string.Empty,
status: RunTestsExecutionStatus.Passed,
hasFailures: false,
Expand All @@ -49,6 +50,7 @@ public void RunTestsResponse_WhenSerialized_OmitsNullFailedTestsAndNullFileLineK
passedCount: 0,
failedCount: 1,
skippedCount: 0,
inconclusiveCount: 0,
xmlPath: "TestResults/example.xml",
status: RunTestsExecutionStatus.Failed,
hasFailures: true,
Expand Down Expand Up @@ -96,6 +98,7 @@ public void RunTestsResponse_WhenSerialized_OmitsOrIncludesSkippedTestsByPresenc
passedCount: 1,
failedCount: 0,
skippedCount: 0,
inconclusiveCount: 0,
xmlPath: string.Empty,
status: RunTestsExecutionStatus.Passed,
hasFailures: false,
Expand All @@ -118,6 +121,7 @@ public void RunTestsResponse_WhenSerialized_OmitsOrIncludesSkippedTestsByPresenc
passedCount: 1,
failedCount: 0,
skippedCount: 1,
inconclusiveCount: 0,
xmlPath: string.Empty,
status: RunTestsExecutionStatus.Passed,
hasFailures: false,
Expand All @@ -139,6 +143,128 @@ public void RunTestsResponse_WhenSerialized_OmitsOrIncludesSkippedTestsByPresenc
Assert.That(skippedTests[0].Value<string>(), Is.EqualTo("Example.Tests.SkippedTest"));
}

/// <summary>
/// What: InconclusiveCount is always written, while InconclusiveTests is omitted without
/// inconclusive leaves and serializes each name and message when present.
/// </summary>
[Test]
public void RunTestsResponse_WhenSerialized_WritesInconclusiveCountAndOmitsOrIncludesInconclusiveTests()
{
RunTestsResponse zeroInconclusive = new RunTestsResponse(
success: true,
message: "Test execution completed with status: Passed",
completedAt: "2026-01-01T00:00:00.0000000Z",
testCount: 1,
passedCount: 1,
failedCount: 0,
skippedCount: 0,
inconclusiveCount: 0,
xmlPath: string.Empty,
status: RunTestsExecutionStatus.Passed,
hasFailures: false,
noTestsFound: false,
noTestsFoundExplanation: string.Empty);

JObject zeroInconclusiveJson = JObject.Parse(
JsonConvert.SerializeObject(
zeroInconclusive,
Formatting.None,
UnityCliLoopJsonResponseSerializerSettings.Settings));

Assert.That(zeroInconclusiveJson.Value<int>("InconclusiveCount"), Is.EqualTo(0));
Assert.That(zeroInconclusiveJson.Property("InconclusiveTests"), Is.Null);

RunTestsResponse populated = new RunTestsResponse(
success: false,
message: "Test execution completed with status: Inconclusive",
completedAt: "2026-01-01T00:00:00.0000000Z",
testCount: 2,
passedCount: 1,
failedCount: 0,
skippedCount: 0,
inconclusiveCount: 1,
xmlPath: "TestResults/example.xml",
status: RunTestsExecutionStatus.Inconclusive,
hasFailures: false,
noTestsFound: false,
noTestsFoundExplanation: string.Empty)
{
InconclusiveTests = new[]
{
new SerializableTestResult.InconclusiveTestDetail
{
FullName = "Example.Tests.InconclusiveTest",
Message = "Release is required."
}
}
};

JObject populatedJson = JObject.Parse(
JsonConvert.SerializeObject(
populated,
Formatting.None,
UnityCliLoopJsonResponseSerializerSettings.Settings));
JArray inconclusiveTests = (JArray)populatedJson["InconclusiveTests"];

Assert.That(populatedJson.Value<int>("InconclusiveCount"), Is.EqualTo(1));
Assert.That(populatedJson.Value<string>("Status"), Is.EqualTo("Inconclusive"));
Assert.That(inconclusiveTests, Is.Not.Null);
Assert.That(inconclusiveTests.Count, Is.EqualTo(1));
JObject first = (JObject)inconclusiveTests[0];
Assert.That(first["FullName"]?.Value<string>(), Is.EqualTo("Example.Tests.InconclusiveTest"));
Assert.That(first["Message"]?.Value<string>(), Is.EqualTo("Release is required."));
}

/// <summary>
/// What: the response built from a stored result carries its inconclusive count and details,
/// and leaves InconclusiveTests unset when the result lists none.
/// </summary>
[Test]
public void FromResult_WhenResultHasInconclusiveLeaves_CopiesCountAndDetails()
{
SerializableTestResult withInconclusive = new SerializableTestResult
{
success = false,
status = RunTestsExecutionStatus.Inconclusive,
message = "Test execution completed with status: Inconclusive",
noTestsFoundExplanation = string.Empty,
completedAt = "2026-01-01T00:00:00.0000000Z",
testCount = 2,
passedCount = 1,
inconclusiveCount = 1,
inconclusiveTests = new[]
{
new SerializableTestResult.InconclusiveTestDetail
{
FullName = "Example.Tests.InconclusiveTest",
Message = "Release is required."
}
}
};
SerializableTestResult withoutInconclusive = new SerializableTestResult
{
success = true,
status = RunTestsExecutionStatus.Passed,
message = "Test execution completed with status: Passed",
noTestsFoundExplanation = string.Empty,
completedAt = "2026-01-01T00:00:00.0000000Z",
testCount = 1,
passedCount = 1,
inconclusiveCount = 0,
inconclusiveTests = new SerializableTestResult.InconclusiveTestDetail[0]
};

RunTestsResponse copied = RunTestsResponseFactory.FromResult(withInconclusive);
RunTestsResponse empty = RunTestsResponseFactory.FromResult(withoutInconclusive);

Assert.That(copied.InconclusiveCount, Is.EqualTo(1));
Assert.That(copied.InconclusiveTests, Is.Not.Null);
Assert.That(copied.InconclusiveTests.Length, Is.EqualTo(1));
Assert.That(copied.InconclusiveTests[0].FullName, Is.EqualTo("Example.Tests.InconclusiveTest"));
Assert.That(empty.InconclusiveCount, Is.EqualTo(0));
Assert.That(empty.InconclusiveTests, Is.Null);
}

/// <summary>
/// What: an empty Warning is omitted from production JSON so the key cannot reappear unnoticed.
/// </summary>
Expand All @@ -153,6 +279,7 @@ public void RunTestsResponse_WhenWarningIsEmpty_OmitsWarningPropertyFromJson()
passedCount: 1,
failedCount: 0,
skippedCount: 0,
inconclusiveCount: 0,
xmlPath: string.Empty,
status: RunTestsExecutionStatus.Passed,
hasFailures: false,
Expand Down Expand Up @@ -182,6 +309,7 @@ public void RunTestsResponse_WhenWarningIsSet_SerializesExactPolicyFormSentence(
passedCount: 1,
failedCount: 0,
skippedCount: 0,
inconclusiveCount: 0,
xmlPath: string.Empty,
status: RunTestsExecutionStatus.Passed,
hasFailures: false,
Expand Down
Loading
Loading