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
11 changes: 11 additions & 0 deletions test/MeshWeaver.AI.Test/MeshWeaver.AI.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,15 @@
<Content Include="..\..\samples\Graph\Data\**\*" LinkBase="TestData" CopyToOutputDirectory="PreserveNewest" />
<Content Include="TestData\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<!-- 🚨 The static-collections guard FOLLOWS its subject (#2276). It discovers assemblies at
RUNTIME (Directory.EnumerateFiles(AppContext.BaseDirectory, "MeshWeaver.*.dll")), so it only
covers what is in the running test's bin — which is why MeshWeaver.PathResolution.Test used
to carry a ProjectReference to MeshWeaver.AI purely to place AI.dll beside it. Dropping that
reference would have silently ended AI's coverage: the guard would still pass, having stopped
looking. Linking the one file here keeps AI scanned by the same rules, with no duplicated
logic, and it travels with this project when the engine leaves. -->
<Compile Include="..\MeshWeaver.PathResolution.Test\NoStaticCollectionsTest.cs"
Link="Guards\NoStaticCollectionsTest.cs" />
</ItemGroup>
</Project>
116 changes: 116 additions & 0 deletions test/MeshWeaver.AI.Test/ThreadAddressResolutionTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Threading.Tasks;
using MeshWeaver.Hosting.Monolith.TestBase;
using MeshWeaver.Mesh;
using MeshWeaver.Mesh.Services;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
Comment thread
rbuergi marked this conversation as resolved.
Resolved

namespace MeshWeaver.AI.Test;

/// <summary>
/// Address resolution over THREAD nodes and their satellites — a thread at
/// <c>User/{u}/_Thread/{id}</c> and a message beneath it.
///
/// <para>Moved out of <c>MeshWeaver.PathResolution.Test.AddressResolutionTest</c> (#2276). The
/// resolution mechanism itself is core and its coverage stays there; these two cases could not,
/// because their fixture is not incidental — they build <c>AI.Thread</c> CONTENT under a
/// <c>_Thread</c> satellite path, and the thing being resolved is that shape. Substituting a
/// non-AI node type would have changed the scenario rather than the fixture.</para>
/// </summary>
public class ThreadAddressResolutionTest(ITestOutputHelper output) : MonolithMeshTestBase(output)
{
protected override MeshBuilder ConfigureMesh(MeshBuilder builder)
// AddAI(), as the original fixture had: these cases resolve a thread AND a ThreadMessage
// beneath it, and AddThreadType() alone leaves ThreadMessage unregistered. In AI's own
// suite the full registration is the natural one.
=> ConfigureMeshBase(builder).AddAI();

/// <summary>
/// Verifies that the Thread node itself still resolves correctly
/// when ThreadMessage children exist.
/// </summary>
[Fact(Timeout = 30000)]
public async Task ResolvePath_ThreadNode_ResolvesCorrectlyWithChildren()
{
var threadPath = "User/Roland/_Thread/test-parent-5678";
var threadNode = new MeshNode("test-parent-5678", "User/Roland/_Thread")
{
Name = "Test Parent",
NodeType = ThreadNodeType.NodeType,
Content = new AI.Thread
{
Messages = ["m1"]
}
};
await NodeFactory.CreateNode(threadNode).Should().Emit();

var msgNode = new MeshNode("m1", threadPath)
{
NodeType = ThreadMessageNodeType.NodeType,
Order = 1,
Content = new ThreadMessage
{
Role = "assistant",
Text = "Response",
Timestamp = System.DateTime.UtcNow,
Type = ThreadMessageType.AgentResponse
}
};
await NodeFactory.CreateNode(msgNode).Should().Emit();

// Resolve the Thread path — should match the Thread node exactly
var resolution = await PathResolver.ResolvePath(threadPath).Should().Emit();

resolution.Should().NotBeNull();
resolution!.Prefix.Should().Be(threadPath);
resolution.Remainder.Should().BeNull();
}

/// <summary>
/// Creates a Thread with a ThreadMessage child and verifies path resolution
/// resolves the full ThreadMessage path (not the Thread path with remainder).
/// </summary>
[Fact(Timeout = 30000)]
public async Task ResolvePath_ThreadMessageNode_ResolvesToFullMessagePath()
{
// Create Thread node
var threadPath = "User/Roland/_Thread/test-resolution-1234";
var threadNode = new MeshNode("test-resolution-1234", "User/Roland/_Thread")
{
Name = "Test Resolution",
NodeType = ThreadNodeType.NodeType,
Content = new AI.Thread
{
Messages = ["msg1"]
}
};
await NodeFactory.CreateNode(threadNode).Should().Emit();

// Create ThreadMessage child node
var msgNode = new MeshNode("msg1", threadPath)
{
NodeType = ThreadMessageNodeType.NodeType,
Order = 1,
Content = new ThreadMessage
{
Role = "user",
Text = "Hello",
Timestamp = System.DateTime.UtcNow,
Type = ThreadMessageType.ExecutedInput
}
};
await NodeFactory.CreateNode(msgNode).Should().Emit();

// Resolve the ThreadMessage path — should return the full message path, no remainder
var resolution = await PathResolver.ResolvePath($"{threadPath}/msg1").Should().Emit();

resolution.Should().NotBeNull("ThreadMessage node exists at {0}/msg1", threadPath);
resolution!.Prefix.Should().Be($"{threadPath}/msg1",
"should resolve to the full ThreadMessage path, not the Thread path with remainder");
resolution.Remainder.Should().BeNull("exact match should have no remainder");
}

}
87 changes: 2 additions & 85 deletions test/MeshWeaver.PathResolution.Test/AddressResolutionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Threading.Tasks;
using System.Reactive.Threading.Tasks;
using System.Reactive.Linq;
using MeshWeaver.AI;
using MeshWeaver.Hosting.Monolith.TestBase;
using MeshWeaver.Mesh;
using MeshWeaver.Mesh.Services;
Expand All @@ -22,8 +21,9 @@ public class AddressResolutionTest(ITestOutputHelper output) : MonolithMeshTestB
private const string AppPath = "app";

