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:
Peter Steinberger
2026-05-01 21:32:22 +01:00
committed by GitHub
parent 2e8e9cd6ca
commit ed8f50f240
294 changed files with 2562 additions and 25454 deletions

View File

@@ -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-"));

View File

@@ -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;

View File

@@ -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 });
}
});
});

View File

@@ -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[] = [];

View File

@@ -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);
});
});

View File

@@ -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(

View File

@@ -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", () => {

View File

@@ -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 });

View File

@@ -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 });
}

View File

@@ -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",

View File

@@ -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");
});

View File

@@ -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"',
);

View File

@@ -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");

View File

@@ -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

View File

@@ -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");
});