diff --git a/src/plugins/install.test.ts b/src/plugins/install.test.ts index c18f97a3739..cc4f8294ed7 100644 --- a/src/plugins/install.test.ts +++ b/src/plugins/install.test.ts @@ -308,6 +308,35 @@ function expectFailedInstallResult< return params.result; } +function expectWarningIncludes(warnings: readonly string[], fragment: string) { + expect(warnings.some((warning) => warning.includes(fragment))).toBe(true); +} + +function expectWarningExcludes(warnings: readonly string[], fragment: string) { + expect(warnings.some((warning) => warning.includes(fragment))).toBe(false); +} + +function requireRecord(value: unknown, label: string): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`expected ${label} to be an object`); + } + return value as Record; +} + +function requireHookPayload(handler: ReturnType): Record { + const payload = handler.mock.calls[0]?.[0]; + return requireRecord(payload, "before_install hook payload"); +} + +function expectHookRequest( + payload: Record, + expected: { kind: string; mode: string }, +) { + const request = requireRecord(payload.request, "before_install hook request"); + expect(request.kind).toBe(expected.kind); + expect(request.mode).toBe(expected.mode); +} + function mockSuccessfulCommandRun(run: ReturnType>) { run.mockResolvedValue({ code: 0, @@ -635,12 +664,14 @@ describe("installPluginFromArchive", () => { }); expect(result.ok).toBe(true); - expect(vi.mocked(runCommandWithTimeout)).toHaveBeenCalledWith( - expect.arrayContaining(["npm", "install"]), - expect.objectContaining({ - cwd: expect.stringContaining(".openclaw-install-stage-"), - }), - ); + const commandRun = vi.mocked(runCommandWithTimeout).mock.calls[0]; + expect(commandRun?.[0]).toContain("npm"); + expect(commandRun?.[0]).toContain("install"); + const commandOptions = commandRun?.[1]; + if (!commandOptions || typeof commandOptions === "number") { + throw new Error("expected command options object"); + } + expect(commandOptions.cwd).toContain(".openclaw-install-stage-"); }); it("installs scoped archives, rejects duplicate installs, and allows updates", async () => { @@ -1142,9 +1173,7 @@ describe("installPluginFromArchive", () => { expect(result.error).toContain('Plugin "dangerous-plugin" installation blocked'); expect(result.error).toContain("dangerous code patterns detected"); } - expect(warnings).toEqual( - expect.arrayContaining([expect.stringContaining("dangerous code pattern")]), - ); + expectWarningIncludes(warnings, "dangerous code pattern"); }); it("allows package installs when dangerous scanner patterns are only in tests", async () => { @@ -1168,9 +1197,7 @@ describe("installPluginFromArchive", () => { const { result, warnings } = await installFromDirWithWarnings({ pluginDir, extensionsDir }); expect(result.ok).toBe(true); - expect(warnings).not.toEqual( - expect.arrayContaining([expect.stringContaining("dangerous code pattern")]), - ); + expectWarningExcludes(warnings, "dangerous code pattern"); }); it("still scans declared package entrypoints when they live under test-looking paths", async () => { @@ -2072,9 +2099,7 @@ describe("installPluginFromArchive", () => { expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); expect(result.error).toContain('Bundle "dangerous-bundle" installation blocked'); } - expect(warnings).toEqual( - expect.arrayContaining([expect.stringContaining("dangerous code pattern")]), - ); + expectWarningIncludes(warnings, "dangerous code pattern"); }); it("allows bundle installs when dangerous scanner patterns are only in tests", async () => { @@ -2092,9 +2117,7 @@ describe("installPluginFromArchive", () => { const { result, warnings } = await installFromDirWithWarnings({ pluginDir, extensionsDir }); expect(result.ok).toBe(true); - expect(warnings).not.toEqual( - expect.arrayContaining([expect.stringContaining("dangerous code pattern")]), - ); + expectWarningExcludes(warnings, "dangerous code pattern"); }); it("blocks bundle installs when a vendored manifest declares a blocked dependency", async () => { @@ -2372,27 +2395,22 @@ describe("installPluginFromArchive", () => { expect(result.ok).toBe(true); expect(handler).toHaveBeenCalledTimes(1); - expect(handler.mock.calls[0]?.[0]).toMatchObject({ - targetName: "hook-findings-plugin", - targetType: "plugin", - origin: "plugin-package", - sourcePath: pluginDir, - sourcePathKind: "directory", - request: { - kind: "plugin-dir", - mode: "install", - }, - builtinScan: { - status: "ok", - findings: [], - }, - plugin: { - contentType: "package", - pluginId: "hook-findings-plugin", - packageName: "hook-findings-plugin", - version: "1.0.0", - extensions: ["index.js"], - }, + const payload = requireHookPayload(handler); + expect(payload.targetName).toBe("hook-findings-plugin"); + expect(payload.targetType).toBe("plugin"); + expect(payload.origin).toBe("plugin-package"); + expect(payload.sourcePath).toBe(pluginDir); + expect(payload.sourcePathKind).toBe("directory"); + expectHookRequest(payload, { kind: "plugin-dir", mode: "install" }); + const builtinScan = requireRecord(payload.builtinScan, "builtin scan"); + expect(builtinScan.status).toBe("ok"); + expect(builtinScan.findings).toEqual([]); + expect(payload.plugin).toEqual({ + contentType: "package", + pluginId: "hook-findings-plugin", + packageName: "hook-findings-plugin", + version: "1.0.0", + extensions: ["index.js"], }); expect(handler.mock.calls[0]?.[1]).toEqual({ origin: "plugin-package", @@ -2436,33 +2454,23 @@ describe("installPluginFromArchive", () => { expect(result.code).toBeUndefined(); } expect(handler).toHaveBeenCalledTimes(1); - expect(handler.mock.calls[0]?.[0]).toMatchObject({ - targetName: "dangerous-blocked-plugin", - targetType: "plugin", - origin: "plugin-package", - request: { - kind: "plugin-dir", - mode: "install", - }, - builtinScan: { - status: "ok", - findings: [ - expect.objectContaining({ - severity: "critical", - }), - ], - }, - plugin: { - contentType: "package", - pluginId: "dangerous-blocked-plugin", - packageName: "dangerous-blocked-plugin", - version: "1.0.0", - extensions: ["index.js"], - }, + const payload = requireHookPayload(handler); + expect(payload.targetName).toBe("dangerous-blocked-plugin"); + expect(payload.targetType).toBe("plugin"); + expect(payload.origin).toBe("plugin-package"); + expectHookRequest(payload, { kind: "plugin-dir", mode: "install" }); + const builtinScan = requireRecord(payload.builtinScan, "builtin scan"); + expect(builtinScan.status).toBe("ok"); + const findings = builtinScan.findings as Array<{ severity?: string }>; + expect(findings.some((finding) => finding.severity === "critical")).toBe(true); + expect(payload.plugin).toEqual({ + contentType: "package", + pluginId: "dangerous-blocked-plugin", + packageName: "dangerous-blocked-plugin", + version: "1.0.0", + extensions: ["index.js"], }); - expect(warnings).toEqual( - expect.arrayContaining([expect.stringContaining("dangerous code pattern")]), - ); + expectWarningIncludes(warnings, "dangerous code pattern"); expect( warnings.some((w) => w.includes("blocked by plugin hook: Blocked by enterprise policy")), ).toBe(true); @@ -2538,12 +2546,7 @@ describe("installPluginFromArchive", () => { expect(result.ok).toBe(true); expect(handler).toHaveBeenCalledTimes(1); - expect(handler.mock.calls[0]?.[0]).toMatchObject({ - request: { - kind: "plugin-dir", - mode: "install", - }, - }); + expectHookRequest(requireHookPayload(handler), { kind: "plugin-dir", mode: "install" }); }); it("reports update mode to before_install when replacing an existing target", async () => { @@ -2576,12 +2579,7 @@ describe("installPluginFromArchive", () => { expect(result.ok).toBe(true); expect(handler).toHaveBeenCalledTimes(1); - expect(handler.mock.calls[0]?.[0]).toMatchObject({ - request: { - kind: "plugin-dir", - mode: "update", - }, - }); + expectHookRequest(requireHookPayload(handler), { kind: "plugin-dir", mode: "update" }); }); it("scans extension entry files in hidden directories", async () => { @@ -2604,12 +2602,8 @@ describe("installPluginFromArchive", () => { const { result, warnings } = await installFromDirWithWarnings({ pluginDir, extensionsDir }); expect(result.ok).toBe(false); - expect(warnings).toEqual( - expect.arrayContaining([expect.stringContaining("hidden/node_modules path")]), - ); - expect(warnings).toEqual( - expect.arrayContaining([expect.stringContaining("dangerous code pattern")]), - ); + expectWarningIncludes(warnings, "hidden/node_modules path"); + expectWarningIncludes(warnings, "dangerous code pattern"); }); it("blocks install when scanner throws", async () => { @@ -2823,9 +2817,7 @@ describe("installPluginFromDir", () => { }); expectInstalledWithPluginId(res, extensionsDir, "matrix"); - expect(infoMessages).not.toEqual( - expect.arrayContaining([expect.stringContaining("differs from npm package name")]), - ); + expectWarningExcludes(infoMessages, "differs from npm package name"); }); it.each([ @@ -3078,8 +3070,6 @@ describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => { const { result, warnings } = await installFromDirWithWarnings({ pluginDir, extensionsDir }); expect(result.ok).toBe(true); - expect(warnings).toEqual( - expect.arrayContaining([expect.stringContaining("Could not locate openclaw package root")]), - ); + expectWarningIncludes(warnings, "Could not locate openclaw package root"); }); });