사내/사외 모드 회귀 보강: 전환 즉시반영 및 URL 경계 테스트 추가
Some checks failed
Release Gate / gate (push) Has been cancelled

- OperationModePolicyTests에 IsExternalUrl 경계 케이스(http/https vs file/mailto) 추가

- OperationModeReadinessTests 정비 및 internal→external 전환 즉시반영 테스트 추가

- README/DEVELOPMENT 이력(2026-04-04 16:24 KST) 동기화
This commit is contained in:
2026-04-04 16:25:41 +09:00
parent 3b9938e01b
commit 2e945e36d5
4 changed files with 122 additions and 1 deletions

View File

@@ -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 |

View File

@@ -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).

View File

@@ -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()
{

View File

@@ -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("사내모드");
}
}