elsa-core/test/unit/Elsa.AI.Host.UnitTests/Context/AIContextResolverTests.cs
Sipke Schoorstra 3936258146
Implement Weaver AI Copilot core (#7523)
* Implement Weaver AI Copilot core

* Address Greptile review feedback

* Address Greptile persistence feedback

* Address Greptile orchestration feedback

* Address Greptile tool isolation feedback

* Wire chat audit events

* Stream chat events over SSE

* Use server identity for AI endpoints

* Validate AI proposal persistence

* Isolate AI audit failures

* Enforce AI tool lookup scope

* Support AI tool result continuations

* Handle AI chat reconnects safely

* Tighten AI context and reconnect behavior

* Guard AI conversation and persistence setup

* Persist AI tool-loop progress

* Tighten AI tool registry and reconnect cleanup

* Handle AI preparation failures cleanly

* Order AI tool messages after assistant turns

* Initialize AI provider sessions

* Align AI context capabilities

* Prevent completed AI reconnect replay

* Enforce AI conversation ownership

* Default AI proposal creation time

* Persist AI session and retention defaults

* Allow AI context provider overrides

* Scope AI tool results per turn

* Apply AI provider configuration

* Scope AI proposal reads

* Avoid duplicate AI tool continuations

* Resolve AI tool registry scopes

* Tighten AI reconnect cleanup

* Honor default AI proposal tools

* Pass AI provider session to turns

* Close AI observability gaps

* Fix AI capabilities options alias

* Harden AI orchestration lifetimes

* Track actual AI reconnect conversation

* Address AI audit and context review findings

* Fix AI reconnect and persistence capabilities

* Handle AI session startup failures

* Tighten AI orchestration review gaps

* Warn on placeholder AI context

* Filter disabled AI provider tools

* Add durable AI conversation persistence

* Fix AI orchestrator persistence lifetime

* Handle failed AI reconnect edge cases

* Harden AI reconnect failure handling

* Address AI reconnect and cleanup review gaps

* Tighten AI audit and cleanup persistence

* Keep expired AI cleanup best effort

* Tighten AI tool lookup and cleanup fallback

* Handle AI provider and tenant edge cases

* Tighten AI proposal and agent authorization

* Address AI tool scope cleanup review

* Close remaining AI greptile findings

* Harden AI stores and tool defaults

* Harden AI conversation persistence edge cases

* Cover AI proposal and tool visibility guards

* Fix AI capabilities and audit batch resilience

* Fix AI conversation truncation for unicode

* Resolve remaining AI persistence review nits

* Wire AI conversation persistence option

* Address AI audit and proposal style review

* Fix AI stream truncation surrogate handling

* Address AI context and cleanup review

* Preserve AI titles and tenant tool defaults

* Guard AI conversation user ownership

* Align in-memory AI conversation ownership

* Fix expired AI conversation cleanup tracking

* Harden AI proposal persistence retry

* Tighten AI proposal reads and cleanup SQL

* Harden AI reconnect and provider defaults

* Optimize AI tool listing and message trimming

* Preserve AI conversation timestamps

* Address final AI persistence review nits

* Normalize AI acronym casing

* Address Copilot AI review comments

* Normalize default tenant handling for AI stores

* Harden AI registry and message truncation

* Make AI tool filtering explicit

* Align AI contracts with implementation

* Align remaining AI review contracts

* address greptile ai persistence feedback

* Address Copilot AI persistence feedback

* Address Copilot AI host feedback

* Order persisted AI conversation messages

* Address Copilot chat and cleanup feedback

* Release unused AI reconnect reservations

* Address Copilot AI review feedback

* Address Copilot tool and conversation feedback

* Address Copilot governance feedback

* Address Copilot tool test feedback

* Address AI review follow-ups

* Address Copilot AI follow-ups

* Clean up AI persistence tests

* Address IAITool disposal review

* Address AI integration review follow-ups

* Address AI chat persistence review

* Address AI registry and truncation review

* Enable read-only AI tools by default

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-04 11:35:44 +02:00

179 lines
6.6 KiB
C#

using Elsa.AI.Abstractions.Contracts;
using Elsa.AI.Abstractions.Models;
using Elsa.AI.Host.Context;
using Microsoft.Extensions.DependencyInjection;
using System.Text.Json.Nodes;
namespace Elsa.AI.Host.UnitTests.Context;
public class AIContextResolverTests
{
[Fact(DisplayName = "Context resolver resolves workflow definition references server-side")]
public async Task ContextResolverResolvesWorkflowDefinitionReferences()
{
using var provider = CreateProvider(services => services.AddSingleton<IAIContextProvider, WorkflowDefinitionContextProvider>());
var resolver = provider.GetRequiredService<AIContextResolver>();
var result = await resolver.ResolveAsync(new AIChatRequest
{
UserId = "user-1",
Attachments =
[
new AIContextAttachment
{
Kind = "WorkflowDefinition",
ReferenceId = "workflow-1"
}
]
});
var context = Assert.Single(result);
Assert.Equal("WorkflowDefinition", context.Kind);
Assert.Equal("workflow-1", context.ReferenceId);
Assert.Empty(context.Data);
}
[Fact(DisplayName = "Context resolver redacts sensitive data and metadata keys")]
public async Task ContextResolverRedactsSensitiveDataAndMetadataKeys()
{
using var provider = CreateProvider(services => services.AddSingleton<IAIContextProvider, SensitiveContextProvider>());
var resolver = provider.GetRequiredService<AIContextResolver>();
var result = await resolver.ResolveAsync(new AIChatRequest
{
UserId = "user-1",
Attachments =
[
new AIContextAttachment
{
Kind = "Sensitive"
}
]
});
var context = Assert.Single(result);
Assert.Equal("Handles OAuth token refresh", context.Summary);
Assert.Equal("[redacted]", context.Data["accessToken"]!.GetValue<string>());
Assert.Equal("[redacted]", context.Data["description"]!.GetValue<string>());
Assert.Equal("[redacted]", context.Metadata["apiKey"]!.GetValue<string>());
Assert.Equal("visible", context.Data["displayName"]!.GetValue<string>());
}
[Fact(DisplayName = "Context resolver uses the last provider for duplicate provider kinds")]
public async Task ContextResolverUsesTheLastProviderForDuplicateProviderKinds()
{
using var provider = CreateProvider(services =>
{
services.AddSingleton<IAIContextProvider>(new DuplicateContextProvider("first"));
services.AddSingleton<IAIContextProvider>(new DuplicateContextProvider("second"));
});
var resolver = provider.GetRequiredService<AIContextResolver>();
var result = await resolver.ResolveAsync(new AIChatRequest
{
UserId = "user-1",
Attachments = [new AIContextAttachment { Kind = "Duplicate" }]
});
var context = Assert.Single(result);
Assert.Equal("second", context.Summary);
}
[Fact(DisplayName = "Context resolver prefers real providers over placeholders")]
public async Task ContextResolverPrefersRealProvidersOverPlaceholders()
{
using var provider = CreateProvider(services =>
{
services.AddSingleton<IAIContextProvider>(new DuplicateContextProvider("real", "WorkflowDefinition"));
services.AddSingleton<IAIContextProvider, WorkflowDefinitionContextProvider>();
});
var resolver = provider.GetRequiredService<AIContextResolver>();
var result = await resolver.ResolveAsync(new AIChatRequest
{
UserId = "user-1",
Attachments = [new AIContextAttachment { Kind = "WorkflowDefinition", ReferenceId = "workflow-1" }]
});
var context = Assert.Single(result);
Assert.Equal("real", context.Summary);
}
[Fact(DisplayName = "Context resolver resolves scoped providers from request scopes")]
public async Task ContextResolverResolvesScopedProvidersFromRequestScopes()
{
using var provider = CreateProvider(services => services.AddScoped<IAIContextProvider, ScopedContextProvider>());
var resolver = provider.GetRequiredService<AIContextResolver>();
var result = await resolver.ResolveAsync(new AIChatRequest
{
UserId = "user-1",
Attachments = [new AIContextAttachment { Kind = "Scoped" }]
});
var context = Assert.Single(result);
Assert.Equal("scoped", context.Summary);
}
private static ServiceProvider CreateProvider(Action<IServiceCollection> configure)
{
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton<AIContextResolver>();
configure(services);
return services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true });
}
private class SensitiveContextProvider : IAIContextProvider
{
public string Kind => "Sensitive";
public ValueTask<AIResolvedContext> ResolveAsync(AIContextResolutionRequest request, CancellationToken cancellationToken = default)
{
return ValueTask.FromResult(new AIResolvedContext
{
Kind = Kind,
Summary = "Handles OAuth token refresh",
Data = new JsonObject
{
["accessToken"] = "token-value",
["description"] = "Bearer eyJhbGciOi",
["displayName"] = "visible"
},
Metadata = new JsonObject
{
["apiKey"] = "key-value"
}
});
}
}
private class DuplicateContextProvider(string summary, string kind = "Duplicate") : IAIContextProvider
{
public string Kind => kind;
public ValueTask<AIResolvedContext> ResolveAsync(AIContextResolutionRequest request, CancellationToken cancellationToken = default)
{
return ValueTask.FromResult(new AIResolvedContext
{
Kind = Kind,
Summary = summary
});
}
}
private class ScopedContextProvider : IAIContextProvider
{
public string Kind => "Scoped";
public ValueTask<AIResolvedContext> ResolveAsync(AIContextResolutionRequest request, CancellationToken cancellationToken = default)
{
return ValueTask.FromResult(new AIResolvedContext
{
Kind = Kind,
Summary = "scoped"
});
}
}
}