























@@ -1,6 +1,32 @@
11import type { LookupFn } from "openclaw/plugin-sdk/ssrf-runtime";
2-import { describe, expect, it, vi } from "vitest";
3-import { __testing } from "./searxng-client.js";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+4+const endpointMockState = vi.hoisted(() => ({
5+calls: [] as Array<{ url: string; timeoutSeconds: number; init: RequestInit }>,
6+responses: [] as Response[],
7+}));
8+9+vi.mock("openclaw/plugin-sdk/provider-web-search", async (importOriginal) => {
10+const actual = await importOriginal<typeof import("openclaw/plugin-sdk/provider-web-search")>();
11+const runEndpoint = async (
12+params: { url: string; timeoutSeconds: number; init: RequestInit },
13+run: (response: Response) => Promise<unknown>,
14+) => {
15+endpointMockState.calls.push(params);
16+const response = endpointMockState.responses.shift();
17+if (!response) {
18+throw new Error("Missing mocked SearXNG response.");
19+}
20+return await run(response);
21+};
22+return {
23+ ...actual,
24+withSelfHostedWebSearchEndpoint: vi.fn(runEndpoint),
25+withTrustedWebSearchEndpoint: vi.fn(runEndpoint),
26+};
27+});
28+29+import { __testing, runSearxngSearch } from "./searxng-client.js";
430531function createLookupFn(addresses: Array<{ address: string; family: number }>): LookupFn {
632return vi.fn(async (_hostname: string, options?: unknown) => {
@@ -12,6 +38,12 @@ function createLookupFn(addresses: Array<{ address: string; family: number }>):
1238}
13391440describe("searxng client", () => {
41+beforeEach(() => {
42+endpointMockState.calls = [];
43+endpointMockState.responses = [];
44+__testing.SEARXNG_SEARCH_CACHE.clear();
45+});
46+1547it("preserves a configured base-path prefix when building the search URL", () => {
1648expect(
1749__testing.buildSearxngSearchUrl({
@@ -39,6 +71,72 @@ describe("searxng client", () => {
3971).toEqual([{ title: "One", url: "https://example.com/1", content: "A" }]);
4072});
417374+it("retries an empty category search with general results", async () => {
75+endpointMockState.responses.push(
76+new Response(JSON.stringify({ results: [] }), { status: 200 }),
77+new Response(
78+JSON.stringify({
79+results: [
80+{
81+title: "Beijing hourly weather",
82+url: "https://example.com/weather",
83+content: "Hourly forecast",
84+},
85+],
86+}),
87+{ status: 200 },
88+),
89+);
90+91+const result = await runSearxngSearch({
92+baseUrl: "http://127.0.0.1:8888",
93+query: "beijing hourly weather",
94+categories: "weather",
95+count: 5,
96+});
97+98+expect(endpointMockState.calls).toHaveLength(2);
99+expect(new URL(endpointMockState.calls[0].url).searchParams.get("categories")).toBe("weather");
100+expect(new URL(endpointMockState.calls[1].url).searchParams.get("categories")).toBe("general");
101+expect(result).toMatchObject({
102+provider: "searxng",
103+count: 1,
104+results: [
105+expect.objectContaining({
106+url: "https://example.com/weather",
107+}),
108+],
109+});
110+});
111+112+it("does not retry empty general category searches", async () => {
113+endpointMockState.responses.push(
114+new Response(JSON.stringify({ results: [] }), { status: 200 }),
115+);
116+117+const result = await runSearxngSearch({
118+baseUrl: "http://127.0.0.1:8888",
119+query: "openclaw",
120+categories: "general",
121+count: 5,
122+});
123+124+expect(endpointMockState.calls).toHaveLength(1);
125+expect(result).toMatchObject({
126+provider: "searxng",
127+count: 0,
128+results: [],
129+});
130+});
131+132+it("detects category searches that should retry with general", () => {
133+expect(__testing.shouldRetryEmptyCategorySearchWithGeneral("weather")).toBe(true);
134+expect(__testing.shouldRetryEmptyCategorySearchWithGeneral("weather,news")).toBe(true);
135+expect(__testing.shouldRetryEmptyCategorySearchWithGeneral("general")).toBe(false);
136+expect(__testing.shouldRetryEmptyCategorySearchWithGeneral("general,news")).toBe(false);
137+expect(__testing.shouldRetryEmptyCategorySearchWithGeneral(undefined)).toBe(false);
138+});
139+42140it("preserves img_src from image search results", () => {
43141expect(
44142__testing.parseSearxngResponseText(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。