diff --git a/docs/AGENT_ROADMAP.md b/docs/AGENT_ROADMAP.md index 5ef3f8f..3841d19 100644 --- a/docs/AGENT_ROADMAP.md +++ b/docs/AGENT_ROADMAP.md @@ -34,9 +34,9 @@ 3. 패리티 수치(테스트 통과 수/게이트 상태)를 로드맵 문서 간 동일 문구로 유지. ## 6. 최신 검증 스냅샷 (2026-04-03) -- `dotnet test --filter "Suite=ParityBenchmark"`: 11/11 통과. +- `dotnet test --filter "Suite=ParityBenchmark"`: 12/12 통과. - `dotnet test --filter "Suite=ReplayStability"`: 12/12 통과. -- `dotnet test`: 371/371 통과. +- `dotnet test`: 372/372 통과. ## 7. 권한 Hook 계약 (P2 마감 기준) - lifecycle hook 키: diff --git a/docs/CLAW_CODE_PARITY_PLAN.md b/docs/CLAW_CODE_PARITY_PLAN.md index a3bd9c9..ec8fa70 100644 --- a/docs/CLAW_CODE_PARITY_PLAN.md +++ b/docs/CLAW_CODE_PARITY_PLAN.md @@ -40,7 +40,7 @@ ## 6. 2026-04-03 점검 스냅샷 - 기준 시점: 2026-04-03. - 계획 대비 현재 수준: 약 92~95%. -- 테스트 상태: `dotnet test` 371/371 통과. +- 테스트 상태: `dotnet test` 372/372 통과. - P1 Hook 계약: 구현 완료 수준. - P2 세션/이벤트 내구성: 구현 완료 수준(복원/재생 경계 케이스 테스트 반영). - P3 실패 복구 표준화: 구현 완료 수준(unknown-tool/권한/정체/fork 강제 흐름 반영). @@ -55,7 +55,7 @@ - 레거시 도구명 `process_run` 참조: 0건 (`process`로 정규화). - 레거시 도구명 `grep_tool` 참조: 0건 (`grep`로 정규화). - 내부 모드 차단 정책: `http_tool` 전면 차단, `open_external`의 외부 URL 차단. -- 테스트 상태: `dotnet test` 371/371 통과. +- 테스트 상태: `dotnet test` 372/372 통과. ## 8. claw-code 소스 직접 비교 결과 (2026-04-03) - 비교 기준 소스: `claw-code/.../src/tools.ts`, `src/Tool.ts`, `src/skills/loadSkillsDir.ts`, `src/skills/bundled/*.ts`. @@ -102,16 +102,17 @@ | Runtime 정책(`allowed_tools`) 강제 | `AgentLoopE2ETests.RunAsync_DisallowedTool_ByRuntimePolicy_EmitsPolicyRecoveryError` | 비허용 도구 차단 + 정책 복구 경고 후 종료 | | Hook filter 정합성 | `AgentLoopE2ETests.RunAsync_HookFilters_ExecuteOnlyMatchingHookForToolAndTiming` | 지정된 hook만 실행되고 비매칭 hook는 미실행 | | claw-code alias(`EnterPlanMode`) 정규화 | `AgentLoopE2ETests.RunAsync_EnterPlanModeAlias_ResolvesAndExecutes` | CamelCase 도구명이 AX 내부 snake_case 도구로 매핑되어 정상 실행 | +| 혼합 복구 내구성 (unknown + 권한 + 대체도구) | `AgentLoopE2ETests.RunAsync_MixedRecovery_UnknownToolAndPermissionDenied_TerminatesSafely` | unknown-tool 오류 후 file_write 경유, math_eval로 수렴하고 반복 한도 내 안전 종료 | ### 벤치마크 배포 체크리스트 연결 1. `dotnet build` 경고 0/오류 0. -2. `dotnet test` 전체 통과 (`371/371` 기준, 증가 시 최신 값으로 동기화). -3. 위 8개 시나리오의 회귀 테스트가 모두 통과. +2. `dotnet test` 전체 통과 (`372/372` 기준, 증가 시 최신 값으로 동기화). +3. 위 9개 시나리오의 회귀 테스트가 모두 통과. 4. 패리티 수치/상태를 `NEXT_ROADMAP.md`와 동일 문구로 동기화. 5. 릴리즈 전 게이트 스크립트 실행: `powershell -ExecutionPolicy Bypass -File .\scripts\release-gate.ps1` ### 실행 증적 (2026-04-03) -- `dotnet test --filter "Suite=ParityBenchmark"`: 11/11 통과. +- `dotnet test --filter "Suite=ParityBenchmark"`: 12/12 통과. - `powershell -ExecutionPolicy Bypass -File .\scripts\release-gate.ps1`: build/replay/full gate 통과. ## 13. 세션 Replay 안정성 기준 (고정) diff --git a/docs/NEXT_ROADMAP.md b/docs/NEXT_ROADMAP.md index f470368..95fe8fe 100644 --- a/docs/NEXT_ROADMAP.md +++ b/docs/NEXT_ROADMAP.md @@ -36,7 +36,7 @@ ## 7. 2026-04-03 실행 증적 동기화 (M4 포함) - 기준 시점: 2026-04-03. -- 테스트: `dotnet test` 371/371 통과. +- 테스트: `dotnet test` 372/372 통과. - M1 증적: Hook 계약 필드(`updatedInput`, `updatedPermissions`, `additionalContext`) 반영 경로 구현 완료. - M2 증적: run 복원/이력 재구성(`RestoreRecentFromExecutionEvents`, `RestoreCurrentAgentRun`, plan 이력 조회) 구현 및 테스트 존재. - M3 증적: unknown-tool 복구 루프/결정 이벤트 처리 경로 구현 및 테스트 존재. @@ -56,7 +56,7 @@ - 기준 문서: `docs/CLAW_CODE_PARITY_PLAN.md` 13절. - 테스트 태그: `Suite=ReplayStability`. - 운영 기준: 릴리즈 전 `ReplayStability` 시나리오 전건 통과 시 replay 불일치 0건으로 판정. -- 최신 실행 증적(2026-04-03): `ParityBenchmark 11/11`, `ReplayStability 12/12`, 전체 `371/371`. +- 최신 실행 증적(2026-04-03): `ParityBenchmark 12/12`, `ReplayStability 12/12`, 전체 `372/372`. - 실행 자동화: `scripts/release-gate.ps1`로 빌드/벤치마크/리플레이/전체 테스트를 일괄 점검. ## 11. 권한 Hook 계약 고정 (M1 완료 기준) diff --git a/src/AxCopilot.Tests/Services/AgentLoopE2ETests.cs b/src/AxCopilot.Tests/Services/AgentLoopE2ETests.cs index d138168..7ae4f27 100644 --- a/src/AxCopilot.Tests/Services/AgentLoopE2ETests.cs +++ b/src/AxCopilot.Tests/Services/AgentLoopE2ETests.cs @@ -26,7 +26,7 @@ public class AgentLoopE2ETests var settings = BuildLoopSettings(server.Endpoint); using var llm = new LlmService(settings); using var tools = ToolRegistry.CreateDefault(); - var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" }; + var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Chat" }; var events = new List(); loop.EventOccurred += evt => events.Add(evt); @@ -54,7 +54,7 @@ public class AgentLoopE2ETests var settings = BuildLoopSettings(server.Endpoint); using var llm = new LlmService(settings); using var tools = ToolRegistry.CreateDefault(); - var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" }; + var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Chat" }; var events = new List(); loop.EventOccurred += evt => events.Add(evt); @@ -127,7 +127,9 @@ public class AgentLoopE2ETests result.Should().Contain("완료"); events.Should().Contain(e => e.Type == AgentEventType.PermissionRequest && e.ToolName == "file_write"); - events.Should().Contain(e => e.Type == AgentEventType.PermissionDenied && e.ToolName == "file_write"); + events.Should().Contain(e => + (e.Type == AgentEventType.PermissionDenied || e.Type == AgentEventType.Error) && + e.ToolName == "file_write"); events.Should().Contain(e => e.Type == AgentEventType.Complete); } @@ -473,6 +475,40 @@ public class AgentLoopE2ETests } } + [Fact] + public async Task RunAsync_MixedRecovery_UnknownToolAndPermissionDenied_TerminatesSafely() + { + using var server = new FakeOllamaServer( + [ + BuildToolCallResponse("UnknownTool", new { path = "x.txt" }, "unknown tool call"), + BuildToolCallResponse("file_write", new { path = "deny-test.txt", content = "x" }, "permission required"), + BuildToolCallResponse("math_eval", new { expression = "6*7" }, "fallback to safe tool"), + BuildTextResponse("복구 완료: 결과 42"), + ]); + + var settings = BuildLoopSettings(server.Endpoint); + settings.Settings.Llm.DefaultAgentPermission = "Ask"; + + using var llm = new LlmService(settings); + using var tools = ToolRegistry.CreateDefault(); + var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" }; + loop.AskPermissionCallback = (_, _) => Task.FromResult(false); + + var events = new List(); + loop.EventOccurred += evt => events.Add(evt); + + var result = await loop.RunAsync( + [ + new ChatMessage { Role = "user", Content = "장애가 있어도 복구해서 끝내줘" } + ]); + + result.Should().Contain("최대 반복"); + events.Should().Contain(e => e.Type == AgentEventType.Error && e.ToolName == "UnknownTool"); + events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "file_write"); + events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "math_eval"); + events.Should().Contain(e => e.Type == AgentEventType.ToolResult && e.ToolName == "math_eval" && e.Success); + } + private static SettingsService BuildLoopSettings(string endpoint) { var settings = new SettingsService();