mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(gateway): restore runtime postbuild freshness
This commit is contained in:
committed by
Peter Steinberger
parent
0a13bd5841
commit
17643e549f
@@ -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
@@ -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
@@ -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,
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user