tests/versions.test.ts
Rama main · réplica de lectura del repositorio original.
// Note versions without git (server/versions.ts, hooked into vault.writeNote).
//
// The promise under test is narrow and load-bearing: a write through the
// vault's one write path leaves the PREVIOUS content somewhere the product
// can reach, on an instance that has never heard of git. Everything else here
// is the shape of that promise — the collapse that keeps a burst of autosaves
// from being forty copies of one mistake, the caps that keep the store from
// being the disk's problem, the rename that carries a note's past with its
// name, and the restore that is itself a version.
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import path from "node:path";
import { after, before, beforeEach, describe, it } from "node:test";
import {
VERSIONS_PER_NOTE,
initVersions,
listVersions,
moveVersions,
readVersion,
resetVersionsForTests,
} from "../server/versions.ts";
import {
createNote,
deleteNote,
initVault,
moveFolder,
purgeFromTrash,
readNote,
renameNote,
restoreFromTrash,
writeNote,
} from "../server/vault.ts";
import { makeDir, makeVault, removeVault } from "./helpers/vault.ts";
const vault = makeVault({
"Essay.md": "first draft\n",
"Burst.md": "b0\n",
"Capped.md": "c0\n",
"Old Name.md": "named\n",
"Restore.md": "r0\n",
"Gone.md": "gone\n",
"ForGood.md": "for good\n",
"Folder/Inner.md": "inner\n",
"Big.md": "big\n",
"Off.md": "off\n",
});
const data = makeDir();
const storeDir = path.join(data, "versions");
/** Where the store keeps one note's versions — the contract's own spelling. */
const dirFor = (rel: string) => path.join(storeDir, createHash("sha1").update(rel).digest("hex"));
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
/** A collapse window short enough to test both sides of in one sitting. */
const WINDOW_MS = 120;
let enabled = true;
before(() => {
initVault(vault);
});
beforeEach(() => {
initVersions({ dir: storeDir, enabled: () => enabled, windowMs: WINDOW_MS });
});
after(() => {
resetVersionsForTests();
removeVault(vault);
removeVault(data);
});
describe("a write keeps what it replaced", () => {
it("stores the previous content under sha1(path)/<at>.md with an index", async () => {
const before = statSync(path.join(vault, "Essay.md")).mtimeMs;
await writeNote("Essay.md", "second draft\n");
const versions = await listVersions("Essay.md");
assert.equal(versions.length, 1);
const [v] = versions;
assert.equal(v.reason, "autosave");
assert.equal(v.mtimeMs, before, "the version carries the mtime of the text it holds");
assert.equal(v.size, Buffer.byteLength("first draft\n"));
const dir = dirFor("Essay.md");
assert.equal(readFileSync(path.join(dir, `${v.at}.md`), "utf8"), "first draft\n");
const index = JSON.parse(readFileSync(path.join(dir, "index.json"), "utf8"));
assert.equal(index.path, "Essay.md");
assert.equal(index.versions.length, 1);
// 0600 on both, like everything else in the data directory.
assert.equal(statSync(path.join(dir, `${v.at}.md`)).mode & 0o777, 0o600);
assert.equal(statSync(path.join(dir, "index.json")).mode & 0o777, 0o600);
assert.equal(await readVersion("Essay.md", v.at), "first draft\n");
});
it("keeps nothing for a new note, and nothing for a write that changes nothing", async () => {
await createNote("Fresh.md");
assert.deepEqual(await listVersions("Fresh.md"), []);
await sleep(WINDOW_MS + 20);
const current = (await readNote("Essay.md")).content;
await writeNote("Essay.md", current);
assert.equal((await listVersions("Essay.md")).length, 1, "same bytes are not a version");
});
it("leaves no temp file behind", async () => {
const names = readdirSync(dirFor("Essay.md"));
assert.ok(names.every((n) => !n.endsWith(".tmp")), names.join(", "));
});
it("answers null for a version that is not there", async () => {
assert.equal(await readVersion("Essay.md", 1), null);
assert.equal(await readVersion("Never.md", 1), null);
assert.deepEqual(await listVersions("Never.md"), []);
});
});
describe("a burst collapses into the text from before it", () => {
it("keeps the OLDER version and drops the ones inside the window", async () => {
await writeNote("Burst.md", "b1\n"); // keeps b0
await writeNote("Burst.md", "b2\n"); // inside the window: collapses
await writeNote("Burst.md", "b3\n"); // still inside
let versions = await listVersions("Burst.md");
assert.equal(versions.length, 1);
assert.equal(await readVersion("Burst.md", versions[0].at), "b0\n", "the copy from before the burst");
await sleep(WINDOW_MS + 20);
await writeNote("Burst.md", "b4\n"); // a new window: keeps b3
versions = await listVersions("Burst.md");
assert.equal(versions.length, 2);
assert.equal(await readVersion("Burst.md", versions[0].at), "b3\n");
assert.ok(versions[0].at > versions[1].at, "newest first");
});
});
describe("the per-note cap evicts the oldest", () => {
it(`holds ${VERSIONS_PER_NOTE} and forgets the first`, async () => {
// A zero window so every write is a version; the cap is the thing under test.
initVersions({ dir: storeDir, enabled: () => true, windowMs: 0 });
for (let i = 1; i <= VERSIONS_PER_NOTE + 3; i++) await writeNote("Capped.md", `c${i}\n`);
const versions = await listVersions("Capped.md");
assert.equal(versions.length, VERSIONS_PER_NOTE);
const oldest = versions.at(-1)!;
assert.equal(await readVersion("Capped.md", oldest.at), "c3\n", "c0..c2 were evicted");
const blobs = readdirSync(dirFor("Capped.md")).filter((n) => n.endsWith(".md"));
assert.equal(blobs.length, VERSIONS_PER_NOTE, "evicted blobs are removed from disk too");
});
});
describe("the store cap evicts the oldest across notes", () => {
it("brings the whole store under its ceiling", async () => {
const own = makeDir();
initVersions({ dir: path.join(own, "versions"), enabled: () => true, windowMs: 0, storeBytes: 70 });
await writeNote("Essay.md", "A".repeat(30) + "\n"); // keeps "second draft\n" (13 bytes)
await writeNote("Essay.md", "B".repeat(30) + "\n"); // keeps 31 bytes → 44
await writeNote("Essay.md", "C".repeat(30) + "\n"); // keeps 31 bytes → 75 → the 13 go
const versions = await listVersions("Essay.md");
const total = versions.reduce((n, v) => n + v.size, 0);
assert.ok(total <= 70, `store holds ${total} bytes`);
assert.equal(versions.length, 2);
assert.equal(await readVersion("Essay.md", versions.at(-1)!.at), "A".repeat(30) + "\n");
removeVault(own);
});
});
describe("a note past the size ceiling is not versioned", () => {
it("skips a 2 MB note", async () => {
await writeNote("Big.md", "x".repeat(2 * 1024 * 1024 + 1)); // keeps the small "big\n"
assert.equal((await listVersions("Big.md")).length, 1);
await sleep(WINDOW_MS + 20);
await writeNote("Big.md", "small again\n");
assert.equal((await listVersions("Big.md")).length, 1, "the 2 MB content was over the ceiling");
});
});
describe("the switch", () => {
it("keeps nothing while off, and resumes when on — read at every write", async () => {
enabled = false;
await writeNote("Off.md", "off1\n");
assert.deepEqual(await listVersions("Off.md"), []);
enabled = true;
await writeNote("Off.md", "off2\n");
assert.equal((await listVersions("Off.md")).length, 1);
});
it("is a no-op with no store initialised (the durability suite's world)", async () => {
resetVersionsForTests();
await writeNote("Off.md", "off3\n");
assert.deepEqual(await listVersions("Off.md"), []);
assert.equal((await readNote("Off.md")).content, "off3\n", "the write itself is untouched");
});
});
describe("deleted for good means the versions too", () => {
it("drops a note's versions on a permanent delete", async () => {
await writeNote("ForGood.md", "for good 2\n");
assert.equal((await listVersions("ForGood.md")).length, 1);
await deleteNote("ForGood.md", { permanent: true });
assert.equal((await listVersions("ForGood.md")).length, 0);
assert.equal(existsSync(dirFor("ForGood.md")), false);
});
});
describe("a vault-wide replace keeps the text before it", () => {
it("is not an autosave: the window does not swallow it", async () => {
await writeNote("Burst.md", "b1\n");
const before = (await listVersions("Burst.md")).length;
await writeNote("Burst.md", "b2\n", undefined, "bulk");
assert.equal((await listVersions("Burst.md")).length, before + 1);
});
});
describe("a rename moves the folder", () => {
it("carries the versions to the new path and rewrites the index", async () => {
await writeNote("Old Name.md", "renamed soon\n");
const oldDir = dirFor("Old Name.md");
assert.ok(existsSync(oldDir));
await renameNote("Old Name.md", "New Name.md");
assert.ok(!existsSync(oldDir), "nothing left under the old hash");
const versions = await listVersions("New Name.md");
assert.equal(versions.length, 1);
assert.equal(await readVersion("New Name.md", versions[0].at), "named\n");
const index = JSON.parse(readFileSync(path.join(dirFor("New Name.md"), "index.json"), "utf8"));
assert.equal(index.path, "New Name.md");
});
it("merges into a destination that already holds versions", async () => {
await sleep(WINDOW_MS + 20);
await writeNote("Essay.md", "merge me\n");
const mine = (await listVersions("Essay.md")).length;
assert.ok(mine >= 1);
await moveVersions("New Name.md", "Essay.md");
const merged = await listVersions("Essay.md");
assert.equal(merged.length, mine + 1);
assert.ok(!existsSync(dirFor("New Name.md")));
});
it("follows every note in a moved folder", async () => {
await writeNote("Folder/Inner.md", "inner 2\n");
await moveFolder("Folder", "Moved");
assert.deepEqual(await listVersions("Folder/Inner.md"), []);
const versions = await listVersions("Moved/Inner.md");
assert.equal(versions.length, 1);
assert.equal(await readVersion("Moved/Inner.md", versions[0].at), "inner\n");
});
});
describe("a restore writes through the same path", () => {
it("puts the version back and keeps the replaced text as a 'restore' version, outside the window", async () => {
await writeNote("Restore.md", "r1\n"); // keeps r0
const [v0] = await listVersions("Restore.md");
const content = await readVersion("Restore.md", v0.at);
assert.equal(content, "r0\n");
// Immediately — inside the collapse window — as the route does it.
await writeNote("Restore.md", content!, undefined, "restore");
assert.equal((await readNote("Restore.md")).content, "r0\n");
const versions = await listVersions("Restore.md");
assert.equal(versions.length, 2, "a restore is exempt from the collapse");
assert.equal(versions[0].reason, "restore");
assert.equal(await readVersion("Restore.md", versions[0].at), "r1\n", "the text the restore replaced");
});
});
describe("the trash", () => {
it("keeps a deleted note's versions until the entry is purged", async () => {
await writeNote("Gone.md", "gone 2\n");
const { trashPath } = await deleteNote("Gone.md");
assert.ok(trashPath);
assert.equal((await listVersions("Gone.md")).length, 1, "a delete keeps them");
await purgeFromTrash(path.posix.basename(trashPath!));
assert.deepEqual(await listVersions("Gone.md"), []);
assert.ok(!existsSync(dirFor("Gone.md")));
});
it("spares the versions of a live note that took the path again", async () => {
await writeNote("Gone.md", "reborn\n");
await writeNote("Gone.md", "reborn 2\n"); // keeps "reborn"
const { trashPath } = await deleteNote("Gone.md");
// A NEW note at the same path, with a history of its own ("third life").
await writeNote("Gone.md", "third life\n");
await writeNote("Gone.md", "third life 2\n");
const beforePurge = await listVersions("Gone.md");
await purgeFromTrash(path.posix.basename(trashPath!));
assert.deepEqual(await listVersions("Gone.md"), beforePurge, "the live note's history is its own");
});
it("moves the versions when a restore has to land beside a taken origin", async () => {
// "Gone.md" is live again; trash another copy of it and restore that.
await writeNote("Gone.md", "to be trashed\n");
const { trashPath } = await deleteNote("Gone.md");
await writeNote("Gone.md", "taken\n"); // origin taken by a fresh note
const result = await restoreFromTrash(path.posix.basename(trashPath!));
assert.equal(result.renamed, true);
assert.equal(result.path, "Gone-2.md");
assert.ok(existsSync(dirFor("Gone-2.md")), "the origin's versions followed the restore");
});
});