fix(sqlite): support long Windows snapshot paths (#113228)

* fix(sqlite): support long Windows snapshot paths

* test(sqlite): run snapshot paths in Windows CI

* fix(sqlite): normalize long Windows VFS paths
This commit is contained in:
Vincent Koc
2026-07-24 14:34:51 +08:00
committed by GitHub
parent 71c558edbd
commit f5cc8b6da6
4 changed files with 62 additions and 6 deletions
+1 -1
View File
@@ -1839,7 +1839,7 @@
"test:unit:fast:audit": "node scripts/test-unit-fast-audit.mjs",
"test:voicecall:closedloop": "node scripts/test-voicecall-closedloop.mjs",
"test:watch": "node scripts/test-projects.mjs --watch",
"test:windows:ci": "node scripts/test-projects.mjs src/shared/runtime-import.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/daemon/schtasks.startup-fallback.test.ts extensions/lobster/src/lobster-runner.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
"test:windows:ci": "node scripts/test-projects.mjs src/shared/runtime-import.test.ts src/infra/sqlite-snapshot.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/daemon/schtasks.startup-fallback.test.ts extensions/lobster/src/lobster-runner.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
"tool-display:check": "node --import tsx scripts/tool-display.ts --check",
"tool-display:write": "node --import tsx scripts/tool-display.ts --write",
"ts-topology": "node --import tsx scripts/ts-topology.ts",
+36
View File
@@ -72,6 +72,42 @@ 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 longestStagingPath = path.join(
targetDirectory,
`.sqlite-publish-${"0".repeat(36)}-${"0".repeat(36)}`,
"database.sqlite",
);
expect(targetPath.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 {
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");
+19 -5
View File
@@ -16,6 +16,16 @@ import { readSqliteUserVersion } from "./sqlite-user-version.js";
const SQLITE_DIRECTORY_MODE = 0o700;
const WINDOWS_DIRECTORY_EXISTS_MARKER = "OPENCLAW_SQLITE_DIRECTORY_EXISTS";
function resolveSqliteFilesystemPath(pathname: string): string {
if (process.platform !== "win32") {
return pathname;
}
// Node normalizes long paths for fs, but DatabaseSync and VACUUM INTO pass
// filesystem names directly to SQLite's Windows VFS.
return path.toNamespacedPath(path.resolve(pathname));
}
// Managed directory creation accepts existing paths. CreateDirectoryW applies the
// protected DACL atomically while preserving fail-if-exists semantics.
const WINDOWS_PRIVATE_DIRECTORY_NATIVE_SOURCE = `
@@ -122,7 +132,9 @@ export async function createPrivateSqliteDirectory(directoryPath: string): Promi
await fs.mkdir(directoryPath, { mode: SQLITE_DIRECTORY_MODE });
return;
}
const encodedPath = Buffer.from(directoryPath, "utf8").toString("base64");
// This raw Win32 call bypasses Node's automatic long-path normalization.
const nativeDirectoryPath = path.toNamespacedPath(path.resolve(directoryPath));
const encodedPath = Buffer.from(nativeDirectoryPath, "utf8").toString("base64");
const encodedNativeSource = Buffer.from(WINDOWS_PRIVATE_DIRECTORY_NATIVE_SOURCE, "utf8").toString(
"base64",
);
@@ -787,7 +799,7 @@ export async function createVerifiedSqliteSnapshot(
const sqlite = requireNodeSqlite();
let stagedIdentity: Stats | undefined;
try {
const source = new sqlite.DatabaseSync(options.sourcePath, {
const source = new sqlite.DatabaseSync(resolveSqliteFilesystemPath(options.sourcePath), {
allowExtension: true,
readOnly: true,
});
@@ -796,13 +808,15 @@ export async function createVerifiedSqliteSnapshot(
await loadSqliteVecExtension({ db: source });
assertSqliteIntegrity(source, options.sourcePath);
options.validate?.(source, options.sourcePath);
source.prepare("VACUUM INTO ?").run(stagedPath);
source.prepare("VACUUM INTO ?").run(resolveSqliteFilesystemPath(stagedPath));
} finally {
source.close();
}
await fs.chmod(stagedPath, 0o600);
const snapshot = new sqlite.DatabaseSync(stagedPath, { allowExtension: true });
const snapshot = new sqlite.DatabaseSync(resolveSqliteFilesystemPath(stagedPath), {
allowExtension: true,
});
try {
snapshot.exec("PRAGMA busy_timeout = 30000; PRAGMA trusted_schema = OFF;");
await loadSqliteVecExtension({ db: snapshot });
@@ -827,7 +841,7 @@ export async function createVerifiedSqliteSnapshot(
beforePublish: options.beforePublish,
afterPublish: options.afterPublish,
validatePublished: async (publishedPath) => {
const published = new sqlite.DatabaseSync(publishedPath, {
const published = new sqlite.DatabaseSync(resolveSqliteFilesystemPath(publishedPath), {
allowExtension: true,
readOnly: true,
});
+6
View File
@@ -175,6 +175,12 @@ describe("package scripts", () => {
);
});
it("runs SQLite snapshot path coverage in Windows CI", () => {
expect(readPackageJson().scripts["test:windows:ci"]).toContain(
"src/infra/sqlite-snapshot.test.ts",
);
});
it("runs cross-OS installer behavior coverage in Windows CI", () => {
expect(readPackageJson().scripts["test:windows:ci"]).toContain(
"test/scripts/openclaw-cross-os-installer.windows.test.ts",