런처 워크스페이스 복원 매칭과 ~restore 명령을 보강

변경 목적:
- 런처 ~ 예약어의 창 복원 품질을 점검하고, 같은 exe의 첫 창만 반복 재사용되어 브라우저/다중 창 배치가 꼬이던 문제를 줄입니다.
- 도움말에만 있던 ~restore, ~list 명령을 실제 핸들러 동작과 맞춥니다.

핵심 수정사항:
- ContextManager가 열린 창 후보를 exe + 제목 유사도 기준으로 매칭하고, 이미 다른 스냅샷에 배정된 핸들은 재사용하지 않도록 변경했습니다.
- WorkspaceHandler에 ~restore <이름>, ~list 지원과 최근 저장 순 프로필 목록 복원 액션 정리를 추가했습니다.
- WorkspaceHandlerTests, ContextManagerTests를 추가해 명령 파싱과 창 매칭 우선순위를 회귀 검증합니다.
- README.md, docs/DEVELOPMENT.md에 검토 결과와 검증 이력을 기록했습니다.

검증 결과:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_workspace_restore_review\\ -p:IntermediateOutputPath=obj\\verify_workspace_restore_review\\
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter WorkspaceHandlerTests^|ContextManagerTests -p:OutputPath=bin\\verify_workspace_restore_review_tests\\ -p:IntermediateOutputPath=obj\\verify_workspace_restore_review_tests\\
- 경고 0 / 오류 0, 테스트 6건 통과
This commit is contained in:
2026-04-15 16:54:06 +09:00
parent 78b3aa2801
commit 964e40718f
6 changed files with 347 additions and 29 deletions

View File

@@ -0,0 +1,65 @@
using AxCopilot.Core;
using AxCopilot.Models;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Core;
public class ContextManagerTests
{
[Fact]
public void NormalizeWindowTitle_StripsKnownBrowserSuffix()
{
var normalized = ContextManager.NormalizeWindowTitle("Inbox - Google Chrome");
normalized.Should().Be("inbox");
}
[Fact]
public void CalculateTitleMatchScore_ExactTitle_BeatsPartialTitle()
{
var exact = ContextManager.CalculateTitleMatchScore("Inbox - Google Chrome", "Inbox - Google Chrome");
var partial = ContextManager.CalculateTitleMatchScore("Inbox - Google Chrome", "Inbox - Chrome");
exact.Should().BeGreaterThan(partial);
}
[Fact]
public void SelectBestMatchingWindow_PrefersExactTitleAmongSameExe()
{
var snapshot = new WindowSnapshot
{
Exe = @"C:\Program Files\Google\Chrome\Application\chrome.exe",
Title = "Inbox - Google Chrome"
};
var candidates = new[]
{
new ContextManager.WindowCandidate(new IntPtr(1), snapshot.Exe, "Docs - Google Chrome"),
new ContextManager.WindowCandidate(new IntPtr(2), snapshot.Exe, "Inbox - Google Chrome")
};
var selected = ContextManager.SelectBestMatchingWindow(snapshot, candidates);
selected.Should().Be(new IntPtr(2));
}
[Fact]
public void SelectBestMatchingWindow_SkipsAlreadyUsedHandle()
{
var snapshot = new WindowSnapshot
{
Exe = @"C:\Program Files\Google\Chrome\Application\chrome.exe",
Title = "Inbox - Google Chrome"
};
var candidates = new[]
{
new ContextManager.WindowCandidate(new IntPtr(1), snapshot.Exe, "Inbox - Google Chrome")
};
var selected = ContextManager.SelectBestMatchingWindow(snapshot, candidates, new HashSet<IntPtr> { new(1) });
selected.Should().Be(IntPtr.Zero);
}
}

View File

@@ -0,0 +1,64 @@
using AxCopilot.Core;
using AxCopilot.Handlers;
using AxCopilot.Models;
using AxCopilot.Services;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Handlers;
public class WorkspaceHandlerTests
{
[Fact]
public async Task GetItemsAsync_RestoreSubcommand_ReturnsRestoreAction()
{
var settings = new SettingsService();
settings.Settings.Profiles.Add(new WorkspaceProfile
{
Name = "업무",
Windows = new List<WindowSnapshot> { new() { Exe = "chrome.exe", Title = "메일" } },
CreatedAt = new DateTime(2026, 4, 15, 9, 0, 0)
});
var handler = new WorkspaceHandler(new ContextManager(settings), settings);
var items = (await handler.GetItemsAsync("restore 업무", CancellationToken.None)).ToList();
items.Should().ContainSingle();
items[0].Title.Should().Contain("업무");
items[0].Data.Should().BeOfType<WorkspaceAction>();
var action = (WorkspaceAction)items[0].Data!;
action.Type.Should().Be(WorkspaceActionType.Restore);
action.Name.Should().Be("업무");
}
[Fact]
public async Task GetItemsAsync_ListSubcommand_ReturnsSavedProfiles()
{
var settings = new SettingsService();
settings.Settings.Profiles.Add(new WorkspaceProfile
{
Name = "업무",
Windows = new List<WindowSnapshot> { new() { Exe = "chrome.exe", Title = "메일" } },
CreatedAt = new DateTime(2026, 4, 15, 9, 0, 0)
});
settings.Settings.Profiles.Add(new WorkspaceProfile
{
Name = "개발",
Windows = new List<WindowSnapshot> { new() { Exe = "code.exe", Title = "AX Copilot" } },
CreatedAt = new DateTime(2026, 4, 15, 10, 0, 0)
});
var handler = new WorkspaceHandler(new ContextManager(settings), settings);
var items = (await handler.GetItemsAsync("list", CancellationToken.None)).ToList();
items.Should().HaveCount(2);
items[0].Title.Should().Be("~개발");
items[1].Title.Should().Be("~업무");
items.All(item => item.Data is WorkspaceAction action && action.Type == WorkspaceActionType.Restore)
.Should()
.BeTrue();
}
}