diff --git a/test/unit/Elsa.AI.Host.UnitTests/AIToolRegistryTests.cs b/test/unit/Elsa.AI.Host.UnitTests/AIToolRegistryTests.cs index d580a3402..ebac45da3 100644 --- a/test/unit/Elsa.AI.Host.UnitTests/AIToolRegistryTests.cs +++ b/test/unit/Elsa.AI.Host.UnitTests/AIToolRegistryTests.cs @@ -253,6 +253,25 @@ public class AIToolRegistryTests Assert.True(enablement.IsEnabled(definition)); } + [Fact(DisplayName = "Tool enablement disables administrative tools by name")] + public void ToolEnablementDisablesAdministrativeToolsByName() + { + var enablement = new AIToolEnablementService(); + var definition = new AIToolDefinition + { + Name = "admin", + DisplayName = "Admin", + Mutability = AIToolMutability.Administrative + }; + + enablement.EnableAdministrative("ADMIN"); + Assert.True(enablement.IsEnabled(definition)); + + enablement.Disable("admin"); + + Assert.False(enablement.IsEnabled(definition)); + } + [Fact(DisplayName = "Tool registry filters agent-scoped tools by agent")] public async Task ToolRegistryFiltersAgentScopedToolsByAgent() { diff --git a/test/unit/Elsa.AI.Host.UnitTests/Context/AIContextResolverTests.cs b/test/unit/Elsa.AI.Host.UnitTests/Context/AIContextResolverTests.cs index c9ac0aadb..90b2f7902 100644 --- a/test/unit/Elsa.AI.Host.UnitTests/Context/AIContextResolverTests.cs +++ b/test/unit/Elsa.AI.Host.UnitTests/Context/AIContextResolverTests.cs @@ -59,6 +59,44 @@ public class AIContextResolverTests Assert.Equal("visible", context.Data["displayName"]!.GetValue()); } + [Fact(DisplayName = "Context resolver redacts nested context payloads")] + public async Task ContextResolverRedactsNestedContextPayloads() + { + using var provider = CreateProvider(services => services.AddSingleton()); + var resolver = provider.GetRequiredService(); + + var result = await resolver.ResolveAsync(new AIChatRequest + { + UserId = "user-1", + Attachments = [new AIContextAttachment { Kind = "NestedSensitive" }] + }); + + var context = Assert.Single(result); + var profile = Assert.IsType(context.Data["profile"]); + var history = Assert.IsType(context.Data["history"]); + + Assert.Equal("[redacted]", profile["password"]!.GetValue()); + Assert.Equal("visible", profile["displayName"]!.GetValue()); + Assert.Equal("[redacted]", history[0]!.GetValue()); + Assert.Equal(42, history[1]!.GetValue()); + Assert.True(history[2]!.GetValue()); + } + + [Fact(DisplayName = "Context resolver ignores attachments without providers")] + public async Task ContextResolverIgnoresAttachmentsWithoutProviders() + { + using var provider = CreateProvider(_ => { }); + var resolver = provider.GetRequiredService(); + + var result = await resolver.ResolveAsync(new AIChatRequest + { + UserId = "user-1", + Attachments = [new AIContextAttachment { Kind = "Unknown" }] + }); + + Assert.Empty(result); + } + [Fact(DisplayName = "Context resolver uses the last provider for duplicate provider kinds")] public async Task ContextResolverUsesTheLastProviderForDuplicateProviderKinds() { @@ -148,6 +186,28 @@ public class AIContextResolverTests } } + private class NestedSensitiveContextProvider : IAIContextProvider + { + public string Kind => "NestedSensitive"; + + public ValueTask ResolveAsync(AIContextResolutionRequest request, CancellationToken cancellationToken = default) + { + return ValueTask.FromResult(new AIResolvedContext + { + Kind = Kind, + Data = new JsonObject + { + ["profile"] = new JsonObject + { + ["password"] = "secret-value", + ["displayName"] = "visible" + }, + ["history"] = new JsonArray("Bearer abcdefgh", 42, true) + } + }); + } + } + private class DuplicateContextProvider(string summary, string kind = "Duplicate") : IAIContextProvider { public string Kind => kind;