import assert from "node:assert/strict"; import test from "node:test"; import * as Automerge from "@automerge/automerge"; import { AUTOMERGE_CHANGES_ENCODING, AUTOMERGE_SNAPSHOT_ENCODING, createAutomergeFieldValue, createAutomergeFieldChanges, encodeAutomergeFieldWrite, isAutomergeFieldDoc, loadAutomergeFieldDoc, materializeAutomergeFieldValue, mergeAutomergeFieldValues, saveAutomergeFieldDoc, } from "../src/crdt-automerge.js"; const type = "camino.CrdtJson"; test("Automerge CRDT fields explicitly create stored envelopes", () => { const stored = createAutomergeFieldValue({ title: "draft" }, type); assert.equal(stored.$caminoCrdtType, type); assert.equal(stored.$caminoCrdtEncoding, AUTOMERGE_SNAPSHOT_ENCODING); assert.deepEqual(materializeAutomergeFieldValue(stored, type), { title: "draft", }); }); test("Automerge CRDT field writes reject plain values", () => { assert.throws( () => encodeAutomergeFieldWrite({ title: "draft" }, type), /CRDT field writes must use an Automerge document/, ); }); test("Automerge CRDT fields encode rehydrated docs for writes", () => { const base = loadAutomergeFieldDoc( createAutomergeFieldValue({ title: "draft" }, type), type, ); const changed = Automerge.change(base, (doc) => { (doc.value as { title: string }).title = "edited"; }); assert.equal(isAutomergeFieldDoc(changed), true); const stored = encodeAutomergeFieldWrite(changed, type); assert.deepEqual(materializeAutomergeFieldValue(stored, type), { title: "edited", }); }); test("Automerge CRDT fields merge divergent saved documents", () => { const base = loadAutomergeFieldDoc( createAutomergeFieldValue({ left: false, right: false }, type), type, ); const left = Automerge.change(Automerge.clone(base, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), (doc) => { (doc.value as { left: boolean }).left = true; }); const right = Automerge.change(Automerge.clone(base, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), (doc) => { (doc.value as { right: boolean }).right = true; }); const result = mergeAutomergeFieldValues({ existing: saveAutomergeFieldDoc(left, type), incoming: saveAutomergeFieldDoc(right, type), type, }); assert.deepEqual(result.materialized, { left: true, right: true }); assert.deepEqual(materializeAutomergeFieldValue(result.stored, type), { left: true, right: true, }); }); test("Automerge CRDT fields encode and apply incremental changes", () => { const stored = createAutomergeFieldValue({ title: "draft" }, type); const base = loadAutomergeFieldDoc(stored, type); const changed = Automerge.change(base, (doc) => { (doc.value as { title: string }).title = "edited"; }); const update = createAutomergeFieldChanges(base, changed, type); assert.equal(update.$caminoCrdtEncoding, AUTOMERGE_CHANGES_ENCODING); assert.ok(update.$caminoCrdtPayload.length < saveAutomergeFieldDoc(changed, type).$caminoCrdtPayload.length); const result = mergeAutomergeFieldValues({ existing: stored, incoming: update, type }); assert.deepEqual(result.materialized, { title: "edited" }); assert.equal(result.stored.$caminoCrdtEncoding, AUTOMERGE_SNAPSHOT_ENCODING); }); test("Automerge change application is idempotent", () => { const stored = createAutomergeFieldValue({ count: 0 }, type); const base = loadAutomergeFieldDoc(stored, type); const changed = Automerge.change(base, (doc) => { (doc.value as { count: number }).count = 1; }); const update = createAutomergeFieldChanges(base, changed, type); const once = mergeAutomergeFieldValues({ existing: stored, incoming: update, type }); const twice = mergeAutomergeFieldValues({ existing: once.stored, incoming: update, type }); assert.deepEqual(twice.materialized, { count: 1 }); }); test("Automerge change batches reject malformed framing", () => { const stored = createAutomergeFieldValue({ count: 0 }, type); assert.throws( () => mergeAutomergeFieldValues({ existing: stored, incoming: { $caminoCrdtType: type, $caminoCrdtEncoding: AUTOMERGE_CHANGES_ENCODING, $caminoCrdtPayload: Buffer.from([0, 0, 0, 1]).toString("base64"), }, type, }), /missing change length/, ); });