- 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\\
377 lines
12 KiB
C#
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]);
|
|
}
|
|
}
|