Files
letta-code/src/tests/tools/tool-truncation.test.ts
Shubham Naik 14e67fa156 chore: multiline traversal support (#51)
Co-authored-by: Shubham Naik <shub@memgpt.ai>
2025-11-01 10:00:04 -07:00

286 lines
9.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { bash } from "../../tools/impl/Bash";
import { bash_output } from "../../tools/impl/BashOutput";
import { glob } from "../../tools/impl/Glob";
import { grep } from "../../tools/impl/Grep";
import { ls } from "../../tools/impl/LS";
import { read } from "../../tools/impl/Read";
import { LIMITS } from "../../tools/impl/truncation";
describe("tool truncation integration tests", () => {
let testDir: string;
let originalUserCwd: string | undefined;
beforeEach(async () => {
// Create a temporary directory for tests
testDir = await mkdtemp(join(tmpdir(), "letta-test-"));
// Save and set USER_CWD so tools operate within the temp dir
originalUserCwd = process.env.USER_CWD;
process.env.USER_CWD = testDir;
});
afterEach(async () => {
// Restore USER_CWD before removing the temp dir to avoid leaving
// an invalid cwd for other tests that may run afterwards.
if (originalUserCwd === undefined) delete process.env.USER_CWD;
else process.env.USER_CWD = originalUserCwd;
// Clean up the temp directory
await rm(testDir, { recursive: true, force: true });
});
describe("Bash tool truncation", () => {
test.skipIf(process.platform === "win32")(
"truncates output exceeding 30K characters",
async () => {
// Generate output larger than 30K chars
const result = await bash({
command: `echo "${Array.from({ length: 1000 }, () => "x".repeat(50)).join("\\n")}"`,
});
const output = result.content[0]?.text || "";
expect(output).toContain("[Output truncated after 30,000 characters");
expect(output.length).toBeLessThan(35000); // Truncated + notice
},
);
test("does not truncate small output", async () => {
const result = await bash({ command: "echo 'Hello, world!'" });
const output = result.content[0]?.text || "";
expect(output).toContain("Hello, world!");
expect(output).not.toContain("truncated");
});
test.skipIf(process.platform === "win32")(
"truncates error output",
async () => {
// Generate large error output
const largeString = "e".repeat(40000);
const result = await bash({
command: `>&2 echo "${largeString}" && exit 1`,
});
const output = result.content[0]?.text || "";
expect(output).toContain("[Output truncated after 30,000 characters");
expect(result.isError).toBe(true);
},
);
});
describe("Read tool truncation", () => {
test("truncates file exceeding 2000 lines", async () => {
const filePath = join(testDir, "large-file.txt");
const lines = Array.from({ length: 3000 }, (_, i) => `Line ${i + 1}`);
await writeFile(filePath, lines.join("\n"));
const result = await read({ file_path: filePath });
expect(result.content).toContain("Line 1");
expect(result.content).toContain("Line 2000");
expect(result.content).not.toContain("Line 2001");
expect(result.content).toContain("showing lines 1-2000 of 3000 total");
});
test("truncates lines exceeding 2000 characters", async () => {
const filePath = join(testDir, "long-lines.txt");
const content = `short line\n${"a".repeat(3000)}\nshort line`;
await writeFile(filePath, content);
const result = await read({ file_path: filePath });
expect(result.content).toContain("short line");
expect(result.content).toContain("a".repeat(2000));
expect(result.content).not.toContain("a".repeat(2001));
expect(result.content).toContain("... [line truncated]");
expect(result.content).toContain(
"Some lines exceeded 2,000 characters and were truncated",
);
});
test("respects user-specified limit parameter", async () => {
const filePath = join(testDir, "file.txt");
const lines = Array.from({ length: 100 }, (_, i) => `Line ${i + 1}`);
await writeFile(filePath, lines.join("\n"));
const result = await read({ file_path: filePath, limit: 50 });
expect(result.content).toContain("Line 1");
expect(result.content).toContain("Line 50");
expect(result.content).not.toContain("Line 51");
// Should not show truncation notice when user explicitly set limit
expect(result.content).not.toContain("File truncated");
});
test("does not truncate small files", async () => {
const filePath = join(testDir, "small.txt");
await writeFile(filePath, "Hello\nWorld\n");
const result = await read({ file_path: filePath });
expect(result.content).toContain("Hello");
expect(result.content).toContain("World");
expect(result.content).not.toContain("truncated");
});
});
describe("Grep tool truncation", () => {
beforeEach(async () => {
// Create test files for grep
for (let i = 1; i <= 100; i++) {
await writeFile(join(testDir, `file${i}.txt`), `match\n`.repeat(100));
}
});
test("truncates content output exceeding 10K characters", async () => {
const result = await grep({
pattern: "match",
path: testDir,
output_mode: "content",
});
expect(result.output.length).toBeLessThanOrEqual(15000); // 10K + notice
expect(result.output).toContain(
"[Output truncated after 10,000 characters",
);
});
test("truncates file list exceeding 10K characters", async () => {
// Create files with long paths
for (let i = 1; i <= 1000; i++) {
await writeFile(
join(testDir, `very-long-filename-to-make-output-large-${i}.txt`),
"match",
);
}
const result = await grep({
pattern: "match",
path: testDir,
output_mode: "files_with_matches",
});
expect(result.output.length).toBeLessThanOrEqual(15000);
if (result.output.length > 10000) {
expect(result.output).toContain(
"[Output truncated after 10,000 characters",
);
}
});
test("does not truncate small results", async () => {
await writeFile(join(testDir, "single.txt"), "match\n");
const result = await grep({
pattern: "match",
path: join(testDir, "single.txt"),
output_mode: "content",
});
expect(result.output).toContain("match");
expect(result.output).not.toContain("truncated");
});
});
describe("Glob tool truncation", () => {
test("truncates file list exceeding 2000 files", async () => {
// Create 2500 files
for (let i = 1; i <= 2500; i++) {
await writeFile(join(testDir, `file${i}.txt`), "content");
}
const result = await glob({ pattern: "**/*.txt", path: testDir });
expect(result.files.length).toBeLessThanOrEqual(
LIMITS.GLOB_MAX_FILES + 1,
); // +1 for notice
expect(result.truncated).toBe(true);
expect(result.totalFiles).toBe(2500);
// Last entry should be the truncation notice
expect(result.files[result.files.length - 1]).toContain(
"showing 2,000 of 2,500 files",
);
});
test("does not truncate when under limit", async () => {
// Create 10 files
for (let i = 1; i <= 10; i++) {
await writeFile(join(testDir, `file${i}.txt`), "content");
}
const result = await glob({ pattern: "**/*.txt", path: testDir });
expect(result.files.length).toBe(10);
expect(result.truncated).toBeUndefined();
expect(result.totalFiles).toBeUndefined();
});
});
describe("LS tool truncation", () => {
test("truncates directory exceeding 1000 entries", async () => {
// Create 1500 files
for (let i = 1; i <= 1500; i++) {
await writeFile(join(testDir, `file${i}.txt`), "content");
}
const result = await ls({ path: testDir });
const output = result.content[0]?.text || "";
expect(output).toContain("[Output truncated");
expect(output).toContain("showing 1,000 of 1,500 entries");
expect(output).toContain("file1.txt");
// Should not contain files beyond 1000
const lines = output.split("\n");
// Count actual file entries (excluding headers and notices)
const fileEntries = lines.filter((line) => line.match(/^\s+- file/));
expect(fileEntries.length).toBeLessThanOrEqual(LIMITS.LS_MAX_ENTRIES);
});
test("does not truncate small directories", async () => {
// Create 5 files
for (let i = 1; i <= 5; i++) {
await writeFile(join(testDir, `file${i}.txt`), "content");
}
const result = await ls({ path: testDir });
const output = result.content[0]?.text || "";
expect(output).not.toContain("truncated");
expect(output).toContain("file1.txt");
expect(output).toContain("file5.txt");
});
});
describe("BashOutput tool truncation", () => {
test.skipIf(process.platform === "win32")(
"truncates accumulated output exceeding 30K characters",
async () => {
// Start a background process that generates lots of output
const startResult = await bash({
command: `for i in {1..1000}; do echo "$(printf 'x%.0s' {1..100})"; done`,
run_in_background: true,
});
const message = startResult.content[0]?.text || "";
const bashIdMatch = message.match(/with ID: (.+)/);
expect(bashIdMatch).toBeTruthy();
const bashId = bashIdMatch?.[1];
// Wait a bit for output to accumulate
await new Promise((resolve) => setTimeout(resolve, 100));
const outputResult = await bash_output({ bash_id: bashId });
expect(outputResult.message.length).toBeLessThan(35000); // 30K + notice
if (outputResult.message.length > 30000) {
expect(outputResult.message).toContain(
"[Output truncated after 30,000 characters",
);
}
},
);
});
});