Some checks failed
Release Gate / gate (push) Has been cancelled
- PermissionModeCatalogTests 추가: 글로벌/도구 정규화, 승인 필요 정책, 한국어 표시 라벨 검증 - PermissionModePresentationCatalogTests 추가: 권한 표면 순서와 unknown fallback(Default) 검증 - SlashCommandCatalogTests 추가: dev 전용 명령 필터링과 /compact,/permissions,/mcp 핵심 명령 등록 검증 - OperationModePolicyTests 보강: deny 패턴이 allow 패턴보다 우선되는 충돌 케이스 추가 - README.md, docs/DEVELOPMENT.md에 2026-04-04 13:40(KST) 기준 이력 반영
194 lines
6.6 KiB
C#
194 lines
6.6 KiB
C#
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<string, string>(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<string, string>(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<string, string>(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();
|
|
}
|
|
}
|