fix(gateway): restore runtime postbuild freshness

This commit is contained in:
Ruben Cuevas
2026-05-02 23:36:51 -04:00
committed by Peter Steinberger
parent 0a13bd5841
commit 17643e549f
3 changed files with 470 additions and 4 deletions
+1
View File
@@ -1072,6 +1072,7 @@ Docs: https://docs.openclaw.ai
- Memory/search: keep sqlite-vec optional in packaged installs and point missing-extension recovery at the valid `agents.defaults.memorySearch.store.vector.extensionPath` setting. Thanks @willemsej and @vincentkoc.
- Gateway: keep directly requested plugin tools invokable under restrictive tool profiles while preserving explicit deny lists and the HTTP safety deny list, preventing catalog/invoke mismatches that surface as "Tool not available". Thanks @BunsDev.
- Gateway/update: allow beta binaries to refresh gateway services when the config was last written by the matching stable release version, avoiding false newer-config downgrade blocks during beta channel updates.
- Gateway/watch mode: detect missing bundled plugin dist and runtime-postbuild outputs before launching, so `pnpm gateway:watch` rebuilds or restages incomplete plugin artifacts instead of trusting stale stamps. Thanks @rubencu.
- Channels: keep Matrix and Mattermost bundled in the core package instead of advertising external npm installs before those channels are cut over. Thanks @vincentkoc.
- Bonjour: disable LAN mDNS advertising after a repeated stuck-announcing recovery instead of repeatedly restarting ciao and saturating the Gateway event loop.
- Channels/setup: label installable channel picker hints as remote npm installs and hide remote install hints for bundled plugins that already ship with OpenClaw.
+171 -2
View File
@@ -4,6 +4,10 @@ import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";
import {
collectBundledPluginBuildEntries,
NON_PACKAGED_BUNDLED_PLUGIN_DIRS,
} from "./lib/bundled-plugin-build-entries.mjs";
import {
BUNDLED_PLUGIN_PATH_PREFIX,
BUNDLED_PLUGIN_ROOT_DIR,
@@ -67,6 +71,11 @@ const resolvePrivateQaRequiredDistEntries = (distRoot) => [
path.join(distRoot, "plugin-sdk", "qa-lab.js"),
path.join(distRoot, "plugin-sdk", "qa-runtime.js"),
];
const shouldIncludePrivateQaBundledOutputs = (env = process.env) =>
env.OPENCLAW_BUILD_PRIVATE_QA === "1";
const shouldRequireBundledPluginRuntimeOutput = (pluginId, env = process.env) =>
shouldIncludePrivateQaBundledOutputs(env) || !NON_PACKAGED_BUNDLED_PLUGIN_DIRS.has(pluginId);
const isExcludedSource = (filePath, sourceRoot, sourceRootName) => {
const relativePath = normalizePath(path.relative(sourceRoot, filePath));
@@ -146,7 +155,13 @@ const hasDirtySourceTree = (deps) => {
if (output === null) {
return null;
}
return parseGitStatusPaths(output).some((repoPath) => isBuildRelevantRunNodePath(repoPath));
return parseGitStatusPaths(output).some((repoPath) => {
const normalizedPath = normalizePath(repoPath).replace(/^\.\/+/, "");
return (
isBuildRelevantRunNodePath(normalizedPath) ||
isDirtyBundledPluginPackageEntryChangeWithoutBuiltOutputs(normalizedPath, deps)
);
});
};
const isRuntimePostBuildRelevantPath = (repoPath) => {
@@ -167,7 +182,8 @@ const isRuntimePostBuildRelevantPath = (repoPath) => {
return false;
}
const pluginRelativePath = normalizedPath.slice(BUNDLED_PLUGIN_PATH_PREFIX.length);
if (pluginRelativePath.startsWith("skills/")) {
const pluginLocalPath = pluginRelativePath.split("/").slice(1).join("/");
if (pluginLocalPath === "skills" || pluginLocalPath.startsWith("skills/")) {
return true;
}
return extensionRestartMetadataFiles.has(path.posix.basename(pluginRelativePath));
@@ -257,6 +273,143 @@ const hasRuntimePostBuildInputMtimeChanged = (stampMtime, deps) => {
return latestInputMtime != null && latestInputMtime > stampMtime;
};
const resolveRuntimePostBuildDistRoot = (deps) => deps.distRoot ?? path.join(deps.cwd, "dist");
const resolveRuntimePostBuildRuntimeRoot = (deps) => path.join(deps.cwd, "dist-runtime");
const collectRunNodeBundledPluginBuildEntries = (deps) => {
if (!deps.fs.existsSync(path.join(deps.cwd, BUNDLED_PLUGIN_ROOT_DIR))) {
return [];
}
return collectBundledPluginBuildEntries({ cwd: deps.cwd, env: deps.env });
};
const resolveBuiltBundledPluginRuntimeEntryPath = (distRoot, pluginId, sourceEntry) =>
path.join(
distRoot,
"extensions",
pluginId,
sourceEntry.replace(/^\.\//, "").replace(/\.[^.]+$/u, ".js"),
);
const listBundledPluginRuntimeEntryPaths = (pluginEntry, deps) => {
const distRoot = resolveRuntimePostBuildDistRoot(deps);
return pluginEntry.sourceEntries
.map((sourceEntry) =>
resolveBuiltBundledPluginRuntimeEntryPath(distRoot, pluginEntry.id, sourceEntry),
)
.toSorted((left, right) => left.localeCompare(right));
};
const isDirtyBundledPluginPackageEntryChangeWithoutBuiltOutputs = (normalizedPath, deps) => {
if (!normalizedPath.startsWith("extensions/") || !normalizedPath.endsWith("/package.json")) {
return false;
}
const [, pluginId] = normalizedPath.split("/");
if (!pluginId || !shouldRequireBundledPluginRuntimeOutput(pluginId, deps.env)) {
return false;
}
const pluginEntry = collectRunNodeBundledPluginBuildEntries(deps).find(
(entry) => entry.id === pluginId,
);
if (!pluginEntry) {
return false;
}
return listBundledPluginRuntimeEntryPaths(pluginEntry, deps).some(
(filePath) => !deps.fs.existsSync(filePath),
);
};
const hasMissingBuiltBundledPluginRuntimeEntryOutput = (deps) => {
return collectRunNodeBundledPluginBuildEntries(deps)
.filter(({ id }) => shouldRequireBundledPluginRuntimeOutput(id, deps.env))
.some((pluginEntry) => {
const entryPaths = listBundledPluginRuntimeEntryPaths(pluginEntry, deps);
return entryPaths.some((filePath) => !deps.fs.existsSync(filePath));
});
};
const listBuiltBundledPluginEntries = (deps) => {
return collectRunNodeBundledPluginBuildEntries(deps)
.filter(({ id }) => shouldRequireBundledPluginRuntimeOutput(id, deps.env))
.filter((pluginEntry) =>
listBundledPluginRuntimeEntryPaths(pluginEntry, deps).some((filePath) =>
deps.fs.existsSync(filePath),
),
)
.toSorted((left, right) => left.id.localeCompare(right.id));
};
const listRequiredBundledPluginMetadataOutputs = (pluginEntries, deps) =>
pluginEntries.flatMap(({ id, hasManifest, hasPackageJson }) => {
const builtPluginDir = path.join(resolveRuntimePostBuildDistRoot(deps), "extensions", id);
const requiredPaths = [];
if (hasPackageJson) {
requiredPaths.push(path.join(builtPluginDir, "package.json"));
}
if (hasManifest) {
requiredPaths.push(path.join(builtPluginDir, "openclaw.plugin.json"));
}
return requiredPaths;
});
const listRuntimeOverlaySourcePaths = (sourceDir, deps) => {
const paths = [];
const queue = [sourceDir];
while (queue.length > 0) {
const current = queue.pop();
if (!current) {
continue;
}
let entries = [];
try {
entries = deps.fs.readdirSync(current, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (entry.name === "node_modules") {
continue;
}
const entryPath = path.join(current, entry.name);
if (entry.isDirectory()) {
queue.push(entryPath);
continue;
}
if (entry.isFile() || entry.isSymbolicLink()) {
paths.push(entryPath);
}
}
}
return paths.toSorted((left, right) => left.localeCompare(right));
};
const listRequiredBundledPluginRuntimeOverlayOutputs = (pluginEntries, 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 sourcePath of listRuntimeOverlaySourcePaths(distPluginDir, deps)) {
runtimePaths.push(path.join(runtimePluginDir, path.relative(distPluginDir, sourcePath)));
}
}
return [...new Set(runtimePaths)].toSorted((left, right) => left.localeCompare(right));
};
const listRequiredRuntimePostBuildOutputs = (deps) => {
const builtPluginEntries = listBuiltBundledPluginEntries(deps);
return [
...listRequiredBundledPluginMetadataOutputs(builtPluginEntries, deps),
...listRequiredBundledPluginRuntimeOverlayOutputs(builtPluginEntries, deps),
];
};
const hasMissingRequiredRuntimePostBuildOutput = (deps) =>
listRequiredRuntimePostBuildOutputs(deps).some(
(filePath) => statMtime(filePath, deps.fs) == null,
);
export const resolveBuildRequirement = (deps) => {
if (deps.env.OPENCLAW_FORCE_BUILD === "1") {
return { shouldBuild: true, reason: "force_build" };
@@ -297,10 +450,17 @@ export const resolveBuildRequirement = (deps) => {
return { shouldBuild: true, reason: "dirty_watched_tree" };
}
if (dirty === false) {
if (hasMissingBuiltBundledPluginRuntimeEntryOutput(deps)) {
return { shouldBuild: true, reason: "missing_bundled_plugin_dist_entry" };
}
return { shouldBuild: false, reason: "clean" };
}
}
if (hasMissingBuiltBundledPluginRuntimeEntryOutput(deps)) {
return { shouldBuild: true, reason: "missing_bundled_plugin_dist_entry" };
}
if (hasSourceMtimeChanged(stamp.mtime, deps)) {
return { shouldBuild: true, reason: "source_mtime_newer" };
}
@@ -338,6 +498,9 @@ export const resolveRuntimePostBuildRequirement = (deps) => {
return { shouldSync: true, reason: "dirty_runtime_postbuild_inputs" };
}
if (dirty === false) {
if (hasMissingRequiredRuntimePostBuildOutput(deps)) {
return { shouldSync: true, reason: "missing_runtime_postbuild_output" };
}
return { shouldSync: false, reason: "clean" };
}
}
@@ -346,6 +509,10 @@ export const resolveRuntimePostBuildRequirement = (deps) => {
return { shouldSync: true, reason: "runtime_postbuild_input_mtime_newer" };
}
if (hasMissingRequiredRuntimePostBuildOutput(deps)) {
return { shouldSync: true, reason: "missing_runtime_postbuild_output" };
}
return { shouldSync: false, reason: "clean" };
};
@@ -357,6 +524,7 @@ const BUILD_REASON_LABELS = {
build_stamp_missing_head: "build stamp missing git head",
git_head_changed: "git head changed",
dirty_watched_tree: "dirty watched source tree",
missing_bundled_plugin_dist_entry: "bundled plugin dist entry missing",
source_mtime_newer: "source mtime newer than build stamp",
missing_private_qa_dist: "private QA dist entry missing",
clean: "clean",
@@ -364,6 +532,7 @@ const BUILD_REASON_LABELS = {
const RUNTIME_POSTBUILD_REASON_LABELS = {
force_runtime_postbuild: "forced by OPENCLAW_FORCE_RUNTIME_POSTBUILD",
missing_runtime_postbuild_output: "required runtime postbuild output missing",
missing_runtime_postbuild_stamp: "runtime postbuild stamp missing",
missing_build_stamp: "build stamp missing",
build_stamp_newer: "build stamp newer than runtime postbuild stamp",
+298 -2
View File
@@ -31,10 +31,21 @@ const BUILD_STAMP = `dist/${BUILD_STAMP_FILE}`;
const RUNTIME_POSTBUILD_STAMP = `dist/${RUNTIME_POSTBUILD_STAMP_FILE}`;
const QA_LAB_PLUGIN_SDK_ENTRY = "dist/plugin-sdk/qa-lab.js";
const QA_RUNTIME_PLUGIN_SDK_ENTRY = "dist/plugin-sdk/qa-runtime.js";
const EXTENSION_INDEX = bundledPluginFile("demo", "index.ts");
const EXTENSION_SRC = bundledPluginFile("demo", "src/index.ts");
const EXTENSION_EXTRA_SRC = bundledPluginFile("demo", "src/extra.ts");
const EXTENSION_SKILL = bundledPluginFile("demo", "skills/SKILL.md");
const EXTENSION_MANIFEST = bundledPluginFile("demo", "openclaw.plugin.json");
const EXTENSION_PACKAGE = bundledPluginFile("demo", "package.json");
const EXTENSION_README = bundledPluginFile("demo", "README.md");
const DIST_EXTENSION_INDEX = bundledDistPluginFile("demo", "index.js");
const DIST_EXTENSION_SRC = bundledDistPluginFile("demo", "src/index.js");
const DIST_EXTENSION_SKILL = bundledDistPluginFile("demo", "skills/SKILL.md");
const DIST_EXTENSION_RUNTIME_SRC = "dist-runtime/extensions/demo/src/index.js";
const DIST_RUNTIME_EXTENSION_INDEX = "dist-runtime/extensions/demo/index.js";
const DIST_RUNTIME_EXTENSION_MANIFEST = "dist-runtime/extensions/demo/openclaw.plugin.json";
const DIST_RUNTIME_EXTENSION_PACKAGE = "dist-runtime/extensions/demo/package.json";
const DIST_RUNTIME_EXTENSION_SKILL = "dist-runtime/extensions/demo/skills/SKILL.md";
const DIST_EXTENSION_MANIFEST = bundledDistPluginFile("demo", "openclaw.plugin.json");
const DIST_EXTENSION_PACKAGE = bundledDistPluginFile("demo", "package.json");
@@ -1013,12 +1024,16 @@ describe("run-node script", () => {
await setupTrackedProject(tmp, {
files: {
[ROOT_SRC]: "export const value = 1;\n",
[EXTENSION_INDEX]: "export default {};\n",
[EXTENSION_MANIFEST]: '{"id":"demo","configSchema":{"type":"object"}}\n',
[DIST_EXTENSION_INDEX]: "export default {};\n",
[RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n',
},
buildPaths: [
ROOT_SRC,
EXTENSION_INDEX,
EXTENSION_MANIFEST,
DIST_EXTENSION_INDEX,
ROOT_TSCONFIG,
ROOT_PACKAGE,
DIST_ENTRY,
@@ -1285,13 +1300,15 @@ describe("run-node script", () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp, {
files: {
[EXTENSION_INDEX]: "export default {};\n",
[EXTENSION_MANIFEST]: '{"id":"demo","configSchema":{"type":"object"}}\n',
[EXTENSION_PACKAGE]: '{"name":"demo","openclaw":{"extensions":["./index.ts"]}}\n',
[ROOT_TSDOWN]: "export default {};\n",
[DIST_EXTENSION_INDEX]: "export default {};\n",
[DIST_EXTENSION_PACKAGE]: '{"name":"demo","openclaw":{"extensions":["./stale.js"]}}\n',
},
oldPaths: [EXTENSION_MANIFEST, ROOT_TSCONFIG, ROOT_PACKAGE, ROOT_TSDOWN],
buildPaths: [DIST_ENTRY, BUILD_STAMP, DIST_EXTENSION_PACKAGE],
oldPaths: [EXTENSION_INDEX, EXTENSION_MANIFEST, ROOT_TSCONFIG, ROOT_PACKAGE, ROOT_TSDOWN],
buildPaths: [DIST_ENTRY, BUILD_STAMP, DIST_EXTENSION_INDEX, DIST_EXTENSION_PACKAGE],
newPaths: [EXTENSION_PACKAGE],
});
@@ -1341,18 +1358,22 @@ describe("run-node script", () => {
await setupTrackedProject(tmp, {
files: {
[ROOT_SRC]: "export const value = 1;\n",
[EXTENSION_INDEX]: "export default {};\n",
[EXTENSION_MANIFEST]: '{"id":"demo","configSchema":{"type":"object"}}\n',
[ROOT_TSDOWN]: "export default {};\n",
[DIST_EXTENSION_INDEX]: "export default {};\n",
[DIST_EXTENSION_MANIFEST]: '{"id":"stale","configSchema":{"type":"object"}}\n',
},
buildPaths: [
ROOT_SRC,
EXTENSION_INDEX,
EXTENSION_MANIFEST,
ROOT_TSCONFIG,
ROOT_PACKAGE,
ROOT_TSDOWN,
DIST_ENTRY,
BUILD_STAMP,
DIST_EXTENSION_INDEX,
DIST_EXTENSION_MANIFEST,
],
});
@@ -1451,6 +1472,144 @@ describe("run-node script", () => {
});
});
it("reports clean in sparse worktrees without bundled plugin sources", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp, {
files: {
[ROOT_SRC]: "export const value = 1;\n",
},
oldPaths: [ROOT_SRC, ROOT_TSCONFIG, ROOT_PACKAGE],
buildPaths: [DIST_ENTRY, BUILD_STAMP],
});
await fs.rm(resolvePath(tmp, "extensions"), { recursive: true, force: true });
const requirement = resolveBuildRequirement(
createBuildRequirementDeps(tmp, {
gitHead: "abc123\n",
gitStatus: "",
}),
);
expect(requirement).toEqual({
shouldBuild: false,
reason: "clean",
});
});
});
it("rebuilds when dirty bundled package entries point at missing dist outputs", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp, {
files: {
[ROOT_SRC]: "export const value = 1;\n",
[EXTENSION_SRC]: "export default {};\n",
[EXTENSION_EXTRA_SRC]: "export const extra = true;\n",
[EXTENSION_MANIFEST]: '{"id":"demo","configSchema":{"type":"object"}}\n',
[EXTENSION_PACKAGE]: '{"openclaw":{"extensions":["./src/index.ts","./src/extra.ts"]}}\n',
[DIST_EXTENSION_SRC]: "export default {};\n",
},
buildPaths: [
ROOT_SRC,
EXTENSION_SRC,
EXTENSION_EXTRA_SRC,
EXTENSION_MANIFEST,
EXTENSION_PACKAGE,
ROOT_TSCONFIG,
ROOT_PACKAGE,
DIST_ENTRY,
DIST_EXTENSION_SRC,
BUILD_STAMP,
],
});
const requirement = resolveBuildRequirement(
createBuildRequirementDeps(tmp, {
gitHead: "abc123\n",
gitStatus: ` M ${EXTENSION_PACKAGE}\n`,
}),
);
expect(requirement).toEqual({
shouldBuild: true,
reason: "dirty_watched_tree",
});
});
});
it("rebuilds when clean bundled plugin dist outputs are partially missing", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp, {
files: {
[ROOT_SRC]: "export const value = 1;\n",
[EXTENSION_SRC]: "export default {};\n",
[EXTENSION_EXTRA_SRC]: "export const extra = true;\n",
[EXTENSION_MANIFEST]: '{"id":"demo","configSchema":{"type":"object"}}\n',
[EXTENSION_PACKAGE]: '{"openclaw":{"extensions":["./src/index.ts","./src/extra.ts"]}}\n',
[DIST_EXTENSION_SRC]: "export default {};\n",
},
buildPaths: [
ROOT_SRC,
EXTENSION_SRC,
EXTENSION_EXTRA_SRC,
EXTENSION_MANIFEST,
EXTENSION_PACKAGE,
ROOT_TSCONFIG,
ROOT_PACKAGE,
DIST_ENTRY,
DIST_EXTENSION_SRC,
BUILD_STAMP,
],
});
const requirement = resolveBuildRequirement(
createBuildRequirementDeps(tmp, {
gitHead: "abc123\n",
gitStatus: "",
}),
);
expect(requirement).toEqual({
shouldBuild: true,
reason: "missing_bundled_plugin_dist_entry",
});
});
});
it("rebuilds when a clean stamped bundled plugin dist directory is missing", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp, {
files: {
[ROOT_SRC]: "export const value = 1;\n",
[EXTENSION_SRC]: "export default {};\n",
[EXTENSION_MANIFEST]: '{"id":"demo","configSchema":{"type":"object"}}\n',
[EXTENSION_PACKAGE]: '{"openclaw":{"extensions":["./src/index.ts"]}}\n',
},
buildPaths: [
ROOT_SRC,
EXTENSION_SRC,
EXTENSION_MANIFEST,
EXTENSION_PACKAGE,
ROOT_TSCONFIG,
ROOT_PACKAGE,
DIST_ENTRY,
BUILD_STAMP,
],
});
const requirement = resolveBuildRequirement(
createBuildRequirementDeps(tmp, {
gitHead: "abc123\n",
gitStatus: "",
}),
);
expect(requirement).toEqual({
shouldBuild: true,
reason: "missing_bundled_plugin_dist_entry",
});
});
});
it("reports clean runtime postbuild artifacts when the runtime stamp matches HEAD", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp, {
@@ -1476,17 +1635,117 @@ describe("run-node script", () => {
});
});
it("reports missing runtime postbuild outputs even when stamps match HEAD", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp, {
files: {
[ROOT_SRC]: "export const value = 1;\n",
[EXTENSION_SRC]: "export default {};\n",
[EXTENSION_MANIFEST]: '{"id":"demo","configSchema":{"type":"object"}}\n',
[EXTENSION_PACKAGE]: '{"openclaw":{"extensions":["./src/index.ts"]}}\n',
[DIST_EXTENSION_SRC]: "export default {};\n",
[DIST_EXTENSION_MANIFEST]: '{"id":"demo","configSchema":{"type":"object"}}\n',
[DIST_EXTENSION_PACKAGE]: '{"openclaw":{"extensions":["./src/index.js"]}}\n',
[DIST_EXTENSION_RUNTIME_SRC]: "export default {};\n",
[DIST_RUNTIME_EXTENSION_MANIFEST]: '{"id":"demo","configSchema":{"type":"object"}}\n',
[DIST_RUNTIME_EXTENSION_PACKAGE]: '{"openclaw":{"extensions":["./src/index.js"]}}\n',
[RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n',
},
buildPaths: [
ROOT_SRC,
EXTENSION_SRC,
EXTENSION_MANIFEST,
EXTENSION_PACKAGE,
DIST_ENTRY,
DIST_EXTENSION_SRC,
DIST_EXTENSION_MANIFEST,
DIST_EXTENSION_PACKAGE,
DIST_EXTENSION_RUNTIME_SRC,
DIST_RUNTIME_EXTENSION_MANIFEST,
DIST_RUNTIME_EXTENSION_PACKAGE,
BUILD_STAMP,
RUNTIME_POSTBUILD_STAMP,
],
});
await fs.rm(resolvePath(tmp, DIST_EXTENSION_PACKAGE));
const requirement = resolveRuntimePostBuildRequirement(
createBuildRequirementDeps(tmp, {
gitHead: "abc123\n",
gitStatus: "",
}),
);
expect(requirement).toEqual({
shouldSync: true,
reason: "missing_runtime_postbuild_output",
});
});
});
it("reports missing runtime skill outputs even when stamps match HEAD", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp, {
files: {
[ROOT_SRC]: "export const value = 1;\n",
[EXTENSION_INDEX]: "export default {};\n",
[EXTENSION_MANIFEST]: '{"id":"demo","skills":["./skills/SKILL.md"]}\n',
[EXTENSION_SKILL]: "# Demo\n",
[DIST_EXTENSION_INDEX]: "export default {};\n",
[DIST_EXTENSION_MANIFEST]: '{"id":"demo","skills":["./skills/SKILL.md"]}\n',
[DIST_EXTENSION_SKILL]: "# Demo\n",
[DIST_RUNTIME_EXTENSION_INDEX]: "export default {};\n",
[DIST_RUNTIME_EXTENSION_MANIFEST]: '{"id":"demo","skills":["./skills/SKILL.md"]}\n',
[DIST_RUNTIME_EXTENSION_SKILL]: "# Demo\n",
[RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n',
},
buildPaths: [
ROOT_SRC,
EXTENSION_INDEX,
EXTENSION_MANIFEST,
EXTENSION_SKILL,
DIST_ENTRY,
DIST_EXTENSION_INDEX,
DIST_EXTENSION_MANIFEST,
DIST_EXTENSION_SKILL,
DIST_RUNTIME_EXTENSION_INDEX,
DIST_RUNTIME_EXTENSION_MANIFEST,
DIST_RUNTIME_EXTENSION_SKILL,
BUILD_STAMP,
RUNTIME_POSTBUILD_STAMP,
],
});
await fs.rm(resolvePath(tmp, DIST_RUNTIME_EXTENSION_SKILL));
const requirement = resolveRuntimePostBuildRequirement(
createBuildRequirementDeps(tmp, {
gitHead: "abc123\n",
gitStatus: "",
}),
);
expect(requirement).toEqual({
shouldSync: true,
reason: "missing_runtime_postbuild_output",
});
});
});
it("reports dirty runtime postbuild inputs separately from rebuild inputs", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp, {
files: {
[ROOT_SRC]: "export const value = 1;\n",
[EXTENSION_INDEX]: "export default {};\n",
[EXTENSION_MANIFEST]: '{"id":"demo","configSchema":{"type":"object"}}\n',
[RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n',
[DIST_EXTENSION_INDEX]: "export default {};\n",
},
buildPaths: [
ROOT_SRC,
EXTENSION_INDEX,
EXTENSION_MANIFEST,
DIST_EXTENSION_INDEX,
ROOT_TSCONFIG,
ROOT_PACKAGE,
DIST_ENTRY,
@@ -1535,21 +1794,58 @@ describe("run-node script", () => {
});
});
it("reports bundled skill edits as runtime postbuild inputs", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp, {
files: {
[ROOT_SRC]: "export const value = 1;\n",
[EXTENSION_MANIFEST]: '{"id":"demo","skills":["./skills/SKILL.md"]}\n',
[EXTENSION_SKILL]: "# Demo\n",
[RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n',
},
buildPaths: [
ROOT_SRC,
EXTENSION_MANIFEST,
EXTENSION_SKILL,
ROOT_TSCONFIG,
ROOT_PACKAGE,
DIST_ENTRY,
BUILD_STAMP,
RUNTIME_POSTBUILD_STAMP,
],
});
const deps = createBuildRequirementDeps(tmp, {
gitHead: "abc123\n",
gitStatus: ` M ${EXTENSION_SKILL}\n`,
});
expect(resolveRuntimePostBuildRequirement(deps)).toEqual({
shouldSync: true,
reason: "dirty_runtime_postbuild_inputs",
});
});
});
it("repairs missing bundled plugin metadata without rerunning tsdown", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp, {
files: {
[ROOT_SRC]: "export const value = 1;\n",
[EXTENSION_INDEX]: "export default {};\n",
[EXTENSION_MANIFEST]: '{"id":"demo","configSchema":{"type":"object"}}\n',
[ROOT_TSDOWN]: "export default {};\n",
[DIST_EXTENSION_INDEX]: "export default {};\n",
},
buildPaths: [
ROOT_SRC,
EXTENSION_INDEX,
EXTENSION_MANIFEST,
ROOT_TSCONFIG,
ROOT_PACKAGE,
ROOT_TSDOWN,
DIST_ENTRY,
DIST_EXTENSION_INDEX,
BUILD_STAMP,
],
});