import { describe, expect, it } from "vitest"; import { between, compare, first, isValid, rebalancePositions } from "../positions.js"; describe("positions (base-62 fractional)", () => { it("first() and between() with no neighbours give canonical start/end", () => { expect(first()).toBe("A"); expect(between()).toBe("A"); const afterA = between("A"); expect(compare("A", afterA)).toBe(-1); }); it("between(a, b) always returns a position strictly between", () => { const a = "B"; const b = "Z"; const mid = between(a, b); expect(compare(a, mid)).toBe(-1); expect(compare(mid, b)).toBe(-1); }); it("generates a sortable chain of 100 successive insertions", () => { let prev: string | undefined; let next: string | undefined; const positions: string[] = []; // repeatedly insert between the last two to create a descending staircase for (let i = 0; i < 100; i++) { const p = between(prev, next); positions.push(p); prev = p; } // every consecutive pair is strictly ordered for (let i = 0; i < positions.length - 1; i++) { expect(compare(positions[i]!, positions[i + 1]!)).toBe(-1); } }); it("handles deep nesting: 50 levels of between(first, next)", () => { let a: string | undefined; let b: string | undefined; for (let depth = 0; depth < 50; depth++) { const mid = between(a, b); expect(isValid(mid)).toBe(true); if (a && b) { expect(compare(a, mid)).toBe(-1); expect(compare(mid, b)).toBe(-1); } a = mid; b = undefined; } // still a valid, ordered chain expect(isValid(a!)).toBe(true); }); it("grows precision as needed when neighbours are adjacent cells", () => { // "0" and "1" are adjacent on the 1-digit grid; between must extend precision const mid = between("0", "1"); expect(compare("0", mid)).toBe(-1); expect(compare(mid, "1")).toBe(-1); // and again between 0 and that midpoint (twice-adjacent) const mid2 = between("0", mid); expect(compare("0", mid2)).toBe(-1); expect(compare(mid2, mid)).toBe(-1); }); it("before(a) positions sort below a, after(b) sort above b", () => { const before = between(undefined, "A"); expect(compare(before, "A")).toBe(-1); const after = between("A"); expect(compare("A", after)).toBe(-1); }); it("compare is a total order (antisymmetric, transitive)", () => { const xs = ["0", "1", "A", "A0", "AA", "a", "z", "Z", "9z"]; for (const x of xs) { expect(compare(x, x)).toBe(0); } for (const x of xs) { for (const y of xs) { if (x === y) continue; const c = compare(x, y); expect([-1, 1]).toContain(c); expect(compare(y, x)).toBe(-c as -1 | 1); } } }); it("isValid accepts base-62 strings and rejects garbage", () => { expect(isValid("A")).toBe(true); expect(isValid("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")).toBe(true); expect(isValid("")).toBe(false); expect(isValid("A B")).toBe(false); expect(isValid("A!")).toBe(false); expect(isValid(".")).toBe(false); }); it("random insertions stay globally ordered (stress: 300 inserts)", () => { const pool: string[] = ["A"]; for (let i = 0; i < 300; i++) { const idx = Math.floor(Math.random() * pool.length); const a = idx > 0 ? pool[idx - 1] : undefined; const b = idx < pool.length ? pool[idx] : undefined; const p = between(a, b); pool.splice(idx, 0, p); } for (let i = 0; i < pool.length - 1; i++) { expect(compare(pool[i]!, pool[i + 1]!)).toBe(-1); expect(pool[i] !== pool[i + 1]).toBe(true); } }); it("throws when a must sort before b is violated", () => { expect(() => between("Z", "A")).toThrow(RangeError); }); it("append (between(a)) extends with a midpoint char instead of converging on the top", () => { // The old rule took the midpoint between `a` and the grid top, which // converged on "zzzz…" (one more z per append) and threw after ~127 // appends — the 93-page/16K-word paste 500. The new rule appends one // midpoint char per call: strictly greater than `a`, no convergence, // and the top ("zzz…") is never approached. expect(between("A")).toBe("AV"); expect(between("AV")).toBe("AVV"); // 127 appends from "A": length grows 1 → 128, every step strictly // ordered, and the tail never converges toward "z" (last char = 'V'). let a = "A"; for (let i = 0; i < 127; i++) { const next = between(a); expect(compare(a, next)).toBe(-1); expect(next.endsWith("V")).toBe(true); a = next; } expect(a.length).toBe(128); // At the documented precision cap the engine asks for a rebalance // (the service layer performs it in-transaction and retries). expect(() => between(a)).toThrow(RangeError); }); it("append then insert-after-appended-tail still splits (headroom below)", () => { // Append twice, then insert BETWEEN the last two — the midpoint rule must // find room below the tail (the old top-midpoint rule left none). const p1 = between(undefined); // "A" const p2 = between(p1); // "AV" const p3 = between(p2); // "AVV" const mid = between(p2, p3); expect(compare(p2, mid)).toBe(-1); expect(compare(mid, p3)).toBe(-1); }); it("rebalancePositions produces an evenly spaced, strictly ordered list", () => { // Simulated exhausted list: consecutive appends under the NEW rule until // the precision cap, hand rebalancePositions an ascending run and check // the result (the service calls this when between() throws). const input: string[] = []; let a: string | undefined; while (true) { try { a = between(a); } catch { break; } input.push(a!); } expect(input.length).toBe(128); const fresh = rebalancePositions(input); expect(fresh.length).toBe(128); const len = fresh[0]!.length; for (const p of fresh) { expect(isValid(p)).toBe(true); expect(p.length).toBe(len); // one uniform grid precision } for (let i = 0; i < fresh.length - 1; i++) { expect(compare(fresh[i]!, fresh[i + 1]!)).toBe(-1); } // Every fresh slot is splittable: the neighbours have room to split. const mid = between(fresh[1]!, fresh[2]!); expect(compare(fresh[1]!, mid)).toBe(-1); expect(compare(mid, fresh[2]!)).toBe(-1); }); it("rebalancePositions repairs duplicate positions into adjacent slots", () => { const fresh = rebalancePositions(["A", "A", "A"]); expect(fresh.length).toBe(3); for (let i = 0; i < fresh.length - 1; i++) { expect(compare(fresh[i]!, fresh[i + 1]!)).toBe(-1); } }); it("rebalancePositions edge cases", () => { expect(rebalancePositions([])).toEqual([]); // A single row gets the one cell of the smallest grid (L=1). expect(rebalancePositions(["A"])).toEqual(["V"]); // 10k duplicate positions → 10k distinct ordered slots, no throw. const fresh = rebalancePositions(new Array(10_000).fill("A")); expect(fresh.length).toBe(10_000); for (let i = 0; i < fresh.length - 1; i++) { expect(compare(fresh[i]!, fresh[i + 1]!)).toBe(-1); } }); });