test: add install and pairing helper coverage

This commit is contained in:
Peter Steinberger 2026-03-13 19:54:09 +00:00
parent 60d308cff0
commit 1fefd4e67f
4 changed files with 287 additions and 0 deletions

View File

@ -0,0 +1,77 @@
import { describe, expect, it, vi } from "vitest";
const validateRegistryNpmSpecMock = vi.hoisted(() => vi.fn());
const installFromNpmSpecArchiveWithInstallerMock = vi.hoisted(() => vi.fn());
const finalizeNpmSpecArchiveInstallMock = vi.hoisted(() => vi.fn());
vi.mock("./npm-registry-spec.js", () => ({
validateRegistryNpmSpec: (...args: unknown[]) => validateRegistryNpmSpecMock(...args),
}));
vi.mock("./npm-pack-install.js", () => ({
installFromNpmSpecArchiveWithInstaller: (...args: unknown[]) =>
installFromNpmSpecArchiveWithInstallerMock(...args),
finalizeNpmSpecArchiveInstall: (...args: unknown[]) => finalizeNpmSpecArchiveInstallMock(...args),
}));
import { installFromValidatedNpmSpecArchive } from "./install-from-npm-spec.js";
describe("installFromValidatedNpmSpecArchive", () => {
it("trims the spec and returns validation errors before running the installer", async () => {
validateRegistryNpmSpecMock.mockReturnValueOnce("unsupported npm spec");
await expect(
installFromValidatedNpmSpecArchive({
spec: " nope ",
timeoutMs: 30_000,
tempDirPrefix: "openclaw-npm-",
installFromArchive: vi.fn(),
archiveInstallParams: {},
}),
).resolves.toEqual({ ok: false, error: "unsupported npm spec" });
expect(validateRegistryNpmSpecMock).toHaveBeenCalledWith("nope");
expect(installFromNpmSpecArchiveWithInstallerMock).not.toHaveBeenCalled();
expect(finalizeNpmSpecArchiveInstallMock).not.toHaveBeenCalled();
});
it("passes the trimmed spec through the archive installer and finalizer", async () => {
const installFromArchive = vi.fn();
const warn = vi.fn();
const onIntegrityDrift = vi.fn();
const flowResult = {
ok: true,
installResult: { ok: true },
npmResolution: { version: "1.2.3" },
};
const finalized = { ok: true, archivePath: "/tmp/pkg.tgz" };
validateRegistryNpmSpecMock.mockReturnValueOnce(null);
installFromNpmSpecArchiveWithInstallerMock.mockResolvedValueOnce(flowResult);
finalizeNpmSpecArchiveInstallMock.mockReturnValueOnce(finalized);
await expect(
installFromValidatedNpmSpecArchive({
spec: " @openclaw/demo@beta ",
timeoutMs: 45_000,
tempDirPrefix: "openclaw-npm-",
expectedIntegrity: "sha512-demo",
onIntegrityDrift,
warn,
installFromArchive,
archiveInstallParams: { destination: "/tmp/demo" },
}),
).resolves.toBe(finalized);
expect(installFromNpmSpecArchiveWithInstallerMock).toHaveBeenCalledWith({
tempDirPrefix: "openclaw-npm-",
spec: "@openclaw/demo@beta",
timeoutMs: 45_000,
expectedIntegrity: "sha512-demo",
onIntegrityDrift,
warn,
installFromArchive,
archiveInstallParams: { destination: "/tmp/demo" },
});
expect(finalizeNpmSpecArchiveInstallMock).toHaveBeenCalledWith(flowResult);
});
});

View File

