Skip to content

Commit 3b2cf33

Browse files
committed
🐛 修复 Agent 工具测试:替换失效的 vi.mock 别名路径为 mockSender 方案
1 parent adadc5c commit 3b2cf33

7 files changed

Lines changed: 57 additions & 53 deletions

File tree

src/app/service/agent/tools/ask_user.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe("ask_user", () => {
2121

2222
// Resolve the question
2323
expect(resolvers.size).toBe(1);
24-
const [askId, resolve] = Array.from(resolvers.entries())[0];
24+
const [_askId, resolve] = Array.from(resolvers.entries())[0];
2525
resolve("Blue");
2626

2727
const result = await resultPromise;
@@ -71,7 +71,7 @@ describe("ask_user", () => {
7171
expect(id1).not.toBe(id2);
7272

7373
// Resolve both
74-
for (const [id, resolve] of resolvers) {
74+
for (const [_id, resolve] of resolvers) {
7575
resolve("answer");
7676
}
7777
await Promise.all([p1, p2]);

src/app/service/agent/tools/sub_agent.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ export const SUB_AGENT_DEFINITION: ToolDefinition = {
2121
},
2222
};
2323

24-
export function createSubAgentTool(params: {
25-
runSubAgent: (prompt: string, description: string) => Promise<string>;
26-
}): { definition: ToolDefinition; executor: ToolExecutor } {
24+
export function createSubAgentTool(params: { runSubAgent: (prompt: string, description: string) => Promise<string> }): {
25+
definition: ToolDefinition;
26+
executor: ToolExecutor;
27+
} {
2728
const executor: ToolExecutor = {
2829
execute: async (args: Record<string, unknown>) => {
2930
const prompt = args.prompt as string;

src/app/service/agent/tools/task_tools.test.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,7 @@ describe("task_tools", () => {
8282

8383
await create.executor.execute({ subject: "Test", description: "Some desc" });
8484

85-
const result = JSON.parse(
86-
(await update.executor.execute({ task_id: "1", description: "" })) as string
87-
);
85+
const result = JSON.parse((await update.executor.execute({ task_id: "1", description: "" })) as string);
8886
expect(result.description).toBe("");
8987
});
9088

@@ -95,9 +93,7 @@ describe("task_tools", () => {
9593

9694
await create.executor.execute({ subject: "Original", description: "Desc" });
9795

98-
const result = JSON.parse(
99-
(await update.executor.execute({ task_id: "1" })) as string
100-
);
96+
const result = JSON.parse((await update.executor.execute({ task_id: "1" })) as string);
10197
expect(result.subject).toBe("Original");
10298
expect(result.description).toBe("Desc");
10399
expect(result.status).toBe("pending");

src/app/service/agent/tools/web_fetch.test.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
import { describe, it, expect, vi, beforeEach } from "vitest";
22
import { WebFetchExecutor, stripHtmlTags } from "./web_fetch";
33

4-
// Mock offscreen client
5-
vi.mock("@App/app/service/offscreen/client", () => ({
6-
extractHtmlContent: vi.fn(),
7-
}));
8-
9-
import { extractHtmlContent } from "@App/app/service/offscreen/client";
10-
const mockExtract = vi.mocked(extractHtmlContent);
4+
// 通过 mockSender.sendMessage 控制 offscreen extractHtmlContent 的返回值
5+
let mockExtractReturnValue: string | null = null;
6+
let mockExtractShouldThrow = false;
117

128
describe("stripHtmlTags", () => {
139
it("should remove HTML tags", () => {
1410
expect(stripHtmlTags("<p>Hello <b>World</b></p>")).toBe("Hello World");
1511
});
1612

1713
it("should remove script and style tags with content", () => {
18-
const html = '<div>text<script>alert(1)</script> <style>.x{}</style>more</div>';
14+
const html = "<div>text<script>alert(1)</script> <style>.x{}</style>more</div>";
1915
expect(stripHtmlTags(html)).toBe("text more");
2016
});
2117

@@ -25,11 +21,26 @@ describe("stripHtmlTags", () => {
2521
});
2622

2723
describe("WebFetchExecutor", () => {
28-
const mockSender = {} as any;
24+
const mockSender = {
25+
sendMessage: vi.fn().mockImplementation(() => {
26+
if (mockExtractShouldThrow) {
27+
return Promise.reject(new Error("Offscreen unavailable"));
28+
}
29+
return Promise.resolve({ data: mockExtractReturnValue });
30+
}),
31+
} as any;
2932

3033
beforeEach(() => {
3134
vi.clearAllMocks();
3235
vi.stubGlobal("fetch", vi.fn());
36+
mockExtractReturnValue = null;
37+
mockExtractShouldThrow = false;
38+
mockSender.sendMessage.mockImplementation(() => {
39+
if (mockExtractShouldThrow) {
40+
return Promise.reject(new Error("Offscreen unavailable"));
41+
}
42+
return Promise.resolve({ data: mockExtractReturnValue });
43+
});
3344
});
3445

3546
it("should throw for missing url", async () => {
@@ -70,7 +81,7 @@ describe("WebFetchExecutor", () => {
7081
text: () => Promise.resolve("<html><body><p>Hello World long content here for testing</p></body></html>"),
7182
});
7283
vi.stubGlobal("fetch", mockFetch);
73-
mockExtract.mockResolvedValue("Hello World long content here for testing extracted properly by offscreen");
84+
mockExtractReturnValue = "Hello World long content here for testing extracted properly by offscreen";
7485

7586
const executor = new WebFetchExecutor(mockSender);
7687
const result = JSON.parse((await executor.execute({ url: "https://example.com" })) as string);
@@ -86,7 +97,7 @@ describe("WebFetchExecutor", () => {
8697
text: () => Promise.resolve("<p>Simple text</p>"),
8798
});
8899
vi.stubGlobal("fetch", mockFetch);
89-
mockExtract.mockResolvedValue(null);
100+
mockExtractReturnValue = null;
90101

91102
const executor = new WebFetchExecutor(mockSender);
92103
const result = JSON.parse((await executor.execute({ url: "https://example.com" })) as string);
@@ -129,7 +140,7 @@ describe("WebFetchExecutor", () => {
129140
text: () => Promise.resolve("<p>Fallback content</p>"),
130141
});
131142
vi.stubGlobal("fetch", mockFetch);
132-
mockExtract.mockRejectedValue(new Error("Offscreen unavailable"));
143+
mockExtractShouldThrow = true;
133144

134145
const executor = new WebFetchExecutor(mockSender);
135146
const result = JSON.parse((await executor.execute({ url: "https://example.com" })) as string);
@@ -145,7 +156,7 @@ describe("WebFetchExecutor", () => {
145156
text: () => Promise.resolve("<p>Hi</p>"),
146157
});
147158
vi.stubGlobal("fetch", mockFetch);
148-
mockExtract.mockResolvedValue("Hi"); // shorter than 50 chars
159+
mockExtractReturnValue = "Hi"; // shorter than 50 chars
149160

150161
const executor = new WebFetchExecutor(mockSender);
151162
const result = JSON.parse((await executor.execute({ url: "https://example.com" })) as string);
@@ -173,16 +184,19 @@ describe("WebFetchExecutor", () => {
173184
const mockFetch = vi.fn().mockResolvedValue({
174185
ok: true,
175186
headers: new Headers({}),
176-
text: () => Promise.resolve("<html><body>Long enough content for extraction to work properly and pass the threshold</body></html>"),
187+
text: () =>
188+
Promise.resolve(
189+
"<html><body>Long enough content for extraction to work properly and pass the threshold</body></html>"
190+
),
177191
});
178192
vi.stubGlobal("fetch", mockFetch);
179-
mockExtract.mockResolvedValue("Long enough content for extraction to work properly and pass the threshold");
193+
mockExtractReturnValue = "Long enough content for extraction to work properly and pass the threshold";
180194

181195
const executor = new WebFetchExecutor(mockSender);
182196
const result = JSON.parse((await executor.execute({ url: "https://example.com" })) as string);
183197

184198
expect(result.content_type).toBe("html");
185-
expect(mockExtract).toHaveBeenCalled();
199+
expect(mockSender.sendMessage).toHaveBeenCalled();
186200
});
187201

188202
it("should handle text/plain content-type as plain text", async () => {
@@ -198,7 +212,7 @@ describe("WebFetchExecutor", () => {
198212

199213
expect(result.content_type).toBe("text");
200214
expect(result.content).toBe("Just plain text");
201-
expect(mockExtract).not.toHaveBeenCalled();
215+
expect(mockSender.sendMessage).not.toHaveBeenCalled();
202216
});
203217

204218
it("should use default max_length of 10000", async () => {

src/app/service/agent/tools/web_search.test.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
22
import { WebSearchExecutor } from "./web_search";
33
import type { SearchConfigRepo } from "./search_config";
44

5-
// Mock offscreen client
6-
vi.mock("@App/app/service/offscreen/client", () => ({
7-
extractSearchResults: vi.fn(),
8-
}));
9-
10-
import { extractSearchResults } from "@App/app/service/offscreen/client";
11-
const mockExtractResults = vi.mocked(extractSearchResults);
5+
// mockExtractResults 存储 mock 返回值,通过 mockSender.sendMessage 传递
6+
let mockExtractReturnValue: any[] = [];
127

138
describe("WebSearchExecutor", () => {
14-
const mockSender = {} as any;
9+
const mockSender = {
10+
sendMessage: vi.fn().mockImplementation(() => Promise.resolve({ data: mockExtractReturnValue })),
11+
} as any;
1512

1613
const createMockConfigRepo = (engine: "duckduckgo" | "google_custom"): SearchConfigRepo => ({
1714
getConfig: vi.fn().mockResolvedValue({
@@ -25,6 +22,8 @@ describe("WebSearchExecutor", () => {
2522
beforeEach(() => {
2623
vi.clearAllMocks();
2724
vi.stubGlobal("fetch", vi.fn());
25+
mockExtractReturnValue = [];
26+
mockSender.sendMessage.mockImplementation(() => Promise.resolve({ data: mockExtractReturnValue }));
2827
});
2928

3029
it("should throw for missing query", async () => {
@@ -39,10 +38,10 @@ describe("WebSearchExecutor", () => {
3938
});
4039
vi.stubGlobal("fetch", mockFetch);
4140

42-
mockExtractResults.mockResolvedValue([
41+
mockExtractReturnValue = [
4342
{ title: "Result 1", url: "https://example.com/1", snippet: "Snippet 1" },
4443
{ title: "Result 2", url: "https://example.com/2", snippet: "Snippet 2" },
45-
]);
44+
];
4645

4746
const executor = new WebSearchExecutor(mockSender, createMockConfigRepo("duckduckgo"));
4847
const result = JSON.parse((await executor.execute({ query: "test search" })) as string);
@@ -64,7 +63,7 @@ describe("WebSearchExecutor", () => {
6463
url: `https://example.com/${i}`,
6564
snippet: `S${i}`,
6665
}));
67-
mockExtractResults.mockResolvedValue(manyResults);
66+
mockExtractReturnValue = manyResults;
6867

6968
const executor = new WebSearchExecutor(mockSender, createMockConfigRepo("duckduckgo"));
7069
const result = JSON.parse((await executor.execute({ query: "test", max_results: 3 })) as string);
@@ -84,7 +83,7 @@ describe("WebSearchExecutor", () => {
8483
url: `https://example.com/${i}`,
8584
snippet: `S${i}`,
8685
}));
87-
mockExtractResults.mockResolvedValue(manyResults);
86+
mockExtractReturnValue = manyResults;
8887

8988
const executor = new WebSearchExecutor(mockSender, createMockConfigRepo("duckduckgo"));
9089
const result = JSON.parse((await executor.execute({ query: "test", max_results: 20 })) as string);
@@ -97,9 +96,7 @@ describe("WebSearchExecutor", () => {
9796
ok: true,
9897
json: () =>
9998
Promise.resolve({
100-
items: [
101-
{ title: "Google Result", link: "https://example.com", snippet: "Google snippet" },
102-
],
99+
items: [{ title: "Google Result", link: "https://example.com", snippet: "Google snippet" }],
103100
}),
104101
});
105102
vi.stubGlobal("fetch", mockFetch);
@@ -170,9 +167,11 @@ describe("WebSearchExecutor", () => {
170167
vi.stubGlobal("fetch", mockFetch);
171168

172169
const manyResults = Array.from({ length: 8 }, (_, i) => ({
173-
title: `R${i}`, url: `https://example.com/${i}`, snippet: `S${i}`,
170+
title: `R${i}`,
171+
url: `https://example.com/${i}`,
172+
snippet: `S${i}`,
174173
}));
175-
mockExtractResults.mockResolvedValue(manyResults);
174+
mockExtractReturnValue = manyResults;
176175

177176
const executor = new WebSearchExecutor(mockSender, createMockConfigRepo("duckduckgo"));
178177
const result = JSON.parse((await executor.execute({ query: "test" })) as string);

src/app/service/agent/tools/web_search.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,7 @@ export class WebSearchExecutor implements ToolExecutor {
6161
return JSON.stringify(results.slice(0, maxResults));
6262
}
6363

64-
private async searchGoogle(
65-
query: string,
66-
maxResults: number,
67-
apiKey: string,
68-
cseId: string
69-
): Promise<string> {
64+
private async searchGoogle(query: string, maxResults: number, apiKey: string, cseId: string): Promise<string> {
7065
if (!apiKey || !cseId) {
7166
throw new Error("Google Custom Search requires API Key and CSE ID. Configure them in Agent Tool Settings.");
7267
}

src/app/service/service_worker/agent.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,8 +1376,7 @@ export class AgentService {
13761376
model,
13771377
prompt,
13781378
signal: abortController.signal,
1379-
sendEvent: (evt) =>
1380-
sendEvent({ type: "sub_agent_event", agentId, description: desc, event: evt }),
1379+
sendEvent: (evt) => sendEvent({ type: "sub_agent_event", agentId, description: desc, event: evt }),
13811380
excludeTools: ["ask_user", "agent"],
13821381
maxIterations: 20,
13831382
});

0 commit comments

Comments
 (0)