diff --git a/CHANGELOG.md b/CHANGELOG.md index 34c4b1f63cf1..215b3e0f4bf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Docs: https://docs.openclaw.ai - Gateway: return the post-expiration pending-work revision from node drains so reconnecting nodes do not observe stale queue revisions after expired items are pruned. - Release/CI/E2E: keep temporary full-sync checkouts alive while slow Crabbox leases boot, so sparse worktree runs do not lose their sync source before file-list generation. - Release/CI/E2E: normalize inherited Linux `C.UTF-8` locale settings before raw AWS macOS Crabbox bootstrap commands, avoiding macOS locale warnings during package-manager hydration. +- Release/CI/E2E: keep gateway watch regression checks from copying large static plugin assets inside the measured idle window. - Agents/providers: keep streaming tool-call argument parsing record-shaped when providers emit valid non-object JSON such as `null` or arrays. - Release/CI/E2E: reset incremental log readers when watched log files rotate without shrinking, so same-size replacements do not hide new readiness or RPC lines. - Talk: preserve explicit `null` payloads on controller-created turn and output-audio lifecycle events. diff --git a/scripts/check-gateway-watch-regression.mjs b/scripts/check-gateway-watch-regression.mjs index b3fac6c9fbd0..a17d272fe6ac 100644 --- a/scripts/check-gateway-watch-regression.mjs +++ b/scripts/check-gateway-watch-regression.mjs @@ -39,6 +39,7 @@ const WATCH_GATEWAY_SKIP_ENV = { OPENCLAW_SKIP_CHANNELS: "1", OPENCLAW_SKIP_CRON: "1", OPENCLAW_SKIP_GMAIL_WATCHER: "1", + OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS: "0", OPENCLAW_TEST_MINIMAL_GATEWAY: "1", NODE_ENV: "test", }; diff --git a/scripts/run-node.mjs b/scripts/run-node.mjs index ae6cab838b66..1895521d867d 100644 --- a/scripts/run-node.mjs +++ b/scripts/run-node.mjs @@ -473,10 +473,22 @@ const listRequiredOpenClawExtensionAliasOutputs = (deps) => { }; const listRequiredStaticExtensionAssetOutputs = (deps) => { + if (deps.env.OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS === "0") { + return []; + } const distRoot = resolveRuntimePostBuildDistRoot(deps); + const runtimeRoot = resolveRuntimePostBuildRuntimeRoot(deps); + const runtimeExtensionsRoot = path.join(runtimeRoot, "extensions"); + const hasRuntimeOverlay = deps.fs.existsSync(runtimeExtensionsRoot); return discoverStaticExtensionAssets({ rootDir: deps.cwd, fs: deps.fs }) .filter((asset) => deps.fs.existsSync(path.join(deps.cwd, asset.src))) - .map((asset) => path.join(distRoot, normalizePath(asset.dest).replace(/^dist\//u, ""))) + .flatMap((asset) => { + const relativeOutput = normalizePath(asset.dest).replace(/^dist\//u, ""); + return [ + path.join(distRoot, relativeOutput), + ...(hasRuntimeOverlay ? [path.join(runtimeRoot, relativeOutput)] : []), + ]; + }) .toSorted((left, right) => left.localeCompare(right)); }; diff --git a/src/infra/run-node.test.ts b/src/infra/run-node.test.ts index cce04ea2e604..8e0d90b4f5ee 100644 --- a/src/infra/run-node.test.ts +++ b/src/infra/run-node.test.ts @@ -2307,6 +2307,82 @@ describe("run-node script", () => { }); }); + it("reports missing static runtime overlay asset outputs when runtime stamps match HEAD", async () => { + await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { + await setupTrackedProject(tmp, { + files: { + [ROOT_SRC]: "export const value = 1;\n", + [DIFFS_PACKAGE]: + '{"openclaw":{"build":{"staticAssets":[{"source":"./assets/viewer-runtime.js","output":"assets/viewer-runtime.js"}]}}}\n', + [DIFFS_VIEWER_RUNTIME_SOURCE]: "export {};\n", + [DIST_DIFFS_VIEWER_RUNTIME]: "export {};\n", + [DIST_RUNTIME_DIFFS_VIEWER_RUNTIME]: "export {};\n", + [RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n', + }, + buildPaths: [ + ROOT_SRC, + DIFFS_PACKAGE, + DIFFS_VIEWER_RUNTIME_SOURCE, + DIST_DIFFS_VIEWER_RUNTIME, + DIST_RUNTIME_DIFFS_VIEWER_RUNTIME, + DIST_ENTRY, + BUILD_STAMP, + RUNTIME_POSTBUILD_STAMP, + ], + }); + await fs.rm(resolvePath(tmp, DIST_RUNTIME_DIFFS_VIEWER_RUNTIME)); + + const requirement = resolveRuntimePostBuildRequirement( + createBuildRequirementDeps(tmp, { + gitHead: "abc123\n", + gitStatus: "", + }), + ); + + expect(requirement).toEqual({ + shouldSync: true, + reason: "missing_runtime_postbuild_output", + }); + }); + }); + + it("does not require static asset outputs when runtime static assets are disabled", async () => { + await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { + await setupTrackedProject(tmp, { + files: { + [ROOT_SRC]: "export const value = 1;\n", + [DIFFS_PACKAGE]: + '{"openclaw":{"build":{"staticAssets":[{"source":"./assets/viewer-runtime.js","output":"assets/viewer-runtime.js"}]}}}\n', + [DIFFS_VIEWER_RUNTIME_SOURCE]: "export {};\n", + [DIST_RUNTIME_EXTENSION_PACKAGE]: '{"openclaw":{"extensions":["./index.js"]}}\n', + [RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n', + }, + buildPaths: [ + ROOT_SRC, + DIFFS_PACKAGE, + DIFFS_VIEWER_RUNTIME_SOURCE, + DIST_RUNTIME_EXTENSION_PACKAGE, + DIST_ENTRY, + BUILD_STAMP, + RUNTIME_POSTBUILD_STAMP, + ], + }); + + const requirement = resolveRuntimePostBuildRequirement( + createBuildRequirementDeps(tmp, { + env: { OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS: "0" }, + gitHead: "abc123\n", + gitStatus: "", + }), + ); + + expect(requirement).toEqual({ + shouldSync: false, + reason: "clean", + }); + }); + }); + it("does not require static asset outputs when the declared source is absent", async () => { await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { await setupTrackedProject(tmp, { diff --git a/test/scripts/check-gateway-watch-regression.test.ts b/test/scripts/check-gateway-watch-regression.test.ts index b909ee914991..c635686623bc 100644 --- a/test/scripts/check-gateway-watch-regression.test.ts +++ b/test/scripts/check-gateway-watch-regression.test.ts @@ -150,6 +150,12 @@ describe("check-gateway-watch-regression", () => { }), ); const waitForGatewayReady = vi.fn(async () => false); + const spawn = vi.fn(() => { + process.nextTick(() => { + child.emit("error", new Error("spawn failed")); + }); + return child; + }); try { const result = await runTimedWatch( @@ -162,12 +168,7 @@ describe("check-gateway-watch-regression", () => { outputDir, { allocateLoopbackPort: async () => 19042, - spawn: () => { - process.nextTick(() => { - child.emit("error", new Error("spawn failed")); - }); - return child; - }, + spawn, sleep, stopTimedWatchChild: stopChild, waitForGatewayReady, @@ -180,6 +181,7 @@ describe("check-gateway-watch-regression", () => { expect(result.spawnError).toBe("spawn failed"); expect(fs.existsSync(isolatedHomeDir)).toBe(false); expect(fs.existsSync(path.join(outputDir, "watch.home.txt"))).toBe(true); + expect(spawn.mock.calls[0]?.[2]?.env?.OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS).toBe("0"); expect(waitForGatewayReady).not.toHaveBeenCalled(); expect(stopChild).not.toHaveBeenCalled(); } finally {