From 3b03b18f83c24eb1fd249615e54bb855ac43bee0 Mon Sep 17 00:00:00 2001 From: lacvet Date: Fri, 3 Apr 2026 19:34:14 +0900 Subject: [PATCH] Sync parity docs and strengthen replay gate coverage --- docs/AGENT_ROADMAP.md | 16 +++++- docs/CLAW_CODE_PARITY_PLAN.md | 25 +++++++-- docs/NEXT_ROADMAP.md | 16 +++++- scripts/release-gate.ps1 | 6 +-- .../Services/AppStateServiceTests.cs | 51 +++++++++++++++++++ .../Services/TaskRunServiceTests.cs | 19 +++++++ 6 files changed, 122 insertions(+), 11 deletions(-) diff --git a/docs/AGENT_ROADMAP.md b/docs/AGENT_ROADMAP.md index f20b0ea..4f75391 100644 --- a/docs/AGENT_ROADMAP.md +++ b/docs/AGENT_ROADMAP.md @@ -35,5 +35,17 @@ ## 6. 최신 검증 스냅샷 (2026-04-03) - `dotnet test --filter "Suite=ParityBenchmark"`: 7/7 통과. -- `dotnet test --filter "Suite=ReplayStability"`: 8/8 통과. -- `dotnet test`: 355/355 통과. +- `dotnet test --filter "Suite=ReplayStability"`: 12/12 통과. +- `dotnet test`: 361/361 통과. + +## 7. 권한 Hook 계약 (P2 마감 기준) +- lifecycle hook 키: + - `__permission_request__` (pre) + - `__permission_granted__` (post) + - `__permission_denied__` (post) +- payload 기준 필드: `runId`, `tool`, `target`, `permission`, `granted`, `reason`. +- 우선순위: + 1. Hook `updatedPermissions`가 현재 run의 `AgentContext.ToolPermissions`를 즉시 갱신. + 2. 갱신 후 `context.CheckToolPermissionAsync()`로 최종 판정. + 3. hook 실패/예외는 non-blocking(권한 흐름 지속). + 4. `additionalContext`는 가능한 경로에서 메시지 컨텍스트로 반영. diff --git a/docs/CLAW_CODE_PARITY_PLAN.md b/docs/CLAW_CODE_PARITY_PLAN.md index f70e939..750b29d 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` 355/355 통과. +- 테스트 상태: `dotnet test` 361/361 통과. - 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` 355/355 통과. +- 테스트 상태: `dotnet test` 361/361 통과. ## 8. claw-code 소스 직접 비교 결과 (2026-04-03) - 비교 기준 소스: `claw-code/.../src/tools.ts`, `src/Tool.ts`, `src/skills/loadSkillsDir.ts`, `src/skills/bundled/*.ts`. @@ -104,13 +104,14 @@ ### 벤치마크 배포 체크리스트 연결 1. `dotnet build` 경고 0/오류 0. -2. `dotnet test` 전체 통과 (`355/355` 기준, 증가 시 최신 값으로 동기화). +2. `dotnet test` 전체 통과 (`361/361` 기준, 증가 시 최신 값으로 동기화). 3. 위 7개 시나리오의 회귀 테스트가 모두 통과. 4. 패리티 수치/상태를 `NEXT_ROADMAP.md`와 동일 문구로 동기화. 5. 릴리즈 전 게이트 스크립트 실행: `powershell -ExecutionPolicy Bypass -File .\scripts\release-gate.ps1` ### 실행 증적 (2026-04-03) - `dotnet test --filter "Suite=ParityBenchmark"`: 7/7 통과. +- `powershell -ExecutionPolicy Bypass -File .\scripts\release-gate.ps1`: build/replay/full gate 통과. ## 13. 세션 Replay 안정성 기준 (고정) @@ -123,10 +124,26 @@ | run 종료 시 dangling 정리 | `TaskRunServiceTests.RestoreRecentFromExecutionEvents_CompleteClearsDanglingRunScopedActiveTasks` | Complete 이후 run 스코프 active task 잔존 0건 | | 현재 run 복원 우선순위 | `AppStateServiceTests.RestoreCurrentAgentRun_PrefersRunningExecutionEventOverHistory` | 실행 중 이벤트가 history보다 우선되어 현재 run 복원 | | recent timeline 재구성 | `AppStateServiceTests.RestoreRecentTasks_RebuildsRecentTaskTimelineFromExecutionEvents` | 도구/권한/에이전트 최근 이력 순서 복원 | +| 권한 거부 후 active 권한 상태 정리 | `AppStateServiceTests.RestoreRecentTasks_PermissionDeniedLeavesNoActivePermissionAfterResume` | PermissionDenied 이후 run 재개 시 active permission 잔존 0건 | +| Hook 타임라인 역순 병합 정합성 | `AppStateServiceTests.ApplyAgentEvent_ReplaysHookTimelineInReverseChronologicalOrder` | Hook 이벤트가 역순 타임라인에서도 시간/의미 순서 보존 | +| 완료 이벤트 우선 정리(병렬 도구) | `TaskRunServiceTests.RestoreRecentFromExecutionEvents_CompleteClearsParallelToolCallsForSameRun` | Complete 도착 시 동일 run의 병렬 도구 active task 즉시 정리 | ### 운영 규칙 1. 위 시나리오는 `Suite=ReplayStability` 테스트 태그로 관리. 2. 릴리즈 전 `Suite=ReplayStability` 전건 통과를 replay 불일치 0건의 최소 조건으로 사용. ### 실행 증적 (2026-04-03) -- `dotnet test --filter "Suite=ReplayStability"`: 8/8 통과. +- `dotnet test --filter "Suite=ReplayStability"`: 12/12 통과. +- `powershell -ExecutionPolicy Bypass -File .\scripts\release-gate.ps1`: `ReplayStability` 포함 게이트 통과. + +## 14. 권한 Hook 계약 (P2 마감 기준) +- lifecycle hook 키: + - `__permission_request__` (pre) + - `__permission_granted__` (post) + - `__permission_denied__` (post) +- payload 기준 필드: `runId`, `tool`, `target`, `permission`, `granted`, `reason`. +- 우선순위: + 1. Hook `updatedPermissions`가 현재 run의 `AgentContext.ToolPermissions`를 즉시 갱신. + 2. 갱신 후 `context.CheckToolPermissionAsync()`로 최종 판정. + 3. hook 실패/예외는 non-blocking(권한 흐름 지속). + 4. `additionalContext`는 가능한 경로에서 메시지 컨텍스트로 반영. diff --git a/docs/NEXT_ROADMAP.md b/docs/NEXT_ROADMAP.md index a461b1a..71a63c3 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` 355/355 통과. +- 테스트: `dotnet test` 361/361 통과. - M1 증적: Hook 계약 필드(`updatedInput`, `updatedPermissions`, `additionalContext`) 반영 경로 구현 완료. - M2 증적: run 복원/이력 재구성(`RestoreRecentFromExecutionEvents`, `RestoreCurrentAgentRun`, plan 이력 조회) 구현 및 테스트 존재. - M3 증적: unknown-tool 복구 루프/결정 이벤트 처리 경로 구현 및 테스트 존재. @@ -56,5 +56,17 @@ - 기준 문서: `docs/CLAW_CODE_PARITY_PLAN.md` 13절. - 테스트 태그: `Suite=ReplayStability`. - 운영 기준: 릴리즈 전 `ReplayStability` 시나리오 전건 통과 시 replay 불일치 0건으로 판정. -- 최신 실행 증적(2026-04-03): `ParityBenchmark 7/7`, `ReplayStability 8/8`, 전체 `355/355`. +- 최신 실행 증적(2026-04-03): `ParityBenchmark 7/7`, `ReplayStability 12/12`, 전체 `361/361`. - 실행 자동화: `scripts/release-gate.ps1`로 빌드/벤치마크/리플레이/전체 테스트를 일괄 점검. + +## 11. 권한 Hook 계약 고정 (M1 완료 기준) +- lifecycle hook 키: + - `__permission_request__` (pre) + - `__permission_granted__` (post) + - `__permission_denied__` (post) +- payload 기준 필드: `runId`, `tool`, `target`, `permission`, `granted`, `reason`. +- 실행 우선순위: + 1. Hook의 `updatedPermissions`를 현재 run 권한 컨텍스트에 즉시 반영. + 2. 반영 후 `CheckToolPermissionAsync()`로 최종 권한 판정 수행. + 3. hook 예외/실패는 non-blocking으로 처리하고 권한 흐름은 지속. + 4. `additionalContext`는 가능한 경로에서 실행 메시지 컨텍스트에 병합. diff --git a/scripts/release-gate.ps1 b/scripts/release-gate.ps1 index 18cfaee..abc4d93 100644 --- a/scripts/release-gate.ps1 +++ b/scripts/release-gate.ps1 @@ -24,11 +24,11 @@ Write-Host "Workspace: $PSScriptRoot\.." Push-Location (Join-Path $PSScriptRoot "..") try { Invoke-Step -Name "Build (warnings/errors gate)" -Action { dotnet build } - Invoke-Step -Name "Parity benchmark suite" -Action { dotnet test --filter "Suite=ParityBenchmark" } - Invoke-Step -Name "Replay stability suite" -Action { dotnet test --filter "Suite=ReplayStability" } + Invoke-Step -Name "Parity benchmark suite" -Action { dotnet test --no-build --filter "Suite=ParityBenchmark" } + Invoke-Step -Name "Replay stability suite" -Action { dotnet test --no-build --filter "Suite=ReplayStability" } if (-not $SkipFullTest) { - Invoke-Step -Name "Full test suite" -Action { dotnet test } + Invoke-Step -Name "Full test suite" -Action { dotnet test --no-build } } else { Write-Host "" Write-Host "==> Full test suite skipped by -SkipFullTest" diff --git a/src/AxCopilot.Tests/Services/AppStateServiceTests.cs b/src/AxCopilot.Tests/Services/AppStateServiceTests.cs index 6132dd4..c375d24 100644 --- a/src/AxCopilot.Tests/Services/AppStateServiceTests.cs +++ b/src/AxCopilot.Tests/Services/AppStateServiceTests.cs @@ -486,6 +486,57 @@ public class AppStateServiceTests state.RecentTasks[2].Kind.Should().Be("tool"); } + [Fact] + [Trait("Suite", "ReplayStability")] + public void RestoreRecentTasks_PermissionDeniedLeavesNoActivePermissionAfterResume() + { + var state = new AppStateService(); + var now = DateTime.Now; + + state.RestoreRecentTasks( + [ + new ChatExecutionEvent { Timestamp = now.AddMinutes(-3), RunId = "run-11", Type = "PermissionRequest", ToolName = "file_write", Summary = "권한 확인", Iteration = 1 }, + new ChatExecutionEvent { Timestamp = now.AddMinutes(-2), RunId = "run-11", Type = "PermissionDenied", ToolName = "file_write", Summary = "권한 거부", Success = false, Iteration = 2 }, + new ChatExecutionEvent { Timestamp = now.AddMinutes(-1), RunId = "run-11", Type = "Thinking", Summary = "다음 단계 재계획", Iteration = 3 }, + ]); + + state.ActiveTasks.Should().NotContain(t => t.Kind == "permission"); + state.ActiveTasks.Should().Contain(t => t.Kind == "agent" && t.Status == "running"); + state.RecentTasks.Should().Contain(t => t.Kind == "permission" && t.Status == "failed"); + } + + [Fact] + [Trait("Suite", "ReplayStability")] + public void ApplyAgentEvent_ReplaysHookTimelineInReverseChronologicalOrder() + { + var state = new AppStateService(); + var now = DateTime.Now; + + state.ApplyAgentEvent(new AgentEvent + { + RunId = "run-hook-replay", + Type = AgentEventType.HookResult, + ToolName = "__permission_request__", + Summary = "[Hook:a] first", + Timestamp = now.AddSeconds(-2), + Success = true, + }); + state.ApplyAgentEvent(new AgentEvent + { + RunId = "run-hook-replay", + Type = AgentEventType.HookResult, + ToolName = "__permission_denied__", + Summary = "[Hook:b] second", + Timestamp = now, + Success = false, + }); + + var hooks = state.GetRecentHookEvents(2); + hooks.Should().HaveCount(2); + hooks[0].ToolName.Should().Be("__permission_denied__"); + hooks[1].ToolName.Should().Be("__permission_request__"); + } + [Fact] public void ApplyAgentEvent_TracksPermissionLifecycleInTaskStore() { diff --git a/src/AxCopilot.Tests/Services/TaskRunServiceTests.cs b/src/AxCopilot.Tests/Services/TaskRunServiceTests.cs index 5243196..540e1b7 100644 --- a/src/AxCopilot.Tests/Services/TaskRunServiceTests.cs +++ b/src/AxCopilot.Tests/Services/TaskRunServiceTests.cs @@ -257,6 +257,25 @@ public class TaskRunServiceTests service.RecentTasks.Should().Contain(t => t.Kind == "agent" && t.Status == "completed"); } + [Fact] + [Trait("Suite", "ReplayStability")] + public void RestoreRecentFromExecutionEvents_CompleteClearsParallelToolCallsForSameRun() + { + var service = new TaskRunService(); + var now = DateTime.Now; + + service.RestoreRecentFromExecutionEvents( + [ + new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-4), RunId = "run-parallel", Type = "ToolCall", ToolName = "file_read", Summary = "read", Iteration = 1 }, + new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-3), RunId = "run-parallel", Type = "ToolCall", ToolName = "grep", Summary = "grep", Iteration = 1 }, + new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-2), RunId = "run-parallel", Type = "ToolResult", ToolName = "file_read", Summary = "done", Success = true, Iteration = 2 }, + new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-1), RunId = "run-parallel", Type = "Complete", Summary = "complete", Success = true, Iteration = 3 }, + ]); + + service.ActiveTasks.Should().BeEmpty(); + service.RecentTasks.Should().Contain(t => t.Kind == "agent" && t.Status == "completed"); + } + [Fact] public void RestoreRecentFromExecutionEvents_RunLevelErrorClearsDanglingRunScopedActiveTasks() {