From dc1e67d07b1d771d5658bee2a041a770a820f0bc Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Wed, 2 Sep 2026 12:55:51 +0200 Subject: [PATCH] fix(tests): stop Ui.Tests order-dependent flakiness on shared Loc.Current MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tests/ClaudeDo.Ui.Tests/TestSetup.cs | 20 +++++++++++++++++++ .../AgentConfigEditorViewModelTests.cs | 13 ++++++++++++ tests/ClaudeDo.Worker.Tests/TestSetup.cs | 18 +++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/tests/ClaudeDo.Ui.Tests/TestSetup.cs b/tests/ClaudeDo.Ui.Tests/TestSetup.cs index 79ef3e65..90277d81 100644 --- a/tests/ClaudeDo.Ui.Tests/TestSetup.cs +++ b/tests/ClaudeDo.Ui.Tests/TestSetup.cs @@ -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"); + } } diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/AgentConfigEditorViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/AgentConfigEditorViewModelTests.cs index 12101a51..b6cdbdc6 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/AgentConfigEditorViewModelTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/AgentConfigEditorViewModelTests.cs @@ -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; diff --git a/tests/ClaudeDo.Worker.Tests/TestSetup.cs b/tests/ClaudeDo.Worker.Tests/TestSetup.cs index ef2daba8..64564ee7 100644 --- a/tests/ClaudeDo.Worker.Tests/TestSetup.cs +++ b/tests/ClaudeDo.Worker.Tests/TestSetup.cs @@ -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"); + } }