mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
test(plugins): share async temp helpers in marketplace tests
This commit is contained in:
@@ -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"),
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user