* 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>
553 lines
20 KiB
C#
553 lines
20 KiB
C#
using Elsa.AI.Abstractions.Contracts;
|
|
using Elsa.AI.Abstractions.Models;
|
|
using Elsa.AI.Host.Services;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace Elsa.AI.Host.UnitTests;
|
|
|
|
public class AIToolRegistryTests
|
|
{
|
|
[Fact(DisplayName = "Tool registry excludes host and cross-tenant denied tools from tenant queries")]
|
|
public async Task ToolRegistryExcludesHostAndCrossTenantDeniedToolsFromTenantQueries()
|
|
{
|
|
var registry = CreateRegistry(
|
|
[
|
|
new TestTool(new AIToolDefinition { Name = "tenant", DisplayName = "Tenant", TenantBehavior = AITenantBehavior.TenantScoped }),
|
|
new TestTool(new AIToolDefinition { Name = "host", DisplayName = "Host", TenantBehavior = AITenantBehavior.HostScoped }),
|
|
new TestTool(new AIToolDefinition { Name = "cross", DisplayName = "Cross", TenantBehavior = AITenantBehavior.CrossTenantDenied })
|
|
]);
|
|
|
|
var tools = await registry.ListAsync(new AIToolQuery
|
|
{
|
|
TenantId = "tenant-1",
|
|
ActorId = "user-1"
|
|
});
|
|
|
|
var tool = Assert.Single(tools);
|
|
Assert.Equal("tenant", tool.Name);
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool registry treats empty tenant ID as tenant context")]
|
|
public async Task ToolRegistryTreatsEmptyTenantIdAsTenantContext()
|
|
{
|
|
var registry = CreateRegistry(
|
|
[
|
|
new TestTool(new AIToolDefinition { Name = "tenant", DisplayName = "Tenant", TenantBehavior = AITenantBehavior.TenantScoped }),
|
|
new TestTool(new AIToolDefinition { Name = "host", DisplayName = "Host", TenantBehavior = AITenantBehavior.HostScoped }),
|
|
new TestTool(new AIToolDefinition
|
|
{
|
|
Name = "default-tenant-cross",
|
|
DisplayName = "Default tenant cross",
|
|
TenantBehavior = AITenantBehavior.CrossTenantDenied,
|
|
TenantIds = [""]
|
|
})
|
|
]);
|
|
|
|
var tools = await registry.ListAsync(new AIToolQuery
|
|
{
|
|
TenantId = "",
|
|
ActorId = "user-1"
|
|
});
|
|
|
|
Assert.Collection(
|
|
tools.OrderBy(x => x.Name),
|
|
tool => Assert.Equal("default-tenant-cross", tool.Name),
|
|
tool => Assert.Equal("tenant", tool.Name));
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool registry allows cross-tenant denied tools for explicit tenant allowlists")]
|
|
public async Task ToolRegistryAllowsCrossTenantDeniedToolsForExplicitTenantAllowlists()
|
|
{
|
|
var registry = CreateRegistry(
|
|
[
|
|
new TestTool(new AIToolDefinition
|
|
{
|
|
Name = "tenant-cross",
|
|
DisplayName = "Tenant cross",
|
|
TenantBehavior = AITenantBehavior.CrossTenantDenied,
|
|
TenantIds = ["tenant-1"]
|
|
})
|
|
]);
|
|
|
|
var tools = await registry.ListAsync(new AIToolQuery
|
|
{
|
|
TenantId = "tenant-1",
|
|
ActorId = "user-1"
|
|
});
|
|
|
|
var tool = Assert.Single(tools);
|
|
Assert.Equal("tenant-cross", tool.Name);
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool registry excludes cross-tenant denied tools from host queries")]
|
|
public async Task ToolRegistryExcludesCrossTenantDeniedToolsFromHostQueries()
|
|
{
|
|
var registry = CreateRegistry(
|
|
[
|
|
new TestTool(new AIToolDefinition { Name = "host", DisplayName = "Host", TenantBehavior = AITenantBehavior.HostScoped }),
|
|
new TestTool(new AIToolDefinition { Name = "cross", DisplayName = "Cross", TenantBehavior = AITenantBehavior.CrossTenantDenied })
|
|
]);
|
|
|
|
var tools = await registry.ListAsync(new AIToolQuery
|
|
{
|
|
ActorId = "user-1"
|
|
});
|
|
|
|
var tool = Assert.Single(tools);
|
|
Assert.Equal("host", tool.Name);
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool registry exposes default tools without tenant context")]
|
|
public async Task ToolRegistryExposesDefaultToolsWithoutTenantContext()
|
|
{
|
|
var registry = CreateRegistry(
|
|
[
|
|
new TestTool(new AIToolDefinition { Name = "default", DisplayName = "Default" })
|
|
]);
|
|
|
|
var tools = await registry.ListAsync(new AIToolQuery { ActorId = "user-1" });
|
|
|
|
var tool = Assert.Single(tools);
|
|
Assert.Equal("default", tool.Name);
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool registry exposes default tools to tenant queries")]
|
|
public async Task ToolRegistryExposesDefaultToolsToTenantQueries()
|
|
{
|
|
var registry = CreateRegistry(
|
|
[
|
|
new TestTool(new AIToolDefinition { Name = "default", DisplayName = "Default" })
|
|
]);
|
|
|
|
var tools = await registry.ListAsync(new AIToolQuery { TenantId = "tenant-1", ActorId = "user-1" });
|
|
|
|
var tool = Assert.Single(tools);
|
|
Assert.Equal("default", tool.Name);
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool registry excludes incomplete tool definition names")]
|
|
public async Task ToolRegistryExcludesIncompleteToolDefinitionNames()
|
|
{
|
|
var registry = CreateRegistry(
|
|
[
|
|
new TestTool(new AIToolDefinition { DisplayName = "Incomplete" }),
|
|
new TestTool(new AIToolDefinition { Name = "valid", DisplayName = "Valid" })
|
|
]);
|
|
|
|
var tools = await registry.ListAsync(new AIToolQuery { ActorId = "user-1" });
|
|
|
|
var tool = Assert.Single(tools);
|
|
Assert.Equal("valid", tool.Name);
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool registry uses cached concrete type lookup after listing tools")]
|
|
public async Task ToolRegistryUsesCachedConcreteTypeLookupAfterListingTools()
|
|
{
|
|
CountingTool.ConstructionCount = 0;
|
|
OtherCountingTool.ConstructionCount = 0;
|
|
var services = new ServiceCollection();
|
|
services.AddTransient<IAITool, CountingTool>();
|
|
services.AddTransient<IAITool, OtherCountingTool>();
|
|
var registry = CreateRegistry(services);
|
|
|
|
await registry.ListAsync(new AIToolQuery { ActorId = "user-1" });
|
|
CountingTool.ConstructionCount = 0;
|
|
OtherCountingTool.ConstructionCount = 0;
|
|
|
|
using var tool = await registry.FindAsync("counting", new AIToolQuery { ActorId = "user-1" });
|
|
|
|
Assert.NotNull(tool);
|
|
Assert.Equal(1, CountingTool.ConstructionCount);
|
|
Assert.Equal(0, OtherCountingTool.ConstructionCount);
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool registry disposes cached concrete tool instances")]
|
|
public async Task ToolRegistryDisposesCachedConcreteToolInstances()
|
|
{
|
|
DisposableCachedTool.ConstructionCount = 0;
|
|
DisposableCachedTool.DisposeCount = 0;
|
|
var services = new ServiceCollection();
|
|
services.AddTransient<IAITool, DisposableCachedTool>();
|
|
var registry = CreateRegistry(services);
|
|
|
|
await registry.ListAsync(new AIToolQuery { ActorId = "user-1" });
|
|
DisposableCachedTool.ConstructionCount = 0;
|
|
DisposableCachedTool.DisposeCount = 0;
|
|
|
|
var tool = await registry.FindAsync("disposable-cached", new AIToolQuery { ActorId = "user-1" });
|
|
tool?.Dispose();
|
|
|
|
Assert.NotNull(tool);
|
|
Assert.Equal(1, DisposableCachedTool.ConstructionCount);
|
|
Assert.Equal(1, DisposableCachedTool.DisposeCount);
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool registry find applies tenant and actor filters")]
|
|
public async Task ToolRegistryFindAppliesTenantAndActorFilters()
|
|
{
|
|
var registry = CreateRegistry(
|
|
[
|
|
new TestTool(new AIToolDefinition
|
|
{
|
|
Name = "restricted",
|
|
DisplayName = "Restricted",
|
|
EnabledByDefault = true,
|
|
TenantBehavior = AITenantBehavior.TenantScoped,
|
|
TenantIds = ["tenant-1"],
|
|
ActorIds = ["user-1"]
|
|
})
|
|
]);
|
|
|
|
using var allowed = await registry.FindAsync("restricted", new AIToolQuery { TenantId = "tenant-1", ActorId = "user-1" });
|
|
var denied = await registry.FindAsync("restricted", new AIToolQuery { TenantId = "tenant-2", ActorId = "user-1" });
|
|
|
|
Assert.NotNull(allowed);
|
|
Assert.Null(denied);
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool registry enforces tool permission requirements")]
|
|
public async Task ToolRegistryEnforcesToolPermissionRequirements()
|
|
{
|
|
var registry = CreateRegistry(
|
|
[
|
|
new TestTool(new AIToolDefinition
|
|
{
|
|
Name = "restricted",
|
|
DisplayName = "Restricted",
|
|
EnabledByDefault = true,
|
|
TenantBehavior = AITenantBehavior.TenantScoped,
|
|
Permissions = ["workflows:write"]
|
|
})
|
|
]);
|
|
|
|
var denied = await registry.FindAsync("restricted", new AIToolQuery { TenantId = "tenant-1", ActorId = "user-1" });
|
|
using var allowed = await registry.FindAsync("restricted", new AIToolQuery
|
|
{
|
|
TenantId = "tenant-1",
|
|
ActorId = "user-1",
|
|
UserPermissions = ["Workflows:Write"]
|
|
});
|
|
|
|
Assert.Null(denied);
|
|
Assert.NotNull(allowed);
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool enablement requires explicit administrative enablement")]
|
|
public void ToolEnablementRequiresExplicitAdministrativeEnablement()
|
|
{
|
|
var enablement = new AIToolEnablementService();
|
|
var definition = new AIToolDefinition
|
|
{
|
|
Name = "admin",
|
|
DisplayName = "Admin",
|
|
Mutability = AIToolMutability.Administrative,
|
|
EnabledByDefault = true
|
|
};
|
|
|
|
Assert.False(enablement.IsEnabled(definition));
|
|
|
|
enablement.Enable("admin");
|
|
Assert.False(enablement.IsEnabled(definition));
|
|
|
|
enablement.EnableAdministrative("admin");
|
|
Assert.True(enablement.IsEnabled(definition));
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool registry filters agent-scoped tools by agent")]
|
|
public async Task ToolRegistryFiltersAgentScopedToolsByAgent()
|
|
{
|
|
var registry = CreateRegistry(
|
|
[
|
|
new TestTool(new AIToolDefinition
|
|
{
|
|
Name = "agent-only",
|
|
DisplayName = "Agent only",
|
|
EnabledByDefault = true,
|
|
TenantBehavior = AITenantBehavior.TenantScoped,
|
|
AgentScopes = ["workflow-author"]
|
|
}),
|
|
new TestTool(new AIToolDefinition
|
|
{
|
|
Name = "agent-permission",
|
|
DisplayName = "Agent permission",
|
|
EnabledByDefault = true,
|
|
TenantBehavior = AITenantBehavior.TenantScoped,
|
|
AgentScopes = ["workflow-author"],
|
|
Permissions = ["workflows:write"]
|
|
})
|
|
]);
|
|
|
|
var deniedWithoutPermission = await registry.ListAsync(new AIToolQuery
|
|
{
|
|
Agent = "workflow-author",
|
|
TenantId = "tenant-1",
|
|
ActorId = "user-1"
|
|
});
|
|
var deniedByAgent = await registry.ListAsync(new AIToolQuery
|
|
{
|
|
Agent = "workflow-editor",
|
|
TenantId = "tenant-1",
|
|
ActorId = "user-1",
|
|
UserPermissions = ["workflows:write"]
|
|
});
|
|
var allowedWithPermission = await registry.ListAsync(new AIToolQuery
|
|
{
|
|
Agent = "workflow-author",
|
|
TenantId = "tenant-1",
|
|
ActorId = "user-1",
|
|
UserPermissions = ["workflows:write"]
|
|
});
|
|
|
|
var tool = Assert.Single(deniedWithoutPermission);
|
|
Assert.Equal("agent-only", tool.Name);
|
|
Assert.Empty(deniedByAgent);
|
|
Assert.Collection(
|
|
allowedWithPermission.OrderBy(x => x.Name),
|
|
agentOnly => Assert.Equal("agent-only", agentOnly.Name),
|
|
agentPermission => Assert.Equal("agent-permission", agentPermission.Name));
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool registry honors tenant and actor allowlists")]
|
|
public async Task ToolRegistryHonorsTenantAndActorAllowlists()
|
|
{
|
|
var registry = CreateRegistry(
|
|
[
|
|
new TestTool(new AIToolDefinition
|
|
{
|
|
Name = "matching",
|
|
DisplayName = "Matching",
|
|
TenantBehavior = AITenantBehavior.TenantScoped,
|
|
TenantIds = ["tenant-1"],
|
|
ActorIds = ["user-1"]
|
|
}),
|
|
new TestTool(new AIToolDefinition
|
|
{
|
|
Name = "wrong-tenant",
|
|
DisplayName = "Wrong tenant",
|
|
TenantBehavior = AITenantBehavior.TenantScoped,
|
|
TenantIds = ["tenant-2"]
|
|
}),
|
|
new TestTool(new AIToolDefinition
|
|
{
|
|
Name = "wrong-actor",
|
|
DisplayName = "Wrong actor",
|
|
ActorIds = ["user-2"]
|
|
})
|
|
]);
|
|
|
|
var tools = await registry.ListAsync(new AIToolQuery
|
|
{
|
|
TenantId = "tenant-1",
|
|
ActorId = "user-1"
|
|
});
|
|
|
|
var tool = Assert.Single(tools);
|
|
Assert.Equal("matching", tool.Name);
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool registry combines tenant actor agent and permission filters")]
|
|
public async Task ToolRegistryCombinesTenantActorAgentAndPermissionFilters()
|
|
{
|
|
var registry = CreateRegistry(
|
|
[
|
|
new TestTool(new AIToolDefinition
|
|
{
|
|
Name = "matching",
|
|
DisplayName = "Matching",
|
|
TenantBehavior = AITenantBehavior.TenantScoped,
|
|
TenantIds = ["tenant-1"],
|
|
ActorIds = ["user-1"],
|
|
AgentScopes = ["workflow-author"],
|
|
Permissions = ["workflows:write"]
|
|
}),
|
|
new TestTool(new AIToolDefinition
|
|
{
|
|
Name = "wrong-agent",
|
|
DisplayName = "Wrong agent",
|
|
TenantBehavior = AITenantBehavior.TenantScoped,
|
|
TenantIds = ["tenant-1"],
|
|
ActorIds = ["user-1"],
|
|
AgentScopes = ["workflow-editor"],
|
|
Permissions = ["workflows:write"]
|
|
}),
|
|
new TestTool(new AIToolDefinition
|
|
{
|
|
Name = "wrong-permission",
|
|
DisplayName = "Wrong permission",
|
|
TenantBehavior = AITenantBehavior.TenantScoped,
|
|
TenantIds = ["tenant-1"],
|
|
ActorIds = ["user-1"],
|
|
AgentScopes = ["workflow-author"],
|
|
Permissions = ["deployments:write"]
|
|
})
|
|
]);
|
|
|
|
var tools = await registry.ListAsync(new AIToolQuery
|
|
{
|
|
TenantId = "tenant-1",
|
|
ActorId = "user-1",
|
|
Agent = "workflow-author",
|
|
UserPermissions = ["workflows:write"]
|
|
});
|
|
|
|
var tool = Assert.Single(tools);
|
|
Assert.Equal("matching", tool.Name);
|
|
}
|
|
|
|
[Fact(DisplayName = "Tool registry skips tools with throwing definitions")]
|
|
public async Task ToolRegistrySkipsToolsWithThrowingDefinitions()
|
|
{
|
|
var tracker = new ScopeDisposalTracker();
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(tracker);
|
|
services.AddScoped<ScopedDependency>();
|
|
services.AddScoped<IAITool, ThrowingDefinitionTool>();
|
|
services.AddScoped<IAITool>(_ => new TestTool(new AIToolDefinition { Name = "healthy", DisplayName = "Healthy" }));
|
|
using var provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true });
|
|
var registry = new AIToolRegistry(provider.GetRequiredService<IServiceScopeFactory>(), new AIToolEnablementService());
|
|
|
|
var missing = await registry.FindAsync("throwing", new AIToolQuery());
|
|
var tools = await registry.ListAsync(new AIToolQuery());
|
|
|
|
Assert.Null(missing);
|
|
var tool = Assert.Single(tools);
|
|
Assert.Equal("healthy", tool.Name);
|
|
Assert.Equal(2, tracker.DisposeCount);
|
|
}
|
|
|
|
private static AIToolRegistry CreateRegistry(IReadOnlyCollection<IAITool> tools)
|
|
{
|
|
var services = new ServiceCollection();
|
|
foreach (var tool in tools)
|
|
services.AddScoped<IAITool>(_ => tool);
|
|
|
|
return CreateRegistry(services);
|
|
}
|
|
|
|
private static AIToolRegistry CreateRegistry(IServiceCollection services)
|
|
{
|
|
var provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true });
|
|
return new AIToolRegistry(provider.GetRequiredService<IServiceScopeFactory>(), new AIToolEnablementService());
|
|
}
|
|
|
|
private class TestTool(AIToolDefinition definition) : IAITool
|
|
{
|
|
public AIToolDefinition Definition { get; } = definition;
|
|
|
|
public ValueTask<AIToolResult> ExecuteAsync(AIToolExecutionContext context, CancellationToken cancellationToken = default) =>
|
|
ValueTask.FromResult(new AIToolResult());
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private class CountingTool : IAITool
|
|
{
|
|
public static int ConstructionCount { get; set; }
|
|
|
|
public CountingTool()
|
|
{
|
|
ConstructionCount++;
|
|
}
|
|
|
|
public AIToolDefinition Definition { get; } = new()
|
|
{
|
|
Name = "counting",
|
|
DisplayName = "Counting",
|
|
EnabledByDefault = true
|
|
};
|
|
|
|
public ValueTask<AIToolResult> ExecuteAsync(AIToolExecutionContext context, CancellationToken cancellationToken = default) =>
|
|
ValueTask.FromResult(new AIToolResult());
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private class OtherCountingTool : IAITool
|
|
{
|
|
public static int ConstructionCount { get; set; }
|
|
|
|
public OtherCountingTool()
|
|
{
|
|
ConstructionCount++;
|
|
}
|
|
|
|
public AIToolDefinition Definition { get; } = new()
|
|
{
|
|
Name = "other-counting",
|
|
DisplayName = "Other counting",
|
|
EnabledByDefault = true
|
|
};
|
|
|
|
public ValueTask<AIToolResult> ExecuteAsync(AIToolExecutionContext context, CancellationToken cancellationToken = default) =>
|
|
ValueTask.FromResult(new AIToolResult());
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private class DisposableCachedTool : IAITool
|
|
{
|
|
public static int ConstructionCount { get; set; }
|
|
public static int DisposeCount { get; set; }
|
|
|
|
public DisposableCachedTool()
|
|
{
|
|
ConstructionCount++;
|
|
}
|
|
|
|
public AIToolDefinition Definition { get; } = new()
|
|
{
|
|
Name = "disposable-cached",
|
|
DisplayName = "Disposable cached",
|
|
EnabledByDefault = true
|
|
};
|
|
|
|
public ValueTask<AIToolResult> ExecuteAsync(AIToolExecutionContext context, CancellationToken cancellationToken = default) =>
|
|
ValueTask.FromResult(new AIToolResult());
|
|
|
|
public void Dispose()
|
|
{
|
|
DisposeCount++;
|
|
}
|
|
}
|
|
|
|
private class ThrowingDefinitionTool(ScopedDependency dependency) : IAITool
|
|
{
|
|
private readonly ScopedDependency _dependency = dependency;
|
|
|
|
public AIToolDefinition Definition
|
|
{
|
|
get
|
|
{
|
|
_ = _dependency;
|
|
throw new InvalidOperationException("Definition unavailable.");
|
|
}
|
|
}
|
|
|
|
public ValueTask<AIToolResult> ExecuteAsync(AIToolExecutionContext context, CancellationToken cancellationToken = default) =>
|
|
ValueTask.FromResult(new AIToolResult());
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private class ScopedDependency(ScopeDisposalTracker tracker) : IDisposable
|
|
{
|
|
public void Dispose()
|
|
{
|
|
tracker.DisposeCount++;
|
|
}
|
|
}
|
|
|
|
private class ScopeDisposalTracker
|
|
{
|
|
public int DisposeCount { get; set; }
|
|
}
|
|
}
|