





















@@ -44,20 +44,29 @@ function pluginTool(name: string, description: string, pluginId = "fake-catalog"
4444return tool;
4545}
464647+function resultDetails(result: { details?: unknown }): Record<string, unknown> {
48+expect(result.details).toBeDefined();
49+expect(typeof result.details).toBe("object");
50+return result.details as Record<string, unknown>;
51+}
52+53+function mockCall(mock: { mock: { calls: unknown[][] } }, index = 0): unknown[] {
54+const call = mock.mock.calls[index];
55+expect(call).toBeDefined();
56+return call;
57+}
58+4759describe("Tool Search", () => {
4860it("enables object config when a mode is set", () => {
49-expect(
50-__testing.resolveToolSearchConfig({
51-tools: {
52-toolSearch: {
53-mode: "tools",
54-},
61+const resolved = __testing.resolveToolSearchConfig({
62+tools: {
63+toolSearch: {
64+mode: "tools",
5565},
56-} as never),
57-).toMatchObject({
58-enabled: true,
59-mode: "tools",
60-});
66+},
67+} as never);
68+expect(resolved.enabled).toBe(true);
69+expect(resolved.mode).toBe("tools");
6170});
62716372it("falls back to structured controls when code mode is unsupported", () => {
@@ -121,24 +130,24 @@ describe("Tool Search", () => {
121130 `,
122131});
123132124-expect(alpha.execute).toHaveBeenCalledWith(
125- "tool_search_code:call-1:fake_create_ticket:1",
126- {
127- value: "ship",
128- },
129- expect.any(AbortSignal),
130- undefined,
131- undefined,
132-);
133-expect(result.details).toMatchObject({
134-ok: true,
135-telemetry: {
136- catalogSize: 2,
137- searchCount: 1,
138- describeCount: 1,
139- callCount: 1,
140- },
141-});
133+const alphaCall = mockCall(vi.mocked(alpha.execute));
134+expect(alphaCall[0]).toBe("tool_search_code:call-1:fake_create_ticket:1");
135+expect(alphaCall[1]).toEqual({ value: "ship" });
136+expect(alphaCall[2]).toBeInstanceOf(AbortSignal);
137+expect(alphaCall[3]).toBeUndefined();
138+expect(alphaCall[4]).toBeUndefined();
139+const details = resultDetails(result);
140+expect(details.ok).toBe(true);
141+const telemetry = details.telemetry as {
142+ catalogSize?: number;
143+searchCount?: number;
144+describeCount?: number;
145+callCount?: number;
146+};
147+expect(telemetry.catalogSize).toBe(2);
148+expect(telemetry.searchCount).toBe(1);
149+expect(telemetry.describeCount).toBe(1);
150+expect(telemetry.callCount).toBe(1);
142151});
143152144153it("scopes catalogs by run id when attempts share a session", async () => {
@@ -304,14 +313,10 @@ describe("Tool Search", () => {
304313305314expect(compacted.tools).toEqual([]);
306315expect(compacted.catalogToolCount).toBe(1);
307-expect(__testing.sessionCatalogs.get("session:session-client")?.entries).toEqual(
308-expect.arrayContaining([
309-expect.objectContaining({
310-id: "client:client:client_pick_file",
311-source: "client",
312-}),
313-]),
314-);
316+const clientEntry = __testing.sessionCatalogs
317+.get("session:session-client")
318+?.entries.find((entry) => entry.id === "client:client:client_pick_file");
319+expect(clientEntry?.source).toBe("client");
315320});
316321317322it("wraps cataloged OpenClaw tools with before_tool_call hooks", async () => {
@@ -343,12 +348,11 @@ describe("Tool Search", () => {
343348await runtimeCodeTool.execute("call-hooks", {
344349code: `return await openclaw.tools.call("fake_hooked", { value: "ok" });`,
345350});
346-expect(target.execute).toHaveBeenCalledWith(
347-"tool_search_code:call-hooks:fake_hooked:1",
348-{ value: "ok" },
349-expect.any(AbortSignal),
350-undefined,
351-);
351+const targetCall = mockCall(vi.mocked(target.execute));
352+expect(targetCall[0]).toBe("tool_search_code:call-hooks:fake_hooked:1");
353+expect(targetCall[1]).toEqual({ value: "ok" });
354+expect(targetCall[2]).toBeInstanceOf(AbortSignal);
355+expect(targetCall[3]).toBeUndefined();
352356});
353357354358it("does not re-wrap abort-wrapped tools that already have before_tool_call hooks", () => {
@@ -402,40 +406,28 @@ describe("Tool Search", () => {
402406 `,
403407});
404408405-expect(target.execute).toHaveBeenNthCalledWith(
406-1,
407-"tool_search_code:call-repeated:fake_repeated:1",
408-{
409-value: "one",
410-},
411-expect.any(AbortSignal),
412-undefined,
413-undefined,
414-);
415-expect(target.execute).toHaveBeenNthCalledWith(
416-2,
417-"tool_search_code:call-repeated:fake_repeated:2",
418-{
419-value: "two",
420-},
421-expect.any(AbortSignal),
422-undefined,
423-undefined,
424-);
409+const firstCall = mockCall(vi.mocked(target.execute));
410+expect(firstCall[0]).toBe("tool_search_code:call-repeated:fake_repeated:1");
411+expect(firstCall[1]).toEqual({ value: "one" });
412+expect(firstCall[2]).toBeInstanceOf(AbortSignal);
413+expect(firstCall[3]).toBeUndefined();
414+expect(firstCall[4]).toBeUndefined();
415+const secondCall = mockCall(vi.mocked(target.execute), 1);
416+expect(secondCall[0]).toBe("tool_search_code:call-repeated:fake_repeated:2");
417+expect(secondCall[1]).toEqual({ value: "two" });
418+expect(secondCall[2]).toBeInstanceOf(AbortSignal);
419+expect(secondCall[3]).toBeUndefined();
420+expect(secondCall[4]).toBeUndefined();
425421await runtimeCodeTool.execute("call-repeated-again", {
426422code: `return await openclaw.tools.call("fake_repeated", { value: "three" });`,
427423});
428424429-expect(target.execute).toHaveBeenNthCalledWith(
430-3,
431-"tool_search_code:call-repeated-again:fake_repeated:1",
432-{
433-value: "three",
434-},
435-expect.any(AbortSignal),
436-undefined,
437-undefined,
438-);
425+const thirdCall = mockCall(vi.mocked(target.execute), 2);
426+expect(thirdCall[0]).toBe("tool_search_code:call-repeated-again:fake_repeated:1");
427+expect(thirdCall[1]).toEqual({ value: "three" });
428+expect(thirdCall[2]).toBeInstanceOf(AbortSignal);
429+expect(thirdCall[3]).toBeUndefined();
430+expect(thirdCall[4]).toBeUndefined();
439431});
440432441433it("routes bridged calls through the configured catalog executor", async () => {
@@ -469,17 +461,22 @@ describe("Tool Search", () => {
469461);
470462471463expect(target.execute).not.toHaveBeenCalled();
472-expect(executeTool).toHaveBeenCalledWith(
473-expect.objectContaining({
474-tool: expect.objectContaining({ name: "fake_lifecycle" }),
475-toolName: "fake_lifecycle",
476-toolCallId: "tool_search_code:call-lifecycle:fake_lifecycle:1",
477-parentToolCallId: "call-lifecycle",
478-input: { value: "ok" },
479-signal: expect.any(AbortSignal),
480- onUpdate,
481-}),
482-);
464+const firstExecuteInput = mockCall(executeTool)[0] as {
465+tool?: { name?: string };
466+toolName?: string;
467+toolCallId?: string;
468+parentToolCallId?: string;
469+input?: unknown;
470+signal?: unknown;
471+onUpdate?: unknown;
472+};
473+expect(firstExecuteInput.tool?.name).toBe("fake_lifecycle");
474+expect(firstExecuteInput.toolName).toBe("fake_lifecycle");
475+expect(firstExecuteInput.toolCallId).toBe("tool_search_code:call-lifecycle:fake_lifecycle:1");
476+expect(firstExecuteInput.parentToolCallId).toBe("call-lifecycle");
477+expect(firstExecuteInput.input).toEqual({ value: "ok" });
478+expect(firstExecuteInput.signal).toBeInstanceOf(AbortSignal);
479+expect(firstExecuteInput.onUpdate).toBe(onUpdate);
483480484481await runtimeCallTool.execute(
485482"call-lifecycle-structured",
@@ -492,18 +489,24 @@ describe("Tool Search", () => {
492489);
493490494491expect(target.execute).not.toHaveBeenCalled();
495-expect(executeTool).toHaveBeenNthCalledWith(
496-2,
497-expect.objectContaining({
498-tool: expect.objectContaining({ name: "fake_lifecycle" }),
499-toolName: "fake_lifecycle",
500-toolCallId: "tool_search_code:call-lifecycle-structured:fake_lifecycle:1",
501-parentToolCallId: "call-lifecycle-structured",
502-input: { value: "structured" },
503-signal: abortController.signal,
504- onUpdate,
505-}),
492+const secondExecuteInput = mockCall(executeTool, 1)[0] as {
493+tool?: { name?: string };
494+toolName?: string;
495+toolCallId?: string;
496+parentToolCallId?: string;
497+input?: unknown;
498+signal?: unknown;
499+onUpdate?: unknown;
500+};
501+expect(secondExecuteInput.tool?.name).toBe("fake_lifecycle");
502+expect(secondExecuteInput.toolName).toBe("fake_lifecycle");
503+expect(secondExecuteInput.toolCallId).toBe(
504+"tool_search_code:call-lifecycle-structured:fake_lifecycle:1",
506505);
506+expect(secondExecuteInput.parentToolCallId).toBe("call-lifecycle-structured");
507+expect(secondExecuteInput.input).toEqual({ value: "structured" });
508+expect(secondExecuteInput.signal).toBe(abortController.signal);
509+expect(secondExecuteInput.onUpdate).toBe(onUpdate);
507510});
508511509512it("projects target tool calls after their Tool Search wrapper result", () => {
@@ -543,25 +546,40 @@ describe("Tool Search", () => {
543546]);
544547545548expect(projected).toHaveLength(5);
546-expect(projected[2]).toMatchObject({
547-role: "assistant",
548-content: [
549-{
550-type: "toolCall",
551-id: "tool_search_code:wrapper-call:fake_target:1",
552-name: "fake_target",
553-arguments: { value: "ok" },
554-input: { value: "ok" },
555-},
556-],
557-});
558-expect(projected[3]).toMatchObject({
559-role: "toolResult",
560-toolCallId: "tool_search_code:wrapper-call:fake_target:1",
561-toolName: "fake_target",
562-isError: false,
563-content: [{ type: "text", text: JSON.stringify({ ok: true }, null, 2) }],
564-});
549+const projectedToolCall = projected[2] as {
550+role?: string;
551+content?: Array<{
552+type?: string;
553+id?: string;
554+name?: string;
555+arguments?: unknown;
556+input?: unknown;
557+}>;
558+};
559+expect(projectedToolCall.role).toBe("assistant");
560+expect(projectedToolCall.content).toEqual([
561+{
562+type: "toolCall",
563+id: "tool_search_code:wrapper-call:fake_target:1",
564+name: "fake_target",
565+arguments: { value: "ok" },
566+input: { value: "ok" },
567+},
568+]);
569+const projectedToolResult = projected[3] as {
570+role?: string;
571+toolCallId?: string;
572+toolName?: string;
573+isError?: boolean;
574+content?: unknown;
575+};
576+expect(projectedToolResult.role).toBe("toolResult");
577+expect(projectedToolResult.toolCallId).toBe("tool_search_code:wrapper-call:fake_target:1");
578+expect(projectedToolResult.toolName).toBe("fake_target");
579+expect(projectedToolResult.isError).toBe(false);
580+expect(projectedToolResult.content).toEqual([
581+{ type: "text", text: JSON.stringify({ ok: true }, null, 2) },
582+]);
565583expect(projected[4]).toBe(messages[2]);
566584});
567585@@ -589,13 +607,10 @@ describe("Tool Search", () => {
589607});
590608591609expect(target.execute).not.toHaveBeenCalled();
592-expect(result.details).toMatchObject({
593-ok: true,
594-value: "done",
595-telemetry: {
596-callCount: 0,
597-},
598-});
610+const details = resultDetails(result);
611+expect(details.ok).toBe(true);
612+expect(details.value).toBe("done");
613+expect((details.telemetry as { callCount?: number }).callCount).toBe(0);
599614});
600615601616it("waits for started bridged calls before returning code-mode success", async () => {
@@ -642,13 +657,10 @@ describe("Tool Search", () => {
642657resolveTool?.();
643658const result = await resultPromise;
644659645-expect(result.details).toMatchObject({
646-ok: true,
647-value: "done",
648-telemetry: {
649-callCount: 1,
650-},
651-});
660+const details = resultDetails(result);
661+expect(details.ok).toBe(true);
662+expect(details.value).toBe("done");
663+expect((details.telemetry as { callCount?: number }).callCount).toBe(1);
652664});
653665654666it("does not expose the host process to model-authored code", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。