mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:10:43 +00:00
refactor: simplify plugin dependency handling
Simplify plugin installation and runtime loading around package-manager-owned dependencies, with Jiti reserved for local/TS fallback paths. Also scans npm plugin install roots so hoisted transitive dependencies are covered by dependency denylist and node_modules symlink checks.
This commit is contained in:
committed by
GitHub
parent
2e8e9cd6ca
commit
ed8f50f240
@@ -1,4 +1,4 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
buildPublishedInstallScenarios,
|
||||
collectInstalledContextEngineRuntimeErrors,
|
||||
collectInstalledRootDependencyManifestErrors,
|
||||
collectInstalledMirroredRootDependencyManifestErrors,
|
||||
collectInstalledPackageErrors,
|
||||
normalizeInstalledBinaryVersion,
|
||||
resolveInstalledBinaryPath,
|
||||
@@ -156,285 +155,6 @@ describe("resolveInstalledBinaryPath", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectInstalledMirroredRootDependencyManifestErrors", () => {
|
||||
function makeInstalledPackageRoot(): string {
|
||||
return mkdtempSync(join(tmpdir(), "openclaw-postpublish-installed-"));
|
||||
}
|
||||
|
||||
function writePackageFile(root: string, relativePath: string, value: unknown): void {
|
||||
const fullPath = join(root, relativePath);
|
||||
mkdirSync(dirname(fullPath), { recursive: true });
|
||||
writeFileSync(fullPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
function writeSlackWebApiProbePackage(params: {
|
||||
root: string;
|
||||
importerSource?: string;
|
||||
importerPath?: string;
|
||||
mirroredRootRuntimeDependencies?: string[];
|
||||
rootDependencies?: Record<string, string>;
|
||||
rootOptionalDependencies?: Record<string, string>;
|
||||
}): void {
|
||||
writePackageFile(params.root, "package.json", {
|
||||
version: "2026.4.10",
|
||||
dependencies: params.rootDependencies,
|
||||
optionalDependencies: params.rootOptionalDependencies,
|
||||
openclaw: params.mirroredRootRuntimeDependencies
|
||||
? {
|
||||
bundle: {
|
||||
mirroredRootRuntimeDependencies: params.mirroredRootRuntimeDependencies,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
writePackageFile(params.root, "dist/extensions/slack/package.json", {
|
||||
dependencies: {
|
||||
"@slack/web-api": "^7.15.0",
|
||||
},
|
||||
});
|
||||
const importerPath = params.importerPath ?? "dist/probe-Cz2PiFtC.js";
|
||||
mkdirSync(join(params.root, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(params.root, importerPath),
|
||||
params.importerSource ?? 'import("@slack/web-api");\n',
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
it("flags bundled plugin deps imported by root dist when root mirrors are missing", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writeSlackWebApiProbePackage({ root: packageRoot });
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
"installed package root is missing mirrored bundled runtime dependency '@slack/web-api' for dist importers: probe-Cz2PiFtC.js. Add it to package.json dependencies/optionalDependencies or keep imports under dist/extensions/slack/.",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("allows bundled plugin deps imported from their own extension dist without root mirrors", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writeSlackWebApiProbePackage({
|
||||
root: packageRoot,
|
||||
importerPath: "dist/extensions/slack/client-Cz2PiFtC.js",
|
||||
});
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("allows bundled plugin deps imported from root chunks sourced from their extension", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writeSlackWebApiProbePackage({
|
||||
root: packageRoot,
|
||||
importerSource: '//#region extensions/slack/client.ts\nimport("@slack/web-api");\n',
|
||||
});
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not require root mirrors for extension-only Matrix crypto deps", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.10",
|
||||
dependencies: {},
|
||||
});
|
||||
writePackageFile(packageRoot, "dist/extensions/matrix/package.json", {
|
||||
dependencies: {
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "18.1.0",
|
||||
},
|
||||
});
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist/extensions/matrix/crypto-node.runtime.js"),
|
||||
'require("@matrix-org/matrix-sdk-crypto-nodejs");\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts mirrored root dependencies declared in package optionalDependencies", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.10",
|
||||
optionalDependencies: {
|
||||
"@discordjs/opus": "^0.10.0",
|
||||
},
|
||||
openclaw: {
|
||||
bundle: {
|
||||
mirroredRootRuntimeDependencies: ["@discordjs/opus"],
|
||||
},
|
||||
},
|
||||
});
|
||||
writePackageFile(packageRoot, "dist/extensions/discord/package.json", {
|
||||
optionalDependencies: {
|
||||
"@discordjs/opus": "^0.10.0",
|
||||
},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "probe-Cz2PiFtC.js"),
|
||||
'require("@discordjs/opus");\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not compare root mirror dependency versions", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writeSlackWebApiProbePackage({
|
||||
root: packageRoot,
|
||||
mirroredRootRuntimeDependencies: ["@slack/web-api"],
|
||||
rootDependencies: {
|
||||
"@slack/web-api": "^7.16.0",
|
||||
},
|
||||
});
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("flags malformed bundled extension manifests instead of silently skipping them", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.10",
|
||||
dependencies: {},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist/extensions/slack"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist/extensions/slack/package.json"),
|
||||
'{\n "openclaw": { invalid json\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
expect.stringContaining("installed bundled extension manifest invalid: failed to parse"),
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("flags bundled extension directories that are missing package.json", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.10",
|
||||
dependencies: {},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist/extensions/slack"), { recursive: true });
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
`installed bundled extension manifest missing: ${join(packageRoot, "dist/extensions/slack/package.json")}.`,
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("skips manifest-only sidecar directories without package.json", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.10",
|
||||
dependencies: {},
|
||||
});
|
||||
writePackageFile(packageRoot, "dist/extensions/device-pair/openclaw.plugin.json", {
|
||||
id: "device-pair",
|
||||
});
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts legacy qa channel sidecar directories without package.json", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.10",
|
||||
dependencies: {},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist/extensions/qa-channel"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist/extensions/qa-channel/runtime-api.js"),
|
||||
"export {};\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects bundled extension manifests that are not regular files", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
const outsideManifestRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.10",
|
||||
dependencies: {},
|
||||
});
|
||||
writePackageFile(outsideManifestRoot, "package.json", {
|
||||
dependencies: {
|
||||
"@slack/web-api": "^7.15.0",
|
||||
},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist/extensions/slack"), { recursive: true });
|
||||
symlinkSync(
|
||||
join(outsideManifestRoot, "package.json"),
|
||||
join(packageRoot, "dist/extensions/slack/package.json"),
|
||||
);
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
expect.stringContaining("installed bundled extension manifest invalid: failed to parse"),
|
||||
]);
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)[0]).toContain(
|
||||
"manifest must be a regular file",
|
||||
);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
rmSync(outsideManifestRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectInstalledRootDependencyManifestErrors", () => {
|
||||
function makeInstalledPackageRoot(): string {
|
||||
return mkdtempSync(join(tmpdir(), "openclaw-postpublish-root-deps-"));
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
|
||||
const REQUIRED_PACKED_PATHS = [
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
"scripts/lib/bundled-runtime-deps-install.mjs",
|
||||
...WORKSPACE_TEMPLATE_PACK_PATHS,
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { bundledDistPluginFile, bundledPluginFile } from "openclaw/plugin-sdk/test-fixtures";
|
||||
@@ -13,13 +13,8 @@ import { collectInstalledRootDependencyManifestErrors } from "../scripts/opencla
|
||||
import {
|
||||
collectAppcastSparkleVersionErrors,
|
||||
collectBundledExtensionManifestErrors,
|
||||
collectBundledPluginRootRuntimeMirrorErrors,
|
||||
collectCriticalPluginSdkEntrypointSizeErrors,
|
||||
collectDeclaredRootRuntimeDependencyMetadataErrors,
|
||||
collectForbiddenPackContentPaths,
|
||||
collectInstalledBundledPluginRuntimeDepErrors,
|
||||
bundledRuntimeDependencySentinelCandidates,
|
||||
collectRootDistBundledRuntimeMirrors,
|
||||
collectForbiddenPackPaths,
|
||||
collectMissingPackPaths,
|
||||
collectPackUnpackedSizeErrors,
|
||||
@@ -275,117 +270,6 @@ describe("bundled plugin root runtime mirrors", () => {
|
||||
expect(packageNameFromSpecifier("./local")).toBeNull();
|
||||
});
|
||||
|
||||
it("derives required root mirrors from built root dist imports", () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-root-mirror-"));
|
||||
|
||||
try {
|
||||
const distDir = join(tempRoot, "dist");
|
||||
mkdirSync(join(distDir, "extensions", "feishu"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(distDir, "probe-Cz2PiFtC.js"),
|
||||
`import("@larksuiteoapi/node-sdk");\nrequire("grammy");\n`,
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
join(distDir, "extensions", "feishu", "index.js"),
|
||||
`import("@larksuiteoapi/node-sdk");\n`,
|
||||
"utf8",
|
||||
);
|
||||
mkdirSync(join(distDir, "extensions", "feishu", "node_modules", "@larksuiteoapi"), {
|
||||
recursive: true,
|
||||
});
|
||||
writeFileSync(
|
||||
join(distDir, "extensions", "feishu", "node_modules", "@larksuiteoapi", "node-sdk.js"),
|
||||
`import("@larksuiteoapi/node-sdk");\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const mirrors = collectRootDistBundledRuntimeMirrors({
|
||||
bundledRuntimeDependencySpecs: makeBundledSpecs(),
|
||||
distDir,
|
||||
});
|
||||
|
||||
expect([...mirrors.keys()].toSorted((left, right) => left.localeCompare(right))).toEqual([
|
||||
"@larksuiteoapi/node-sdk",
|
||||
]);
|
||||
expect([...mirrors.get("@larksuiteoapi/node-sdk")!.importers]).toEqual(["probe-Cz2PiFtC.js"]);
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("flags missing root mirrors for plugin deps imported by root dist", () => {
|
||||
expect(
|
||||
collectBundledPluginRootRuntimeMirrorErrors({
|
||||
bundledRuntimeDependencySpecs: makeBundledSpecs(),
|
||||
requiredRootMirrors: new Map([
|
||||
[
|
||||
"@larksuiteoapi/node-sdk",
|
||||
{
|
||||
importers: new Set(["probe-Cz2PiFtC.js"]),
|
||||
pluginIds: ["feishu"],
|
||||
spec: "^1.60.0",
|
||||
},
|
||||
],
|
||||
]),
|
||||
rootPackageJson: { dependencies: {} },
|
||||
}),
|
||||
).toEqual([
|
||||
"installed package root is missing mirrored bundled runtime dependency '@larksuiteoapi/node-sdk' for dist importers: probe-Cz2PiFtC.js. Add it to package.json dependencies/optionalDependencies or keep imports under dist/extensions/feishu/.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags mirrored root runtime metadata without root deps", () => {
|
||||
expect(
|
||||
collectDeclaredRootRuntimeDependencyMetadataErrors({
|
||||
dependencies: { semver: "7.7.4" },
|
||||
openclaw: {
|
||||
bundle: {
|
||||
mirroredRootRuntimeDependencies: ["json5", "semver"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual([
|
||||
"package.json openclaw.bundle.mirroredRootRuntimeDependencies declares 'json5' but package.json dependencies/optionalDependencies do not include it.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("accepts mirrored root runtime metadata backed by root deps", () => {
|
||||
expect(
|
||||
collectDeclaredRootRuntimeDependencyMetadataErrors({
|
||||
dependencies: { json5: "^2.2.3", semver: "7.7.4" },
|
||||
openclaw: {
|
||||
bundle: {
|
||||
mirroredRootRuntimeDependencies: ["json5", "semver"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not derive root mirrors for root chunks sourced from the owning plugin", () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-root-mirror-owned-"));
|
||||
|
||||
try {
|
||||
const distDir = join(tempRoot, "dist");
|
||||
mkdirSync(distDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(distDir, "probe-Cz2PiFtC.js"),
|
||||
`//#region extensions/feishu/client.ts\nimport("@larksuiteoapi/node-sdk");\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const mirrors = collectRootDistBundledRuntimeMirrors({
|
||||
bundledRuntimeDependencySpecs: makeBundledSpecs(),
|
||||
distDir,
|
||||
});
|
||||
|
||||
expect([...mirrors.keys()]).toEqual([]);
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not require root deps for root chunks sourced from the owning installed plugin", () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-root-owned-installed-"));
|
||||
|
||||
@@ -441,100 +325,6 @@ describe("bundled plugin root runtime mirrors", () => {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not compare root mirror versions for plugin manifest deps", () => {
|
||||
expect(
|
||||
collectBundledPluginRootRuntimeMirrorErrors({
|
||||
bundledRuntimeDependencySpecs: makeBundledSpecs(),
|
||||
requiredRootMirrors: new Map([
|
||||
[
|
||||
"@larksuiteoapi/node-sdk",
|
||||
{
|
||||
importers: new Set(["probe-Cz2PiFtC.js"]),
|
||||
pluginIds: ["feishu"],
|
||||
spec: "^1.60.0",
|
||||
},
|
||||
],
|
||||
]),
|
||||
rootPackageJson: {
|
||||
dependencies: { "@larksuiteoapi/node-sdk": "^1.61.0" },
|
||||
openclaw: {
|
||||
bundle: {
|
||||
mirroredRootRuntimeDependencies: ["@larksuiteoapi/node-sdk"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("flags root mirrors omitted from mirrored root runtime metadata", () => {
|
||||
expect(
|
||||
collectBundledPluginRootRuntimeMirrorErrors({
|
||||
bundledRuntimeDependencySpecs: makeBundledSpecs(),
|
||||
requiredRootMirrors: new Map([
|
||||
[
|
||||
"@larksuiteoapi/node-sdk",
|
||||
{
|
||||
importers: new Set(["probe-Cz2PiFtC.js"]),
|
||||
pluginIds: ["feishu"],
|
||||
spec: "^1.60.0",
|
||||
},
|
||||
],
|
||||
]),
|
||||
rootPackageJson: { dependencies: { "@larksuiteoapi/node-sdk": "^1.60.0" } },
|
||||
}),
|
||||
).toEqual([
|
||||
"installed package root mirror '@larksuiteoapi/node-sdk' for dist importers: probe-Cz2PiFtC.js is missing from package.json openclaw.bundle.mirroredRootRuntimeDependencies. Add it there so packaged runtime installs the mirrored dependency, or keep imports under dist/extensions/feishu/.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("accepts matching root mirrors for plugin deps imported by root dist", () => {
|
||||
expect(
|
||||
collectBundledPluginRootRuntimeMirrorErrors({
|
||||
bundledRuntimeDependencySpecs: makeBundledSpecs(),
|
||||
requiredRootMirrors: new Map([
|
||||
[
|
||||
"@larksuiteoapi/node-sdk",
|
||||
{
|
||||
importers: new Set(["probe-Cz2PiFtC.js"]),
|
||||
pluginIds: ["feishu"],
|
||||
spec: "^1.60.0",
|
||||
},
|
||||
],
|
||||
]),
|
||||
rootPackageJson: {
|
||||
dependencies: { "@larksuiteoapi/node-sdk": "^1.60.0" },
|
||||
openclaw: {
|
||||
bundle: {
|
||||
mirroredRootRuntimeDependencies: ["@larksuiteoapi/node-sdk"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("flags conflicting plugin dependency specs", () => {
|
||||
expect(
|
||||
collectBundledPluginRootRuntimeMirrorErrors({
|
||||
bundledRuntimeDependencySpecs: new Map([
|
||||
[
|
||||
"@example/sdk",
|
||||
{
|
||||
conflicts: [{ pluginId: "right", spec: "2.0.0" }],
|
||||
pluginIds: ["left"],
|
||||
spec: "1.0.0",
|
||||
},
|
||||
],
|
||||
]),
|
||||
requiredRootMirrors: new Map(),
|
||||
rootPackageJson: { dependencies: {} },
|
||||
}),
|
||||
).toEqual([
|
||||
"bundled runtime dependency '@example/sdk' has conflicting plugin specs: left use '1.0.0', right uses '2.0.0'.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectForbiddenPackPaths", () => {
|
||||
@@ -690,7 +480,6 @@ describe("collectMissingPackPaths", () => {
|
||||
"dist/control-ui/index.html",
|
||||
"scripts/npm-runner.mjs",
|
||||
"scripts/preinstall-package-manager-warning.mjs",
|
||||
"scripts/lib/bundled-runtime-deps-install.mjs",
|
||||
"scripts/lib/package-dist-imports.mjs",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"dist/task-registry-control.runtime.js",
|
||||
@@ -723,7 +512,6 @@ describe("collectMissingPackPaths", () => {
|
||||
...WORKSPACE_TEMPLATE_PACK_PATHS,
|
||||
"scripts/npm-runner.mjs",
|
||||
"scripts/preinstall-package-manager-warning.mjs",
|
||||
"scripts/lib/bundled-runtime-deps-install.mjs",
|
||||
"scripts/lib/package-dist-imports.mjs",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"dist/plugin-sdk/root-alias.cjs",
|
||||
@@ -833,112 +621,3 @@ describe("createPackedBundledPluginPostinstallEnv", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectInstalledBundledPluginRuntimeDepErrors", () => {
|
||||
function createPackageRoot(): string {
|
||||
const packageRoot = mkdtempSync(join(tmpdir(), "release-check-installed-bundled-"));
|
||||
mkdirSync(join(packageRoot, "dist", "extensions"), { recursive: true });
|
||||
return packageRoot;
|
||||
}
|
||||
|
||||
function writeBundledPluginPackageJson(
|
||||
packageRoot: string,
|
||||
pluginId: string,
|
||||
packageJson: Record<string, unknown>,
|
||||
): void {
|
||||
const pluginRoot = join(packageRoot, "dist", "extensions", pluginId);
|
||||
mkdirSync(pluginRoot, { recursive: true });
|
||||
writeFileSync(join(pluginRoot, "package.json"), JSON.stringify(packageJson, null, 2));
|
||||
}
|
||||
|
||||
function installRuntimeDependencyAtPackageRoot(
|
||||
packageRoot: string,
|
||||
dependencyName: string,
|
||||
version: string,
|
||||
): void {
|
||||
const dependencyRoot = join(packageRoot, "node_modules", ...dependencyName.split("/"));
|
||||
mkdirSync(dependencyRoot, { recursive: true });
|
||||
writeFileSync(
|
||||
join(dependencyRoot, "package.json"),
|
||||
JSON.stringify({ name: dependencyName, version }, null, 2),
|
||||
);
|
||||
}
|
||||
|
||||
it("returns no errors when declared deps are installed at the openclaw package root", () => {
|
||||
const packageRoot = createPackageRoot();
|
||||
try {
|
||||
writeBundledPluginPackageJson(packageRoot, "whatsapp", {
|
||||
name: "@openclaw/whatsapp",
|
||||
dependencies: { "@whiskeysockets/baileys": "7.0.0-rc.9" },
|
||||
openclaw: { bundle: { stageRuntimeDependencies: true } },
|
||||
});
|
||||
installRuntimeDependencyAtPackageRoot(packageRoot, "@whiskeysockets/baileys", "7.0.0-rc.9");
|
||||
|
||||
expect(collectInstalledBundledPluginRuntimeDepErrors(packageRoot)).toEqual([]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("surfaces an error naming the owning plugin and missing dependency", () => {
|
||||
const packageRoot = createPackageRoot();
|
||||
try {
|
||||
writeBundledPluginPackageJson(packageRoot, "whatsapp", {
|
||||
name: "@openclaw/whatsapp",
|
||||
dependencies: { "@whiskeysockets/baileys": "7.0.0-rc.9" },
|
||||
openclaw: { bundle: { stageRuntimeDependencies: true } },
|
||||
});
|
||||
|
||||
expect(collectInstalledBundledPluginRuntimeDepErrors(packageRoot)).toEqual([
|
||||
"bundled plugin runtime dependency '@whiskeysockets/baileys@7.0.0-rc.9' (owners: whatsapp) is missing at node_modules/@whiskeysockets/baileys/package.json.",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("bundledRuntimeDependencySentinelCandidates", () => {
|
||||
it("checks canonical external runtime-deps roots for packed installs", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "release-check-runtime-candidates-"));
|
||||
const packageRoot = join(root, "package");
|
||||
const aliasRoot = join(root, "package-alias");
|
||||
const homeRoot = join(root, "home");
|
||||
try {
|
||||
mkdirSync(join(packageRoot, "dist", "extensions", "browser"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2026.4.25-beta.1" }, null, 2),
|
||||
);
|
||||
symlinkSync(packageRoot, aliasRoot, "dir");
|
||||
|
||||
const candidates = bundledRuntimeDependencySentinelCandidates(
|
||||
aliasRoot,
|
||||
"browser",
|
||||
"playwright-core",
|
||||
{ HOME: homeRoot } as NodeJS.ProcessEnv,
|
||||
);
|
||||
const realRootCandidates = bundledRuntimeDependencySentinelCandidates(
|
||||
packageRoot,
|
||||
"browser",
|
||||
"playwright-core",
|
||||
{ HOME: homeRoot } as NodeJS.ProcessEnv,
|
||||
);
|
||||
const externalCandidates = candidates.filter(
|
||||
(candidate) =>
|
||||
candidate.startsWith(join(homeRoot, ".openclaw", "plugin-runtime-deps")) &&
|
||||
candidate.endsWith(join("node_modules", "playwright-core", "package.json")),
|
||||
);
|
||||
const realRootExternalCandidates = realRootCandidates.filter(
|
||||
(candidate) =>
|
||||
candidate.startsWith(join(homeRoot, ".openclaw", "plugin-runtime-deps")) &&
|
||||
candidate.endsWith(join("node_modules", "playwright-core", "package.json")),
|
||||
);
|
||||
|
||||
expect(externalCandidates).toEqual(realRootExternalCandidates);
|
||||
expect(externalCandidates).toHaveLength(1);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,6 +87,14 @@ describe("bundled plugin build entries", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps explicitly downloadable plugins out of bundled package artifacts", () => {
|
||||
const entries = listBundledPluginBuildEntries();
|
||||
const artifacts = listBundledPluginPackArtifacts();
|
||||
|
||||
expect(Object.keys(entries).some((entry) => entry.startsWith("extensions/qqbot/"))).toBe(false);
|
||||
expect(artifacts.some((artifact) => artifact.startsWith("dist/extensions/qqbot/"))).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps bundled channel secret contracts on packed top-level sidecars", () => {
|
||||
const artifacts = listBundledPluginPackArtifacts();
|
||||
const offenders: string[] = [];
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { collectBuiltBundledPluginStagedRuntimeDependencyErrors } from "../../scripts/lib/bundled-plugin-root-runtime-mirrors.mjs";
|
||||
import { createScriptTestHarness } from "./test-helpers.js";
|
||||
|
||||
const { createTempDir } = createScriptTestHarness();
|
||||
|
||||
function writeJson(root: string, relativePath: string, value: unknown) {
|
||||
const fullPath = path.join(root, relativePath);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
describe("collectBuiltBundledPluginStagedRuntimeDependencyErrors", () => {
|
||||
it("flags built staged plugins whose dist node_modules are missing runtime deps", () => {
|
||||
const repoRoot = createTempDir("openclaw-runtime-contracts-");
|
||||
|
||||
writeJson(repoRoot, "dist/extensions/diffs/package.json", {
|
||||
name: "@openclaw/diffs",
|
||||
dependencies: {
|
||||
"@pierre/diffs": "^0.1.0",
|
||||
},
|
||||
openclaw: {
|
||||
bundle: {
|
||||
stageRuntimeDependencies: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
collectBuiltBundledPluginStagedRuntimeDependencyErrors({
|
||||
bundledPluginsDir: path.join(repoRoot, "dist/extensions"),
|
||||
}),
|
||||
).toEqual([
|
||||
"built bundled plugin 'diffs' is missing staged runtime dependency '@pierre/diffs: ^0.1.0' under dist/extensions/diffs/node_modules.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("accepts built staged plugins when their staged runtime deps are present", () => {
|
||||
const repoRoot = createTempDir("openclaw-runtime-contracts-");
|
||||
|
||||
writeJson(repoRoot, "dist/extensions/diffs/package.json", {
|
||||
name: "@openclaw/diffs",
|
||||
dependencies: {
|
||||
"@pierre/diffs": "^0.1.0",
|
||||
},
|
||||
openclaw: {
|
||||
bundle: {
|
||||
stageRuntimeDependencies: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
writeJson(repoRoot, "dist/extensions/diffs/node_modules/@pierre/diffs/package.json", {
|
||||
name: "@pierre/diffs",
|
||||
version: "0.1.0",
|
||||
});
|
||||
|
||||
expect(
|
||||
collectBuiltBundledPluginStagedRuntimeDependencyErrors({
|
||||
bundledPluginsDir: path.join(repoRoot, "dist/extensions"),
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps the WhatsApp bundled plugin opted into staged runtime dependencies", () => {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(path.join(process.cwd(), "extensions/whatsapp/package.json"), "utf8"),
|
||||
) as {
|
||||
dependencies?: Record<string, string>;
|
||||
openclaw?: {
|
||||
bundle?: {
|
||||
stageRuntimeDependencies?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
expect(packageJson.dependencies?.["@whiskeysockets/baileys"]).toBe("7.0.0-rc.9");
|
||||
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "../../scripts/lib/local-build-metadata-paths.mjs";
|
||||
|
||||
describe("check-gateway-watch-regression", () => {
|
||||
it("ignores top-level dist-runtime extension dependency repairs", () => {
|
||||
it("ignores top-level dist-runtime extension dependency debris", () => {
|
||||
expect(isIgnoredDistRuntimeWatchPath("dist-runtime/extensions/node_modules")).toBe(true);
|
||||
expect(
|
||||
isIgnoredDistRuntimeWatchPath(
|
||||
|
||||
@@ -144,8 +144,6 @@ describe("docker build helper", () => {
|
||||
expect(scenarios).toContain("`bundled-plugin-install-uninstall-${index}`");
|
||||
expect(scenarios).toContain("pnpm test:docker:bundled-plugin-install-uninstall");
|
||||
expect(scenarios).toContain("OPENCLAW_PLUGINS_E2E_CLAWHUB=0");
|
||||
expect(scenarios).toContain('"bundled-channel-deps-compat"');
|
||||
expect(scenarios).toContain("test:docker:bundled-channel-deps:fast");
|
||||
});
|
||||
|
||||
it("allows plugin update smoke to tolerate config metadata migrations", () => {
|
||||
|
||||
@@ -46,8 +46,6 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
expect(plan.lanes.map((lane) => lane.name)).toContain("install-e2e-anthropic");
|
||||
expect(plan.lanes.map((lane) => lane.name)).toContain("mcp-channels");
|
||||
expect(plan.lanes.map((lane) => lane.name)).toContain("commitments-safety");
|
||||
expect(plan.lanes.map((lane) => lane.name)).toContain("bundled-channel-feishu");
|
||||
expect(plan.lanes.map((lane) => lane.name)).toContain("bundled-channel-update-acpx");
|
||||
expect(plan.lanes.map((lane) => lane.name)).toContain("bundled-plugin-install-uninstall-0");
|
||||
expect(plan.lanes.map((lane) => lane.name)).toContain("bundled-plugin-install-uninstall-23");
|
||||
expect(plan.lanes.filter((lane) => lane.name === "install-e2e-openai")).toHaveLength(1);
|
||||
@@ -141,31 +139,6 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
profile: RELEASE_PATH_PROFILE,
|
||||
releaseChunk: "plugins-runtime-install-h",
|
||||
});
|
||||
const bundledChannelsCore = planFor({
|
||||
includeOpenWebUI: true,
|
||||
profile: RELEASE_PATH_PROFILE,
|
||||
releaseChunk: "bundled-channels-core",
|
||||
});
|
||||
const bundledChannelsUpdateA = planFor({
|
||||
includeOpenWebUI: true,
|
||||
profile: RELEASE_PATH_PROFILE,
|
||||
releaseChunk: "bundled-channels-update-a",
|
||||
});
|
||||
const bundledChannelsUpdateDiscord = planFor({
|
||||
includeOpenWebUI: true,
|
||||
profile: RELEASE_PATH_PROFILE,
|
||||
releaseChunk: "bundled-channels-update-discord",
|
||||
});
|
||||
const bundledChannelsUpdateB = planFor({
|
||||
includeOpenWebUI: true,
|
||||
profile: RELEASE_PATH_PROFILE,
|
||||
releaseChunk: "bundled-channels-update-b",
|
||||
});
|
||||
const bundledChannelsContracts = planFor({
|
||||
includeOpenWebUI: true,
|
||||
profile: RELEASE_PATH_PROFILE,
|
||||
releaseChunk: "bundled-channels-contracts",
|
||||
});
|
||||
|
||||
expect(packageInstallOpenAi.lanes.map((lane) => lane.name)).toEqual(["install-e2e-openai"]);
|
||||
expect(packageInstallAnthropic.lanes.map((lane) => lane.name)).toEqual([
|
||||
@@ -260,42 +233,6 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
"bundled-plugin-install-uninstall-22",
|
||||
"bundled-plugin-install-uninstall-23",
|
||||
]);
|
||||
expect(bundledChannelsCore.lanes.map((lane) => lane.name)).toEqual([
|
||||
"plugin-update",
|
||||
"bundled-channel-telegram",
|
||||
"bundled-channel-discord",
|
||||
"bundled-channel-slack",
|
||||
"bundled-channel-feishu",
|
||||
"bundled-channel-memory-lancedb",
|
||||
]);
|
||||
expect(bundledChannelsCore.lanes[0]).toMatchObject({
|
||||
name: "plugin-update",
|
||||
stateScenario: "empty",
|
||||
});
|
||||
expect(bundledChannelsUpdateA.lanes.map((lane) => lane.name)).toEqual([
|
||||
"bundled-channel-update-telegram",
|
||||
"bundled-channel-update-memory-lancedb",
|
||||
]);
|
||||
expect(bundledChannelsUpdateDiscord.lanes.map((lane) => lane.name)).toEqual([
|
||||
"bundled-channel-update-discord",
|
||||
]);
|
||||
expect(bundledChannelsUpdateDiscord.lanes[0]).toMatchObject({
|
||||
noOutputTimeoutMs: 4 * 60 * 1000,
|
||||
timeoutMs: 6 * 60 * 1000,
|
||||
});
|
||||
expect(bundledChannelsUpdateB.lanes.map((lane) => lane.name)).toEqual([
|
||||
"bundled-channel-update-slack",
|
||||
"bundled-channel-update-feishu",
|
||||
"bundled-channel-update-acpx",
|
||||
]);
|
||||
expect(bundledChannelsContracts.lanes.map((lane) => lane.name)).toEqual([
|
||||
"bundled-channel-root-owned",
|
||||
"bundled-channel-setup-entry",
|
||||
"bundled-channel-load-failure",
|
||||
"bundled-channel-disabled-config",
|
||||
]);
|
||||
expect(bundledChannelsCore.lanes.map((lane) => lane.name)).not.toContain("plugins");
|
||||
expect(bundledChannelsUpdateA.lanes.map((lane) => lane.name)).not.toContain("openwebui");
|
||||
});
|
||||
|
||||
it("keeps legacy release chunk names as aggregate aliases", () => {
|
||||
@@ -309,11 +246,6 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
profile: RELEASE_PATH_PROFILE,
|
||||
releaseChunk: "plugins-runtime",
|
||||
});
|
||||
const bundledChannelsUpdateALegacy = planFor({
|
||||
includeOpenWebUI: true,
|
||||
profile: RELEASE_PATH_PROFILE,
|
||||
releaseChunk: "bundled-channels-update-a-legacy",
|
||||
});
|
||||
const legacy = planFor({
|
||||
includeOpenWebUI: true,
|
||||
profile: RELEASE_PATH_PROFILE,
|
||||
@@ -335,19 +267,8 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
"openwebui",
|
||||
]),
|
||||
);
|
||||
expect(bundledChannelsUpdateALegacy.lanes.map((lane) => lane.name)).toEqual([
|
||||
"bundled-channel-update-telegram",
|
||||
"bundled-channel-update-discord",
|
||||
"bundled-channel-update-memory-lancedb",
|
||||
]);
|
||||
expect(legacy.lanes.map((lane) => lane.name)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"plugins",
|
||||
"bundled-plugin-install-uninstall-0",
|
||||
"plugin-update",
|
||||
"bundled-channel-update-acpx",
|
||||
"openwebui",
|
||||
]),
|
||||
expect.arrayContaining(["plugins", "bundled-plugin-install-uninstall-0", "openwebui"]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -487,8 +408,6 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
"plugin-update",
|
||||
"plugins",
|
||||
"kitchen-sink-plugin",
|
||||
"bundled-channel-deps-compat",
|
||||
"bundled-channel-setup-entry",
|
||||
"bundled-plugin-install-uninstall-0",
|
||||
"commitments-safety",
|
||||
"update-channel-switch",
|
||||
@@ -557,14 +476,6 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
name: "kitchen-sink-plugin",
|
||||
stateScenario: "empty",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "bundled-channel-deps-compat",
|
||||
stateScenario: "empty",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "bundled-channel-setup-entry",
|
||||
stateScenario: "empty",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "bundled-plugin-install-uninstall-0",
|
||||
stateScenario: "empty",
|
||||
@@ -584,19 +495,6 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("maps the legacy bundled channel deps lane to the split compat lane", () => {
|
||||
const selectedLaneNames = parseLaneSelection("bundled-channel-deps");
|
||||
const plan = planFor({ selectedLaneNames });
|
||||
|
||||
expect(selectedLaneNames).toEqual(["bundled-channel-deps-compat"]);
|
||||
expect(plan.lanes).toEqual([
|
||||
expect.objectContaining({
|
||||
imageKind: "bare",
|
||||
name: "bundled-channel-deps-compat",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("maps installer E2E to provider-specific package install lanes", () => {
|
||||
const selectedLaneNames = parseLaneSelection("install-e2e");
|
||||
const plan = planFor({ selectedLaneNames });
|
||||
|
||||
@@ -98,17 +98,7 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("retries transient bundled runtime deps staging failures during agent turns", () => {
|
||||
expect(
|
||||
shouldRetryCrossOsAgentTurnError(
|
||||
new Error("document-extract: failed to install bundled runtime deps: npm install failed"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldRetryCrossOsAgentTurnError(
|
||||
new Error("document-extract failed to stage bundled runtime deps after 463ms"),
|
||||
),
|
||||
).toBe(true);
|
||||
it("retries transient agent-turn failures", () => {
|
||||
expect(
|
||||
shouldRetryCrossOsAgentTurnError(
|
||||
new Error("Agent output did not contain the expected OK marker."),
|
||||
@@ -671,7 +661,7 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects bundled runtime-deps staging debris before candidate inventory generation", async () => {
|
||||
it("rejects legacy plugin dependency staging debris before candidate inventory generation", async () => {
|
||||
const packageRoot = mkdtempSync(join(tmpdir(), "openclaw-cross-os-stage-debris-"));
|
||||
try {
|
||||
mkdirSync(
|
||||
@@ -689,7 +679,7 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
|
||||
sourceDir: packageRoot,
|
||||
logPath: join(packageRoot, "npm-pack-dry-run.log"),
|
||||
}),
|
||||
).rejects.toThrow("unexpected bundled-runtime-deps install staging debris");
|
||||
).rejects.toThrow("unexpected legacy plugin dependency staging debris");
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ describe("package acceptance workflow", () => {
|
||||
expect(workflow).toContain("npm-onboard-channel-agent doctor-switch");
|
||||
expect(workflow).toContain("update-channel-switch upgrade-survivor");
|
||||
expect(workflow).toContain("published-upgrade-survivor");
|
||||
expect(workflow).toContain("bundled-channel-deps-compat");
|
||||
expect(workflow).toContain("plugins-offline plugin-update");
|
||||
expect(workflow).toContain("include_release_path_suites=true");
|
||||
expect(workflow).not.toContain("telegram_mode requires source=npm");
|
||||
@@ -376,7 +375,7 @@ describe("package artifact reuse", () => {
|
||||
"package_sha256: ${{ needs.prepare_release_package.outputs.package_sha256 }}",
|
||||
);
|
||||
expect(workflow).toContain("suite_profile: custom");
|
||||
expect(workflow).toContain("docker_lanes: bundled-channel-deps-compat plugins-offline");
|
||||
expect(workflow).toContain("docker_lanes: plugins-offline plugin-update");
|
||||
expect(workflow).toContain("telegram_mode: mock-openai");
|
||||
expect(workflow).toContain(
|
||||
"telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-context-command,telegram-mention-gating",
|
||||
|
||||
@@ -428,7 +428,7 @@ console.log(JSON.stringify(result));
|
||||
expect(macos).not.toContain("Authorization: Bot");
|
||||
expect(discord).toContain("Authorization: Bot");
|
||||
expect(discord).toContain('"--silent"');
|
||||
expect(discord).toContain("plugins deps --repair");
|
||||
expect(discord).toContain("doctor --fix --yes --non-interactive");
|
||||
expect(discord).toContain("channels status --probe --json");
|
||||
expect(discord).toContain("Stop ${this.input.vmName} after successful Discord smoke");
|
||||
});
|
||||
|
||||
@@ -38,7 +38,6 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
|
||||
"npm-onboard-channel-agent",
|
||||
"doctor-switch",
|
||||
"update-channel-switch",
|
||||
"bundled-channel-deps-compat",
|
||||
"plugins-offline",
|
||||
"plugins",
|
||||
"kitchen-sink-plugin",
|
||||
@@ -128,12 +127,9 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
|
||||
expect(assertionsScript).toContain("assertClawHubExternalInstallContract");
|
||||
expect(assertionsScript).toContain("expectedErrorMessages");
|
||||
expect(assertionsScript).toContain(
|
||||
'const INVALID_PROBE_DIAGNOSTIC_SURFACE_MODES = new Set(["full", "adversarial"]);',
|
||||
'const INVALID_PROBE_DIAGNOSTIC_SURFACE_MODES = new Set(["full", "conformance", "adversarial"]);',
|
||||
);
|
||||
expect(assertionsScript).toContain("!INVALID_PROBE_DIAGNOSTIC_SURFACE_MODES.has(surfaceMode)");
|
||||
expect(assertionsScript).not.toContain(
|
||||
'const INVALID_PROBE_DIAGNOSTIC_SURFACE_MODES = new Set(["full", "conformance"',
|
||||
);
|
||||
expect(readFileSync("scripts/e2e/lib/clawhub-fixture-server.cjs", "utf8")).toContain(
|
||||
'from "openclaw/plugin-sdk/plugin-entry"',
|
||||
);
|
||||
|
||||
@@ -2,16 +2,10 @@ import fs from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createBundledRuntimeDependencyInstallArgs,
|
||||
createBundledRuntimeDependencyInstallEnv,
|
||||
createNestedNpmInstallEnv,
|
||||
} from "../../scripts/lib/bundled-runtime-deps-install.mjs";
|
||||
import {
|
||||
isDirectPostinstallInvocation,
|
||||
pruneOpenClawCompileCache,
|
||||
pruneInstalledPackageDist,
|
||||
discoverBundledPluginRuntimeDeps,
|
||||
pruneBundledPluginSourceNodeModules,
|
||||
runBundledPluginPostinstall,
|
||||
runPluginRegistryPostinstallMigration,
|
||||
@@ -67,77 +61,6 @@ describe("bundled plugin postinstall", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
async function writeDiscordDaveyOptionalDependencyFixture(
|
||||
extensionsDir: string,
|
||||
packageRoot: string,
|
||||
) {
|
||||
await writePluginPackage(extensionsDir, "discord", {
|
||||
dependencies: {
|
||||
"@snazzah/davey": "0.1.11",
|
||||
},
|
||||
});
|
||||
await fs.mkdir(path.join(packageRoot, "node_modules", "@snazzah", "davey"), {
|
||||
recursive: true,
|
||||
});
|
||||
await fs.writeFile(
|
||||
path.join(packageRoot, "node_modules", "@snazzah", "davey", "package.json"),
|
||||
JSON.stringify({
|
||||
optionalDependencies: {
|
||||
"@snazzah/davey-win32-arm64-msvc": "0.1.11",
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
it("clears global npm config before nested installs", () => {
|
||||
expect(
|
||||
createNestedNpmInstallEnv({
|
||||
NPM_CONFIG_WORKSPACES: "true",
|
||||
npm_config_global: "true",
|
||||
npm_config_include_workspace_root: "true",
|
||||
npm_config_ignore_scripts: "false",
|
||||
npm_config_location: "global",
|
||||
npm_config_prefix: "/opt/homebrew",
|
||||
npm_config_workspace: "extensions/telegram",
|
||||
npm_config_workspaces: "true",
|
||||
HOME: "/tmp/home",
|
||||
}),
|
||||
).toEqual({
|
||||
HOME: "/tmp/home",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses package-manager-neutral runtime install args with npm config env", () => {
|
||||
expect(createBundledRuntimeDependencyInstallArgs(["acpx@0.4.1"])).toEqual([
|
||||
"install",
|
||||
"--ignore-scripts",
|
||||
"--workspaces=false",
|
||||
"acpx@0.4.1",
|
||||
]);
|
||||
expect(
|
||||
createBundledRuntimeDependencyInstallEnv({
|
||||
HOME: "/tmp/home",
|
||||
NPM_CONFIG_IGNORE_SCRIPTS: "false",
|
||||
npm_config_dry_run: "true",
|
||||
npm_config_ignore_scripts: "false",
|
||||
npm_config_prefix: "/opt/homebrew",
|
||||
npm_config_workspaces: "true",
|
||||
}),
|
||||
).toEqual({
|
||||
HOME: "/tmp/home",
|
||||
npm_config_dry_run: "false",
|
||||
npm_config_fetch_retries: "5",
|
||||
npm_config_fetch_retry_maxtimeout: "120000",
|
||||
npm_config_fetch_retry_mintimeout: "10000",
|
||||
npm_config_fetch_timeout: "300000",
|
||||
npm_config_ignore_scripts: "true",
|
||||
npm_config_legacy_peer_deps: "true",
|
||||
npm_config_package_lock: "false",
|
||||
npm_config_save: "false",
|
||||
npm_config_workspaces: "false",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not install bundled plugin deps outside of source checkouts by default", async () => {
|
||||
const extensionsDir = await createExtensionsDir();
|
||||
const packageRoot = path.dirname(path.dirname(extensionsDir));
|
||||
@@ -717,81 +640,6 @@ describe("bundled plugin postinstall", () => {
|
||||
expect(spawnSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not reinstall when only another platform optional native child is missing", async () => {
|
||||
const extensionsDir = await createExtensionsDir();
|
||||
const packageRoot = path.dirname(path.dirname(extensionsDir));
|
||||
await writeDiscordDaveyOptionalDependencyFixture(extensionsDir, packageRoot);
|
||||
const spawnSync = vi.fn();
|
||||
|
||||
runBundledPluginPostinstall({
|
||||
env: { HOME: "/tmp/home" },
|
||||
extensionsDir,
|
||||
packageRoot,
|
||||
arch: "arm64",
|
||||
platform: "darwin",
|
||||
spawnSync,
|
||||
log: { log: vi.fn(), warn: vi.fn() },
|
||||
});
|
||||
|
||||
expect(spawnSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("discovers bundled plugin runtime deps from extension manifests", async () => {
|
||||
const extensionsDir = await createExtensionsDir();
|
||||
await writePluginPackage(extensionsDir, "slack", {
|
||||
dependencies: {
|
||||
"@slack/web-api": "7.11.0",
|
||||
},
|
||||
});
|
||||
await writePluginPackage(extensionsDir, "amazon-bedrock", {
|
||||
dependencies: {
|
||||
"@aws-sdk/client-bedrock": "3.1020.0",
|
||||
},
|
||||
});
|
||||
|
||||
expect(discoverBundledPluginRuntimeDeps({ extensionsDir })).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
name: "@slack/web-api",
|
||||
pluginIds: ["slack"],
|
||||
sentinelPath: path.join("node_modules", "@slack", "web-api", "package.json"),
|
||||
version: "7.11.0",
|
||||
},
|
||||
{
|
||||
name: "@aws-sdk/client-bedrock",
|
||||
pluginIds: ["amazon-bedrock"],
|
||||
sentinelPath: path.join("node_modules", "@aws-sdk", "client-bedrock", "package.json"),
|
||||
version: "3.1020.0",
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("merges duplicate bundled runtime deps across plugins", async () => {
|
||||
const extensionsDir = await createExtensionsDir();
|
||||
await writePluginPackage(extensionsDir, "slack", {
|
||||
dependencies: {
|
||||
"https-proxy-agent": "^8.0.0",
|
||||
},
|
||||
});
|
||||
await writePluginPackage(extensionsDir, "feishu", {
|
||||
dependencies: {
|
||||
"https-proxy-agent": "^8.0.0",
|
||||
},
|
||||
});
|
||||
|
||||
expect(discoverBundledPluginRuntimeDeps({ extensionsDir })).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
name: "https-proxy-agent",
|
||||
pluginIds: ["feishu", "slack"],
|
||||
sentinelPath: path.join("node_modules", "https-proxy-agent", "package.json"),
|
||||
version: "^8.0.0",
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("prunes only bundled plugin package node_modules in source checkouts", async () => {
|
||||
const packageRoot = await createTempDirAsync("openclaw-source-prune-");
|
||||
const extensionsDir = path.join(packageRoot, "extensions");
|
||||
|
||||
@@ -63,24 +63,10 @@ describe("collectModuleSpecifiers", () => {
|
||||
});
|
||||
|
||||
describe("classifyRootDependencyOwnership", () => {
|
||||
it("treats root-dist bundled runtime imports as localizable extension deps", () => {
|
||||
expect(
|
||||
classifyRootDependencyOwnership({
|
||||
sections: ["extensions"],
|
||||
rootMirrorImporters: ["discovery-DZDwKJdJ.js"],
|
||||
}),
|
||||
).toEqual({
|
||||
category: "extension_only_localizable",
|
||||
recommendation:
|
||||
"remove from root package.json and rely on owning extension manifests plus doctor --fix",
|
||||
});
|
||||
});
|
||||
|
||||
it("treats scripts and tests as dev-only candidates", () => {
|
||||
expect(
|
||||
classifyRootDependencyOwnership({
|
||||
sections: ["scripts", "test"],
|
||||
rootMirrorImporters: [],
|
||||
}),
|
||||
).toEqual({
|
||||
category: "script_or_test_only",
|
||||
@@ -88,11 +74,11 @@ describe("classifyRootDependencyOwnership", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("treats extension-only deps as localizable when no root mirror exists", () => {
|
||||
it("treats extension-only deps as localizable", () => {
|
||||
expect(
|
||||
classifyRootDependencyOwnership({
|
||||
depName: "vendor-sdk",
|
||||
sections: ["extensions", "test"],
|
||||
rootMirrorImporters: [],
|
||||
}),
|
||||
).toEqual({
|
||||
category: "extension_only_localizable",
|
||||
@@ -101,11 +87,23 @@ describe("classifyRootDependencyOwnership", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("allows explicit root-owned internal extension runtime dependencies", () => {
|
||||
expect(
|
||||
classifyRootDependencyOwnership({
|
||||
depName: "playwright-core",
|
||||
sections: ["extensions", "test"],
|
||||
}),
|
||||
).toEqual({
|
||||
category: "root_owned_extension_runtime",
|
||||
recommendation:
|
||||
"keep at root; the internal browser runtime is shipped with core even though downloadable browser-adjacent plugins also declare it",
|
||||
});
|
||||
});
|
||||
|
||||
it("treats src-owned deps as core runtime", () => {
|
||||
expect(
|
||||
classifyRootDependencyOwnership({
|
||||
sections: ["src"],
|
||||
rootMirrorImporters: [],
|
||||
}),
|
||||
).toEqual({
|
||||
category: "core_runtime",
|
||||
@@ -117,7 +115,6 @@ describe("classifyRootDependencyOwnership", () => {
|
||||
expect(
|
||||
classifyRootDependencyOwnership({
|
||||
sections: [],
|
||||
rootMirrorImporters: [],
|
||||
}),
|
||||
).toEqual({
|
||||
category: "unreferenced",
|
||||
@@ -224,4 +221,34 @@ describe("collectRootDependencyOwnershipCheckErrors", () => {
|
||||
"root dependency '@tencent-connect/qqbot-connector' is extension-owned (remove from root package.json and rely on owning extension manifests plus doctor --fix); extension declarations: qqbot:dependencies; sample imports: extensions/qqbot/src/bridge/setup/finalize.ts",
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not fail explicitly root-owned internal extension runtime dependencies", () => {
|
||||
const repoRoot = makeTempRepo();
|
||||
writeRepoFile(
|
||||
repoRoot,
|
||||
"package.json",
|
||||
JSON.stringify({ dependencies: { "playwright-core": "1.59.1" } }),
|
||||
);
|
||||
writeRepoFile(
|
||||
repoRoot,
|
||||
"extensions/browser/package.json",
|
||||
JSON.stringify({ dependencies: { "playwright-core": "1.59.1" } }),
|
||||
);
|
||||
writeRepoFile(
|
||||
repoRoot,
|
||||
"extensions/browser/src/browser/playwright-core.runtime.ts",
|
||||
'const runtime = require("playwright-core");\n',
|
||||
);
|
||||
|
||||
const records = collectRootDependencyOwnershipAudit({ repoRoot, scanRoots: ["extensions"] });
|
||||
|
||||
expect(records).toMatchObject([
|
||||
{
|
||||
category: "root_owned_extension_runtime",
|
||||
depName: "playwright-core",
|
||||
sections: ["extensions"],
|
||||
},
|
||||
]);
|
||||
expect(collectRootDependencyOwnershipCheckErrors(records)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -106,68 +106,18 @@ describe("resolveTsdownBuildInvocation", () => {
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("cleans tsdown output roots before using tsdown --no-clean without deleting staged runtime deps", async () => {
|
||||
it("cleans tsdown output roots before using tsdown --no-clean", async () => {
|
||||
const rootDir = createTempDir("openclaw-tsdown-clean-");
|
||||
const distFile = path.join(rootDir, "dist", "stale.js");
|
||||
const pluginManifest = path.join(rootDir, "extensions", "telegram", "openclaw.plugin.json");
|
||||
const pluginSourceManifest = path.join(rootDir, "extensions", "telegram", "package.json");
|
||||
const pluginGeneratedFile = path.join(rootDir, "dist", "extensions", "telegram", "index.js");
|
||||
const pluginRuntimeDepFile = path.join(
|
||||
rootDir,
|
||||
"dist",
|
||||
"extensions",
|
||||
"telegram",
|
||||
"node_modules",
|
||||
"grammy",
|
||||
"package.json",
|
||||
);
|
||||
const stalePluginRuntimeDepFile = path.join(
|
||||
rootDir,
|
||||
"dist",
|
||||
"extensions",
|
||||
"old-plugin",
|
||||
"node_modules",
|
||||
"left-pad",
|
||||
"package.json",
|
||||
);
|
||||
const unstagedPluginSourceManifest = path.join(
|
||||
rootDir,
|
||||
"extensions",
|
||||
"unstaged-plugin",
|
||||
"package.json",
|
||||
);
|
||||
const unstagedPluginRuntimeDepFile = path.join(
|
||||
rootDir,
|
||||
"dist",
|
||||
"extensions",
|
||||
"unstaged-plugin",
|
||||
"node_modules",
|
||||
"left-pad",
|
||||
"package.json",
|
||||
);
|
||||
const distRuntimeFile = path.join(rootDir, "dist-runtime", "stale.js");
|
||||
const unrelatedFile = path.join(rootDir, "tmp", "keep.js");
|
||||
await fsPromises.mkdir(path.dirname(distFile), { recursive: true });
|
||||
await fsPromises.mkdir(path.dirname(pluginManifest), { recursive: true });
|
||||
await fsPromises.mkdir(path.dirname(pluginSourceManifest), { recursive: true });
|
||||
await fsPromises.mkdir(path.dirname(pluginGeneratedFile), { recursive: true });
|
||||
await fsPromises.mkdir(path.dirname(pluginRuntimeDepFile), { recursive: true });
|
||||
await fsPromises.mkdir(path.dirname(stalePluginRuntimeDepFile), { recursive: true });
|
||||
await fsPromises.mkdir(path.dirname(unstagedPluginSourceManifest), { recursive: true });
|
||||
await fsPromises.mkdir(path.dirname(unstagedPluginRuntimeDepFile), { recursive: true });
|
||||
await fsPromises.mkdir(path.dirname(distRuntimeFile), { recursive: true });
|
||||
await fsPromises.mkdir(path.dirname(unrelatedFile), { recursive: true });
|
||||
await fsPromises.writeFile(distFile, "stale\n");
|
||||
await fsPromises.writeFile(pluginManifest, '{"id":"telegram"}\n');
|
||||
await fsPromises.writeFile(
|
||||
pluginSourceManifest,
|
||||
'{"openclaw":{"bundle":{"stageRuntimeDependencies":true}}}\n',
|
||||
);
|
||||
await fsPromises.writeFile(pluginGeneratedFile, "generated\n");
|
||||
await fsPromises.writeFile(pluginRuntimeDepFile, "{}\n");
|
||||
await fsPromises.writeFile(stalePluginRuntimeDepFile, "{}\n");
|
||||
await fsPromises.writeFile(unstagedPluginSourceManifest, "{}\n");
|
||||
await fsPromises.writeFile(unstagedPluginRuntimeDepFile, "{}\n");
|
||||
await fsPromises.writeFile(distRuntimeFile, "stale\n");
|
||||
await fsPromises.writeFile(unrelatedFile, "keep\n");
|
||||
|
||||
@@ -175,13 +125,6 @@ describe("resolveTsdownBuildInvocation", () => {
|
||||
|
||||
await expect(fsPromises.stat(distFile)).rejects.toThrow();
|
||||
await expect(fsPromises.stat(pluginGeneratedFile)).rejects.toThrow();
|
||||
await expect(fsPromises.readFile(pluginRuntimeDepFile, "utf8")).resolves.toBe("{}\n");
|
||||
await expect(
|
||||
fsPromises.stat(path.join(rootDir, "dist", "extensions", "old-plugin")),
|
||||
).rejects.toThrow();
|
||||
await expect(
|
||||
fsPromises.stat(path.join(rootDir, "dist", "extensions", "unstaged-plugin")),
|
||||
).rejects.toThrow();
|
||||
await expect(fsPromises.stat(path.join(rootDir, "dist-runtime"))).rejects.toThrow();
|
||||
await expect(fsPromises.readFile(unrelatedFile, "utf8")).resolves.toBe("keep\n");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user