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,67 @@
//
// OriginGuardTests.swift T-iOS-16 5Origin spike
//
// plan §5.1 " CI Origin "= CRITICAL
// 401/403 ""
// - WS Origin 401src/server.ts:646-651 Origin
// - WS Origin 401src/http/origin.ts:47-51
// :443/:80 new URL()
// - DELETE /live-sessions/:id Origin 403G id
// src/server.ts:332-339 requireAllowedOrigin
import Foundation
import Testing
import WireProtocol
@Suite("T-iOS-16 Origin 守卫(真服务器)", .serialized, .timeLimit(.minutes(5)))
struct OriginGuardTests {
@Test("无 Origin 的 WS 升级被 401 拒绝(服务器默认拒空 Origin)")
func upgradeWithoutOriginIsRejectedWith401() async throws {
let server = try await ServerHarness.shared.server()
let client = WSTestClient(server: server, origin: nil)
defer { client.close() }
let failure = await client.handshakeFailure(timeout: HarnessTunables.frameTimeout)
#expect(failure != nil, "无 Origin 升级必须失败")
#expect(client.handshakeStatusCode == 401,
"应收 401(src/server.ts:646-651),实际 status=\(String(describing: client.handshakeStatusCode)) err=\(failure.map(describeError) ?? "nil")")
}
@Test("端口失配的 Origin → 401(端口精确比对;默认端口规范化不是失配案例)")
func portMismatchedOriginIsRejectedWith401() async throws {
let server = try await ServerHarness.shared.server()
let mismatchPort = server.port == HarnessTunables.mismatchedOriginPort
? HarnessTunables.mismatchedOriginPortFallback
: HarnessTunables.mismatchedOriginPort
let badOrigin = "http://127.0.0.1:\(mismatchPort)"
let client = WSTestClient(server: server, origin: badOrigin)
defer { client.close() }
let failure = await client.handshakeFailure(timeout: HarnessTunables.frameTimeout)
#expect(failure != nil, "端口失配 Origin 升级必须失败")
#expect(client.handshakeStatusCode == 401,
"应收 401(src/http/origin.ts:47-51),实际 status=\(String(describing: client.handshakeStatusCode)) err=\(failure.map(describeError) ?? "nil")")
}
@Test("DELETE /live-sessions/:id 无 Origin → 403;带 Origin 的同样请求 → 404(差分证明 403 来自守卫)")
func deleteWithoutOriginIsRejectedWith403() async throws {
// Arrange: id id ,
let server = try await ServerHarness.shared.server()
let bogusId = UUID().uuidString.lowercased()
// Act
let statusWithoutOrigin = try await deleteLiveSession(
server: server, id: bogusId, origin: nil)
let statusWithOrigin = try await deleteLiveSession(
server: server, id: bogusId, origin: server.origin)
// Assert: Origin (403); Origin id (404)
#expect(statusWithoutOrigin == 403,
"无 Origin 的 DELETE 应 403(src/server.ts:332-339),实际 \(statusWithoutOrigin)")
#expect(statusWithOrigin == 404,
"带 Origin 但 id 不存在应 404(src/server.ts:354-359),实际 \(statusWithOrigin)")
}
}