Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
99b4127
chore: Pull requests now fail when Go test coverage drops below the r…
hatayama Oct 2, 2026
1a6ca1c
chore: Skill scanning, tool docs and the tool catalog in the common C…
hatayama Oct 2, 2026
005f01b
chore: Dispatcher pin guards in the release-automation CLI now have t…
hatayama Oct 2, 2026
9fd0f8a
chore: Command routing and run paths in the project-runner CLI now ha…
hatayama Oct 2, 2026
a886408
chore: CLI core, project resolution and CLI errors in the common CLI …
hatayama Oct 2, 2026
82d5cf1
chore: Pause point paths in the project-runner CLI now have tests for…
hatayama Oct 2, 2026
7fb8538
chore: Package manager and release scheduling commands in the release…
hatayama Oct 2, 2026
c3b6ff0
chore: Dispatcher install, update, and download paths in the dispatch…
hatayama Oct 2, 2026
cfb50ab
chore: The IPC client, endpoint checks and process parsing in the com…
hatayama Oct 2, 2026
556197d
chore: Wait, compile, and retry paths in the project-runner CLI now h…
hatayama Oct 2, 2026
7965fbd
chore: Protocol and release PR guards in the release-automation CLI n…
hatayama Oct 2, 2026
42054fb
chore: File length checks now count C# lines correctly after escaped …
hatayama Oct 2, 2026
f16fdd4
test(dispatcher): Cover skill install, sync, discovery, and dir-mode …
hatayama Oct 2, 2026
d67a15e
chore: Dispatcher support packages now have tests for compile-check l…
hatayama Oct 2, 2026
963647b
chore: The release-automation legacy-probe failure test no longer fla…
hatayama Oct 2, 2026
93ba78e
chore: Repository checks in the release-automation CLI now have tests…
hatayama Oct 2, 2026
68ac566
chore: File length checks now count C# lines correctly after a brace …
hatayama Oct 2, 2026
4d13ca0
test(dispatcher): Cover package, launch, compile-check, help, and att…
hatayama Oct 2, 2026
ba15989
chore: Dispatcher help, platform errors, command routing, and version…
hatayama Oct 2, 2026
fef8906
fix: Unsupported-platform errors from install, update, and uninstall …
hatayama Oct 2, 2026
fd7230a
chore: Nightly runs now record Go and C# coverage in a tracking issue…
hatayama Oct 2, 2026
9c4bcdd
chore: Coverage baselines now hold the raised Go test coverage (#3095)
hatayama Oct 2, 2026
7c7c60a
fix: Tool migration no longer edits screenshot capture calls whose pa…
hatayama Oct 2, 2026
6f2fccd
chore: Cover presentation-layer setup wizard and Settings presenters …
hatayama Oct 2, 2026
46cad6e
chore: Domain migration rules, CLI path setup, and registry checks no…
hatayama Oct 2, 2026
3ccea65
chore: Cover tool edge paths for run-tests, hierarchy, compile, watch…
hatayama Oct 2, 2026
26d0bbf
Merge main into feature/test-coverage
hatayama Oct 3, 2026
236009c
chore: ToolContracts and Application use cases, waiters, and schema g…
hatayama Oct 3, 2026
53c94d2
chore: The C# coverage baseline now records the measured 82.4% (#3103)
hatayama Oct 3, 2026
a2de7d6
chore: Source scanning, path resolution, diagnostics, and the compile…
hatayama Oct 3, 2026
37cdc16
chore: Infrastructure bridge framing, CLI install paths, skill sync, …
hatayama Oct 3, 2026
f0f6072
fix: execute-dynamic-code reports an empty result for an async Task E…
hatayama Oct 3, 2026
fc29e0a
fix: Tool migration now removes the timing argument from a call neste…
hatayama Oct 3, 2026
9c37809
fix: Tool migration no longer leaves an attribute behind when it remo…
hatayama Oct 3, 2026
3aa9036
fix: Tool migration now resolves base-qualified timing calls against …
hatayama Oct 3, 2026
ea7d13a
fix: Raw string literals in execute-dynamic-code no longer turn into …
hatayama Oct 3, 2026
258d805
chore: Screenshot and RecordVideo validation, annotation, overlay, an…
hatayama Oct 3, 2026
5ee7e90
fix: Tool migration no longer leaves an attribute behind when it remo…
hatayama Oct 3, 2026
95f1645
fix: Tool migration no longer deletes a timing declaration statement …
hatayama Oct 3, 2026
15a1da8
fix: Project IPC warmup now reports a string JSON-RPC error with its …
hatayama Oct 3, 2026
0c8758e
chore: HotReload snapshot, fingerprint, validator and registry edge p…
hatayama Oct 3, 2026
628e11c
fix: The Unity IPC server now closes a session whose frame header has…
hatayama Oct 3, 2026
c46d99d
fix: CLI PATH setup no longer treats a PATH line with spaces around '…
hatayama Oct 3, 2026
2cf11c3
fix: A transform worker that refuses to be killed is now always repor…
hatayama Oct 3, 2026
38f1530
chore: HotReload validation, wired-value, patch-target and patcher pa…
hatayama Oct 3, 2026
f0bec96
chore: ExecuteDynamicCode, pause point preview and capture, tool sett…
hatayama Oct 3, 2026
8745dd9
fix: A HashSet in pause-point captured variables and watch values now…
hatayama Oct 3, 2026
93e10cb
fix: Screenshot annotations no longer list non-interactable selectabl…
hatayama Oct 3, 2026
ef43bfa
fix: An execute-dynamic-code snippet that returns a Task now reports …
hatayama Oct 3, 2026
aa6a005
chore: The C# coverage baseline now records the measured 85.4% (#3124)
hatayama Oct 3, 2026
99a8047
Merge main into feature/test-coverage
hatayama Oct 3, 2026
b5419ef
chore: Hot reload snapshot capture and changed-file aggregation take …
hatayama Oct 3, 2026
aaa527d
chore: Open the bridge client session loop and the disconnect classif…
hatayama Oct 3, 2026
94e4635
chore: Server recovery's bind-retry wait can now be injected so its r…
hatayama Oct 3, 2026
de2de18
chore: Extract the project IPC warmup frame exchange so tests can dri…
hatayama Oct 3, 2026
0760a3f
Merge main into feature/test-coverage
hatayama Oct 3, 2026
6738ea9
chore: Add tests for how compile requests start, wait, and clean up (…
hatayama Oct 4, 2026
958be76
fix: Tool migration now removes the timing argument from inherited ca…
hatayama Oct 4, 2026
7372548
chore: Add tests for how video recording starts, stops, and reacts to…
hatayama Oct 4, 2026
92660db
chore: Cover CLI installation detection and Windows path lookup with …
hatayama Oct 4, 2026
7a51f97
chore: Add tests for window screenshots, including the Simulator fall…
hatayama Oct 4, 2026
10332c6
chore: Cover the skills install workflows in settings and the setup w…
hatayama Oct 4, 2026
19d0758
chore: Cover editor settings file cleanup and migration edge cases wi…
hatayama Oct 4, 2026
cbcf2fd
chore: Add tests for waiting on Editor frames with a timeout (#3140)
hatayama Oct 4, 2026
f556b49
chore: Cover CLI install, PATH repair, and uninstall in settings and …
hatayama Oct 4, 2026
082f3d7
chore: Cover the migration wizard and the startup migration auto-scan…
hatayama Oct 4, 2026
d7b8762
chore: Add tests for how the Unity IPC server handles start failures,…
hatayama Oct 4, 2026
a1e915e
chore: Add tests for the runInBackground override in CLI-started Play…
hatayama Oct 4, 2026
929114e
chore: Fail async editor tests when an awaited task is canceled (#3145)
hatayama Oct 4, 2026
d2d752f
chore: The C# coverage baseline now records the measured 87.3% (#3146)
hatayama Oct 4, 2026
fce8557
fix: Tool migration now updates inherited calls to methods that retur…
hatayama Oct 4, 2026
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
348 changes: 348 additions & 0 deletions Assets/Tests/Editor/CliInstallationDetectorCacheTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.Application;
using io.github.hatayama.UnityCliLoop.Infrastructure;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Test fixture that verifies how the CLI installation detector caches, refreshes, and invalidates detection results.
/// </summary>
public sealed class CliInstallationDetectorCacheTests
{
private const string DetectedVersion = "3.6.0";
private const string DetectedPath = "<PROJECT_ROOT>/bin/uloop";

/// <summary>
/// Verifies a refresh stores the detected version, path, and dispatcher flag and marks the check completed.
/// </summary>
[Test]
public async Task RefreshCliVersionAsync_WhenDetectionSucceeds_CachesDetection()
{
CountingDetection detection = new(new CliInstallationDetection(DetectedVersion, DetectedPath, true));
CliInstallationDetector detector = CreateDetector(detection);

await detector.RefreshCliVersionAsync(CancellationToken.None);

Assert.That(detector.IsCheckCompleted(), Is.True);
Assert.That(detector.IsCliInstalled(), Is.True);
Assert.That(detector.GetCachedCliVersion(), Is.EqualTo(DetectedVersion));
Assert.That(detector.GetCachedCliExecutablePath(), Is.EqualTo(DetectedPath));
Assert.That(detector.GetCachedCliIsDispatcher(), Is.True);
Assert.That(detection.CallCount, Is.EqualTo(1));
}

/// <summary>
/// Verifies a refresh hands the caller's token to the detection so cancelling it can stop the CLI process.
/// </summary>
[Test]
public async Task RefreshCliVersionAsync_PassesTheCallerTokenToDetection()
{
CountingDetection detection = new(new CliInstallationDetection(DetectedVersion, DetectedPath));
CliInstallationDetector detector = CreateDetector(detection);
using CancellationTokenSource cancellation = new();

await detector.RefreshCliVersionAsync(cancellation.Token);

Assert.That(detection.ReceivedTokens, Is.EqualTo(new[] { cancellation.Token }));
}

/// <summary>
/// Verifies a forced refresh hands the caller's token to the detection so cancelling it can stop the CLI process.
/// </summary>
[Test]
public async Task ForceRefreshCliVersionAsync_PassesTheCallerTokenToDetection()
{
CountingDetection detection = new(new CliInstallationDetection(DetectedVersion, DetectedPath));
CliInstallationDetector detector = CreateDetector(detection);
using CancellationTokenSource cancellation = new();

await detector.ForceRefreshCliVersionAsync(cancellation.Token);

Assert.That(detection.ReceivedTokens, Is.EqualTo(new[] { cancellation.Token }));
}

/// <summary>
/// Verifies a completed check without a detected version reports the CLI as not installed.
/// </summary>
[Test]
public async Task RefreshCliVersionAsync_WhenNoVersionIsDetected_ReportsNotInstalled()
{
CountingDetection detection = new(new CliInstallationDetection(null, DetectedPath));
CliInstallationDetector detector = CreateDetector(detection);

await detector.RefreshCliVersionAsync(CancellationToken.None);

Assert.That(detector.IsCheckCompleted(), Is.True);
Assert.That(detector.IsCliInstalled(), Is.False);
Assert.That(detector.GetCachedCliExecutablePath(), Is.EqualTo(DetectedPath));
Assert.That(detector.GetCachedCliIsDispatcher(), Is.False);
}

/// <summary>
/// Verifies a second refresh reuses the cached result instead of detecting again.
/// </summary>
[Test]
public async Task RefreshCliVersionAsync_WhenCacheIsInitialized_DoesNotDetectAgain()
{
CountingDetection detection = new(new CliInstallationDetection(DetectedVersion, DetectedPath));
CliInstallationDetector detector = CreateDetector(detection);
await detector.RefreshCliVersionAsync(CancellationToken.None);

await detector.RefreshCliVersionAsync(CancellationToken.None);

Assert.That(detection.CallCount, Is.EqualTo(1));
}

/// <summary>
/// Verifies a refresh requested while another refresh is pending returns without starting a second detection.
/// </summary>
[Test]
public async Task RefreshCliVersionAsync_WhenRefreshIsPending_DoesNotStartSecondDetection()
{
PendingDetection detection = new();
CliInstallationDetector detector = CreateDetector(detection);
Task firstRefresh = detector.RefreshCliVersionAsync(CancellationToken.None);

Task secondRefresh = detector.RefreshCliVersionAsync(CancellationToken.None);

Assert.That(secondRefresh.IsCompleted, Is.True);
Assert.That(detection.CallCount, Is.EqualTo(1));
Assert.That(detector.IsCheckCompleted(), Is.False);

detection.Complete(new CliInstallationDetection(DetectedVersion, DetectedPath));
await firstRefresh;

Assert.That(detector.GetCachedCliVersion(), Is.EqualTo(DetectedVersion));
}

/// <summary>
/// Verifies a failed detection clears the in-progress flag so the next refresh detects again.
/// </summary>
[Test]
public async Task RefreshCliVersionAsync_WhenDetectionThrows_AllowsNextRefresh()
{
ThrowOnceDetection detection = new(new CliInstallationDetection(DetectedVersion, DetectedPath));
CliInstallationDetector detector = CreateDetector(detection);

// Awaited in try / catch instead of Assert.ThrowsAsync, which blocks the main thread in this NUnit.
try
{
await detector.RefreshCliVersionAsync(CancellationToken.None);
Assert.Fail("Expected the detection failure to propagate.");
}
catch (InvalidOperationException exception)
{
Assert.That(exception.Message, Is.EqualTo(ThrowOnceDetection.ErrorMessage));
}

Assert.That(detector.IsCheckCompleted(), Is.False);

await detector.RefreshCliVersionAsync(CancellationToken.None);

Assert.That(detection.CallCount, Is.EqualTo(2));
Assert.That(detector.GetCachedCliVersion(), Is.EqualTo(DetectedVersion));
}

/// <summary>
/// Verifies a forced refresh detects again and replaces an already cached result.
/// </summary>
[Test]
public async Task ForceRefreshCliVersionAsync_WhenCacheIsInitialized_ReplacesCachedDetection()
{
SequenceDetection detection = new(
new CliInstallationDetection(DetectedVersion, DetectedPath, true),
new CliInstallationDetection("3.7.0", "<PROJECT_ROOT>/other/uloop"));
CliInstallationDetector detector = CreateDetector(detection);
await detector.RefreshCliVersionAsync(CancellationToken.None);

await detector.ForceRefreshCliVersionAsync(CancellationToken.None);

Assert.That(detection.CallCount, Is.EqualTo(2));
Assert.That(detector.GetCachedCliVersion(), Is.EqualTo("3.7.0"));
Assert.That(detector.GetCachedCliExecutablePath(), Is.EqualTo("<PROJECT_ROOT>/other/uloop"));
Assert.That(detector.GetCachedCliIsDispatcher(), Is.False);
Assert.That(detector.IsCheckCompleted(), Is.True);
}

/// <summary>
/// Verifies a forced refresh fills the cache even when no refresh ran before.
/// </summary>
[Test]
public async Task ForceRefreshCliVersionAsync_WhenCacheIsEmpty_CachesDetection()
{
CountingDetection detection = new(new CliInstallationDetection(DetectedVersion, DetectedPath, true));
CliInstallationDetector detector = CreateDetector(detection);

await detector.ForceRefreshCliVersionAsync(CancellationToken.None);

Assert.That(detector.IsCheckCompleted(), Is.True);
Assert.That(detector.GetCachedCliVersion(), Is.EqualTo(DetectedVersion));
Assert.That(detector.GetCachedCliIsDispatcher(), Is.True);
}

/// <summary>
/// Verifies invalidating the cache hides the cached result and lets the next refresh detect again.
/// </summary>
[Test]
public async Task InvalidateCache_WhenCacheIsInitialized_ClearsResultAndAllowsRefresh()
{
CountingDetection detection = new(new CliInstallationDetection(DetectedVersion, DetectedPath, true));
CliInstallationDetector detector = CreateDetector(detection);
await detector.RefreshCliVersionAsync(CancellationToken.None);

detector.InvalidateCache();

Assert.That(detector.IsCheckCompleted(), Is.False);
Assert.That(detector.IsCliInstalled(), Is.False);
Assert.That(detector.GetCachedCliVersion(), Is.Null);
Assert.That(detector.GetCachedCliExecutablePath(), Is.Null);
Assert.That(detector.GetCachedCliIsDispatcher(), Is.False);

await detector.RefreshCliVersionAsync(CancellationToken.None);

Assert.That(detection.CallCount, Is.EqualTo(2));
}

/// <summary>
/// Verifies invalidating the cache during a pending refresh lets a new refresh start its own detection.
/// </summary>
[Test]
public async Task InvalidateCache_WhenRefreshIsPending_AllowsNewRefresh()
{
PendingDetection detection = new();
CliInstallationDetector detector = CreateDetector(detection);
Task firstRefresh = detector.RefreshCliVersionAsync(CancellationToken.None);

detector.InvalidateCache();
Task secondRefresh = detector.RefreshCliVersionAsync(CancellationToken.None);

Assert.That(detection.CallCount, Is.EqualTo(2));

detection.Complete(new CliInstallationDetection(DetectedVersion, DetectedPath));
await firstRefresh;
await secondRefresh;

Assert.That(detector.GetCachedCliVersion(), Is.EqualTo(DetectedVersion));
}

private static CliInstallationDetector CreateDetector(IFakeDetection detection)
{
CliInstallationDetector detector = new(new UnusedPinReader());
detector.SetDetectionForTesting(detection.DetectAsync);
return detector;
}

private interface IFakeDetection
{
Task<CliInstallationDetection> DetectAsync(CancellationToken ct);
}

private sealed class CountingDetection : IFakeDetection
{
private readonly CliInstallationDetection _result;

public CountingDetection(CliInstallationDetection result)
{
_result = result;
}

public int CallCount { get; private set; }

public List<CancellationToken> ReceivedTokens { get; } = new();

public Task<CliInstallationDetection> DetectAsync(CancellationToken ct)
{
CallCount++;
ReceivedTokens.Add(ct);
return Task.FromResult(_result);
}
}

private sealed class SequenceDetection : IFakeDetection
{
private readonly CliInstallationDetection[] _results;

public SequenceDetection(params CliInstallationDetection[] results)
{
_results = results;
}

public int CallCount { get; private set; }

public Task<CliInstallationDetection> DetectAsync(CancellationToken ct)
{
CliInstallationDetection result = _results[CallCount];
CallCount++;
return Task.FromResult(result);
}
}

private sealed class ThrowOnceDetection : IFakeDetection
{
public const string ErrorMessage = "detection failed in this test";

private readonly CliInstallationDetection _result;

public ThrowOnceDetection(CliInstallationDetection result)
{
_result = result;
}

public int CallCount { get; private set; }

public Task<CliInstallationDetection> DetectAsync(CancellationToken ct)
{
CallCount++;
if (CallCount == 1)
{
return Task.FromException<CliInstallationDetection>(new InvalidOperationException(ErrorMessage));
}

return Task.FromResult(_result);
}
}

// Every call shares one task that the test completes itself, so nothing outlives the test.
private sealed class PendingDetection : IFakeDetection
{
private readonly TaskCompletionSource<CliInstallationDetection> _completion = new();

public int CallCount { get; private set; }

public Task<CliInstallationDetection> DetectAsync(CancellationToken ct)
{
CallCount++;
return _completion.Task;
}

public void Complete(CliInstallationDetection result)
{
_completion.SetResult(result);
}
}

private sealed class UnusedPinReader : ICliPinReader
{
public CliPinLoadResult LoadPackagePin()
{
throw new InvalidOperationException("The cache tests never read the pin.");
}

public DispatcherBootstrapPinLoadResult LoadDispatcherBootstrapPin()
{
throw new InvalidOperationException("The cache tests never read the pin.");
}

public string LoadMinimumDispatcherVersionOrThrow()
{
throw new InvalidOperationException("The cache tests never read the pin.");
}
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/CliInstallationDetectorCacheTests.cs.meta

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

Loading
Loading