protected override MeshBuilder ConfigureMesh(MeshBuilder builder)
// The thread cases that needed AddAI() moved to MeshWeaver.AI.Test.ThreadAddressResolutionTest
// (#2276); what remains resolves plain node paths and needs no AI types.
=> base.ConfigureMesh(builder)
.AddAI()
.AddSampleUsers();

private async Task EnsureNodesCreated()
Expand Down Expand Up @@ -226,90 +226,7 @@ public async Task ResolvePath_CaseInsensitive_MatchesCorrectly()

#region ThreadMessage Path Resolution

/// <summary>
/// Creates a Thread with a ThreadMessage child and verifies path resolution
/// resolves the full ThreadMessage path (not the Thread path with remainder).
/// </summary>
[Fact(Timeout = 30000)]
public async Task ResolvePath_ThreadMessageNode_ResolvesToFullMessagePath()
{
// Create Thread node
var threadPath = "User/Roland/_Thread/test-resolution-1234";
var threadNode = new MeshNode("test-resolution-1234", "User/Roland/_Thread")
{
Name = "Test Resolution",
NodeType = ThreadNodeType.NodeType,
Content = new AI.Thread
{
Messages = ["msg1"]
}
};
await NodeFactory.CreateNode(threadNode).Should().Emit();

// Create ThreadMessage child node
var msgNode = new MeshNode("msg1", threadPath)
{
NodeType = ThreadMessageNodeType.NodeType,
Order = 1,
Content = new ThreadMessage
{
Role = "user",
Text = "Hello",
Timestamp = System.DateTime.UtcNow,
Type = ThreadMessageType.ExecutedInput
}
};
await NodeFactory.CreateNode(msgNode).Should().Emit();

// Resolve the ThreadMessage path — should return the full message path, no remainder
var resolution = await PathResolver.ResolvePath($"{threadPath}/msg1").Should().Emit();

resolution.Should().NotBeNull("ThreadMessage node exists at {0}/msg1", threadPath);
resolution!.Prefix.Should().Be($"{threadPath}/msg1",
"should resolve to the full ThreadMessage path, not the Thread path with remainder");
resolution.Remainder.Should().BeNull("exact match should have no remainder");
}

