using AxCopilot.Models; using AxCopilot.Services; using AxCopilot.Services.Agent; using FluentAssertions; using Xunit; namespace AxCopilot.Tests.Services; public class OperationModePolicyTests { [Fact] public void Normalize_DefaultsToInternalWhenMissingOrUnknown() { OperationModePolicy.Normalize(null).Should().Be(OperationModePolicy.InternalMode); OperationModePolicy.Normalize("").Should().Be(OperationModePolicy.InternalMode); OperationModePolicy.Normalize("unknown").Should().Be(OperationModePolicy.InternalMode); } [Fact] public void Normalize_MapsExternalMode() { OperationModePolicy.Normalize("external").Should().Be(OperationModePolicy.ExternalMode); OperationModePolicy.Normalize("EXTERNAL").Should().Be(OperationModePolicy.ExternalMode); } [Fact] public void IsBlockedAgentToolInInternalMode_BlocksExternalHttpAndUrlOpen() { OperationModePolicy.IsBlockedAgentToolInInternalMode("http_tool", "https://example.com").Should().BeTrue(); OperationModePolicy.IsBlockedAgentToolInInternalMode("open_external", "https://example.com").Should().BeTrue(); OperationModePolicy.IsBlockedAgentToolInInternalMode("open_external", @"E:\work\report.html").Should().BeFalse(); } [Fact] public async Task AgentContext_CheckToolPermissionAsync_BlocksRestrictedToolsInInternalMode() { var context = new AgentContext { OperationMode = OperationModePolicy.InternalMode, Permission = "AcceptEdits" }; var blocked = await context.CheckToolPermissionAsync("http_tool", "https://example.com"); var allowed = await context.CheckToolPermissionAsync("file_read", @"E:\work\a.txt"); blocked.Should().BeFalse(); allowed.Should().BeTrue(); } [Fact] public async Task AgentContext_CheckToolPermissionAsync_AllowsRestrictedToolsInExternalMode() { var context = new AgentContext { OperationMode = OperationModePolicy.ExternalMode, Permission = "AcceptEdits" }; var allowed = await context.CheckToolPermissionAsync("http_tool", "https://example.com"); allowed.Should().BeTrue(); } [Fact] public void AgentContext_GetEffectiveToolPermission_PrefersPatternRule() { var context = new AgentContext { Permission = "Default", ToolPermissions = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["process"] = "deny", ["process@git *"] = "acceptedits", ["*@*.md"] = "default", } }; context.GetEffectiveToolPermission("process", "git status").Should().Be("Default"); context.GetEffectiveToolPermission("process", "powershell -NoProfile").Should().Be("Deny"); context.GetEffectiveToolPermission("file_read", @"E:\work\README.md").Should().Be("AcceptEdits"); } [Fact] public async Task AgentContext_CheckToolPermissionAsync_UsesPatternRuleWithoutPrompt() { var askCalled = false; var context = new AgentContext { OperationMode = OperationModePolicy.ExternalMode, Permission = "Default", ToolPermissions = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["process@git *"] = "bypassPermissions", }, AskPermission = (_, _) => { askCalled = true; return Task.FromResult(false); } }; var allowed = await context.CheckToolPermissionAsync("process", "git status"); allowed.Should().BeTrue(); askCalled.Should().BeFalse(); } [Fact] public void AgentContext_GetEffectiveToolPermission_DenyPatternPrecedesAllowPattern() { var context = new AgentContext { Permission = "AcceptEdits", ToolPermissions = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["process@git *"] = "acceptedits", ["process@git push *"] = "deny", } }; context.GetEffectiveToolPermission("process", "git status").Should().Be("Default"); context.GetEffectiveToolPermission("process", "git push origin main").Should().Be("Deny"); } [Fact] public void AgentContext_GetEffectiveToolPermission_AcceptEditsAllowsWriteButKeepsProcessPrompted() { var context = new AgentContext { Permission = "AcceptEdits" }; context.GetEffectiveToolPermission("process", "git status").Should().Be("Default"); context.GetEffectiveToolPermission("file_write", @"E:\work\out.txt").Should().Be("AcceptEdits"); } [Fact] public async Task AgentContext_CheckToolPermissionAsync_PlanModeBlocksWriteButAllowsRead() { var askCalled = false; var context = new AgentContext { OperationMode = OperationModePolicy.ExternalMode, Permission = "Plan", AskPermission = (_, _) => { askCalled = true; return Task.FromResult(true); } }; var writeAllowed = await context.CheckToolPermissionAsync("file_write", @"E:\work\out.txt"); var readAllowed = await context.CheckToolPermissionAsync("file_read", @"E:\work\in.txt"); writeAllowed.Should().BeFalse(); readAllowed.Should().BeTrue(); askCalled.Should().BeFalse(); } [Fact] public async Task AgentContext_CheckToolPermissionAsync_BypassPermissionsSkipsPrompt() { var askCalled = false; var context = new AgentContext { OperationMode = OperationModePolicy.ExternalMode, Permission = "BypassPermissions", AskPermission = (_, _) => { askCalled = true; return Task.FromResult(false); } }; var allowed = await context.CheckToolPermissionAsync("process", "git status"); allowed.Should().BeTrue(); askCalled.Should().BeFalse(); } [Fact] public async Task AgentContext_CheckToolPermissionAsync_DenyModeBlocksWriteButAllowsRead() { var context = new AgentContext { OperationMode = OperationModePolicy.ExternalMode, Permission = "Deny" }; var writeAllowed = await context.CheckToolPermissionAsync("file_write", @"E:\work\out.txt"); var readAllowed = await context.CheckToolPermissionAsync("file_read", @"E:\work\in.txt"); writeAllowed.Should().BeFalse(); readAllowed.Should().BeTrue(); } }