변경 목적: - AX Agent의 도구 이름, 내부 설정, 스킬 정책, 실행 루프 사이의 불일치를 줄이고 전체 동작 품질을 높인다. - claw-code 수준의 일관된 동작 품질을 참고하되 AX 구조에 맞는 고유한 카탈로그·정규화 레이어로 재구성한다. 핵심 수정사항: - 도구 canonical id, legacy alias, 탭 노출, 설정 카테고리, read-only 분류를 중앙 카탈로그로 통합했다. - ToolRegistry, AgentLoopService, 병렬 실행 분류, 권한 처리, 훅 처리, 스킬 allowed-tools 해석이 같은 이름 체계를 사용하도록 정리했다. - Agent 설정/일반 설정/도움말의 도구 카드와 훅 편집기, 스킬 설명을 현재 런타임 구조에 맞게 갱신했다. - 컨텍스트 압축, intent gate, spawn agents, session learning, model prompt adapter, workspace context 관련 변경과 테스트 추가를 함께 반영했다. - 문서 이력과 비교/로드맵 문서를 최신 상태로 갱신했다. 검증 결과: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_toolcat\ -p:IntermediateOutputPath=obj\verify_toolcat\ : 경고 0 / 오류 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentToolCatalogTests -p:OutputPath=bin\verify_toolcat_tests\ -p:IntermediateOutputPath=obj\verify_toolcat_tests\ : 통과 8
243 lines
9.4 KiB
C#
243 lines
9.4 KiB
C#
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 = """
|
|
<Project Sdk="Microsoft.NET.Sdk">
|
|
<PropertyGroup>
|
|
<TargetFramework>net8.0</TargetFramework>
|
|
</PropertyGroup>
|
|
<ItemGroup>
|
|
<PackageReference Include="xunit" Version="2.9.0" />
|
|
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
|
</ItemGroup>
|
|
</Project>
|
|
""";
|
|
|
|
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);
|
|
// 크래시 없이 완료 = 성공
|
|
}
|
|
}
|