|
| 1 | +import { describe, expect, test } from "bun:test"; |
| 2 | +import { createSizeCapTransform } from "@intx/inference"; |
| 3 | +import type { StrategyContext, ToolResult } from "@intx/types/runtime"; |
| 4 | +import { MAX_RESULT_CHARS, truncateToolResultContent } from "./result-truncation-plugin.js"; |
| 5 | + |
| 6 | +describe("truncateToolResultContent", () => { |
| 7 | + test("within-cap content passes through unchanged", () => { |
| 8 | + const content = "x".repeat(100); |
| 9 | + expect(truncateToolResultContent(content)).toBe(content); |
| 10 | + }); |
| 11 | + |
| 12 | + test("oversized content gets a marker that never promises retrievable remainder", () => { |
| 13 | + const content = "x".repeat(MAX_RESULT_CHARS + 500); |
| 14 | + const truncated = truncateToolResultContent(content); |
| 15 | + |
| 16 | + expect(truncated).toContain("[output truncated"); |
| 17 | + expect(truncated).toContain("NOT retrievable"); |
| 18 | + // The pre-cap discard must never be described as recoverable elsewhere. |
| 19 | + expect(truncated).not.toContain("see the rest"); |
| 20 | + expect(truncated).not.toContain("Full output available"); |
| 21 | + }); |
| 22 | + |
| 23 | + test("truncation marker survives the size-cap blob spill", async () => { |
| 24 | + // Reproduce the production pipeline for an output over MAX_RESULT_CHARS: |
| 25 | + // truncation runs first (at the tool), size-cap spills the already-cut |
| 26 | + // text to a blob and tells the model the blob holds the full output. The |
| 27 | + // blob's tail must therefore carry the honest "discarded, NOT retrievable" |
| 28 | + // marker so the model does not loop re-running the command. |
| 29 | + const original = "x".repeat(MAX_RESULT_CHARS + 500); |
| 30 | + const truncated = truncateToolResultContent(original); |
| 31 | + |
| 32 | + const blobs = new Map<string, string>(); |
| 33 | + const transform = createSizeCapTransform({ |
| 34 | + maxChars: 10_000, |
| 35 | + contextStore: { |
| 36 | + writeBlob: async (key: string, bytes: Uint8Array) => { |
| 37 | + blobs.set(key, new TextDecoder().decode(bytes)); |
| 38 | + }, |
| 39 | + }, |
| 40 | + }); |
| 41 | + |
| 42 | + const result: ToolResult = { |
| 43 | + callId: "call-1", |
| 44 | + content: truncated, |
| 45 | + isError: false, |
| 46 | + }; |
| 47 | + const { output } = await transform.apply( |
| 48 | + { call: { id: "call-1", name: "run_shell", arguments: {} }, result }, |
| 49 | + {} as StrategyContext, |
| 50 | + ); |
| 51 | + |
| 52 | + const spilled = blobs.get("call-1"); |
| 53 | + expect(spilled).toBe(truncated); |
| 54 | + // The blob's tail tells the truth about the pre-spill discard. |
| 55 | + expect(spilled).toContain("NOT retrievable"); |
| 56 | + expect(spilled?.endsWith("Use offset/limit or a narrower query.]")).toBe(true); |
| 57 | + // The inline marker's blob promise is now genuine: the blob really does |
| 58 | + // hold everything that still exists. |
| 59 | + expect(output.content).toContain("tool-output:///call-1"); |
| 60 | + }); |
| 61 | +}); |
0 commit comments