Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0ff89bc
test(ios): expose surface lane pending-open credit race
austinywang Sep 29, 2026
6c1cf62
Merge remote-tracking branch 'origin/main' into 15482-ios-testflight-…
austinywang Sep 29, 2026
90a2600
fix(ios): bound mobile surface event stream churn
austinywang Sep 29, 2026
2d36ff0
fix(ios): quarantine stale surface lane generations
austinywang Sep 29, 2026
4cfea93
Merge remote-tracking branch 'origin/main' into 15482-ios-testflight-…
austinywang Sep 29, 2026
614e52a
chore: carry main's ghostty submodule update
austinywang Sep 29, 2026
22aca35
chore: restore main ghostty submodule pointer
austinywang Sep 29, 2026
80b808f
Merge remote-tracking branch 'origin/main' into 15482-ios-testflight-…
austinywang Sep 29, 2026
c8d90e1
fix(ios): serialize focus transitions and late lane opens
austinywang Sep 29, 2026
0574064
fix(ios): focus only accepted terminal input
austinywang Sep 29, 2026
0f1d0db
docs(ios): clarify admitted input-lane focus
austinywang Sep 29, 2026
b4563a9
test(ios): use a local focus-ordering signal
austinywang Sep 29, 2026
1c10cc0
Merge remote-tracking branch 'origin/main' into 15482-ios-testflight-…
austinywang Sep 29, 2026
c6ea634
test(ios): gate focus race on newer note delivery
austinywang Sep 29, 2026
6c366ef
Merge remote-tracking branch 'origin/main' into 15482-ios-testflight-…
austinywang Sep 29, 2026
797e491
Merge remote-tracking branch 'origin/main' into 15482-ios-testflight-…
austinywang Sep 29, 2026
182db45
Merge remote-tracking branch 'origin/main' into 15482-ios-testflight-…
austinywang Sep 29, 2026
35aba1a
Merge remote-tracking branch 'origin/main' into 15482-ios-testflight-…
austinywang Sep 29, 2026
82dcaa8
fix(ios): quarantine lanes through native priority commit
austinywang Sep 29, 2026
9e3ffca
fix(ios): expose cross-file focus state internally
austinywang Sep 29, 2026
604f8ac
fix(ios): keep focus queue helper visibility valid
austinywang Sep 29, 2026
a64eba0
docs(ios): explain focus queue file split
austinywang Sep 29, 2026
685c7da
fix(ios): evict queued surface frames by canonical key
austinywang Sep 29, 2026
4de1616
fix(ios): import host queue in surface focus extension
austinywang Sep 29, 2026
c3eacf0
Merge main (02e2c39e9870) into 15482-ios-testflight-reconnect-loop
austinywang Sep 29, 2026
b01c662
test(ios): cover surface lane lifecycle invariants
austinywang Sep 29, 2026
c084fec
fix(ios): bound retiring surface streams
austinywang Sep 29, 2026
744a9c1
Merge remote-tracking branch 'origin/main' into 15482-ios-testflight-…
austinywang Sep 29, 2026
66f396d
test(ios): namespace transport test helpers
austinywang Sep 29, 2026
8c6694c
fix(ios): expose shared lane fallback state
austinywang Sep 29, 2026
13a55a5
test(mobile): use canonical terminal ids in ordering fixture
austinywang Sep 29, 2026
f36ba32
Merge main (49d7a0df097c) into 15482-ios-testflight-reconnect-loop
austinywang Sep 29, 2026
e3fb1df
Merge main (9b0d37a42c57) into 15482-ios-testflight-reconnect-loop
austinywang Sep 29, 2026
5faf56c
Merge main (af7e3274d327) into 15482-ios-testflight-reconnect-loop
teamleaderleo Sep 30, 2026
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
import CMUXMobileCore
import Foundation
import Testing
@testable import CmuxIrxTransport

// MARK: - Fakes

