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
This commit is contained in:
Yaojia Wang
2026-07-05 00:13:14 +02:00
parent 5c8c87fb7a
commit 5098643355
33 changed files with 5946 additions and 616 deletions

View File

@@ -0,0 +1,79 @@
//
// 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.maxWSMessageBytes16 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)
}
}