From fa187be46a075fbe02e7157c46dbda86f2c34ba8 Mon Sep 17 00:00:00 2001 From: onevcat Date: Tue, 30 Jun 2026 21:18:48 +0900 Subject: [PATCH] Fix duplicate worktree watcher IDs --- .../WorktreeInfoWatcherManager.swift | 11 +++++--- .../WorktreeInfoWatcherManagerTests.swift | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/supacode/Features/Repositories/BusinessLogic/WorktreeInfoWatcherManager.swift b/supacode/Features/Repositories/BusinessLogic/WorktreeInfoWatcherManager.swift index cf514771..ee6188a8 100644 --- a/supacode/Features/Repositories/BusinessLogic/WorktreeInfoWatcherManager.swift +++ b/supacode/Features/Repositories/BusinessLogic/WorktreeInfoWatcherManager.swift @@ -177,7 +177,12 @@ final class WorktreeInfoWatcherManager { private func setWorktrees(_ worktrees: [Worktree]) { let isInitialWorktreeLoad = !hasCompletedInitialWorktreeLoad && self.worktrees.isEmpty && !worktrees.isEmpty - let worktreesByID = Dictionary(uniqueKeysWithValues: worktrees.map { ($0.id, $0) }) + var worktreesByID: [Worktree.ID: Worktree] = [:] + var uniqueWorktrees: [Worktree] = [] + for worktree in worktrees where worktreesByID[worktree.id] == nil { + worktreesByID[worktree.id] = worktree + uniqueWorktrees.append(worktree) + } let desiredIDs = Set(worktreesByID.keys) let currentIDs = Set(self.worktrees.keys) let removedIDs = currentIDs.subtracting(desiredIDs) @@ -193,7 +198,7 @@ final class WorktreeInfoWatcherManager { deferredLineChangeIDs.formUnion(newIDs) } self.worktrees = worktreesByID - for worktree in worktrees { + for worktree in uniqueWorktrees { configureWatcher(for: worktree) if isInitialWorktreeLoad || !deferredLineChangeIDs.contains(worktree.id) { emitLineChangesChanged(worktreeID: worktree.id) @@ -205,7 +210,7 @@ final class WorktreeInfoWatcherManager { if isInitialWorktreeLoad { hasCompletedInitialWorktreeLoad = true } - let repositoryRoots = Set(worktrees.map(\.repositoryRootURL)) + let repositoryRoots = Set(uniqueWorktrees.map(\.repositoryRootURL)) let normalizedRoots = Set(repositoryRoots.map { $0.standardizedFileURL }) refreshRepositoryTimings(for: normalizedRoots) syncWorktreeRegistryMonitors(repositoryRoots: repositoryRoots) diff --git a/supacodeTests/WorktreeInfoWatcherManagerTests.swift b/supacodeTests/WorktreeInfoWatcherManagerTests.swift index 38867e48..994600e9 100644 --- a/supacodeTests/WorktreeInfoWatcherManagerTests.swift +++ b/supacodeTests/WorktreeInfoWatcherManagerTests.swift @@ -65,6 +65,32 @@ struct WorktreeInfoWatcherManagerTests { try FileManager.default.removeItem(at: tempRepository.tempRoot) } + @Test func buildsWorktreeLookupWithoutTrappingOnDuplicateID() async throws { + let tempWorktree = try makeTempWorktree() + let duplicate = Worktree( + id: tempWorktree.worktree.id, + name: "eagle-duplicate", + detail: "duplicate", + workingDirectory: tempWorktree.worktree.workingDirectory, + repositoryRootURL: tempWorktree.worktree.repositoryRootURL + ) + let manager = WorktreeInfoWatcherManager( + focusedInterval: .seconds(3_600), + unfocusedInterval: .seconds(3_600) + ) + let (collector, task) = startCollecting(manager.eventStream()) + + manager.handleCommand(.setPullRequestTrackingEnabled(false)) + manager.handleCommand(.setWorktrees([tempWorktree.worktree, duplicate])) + + await drainAsyncEvents(120) + #expect(await collector.filesChangedCount(worktreeID: tempWorktree.worktree.id) == 1) + + manager.handleCommand(.stop) + await task.value + try FileManager.default.removeItem(at: tempWorktree.tempRoot) + } + @Test func staggersDeferredLineChangesAcrossWorktrees() async throws { let clock = TestClock() let tempRepository = try makeTempRepository(worktreeNames: ["sparrow", "swift", "eagle"]) -- 2.51.2