/// In-memory server->client lane read half. Chunks are pushed by the test;
/// `readRaw` waits for the next one, like a QUIC stream with no data yet.
private actor FakeEventLaneReader: IrxEventLaneReading {
private var chunks: [Data] = []
private var waiter: CheckedContinuation<Data?, any Error>?
private var ended = false
private(set) var stopCodes: [UInt64] = []

func push(_ chunk: Data) {
if let waiter {
self.waiter = nil
waiter.resume(returning: chunk)
} else {
chunks.append(chunk)
}
}

func end() {
ended = true
if let waiter {
self.waiter = nil
waiter.resume(returning: nil)
}
}

func readRaw() async throws -> Data? {
if !chunks.isEmpty { return chunks.removeFirst() }
if ended { return nil }
return try await withCheckedThrowingContinuation { waiter = $0 }
}

func stop(errorCode: UInt64) {
stopCodes.append(errorCode)
end()
}
}


/// Feeds accepted lanes to a hub in the order the test opens them.
private final class FakeLaneAcceptor: @unchecked Sendable {
private let stream: AsyncStream<(IrxLaneDescriptor, any IrxEventLaneReading)>
private let continuation: AsyncStream<(IrxLaneDescriptor, any IrxEventLaneReading)>.Continuation

init() {
(stream, continuation) = AsyncStream.makeStream()
}

func open(_ descriptor: IrxLaneDescriptor) -> FakeEventLaneReader {
let reader = FakeEventLaneReader()
continuation.yield((descriptor, reader))
return reader
}

func closeConnection() { continuation.finish() }

var accept: IrxServerEventLaneHub.AcceptLane {
let stream = stream
return {
var iterator = stream.makeAsyncIterator()
return await iterator.next()
}
}
}


// MARK: - Helpers

private func hubFrame(_ text: String) -> Data {
var length = UInt32(text.utf8.count).bigEndian
var data = Data(bytes: &length, count: 4)
data.append(Data(text.utf8))
return data
}

private func hubWaitUntil(
_ condition: @escaping @Sendable () async -> Bool
) async throws -> Bool {
let reached = try await withIrxDeadline(.seconds(2), onTimeout: {}) {
while !Task.isCancelled {
if await condition() { return true }
try await Task.sleep(for: .milliseconds(2))
}
return false
}
return reached == true
}

private actor FrameCollector {
private(set) var frames: [String] = []
/// Each forwarded frame with the marker scope stamped before it.
private(set) var scopedFrames: [(scope: UUID?, frame: String)] = []
private var pendingScope: UUID?

func append(_ data: Data) {
var buffer = data
while buffer.count >= 4 {
let length = buffer.prefix(4).reduce(0) { ($0 << 8) | Int($1) }
guard buffer.count >= 4 + length else { break }
let payload = Data(buffer.dropFirst(4).prefix(length))
buffer.removeFirst(4 + length)
if let scope = MobileEventLaneScope().markerScope(inPayload: payload) {
pendingScope = scope
continue
}
let text = String(decoding: payload, as: UTF8.self)
frames.append(text)
scopedFrames.append((pendingScope, text))
pendingScope = nil
}
}
}

private func collect(_ stream: IrxServerEventLaneHub.Output, into collector: FrameCollector) -> Task<Void, Never> {
Task {
do {
for try await chunk in stream { await collector.append(chunk) }
} catch {}
}
}


// MARK: - Client hub

