test: add clipboard and package helper coverage

This commit is contained in:
Peter Steinberger 2026-03-13 19:49:31 +00:00
parent ba9fb4d994
commit c84c76ee66
3 changed files with 113 additions and 0 deletions

View File

@ -0,0 +1,52 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn());
vi.mock("../process/exec.js", () => ({
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
}));
const { copyToClipboard } = await import("./clipboard.js");
describe("copyToClipboard", () => {
beforeEach(() => {
runCommandWithTimeoutMock.mockReset();
});
it("returns true on the first successful clipboard command", async () => {
runCommandWithTimeoutMock.mockResolvedValueOnce({ code: 0, killed: false });
await expect(copyToClipboard("hello")).resolves.toBe(true);
expect(runCommandWithTimeoutMock).toHaveBeenCalledWith(["pbcopy"], {
timeoutMs: 3000,
input: "hello",
});
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
});
it("falls through failed attempts until a later command succeeds", async () => {
runCommandWithTimeoutMock
.mockRejectedValueOnce(new Error("missing pbcopy"))
.mockResolvedValueOnce({ code: 1, killed: false })
.mockResolvedValueOnce({ code: 0, killed: false });
await expect(copyToClipboard("hello")).resolves.toBe(true);
expect(runCommandWithTimeoutMock.mock.calls.map((call) => call[0])).toEqual([
["pbcopy"],
["xclip", "-selection", "clipboard"],
["wl-copy"],
]);
});
it("returns false when every clipboard backend fails or is killed", async () => {
runCommandWithTimeoutMock
.mockResolvedValueOnce({ code: 0, killed: true })
.mockRejectedValueOnce(new Error("missing xclip"))
.mockResolvedValueOnce({ code: 1, killed: false })
.mockRejectedValueOnce(new Error("missing clip.exe"))
.mockResolvedValueOnce({ code: 2, killed: false });
await expect(copyToClipboard("hello")).resolves.toBe(false);
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(5);
});
});

View File

@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { normalizePackageTagInput } from "./package-tag.js";
describe("normalizePackageTagInput", () => {
const packageNames = ["openclaw", "@openclaw/plugin"] as const;
it("returns null for blank inputs", () => {
expect(normalizePackageTagInput(undefined, packageNames)).toBeNull();
expect(normalizePackageTagInput(" ", packageNames)).toBeNull();
});
it("strips known package-name prefixes before returning the tag", () => {
expect(normalizePackageTagInput("openclaw@beta", packageNames)).toBe("beta");
expect(normalizePackageTagInput("@openclaw/plugin@2026.2.24", packageNames)).toBe("2026.2.24");
});
it("returns trimmed raw values when no package prefix matches", () => {
expect(normalizePackageTagInput(" latest ", packageNames)).toBe("latest");
expect(normalizePackageTagInput("@other/plugin@beta", packageNames)).toBe("@other/plugin@beta");
});
});

View File

@ -0,0 +1,40 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { resolveStableNodePath } from "./stable-node-path.js";
describe("resolveStableNodePath", () => {
it("returns non-cellar paths unchanged", async () => {
await expect(resolveStableNodePath("/usr/local/bin/node")).resolves.toBe("/usr/local/bin/node");
});
it("prefers the Homebrew opt symlink for default and versioned formulas", async () => {
const prefix = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-stable-node-"));
const defaultNode = path.join(prefix, "Cellar", "node", "25.7.0", "bin", "node");
const versionedNode = path.join(prefix, "Cellar", "node@22", "22.17.0", "bin", "node");
const optDefault = path.join(prefix, "opt", "node", "bin", "node");
const optVersioned = path.join(prefix, "opt", "node@22", "bin", "node");
await fs.mkdir(path.dirname(optDefault), { recursive: true });
await fs.mkdir(path.dirname(optVersioned), { recursive: true });
await fs.writeFile(optDefault, "", "utf8");
await fs.writeFile(optVersioned, "", "utf8");
await expect(resolveStableNodePath(defaultNode)).resolves.toBe(optDefault);
await expect(resolveStableNodePath(versionedNode)).resolves.toBe(optVersioned);
});
it("falls back to the bin symlink for the default formula, otherwise original path", async () => {
const prefix = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-stable-node-"));
const defaultNode = path.join(prefix, "Cellar", "node", "25.7.0", "bin", "node");
const versionedNode = path.join(prefix, "Cellar", "node@22", "22.17.0", "bin", "node");
const binNode = path.join(prefix, "bin", "node");
await fs.mkdir(path.dirname(binNode), { recursive: true });
await fs.writeFile(binNode, "", "utf8");
await expect(resolveStableNodePath(defaultNode)).resolves.toBe(binNode);
await expect(resolveStableNodePath(versionedNode)).resolves.toBe(versionedNode);
});
});