co-mono/packages/coding-agent/test/tools.test.ts
Mario Zechner 84dcab219b Add image support in tool results across all providers
Tool results now use content blocks and can include both text and images.
All providers (Anthropic, Google, OpenAI Completions, OpenAI Responses)
correctly pass images from tool results to LLMs.

- Update ToolResultMessage type to use content blocks
- Add placeholder text for image-only tool results in Google/Anthropic
- OpenAI providers send tool result + follow-up user message with images
- Fix Anthropic JSON parsing for empty tool arguments
- Add comprehensive tests for image-only and text+image tool results
- Update README with tool result content blocks API
2025-11-12 10:45:56 +01:00

143 lines
4.3 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", async () => {
const testFile = join(testDir, "test.txt");
const content = "Hello, world!";
writeFileSync(testFile, content);
const result = await readTool.execute("test-call-1", { path: testFile });
expect(getTextOutput(result)).toBe(content);
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");
});
});
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);
});
});