// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Agents.AI.UnitTests.AgentSkills; /// /// Unit tests for and . /// public sealed class AgentClassSkillTests { [Fact] public void MinimalClassSkill_HasNullOverrides_AndSynthesizesContent() { // Arrange var skill = new MinimalClassSkill(); // Act & Assert — null overrides Assert.Equal("minimal", skill.Frontmatter.Name); Assert.Null(skill.Resources); Assert.Null(skill.Scripts); // Act & Assert — synthesized XML content Assert.Contains("minimal", skill.Content); Assert.Contains("A minimal skill.", skill.Content); Assert.Contains("", skill.Content); Assert.Contains("Minimal skill body.", skill.Content); Assert.Contains("", skill.Content); } [Fact] public void FullClassSkill_ReturnsOverriddenLists_AndCachesContent() { // Arrange var skill = new FullClassSkill(); // Act & Assert — overridden resources and scripts Assert.Single(skill.Resources!); Assert.Equal("test-resource", skill.Resources![0].Name); Assert.Single(skill.Scripts!); Assert.Equal("TestScript", skill.Scripts![0].Name); // Act & Assert — Content is cached Assert.Same(skill.Content, skill.Content); // Act & Assert — Content includes parameter schema from typed script Assert.Contains("parameters_schema", skill.Content); Assert.Contains("value", skill.Content); } [Fact] public void ResourcesAndScripts_CanBeLazyLoaded_AndCached() { // Arrange var skill = new LazyLoadedSkill(); // Act & Assert Assert.Equal(0, skill.ResourceCreationCount); Assert.Equal(0, skill.ScriptCreationCount); var firstResources = skill.Resources; var firstScripts = skill.Scripts; var secondResources = skill.Resources; var secondScripts = skill.Scripts; Assert.Single(firstResources!); Assert.Single(firstScripts!); Assert.Same(firstResources, secondResources); Assert.Same(firstScripts, secondScripts); Assert.Equal(1, skill.ResourceCreationCount); Assert.Equal(1, skill.ScriptCreationCount); } [Fact] public async Task AgentInMemorySkillsSource_ReturnsAllSkillsAsync() { // Arrange var skills = new AgentSkill[] { new MinimalClassSkill(), new FullClassSkill() }; var source = new AgentInMemorySkillsSource(skills); // Act var result = await source.GetSkillsAsync(CancellationToken.None); // Assert Assert.Equal(2, result.Count); Assert.Equal("minimal", result[0].Frontmatter.Name); Assert.Equal("full", result[1].Frontmatter.Name); } [Fact] public void AgentClassSkill_InvalidFrontmatter_ThrowsArgumentException() { // Act & Assert Assert.Throws(() => new AgentSkillFrontmatter("INVALID-NAME", "An invalid skill.")); } [Fact] public void PartialOverrides_OneCollectionNull_OtherHasValues() { // Arrange var resourceOnly = new ResourceOnlySkill(); var scriptOnly = new ScriptOnlySkill(); // Act & Assert Assert.Single(resourceOnly.Resources!); Assert.Null(resourceOnly.Scripts); Assert.Null(scriptOnly.Resources); Assert.Single(scriptOnly.Scripts!); } [Fact] public async Task CreateScriptAndResource_WithSerializerOptions_HandleCustomTypesAsync() { // Arrange var skill = new CustomTypeSkill(); var jso = SkillTestJsonContext.Default.Options; // Act — script with custom type deserialization var script = skill.Scripts![0]; var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "test", MaxResults = 5 }, jso); using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }"""); var args = argsDoc.RootElement; var scriptResult = await script.RunAsync(skill, args, null, CancellationToken.None); // Assert Assert.NotNull(scriptResult); var resultText = scriptResult!.ToString()!; Assert.Contains("result for test", resultText); Assert.Contains("5", resultText); // Act — resource with custom type serialization var resourceResult = await skill.Resources![0].ReadAsync(); // Assert Assert.NotNull(resourceResult); Assert.Contains("dark", resourceResult!.ToString()!); } [Fact] public void Scripts_DiscoveredViaAttribute_WithCorrectNamesAndDescriptions() { // Arrange var skill = new AttributedScriptsSkill(); // Act var scripts = skill.Scripts; // Assert — all scripts discovered with correct metadata Assert.NotNull(scripts); Assert.Equal(4, scripts!.Count); Assert.Contains(scripts, s => s.Name == "do-work"); Assert.Contains(scripts, s => s.Name == "DefaultNamed"); Assert.Contains(scripts, s => s.Name == "append"); var processScript = scripts.First(s => s.Name == "process"); Assert.Equal("Processes the input.", processScript.Description); } [Fact] public async Task Scripts_DiscoveredViaAttribute_StaticAndInstance_CanBeInvokedAsync() { // Arrange var skill = new AttributedScriptsSkill(); // Act & Assert — static method var doWorkScript = skill.Scripts!.First(s => s.Name == "do-work"); using var doWorkDoc = JsonDocument.Parse("""{"input":"hello"}"""); var doWorkResult = await doWorkScript.RunAsync(skill, doWorkDoc.RootElement, null, CancellationToken.None); Assert.Equal("HELLO", doWorkResult?.ToString()); // Act & Assert — instance method var appendScript = skill.Scripts!.First(s => s.Name == "append"); using var appendDoc = JsonDocument.Parse("""{"input":"test"}"""); var appendResult = await appendScript.RunAsync(skill, appendDoc.RootElement, null, CancellationToken.None); Assert.Equal("test-suffix", appendResult?.ToString()); } [Fact] public void Resources_DiscoveredViaAttribute_OnProperties_WithCorrectMetadata() { // Arrange var skill = new AttributedResourcePropertiesSkill(); // Act var resources = skill.Resources; // Assert — all resources discovered with correct metadata Assert.NotNull(resources); Assert.Equal(4, resources!.Count); Assert.Contains(resources, r => r.Name == "ref-data"); Assert.Contains(resources, r => r.Name == "DefaultNamed"); Assert.Contains(resources, r => r.Name == "static-data"); var describedResource = resources.First(r => r.Name == "data"); Assert.Equal("Some important data.", describedResource.Description); } [Fact] public async Task Resources_DiscoveredViaAttribute_OnProperties_CanBeReadAsync() { // Arrange var skill = new AttributedResourcePropertiesSkill(); // Act & Assert — instance property var refData = skill.Resources!.First(r => r.Name == "ref-data"); Assert.Equal("Reference content.", (await refData.ReadAsync())?.ToString()); // Act & Assert — static property var staticData = skill.Resources!.First(r => r.Name == "static-data"); Assert.Equal("Static content.", (await staticData.ReadAsync())?.ToString()); } [Fact] public async Task Resources_DiscoveredViaAttribute_OnProperty_InvokedEachTimeAsync() { // Arrange var skill = new AttributedResourceDynamicPropertySkill(); var resource = skill.Resources![0]; // Act var first = await resource.ReadAsync(); var second = await resource.ReadAsync(); // Assert — property getter is called on each ReadAsync, producing different values Assert.Equal("call-1", first?.ToString()); Assert.Equal("call-2", second?.ToString()); Assert.Equal(2, skill.CallCount); } [Fact] public void Resources_DiscoveredViaAttribute_OnMethods_WithCorrectMetadata() { // Arrange var skill = new AttributedResourceMethodsSkill(); // Act var resources = skill.Resources; // Assert Assert.NotNull(resources); Assert.Equal(4, resources!.Count); Assert.Contains(resources, r => r.Name == "dynamic"); Assert.Contains(resources, r => r.Name == "GetData"); Assert.Contains(resources, r => r.Name == "instance-dynamic"); var describedResource = resources.First(r => r.Name == "info"); Assert.Equal("Returns runtime info.", describedResource.Description); } [Fact] public async Task Resources_DiscoveredViaAttribute_OnMethods_CanBeReadAsync() { // Arrange var skill = new AttributedResourceMethodsSkill(); // Act & Assert — static method var dynamicResource = skill.Resources!.First(r => r.Name == "dynamic"); Assert.Equal("dynamic-value", (await dynamicResource.ReadAsync())?.ToString()); // Act & Assert — instance method var instanceResource = skill.Resources!.First(r => r.Name == "instance-dynamic"); Assert.Equal("instance-method-value", (await instanceResource.ReadAsync())?.ToString()); } [Fact] public void AttributedFullSkill_IncludesContentWithSchema_AndCachesMembers() { // Arrange var skill = new AttributedFullSkill(); // Act & Assert — Content includes reflected resources and scripts Assert.Contains("", skill.Content); Assert.Contains("conversion-table", skill.Content); Assert.Contains("", skill.Content); Assert.Contains("convert", skill.Content); // Act & Assert — discovered members are cached Assert.Same(skill.Resources, skill.Resources); Assert.Same(skill.Scripts, skill.Scripts); // Act & Assert — script has parameters schema var script = skill.Scripts![0]; Assert.NotNull(script.ParametersSchema); Assert.Contains("value", script.ParametersSchema!.Value.GetRawText()); } [Fact] public void NoAttributedMembers_NoOverrides_ReturnsNull() { // Arrange — skill with no attributes and no overrides; base discovery returns null (not empty list) var skill = new NoAttributesNoOverridesSkill(); var baseType = typeof(AgentClassSkill); var resourcesDiscoveredField = baseType.GetField("_resourcesDiscovered", BindingFlags.Instance | BindingFlags.NonPublic); var scriptsDiscoveredField = baseType.GetField("_scriptsDiscovered", BindingFlags.Instance | BindingFlags.NonPublic); var reflectedResourcesField = baseType.GetField("_reflectedResources", BindingFlags.Instance | BindingFlags.NonPublic); var reflectedScriptsField = baseType.GetField("_reflectedScripts", BindingFlags.Instance | BindingFlags.NonPublic); Assert.NotNull(resourcesDiscoveredField); Assert.NotNull(scriptsDiscoveredField); Assert.NotNull(reflectedResourcesField); Assert.NotNull(reflectedScriptsField); Assert.False((bool)resourcesDiscoveredField!.GetValue(skill)!); Assert.False((bool)scriptsDiscoveredField!.GetValue(skill)!); // Act & Assert Assert.Null(skill.Resources); Assert.Null(skill.Scripts); Assert.True((bool)resourcesDiscoveredField.GetValue(skill)!); Assert.True((bool)scriptsDiscoveredField.GetValue(skill)!); Assert.Null(reflectedResourcesField!.GetValue(skill)); Assert.Null(reflectedScriptsField!.GetValue(skill)); // Repeated access should not re-trigger discovery even when discovered value is null. Assert.Null(skill.Resources); Assert.Null(skill.Scripts); Assert.True((bool)resourcesDiscoveredField.GetValue(skill)!); Assert.True((bool)scriptsDiscoveredField.GetValue(skill)!); Assert.Null(reflectedResourcesField.GetValue(skill)); Assert.Null(reflectedScriptsField.GetValue(skill)); } [Fact] public void SubclassOverride_TakesPrecedence_OverAttributes() { // Arrange — skill has attributes AND overrides Resources/Scripts var skill = new AttributedWithOverrideSkill(); // Act var resources = skill.Resources; var scripts = skill.Scripts; // Assert — overrides win, not reflected members Assert.NotNull(resources); Assert.Single(resources!); Assert.Equal("manual-resource", resources![0].Name); Assert.NotNull(scripts); Assert.Single(scripts!); Assert.Equal("ManualScript", scripts![0].Name); } [Fact] public async Task MixedStaticAndInstance_AllDiscoveredAndInvocableAsync() { // Arrange var skill = new MixedStaticInstanceSkill(); // Act & Assert — correct counts Assert.NotNull(skill.Resources); Assert.Equal(2, skill.Resources!.Count); Assert.NotNull(skill.Scripts); Assert.Equal(2, skill.Scripts!.Count); // Act & Assert — all resources produce values foreach (var resource in skill.Resources!) { var value = await resource.ReadAsync(); Assert.NotNull(value); } // Act & Assert — all scripts produce values foreach (var script in skill.Scripts!) { var result = await script.RunAsync(skill, null, null, CancellationToken.None); Assert.NotNull(result); } } [Fact] public async Task SerializerOptions_UsedForReflectedMembersAsync() { // Arrange var skill = new AttributedSkillWithCustomSerializer(); var jso = SkillTestJsonContext.Default.Options; // Act & Assert — script with custom JSO var script = skill.Scripts![0]; var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "test", MaxResults = 3 }, jso); using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }"""); var args = argsDoc.RootElement; var scriptResult = await script.RunAsync(skill, args, null, CancellationToken.None); Assert.NotNull(scriptResult); Assert.Contains("test", scriptResult!.ToString()!); Assert.Contains("3", scriptResult!.ToString()!); // Act & Assert — resource with custom JSO var resourceResult = await skill.Resources![0].ReadAsync(); Assert.NotNull(resourceResult); Assert.Contains("light", resourceResult!.ToString()!); } [Fact] public void Content_IncludesDescription_ForReflectedResources() { // Arrange var skill = new AttributedResourcePropertiesSkill(); // Act var content = skill.Content; // Assert — descriptions from [Description] attribute appear in synthesized content Assert.Contains("Some important data.", content); } [Fact] public void IndexerPropertyWithResourceAttribute_ThrowsInvalidOperationException() { // Arrange var skill = new IndexerResourceSkill(); // Act & Assert — accessing Resources triggers discovery which should throw var ex = Assert.Throws(() => skill.Resources); Assert.Contains("indexer", ex.Message, StringComparison.OrdinalIgnoreCase); Assert.Contains("IndexerResourceSkill", ex.Message); } [Fact] public void ResourceMethodWithUnsupportedParameters_ThrowsInvalidOperationException() { // Arrange var skill = new UnsupportedParamResourceMethodSkill(); // Act & Assert — accessing Resources triggers discovery which should throw var ex = Assert.Throws(() => skill.Resources); Assert.Contains("content", ex.Message); Assert.Contains("String", ex.Message); } [Fact] public async Task ResourceMethodWithServiceProviderParam_IsDiscoveredSuccessfullyAsync() { // Arrange var skill = new ServiceProviderResourceMethodSkill(); var sp = new ServiceCollection().BuildServiceProvider(); // Act var resources = skill.Resources; // Assert Assert.NotNull(resources); Assert.Single(resources!); Assert.Equal("sp-resource", resources![0].Name); var value = await resources[0].ReadAsync(sp); Assert.Equal("from-sp-method", value?.ToString()); } [Fact] public async Task ResourceMethodWithCancellationTokenParam_IsDiscoveredSuccessfullyAsync() { // Arrange var skill = new CancellationTokenResourceMethodSkill(); // Act var resources = skill.Resources; // Assert Assert.NotNull(resources); Assert.Single(resources!); Assert.Equal("ct-resource", resources![0].Name); var value = await resources[0].ReadAsync(); Assert.Equal("from-ct-method", value?.ToString()); } [Fact] public async Task ResourceMethodWithBothServiceProviderAndCancellationToken_IsDiscoveredSuccessfullyAsync() { // Arrange var skill = new BothParamsResourceMethodSkill(); var sp = new ServiceCollection().BuildServiceProvider(); // Act var resources = skill.Resources; // Assert Assert.NotNull(resources); Assert.Single(resources!); Assert.Equal("both-resource", resources![0].Name); var value = await resources[0].ReadAsync(sp); Assert.Equal("from-both-method", value?.ToString()); } [Fact] public async Task CreateScript_FallsBackToSerializerOptions_WhenNoExplicitJsoAsync() { // Arrange var skill = new CreateMethodsFallbackSkill(); // Act — invoke script that uses custom types, relying on SerializerOptions fallback var script = skill.Scripts!.First(s => s.Name == "Lookup"); var jso = SkillTestJsonContext.Default.Options; var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "fallback", MaxResults = 7 }, jso); using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }"""); var args = argsDoc.RootElement; var result = await script.RunAsync(skill, args, null, CancellationToken.None); // Assert Assert.NotNull(result); Assert.Contains("fallback", result!.ToString()!); Assert.Contains("7", result!.ToString()!); } [Fact] public async Task CreateResource_FallsBackToSerializerOptions_WhenNoExplicitJsoAsync() { // Arrange var skill = new CreateMethodsFallbackSkill(); // Act — read resource that uses custom types, relying on SerializerOptions fallback var resource = skill.Resources!.First(r => r.Name == "config"); var result = await resource.ReadAsync(); // Assert Assert.NotNull(result); Assert.Contains("dark", result!.ToString()!); } [Fact] public async Task CreateScript_UsesExplicitJso_OverSerializerOptionsAsync() { // Arrange var skill = new CreateMethodsExplicitJsoSkill(); // Act — invoke script that passes explicit JSO (should take precedence over SerializerOptions) var script = skill.Scripts!.First(s => s.Name == "Lookup"); var jso = SkillTestJsonContext.Default.Options; var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "explicit", MaxResults = 2 }, jso); using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }"""); var args = argsDoc.RootElement; var result = await script.RunAsync(skill, args, null, CancellationToken.None); // Assert Assert.NotNull(result); Assert.Contains("explicit", result!.ToString()!); Assert.Contains("2", result!.ToString()!); } [Fact] public async Task CreateResource_UsesExplicitJso_OverSerializerOptionsAsync() { // Arrange var skill = new CreateMethodsExplicitJsoSkill(); // Act — read resource that passes explicit JSO (should take precedence over SerializerOptions) var resource = skill.Resources!.First(r => r.Name == "config"); var result = await resource.ReadAsync(); // Assert Assert.NotNull(result); Assert.Contains("explicit-theme", result!.ToString()!); } [Fact] public void DuplicateResourceNames_FromProperties_ThrowsInvalidOperationException() { // Arrange var skill = new DuplicateResourcePropertiesSkill(); // Act & Assert var ex = Assert.Throws(() => _ = skill.Resources); Assert.Contains("data", ex.Message); Assert.Contains("already has a resource", ex.Message); } [Fact] public void DuplicateResourceNames_FromPropertyAndMethod_ThrowsInvalidOperationException() { // Arrange var skill = new DuplicateResourcePropertyAndMethodSkill(); // Act & Assert var ex = Assert.Throws(() => _ = skill.Resources); Assert.Contains("data", ex.Message); Assert.Contains("already has a resource", ex.Message); } [Fact] public void DuplicateResourceNames_FromMethods_ThrowsInvalidOperationException() { // Arrange var skill = new DuplicateResourceMethodsSkill(); // Act & Assert var ex = Assert.Throws(() => _ = skill.Resources); Assert.Contains("data", ex.Message); Assert.Contains("already has a resource", ex.Message); } [Fact] public void DuplicateScriptNames_ThrowsInvalidOperationException() { // Arrange var skill = new DuplicateScriptsSkill(); // Act & Assert var ex = Assert.Throws(() => _ = skill.Scripts); Assert.Contains("do-work", ex.Message); Assert.Contains("already has a script", ex.Message); } #region Test skill classes private sealed class MinimalClassSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("minimal", "A minimal skill."); protected override string Instructions => "Minimal skill body."; } private sealed class FullClassSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("full", "A full skill with resources and scripts."); protected override string Instructions => "Full skill body."; public override IReadOnlyList? Resources => [ this.CreateResource("test-resource", "resource content"), ]; public override IReadOnlyList? Scripts => [ this.CreateScript("TestScript", TestScript), ]; private static string TestScript(double value) => JsonSerializer.Serialize(new { result = value * 2 }); } private sealed class ResourceOnlySkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("resource-only", "Skill with resources only."); protected override string Instructions => "Body."; public override IReadOnlyList? Resources => [ this.CreateResource("data", "some data"), ]; } private sealed class ScriptOnlySkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("script-only", "Skill with scripts only."); protected override string Instructions => "Body."; public override IReadOnlyList? Scripts => [ this.CreateScript("ToUpper", (string input) => input.ToUpperInvariant()), ]; } private sealed class LazyLoadedSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("lazy-loaded", "Skill with lazily created resources and scripts."); protected override string Instructions => "Body."; public int ResourceCreationCount { get; private set; } public int ScriptCreationCount { get; private set; } private IReadOnlyList? _resources; private IReadOnlyList? _scripts; public override IReadOnlyList? Resources => this._resources ??= this.CreateResources(); public override IReadOnlyList? Scripts => this._scripts ??= this.CreateScripts(); private IReadOnlyList CreateResources() { this.ResourceCreationCount++; return [this.CreateResource("lazy-resource", "resource content")]; } private IReadOnlyList CreateScripts() { this.ScriptCreationCount++; return [this.CreateScript("LazyScript", () => "done")]; } } private sealed class CustomTypeSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("custom-type-skill", "Skill with custom-typed scripts and resources."); protected override string Instructions => "Body."; public override IReadOnlyList? Resources => [ this.CreateResource("config", () => new SkillConfig { Theme = "dark", Verbose = true }, serializerOptions: SkillTestJsonContext.Default.Options), ]; public override IReadOnlyList? Scripts => [ this.CreateScript("Lookup", (LookupRequest request) => new LookupResponse { Items = [$"result for {request.Query}"], TotalCount = request.MaxResults, }, serializerOptions: SkillTestJsonContext.Default.Options), ]; } #pragma warning disable IDE0051 // Remove unused private members private sealed class AttributedScriptsSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("attributed-scripts", "Skill with various attributed scripts."); protected override string Instructions => "Body."; [AgentSkillScript("do-work")] private static string DoWork(string input) => input.ToUpperInvariant(); [AgentSkillScript] private static string DefaultNamed(string input) => input.ToUpperInvariant(); [AgentSkillScript("process")] [Description("Processes the input.")] private static string Process(string input) => input; [AgentSkillScript("append")] private string Append(string input) => input + "-suffix"; } private sealed class AttributedResourcePropertiesSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("attributed-resource-props", "Skill with various attributed resource properties."); protected override string Instructions => "Body."; [AgentSkillResource("ref-data")] public string ReferenceData => "Reference content."; [AgentSkillResource] public string DefaultNamed => "Some data."; [AgentSkillResource("data")] [Description("Some important data.")] public string DescribedData => "content"; [AgentSkillResource("static-data")] public static string StaticData => "Static content."; } private sealed class AttributedResourceMethodsSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("attributed-resource-methods", "Skill with various attributed resource methods."); protected override string Instructions => "Body."; [AgentSkillResource("dynamic")] private static string GetDynamic() => "dynamic-value"; [AgentSkillResource] private static string GetData() => "data"; [AgentSkillResource("info")] [Description("Returns runtime info.")] private static string GetInfo() => "runtime-info"; [AgentSkillResource("instance-dynamic")] private string GetValue() => "instance-method-value"; } private sealed class AttributedFullSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("attributed-full", "Full skill with attributed resources and scripts."); protected override string Instructions => "Convert units using the table."; [AgentSkillResource("conversion-table")] public string ConversionTable => "miles -> km: 1.60934"; [AgentSkillScript("convert")] private static string Convert(double value, double factor) => JsonSerializer.Serialize(new { result = value * factor }); } private sealed class NoAttributesNoOverridesSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("no-attrs", "Skill with no attributes or overrides."); protected override string Instructions => "Body."; } private sealed class AttributedWithOverrideSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("attributed-override", "Skill with attributes and overrides."); protected override string Instructions => "Body."; // These attributes should be ignored because Resources/Scripts are overridden. [AgentSkillResource("ignored-resource")] public string IgnoredData => "ignored"; [AgentSkillScript("ignored-script")] private static string IgnoredScript() => "ignored"; public override IReadOnlyList? Resources => [ this.CreateResource("manual-resource", "manual content"), ]; public override IReadOnlyList? Scripts => [ this.CreateScript("ManualScript", () => "manual result"), ]; } private sealed class AttributedResourceDynamicPropertySkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("attributed-resource-dynamic-prop", "Skill with dynamic property resource."); protected override string Instructions => "Body."; public int CallCount { get; private set; } [AgentSkillResource("counter")] public string Counter => $"call-{++this.CallCount}"; } private sealed class AttributedSkillWithCustomSerializer : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("attributed-custom-jso", "Skill with custom serializer options."); protected override string Instructions => "Body."; protected override JsonSerializerOptions? SerializerOptions => SkillTestJsonContext.Default.Options; [AgentSkillResource("config")] public SkillConfig Config => new() { Theme = "light", Verbose = false }; [AgentSkillScript("lookup")] private static LookupResponse Lookup(LookupRequest request) => new() { Items = [$"result for {request.Query}"], TotalCount = request.MaxResults, }; } private sealed class MixedStaticInstanceSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("mixed-static-instance", "Skill with both static and instance members."); protected override string Instructions => "Body."; [AgentSkillResource("static-resource")] public static string StaticResource => "static-value"; [AgentSkillResource("instance-resource")] public string InstanceResource => "instance-data"; [AgentSkillScript("static-script")] private static string StaticScript() => "static-result"; [AgentSkillScript("instance-script")] private string InstanceScript() => "instance-data"; } private sealed class CreateMethodsFallbackSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("create-fallback", "Skill testing SerializerOptions fallback for CreateScript/CreateResource."); protected override string Instructions => "Body."; protected override JsonSerializerOptions? SerializerOptions => SkillTestJsonContext.Default.Options; public override IReadOnlyList? Resources => [ this.CreateResource("config", () => new SkillConfig { Theme = "dark", Verbose = true, }), ]; public override IReadOnlyList? Scripts => [ this.CreateScript("Lookup", (LookupRequest request) => new LookupResponse { Items = [$"result for {request.Query}"], TotalCount = request.MaxResults, }), ]; } private sealed class CreateMethodsExplicitJsoSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("create-explicit-jso", "Skill testing explicit JSO overrides SerializerOptions."); protected override string Instructions => "Body."; // SerializerOptions is intentionally null — explicit JSO passed to CreateScript/CreateResource should be used. public override IReadOnlyList? Resources => [ this.CreateResource("config", () => new SkillConfig { Theme = "explicit-theme", Verbose = false, }, serializerOptions: SkillTestJsonContext.Default.Options), ]; public override IReadOnlyList? Scripts => [ this.CreateScript("Lookup", (LookupRequest request) => new LookupResponse { Items = [$"result for {request.Query}"], TotalCount = request.MaxResults, }, serializerOptions: SkillTestJsonContext.Default.Options), ]; } private sealed class IndexerResourceSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("indexer-skill", "Skill with indexer resource."); protected override string Instructions => "Body."; private readonly Dictionary _data = new() { ["key"] = "value" }; [AgentSkillResource("indexed")] public string this[string key] => this._data[key]; } private sealed class UnsupportedParamResourceMethodSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("unsupported-param-skill", "Skill with unsupported param resource method."); protected override string Instructions => "Body."; [AgentSkillResource("bad-resource")] private static string GetData(string content) => content; } private sealed class ServiceProviderResourceMethodSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("sp-param-skill", "Skill with IServiceProvider param resource method."); protected override string Instructions => "Body."; [AgentSkillResource("sp-resource")] private static string GetData(IServiceProvider? sp) => "from-sp-method"; } private sealed class CancellationTokenResourceMethodSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("ct-param-skill", "Skill with CancellationToken param resource method."); protected override string Instructions => "Body."; [AgentSkillResource("ct-resource")] private static string GetData(CancellationToken ct) => "from-ct-method"; } private sealed class BothParamsResourceMethodSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("both-param-skill", "Skill with both IServiceProvider and CancellationToken param resource method."); protected override string Instructions => "Body."; [AgentSkillResource("both-resource")] private static string GetData(IServiceProvider? sp, CancellationToken ct) => "from-both-method"; } private sealed class DuplicateResourcePropertiesSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("dup-res-props", "Skill with duplicate resource property names."); protected override string Instructions => "Body."; [AgentSkillResource("data")] public string Data1 => "value1"; [AgentSkillResource("data")] public string Data2 => "value2"; } private sealed class DuplicateResourcePropertyAndMethodSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("dup-res-prop-method", "Skill with duplicate resource from property and method."); protected override string Instructions => "Body."; [AgentSkillResource("data")] public string Data => "property-value"; [AgentSkillResource("data")] private static string GetData() => "method-value"; } private sealed class DuplicateResourceMethodsSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("dup-res-methods", "Skill with duplicate resource method names."); protected override string Instructions => "Body."; [AgentSkillResource("data")] private static string GetData1() => "value1"; [AgentSkillResource("data")] private static string GetData2() => "value2"; } private sealed class DuplicateScriptsSkill : AgentClassSkill { public override AgentSkillFrontmatter Frontmatter { get; } = new("dup-scripts", "Skill with duplicate script names."); protected override string Instructions => "Body."; [AgentSkillScript("do-work")] private static string DoWork1(string input) => input.ToUpperInvariant(); [AgentSkillScript("do-work")] private static string DoWork2(string input) => input + "-suffix"; } #pragma warning restore IDE0051 // Remove unused private members #endregion }