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
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,20 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
/// recovery-scoped UI attributes flags to the exact pairing being
/// redialed, never a healthy sibling build on the same physical Mac.
private(set) var recoveryTargetInstanceTag: String?
/// The foreground pairing that a new connection attempt replaces. During
/// recovery the live identity is intentionally cleared before redial, so
/// the retained target remains the ownership authority for distinguishing
/// a same-Mac reconnect from a real Mac switch.
private var foregroundOrRecoveryMacKey: MacPairingKey {
guard foregroundMacDeviceID == nil,
let recoveryTargetMacDeviceID else {
return foregroundMacKey
}
return MacPairingKey(
macDeviceID: recoveryTargetMacDeviceID,
instanceTag: recoveryTargetInstanceTag
)
}
/// Compatibility view over registry entries whose role is `.control`.
var secondaryMacSubscriptions: MobileMacConnectionRegistry.ControlSubscriptions {
macConnectionRegistry.controlSubscriptions
Expand Down Expand Up @@ -8020,7 +8034,7 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
.trimmingCharacters(in: .whitespacesAndNewlines)
let requestedMacDeviceID = pairedMacDeviceID
?? (ticketMacDeviceID.isEmpty ? nil : ticketMacDeviceID)
let previousForegroundKeyBeforeConnect = foregroundMacKey
let previousForegroundKeyBeforeConnect = foregroundOrRecoveryMacKey
let currentFocusedConnection: MacConnection? =
foregroundMacDeviceID.flatMap { macID in
guard let connection = connections[macID],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,30 @@ extension ReconnectRouteSelectionTests {
#expect(attemptedKinds.allSatisfy { $0 == .iroh })
}

@Test func sameMacEventStreamRecoveryPreservesSelectedWorkspace() async throws {
let fixture = try await makeRecoveryOwnerFixture()
defer { fixture.release() }
await fixture.router.setWorkspaceIDs(["cmux-master", "hevy-cli"])

#expect(await fixture.store.reconnectActiveMacIfAvailable(stackUserID: "user-1"))
#expect(await fixture.router.waitForCount(of: "mobile.events.subscribe", atLeast: 1))
let firstClient = try #require(fixture.store.remoteClient)
let hevyWorkspace = try #require(
fixture.store.workspaces.first { $0.rpcWorkspaceID.rawValue == "hevy-cli" }
)
fixture.store.selectedWorkspaceID = hevyWorkspace.id
let firstTransport = try #require(fixture.box.get())

await firstTransport.close()

#expect(try await pollUntil {
guard let replacement = fixture.store.remoteClient else { return false }
return replacement !== firstClient
&& fixture.store.connectionState == .connected
})
#expect(fixture.store.selectedWorkspace?.rpcWorkspaceID.rawValue == "hevy-cli")
}

@Test func recoveryWaitsForOldPhysicalTransportBeforeRedialing() async throws {
let closeGate = LivenessTransportCloseGate()
let fixture = try await makeRecoveryOwnerFixture(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ actor LivenessHostRouter {
private var macInstanceTag: String? = "default"
private var macDisplayName: String? = "Test Mac"
private var workspaceListResponseHook: (@Sendable () -> Void)?
private var workspaceIDs = ["live-workspace"]
private var workspaceListTitles: [String] = []
/// FIFO of scripted `mobile.sync.fetch` results (state sync v2 tests).
private var syncFetchResults: [[String: Any]] = []
Expand Down Expand Up @@ -336,6 +337,10 @@ actor LivenessHostRouter {
workspaceListTitles.append(contentsOf: titles)
}

func setWorkspaceIDs(_ workspaceIDs: [String]) {
self.workspaceIDs = workspaceIDs
}

func scriptNotificationFeedRevisions(_ revisions: [Int]) {
notificationFeedRevisions.append(contentsOf: revisions)
}
Expand Down Expand Up @@ -483,24 +488,27 @@ actor LivenessHostRouter {
message: "scripted workspace list failure"
)
}
return try? Self.resultFrame(id: id, result: [
"workspaces": [
[
"id": "live-workspace",
"title": workspaceTitle,
"current_directory": "/Users/test/project",
"is_selected": true,
"terminals": [
[
"id": "live-terminal",
"title": "Terminal",
"current_directory": "/Users/test/project",
"is_ready": true,
"is_focused": true,
],
let workspaces: [[String: Any]] = workspaceIDs.enumerated().map { index, workspaceID in
[
"id": workspaceID,
"title": index == 0 ? workspaceTitle : workspaceID,
"current_directory": "/Users/test/project",
"is_selected": index == 0,
"terminals": [
[
"id": workspaceID == "live-workspace"
? "live-terminal"
: "\(workspaceID)-terminal",
"title": "Terminal",
"current_directory": "/Users/test/project",
"is_ready": true,
"is_focused": true,
],
],
],
]
}
return try? Self.resultFrame(id: id, result: [
"workspaces": workspaces,
])
case "mobile.host.status":
hostStatusRequestCount += 1
Expand Down