Files
web-terminal/ios/IntegrationTests/MirrorLifecycleTests.swift
Yaojia Wang 5098643355 feat(ios): W3 UI layer + integration CI + ntfy docs
T-iOS-11: TerminalScreen/KeyBar/TerminalViewModel + KeyByteMap (byte-for-byte keybar.ts, arrows excluded from UIKeyCommand to preserve DECCKM)
T-iOS-12: PairingScreen/VM — confirm-before-network (zero-call assertions), §5.4 four-tier warnings, Host construction per contract ruling
T-iOS-13: SessionListScreen/VM — Tunables-paced polling with leak-free teardown, optimistic kill+rollback, pending via overlay (LiveSessionInfo has no pending field)
T-iOS-14: GateBanner/PlanGateSheet/AwayDigestView/GateViewModel — three-way mapping from SessionCore Affordance single source, tap-epoch guard, per-epoch haptics
T-iOS-16: IntegrationTests vs real Node server (10 tests: origin guards, mirror, kill-close vs exit-frame differential, 16MiB+ESC/C0 replay) + ios.yml own-sources coverage gate (red-once demoed)
T-iOS-17: ios/README.md ntfy chapter (read-only verification, file:line cites)
Verified: 224 unit + 10 integration tests green; 5/5 semantic spot-checks; zero Owns violations
2026-07-05 00:13:14 +02:00

101 lines
4.8 KiB
Swift
Raw 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.

//
// MirrorLifecycleTests.swift T-iOS-16 4/6/7JOIN mirror
//
// 4. JOIN mirrorAB attach attach
// src/session/manager.ts:108-174A inputB output
// 6. DELETE /live-sessions/:idkill WS close
// exit manager.killById ws.close() kill PTY
// src/session/manager.ts:202-212exit 广 OPEN socket
// sendIfOpen id GET /live-sessions
// 7. 退广A input "exit\r" bash 退 pty.onExit
// socket 广 {type:'exit'}src/session/session.ts:146-151 B
import Foundation
import Testing
import WireProtocol
@Suite("T-iOS-16 镜像与生命周期(真服务器)", .serialized, .timeLimit(.minutes(5)))
struct MirrorLifecycleTests {
@Test("双客户端 JOIN mirror:B attach 同一会话拿到同 id,A input → B 收 output")
func mirrorJoinSharesOutput() async throws {
// Arrange: A ,B id (, A)
let server = try await ServerHarness.shared.server()
let clientA = WSTestClient(server: server, origin: server.origin)
let clientB = WSTestClient(server: server, origin: server.origin)
defer {
clientA.close()
clientB.close()
}
let sessionId = try await attachAndAwaitAttached(client: clientA, sessionId: nil)
let adoptedByB = try await attachAndAwaitAttached(client: clientB, sessionId: sessionId)
#expect(adoptedByB == sessionId, "镜像 attach 应采用同一会话 id(JOIN 不踢)")
// Act: A $((1300+37)) shell 1337
var readerB = TranscriptReader(client: clientB)
try await clientA.send(shellCommand("echo \"MIR:$((1300+37))\""))
// Assert: B() A 广
try await readerB.awaitContains("MIR:1337")
// Cleanup
clientA.close()
clientB.close()
await killSessionBestEffort(server: server, id: sessionId)
}
@Test("DELETE kill → 镜像观察到 WS close(非 exit 帧),id 从 /live-sessions 消失")
func killClosesMirrorSocketsWithoutExitFrame() async throws {
// Arrange
let server = try await ServerHarness.shared.server()
let clientA = WSTestClient(server: server, origin: server.origin)
let clientB = WSTestClient(server: server, origin: server.origin)
defer {
clientA.close()
clientB.close()
}
let sessionId = try await attachAndAwaitAttached(client: clientA, sessionId: nil)
_ = try await attachAndAwaitAttached(client: clientB, sessionId: sessionId)
let idString = sessionId.uuidString.lowercased()
// Act: Origin kill(G ,src/server.ts:354-359)
let status = try await deleteLiveSession(server: server, id: idString, origin: server.origin)
// Assert: 204;id (killById ); close
// exit (close-first ,src/session/manager.ts:202-212)
#expect(status == 204, "kill 应 204,实际 \(status)")
let idsAfter = try await liveSessionIds(server: server)
#expect(!idsAfter.contains(idString), "kill 后 id 不应再出现在 GET /live-sessions")
let observedB = await drainUntilClose(client: clientB)
#expect(observedB.closed, "镜像客户端 B 应观察到 WS close")
#expect(!observedB.sawExit, "kill 路径不广播 exit 帧(先 close 后 kill)")
}
@Test("自然退出广播:A input exit → 镜像 B 收到 {type:'exit'} 帧")
func naturalExitBroadcastsExitFrameToMirror() async throws {
// Arrange
let server = try await ServerHarness.shared.server()
let clientA = WSTestClient(server: server, origin: server.origin)
let clientB = WSTestClient(server: server, origin: server.origin)
defer {
clientA.close()
clientB.close()
}
let sessionId = try await attachAndAwaitAttached(client: clientA, sessionId: nil)
_ = try await attachAndAwaitAttached(client: clientB, sessionId: sessionId)
// Act: A shell 退
try await clientA.send(shellCommand("exit"))
// Assert: B exit (pty.onExit 广,src/session/session.ts:146-151)
let exit = try await awaitExitFrame(client: clientB)
#expect(exit.code == 0, "bash `exit` 应干净退出,实际 code=\(exit.code)")
// Cleanup(退;kill )
clientA.close()
clientB.close()
await killSessionBestEffort(server: server, id: sessionId)
}
}