
























@@ -67,6 +67,44 @@ function writePackagePlugin(rootDir: string, options: { configPaths?: readonly s
6767);
6868}
696970+function writeBundledPlugin(rootDir: string, pluginId: string, entryPath: string) {
71+fs.mkdirSync(rootDir, { recursive: true });
72+fs.writeFileSync(path.join(rootDir, entryPath), "export default { register() {} };\n", "utf8");
73+fs.writeFileSync(
74+path.join(rootDir, "openclaw.plugin.json"),
75+JSON.stringify({
76+id: pluginId,
77+name: pluginId,
78+description: pluginId,
79+configSchema: { type: "object" },
80+}),
81+"utf8",
82+);
83+fs.writeFileSync(
84+path.join(rootDir, "package.json"),
85+JSON.stringify({
86+name: `@openclaw/${pluginId}`,
87+version: "1.0.0",
88+openclaw: { extensions: [`./${entryPath}`] },
89+}),
90+"utf8",
91+);
92+}
93+94+function mockLinuxMountInfo(mountPoints: readonly string[]) {
95+const originalReadFileSync = fs.readFileSync;
96+return vi.spyOn(fs, "readFileSync").mockImplementation((filePath, options) => {
97+if (filePath === "/proc/self/mountinfo") {
98+return mountPoints
99+.map(
100+(mountPoint, index) => `${100 + index} 99 0:${index} / ${mountPoint} rw - tmpfs tmpfs rw`,
101+)
102+.join("\n");
103+}
104+return originalReadFileSync(filePath, options as never) as never;
105+});
106+}
107+70108function createCandidate(rootDir: string, pluginId = "demo"): PluginCandidate {
71109fs.mkdirSync(rootDir, { recursive: true });
72110fs.writeFileSync(path.join(rootDir, "index.ts"), "export default { register() {} };\n", "utf8");
@@ -753,6 +791,108 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => {
753791expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source");
754792});
755793794+it("keeps mixed source-checkout bundled roots from the same checkout", () => {
795+const tempRoot = makeTempDir();
796+const packageRoot = path.join(tempRoot, "openclaw");
797+const bundledRoot = path.join(packageRoot, "dist", "extensions");
798+const sourceRoot = path.join(packageRoot, "extensions");
799+const stateDir = path.join(tempRoot, "state");
800+const env = {
801+OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
802+OPENCLAW_STATE_DIR: stateDir,
803+OPENCLAW_VERSION: "2026.4.26",
804+VITEST: "true",
805+};
806+807+fs.mkdirSync(path.join(packageRoot, "src"), { recursive: true });
808+fs.writeFileSync(path.join(packageRoot, ".git"), "gitdir: /tmp/mock\n", "utf8");
809+fs.writeFileSync(path.join(packageRoot, "pnpm-workspace.yaml"), "packages: []\n", "utf8");
810+writeBundledPlugin(path.join(bundledRoot, "codex"), "codex", "index.js");
811+writeBundledPlugin(path.join(sourceRoot, "whatsapp"), "whatsapp", "index.ts");
812+813+const index = loadInstalledPluginIndex({ config: {}, env, stateDir });
814+expect(index.plugins.map((plugin) => plugin.pluginId)).toEqual(["codex", "whatsapp"]);
815+expect(index.plugins.map((plugin) => plugin.rootDir)).toEqual([
816+fs.realpathSync(path.join(bundledRoot, "codex")),
817+fs.realpathSync(path.join(sourceRoot, "whatsapp")),
818+]);
819+writePersistedInstalledPluginIndexSync(index, { stateDir });
820+821+const result = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, stateDir });
822+823+expect(result.source).toBe("persisted");
824+expect(result.diagnostics).toStrictEqual([]);
825+expect(result.snapshot.plugins.map((plugin) => plugin.pluginId)).toEqual(["codex", "whatsapp"]);
826+});
827+828+it("treats a persisted source bundled root as stale once its built peer appears", () => {
829+const tempRoot = makeTempDir();
830+const packageRoot = path.join(tempRoot, "openclaw");
831+const bundledRoot = path.join(packageRoot, "dist", "extensions");
832+const sourceRoot = path.join(packageRoot, "extensions");
833+const stateDir = path.join(tempRoot, "state");
834+const env = {
835+OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
836+OPENCLAW_STATE_DIR: stateDir,
837+OPENCLAW_VERSION: "2026.4.26",
838+VITEST: "true",
839+};
840+841+fs.mkdirSync(path.join(packageRoot, "src"), { recursive: true });
842+fs.mkdirSync(bundledRoot, { recursive: true });
843+fs.writeFileSync(path.join(packageRoot, ".git"), "gitdir: /tmp/mock\n", "utf8");
844+fs.writeFileSync(path.join(packageRoot, "pnpm-workspace.yaml"), "packages: []\n", "utf8");
845+writeBundledPlugin(path.join(sourceRoot, "whatsapp"), "whatsapp", "index.ts");
846+847+const sourceIndex = loadInstalledPluginIndex({ config: {}, env, stateDir });
848+expect(sourceIndex.plugins.map((plugin) => plugin.rootDir)).toEqual([
849+fs.realpathSync(path.join(sourceRoot, "whatsapp")),
850+]);
851+writePersistedInstalledPluginIndexSync(sourceIndex, { stateDir });
852+writeBundledPlugin(path.join(bundledRoot, "whatsapp"), "whatsapp", "index.js");
853+854+const result = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, stateDir });
855+856+expect(result.source).toBe("derived");
857+expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source");
858+expect(result.snapshot.plugins.map((plugin) => plugin.rootDir)).toEqual([
859+fs.realpathSync(path.join(bundledRoot, "whatsapp")),
860+]);
861+});
862+863+it("keeps a persisted bind-mounted source overlay when its built peer exists", () => {
864+const tempRoot = makeTempDir();
865+const packageRoot = path.join(tempRoot, "openclaw");
866+const bundledRoot = path.join(packageRoot, "dist", "extensions");
867+const sourcePluginDir = path.join(packageRoot, "extensions", "whatsapp");
868+const stateDir = path.join(tempRoot, "state");
869+const env = {
870+OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
871+OPENCLAW_STATE_DIR: stateDir,
872+OPENCLAW_VERSION: "2026.4.26",
873+VITEST: "true",
874+};
875+876+fs.mkdirSync(path.join(packageRoot, "src"), { recursive: true });
877+fs.mkdirSync(bundledRoot, { recursive: true });
878+fs.writeFileSync(path.join(packageRoot, ".git"), "gitdir: /tmp/mock\n", "utf8");
879+fs.writeFileSync(path.join(packageRoot, "pnpm-workspace.yaml"), "packages: []\n", "utf8");
880+writeBundledPlugin(sourcePluginDir, "whatsapp", "index.ts");
881+882+const sourceIndex = loadInstalledPluginIndex({ config: {}, env, stateDir });
883+writePersistedInstalledPluginIndexSync(sourceIndex, { stateDir });
884+writeBundledPlugin(path.join(bundledRoot, "whatsapp"), "whatsapp", "index.js");
885+mockLinuxMountInfo([sourcePluginDir]);
886+887+const result = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, stateDir });
888+889+expect(result.source).toBe("persisted");
890+expect(result.diagnostics).toStrictEqual([]);
891+expect(result.snapshot.plugins.map((plugin) => plugin.rootDir)).toEqual([
892+fs.realpathSync(sourcePluginDir),
893+]);
894+});
895+756896it("treats persisted registry as stale when a plugin diagnostic source path no longer exists", () => {
757897const tempRoot = makeTempDir();
758898const stateDir = path.join(tempRoot, "state");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。