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:
@@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
import WireProtocol
|
||||
|
||||
// T-iOS-3 · TimelineEvent(GET /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("TimelineEvent:toolName 可选缺省")
|
||||
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("decodeList:5 个已知 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)) == [])
|
||||
}
|
||||
Reference in New Issue
Block a user