mana-sync serialisiert sequenceNumber als String (`json:",string"`, types.go:40/66). Der bisherige Int64-Decode hätte beim Pull + bei der Append-Response geworfen → ganzer Sync-Response scheitert → Cross-Device still tot (Single-Device merkt's nicht, weil Push sequenceNumber weglässt). Latent auch in pageta/nutriphi. EventEnvelope + AppendedEvent decoden sequenceNumber jetzt String-oder-Zahl-tolerant (decodeFlexibleInt64). 6 neue Wire-Parität-Tests gegen die echte Server-JSON-Shape, 24/24 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
3.3 KiB
Swift
70 lines
3.3 KiB
Swift
import Foundation
|
|
import XCTest
|
|
@testable import ManaEventSync
|
|
|
|
/// Verifiziert das Wire-Decode gegen die echte `mana-sync`-JSON-Shape.
|
|
/// Kern: der Server serialisiert `sequenceNumber` als **String**
|
|
/// (`json:"...,string"`); ein naiver Int64-Decode würfe → ganzer Pull/
|
|
/// Append-Response scheitert → Cross-Device-Sync still tot.
|
|
final class WireParityTests: XCTestCase {
|
|
private func envelopeJSON(sequence: String?) -> String {
|
|
let seq = sequence.map { "\"sequenceNumber\":\($0)," } ?? ""
|
|
return """
|
|
{"eventId":"e1","aggregateId":"highlight:01HX","appId":"pageta","eventType":"HighlightCreated",\
|
|
"eventVersion":1,"occurredAt":"2026-05-25T10:00:00.000Z","receivedAt":"2026-05-25T10:00:01.000Z",\
|
|
\(seq)"actor":{"kind":"user","principalId":"user_42"},"attributedToUserId":"user_42",\
|
|
"origin":"user","idempotencyKey":"k","payload":{"text":"hallo"}}
|
|
"""
|
|
}
|
|
|
|
func testEnvelopeDecodesStringSequenceNumber() throws {
|
|
// So sendet der Server (sequenceNumber als String).
|
|
let env = try JSONDecoder().decode(EventEnvelope.self, from: Data(envelopeJSON(sequence: "\"42\"").utf8))
|
|
XCTAssertEqual(env.sequenceNumber, 42)
|
|
XCTAssertEqual(env.eventType, "HighlightCreated")
|
|
XCTAssertEqual(env.payload.get("text"), .string("hallo"))
|
|
}
|
|
|
|
func testEnvelopeDecodesNumberSequenceNumber() throws {
|
|
// Toleranz: auch reine Zahl muss gehen (Robustheit).
|
|
let env = try JSONDecoder().decode(EventEnvelope.self, from: Data(envelopeJSON(sequence: "42").utf8))
|
|
XCTAssertEqual(env.sequenceNumber, 42)
|
|
}
|
|
|
|
func testEnvelopeAbsentSequenceNumberIsNil() throws {
|
|
let env = try JSONDecoder().decode(EventEnvelope.self, from: Data(envelopeJSON(sequence: nil).utf8))
|
|
XCTAssertNil(env.sequenceNumber)
|
|
}
|
|
|
|
func testEnvelopeEncodeDecodeRoundTrip() throws {
|
|
// Beim Emit ist sequenceNumber nil → wird weggelassen (Server vergibt).
|
|
let original = EventEnvelope(
|
|
eventId: "e1", aggregateId: "a:1", appId: "x", eventType: "T", eventVersion: 1,
|
|
occurredAt: "2026-05-25T10:00:00.000Z", actor: .user(principalId: "u"),
|
|
attributedToUserId: "u", origin: "user", idempotencyKey: "k", payload: .object(["v": .int(1)])
|
|
)
|
|
let data = try JSONEncoder().encode(original)
|
|
let decoded = try JSONDecoder().decode(EventEnvelope.self, from: data)
|
|
XCTAssertEqual(original, decoded)
|
|
}
|
|
|
|
func testPullResponseWithStringSequenceNumber() throws {
|
|
let json = """
|
|
{"events":[\(envelopeJSON(sequence: "\"7\""))],"hasMore":false,"nextCursor":"7"}
|
|
"""
|
|
let resp = try JSONDecoder().decode(SyncHTTPClient.PullResponse.self, from: Data(json.utf8))
|
|
XCTAssertEqual(resp.events.count, 1)
|
|
XCTAssertEqual(resp.events.first?.sequenceNumber, 7)
|
|
XCTAssertEqual(resp.nextCursor, "7")
|
|
XCTAssertFalse(resp.hasMore)
|
|
}
|
|
|
|
func testAppendResponseWithStringSequenceNumber() throws {
|
|
let json = """
|
|
{"accepted":[{"eventId":"e1","sequenceNumber":"3","aggregateId":"a:1"}],"rejected":[],"nowMs":123}
|
|
"""
|
|
let resp = try JSONDecoder().decode(SyncHTTPClient.AppendResponse.self, from: Data(json.utf8))
|
|
XCTAssertEqual(resp.accepted.first?.sequenceNumber, 3)
|
|
XCTAssertTrue(resp.rejected.isEmpty)
|
|
}
|
|
}
|