diff --git a/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/AgentMcpSkillsSource.cs b/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/AgentMcpSkillsSource.cs index 123c4100b40..d69b798c7d8 100644 --- a/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/AgentMcpSkillsSource.cs +++ b/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/AgentMcpSkillsSource.cs @@ -78,7 +78,7 @@ public AgentMcpSkillsSource(McpClient client, AgentMcpSkillsSourceOptions? optio } /// - public override async Task> GetSkillsAsync(CancellationToken cancellationToken = default) + public override async Task> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default) { if (this.TryGetCachedSkills() is { } cached) { @@ -99,7 +99,7 @@ public override async Task> GetSkillsAsync(CancellationToken c { // The refresh owner uses CancellationToken.None so that a single caller's cancellation // does not abort the shared refresh for all concurrent waiters. - var skills = await this.GetCoreSkillsAsync(CancellationToken.None).ConfigureAwait(false); + var skills = await this.GetCoreSkillsAsync(context, CancellationToken.None).ConfigureAwait(false); this.UpdateCache(skills); @@ -155,7 +155,7 @@ private void UpdateCache(IList skills) /// Reads the skill index from the MCP server, dispatches entries to registered loaders, and /// returns the aggregated skill list. /// - private async Task> GetCoreSkillsAsync(CancellationToken cancellationToken) + private async Task> GetCoreSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken) { McpSkillIndex? index = await this.TryReadIndexAsync(cancellationToken).ConfigureAwait(false); @@ -185,7 +185,7 @@ private async Task> GetCoreSkillsAsync(CancellationToken cance foreach (var loader in this._loaders.Values) { var entries = entriesByType.TryGetValue(loader.EntryType, out List? matched) ? matched : []; - skills.AddRange(await loader.LoadAsync(entries, cancellationToken).ConfigureAwait(false)); + skills.AddRange(await loader.LoadAsync(entries, context, cancellationToken).ConfigureAwait(false)); } LogSkillsLoadedTotal(this._logger, skills.Count); diff --git a/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/Loaders/ArchiveEntryLoader.cs b/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/Loaders/ArchiveEntryLoader.cs index b483eefa4f2..4617e0bd5a1 100644 --- a/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/Loaders/ArchiveEntryLoader.cs +++ b/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/Loaders/ArchiveEntryLoader.cs @@ -50,7 +50,7 @@ public ArchiveEntryLoader(McpClient client, AgentMcpSkillsSourceOptions? options public string EntryType => "archive"; /// - public async Task> LoadAsync(IReadOnlyList entries, CancellationToken cancellationToken) + public async Task> LoadAsync(IReadOnlyList entries, AgentSkillsSourceContext context, CancellationToken cancellationToken) { // Filter out entries that are missing required fields or have invalid names. var archiveEntries = this.FilterValidEntries(entries); @@ -81,7 +81,7 @@ public async Task> LoadAsync(IReadOnlyList // Delegate discovery of extracted content to a file-based skills source. AgentFileSkillsSource fileSource = this.CreateFileSkillsSource(skillDirectories); - return await fileSource.GetSkillsAsync(cancellationToken).ConfigureAwait(false); + return await fileSource.GetSkillsAsync(context, cancellationToken).ConfigureAwait(false); } /// diff --git a/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/Loaders/IMcpSkillEntryLoader.cs b/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/Loaders/IMcpSkillEntryLoader.cs index 34b678165f5..358ad63623f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/Loaders/IMcpSkillEntryLoader.cs +++ b/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/Loaders/IMcpSkillEntryLoader.cs @@ -30,6 +30,7 @@ internal interface IMcpSkillEntryLoader /// loaded successfully. /// /// The index entries of this loader's . May be empty. + /// The skills source context for the current invocation. /// A token to cancel the operation. - Task> LoadAsync(IReadOnlyList entries, CancellationToken cancellationToken); + Task> LoadAsync(IReadOnlyList entries, AgentSkillsSourceContext context, CancellationToken cancellationToken); } diff --git a/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/Loaders/SkillMdEntryLoader.cs b/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/Loaders/SkillMdEntryLoader.cs index bf910478485..fa8ff02bd9d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/Loaders/SkillMdEntryLoader.cs +++ b/dotnet/src/Microsoft.Agents.AI.Mcp/Skills/Loaders/SkillMdEntryLoader.cs @@ -29,7 +29,7 @@ public SkillMdEntryLoader(McpClient client, ILoggerFactory loggerFactory) public string EntryType => "skill-md"; /// - public Task> LoadAsync(IReadOnlyList entries, CancellationToken cancellationToken) + public Task> LoadAsync(IReadOnlyList entries, AgentSkillsSourceContext context, CancellationToken cancellationToken) { var skills = new List(); diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentInMemorySkillsSource.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentInMemorySkillsSource.cs index 57c9295c249..b4c71cf0b3c 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/AgentInMemorySkillsSource.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentInMemorySkillsSource.cs @@ -28,7 +28,7 @@ public AgentInMemorySkillsSource(IEnumerable skills) } /// - public override Task> GetSkillsAsync(CancellationToken cancellationToken = default) + public override Task> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default) { return Task.FromResult>(this._skills); } diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs index 5e8d07cc428..f4841a9cc7d 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs @@ -232,7 +232,8 @@ protected override async ValueTask ProvideAIContextAsync(InvokingCont private async Task CreateContextAsync(InvokingContext context, CancellationToken cancellationToken) { - var skills = await this._source.GetSkillsAsync(cancellationToken).ConfigureAwait(false); + var skillsContext = new AgentSkillsSourceContext(context.Agent); + var skills = await this._source.GetSkillsAsync(skillsContext, cancellationToken).ConfigureAwait(false); if (skills is not { Count: > 0 }) { return await base.ProvideAIContextAsync(context, cancellationToken).ConfigureAwait(false); diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsSource.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsSource.cs index 6a72d0c01a9..ff51ce3cd8d 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsSource.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsSource.cs @@ -18,7 +18,8 @@ public abstract class AgentSkillsSource /// /// Gets the skills provided by this source. /// + /// The context for the current invocation, exposing the invoking . /// Cancellation token. /// A collection of skills from this source. - public abstract Task> GetSkillsAsync(CancellationToken cancellationToken = default); + public abstract Task> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default); } diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsSourceContext.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsSourceContext.cs new file mode 100644 index 00000000000..7baacd2c144 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsSourceContext.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI; + +/// +/// Provides context about the current invocation to . +/// +/// +/// This context is created internally by and passed through the +/// source pipeline, allowing skill sources to make context-aware decisions such as filtering skills +/// per agent. +/// +[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] +public sealed class AgentSkillsSourceContext +{ + /// + /// Initializes a new instance of the class. + /// + /// The agent that is invoking the skill source. + /// is . + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] + public AgentSkillsSourceContext(AIAgent agent) + { + this.Agent = Throw.IfNull(agent); + } + + /// + /// Gets the agent that is invoking the skill source. + /// + public AIAgent Agent { get; } +} diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AggregatingAgentSkillsSource.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AggregatingAgentSkillsSource.cs index 7dc468742fd..8173b65276b 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/AggregatingAgentSkillsSource.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/AggregatingAgentSkillsSource.cs @@ -31,12 +31,12 @@ public AggregatingAgentSkillsSource(IEnumerable sources) } /// - public override async Task> GetSkillsAsync(CancellationToken cancellationToken = default) + public override async Task> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default) { var allSkills = new List(); foreach (var source in this._sources) { - var skills = await source.GetSkillsAsync(cancellationToken).ConfigureAwait(false); + var skills = await source.GetSkillsAsync(context, cancellationToken).ConfigureAwait(false); allSkills.AddRange(skills); } diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Decorators/DeduplicatingAgentSkillsSource.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Decorators/DeduplicatingAgentSkillsSource.cs index bf943daae55..269b03ec1ff 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/Decorators/DeduplicatingAgentSkillsSource.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Decorators/DeduplicatingAgentSkillsSource.cs @@ -31,9 +31,9 @@ public DeduplicatingAgentSkillsSource(AgentSkillsSource innerSource, ILoggerFact } /// - public override async Task> GetSkillsAsync(CancellationToken cancellationToken = default) + public override async Task> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default) { - var allSkills = await this.InnerSource.GetSkillsAsync(cancellationToken).ConfigureAwait(false); + var allSkills = await this.InnerSource.GetSkillsAsync(context, cancellationToken).ConfigureAwait(false); var deduplicated = new List(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Decorators/DelegatingAgentSkillsSource.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Decorators/DelegatingAgentSkillsSource.cs index 920ad0428b3..9c1f06694a4 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/Decorators/DelegatingAgentSkillsSource.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Decorators/DelegatingAgentSkillsSource.cs @@ -36,6 +36,6 @@ protected DelegatingAgentSkillsSource(AgentSkillsSource innerSource) protected AgentSkillsSource InnerSource { get; } /// - public override Task> GetSkillsAsync(CancellationToken cancellationToken = default) - => this.InnerSource.GetSkillsAsync(cancellationToken); + public override Task> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default) + => this.InnerSource.GetSkillsAsync(context, cancellationToken); } diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Decorators/FilteringAgentSkillsSource.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Decorators/FilteringAgentSkillsSource.cs index 2bd26acce26..73c95b635db 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/Decorators/FilteringAgentSkillsSource.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Decorators/FilteringAgentSkillsSource.cs @@ -45,9 +45,9 @@ public FilteringAgentSkillsSource( } /// - public override async Task> GetSkillsAsync(CancellationToken cancellationToken = default) + public override async Task> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default) { - var allSkills = await this.InnerSource.GetSkillsAsync(cancellationToken).ConfigureAwait(false); + var allSkills = await this.InnerSource.GetSkillsAsync(context, cancellationToken).ConfigureAwait(false); var filtered = new List(); foreach (var skill in allSkills) diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSource.cs b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSource.cs index 0fc3748983a..9b7770908eb 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSource.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSource.cs @@ -114,7 +114,7 @@ public AgentFileSkillsSource( } /// - public override Task> GetSkillsAsync(CancellationToken cancellationToken = default) + public override Task> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default) { var discoveredPaths = DiscoverSkillDirectories(this._skillPaths); diff --git a/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/Skills/AgentMcpSkillsSourceArchiveTests.cs b/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/Skills/AgentMcpSkillsSourceArchiveTests.cs index f3894df8288..2f500e2111d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/Skills/AgentMcpSkillsSourceArchiveTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/Skills/AgentMcpSkillsSourceArchiveTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -56,6 +56,8 @@ Content B. private readonly string _extractionRoot = Path.Combine(Path.GetTempPath(), "af-mcp-archive-tests", Guid.NewGuid().ToString("N")); + private readonly AgentSkillsSourceContext _context = new(new TestAIAgent()); + private const int ManyFileArchiveFileCount = 60; [Fact] @@ -68,7 +70,7 @@ public async Task GetSkillsAsync_ZipArchive_DiscoversSkillAsync() var source = new AgentMcpSkillsSource(client, options); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert var skill = Assert.Single(skills); @@ -87,7 +89,7 @@ public async Task GetSkillsAsync_TarGzArchive_DiscoversSkillAsync() var source = new AgentMcpSkillsSource(client, options); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert var skill = Assert.Single(skills); @@ -110,7 +112,7 @@ public async Task GetSkillsAsync_ArchiveWithScript_SurfacesScriptAsReadableResou var source = new AgentMcpSkillsSource(client, options); // Act - var skill = Assert.Single(await source.GetSkillsAsync()); + var skill = Assert.Single(await source.GetSkillsAsync(this._context)); var resource = await skill.GetResourceAsync("scripts/run.py"); // Assert - the .py file is readable as a resource (not an executable script). @@ -133,8 +135,8 @@ public async Task GetSkillsAsync_TwoSourcesWithSeparateDirectories_DoNotCollideA var sourceB = new AgentMcpSkillsSource(clientB, new() { ArchiveSkillsDirectory = Path.Combine(this._extractionRoot, "b") }); // Act - var skillA = Assert.Single(await sourceA.GetSkillsAsync()); - var skillB = Assert.Single(await sourceB.GetSkillsAsync()); + var skillA = Assert.Single(await sourceA.GetSkillsAsync(this._context)); + var skillB = Assert.Single(await sourceB.GetSkillsAsync(this._context)); // Assert - each source kept its own content despite the shared skill name. Assert.Equal("shared-skill", skillA.Frontmatter.Name); @@ -157,7 +159,7 @@ public async Task GetSkillsAsync_FirstRun_PrunesLeftoverSkillDirectoryAsync() var source = new AgentMcpSkillsSource(client, options); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert - the leftover directory is pruned and only the advertised skill remains. Assert.False(Directory.Exists(staleDir)); @@ -172,7 +174,7 @@ public async Task GetSkillsAsync_SkillNoLongerAdvertised_IsPrunedAsync() await using var fullServer = new InMemoryMcpServer(builder => builder.WithResources()); await using var fullClient = await fullServer.CreateClientAsync(); var firstSource = new AgentMcpSkillsSource(fullClient, new() { ArchiveSkillsDirectory = this._extractionRoot }); - var firstSkills = await firstSource.GetSkillsAsync(); + var firstSkills = await firstSource.GetSkillsAsync(this._context); Assert.Equal(2, firstSkills.Count); Assert.True(Directory.Exists(Path.Combine(this._extractionRoot, "skill-b"))); @@ -180,7 +182,7 @@ public async Task GetSkillsAsync_SkillNoLongerAdvertised_IsPrunedAsync() await using var partialServer = new InMemoryMcpServer(builder => builder.WithResources()); await using var partialClient = await partialServer.CreateClientAsync(); var secondSource = new AgentMcpSkillsSource(partialClient, new() { ArchiveSkillsDirectory = this._extractionRoot }); - var secondSkills = await secondSource.GetSkillsAsync(); + var secondSkills = await secondSource.GetSkillsAsync(this._context); // Assert - skill-b's directory was pruned; only skill-a remains. var skill = Assert.Single(secondSkills); @@ -203,7 +205,7 @@ public async Task GetSkillsAsync_ServerListsNoArchives_PrunesLeftoversAsync() var source = new AgentMcpSkillsSource(client, options); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert - the leftover directory is pruned and no skills are returned. Assert.Empty(skills); @@ -218,14 +220,14 @@ public async Task GetSkillsAsync_SecondDiscovery_ReExtractsContentAsync() await using (var clientA = await serverA.CreateClientAsync()) { var firstSource = new AgentMcpSkillsSource(clientA, new() { ArchiveSkillsDirectory = this._extractionRoot }); - Assert.Single(await firstSource.GetSkillsAsync()); + Assert.Single(await firstSource.GetSkillsAsync(this._context)); } // Act - a second run over the same directory re-extracts server B's content. await using var serverB = new InMemoryMcpServer(builder => builder.WithResources()); await using var clientB = await serverB.CreateClientAsync(); var source = new AgentMcpSkillsSource(clientB, new() { ArchiveSkillsDirectory = this._extractionRoot }); - var skill = Assert.Single(await source.GetSkillsAsync()); + var skill = Assert.Single(await source.GetSkillsAsync(this._context)); // Assert - the content was replaced with server B's. Assert.Contains("Content from server B.", await skill.GetContentAsync()); @@ -258,7 +260,7 @@ Stale content. var source = new AgentMcpSkillsSource(client, new() { ArchiveSkillsDirectory = this._extractionRoot }); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert - failed cleanup prevents the stale directory from being proxied to AgentFileSkillsSource. Assert.Empty(skills); @@ -412,7 +414,7 @@ public async Task GetSkillsAsync_ArchiveExceedsDefaultFileCount_SkillSkippedAsyn var source = new AgentMcpSkillsSource(client, new() { ArchiveSkillsDirectory = this._extractionRoot }); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -432,7 +434,7 @@ public async Task GetSkillsAsync_ArchiveExceedsConfiguredArchiveSize_SkillSkippe var source = new AgentMcpSkillsSource(client, options); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -452,7 +454,7 @@ public async Task GetSkillsAsync_RaisedFileCountOption_LoadsLargerArchiveAsync() var source = new AgentMcpSkillsSource(client, options); // Act - var skill = Assert.Single(await source.GetSkillsAsync()); + var skill = Assert.Single(await source.GetSkillsAsync(this._context)); // Assert Assert.Equal("archived-skill", skill.Frontmatter.Name); @@ -468,7 +470,7 @@ public async Task GetSkillsAsync_EntryMissingName_SkillSkippedAsync() var source = new AgentMcpSkillsSource(client, options); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert - the invalid entry is skipped; no skills are surfaced. Assert.Empty(skills); @@ -484,7 +486,7 @@ public async Task GetSkillsAsync_EntryWithInvalidNameChars_SkillSkippedAsync() var source = new AgentMcpSkillsSource(client, options); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -500,7 +502,7 @@ public async Task GetSkillsAsync_EntryMissingUrl_SkillSkippedAsync() var source = new AgentMcpSkillsSource(client, options); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -517,7 +519,7 @@ public async Task GetSkillsAsync_ArchiveWithUnsupportedFormat_SkillSkippedAsync( var source = new AgentMcpSkillsSource(client, options); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -533,7 +535,7 @@ public async Task GetSkillsAsync_ArchiveReturnsTextNotBlob_SkillSkippedAsync() var source = new AgentMcpSkillsSource(client, options); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -837,3 +839,4 @@ private sealed class TextOnlyArchiveServer #endregion } + diff --git a/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/Skills/AgentMcpSkillsSourceTests.cs b/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/Skills/AgentMcpSkillsSourceTests.cs index 891f2a726b4..672ba11e962 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/Skills/AgentMcpSkillsSourceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/Skills/AgentMcpSkillsSourceTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Threading.Tasks; using Microsoft.Extensions.AI; @@ -13,6 +13,7 @@ namespace Microsoft.Agents.AI.Skills.Mcp.UnitTests; /// public sealed class AgentMcpSkillsSourceTests { + private readonly AgentSkillsSourceContext _context = new(new TestAIAgent()); private const string SampleSkillMd = """ --- name: unit-converter @@ -47,7 +48,7 @@ public async Task GetSkillsAsync_IndexBasedDiscovery_ReturnsSkillAsync() var source = new AgentMcpSkillsSource(client); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert - frontmatter comes from index; Content is the actual SKILL.md body from the server. var skill = Assert.Single(skills); @@ -71,7 +72,7 @@ public async Task GetSkillsAsync_NoIndex_ReturnsEmptyAsync() var source = new AgentMcpSkillsSource(client); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -88,7 +89,7 @@ public async Task GetResourceAsync_SiblingText_ReturnsContentAsync() var source = new AgentMcpSkillsSource(client); // Act - var skill = Assert.Single(await source.GetSkillsAsync()); + var skill = Assert.Single(await source.GetSkillsAsync(this._context)); var resource = await skill.GetResourceAsync("references/checklist.md"); // Assert @@ -107,7 +108,7 @@ public async Task GetResourceAsync_SiblingBinary_ReturnsDataContentAsync() var source = new AgentMcpSkillsSource(client); // Act - var skill = Assert.Single(await source.GetSkillsAsync()); + var skill = Assert.Single(await source.GetSkillsAsync(this._context)); var resource = await skill.GetResourceAsync("assets/icon.bin"); // Assert @@ -130,7 +131,7 @@ public async Task GetResourceAsync_UnknownName_ReturnsNullAsync() var source = new AgentMcpSkillsSource(client); // Act - var skill = Assert.Single(await source.GetSkillsAsync()); + var skill = Assert.Single(await source.GetSkillsAsync(this._context)); var resource = await skill.GetResourceAsync("references/does-not-exist.md"); // Assert - resource does not exist on the server, so null is returned @@ -151,7 +152,7 @@ public async Task GetResourceAsync_PathTraversalName_ReturnsNullAsync(string nam var source = new AgentMcpSkillsSource(client); // Act - var skill = Assert.Single(await source.GetSkillsAsync()); + var skill = Assert.Single(await source.GetSkillsAsync(this._context)); var resource = await skill.GetResourceAsync(name); // Assert - resource does not exist on the server, so null is returned @@ -169,7 +170,7 @@ public async Task GetSkillsAsync_DoesNotReadSkillMdAsync() var source = new AgentMcpSkillsSource(client); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert - discovery succeeds from index alone. var skill = Assert.Single(skills); @@ -186,7 +187,7 @@ public async Task GetSkillsAsync_IndexEntryWithInvalidName_IsSkippedAsync() var source = new AgentMcpSkillsSource(client); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -202,7 +203,7 @@ public async Task GetSkillsAsync_IndexEntryWithMissingRequiredFields_IsSkippedAs var source = new AgentMcpSkillsSource(client); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -219,7 +220,7 @@ public async Task GetSkillsAsync_ArchiveEntryWithUnreadableResource_IsSkippedAsy var source = new AgentMcpSkillsSource(client); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -236,7 +237,7 @@ public async Task GetSkillsAsync_IndexEntryWithTemplateType_IsSkippedAsync() var source = new AgentMcpSkillsSource(client); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -391,3 +392,4 @@ public static string Index() => """ #endregion } + diff --git a/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/TestAIAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/TestAIAgent.cs new file mode 100644 index 00000000000..c993626af51 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/TestAIAgent.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Skills.Mcp.UnitTests; + +/// +/// A minimal implementation for unit tests. +/// +internal sealed class TestAIAgent : AIAgent +{ + protected override ValueTask CreateSessionCoreAsync(CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + protected override ValueTask SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + protected override ValueTask DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + protected override Task RunCoreAsync(IEnumerable messages, AgentSession? session, AgentRunOptions? options, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + protected override async IAsyncEnumerable RunCoreStreamingAsync(IEnumerable messages, AgentSession? session, AgentRunOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.CompletedTask; + yield break; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs index 5fd1fa77bab..cbc67bb0f8e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -86,7 +86,7 @@ public async Task AgentInMemorySkillsSource_ReturnsAllSkillsAsync() var source = new AgentInMemorySkillsSource(skills); // Act - var result = await source.GetSkillsAsync(CancellationToken.None); + var result = await source.GetSkillsAsync(new AgentSkillsSourceContext(new TestAIAgent()), CancellationToken.None); // Assert Assert.Equal(2, result.Count); @@ -1138,3 +1138,4 @@ private sealed class DuplicateScriptsSkill : AgentClassSkill Task.FromResult(null); private readonly string _testRoot; + private readonly AgentSkillsSourceContext _context = new(new TestAIAgent()); public AgentFileSkillsSourceScriptTests() { @@ -40,7 +41,7 @@ public async Task GetSkillsAsync_WithScriptFiles_DiscoversScriptsAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(CancellationToken.None); + var skills = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Single(skills); @@ -64,7 +65,7 @@ public async Task GetSkillsAsync_WithMultipleScriptExtensions_DiscoversAllAsync( var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(CancellationToken.None); + var skills = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Single(skills); @@ -88,7 +89,7 @@ public async Task GetSkillsAsync_NonScriptExtensionsAreNotDiscoveredAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(CancellationToken.None); + var skills = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Single(skills); @@ -103,7 +104,7 @@ public async Task GetSkillsAsync_NoScriptFiles_ReturnsEmptyScriptsAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(CancellationToken.None); + var skills = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Single(skills); @@ -120,7 +121,7 @@ public async Task GetSkillsAsync_ScriptsInRootAndSubdirectories_AreDiscoveredByD var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(CancellationToken.None); + var skills = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert — both root and subdirectory scripts are discovered Assert.Single(skills); @@ -146,7 +147,7 @@ public async Task GetSkillsAsync_WithRunner_ScriptsCanRunAsync() }); // Act - var skills = await source.GetSkillsAsync(CancellationToken.None); + var skills = await source.GetSkillsAsync(this._context, CancellationToken.None); var scriptResult = await (await skills[0].GetScriptAsync("scripts/test.py"))!.RunAsync(skills[0], null, null, CancellationToken.None); // Assert @@ -171,7 +172,7 @@ public async Task GetSkillsAsync_ScriptsWithNoRunner_ThrowsOnRunAsync() var source = new AgentFileSkillsSource(this._testRoot, scriptRunner: null); // Act — discovery succeeds even without a runner - var skills = await source.GetSkillsAsync(CancellationToken.None); + var skills = await source.GetSkillsAsync(this._context, CancellationToken.None); var script = (await skills[0].GetScriptAsync("scripts/run.sh"))!; // Assert — running the script throws because no runner was provided @@ -188,7 +189,7 @@ public async Task GetSkillsAsync_CustomScriptExtensions_OnlyDiscoversMatchingAsy var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { AllowedScriptExtensions = s_rubyExtension }); // Act - var skills = await source.GetSkillsAsync(CancellationToken.None); + var skills = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Single(skills); @@ -212,7 +213,7 @@ public async Task GetSkillsAsync_ExecutorReceivesArgumentsAsync() }); // Act - var skills = await source.GetSkillsAsync(CancellationToken.None); + var skills = await source.GetSkillsAsync(this._context, CancellationToken.None); using var argumentsDoc = JsonDocument.Parse("""{"value":26.2,"factor":1.60934}"""); var arguments = argumentsDoc.RootElement; await (await skills[0].GetScriptAsync("scripts/test.py"))!.RunAsync(skills[0], arguments, null, CancellationToken.None); @@ -234,7 +235,7 @@ public async Task GetSkillsAsync_DeepScript_DiscoveredWithHigherDepthAsync() new AgentFileSkillsSourceOptions { SearchDepth = 5 }); // Act - var skills = await source.GetSkillsAsync(CancellationToken.None); + var skills = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert — script file inside the deeply nested directory is discovered Assert.Single(skills); @@ -254,7 +255,7 @@ public async Task GetSkillsAsync_ScriptFilter_ExcludesFilteredScriptsAsync() new AgentFileSkillsSourceOptions { ScriptFilter = ctx => !ctx.RelativeFilePath.StartsWith("f2/", StringComparison.OrdinalIgnoreCase) }); // Act - var skills = await source.GetSkillsAsync(CancellationToken.None); + var skills = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert — only scripts/ script is included; f2/ is excluded by filter Assert.Single(skills); @@ -287,3 +288,4 @@ private static void CreateFile(string root, string relativePath, string content) File.WriteAllText(fullPath, content); } } + diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentInMemorySkillsSourceTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentInMemorySkillsSourceTests.cs index 69e96f0957c..3f5b4b91021 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentInMemorySkillsSourceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentInMemorySkillsSourceTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Threading; @@ -11,6 +11,7 @@ namespace Microsoft.Agents.AI.UnitTests.AgentSkills; /// public sealed class AgentInMemorySkillsSourceTests { + private readonly AgentSkillsSourceContext _context = new(new TestAIAgent()); [Fact] public async Task GetSkillsAsync_ValidSkills_ReturnsAllAsync() { @@ -23,7 +24,7 @@ public async Task GetSkillsAsync_ValidSkills_ReturnsAllAsync() var source = new AgentInMemorySkillsSource(skills); // Act - var result = await source.GetSkillsAsync(CancellationToken.None); + var result = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Equal(2, result.Count); @@ -49,3 +50,4 @@ public void Constructor_NullSkills_Throws() Assert.Throws(() => new AgentInMemorySkillsSource(null!)); } } + diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs index f5157ea192d..5028467d98d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -404,7 +404,8 @@ public async Task Builder_UseFileSkillWithOptionsResourceFilter_FiltersResources var source = new AgentFileSkillsSource(skillDir, s_noOpExecutor, options); // Act - var skills = await source.GetSkillsAsync(); + var skillsContext = new AgentSkillsSourceContext(this._agent); + var skills = await source.GetSkillsAsync(skillsContext); // Assert Assert.Single(skills); @@ -1337,7 +1338,7 @@ public CountingAgentSkillsSource(IList skills) public int GetSkillsCallCount => this._callCount; - public override Task> GetSkillsAsync(CancellationToken cancellationToken = default) + public override Task> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default) { Interlocked.Increment(ref this._callCount); return Task.FromResult(this._skills); @@ -1359,3 +1360,4 @@ public TestClassSkill(string name, string description, string instructions) protected override string Instructions => this._instructions; } } + diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsSourceContextTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsSourceContextTests.cs new file mode 100644 index 00000000000..e1a7ee05260 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsSourceContextTests.cs @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.UnitTests.AgentSkills; + +/// +/// Unit tests for and its integration with +/// and . +/// +public sealed class AgentSkillsSourceContextTests +{ + private readonly TestAIAgent _agent = new(); + + [Fact] + public void Constructor_NullAgent_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => new AgentSkillsSourceContext(null!)); + } + + [Fact] + public void Constructor_ValidAgent_SetsAgentProperty() + { + // Arrange & Act + var context = new AgentSkillsSourceContext(this._agent); + + // Assert + Assert.Same(this._agent, context.Agent); + } + + [Fact] + public async Task GetSkillsAsync_ReceivesContextWithCorrectAgentAsync() + { + // Arrange + var capturingSource = new ContextCapturingSkillsSource( + new AgentInlineSkill("test-skill", "Test skill.", "Instructions.")); + var context = new AgentSkillsSourceContext(this._agent); + + // Act + await capturingSource.GetSkillsAsync(context, CancellationToken.None); + + // Assert + Assert.NotNull(capturingSource.CapturedContext); + Assert.Same(this._agent, capturingSource.CapturedContext!.Agent); + } + + [Fact] + public async Task AgentSkillsProvider_PassesAgentContextToSourceAsync() + { + // Arrange + var capturingSource = new ContextCapturingSkillsSource( + new AgentInlineSkill("provider-skill", "Provider skill.", "Instructions.")); + var provider = new AgentSkillsProvider(capturingSource, options: new AgentSkillsProviderOptions { DisableCaching = true }); + var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext()); + + // Act + await provider.InvokingAsync(invokingContext, CancellationToken.None); + + // Assert — the provider should have created a context with the invoking agent + Assert.NotNull(capturingSource.CapturedContext); + Assert.Same(this._agent, capturingSource.CapturedContext!.Agent); + } + + [Fact] + public async Task AgentSkillsProvider_DifferentAgents_PassesDifferentContextsAsync() + { + // Arrange + var agentA = new TestAIAgent(); + var agentB = new TestAIAgent(); + var capturingSource = new ContextCapturingSkillsSource( + new AgentInlineSkill("multi-agent-skill", "Skill.", "Instructions.")); + var provider = new AgentSkillsProvider(capturingSource, options: new AgentSkillsProviderOptions { DisableCaching = true }); + + var contextA = new AIContextProvider.InvokingContext(agentA, session: null, new AIContext()); + var contextB = new AIContextProvider.InvokingContext(agentB, session: null, new AIContext()); + + // Act — invoke for agent A + await provider.InvokingAsync(contextA, CancellationToken.None); + var capturedAgentA = capturingSource.CapturedContext?.Agent; + + // Act — invoke for agent B + await provider.InvokingAsync(contextB, CancellationToken.None); + var capturedAgentB = capturingSource.CapturedContext?.Agent; + + // Assert + Assert.Same(agentA, capturedAgentA); + Assert.Same(agentB, capturedAgentB); + Assert.NotSame(capturedAgentA, capturedAgentB); + } + + [Fact] + public async Task DelegatingSource_PropagatesContextToInnerSourceAsync() + { + // Arrange + var inner = new ContextCapturingSkillsSource( + new AgentInlineSkill("delegated-skill", "Delegated skill.", "Instructions.")); + var outer = new PassThroughDelegatingSource(inner); + var context = new AgentSkillsSourceContext(this._agent); + + // Act + await outer.GetSkillsAsync(context, CancellationToken.None); + + // Assert — the inner source should have received the same context + Assert.NotNull(inner.CapturedContext); + Assert.Same(context, inner.CapturedContext); + } + + [Fact] + public async Task FilteringSource_PropagatesContextToInnerSourceAsync() + { + // Arrange + var inner = new ContextCapturingSkillsSource( + new AgentInlineSkill("filter-skill", "Skill to filter.", "Instructions.")); + var filtering = new FilteringAgentSkillsSource(inner, _ => true); + var context = new AgentSkillsSourceContext(this._agent); + + // Act + await filtering.GetSkillsAsync(context, CancellationToken.None); + + // Assert + Assert.NotNull(inner.CapturedContext); + Assert.Same(context, inner.CapturedContext); + } + + [Fact] + public async Task DeduplicatingSource_PropagatesContextToInnerSourceAsync() + { + // Arrange + var inner = new ContextCapturingSkillsSource( + new AgentInlineSkill("dedup-skill", "Dedup skill.", "Instructions.")); + var deduplicating = new DeduplicatingAgentSkillsSource(inner); + var context = new AgentSkillsSourceContext(this._agent); + + // Act + await deduplicating.GetSkillsAsync(context, CancellationToken.None); + + // Assert + Assert.NotNull(inner.CapturedContext); + Assert.Same(context, inner.CapturedContext); + } + + [Fact] + public async Task AggregatingSource_PropagatesContextToAllChildSourcesAsync() + { + // Arrange + var innerA = new ContextCapturingSkillsSource( + new AgentInlineSkill("skill-a", "Skill A.", "Instructions.")); + var innerB = new ContextCapturingSkillsSource( + new AgentInlineSkill("skill-b", "Skill B.", "Instructions.")); + var aggregating = new AggregatingAgentSkillsSource([innerA, innerB]); + var context = new AgentSkillsSourceContext(this._agent); + + // Act + await aggregating.GetSkillsAsync(context, CancellationToken.None); + + // Assert — both child sources receive the same context + Assert.NotNull(innerA.CapturedContext); + Assert.Same(context, innerA.CapturedContext); + Assert.NotNull(innerB.CapturedContext); + Assert.Same(context, innerB.CapturedContext); + } + + [Fact] + public async Task TestAgentSkillsSource_CapturesContextAsync() + { + // Arrange + var source = new TestAgentSkillsSource( + new AgentInlineSkill("ts-skill", "Test source skill.", "Instructions.")); + var context = new AgentSkillsSourceContext(this._agent); + + // Act + await source.GetSkillsAsync(context, CancellationToken.None); + + // Assert + Assert.Same(context, source.LastContext); + } + + /// + /// A skill source that captures the context passed to . + /// + private sealed class ContextCapturingSkillsSource : AgentSkillsSource + { + private readonly List _skills; + + public ContextCapturingSkillsSource(params AgentSkill[] skills) + { + this._skills = [.. skills]; + } + + public AgentSkillsSourceContext? CapturedContext { get; private set; } + + public override Task> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default) + { + this.CapturedContext = context; + return Task.FromResult>(this._skills); + } + } + + /// + /// A minimal delegating source that passes through to the inner source unchanged. + /// Used to verify context propagation through the decorator chain. + /// + private sealed class PassThroughDelegatingSource : DelegatingAgentSkillsSource + { + public PassThroughDelegatingSource(AgentSkillsSource innerSource) + : base(innerSource) + { + } + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/DeduplicatingAgentSkillsSourceTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/DeduplicatingAgentSkillsSourceTests.cs index 78950236812..6775d58212b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/DeduplicatingAgentSkillsSourceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/DeduplicatingAgentSkillsSourceTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; using System.Linq; @@ -12,6 +12,7 @@ namespace Microsoft.Agents.AI.UnitTests.AgentSkills; /// public sealed class DeduplicatingAgentSkillsSourceTests { + private readonly AgentSkillsSourceContext _context = new(new TestAIAgent()); [Fact] public async Task GetSkillsAsync_NoDuplicates_ReturnsAllSkillsAsync() { @@ -24,7 +25,7 @@ public async Task GetSkillsAsync_NoDuplicates_ReturnsAllSkillsAsync() var source = new DeduplicatingAgentSkillsSource(inner); // Act - var result = await source.GetSkillsAsync(CancellationToken.None); + var result = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Equal(2, result.Count); @@ -44,7 +45,7 @@ public async Task GetSkillsAsync_WithDuplicates_KeepsFirstOccurrenceAsync() var source = new DeduplicatingAgentSkillsSource(inner); // Act - var result = await source.GetSkillsAsync(CancellationToken.None); + var result = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Equal(2, result.Count); @@ -60,7 +61,7 @@ public async Task GetSkillsAsync_CaseInsensitiveDuplication_KeepsFirstAsync() var source = new DeduplicatingAgentSkillsSource(inner); // Act - var result = await source.GetSkillsAsync(CancellationToken.None); + var result = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Single(result); @@ -75,7 +76,7 @@ public async Task GetSkillsAsync_EmptySource_ReturnsEmptyAsync() var source = new DeduplicatingAgentSkillsSource(inner); // Act - var result = await source.GetSkillsAsync(CancellationToken.None); + var result = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Empty(result); @@ -86,7 +87,7 @@ public async Task GetSkillsAsync_EmptySource_ReturnsEmptyAsync() /// private sealed class FakeDuplicateCaseSource : AgentSkillsSource { - public override Task> GetSkillsAsync(CancellationToken cancellationToken = default) + public override Task> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default) { // AgentSkillFrontmatter validates names must be lowercase, so we build // two skills with the same lowercase name to test case-insensitive dedup. @@ -99,3 +100,4 @@ public override Task> GetSkillsAsync(CancellationToken cancell } } } + diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs index b001b46504e..2754ddcdce9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.IO; @@ -18,6 +18,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable private static readonly AgentFileSkillScriptRunner s_noOpExecutor = (skill, script, args, sp, ct) => Task.FromResult(null); private readonly string _testRoot; + private readonly AgentSkillsSourceContext _context = new(new TestAIAgent()); public FileAgentSkillLoaderTests() { @@ -41,7 +42,7 @@ public async Task GetSkillsAsync_ValidSkill_ReturnsSkillAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -61,7 +62,7 @@ public async Task GetSkillsAsync_QuotedFrontmatterValues_ParsesCorrectlyAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -81,7 +82,7 @@ public async Task GetSkillsAsync_BlockScalarDescription_ParsesMultilineValueAsyn var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -100,7 +101,7 @@ public async Task GetSkillsAsync_FoldedScalarDescription_ParsesMultilineValueAsy var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -125,7 +126,7 @@ public async Task GetSkillsAsync_ScalarDescriptionWithChompingIndicator_ParsesVa var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -142,7 +143,7 @@ public async Task GetSkillsAsync_MissingFrontmatter_ExcludesSkillAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -160,7 +161,7 @@ public async Task GetSkillsAsync_MissingNameField_ExcludesSkillAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -178,7 +179,7 @@ public async Task GetSkillsAsync_MissingDescriptionField_ExcludesSkillAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -206,7 +207,7 @@ public async Task GetSkillsAsync_InvalidName_ExcludesSkillAsync(string invalidNa var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -234,7 +235,7 @@ public async Task GetSkillsAsync_DuplicateNames_KeepsFirstOnlyAsync() var source = new DeduplicatingAgentSkillsSource(fileSource); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert – filesystem enumeration order is not guaranteed, so we only // verify that exactly one of the two duplicates was kept. @@ -253,7 +254,7 @@ public async Task GetSkillsAsync_NameMismatchesDirectory_ExcludesSkillAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -276,7 +277,7 @@ public async Task GetSkillsAsync_FilesWithMatchingExtensions_DiscoveredAsResourc var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -301,7 +302,7 @@ public async Task GetSkillsAsync_FilesWithNonMatchingExtensions_NotDiscoveredAsy var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -324,7 +325,7 @@ public async Task GetSkillsAsync_SkillMdFile_NotIncludedAsResourceAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -350,7 +351,7 @@ public async Task GetSkillsAsync_NestedResourceFiles_DiscoveredAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — only the file directly in references/ is discovered; the nested file is not Assert.Single(skills); @@ -375,7 +376,7 @@ public async Task GetSkillsAsync_CustomResourceExtensions_UsedForDiscoveryAsync( var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { AllowedResourceExtensions = s_customExtensions }); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — only .custom files should be discovered, not .json Assert.Single(skills); @@ -405,7 +406,7 @@ public async Task Constructor_NullExtensions_UsesDefaultsAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Assert — default extensions include .md - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); Assert.Single(skills[0].GetTestResources()!); } @@ -438,7 +439,7 @@ public async Task GetSkillsAsync_ResourceInSkillRoot_DiscoveredByDefaultAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — root-level files are discovered by default (depth=2 includes root) Assert.Single(skills); @@ -475,7 +476,7 @@ public async Task GetSkillsAsync_ResourceInSubdirectory_DiscoveredByDefaultAsync var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — subdirectory files are discovered by default Assert.Single(skills); @@ -501,7 +502,7 @@ public async Task GetSkillsAsync_ResourceFilter_ExcludesFilteredFilesAsync() new AgentFileSkillsSourceOptions { ResourceFilter = ctx => !ctx.RelativeFilePath.StartsWith("docs/", StringComparison.OrdinalIgnoreCase) }); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — only references/ resource is included; docs/ is excluded by filter Assert.Single(skills); @@ -518,7 +519,7 @@ public async Task GetSkillsAsync_NoResourceFiles_ReturnsEmptyResourcesAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -532,7 +533,7 @@ public async Task GetSkillsAsync_EmptyPaths_ReturnsEmptyListAsync() var source = new AgentFileSkillsSource(Enumerable.Empty(), s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -545,7 +546,7 @@ public async Task GetSkillsAsync_NonExistentPath_ReturnsEmptyListAsync() var source = new AgentFileSkillsSource(Path.Combine(this._testRoot, "does-not-exist"), s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -563,7 +564,7 @@ public async Task GetSkillsAsync_NestedSkillDirectory_DiscoveredWithinDepthLimit var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -579,7 +580,7 @@ public async Task ReadSkillResourceAsync_ValidResource_ReturnsContentAsync() Directory.CreateDirectory(refsDir); File.WriteAllText(Path.Combine(refsDir, "doc.md"), "Document content here."); var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); var resource = skills[0].GetTestResources()!.First(r => r.Name == "references/doc.md"); // Act @@ -602,7 +603,7 @@ public async Task GetSkillsAsync_NameExceedsMaxLength_ExcludesSkillAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -621,7 +622,7 @@ public async Task GetSkillsAsync_DescriptionExceedsMaxLength_ExcludesSkillAsync( var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Empty(skills); @@ -659,7 +660,7 @@ public async Task GetSkillsAsync_SymlinkInPath_SkipsSymlinkedResourcesAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — skill should still load, the symlinked references/ is skipped, assets/legit.md is found var skill = skills.FirstOrDefault(s => s.Frontmatter.Name == "symlink-escape-skill"); @@ -701,7 +702,7 @@ public async Task GetSkillsAsync_SymlinkedResourceDirectory_SkipsWithoutEnumerat var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — only assets/legit.md is found; the symlinked references/ directory is skipped entirely var skill = skills.FirstOrDefault(s => s.Frontmatter.Name == "symlink-directory-skip"); @@ -738,7 +739,7 @@ public async Task GetSkillsAsync_SymlinkedScriptDirectory_SkipsWithoutEnumeratin var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — skill loads but scripts from the symlinked directory are not discovered var skill = skills.FirstOrDefault(s => s.Frontmatter.Name == "symlink-script-skip"); @@ -778,7 +779,7 @@ public async Task GetSkillsAsync_SymlinkedIntermediateSegment_SkipsSymlinkedDire new AgentFileSkillsSourceOptions { SearchDepth = 4 }); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — the symlinked intermediate segment causes the directory to be skipped var skill = skills.FirstOrDefault(s => s.Frontmatter.Name == "symlink-intermediate"); @@ -797,7 +798,7 @@ public async Task GetSkillsAsync_FileWithUtf8Bom_ParsesSuccessfullyAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -815,7 +816,7 @@ public async Task GetSkillsAsync_LicenseField_ParsedCorrectlyAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -832,7 +833,7 @@ public async Task GetSkillsAsync_CompatibilityField_ParsedCorrectlyAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -849,7 +850,7 @@ public async Task GetSkillsAsync_AllowedToolsField_ParsedCorrectlyAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -866,7 +867,7 @@ public async Task GetSkillsAsync_MetadataField_ParsedCorrectlyAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -885,7 +886,7 @@ public async Task GetSkillsAsync_MetadataWithQuotedValues_ParsedCorrectlyAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -915,7 +916,7 @@ public async Task GetSkillsAsync_AllOptionalFields_ParsedCorrectlyAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -938,7 +939,7 @@ public async Task GetSkillsAsync_NoOptionalFields_DefaultsToNullAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert Assert.Single(skills); @@ -964,7 +965,7 @@ public async Task GetSkillsAsync_SearchDepthOne_OnlyRootFilesDiscoveredAsync() new AgentFileSkillsSourceOptions { SearchDepth = 1 }); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — scripts in subdirectories are NOT discovered at depth 1 Assert.Single(skills); @@ -985,7 +986,7 @@ public async Task GetSkillsAsync_ResourceInSubdirectory_DiscoveredWithDefaultDep var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — resource is discovered once Assert.Single(skills); @@ -1007,7 +1008,7 @@ public async Task GetSkillsAsync_ScriptInSubdirectory_DiscoveredWithDefaultDepth var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — script is discovered Assert.Single(skills); @@ -1034,7 +1035,7 @@ public async Task GetSkillsAsync_ResourceFilterWhitelist_OnlyMatchingFilesDiscov new AgentFileSkillsSourceOptions { ResourceFilter = ctx => ctx.RelativeFilePath.StartsWith("references/", StringComparison.OrdinalIgnoreCase) }); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — only the references/ resource is included Assert.Single(skills); @@ -1056,7 +1057,7 @@ public async Task GetSkillsAsync_DeepResource_NotDiscoveredWithDefaultDepthAsync var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — resource at depth 4 is NOT discovered with default depth=2 Assert.Single(skills); @@ -1078,7 +1079,7 @@ public async Task GetSkillsAsync_DeepResource_DiscoveredWithHigherDepthAsync() new AgentFileSkillsSourceOptions { SearchDepth = 5 }); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — resource file inside the deeply nested directory is discovered Assert.Single(skills); @@ -1129,7 +1130,7 @@ public async Task GetSkillsAsync_SkillBeyondMaxDepth_NotDiscoveredAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — skill at depth 3 should not be discovered Assert.DoesNotContain(skills, s => s.Frontmatter.Name == "deep-skill"); @@ -1148,7 +1149,7 @@ public async Task GetSkillsAsync_ScriptInSkillRoot_DiscoveredByDefaultAsync() var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — script at the skill root is discovered by default var skill = skills.FirstOrDefault(s => s.Frontmatter.Name == "root-script-skill"); @@ -1190,7 +1191,7 @@ public async Task GetSkillsAsync_SymlinkedFileInRealDirectory_SkipsSymlinkedFile var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — only legit.md should be discovered; the symlinked leak.md is skipped var skill = skills.FirstOrDefault(s => s.Frontmatter.Name == "symlink-file-skill"); @@ -1216,10 +1217,11 @@ public async Task GetSkillsAsync_NestedSkillMd_DoesNotTreatSubdirectoryAsIndepen var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act - var skills = await source.GetSkillsAsync(); + var skills = await source.GetSkillsAsync(this._context); // Assert — only the parent skill is discovered; the nested child is not an independent skill Assert.Single(skills); Assert.Equal("parent-skill", skills[0].Frontmatter.Name); } } + diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FilteringAgentSkillsSourceTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FilteringAgentSkillsSourceTests.cs index 12bdb28e05c..677b6edc53b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FilteringAgentSkillsSourceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FilteringAgentSkillsSourceTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Threading; @@ -11,6 +11,7 @@ namespace Microsoft.Agents.AI.UnitTests.AgentSkills; /// public sealed class FilteringAgentSkillsSourceTests { + private readonly AgentSkillsSourceContext _context = new(new TestAIAgent()); [Fact] public async Task GetSkillsAsync_PredicateIncludesAll_ReturnsAllSkillsAsync() { @@ -23,7 +24,7 @@ public async Task GetSkillsAsync_PredicateIncludesAll_ReturnsAllSkillsAsync() var source = new FilteringAgentSkillsSource(inner, _ => true); // Act - var result = await source.GetSkillsAsync(CancellationToken.None); + var result = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Equal(2, result.Count); @@ -41,7 +42,7 @@ public async Task GetSkillsAsync_PredicateExcludesAll_ReturnsEmptyAsync() var source = new FilteringAgentSkillsSource(inner, _ => false); // Act - var result = await source.GetSkillsAsync(CancellationToken.None); + var result = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Empty(result); @@ -62,7 +63,7 @@ public async Task GetSkillsAsync_PartialFilter_ReturnsMatchingSkillsOnlyAsync() skill => skill.Frontmatter.Name.StartsWith("keep", StringComparison.OrdinalIgnoreCase)); // Act - var result = await source.GetSkillsAsync(CancellationToken.None); + var result = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Equal(2, result.Count); @@ -77,7 +78,7 @@ public async Task GetSkillsAsync_EmptySource_ReturnsEmptyAsync() var source = new FilteringAgentSkillsSource(inner, _ => true); // Act - var result = await source.GetSkillsAsync(CancellationToken.None); + var result = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Empty(result); @@ -118,7 +119,7 @@ public async Task GetSkillsAsync_PreservesOrderAsync() skill => skill.Frontmatter.Name is "alpha" or "gamma"); // Act - var result = await source.GetSkillsAsync(CancellationToken.None); + var result = await source.GetSkillsAsync(this._context, CancellationToken.None); // Assert Assert.Equal(2, result.Count); @@ -126,3 +127,4 @@ public async Task GetSkillsAsync_PreservesOrderAsync() Assert.Equal("gamma", result[1].Frontmatter.Name); } } + diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/TestSkillTypes.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/TestSkillTypes.cs index 7110fb438ba..9065a759ffe 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/TestSkillTypes.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/TestSkillTypes.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; using System.Text.Json.Serialization; @@ -60,8 +60,12 @@ public TestAgentSkillsSource(params AgentSkill[] skills) } /// - public override Task> GetSkillsAsync(CancellationToken cancellationToken = default) + public AgentSkillsSourceContext? LastContext { get; private set; } + + /// + public override Task> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default) { + this.LastContext = context; return Task.FromResult(this._skills); } } @@ -114,3 +118,4 @@ internal sealed class SkillConfig internal sealed partial class SkillTestJsonContext : JsonSerializerContext { } +