test(plugins): share async temp helpers in marketplace tests

This commit is contained in:
Vincent Koc
2026-04-06 05:52:10 +01:00
parent f3f42e6bbf
commit ddea9a6c01
3 changed files with 52 additions and 33 deletions

View File

@@ -3,6 +3,11 @@ import os from "node:os";
import path from "node:path";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { withEnvAsync } from "../test-utils/env.js";
import { withTempDir } from "../test-utils/temp-dir.js";
import {
cleanupTrackedTempDirsAsync,
makeTrackedTempDirAsync,
} from "./test-helpers/fs-fixtures.js";
const installPluginFromPathMock = vi.fn();
const fetchWithSsrFGuardMock = vi.hoisted(() =>
@@ -47,15 +52,6 @@ beforeAll(async () => {
await import("./marketplace.js"));
});
async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-marketplace-test-"));
try {
return await fn(dir);
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
}
async function listMarketplaceDownloadTempDirs(): Promise<string[]> {
const entries = await fs.readdir(os.tmpdir(), { withFileTypes: true });
return entries
@@ -134,8 +130,10 @@ function mockRemoteMarketplaceCloneWithOutsideSymlink(params: {
repoDir: repoDir as string,
manifest: params.manifest,
});
const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-marketplace-outside-"));
tempOutsideDirs.push(outsideDir);
const outsideDir = await makeTrackedTempDirAsync(
"openclaw-marketplace-outside",
tempOutsideDirs,
);
await fs.mkdir(path.dirname(path.join(repoDir as string, params.symlinkPath)), {
recursive: true,
});
@@ -221,15 +219,11 @@ describe("marketplace plugins", () => {
installPluginFromPathMock.mockReset();
runCommandWithTimeoutMock.mockReset();
vi.unstubAllGlobals();
await Promise.all(
tempOutsideDirs.splice(0, tempOutsideDirs.length).map(async (dir) => {
await fs.rm(dir, { recursive: true, force: true });
}),
);
await cleanupTrackedTempDirsAsync(tempOutsideDirs);
});
it("lists plugins from a local marketplace root", async () => {
await withTempDir(async (rootDir) => {
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
await writeMarketplaceManifest(rootDir, {
name: "Example Marketplace",
version: "1.0.0",
@@ -248,7 +242,7 @@ describe("marketplace plugins", () => {
});
it("resolves relative plugin paths against the marketplace root", async () => {
await withTempDir(async (rootDir) => {
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
const pluginDir = path.join(rootDir, "plugins", "frontend-design");
const manifestPath = await writeLocalMarketplaceFixture({
rootDir,
@@ -284,7 +278,7 @@ describe("marketplace plugins", () => {
});
it("preserves the logical local install path instead of canonicalizing it", async () => {
await withTempDir(async (rootDir) => {
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
const canonicalRootDir = await fs.realpath(rootDir);
const pluginDir = path.join(rootDir, "plugins", "frontend-design");
const canonicalPluginDir = path.join(canonicalRootDir, "plugins", "frontend-design");
@@ -329,7 +323,7 @@ describe("marketplace plugins", () => {
});
it("passes dangerous force unsafe install through to marketplace path installs", async () => {
await withTempDir(async (rootDir) => {
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
const pluginDir = path.join(rootDir, "plugins", "frontend-design");
const manifestPath = await writeLocalMarketplaceFixture({
rootDir,
@@ -367,7 +361,7 @@ describe("marketplace plugins", () => {
});
it("resolves Claude-style plugin@marketplace shortcuts from known_marketplaces.json", async () => {
await withTempDir(async (homeDir) => {
await withTempDir("openclaw-marketplace-test-", async (homeDir) => {
const openClawHome = path.join(homeDir, "openclaw-home");
await fs.mkdir(path.join(homeDir, ".claude", "plugins"), { recursive: true });
await fs.mkdir(openClawHome, { recursive: true });
@@ -532,7 +526,7 @@ describe("marketplace plugins", () => {
);
it("returns a structured error for archive downloads with an empty response body", async () => {
await withTempDir(async (rootDir) => {
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
const release = vi.fn(async () => undefined);
fetchWithSsrFGuardMock.mockResolvedValueOnce({
response: new Response(null, { status: 200 }),
@@ -570,7 +564,7 @@ describe("marketplace plugins", () => {
});
it("returns a structured error for invalid archive URLs", async () => {
await withTempDir(async (rootDir) => {
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
const manifestPath = await writeMarketplaceManifest(rootDir, {
plugins: [
{
@@ -595,7 +589,7 @@ describe("marketplace plugins", () => {
});
it("rejects Windows drive-relative archive filenames from redirects", async () => {
await withTempDir(async (rootDir) => {
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
fetchWithSsrFGuardMock.mockResolvedValueOnce({
response: new Response(new Blob([Buffer.from("tgz-bytes")]), {
status: 200,
@@ -627,7 +621,7 @@ describe("marketplace plugins", () => {
});
it("falls back to the default archive timeout when the caller passes NaN", async () => {
await withTempDir(async (rootDir) => {
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
fetchWithSsrFGuardMock.mockResolvedValueOnce({
response: new Response(new Blob([Buffer.from("tgz-bytes")]), {
status: 200,
@@ -672,7 +666,7 @@ describe("marketplace plugins", () => {
});
it("downloads archive plugin sources through the SSRF guard", async () => {
await withTempDir(async (rootDir) => {
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
const release = vi.fn(async () => {
throw new Error("dispatcher close failed");
});
@@ -727,7 +721,7 @@ describe("marketplace plugins", () => {
});
it("rejects non-streaming archive responses before buffering them", async () => {
await withTempDir(async (rootDir) => {
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
const arrayBuffer = vi.fn(async () => new Uint8Array([1, 2, 3]).buffer);
fetchWithSsrFGuardMock.mockResolvedValueOnce({
response: {
@@ -766,7 +760,7 @@ describe("marketplace plugins", () => {
});
it("rejects oversized streamed archive responses without falling back to arrayBuffer", async () => {
await withTempDir(async (rootDir) => {
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
const arrayBuffer = vi.fn(async () => new Uint8Array([1, 2, 3]).buffer);
const reader = {
read: vi
@@ -820,7 +814,7 @@ describe("marketplace plugins", () => {
});
it("cleans up a partial download temp dir when streaming the archive fails", async () => {
await withTempDir(async (rootDir) => {
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
const beforeTempDirs = await listMarketplaceDownloadTempDirs();
fetchWithSsrFGuardMock.mockResolvedValueOnce({
response: new Response("x".repeat(1024), {
@@ -858,7 +852,7 @@ describe("marketplace plugins", () => {
});
it("sanitizes archive download errors before returning them", async () => {
await withTempDir(async (rootDir) => {
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
fetchWithSsrFGuardMock.mockRejectedValueOnce(
new Error(
"blocked\n\u001b[31mAuthorization: Bearer sk-1234567890abcdefghijklmnop\u001b[0m",
@@ -901,7 +895,7 @@ describe("marketplace plugins", () => {
});
it("returns a structured error when the SSRF guard rejects an archive URL", async () => {
await withTempDir(async (rootDir) => {
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
fetchWithSsrFGuardMock.mockRejectedValueOnce(
new Error("Blocked hostname (not in allowlist): 169.254.169.254"),
);

View File

@@ -1,4 +1,5 @@
import fs from "node:fs";
import fsPromises from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -21,6 +22,13 @@ export function makeTrackedTempDir(prefix: string, trackedDirs: string[]) {
return dir;
}
export async function makeTrackedTempDirAsync(prefix: string, trackedDirs: string[]) {
const dir = await fsPromises.mkdtemp(path.join(os.tmpdir(), String(prefix) + "-"));
chmodSafeDir(dir);
trackedDirs.push(dir);
return dir;
}
export function cleanupTrackedTempDirs(trackedDirs: string[]) {
for (const dir of trackedDirs.splice(0)) {
try {
@@ -30,3 +38,15 @@ export function cleanupTrackedTempDirs(trackedDirs: string[]) {
}
}
}
export async function cleanupTrackedTempDirsAsync(trackedDirs: string[]) {
await Promise.all(
trackedDirs.splice(0).map(async (dir) => {
try {
await fsPromises.rm(dir, { recursive: true, force: true });
} catch {
// ignore cleanup failures
}
}),
);
}

View File

@@ -4,6 +4,10 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { resolvePluginInstallDir } from "./install.js";
import {
cleanupTrackedTempDirsAsync,
makeTrackedTempDirAsync,
} from "./test-helpers/fs-fixtures.js";
import {
removePluginFromConfig,
resolveUninstallChannelConfigKeys,
@@ -597,13 +601,14 @@ describe("removePluginFromConfig", () => {
describe("uninstallPlugin", () => {
let tempDir: string;
const tempDirs: string[] = [];
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "uninstall-test-"));
tempDir = await makeTrackedTempDirAsync("uninstall-test", tempDirs);
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
await cleanupTrackedTempDirsAsync(tempDirs);
});
it("returns error when plugin not found", async () => {