mirror of
http://192.168.0.88:13333/lywsvip/openclaw-zero-token.git
synced 2026-05-10 17:21:30 +08:00
Major upgrade from e26988a38 to upstream v2026.3.28 (f9b107928).
Key changes:
- Upstream src/, ui/, extensions/ (89 bundled extensions)
- Zero-token web providers preserved in src/zero-token/
- AskOnce plugin restored and registered as CLI command
- Added missing packages: @anthropic-ai/vertex-sdk, @modelcontextprotocol/sdk
- Fixed tsconfig rootDir, skipLibCheck for plugin-sdk DTS build
- Added askonce to bundled plugin metadata and package.json exports
- Fixed AskOnce CLI command registration (missing commands metadata)
- Restored AskOnce adapter imports (correct 5-level relative paths)
- Removed stale migration artifacts from root directory
356 lines
11 KiB
TypeScript
356 lines
11 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { withEnvAsync } from "../test-utils/env.js";
|
|
|
|
const installPluginFromPathMock = vi.fn();
|
|
const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("./install.js", () => ({
|
|
installPluginFromPath: (...args: unknown[]) => installPluginFromPathMock(...args),
|
|
}));
|
|
|
|
vi.mock("../process/exec.js", () => ({
|
|
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
|
|
}));
|
|
|
|
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 writeMarketplaceManifest(rootDir: string, manifest: unknown): Promise<string> {
|
|
const manifestPath = path.join(rootDir, ".claude-plugin", "marketplace.json");
|
|
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
await fs.writeFile(manifestPath, JSON.stringify(manifest));
|
|
return manifestPath;
|
|
}
|
|
|
|
async function writeRemoteMarketplaceFixture(params: {
|
|
repoDir: string;
|
|
manifest: unknown;
|
|
pluginDir?: string;
|
|
}) {
|
|
await fs.mkdir(path.join(params.repoDir, ".claude-plugin"), { recursive: true });
|
|
if (params.pluginDir) {
|
|
await fs.mkdir(path.join(params.repoDir, params.pluginDir), { recursive: true });
|
|
}
|
|
await fs.writeFile(
|
|
path.join(params.repoDir, ".claude-plugin", "marketplace.json"),
|
|
JSON.stringify(params.manifest),
|
|
);
|
|
}
|
|
|
|
async function writeLocalMarketplaceFixture(params: {
|
|
rootDir: string;
|
|
manifest: unknown;
|
|
pluginDir?: string;
|
|
}) {
|
|
if (params.pluginDir) {
|
|
await fs.mkdir(params.pluginDir, { recursive: true });
|
|
}
|
|
return writeMarketplaceManifest(params.rootDir, params.manifest);
|
|
}
|
|
|
|
function mockRemoteMarketplaceClone(params: { manifest: unknown; pluginDir?: string }) {
|
|
runCommandWithTimeoutMock.mockImplementationOnce(async (argv: string[]) => {
|
|
const repoDir = argv.at(-1);
|
|
expect(typeof repoDir).toBe("string");
|
|
await writeRemoteMarketplaceFixture({
|
|
repoDir: repoDir as string,
|
|
manifest: params.manifest,
|
|
...(params.pluginDir ? { pluginDir: params.pluginDir } : {}),
|
|
});
|
|
return { code: 0, stdout: "", stderr: "", killed: false };
|
|
});
|
|
}
|
|
|
|
async function expectRemoteMarketplaceError(params: { manifest: unknown; expectedError: string }) {
|
|
mockRemoteMarketplaceClone({ manifest: params.manifest });
|
|
|
|
const { listMarketplacePlugins } = await import("./marketplace.js");
|
|
const result = await listMarketplacePlugins({ marketplace: "owner/repo" });
|
|
|
|
expect(result).toEqual({
|
|
ok: false,
|
|
error: params.expectedError,
|
|
});
|
|
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
|
|
}
|
|
|
|
function expectRemoteMarketplaceInstallResult(result: unknown) {
|
|
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
|
|
expect(runCommandWithTimeoutMock).toHaveBeenCalledWith(
|
|
["git", "clone", "--depth", "1", "https://github.com/owner/repo.git", expect.any(String)],
|
|
{ timeoutMs: 120_000 },
|
|
);
|
|
expect(installPluginFromPathMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
path: expect.stringMatching(/[\\/]repo[\\/]plugins[\\/]frontend-design$/),
|
|
}),
|
|
);
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
pluginId: "frontend-design",
|
|
marketplacePlugin: "frontend-design",
|
|
marketplaceSource: "owner/repo",
|
|
});
|
|
}
|
|
|
|
function expectMarketplaceManifestListing(
|
|
result: Awaited<ReturnType<typeof import("./marketplace.js").listMarketplacePlugins>>,
|
|
) {
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) {
|
|
throw new Error("expected marketplace listing to succeed");
|
|
}
|
|
expect(result.sourceLabel.replaceAll("\\", "/")).toContain(".claude-plugin/marketplace.json");
|
|
expect(result.manifest).toEqual({
|
|
name: "Example Marketplace",
|
|
version: "1.0.0",
|
|
plugins: [
|
|
{
|
|
name: "frontend-design",
|
|
version: "0.1.0",
|
|
description: "Design system bundle",
|
|
source: { kind: "path", path: "./plugins/frontend-design" },
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
function expectLocalMarketplaceInstallResult(params: {
|
|
result: unknown;
|
|
pluginDir: string;
|
|
marketplaceSource: string;
|
|
}) {
|
|
expect(installPluginFromPathMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
path: params.pluginDir,
|
|
}),
|
|
);
|
|
expect(params.result).toMatchObject({
|
|
ok: true,
|
|
pluginId: "frontend-design",
|
|
marketplacePlugin: "frontend-design",
|
|
marketplaceSource: params.marketplaceSource,
|
|
});
|
|
}
|
|
|
|
describe("marketplace plugins", () => {
|
|
afterEach(() => {
|
|
installPluginFromPathMock.mockReset();
|
|
runCommandWithTimeoutMock.mockReset();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("lists plugins from a local marketplace root", async () => {
|
|
await withTempDir(async (rootDir) => {
|
|
await writeMarketplaceManifest(rootDir, {
|
|
name: "Example Marketplace",
|
|
version: "1.0.0",
|
|
plugins: [
|
|
{
|
|
name: "frontend-design",
|
|
version: "0.1.0",
|
|
description: "Design system bundle",
|
|
source: "./plugins/frontend-design",
|
|
},
|
|
],
|
|
});
|
|
|
|
const { listMarketplacePlugins } = await import("./marketplace.js");
|
|
expectMarketplaceManifestListing(await listMarketplacePlugins({ marketplace: rootDir }));
|
|
});
|
|
});
|
|
|
|
it("resolves relative plugin paths against the marketplace root", async () => {
|
|
await withTempDir(async (rootDir) => {
|
|
const pluginDir = path.join(rootDir, "plugins", "frontend-design");
|
|
const manifestPath = await writeLocalMarketplaceFixture({
|
|
rootDir,
|
|
pluginDir,
|
|
manifest: {
|
|
plugins: [
|
|
{
|
|
name: "frontend-design",
|
|
source: "./plugins/frontend-design",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
installPluginFromPathMock.mockResolvedValue({
|
|
ok: true,
|
|
pluginId: "frontend-design",
|
|
targetDir: "/tmp/frontend-design",
|
|
version: "0.1.0",
|
|
extensions: ["index.ts"],
|
|
});
|
|
|
|
const { installPluginFromMarketplace } = await import("./marketplace.js");
|
|
const result = await installPluginFromMarketplace({
|
|
marketplace: manifestPath,
|
|
plugin: "frontend-design",
|
|
});
|
|
|
|
expectLocalMarketplaceInstallResult({
|
|
result,
|
|
pluginDir,
|
|
marketplaceSource: path.join(rootDir, ".claude-plugin", "marketplace.json"),
|
|
});
|
|
});
|
|
});
|
|
|
|
it("resolves Claude-style plugin@marketplace shortcuts from known_marketplaces.json", async () => {
|
|
await withTempDir(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 });
|
|
await fs.writeFile(
|
|
path.join(homeDir, ".claude", "plugins", "known_marketplaces.json"),
|
|
JSON.stringify({
|
|
"claude-plugins-official": {
|
|
source: {
|
|
source: "github",
|
|
repo: "anthropics/claude-plugins-official",
|
|
},
|
|
installLocation: path.join(homeDir, ".claude", "plugins", "marketplaces", "official"),
|
|
},
|
|
}),
|
|
);
|
|
|
|
const { resolveMarketplaceInstallShortcut } = await import("./marketplace.js");
|
|
const shortcut = await withEnvAsync(
|
|
{ HOME: homeDir, OPENCLAW_HOME: openClawHome },
|
|
async () => await resolveMarketplaceInstallShortcut("superpowers@claude-plugins-official"),
|
|
);
|
|
|
|
expect(shortcut).toEqual({
|
|
ok: true,
|
|
plugin: "superpowers",
|
|
marketplaceName: "claude-plugins-official",
|
|
marketplaceSource: "claude-plugins-official",
|
|
});
|
|
});
|
|
});
|
|
|
|
it("installs remote marketplace plugins from relative paths inside the cloned repo", async () => {
|
|
mockRemoteMarketplaceClone({
|
|
pluginDir: path.join("plugins", "frontend-design"),
|
|
manifest: {
|
|
plugins: [
|
|
{
|
|
name: "frontend-design",
|
|
source: "./plugins/frontend-design",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
installPluginFromPathMock.mockResolvedValue({
|
|
ok: true,
|
|
pluginId: "frontend-design",
|
|
targetDir: "/tmp/frontend-design",
|
|
version: "0.1.0",
|
|
extensions: ["index.ts"],
|
|
});
|
|
|
|
const { installPluginFromMarketplace } = await import("./marketplace.js");
|
|
const result = await installPluginFromMarketplace({
|
|
marketplace: "owner/repo",
|
|
plugin: "frontend-design",
|
|
});
|
|
|
|
expectRemoteMarketplaceInstallResult(result);
|
|
});
|
|
|
|
it("returns a structured error for archive downloads with an empty response body", async () => {
|
|
await withTempDir(async (rootDir) => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => new Response(null, { status: 200 })),
|
|
);
|
|
const manifestPath = await writeMarketplaceManifest(rootDir, {
|
|
plugins: [
|
|
{
|
|
name: "frontend-design",
|
|
source: "https://example.com/frontend-design.tgz",
|
|
},
|
|
],
|
|
});
|
|
|
|
const { installPluginFromMarketplace } = await import("./marketplace.js");
|
|
const result = await installPluginFromMarketplace({
|
|
marketplace: manifestPath,
|
|
plugin: "frontend-design",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
ok: false,
|
|
error: "failed to download https://example.com/frontend-design.tgz: empty response body",
|
|
});
|
|
expect(installPluginFromPathMock).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "rejects remote marketplace git plugin sources before cloning nested remotes",
|
|
manifest: {
|
|
plugins: [
|
|
{
|
|
name: "frontend-design",
|
|
source: {
|
|
type: "git",
|
|
url: "https://evil.example/repo.git",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
expectedError:
|
|
'invalid marketplace entry "frontend-design" in owner/repo: ' +
|
|
"remote marketplaces may not use git plugin sources",
|
|
},
|
|
{
|
|
name: "rejects remote marketplace absolute plugin paths",
|
|
manifest: {
|
|
plugins: [
|
|
{
|
|
name: "frontend-design",
|
|
source: {
|
|
type: "path",
|
|
path: "/tmp/frontend-design",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
expectedError:
|
|
'invalid marketplace entry "frontend-design" in owner/repo: ' +
|
|
"remote marketplaces may only use relative plugin paths",
|
|
},
|
|
{
|
|
name: "rejects remote marketplace HTTP plugin paths",
|
|
manifest: {
|
|
plugins: [
|
|
{
|
|
name: "frontend-design",
|
|
source: {
|
|
type: "path",
|
|
path: "https://evil.example/plugin.tgz",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
expectedError:
|
|
'invalid marketplace entry "frontend-design" in owner/repo: ' +
|
|
"remote marketplaces may not use HTTP(S) plugin paths",
|
|
},
|
|
] as const)("$name", async ({ manifest, expectedError }) => {
|
|
await expectRemoteMarketplaceError({ manifest, expectedError });
|
|
});
|
|
});
|