co-mono/packages/coding-agent/test/tools.test.ts
Mario Zechner 2f0f0a913e Add proper truncation notices and comprehensive tests for read tool
**Improved output messages:**
1. File fits within limits: Just outputs content (no notices)
2. Lines get truncated: Shows "Some lines were truncated to 2000 characters for display"
3. File doesn't fit limit: Shows "N more lines not shown. Use offset=X to continue reading"
4. Offset beyond file: Shows "Error: Offset X is beyond end of file (N lines total)"
5. Both truncations: Combines both notices with ". " separator

**Comprehensive test coverage:**
- Files within limits (no notices)
- Large files (line truncation)
- Long lines (character truncation)
- Offset parameter
- Limit parameter
- Offset + limit together
- Invalid offset (out of bounds)
- Combined truncations (both notices)

All 17 tests passing ✓
2025-11-12 17:13:03 +01:00

247 lines
8.4 KiB
TypeScript

import { mkdirSync, rmSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { bashTool } from "../src/tools/bash.js";
import { editTool } from "../src/tools/edit.js";
import { readTool } from "../src/tools/read.js";
import { writeTool } from "../src/tools/write.js";
// Helper to extract text from content blocks
function getTextOutput(result: any): string {
return (
result.content
?.filter((c: any) => c.type === "text")
.map((c: any) => c.text)
.join("\n") || ""
);
}
describe("Coding Agent Tools", () => {
let testDir: string;
beforeEach(() => {
// Create a unique temporary directory for each test
testDir = join(tmpdir(), `coding-agent-test-${Date.now()}`);
mkdirSync(testDir, { recursive: true });
});
afterEach(() => {
// Clean up test directory
rmSync(testDir, { recursive: true, force: true });
});
describe("read tool", () => {
it("should read file contents that fit within limits", async () => {
const testFile = join(testDir, "test.txt");
const content = "Hello, world!\nLine 2\nLine 3";
writeFileSync(testFile, content);
const result = await readTool.execute("test-call-1", { path: testFile });
expect(getTextOutput(result)).toBe(content);
expect(getTextOutput(result)).not.toContain("more lines not shown");
expect(result.details).toBeUndefined();
});
it("should handle non-existent files", async () => {
const testFile = join(testDir, "nonexistent.txt");
const result = await readTool.execute("test-call-2", { path: testFile });
expect(getTextOutput(result)).toContain("Error");
expect(getTextOutput(result)).toContain("File not found");
});
it("should truncate files exceeding line limit", async () => {
const testFile = join(testDir, "large.txt");
const lines = Array.from({ length: 2500 }, (_, i) => `Line ${i + 1}`);
writeFileSync(testFile, lines.join("\n"));
const result = await readTool.execute("test-call-3", { path: testFile });
const output = getTextOutput(result);
expect(output).toContain("Line 1");
expect(output).toContain("Line 2000");
expect(output).not.toContain("Line 2001");
expect(output).toContain("500 more lines not shown");
expect(output).toContain("Use offset=2001 to continue reading");
});
it("should truncate long lines and show notice", async () => {
const testFile = join(testDir, "long-lines.txt");
const longLine = "a".repeat(3000);
const content = `Short line\n${longLine}\nAnother short line`;
writeFileSync(testFile, content);
const result = await readTool.execute("test-call-4", { path: testFile });
const output = getTextOutput(result);
expect(output).toContain("Short line");
expect(output).toContain("Another short line");
expect(output).toContain("Some lines were truncated to 2000 characters");
expect(output.split("\n")[1].length).toBe(2000);
});
it("should handle offset parameter", async () => {
const testFile = join(testDir, "offset-test.txt");
const lines = Array.from({ length: 100 }, (_, i) => `Line ${i + 1}`);
writeFileSync(testFile, lines.join("\n"));
const result = await readTool.execute("test-call-5", { path: testFile, offset: 51 });
const output = getTextOutput(result);
expect(output).not.toContain("Line 50");
expect(output).toContain("Line 51");
expect(output).toContain("Line 100");
expect(output).not.toContain("more lines not shown");
});
it("should handle limit parameter", async () => {
const testFile = join(testDir, "limit-test.txt");
const lines = Array.from({ length: 100 }, (_, i) => `Line ${i + 1}`);
writeFileSync(testFile, lines.join("\n"));
const result = await readTool.execute("test-call-6", { path: testFile, limit: 10 });
const output = getTextOutput(result);
expect(output).toContain("Line 1");
expect(output).toContain("Line 10");
expect(output).not.toContain("Line 11");
expect(output).toContain("90 more lines not shown");
expect(output).toContain("Use offset=11 to continue reading");
});
it("should handle offset + limit together", async () => {
const testFile = join(testDir, "offset-limit-test.txt");
const lines = Array.from({ length: 100 }, (_, i) => `Line ${i + 1}`);
writeFileSync(testFile, lines.join("\n"));
const result = await readTool.execute("test-call-7", {
path: testFile,
offset: 41,
limit: 20,
});
const output = getTextOutput(result);
expect(output).not.toContain("Line 40");
expect(output).toContain("Line 41");
expect(output).toContain("Line 60");
expect(output).not.toContain("Line 61");
expect(output).toContain("40 more lines not shown");
expect(output).toContain("Use offset=61 to continue reading");
});
it("should show error when offset is beyond file length", async () => {
const testFile = join(testDir, "short.txt");
writeFileSync(testFile, "Line 1\nLine 2\nLine 3");
const result = await readTool.execute("test-call-8", { path: testFile, offset: 100 });
const output = getTextOutput(result);
expect(output).toContain("Error: Offset 100 is beyond end of file");
expect(output).toContain("3 lines total");
});
it("should show both truncation notices when applicable", async () => {
const testFile = join(testDir, "both-truncations.txt");
const longLine = "b".repeat(3000);
const lines = Array.from({ length: 2500 }, (_, i) => (i === 500 ? longLine : `Line ${i + 1}`));
writeFileSync(testFile, lines.join("\n"));
const result = await readTool.execute("test-call-9", { path: testFile });
const output = getTextOutput(result);
expect(output).toContain("Some lines were truncated to 2000 characters");
expect(output).toContain("500 more lines not shown");
});
});
describe("write tool", () => {
it("should write file contents", async () => {
const testFile = join(testDir, "write-test.txt");
const content = "Test content";
const result = await writeTool.execute("test-call-3", { path: testFile, content });
expect(getTextOutput(result)).toContain("Successfully wrote");
expect(getTextOutput(result)).toContain(testFile);
expect(result.details).toBeUndefined();
});
it("should create parent directories", async () => {
const testFile = join(testDir, "nested", "dir", "test.txt");
const content = "Nested content";
const result = await writeTool.execute("test-call-4", { path: testFile, content });
expect(getTextOutput(result)).toContain("Successfully wrote");
});
});
describe("edit tool", () => {
it("should replace text in file", async () => {
const testFile = join(testDir, "edit-test.txt");
const originalContent = "Hello, world!";
writeFileSync(testFile, originalContent);
const result = await editTool.execute("test-call-5", {
path: testFile,
oldText: "world",
newText: "testing",
});
expect(getTextOutput(result)).toContain("Successfully replaced");
expect(result.details).toBeUndefined();
});
it("should fail if text not found", async () => {
const testFile = join(testDir, "edit-test.txt");
const originalContent = "Hello, world!";
writeFileSync(testFile, originalContent);
const result = await editTool.execute("test-call-6", {
path: testFile,
oldText: "nonexistent",
newText: "testing",
});
expect(getTextOutput(result)).toContain("Could not find the exact text");
});
it("should fail if text appears multiple times", async () => {
const testFile = join(testDir, "edit-test.txt");
const originalContent = "foo foo foo";
writeFileSync(testFile, originalContent);
const result = await editTool.execute("test-call-7", {
path: testFile,
oldText: "foo",
newText: "bar",
});
expect(getTextOutput(result)).toContain("Found 3 occurrences");
});
});
describe("bash tool", () => {
it("should execute simple commands", async () => {
const result = await bashTool.execute("test-call-8", { command: "echo 'test output'" });
expect(getTextOutput(result)).toContain("test output");
expect(result.details).toBeUndefined();
});
it("should handle command errors", async () => {
const result = await bashTool.execute("test-call-9", { command: "exit 1" });
expect(getTextOutput(result)).toContain("Command failed");
});
it("should respect timeout", async () => {
const result = await bashTool.execute("test-call-10", { command: "sleep 35" });
expect(getTextOutput(result)).toContain("Command failed");
}, 35000);
});
});