


























@@ -1,7 +1,7 @@
11import fs from "node:fs";
22import os from "node:os";
33import path from "node:path";
4-import { afterEach, describe, expect, it } from "vitest";
4+import { afterEach, describe, expect, it, vi } from "vitest";
55import { resolveBundledRuntimeDependencyInstallRoot } from "./bundled-runtime-deps.js";
66import { prepareBundledPluginRuntimeRoot } from "./bundled-runtime-root.js";
77@@ -14,6 +14,7 @@ function makeTempRoot(): string {
1414}
15151616afterEach(() => {
17+vi.restoreAllMocks();
1718for (const root of tempRoots.splice(0)) {
1819fs.rmSync(root, { recursive: true, force: true });
1920}
@@ -23,6 +24,17 @@ async function waitForFilesystemTimestampTick(): Promise<void> {
2324await new Promise((resolve) => setTimeout(resolve, 50));
2425}
252627+function isPathInsideRoot(candidate: string, root: string): boolean {
28+const relative = path.relative(root, candidate);
29+return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
30+}
31+32+function isBigIntStatOptions(options: unknown): boolean {
33+return Boolean(
34+options && typeof options === "object" && "bigint" in options && options.bigint === true,
35+);
36+}
37+2638describe("prepareBundledPluginRuntimeRoot", () => {
2739it("materializes root JavaScript chunks in external mirrors", () => {
2840const packageRoot = makeTempRoot();
@@ -265,6 +277,84 @@ describe("prepareBundledPluginRuntimeRoot", () => {
265277).toContain("onboard-abc123");
266278});
267279280+it("fingerprints runtime mirror source roots before taking the mirror lock", () => {
281+const packageRoot = makeTempRoot();
282+const stageDir = makeTempRoot();
283+const canonicalPluginRoot = path.join(packageRoot, "dist", "extensions", "qqbot");
284+const runtimePluginRoot = path.join(packageRoot, "dist-runtime", "extensions", "qqbot");
285+const env = { ...process.env, OPENCLAW_PLUGIN_STAGE_DIR: stageDir };
286+fs.mkdirSync(canonicalPluginRoot, { recursive: true });
287+fs.mkdirSync(runtimePluginRoot, { recursive: true });
288+fs.writeFileSync(
289+path.join(packageRoot, "package.json"),
290+JSON.stringify({ name: "openclaw", version: "2026.4.27", type: "module" }),
291+"utf8",
292+);
293+fs.writeFileSync(
294+path.join(canonicalPluginRoot, "index.js"),
295+"export default { id: 'qqbot' };\n",
296+"utf8",
297+);
298+fs.writeFileSync(
299+path.join(canonicalPluginRoot, "package.json"),
300+JSON.stringify({ name: "@openclaw/qqbot", version: "1.0.0", type: "module" }, null, 2),
301+"utf8",
302+);
303+fs.writeFileSync(
304+path.join(runtimePluginRoot, "index.js"),
305+`export { default } from ${JSON.stringify("../../../dist/extensions/qqbot/index.js")};\n`,
306+"utf8",
307+);
308+fs.writeFileSync(
309+path.join(runtimePluginRoot, "package.json"),
310+JSON.stringify(
311+{
312+name: "@openclaw/qqbot",
313+version: "1.0.0",
314+type: "module",
315+dependencies: { "qqbot-runtime": "1.0.0" },
316+openclaw: { extensions: ["./index.js"] },
317+},
318+null,
319+2,
320+),
321+"utf8",
322+);
323+const installRoot = resolveBundledRuntimeDependencyInstallRoot(runtimePluginRoot, { env });
324+fs.mkdirSync(path.join(installRoot, "node_modules", "qqbot-runtime"), { recursive: true });
325+fs.writeFileSync(
326+path.join(installRoot, "node_modules", "qqbot-runtime", "package.json"),
327+JSON.stringify({ name: "qqbot-runtime", version: "1.0.0", type: "module" }),
328+"utf8",
329+);
330+331+const lockPath = path.join(installRoot, ".openclaw-runtime-mirror.lock");
332+const fingerprintLockStates: Array<{ source: "runtime" | "canonical"; locked: boolean }> = [];
333+const realLstatSync = fs.lstatSync.bind(fs) as typeof fs.lstatSync;
334+vi.spyOn(fs, "lstatSync").mockImplementation(((target, options) => {
335+const targetPath = target.toString();
336+if (isBigIntStatOptions(options)) {
337+if (isPathInsideRoot(targetPath, runtimePluginRoot)) {
338+fingerprintLockStates.push({ source: "runtime", locked: fs.existsSync(lockPath) });
339+} else if (isPathInsideRoot(targetPath, canonicalPluginRoot)) {
340+fingerprintLockStates.push({ source: "canonical", locked: fs.existsSync(lockPath) });
341+}
342+}
343+return realLstatSync(target, options as never);
344+}) as typeof fs.lstatSync);
345+346+prepareBundledPluginRuntimeRoot({
347+pluginId: "qqbot",
348+pluginRoot: runtimePluginRoot,
349+modulePath: path.join(runtimePluginRoot, "index.js"),
350+ env,
351+});
352+353+expect(fingerprintLockStates.some((entry) => entry.source === "runtime")).toBe(true);
354+expect(fingerprintLockStates.some((entry) => entry.source === "canonical")).toBe(true);
355+expect(fingerprintLockStates.filter((entry) => entry.locked)).toEqual([]);
356+});
357+268358it("reuses unchanged external runtime mirrors from the original plugin root", async () => {
269359const packageRoot = makeTempRoot();
270360const stageDir = makeTempRoot();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。