Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions Sources/Surfaces/SurfaceCatalog+CloudDisplayMembership.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,21 @@ extension SurfaceCatalog {
}
return view
}
if resources[placement.resource]?.kind == .display,
resources[placement.resource]?.remoteViews == nil,
let workspaceID,
let view = cloudDisplayMembershipView(for: placement.resource, workspaceID: workspaceID) {
return view
if let resource = resources[placement.resource],
placement.remoteTabID == nil,
resource.remoteViews == nil,
(resource.kind == .display || resource.id.isForwardedPort) {
// Displays and forwarded ports can be local previews in more than
// one bound workspace. They have no daemon tab or membership row
// to resolve, so an absent membership is a valid local placement.
// Keep the accepted membership view when one exists, but do not
// turn a missing preview membership into a reconcile failure.
if resource.kind == .display,
let workspaceID,
let view = cloudDisplayMembershipView(for: placement.resource, workspaceID: workspaceID) {
return view
}
return nil
}
return try remoteView(
for: placement.resource,
Expand Down
27 changes: 27 additions & 0 deletions cmuxTests/CloudWorkspaceLiveProjectionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -459,4 +459,31 @@ struct CloudWorkspaceLiveProjectionTests {
)
#expect(plan.obsolete.isEmpty, "A local preview with its bound workspace is not obsolete")
}

@Test("A membership-less preview does not block a second bound workspace", arguments: [false, true])
func membershiplessPreviewDoesNotBlockSecondWorkspace(isPort: Bool) async throws {
let live = LiveWorkspaceFixture()
defer { live.tearDown() }
let first = live.id()
let second = live.id()
let binding = WorkspaceCloudVMBinding(vmID: machine.rawValue, isBase: false, remoteWorkspaceID: "a")
let bindings = [first: binding, second: binding]
let coordinator = CloudWorkspaceProjectionCoordinator(environment: .init(bindings: { bindings }))
let placement = CloudPlacementCoordinator(binding: { bindings[$0] })
let catalog = SurfaceCatalog(live: live, cloudPlacementCoordinator: placement, cloudWorkspaceProjectionCoordinator: coordinator)
catalog.register(CloudPlacementTestProvider(machine: machine))
let preview = isPort
? CmuxTuiSnapshotParser.portBrowser(machine: machine, port: 6969)
: CmuxTuiSnapshotParser.display(machine: machine)

install(try graph(["first": "a"], revision: 1), catalog: catalog, extraResources: [preview])
catalog.record(SurfaceProjection(
resource: preview.id, workspaceID: first, panelID: UUID(), remoteWorkspaceID: "a"
))
coordinator.request(machine: machine, catalog: catalog)
await coordinator.waitForIdle()

#expect(coordinator.failures[second] == nil)
#expect(catalog.projections.contains { $0.resource == preview.id && $0.workspaceID == second })
}
}
49 changes: 49 additions & 0 deletions cmuxTests/SurfaceCatalogTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,55 @@ struct SurfaceCatalogTests {
}
}

@Test("A membership-less terminal placement still fails closed")
func membershiplessTerminalPlacementFailsClosed() throws {
// The membership-less preview allowance is for displays and forwarded
// ports only. A terminal with no view metadata has no local-preview
// reading, so it must keep failing closed rather than projecting with
// no remote provenance.
let machine = SurfaceMachineID.cloud("vivid-newt")
let catalog = SurfaceCatalog(live: live)
let provider = FakeProvider(machine: machine)
catalog.register(provider)
var resource = terminal(machine, "term_1")
resource.remoteViews = nil
catalog.upsert(resource)

#expect(throws: SurfaceCatalogError.unavailable(
resource.id,
reason: "remote placement data is unavailable"
)) {
try catalog.remoteView(
for: SurfaceResourcePlacement(resource: resource.id, remoteWorkspaceID: "a")
)
}
}

@Test("A display placement keeps its daemon tab identity")
func displayPlacementKeepsDaemonTabIdentity() throws {
// A display that really is a daemon tab must resolve to that tab. The
// membership-less allowance only applies when the resource carries no
// view metadata at all.
let machine = SurfaceMachineID.cloud("vivid-newt")
let catalog = SurfaceCatalog(live: live)
let provider = FakeProvider(machine: machine)
catalog.register(provider)
let workspace = SurfaceRemoteWorkspace(id: "a", name: "Workspace a", index: 0, focused: true)
var resource = SurfaceResource(
id: SurfaceResourceID(machine: machine, kind: .display, key: "display_1"),
title: "Display", detail: nil, lifecycle: .running, agent: nil,
remoteWorkspace: workspace, remoteViews: nil, port: nil, url: nil
)
resource.remoteViews = [SurfaceRemoteView(tabID: "tab_d", workspace: workspace)]
catalog.upsert(resource)

let view = try catalog.remoteView(
for: SurfaceResourcePlacement(resource: resource.id, remoteWorkspaceID: "a")
)
#expect(view?.tabID == "tab_d")
#expect(view?.workspace.id == "a")
}

@Test("Duplicate remote tab placement fails closed")
func duplicateRemoteTabPlacementFailsClosed() throws {
let machine = SurfaceMachineID.cloud("vivid-newt")
Expand Down
Loading