From b1fa8f692af36ecde9874233abd9e5c87fc00be7 Mon Sep 17 00:00:00 2001 From: lacvet Date: Sat, 4 Apr 2026 13:40:58 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=9A=8C?= =?UTF-8?q?=EA=B7=80=EB=A7=9D=20=EA=B0=95=ED=99=94:=20=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=20=EB=AA=A8=EB=93=9C/=EC=8A=AC=EB=9E=98=EC=8B=9C=20=EC=B9=B4?= =?UTF-8?q?=ED=83=88=EB=A1=9C=EA=B7=B8=20L4=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PermissionModeCatalogTests 추가: 글로벌/도구 정규화, 승인 필요 정책, 한국어 표시 라벨 검증 - PermissionModePresentationCatalogTests 추가: 권한 표면 순서와 unknown fallback(Default) 검증 - SlashCommandCatalogTests 추가: dev 전용 명령 필터링과 /compact,/permissions,/mcp 핵심 명령 등록 검증 - OperationModePolicyTests 보강: deny 패턴이 allow 패턴보다 우선되는 충돌 케이스 추가 - README.md, docs/DEVELOPMENT.md에 2026-04-04 13:40(KST) 기준 이력 반영 --- README.md | 3 +- docs/DEVELOPMENT.md | 26 ++++++++ .../Services/OperationModePolicyTests.cs | 17 ++++++ .../Services/PermissionModeCatalogTests.cs | 61 +++++++++++++++++++ .../PermissionModePresentationCatalogTests.cs | 29 +++++++++ .../Views/SlashCommandCatalogTests.cs | 31 ++++++++++ 6 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 src/AxCopilot.Tests/Services/PermissionModeCatalogTests.cs create mode 100644 src/AxCopilot.Tests/Services/PermissionModePresentationCatalogTests.cs create mode 100644 src/AxCopilot.Tests/Views/SlashCommandCatalogTests.cs diff --git a/README.md b/README.md index 68b710b..d64372a 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,7 @@ public class MyHandler : IActionHandler ### v0.7.3 — AX Agent 권한 코어 재구성 + 입력 계층 정리 -업데이트: 2026-04-04 13:32 (KST) +업데이트: 2026-04-04 13:40 (KST) | 분류 | 내용 | |------|------| @@ -279,6 +279,7 @@ public class MyHandler : IActionHandler | slash 조회 API 전환 | 내장 slash 매칭/조회 경로를 `SlashCommandCatalog.MatchBuiltinCommands`/`TryGetEntry`로 통일 | | 권한 표시 카탈로그 분리 | 권한 모드 라벨/설명/아이콘/색을 `PermissionModePresentationCatalog`로 분리해 팝업 표면 기준을 단일화 | | 탭별 설정 해석기 도입 | `AgentTabSettingsResolver`를 추가해 Cowork/Code 분기(검증 활성/Code 전용 도구 비활성)를 단일 경로로 정리 | +| L4 통합 회귀 보강 | `PermissionModeCatalogTests`/`PermissionModePresentationCatalogTests`/`SlashCommandCatalogTests`를 추가하고 deny 우선 규칙을 `OperationModePolicyTests`에 반영해 권한·슬래시 회귀망을 강화 | | Slash palette 상태 분리 시작 | `ChatWindow`에 몰려 있던 slash 상태를 `SlashPaletteState`로 분리해 이후 Codex/Claude형 composer 개편 기반 마련 | | 런처 이미지 미리보기 추가 | `#` 클립보드 이미지 항목에서 `Shift+Enter`로 전용 미리보기 창을 열고, 줌·원본 해상도 확인·PNG/JPEG/BMP 저장·클립보드 복사를 지원 | | 검증 | `dotnet build` 경고 0 / 오류 0, `dotnet test` 436 passed / 0 failed | diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 3ed3bd6..0529c7b 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -3367,3 +3367,29 @@ else: ### 4) 품질 게이트 - dotnet build src/AxCopilot/AxCopilot.csproj -p:UseSharedCompilation=false -nodeReuse:false 통과 (경고 0, 오류 0). - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj --filter "FullyQualifiedName~AgentTabSettingsResolverTests|FullyQualifiedName~ChatWindowSlashPolicyTests|FullyQualifiedName~OperationModeReadinessTests" 통과 (49 passed, 0 failed). + +## 2026-04-04 추가 진행 기록 (연속 실행 29차: 권한/슬래시 L4 통합 회귀 보강) + +업데이트: 2026-04-04 13:40 (KST) + +### 1) 권한 모드 카탈로그 테스트 추가 +- 신규 `PermissionModeCatalogTests`: + - 글로벌/도구 권한 정규화 매핑 검증 + - 사용자 승인 필요 여부 정책 검증 + - 권한 모드 표시 라벨(활용하지 않음/소극 활용/적극 활용/계획 중심/완전 자동/질문 없이 진행) 검증 +- 신규 `PermissionModePresentationCatalogTests`: + - 권한 표면 순서(Deny→Default→AcceptEdits→Plan→BypassPermissions→DontAsk) 검증 + - 미정의 모드 fallback이 `Default`로 수렴하는지 검증 + +### 2) 권한 규칙 우선순위 회귀 보강 +- `OperationModePolicyTests`에 deny/allow 패턴 충돌 케이스 추가: + - `process@git push * = deny`가 `process@git * = acceptedits`보다 우선 적용되는지 검증 + +### 3) slash 카탈로그 회귀 보강 +- 신규 `SlashCommandCatalogTests`: + - Chat 탭에서 dev 전용 명령(`/review`)이 숨겨지는지 검증 + - 핵심 parity 명령(`/compact`, `/permissions`, `/mcp`)의 카탈로그 등록 검증 + +### 4) 품질 게이트 +- `dotnet build src/AxCopilot/AxCopilot.csproj -c Debug -p:UseSharedCompilation=false -nodeReuse:false` 통과 (경고 0, 오류 0). +- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj --filter "FullyQualifiedName~PermissionModeCatalogTests|FullyQualifiedName~PermissionModePresentationCatalogTests|FullyQualifiedName~SlashCommandCatalogTests|FullyQualifiedName~OperationModePolicyTests|FullyQualifiedName~OperationModeReadinessTests|FullyQualifiedName~ChatWindowSlashPolicyTests"` 통과 (88 passed, 0 failed). diff --git a/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs b/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs index 3ec2a9c..cacc962 100644 --- a/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs +++ b/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs @@ -103,6 +103,23 @@ public class OperationModePolicyTests 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() { diff --git a/src/AxCopilot.Tests/Services/PermissionModeCatalogTests.cs b/src/AxCopilot.Tests/Services/PermissionModeCatalogTests.cs new file mode 100644 index 0000000..a21d382 --- /dev/null +++ b/src/AxCopilot.Tests/Services/PermissionModeCatalogTests.cs @@ -0,0 +1,61 @@ +using AxCopilot.Services.Agent; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class PermissionModeCatalogTests +{ + [Theory] + [InlineData(null, PermissionModeCatalog.Default)] + [InlineData("", PermissionModeCatalog.Default)] + [InlineData("ask", PermissionModeCatalog.Default)] + [InlineData("auto", PermissionModeCatalog.AcceptEdits)] + [InlineData("accept", PermissionModeCatalog.AcceptEdits)] + [InlineData("plan", PermissionModeCatalog.Plan)] + [InlineData("bypass", PermissionModeCatalog.BypassPermissions)] + [InlineData("dontask", PermissionModeCatalog.DontAsk)] + [InlineData("deny", PermissionModeCatalog.Deny)] + [InlineData("unknown", PermissionModeCatalog.Default)] + public void NormalizeGlobalMode_ShouldMapExpectedModes(string? input, string expected) + { + PermissionModeCatalog.NormalizeGlobalMode(input).Should().Be(expected); + } + + [Theory] + [InlineData("ask", PermissionModeCatalog.Default)] + [InlineData("auto", PermissionModeCatalog.AcceptEdits)] + [InlineData("plan", PermissionModeCatalog.Plan)] + [InlineData("bypass", PermissionModeCatalog.BypassPermissions)] + [InlineData("dontask", PermissionModeCatalog.DontAsk)] + [InlineData("deny", PermissionModeCatalog.Deny)] + [InlineData("unknown", PermissionModeCatalog.Default)] + public void NormalizeToolOverride_ShouldMapExpectedModes(string? input, string expected) + { + PermissionModeCatalog.NormalizeToolOverride(input).Should().Be(expected); + } + + [Theory] + [InlineData("ask", true)] + [InlineData("auto", false)] + [InlineData("acceptedits", false)] + [InlineData("bypassPermissions", false)] + [InlineData("dontAsk", false)] + [InlineData("deny", false)] + public void RequiresUserApproval_ShouldMatchPolicy(string? input, bool expected) + { + PermissionModeCatalog.RequiresUserApproval(input).Should().Be(expected); + } + + [Theory] + [InlineData(PermissionModeCatalog.Deny, "활용하지 않음")] + [InlineData(PermissionModeCatalog.Default, "소극 활용")] + [InlineData(PermissionModeCatalog.AcceptEdits, "적극 활용")] + [InlineData(PermissionModeCatalog.Plan, "계획 중심")] + [InlineData(PermissionModeCatalog.BypassPermissions, "완전 자동")] + [InlineData(PermissionModeCatalog.DontAsk, "질문 없이 진행")] + public void ToDisplayLabel_ShouldReturnKoreanLabel(string mode, string expected) + { + PermissionModeCatalog.ToDisplayLabel(mode).Should().Be(expected); + } +} diff --git a/src/AxCopilot.Tests/Services/PermissionModePresentationCatalogTests.cs b/src/AxCopilot.Tests/Services/PermissionModePresentationCatalogTests.cs new file mode 100644 index 0000000..01df2f5 --- /dev/null +++ b/src/AxCopilot.Tests/Services/PermissionModePresentationCatalogTests.cs @@ -0,0 +1,29 @@ +using AxCopilot.Services.Agent; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class PermissionModePresentationCatalogTests +{ + [Fact] + public void Ordered_ShouldMatchExpectedModeOrder() + { + PermissionModePresentationCatalog.Ordered.Select(x => x.Mode).Should().ContainInOrder( + [ + PermissionModeCatalog.Deny, + PermissionModeCatalog.Default, + PermissionModeCatalog.AcceptEdits, + PermissionModeCatalog.Plan, + PermissionModeCatalog.BypassPermissions, + PermissionModeCatalog.DontAsk, + ]); + } + + [Fact] + public void Resolve_ShouldFallbackToDefaultPresentation_OnUnknownMode() + { + var resolved = PermissionModePresentationCatalog.Resolve("unknown-mode"); + resolved.Mode.Should().Be(PermissionModeCatalog.Default); + } +} diff --git a/src/AxCopilot.Tests/Views/SlashCommandCatalogTests.cs b/src/AxCopilot.Tests/Views/SlashCommandCatalogTests.cs new file mode 100644 index 0000000..454f46f --- /dev/null +++ b/src/AxCopilot.Tests/Views/SlashCommandCatalogTests.cs @@ -0,0 +1,31 @@ +using AxCopilot.Views; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Views; + +public class SlashCommandCatalogTests +{ + [Fact] + public void MatchBuiltinCommands_ShouldFilterDevCommandsInChatTab() + { + var chatMatches = SlashCommandCatalog.MatchBuiltinCommands("/rev", isDevTab: false); + var devMatches = SlashCommandCatalog.MatchBuiltinCommands("/rev", isDevTab: true); + + chatMatches.Should().BeEmpty(); + devMatches.Should().ContainSingle(x => x.Cmd == "/review"); + } + + [Fact] + public void Catalog_ShouldContainCoreParityCommands() + { + SlashCommandCatalog.TryGetEntry("/compact", out var compactEntry).Should().BeTrue(); + compactEntry.SystemPrompt.Should().Be("__COMPACT__"); + + SlashCommandCatalog.TryGetEntry("/permissions", out var permissionEntry).Should().BeTrue(); + permissionEntry.SystemPrompt.Should().Be("__PERMISSIONS__"); + + SlashCommandCatalog.TryGetEntry("/mcp", out var mcpEntry).Should().BeTrue(); + mcpEntry.SystemPrompt.Should().Be("__MCP__"); + } +}