@Suite(.timeLimit(.minutes(1)))
struct IrxServerEventLaneHubTests {
@Test func surfaceFrameIsDeliveredWhileAnotherLaneIsStalledMidFrame() async throws {
let acceptor = FakeLaneAcceptor()
let hub = IrxServerEventLaneHub(acceptLane: acceptor.accept)
let collector = FrameCollector()
let consumer = collect(await hub.subscribe(), into: collector)
defer { consumer.cancel() }

let shared = acceptor.open(IrxLaneDescriptor(lane: .events))
let busy = acceptor.open(IrxSurfaceEventLaneProtocol().descriptor(surfaceID: "A"))
let typed = acceptor.open(IrxSurfaceEventLaneProtocol().descriptor(surfaceID: "B"))

// Surface A's large replay has only partly arrived; its lane is
// waiting for the rest. Surface B's echo must not wait behind it.
let replay = hubFrame(String(repeating: "a", count: 64 * 1024))
await busy.push(replay.prefix(10_000))
await shared.push(hubFrame("workspace.updated"))
await typed.push(hubFrame("echo-b"))

#expect(try await hubWaitUntil { await collector.frames.contains("echo-b") })
#expect(await collector.frames.sorted() == ["echo-b", "workspace.updated"])

await busy.push(replay.dropFirst(10_000))
#expect(try await hubWaitUntil { await collector.frames.count == 3 })
await hub.stop()
}

@Test func everySurfaceLaneFrameArrivesBehindItsOwnTerminalsMarker() async throws {
let acceptor = FakeLaneAcceptor()
let hub = IrxServerEventLaneHub(acceptLane: acceptor.accept)
let collector = FrameCollector()
let consumer = collect(await hub.subscribe(), into: collector)
defer { consumer.cancel() }

let surfaceA = UUID()
let surfaceB = UUID()
let shared = acceptor.open(IrxLaneDescriptor(lane: .events))
let laneA = acceptor.open(IrxSurfaceEventLaneProtocol().descriptor(surfaceID: surfaceA.uuidString))
let laneB = acceptor.open(IrxSurfaceEventLaneProtocol().descriptor(surfaceID: surfaceB.uuidString))
await laneA.push(hubFrame("grid-a1") + hubFrame("grid-a2"))
await shared.push(hubFrame("workspace.updated"))
await laneB.push(hubFrame("grid-b"))

#expect(try await hubWaitUntil { await collector.frames.count == 4 })
let scoped = await collector.scopedFrames
#expect(scoped.filter { $0.frame.hasPrefix("grid-a") }.allSatisfy { $0.scope == surfaceA })
#expect(scoped.first { $0.frame == "grid-b" }?.scope == surfaceB)
#expect(scoped.first { $0.frame == "workspace.updated" }.map { $0.scope == nil } == true)
await hub.stop()
}

@Test func framesSplitAcrossChunksArriveWholeAndInLaneOrder() async throws {
let acceptor = FakeLaneAcceptor()
let hub = IrxServerEventLaneHub(acceptLane: acceptor.accept)
let collector = FrameCollector()
let consumer = collect(await hub.subscribe(), into: collector)
defer { consumer.cancel() }

let shared = acceptor.open(IrxLaneDescriptor(lane: .events))
let surface = acceptor.open(IrxSurfaceEventLaneProtocol().descriptor(surfaceID: "S"))
var surfaceBytes = Data()
for index in 0..<20 { surfaceBytes.append(hubFrame("s\(index)")) }
var sharedBytes = Data()
for index in 0..<20 { sharedBytes.append(hubFrame("e\(index)")) }
// Interleave odd-sized chunks from both lanes.
var surfaceOffset = 0
var sharedOffset = 0
while surfaceOffset < surfaceBytes.count || sharedOffset < sharedBytes.count {
if surfaceOffset < surfaceBytes.count {
let end = min(surfaceBytes.count, surfaceOffset + 7)
await surface.push(surfaceBytes.subdata(in: surfaceOffset..<end))
surfaceOffset = end
}
if sharedOffset < sharedBytes.count {
let end = min(sharedBytes.count, sharedOffset + 5)
await shared.push(sharedBytes.subdata(in: sharedOffset..<end))
sharedOffset = end
}
}
#expect(try await hubWaitUntil { await collector.frames.count == 40 })
let frames = await collector.frames
#expect(frames.filter { $0.hasPrefix("s") } == (0..<20).map { "s\($0)" })
#expect(frames.filter { $0.hasPrefix("e") } == (0..<20).map { "e\($0)" })
await hub.stop()
}

@Test func surfaceLanesBeyondTheLimitAreRefused() async throws {
let acceptor = FakeLaneAcceptor()
let hub = IrxServerEventLaneHub(
limits: .init(maximumSurfaceLaneCount: 2),
acceptLane: acceptor.accept
)
_ = await hub.subscribe()
_ = acceptor.open(IrxSurfaceEventLaneProtocol().descriptor(surfaceID: "1"))
_ = acceptor.open(IrxSurfaceEventLaneProtocol().descriptor(surfaceID: "2"))
let third = acceptor.open(IrxSurfaceEventLaneProtocol().descriptor(surfaceID: "3"))
#expect(try await hubWaitUntil { await third.stopCodes == [IrxServerEventLaneHub.laneLimitStopCode] })
#expect(await hub.activeSurfaceLaneCount() == 2)
await hub.stop()
}

@Test func endedSurfaceLaneFreesItsSlotAndKeepsTheHubAlive() async throws {
let acceptor = FakeLaneAcceptor()
let hub = IrxServerEventLaneHub(acceptLane: acceptor.accept)
let collector = FrameCollector()
let consumer = collect(await hub.subscribe(), into: collector)
defer { consumer.cancel() }
let first = acceptor.open(IrxSurfaceEventLaneProtocol().descriptor(surfaceID: "S"))
#expect(try await hubWaitUntil { await hub.activeSurfaceLaneCount() == 1 })
await first.push(hubFrame("partial").prefix(6))
await first.end()
#expect(try await hubWaitUntil { await hub.activeSurfaceLaneCount() == 0 })
// The host reopens the surface on a fresh stream after a failure.
let reopened = acceptor.open(IrxSurfaceEventLaneProtocol().descriptor(surfaceID: "S"))
await reopened.push(hubFrame("full"))
#expect(try await hubWaitUntil { await collector.frames == ["full"] })
#expect(await hub.isAlive)
await hub.stop()
}

@Test func replacingTheSubscriberRoutesLaterFramesToTheNewOne() async throws {
let acceptor = FakeLaneAcceptor()
let hub = IrxServerEventLaneHub(acceptLane: acceptor.accept)
let firstCollector = FrameCollector()
let first = collect(await hub.subscribe(), into: firstCollector)
let shared = acceptor.open(IrxLaneDescriptor(lane: .events))
await shared.push(hubFrame("one"))
#expect(try await hubWaitUntil { await firstCollector.frames == ["one"] })

let secondCollector = FrameCollector()
let second = collect(await hub.subscribe(), into: secondCollector)
defer { second.cancel() }
await first.value
await shared.push(hubFrame("two"))
#expect(try await hubWaitUntil { await secondCollector.frames == ["two"] })
#expect(await firstCollector.frames == ["one"])
await hub.stop()
}

@Test func connectionClosureFinishesTheSubscriber() async throws {
let acceptor = FakeLaneAcceptor()
let hub = IrxServerEventLaneHub(acceptLane: acceptor.accept)
let stream = await hub.subscribe()
acceptor.closeConnection()
var iterator = stream.makeAsyncIterator()
await #expect(throws: (any Error).self) { _ = try await iterator.next() }
#expect(await !hub.isAlive)
}

@Test func oversizedFrameStopsOnlyThatLane() async throws {
let acceptor = FakeLaneAcceptor()
let hub = IrxServerEventLaneHub(
limits: .init(maximumFrameByteCount: 16),
acceptLane: acceptor.accept
)
let collector = FrameCollector()
let consumer = collect(await hub.subscribe(), into: collector)
defer { consumer.cancel() }
let bad = acceptor.open(IrxSurfaceEventLaneProtocol().descriptor(surfaceID: "bad"))
let good = acceptor.open(IrxSurfaceEventLaneProtocol().descriptor(surfaceID: "good"))
await bad.push(hubFrame(String(repeating: "x", count: 64)))
await good.push(hubFrame("ok"))
#expect(try await hubWaitUntil { await bad.stopCodes == [IrxServerEventLaneHub.malformedFrameStopCode] })
#expect(try await hubWaitUntil { await collector.frames == ["ok"] })
await hub.stop()
}
}

Loading
Loading