import Foundation import RastrilloApple import XCTest @testable import DocsKit final class DocumentSyncIntegrationTests: XCTestCase { func testFailedMergeCannotPoisonEditorOrNextMerge() async throws { let replica = CRDTReplica(guid: "merge-isolation") _ = try await replica.open(update: Data()) _ = try await replica.command(["op": .string("insert_element"), "at": .number(0), "type": .string("paragraph")]) do { _ = try await replica.mergeUpdates([Data([255]), Data([255])]) XCTFail("Malformed updates must fail") } catch { } _ = try await replica.projection() let (first, _) = try await replica.command(["op": .string("display_edit"), "at": .number(0), "length": .number(0), "text": .string("a")]) let (second, projection) = try await replica.command(["op": .string("display_edit"), "at": .number(1), "length": .number(0), "text": .string("b")]) let merged = try await replica.mergeUpdates([first, second]) XCTAssertFalse(merged.isEmpty) let after = try await replica.projection() XCTAssertEqual(after.children.map(\.plainText), projection.children.map(\.plainText)) await replica.close() } @MainActor func testTypingDoesNotAccumulateOneRoundTripPerKeystroke() async throws { guard let origin = ProcessInfo.processInfo.environment["DOCS_SYNC_TEST_ORIGIN"] else { throw XCTSkip("Run native/scripts/test-sync-latency.py for the delayed HTTP fixture") } let client = NativeHTTP(origin: URL(string: origin)!) let model = DocsModel(client: client) await model.open("fixture") XCTAssertNotNil(model.projection) model.didRender(model.projectionGeneration) for _ in 0..<20 { XCTAssertTrue(model.enqueue(["op": .string("display_edit"), "at": .number(0), "length": .number(0), "text": .string("X")])) while model.hasPendingInput { try await Task.sleep(for: .milliseconds(1)) } model.didRender(model.projectionGeneration) try await Task.sleep(for: .milliseconds(50)) } let finishedTyping = ContinuousClock.now let deadline = finishedTyping.advanced(by: .seconds(15)) while model.hasUnsavedChanges && ContinuousClock.now < deadline { await model.flush() try await Task.sleep(for: .milliseconds(10)) } XCTAssertFalse(model.hasUnsavedChanges) let lag = finishedTyping.duration(to: .now) struct Stats: Decodable { let updates: [Data] } let stats: Stats = try await client.request("/stats") print("SYNC: 20 edits, 250ms/request, uploads=\(stats.updates.count), catch-up after typing=\(lag)") XCTAssertLessThanOrEqual(stats.updates.count, 10, "Unsent edits must share a request") XCTAssertLessThan(lag, .seconds(1), "Typing must not leave a growing serial-request backlog") let (initial, _) = try await client.response("/d/fixture/updates") let peer = CRDTReplica(guid: "sync-fixture") _ = try await peer.open(update: initial) for update in stats.updates { _ = try await peer.apply(update) } let projection = try await peer.projection() XCTAssertEqual(projection.children.map(\.plainText), model.projection?.children.map(\.plainText), "A receiver must get every edit") await peer.close() await model.stop() } }