AX Agent ?? ?? ??? MCP ?? ??? ???? ??? ??? ??

- MCP ?? ?????? synthetic skill? ???? McpSkillCatalog? ???? ToolRegistry ?? snapshot ?? ??? ???
- managed/user/additional/project/plugin/mcp/legacy ?? source ??, plugin-only ??, source? inline shell trust boundary? SkillService/AppSettings/Settings UI? ???
- SlashCommandCatalog? ChatWindow?? builtin command? skill? ???? ???? ??? ?? ? ????? dedupe?? MCP ???? ? synthetic skill ?? ??? SkillGallery/AgentSettings? ???
- README.md? docs/DEVELOPMENT.md? 2026-04-14 19:13 (KST) ?? ?? ??? ?? ??? ???
- ??: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase4\\ -p:IntermediateOutputPath=obj\\verify_phase4\\ (?? 0, ?? 0)
- ??: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SkillServiceRuntimePolicyTests|SlashCommandCatalogTests|McpSkillCatalogTests" -p:OutputPath=bin\\verify_phase4_tests\\ -p:IntermediateOutputPath=obj\\verify_phase4_tests\\ (?? 17, ?? WorkspaceContextGeneratorTests.cs nullable ?? 1? ??)
This commit is contained in:
2026-04-14 19:15:12 +09:00
parent 3747a92c12
commit 946c31e275
17 changed files with 956 additions and 81 deletions

View File

@@ -0,0 +1,59 @@
using AxCopilot.Models;
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class McpSkillCatalogTests
{
[Fact]
public void BuildSyntheticSkills_ShouldCreateMcpScopedSkill_ForEnabledServer()
{
var skills = McpSkillCatalog.BuildSyntheticSkills(
[
new McpServerEntry
{
Name = "chrome-devtools",
Enabled = true,
Transport = "stdio",
Command = "npx"
},
new McpServerEntry
{
Name = "disabled-server",
Enabled = false
}
]);
skills.Should().ContainSingle();
var skill = skills[0];
skill.Name.Should().Be("mcp:chrome-devtools");
skill.SourceScope.Should().Be("mcp");
skill.Label.Should().Be("chrome-devtools MCP");
skill.AllowedTools.Should().Contain("mcp_list_resources");
skill.AllowedTools.Should().Contain("mcp_read_resource");
skill.SystemPrompt.Should().Contain("Use the MCP server");
}
[Fact]
public void ComputeSignature_ShouldReflectEnabledServerConfiguration()
{
var servers = new[]
{
new McpServerEntry
{
Name = "docs",
Enabled = true,
Transport = "sse",
Url = "https://intra.example.local/mcp/sse"
}
};
var signature = McpSkillCatalog.ComputeSignature(servers);
signature.Should().Contain("docs");
signature.Should().Contain("sse");
signature.Should().Contain("https://intra.example.local/mcp/sse");
}
}

View File

@@ -1,4 +1,5 @@
using AxCopilot.Services.Agent;
using AxCopilot.Models;
using FluentAssertions;
using Xunit;
using System;
@@ -303,4 +304,60 @@ public class SkillServiceRuntimePolicyTests
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
}
}
[Fact]
public void IsSkillSourceEnabled_ShouldRespectSourceFlags_AndPluginOnlyMode()
{
var llm = new LlmSettings
{
EnableManagedSkillSource = true,
EnableUserSkillSource = false,
EnableAdditionalSkillDiscovery = false,
EnableProjectSkillDiscovery = true,
EnablePluginSkillDiscovery = true,
EnableMcpSkillDiscovery = false,
EnableLegacyCommandSkills = false,
EnablePluginOnlySkillMode = false,
};
SkillService.IsSkillSourceEnabled("managed", llm).Should().BeTrue();
SkillService.IsSkillSourceEnabled("user", llm).Should().BeFalse();
SkillService.IsSkillSourceEnabled("additional", llm).Should().BeFalse();
SkillService.IsSkillSourceEnabled("project", llm).Should().BeTrue();
SkillService.IsSkillSourceEnabled("mcp", llm).Should().BeFalse();
SkillService.IsSkillSourceEnabled("legacy", llm).Should().BeFalse();
llm.EnablePluginOnlySkillMode = true;
SkillService.IsSkillSourceEnabled("managed", llm).Should().BeTrue();
SkillService.IsSkillSourceEnabled("plugin", llm).Should().BeTrue();
SkillService.IsSkillSourceEnabled("project", llm).Should().BeFalse();
SkillService.IsSkillSourceEnabled("mcp", llm).Should().BeFalse();
SkillService.IsSkillSourceEnabled("bundled", llm).Should().BeTrue();
}
[Fact]
public void GetInlineShellBlockedReason_ShouldRespectSourceTrustBoundaries()
{
var llm = new LlmSettings
{
EnableSkillInlineShell = true,
AllowPluginSkillInlineShell = false,
AllowMcpSkillInlineShell = false,
};
var pluginSkill = new SkillDefinition { Name = "plugin-a", SourceScope = "plugin" };
var mcpSkill = new SkillDefinition { Name = "mcp-a", SourceScope = "mcp" };
var projectSkill = new SkillDefinition { Name = "project-a", SourceScope = "project" };
SkillService.GetInlineShellBlockedReason(pluginSkill, llm).Should().Be("[inline-shell blocked for plugin skills]");
SkillService.GetInlineShellBlockedReason(mcpSkill, llm).Should().Be("[inline-shell blocked for mcp skills]");
SkillService.GetInlineShellBlockedReason(projectSkill, llm).Should().BeNull();
llm.AllowPluginSkillInlineShell = true;
llm.AllowMcpSkillInlineShell = true;
SkillService.GetInlineShellBlockedReason(pluginSkill, llm).Should().BeNull();
SkillService.GetInlineShellBlockedReason(mcpSkill, llm).Should().BeNull();
}
}

View File

@@ -28,4 +28,25 @@ public class SlashCommandCatalogTests
SlashCommandCatalog.TryGetEntry("/mcp", out var mcpEntry).Should().BeTrue();
mcpEntry.SystemPrompt.Should().Be("__MCP__");
}
[Fact]
public void ComposeMatches_ShouldPreferHigherPriorityEntry_WhenCommandCollides()
{
var matches = SlashCommandCatalog.ComposeMatches(
[
("/review", "Project Review Skill", true, 900),
("/review", "Code Review", false, 2100),
("/verify-change", "Verify Skill", true, 600)
]);
matches.Should().ContainSingle(x => x.Cmd == "/review");
matches.Should().Contain(x => x.Cmd == "/review" && x.IsSkill == false && x.Label == "Code Review");
matches.Should().Contain(x => x.Cmd == "/verify-change" && x.IsSkill);
}
[Fact]
public void GetBuiltInCommandPriority_ShouldGiveDevCommandsHigherPriority()
{
SlashCommandCatalog.GetBuiltInCommandPriority("/review").Should().BeGreaterThan(SlashCommandCatalog.GetBuiltInCommandPriority("/clear"));
}
}