diff --git a/Scripts/Fixtures/test-suite-contract-ledger.tsv b/Scripts/Fixtures/test-suite-contract-ledger.tsv index 91edb1884..24a8720a3 100644 --- a/Scripts/Fixtures/test-suite-contract-ledger.tsv +++ b/Scripts/Fixtures/test-suite-contract-ledger.tsv @@ -240,7 +240,7 @@ root/RepoPromptTests.AgentModeViewModelInactiveRefreshTests/testActivationRepubl root/RepoPromptTests.AgentModeViewModelInactiveRefreshTests/testActiveCodexAssistantDeltaUsesPresentationOnlyRefresh root Tests/RepoPromptTests/AgentMode/AgentModeViewModelInactiveRefreshTests.swift RepoPromptTests.AgentModeViewModelInactiveRefreshTests testActiveCodexAssistantDeltaUsesPresentationOnlyRefresh AgentMode unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.005000 unreviewed retain_pending_review 0 initial census source line 376 root/RepoPromptTests.AgentModeViewModelInactiveRefreshTests/testActiveGenericAssistantDeltaPublishesTranscriptWithoutFullBindingSync root Tests/RepoPromptTests/AgentMode/AgentModeViewModelInactiveRefreshTests.swift RepoPromptTests.AgentModeViewModelInactiveRefreshTests testActiveGenericAssistantDeltaPublishesTranscriptWithoutFullBindingSync AgentMode unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.003000 unreviewed retain_pending_review 0 initial census source line 349 root/RepoPromptTests.AgentModeViewModelInactiveRefreshTests/testActiveRefreshCompactsSummaryOnlyToolResultSourceWhilePreservingRawRenderPayload root Tests/RepoPromptTests/AgentMode/AgentModeViewModelInactiveRefreshTests.swift RepoPromptTests.AgentModeViewModelInactiveRefreshTests testActiveRefreshCompactsSummaryOnlyToolResultSourceWhilePreservingRawRenderPayload AgentMode unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.003000 unreviewed retain_pending_review 0 initial census source line 7 -root/RepoPromptTests.AgentModeViewModelInactiveRefreshTests/testAmbiguousPersistentBindingFailsRoutingWithoutLeakingCandidates root Tests/RepoPromptTests/AgentMode/AgentModeViewModelInactiveRefreshTests.swift RepoPromptTests.AgentModeViewModelInactiveRefreshTests testAmbiguousPersistentBindingFailsRoutingWithoutLeakingCandidates AgentMode unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.001500 unreviewed retain_pending_review 0 initial census source line 597 +root/RepoPromptTests.AgentModeViewModelInactiveRefreshTests/testAmbiguousPersistentBindingFailsRoutingWithoutLeakingCandidates root Tests/RepoPromptTests/AgentMode/AgentModeViewModelInactiveRefreshTests.swift RepoPromptTests.AgentModeViewModelInactiveRefreshTests testAmbiguousPersistentBindingFailsRoutingWithoutLeakingCandidates AgentMode agent_mode.persistent_binding.ambiguous_worktree_state ambiguous_routing,worktree_binding_state,persistent_binding_resolution,single_scan,no_candidate_leak,missing_worktree_state deterministic_unit root_swiftpm routine 2 Ambiguous and missing persistent session claims return unavailable worktree state after exactly one authority scan, and ambiguous routing errors disclose no candidate tab IDs. critical 0.001500 in_memory_view_model retain 0 fail-closed ambiguous binding authority and bounded resolution work root/RepoPromptTests.AgentModeViewModelInactiveRefreshTests/testAssistantPresentationRejectsStaleRevisionGenerationAndTabOwnership root Tests/RepoPromptTests/AgentMode/AgentModeViewModelInactiveRefreshTests.swift RepoPromptTests.AgentModeViewModelInactiveRefreshTests testAssistantPresentationRejectsStaleRevisionGenerationAndTabOwnership AgentMode unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.003000 unreviewed retain_pending_review 0 initial census source line 425 root/RepoPromptTests.AgentModeViewModelInactiveRefreshTests/testAssistantPresentationRequiresAuthoritativeHydratedBindingGeneration root Tests/RepoPromptTests/AgentMode/AgentModeViewModelInactiveRefreshTests.swift RepoPromptTests.AgentModeViewModelInactiveRefreshTests testAssistantPresentationRequiresAuthoritativeHydratedBindingGeneration AgentMode unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.004000 unreviewed retain_pending_review 0 initial census source line 473 root/RepoPromptTests.AgentModeViewModelInactiveRefreshTests/testAutoArchiveSkipsMutationAfterSameWorkspaceReactivation root Tests/RepoPromptTests/AgentMode/AgentModeViewModelInactiveRefreshTests.swift RepoPromptTests.AgentModeViewModelInactiveRefreshTests testAutoArchiveSkipsMutationAfterSameWorkspaceReactivation AgentMode/SidebarArchive agent.sidebar_archive.owner_revalidation same_workspace_reactivation;awaited_mutation_guard async_state_machine root_swiftpm routine 1 A newer activation epoch for the same workspace invalidates an in-flight archive before any compose-tab or stash mutation high 0.006500 test_case retain 0 PR #246 pair-review archive ownership regression @@ -2114,7 +2114,7 @@ root/RepoPromptTests.PathMatchingRecoveryTests/testMovePathResolverRejectsAmbigu root/RepoPromptTests.PathMatchingRecoveryTests/testUnicodeAndCaseInsensitiveLookupPreserveStoredRelativePath root Tests/RepoPromptTests/WorkspaceContext/PathMatching/PathMatchingRecoveryTests.swift RepoPromptTests.PathMatchingRecoveryTests testUnicodeAndCaseInsensitiveLookupPreserveStoredRelativePath WorkspaceContext unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.000000 unreviewed retain_pending_review 0 initial census source line 21 root/RepoPromptTests.PathSearchIndexRecoveryTests/testSearchMatchesFilenameSubpathTokensAndPublishesDeterministicRankMetadata root Tests/RepoPromptTests/WorkspaceContext/Search/PathSearchIndexRecoveryTests.swift RepoPromptTests.PathSearchIndexRecoveryTests testSearchMatchesFilenameSubpathTokensAndPublishesDeterministicRankMetadata WorkspaceContext unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.005000 unreviewed retain_pending_review 0 initial census source line 5 root/RepoPromptTests.PersistentAgentModeMCPReadFileConnectionTests/testAgentOwnedExplicitSetPersistsForIndependentCanonicalLookup root Tests/RepoPromptTests/MCP/Control/PersistentAgentModeMCPReadFileConnectionTests.swift RepoPromptTests.PersistentAgentModeMCPReadFileConnectionTests testAgentOwnedExplicitSetPersistsForIndependentCanonicalLookup MCP unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.541500 unreviewed retain_pending_review 0 initial census source line 28 -root/RepoPromptTests.PersistentAgentModeMCPReadFileConnectionTests/testAgentOwnedHiddenWorktreeWatcherRebases6500LineReadSlicesBeforePostEditReads root Tests/RepoPromptTests/MCP/Control/PersistentAgentModeMCPReadFileConnectionTests.swift RepoPromptTests.PersistentAgentModeMCPReadFileConnectionTests testAgentOwnedHiddenWorktreeWatcherRebases6500LineReadSlicesBeforePostEditReads MCP mcp.hidden_session_worktree.read_slice_watcher_rebase persistent_agent,read_file,6500_lines,watcher_rebase,physical_logical_mapping,full_selection_dominance,no_hidden_ui_tree,stale_deferred_commit,partition_cas deterministic_integration root_swiftpm routine 5 PersistentAgentModeMCPConnectionFixture,SessionWorktreeOwnershipFixture,WorkspaceSelectionSliceFixture Three canonical read_file slices rebase exactly after atomic replacement; a gated successor cannot resurrect a concurrently removed logical target or physical partition entry; the hidden root never projects into UI and later reads cannot downgrade a full selection. critical 1.386500 socketpair,temporary_worktree,actor_store,watcher_publisher,async_tasks test_fixture+explicit_root_unload retain 0 hidden session-root watcher rebase, stale deferred partition/target fencing, and full-file-wins integration +root/RepoPromptTests.PersistentAgentModeMCPReadFileConnectionTests/testAgentOwnedHiddenWorktreeWatcherRebases6500LineReadSlicesBeforePostEditReads root Tests/RepoPromptTests/MCP/Control/PersistentAgentModeMCPReadFileConnectionTests.swift RepoPromptTests.PersistentAgentModeMCPReadFileConnectionTests testAgentOwnedHiddenWorktreeWatcherRebases6500LineReadSlicesBeforePostEditReads MCP mcp.hidden_session_worktree.read_slice_watcher_rebase persistent_agent,read_file,6500_lines,watcher_rebase,physical_logical_mapping,full_selection_dominance,no_hidden_ui_tree,stale_deferred_commit,partition_cas,per_event_binding_resolution,keyed_applied_index_lookup deterministic_integration root_swiftpm routine 6 PersistentAgentModeMCPConnectionFixture,SessionWorktreeOwnershipFixture,WorkspaceSelectionSliceFixture Three canonical read_file slices rebase exactly after atomic replacement; a gated successor cannot resurrect a concurrently removed logical target or physical partition entry; the hidden root never projects into UI and later reads cannot downgrade a full selection; a multi-file applied-index event uses one binding-provider pass, one keyed record lookup, and zero root snapshots. critical 1.386500 socketpair,temporary_worktree,actor_store,watcher_publisher,async_tasks test_fixture+explicit_root_unload retain 0 hidden session-root watcher rebase, stale deferred partition/target fencing, full-file-wins integration, and bounded per-event authority work root/RepoPromptTests.PersistentAgentModeMCPReadFileConnectionTests/testAgentOwnedWorktreeContentSearchCarriesPhysicalCoverageAndPreservesFullSelections root Tests/RepoPromptTests/MCP/Control/PersistentAgentModeMCPReadFileConnectionTests.swift RepoPromptTests.PersistentAgentModeMCPReadFileConnectionTests testAgentOwnedWorktreeContentSearchCarriesPhysicalCoverageAndPreservesFullSelections MCP mcp.hidden_session_worktree.search_context_watcher_rebase persistent_agent,file_search,context_lines,physical_coverage_identity,no_path_leak,watcher_rebase,full_selection_dominance deterministic_integration root_swiftpm routine 5 PersistentAgentModeMCPConnectionFixture,SessionWorktreeOwnershipFixture,WorkspaceSelectionSliceFixture Watcher-created worktree content is searchable without physical-path disclosure; context_lines=2 selects exact 2-6/13-17/25-29 logical slices, atomic replacement rebases them to 6-10/20-24/30-34, unrelated and matching full selections remain full. critical 0.821500 socketpair,temporary_worktree,actor_store,watcher_publisher,async_tasks test_fixture+explicit_session_owner_release retain 0 search physical coverage sidecar, hidden watcher rebase, and full-file-wins integration root/RepoPromptTests.PersistentAgentModeMCPReadFileConnectionTests/testCodexAgentModeLeaseRetainsOneMCPServerSessionAcrossSerialExactAbsoluteReadFileCalls root Tests/RepoPromptTests/MCP/Control/PersistentAgentModeMCPReadFileConnectionTests.swift RepoPromptTests.PersistentAgentModeMCPReadFileConnectionTests testCodexAgentModeLeaseRetainsOneMCPServerSessionAcrossSerialExactAbsoluteReadFileCalls MCP unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed unreviewed retain_pending_review 0 initial census source line 59 root/RepoPromptTests.PersistentAgentModeMCPReadFileConnectionTests/testManageSelectionGetStopsAtCanonicalHandoverWhileMirrorBlocked root Tests/RepoPromptTests/MCP/Control/PersistentAgentModeMCPReadFileConnectionTests.swift RepoPromptTests.PersistentAgentModeMCPReadFileConnectionTests testManageSelectionGetStopsAtCanonicalHandoverWhileMirrorBlocked MCP unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.333500 unreviewed retain_pending_review 0 initial census source line 49 diff --git a/Sources/RepoPrompt/Features/AgentMode/ViewModels/AgentModeViewModel.swift b/Sources/RepoPrompt/Features/AgentMode/ViewModels/AgentModeViewModel.swift index 2a6f97293..920eed774 100644 --- a/Sources/RepoPrompt/Features/AgentMode/ViewModels/AgentModeViewModel.swift +++ b/Sources/RepoPrompt/Features/AgentMode/ViewModels/AgentModeViewModel.swift @@ -772,6 +772,14 @@ final class AgentModeViewModel: ObservableObject { persistentBindingResolution(for: sessionID) } + func test_resetPersistentBindingResolutionCallCount() { + persistentBindingResolutionCallCountForTesting = 0 + } + + var test_persistentBindingResolutionCallCount: Int { + persistentBindingResolutionCallCountForTesting + } + func test_bindingTransitionToken(for session: TabSession) -> PersistentBindingTransitionToken { session.persistentBindingTransitionToken() } @@ -3429,7 +3437,14 @@ final class AgentModeViewModel: ObservableObject { )?.sessionID } + #if DEBUG + private var persistentBindingResolutionCallCountForTesting = 0 + #endif + private func persistentBindingResolution(for sessionID: UUID) -> PersistentBindingResolution { + #if DEBUG + persistentBindingResolutionCallCountForTesting += 1 + #endif var authoritativeCandidates = Set() var conflictingTabIDs = Set() let liveClaims = Dictionary(uniqueKeysWithValues: sessions.values.compactMap { session in @@ -5470,20 +5485,21 @@ final class AgentModeViewModel: ObservableObject { forAgentSessionID sessionID: UUID, tabID: UUID? = nil ) -> AgentSessionWorktreeBindingState { - do { - if let live = try authoritativeLiveSession(for: sessionID) { + switch persistentBindingResolution(for: sessionID) { + case let .unique(resolvedTabID): + if let live = sessions[resolvedTabID], live.activeAgentSessionID == sessionID { + return live.hasLoadedPersistedState ? .hydrated(live.worktreeBindings) : .unhydrated + } + if let tabID, let live = sessions[tabID], live.activeAgentSessionID == sessionID { return live.hasLoadedPersistedState ? .hydrated(live.worktreeBindings) : .unhydrated } - } catch { - return .unavailable - } - if let tabID, let live = sessions[tabID], live.activeAgentSessionID == sessionID { - return live.hasLoadedPersistedState ? .hydrated(live.worktreeBindings) : .unhydrated - } - switch persistentBindingResolution(for: sessionID) { - case .unique: return .unhydrated - case .notFound, .ambiguous: + case .notFound: + if let tabID, let live = sessions[tabID], live.activeAgentSessionID == sessionID { + return live.hasLoadedPersistedState ? .hydrated(live.worktreeBindings) : .unhydrated + } + return .unavailable + case .ambiguous: return .unavailable } } diff --git a/Sources/RepoPrompt/Features/WorkspaceFiles/ViewModels/WorkspaceFilesViewModel.swift b/Sources/RepoPrompt/Features/WorkspaceFiles/ViewModels/WorkspaceFilesViewModel.swift index 26563aa33..8375e6fee 100644 --- a/Sources/RepoPrompt/Features/WorkspaceFiles/ViewModels/WorkspaceFilesViewModel.swift +++ b/Sources/RepoPrompt/Features/WorkspaceFiles/ViewModels/WorkspaceFilesViewModel.swift @@ -387,6 +387,15 @@ private struct HiddenSessionSliceRebaseTarget { let physicalRootPaths: Set } +private struct HiddenSessionSliceRebaseTabContext { + let identity: WorkspaceSelectionIdentity + let agentSessionID: UUID + let binding: AgentSessionWorktreeBinding + let bindingFingerprint: String + let physicalRootPaths: Set + let slicesByLogicalPath: [String: [LineRange]] +} + private struct HiddenSessionSliceRebaseRequest { let rootID: UUID let rootLifetimeID: UUID @@ -1557,9 +1566,13 @@ class WorkspaceFilesViewModel: ObservableObject { hiddenSessionHandledGenerationByRootLifetime.removeValue(forKey: lifetimeKey) return true } - guard let snapshot = await workspaceFileContextStore.appliedIndexRootSnapshot(rootID: event.rootID), - snapshot.root.kind == .sessionWorktree, - snapshot.root.standardizedFullPath == StandardizedPath.absolute(event.rootPath) + guard let lookup = await workspaceFileContextStore.appliedIndexRecordLookup( + rootID: event.rootID, + fileIDs: event.modifiedFileIDs, + folderIDs: [] + ), + lookup.root.kind == .sessionWorktree, + lookup.root.standardizedFullPath == StandardizedPath.absolute(event.rootPath) else { return false } let handled = hiddenSessionHandledGenerationByRootLifetime[lifetimeKey] ?? 0 @@ -1570,16 +1583,26 @@ class WorkspaceFilesViewModel: ObservableObject { hiddenSessionHandledGenerationByRootLifetime[lifetimeKey] = event.generation return true } + guard !event.modifiedFileIDs.isEmpty else { + hiddenSessionHandledGenerationByRootLifetime[lifetimeKey] = event.generation + return true + } + + let tabContexts = await hiddenSessionSliceRebaseTabContexts( + physicalRootPath: lookup.root.standardizedFullPath + ) ?? [] + guard !tabContexts.isEmpty else { + hiddenSessionHandledGenerationByRootLifetime[lifetimeKey] = event.generation + return true + } for fileID in event.modifiedFileIDs { - guard let file = snapshot.files.first(where: { $0.id == fileID }), - file.rootID == event.rootID, - let targets = await hiddenSessionSliceRebaseTargets( - physicalRootPath: snapshot.root.standardizedFullPath, - physicalFullPath: file.standardizedFullPath - ), - !targets.isEmpty - else { continue } + guard let file = lookup.filesByID[fileID], file.rootID == event.rootID else { continue } + let targets = hiddenSessionSliceRebaseTargets( + tabContexts: tabContexts, + physicalFullPath: file.standardizedFullPath + ) + guard !targets.isEmpty else { continue } let eventSource = event.modifiedFileSourceSnapshotsByID[fileID] let sourceSnapshot: SliceRebaseSourceSnapshot? = eventSource.flatMap { source in guard source.rootID == event.rootID, @@ -1597,7 +1620,7 @@ class WorkspaceFilesViewModel: ObservableObject { rootID: event.rootID, rootLifetimeID: rootLifetimeID, fileID: fileID, - physicalRootPath: snapshot.root.standardizedFullPath, + physicalRootPath: lookup.root.standardizedFullPath, physicalFullPath: file.standardizedFullPath, relativePath: file.standardizedRelativePath, sourceSnapshot: sourceSnapshot, @@ -1609,16 +1632,16 @@ class WorkspaceFilesViewModel: ObservableObject { } @MainActor - private func hiddenSessionSliceRebaseTargets( - physicalRootPath: String, - physicalFullPath: String - ) async -> [HiddenSessionSliceRebaseTarget]? { + private func hiddenSessionSliceRebaseTabContexts( + physicalRootPath: String + ) async -> [HiddenSessionSliceRebaseTabContext]? { guard let provider = sessionWorktreeBindingsProvider, let workspace = workspaceManager?.activeWorkspace else { return nil } - var targets: [HiddenSessionSliceRebaseTarget] = [] + var contexts: [HiddenSessionSliceRebaseTabContext] = [] for tab in workspace.composeTabs { - guard let sessionID = tab.activeAgentSessionID else { continue } + let slices = StoredSelectionPathNormalization.standardizedSlices(tab.selection.slices) + guard !slices.isEmpty, let sessionID = tab.activeAgentSessionID else { continue } let bindings = provider(sessionID) let matchingBindings = bindings.filter { StandardizedPath.absolute(($0.worktreeRootPath as NSString).expandingTildeInPath) == physicalRootPath @@ -1632,21 +1655,37 @@ class WorkspaceFilesViewModel: ObservableObject { ownerID: sessionID, bindingFingerprint: fingerprint, physicalRootPaths: physicalRootPaths - ), let logicalFullPath = WorkspaceRootBindingProjection.logicalAbsolutePath( - forPhysicalPath: physicalFullPath, - binding: binding ) else { continue } - let slices = StoredSelectionPathNormalization.standardizedSlices(tab.selection.slices) - guard slices[logicalFullPath]?.isEmpty == false else { continue } - targets.append(HiddenSessionSliceRebaseTarget( + contexts.append(HiddenSessionSliceRebaseTabContext( identity: WorkspaceSelectionIdentity(workspaceID: workspace.id, tabID: tab.id), - logicalFullPath: logicalFullPath, agentSessionID: sessionID, + binding: binding, bindingFingerprint: fingerprint, - physicalRootPaths: physicalRootPaths + physicalRootPaths: physicalRootPaths, + slicesByLogicalPath: slices )) } - return targets + return contexts + } + + private func hiddenSessionSliceRebaseTargets( + tabContexts: [HiddenSessionSliceRebaseTabContext], + physicalFullPath: String + ) -> [HiddenSessionSliceRebaseTarget] { + tabContexts.compactMap { context in + guard let logicalFullPath = WorkspaceRootBindingProjection.logicalAbsolutePath( + forPhysicalPath: physicalFullPath, + binding: context.binding + ), context.slicesByLogicalPath[logicalFullPath]?.isEmpty == false + else { return nil } + return HiddenSessionSliceRebaseTarget( + identity: context.identity, + logicalFullPath: logicalFullPath, + agentSessionID: context.agentSessionID, + bindingFingerprint: context.bindingFingerprint, + physicalRootPaths: context.physicalRootPaths + ) + } } @MainActor diff --git a/Tests/RepoPromptTests/AgentMode/AgentModeViewModelInactiveRefreshTests.swift b/Tests/RepoPromptTests/AgentMode/AgentModeViewModelInactiveRefreshTests.swift index 18248cf0d..baeb9bd27 100644 --- a/Tests/RepoPromptTests/AgentMode/AgentModeViewModelInactiveRefreshTests.swift +++ b/Tests/RepoPromptTests/AgentMode/AgentModeViewModelInactiveRefreshTests.swift @@ -733,6 +733,21 @@ final class AgentModeViewModelInactiveRefreshTests: XCTestCase { } XCTAssertEqual(Set(tabIDs), Set([first.tabID, second.tabID])) + viewModel.test_resetPersistentBindingResolutionCallCount() + XCTAssertEqual( + viewModel.worktreeBindingState(forAgentSessionID: sessionID, tabID: first.tabID), + .unavailable + ) + XCTAssertEqual(viewModel.test_persistentBindingResolutionCallCount, 1) + + let missingSessionID = UUID() + viewModel.test_resetPersistentBindingResolutionCallCount() + XCTAssertEqual( + viewModel.worktreeBindingState(forAgentSessionID: missingSessionID), + .unavailable + ) + XCTAssertEqual(viewModel.test_persistentBindingResolutionCallCount, 1) + do { _ = try await viewModel.mcpResolveOrCreateSessionTarget( tabID: nil, diff --git a/Tests/RepoPromptTests/MCP/Control/PersistentAgentModeMCPReadFileConnectionTests.swift b/Tests/RepoPromptTests/MCP/Control/PersistentAgentModeMCPReadFileConnectionTests.swift index 53f3a8b99..f7dc39320 100644 --- a/Tests/RepoPromptTests/MCP/Control/PersistentAgentModeMCPReadFileConnectionTests.swift +++ b/Tests/RepoPromptTests/MCP/Control/PersistentAgentModeMCPReadFileConnectionTests.swift @@ -659,6 +659,63 @@ final class PersistentAgentModeMCPReadFileConnectionTests: XCTestCase { ) XCTAssertEqual(preservedFull.selectedPaths, [targetLogicalPath]) XCTAssertNil(preservedFull.slices[targetLogicalPath]) + + try await clearSelection(fixture: fixture, id: 10) + _ = try await readFile( + fixture: fixture, + id: 11, + path: targetLogicalPath, + startLine: 200, + limit: 10 + ) + await assertReadFileAutoSelectionSettled(fixture: fixture) + let slicedSelection = try XCTUnwrap( + fixture.window.workspaceManager.composeTab(with: Fixture.tabID)?.selection + ) + XCTAssertEqual(slicedSelection.slices[targetLogicalPath], [LineRange(start: 200, end: 209)]) + + let worktreeRootID = try fixture.installedWorktreeRootID + let worktreeFiles = await fixture.window.workspaceFileContextStore.files( + inRoot: worktreeRootID + ) + XCTAssertGreaterThan(worktreeFiles.count, 1) + let rootLifetimeID = try await fixture.window.workspaceFileContextStore.rootLifetimeIDForTesting( + rootID: worktreeRootID + ) + let handled = fixture.window.workspaceFilesViewModel.hiddenSessionSliceRebaseDebugSnapshotForTesting( + fullPath: physicalURL.path, + rootID: worktreeRootID, + rootLifetimeID: rootLifetimeID + ).handledGeneration + await fixture.window.workspaceFileContextStore.resetAppliedIndexRecordLookupDiagnosticsForTesting() + fixture.resetHiddenSessionBindingsProviderCallCount() + + await fixture.window.workspaceFilesViewModel.applyWorkspaceAppliedIndexEventForTesting( + WorkspaceAppliedIndexBatchEvent( + rootID: worktreeRootID, + rootPath: physicalRootPath, + generation: handled &+ 1, + rootLifetimeID: rootLifetimeID, + modifiedFileIDs: worktreeFiles.map(\.id) + ) + ) + + XCTAssertEqual(fixture.hiddenSessionBindingsProviderCallCount, 1) + let lookupDiagnostics = await fixture.window.workspaceFileContextStore + .appliedIndexRecordLookupDiagnosticsForTesting() + XCTAssertEqual(lookupDiagnostics.lookupRequests, 1) + XCTAssertEqual(lookupDiagnostics.requestedRecords, worktreeFiles.count) + XCTAssertEqual(lookupDiagnostics.rootSnapshots, 0) + + await fixture.window.workspaceFilesViewModel.applyWorkspaceAppliedIndexEventForTesting( + WorkspaceAppliedIndexBatchEvent( + rootID: worktreeRootID, + rootPath: physicalRootPath, + generation: handled &+ 2, + rootLifetimeID: rootLifetimeID, + isRootUnload: true + ) + ) } func assertWorktreeSearchPhysicalCoverage(fixture: Fixture) async throws { @@ -2783,6 +2840,7 @@ final class PersistentAgentModeMCPReadFileConnectionTests: XCTestCase { private var auxiliaryRootID: UUID? private var peerRootID: UUID? private var peerTargetStateVersionBeforeSelection: Int? + private(set) var hiddenSessionBindingsProviderCallCount = 0 private var peerCatalogService: MCPWindowToolCatalogService? private var ownedRoutingService: WindowRoutingService? private var cleanedUp = false @@ -3200,8 +3258,10 @@ final class PersistentAgentModeMCPReadFileConnectionTests: XCTestCase { ) worktreeBinding = binding installWorktreeBindingProvider(binding) - window.workspaceFilesViewModel.setSessionWorktreeBindingsProvider { sessionID in - sessionID == Self.agentSessionID ? [binding] : [] + window.workspaceFilesViewModel.setSessionWorktreeBindingsProvider { [weak self] sessionID in + guard let self else { return [] } + hiddenSessionBindingsProviderCallCount += 1 + return sessionID == Self.agentSessionID ? [binding] : [] } let projection = try await WorkspaceRootBindingProjectionMaterializer( store: window.workspaceFileContextStore @@ -3209,6 +3269,10 @@ final class PersistentAgentModeMCPReadFileConnectionTests: XCTestCase { XCTAssertNotNil(projection) } + func resetHiddenSessionBindingsProviderCallCount() { + hiddenSessionBindingsProviderCallCount = 0 + } + private func installWorktreeBindingProvider(_ binding: AgentSessionWorktreeBinding) { window.mcpServer.registerAgentWorktreeBindingsProvider { sessionID, tabID in guard sessionID == Self.agentSessionID, tabID == Self.tabID else { return .hydrated([]) }