Files
openclaw/src/hooks/update.test.ts
Phil 00ca654c74 fix(plugins): persist resolved npm install specs
Preserve npm install selectors while recording resolved npm provenance for plugin and hook install/update records. Active `record.spec` stays the requested selector unless explicitly pinned, while resolved npm fields remain available for audit and diagnostics.

Adds focused coverage for hook-pack npm fallback provenance after the maintainer review found that path worth pinning down.

Co-authored-by: Phil <99397913+GitHoubi@users.noreply.github.com>
2026-05-29 09:42:46 +01:00

141 lines
4.2 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { HookNpmIntegrityDriftParams } from "./install.js";
const installHooksFromNpmSpecMock = vi.fn();
vi.mock("./install.js", () => ({
installHooksFromNpmSpec: (...args: unknown[]) => installHooksFromNpmSpecMock(...args),
resolveHookInstallDir: (hookId: string) => `/tmp/hooks/${hookId}`,
}));
const { updateNpmInstalledHookPacks } = await import("./update.js");
function createHookInstallConfig(params: {
hookId: string;
spec: string;
integrity?: string;
}): OpenClawConfig {
return {
hooks: {
internal: {
installs: {
[params.hookId]: {
source: "npm",
spec: params.spec,
installPath: `/tmp/hooks/${params.hookId}`,
...(params.integrity ? { integrity: params.integrity } : {}),
},
},
},
},
} as OpenClawConfig;
}
describe("updateNpmInstalledHookPacks", () => {
beforeEach(() => {
installHooksFromNpmSpecMock.mockReset();
});
it("aborts exact pinned hook pack updates on integrity drift by default", async () => {
const warn = vi.fn();
installHooksFromNpmSpecMock.mockImplementation(
async (params: {
spec: string;
onIntegrityDrift?: (drift: HookNpmIntegrityDriftParams) => boolean | Promise<boolean>;
}) => {
const proceed = await params.onIntegrityDrift?.({
spec: params.spec,
expectedIntegrity: "sha512-old",
actualIntegrity: "sha512-new",
resolution: {
integrity: "sha512-new",
resolvedSpec: "@openclaw/demo-hooks@1.0.0",
version: "1.0.0",
},
});
if (proceed === false) {
return {
ok: false,
error: "aborted: npm package integrity drift detected for @openclaw/demo-hooks@1.0.0",
};
}
return {
ok: true,
hookPackId: "demo-hooks",
hooks: ["demo"],
targetDir: "/tmp/hooks/demo-hooks",
version: "1.0.0",
};
},
);
const config = createHookInstallConfig({
hookId: "demo-hooks",
spec: "@openclaw/demo-hooks@1.0.0",
integrity: "sha512-old",
});
const result = await updateNpmInstalledHookPacks({
config,
hookIds: ["demo-hooks"],
logger: { warn },
});
expect(warn).toHaveBeenCalledWith(
'Integrity drift for hook pack "demo-hooks" (@openclaw/demo-hooks@1.0.0): expected sha512-old, got sha512-new',
);
expect(result.changed).toBe(false);
expect(result.config).toBe(config);
expect(result.outcomes).toEqual([
{
hookId: "demo-hooks",
status: "error",
message:
'Failed to update hook pack "demo-hooks": aborted: npm package integrity drift detected for @openclaw/demo-hooks@1.0.0',
},
]);
});
it("preserves hook pack update selector and records npm resolution metadata after update", async () => {
installHooksFromNpmSpecMock.mockResolvedValue({
ok: true,
hookPackId: "demo-hooks",
hooks: ["demo"],
targetDir: "/tmp/hooks/demo-hooks",
version: "1.2.3",
npmResolution: {
name: "@openclaw/demo-hooks",
version: "1.2.3",
resolvedSpec: "@openclaw/demo-hooks@1.2.3",
integrity: "sha512-new",
shasum: "abc123",
resolvedAt: "2026-05-11T20:00:00.000Z",
},
});
const result = await updateNpmInstalledHookPacks({
config: createHookInstallConfig({
hookId: "demo-hooks",
spec: "@openclaw/demo-hooks",
}),
hookIds: ["demo-hooks"],
});
expect(result.changed).toBe(true);
expect(result.config.hooks?.internal?.installs?.["demo-hooks"]).toEqual({
source: "npm",
spec: "@openclaw/demo-hooks",
installPath: "/tmp/hooks/demo-hooks",
version: "1.2.3",
resolvedName: "@openclaw/demo-hooks",
resolvedVersion: "1.2.3",
resolvedSpec: "@openclaw/demo-hooks@1.2.3",
integrity: "sha512-new",
shasum: "abc123",
resolvedAt: "2026-05-11T20:00:00.000Z",
hooks: ["demo"],
installedAt: expect.any(String),
});
});
});