using AxCopilot.Services.Agent; using FluentAssertions; using Xunit; namespace AxCopilot.Tests.Services; public class SessionLearningCollectorTests { // ═══════════════════════════════════════════ // 기본 동작 // ═══════════════════════════════════════════ [Fact] public void Empty_BuildInjectionMessage_ReturnsNull() { var collector = new SessionLearningCollector(); collector.BuildInjectionMessage().Should().BeNull(); } [Fact] public void Count_StartsAtZero() { var collector = new SessionLearningCollector(); collector.Count.Should().Be(0); } [Fact] public void Clear_ResetsCount() { var collector = new SessionLearningCollector(); collector.TryExtract("build_run", "error CS1234: Something failed\nMSBuild error", false); collector.Count.Should().BeGreaterThan(0); collector.Clear(); collector.Count.Should().Be(0); collector.BuildInjectionMessage().Should().BeNull(); } // ═══════════════════════════════════════════ // 빌드/테스트 실패 추출 // ═══════════════════════════════════════════ [Fact] public void TryExtract_BuildRunFailure_ExtractsBuildConfig() { var collector = new SessionLearningCollector(); var output = """ Build FAILED. error CS0246: The type or namespace name 'Foo' could not be found TargetFramework: net8.0-windows10.0.17763 MSBuild version 17.8.0 """; collector.TryExtract("build_run", output, success: false); collector.Count.Should().Be(1); var msg = collector.BuildInjectionMessage(); msg.Should().NotBeNull(); msg.Should().Contain("[build_config]"); msg.Should().Contain("CS0246"); } [Fact] public void TryExtract_TestLoopFailure_ExtractsBuildConfig() { var collector = new SessionLearningCollector(); collector.TryExtract("test_loop", "error TS2304: Cannot find name 'x'", success: false); collector.Count.Should().Be(1); } // ═══════════════════════════════════════════ // grep/glob 결과 추출 // ═══════════════════════════════════════════ [Fact] public void TryExtract_GrepSuccess_ExtractsCodeLocation() { var collector = new SessionLearningCollector(); var output = """ src/Services/Agent/IntentGateService.cs:10: something src/Services/Agent/SessionLearning.cs:5: another thing src/Services/Agent/SubAgentProfile.cs:20: more """; collector.TryExtract("grep", output, success: true); collector.Count.Should().Be(1); var msg = collector.BuildInjectionMessage()!; msg.Should().Contain("[code_location]"); msg.Should().Contain("Services/Agent"); } [Fact] public void TryExtract_GrepSingleFile_NoLearning() { var collector = new SessionLearningCollector(); // 단일 파일 경로만 있으면 학습 가치 없음 collector.TryExtract("grep", "src/file.cs:10: something", success: true); collector.Count.Should().Be(0); } // ═══════════════════════════════════════════ // 프로젝트 메타 파일 추출 // ═══════════════════════════════════════════ [Fact] public void TryExtract_CsprojRead_ExtractsProjectStructure() { var collector = new SessionLearningCollector(); var output = """ net8.0 """; collector.TryExtract("file_read", output, success: true); collector.Count.Should().Be(1); var msg = collector.BuildInjectionMessage()!; msg.Should().Contain("[project_structure]"); msg.Should().Contain("net8.0"); msg.Should().Contain("xunit"); } // ═══════════════════════════════════════════ // 런타임 감지 추출 // ═══════════════════════════════════════════ [Fact] public void TryExtract_DevEnvDetect_ExtractsDependency() { var collector = new SessionLearningCollector(); var output = """ .NET SDK version: 8.0.300 runtime: Microsoft.NETCore.App 8.0.5 node version: v20.10.0 """; collector.TryExtract("dev_env_detect", output, success: true); collector.Count.Should().Be(1); var msg = collector.BuildInjectionMessage()!; msg.Should().Contain("[dependency]"); } // ═══════════════════════════════════════════ // 파일 조작 에러 추출 // ═══════════════════════════════════════════ [Fact] public void TryExtract_FileWriteFailure_ExtractsErrorPattern() { var collector = new SessionLearningCollector(); collector.TryExtract("file_write", "Access denied: C:/readonly/file.cs", success: false); collector.Count.Should().Be(1); var msg = collector.BuildInjectionMessage()!; msg.Should().Contain("[error_pattern]"); msg.Should().Contain("file_write"); } // ═══════════════════════════════════════════ // FIFO 관리 // ═══════════════════════════════════════════ [Fact] public void TryExtract_FifoEviction_MaintainsMaxLimit() { var collector = new SessionLearningCollector(maxLearnings: 3); for (int i = 0; i < 5; i++) { collector.TryExtract("file_write", $"Error {i}: unique error message number {i}", success: false); } collector.Count.Should().BeLessOrEqualTo(3); } // ═══════════════════════════════════════════ // 중복 방지 // ═══════════════════════════════════════════ [Fact] public void TryExtract_DuplicateContent_NotAdded() { var collector = new SessionLearningCollector(); var output = "Access denied: C:/readonly/file.cs"; collector.TryExtract("file_write", output, success: false); collector.TryExtract("file_write", output, success: false); collector.Count.Should().Be(1); } // ═══════════════════════════════════════════ // BuildInjectionMessage 포맷 // ═══════════════════════════════════════════ [Fact] public void BuildInjectionMessage_ContainsHeader() { var collector = new SessionLearningCollector(); collector.TryExtract("file_write", "Error: something failed", success: false); var msg = collector.BuildInjectionMessage()!; msg.Should().StartWith("[System:SessionLearnings]"); msg.Should().Contain("위 내용을 참고하여 동일 실수를 반복하지 마세요"); } // ═══════════════════════════════════════════ // 안전 가드 // ═══════════════════════════════════════════ [Theory] [InlineData(null, "output")] [InlineData("build_run", null)] [InlineData("", "output")] [InlineData("build_run", "")] [InlineData(" ", "output")] public void TryExtract_NullOrEmptyArgs_DoesNotThrow(string? toolName, string? output) { var collector = new SessionLearningCollector(); collector.TryExtract(toolName!, output!, false); // No exception = pass } [Fact] public void TryExtract_LargeOutput_TruncatesWithoutCrash() { var collector = new SessionLearningCollector(); // 50KB의 큰 출력 var largeOutput = "error CS0001: Big error\n" + new string('x', 60_000); collector.TryExtract("build_run", largeOutput, success: false); // 크래시 없이 완료 = 성공 } }