Files
AX-Copilot-Codex/src/AxCopilot.Tests/Services/SettingsServiceTests.cs
lacvet e07b6dbed0 코드 탭 컨텍스트 영속화와 Auto budget 복구 적용
- AxAgentExecutionEngine에서 시스템 프롬프트 중복을 제거하고 structured tool_use/tool_result 전사본을 conversation.Messages로 동기화해 다음 턴과 저장 이력에서도 코드 작업 컨텍스트가 유지되도록 수정
- AgentQueryContextBuilder와 ContextCondenser에 post-compact tool snippet 복원, recent window 확대, tool result 보존 강화 로직을 추가해 장기 코드 실행 중 빌드/파일 근거 손실을 줄임
- MaxContextTokens=0 Auto 모드를 AppSettings, SettingsService 마이그레이션, 설정 UI, 오버레이 UI, 컨텍스트 사용량 표시, LLM 요청 본문에 연결하고 Auto 모드에서는 provider output cap 강제 주입을 제거
- 관련 회귀 테스트와 문서 README/DEVELOPMENT/CODE_CONTEXT_RELIABILITY_PLAN을 갱신하고 깨진 진단 문자열 기대값을 영어 기준으로 정리

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_context_reliability_followup\\ -p:IntermediateOutputPath=obj\\verify_context_reliability_followup\\
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AxAgentExecutionEngineTests|AgentQueryContextBuilderTests|ContextCondenserTests|SettingsServiceTests|AgentLoopDiagnosticsFormatterTests" -p:OutputPath=bin\\verify_context_reliability_followup_tests\\ -p:IntermediateOutputPath=obj\\verify_context_reliability_followup_tests\\
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopQueryAssemblyServiceTests|AgentLoopPreLlmStageServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentMessageInvariantHelperTests|CodeTaskWorkingSetServiceTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_context_reliability_followup_tests2\\ -p:IntermediateOutputPath=obj\\verify_context_reliability_followup_tests2\\
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_context_reliability_final\\ -p:IntermediateOutputPath=obj\\verify_context_reliability_final\\
2026-04-16 07:45:36 +09:00

377 lines
12 KiB
C#

