Files
openclaw/src/cli/plugins-deps-command.test.ts
Peter Steinberger 250376f885 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>
2026-05-01 07:49:02 +00:00

267 lines
7.9 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
type RuntimeDepFixture = {
name: string;
version: string;
pluginIds: string[];
};
const mocks = vi.hoisted(() => {
const runtimeLogs: string[] = [];
const stringifyArgs = (args: unknown[]) => args.map((value) => String(value)).join(" ");
return {
runtimeLogs,
defaultRuntime: {
log: vi.fn((...args: unknown[]) => {
runtimeLogs.push(stringifyArgs(args));
}),
error: vi.fn((...args: unknown[]) => {
runtimeLogs.push(stringifyArgs(args));
}),
writeStdout: vi.fn((value: string) => {
runtimeLogs.push(value.endsWith("\n") ? value.slice(0, -1) : value);
}),
writeJson: vi.fn((value: unknown, space = 2) => {
runtimeLogs.push(JSON.stringify(value, null, space > 0 ? space : undefined));
}),
exit: vi.fn((code: number) => {
throw new Error(`__exit__:${code}`);
}),
},
createBundledRuntimeDepsPackagePlan: vi.fn((params: { packageRoot: string }) => {
const plan = mocks.runtimeDepsPlan(params);
const installRootPlan = mocks.resolveBundledRuntimeDependencyPackageInstallRootPlan();
const specs = (deps: readonly RuntimeDepFixture[]) =>
deps.map((dep) => `${dep.name}@${dep.version}`);
return {
packageRoot: params.packageRoot,
installRootPlan,
deps: plan.deps,
missing: plan.missing,
conflicts: plan.conflicts,
installSpecs: specs(plan.deps),
missingSpecs: specs(plan.missing),
};
}),
pruneUnknownBundledRuntimeDepsRoots: vi.fn(),
repairBundledRuntimeDepsPackagePlanAsync: vi.fn(),
resolveBundledRuntimeDependencyPackageInstallRootPlan: vi.fn(),
resolveOpenClawPackageRootSync: vi.fn(),
runtimeDepsPlan: vi.fn(),
};
});
vi.mock("../runtime.js", () => ({
defaultRuntime: mocks.defaultRuntime,
}));
vi.mock("../infra/openclaw-root.js", () => ({
resolveOpenClawPackageRootSync: mocks.resolveOpenClawPackageRootSync,
}));
vi.mock("../plugins/bundled-runtime-deps.js", () => ({
createBundledRuntimeDepsPackagePlan: mocks.createBundledRuntimeDepsPackagePlan,
repairBundledRuntimeDepsPackagePlanAsync: mocks.repairBundledRuntimeDepsPackagePlanAsync,
}));
vi.mock("../plugins/bundled-runtime-deps-roots.js", () => ({
pruneUnknownBundledRuntimeDepsRoots: mocks.pruneUnknownBundledRuntimeDepsRoots,
resolveBundledRuntimeDependencyPackageInstallRootPlan:
mocks.resolveBundledRuntimeDependencyPackageInstallRootPlan,
}));
const { runPluginsDepsCommand } = await import("./plugins-deps-command.js");
describe("plugins deps command", () => {
beforeEach(() => {
mocks.runtimeLogs.length = 0;
mocks.defaultRuntime.log.mockClear();
mocks.defaultRuntime.error.mockClear();
mocks.defaultRuntime.writeStdout.mockClear();
mocks.defaultRuntime.writeJson.mockClear();
mocks.defaultRuntime.exit.mockClear();
mocks.createBundledRuntimeDepsPackagePlan.mockClear();
mocks.pruneUnknownBundledRuntimeDepsRoots.mockReset();
mocks.repairBundledRuntimeDepsPackagePlanAsync.mockReset();
mocks.resolveBundledRuntimeDependencyPackageInstallRootPlan.mockReset();
mocks.resolveOpenClawPackageRootSync.mockReset();
mocks.runtimeDepsPlan.mockReset();
mocks.runtimeDepsPlan.mockReturnValue({
deps: [],
missing: [],
conflicts: [],
});
mocks.resolveBundledRuntimeDependencyPackageInstallRootPlan.mockReturnValue({
installRoot: "/runtime-deps",
searchRoots: ["/runtime-deps"],
external: true,
});
});
it("does not reinstall already materialized bundled runtime deps", async () => {
mocks.runtimeDepsPlan.mockReturnValue({
deps: [{ name: "zod", version: "4.0.0", pluginIds: ["openclaw-demo"] }],
missing: [],
conflicts: [],
});
await runPluginsDepsCommand({
config: {},
options: {
json: true,
packageRoot: "/openclaw-package",
repair: true,
},
});
expect(mocks.repairBundledRuntimeDepsPackagePlanAsync).not.toHaveBeenCalled();
expect(JSON.parse(mocks.runtimeLogs[0] ?? "null")).toEqual(
expect.objectContaining({
packageRoot: "/openclaw-package",
installSpecs: ["zod@4.0.0"],
missingSpecs: [],
repairedSpecs: [],
}),
);
});
it("repairs only when bundled runtime deps are missing", async () => {
const dep = { name: "zod", version: "4.0.0", pluginIds: ["openclaw-demo"] };
mocks.runtimeDepsPlan
.mockReturnValueOnce({
deps: [dep],
missing: [dep],
conflicts: [],
})
.mockReturnValueOnce({
deps: [dep],
missing: [],
conflicts: [],
});
mocks.repairBundledRuntimeDepsPackagePlanAsync.mockResolvedValue({
repairedSpecs: ["zod@4.0.0"],
});
await runPluginsDepsCommand({
config: {},
options: {
json: true,
packageRoot: "/openclaw-package",
repair: true,
},
});
expect(mocks.repairBundledRuntimeDepsPackagePlanAsync).toHaveBeenCalledWith(
expect.objectContaining({
packageRoot: "/openclaw-package",
includeConfiguredChannels: true,
}),
);
expect(JSON.parse(mocks.runtimeLogs[0] ?? "null")).toEqual(
expect.objectContaining({
missing: [],
missingSpecs: [],
repairedSpecs: ["zod@4.0.0"],
warnings: [],
}),
);
});
it("keeps repair warnings inside JSON output", async () => {
const dep = { name: "zod", version: "4.0.0", pluginIds: ["openclaw-demo"] };
mocks.runtimeDepsPlan
.mockReturnValueOnce({
deps: [dep],
missing: [dep],
conflicts: [],
})
.mockReturnValueOnce({
deps: [dep],
missing: [],
conflicts: [],
});
mocks.repairBundledRuntimeDepsPackagePlanAsync.mockImplementation(async (params: unknown) => {
(params as { warn: (message: string) => void }).warn("low disk space");
return {
repairedSpecs: ["zod@4.0.0"],
};
});
await runPluginsDepsCommand({
config: {},
options: {
json: true,
packageRoot: "/openclaw-package",
repair: true,
},
});
expect(mocks.runtimeLogs).toHaveLength(1);
expect(JSON.parse(mocks.runtimeLogs[0] ?? "null")).toEqual(
expect.objectContaining({
missing: [],
repairedSpecs: ["zod@4.0.0"],
warnings: ["low disk space"],
}),
);
});
it("repairs missing deps even when separate deps have version conflicts", async () => {
const dep = { name: "zod", version: "4.0.0", pluginIds: ["openclaw-demo"] };
const conflict = {
name: "shared-conflict",
versions: ["1.0.0", "2.0.0"],
pluginIdsByVersion: new Map([
["1.0.0", ["openclaw-one"]],
["2.0.0", ["openclaw-two"]],
]),
};
mocks.runtimeDepsPlan
.mockReturnValueOnce({
deps: [dep],
missing: [dep],
conflicts: [conflict],
})
.mockReturnValueOnce({
deps: [dep],
missing: [],
conflicts: [conflict],
});
mocks.repairBundledRuntimeDepsPackagePlanAsync.mockResolvedValue({
repairedSpecs: ["zod@4.0.0"],
});
await runPluginsDepsCommand({
config: {},
options: {
json: true,
packageRoot: "/openclaw-package",
repair: true,
},
});
expect(mocks.repairBundledRuntimeDepsPackagePlanAsync).toHaveBeenCalledWith(
expect.objectContaining({
packageRoot: "/openclaw-package",
includeConfiguredChannels: true,
}),
);
expect(JSON.parse(mocks.runtimeLogs[0] ?? "null")).toEqual(
expect.objectContaining({
missing: [],
conflicts: [
{
name: "shared-conflict",
versions: ["1.0.0", "2.0.0"],
pluginIdsByVersion: {
"1.0.0": ["openclaw-one"],
"2.0.0": ["openclaw-two"],
},
},
],
repairedSpecs: ["zod@4.0.0"],
}),
);
});
});