

























@@ -89,6 +89,17 @@ function createToolExtensionContext(): ExtensionContext {
8989return {} as ExtensionContext;
9090}
919192+async function waitForAfterToolCall(hooks: {
93+afterToolCall: { mock: { calls: unknown[][] } };
94+}): Promise<[Record<string, unknown>, Record<string, unknown>]> {
95+await vi.waitFor(() => {
96+expect(hooks.afterToolCall.mock.calls.length).toBe(1);
97+});
98+const call = hooks.afterToolCall.mock.calls[0];
99+expect(call).toBeDefined();
100+return call as [Record<string, unknown>, Record<string, unknown>];
101+}
102+92103describe("OpenClaw-owned tool runtime contract — Pi adapter", () => {
93104afterEach(() => {
94105resetOpenClawOwnedToolHooks();
@@ -140,26 +151,19 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => {
140151141152expect(hooks.beforeToolCall).toHaveBeenCalledTimes(1);
142153expect(execute).toHaveBeenCalledWith(toolCallId, mergedParams, undefined, undefined);
143-await vi.waitFor(() => {
144-expect(hooks.afterToolCall).toHaveBeenCalledWith(
145-expect.objectContaining({
146-toolName: "exec",
147- toolCallId,
148-params: mergedParams,
149-result: expect.objectContaining({
150-content: [{ type: "text", text: "done" }],
151-details: { ok: true },
152-}),
153-}),
154-expect.objectContaining({
155-agentId: "agent-1",
156-sessionId: "session-1",
157-sessionKey: "agent:agent-1:session-1",
158-runId: "run-contract",
159- toolCallId,
160-}),
161-);
154+const [afterPayload, afterContext] = await waitForAfterToolCall(hooks);
155+expect(afterPayload.toolName).toBe("exec");
156+expect(afterPayload.toolCallId).toBe(toolCallId);
157+expect(afterPayload.params).toEqual(mergedParams);
158+expect(afterPayload.result).toEqual({
159+content: [{ type: "text", text: "done" }],
160+details: { ok: true },
162161});
162+expect(afterContext.agentId).toBe("agent-1");
163+expect(afterContext.sessionId).toBe("session-1");
164+expect(afterContext.sessionKey).toBe("agent:agent-1:session-1");
165+expect(afterContext.runId).toBe("run-contract");
166+expect(afterContext.toolCallId).toBe(toolCallId);
163167});
164168165169it("reports Pi dynamic tool execution errors through after_tool_call", async () => {
@@ -199,14 +203,9 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => {
199203undefined,
200204createToolExtensionContext(),
201205);
202-expect(result).toEqual(
203-expect.objectContaining({
204-details: expect.objectContaining({
205-status: "error",
206-error: "tool failed",
207-}),
208-}),
209-);
206+const resultDetails = (result as { details?: Record<string, unknown> }).details;
207+expect(resultDetails?.status).toBe("error");
208+expect(resultDetails?.error).toBe("tool failed");
210209await handleToolExecutionEnd(
211210ctx,
212211toolExecutionEndEvent({
@@ -219,20 +218,13 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => {
219218220219expect(hooks.beforeToolCall).toHaveBeenCalledTimes(1);
221220expect(execute).toHaveBeenCalledWith(toolCallId, mergedParams, undefined, undefined);
222-await vi.waitFor(() => {
223-expect(hooks.afterToolCall).toHaveBeenCalledWith(
224-expect.objectContaining({
225-toolName: "exec",
226- toolCallId,
227-params: mergedParams,
228-error: "tool failed",
229-}),
230-expect.objectContaining({
231-runId: "run-error",
232- toolCallId,
233-}),
234-);
235-});
221+const [afterPayload, afterContext] = await waitForAfterToolCall(hooks);
222+expect(afterPayload.toolName).toBe("exec");
223+expect(afterPayload.toolCallId).toBe(toolCallId);
224+expect(afterPayload.params).toEqual(mergedParams);
225+expect(afterPayload.error).toBe("tool failed");
226+expect(afterContext.runId).toBe("run-error");
227+expect(afterContext.toolCallId).toBe(toolCallId);
236228});
237229238230it("commits successful Pi messaging text, media, and target telemetry", async () => {
@@ -286,31 +278,32 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => {
286278287279expect(ctx.state.messagingToolSentTexts).toEqual(["hello from Pi"]);
288280expect(ctx.state.messagingToolSentMediaUrls).toEqual(["/tmp/pi-reply.png"]);
289-expect(ctx.state.messagingToolSentTargets).toEqual([
290-expect.objectContaining({
281+expect(
282+ctx.state.messagingToolSentTargets.map((target) => ({
283+tool: "message",
284+provider: target.provider,
285+to: target.to,
286+text: target.text,
287+mediaUrls: target.mediaUrls,
288+})),
289+).toEqual([
290+{
291291tool: "message",
292292provider: "telegram",
293293to: "chat-1",
294294text: "hello from Pi",
295295mediaUrls: ["/tmp/pi-reply.png"],
296-}),
296+},
297297]);
298-await vi.waitFor(() => {
299-expect(hooks.afterToolCall).toHaveBeenCalledWith(
300-expect.objectContaining({
301-toolName: "message",
302- toolCallId,
303-params: originalParams,
304-result: expect.objectContaining({
305-content: [{ type: "text", text: "sent" }],
306-}),
307-}),
308-expect.objectContaining({
309-runId: "run-message",
310- toolCallId,
311-}),
312-);
313-});
298+const [afterPayload, afterContext] = await waitForAfterToolCall(hooks);
299+expect(afterPayload.toolName).toBe("message");
300+expect(afterPayload.toolCallId).toBe(toolCallId);
301+expect(afterPayload.params).toEqual(originalParams);
302+expect((afterPayload.result as { content?: unknown }).content).toEqual([
303+{ type: "text", text: "sent" },
304+]);
305+expect(afterContext.runId).toBe("run-message");
306+expect(afterContext.toolCallId).toBe(toolCallId);
314307});
315308316309it("fails closed when before_tool_call blocks a Pi dynamic tool", async () => {
@@ -351,15 +344,10 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => {
351344undefined,
352345createToolExtensionContext(),
353346);
354-expect(result).toEqual(
355-expect.objectContaining({
356-details: expect.objectContaining({
357-status: "blocked",
358-deniedReason: "plugin-before-tool-call",
359-reason: "blocked by policy",
360-}),
361-}),
362-);
347+const resultDetails = (result as { details?: Record<string, unknown> }).details;
348+expect(resultDetails?.status).toBe("blocked");
349+expect(resultDetails?.deniedReason).toBe("plugin-before-tool-call");
350+expect(resultDetails?.reason).toBe("blocked by policy");
363351await handleToolExecutionEnd(
364352ctx,
365353toolExecutionEndEvent({
@@ -372,30 +360,23 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => {
372360373361expect(hooks.beforeToolCall).toHaveBeenCalledTimes(1);
374362expect(execute).not.toHaveBeenCalled();
375-await vi.waitFor(() => {
376-expect(hooks.afterToolCall).toHaveBeenCalledWith(
377-expect.objectContaining({
378-toolName: "message",
379- toolCallId,
380-params: originalParams,
381-result: expect.objectContaining({
382-content: [{ type: "text", text: "blocked by policy" }],
383-details: {
384-status: "blocked",
385-deniedReason: "plugin-before-tool-call",
386-reason: "blocked by policy",
387-},
388-}),
389-error: "blocked by policy",
390-}),
391-expect.objectContaining({
392-agentId: "agent-1",
393-sessionId: "session-1",
394-sessionKey: "agent:agent-1:session-1",
395-runId: "run-blocked",
396- toolCallId,
397-}),
398-);
363+const [afterPayload, afterContext] = await waitForAfterToolCall(hooks);
364+expect(afterPayload.toolName).toBe("message");
365+expect(afterPayload.toolCallId).toBe(toolCallId);
366+expect(afterPayload.params).toEqual(originalParams);
367+expect(afterPayload.result).toEqual({
368+content: [{ type: "text", text: "blocked by policy" }],
369+details: {
370+status: "blocked",
371+deniedReason: "plugin-before-tool-call",
372+reason: "blocked by policy",
373+},
399374});
375+expect(afterPayload.error).toBe("blocked by policy");
376+expect(afterContext.agentId).toBe("agent-1");
377+expect(afterContext.sessionId).toBe("session-1");
378+expect(afterContext.sessionKey).toBe("agent:agent-1:session-1");
379+expect(afterContext.runId).toBe("run-blocked");
380+expect(afterContext.toolCallId).toBe(toolCallId);
400381});
401382});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。