| File: Agents\AgentTelemetryCatalogTests.cs | Web Access |
| Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Formats.Tar; using System.IO.Compression; using System.Text; using System.Text.Json; using Aspire.Cli.Agents.AspireSkills; using Aspire.Cli.Agents.Hooks; namespace Aspire.Cli.Tests.Agents; public class AgentTelemetryCatalogTests { private const string Script = """ASPIRE_MCP_TOOLS="new_tool" """; private const string Manifest = """ { "version": "test", "skills": [ { "name": "new-skill", "files": [ { "relativePath": "SKILL.md" }, { "relativePath": "references/new.md" }, { "relativePath": "evals/private.json" }, { "relativePath": "scripts/helper.sh" } ] }, { "name": "another-skill", "files": [ { "relativePath": "SKILL.md" }, { "relativePath": "references/guide.yml" } ] } ] } """; [Theory] [InlineData("\n")] [InlineData("\r\n")] public void ReadsSkillsAndReferencesFromManifestWithoutScriptDeclarations(string newline) { var catalog = AgentTelemetryCatalog.Parse(ParseManifest(Manifest.ReplaceLineEndings(newline)), Script); Assert.Equal(["another-skill", "new-skill"], catalog.Skills.Order()); Assert.Equal(["new_tool"], catalog.Tools); Assert.Equal(["another-skill/references/guide.yml", "new-skill/references/new.md"], catalog.References.Order()); Assert.Contains("NEW-SKILL", catalog.Skills); Assert.False(catalog.Skills.Contains("third-party")); } [Fact] public void ManifestControlsSkillsEvenIfScriptHasStaleSkillDeclarations() { var script = Script + "\n" + """ ASPIRE_SKILLS="stale-skill" ASPIRE_REFERENCE_FILES="stale-skill/references/old.md" """; var catalog = AgentTelemetryCatalog.Parse(ParseManifest(Manifest), script); Assert.Equal(["another-skill", "new-skill"], catalog.Skills.Order()); Assert.Equal(["another-skill/references/guide.yml", "new-skill/references/new.md"], catalog.References.Order()); } [Theory] [InlineData("""{}""")] [InlineData("""{"skills":null}""")] [InlineData("""{"skills":[null]}""")] [InlineData("""{"skills":[{"name":""}]}""")] [InlineData("""{"skills":[{"name":"missing-files"}]}""")] [InlineData("""{"skills":[{"name":"null-files","files":null}]}""")] [InlineData("""{"skills":[{"name":"duplicate","files":[]},{"name":"duplicate","files":[]}]}""")] public void InvalidSkillInventoryFailsExplicitly(string json) { Assert.Throws<InvalidDataException>(() => AgentTelemetryCatalog.Parse(ParseManifest(json), Script)); } [Theory] [InlineData("../private")] [InlineData("third/party")] [InlineData("C:\\private")] public void SkillNamesUseExistingBundleValidation(string name) { var manifest = new SkillBundleManifest { Skills = [new() { Name = name }] }; Assert.Throws<InvalidOperationException>(() => AgentTelemetryCatalog.Parse(manifest, Script)); } [Theory] [InlineData("../private.md")] [InlineData("/private.md")] [InlineData("references/../../private.md")] public void ReferencePathsUseExistingBundleValidation(string path) { var manifest = new SkillBundleManifest { Skills = [new() { Name = "test-skill", Files = [new() { RelativePath = path }] }] }; Assert.Throws<InvalidOperationException>(() => AgentTelemetryCatalog.Parse(manifest, Script)); } [Theory] [InlineData("")] [InlineData("$(command)")] [InlineData("name;command")] public void InvalidToolDeclarationIsRejected(string values) { Assert.Throws<InvalidDataException>(() => AgentTelemetryCatalog.Parse(ParseManifest(Manifest), Script.Replace("new_tool", values))); } [Fact] public void MissingOrDuplicateToolDeclarationsAreRejected() { Assert.Throws<InvalidDataException>(() => AgentTelemetryCatalog.Parse(ParseManifest(Manifest), "")); Assert.Throws<InvalidDataException>(() => AgentTelemetryCatalog.Parse(ParseManifest(Manifest), Script + "\n" + Script)); } [Theory] [InlineData("skill-manifest.json")] [InlineData("package/skill-manifest.json")] [InlineData("aspire-skills-v1.2.3/skill-manifest.json")] public void ReadsStructuredManifestInMemoryWithoutVersionSpecificPaths(string path) { using var archive = CreateArchive((path, Manifest), ("skills/third-party/SKILL.md", "Not in the manifest.")); var manifest = AgentTelemetryCatalog.ReadManifest(archive); var catalog = AgentTelemetryCatalog.Parse(manifest, Script); Assert.Equal(["another-skill", "new-skill"], catalog.Skills.Order()); Assert.True(archive.CanRead); } [Fact] public void MissingOrDuplicateManifestsAreRejected() { using var missing = CreateArchive(("skills/not-a-manifest.txt", "{}")); using var duplicate = CreateArchive(("first/skill-manifest.json", Manifest), ("second/skill-manifest.json", Manifest)); Assert.Throws<InvalidDataException>(() => AgentTelemetryCatalog.ReadManifest(missing)); Assert.Throws<InvalidDataException>(() => AgentTelemetryCatalog.ReadManifest(duplicate)); } [Theory] [InlineData("null")] [InlineData("{malformed")] public void InvalidManifestJsonIsRejected(string content) { using var archive = CreateArchive(("skill-manifest.json", content)); if (content == "null") { Assert.Throws<InvalidDataException>(() => AgentTelemetryCatalog.ReadManifest(archive)); } else { Assert.Throws<JsonException>(() => AgentTelemetryCatalog.ReadManifest(archive)); } } private static SkillBundleManifest ParseManifest(string json) => JsonSerializer.Deserialize(json, AspireSkillsJsonSerializerContext.Default.SkillBundleManifest)!; private static MemoryStream CreateArchive(params (string Path, string Content)[] entries) { var archive = new MemoryStream(); using (var gzip = new GZipStream(archive, CompressionMode.Compress, leaveOpen: true)) using (var writer = new TarWriter(gzip, leaveOpen: true)) { foreach (var (path, content) in entries) { using var data = new MemoryStream(Encoding.UTF8.GetBytes(content)); writer.WriteEntry(new PaxTarEntry(TarEntryType.RegularFile, path) { DataStream = data }); } } archive.Position = 0; return archive; } }