mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user