fix(tests): stop Ui.Tests order-dependent flakiness on shared Loc.Current
Loc.Current (src/ClaudeDo.Ui/Localization/Loc.cs) is a process-wide mutable static; which localizer is active depended on xUnit's parallel test scheduling. Ui.Tests: disable collection parallelization (cheap, pure VM suite) and install the real localizer in a module initializer so no test can ever observe the uninitialized key-echo default. Worker.Tests: install the same module initializer but leave parallelization on — disabling it measured a 27% wall-clock regression on its real-SQLite/git suite, and the initializer alone already removes the race. AgentConfigEditorViewModelTests now installs its own localizer instead of relying on ambient state.
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using ClaudeDo.Localization;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using Xunit;
|
||||
|
||||
// Loc.Current (src/ClaudeDo.Ui/Localization/Loc.cs) is a process-wide mutable static that many
|
||||
// ViewModel test constructors assign — parallel collections race on which localizer is active.
|
||||
[assembly: CollectionBehavior(DisableTestParallelization = true)]
|
||||
|
||||
namespace ClaudeDo.Ui.Tests;
|
||||
|
||||
@@ -17,4 +24,17 @@ internal static class TestSetup
|
||||
var path = Path.Combine(Path.GetTempPath(), "claudedo-ui-tests", $"operation-timing-{Environment.ProcessId}.ndjson");
|
||||
OperationTiming.Shared = new OperationTiming(path);
|
||||
}
|
||||
|
||||
// Installs the real localizer before any test runs, so a class that reads Loc.T/Loc.Current
|
||||
// without setting it up itself (e.g. via a ViewModel error message) doesn't fall back to
|
||||
// Loc's key-echo default depending on which test class happens to run first.
|
||||
[ModuleInitializer]
|
||||
internal static void InstallRealLocalizer()
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
while (dir is not null && !Directory.Exists(Path.Combine(dir, "src", "ClaudeDo.Localization", "locales")))
|
||||
dir = Path.GetDirectoryName(dir);
|
||||
Loc.Current = new Localizer(
|
||||
LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Localization;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Agent;
|
||||
@@ -9,6 +10,18 @@ namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class AgentConfigEditorViewModelTests
|
||||
{
|
||||
public AgentConfigEditorViewModelTests()
|
||||
{
|
||||
// Loc.T(...) is read below without installing a localizer via a ViewModel constructor
|
||||
// (unlike its sibling test classes) — install one directly so this class doesn't depend
|
||||
// on whichever neighbour test happened to run first and set Loc.Current.
|
||||
var dir = AppContext.BaseDirectory;
|
||||
while (dir is not null && !Directory.Exists(Path.Combine(dir, "src", "ClaudeDo.Localization", "locales")))
|
||||
dir = Path.GetDirectoryName(dir);
|
||||
Loc.Current = new Localizer(
|
||||
LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en");
|
||||
}
|
||||
|
||||
private sealed class FakeWorker : StubWorkerClient
|
||||
{
|
||||
public AppSettingsDto? App;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using ClaudeDo.Localization;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests;
|
||||
@@ -16,4 +18,20 @@ internal static class TestSetup
|
||||
var path = Path.Combine(Path.GetTempPath(), "claudedo-worker-tests", $"operation-timing-{Environment.ProcessId}.ndjson");
|
||||
OperationTiming.Shared = new OperationTiming(path);
|
||||
}
|
||||
|
||||
// Installs the real localizer before any test runs, mirroring ClaudeDo.Ui.Tests' TestSetup,
|
||||
// so a UiVm test that reads Loc.T without setting up its own localizer doesn't depend on
|
||||
// ambient state left by whichever test class happened to run first. Deliberately does NOT
|
||||
// also disable test parallelization here (unlike Ui.Tests) — measured 14m49s -> 18m46s
|
||||
// (+27%) on this project's real-SQLite/git suite, too costly for a test-only ordering issue
|
||||
// that the module initializer alone already closes.
|
||||
[ModuleInitializer]
|
||||
internal static void InstallRealLocalizer()
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
while (dir is not null && !Directory.Exists(Path.Combine(dir, "src", "ClaudeDo.Localization", "locales")))
|
||||
dir = Path.GetDirectoryName(dir);
|
||||
Loc.Current = new Localizer(
|
||||
LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user