From f606d2656ea68ff30d82af4a796be7bbd64bfca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= Date: Sun, 9 Aug 2026 22:41:09 +0200 Subject: [PATCH] =?UTF-8?q?perf(test):=20per-class=20test=20partition,=20a?= =?UTF-8?q?nd=20parallelise=20AI.Test=20=E2=80=94=202.11x?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the root cause behind #1040 and re-lands #1035. ## The defect `MonolithMeshTestBase.TestPartition` was `public const string = "TestData"`, so EVERY test class in EVERY project wrote into one partition — and a partition has a single owning hub that serialises every write routed through it. Harmless while classes run one at a time; a contention point the moment they do not. That is why #1035 (AI.Test parallel) red on CI: waits that never emitted, in a class that is green in isolation under identical CPU pressure (ThreadTokenUsageTest: 8/8 in 5.6 s alone, failing alongside 1,130 neighbours). Not a timeout budget — 🚨 do not "fix" that shape by widening the wait. ## The fix `TestPartition` becomes `public virtual`, DEFAULT UNCHANGED ("TestData"), so no other project moves. Isolation is opt-in per project base — here, one override: // AITestBase public override string TestPartition => $"TestData{GetType().Name}"; giving all 39 AI.Test classes their own partition from a single line. ## Cascade the compiler forced (each is a hard error, never a silent change) 42 static-qualified refs (MonolithMeshTestBase.TestPartition) -> instance, 15 files 3 hardcoded node paths in ThreadComposerFlowTest -> $"{TestPartition}/…" 2 CS1736 default-parameter values -> null-default + `?? TestPartition` 2 CS0236 static field initialisers -> instance properties The 20 other "TestData" strings in AI.Test are `Path.Combine(AppContext.BaseDirectory, "TestData")` — on-disk directories, unrelated to the partition — and are untouched. 🚨 The CS1736 sites needed the `?? TestPartition` coalesce: `string? ns = null` alone would have passed null where the partition used to be — a silent behaviour change the compiler could NOT have caught. ## Verified on a representative machine, not this one A dev Mac has 18 cores; the runner has 4, so "maxParallelThreads: 4" means something completely different on each — which is exactly why #1033 and #1035 passed locally and red on CI. `DOTNET_PROCESSOR_COUNT=4` makes .NET size its thread pool and GC as if it had 4 cores: before fix, DOTNET_PROCESSOR_COUNT=4 : 1138 tests, 2 FAILED (ThreadTokenUsageTest) after fix, DOTNET_PROCESSOR_COUNT=4 : 1138 tests, 0 failed, 117 s Serial baseline for reference: 198.2 s (2.11x). Hosting.Monolith.Test (#1033, measured 2.57x) is NOT re-enabled here — it also needs GcReachabilityCollection from branch perf/share-mesh-cluster. One project at a time. Co-Authored-By: Claude Opus 5 (1M context) --- .../MonolithMeshTestBase.cs | 5 ++++- test/MeshWeaver.AI.Test/AITestBase.cs | 6 ++++++ .../AutoRouterDispatchTest.cs | 4 ++-- test/MeshWeaver.AI.Test/CancellationAckTest.cs | 4 ++-- .../ChatClientCredentialResolverTest.cs | 2 +- .../ChatCommandSubmissionTest.cs | 6 +++--- .../CrossHubPatchAtomicityTest.cs | 4 ++-- test/MeshWeaver.AI.Test/DocumentNodeGetTest.cs | 2 +- .../HarnessInstallGateTest.cs | 18 +++++++++--------- .../InboxToolIntegrationTest.cs | 4 ++-- .../McpCompileInFlightTruthTest.cs | 2 +- .../McpFailureSurfacingTest.cs | 4 ++-- .../McpNegativeOperationsTest.cs | 4 ++-- .../ModelSubstitutionTest.cs | 4 ++-- .../ProviderRefusalRoundTest.cs | 4 ++-- .../RoundFaultTerminalStateTest.cs | 4 ++-- .../MeshWeaver.AI.Test/StreamingTimeoutTest.cs | 4 ++-- .../ThreadComposerFlowTest.cs | 16 ++++++++-------- .../ThreadSubmissionIntegrationTest.cs | 10 +++++----- .../MeshWeaver.AI.Test/ThreadTokenUsageTest.cs | 4 ++-- test/MeshWeaver.AI.Test/xunit.runner.json | 6 ++++++ .../ScriptTemplateExecuteAccessTest.cs | 2 +- 22 files changed, 67 insertions(+), 52 deletions(-) create mode 100644 test/MeshWeaver.AI.Test/xunit.runner.json diff --git a/src/MeshWeaver.Hosting.Monolith.TestBase/MonolithMeshTestBase.cs b/src/MeshWeaver.Hosting.Monolith.TestBase/MonolithMeshTestBase.cs index 18a923bfc1..f6cc509006 100644 --- a/src/MeshWeaver.Hosting.Monolith.TestBase/MonolithMeshTestBase.cs +++ b/src/MeshWeaver.Hosting.Monolith.TestBase/MonolithMeshTestBase.cs @@ -57,7 +57,10 @@ public abstract class MonolithMeshTestBase : Fixture.TestBase /// (e.g., "TestData/mynode") and they'll have proper mesh node hubs. /// Registered as a Markdown node so the hub gets AddMeshDataSource + WithNodeOperationHandlers. /// - public const string TestPartition = "TestData"; + /// Per-class overridable so concurrently-running classes do not contend on one + /// partition hub. Default is unchanged ("TestData"): only a project that opts in — by + /// overriding in its own base — gets isolation, so no existing test moves. See #1040. + public virtual string TestPartition => "TestData"; /// /// Quiescing budget for test-mesh hubs. diff --git a/test/MeshWeaver.AI.Test/AITestBase.cs b/test/MeshWeaver.AI.Test/AITestBase.cs index 257780e13b..156b49485b 100644 --- a/test/MeshWeaver.AI.Test/AITestBase.cs +++ b/test/MeshWeaver.AI.Test/AITestBase.cs @@ -10,6 +10,12 @@ namespace MeshWeaver.AI.Test; /// public abstract class AITestBase(ITestOutputHelper output) : MonolithMeshTestBase(output) { + + /// Per-class partition, so concurrently-running AI.Test classes do not contend on one + /// partition hub — the root cause behind the parallel failures in #1040 (a partition has ONE + /// owning hub that serialises every write routed through it). Overriding here covers all + /// AI.Test classes in one place; other projects keep the "TestData" default and are unaffected. + public override string TestPartition => $"TestData{GetType().Name}"; /// protected override MeshBuilder ConfigureMesh(MeshBuilder builder) => base.ConfigureMesh(builder).AddAI(); diff --git a/test/MeshWeaver.AI.Test/AutoRouterDispatchTest.cs b/test/MeshWeaver.AI.Test/AutoRouterDispatchTest.cs index f4f7e43f64..c1cec27107 100644 --- a/test/MeshWeaver.AI.Test/AutoRouterDispatchTest.cs +++ b/test/MeshWeaver.AI.Test/AutoRouterDispatchTest.cs @@ -376,12 +376,12 @@ await NodeFactory.CreateNode(new MeshNode(id, ProviderPath) private async Task SeedThread() { var threadId = Guid.NewGuid().AsString(); - var threadPath = $"{MonolithMeshTestBase.TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; + var threadPath = $"{TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; await NodeFactory.CreateNode(MeshNode.FromPath(threadPath) with { Name = $"Auto Dispatch Thread {threadId}", NodeType = ThreadNodeType.NodeType, - MainNode = MonolithMeshTestBase.TestPartition, + MainNode = TestPartition, Content = new MeshThread { CreatedBy = TestUser } }).Should().Within(30.Seconds()).Emit(); return threadPath; diff --git a/test/MeshWeaver.AI.Test/CancellationAckTest.cs b/test/MeshWeaver.AI.Test/CancellationAckTest.cs index d472bec842..9345da8f60 100644 --- a/test/MeshWeaver.AI.Test/CancellationAckTest.cs +++ b/test/MeshWeaver.AI.Test/CancellationAckTest.cs @@ -56,12 +56,12 @@ public async Task Stop_ImmediatelyStampsCancellationAck_BeforeRoundTearsDown() { var ct = TestContext.Current.CancellationToken; var threadId = Guid.NewGuid().AsString(); - var threadPath = $"{MonolithMeshTestBase.TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; + var threadPath = $"{TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; await NodeFactory.CreateNode(MeshNode.FromPath(threadPath) with { Name = $"Cancellation Ack Test Thread {threadId}", NodeType = ThreadNodeType.NodeType, - MainNode = MonolithMeshTestBase.TestPartition, + MainNode = TestPartition, Content = new MeshThread { CreatedBy = "rbuergi@systemorph.com" } }).Should().Emit(); diff --git a/test/MeshWeaver.AI.Test/ChatClientCredentialResolverTest.cs b/test/MeshWeaver.AI.Test/ChatClientCredentialResolverTest.cs index 42a280726e..8935b3c1e5 100644 --- a/test/MeshWeaver.AI.Test/ChatClientCredentialResolverTest.cs +++ b/test/MeshWeaver.AI.Test/ChatClientCredentialResolverTest.cs @@ -551,7 +551,7 @@ await MeshService.CreateNode(new MeshNode(defaultModelId, providerPath) // Seed the user's MASTER composer pinned to a GONE model (never created in the catalog) — the // exact stuck-selection the maintainer reported. var user = Mesh.ServiceProvider.GetRequiredService().Context?.ObjectId - ?? MonolithMeshTestBase.TestPartition; + ?? TestPartition; var masterPath = ThreadComposerNodeType.PathFor(user); var goneModel = $"gone-model-{suffix}"; await MeshService.CreateNode(MeshNode.FromPath(masterPath) with diff --git a/test/MeshWeaver.AI.Test/ChatCommandSubmissionTest.cs b/test/MeshWeaver.AI.Test/ChatCommandSubmissionTest.cs index 7963d50b31..02e54e67f5 100644 --- a/test/MeshWeaver.AI.Test/ChatCommandSubmissionTest.cs +++ b/test/MeshWeaver.AI.Test/ChatCommandSubmissionTest.cs @@ -147,7 +147,7 @@ public async Task StartThread_WhitespaceOnly_CreatesThread_ButRunsNoRound() var client = GetClient(); client.StartThread( - MonolithMeshTestBase.TestPartition, + TestPartition, " ", agentName: "Assistant", createdBy: "rbuergi@systemorph.com", @@ -180,12 +180,12 @@ await Mesh.GetWorkspace().GetMeshNodeStream(threadPath) private async Task SeedEmptyThread() { var threadId = Guid.NewGuid().AsString(); - var threadPath = $"{MonolithMeshTestBase.TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; + var threadPath = $"{TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; await NodeFactory.CreateNode(MeshNode.FromPath(threadPath) with { Name = $"Test Thread {threadId}", NodeType = ThreadNodeType.NodeType, - MainNode = MonolithMeshTestBase.TestPartition, + MainNode = TestPartition, Content = new MeshThread { CreatedBy = "rbuergi@systemorph.com" } }).Should().Emit(); return threadPath; diff --git a/test/MeshWeaver.AI.Test/CrossHubPatchAtomicityTest.cs b/test/MeshWeaver.AI.Test/CrossHubPatchAtomicityTest.cs index 53bcacaf9a..cf0b096463 100644 --- a/test/MeshWeaver.AI.Test/CrossHubPatchAtomicityTest.cs +++ b/test/MeshWeaver.AI.Test/CrossHubPatchAtomicityTest.cs @@ -84,7 +84,7 @@ public async Task ConcurrentCrossHubPatches_DoNotDropAQueuedMessage() for (var round = 0; round < rounds; round++) { var nodeId = Guid.NewGuid().AsString(); - var path = $"{MonolithMeshTestBase.TestPartition}/CrossHubAtomicity/{nodeId}"; + var path = $"{TestPartition}/CrossHubAtomicity/{nodeId}"; // Fresh node per round, empty merge-safe dict. NodeType is a plain "Markdown" // node (NOT Thread) so NO submission watcher runs — this isolates the OWNER @@ -93,7 +93,7 @@ await NodeFactory.CreateNode(MeshNode.FromPath(path) with { Name = $"Atomicity Node {nodeId}", NodeType = "Markdown", - MainNode = MonolithMeshTestBase.TestPartition, + MainNode = TestPartition, Content = new MeshThread { CreatedBy = "rbuergi@systemorph.com" } }).FirstAsync().ToTask(ct); diff --git a/test/MeshWeaver.AI.Test/DocumentNodeGetTest.cs b/test/MeshWeaver.AI.Test/DocumentNodeGetTest.cs index 7bc9b52951..16d3e6d532 100644 --- a/test/MeshWeaver.AI.Test/DocumentNodeGetTest.cs +++ b/test/MeshWeaver.AI.Test/DocumentNodeGetTest.cs @@ -21,7 +21,7 @@ namespace MeshWeaver.AI.Test; public class DocumentNodeGetTest(ITestOutputHelper output) : MonolithMeshTestBase(output) { /// The collection segment is deliberately the UCR keyword content. - private static readonly string Collection = TestPartition + "/content"; + private string Collection => TestPartition + "/content"; /// Registers the Document NodeType + sink on the test mesh. protected override MeshBuilder ConfigureMesh(MeshBuilder builder) diff --git a/test/MeshWeaver.AI.Test/HarnessInstallGateTest.cs b/test/MeshWeaver.AI.Test/HarnessInstallGateTest.cs index a507945ca9..d9eb2bdad1 100644 --- a/test/MeshWeaver.AI.Test/HarnessInstallGateTest.cs +++ b/test/MeshWeaver.AI.Test/HarnessInstallGateTest.cs @@ -123,7 +123,7 @@ public async Task RequiresInstall_WithoutInstalledNode_ResolvesNull() // The absent-node case emits only after the InstallProbeTimeout elapses — give the // assertion strictly more than that so it never races the probe. var resolved = await HarnessNodeType - .ResolveInstalledHarness(hub, $"{MonolithMeshTestBase.TestPartition}/Harness/{GatedId}") + .ResolveInstalledHarness(hub, $"{TestPartition}/Harness/{GatedId}") .Should().Within(HarnessNodeType.InstallProbeTimeout + TimeSpan.FromSeconds(10)).Emit(); resolved.Should().BeNull( "a RequiresInstall harness without its installed node must fall back — this is what " + @@ -138,7 +138,7 @@ public async Task RequiresInstall_WithInstalledNode_ResolvesHarness() var meshService = Mesh.ServiceProvider.GetRequiredService(); // The node the Store plugin's install localizes into the viewer's partition. - var installed = new MeshNode(GatedId, $"{MonolithMeshTestBase.TestPartition}/Harness") + var installed = new MeshNode(GatedId, $"{TestPartition}/Harness") { NodeType = HarnessNodeType.NodeType, Name = "Gated CLI", @@ -182,9 +182,9 @@ public async Task Round_WithInstalledNode_RunsTheGatedHarness() { var client = GetClient(); var meshService = Mesh.ServiceProvider.GetRequiredService(); - var harnessPath = $"{MonolithMeshTestBase.TestPartition}/Harness/{GatedId}"; + var harnessPath = $"{TestPartition}/Harness/{GatedId}"; - await meshService.CreateNode(new MeshNode(GatedId, $"{MonolithMeshTestBase.TestPartition}/Harness") + await meshService.CreateNode(new MeshNode(GatedId, $"{TestPartition}/Harness") { NodeType = HarnessNodeType.NodeType, Name = "Gated CLI", @@ -194,12 +194,12 @@ await meshService.CreateNode(new MeshNode(GatedId, $"{MonolithMeshTestBase.TestP var threadCreated = new System.Reactive.Subjects.AsyncSubject(); client.StartThread( - namespacePath: MonolithMeshTestBase.TestPartition, + namespacePath: TestPartition, userText: "run on the gated harness", agentName: "Agent/Assistant", modelName: "_Provider/Fake/fake-model", harness: harnessPath, - contextPath: MonolithMeshTestBase.TestPartition, + contextPath: TestPartition, createdBy: "rbuergi@systemorph.com", onCreated: node => { threadCreated.OnNext(node); threadCreated.OnCompleted(); }); @@ -222,16 +222,16 @@ public async Task Round_WithoutInstalledNode_FallsBackToAgentPath() { var client = GetClient(); // A picked path whose node deliberately does not exist (uninstalled / never installed). - var harnessPath = $"{MonolithMeshTestBase.TestPartition}/Harness/never-installed-{GatedId}"; + var harnessPath = $"{TestPartition}/Harness/never-installed-{GatedId}"; var threadCreated = new System.Reactive.Subjects.AsyncSubject(); client.StartThread( - namespacePath: MonolithMeshTestBase.TestPartition, + namespacePath: TestPartition, userText: "run without an install", agentName: "Agent/Assistant", modelName: "_Provider/Fake/fake-model", harness: harnessPath, - contextPath: MonolithMeshTestBase.TestPartition, + contextPath: TestPartition, createdBy: "rbuergi@systemorph.com", onCreated: node => { threadCreated.OnNext(node); threadCreated.OnCompleted(); }); diff --git a/test/MeshWeaver.AI.Test/InboxToolIntegrationTest.cs b/test/MeshWeaver.AI.Test/InboxToolIntegrationTest.cs index b6e2a0ea9b..d43bf7a024 100644 --- a/test/MeshWeaver.AI.Test/InboxToolIntegrationTest.cs +++ b/test/MeshWeaver.AI.Test/InboxToolIntegrationTest.cs @@ -697,7 +697,7 @@ public void ExtractPendingTexts_NullThread_ReturnsEmpty() private async Task SeedEmptyThreadAsync(CancellationToken ct) { var threadId = Guid.NewGuid().AsString(); - var threadPath = $"{MonolithMeshTestBase.TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; + var threadPath = $"{TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; // FromPath splits the namespace ("TestData/_Thread") from the id — `new MeshNode(path)` // would bake the slashes into the Id with an EMPTY namespace, which the // PartitionWriteGuard (correctly) rejects as a malformed top-level node. @@ -705,7 +705,7 @@ await NodeFactory.CreateNode(MeshNode.FromPath(threadPath) with { Name = $"Inbox Test Thread {threadId}", NodeType = ThreadNodeType.NodeType, - MainNode = MonolithMeshTestBase.TestPartition, + MainNode = TestPartition, Content = new MeshThread { CreatedBy = "rbuergi@systemorph.com" } }); return threadPath; diff --git a/test/MeshWeaver.AI.Test/McpCompileInFlightTruthTest.cs b/test/MeshWeaver.AI.Test/McpCompileInFlightTruthTest.cs index c74de4f07d..c8cff81dd6 100644 --- a/test/MeshWeaver.AI.Test/McpCompileInFlightTruthTest.cs +++ b/test/MeshWeaver.AI.Test/McpCompileInFlightTruthTest.cs @@ -66,7 +66,7 @@ public override async ValueTask DisposeAsync() } private const string TypeName = "InFlightTruthType"; - private static readonly string RunningActivityPath = + private string RunningActivityPath => TestPartition + "/" + TypeName + "/_Activity/compile-inflight-under-test"; [Fact(Timeout = 120000)] diff --git a/test/MeshWeaver.AI.Test/McpFailureSurfacingTest.cs b/test/MeshWeaver.AI.Test/McpFailureSurfacingTest.cs index b877778d56..7108f5f4ba 100644 --- a/test/MeshWeaver.AI.Test/McpFailureSurfacingTest.cs +++ b/test/MeshWeaver.AI.Test/McpFailureSurfacingTest.cs @@ -37,8 +37,8 @@ private Task UpdateAsync(string json) => private Task PatchAsync(string path, string fields) => new MeshOperations(Mesh).Patch(path, fields).FirstAsync().Timeout(TimeSpan.FromSeconds(45)).ToTask(Ct); - private MeshNode ValidMarkdown(string id, string ns = TestPartition) => - new(id, ns) { Name = id, NodeType = "Markdown", Content = new MarkdownContent { Content = $"# {id}" } }; + private MeshNode ValidMarkdown(string id, string? ns = null) => + new(id, ns ?? TestPartition) { Name = id, NodeType = "Markdown", Content = new MarkdownContent { Content = $"# {id}" } }; [Fact(Timeout = 60000)] public async Task Create_InvalidJson_ReturnsError() diff --git a/test/MeshWeaver.AI.Test/McpNegativeOperationsTest.cs b/test/MeshWeaver.AI.Test/McpNegativeOperationsTest.cs index 3786c4131f..eca11d6b15 100644 --- a/test/MeshWeaver.AI.Test/McpNegativeOperationsTest.cs +++ b/test/MeshWeaver.AI.Test/McpNegativeOperationsTest.cs @@ -29,8 +29,8 @@ public class McpNegativeOperationsTest(ITestOutputHelper output) : MonolithMeshT private Task Run(IObservable op) => op.FirstAsync().Timeout(TimeSpan.FromSeconds(45)).ToTask(Ct); - private MeshNode ValidMarkdown(string id, string ns = TestPartition) => - new(id, ns) { Name = id, NodeType = "Markdown", Content = new MarkdownContent { Content = $"# {id}" } }; + private MeshNode ValidMarkdown(string id, string? ns = null) => + new(id, ns ?? TestPartition) { Name = id, NodeType = "Markdown", Content = new MarkdownContent { Content = $"# {id}" } }; private static string Unique(string prefix) => prefix + Guid.NewGuid().ToString("N")[..8]; diff --git a/test/MeshWeaver.AI.Test/ModelSubstitutionTest.cs b/test/MeshWeaver.AI.Test/ModelSubstitutionTest.cs index 3c45c2d480..6ed4dea48c 100644 --- a/test/MeshWeaver.AI.Test/ModelSubstitutionTest.cs +++ b/test/MeshWeaver.AI.Test/ModelSubstitutionTest.cs @@ -251,12 +251,12 @@ await NodeFactory.CreateNode(new MeshNode(id, providerPath) private async Task SeedThread() { var threadId = Guid.NewGuid().AsString(); - var threadPath = $"{MonolithMeshTestBase.TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; + var threadPath = $"{TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; await NodeFactory.CreateNode(MeshNode.FromPath(threadPath) with { Name = $"Model Substitution Thread {threadId}", NodeType = ThreadNodeType.NodeType, - MainNode = MonolithMeshTestBase.TestPartition, + MainNode = TestPartition, Content = new MeshThread { CreatedBy = TestUser } }).Should().Within(30.Seconds()).Emit(); return threadPath; diff --git a/test/MeshWeaver.AI.Test/ProviderRefusalRoundTest.cs b/test/MeshWeaver.AI.Test/ProviderRefusalRoundTest.cs index e0d22ecfb1..554bb3732d 100644 --- a/test/MeshWeaver.AI.Test/ProviderRefusalRoundTest.cs +++ b/test/MeshWeaver.AI.Test/ProviderRefusalRoundTest.cs @@ -276,12 +276,12 @@ await NodeFactory.CreateNode(new MeshNode(id, providerPath) private async Task SeedThread() { var threadId = Guid.NewGuid().AsString(); - var threadPath = $"{MonolithMeshTestBase.TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; + var threadPath = $"{TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; await NodeFactory.CreateNode(MeshNode.FromPath(threadPath) with { Name = $"Provider Refusal Thread {threadId}", NodeType = ThreadNodeType.NodeType, - MainNode = MonolithMeshTestBase.TestPartition, + MainNode = TestPartition, Content = new MeshThread { CreatedBy = TestUser } }).Should().Within(TimeSpan.FromMilliseconds(SeedMs)).Emit(); return threadPath; diff --git a/test/MeshWeaver.AI.Test/RoundFaultTerminalStateTest.cs b/test/MeshWeaver.AI.Test/RoundFaultTerminalStateTest.cs index 8b872f037f..8c0041b390 100644 --- a/test/MeshWeaver.AI.Test/RoundFaultTerminalStateTest.cs +++ b/test/MeshWeaver.AI.Test/RoundFaultTerminalStateTest.cs @@ -51,12 +51,12 @@ protected override MessageHubConfiguration ConfigureClient(MessageHubConfigurati public async Task StreamThrows_AfterExecutingFlip_ThreadReachesTerminalState_NoWatchdog() { var threadId = Guid.NewGuid().AsString(); - var threadPath = $"{MonolithMeshTestBase.TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; + var threadPath = $"{TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; await NodeFactory.CreateNode(MeshNode.FromPath(threadPath) with { Name = $"Fault Test Thread {threadId}", NodeType = ThreadNodeType.NodeType, - MainNode = MonolithMeshTestBase.TestPartition, + MainNode = TestPartition, Content = new MeshThread { CreatedBy = "rbuergi@systemorph.com" } }).Should().Emit(); diff --git a/test/MeshWeaver.AI.Test/StreamingTimeoutTest.cs b/test/MeshWeaver.AI.Test/StreamingTimeoutTest.cs index 7c57b0d702..afe0310956 100644 --- a/test/MeshWeaver.AI.Test/StreamingTimeoutTest.cs +++ b/test/MeshWeaver.AI.Test/StreamingTimeoutTest.cs @@ -63,12 +63,12 @@ protected override MessageHubConfiguration ConfigureClient(MessageHubConfigurati public async Task StreamHangsForever_RoundEndsWithTimeoutError_NotSilentWedge() { var threadId = Guid.NewGuid().AsString(); - var threadPath = $"{MonolithMeshTestBase.TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; + var threadPath = $"{TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; await NodeFactory.CreateNode(MeshNode.FromPath(threadPath) with { Name = $"Streaming Timeout Test Thread {threadId}", NodeType = ThreadNodeType.NodeType, - MainNode = MonolithMeshTestBase.TestPartition, + MainNode = TestPartition, Content = new MeshThread { CreatedBy = "rbuergi@systemorph.com" } }).Should().Emit(); diff --git a/test/MeshWeaver.AI.Test/ThreadComposerFlowTest.cs b/test/MeshWeaver.AI.Test/ThreadComposerFlowTest.cs index 8e596c27c6..997982dd3b 100644 --- a/test/MeshWeaver.AI.Test/ThreadComposerFlowTest.cs +++ b/test/MeshWeaver.AI.Test/ThreadComposerFlowTest.cs @@ -74,13 +74,13 @@ public async Task StartThread_WithComposer_CreatesUnderContext_AndEmbedsEmptiedC Harness = $"{HarnessNodeType.RootNamespace}/{Harnesses.MeshWeaver}", AgentName = "Agent/Assistant", ModelName = "_Provider/Fake/fake-model", - ContextPath = MonolithMeshTestBase.TestPartition, - Attachments = ImmutableList.Create("TestData/SomeDoc"), + ContextPath = TestPartition, + Attachments = ImmutableList.Create($"{TestPartition}/SomeDoc"), OpenThreadPath = "stale/_Thread/previous" // must never carry onto the thread }; client.StartThread( - namespacePath: MonolithMeshTestBase.TestPartition, + namespacePath: TestPartition, userText: composer.MessageContent!, agentName: composer.AgentName, modelName: composer.ModelName, @@ -92,7 +92,7 @@ public async Task StartThread_WithComposer_CreatesUnderContext_AndEmbedsEmptiedC var created = await threadCreated.Should().Emit(); created!.Path.Should().StartWith( - $"{MonolithMeshTestBase.TestPartition}/{ThreadNodeType.ThreadPartition}/", + $"{TestPartition}/{ThreadNodeType.ThreadPartition}/", "the thread must live under {context}/_Thread/{speakingId}"); var thread = (created.Content as MeshThread)!; @@ -123,7 +123,7 @@ public async Task SubmitComposer_DrainsDraft_CarriesSelection_AndEmptiesComposer Harness = $"{HarnessNodeType.RootNamespace}/{Harnesses.MeshWeaver}", AgentName = "Agent/Assistant", ModelName = "_Provider/Fake/fake-model", - Attachments = ImmutableList.Create("TestData/Chip") + Attachments = ImmutableList.Create($"{TestPartition}/Chip") }); var client = GetClient(); @@ -151,7 +151,7 @@ public async Task SubmitComposer_DrainsDraft_CarriesSelection_AndEmptiesComposer userMessage.Text.Should().Be("typed into the composer"); userMessage.AgentName.Should().Be("Agent/Assistant"); userMessage.Harness.Should().Be($"{HarnessNodeType.RootNamespace}/{Harnesses.MeshWeaver}"); - userMessage.Attachments.Should().Equal("TestData/Chip"); + userMessage.Attachments.Should().Equal($"{TestPartition}/Chip"); } [Fact] @@ -238,12 +238,12 @@ public void ThreadComposer_Equality_ComparesAttachmentsBySequence() private async Task SeedThreadWithComposer(ThreadComposer composer) { var threadId = Guid.NewGuid().AsString(); - var threadPath = $"{MonolithMeshTestBase.TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; + var threadPath = $"{TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; await NodeFactory.CreateNode(MeshNode.FromPath(threadPath) with { Name = $"Composer Test Thread {threadId}", NodeType = ThreadNodeType.NodeType, - MainNode = MonolithMeshTestBase.TestPartition, + MainNode = TestPartition, Content = new MeshThread { CreatedBy = "rbuergi@systemorph.com", diff --git a/test/MeshWeaver.AI.Test/ThreadSubmissionIntegrationTest.cs b/test/MeshWeaver.AI.Test/ThreadSubmissionIntegrationTest.cs index beb67d7423..ac0ef7bad3 100644 --- a/test/MeshWeaver.AI.Test/ThreadSubmissionIntegrationTest.cs +++ b/test/MeshWeaver.AI.Test/ThreadSubmissionIntegrationTest.cs @@ -99,7 +99,7 @@ public async Task CreateThreadAndSubmit_CreatesThreadAndFirstRound() var client = GetClient(); client.StartThread( - MonolithMeshTestBase.TestPartition, + TestPartition, "New thread first message", createdBy: "rbuergi@systemorph.com", onCreated: node => { threadCreated.OnNext(node); threadCreated.OnCompleted(); }); @@ -438,7 +438,7 @@ await Mesh.GetWorkspace().GetMeshNodeStream(threadPath) public async Task ThreadComposer_PersistsMessageContentAndComboboxes_ReadBackViaGetMeshNodeStream() { var client = GetClient(); - var chatInputPath = ThreadComposerNodeType.PathFor(MonolithMeshTestBase.TestPartition); + var chatInputPath = ThreadComposerNodeType.PathFor(TestPartition); // The per-user ThreadComposer singleton is seeded at onboarding (ThreadComposerSeedHandler), // so the composer always updates an EXISTING node. Mirror that here. @@ -502,7 +502,7 @@ public async Task StartThread_FromComposer_CreatesThread_SelectedAgentRidesOntoU var threadCreated = new System.Reactive.Subjects.AsyncSubject(); client.StartThread( - MonolithMeshTestBase.TestPartition, + TestPartition, "Compose then start", agentName: "Assistant", createdBy: "rbuergi@systemorph.com", @@ -529,7 +529,7 @@ public async Task StartThread_FromComposer_CreatesThread_SelectedAgentRidesOntoU private async Task SeedEmptyThread() { var threadId = Guid.NewGuid().AsString(); - var threadPath = $"{MonolithMeshTestBase.TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; + var threadPath = $"{TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; // FromPath splits the namespace ("TestData/_Thread") from the id — `new MeshNode(path)` // would bake the slashes into the Id with an EMPTY namespace, which the // PartitionWriteGuard (correctly) rejects as a malformed top-level node. @@ -537,7 +537,7 @@ await NodeFactory.CreateNode(MeshNode.FromPath(threadPath) with { Name = $"Test Thread {threadId}", NodeType = ThreadNodeType.NodeType, - MainNode = MonolithMeshTestBase.TestPartition, + MainNode = TestPartition, Content = new MeshThread { CreatedBy = "rbuergi@systemorph.com" } }).Should().Emit(); return threadPath; diff --git a/test/MeshWeaver.AI.Test/ThreadTokenUsageTest.cs b/test/MeshWeaver.AI.Test/ThreadTokenUsageTest.cs index 90e6bca818..119f2f91ba 100644 --- a/test/MeshWeaver.AI.Test/ThreadTokenUsageTest.cs +++ b/test/MeshWeaver.AI.Test/ThreadTokenUsageTest.cs @@ -321,12 +321,12 @@ public async Task CompletedRound_WithPromptCache_RecordsCacheTokens_OnCellAndSat private async Task SeedThread() { var threadId = Guid.NewGuid().AsString(); - var threadPath = $"{MonolithMeshTestBase.TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; + var threadPath = $"{TestPartition}/{ThreadNodeType.ThreadPartition}/{threadId}"; await NodeFactory.CreateNode(MeshNode.FromPath(threadPath) with { Name = $"Token Usage Thread {threadId}", NodeType = ThreadNodeType.NodeType, - MainNode = MonolithMeshTestBase.TestPartition, + MainNode = TestPartition, Content = new MeshThread { CreatedBy = TestUser } }).Should().Emit(); return threadPath; diff --git a/test/MeshWeaver.AI.Test/xunit.runner.json b/test/MeshWeaver.AI.Test/xunit.runner.json new file mode 100644 index 0000000000..179bf064a9 --- /dev/null +++ b/test/MeshWeaver.AI.Test/xunit.runner.json @@ -0,0 +1,6 @@ +{ + "parallelizeAssembly": false, + "parallelizeTestCollections": true, + "maxParallelThreads": 4, + "methodTimeout": 30000 +} diff --git a/test/MeshWeaver.Security.Test/ScriptTemplateExecuteAccessTest.cs b/test/MeshWeaver.Security.Test/ScriptTemplateExecuteAccessTest.cs index f119ae8d5b..a3ea008952 100644 --- a/test/MeshWeaver.Security.Test/ScriptTemplateExecuteAccessTest.cs +++ b/test/MeshWeaver.Security.Test/ScriptTemplateExecuteAccessTest.cs @@ -157,7 +157,7 @@ public async Task TemplatesGrant_DoesNotLeakOutsideTheTemplatesPartition() "the Templates grant is scoped to Templates — it must confer nothing at ROOT"); // A partition the user has no business in stays closed. - var elsewhere = await EffectivePermissions(MonolithMeshTestBase.TestPartition); + var elsewhere = await EffectivePermissions(TestPartition); elsewhere.Should().NotHaveFlag(Permission.Read, "an ungranted partition must stay unreadable — the Templates grant must not widen it"); }