mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-22 14:41:34 +00:00
226 lines
7.5 KiB
TypeScript
226 lines
7.5 KiB
TypeScript
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
type FakeFsEntry = { kind: "file"; content: string } | { kind: "dir" };
|
|
|
|
const VITEST_FS_BASE = path.join(path.parse(process.cwd()).root, "__openclaw_vitest__");
|
|
const FIXTURE_BASE = path.join(VITEST_FS_BASE, "openclaw-root");
|
|
|
|
const state = vi.hoisted(() => ({
|
|
entries: new Map<string, FakeFsEntry>(),
|
|
realpaths: new Map<string, string>(),
|
|
realpathErrors: new Set<string>(),
|
|
}));
|
|
|
|
const abs = (p: string) => path.resolve(p);
|
|
const fx = (...parts: string[]) => path.join(FIXTURE_BASE, ...parts);
|
|
const vitestRootWithSep = `${abs(VITEST_FS_BASE)}${path.sep}`;
|
|
const isFixturePath = (p: string) => {
|
|
const resolved = abs(p);
|
|
return resolved === vitestRootWithSep.slice(0, -1) || resolved.startsWith(vitestRootWithSep);
|
|
};
|
|
|
|
function setFile(p: string, content = "") {
|
|
state.entries.set(abs(p), { kind: "file", content });
|
|
}
|
|
|
|
function setPackageRoot(root: string, name = "openclaw") {
|
|
setFile(path.join(root, "package.json"), JSON.stringify({ name }));
|
|
}
|
|
|
|
function expectResolvedPackageRoot(
|
|
syncResolver: typeof import("./openclaw-root.js").resolveOpenClawPackageRootSync,
|
|
asyncResolver: typeof import("./openclaw-root.js").resolveOpenClawPackageRoot,
|
|
opts: Parameters<typeof import("./openclaw-root.js").resolveOpenClawPackageRootSync>[0],
|
|
expected: string | null,
|
|
) {
|
|
expect(syncResolver(opts)).toBe(expected);
|
|
return expect(asyncResolver(opts)).resolves.toBe(expected);
|
|
}
|
|
|
|
vi.mock("node:fs", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("node:fs")>();
|
|
const wrapped = {
|
|
...actual,
|
|
existsSync: (p: string) =>
|
|
isFixturePath(p) ? state.entries.has(abs(p)) : actual.existsSync(p),
|
|
readFileSync: (p: string, encoding?: BufferEncoding) => {
|
|
if (!isFixturePath(p)) {
|
|
return actual.readFileSync(p, encoding);
|
|
}
|
|
const entry = state.entries.get(abs(p));
|
|
if (!entry || entry.kind !== "file") {
|
|
throw new Error(`ENOENT: no such file, open '${p}'`);
|
|
}
|
|
return encoding ? entry.content : Buffer.from(entry.content, "utf-8");
|
|
},
|
|
statSync: (p: string) => {
|
|
if (!isFixturePath(p)) {
|
|
return actual.statSync(p);
|
|
}
|
|
const entry = state.entries.get(abs(p));
|
|
if (!entry) {
|
|
throw new Error(`ENOENT: no such file or directory, stat '${p}'`);
|
|
}
|
|
return {
|
|
isFile: () => entry.kind === "file",
|
|
isDirectory: () => entry.kind === "dir",
|
|
};
|
|
},
|
|
realpathSync: (p: string) =>
|
|
isFixturePath(p)
|
|
? (() => {
|
|
const resolved = abs(p);
|
|
if (state.realpathErrors.has(resolved)) {
|
|
throw new Error(`ENOENT: no such file or directory, realpath '${p}'`);
|
|
}
|
|
return state.realpaths.get(resolved) ?? resolved;
|
|
})()
|
|
: actual.realpathSync(p),
|
|
};
|
|
return { ...wrapped, default: wrapped };
|
|
});
|
|
|
|
vi.mock("node:fs/promises", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("node:fs/promises")>();
|
|
const wrapped = {
|
|
...actual,
|
|
readFile: async (p: string, encoding?: BufferEncoding) => {
|
|
if (!isFixturePath(p)) {
|
|
return await actual.readFile(p, encoding);
|
|
}
|
|
const entry = state.entries.get(abs(p));
|
|
if (!entry || entry.kind !== "file") {
|
|
throw new Error(`ENOENT: no such file, open '${p}'`);
|
|
}
|
|
return entry.content;
|
|
},
|
|
};
|
|
return { ...wrapped, default: wrapped };
|
|
});
|
|
|
|
describe("resolveOpenClawPackageRoot", () => {
|
|
let resolveOpenClawPackageRoot: typeof import("./openclaw-root.js").resolveOpenClawPackageRoot;
|
|
let resolveOpenClawPackageRootSync: typeof import("./openclaw-root.js").resolveOpenClawPackageRootSync;
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
({ resolveOpenClawPackageRoot, resolveOpenClawPackageRootSync } =
|
|
await import("./openclaw-root.js"));
|
|
state.entries.clear();
|
|
state.realpaths.clear();
|
|
state.realpathErrors.clear();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "resolves package root from .bin argv1",
|
|
setup: () => {
|
|
const project = fx("bin-scenario");
|
|
const argv1 = path.join(project, "node_modules", ".bin", "openclaw");
|
|
const pkgRoot = path.join(project, "node_modules", "openclaw");
|
|
setPackageRoot(pkgRoot);
|
|
return { opts: { argv1 }, expected: pkgRoot };
|
|
},
|
|
},
|
|
{
|
|
name: "resolves package root via symlinked argv1",
|
|
setup: () => {
|
|
const project = fx("symlink-scenario");
|
|
const bin = path.join(project, "bin", "openclaw");
|
|
const realPkg = path.join(project, "real-pkg");
|
|
state.realpaths.set(abs(bin), abs(path.join(realPkg, "openclaw.mjs")));
|
|
setPackageRoot(realPkg);
|
|
return { opts: { argv1: bin }, expected: realPkg };
|
|
},
|
|
},
|
|
{
|
|
name: "falls back when argv1 realpath throws",
|
|
setup: () => {
|
|
const project = fx("realpath-throw-scenario");
|
|
const argv1 = path.join(project, "node_modules", ".bin", "openclaw");
|
|
const pkgRoot = path.join(project, "node_modules", "openclaw");
|
|
state.realpathErrors.add(abs(argv1));
|
|
setPackageRoot(pkgRoot);
|
|
return { opts: { argv1 }, expected: pkgRoot };
|
|
},
|
|
},
|
|
{
|
|
name: "prefers moduleUrl candidates",
|
|
setup: () => {
|
|
const pkgRoot = fx("moduleurl");
|
|
setPackageRoot(pkgRoot);
|
|
return {
|
|
opts: { moduleUrl: pathToFileURL(path.join(pkgRoot, "dist", "index.js")).toString() },
|
|
expected: pkgRoot,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
name: "falls through from a non-openclaw moduleUrl candidate to cwd",
|
|
setup: () => {
|
|
const wrongPkgRoot = fx("moduleurl-fallthrough", "wrong");
|
|
const cwdPkgRoot = fx("moduleurl-fallthrough", "cwd");
|
|
setPackageRoot(wrongPkgRoot, "not-openclaw");
|
|
setPackageRoot(cwdPkgRoot);
|
|
return {
|
|
opts: {
|
|
moduleUrl: pathToFileURL(path.join(wrongPkgRoot, "dist", "index.js")).toString(),
|
|
cwd: cwdPkgRoot,
|
|
},
|
|
expected: cwdPkgRoot,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
name: "ignores invalid moduleUrl values and falls back to cwd",
|
|
setup: () => {
|
|
const pkgRoot = fx("invalid-moduleurl");
|
|
setPackageRoot(pkgRoot);
|
|
return {
|
|
opts: { moduleUrl: "not-a-file-url", cwd: pkgRoot },
|
|
expected: pkgRoot,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
name: "returns null for non-openclaw package roots",
|
|
setup: () => {
|
|
const pkgRoot = fx("not-openclaw");
|
|
setPackageRoot(pkgRoot, "not-openclaw");
|
|
return { opts: { cwd: pkgRoot }, expected: null };
|
|
},
|
|
},
|
|
{
|
|
name: "falls back from a symlinked argv1 to the node_modules package root",
|
|
setup: () => {
|
|
const project = fx("symlink-node-modules-fallback");
|
|
const argv1 = path.join(project, "node_modules", ".bin", "openclaw");
|
|
state.realpaths.set(
|
|
abs(argv1),
|
|
abs(path.join(project, "versions", "current", "openclaw.mjs")),
|
|
);
|
|
const pkgRoot = path.join(project, "node_modules", "openclaw");
|
|
setPackageRoot(pkgRoot);
|
|
return { opts: { argv1 }, expected: pkgRoot };
|
|
},
|
|
},
|
|
{
|
|
name: "returns null when no package roots exist",
|
|
setup: () => ({
|
|
opts: { cwd: fx("missing") },
|
|
expected: null,
|
|
}),
|
|
},
|
|
])("$name", async ({ setup }) => {
|
|
const { opts, expected } = setup();
|
|
await expectResolvedPackageRoot(
|
|
resolveOpenClawPackageRootSync,
|
|
resolveOpenClawPackageRoot,
|
|
opts,
|
|
expected,
|
|
);
|
|
});
|
|
});
|