From 2e945e36d560955eca6c18035f89073c7374f936 Mon Sep 17 00:00:00 2001 From: lacvet Date: Sat, 4 Apr 2026 16:25:41 +0900 Subject: [PATCH] =?UTF-8?q?=EC=82=AC=EB=82=B4/=EC=82=AC=EC=99=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=20=ED=9A=8C=EA=B7=80=20=EB=B3=B4=EA=B0=95:?= =?UTF-8?q?=20=EC=A0=84=ED=99=98=20=EC=A6=89=EC=8B=9C=EB=B0=98=EC=98=81=20?= =?UTF-8?q?=EB=B0=8F=20URL=20=EA=B2=BD=EA=B3=84=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OperationModePolicyTests에 IsExternalUrl 경계 케이스(http/https vs file/mailto) 추가 - OperationModeReadinessTests 정비 및 internal→external 전환 즉시반영 테스트 추가 - README/DEVELOPMENT 이력(2026-04-04 16:24 KST) 동기화 --- README.md | 3 +- docs/DEVELOPMENT.md | 20 +++++ .../Services/OperationModePolicyTests.cs | 10 +++ .../Services/OperationModeReadinessTests.cs | 90 +++++++++++++++++++ 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs diff --git a/README.md b/README.md index d83f06c..dd4a2b9 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,7 @@ public class MyHandler : IActionHandler ### v0.7.3 — AX Agent 권한 코어 재구성 + 입력 계층 정리 -업데이트: 2026-04-04 16:18 (KST) +업데이트: 2026-04-04 16:24 (KST) | 분류 | 내용 | |------|------| @@ -293,6 +293,7 @@ public class MyHandler : IActionHandler | 권한 팝업 즉시반영 정렬 | 권한 팝업에 `활용하지 않음`을 핵심 영역 맨 위에 배치하고, 대화 권한이 없을 때도 탭 기본값(Deny/DefaultAgentPermission)을 즉시 반영하도록 로딩 경로를 보강 | | 권한 색상 체계 통일 | 권한 요약 카드/상단 배너에서 모드별 색상(Deny=녹색, Passive=파랑, Active=녹색, Plan=보라, FullAuto=주황, Silent=빨강)을 팝업 체계와 일치시킴 | | 슬래시 네비게이션 입력 보강 | InputBox 포커스 상태에서도 방향키/Page/Home/End/Tab이 슬래시 목록 탐색에 즉시 반영되도록 키 처리 경로를 통합하고, 모든 그룹 접힘 상태에서 휠 스크롤 fallback을 추가 | +| 사내/사외 모드 회귀 보강 | operationMode 전환 직후 WebSearch 동작 반영과 URL 판별 경계(HTTP/파일/mailto) 테스트를 추가해 내부 차단 정책의 즉시성/정확성을 강화 | | 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 7a16cab..975d769 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -3671,3 +3671,23 @@ else: ### 4) 품질 게이트 - `dotnet build src/AxCopilot/AxCopilot.csproj -c Debug -p:UseSharedCompilation=false -nodeReuse:false` 통과 (경고 0, 오류 0). - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -p:UseSharedCompilation=false -nodeReuse:false --filter "FullyQualifiedName~ChatWindowSlashPolicyTests|FullyQualifiedName~PermissionModeCatalogTests|FullyQualifiedName~ChatSessionStateServiceTests"` 통과 (109 passed, 0 failed). + +## 2026-04-04 추가 진행 기록 (연속 실행 43차: 사내/사외 모드 회귀 보강) + +업데이트: 2026-04-04 16:24 (KST) + +### 1) 정책 경계 테스트 보강 +- `OperationModePolicyTests`에 `IsExternalUrl_DetectsOnlyHttpSchemes` 추가. +- 검증 범위: + - `http/https`는 외부 URL로 판정 + - 로컬 파일 경로, `mailto`, 일반 문자열은 외부 URL이 아님 + +### 2) 모드 전환 즉시 반영 테스트 추가 +- `OperationModeReadinessTests`를 정리/보강: + - `WebSearch_ModeSwitch_InternalToExternal_ShouldReflectImmediately` 추가 + - 같은 핸들러 인스턴스에서 설정값 전환 후 결과가 즉시 `차단`→`검색`으로 반영되는지 검증 +- 기존 readiness 테스트의 문자열 비교도 정상 한글 기준으로 정비. + +### 3) 품질 게이트 +- `dotnet build src/AxCopilot/AxCopilot.csproj -c Debug -p:UseSharedCompilation=false -nodeReuse:false` 통과 (경고 0, 오류 0). +- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -p:UseSharedCompilation=false -nodeReuse:false --filter "FullyQualifiedName~OperationModePolicyTests|FullyQualifiedName~OperationModeReadinessTests"` 통과 (20 passed, 0 failed). diff --git a/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs b/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs index 64a7c8a..7c96295 100644 --- a/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs +++ b/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs @@ -31,6 +31,16 @@ public class OperationModePolicyTests OperationModePolicy.IsBlockedAgentToolInInternalMode("open_external", @"E:\work\report.html").Should().BeFalse(); } + [Fact] + public void IsExternalUrl_DetectsOnlyHttpSchemes() + { + OperationModePolicy.IsExternalUrl("https://example.com").Should().BeTrue(); + OperationModePolicy.IsExternalUrl("http://intranet.local").Should().BeTrue(); + OperationModePolicy.IsExternalUrl(@"E:\work\report.html").Should().BeFalse(); + OperationModePolicy.IsExternalUrl("mailto:admin@example.com").Should().BeFalse(); + OperationModePolicy.IsExternalUrl("not-a-url").Should().BeFalse(); + } + [Fact] public async Task AgentContext_CheckToolPermissionAsync_BlocksRestrictedToolsInInternalMode() { diff --git a/src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs b/src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs new file mode 100644 index 0000000..f6d132c --- /dev/null +++ b/src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs @@ -0,0 +1,90 @@ +using System.Text.Json; +using AxCopilot.Handlers; +using AxCopilot.Services; +using AxCopilot.Services.Agent; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class OperationModeReadinessTests +{ + [Fact] + public async Task WebSearch_InternalMode_ShowsBlockedMessage() + { + var settings = new SettingsService(); + settings.Settings.OperationMode = OperationModePolicy.InternalMode; + var handler = new WebSearchHandler(settings); + + var items = (await handler.GetItemsAsync("ax copilot", CancellationToken.None)).ToList(); + + items.Should().NotBeEmpty(); + items[0].Title.Should().Contain("차단"); + } + + [Fact] + public async Task WebSearch_ExternalMode_ReturnsSearchItems() + { + var settings = new SettingsService(); + settings.Settings.OperationMode = OperationModePolicy.ExternalMode; + var handler = new WebSearchHandler(settings); + + var items = (await handler.GetItemsAsync("ax copilot", CancellationToken.None)).ToList(); + + items.Should().NotBeEmpty(); + items.Any(i => i.Title.Contains("검색")).Should().BeTrue(); + items.Any(i => i.Title.Contains("차단")).Should().BeFalse(); + } + + [Fact] + public async Task WebSearch_ModeSwitch_InternalToExternal_ShouldReflectImmediately() + { + var settings = new SettingsService(); + var handler = new WebSearchHandler(settings); + + settings.Settings.OperationMode = OperationModePolicy.InternalMode; + var internalItems = (await handler.GetItemsAsync("ax copilot", CancellationToken.None)).ToList(); + + settings.Settings.OperationMode = OperationModePolicy.ExternalMode; + var externalItems = (await handler.GetItemsAsync("ax copilot", CancellationToken.None)).ToList(); + + internalItems.Should().NotBeEmpty(); + externalItems.Should().NotBeEmpty(); + internalItems[0].Title.Should().Contain("차단"); + externalItems.Any(i => i.Title.Contains("검색")).Should().BeTrue(); + } + + [Fact] + public async Task HttpTool_InternalMode_IsBlockedEvenWhenCalledDirectly() + { + var tool = new HttpTool(); + using var doc = JsonDocument.Parse("""{"method":"GET","url":"http://127.0.0.1:8080/health"}"""); + var context = new AgentContext + { + OperationMode = OperationModePolicy.InternalMode, + Permission = "Auto" + }; + + var result = await tool.ExecuteAsync(doc.RootElement, context, CancellationToken.None); + + result.Success.Should().BeFalse(); + result.Output.Should().Contain("사내모드"); + } + + [Fact] + public async Task OpenExternal_InternalMode_BlocksExternalUrl() + { + var tool = new OpenExternalTool(); + using var doc = JsonDocument.Parse("""{"path":"https://example.com"}"""); + var context = new AgentContext + { + OperationMode = OperationModePolicy.InternalMode, + Permission = "Auto" + }; + + var result = await tool.ExecuteAsync(doc.RootElement, context, CancellationToken.None); + + result.Success.Should().BeFalse(); + result.Output.Should().Contain("사내모드"); + } +}