fix(openclaw): normalize Windows skill-loader URLs before fileURLToPath (#5679)

This commit is contained in:
Rod Boev
2026-06-25 07:06:11 -04:00
committed by GitHub
parent b91c745fbc
commit 1f66aadfa3
2 changed files with 37 additions and 2 deletions
@@ -4,10 +4,12 @@
import { describe, it, expect } from "vitest";
import {
safePath,
normalizeModuleUrlToPath,
loadSkill,
loadTriagePrompt,
loadCompactTriagePrompt,
} from "./skill-loader.ts";
import { fileURLToPath, pathToFileURL } from "node:url";
// ---------------------------------------------------------------------------
// safePath — path containment
@@ -74,6 +76,31 @@ describe("loadSkill path traversal", () => {
});
});
describe("normalizeModuleUrlToPath", () => {
it("normalizes raw Windows paths before fileURLToPath conversion", () => {
const rawWindowsMetaUrl = "C:\\Users\\example\\openclaw\\index.ts";
const result = normalizeModuleUrlToPath(rawWindowsMetaUrl);
// Assert the decoded property directly rather than reconstructing via the function body
expect(typeof result).toBe("string");
expect(result).not.toContain("%5C");
});
it("leaves already-correct file URLs unchanged", () => {
const fileMetaUrl = "file:///C:/Users/example/openclaw/index.ts";
const expected = fileURLToPath(fileMetaUrl);
expect(normalizeModuleUrlToPath(fileMetaUrl)).toBe(expected);
});
it.skipIf(process.platform === "win32")(
"passes POSIX absolute paths through unchanged",
() => {
const posixPath = "/home/user/openclaw/index.ts";
expect(normalizeModuleUrlToPath(posixPath)).toBe(posixPath);
},
);
});
describe("loadCompactTriagePrompt", () => {
it("keeps the core triage instructions without inlining the full skill body", () => {
const prompt = loadCompactTriagePrompt();
+10 -2
View File
@@ -4,7 +4,7 @@
*/
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import type { SkillsConfig, CategoryConfig } from "./types.ts";
import { readText, exists } from "./fs-safe.ts";
@@ -84,6 +84,14 @@ function parseSkillFile(content: string): ParsedSkill {
};
}
/** @internal — exported for testing only */
export function normalizeModuleUrlToPath(moduleUrl: string): string {
const normalizedUrl = moduleUrl.startsWith("file:")
? moduleUrl
: pathToFileURL(moduleUrl).toString();
return fileURLToPath(normalizedUrl);
}
// ============================================================================
// Skill Loader
// ============================================================================
@@ -96,7 +104,7 @@ function resolveSkillsDir(): string {
// Strategy 1: import.meta.url (works in native ESM)
try {
const metaDir = path.dirname(fileURLToPath(import.meta.url));
const metaDir = path.dirname(normalizeModuleUrlToPath(import.meta.url));
candidates.push(path.join(metaDir, "skills"));
candidates.push(path.join(metaDir, "..", "skills"));
} catch {