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,74 @@
//
// ProtocolFlowTests.swift T-iOS-16 1-2 spike
//
// 1. attach(null) attached input "echo hi\r" output "hi"
// RUN:42 shell
// 2. resize(120,40) `stty size` "40 120" resize 1/1000
// stty "" resize
// src/protocol.ts:113-115
//
// MessageCodec MessageCodec suite
// plan §9
import Foundation
import Testing
import WireProtocol
@Suite("T-iOS-16 协议流程(真服务器)", .serialized, .timeLimit(.minutes(5)))
struct ProtocolFlowTests {
@Test("attach(null)→attached(101);echo hi → output 含 hi 且命令确被执行")
func attachEchoRoundtrip() async throws {
// Arrange
let server = try await ServerHarness.shared.server()
let client = WSTestClient(server: server, origin: server.origin)
defer { client.close() }
// Act: attach(null) attached(spike : Origin 101)
let sessionId = try await attachAndAwaitAttached(client: client, sessionId: nil)
#expect(client.handshakeStatusCode == 101,
"升级应 101,实际 \(String(describing: client.handshakeStatusCode))")
var reader = TranscriptReader(client: client)
try await client.send(shellCommand("echo hi"))
try await reader.awaitContains("hi")
// :$((6*7)) shell 42()
try await client.send(shellCommand("echo \"RUN:$((6*7))\""))
try await reader.awaitContains("RUN:42")
// Cleanup
client.close()
await killSessionBestEffort(server: server, id: sessionId)
}
@Test("resize(120,40) → stty size = 40 120;边界 1/1000 均被采纳且连接存活")
func resizeTakesEffectIncludingBoundaries() async throws {
// Arrange
let server = try await ServerHarness.shared.server()
let client = WSTestClient(server: server, origin: server.origin)
defer { client.close() }
let sessionId = try await attachAndAwaitAttached(client: client, sessionId: nil)
var reader = TranscriptReader(client: client)
// Act + Assert: ()SZ:
// $(stty size), "SZ:<rows> <cols>"
try await client.send(.resize(cols: 120, rows: 40))
try await client.send(shellCommand("echo \"SZ:$(stty size)\""))
try await reader.awaitContains("SZ:40 120")
// :1×1 (stty 1 1),
try await client.send(.resize(cols: 1, rows: 1))
try await client.send(shellCommand("echo \"SZ:$(stty size)\""))
try await reader.awaitContains("SZ:1 1")
// :1000×1000 ,
try await client.send(.resize(cols: 1000, rows: 1000))
try await client.send(shellCommand("echo \"SZ:$(stty size)\""))
try await reader.awaitContains("SZ:1000 1000")
// Cleanup
client.close()
await killSessionBestEffort(server: server, id: sessionId)
}
}