From ddea9a6c01af973f5aa028b3f9e375b00d97a896 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 6 Apr 2026 05:52:10 +0100 Subject: [PATCH] test(plugins): share async temp helpers in marketplace tests --- src/plugins/marketplace.test.ts | 56 +++++++++++-------------- src/plugins/test-helpers/fs-fixtures.ts | 20 +++++++++ src/plugins/uninstall.test.ts | 9 +++- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/src/plugins/marketplace.test.ts b/src/plugins/marketplace.test.ts index cecd8f938bd..245e9439ebc 100644 --- a/src/plugins/marketplace.test.ts +++ b/src/plugins/marketplace.test.ts @@ -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(fn: (dir: string) => Promise): Promise { - 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 { 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"), ); diff --git a/src/plugins/test-helpers/fs-fixtures.ts b/src/plugins/test-helpers/fs-fixtures.ts index 4c65d0662fd..58b0e1ffd43 100644 --- a/src/plugins/test-helpers/fs-fixtures.ts +++ b/src/plugins/test-helpers/fs-fixtures.ts @@ -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 + } + }), + ); +} diff --git a/src/plugins/uninstall.test.ts b/src/plugins/uninstall.test.ts index 733bfbb54db..fcb747b6ec7 100644 --- a/src/plugins/uninstall.test.ts +++ b/src/plugins/uninstall.test.ts @@ -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 () => {