test(sqlite): consolidate snapshot fixtures (#118264)

This commit is contained in:
Peter Steinberger
2026-08-02 16:11:39 -07:00
committed by GitHub
parent ee82689ea4
commit 0d04af1231
+170 -374
View File
@@ -3,7 +3,7 @@ import fsSync from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { requireNodeSqlite } from "./node-sqlite.js";
import { createPrivateSqliteDirectory } from "./sqlite-private-directory.js";
@@ -46,6 +46,7 @@ function isDirectoryOpen(flags: string | number | undefined): boolean {
afterEach(async () => {
durabilityTestState.syncOutcome = undefined;
vi.restoreAllMocks();
await Promise.all(tempDirs.splice(0).map((tempDir) => fs.rm(tempDir, { recursive: true })));
});
@@ -151,23 +152,76 @@ function appendSuperJournalPointer(journalPath: string, superJournalPath: string
fsSync.appendFileSync(journalPath, trailer);
}
function createEmptySqliteDatabase(
sqlite: ReturnType<typeof requireNodeSqlite>,
sqlitePath: string,
): void {
const database = new sqlite.DatabaseSync(sqlitePath);
let sqlite: ReturnType<typeof requireNodeSqlite>;
let sourcePath: string;
let targetPath: string;
let tempDir: string;
beforeEach(async () => {
tempDir = await createTempDir();
sourcePath = path.join(tempDir, "source.sqlite");
targetPath = path.join(tempDir, "snapshot.sqlite");
sqlite = requireNodeSqlite();
const database = new sqlite.DatabaseSync(sourcePath);
try {
database.exec("VACUUM;");
} finally {
database.close();
}
});
type SnapshotOptions = Parameters<typeof createVerifiedSqliteSnapshot>[0];
async function expectSnapshotSuccess(options: SnapshotOptions): Promise<void> {
await expect(createVerifiedSqliteSnapshot(options)).resolves.toEqual({
path: options.targetPath,
userVersion: 0,
});
}
async function expectSnapshotFailureWithoutTarget(
options: SnapshotOptions,
pattern: RegExp,
): Promise<void> {
await expect(createVerifiedSqliteSnapshot(options)).rejects.toThrow(pattern);
await expect(fs.access(options.targetPath)).rejects.toMatchObject({ code: "ENOENT" });
}
async function expectRejectedPublicationGuard(
phase: "afterPublish" | "beforePublish",
message: string,
): Promise<void> {
let guarded = false;
const reject = () => {
guarded = true;
throw new Error(message);
};
const options: SnapshotOptions =
phase === "afterPublish"
? { sourcePath, targetPath, afterPublish: reject }
: { sourcePath, targetPath, beforePublish: reject };
await expect(createVerifiedSqliteSnapshot(options)).rejects.toThrow(message);
expect(guarded).toBe(true);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
}
function withReadOnlySnapshot<T>(
sqliteModule: ReturnType<typeof requireNodeSqlite>,
snapshotPath: string,
operation: (snapshot: import("node:sqlite").DatabaseSync) => T,
): T {
const snapshot = new sqliteModule.DatabaseSync(snapshotPath, { readOnly: true });
try {
return operation(snapshot);
} finally {
snapshot.close();
}
}
describe("createVerifiedSqliteSnapshot", () => {
it.runIf(process.platform === "win32")(
"creates private staging directories exclusively under races",
async () => {
const tempDir = await createTempDir();
const directoryPath = path.join(tempDir, "private");
const results = await Promise.allSettled([
createPrivateSqliteDirectory(directoryPath),
@@ -185,45 +239,32 @@ describe("createVerifiedSqliteSnapshot", () => {
it.runIf(process.platform === "win32")(
"snapshots when its private staging path exceeds MAX_PATH",
async () => {
const tempDir = await createTempDir();
let targetDirectory = tempDir;
while (targetDirectory.length < 205) {
targetDirectory = path.join(targetDirectory, `segment-${"x".repeat(24)}`);
}
await fs.mkdir(targetDirectory, { recursive: true });
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(targetDirectory, "snapshot.sqlite");
const longTargetPath = path.join(targetDirectory, "snapshot.sqlite");
const longestStagingPath = path.join(
targetDirectory,
`.sqlite-publish-${"0".repeat(36)}-${"0".repeat(36)}`,
"database.sqlite",
);
expect(targetPath.length).toBeLessThan(260);
expect(longTargetPath.length).toBeLessThan(260);
expect(longestStagingPath.length).toBeGreaterThan(260);
const sqlite = requireNodeSqlite();
const source = new sqlite.DatabaseSync(sourcePath);
source.exec("CREATE TABLE records (value TEXT NOT NULL); INSERT INTO records VALUES ('ok');");
source.close();
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).resolves.toEqual({
path: targetPath,
userVersion: 0,
});
const snapshot = new sqlite.DatabaseSync(targetPath, { readOnly: true });
try {
await expectSnapshotSuccess({ sourcePath, targetPath: longTargetPath });
withReadOnlySnapshot(sqlite, longTargetPath, (snapshot) => {
expect(snapshot.prepare("SELECT value FROM records").get()).toEqual({ value: "ok" });
} finally {
snapshot.close();
}
});
},
);
it("captures committed WAL state and removes deleted page contents", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const deletedValue = `deleted-secret-${"x".repeat(256)}`;
const sqlite = requireNodeSqlite();
const source = new sqlite.DatabaseSync(sourcePath);
try {
source.exec(`
@@ -241,17 +282,14 @@ describe("createVerifiedSqliteSnapshot", () => {
expect(result).toEqual({ path: targetPath, userVersion: 0 });
expect((await fs.readFile(targetPath)).includes(deletedValue)).toBe(false);
const snapshot = new sqlite.DatabaseSync(targetPath, { readOnly: true });
try {
withReadOnlySnapshot(sqlite, targetPath, (snapshot) => {
expect(snapshot.prepare("SELECT value FROM records").all()).toEqual([
{ value: "survivor" },
]);
expect(snapshot.prepare("PRAGMA journal_mode;").get()).toEqual({
journal_mode: "delete",
});
} finally {
snapshot.close();
}
});
await expect(fs.access(`${targetPath}-wal`)).rejects.toMatchObject({ code: "ENOENT" });
await expect(fs.access(`${targetPath}-shm`)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
@@ -262,23 +300,15 @@ describe("createVerifiedSqliteSnapshot", () => {
it.skipIf(process.platform === "win32")(
"snapshots committed state from a hot rollback journal without recovering the source",
async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
createHotRollbackJournal(sourcePath);
const sourceBefore = await fs.readFile(sourcePath);
const journalBefore = await fs.readFile(`${sourcePath}-journal`);
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).resolves.toEqual({
path: targetPath,
userVersion: 0,
});
await expectSnapshotSuccess({ sourcePath, targetPath });
await expect(fs.readFile(sourcePath)).resolves.toEqual(sourceBefore);
await expect(fs.readFile(`${sourcePath}-journal`)).resolves.toEqual(journalBefore);
const sqlite = requireNodeSqlite();
const snapshot = new sqlite.DatabaseSync(targetPath, { readOnly: true });
try {
withReadOnlySnapshot(sqlite, targetPath, (snapshot) => {
expect(
snapshot.prepare("SELECT COUNT(*) AS count FROM records WHERE value = 'committed'").get(),
).toEqual({ count: 256 });
@@ -290,9 +320,7 @@ describe("createVerifiedSqliteSnapshot", () => {
expect(snapshot.prepare("PRAGMA integrity_check").get()).toEqual({
integrity_check: "ok",
});
} finally {
snapshot.close();
}
});
await expect(fs.access(`${targetPath}-journal`)).rejects.toMatchObject({ code: "ENOENT" });
},
);
@@ -300,9 +328,6 @@ describe("createVerifiedSqliteSnapshot", () => {
it.skipIf(process.platform === "win32")(
"rechecks for a hot rollback journal after the direct source open fails",
async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
createHotRollbackJournal(sourcePath);
const canonicalJournalPath = `${fsSync.realpathSync.native(sourcePath)}-journal`;
const lstatSync = fsSync.lstatSync.bind(fsSync);
@@ -317,26 +342,19 @@ describe("createVerifiedSqliteSnapshot", () => {
return lstatSync(pathname, options as never);
}) as typeof fsSync.lstatSync);
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).resolves.toEqual({
path: targetPath,
userVersion: 0,
});
await expectSnapshotSuccess({ sourcePath, targetPath });
expect(hidJournal).toBe(true);
const sqlite = requireNodeSqlite();
const snapshot = new sqlite.DatabaseSync(targetPath, { readOnly: true });
expect(
snapshot.prepare("SELECT COUNT(*) AS count FROM records WHERE value = 'committed'").get(),
).toEqual({ count: 256 });
snapshot.close();
withReadOnlySnapshot(sqlite, targetPath, (snapshot) => {
expect(
snapshot.prepare("SELECT COUNT(*) AS count FROM records WHERE value = 'committed'").get(),
).toEqual({ count: 256 });
});
},
);
it.skipIf(process.platform === "win32")(
"refuses private recovery when a hot journal depends on a super-journal",
async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const superJournalPath = path.join(tempDir, "source-mj000000900");
createHotRollbackJournal(sourcePath);
await fs.writeFile(superJournalPath, "super-journal");
@@ -357,10 +375,6 @@ describe("createVerifiedSqliteSnapshot", () => {
);
it("ignores a stale rollback journal without changing the source family", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
const source = new sqlite.DatabaseSync(sourcePath);
source.exec("CREATE TABLE records (value TEXT NOT NULL); INSERT INTO records VALUES ('ok');");
source.close();
@@ -368,51 +382,31 @@ describe("createVerifiedSqliteSnapshot", () => {
await fs.writeFile(`${sourcePath}-journal`, staleJournal);
const sourceBefore = await fs.readFile(sourcePath);
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).resolves.toEqual({
path: targetPath,
userVersion: 0,
});
await expectSnapshotSuccess({ sourcePath, targetPath });
await expect(fs.readFile(sourcePath)).resolves.toEqual(sourceBefore);
await expect(fs.readFile(`${sourcePath}-journal`)).resolves.toEqual(staleJournal);
const snapshot = new sqlite.DatabaseSync(targetPath, { readOnly: true });
expect(snapshot.prepare("SELECT value FROM records").get()).toEqual({ value: "ok" });
snapshot.close();
withReadOnlySnapshot(sqlite, targetPath, (snapshot) => {
expect(snapshot.prepare("SELECT value FROM records").get()).toEqual({ value: "ok" });
});
});
it("uses online backup before compacting the private copy", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
const source = new sqlite.DatabaseSync(sourcePath);
source.exec("CREATE TABLE records (value TEXT NOT NULL); INSERT INTO records VALUES ('ok');");
source.close();
const setup = new sqlite.DatabaseSync(sourcePath);
setup.exec("CREATE TABLE records (value TEXT NOT NULL); INSERT INTO records VALUES ('ok');");
setup.close();
const backupSpy = vi.spyOn(sqlite, "backup");
const prepareSpy = vi.spyOn(sqlite.DatabaseSync.prototype, "prepare");
try {
await createVerifiedSqliteSnapshot({ sourcePath, targetPath });
expect(backupSpy).toHaveBeenCalledTimes(1);
expect(prepareSpy.mock.calls.some(([sql]) => /\bVACUUM\s+INTO\b/iu.test(sql))).toBe(false);
const snapshot = new sqlite.DatabaseSync(targetPath, { readOnly: true });
try {
expect(snapshot.prepare("SELECT value FROM records").get()).toEqual({ value: "ok" });
} finally {
snapshot.close();
}
} finally {
prepareSpy.mockRestore();
backupSpy.mockRestore();
}
await createVerifiedSqliteSnapshot({ sourcePath, targetPath });
expect(backupSpy).toHaveBeenCalledTimes(1);
expect(prepareSpy.mock.calls.some(([sql]) => /\bVACUUM\s+INTO\b/iu.test(sql))).toBe(false);
withReadOnlySnapshot(sqlite, targetPath, (snapshot) => {
expect(snapshot.prepare("SELECT value FROM records").get()).toEqual({ value: "ok" });
});
});
it("pins validation and backup to one WAL snapshot", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
const writer = new sqlite.DatabaseSync(sourcePath);
writer.exec(`
PRAGMA journal_mode = WAL;
@@ -422,7 +416,7 @@ describe("createVerifiedSqliteSnapshot", () => {
PRAGMA wal_checkpoint(TRUNCATE);
`);
const backup = sqlite.backup.bind(sqlite);
const backupSpy = vi.spyOn(sqlite, "backup").mockImplementationOnce(async (...args) => {
vi.spyOn(sqlite, "backup").mockImplementationOnce(async (...args) => {
writer.prepare("INSERT INTO records VALUES (?)").run("during");
return await backup(...args);
});
@@ -434,67 +428,46 @@ describe("createVerifiedSqliteSnapshot", () => {
{ value: "before" },
{ value: "during" },
]);
const snapshot = new sqlite.DatabaseSync(targetPath, { readOnly: true });
try {
withReadOnlySnapshot(sqlite, targetPath, (snapshot) => {
expect(snapshot.prepare("SELECT value FROM records ORDER BY rowid").all()).toEqual([
{ value: "before" },
]);
} finally {
snapshot.close();
}
});
} finally {
backupSpy.mockRestore();
writer.close();
}
});
it("rejects unsafe index drift and removes the failed target", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
createUnsafeIndexDrift(sourcePath);
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).rejects.toThrow(
await expectSnapshotFailureWithoutTarget(
{ sourcePath, targetPath },
/integrity_check failed|malformed database schema/iu,
);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
});
it("snapshots a zero-byte generic source as an empty SQLite database", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
await fs.writeFile(sourcePath, "");
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).resolves.toEqual({
path: targetPath,
userVersion: 0,
});
await expectSnapshotSuccess({ sourcePath, targetPath });
expect((await fs.stat(targetPath)).size).toBeGreaterThan(0);
});
it("rejects a zero-byte source when nonempty input is required", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
await fs.writeFile(sourcePath, "");
await expect(
createVerifiedSqliteSnapshot({
await expectSnapshotFailureWithoutTarget(
{
sourcePath,
targetPath,
requireNonEmptySource: true,
}),
).rejects.toThrow(/snapshot source must not be empty/u);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
},
/snapshot source must not be empty/u,
);
});
it("rejects an existing target without modifying it", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
await fs.writeFile(targetPath, "keep");
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).rejects.toThrow(
@@ -504,12 +477,6 @@ describe("createVerifiedSqliteSnapshot", () => {
});
it("preserves a target created while the snapshot is being prepared", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
await expect(
createVerifiedSqliteSnapshot({
sourcePath,
@@ -523,14 +490,9 @@ describe("createVerifiedSqliteSnapshot", () => {
});
it("rejects staged bytes changed after validation", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
const originalOpen = fs.open.bind(fs);
let stagedReadCount = 0;
const openSpy = vi.spyOn(fs, "open").mockImplementation(async (filePath, flags, mode) => {
vi.spyOn(fs, "open").mockImplementation(async (filePath, flags, mode) => {
const resolvedPath = path.resolve(String(filePath));
if (
flags === "r" &&
@@ -545,105 +507,49 @@ describe("createVerifiedSqliteSnapshot", () => {
return await originalOpen(filePath, flags, mode);
});
try {
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).rejects.toThrow(
/size mismatch|hash mismatch/u,
);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
openSpy.mockRestore();
}
await expectSnapshotFailureWithoutTarget(
{ sourcePath, targetPath },
/size mismatch|hash mismatch/u,
);
});
it("runs the final caller guard before publishing the target", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
let guarded = false;
await expect(
createVerifiedSqliteSnapshot({
sourcePath,
targetPath,
beforePublish: () => {
guarded = true;
throw new Error("publication refused");
},
}),
).rejects.toThrow(/publication refused/u);
expect(guarded).toBe(true);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
await expectRejectedPublicationGuard("beforePublish", "publication refused");
});
it("removes its published target when the caller rejects it", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
let guarded = false;
await expect(
createVerifiedSqliteSnapshot({
sourcePath,
targetPath,
afterPublish: () => {
guarded = true;
throw new Error("published target refused");
},
}),
).rejects.toThrow(/published target refused/u);
expect(guarded).toBe(true);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
await expectRejectedPublicationGuard("afterPublish", "published target refused");
});
it("rejects an asynchronous after-publication guard", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
const asynchronousGuard = (async () => {}) as unknown as () => void;
await expect(
createVerifiedSqliteSnapshot({
await expectSnapshotFailureWithoutTarget(
{
sourcePath,
targetPath,
afterPublish: asynchronousGuard,
}),
).rejects.toThrow(/after-publication guard must be synchronous/u);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
},
/after-publication guard must be synchronous/u,
);
});
it("rejects an asynchronous final publication check", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
const asynchronousFinalCheck = (async () => {}) as unknown as () => void;
await expect(
createVerifiedSqliteSnapshot({
await expectSnapshotFailureWithoutTarget(
{
sourcePath,
targetPath,
afterPublish: (guard) => {
guard.assertTargetUnchanged(asynchronousFinalCheck);
},
}),
).rejects.toThrow(/publication final check must be synchronous/u);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
},
/publication final check must be synchronous/u,
);
});
it("preserves a target replaced by the caller after publication", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
await expect(
createVerifiedSqliteSnapshot({
sourcePath,
@@ -659,13 +565,8 @@ describe("createVerifiedSqliteSnapshot", () => {
});
it("rejects a target replaced after atomic publication", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
const originalLink = fs.link.bind(fs);
const linkSpy = vi.spyOn(fs, "link").mockImplementation(async (source, target) => {
vi.spyOn(fs, "link").mockImplementation(async (source, target) => {
await originalLink(source, target);
if (path.resolve(String(target)) === targetPath) {
await fs.unlink(targetPath);
@@ -673,26 +574,17 @@ describe("createVerifiedSqliteSnapshot", () => {
}
});
try {
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).rejects.toThrow(
/target changed during publication|staging path changed|snapshot file changed/u,
);
await expect(fs.readFile(targetPath, "utf8")).resolves.toBe("racer");
} finally {
linkSpy.mockRestore();
}
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).rejects.toThrow(
/target changed during publication|staging path changed|snapshot file changed/u,
);
await expect(fs.readFile(targetPath, "utf8")).resolves.toBe("racer");
});
it.runIf(process.platform !== "win32")(
"removes target bytes linked from a replaced staging pathname",
async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
const originalLink = fs.link.bind(fs);
const linkSpy = vi.spyOn(fs, "link").mockImplementation(async (source, target) => {
vi.spyOn(fs, "link").mockImplementation(async (source, target) => {
if (path.resolve(String(target)) === targetPath) {
await fs.unlink(source);
const replacement = new sqlite.DatabaseSync(String(source));
@@ -702,34 +594,25 @@ describe("createVerifiedSqliteSnapshot", () => {
await originalLink(source, target);
});
try {
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).rejects.toThrow(
/staging file changed during publication|size mismatch|hash mismatch/u,
);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
linkSpy.mockRestore();
}
await expectSnapshotFailureWithoutTarget(
{ sourcePath, targetPath },
/staging file changed during publication|size mismatch|hash mismatch/u,
);
},
);
it("removes its target when inspection fails after atomic publication", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
const originalLink = fs.link.bind(fs);
const originalLstat = fs.lstat.bind(fs);
let linked = false;
let failedInspection = false;
const linkSpy = vi.spyOn(fs, "link").mockImplementation(async (source, target) => {
vi.spyOn(fs, "link").mockImplementation(async (source, target) => {
await originalLink(source, target);
if (path.resolve(String(target)) === targetPath) {
linked = true;
}
});
const lstatSpy = vi.spyOn(fs, "lstat").mockImplementation(async (filePath) => {
vi.spyOn(fs, "lstat").mockImplementation(async (filePath) => {
if (linked && !failedInspection && path.resolve(String(filePath)) === targetPath) {
failedInspection = true;
throw Object.assign(new Error("target inspection failed"), { code: "EIO" });
@@ -737,153 +620,91 @@ describe("createVerifiedSqliteSnapshot", () => {
return await originalLstat(filePath);
});
try {
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).rejects.toThrow(
/target inspection failed/u,
);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
lstatSpy.mockRestore();
linkSpy.mockRestore();
}
await expectSnapshotFailureWithoutTarget(
{ sourcePath, targetPath },
/target inspection failed/u,
);
});
it("uses a private sibling staging file for atomic publication", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
const originalOpen = fs.open.bind(fs);
const openSpy = vi.spyOn(fs, "open").mockImplementation(originalOpen);
try {
await createVerifiedSqliteSnapshot({ sourcePath, targetPath });
expect(
openSpy.mock.calls.some(
([filePath, flags]) =>
flags === "wx+" &&
path.basename(path.dirname(String(filePath))).startsWith(".sqlite-publish-"),
),
).toBe(true);
} finally {
openSpy.mockRestore();
}
await createVerifiedSqliteSnapshot({ sourcePath, targetPath });
expect(
openSpy.mock.calls.some(
([filePath, flags]) =>
flags === "wx+" &&
path.basename(path.dirname(String(filePath))).startsWith(".sqlite-publish-"),
),
).toBe(true);
});
it("falls back to an exclusive copy when hard links are unavailable", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
const linkSpy = vi
.spyOn(fs, "link")
.mockRejectedValue(Object.assign(new Error("hard links unsupported"), { code: "ENOTSUP" }));
vi.spyOn(fs, "link").mockRejectedValue(
Object.assign(new Error("hard links unsupported"), { code: "ENOTSUP" }),
);
try {
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).resolves.toEqual({
path: targetPath,
userVersion: 0,
});
const restored = new sqlite.DatabaseSync(targetPath, { readOnly: true });
restored.close();
} finally {
linkSpy.mockRestore();
}
await expectSnapshotSuccess({ sourcePath, targetPath });
const restored = new sqlite.DatabaseSync(targetPath, { readOnly: true });
restored.close();
});
it("removes a fallback target whose copied bytes fail verification", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
const linkSpy = vi.spyOn(fs, "link").mockImplementation(async (source, target) => {
vi.spyOn(fs, "link").mockImplementation(async (source, target) => {
if (path.resolve(String(target)) === targetPath) {
await fs.appendFile(source, "changed-before-fallback");
}
throw Object.assign(new Error("hard links unsupported"), { code: "ENOTSUP" });
});
try {
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).rejects.toThrow(
/size mismatch|hash mismatch/u,
);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
linkSpy.mockRestore();
}
await expectSnapshotFailureWithoutTarget(
{ sourcePath, targetPath },
/size mismatch|hash mismatch/u,
);
});
it("removes its hard link when opening the published target fails", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
const originalLink = fs.link.bind(fs);
const originalOpen = fs.open.bind(fs);
let linked = false;
const linkSpy = vi.spyOn(fs, "link").mockImplementation(async (source, target) => {
vi.spyOn(fs, "link").mockImplementation(async (source, target) => {
await originalLink(source, target);
if (path.resolve(String(target)) === targetPath) {
linked = true;
}
});
const openSpy = vi.spyOn(fs, "open").mockImplementation(async (filePath, flags, mode) => {
vi.spyOn(fs, "open").mockImplementation(async (filePath, flags, mode) => {
if (linked && path.resolve(String(filePath)) === targetPath && flags === "r") {
throw Object.assign(new Error("target open failed"), { code: "EIO" });
}
return await originalOpen(filePath, flags, mode);
});
try {
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).rejects.toThrow(
/target open failed/u,
);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
openSpy.mockRestore();
linkSpy.mockRestore();
}
await expectSnapshotFailureWithoutTarget({ sourcePath, targetPath }, /target open failed/u);
});
it("cleans publication staging when initialization fails", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
const originalChmod = fs.chmod.bind(fs);
const chmodSpy = vi.spyOn(fs, "chmod").mockImplementation(async (filePath, mode) => {
vi.spyOn(fs, "chmod").mockImplementation(async (filePath, mode) => {
if (path.basename(String(filePath)).startsWith(".sqlite-publish-")) {
throw Object.assign(new Error("chmod refused"), { code: "EACCES" });
}
await originalChmod(filePath, mode);
});
try {
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).rejects.toThrow(
/chmod refused/u,
);
expect(
(await fs.readdir(tempDir)).every((name) => !name.startsWith(".sqlite-publish-")),
).toBe(true);
} finally {
chmodSpy.mockRestore();
}
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).rejects.toThrow(
/chmod refused/u,
);
expect((await fs.readdir(tempDir)).every((name) => !name.startsWith(".sqlite-publish-"))).toBe(
true,
);
});
it("removes its published target when final directory sync fails", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
const originalOpen = fs.open.bind(fs);
let targetDirectoryOpenCount = 0;
const openSpy = vi.spyOn(fs, "open").mockImplementation(async (filePath, flags, mode) => {
vi.spyOn(fs, "open").mockImplementation(async (filePath, flags, mode) => {
if (isDirectoryOpen(flags) && path.resolve(String(filePath)) === tempDir) {
targetDirectoryOpenCount += 1;
}
@@ -893,47 +714,30 @@ describe("createVerifiedSqliteSnapshot", () => {
return await originalOpen(filePath, flags, mode);
});
try {
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).rejects.toThrow(
/directory sync failed/u,
);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
openSpy.mockRestore();
}
await expectSnapshotFailureWithoutTarget({ sourcePath, targetPath }, /directory sync failed/u);
});
it.runIf(process.platform !== "win32")(
"removes its published target when directory sync is unsupported",
async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
durabilityTestState.syncOutcome = { status: "unsupported", code: "ENOTSUP" };
await expect(createVerifiedSqliteSnapshot({ sourcePath, targetPath })).rejects.toThrow(
await expectSnapshotFailureWithoutTarget(
{ sourcePath, targetPath },
/SQLite publication directory does not support crash-durable directory synchronization \(ENOTSUP\)/u,
);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.runIf(process.platform !== "win32")(
"rejects a transient publication directory replacement during sync",
async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const displacedPath = `${tempDir}.displaced`;
const replacementPath = `${tempDir}.replacement`;
const sqlite = requireNodeSqlite();
createEmptySqliteDatabase(sqlite, sourcePath);
const originalOpen = fs.open.bind(fs);
let targetDirectoryOpenCount = 0;
let replaced = false;
const openSpy = vi.spyOn(fs, "open").mockImplementation(async (filePath, flags, mode) => {
vi.spyOn(fs, "open").mockImplementation(async (filePath, flags, mode) => {
const resolvedPath = path.resolve(String(filePath));
if (isDirectoryOpen(flags) && resolvedPath === tempDir) {
targetDirectoryOpenCount += 1;
@@ -957,7 +761,6 @@ describe("createVerifiedSqliteSnapshot", () => {
expect(replaced).toBe(true);
await expect(fs.access(targetPath)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
openSpy.mockRestore();
await fs.rm(replacementPath, { recursive: true, force: true });
await fs.rename(displacedPath, tempDir).catch(() => undefined);
}
@@ -965,11 +768,7 @@ describe("createVerifiedSqliteSnapshot", () => {
);
it("validates both the source and transformed snapshot", async () => {
const tempDir = await createTempDir();
const sourcePath = path.join(tempDir, "source.sqlite");
const targetPath = path.join(tempDir, "snapshot.sqlite");
const removedValue = `removed-secret-${"x".repeat(256)}`;
const sqlite = requireNodeSqlite();
const source = new sqlite.DatabaseSync(sourcePath);
source.exec("PRAGMA secure_delete = OFF; CREATE TABLE records (value TEXT NOT NULL);");
source.prepare("INSERT INTO records VALUES (?)").run(removedValue);
@@ -988,11 +787,8 @@ describe("createVerifiedSqliteSnapshot", () => {
expect(labels).toEqual([sourcePath, targetPath, targetPath]);
expect((await fs.readFile(targetPath)).includes(removedValue)).toBe(false);
const snapshot = new sqlite.DatabaseSync(targetPath, { readOnly: true });
try {
withReadOnlySnapshot(sqlite, targetPath, (snapshot) => {
expect(snapshot.prepare("SELECT value FROM records").get()).toEqual({ value: "new" });
} finally {
snapshot.close();
}
});
});
});