--- phase: 07-user-access-audit plan: 10 type: execute wave: 6 depends_on: ["07-08"] files_modified: - SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs autonomous: true requirements: - UACC-01 gap_closure: true source_gaps: - "Gap 3: Debounced search test absent (Plan 08 truth partially unmet)" must_haves: truths: - "A unit test verifies that setting SearchQuery to a value of length >= 2 triggers IGraphUserSearchService.SearchUsersAsync after the debounce delay" artifacts: - path: "SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs" provides: "Debounced search unit test" contains: "SearchQuery_debounced_calls_SearchUsersAsync" key_links: - from: "SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs" to: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs" via: "Tests SearchQuery property change → DebounceSearchAsync → SearchUsersAsync" pattern: "SearchUsersAsync" --- Add a unit test for the debounced search path in UserAccessAuditViewModel. Purpose: Close verification gap 3 — plan 08 required "ViewModel tests verify: debounced search triggers service" but no such test exists. Output: One new test method added to UserAccessAuditViewModelTests.cs. @C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/dev/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/07-user-access-audit/07-CONTEXT.md @.planning/phases/07-user-access-audit/07-08-SUMMARY.md @.planning/phases/07-user-access-audit/07-VERIFICATION.md ```csharp // Line 281-290: OnSearchQueryChanged triggers DebounceSearchAsync partial void OnSearchQueryChanged(string value) { _searchCts?.Cancel(); _searchCts?.Dispose(); _searchCts = new CancellationTokenSource(); var ct = _searchCts.Token; _ = DebounceSearchAsync(value, ct); } // Line 406-458: DebounceSearchAsync waits 300ms then calls SearchUsersAsync private async Task DebounceSearchAsync(string query, CancellationToken ct) { await Task.Delay(300, ct); // ... guard: query null/whitespace or < 2 chars → clear and return var clientId = _currentProfile?.ClientId ?? string.Empty; var results = await _graphUserSearchService.SearchUsersAsync(clientId, query, 10, ct); // ... dispatches results to SearchResults collection } ``` ```csharp // Uses Moq + xUnit. CreateViewModel helper returns (vm, auditMock). // mockGraph is Mock created inside CreateViewModel. // The test needs access to mockGraph — may need to extend CreateViewModel to return it. ``` ```csharp public interface IGraphUserSearchService { Task> SearchUsersAsync( string clientId, string query, int maxResults, CancellationToken ct); } ``` Task 1: Add debounced search unit test SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs **Step 1**: Extend the `CreateViewModel` helper to also return the `Mock` so tests can set up expectations and verify calls on it. Change the return tuple from `(vm, auditMock)` to `(vm, auditMock, graphMock)`. Update all 8 existing test calls to destructure the third element (use `_` discard). **Step 2**: Add the following test method after Test 8: ```csharp // ── Test 9: Debounced search triggers SearchUsersAsync ────────────── [Fact] public async Task SearchQuery_debounced_calls_SearchUsersAsync() { var graphResults = new List { new("Alice Smith", "alice@contoso.com", "alice@contoso.com") }; var (vm, _, graphMock) = CreateViewModel(); graphMock .Setup(s => s.SearchUsersAsync( It.IsAny(), It.Is(q => q == "Ali"), It.IsAny(), It.IsAny())) .ReturnsAsync(graphResults); // Set a TenantProfile so _currentProfile is non-null var profile = new TenantProfile { Name = "Test", TenantUrl = "https://contoso.sharepoint.com", ClientId = "test-client-id" }; WeakReferenceMessenger.Default.Send(new TenantSwitchedMessage(profile)); // Act: set SearchQuery which triggers OnSearchQueryChanged → DebounceSearchAsync vm.SearchQuery = "Ali"; // Wait longer than 300ms debounce to allow async fire-and-forget to complete await Task.Delay(600); // Assert: SearchUsersAsync was called with the query graphMock.Verify( s => s.SearchUsersAsync( It.IsAny(), "Ali", It.IsAny(), It.IsAny()), Times.Once); } ``` **Important notes:** - The `DebounceSearchAsync` method uses `Application.Current?.Dispatcher` which will be null in tests. The else branch (lines 438-442) handles this by adding directly to SearchResults — this is the test-safe path. - The 600ms delay in the test ensures the 300ms debounce + async execution has time to complete. - The TenantSwitchedMessage sets `_currentProfile` so that `_currentProfile?.ClientId` is non-null. cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~UserAccessAuditViewModelTests" 2>&1 | tail -10 Debounced search test passes: setting SearchQuery to "Ali" triggers SearchUsersAsync after the 300ms debounce. All 9 ViewModel tests pass with no regressions. - `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~UserAccessAuditViewModelTests"` — all 9 tests pass - `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj` — no regressions The debounced search path (SearchQuery → 300ms delay → SearchUsersAsync) has unit test coverage, closing verification gap 3. After completion, create `.planning/phases/07-user-access-audit/07-10-SUMMARY.md`