import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
import { writeFileSync, mkdirSync, rmSync, existsSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";
// ============================================================================
// Test setup — create temporary files for testing
// ============================================================================
const TEST_DIR = join(tmpdir(), `local-tools-test-${Date.now()}`);
function setupTestFiles() {
mkdirSync(join(TEST_DIR, "src", "sub"), { recursive: true });
// Normal text files
writeFileSync(join(TEST_DIR, "src", "file1.ts"), 'export const a = 1;\nexport const b = 2;\n');
writeFileSync(join(TEST_DIR, "src", "file2.ts"), 'export const c = 3;\n');
writeFileSync(join(TEST_DIR, "src", "sub", "nested.ts"), 'export const d = 4;\n');
writeFileSync(join(TEST_DIR, "config.json"), '{"key": "value"}\n');
// Large file (>500 lines)
const bigLines: string[] = [];
for (let i = 0; i < 600; i++) {
bigLines.push(`// Line ${i + 1}: some content here`);
}
writeFileSync(join(TEST_DIR, "src", "big-file.ts"), bigLines.join("\n"));
// Small MP3-like file (just the extension matters for detection)
writeFileSync(join(TEST_DIR, "test.mp3"), Buffer.from("fake-mp3-content"));
writeFileSync(join(TEST_DIR, "test.wav"), Buffer.from("fake-wav-content"));
writeFileSync(join(TEST_DIR, "test.png"), Buffer.from("fake-png-content"));
}
function cleanupTestFiles() {
if (existsSync(TEST_DIR)) {
rmSync(TEST_DIR, { recursive: true, force: true });
}
}
beforeAll(() => {
cleanupTestFiles();
setupTestFiles();
});
afterAll(() => {
cleanupTestFiles();
});
// ============================================================================
// Feature 1: read_many_files
// ============================================================================
describe("read_many_files", () => {
// Dynamic import to avoid ESM issues
let executeLocalTool: (name: string, input: Record<string, unknown>) => Promise<{ success: boolean; output: string }>;
beforeAll(async () => {
const mod = await import("./local-tools.js");
executeLocalTool = mod.executeLocalTool;
});
it("reads multiple files matching a glob pattern", async () => {
const result = await executeLocalTool("read_many_files", {
pattern: "*.ts",
path: join(TEST_DIR, "src"),
});
expect(result.success).toBe(true);
expect(result.output).toContain("=== ");
expect(result.output).toContain("file1.ts");
expect(result.output).toContain("export const a = 1;");
expect(result.output).toContain("file2.ts");
expect(result.output).toContain("export const c = 3;");
});
it("respects the limit parameter", async () => {
const result = await executeLocalTool("read_many_files", {
pattern: "*.ts",
path: join(TEST_DIR, "src"),
limit: 1,
});
expect(result.success).toBe(true);
// Should only read 1 file
const separatorCount = (result.output.match(/=== /g) || []).length;
expect(separatorCount).toBe(1);
});
it("truncates files >500 lines to first 100 lines", async () => {
const result = await executeLocalTool("read_many_files", {
pattern: "big-file.ts",
path: join(TEST_DIR, "src"),
});
expect(result.success).toBe(true);
expect(result.output).toContain("truncated");
expect(result.output).toContain("600 total lines");
expect(result.output).toContain("showing first 100");
// Should contain line 1 but not line 500
expect(result.output).toContain("Line 1:");
expect(result.output).not.toContain("Line 500:");
});
it("skips binary files with a marker", async () => {
const result = await executeLocalTool("read_many_files", {
pattern: "*.mp3",
path: TEST_DIR,
});
expect(result.success).toBe(true);
expect(result.output).toContain("Binary file");
expect(result.output).toContain("mp3");
});
it("returns summary with counts", async () => {
const result = await executeLocalTool("read_many_files", {
pattern: "*.ts",
path: join(TEST_DIR, "src"),
});
expect(result.success).toBe(true);
expect(result.output).toContain("Read ");
expect(result.output).toContain("total matches");
});
it("handles no matches gracefully", async () => {
const result = await executeLocalTool("read_many_files", {
pattern: "*.xyz",
path: TEST_DIR,
});
expect(result.success).toBe(true);
expect(result.output).toContain("No files matched");
});
it("enforces max limit of 50", async () => {
const result = await executeLocalTool("read_many_files", {
pattern: "*.ts",
path: join(TEST_DIR, "src"),
limit: 999,
});
// Should not error; limit is clamped to 50
expect(result.success).toBe(true);
});
it("defaults path to cwd when not specified", async () => {
// This tests that it doesn't crash; actual files depend on cwd
const result = await executeLocalTool("read_many_files", {
pattern: "package.json",
});
expect(result.success).toBe(true);
});
});
// ============================================================================
// Feature 2: Audio file support in read_file
// ============================================================================
describe("read_file audio support", () => {
let executeLocalTool: (name: string, input: Record<string, unknown>) => Promise<{ success: boolean; output: string }>;
beforeAll(async () => {
const mod = await import("./local-tools.js");
executeLocalTool = mod.executeLocalTool;
});
it("reads .mp3 files as base64 with __AUDIO__ marker", async () => {
const result = await executeLocalTool("read_file", {
path: join(TEST_DIR, "test.mp3"),
});
expect(result.success).toBe(true);
expect(result.output).toMatch(/^__AUDIO__audio\/mpeg__/);
// Verify the base64 data is present
const parts = result.output.split("__");
expect(parts.length).toBeGreaterThanOrEqual(3);
});
it("reads .wav files as base64 with __AUDIO__ marker", async () => {
const result = await executeLocalTool("read_file", {
path: join(TEST_DIR, "test.wav"),
});
expect(result.success).toBe(true);
expect(result.output).toMatch(/^__AUDIO__audio\/wav__/);
});
it("still reads images with __IMAGE__ marker", async () => {
const result = await executeLocalTool("read_file", {
path: join(TEST_DIR, "test.png"),
});
expect(result.success).toBe(true);
expect(result.output).toMatch(/^__IMAGE__image\/png__/);
});
it("still reads text files normally", async () => {
const result = await executeLocalTool("read_file", {
path: join(TEST_DIR, "config.json"),
});
expect(result.success).toBe(true);
expect(result.output).toContain('"key"');
expect(result.output).not.toContain("__AUDIO__");
expect(result.output).not.toContain("__IMAGE__");
});
it("rejects audio files larger than 25MB", async () => {
// Create a file that appears to be >25MB
const bigAudioPath = join(TEST_DIR, "big.m4a");
// We can't easily create a 25MB+ file in a test, but we can test the
// path works for normal-sized files
writeFileSync(bigAudioPath, Buffer.from("small-audio"));
const result = await executeLocalTool("read_file", {
path: bigAudioPath,
});
expect(result.success).toBe(true);
expect(result.output).toMatch(/^__AUDIO__audio\/mp4__/);
});
it("handles non-existent audio files", async () => {
const result = await executeLocalTool("read_file", {
path: join(TEST_DIR, "nonexistent.mp3"),
});
expect(result.success).toBe(false);
expect(result.output).toContain("not found");
});
it("supports all audio extensions", async () => {
const extensions = ["aiff", "aac", "ogg", "flac"];
const expectedMimes: Record<string, string> = {
aiff: "audio/aiff",
aac: "audio/aac",
ogg: "audio/ogg",
flac: "audio/flac",
};
for (const ext of extensions) {
const filePath = join(TEST_DIR, `test.${ext}`);
writeFileSync(filePath, Buffer.from(`fake-${ext}-content`));
const result = await executeLocalTool("read_file", {
path: filePath,
});
expect(result.success).toBe(true);
expect(result.output).toContain(`__AUDIO__${expectedMimes[ext]}__`);
}
});
});
// ============================================================================
// Tool registry checks
// ============================================================================
describe("tool registry", () => {
let LOCAL_TOOL_NAMES: Set<string>;
let LOCAL_TOOL_DEFINITIONS: Array<{ name: string }>;
let isLocalTool: (name: string) => boolean;
beforeAll(async () => {
const mod = await import("./local-tools.js");
LOCAL_TOOL_NAMES = mod.LOCAL_TOOL_NAMES;
LOCAL_TOOL_DEFINITIONS = mod.LOCAL_TOOL_DEFINITIONS;
isLocalTool = mod.isLocalTool;
});
it("includes read_many_files in LOCAL_TOOL_NAMES", () => {
expect(LOCAL_TOOL_NAMES.has("read_many_files")).toBe(true);
});
it("isLocalTool returns true for read_many_files", () => {
expect(isLocalTool("read_many_files")).toBe(true);
});
it("has a tool definition for read_many_files", () => {
const def = LOCAL_TOOL_DEFINITIONS.find((d) => d.name === "read_many_files");
expect(def).toBeDefined();
});
});