/// <summary>
/// Verifies that the Thread node itself still resolves correctly
/// when ThreadMessage children exist.
/// </summary>
[Fact(Timeout = 30000)]
public async Task ResolvePath_ThreadNode_ResolvesCorrectlyWithChildren()
{
var threadPath = "User/Roland/_Thread/test-parent-5678";
var threadNode = new MeshNode("test-parent-5678", "User/Roland/_Thread")
{
Name = "Test Parent",
NodeType = ThreadNodeType.NodeType,
Content = new AI.Thread
{
Messages = ["m1"]
}
};
await NodeFactory.CreateNode(threadNode).Should().Emit();

var msgNode = new MeshNode("m1", threadPath)
{
NodeType = ThreadMessageNodeType.NodeType,
Order = 1,
Content = new ThreadMessage
{
Role = "assistant",
Text = "Response",
Timestamp = System.DateTime.UtcNow,
Type = ThreadMessageType.AgentResponse
}
};
await NodeFactory.CreateNode(msgNode).Should().Emit();

// Resolve the Thread path — should match the Thread node exactly
var resolution = await PathResolver.ResolvePath(threadPath).Should().Emit();

resolution.Should().NotBeNull();
resolution!.Prefix.Should().Be(threadPath);
resolution.Remainder.Should().BeNull();
}

#endregion
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<NoWarn>$(NoWarn);xUnit1051</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MeshWeaver.AI\MeshWeaver.AI.csproj" />
<ProjectReference Include="..\..\src\MeshWeaver.Hosting.Monolith.TestBase\MeshWeaver.Hosting.Monolith.TestBase.csproj" />
<ProjectReference Include="..\..\src\MeshWeaver.Graph\MeshWeaver.Graph.csproj" />
</ItemGroup>
Expand Down
16 changes: 16 additions & 0 deletions test/MeshWeaver.PathResolution.Test/NoStaticCollectionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ public class NoStaticCollectionsTest
private static readonly IReadOnlyDictionary<string, string> Allowed = new Dictionary<string, string>
{
// ---- CONST: immutable, write-once constant lookups (allowed) ----
// Read-only satellite-table lookups built once from SatelliteTableMapping.Defaults and
// never written — surfaced when this guard began covering MeshWeaver.AI.Test's own
// assembly (#2276), not new violations.
["MeshWeaver.AI.Test.ThreadComposerRoutingTest.SegmentTable"] = "CONST",
["MeshWeaver.AI.Test.ThreadComposerRoutingTest.NodeTypeTable"] = "CONST",
// 🚨 Surfaced by WIDENED COVERAGE, not by new code. This guard enumerates
// MeshWeaver.*.dll from the running test's bin, so it only ever saw the assemblies that
// project happened to reference. Linking it into MeshWeaver.AI.Test (#2276) brought that
// project's larger closure into scope for the first time. Each entry below was verified to
// be a declaration-with-initializer whose only later uses are reads.
["MeshWeaver.Hosting.AspNetCore.Portal.NonfileRouteConstraint.ExcludedPrefixes"] = "CONST",
["MeshWeaver.Hosting.PostgreSql.PostgreSqlCrossSchemaQueryProvider.ExcludedSchemas"] = "CONST",
["MeshWeaver.Hosting.PostgreSql.PostgreSqlSqlGenerator.AllowedSqlFunctions"] = "CONST",
["MeshWeaver.Hosting.PostgreSql.PostgreSqlSqlGenerator.NonTextColumns"] = "CONST",
["MeshWeaver.Hosting.PostgreSql.PostgreSqlSqlGenerator.PropertyMap"] = "CONST",
["MeshWeaver.Hosting.PostgreSql.PostgreSqlStorageAdapter.TransientPostgresSqlStates"] = "CONST",
["MeshWeaver.AI.AccessContextAIFunction.TimeoutExemptTools"] = "CONST",
["MeshWeaver.AI.AgentChatClient.BinaryExtensions"] = "CONST",
["MeshWeaver.AI.AgentChatClient.ExtensionToMediaType"] = "CONST",
Expand Down
Loading