@ -0,0 +1,129 @@
import fs from "node:fs/promises";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { withTempDir } from "../test-helpers/temp-dir.js";
const fileExistsMock = vi.hoisted(() => vi.fn());
const resolveSafeInstallDirMock = vi.hoisted(() => vi.fn());
const assertCanonicalPathWithinBaseMock = vi.hoisted(() => vi.fn());
vi.mock("./archive.js", () => ({
fileExists: (...args: unknown[]) => fileExistsMock(...args),
}));
vi.mock("./install-safe-path.js", () => ({
resolveSafeInstallDir: (...args: unknown[]) => resolveSafeInstallDirMock(...args),
assertCanonicalPathWithinBase: (...args: unknown[]) => assertCanonicalPathWithinBaseMock(...args),
}));
import { ensureInstallTargetAvailable, resolveCanonicalInstallTarget } from "./install-target.js";
beforeEach(() => {
fileExistsMock.mockReset();
resolveSafeInstallDirMock.mockReset();
assertCanonicalPathWithinBaseMock.mockReset();
});
describe("resolveCanonicalInstallTarget", () => {
it("creates the base dir and returns early for invalid install ids", async () => {
await withTempDir({ prefix: "openclaw-install-target-" }, async (root) => {
const baseDir = path.join(root, "plugins");
resolveSafeInstallDirMock.mockReturnValueOnce({
ok: false,
error: "bad id",
});
await expect(
resolveCanonicalInstallTarget({
baseDir,
id: "../oops",
invalidNameMessage: "bad id",
boundaryLabel: "plugin dir",
}),
).resolves.toEqual({ ok: false, error: "bad id" });
await expect(fs.stat(baseDir)).resolves.toMatchObject({ isDirectory: expect.any(Function) });
expect(assertCanonicalPathWithinBaseMock).not.toHaveBeenCalled();
});
});
it("returns canonical boundary errors for Error and non-Error throws", async () => {
await withTempDir({ prefix: "openclaw-install-target-" }, async (baseDir) => {
const targetDir = path.join(baseDir, "demo");
resolveSafeInstallDirMock.mockReturnValue({
ok: true,
path: targetDir,
});
assertCanonicalPathWithinBaseMock.mockRejectedValueOnce(new Error("escaped"));
assertCanonicalPathWithinBaseMock.mockRejectedValueOnce("boom");
await expect(
resolveCanonicalInstallTarget({
baseDir,
id: "demo",
invalidNameMessage: "bad id",
boundaryLabel: "plugin dir",
}),
).resolves.toEqual({ ok: false, error: "escaped" });
await expect(
resolveCanonicalInstallTarget({
baseDir,
id: "demo",
invalidNameMessage: "bad id",
boundaryLabel: "plugin dir",
}),
).resolves.toEqual({ ok: false, error: "boom" });
});
});
it("returns the resolved target path on success", async () => {
await withTempDir({ prefix: "openclaw-install-target-" }, async (baseDir) => {
const targetDir = path.join(baseDir, "demo");
resolveSafeInstallDirMock.mockReturnValueOnce({
ok: true,
path: targetDir,
});
await expect(
resolveCanonicalInstallTarget({
baseDir,
id: "demo",
invalidNameMessage: "bad id",
boundaryLabel: "plugin dir",
}),
).resolves.toEqual({ ok: true, targetDir });
});
});
});
describe("ensureInstallTargetAvailable", () => {
it("blocks only install mode when the target already exists", async () => {
fileExistsMock.mockResolvedValueOnce(true);
fileExistsMock.mockResolvedValueOnce(false);
await expect(
ensureInstallTargetAvailable({
mode: "install",
targetDir: "/tmp/demo",
alreadyExistsError: "already there",
}),
).resolves.toEqual({ ok: false, error: "already there" });
await expect(
ensureInstallTargetAvailable({
mode: "update",
targetDir: "/tmp/demo",
alreadyExistsError: "already there",
}),
).resolves.toEqual({ ok: true });
await expect(
ensureInstallTargetAvailable({
mode: "install",
targetDir: "/tmp/demo",
alreadyExistsError: "already there",
}),
).resolves.toEqual({ ok: true });
});
});

View File

@ -0,0 +1,33 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { withTempDir } from "../test-helpers/temp-dir.js";
import { loadJsonFile, saveJsonFile } from "./json-file.js";
describe("json-file helpers", () => {
it("returns undefined for missing and invalid JSON files", async () => {
await withTempDir({ prefix: "openclaw-json-file-" }, async (root) => {
const pathname = path.join(root, "config.json");
expect(loadJsonFile(pathname)).toBeUndefined();
fs.writeFileSync(pathname, "{", "utf8");
expect(loadJsonFile(pathname)).toBeUndefined();
});
});
it("creates parent dirs, writes a trailing newline, and loads the saved object", async () => {
await withTempDir({ prefix: "openclaw-json-file-" }, async (root) => {
const pathname = path.join(root, "nested", "config.json");
saveJsonFile(pathname, { enabled: true, count: 2 });
const raw = fs.readFileSync(pathname, "utf8");
expect(raw.endsWith("\n")).toBe(true);
expect(loadJsonFile(pathname)).toEqual({ enabled: true, count: 2 });
const fileMode = fs.statSync(pathname).mode & 0o777;
const dirMode = fs.statSync(path.dirname(pathname)).mode & 0o777;
expect(fileMode).toBe(0o600);
expect(dirMode).toBe(0o700);
});
});
});

View File

@ -0,0 +1,48 @@
import { describe, expect, it, vi } from "vitest";
import { rejectPendingPairingRequest } from "./pairing-pending.js";
describe("rejectPendingPairingRequest", () => {
it("returns null and skips persistence when the request is missing", async () => {
const persistState = vi.fn();
await expect(
rejectPendingPairingRequest({
requestId: "missing",
idKey: "deviceId",
loadState: async () => ({ pendingById: {} }),
persistState,
getId: (pending: { id: string }) => pending.id,
}),
).resolves.toBeNull();
expect(persistState).not.toHaveBeenCalled();
});
it("removes the request, persists, and returns the dynamic id key", async () => {
const state = {
pendingById: {
keep: { accountId: "keep-me" },
reject: { accountId: "acct-42" },
},
};
const persistState = vi.fn(async () => undefined);
await expect(
rejectPendingPairingRequest({
requestId: "reject",
idKey: "accountId",
loadState: async () => state,
persistState,
getId: (pending) => pending.accountId,
}),
).resolves.toEqual({
requestId: "reject",
accountId: "acct-42",
});
expect(state.pendingById).toEqual({
keep: { accountId: "keep-me" },
});
expect(persistState).toHaveBeenCalledWith(state);
});
});