feat(ios): W0 scaffold + day-1 spike + WireProtocol frozen contract + TestSupport doubles

T-iOS-1: ios/ XcodeGen project (iOS 17, Swift 6 strict concurrency, ATS per PLAN §5.2), 5 SPM package shells, CI skeleton
T-iOS-2: Origin spike vs real server — URLSessionWebSocketTask custom Origin CONFIRMED (no Starscream); 16MiB replay + EMSGSIZE(40) errno correction written back to plan
T-iOS-3: WireProtocol frozen contract, 59 tests, 100% line coverage, cross-impl vectors vs src/protocol.ts via tsx
T-iOS-4: FakeTransport/FakeClock/FakeHTTPTransport doubles
Verify: independent agent re-ran all acceptance — 6/6 PASS
This commit is contained in:
Yaojia Wang
2026-07-04 21:19:30 +02:00
parent 9b41ffa574
commit cbaa08daba
46 changed files with 3203 additions and 3 deletions

View File

@@ -0,0 +1,77 @@
import Foundation
import Testing
import WireProtocol
// T-iOS-3 · TimelineEventGET /live-sessions/:id/events src/types.ts:428-433
// decodeList throw/ class
@Test("TimelineEvent合法条目全字段解码")
func decodesValidEntryWithAllFields() throws {
// Arrange
let json = Data("{\"at\":1751600000000,\"class\":\"tool\",\"toolName\":\"Bash\",\"label\":\"ran Bash\"}".utf8)
// Act
let event = try JSONDecoder().decode(TimelineEvent.self, from: json)
// Assert
#expect(event == TimelineEvent(at: 1_751_600_000_000, class: "tool", toolName: "Bash", label: "ran Bash"))
#expect(event.hasKnownClass)
}
@Test("TimelineEventtoolName 可选缺省")
func decodesEntryWithoutToolName() throws {
// Arrange
let json = Data("{\"at\":1,\"class\":\"waiting\",\"label\":\"waiting for approval\"}".utf8)
// Act
let event = try JSONDecoder().decode(TimelineEvent.self, from: json)
// Assert
#expect(event.toolName == nil)
#expect(event.hasKnownClass)
}
@Test("knownClasses 与服务器 TimelineClass 全集一致src/types.ts:423")
func knownClassesMirrorServerUnion() {
#expect(TimelineEvent.knownClasses == ["tool", "waiting", "done", "stuck", "user"])
}
@Test("decodeList未知 class 条目被丢弃,合法条目保留(消费方丢弃语义)")
func decodeListDropsUnknownClassEntries() {
// Arrange 1 + 1 class + 1 label + 1
let json = Data("""
[{"at":1,"class":"tool","label":"ran Bash"},
{"at":2,"class":"alien","label":"???"},
{"at":3,"class":"done"},
42]
""".utf8)
// Act
let events = TimelineEvent.decodeList(from: json)
// Assert
#expect(events == [TimelineEvent(at: 1, class: "tool", toolName: nil, label: "ran Bash")])
}
@Test("decodeList5 个已知 class 全部保留")
func decodeListKeepsAllKnownClasses() {
// Arrange
let json = Data("""
[{"at":1,"class":"tool","label":"a"},{"at":2,"class":"waiting","label":"b"},
{"at":3,"class":"done","label":"c"},{"at":4,"class":"stuck","label":"d"},
{"at":5,"class":"user","label":"e"}]
""".utf8)
// Act
let events = TimelineEvent.decodeList(from: json)
// Assert
#expect(events.count == 5)
#expect(events.allSatisfy { $0.hasKnownClass })
}
@Test("decodeList坏 JSON / 顶层非数组 → 空数组,永不 throw",
arguments: ["not json", "{\"at\":1}", "42", ""])
func decodeListNeverThrowsOnMalformedInput(text: String) {
#expect(TimelineEvent.decodeList(from: Data(text.utf8)) == [])
}