mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
c7e7ac2728
* docs(secrets): remove retired web credential paths * refactor(web): remove retired provider compatibility paths * refactor(providers): delete retired compatibility routes * refactor(secrets): remove retired credential aliases * refactor(plugin-sdk): delete retired compatibility surfaces * docs(plugin-sdk): remove retired migration guidance * chore(plugin-sdk): refresh rebased surface budgets * chore(plugin-sdk): refresh API removal baseline * refactor(compat): migrate retired internal callers * chore(plugin-sdk): refresh current-main baselines * test(config): migrate plugin-owned secret assertions * test(gateway): narrow plugin secret refs * fix(plugin-sdk): preserve private boundary type identity * chore(compat): remove stale sweep references * chore(lint): lower max-lines budget * refactor(secrets): remove unused web helper * build(plugin-sdk): drop removed compat entries * chore(plugin-sdk): refresh rebased API baseline * chore(plugin-sdk): use Linux API baseline hash * fix(plugin-sdk): preserve private bundled build entries * fix(plugin-sdk): package private runtime facades * fix(plugins): preserve external credential contracts
243 lines
9.2 KiB
TypeScript
243 lines
9.2 KiB
TypeScript
// Perplexity tests cover perplexity web search provider plugin behavior.
|
|
import { withEnv, withEnvAsync } from "openclaw/plugin-sdk/test-env";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { createStreamingResponse } from "../../test-support/streaming-error-response.js";
|
|
|
|
const withTrustedWebSearchEndpointMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("openclaw/plugin-sdk/provider-web-search", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/provider-web-search")>();
|
|
return {
|
|
...actual,
|
|
withTrustedWebSearchEndpoint: withTrustedWebSearchEndpointMock,
|
|
};
|
|
});
|
|
|
|
import { createPerplexityWebSearchProvider } from "./perplexity-web-search-provider.js";
|
|
import { testing } from "./perplexity-web-search-provider.runtime.js";
|
|
|
|
const openRouterApiKeyEnv = ["OPENROUTER_API", "KEY"].join("_");
|
|
const perplexityApiKeyEnv = ["PERPLEXITY_API", "KEY"].join("_");
|
|
const openRouterPerplexityApiKey = ["sk", "or", "v1", "test"].join("-");
|
|
const directPerplexityApiKey = ["pplx", "test"].join("-");
|
|
const enterprisePerplexityApiKey = ["enterprise", "perplexity", "test"].join("-");
|
|
|
|
describe("perplexity web search provider", () => {
|
|
it("points missing-key users to fetch/browser alternatives", async () => {
|
|
await withEnvAsync(
|
|
{ [perplexityApiKeyEnv]: undefined, [openRouterApiKeyEnv]: undefined },
|
|
async () => {
|
|
const provider = createPerplexityWebSearchProvider();
|
|
const tool = provider.createTool({ config: {}, searchConfig: {} });
|
|
if (!tool) {
|
|
throw new Error("Expected tool definition");
|
|
}
|
|
|
|
await expect(tool.execute({ query: "OpenClaw docs" })).resolves.toEqual({
|
|
error: "missing_perplexity_api_key",
|
|
message:
|
|
"web_search (perplexity) needs an API key. Set PERPLEXITY_API_KEY or OPENROUTER_API_KEY in the Gateway environment, or configure plugins.entries.perplexity.config.webSearch.apiKey. If you do not want to configure a search API key, use web_fetch for a specific URL or the browser tool for interactive pages.",
|
|
docs: "https://docs.openclaw.ai/tools/web",
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it("infers provider routing from api key prefixes", () => {
|
|
expect(testing.inferPerplexityBaseUrlFromApiKey("pplx-abc")).toBe("direct");
|
|
expect(testing.inferPerplexityBaseUrlFromApiKey("sk-or-v1-abc")).toBe("openrouter");
|
|
expect(testing.inferPerplexityBaseUrlFromApiKey("unknown")).toBeUndefined();
|
|
});
|
|
|
|
it("resolves base url from auth source and request model by transport", () => {
|
|
expect(testing.resolvePerplexityBaseUrl(undefined, "perplexity_env")).toBe(
|
|
"https://api.perplexity.ai",
|
|
);
|
|
expect(testing.resolvePerplexityBaseUrl(undefined, "openrouter_env")).toBe(
|
|
"https://openrouter.ai/api/v1",
|
|
);
|
|
expect(
|
|
testing.resolvePerplexityRequestModel("https://api.perplexity.ai", "perplexity/sonar-pro"),
|
|
).toBe("sonar-pro");
|
|
expect(
|
|
testing.resolvePerplexityRequestModel("https://openrouter.ai/api/v1", "perplexity/sonar-pro"),
|
|
).toBe("perplexity/sonar-pro");
|
|
});
|
|
|
|
it("chooses direct search_api transport only for direct base urls without legacy overrides", () => {
|
|
expect(
|
|
testing.resolvePerplexityTransport({
|
|
baseUrl: "https://api.perplexity.ai",
|
|
}).transport,
|
|
).toBe("chat_completions");
|
|
|
|
expect(
|
|
testing.resolvePerplexityTransport({
|
|
apiKey: "pplx-secret",
|
|
}).transport,
|
|
).toBe("search_api");
|
|
});
|
|
|
|
it("prefers explicit baseUrl over key-based defaults", () => {
|
|
expect(
|
|
testing.resolvePerplexityBaseUrl({ baseUrl: "https://example.com" }, "config", "pplx-123"),
|
|
).toBe("https://example.com");
|
|
});
|
|
|
|
it("resolves OpenRouter env auth and transport", () => {
|
|
withEnv(
|
|
{ [perplexityApiKeyEnv]: undefined, [openRouterApiKeyEnv]: openRouterPerplexityApiKey },
|
|
() => {
|
|
expect(testing.resolvePerplexityApiKey(undefined)).toEqual({
|
|
apiKey: openRouterPerplexityApiKey,
|
|
source: "openrouter_env",
|
|
});
|
|
expect(testing.resolvePerplexityTransport(undefined)).toEqual({
|
|
apiKey: openRouterPerplexityApiKey,
|
|
source: "openrouter_env",
|
|
baseUrl: "https://openrouter.ai/api/v1",
|
|
model: "perplexity/sonar-pro",
|
|
transport: "chat_completions",
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it("uses native Search API for direct Perplexity when no legacy overrides exist", () => {
|
|
withEnv(
|
|
{ [perplexityApiKeyEnv]: directPerplexityApiKey, [openRouterApiKeyEnv]: undefined },
|
|
() => {
|
|
expect(testing.resolvePerplexityTransport(undefined)).toEqual({
|
|
apiKey: directPerplexityApiKey,
|
|
source: "perplexity_env",
|
|
baseUrl: "https://api.perplexity.ai",
|
|
model: "perplexity/sonar-pro",
|
|
transport: "search_api",
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it("switches direct Perplexity to chat completions when model override is configured", () => {
|
|
expect(testing.resolvePerplexityModel({ model: "perplexity/sonar-reasoning-pro" })).toBe(
|
|
"perplexity/sonar-reasoning-pro",
|
|
);
|
|
expect(
|
|
testing.resolvePerplexityTransport({
|
|
apiKey: directPerplexityApiKey,
|
|
model: "perplexity/sonar-reasoning-pro",
|
|
}),
|
|
).toEqual({
|
|
apiKey: directPerplexityApiKey,
|
|
source: "config",
|
|
baseUrl: "https://api.perplexity.ai",
|
|
model: "perplexity/sonar-reasoning-pro",
|
|
transport: "chat_completions",
|
|
});
|
|
});
|
|
|
|
it("treats unrecognized configured keys as direct Perplexity by default", () => {
|
|
expect(
|
|
testing.resolvePerplexityTransport({
|
|
apiKey: enterprisePerplexityApiKey,
|
|
}),
|
|
).toEqual({
|
|
apiKey: enterprisePerplexityApiKey,
|
|
source: "config",
|
|
baseUrl: "https://api.perplexity.ai",
|
|
model: "perplexity/sonar-pro",
|
|
transport: "search_api",
|
|
});
|
|
});
|
|
|
|
it("sends official date filter fields in the Search API request body", async () => {
|
|
withTrustedWebSearchEndpointMock.mockImplementationOnce(
|
|
async (_params: { init: RequestInit }, run: (response: Response) => Promise<unknown>) =>
|
|
await run(
|
|
new Response(JSON.stringify({ results: [] }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
),
|
|
);
|
|
|
|
await withEnvAsync(
|
|
{ [perplexityApiKeyEnv]: directPerplexityApiKey, [openRouterApiKeyEnv]: undefined },
|
|
async () => {
|
|
const provider = createPerplexityWebSearchProvider();
|
|
const tool = provider.createTool({ config: {}, searchConfig: {} });
|
|
if (!tool) {
|
|
throw new Error("Expected tool definition");
|
|
}
|
|
|
|
await tool.execute({
|
|
query: "OpenClaw releases",
|
|
date_after: "2024-01-01",
|
|
date_before: "2024-06-30",
|
|
});
|
|
},
|
|
);
|
|
|
|
expect(withTrustedWebSearchEndpointMock).toHaveBeenCalledOnce();
|
|
const [request] = withTrustedWebSearchEndpointMock.mock.calls[0] as [{ init: RequestInit }];
|
|
expect(JSON.parse(request.init.body as string)).toEqual({
|
|
query: "OpenClaw releases",
|
|
max_results: 5,
|
|
search_after_date_filter: "1/1/2024",
|
|
search_before_date_filter: "6/30/2024",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["max_tokens", 0, "max_tokens must be a positive integer."],
|
|
["max_tokens", 1.5, "max_tokens must be a positive integer."],
|
|
["max_tokens", 1_000_001, "max_tokens must be a positive integer."],
|
|
["max_tokens_per_page", 1.5, "max_tokens_per_page must be a positive integer."],
|
|
])("rejects invalid native token budget %s=%s", async (key, value, message) => {
|
|
await withEnvAsync(
|
|
{ [perplexityApiKeyEnv]: directPerplexityApiKey, [openRouterApiKeyEnv]: undefined },
|
|
async () => {
|
|
const provider = createPerplexityWebSearchProvider();
|
|
const tool = provider.createTool({ config: {}, searchConfig: {} });
|
|
if (!tool) {
|
|
throw new Error("Expected tool definition");
|
|
}
|
|
|
|
await expect(tool.execute({ query: "OpenClaw docs", [key]: value })).rejects.toThrow(
|
|
message,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("reports malformed Search API JSON with a stable provider error", async () => {
|
|
await expect(
|
|
testing.readPerplexityJsonResponse(new Response("{ nope"), "Perplexity Search"),
|
|
).rejects.toThrow("Perplexity Search: malformed JSON response");
|
|
});
|
|
|
|
it("reports malformed chat completion JSON with a stable provider error", async () => {
|
|
await expect(
|
|
testing.readPerplexityJsonResponse(new Response("{ nope"), "Perplexity"),
|
|
).rejects.toThrow("Perplexity: malformed JSON response");
|
|
});
|
|
|
|
it("bounds successful Perplexity JSON bodies before parsing", async () => {
|
|
const streamed = createStreamingResponse({
|
|
chunkCount: 32,
|
|
chunkSize: 1024 * 1024,
|
|
text: "x",
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
const jsonSpy = vi.spyOn(streamed.response, "json").mockRejectedValue(new Error("unbounded"));
|
|
|
|
await expect(
|
|
testing.readPerplexityJsonResponse(streamed.response, "Perplexity Search"),
|
|
).rejects.toThrow("Perplexity Search: JSON response exceeds 16777216 bytes");
|
|
|
|
expect(streamed.getReadCount()).toBeLessThan(32);
|
|
expect(streamed.wasCanceled()).toBe(true);
|
|
expect(jsonSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|