diff --git a/scripts/run-node.mjs b/scripts/run-node.mjs index 60084a0e4a7e..8eb95dab34b7 100644 --- a/scripts/run-node.mjs +++ b/scripts/run-node.mjs @@ -32,10 +32,7 @@ import { runNodeSourceRoots, runNodeWatchedPaths, } from "./run-node-watch-paths.mjs"; -import { - listCoreRuntimePostBuildOutputs, - runRuntimePostBuild, -} from "./runtime-postbuild.mjs"; +import { listCoreRuntimePostBuildOutputs, runRuntimePostBuild } from "./runtime-postbuild.mjs"; export { isBuildRelevantRunNodePath, isRestartRelevantRunNodePath, runNodeWatchedPaths }; @@ -405,6 +402,10 @@ const listRequiredBundledPluginRuntimeOverlayOutputs = (pluginEntries, deps) => const listRequiredOpenClawExtensionAliasOutputs = (deps) => { const distRoot = resolveRuntimePostBuildDistRoot(deps); + const distExtensionsRoot = path.join(distRoot, "extensions"); + if (!deps.fs.existsSync(distExtensionsRoot)) { + return []; + } const pluginSdkDir = path.join(distRoot, "plugin-sdk"); let dirents = []; try { @@ -1070,7 +1071,8 @@ const writeBuildStamp = (deps) => { const shouldSkipWatchRuntimeSync = (deps, requirement) => deps.env.OPENCLAW_WATCH_MODE === "1" && requirement.reason === "missing_runtime_postbuild_stamp" && - hasDirtyRuntimePostBuildInputs(deps) !== true; + hasDirtyRuntimePostBuildInputs(deps) !== true && + !hasMissingRequiredRuntimePostBuildOutput(deps); const isGatewayClientCommand = (args) => args[0] === "gateway" && (args[1] === "call" || args[1] === "status"); diff --git a/src/infra/run-node.test.ts b/src/infra/run-node.test.ts index 52de06736e69..1c46a38b8de3 100644 --- a/src/infra/run-node.test.ts +++ b/src/infra/run-node.test.ts @@ -569,9 +569,21 @@ describe("run-node script", () => { await setupTrackedProject(tmp, { files: { [ROOT_SRC]: "export const value = 1;\n", + [DIST_PLUGIN_SDK_ROOT_ALIAS]: "module.exports = {};\n", + [DIST_CHANNEL_CATALOG]: '{"entries":[]}\n', + [DIST_LEGACY_CLI_EXIT_COMPAT]: "export function hasMemoryRuntime() { return false; }\n", + [DIST_LEGACY_CLI_EXIT_COMPAT_ALT]: + "export function hasMemoryRuntime() { return false; }\n", }, oldPaths: [ROOT_SRC, ROOT_TSCONFIG, ROOT_PACKAGE], - buildPaths: [DIST_ENTRY, BUILD_STAMP], + buildPaths: [ + DIST_ENTRY, + DIST_PLUGIN_SDK_ROOT_ALIAS, + DIST_CHANNEL_CATALOG, + DIST_LEGACY_CLI_EXIT_COMPAT, + DIST_LEGACY_CLI_EXIT_COMPAT_ALT, + BUILD_STAMP, + ], }); const profileDir = path.join(tmp, ".artifacts", "profiles"); const spawnCalls: Array<{ args: string[]; env: Record }> = []; @@ -901,6 +913,46 @@ describe("run-node script", () => { }); }); + it("reruns runtime postbuild in watch mode when required outputs are missing with no runtime stamp", async () => { + await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { + await setupTrackedProject(tmp, { + files: { + [ROOT_SRC]: "export const value = 1;\n", + [DIST_PLUGIN_SDK_ROOT_ALIAS]: "module.exports = {};\n", + [DIST_LEGACY_CLI_EXIT_COMPAT]: "export function hasMemoryRuntime() { return false; }\n", + [DIST_LEGACY_CLI_EXIT_COMPAT_ALT]: + "export function hasMemoryRuntime() { return false; }\n", + }, + oldPaths: [ROOT_SRC, ROOT_TSCONFIG, ROOT_PACKAGE], + buildPaths: [ + DIST_ENTRY, + DIST_PLUGIN_SDK_ROOT_ALIAS, + DIST_LEGACY_CLI_EXIT_COMPAT, + DIST_LEGACY_CLI_EXIT_COMPAT_ALT, + BUILD_STAMP, + ], + }); + await fs.rm(resolvePath(tmp, DIST_OPENCLAW_ALIAS_PACKAGE)); + + const runRuntimePostBuild = vi.fn(); + const { spawnCalls, spawn, spawnSync } = createSpawnRecorder({ + gitHead: "abc123\n", + gitStatus: "", + }); + const exitCode = await runStatusCommand({ + tmp, + spawn, + spawnSync, + env: { OPENCLAW_WATCH_MODE: "1" }, + runRuntimePostBuild, + }); + + expect(exitCode).toBe(0); + expect(spawnCalls).toEqual([statusCommandSpawn()]); + expect(runRuntimePostBuild).toHaveBeenCalledOnce(); + }); + }); + it("reruns runtime postbuild for dirty extension package metadata in watch mode", async () => { await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { await setupTrackedProject(tmp, { @@ -1722,6 +1774,47 @@ describe("run-node script", () => { }); }); + 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, { + files: { + [ROOT_SRC]: "export const value = 1;\n", + [DIST_PLUGIN_SDK_INDEX]: "export * from './core.js';\n", + [DIST_PLUGIN_SDK_ROOT_ALIAS]: "module.exports = {};\n", + [DIST_CHANNEL_CATALOG]: '{"entries":[]}\n', + [DIST_LEGACY_CLI_EXIT_COMPAT]: "export function hasMemoryRuntime() { return false; }\n", + [DIST_LEGACY_CLI_EXIT_COMPAT_ALT]: + "export function hasMemoryRuntime() { return false; }\n", + [RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n', + }, + buildPaths: [ + ROOT_SRC, + DIST_ENTRY, + DIST_PLUGIN_SDK_INDEX, + DIST_PLUGIN_SDK_ROOT_ALIAS, + DIST_CHANNEL_CATALOG, + DIST_LEGACY_CLI_EXIT_COMPAT, + DIST_LEGACY_CLI_EXIT_COMPAT_ALT, + BUILD_STAMP, + RUNTIME_POSTBUILD_STAMP, + ], + }); + await fs.rm(path.join(tmp, "dist", "extensions"), { recursive: true, force: true }); + + const requirement = resolveRuntimePostBuildRequirement( + createBuildRequirementDeps(tmp, { + gitHead: "abc123\n", + gitStatus: "", + }), + ); + + expect(requirement).toEqual({ + shouldSync: false, + reason: "clean", + }); + }); + }); + it("reports missing OpenClaw SDK alias outputs when runtime stamps match HEAD", async () => { await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { await setupTrackedProject(tmp, {