

























@@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
44import * as loggingConfigModule from "../logging/config.js";
55import {
66buildToolLifecycleErrorResult,
7+extractToolResultText,
78extractToolErrorCode,
89extractToolErrorMessage,
910isToolResultError,
@@ -27,6 +28,12 @@ describe("extractToolErrorMessage", () => {
2728it("keeps error-like status values", () => {
2829expect(extractToolErrorMessage({ details: { status: "failed" } })).toBe("failed");
2930expect(extractToolErrorMessage({ details: { status: "timeout" } })).toBe("timeout");
31+expect(
32+extractToolErrorMessage({
33+content: [{ type: "text", text: "Approval is unavailable." }],
34+details: { status: "approval-unavailable" },
35+}),
36+).toBe("Approval is unavailable.");
3037});
31383239it("prefers node-host aggregated denial text over generic failed status", () => {
@@ -388,3 +395,151 @@ describe("sanitizeToolArgs", () => {
388395});
389396});
390397});
398+399+describe("extractToolResultText", () => {
400+it("serializes structured non-image tool result blocks for visible output", () => {
401+const text = extractToolResultText({
402+content: [
403+{ type: "json", data: { status: "ok", value: 42 } },
404+{ type: "resource", resource: { uri: "file:///tmp/result.json", text: "payload" } },
405+],
406+});
407+408+expect(text).toContain('"type":"json"');
409+expect(text).toContain('"status":"ok"');
410+expect(text).toContain('"type":"resource"');
411+expect(text).not.toContain("see attached image");
412+});
413+414+it("normalizes top-level CLI result arrays and objects", () => {
415+expect(
416+extractToolResultText([
417+{ type: "web_search_result", title: "OpenClaw", url: "https://example.com" },
418+]),
419+).toContain('"title":"OpenClaw"');
420+expect(extractToolResultText([{ type: "text", text: "hello" }])).toBe("hello");
421+expect(
422+extractToolResultText({ type: "web_search_tool_result_error", error_code: "unavailable" }),
423+).toContain('"error_code":"unavailable"');
424+expect(
425+extractToolResultText({
426+type: "code_execution_result",
427+content: [],
428+return_code: 0,
429+stderr: "",
430+stdout: "command output",
431+}),
432+).toContain('"stdout":"command output"');
433+});
434+435+it("keeps existing text blocks and skips image blocks", () => {
436+const text = extractToolResultText({
437+content: [
438+{ type: "text", text: "hello" },
439+{ type: "image", data: "abc", mimeType: "image/png" },
440+],
441+});
442+443+expect(text).toBe("hello");
444+});
445+446+it("keeps existing text output before structured fallback", () => {
447+const text = extractToolResultText({
448+content: [
449+{ type: "text", text: "hello" },
450+{ type: "json", data: { status: "ok" } },
451+],
452+});
453+454+expect(text).toBe("hello");
455+});
456+457+it("caps top-level text arrays", () => {
458+const text = extractToolResultText([{ type: "text", text: "x".repeat(9000) }]);
459+460+expect(text).toContain("…(truncated)…");
461+expect(text?.length).toBeLessThanOrEqual(8020);
462+});
463+464+it("redacts whole data URI values without rewriting ordinary data substrings", () => {
465+const text = extractToolResultText({
466+content: [
467+{
468+type: "json",
469+note: "metadata:foo",
470+uri: "data:text/plain;base64,abcdefghijklmnopqrstuvwxyz0123456789",
471+},
472+],
473+});
474+475+expect(text).toContain('"note":"metadata:foo"');
476+expect(text).toContain('"uri":"[inline data URI:');
477+expect(text).not.toContain("abcdefghijklmnopqrstuvwxyz0123456789");
478+});
479+480+it("suppresses MCP binary fields and structured secrets", () => {
481+const text = extractToolResultText({
482+content: [
483+{ type: "audio", data: "audio-base64-secret", mimeType: "audio/mpeg" },
484+{
485+type: "document",
486+source: {
487+type: "base64",
488+media_type: "application/pdf",
489+data: "document-base64-secret",
490+},
491+},
492+{
493+type: "resource",
494+apiKey: "sk-structured-secret-1234567890",
495+resource: {
496+uri: "blob://result",
497+blob: "resource-base64-secret",
498+mimeType: "application/pdf",
499+},
500+},
501+],
502+});
503+504+expect(text).toContain('"uri":"blob://result"');
505+expect(text).toContain('"blob":"[binary omitted:');
506+expect(text).not.toContain("audio-base64-secret");
507+expect(text).not.toContain("document-base64-secret");
508+expect(text).not.toContain("resource-base64-secret");
509+expect(text).not.toContain("sk-structured-secret-1234567890");
510+});
511+512+it("redacts structured headers and omits opaque CLI payloads before the output cap", () => {
513+const text = extractToolResultText([
514+{
515+type: "web_search_result",
516+encrypted_content: "opaque-search-ciphertext".repeat(500),
517+encrypted_stdout: "opaque-command-ciphertext".repeat(500),
518+apiKey: ["array-valued-api-secret"],
519+headers: {
520+cookie: ["session=structured-cookie-secret"],
521+"set-cookie": ["sid=structured-set-cookie-secret; HttpOnly"],
522+},
523+title: "Useful result",
524+},
525+]);
526+527+expect(text).toContain('"encrypted_content":"[opaque data omitted:');
528+expect(text).toContain('"encrypted_stdout":"[opaque data omitted:');
529+expect(text).toContain('"title":"Useful result"');
530+expect(text).not.toContain("opaque-search-ciphertext");
531+expect(text).not.toContain("opaque-command-ciphertext");
532+expect(text).not.toContain("array-valued-api-secret");
533+expect(text).not.toContain("structured-cookie-secret");
534+expect(text).not.toContain("structured-set-cookie-secret");
535+});
536+537+it("caps structured fallback output", () => {
538+const text = extractToolResultText({
539+content: [{ type: "json", data: "x".repeat(9000) }],
540+});
541+542+expect(text).toContain("…(truncated)…");
543+expect(text?.length).toBeLessThanOrEqual(8020);
544+});
545+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。