






























@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
22import {
33onDiagnosticEvent,
44resetDiagnosticEventsForTest,
5+type DiagnosticEventPayload,
56type DiagnosticToolLoopEvent,
67} from "../infra/diagnostic-events.js";
78import { resetDiagnosticSessionStateForTest } from "../logging/diagnostic-session-state.js";
@@ -85,6 +86,22 @@ describe("before_tool_call loop detection behavior", () => {
8586}
8687}
878889+async function withToolExecutionEvents(
90+run: (emitted: DiagnosticEventPayload[]) => Promise<void>,
91+) {
92+const emitted: DiagnosticEventPayload[] = [];
93+const stop = onDiagnosticEvent((evt) => {
94+if (evt.type.startsWith("tool.execution.")) {
95+emitted.push(evt);
96+}
97+});
98+try {
99+await run(emitted);
100+} finally {
101+stop();
102+}
103+}
104+88105function createPingPongTools(options?: { withProgress?: boolean }) {
89106const readExecute = options?.withProgress
90107 ? vi.fn().mockImplementation(async (toolCallId: string) => ({
@@ -331,6 +348,115 @@ describe("before_tool_call loop detection behavior", () => {
331348});
332349});
333350});
351+352+it("emits diagnostic tool execution events without parameter values", async () => {
353+const trace = {
354+traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
355+spanId: "00f067aa0ba902b7",
356+traceFlags: "01",
357+};
358+const execute = vi.fn().mockResolvedValue({
359+content: [{ type: "text", text: "ok" }],
360+});
361+const tool = wrapToolWithBeforeToolCallHook({ name: "bash", execute } as any, {
362+agentId: "main",
363+sessionKey: "session-key",
364+sessionId: "session-id",
365+runId: "run-1",
366+ trace,
367+loopDetection: { enabled: false },
368+});
369+370+await withToolExecutionEvents(async (emitted) => {
371+await tool.execute(
372+"tool-call-1",
373+{ command: "pwd", token: "sk-1234567890abcdef1234567890abcdef" },
374+undefined,
375+undefined,
376+);
377+378+expect(emitted.map((evt) => evt.type)).toEqual([
379+"tool.execution.started",
380+"tool.execution.completed",
381+]);
382+expect(emitted[0]).toMatchObject({
383+type: "tool.execution.started",
384+runId: "run-1",
385+sessionKey: "session-key",
386+sessionId: "session-id",
387+toolName: "exec",
388+toolCallId: "tool-call-1",
389+paramsSummary: {
390+kind: "object",
391+},
392+ trace,
393+});
394+expect(emitted[0]?.trace).not.toBe(trace);
395+expect(Object.isFrozen(emitted[0]?.trace)).toBe(true);
396+expect(emitted[1]).toMatchObject({
397+type: "tool.execution.completed",
398+durationMs: expect.any(Number),
399+});
400+expect(JSON.stringify(emitted)).not.toContain("sk-1234567890abcdef1234567890abcdef");
401+expect(JSON.stringify(emitted)).not.toContain("pwd");
402+});
403+});
404+405+it("emits diagnostic tool execution error events with redacted errors", async () => {
406+const execute = vi
407+.fn()
408+.mockRejectedValue(new Error("failed with key sk-1234567890abcdef1234567890abcdef"));
409+const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
410+agentId: "main",
411+sessionKey: "session-key",
412+loopDetection: { enabled: false },
413+});
414+415+await withToolExecutionEvents(async (emitted) => {
416+await expect(
417+tool.execute("tool-call-error", { path: "/tmp/file" }, undefined, undefined),
418+).rejects.toThrow("failed with key");
419+420+expect(emitted.map((evt) => evt.type)).toEqual([
421+"tool.execution.started",
422+"tool.execution.error",
423+]);
424+expect(emitted[1]).toMatchObject({
425+type: "tool.execution.error",
426+toolName: "read",
427+toolCallId: "tool-call-error",
428+durationMs: expect.any(Number),
429+errorCategory: "Error",
430+});
431+expect(JSON.stringify(emitted[1])).not.toContain("sk-1234567890abcdef1234567890abcdef");
432+});
433+});
434+435+it("summarizes hostile object params without enumerating keys", async () => {
436+const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
437+const tool = wrapToolWithBeforeToolCallHook({ name: "bash", execute } as any, {
438+agentId: "main",
439+sessionKey: "session-key",
440+loopDetection: { enabled: false },
441+});
442+const params = new Proxy(
443+{},
444+{
445+ownKeys() {
446+throw new Error("should not enumerate params");
447+},
448+},
449+);
450+451+await withToolExecutionEvents(async (emitted) => {
452+await tool.execute("tool-call-proxy", params, undefined, undefined);
453+454+expect(emitted[0]).toMatchObject({
455+type: "tool.execution.started",
456+paramsSummary: { kind: "object" },
457+});
458+});
459+});
334460});
335461336462describe("before_tool_call requireApproval handling", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。