



























@@ -802,6 +802,268 @@ describe("handleToolExecutionEnd derived tool events", () => {
802802}
803803});
804804805+it("drops throttled exec output before emitting live events or callbacks", async () => {
806+vi.useFakeTimers();
807+vi.setSystemTime(1_000);
808+resetAgentEventsForTest();
809+const events: Array<{ stream?: string; data?: Record<string, unknown> }> = [];
810+registerAgentEventListener((evt) => {
811+events.push(evt as never);
812+});
813+try {
814+const { ctx, onAgentEvent } = createTestContext();
815+816+await handleToolExecutionStart(
817+ctx as never,
818+{
819+type: "tool_execution_start",
820+toolName: "exec",
821+toolCallId: "tool-exec-drop-suppressed-output",
822+args: { command: "yes" },
823+} as never,
824+);
825+826+handleToolExecutionUpdate(
827+ctx as never,
828+{
829+type: "tool_execution_update",
830+toolName: "exec",
831+toolCallId: "tool-exec-drop-suppressed-output",
832+partialResult: { details: { aggregated: "first" } },
833+} as never,
834+);
835+const emittedEventCount = events.length;
836+const callbackCount = onAgentEvent.mock.calls.length;
837+const itemStartedCount = ctx.state.itemStartedCount;
838+839+handleToolExecutionUpdate(
840+ctx as never,
841+{
842+type: "tool_execution_update",
843+toolName: "exec",
844+toolCallId: "tool-exec-drop-suppressed-output",
845+partialResult: { details: { aggregated: "x".repeat(1024 * 1024) } },
846+} as never,
847+);
848+849+expect(events).toHaveLength(emittedEventCount);
850+expect(onAgentEvent).toHaveBeenCalledTimes(callbackCount);
851+expect(ctx.state.itemStartedCount).toBe(itemStartedCount);
852+} finally {
853+resetAgentEventsForTest();
854+vi.useRealTimers();
855+}
856+});
857+858+it("throttles exec output independently per tool call", async () => {
859+vi.useFakeTimers();
860+vi.setSystemTime(1_000);
861+try {
862+const { ctx, onAgentEvent } = createTestContext();
863+864+for (const toolCallId of ["tool-exec-per-tool-a", "tool-exec-per-tool-b"]) {
865+await handleToolExecutionStart(
866+ctx as never,
867+{
868+type: "tool_execution_start",
869+toolName: "exec",
870+ toolCallId,
871+args: { command: "yes" },
872+} as never,
873+);
874+}
875+876+for (const toolCallId of ["tool-exec-per-tool-a", "tool-exec-per-tool-b"]) {
877+handleToolExecutionUpdate(
878+ctx as never,
879+{
880+type: "tool_execution_update",
881+toolName: "exec",
882+ toolCallId,
883+partialResult: { details: { aggregated: `first-${toolCallId}` } },
884+} as never,
885+);
886+}
887+888+const commandOutputCalls = onAgentEvent.mock.calls
889+.map((call) => call[0] as { stream?: string; data?: { output?: string } })
890+.filter((event) => event.stream === "command_output");
891+892+expect(commandOutputCalls.map((event) => event.data?.output)).toEqual([
893+"first-tool-exec-per-tool-a",
894+"first-tool-exec-per-tool-b",
895+]);
896+} finally {
897+vi.useRealTimers();
898+}
899+});
900+901+it("clears exec output throttle state when the tool ends", async () => {
902+vi.useFakeTimers();
903+vi.setSystemTime(1_000);
904+try {
905+const { ctx, onAgentEvent } = createTestContext();
906+const toolCallId = "tool-exec-throttle-cleared";
907+908+await handleToolExecutionStart(
909+ctx as never,
910+{
911+type: "tool_execution_start",
912+toolName: "exec",
913+ toolCallId,
914+args: { command: "yes" },
915+} as never,
916+);
917+handleToolExecutionUpdate(
918+ctx as never,
919+{
920+type: "tool_execution_update",
921+toolName: "exec",
922+ toolCallId,
923+partialResult: { details: { aggregated: "first run output" } },
924+} as never,
925+);
926+await handleToolExecutionEnd(
927+ctx as never,
928+{
929+type: "tool_execution_end",
930+toolName: "exec",
931+ toolCallId,
932+isError: false,
933+result: { details: { status: "completed", aggregated: "done" } },
934+} as never,
935+);
936+await handleToolExecutionStart(
937+ctx as never,
938+{
939+type: "tool_execution_start",
940+toolName: "exec",
941+ toolCallId,
942+args: { command: "yes" },
943+} as never,
944+);
945+handleToolExecutionUpdate(
946+ctx as never,
947+{
948+type: "tool_execution_update",
949+toolName: "exec",
950+ toolCallId,
951+partialResult: { details: { aggregated: "second run output" } },
952+} as never,
953+);
954+955+const commandOutputCalls = onAgentEvent.mock.calls
956+.map((call) => call[0] as { stream?: string; data?: { output?: string } })
957+.filter((event) => event.stream === "command_output");
958+959+expect(commandOutputCalls.map((event) => event.data?.output)).toContain("second run output");
960+} finally {
961+vi.useRealTimers();
962+}
963+});
964+965+it("does not throttle exec update events that carry no output", async () => {
966+vi.useFakeTimers();
967+vi.setSystemTime(1_000);
968+try {
969+const { ctx, onAgentEvent } = createTestContext();
970+971+await handleToolExecutionStart(
972+ctx as never,
973+{
974+type: "tool_execution_start",
975+toolName: "exec",
976+toolCallId: "tool-exec-no-output-updates",
977+args: { command: "sleep 1" },
978+} as never,
979+);
980+981+for (let i = 0; i < 2; i += 1) {
982+handleToolExecutionUpdate(
983+ctx as never,
984+{
985+type: "tool_execution_update",
986+toolName: "exec",
987+toolCallId: "tool-exec-no-output-updates",
988+partialResult: { details: { status: "running", pid: 1234 + i } },
989+} as never,
990+);
991+}
992+993+const updateCallbacks = onAgentEvent.mock.calls
994+.map((call) => call[0] as { stream?: string; data?: { phase?: string } })
995+.filter((event) => event.stream === "tool" && event.data?.phase === "update");
996+997+expect(updateCallbacks).toHaveLength(2);
998+} finally {
999+vi.useRealTimers();
1000+}
1001+});
1002+1003+it("caps oversized exec update payloads that pass the throttle window", async () => {
1004+vi.useFakeTimers();
1005+vi.setSystemTime(1_000);
1006+resetAgentEventsForTest();
1007+const events: Array<{ stream?: string; data?: Record<string, unknown> }> = [];
1008+registerAgentEventListener((evt) => {
1009+events.push(evt as never);
1010+});
1011+try {
1012+const { ctx, onAgentEvent } = createTestContext();
1013+const aggregated = `head-${"x".repeat(90 * 1024)}-tail`;
1014+1015+await handleToolExecutionStart(
1016+ctx as never,
1017+{
1018+type: "tool_execution_start",
1019+toolName: "exec",
1020+toolCallId: "tool-exec-update-long-output",
1021+args: { command: "yes" },
1022+} as never,
1023+);
1024+1025+handleToolExecutionUpdate(
1026+ctx as never,
1027+{
1028+type: "tool_execution_update",
1029+toolName: "exec",
1030+toolCallId: "tool-exec-update-long-output",
1031+partialResult: { details: { aggregated: "first" } },
1032+} as never,
1033+);
1034+vi.setSystemTime(1_300);
1035+handleToolExecutionUpdate(
1036+ctx as never,
1037+{
1038+type: "tool_execution_update",
1039+toolName: "exec",
1040+toolCallId: "tool-exec-update-long-output",
1041+partialResult: { details: { aggregated } },
1042+} as never,
1043+);
1044+1045+const lastCommandOutput = onAgentEvent.mock.calls
1046+.map((call) => call[0] as { stream?: string; data?: { output?: string } })
1047+.findLast((event) => event.stream === "command_output");
1048+expect(lastCommandOutput?.data?.output).toContain("live command output truncated");
1049+expect(lastCommandOutput?.data?.output).toContain("-tail");
1050+expect(lastCommandOutput?.data?.output).not.toContain("head-");
1051+1052+const updateEvent = events.findLast(
1053+(evt) => evt.stream === "tool" && (evt.data as { phase?: string })?.phase === "update",
1054+);
1055+const partialResult = updateEvent?.data?.partialResult as
1056+| { details?: { aggregated?: string } }
1057+| undefined;
1058+expect(partialResult?.details?.aggregated).toContain("live command output truncated");
1059+expect(partialResult?.details?.aggregated).toContain("-tail");
1060+expect(partialResult?.details?.aggregated).not.toContain("head-");
1061+} finally {
1062+resetAgentEventsForTest();
1063+vi.useRealTimers();
1064+}
1065+});
1066+8051067it("emits command output events for exec results", async () => {
8061068const { ctx, onAgentEvent } = createTestContext();
8071069@@ -1278,6 +1540,57 @@ describe("control UI credential redaction (issue #72283)", () => {
12781540expect(result?.details?.aggregated).toContain("-tail");
12791541});
128015421543+it("parses exec approval resolution from raw output even when live output is capped", async () => {
1544+const { ctx, onAgentEvent } = createTestContext();
1545+const aggregated = `exec denied (user-denied): blocked by reviewer\n${"x".repeat(
1546+ 90 * 1024,
1547+ )}-tail`;
1548+1549+await handleToolExecutionStart(
1550+ctx as never,
1551+{
1552+type: "tool_execution_start",
1553+toolName: "exec",
1554+toolCallId: "tool-exec-denied-long-output",
1555+args: { command: "rm -rf /tmp/example" },
1556+} as never,
1557+);
1558+1559+await handleToolExecutionEnd(
1560+ctx as never,
1561+{
1562+type: "tool_execution_end",
1563+toolName: "exec",
1564+toolCallId: "tool-exec-denied-long-output",
1565+isError: true,
1566+result: {
1567+details: {
1568+status: "failed",
1569+ aggregated,
1570+exitCode: 1,
1571+},
1572+},
1573+} as never,
1574+);
1575+1576+const commandOutput = onAgentEvent.mock.calls
1577+.map((call) => call[0] as { stream?: string; data?: { output?: string } })
1578+.findLast((event) => event.stream === "command_output");
1579+expect(commandOutput?.data?.output).toContain("live command output truncated");
1580+expect(commandOutput?.data?.output).not.toContain("exec denied");
1581+1582+expect(onAgentEvent).toHaveBeenCalledWith(
1583+expect.objectContaining({
1584+stream: "approval",
1585+data: expect.objectContaining({
1586+phase: "resolved",
1587+status: "denied",
1588+message: expect.stringContaining("blocked by reviewer"),
1589+}),
1590+}),
1591+);
1592+});
1593+12811594it("redacts details-only results before emitting the tool result event", async () => {
12821595const events: Array<{ stream?: string; data?: Record<string, unknown> }> = [];
12831596registerAgentEventListener((evt) => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。