import { describe, expect, it } from "@suaveplan/testing/runner"; import { CRDT_CODES, CrdtError } from "../../errors/crdt-error.js"; import { PNCounter } from "./pn-counter.js"; describe("PNCounter", () => { it("starts at 0", { timeout: 5000 }, () => { expect(new PNCounter().total()).toBe(0); }); it("increments", { timeout: 5000 }, () => { const c = new PNCounter(); c.increment("n1", 10); expect(c.total()).toBe(10); }); it("decrements", { timeout: 5000 }, () => { const c = new PNCounter(); c.increment("n1", 5); c.decrement("n1", 3); expect(c.total()).toBe(2); }); it("can go negative", { timeout: 5000 }, () => { const c = new PNCounter(); c.decrement("n1", 5); expect(c.total()).toBe(-5); }); it("throws for zero increment delta", { timeout: 5000 }, () => { expect(() => new PNCounter().increment("n1", 0)).toThrow(CrdtError); }); it("throws for negative decrement delta", { timeout: 5000 }, () => { expect(() => new PNCounter().decrement("n1", -1)).toThrow(CrdtError); }); it("decrement throws for zero delta", { timeout: 5000 }, () => { const err = (() => { try { new PNCounter().decrement("n1", 0); } catch (e) { return e; } })(); expect((err as CrdtError).code).toBe(CRDT_CODES.OPERATION_INVALID); }); it("merge", { timeout: 5000 }, () => { const a = new PNCounter(); a.increment("n1", 5); const b = new PNCounter(); b.increment("n2", 3); b.decrement("n1", 2); a.merge(b.state()); expect(a.total()).toBe(6); // 5+3-2 }); it("toJSON / fromJSON round-trip", { timeout: 5000 }, () => { const a = new PNCounter(); a.increment("n1", 10); a.decrement("n1", 4); const b = new PNCounter(); b.fromJSON(a.toJSON()); expect(b.total()).toBe(6); }); it("fromJSON throws for invalid type", { timeout: 5000 }, () => { expect(() => new PNCounter().fromJSON( JSON.stringify({ type: "wrong", increments: {}, decrements: {}, }) ) ).toThrow(CrdtError); }); it("fromJSON throws for bad JSON", { timeout: 5000 }, () => { expect(() => new PNCounter().fromJSON("bad")).toThrow(CrdtError); }); it("clone is independent", { timeout: 5000 }, () => { const a = new PNCounter(); a.increment("n1", 5); const b = a.clone(); b.increment("n1", 10); expect(a.total()).toBe(5); }); it("value() equals total()", { timeout: 5000 }, () => { const c = new PNCounter(); c.increment("n1", 7); expect(c.value()).toBe(c.total()); }); it("default delta=1 for increment", { timeout: 5000 }, () => { const c = new PNCounter(); c.increment("n1"); expect(c.total()).toBe(1); }); it("default delta=1 for decrement", { timeout: 5000 }, () => { const c = new PNCounter(); c.increment("n1", 5); c.decrement("n1"); expect(c.total()).toBe(4); }); it("merge does not regress lower remote increment", { timeout: 5000, }, () => { const a = new PNCounter(); a.increment("n1", 10); const b = new PNCounter(); b.increment("n1", 3); // lower than a a.merge(b.state()); expect(a.total()).toBe(10); // should not decrease }); it("merge does not regress lower remote decrement", { timeout: 5000, }, () => { const a = new PNCounter(); a.decrement("n1", 10); const b = new PNCounter(); b.decrement("n1", 3); // lower than a a.merge(b.state()); expect(a.total()).toBe(-10); // should not change }); });