

























@@ -0,0 +1,251 @@
1+import { beforeEach, describe, expect, it, vi } from "vitest";
2+3+type RuntimeDepFixture = {
4+name: string;
5+version: string;
6+pluginIds: string[];
7+};
8+9+const mocks = vi.hoisted(() => {
10+const runtimeLogs: string[] = [];
11+const stringifyArgs = (args: unknown[]) => args.map((value) => String(value)).join(" ");
12+return {
13+ runtimeLogs,
14+defaultRuntime: {
15+log: vi.fn((...args: unknown[]) => {
16+runtimeLogs.push(stringifyArgs(args));
17+}),
18+error: vi.fn((...args: unknown[]) => {
19+runtimeLogs.push(stringifyArgs(args));
20+}),
21+writeStdout: vi.fn((value: string) => {
22+runtimeLogs.push(value.endsWith("\n") ? value.slice(0, -1) : value);
23+}),
24+writeJson: vi.fn((value: unknown, space = 2) => {
25+runtimeLogs.push(JSON.stringify(value, null, space > 0 ? space : undefined));
26+}),
27+exit: vi.fn((code: number) => {
28+throw new Error(`__exit__:${code}`);
29+}),
30+},
31+createBundledRuntimeDepsInstallSpecs: vi.fn((params: { deps: readonly RuntimeDepFixture[] }) =>
32+params.deps.map((dep) => `${dep.name}@${dep.version}`),
33+),
34+pruneUnknownBundledRuntimeDepsRoots: vi.fn(),
35+repairBundledRuntimeDepsInstallRootAsync: vi.fn(),
36+resolveBundledRuntimeDependencyPackageInstallRootPlan: vi.fn(),
37+resolveOpenClawPackageRootSync: vi.fn(),
38+scanBundledPluginRuntimeDeps: vi.fn(),
39+};
40+});
41+42+vi.mock("../runtime.js", () => ({
43+defaultRuntime: mocks.defaultRuntime,
44+}));
45+46+vi.mock("../infra/openclaw-root.js", () => ({
47+resolveOpenClawPackageRootSync: mocks.resolveOpenClawPackageRootSync,
48+}));
49+50+vi.mock("../plugins/bundled-runtime-deps.js", () => ({
51+createBundledRuntimeDepsInstallSpecs: mocks.createBundledRuntimeDepsInstallSpecs,
52+pruneUnknownBundledRuntimeDepsRoots: mocks.pruneUnknownBundledRuntimeDepsRoots,
53+repairBundledRuntimeDepsInstallRootAsync: mocks.repairBundledRuntimeDepsInstallRootAsync,
54+resolveBundledRuntimeDependencyPackageInstallRootPlan:
55+mocks.resolveBundledRuntimeDependencyPackageInstallRootPlan,
56+scanBundledPluginRuntimeDeps: mocks.scanBundledPluginRuntimeDeps,
57+}));
58+59+const { runPluginsDepsCommand } = await import("./plugins-deps-command.js");
60+61+describe("plugins deps command", () => {
62+beforeEach(() => {
63+mocks.runtimeLogs.length = 0;
64+mocks.defaultRuntime.log.mockClear();
65+mocks.defaultRuntime.error.mockClear();
66+mocks.defaultRuntime.writeStdout.mockClear();
67+mocks.defaultRuntime.writeJson.mockClear();
68+mocks.defaultRuntime.exit.mockClear();
69+mocks.createBundledRuntimeDepsInstallSpecs.mockClear();
70+mocks.pruneUnknownBundledRuntimeDepsRoots.mockReset();
71+mocks.repairBundledRuntimeDepsInstallRootAsync.mockReset();
72+mocks.resolveBundledRuntimeDependencyPackageInstallRootPlan.mockReset();
73+mocks.resolveOpenClawPackageRootSync.mockReset();
74+mocks.scanBundledPluginRuntimeDeps.mockReset();
75+mocks.resolveBundledRuntimeDependencyPackageInstallRootPlan.mockReturnValue({
76+installRoot: "/runtime-deps",
77+searchRoots: ["/runtime-deps"],
78+external: true,
79+});
80+});
81+82+it("does not reinstall already materialized bundled runtime deps", async () => {
83+mocks.scanBundledPluginRuntimeDeps.mockReturnValue({
84+deps: [{ name: "zod", version: "4.0.0", pluginIds: ["openclaw-demo"] }],
85+missing: [],
86+conflicts: [],
87+});
88+89+await runPluginsDepsCommand({
90+config: {},
91+options: {
92+json: true,
93+packageRoot: "/openclaw-package",
94+repair: true,
95+},
96+});
97+98+expect(mocks.repairBundledRuntimeDepsInstallRootAsync).not.toHaveBeenCalled();
99+expect(JSON.parse(mocks.runtimeLogs[0] ?? "null")).toEqual(
100+expect.objectContaining({
101+packageRoot: "/openclaw-package",
102+installSpecs: ["zod@4.0.0"],
103+missingSpecs: [],
104+repairedSpecs: [],
105+}),
106+);
107+});
108+109+it("repairs only when bundled runtime deps are missing", async () => {
110+const dep = { name: "zod", version: "4.0.0", pluginIds: ["openclaw-demo"] };
111+mocks.scanBundledPluginRuntimeDeps
112+.mockReturnValueOnce({
113+deps: [dep],
114+missing: [dep],
115+conflicts: [],
116+})
117+.mockReturnValueOnce({
118+deps: [dep],
119+missing: [],
120+conflicts: [],
121+});
122+mocks.repairBundledRuntimeDepsInstallRootAsync.mockResolvedValue({
123+installSpecs: ["zod@4.0.0"],
124+skipped: false,
125+});
126+127+await runPluginsDepsCommand({
128+config: {},
129+options: {
130+json: true,
131+packageRoot: "/openclaw-package",
132+repair: true,
133+},
134+});
135+136+expect(mocks.repairBundledRuntimeDepsInstallRootAsync).toHaveBeenCalledWith(
137+expect.objectContaining({
138+installRoot: "/runtime-deps",
139+installSpecs: ["zod@4.0.0"],
140+missingSpecs: ["zod@4.0.0"],
141+}),
142+);
143+expect(JSON.parse(mocks.runtimeLogs[0] ?? "null")).toEqual(
144+expect.objectContaining({
145+missing: [],
146+missingSpecs: [],
147+repairedSpecs: ["zod@4.0.0"],
148+warnings: [],
149+}),
150+);
151+});
152+153+it("keeps repair warnings inside JSON output", async () => {
154+const dep = { name: "zod", version: "4.0.0", pluginIds: ["openclaw-demo"] };
155+mocks.scanBundledPluginRuntimeDeps
156+.mockReturnValueOnce({
157+deps: [dep],
158+missing: [dep],
159+conflicts: [],
160+})
161+.mockReturnValueOnce({
162+deps: [dep],
163+missing: [],
164+conflicts: [],
165+});
166+mocks.repairBundledRuntimeDepsInstallRootAsync.mockImplementation(async (params: unknown) => {
167+(params as { warn: (message: string) => void }).warn("low disk space");
168+return {
169+installSpecs: ["zod@4.0.0"],
170+skipped: false,
171+};
172+});
173+174+await runPluginsDepsCommand({
175+config: {},
176+options: {
177+json: true,
178+packageRoot: "/openclaw-package",
179+repair: true,
180+},
181+});
182+183+expect(mocks.runtimeLogs).toHaveLength(1);
184+expect(JSON.parse(mocks.runtimeLogs[0] ?? "null")).toEqual(
185+expect.objectContaining({
186+missing: [],
187+repairedSpecs: ["zod@4.0.0"],
188+warnings: ["low disk space"],
189+}),
190+);
191+});
192+193+it("repairs missing deps even when separate deps have version conflicts", async () => {
194+const dep = { name: "zod", version: "4.0.0", pluginIds: ["openclaw-demo"] };
195+const conflict = {
196+name: "shared-conflict",
197+versions: ["1.0.0", "2.0.0"],
198+pluginIdsByVersion: new Map([
199+["1.0.0", ["openclaw-one"]],
200+["2.0.0", ["openclaw-two"]],
201+]),
202+};
203+mocks.scanBundledPluginRuntimeDeps
204+.mockReturnValueOnce({
205+deps: [dep],
206+missing: [dep],
207+conflicts: [conflict],
208+})
209+.mockReturnValueOnce({
210+deps: [dep],
211+missing: [],
212+conflicts: [conflict],
213+});
214+mocks.repairBundledRuntimeDepsInstallRootAsync.mockResolvedValue({
215+installSpecs: ["zod@4.0.0"],
216+skipped: false,
217+});
218+219+await runPluginsDepsCommand({
220+config: {},
221+options: {
222+json: true,
223+packageRoot: "/openclaw-package",
224+repair: true,
225+},
226+});
227+228+expect(mocks.repairBundledRuntimeDepsInstallRootAsync).toHaveBeenCalledWith(
229+expect.objectContaining({
230+installSpecs: ["zod@4.0.0"],
231+missingSpecs: ["zod@4.0.0"],
232+}),
233+);
234+expect(JSON.parse(mocks.runtimeLogs[0] ?? "null")).toEqual(
235+expect.objectContaining({
236+missing: [],
237+conflicts: [
238+{
239+name: "shared-conflict",
240+versions: ["1.0.0", "2.0.0"],
241+pluginIdsByVersion: {
242+"1.0.0": ["openclaw-one"],
243+"2.0.0": ["openclaw-two"],
244+},
245+},
246+],
247+repairedSpecs: ["zod@4.0.0"],
248+}),
249+);
250+});
251+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。