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,77 @@
import Testing
@testable import SessionCore
/// T-iOS-11 · KeyByteMap (plan §7). The key-bar byte table must mirror
/// `public/keybar.ts` `KEY_MAP` byte-for-byte every entry is compared
/// against an independently written expectation (raw scalar values, not the
/// implementation's own literals) so a typo in either side turns the suite red.
@Suite("KeyByteMap")
struct KeyByteMapTests {
/// Expected byte strings, transcribed key-by-key from public/keybar.ts
/// KEY_MAP (the web client's single source of truth). Values are built
/// from Unicode scalars so this table cannot share a typo with the
/// implementation's string literals.
private static let expectedBytesByKey: [KeyByteMap.Key: [UInt8]] = [
.esc: [0x1B], // '\x1b'
.escEsc: [0x1B, 0x1B], // '\x1b\x1b'
.shiftTab: [0x1B, 0x5B, 0x5A], // '\x1b[Z'
.arrowUp: [0x1B, 0x5B, 0x41], // '\x1b[A'
.arrowDown: [0x1B, 0x5B, 0x42], // '\x1b[B'
.arrowLeft: [0x1B, 0x5B, 0x44], // '\x1b[D'
.arrowRight: [0x1B, 0x5B, 0x43], // '\x1b[C'
.enter: [0x0D], // '\r' carriage return, NOT '\n'
.ctrlC: [0x03],
.ctrlR: [0x12],
.ctrlO: [0x0F],
.ctrlL: [0x0C],
.ctrlT: [0x14],
.ctrlB: [0x02],
.ctrlD: [0x04],
.tab: [0x09], // '\t'
.slash: [0x2F], // '/'
]
@Test("every key resolves to the exact bytes of public/keybar.ts KEY_MAP")
func everyEntryMatchesWebKeyMapByteForByte() {
for key in KeyByteMap.Key.allCases {
// Arrange
let expected = Self.expectedBytesByKey[key]
// Act
let actual = Array(KeyByteMap.bytes(for: key).utf8)
// Assert
#expect(actual == expected, "bytes mismatch for \(key.rawValue)")
}
}
@Test("the table covers exactly the 17 web KEY_MAP entries — no more, no less")
func tableCoversExactlyTheWebKeyMapEntries() {
#expect(KeyByteMap.Key.allCases.count == Self.expectedBytesByKey.count)
#expect(Set(KeyByteMap.Key.allCases).count == KeyByteMap.Key.allCases.count)
}
@Test("Enter sends carriage return (0x0D), never linefeed (gotcha: \\r not \\n)")
func enterIsCarriageReturnNotLinefeed() {
#expect(KeyByteMap.bytes(for: .enter) == "\r")
#expect(KeyByteMap.bytes(for: .enter) != "\n")
}
@Test("arrow CSI letters: up=A down=B left=D right=C (left/right are the classic swap)")
func arrowFinalLettersAreNotSwapped() {
#expect(KeyByteMap.bytes(for: .arrowUp).hasSuffix("A"))
#expect(KeyByteMap.bytes(for: .arrowDown).hasSuffix("B"))
#expect(KeyByteMap.bytes(for: .arrowLeft).hasSuffix("D"))
#expect(KeyByteMap.bytes(for: .arrowRight).hasSuffix("C"))
}
@Test("raw values mirror the web KEY_MAP property names (stable lookup identity)")
func rawValuesMirrorWebKeyNames() {
let expectedNames = [
"esc", "escEsc", "shiftTab", "arrowUp", "arrowDown", "arrowLeft",
"arrowRight", "enter", "ctrlC", "ctrlR", "ctrlO", "ctrlL", "ctrlT",
"ctrlB", "ctrlD", "tab", "slash",
]
#expect(KeyByteMap.Key.allCases.map(\.rawValue) == expectedNames)
}
}