using System.Text.Json;
using FluentAssertions;
using AxCopilot.Models;
using Xunit;
namespace AxCopilot.Tests.Services;
public class SettingsServiceTests
{
// ─── AppSettings 기본값 ──────────────────────────────────────────────────
[Fact]
public void AppSettings_DefaultHotkey_IsAltSpace()
{
new AppSettings().Hotkey.Should().Be("Alt+Space");
}
[Fact]
public void LauncherSettings_DefaultMaxResults_IsSeven()
{
new LauncherSettings().MaxResults.Should().Be(7);
}
[Fact]
public void LauncherSettings_DefaultTheme_IsSystem()
{
new LauncherSettings().Theme.Should().Be("system");
}
[Fact]
public void LauncherSettings_DefaultOpacity_IsValid()
{
var opacity = new LauncherSettings().Opacity;
opacity.Should().BeInRange(0.0, 1.0);
}
[Fact]
public void LauncherSettings_DefaultBrowserSessionRestore_IsEnabled()
{
new LauncherSettings().EnableBrowserSessionRestore.Should().BeTrue();
}
[Fact]
public void LauncherSettings_DefaultAdaptiveWorkspaceRestoreThrottle_IsEnabled()
{
new LauncherSettings().EnableAdaptiveWorkspaceRestoreThrottle.Should().BeTrue();
}
[Fact]
public void LauncherSettings_DefaultWorkspaceRestoreDelayRange_IsValid()
{
var settings = new LauncherSettings();
settings.WorkspaceRestoreBaseDelayMs.Should().Be(250);
settings.WorkspaceRestoreMaxDelayMs.Should().BeGreaterOrEqualTo(settings.WorkspaceRestoreBaseDelayMs);
}
[Fact]
public void AppSettings_DefaultMonitorMismatch_IsWarn()
{
new AppSettings().MonitorMismatch.Should().Be("warn");
}
[Fact]
public void AppSettings_DefaultCleanupPeriodDays_IsThirty()
{
new AppSettings().CleanupPeriodDays.Should().Be(30);
}
[Fact]
public void AppSettings_DefaultIndexPaths_NotEmpty()
{
new AppSettings().IndexPaths.Should().NotBeEmpty();
}
[Fact]
public void LlmSettings_DefaultMaxContextTokens_IsAuto()
{
new LlmSettings().MaxContextTokens.Should().Be(0);
}
// ─── LauncherSettings 테마 ───────────────────────────────────────────────
[Theory]
[InlineData("system")]
[InlineData("dark")]
[InlineData("light")]
public void LauncherSettings_Theme_AcceptsValidValues(string theme)
{
var settings = new LauncherSettings { Theme = theme };
settings.Theme.Should().Be(theme);
}
// ─── JSON 직렬화 라운드트립 ──────────────────────────────────────────────
[Fact]
public void AppSettings_Serialization_PreservesHotkey()
{
var original = new AppSettings { Hotkey = "Ctrl+Space" };
var restored = RoundTrip(original);
restored.Hotkey.Should().Be("Ctrl+Space");
}
[Fact]
public void AppSettings_Serialization_PreservesTheme()
{
var original = new AppSettings
{
Launcher = new LauncherSettings { Theme = "dark" }
};
var restored = RoundTrip(original);
restored.Launcher.Theme.Should().Be("dark");
}
[Fact]
public void AppSettings_Serialization_PreservesMaxResults()
{
var original = new AppSettings
{
Launcher = new LauncherSettings { MaxResults = 15 }
};
var restored = RoundTrip(original);
restored.Launcher.MaxResults.Should().Be(15);
}
[Fact]
public void AppSettings_Serialization_PreservesAliases()
{
var original = new AppSettings
{
Aliases =
[
new() { Key = "@test", Type = "url", Target = "https://example.com" }
]
};
var restored = RoundTrip(original);
restored.Aliases.Should().HaveCount(1);
restored.Aliases[0].Key.Should().Be("@test");
restored.Aliases[0].Target.Should().Be("https://example.com");
}
[Fact]
public void AppSettings_Serialization_PreservesCleanupPeriodDays()
{
var original = new AppSettings { CleanupPeriodDays = 14 };
var restored = RoundTrip(original);
restored.CleanupPeriodDays.Should().Be(14);
}
// ─── AliasEntry ──────────────────────────────────────────────────────────
[Fact]
public void AliasEntry_DefaultShowWindow_IsFalse()
{
new AliasEntry().ShowWindow.Should().BeFalse();
}
[Theory]
[InlineData("url")]
[InlineData("folder")]
[InlineData("app")]
[InlineData("batch")]
[InlineData("api")]
[InlineData("clipboard")]
public void AliasEntry_Type_AcceptsAllTypes(string type)
{
var entry = new AliasEntry { Type = type };
entry.Type.Should().Be(type);
}
// ─── WorkspaceProfile / WindowSnapshot ───────────────────────────────────
[Fact]
public void WorkspaceProfile_DefaultWindows_IsEmpty()
{
new WorkspaceProfile().Windows.Should().BeEmpty();
}
[Fact]
public void WindowSnapshot_DefaultShowCmd_IsNormal()
{
new WindowSnapshot().ShowCmd.Should().Be("Normal");
}
[Fact]
public void WindowSnapshot_DefaultMonitor_IsZero()
{
new WindowSnapshot().Monitor.Should().Be(0);
}
[Fact]
public void WindowRect_DefaultValues_AreZero()
{
var rect = new WindowRect();
rect.X.Should().Be(0);
rect.Y.Should().Be(0);
rect.Width.Should().Be(0);
rect.Height.Should().Be(0);
}
[Fact]
public void WorkspaceProfile_Serialization_RoundTrip()
{
var profile = new WorkspaceProfile
{
Name = "작업 프로필",
Windows =
[
new() { Exe = "notepad.exe", ShowCmd = "Maximized", Monitor = 1 }
]
};
var json = JsonSerializer.Serialize(profile, JsonOptions);
var restored = JsonSerializer.Deserialize<WorkspaceProfile>(json, JsonOptions)!;
restored.Name.Should().Be("작업 프로필");
restored.Windows.Should().HaveCount(1);
restored.Windows[0].Exe.Should().Be("notepad.exe");
restored.Windows[0].ShowCmd.Should().Be("Maximized");
restored.Windows[0].Monitor.Should().Be(1);
}
[Fact]
public void WorkspaceProfile_Serialization_PreservesBrowserState()
{
var profile = new WorkspaceProfile
{
Name = "브라우저 프로필",
Windows =
[
new()
{
Exe = "msedge.exe",
Title = "업무 포털 - Microsoft Edge",
Browser = new BrowserWindowState
{
Kind = "edge",
UserDataDir = @"C:\Users\tester\AppData\Local\Microsoft\Edge\User Data",
ProfileDirectory = "Profile 3",
ActiveUrl = "https://portal.example.com",
ActiveTabIndex = 1,
TabUrls = ["https://mail.example.com", "https://portal.example.com"]
}
}
]
};
var json = JsonSerializer.Serialize(profile, JsonOptions);
var restored = JsonSerializer.Deserialize<WorkspaceProfile>(json, JsonOptions)!;
restored.Windows.Should().HaveCount(1);
restored.Windows[0].Browser.Should().NotBeNull();
restored.Windows[0].Browser!.Kind.Should().Be("edge");
restored.Windows[0].Browser!.ProfileDirectory.Should().Be("Profile 3");
restored.Windows[0].Browser!.ActiveTabIndex.Should().Be(1);
restored.Windows[0].Browser!.TabUrls.Should().ContainInOrder(
"https://mail.example.com",
"https://portal.example.com");
}
[Fact]
public void WorkspaceProfile_Serialization_PreservesAppState()
{
var profile = new WorkspaceProfile
{
Name = "앱 상태 프로필",
Windows =
[
new()
{
Exe = "explorer.exe",
Title = "문서 - 파일 탐색기",
AppState = new AppWindowState
{
Kind = "explorer",
PrimaryPath = @"C:\Docs",
ActivePathIndex = 0,
Paths = [@"C:\Docs", @"C:\Docs\Archive"]
}
}
]
};
var json = JsonSerializer.Serialize(profile, JsonOptions);
var restored = JsonSerializer.Deserialize<WorkspaceProfile>(json, JsonOptions)!;
restored.Windows.Should().HaveCount(1);
restored.Windows[0].AppState.Should().NotBeNull();
restored.Windows[0].AppState!.Kind.Should().Be("explorer");
restored.Windows[0].AppState!.PrimaryPath.Should().Be(@"C:\Docs");
restored.Windows[0].AppState!.Paths.Should().ContainInOrder(@"C:\Docs", @"C:\Docs\Archive");
}
// ─── ClipboardTransformer ────────────────────────────────────────────────
[Fact]
public void ClipboardTransformer_DefaultTimeout_IsFiveSeconds()
{
new ClipboardTransformer().Timeout.Should().Be(5000);
}
[Fact]
public void ClipboardTransformer_DefaultType_IsRegex()
{
new ClipboardTransformer().Type.Should().Be("regex");
}
[Fact]
public void NormalizeLlmThresholds_ClampsConfiguredValues()
{
var llm = new LlmSettings
{
ReadOnlySignatureLoopThreshold = 1,
ReadOnlyStagnationThreshold = 99,
NoProgressRecoveryThreshold = 2,
NoProgressAbortThreshold = 200,
NoProgressRecoveryMaxRetries = 9,
ToolExecutionTimeoutMs = 1000
};
InvokeNormalizeLlmThresholds(llm);
llm.ReadOnlySignatureLoopThreshold.Should().Be(2);
llm.ReadOnlyStagnationThreshold.Should().Be(20);
llm.NoProgressRecoveryThreshold.Should().Be(4);
llm.NoProgressAbortThreshold.Should().Be(50);
llm.NoProgressRecoveryMaxRetries.Should().Be(5);
llm.ToolExecutionTimeoutMs.Should().Be(5000);
}
[Fact]
public void NormalizeLlmThresholds_PreservesZeroAsUnset()
{
var llm = new LlmSettings
{
ReadOnlySignatureLoopThreshold = 0,
ReadOnlyStagnationThreshold = 0,
NoProgressRecoveryThreshold = 0,
NoProgressAbortThreshold = 0,
NoProgressRecoveryMaxRetries = 0,
ToolExecutionTimeoutMs = 0
};
InvokeNormalizeLlmThresholds(llm);
llm.ReadOnlySignatureLoopThreshold.Should().Be(0);
llm.ReadOnlyStagnationThreshold.Should().Be(0);
llm.NoProgressRecoveryThreshold.Should().Be(0);
llm.NoProgressAbortThreshold.Should().Be(0);
llm.NoProgressRecoveryMaxRetries.Should().Be(0);
llm.ToolExecutionTimeoutMs.Should().Be(0);
}
// ─── Helpers ─────────────────────────────────────────────────────────────
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = false,
PropertyNameCaseInsensitive = true
};
private static AppSettings RoundTrip(AppSettings original)
{
var json = JsonSerializer.Serialize(original, JsonOptions);
return JsonSerializer.Deserialize<AppSettings>(json, JsonOptions)!;
}
private static void InvokeNormalizeLlmThresholds(LlmSettings llm)
{
var method = typeof(AxCopilot.Services.SettingsService)
.GetMethod("NormalizeLlmThresholds", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
method.Should().NotBeNull();
method!.Invoke(null, [llm]);
}
}