mana-swift-event-sync/Tests/ManaEventSyncTests/WireParityTests.swift
Till JS 4c1a1415f3 fix(wire): sequenceNumber als JSON-String decoden (Cross-Device-Sync)
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>
2026-05-25 16:43:42 +02:00

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)
}
}