From 9ce359b370bccce77af4fb971d69a91c23aeef3a Mon Sep 17 00:00:00 2001 From: Ruben Cuevas Date: Sat, 9 May 2026 03:12:22 -0400 Subject: [PATCH] fix(gateway): check restored runtime overlays --- scripts/run-node.mjs | 24 ++++++++++++++++----- src/infra/run-node.test.ts | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/scripts/run-node.mjs b/scripts/run-node.mjs index 8eb95dab34b..fde703e33f7 100644 --- a/scripts/run-node.mjs +++ b/scripts/run-node.mjs @@ -342,6 +342,20 @@ const listBuiltBundledPluginEntries = (deps) => { .toSorted((left, right) => left.id.localeCompare(right.id)); }; +const listBuiltBundledPluginRuntimeOverlayDirs = (deps) => { + const distExtensionsRoot = path.join(resolveRuntimePostBuildDistRoot(deps), "extensions"); + let entries = []; + try { + entries = deps.fs.readdirSync(distExtensionsRoot, { withFileTypes: true }); + } catch { + return []; + } + return entries + .filter((entry) => entry.isDirectory() && entry.name !== "node_modules") + .map((entry) => entry.name) + .toSorted((left, right) => left.localeCompare(right)); +}; + const listRequiredBundledPluginMetadataOutputs = (pluginEntries, deps) => pluginEntries.flatMap(({ id, hasManifest, hasPackageJson }) => { const builtPluginDir = path.join(resolveRuntimePostBuildDistRoot(deps), "extensions", id); @@ -386,13 +400,13 @@ const listRuntimeOverlaySourcePaths = (sourceDir, deps) => { return paths.toSorted((left, right) => left.localeCompare(right)); }; -const listRequiredBundledPluginRuntimeOverlayOutputs = (pluginEntries, deps) => { +const listRequiredBundledPluginRuntimeOverlayOutputs = (deps) => { const distRoot = resolveRuntimePostBuildDistRoot(deps); const runtimeRoot = resolveRuntimePostBuildRuntimeRoot(deps); const runtimePaths = []; - for (const pluginEntry of pluginEntries) { - const distPluginDir = path.join(distRoot, "extensions", pluginEntry.id); - const runtimePluginDir = path.join(runtimeRoot, "extensions", pluginEntry.id); + for (const pluginId of listBuiltBundledPluginRuntimeOverlayDirs(deps)) { + const distPluginDir = path.join(distRoot, "extensions", pluginId); + const runtimePluginDir = path.join(runtimeRoot, "extensions", pluginId); for (const sourcePath of listRuntimeOverlaySourcePaths(distPluginDir, deps)) { runtimePaths.push(path.join(runtimePluginDir, path.relative(distPluginDir, sourcePath))); } @@ -443,7 +457,7 @@ export const listRequiredRuntimePostBuildOutputs = (deps) => { ...listRequiredOpenClawExtensionAliasOutputs(deps), ...listRequiredStaticExtensionAssetOutputs(deps), ...listRequiredBundledPluginMetadataOutputs(builtPluginEntries, deps), - ...listRequiredBundledPluginRuntimeOverlayOutputs(builtPluginEntries, deps), + ...listRequiredBundledPluginRuntimeOverlayOutputs(deps), ]; }; diff --git a/src/infra/run-node.test.ts b/src/infra/run-node.test.ts index 00f55ecda43..e1c5fb9d874 100644 --- a/src/infra/run-node.test.ts +++ b/src/infra/run-node.test.ts @@ -1777,6 +1777,49 @@ describe("run-node script", () => { }); }); + it("reports missing runtime overlay outputs from restored dist without plugin sources", async () => { + await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { + await setupTrackedProject(tmp, { + files: { + [ROOT_SRC]: "export const value = 1;\n", + [DIST_EXTENSION_INDEX]: "export default {};\n", + [DIST_EXTENSION_MANIFEST]: '{"id":"demo","configSchema":{"type":"object"}}\n', + [DIST_EXTENSION_PACKAGE]: '{"openclaw":{"extensions":["./index.js"]}}\n', + [DIST_RUNTIME_EXTENSION_INDEX]: "export default {};\n", + [DIST_RUNTIME_EXTENSION_MANIFEST]: '{"id":"demo","configSchema":{"type":"object"}}\n', + [DIST_RUNTIME_EXTENSION_PACKAGE]: '{"openclaw":{"extensions":["./index.js"]}}\n', + [RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n', + }, + buildPaths: [ + ROOT_SRC, + DIST_ENTRY, + DIST_EXTENSION_INDEX, + DIST_EXTENSION_MANIFEST, + DIST_EXTENSION_PACKAGE, + DIST_RUNTIME_EXTENSION_INDEX, + DIST_RUNTIME_EXTENSION_MANIFEST, + DIST_RUNTIME_EXTENSION_PACKAGE, + BUILD_STAMP, + RUNTIME_POSTBUILD_STAMP, + ], + }); + await fs.rm(resolvePath(tmp, "extensions"), { recursive: true, force: true }); + await fs.rm(resolvePath(tmp, DIST_RUNTIME_EXTENSION_INDEX)); + + const requirement = resolveRuntimePostBuildRequirement( + createBuildRequirementDeps(tmp, { + gitHead: "abc123\n", + gitStatus: "", + }), + ); + + expect(requirement).toEqual({ + shouldSync: true, + reason: "missing_runtime_postbuild_output", + }); + }); + }); + it("does not require OpenClaw SDK alias outputs when dist extensions are absent", async () => { await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { await setupTrackedProject(tmp, {