Files
web-terminal/ios/App/WebTermTests/EventFanOutTests.swift
Yaojia Wang cc4d3129cc feat(ios): W4 app wiring — DI graph, event fan-out, lifecycle, privacy shade
T-iOS-15: AppEnvironment production DI (real Keychain/probe/transports), EventFanOut
(engine.events → TerminalVM + GateVM + activity bridge), scenePhase lifecycle
(.background→close, suspend→rebuild+reopen reclaims size), privacy shade on != .active,
spike screen deleted, cold-start routing
Walkthrough proxies: hosted live-server smoke over the production DI graph (probe→store→
attach→echo→close) + real-Keychain kSecAttrAccessible assertion (closes T-iOS-7 deferral)
Deviation closed: TerminalViewModel.lastSentDims (valid-only) + alive-engine .active →
notifyForegrounded(dims:) (orchestrator fix on behalf of T-iOS-11 owner)
Env finding: repo under ~/Documents → TCC blocks sim-spawned node; SimServerHarness dual-mode
(self-bootstrap / WEBTERM_SERVER_URL via simctl launchctl setenv)
Verified: 214 pkg + 76 app + 10 integration tests green; verify agent 8/8 PASS
2026-07-05 01:04:42 +02:00

80 lines
2.8 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Testing
@testable import WebTerm
/// T-iOS-15 · Fan-out adapter tests. `SessionEngine.events` is a
/// single-consumer `AsyncStream`, but TerminalViewModel, GateViewModel AND the
/// session-activity bridge must all observe it `EventFanOut` is the wiring
/// layer's broadcast seam. These tests pin its contract: every branch receives
/// every element, in order, and source termination / cancel() propagate.
@Suite("EventFanOut")
struct EventFanOutTests {
private static let branchCount = 3
@Test("每个分支都按序收到全部元素")
func allBranchesReceiveAllElementsInOrder() async {
// Arrange
let (source, continuation) = AsyncStream<Int>.makeStream()
let fanOut = EventFanOut(source: source, branchCount: Self.branchCount)
// Act
for value in 1...5 {
continuation.yield(value)
}
continuation.finish()
// Assert: unbounded buffering late consumption still sees everything.
for branch in fanOut.branches {
var received: [Int] = []
for await value in branch {
received.append(value)
}
#expect(received == [1, 2, 3, 4, 5])
}
}
@Test("分支数量与请求一致")
func branchCountMatchesRequest() {
let (source, _) = AsyncStream<Int>.makeStream()
let fanOut = EventFanOut(source: source, branchCount: Self.branchCount)
#expect(fanOut.branches.count == Self.branchCount)
}
@Test("源结束 → 所有分支结束(消费循环退出,不悬挂)")
func sourceFinishFinishesEveryBranch() async {
// Arrange
let (source, continuation) = AsyncStream<String>.makeStream()
let fanOut = EventFanOut(source: source, branchCount: Self.branchCount)
// Act
continuation.yield("only")
continuation.finish()
// Assert: every branch loop terminates after draining.
for branch in fanOut.branches {
var count = 0
for await _ in branch {
count += 1
}
#expect(count == 1)
}
}
@Test("cancel() → 分支立即终止teardown 不泄漏消费任务)")
func cancelTerminatesBranches() async {
// Arrange
let (source, continuation) = AsyncStream<Int>.makeStream()
let fanOut = EventFanOut(source: source, branchCount: Self.branchCount)
// Act: cancel without ever finishing the source.
fanOut.cancel()
continuation.yield(42) // post-cancel input must not hang consumers
// Assert: iteration completes (content is unspecified mid-flight
// termination is the contract under test).
for branch in fanOut.branches {
for await _ in branch {}
}
#expect(Bool(true)) // reaching here = no hang
}
}