/** * Persistence Tests (Section 2 of TESTS.md) * * Tests debouncedSave, local file read/write, and flushAll. * Uses a temporary directory to avoid polluting real data. */ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdirSync, existsSync, readFileSync, writeFileSync } from "fs"; import { mkdtempSync, rmSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import * as Y from "yjs"; import { setDataDir, getDataDir, docPath } from "../src/utils.js"; import { debouncedSave, resetSaveTimers } from "../src/create-app.js"; let tmpDir: string; beforeEach(() => { tmpDir = mkdtempSync(join(tmpdir(), "collab-test-")); mkdirSync(tmpDir, { recursive: true }); setDataDir(tmpDir); }); afterEach(() => { resetSaveTimers(); setDataDir(undefined); try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} }); function makeDoc(text: string): Y.Doc { const ydoc = new Y.Doc(); const fragment = ydoc.getXmlFragment("default"); const el = new Y.XmlElement("paragraph"); el.insert(0, [new Y.XmlText(text)]); fragment.insert(0, [el]); return ydoc; } describe("2.1 Local persistence", () => { it("2.1.1 debouncedSave writes .yjs file after debounce", async () => { const ydoc = makeDoc("Hello world"); debouncedSave("test-doc", ydoc); // File should NOT exist immediately (debounce is 2s) const p = docPath("test-doc"); expect(existsSync(p)).toBe(false); // Wait for debounce to fire await new Promise((r) => setTimeout(r, 2500)); expect(existsSync(p)).toBe(true); const buf = readFileSync(p); expect(buf.length).toBeGreaterThan(0); // Verify it's valid Yjs binary by applying it to a new doc const restored = new Y.Doc(); Y.applyUpdate(restored, new Uint8Array(buf)); const fragment = restored.getXmlFragment("default"); expect(fragment.length).toBeGreaterThan(0); }); it("2.1.2 docPath reads from configured DATA_DIR", () => { // Write a .yjs file manually, then verify docPath points to it const ydoc = makeDoc("Existing content"); const state = Y.encodeStateAsUpdate(ydoc); const p = docPath("existing"); writeFileSync(p, Buffer.from(state)); // Read it back and verify content expect(existsSync(p)).toBe(true); const buf = readFileSync(p); const restored = new Y.Doc(); Y.applyUpdate(restored, new Uint8Array(buf)); const fragment = restored.getXmlFragment("default"); expect(fragment.length).toBeGreaterThan(0); }); it("2.1.3 debounce collapses rapid saves into one write", async () => { const ydoc = makeDoc("Version 1"); // Trigger 3 rapid saves debouncedSave("rapid", ydoc); const ydoc2 = makeDoc("Version 2"); debouncedSave("rapid", ydoc2); const ydoc3 = makeDoc("Version 3"); debouncedSave("rapid", ydoc3); const p = docPath("rapid"); // Nothing written yet expect(existsSync(p)).toBe(false); // Wait for the single debounced write await new Promise((r) => setTimeout(r, 2500)); expect(existsSync(p)).toBe(true); // The file should contain the last version (ydoc3) const buf = readFileSync(p); const restored = new Y.Doc(); Y.applyUpdate(restored, new Uint8Array(buf)); const fragment = restored.getXmlFragment("default"); expect(fragment.length).toBeGreaterThan(0); }); });