cultcache-ts: drop a redundant clause in the Windows replace, export the bound kernel for real-kernel tests, add survivor-killing tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d5a4f7a9da
commit
6696ab6db6
2 changed files with 97 additions and 6 deletions
|
|
@ -132,11 +132,7 @@ export async function replaceStoreFileWindows(
|
|||
let lastCode = 0;
|
||||
for (let attempt = 1; attempt <= REPLACE_ATTEMPTS; attempt++) {
|
||||
let code = posix ? kernel.renamePosix(staged, destination) : kernel.moveFileEx(staged, destination, MOVEFILE_REPLACE_WRITE_THROUGH);
|
||||
if (
|
||||
posix &&
|
||||
code !== 0 &&
|
||||
(code === 1 || code === ERROR_NOT_SUPPORTED || code === ERROR_INVALID_PARAMETER)
|
||||
) {
|
||||
if (posix && (code === 1 || code === ERROR_NOT_SUPPORTED || code === ERROR_INVALID_PARAMETER)) {
|
||||
// POSIX semantics are unsupported here (FAT, exFAT, before Windows 10 1607): the rest of this call is write-through.
|
||||
posix = false;
|
||||
code = kernel.moveFileEx(staged, destination, MOVEFILE_REPLACE_WRITE_THROUGH);
|
||||
|
|
@ -312,7 +308,8 @@ function loadLibc(platform: "linux" | "darwin"): Libc {
|
|||
return libcCache;
|
||||
}
|
||||
|
||||
function windowsKernel(): ReplaceKernel {
|
||||
/** The bound kernel32 replace calls; exported for the tests that run them against the real kernel. */
|
||||
export function windowsKernel(): ReplaceKernel {
|
||||
const k = loadKernel32();
|
||||
return {
|
||||
renamePosix: (staged, destination) => {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
nodeStoreFileOps,
|
||||
replaceStoreFile,
|
||||
replaceStoreFileWindows,
|
||||
windowsKernel,
|
||||
withStoreWriteLock,
|
||||
} from "../src/store-file";
|
||||
import type { ReplaceKernel, StoreFileOps } from "../src/store-file";
|
||||
|
|
@ -213,6 +214,13 @@ test("the Windows replace falls back to write-through MoveFileExW where POSIX re
|
|||
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]);
|
||||
|
|
@ -520,3 +528,89 @@ test("a read of a current store never waits for the store lock, and a missing st
|
|||
await rm(directory, { recursive: true, force: 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.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 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 () => undefined);
|
||||
// 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 });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue