fix: simplify bundled runtime dependency repair (#75183)

Summary:
- Merged fix: simplify bundled runtime dependency repair after ClawSweeper review.

ClawSweeper fixups:
- Included follow-up commit: fix: verify cached bundled runtime roots
- Included follow-up commit: refactor: simplify plugin runtime startup paths
- Included follow-up commit: refactor: trim plugin startup policy helpers
- Included follow-up commit: refactor: trust package manager runtime deps materialization
- Included follow-up commit: fix: narrow channel runtime deps skip policy
- Included follow-up commit: refactor: defer startup plugin runtime deps
- Ran the ClawSweeper repair loop before final review.

Validation:
- ClawSweeper review passed for head 04dc566534.
- Required merge gates passed before the squash merge.

Prepared head SHA: 04dc566534
Review: https://github.com/openclaw/openclaw/pull/75183#issuecomment-4358383786

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Shakker <shakkerdroid@gmail.com>
Co-authored-by: clawsweeper-repair <clawsweeper-repair@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-05-01 08:49:02 +01:00
committed by GitHub
parent 8ce44b057f
commit 250376f885
120 changed files with 4642 additions and 1758 deletions

View File

@@ -6,6 +6,8 @@ import {
createBundledRuntimeDependencyInstallArgs,
createBundledRuntimeDependencyInstallEnv,
createNestedNpmInstallEnv,
} from "../../scripts/lib/bundled-runtime-deps-install.mjs";
import {
isDirectPostinstallInvocation,
pruneOpenClawCompileCache,
pruneInstalledPackageDist,
@@ -51,41 +53,6 @@ async function writePluginPackage(
}
describe("bundled plugin postinstall", () => {
function createNpmInstallArgs(...packages: string[]) {
return createBundledRuntimeDependencyInstallArgs(packages);
}
function createBareNpmRunner(packages: string[]) {
return {
command: "npm",
args: createNpmInstallArgs(...packages),
env: {
HOME: "/tmp/home",
PATH: "/tmp/node/bin",
},
shell: false as const,
};
}
function expectNpmInstallSpawn(
spawnSync: ReturnType<typeof vi.fn>,
packageRoot: string,
packages: string[],
) {
expect(spawnSync).toHaveBeenCalledWith("npm", createNpmInstallArgs(...packages), {
cwd: packageRoot,
encoding: "utf8",
env: {
HOME: "/tmp/home",
PATH: "/tmp/node/bin",
},
shell: false,
stdio: "pipe",
windowsHide: true,
windowsVerbatimArguments: undefined,
});
}
it("recognizes direct invocation through symlinked temp prefixes", () => {
const realpathSync = vi.fn((value: string) =>
value.replace(/^\/var\/folders\//u, "/private/var/folders/"),
@@ -125,9 +92,14 @@ describe("bundled plugin postinstall", () => {
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({
@@ -139,13 +111,17 @@ describe("bundled plugin postinstall", () => {
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",
@@ -154,9 +130,11 @@ describe("bundled plugin postinstall", () => {
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",
});
});
@@ -174,7 +152,6 @@ describe("bundled plugin postinstall", () => {
env: { HOME: "/tmp/home" },
extensionsDir,
packageRoot,
npmRunner: createBareNpmRunner(["acpx@0.4.1"]),
spawnSync,
log: { log: vi.fn(), warn: vi.fn() },
});
@@ -714,34 +691,6 @@ describe("bundled plugin postinstall", () => {
expect(unlinkSync).toHaveBeenCalledWith("/pkg/dist/stale.js");
});
it("runs nested local installs with sanitized env when the sentinel package is missing", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));
await writePluginPackage(extensionsDir, "acpx", {
dependencies: {
acpx: "0.4.1",
},
});
const spawnSync = vi.fn(() => ({ status: 0, stderr: "", stdout: "" }));
runBundledPluginPostinstall({
env: {
OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1",
npm_config_global: "true",
npm_config_location: "global",
npm_config_prefix: "/opt/homebrew",
HOME: "/tmp/home",
},
extensionsDir,
packageRoot,
npmRunner: createBareNpmRunner(["acpx@0.4.1"]),
spawnSync,
log: { log: vi.fn(), warn: vi.fn() },
});
expectNpmInstallSpawn(spawnSync, packageRoot, ["acpx@0.4.1"]);
});
it("skips reinstall when the bundled sentinel package already exists", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));
@@ -768,26 +717,6 @@ describe("bundled plugin postinstall", () => {
expect(spawnSync).not.toHaveBeenCalled();
});
it("reinstalls bundled runtime deps when optional native children are missing", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));
await writeDiscordDaveyOptionalDependencyFixture(extensionsDir, packageRoot);
const spawnSync = vi.fn(() => ({ status: 0, stderr: "", stdout: "" }));
runBundledPluginPostinstall({
env: { HOME: "/tmp/home", OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1" },
extensionsDir,
packageRoot,
arch: "arm64",
npmRunner: createBareNpmRunner(["@snazzah/davey@0.1.11"]),
platform: "win32",
spawnSync,
log: { log: vi.fn(), warn: vi.fn() },
});
expectNpmInstallSpawn(spawnSync, packageRoot, ["@snazzah/davey@0.1.11"]);
});
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));
@@ -863,103 +792,6 @@ describe("bundled plugin postinstall", () => {
);
});
it("installs missing bundled plugin runtime deps during global installs", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));
await writePluginPackage(extensionsDir, "slack", {
dependencies: {
"@slack/web-api": "7.11.0",
},
});
await writePluginPackage(extensionsDir, "telegram", {
dependencies: {
grammy: "1.38.4",
},
});
const spawnSync = vi.fn(() => ({ status: 0, stderr: "", stdout: "" }));
runBundledPluginPostinstall({
env: {
OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1",
npm_config_global: "true",
npm_config_location: "global",
npm_config_prefix: "/opt/homebrew",
HOME: "/tmp/home",
},
extensionsDir,
packageRoot,
npmRunner: createBareNpmRunner(["@slack/web-api@7.11.0", "grammy@1.38.4"]),
spawnSync,
log: { log: vi.fn(), warn: vi.fn() },
});
expectNpmInstallSpawn(spawnSync, packageRoot, ["@slack/web-api@7.11.0", "grammy@1.38.4"]);
});
it("installs only missing bundled plugin runtime deps", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));
await writePluginPackage(extensionsDir, "slack", {
dependencies: {
"@slack/web-api": "7.11.0",
},
});
await writePluginPackage(extensionsDir, "telegram", {
dependencies: {
grammy: "1.38.4",
},
});
await fs.mkdir(path.join(packageRoot, "node_modules", "@slack", "web-api"), {
recursive: true,
});
await fs.writeFile(
path.join(packageRoot, "node_modules", "@slack", "web-api", "package.json"),
"{}\n",
);
const spawnSync = vi.fn(() => ({ status: 0, stderr: "", stdout: "" }));
runBundledPluginPostinstall({
env: {
OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1",
HOME: "/tmp/home",
},
extensionsDir,
packageRoot,
npmRunner: createBareNpmRunner(["grammy@1.38.4"]),
spawnSync,
log: { log: vi.fn(), warn: vi.fn() },
});
expectNpmInstallSpawn(spawnSync, packageRoot, ["grammy@1.38.4"]);
});
it("installs bundled plugin deps when npm location is global", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));
await writePluginPackage(extensionsDir, "telegram", {
dependencies: {
grammy: "1.38.4",
},
});
const spawnSync = vi.fn(() => ({ status: 0, stderr: "", stdout: "" }));
runBundledPluginPostinstall({
env: {
OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1",
npm_config_location: "global",
npm_config_prefix: "/opt/homebrew",
HOME: "/tmp/home",
},
extensionsDir,
packageRoot,
npmRunner: createBareNpmRunner(["grammy@1.38.4"]),
spawnSync,
log: { log: vi.fn(), warn: vi.fn() },
});
expectNpmInstallSpawn(spawnSync, packageRoot, ["grammy@1.38.4"]);
});
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");