mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
200 lines
6.0 KiB
TypeScript
200 lines
6.0 KiB
TypeScript
import path from "node:path";
|
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const copyToClipboard = vi.fn();
|
|
const runtime = {
|
|
log: vi.fn(),
|
|
error: vi.fn(),
|
|
exit: vi.fn(),
|
|
};
|
|
|
|
type FakeFsEntry = { kind: "file"; content: string } | { kind: "dir" };
|
|
|
|
const state = vi.hoisted(() => ({
|
|
entries: new Map<string, FakeFsEntry>(),
|
|
counter: 0,
|
|
}));
|
|
|
|
const abs = (p: string) => path.resolve(p);
|
|
|
|
function setFile(p: string, content = "") {
|
|
const resolved = abs(p);
|
|
state.entries.set(resolved, { kind: "file", content });
|
|
setDir(path.dirname(resolved));
|
|
}
|
|
|
|
function setDir(p: string) {
|
|
const resolved = abs(p);
|
|
if (!state.entries.has(resolved)) {
|
|
state.entries.set(resolved, { kind: "dir" });
|
|
}
|
|
}
|
|
|
|
function copyTree(src: string, dest: string) {
|
|
const srcAbs = abs(src);
|
|
const destAbs = abs(dest);
|
|
const srcPrefix = `${srcAbs}${path.sep}`;
|
|
for (const [key, entry] of state.entries.entries()) {
|
|
if (key === srcAbs || key.startsWith(srcPrefix)) {
|
|
const rel = key === srcAbs ? "" : key.slice(srcPrefix.length);
|
|
const next = rel ? path.join(destAbs, rel) : destAbs;
|
|
state.entries.set(next, entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
vi.mock("node:fs", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("node:fs")>();
|
|
const pathMod = await import("node:path");
|
|
const absInMock = (p: string) => pathMod.resolve(p);
|
|
|
|
const wrapped = {
|
|
...actual,
|
|
existsSync: (p: string) => state.entries.has(absInMock(p)),
|
|
mkdirSync: (p: string, _opts?: unknown) => {
|
|
setDir(p);
|
|
},
|
|
writeFileSync: (p: string, content: string) => {
|
|
setFile(p, content);
|
|
},
|
|
renameSync: (from: string, to: string) => {
|
|
const fromAbs = absInMock(from);
|
|
const toAbs = absInMock(to);
|
|
const entry = state.entries.get(fromAbs);
|
|
if (!entry) {
|
|
throw new Error(`ENOENT: no such file or directory, rename '${from}' -> '${to}'`);
|
|
}
|
|
state.entries.delete(fromAbs);
|
|
state.entries.set(toAbs, entry);
|
|
},
|
|
rmSync: (p: string) => {
|
|
const root = absInMock(p);
|
|
const prefix = `${root}${pathMod.sep}`;
|
|
const keys = Array.from(state.entries.keys());
|
|
for (const key of keys) {
|
|
if (key === root || key.startsWith(prefix)) {
|
|
state.entries.delete(key);
|
|
}
|
|
}
|
|
},
|
|
mkdtempSync: (prefix: string) => {
|
|
const dir = `${prefix}${state.counter++}`;
|
|
setDir(dir);
|
|
return dir;
|
|
},
|
|
promises: {
|
|
...actual.promises,
|
|
cp: async (src: string, dest: string, _opts?: unknown) => {
|
|
copyTree(src, dest);
|
|
},
|
|
},
|
|
};
|
|
|
|
return { ...wrapped, default: wrapped };
|
|
});
|
|
|
|
vi.mock("../infra/clipboard.js", () => ({
|
|
copyToClipboard,
|
|
}));
|
|
|
|
vi.mock("../runtime.js", () => ({
|
|
defaultRuntime: runtime,
|
|
}));
|
|
|
|
let resolveBundledExtensionRootDir: typeof import("./browser-cli-extension.js").resolveBundledExtensionRootDir;
|
|
let installChromeExtension: typeof import("./browser-cli-extension.js").installChromeExtension;
|
|
let registerBrowserExtensionCommands: typeof import("./browser-cli-extension.js").registerBrowserExtensionCommands;
|
|
|
|
beforeAll(async () => {
|
|
({ resolveBundledExtensionRootDir, installChromeExtension, registerBrowserExtensionCommands } =
|
|
await import("./browser-cli-extension.js"));
|
|
});
|
|
|
|
beforeEach(() => {
|
|
state.entries.clear();
|
|
state.counter = 0;
|
|
copyToClipboard.mockReset();
|
|
runtime.log.mockReset();
|
|
runtime.error.mockReset();
|
|
runtime.exit.mockReset();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
function writeManifest(dir: string) {
|
|
setDir(dir);
|
|
setFile(path.join(dir, "manifest.json"), JSON.stringify({ manifest_version: 3 }));
|
|
}
|
|
|
|
describe("bundled extension resolver (fs-mocked)", () => {
|
|
it("walks up to find the assets directory", () => {
|
|
const root = abs("/tmp/openclaw-ext-root");
|
|
const here = path.join(root, "dist", "cli");
|
|
const assets = path.join(root, "assets", "chrome-extension");
|
|
|
|
writeManifest(assets);
|
|
setDir(here);
|
|
|
|
expect(resolveBundledExtensionRootDir(here)).toBe(assets);
|
|
});
|
|
|
|
it("prefers the nearest assets directory", () => {
|
|
const root = abs("/tmp/openclaw-ext-root-nearest");
|
|
const here = path.join(root, "dist", "cli");
|
|
const distAssets = path.join(root, "dist", "assets", "chrome-extension");
|
|
const rootAssets = path.join(root, "assets", "chrome-extension");
|
|
|
|
writeManifest(distAssets);
|
|
writeManifest(rootAssets);
|
|
setDir(here);
|
|
|
|
expect(resolveBundledExtensionRootDir(here)).toBe(distAssets);
|
|
});
|
|
});
|
|
|
|
describe("browser extension install (fs-mocked)", () => {
|
|
it("installs into the state dir (never node_modules)", async () => {
|
|
const tmp = abs("/tmp/openclaw-ext-install");
|
|
const sourceDir = path.join(tmp, "source-ext");
|
|
writeManifest(sourceDir);
|
|
setFile(path.join(sourceDir, "test.txt"), "ok");
|
|
|
|
const result = await installChromeExtension({ stateDir: tmp, sourceDir });
|
|
|
|
expect(result.path).toBe(path.join(tmp, "browser", "chrome-extension"));
|
|
expect(state.entries.has(abs(path.join(result.path, "manifest.json")))).toBe(true);
|
|
expect(state.entries.has(abs(path.join(result.path, "test.txt")))).toBe(true);
|
|
expect(result.path.includes("node_modules")).toBe(false);
|
|
});
|
|
|
|
it("copies extension path to clipboard", async () => {
|
|
const prev = process.env.OPENCLAW_STATE_DIR;
|
|
const tmp = abs("/tmp/openclaw-ext-path");
|
|
process.env.OPENCLAW_STATE_DIR = tmp;
|
|
|
|
try {
|
|
copyToClipboard.mockResolvedValue(true);
|
|
|
|
const dir = path.join(tmp, "browser", "chrome-extension");
|
|
writeManifest(dir);
|
|
|
|
const { Command } = await import("commander");
|
|
|
|
const program = new Command();
|
|
const browser = program.command("browser").option("--json", false);
|
|
registerBrowserExtensionCommands(
|
|
browser,
|
|
(cmd) => cmd.parent?.opts?.() as { json?: boolean },
|
|
);
|
|
|
|
await program.parseAsync(["browser", "extension", "path"], { from: "user" });
|
|
expect(copyToClipboard).toHaveBeenCalledWith(dir);
|
|
} finally {
|
|
if (prev === undefined) {
|
|
delete process.env.OPENCLAW_STATE_DIR;
|
|
} else {
|
|
process.env.OPENCLAW_STATE_DIR = prev;
|
|
}
|
|
}
|
|
});
|
|
});
|