CultLib/packages/cultcache-ts/test/store-file.test.ts
Meta Vens 289d7f7d49 cultcache-ts: pin the default write path's sync and close order, and a directory at the lock path failing fast
The default StoreFileOps were never observed. One test now wraps fs/promises.open and each handle's
sync and close, and records the store's bytes at every call: the staged file is synced and closed
while the store still holds its old bytes, and on Unix the directory is synced and closed after the
store changed. A second test puts a directory at <store>.lock and requires the lock to fail within
3 s with the lock path named, under the default 30 s wait bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:42:24 +02:00

716 lines
29 KiB
TypeScript

import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mkdirSync, readFileSync } from "node:fs";
import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
import fsp = require("node:fs/promises");
import { tmpdir } from "node:os";
import { basename, join, resolve } from "node:path";
import test from "node:test";
import { promisify } from "node:util";
import { decode, encode } from "@msgpack/msgpack";
import { documentPayloadBytes, documentPayloadFromBytes } from "../src/document";
import { SingleFileMessagePackBackingStore } from "../src/single-file-messagepack-backing-store";
import type { CultCacheEnvelope } from "../src/types";
import {
MOVEFILE_REPLACE_WRITE_THROUGH,
StoreReplacedNotDurableError,
commitStoreFile,
nodeStoreFileOps,
replaceStoreFile,
replaceStoreFileWindows,
windowsKernel,
withStoreWriteLock,
} from "../src/store-file";
import type { ReplaceKernel, StoreFileOps } from "../src/store-file";
const execFileAsync = promisify(execFile);
async function scratch(): Promise<string> {
return mkdtemp(join(tmpdir(), "cultcache-store-file-"));
}
function recordingOps(calls: string[], overrides: Partial<StoreFileOps> = {}): StoreFileOps {
const real = nodeStoreFileOps;
return {
createExclusive: async (path) => {
calls.push("create");
return real.createExclusive(path);
},
write: async (handle, bytes) => {
calls.push("write");
await real.write(handle, bytes);
},
sync: async (handle) => {
calls.push("sync");
await real.sync(handle);
},
close: async (handle) => {
calls.push("close");
await real.close(handle);
},
replace: async (staged, destination) => {
calls.push("replace");
await real.replace(staged, destination);
},
syncDirectory: async (directory) => {
calls.push("syncDirectory");
await real.syncDirectory(directory);
},
remove: async (path) => {
calls.push("remove");
await real.remove(path);
},
...overrides,
};
}
test("a commit syncs the staged file before it replaces the store, and the directory after (Unix)", async () => {
const directory = await scratch();
try {
const store = join(directory, "store.cc");
const calls: string[] = [];
await commitStoreFile(store, new Uint8Array([1, 2, 3]), recordingOps(calls));
const expected = ["create", "write", "sync", "close", "replace"];
if (process.platform !== "win32") {
expected.push("syncDirectory");
}
assert.deepEqual(calls, expected);
assert.deepEqual([...(await readFile(store))], [1, 2, 3]);
assert.deepEqual(await readdir(directory), ["store.cc"]);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
// The default write path, with nothing injected: every file handle it opens is observed through
// fs/promises.open (the module object, read at call time), recording what the handle did and what the
// store held at that moment. The replace is seen as the store's bytes changing, so the order holds on
// every platform without a seam in the replace itself.
test("the default write path syncs and closes the staged file before the store changes, and syncs the directory after (Unix)", async () => {
const directory = await scratch();
const realOpen = fsp.open;
try {
const store = join(directory, "store.cc");
await writeFile(store, "old");
const storeNow = () => readFileSync(store, "utf8");
const events: string[] = [];
(fsp as { open: typeof fsp.open }).open = (async (path: string, flags?: string) => {
const handle = await realOpen(path, flags as never);
const kind = basename(String(path)) === basename(directory) ? "dir" : String(path).includes(".tmp-") ? "staged" : "other";
const sync = handle.sync.bind(handle);
const close = handle.close.bind(handle);
handle.sync = () => {
events.push(`sync:${kind}:${storeNow()}`);
return sync();
};
handle.close = () => {
events.push(`close:${kind}:${storeNow()}`);
return close();
};
return handle;
}) as typeof fsp.open;
await commitStoreFile(store, new TextEncoder().encode("new"));
(fsp as { open: typeof fsp.open }).open = realOpen;
// The staged file is synced, then closed, while the store still holds its old bytes.
assert.deepEqual(events.filter((event) => event.includes(":staged:")), ["sync:staged:old", "close:staged:old"]);
if (process.platform === "win32") {
assert.deepEqual(events.filter((event) => event.includes(":dir:")), [], "Windows relies on the replace and opens no directory");
} else {
// The directory is synced and closed after the store changed.
assert.deepEqual(events.filter((event) => event.includes(":dir:")), ["sync:dir:new", "close:dir:new"]);
}
assert.equal(storeNow(), "new");
} finally {
(fsp as { open: typeof fsp.open }).open = realOpen;
await rm(directory, { recursive: true, force: true });
}
});
test("a commit that fails before the replace leaves the store byte for byte and no staged file behind", async () => {
const directory = await scratch();
try {
const store = join(directory, "store.cc");
await writeFile(store, Buffer.from([9, 9, 9]));
const calls: string[] = [];
const ops = recordingOps(calls, {
replace: async () => {
calls.push("replace");
throw new Error("replace refused");
},
});
await assert.rejects(commitStoreFile(store, new Uint8Array([1]), ops), /replace refused/);
assert.deepEqual([...(await readFile(store))], [9, 9, 9]);
assert.deepEqual(await readdir(directory), ["store.cc"]);
assert.ok(calls.includes("remove"));
assert.equal(calls.filter((call) => call === "close").length, 1, "the staged handle is closed exactly once");
assert.ok(!calls.includes("syncDirectory"));
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("a commit whose staged file cannot be synced never replaces the store", async () => {
const directory = await scratch();
try {
const store = join(directory, "store.cc");
await writeFile(store, Buffer.from([9]));
const calls: string[] = [];
const ops = recordingOps(calls, {
sync: async () => {
calls.push("sync");
throw new Error("disk full");
},
});
await assert.rejects(commitStoreFile(store, new Uint8Array([1]), ops), /disk full/);
assert.ok(!calls.includes("replace"));
assert.ok(calls.indexOf("close") !== -1 && calls.indexOf("close") < calls.indexOf("remove"), "the staged handle is closed, then the file removed");
assert.deepEqual([...(await readFile(store))], [9]);
assert.deepEqual(await readdir(directory), ["store.cc"]);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test(
"a directory sync failure after the replace reports the store replaced but not durable",
{ skip: process.platform === "win32" },
async () => {
const directory = await scratch();
try {
const store = join(directory, "store.cc");
await writeFile(store, Buffer.from([9]));
const ops = recordingOps([], {
syncDirectory: async () => {
throw new Error("EIO");
},
});
await assert.rejects(commitStoreFile(store, new Uint8Array([1]), ops), (error: unknown) => {
assert.ok(error instanceof StoreReplacedNotDurableError);
assert.equal(error.replaced, true);
assert.equal(error.name, "StoreReplacedNotDurableError");
assert.match(error.message, /was replaced/);
return true;
});
assert.deepEqual([...(await readFile(store))], [1]);
} finally {
await rm(directory, { recursive: true, force: true });
}
},
);
function fakeKernel(
posix: number[],
moveEx: number[],
): { kernel: ReplaceKernel; log: string[]; sleeps: number[] } {
const log: string[] = [];
const sleeps: number[] = [];
const kernel: ReplaceKernel = {
renamePosix: () => {
log.push("posix");
return posix.shift() ?? 0;
},
moveFileEx: (_staged, _destination, flags) => {
log.push(`moveex:${flags}`);
return moveEx.shift() ?? 0;
},
sleep: async (ms) => {
sleeps.push(ms);
},
};
return { kernel, log, sleeps };
}
test("the Windows replace falls back to write-through MoveFileExW where POSIX rename is unsupported, retries 5/32/33, and gives up after 25 attempts", async () => {
for (const unsupported of [1, 50, 87]) {
const { kernel, log } = fakeKernel([unsupported], [0]);
await replaceStoreFileWindows("a", "b", kernel);
assert.deepEqual(log, ["posix", `moveex:${MOVEFILE_REPLACE_WRITE_THROUGH}`]);
}
assert.equal(MOVEFILE_REPLACE_WRITE_THROUGH, 0x9);
// Once POSIX is unsupported the rest of the call stays on MoveFileExW.
{
const { kernel, log } = fakeKernel([87], [32, 0]);
await replaceStoreFileWindows("a", "b", kernel);
assert.deepEqual(log, ["posix", "moveex:9", "moveex:9"]);
}
for (const transient of [5, 32, 33]) {
const { kernel, log, sleeps } = fakeKernel([transient, transient, 0], []);
await replaceStoreFileWindows("a", "b", kernel);
assert.deepEqual(log, ["posix", "posix", "posix"], "a transient error retries the same primitive");
assert.deepEqual(sleeps, [20, 20]);
}
// A POSIX rename that works is the whole replace.
{
const { kernel, log, sleeps } = fakeKernel([0], []);
await replaceStoreFileWindows("a", "b", kernel);
assert.deepEqual(log, ["posix"]);
assert.deepEqual(sleeps, []);
}
// Once on MoveFileExW, an "unsupported" code is an error in any later attempt, never another fallback.
{
const { kernel, log } = fakeKernel([87], [32, 87]);
await assert.rejects(replaceStoreFileWindows("a", "b", kernel), /Win32 error 87/);
assert.deepEqual(log, ["posix", "moveex:9", "moveex:9"]);
}
// After the fallback, an "unsupported" code is an error, not another fallback.
for (const unsupported of [1, 50, 87]) {
const { kernel, log } = fakeKernel([87], [unsupported]);
await assert.rejects(replaceStoreFileWindows("a", "b", kernel), new RegExp(`Win32 error ${unsupported}`));
assert.deepEqual(log, ["posix", "moveex:9"]);
}
{
const { kernel, log } = fakeKernel(Array(30).fill(32), []);
await assert.rejects(replaceStoreFileWindows("a", "b", kernel), /after 25 attempts.*32/);
assert.equal(log.length, 25);
}
{
const { kernel, log } = fakeKernel([2], []);
await assert.rejects(replaceStoreFileWindows("a", "b", kernel), /Win32 error 2/);
assert.equal(log.length, 1);
}
{
const { kernel } = fakeKernel([87], [3]);
await assert.rejects(replaceStoreFileWindows("a", "b", kernel), /Win32 error 3/);
}
});
// The reader is a koffi CreateFileW holding the store sharing read, write and delete, the way
// the C# and Rust readers hold it.
function openSharingAll(path: string): { close: () => void } {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const koffi = require("koffi");
const lib = koffi.load("kernel32.dll");
const createFileW = lib.func(
"intptr_t CreateFileW(str16 name, uint32_t access, uint32_t share, void *security, uint32_t disposition, uint32_t flags, void *template)",
);
const closeHandle = lib.func("int CloseHandle(intptr_t handle)");
const handle = Number(createFileW(path, 0x80000000, 0x7, null, 3, 0x80, null));
assert.notEqual(handle, -1, "the reader could not open the store");
let closed = false;
return { close: () => { if (!closed) { closed = true; closeHandle(handle); } } };
}
function openSharingReadWrite(path: string): { close: () => void } {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const koffi = require("koffi");
const lib = koffi.load("kernel32.dll");
const createFileW = lib.func(
"intptr_t CreateFileW(str16 name, uint32_t access, uint32_t share, void *security, uint32_t disposition, uint32_t flags, void *template)",
);
const closeHandle = lib.func("int CloseHandle(intptr_t handle)");
const handle = Number(createFileW(path, 0x80000000, 0x3, null, 3, 0x80, null));
assert.notEqual(handle, -1, "the reader could not open the store");
let closed = false;
return { close: () => { if (!closed) { closed = true; closeHandle(handle); } } };
}
test(
"windows: a store replace lands while a reader holds the store sharing delete, as C# and Rust readers do",
{ skip: process.platform !== "win32" },
async () => {
const directory = await scratch();
try {
const store = join(directory, "store.cc");
const staged = join(directory, "staged.tmp");
await writeFile(store, Buffer.from([1, 1, 1]));
await writeFile(staged, Buffer.from([2, 2, 2]));
const reader = openSharingAll(store);
try {
await replaceStoreFile(staged, store);
assert.deepEqual([...(await readFile(store))], [2, 2, 2]);
} finally {
reader.close();
}
} finally {
await rm(directory, { recursive: true, force: true });
}
},
);
test(
"windows: a replace waits out a reader that holds the store without delete sharing",
{ skip: process.platform !== "win32" },
async () => {
const directory = await scratch();
try {
const store = join(directory, "store.cc");
const staged = join(directory, "staged.tmp");
await writeFile(store, Buffer.from([1, 1, 1]));
await writeFile(staged, Buffer.from([2, 2, 2]));
const reader = openSharingReadWrite(store);
const release = setTimeout(() => reader.close(), 120);
try {
await replaceStoreFile(staged, store);
assert.deepEqual([...(await readFile(store))], [2, 2, 2]);
} finally {
clearTimeout(release);
reader.close();
}
} finally {
await rm(directory, { recursive: true, force: true });
}
},
);
// ---------------------------------------------------------------------------------------------
// The cross-process write lock (TypeScript against TypeScript; C# and Rust are in cult-cache.test.ts).
const entryOf = (key: string, type = "t", marker = 0): CultCacheEnvelope => ({
key,
type,
payload: documentPayloadFromBytes(Uint8Array.of(0x91, marker)),
storedAt: "2026-10-20T00:00:00Z",
});
function sleepFor(milliseconds: number): Promise<void> {
return new Promise((resolveSleep) => setTimeout(resolveSleep, milliseconds));
}
function within<T>(promise: Promise<T>, milliseconds: number, what: string): Promise<T> {
let timer: NodeJS.Timeout | undefined;
const expired = new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => reject(new Error(`${what} did not finish within ${milliseconds} ms (deadlock)`)), milliseconds);
});
return Promise.race([promise, expired]).finally(() => clearTimeout(timer));
}
test("a second writer in this process waits for the store lock and runs after release", async () => {
const directory = await scratch();
try {
const store = join(directory, "store.cc");
const order: string[] = [];
let second: Promise<void> | undefined;
await withStoreWriteLock(store, async () => {
second = withStoreWriteLock(store, async () => {
order.push("second");
});
await sleepFor(150);
order.push("first-done");
});
await second;
assert.deepEqual(order, ["first-done", "second"]);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("the store lock gives up after its wait bound and names the lock path", async () => {
const directory = await scratch();
try {
const store = join(directory, "store.cc");
await withStoreWriteLock(store, async () => {
await assert.rejects(
withStoreWriteLock(store, async () => undefined, { timeoutMs: 200 }),
(error: Error) => error.message.includes(`${store}.lock`) && /200 ms/.test(error.message),
);
});
// The failed waiter released nothing it did not hold: the lock is free again.
assert.equal(await withStoreWriteLock(store, async () => "free", { timeoutMs: 200 }), "free");
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("a directory at the lock path fails at once with the lock path named, not after the 30 s wait", async () => {
const directory = await scratch();
try {
const store = join(directory, "store.cc");
mkdirSync(`${store}.lock`);
const started = Date.now();
let ran = false;
// The default wait bound (30 s) is in force: only a failure that does not wait can beat the race.
const outcome = await Promise.race([
withStoreWriteLock(store, async () => {
ran = true;
}).then(
() => ({ error: undefined as Error | undefined }),
(error: Error) => ({ error }),
),
new Promise<"waiting">((resolveWait) => setTimeout(() => resolveWait("waiting"), 3000).unref()),
]);
assert.notEqual(outcome, "waiting", "the lock attempt was still waiting after 3 s");
const { error } = outcome as { error: Error | undefined };
assert.ok(error, "locking a directory must fail");
assert.ok(error.message.includes(`${store}.lock`), error.message);
assert.ok(!/timed out/.test(error.message), error.message);
assert.ok(Date.now() - started < 3000);
assert.equal(ran, false);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("the store lock is released when the operation throws", async () => {
const directory = await scratch();
try {
const store = join(directory, "store.cc");
await assert.rejects(withStoreWriteLock(store, async () => Promise.reject(new Error("boom"))), /boom/);
assert.equal(await withStoreWriteLock(store, async () => "free", { timeoutMs: 200 }), "free");
} finally {
await rm(directory, { recursive: true, force: true });
}
});
const legacyObjectPayload = (): Uint8Array =>
encode([{ key: "note:hello", type: "note", payload: { title: "Hello" }, storedAt: "2026-10-20T00:00:00Z" }]);
test("an open that heals a legacy store waits for the store lock and heals from its own read", async () => {
const directory = await scratch();
try {
const path = join(directory, "legacy.cc");
const legacy = legacyObjectPayload();
await writeFile(path, legacy);
const store = new SingleFileMessagePackBackingStore(path);
let healed: Promise<unknown> | undefined;
let settled = false;
await withStoreWriteLock(path, async () => {
healed = store.pullAll().then(() => {
settled = true;
});
await sleepFor(300);
assert.equal(settled, false, "the heal must wait for the lock");
assert.deepEqual([...(await readFile(path))], [...legacy], "the file keeps its legacy bytes while the lock is held elsewhere");
});
await within(healed!, 5000, "the heal");
assert.equal((decode(await readFile(path)) as unknown[])[0], "cultcache.store.v1");
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("push and delete over a legacy store that needs healing finish instead of waiting on their own lock", async () => {
const directory = await scratch();
try {
const path = join(directory, "legacy.cc");
await writeFile(path, legacyObjectPayload());
const store = new SingleFileMessagePackBackingStore(path);
await within(store.push(entryOf("a")), 5000, "push");
await writeFile(path, legacyObjectPayload());
await within(store.delete(entryOf("a")), 5000, "delete");
const keys = (await store.pullAll()).map((entry) => entry.key);
assert.deepEqual(keys, ["note:hello"]);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("two TypeScript processes pushing distinct keys to one store lose none", async () => {
const directory = await scratch();
try {
const path = join(directory, "store.cc");
const dist = resolve(__dirname, "..", "..", "dist");
const script = `
const { SingleFileMessagePackBackingStore } = require(${JSON.stringify(join(dist, "single-file-messagepack-backing-store.js"))});
const { documentPayloadFromBytes } = require(${JSON.stringify(join(dist, "document.js"))});
(async () => {
const store = new SingleFileMessagePackBackingStore(${JSON.stringify(path)});
for (let i = 0; i < 40; i++) {
await store.push({ key: process.argv[1] + "-" + i, type: "t", payload: documentPayloadFromBytes(Uint8Array.of(0x90)), storedAt: "2026-10-20T00:00:00Z" });
}
})().catch((error) => { console.error(error); process.exit(1); });
`;
await Promise.all(
["p", "q"].map((name) => execFileAsync(process.execPath, ["-e", script, name], { timeout: 120_000 })),
);
const keys = (await new SingleFileMessagePackBackingStore(path).pullAll()).map((entry) => entry.key).sort();
assert.equal(keys.length, 80);
assert.equal(new Set(keys).size, 80);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
const snapshotOf = async (store: SingleFileMessagePackBackingStore): Promise<string[]> =>
(await store.pullAll())
.map((entry) => `${entry.type}/${entry.key}:${[...documentPayloadBytes(entry.payload)].at(-1)}`)
.sort();
test("push replaces the record at the same type and key and keeps every other", async () => {
const directory = await scratch();
try {
const store = new SingleFileMessagePackBackingStore(join(directory, "store.cc"));
await store.pushAll([entryOf("a", "t", 1), entryOf("b", "t", 2), entryOf("a", "u", 3)]);
await store.push(entryOf("a", "t", 9));
assert.deepEqual(await snapshotOf(store), ["t/a:9", "t/b:2", "u/a:3"]);
await store.push(entryOf("c", "t", 4));
assert.deepEqual(await snapshotOf(store), ["t/a:9", "t/b:2", "t/c:4", "u/a:3"]);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("delete removes only the record at that type and key", async () => {
const directory = await scratch();
try {
const store = new SingleFileMessagePackBackingStore(join(directory, "store.cc"));
await store.pushAll([entryOf("a", "t", 1), entryOf("b", "t", 2), entryOf("a", "u", 3)]);
await store.delete(entryOf("a", "t"));
assert.deepEqual(await snapshotOf(store), ["t/b:2", "u/a:3"]);
await store.delete(entryOf("zzz", "t"));
assert.deepEqual(await snapshotOf(store), ["t/b:2", "u/a:3"]);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("pushAll replaces the whole store, and a soft pushAll leaves an existing store alone", async () => {
const directory = await scratch();
try {
const path = join(directory, "store.cc");
const store = new SingleFileMessagePackBackingStore(path);
await store.pushAll([entryOf("a", "t", 1)], { soft: true });
assert.deepEqual(await snapshotOf(store), ["t/a:1"]);
const before = await readFile(path);
await store.pushAll([entryOf("b", "t", 2)], { soft: true });
assert.deepEqual([...(await readFile(path))], [...before], "soft leaves an existing store byte for byte");
await store.pushAll([entryOf("b", "t", 2)]);
assert.deepEqual(await snapshotOf(store), ["t/b:2"]);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("a read of a current store never waits for the store lock, and a missing store reads empty", async () => {
const directory = await scratch();
try {
const path = join(directory, "store.cc");
const store = new SingleFileMessagePackBackingStore(path);
await store.push(entryOf("a"));
await withStoreWriteLock(path, async () => {
assert.deepEqual(await within(store.pullAll().then((rows) => rows.map((row) => row.key)), 2000, "a read"), ["a"]);
const missing = new SingleFileMessagePackBackingStore(join(directory, "absent.cc"));
assert.deepEqual(await within(missing.pullAll(), 2000, "a read of a missing store"), []);
});
} finally {
await rm(directory, { recursive: true, force: true });
}
});
// A share-none open succeeds only when no other handle on the file is open, as C# readers and
// writers need it to.
function canOpenShareNone(path: string): boolean {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const koffi = require("koffi");
const lib = koffi.load("kernel32.dll");
const createFileW = lib.func(
"intptr_t CreateFileW(str16 name, uint32_t access, uint32_t share, void *security, uint32_t disposition, uint32_t flags, void *template)",
);
const closeHandle = lib.func("int CloseHandle(intptr_t handle)");
const handle = Number(createFileW(path, 0x80000000, 0, null, 3, 0x80, null));
if (handle === -1) {
return false;
}
closeHandle(handle);
return true;
}
test(
"windows: the real kernel replaces through both primitives and reports the Win32 error of a missing source",
{ skip: process.platform !== "win32" },
async () => {
const directory = await scratch();
try {
const kernel = windowsKernel();
for (const [name, replace] of [
["posix", (staged: string, store: string) => kernel.renamePosix(staged, store)],
["moveex", (staged: string, store: string) => kernel.moveFileEx(staged, store, MOVEFILE_REPLACE_WRITE_THROUGH)],
] as const) {
const store = join(directory, `${name}.cc`);
const staged = join(directory, `${name}.tmp`);
await writeFile(store, Buffer.from([1]));
await writeFile(staged, Buffer.from([2]));
assert.equal(replace(staged, store), 0, name);
assert.ok(canOpenShareNone(store), `${name}: the replace left a handle open on the store`);
assert.deepEqual([...(await readFile(store))], [2], name);
assert.deepEqual(await readdir(directory).then((names) => names.filter((n) => n === `${name}.tmp`)), [], name);
assert.equal(replace(join(directory, "missing.tmp"), store), 2, `${name}: ERROR_FILE_NOT_FOUND`);
}
} finally {
await rm(directory, { recursive: true, force: true });
}
},
);
test(
"windows: after a write and after a waiter gave up, the lock file is held by no handle, so a share-none open (C#'s lock) succeeds",
{ skip: process.platform !== "win32" },
async () => {
const directory = await scratch();
try {
const store = join(directory, "store.cc");
await withStoreWriteLock(store, async () => {
// A waiter that polls and gives up must not leave a handle on the lock file either.
await assert.rejects(withStoreWriteLock(store, async () => undefined, { timeoutMs: 100 }), /timed out/);
});
// eslint-disable-next-line @typescript-eslint/no-var-requires
const koffi = require("koffi");
const lib = koffi.load("kernel32.dll");
const createFileW = lib.func(
"intptr_t CreateFileW(str16 name, uint32_t access, uint32_t share, void *security, uint32_t disposition, uint32_t flags, void *template)",
);
const closeHandle = lib.func("int CloseHandle(intptr_t handle)");
const handle = Number(createFileW(`${store}.lock`, 0xc0000000, 0, null, 4, 0x80, null));
assert.notEqual(handle, -1, "the lock file is still held open after the write");
closeHandle(handle);
} finally {
await rm(directory, { recursive: true, force: true });
}
},
);
test("a store file of an unrecognized shape is refused, and a store naming a missing schema id is refused naming the record", async () => {
const directory = await scratch();
try {
const odd = join(directory, "odd.cc");
await writeFile(odd, encode({ not: "a store" }));
await assert.rejects(new SingleFileMessagePackBackingStore(odd).pullAll(), /is not a recognized CultCache MessagePack store/);
const dangling = join(directory, "dangling.cc");
await writeFile(dangling, encode(["cultcache.store.v1", [], [["rec:1", "sha256:missing", "2026-10-20T00:00:00Z", Uint8Array.of(0x90)]]]));
await assert.rejects(
new SingleFileMessagePackBackingStore(dangling).pullAll(),
/persisted record "rec:1" references missing schema id "sha256:missing"/,
);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("opening a store that needs no healing writes nothing: a missing store leaves no lock file, a legacy store with byte payloads keeps its bytes", async () => {
const directory = await scratch();
try {
const absent = join(directory, "nested", "absent.cc");
assert.deepEqual(await new SingleFileMessagePackBackingStore(absent).pullAll(), []);
assert.deepEqual(await readdir(directory), [], "a read of a missing store creates neither its directory nor its lock");
const legacyBytes = encode([{ key: "k", type: "t", payload: Uint8Array.of(0x90), storedAt: "2026-10-20T00:00:00Z" }]);
const path = join(directory, "legacy-bytes.cc");
await writeFile(path, legacyBytes);
assert.deepEqual((await new SingleFileMessagePackBackingStore(path).pullAll()).map((entry) => entry.key), ["k"]);
assert.deepEqual([...(await readFile(path))], [...legacyBytes], "a legacy store whose payloads are already bytes is not rewritten");
assert.deepEqual((await readdir(directory)).sort(), ["legacy-bytes.cc"]);
} finally {
await rm(directory, { recursive: true, force: true });
}
});