From 15f88dbc4af90a7c86c0d0acc2e215f3fc1be0fb Mon Sep 17 00:00:00 2001 From: Kevin Kenyon Date: Fri, 4 Sep 2026 11:52:25 -0500 Subject: [PATCH] fix(native): retain mounted review canvas --- .../RTCReviewWorkspace/ReviewWorkspace.swift | 2 +- .../ReviewWorkspaceTests.swift | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/native/Sources/RTCReviewWorkspace/ReviewWorkspace.swift b/native/Sources/RTCReviewWorkspace/ReviewWorkspace.swift index f7bfe63..bf54059 100644 --- a/native/Sources/RTCReviewWorkspace/ReviewWorkspace.swift +++ b/native/Sources/RTCReviewWorkspace/ReviewWorkspace.swift @@ -368,7 +368,7 @@ private struct ReviewCanvasHost: NSViewRepresentable { } } final class Coordinator: NSObject, ReviewCanvasDelegate { - weak var controller: ReviewCanvasController? + var controller: ReviewCanvasController? var model: ReviewWorkspaceModel var lastNavigationID: UUID? init(model: ReviewWorkspaceModel) { self.model=model } diff --git a/native/Tests/RTCReviewWorkspaceFeatureTests/ReviewWorkspaceTests.swift b/native/Tests/RTCReviewWorkspaceFeatureTests/ReviewWorkspaceTests.swift index f2ffcf7..e8bf26a 100644 --- a/native/Tests/RTCReviewWorkspaceFeatureTests/ReviewWorkspaceTests.swift +++ b/native/Tests/RTCReviewWorkspaceFeatureTests/ReviewWorkspaceTests.swift @@ -1,9 +1,11 @@ +import AppKit import Foundation import RTCContracts import RTCDiffCanvas import RTCDomain import RTCReview import RTCReviewWorkspace +import SwiftUI private struct Source: AnchorArtifactSource { func validate(_ anchor: ReviewAnchor) async throws -> Bool { true } } @@ -21,6 +23,7 @@ private struct Source: AnchorArtifactSource { func validate(_ anchor: ReviewAnch let handler = try await ReviewCommandHandler.open(manifest: manifest, repository: repository, anchors: Source(), mutationPreflight: FixedReviewMutationPreflight(headSHA: revision.headSHA)) let model = ReviewWorkspaceModel(revision: revision, files: [CanvasFile(artifact: artifact)], handler: handler) await model.refresh() + try mountedCanvasRetainsControllerAndEnablesComment(model: model, artifact: artifact) let tourSelection = CanvasSelection(path: artifact.path, side: .new, startLine: 8, endLine: 8) model.navigate(to: tourSelection) precondition(model.selection == tourSelection && model.navigationRequest?.selection == tourSelection) @@ -53,4 +56,42 @@ private struct Source: AnchorArtifactSource { func validate(_ anchor: ReviewAnch precondition(snapshot.status == .approved && snapshot.threads.first?.anchor == anchor && snapshot.progress.first?.viewed == true, "workspace state survives restart") print("RTC review workspace feature checks passed") } + + @MainActor private static func mountedCanvasRetainsControllerAndEnablesComment(model: ReviewWorkspaceModel, artifact: DiffArtifact) throws { + let mountedAt = ContinuousClock.now + let host = NSHostingView(rootView: RTCReviewWorkspaceView(model: model)) + host.frame = NSRect(x: 0, y: 0, width: 1_200, height: 900) + host.layoutSubtreeIfNeeded() + RunLoop.current.run(until: Date().addingTimeInterval(0.05)) + host.layoutSubtreeIfNeeded() + + guard let canvas = descendant(of: host, as: NSCollectionView.self) else { + throw FeatureFailure.failed("mounted workspace did not create a collection view") + } + let diffReady = mountedAt.duration(to: .now) + precondition(canvas.numberOfSections > 0 && canvas.numberOfItems(inSection: 0) > 0, "mounted workspace bridge must retain nonzero diff canvas items") + guard let controller = canvas.delegate as? ReviewCanvasController else { + throw FeatureFailure.failed("mounted workspace lost its canvas controller") + } + + let interactionAt = ContinuousClock.now + controller.collectionView(canvas, didSelectItemsAt: [IndexPath(item: 1, section: 0)]) + let firstInteraction = interactionAt.duration(to: .now) + precondition(model.selection?.path == artifact.path, "mounted canvas line navigation must select exact diff evidence") + model.openComposer() + precondition(model.composer?.selection == model.selection, "Comment must enable after mounted canvas line selection") + precondition(diffReady < .seconds(2), "mounted diff-ready timing exceeded twice the captured 0.746-second baseline: \(diffReady)") + precondition(firstInteraction < .seconds(1), "mounted first-interaction timing exceeded budget: \(firstInteraction)") + print("RTC mounted canvas diff-ready \(diffReady); first-interaction \(firstInteraction)") + } + + @MainActor private static func descendant(of root: NSView, as type: T.Type) -> T? { + if let match = root as? T { return match } + for child in root.subviews { + if let match = descendant(of: child, as: type) { return match } + } + return nil + } } + +private enum FeatureFailure: Error { case failed(String) }