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
78 lines
2.6 KiB
Swift
78 lines
2.6 KiB
Swift
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)) == [])
|
||
}
|