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
80 lines
4.4 KiB
Swift
80 lines
4.4 KiB
Swift
//
|
||
// ReplayTests.swift — T-iOS-16 测试 3:>1 MiB 单帧回放(吸收 spike③ + 对抗变体)
|
||
//
|
||
// ring-buffer 回放是单帧全量(src/session/session.ts attachWs → buffer.snapshot()),
|
||
// JSON 会把控制字节 \uXXXX 转义膨胀 1-6×(src/protocol.ts:186):
|
||
// - 复现分支:平台默认 maximumMessageSize=1 MiB 收不下(withKnownIssue 常驻记录,
|
||
// 若"意外通过"说明平台行为变了,须重新评估 Tunables.maxWSMessageBytes)
|
||
// - 修复分支:Tunables.maxWSMessageBytes(16 MiB)→ 回放完整
|
||
// - 对抗变体:ESC/C0 密集输出(转义膨胀 ~6×)→ 仍完整
|
||
|
||
import Foundation
|
||
import Testing
|
||
import WireProtocol
|
||
|
||
@Suite("T-iOS-16 回放上限(真服务器)", .serialized, .timeLimit(.minutes(5)))
|
||
struct ReplayTests {
|
||
|
||
@Test(">1MiB 单帧回放 — 默认 1MiB 上限复现失败(known issue);Tunables 16MiB 成功")
|
||
func replayOver1MiBFailsAtDefaultLimitAndSucceedsAt16MiB() async throws {
|
||
let server = try await ServerHarness.shared.server()
|
||
|
||
// 造数据:attach(null) 后灌 >1MiB 普通输出,再 detach(PTY/ring 存活)。
|
||
let generator = WSTestClient(server: server, origin: server.origin)
|
||
let sessionId = try await attachAndAwaitAttached(client: generator, sessionId: nil)
|
||
try await generator.send(shellCommand(
|
||
"perl -e 'print \"a\" x \(HarnessTunables.bulkOutputBytes)'"))
|
||
let produced = try await accumulateOutputBytes(
|
||
client: generator, minBytes: HarnessTunables.bulkOutputBytes)
|
||
#expect(produced >= HarnessTunables.bulkOutputBytes)
|
||
generator.close()
|
||
|
||
// 复现分支:平台默认 maximumMessageSize(1 MiB) → 回放必失败。
|
||
let defaultClient = WSTestClient(server: server, origin: server.origin, maxMessageBytes: nil)
|
||
let defaultResult = await replayResult(
|
||
client: defaultClient, sessionId: sessionId, minBytes: HarnessTunables.bulkOutputBytes)
|
||
defaultClient.close()
|
||
if case .failure(let error) = defaultResult {
|
||
#expect(isMessageTooLongError(error),
|
||
"失败模式应为 NSPOSIXErrorDomain EMSGSIZE(40)/ENOBUFS(55),实际 \(describeError(error))")
|
||
}
|
||
withKnownIssue("默认 maximumMessageSize=1MiB 收不下 >1MiB 单帧 ring-buffer 回放 — 平台限制,修法即 Tunables.maxWSMessageBytes=16MiB(若此处'意外通过',说明平台行为变了,须重新评估)") {
|
||
_ = try defaultResult.get()
|
||
}
|
||
|
||
// 修复分支:16 MiB → 全量回放成功,attached 采用同一 sessionId。
|
||
let bigClient = WSTestClient(server: server, origin: server.origin)
|
||
let summary = try await replayResult(
|
||
client: bigClient, sessionId: sessionId, minBytes: HarnessTunables.bulkOutputBytes).get()
|
||
#expect(summary.adoptedSessionId == sessionId)
|
||
#expect(summary.totalOutputBytes >= HarnessTunables.bulkOutputBytes)
|
||
bigClient.close()
|
||
await killSessionBestEffort(server: server, id: sessionId)
|
||
}
|
||
|
||
@Test("对抗: ESC/C0 密集输出(JSON 转义膨胀~6×)回放,16MiB 仍完整")
|
||
func escDenseReplayStillSucceedsAt16MiB() async throws {
|
||
let server = try await ServerHarness.shared.server()
|
||
|
||
let generator = WSTestClient(server: server, origin: server.origin)
|
||
let sessionId = try await attachAndAwaitAttached(client: generator, sessionId: nil)
|
||
// perl 的 \e = ESC(0x1B):每 4 原始字节 JSON 转义成 9 字节("[0m")。
|
||
try await generator.send(shellCommand(
|
||
"perl -e 'print \"\\e[0m\" x \(HarnessTunables.escSequenceRepeats)'"))
|
||
let produced = try await accumulateOutputBytes(
|
||
client: generator, minBytes: HarnessTunables.escSequenceRawBytes)
|
||
#expect(produced >= HarnessTunables.escSequenceRawBytes)
|
||
generator.close()
|
||
|
||
let bigClient = WSTestClient(server: server, origin: server.origin)
|
||
let summary = try await replayResult(
|
||
client: bigClient, sessionId: sessionId,
|
||
minBytes: HarnessTunables.escSequenceRawBytes).get()
|
||
#expect(summary.adoptedSessionId == sessionId)
|
||
#expect(summary.totalOutputBytes >= HarnessTunables.escSequenceRawBytes)
|
||
#expect(summary.containsSoftReset, "回放应含 ESC[0m(转义序列在回放中原样保留)")
|
||
bigClient.close()
|
||
await killSessionBestEffort(server: server, id: sessionId)
|
||
}
|
||
}
|