test(tools): add unit tests for glob and param-helpers
Add tests for: - Glob tool pattern matching, limits, and ignore patterns - Parameter helpers for string/number parsing and JSON results Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5353161b1d
commit
44d0cef838
2 changed files with 456 additions and 0 deletions
218
src/agent/tools/glob.test.ts
Normal file
218
src/agent/tools/glob.test.ts
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { createGlobTool, type GlobResult } from "./glob.js";
|
||||
|
||||
describe("glob", () => {
|
||||
const testDir = join(tmpdir(), `multica-glob-test-${Date.now()}`);
|
||||
|
||||
beforeEach(() => {
|
||||
if (existsSync(testDir)) {
|
||||
rmSync(testDir, { recursive: true });
|
||||
}
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
|
||||
// Create test file structure
|
||||
mkdirSync(join(testDir, "src"), { recursive: true });
|
||||
mkdirSync(join(testDir, "src/components"), { recursive: true });
|
||||
mkdirSync(join(testDir, "test"), { recursive: true });
|
||||
|
||||
writeFileSync(join(testDir, "package.json"), "{}");
|
||||
writeFileSync(join(testDir, "src/index.ts"), "export {}");
|
||||
writeFileSync(join(testDir, "src/utils.ts"), "export {}");
|
||||
writeFileSync(join(testDir, "src/components/Button.tsx"), "export {}");
|
||||
writeFileSync(join(testDir, "src/components/Input.tsx"), "export {}");
|
||||
writeFileSync(join(testDir, "test/index.test.ts"), "test()");
|
||||
writeFileSync(join(testDir, ".config"), "hidden");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (existsSync(testDir)) {
|
||||
rmSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("createGlobTool", () => {
|
||||
it("should create a glob tool with correct properties", () => {
|
||||
const tool = createGlobTool(testDir);
|
||||
|
||||
expect(tool.name).toBe("glob");
|
||||
expect(tool.label).toBe("Glob");
|
||||
expect(tool.description).toContain("Find files matching a glob pattern");
|
||||
expect(tool.execute).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
it("should find files matching simple pattern", async () => {
|
||||
const tool = createGlobTool(testDir);
|
||||
const result = await tool.execute("test-id", { pattern: "*.json" }, new AbortController().signal);
|
||||
|
||||
expect(result.details.files).toContain("package.json");
|
||||
expect(result.details.count).toBe(1);
|
||||
expect(result.details.truncated).toBe(false);
|
||||
});
|
||||
|
||||
it("should find TypeScript files recursively", async () => {
|
||||
const tool = createGlobTool(testDir);
|
||||
const result = await tool.execute("test-id", { pattern: "**/*.ts" }, new AbortController().signal);
|
||||
|
||||
expect(result.details.files).toContain("src/index.ts");
|
||||
expect(result.details.files).toContain("src/utils.ts");
|
||||
expect(result.details.files).toContain("test/index.test.ts");
|
||||
expect(result.details.count).toBe(3);
|
||||
});
|
||||
|
||||
it("should find TSX files in specific directory", async () => {
|
||||
const tool = createGlobTool(testDir);
|
||||
const result = await tool.execute(
|
||||
"test-id",
|
||||
{ pattern: "src/components/*.tsx" },
|
||||
new AbortController().signal
|
||||
);
|
||||
|
||||
expect(result.details.files).toContain("src/components/Button.tsx");
|
||||
expect(result.details.files).toContain("src/components/Input.tsx");
|
||||
expect(result.details.count).toBe(2);
|
||||
});
|
||||
|
||||
it("should include dotfiles", async () => {
|
||||
const tool = createGlobTool(testDir);
|
||||
const result = await tool.execute("test-id", { pattern: ".*" }, new AbortController().signal);
|
||||
|
||||
expect(result.details.files).toContain(".config");
|
||||
});
|
||||
|
||||
it("should respect limit parameter", async () => {
|
||||
const tool = createGlobTool(testDir);
|
||||
const result = await tool.execute(
|
||||
"test-id",
|
||||
{ pattern: "**/*", limit: 2 },
|
||||
new AbortController().signal
|
||||
);
|
||||
|
||||
expect(result.details.count).toBe(2);
|
||||
expect(result.details.truncated).toBe(true);
|
||||
});
|
||||
|
||||
it("should respect ignore patterns", async () => {
|
||||
const tool = createGlobTool(testDir);
|
||||
const result = await tool.execute(
|
||||
"test-id",
|
||||
{ pattern: "**/*.ts", ignore: ["test/**"] },
|
||||
new AbortController().signal
|
||||
);
|
||||
|
||||
expect(result.details.files).toContain("src/index.ts");
|
||||
expect(result.details.files).not.toContain("test/index.test.ts");
|
||||
});
|
||||
|
||||
it("should use custom cwd", async () => {
|
||||
const tool = createGlobTool("/other/path");
|
||||
const result = await tool.execute(
|
||||
"test-id",
|
||||
{ pattern: "*.ts", cwd: join(testDir, "src") },
|
||||
new AbortController().signal
|
||||
);
|
||||
|
||||
expect(result.details.files).toContain("index.ts");
|
||||
expect(result.details.files).toContain("utils.ts");
|
||||
});
|
||||
|
||||
it("should throw error for empty pattern", async () => {
|
||||
const tool = createGlobTool(testDir);
|
||||
|
||||
await expect(
|
||||
tool.execute("test-id", { pattern: "" }, new AbortController().signal)
|
||||
).rejects.toThrow("Pattern must not be empty");
|
||||
});
|
||||
|
||||
it("should throw error for whitespace-only pattern", async () => {
|
||||
const tool = createGlobTool(testDir);
|
||||
|
||||
await expect(
|
||||
tool.execute("test-id", { pattern: " " }, new AbortController().signal)
|
||||
).rejects.toThrow("Pattern must not be empty");
|
||||
});
|
||||
|
||||
it("should throw error for non-existent directory", async () => {
|
||||
const tool = createGlobTool(testDir);
|
||||
|
||||
await expect(
|
||||
tool.execute("test-id", { pattern: "*.ts", cwd: "/non/existent/path" }, new AbortController().signal)
|
||||
).rejects.toThrow("Directory not found");
|
||||
});
|
||||
|
||||
it("should throw error when cwd is a file", async () => {
|
||||
const tool = createGlobTool(testDir);
|
||||
const filePath = join(testDir, "package.json");
|
||||
|
||||
await expect(
|
||||
tool.execute("test-id", { pattern: "*.ts", cwd: filePath }, new AbortController().signal)
|
||||
).rejects.toThrow("Path is not a directory");
|
||||
});
|
||||
|
||||
it("should return message when no files match", async () => {
|
||||
const tool = createGlobTool(testDir);
|
||||
const result = await tool.execute(
|
||||
"test-id",
|
||||
{ pattern: "**/*.xyz" },
|
||||
new AbortController().signal
|
||||
);
|
||||
|
||||
expect(result.details.count).toBe(0);
|
||||
expect(result.details.files).toHaveLength(0);
|
||||
expect(result.content[0].text).toContain("No files found");
|
||||
});
|
||||
|
||||
it("should sort files by modification time (most recent first)", async () => {
|
||||
// Create files with different modification times
|
||||
const laterFile = join(testDir, "later.ts");
|
||||
writeFileSync(laterFile, "// created later");
|
||||
|
||||
// Wait a bit to ensure different mtime
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const latestFile = join(testDir, "latest.ts");
|
||||
writeFileSync(latestFile, "// created latest");
|
||||
|
||||
const tool = createGlobTool(testDir);
|
||||
const result = await tool.execute(
|
||||
"test-id",
|
||||
{ pattern: "*.ts" },
|
||||
new AbortController().signal
|
||||
);
|
||||
|
||||
// The latest file should be first
|
||||
expect(result.details.files[0]).toBe("latest.ts");
|
||||
});
|
||||
|
||||
it("should use default limit of 100", async () => {
|
||||
// Create more than 100 files
|
||||
for (let i = 0; i < 110; i++) {
|
||||
writeFileSync(join(testDir, `file${i}.txt`), "content");
|
||||
}
|
||||
|
||||
const tool = createGlobTool(testDir);
|
||||
const result = await tool.execute(
|
||||
"test-id",
|
||||
{ pattern: "*.txt" },
|
||||
new AbortController().signal
|
||||
);
|
||||
|
||||
expect(result.details.count).toBe(100);
|
||||
expect(result.details.truncated).toBe(true);
|
||||
});
|
||||
|
||||
it("should limit to max 1000 files", async () => {
|
||||
const tool = createGlobTool(testDir);
|
||||
const result = await tool.execute(
|
||||
"test-id",
|
||||
{ pattern: "**/*", limit: 5000 },
|
||||
new AbortController().signal
|
||||
);
|
||||
|
||||
// The limit should be capped at 1000
|
||||
expect(result.details.count).toBeLessThanOrEqual(1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
238
src/agent/tools/web/param-helpers.test.ts
Normal file
238
src/agent/tools/web/param-helpers.test.ts
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { readStringParam, readNumberParam, jsonResult } from "./param-helpers.js";
|
||||
|
||||
describe("param-helpers", () => {
|
||||
describe("readStringParam", () => {
|
||||
it("should return string value when present", () => {
|
||||
const params = { name: "test" };
|
||||
const result = readStringParam(params, "name");
|
||||
expect(result).toBe("test");
|
||||
});
|
||||
|
||||
it("should trim whitespace by default", () => {
|
||||
const params = { name: " test " };
|
||||
const result = readStringParam(params, "name");
|
||||
expect(result).toBe("test");
|
||||
});
|
||||
|
||||
it("should not trim when trim is false", () => {
|
||||
const params = { name: " test " };
|
||||
const result = readStringParam(params, "name", { trim: false });
|
||||
expect(result).toBe(" test ");
|
||||
});
|
||||
|
||||
it("should return undefined for missing key", () => {
|
||||
const params = {};
|
||||
const result = readStringParam(params, "name");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined for non-string value", () => {
|
||||
const params = { name: 123 };
|
||||
const result = readStringParam(params, "name");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should throw when required and missing", () => {
|
||||
const params = {};
|
||||
expect(() => readStringParam(params, "name", { required: true })).toThrow("name required");
|
||||
});
|
||||
|
||||
it("should throw when required and not a string", () => {
|
||||
const params = { name: null };
|
||||
expect(() => readStringParam(params, "name", { required: true })).toThrow("name required");
|
||||
});
|
||||
|
||||
it("should use custom label in error message", () => {
|
||||
const params = {};
|
||||
expect(() =>
|
||||
readStringParam(params, "name", { required: true, label: "Username" })
|
||||
).toThrow("Username required");
|
||||
});
|
||||
|
||||
it("should return undefined for empty string when not allowEmpty", () => {
|
||||
const params = { name: "" };
|
||||
const result = readStringParam(params, "name");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return empty string when allowEmpty is true", () => {
|
||||
const params = { name: "" };
|
||||
const result = readStringParam(params, "name", { allowEmpty: true });
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("should return undefined for whitespace-only when trimmed", () => {
|
||||
const params = { name: " " };
|
||||
const result = readStringParam(params, "name");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should throw for required empty string", () => {
|
||||
const params = { name: "" };
|
||||
expect(() => readStringParam(params, "name", { required: true })).toThrow("name required");
|
||||
});
|
||||
});
|
||||
|
||||
describe("readNumberParam", () => {
|
||||
it("should return number value when present", () => {
|
||||
const params = { count: 42 };
|
||||
const result = readNumberParam(params, "count");
|
||||
expect(result).toBe(42);
|
||||
});
|
||||
|
||||
it("should parse string numbers", () => {
|
||||
const params = { count: "42" };
|
||||
const result = readNumberParam(params, "count");
|
||||
expect(result).toBe(42);
|
||||
});
|
||||
|
||||
it("should parse float numbers from string", () => {
|
||||
const params = { value: "3.14" };
|
||||
const result = readNumberParam(params, "value");
|
||||
expect(result).toBe(3.14);
|
||||
});
|
||||
|
||||
it("should return undefined for missing key", () => {
|
||||
const params = {};
|
||||
const result = readNumberParam(params, "count");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined for non-numeric string", () => {
|
||||
const params = { count: "abc" };
|
||||
const result = readNumberParam(params, "count");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined for NaN", () => {
|
||||
const params = { count: NaN };
|
||||
const result = readNumberParam(params, "count");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined for Infinity", () => {
|
||||
const params = { count: Infinity };
|
||||
const result = readNumberParam(params, "count");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should throw when required and missing", () => {
|
||||
const params = {};
|
||||
expect(() => readNumberParam(params, "count", { required: true })).toThrow("count required");
|
||||
});
|
||||
|
||||
it("should throw when required and invalid", () => {
|
||||
const params = { count: "invalid" };
|
||||
expect(() => readNumberParam(params, "count", { required: true })).toThrow("count required");
|
||||
});
|
||||
|
||||
it("should use custom label in error message", () => {
|
||||
const params = {};
|
||||
expect(() =>
|
||||
readNumberParam(params, "count", { required: true, label: "Item Count" })
|
||||
).toThrow("Item Count required");
|
||||
});
|
||||
|
||||
it("should truncate to integer when integer option is true", () => {
|
||||
const params = { count: 3.9 };
|
||||
const result = readNumberParam(params, "count", { integer: true });
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
|
||||
it("should truncate negative float to integer", () => {
|
||||
const params = { count: -3.9 };
|
||||
const result = readNumberParam(params, "count", { integer: true });
|
||||
expect(result).toBe(-3);
|
||||
});
|
||||
|
||||
it("should handle empty string", () => {
|
||||
const params = { count: "" };
|
||||
const result = readNumberParam(params, "count");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle whitespace-only string", () => {
|
||||
const params = { count: " " };
|
||||
const result = readNumberParam(params, "count");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should trim string before parsing", () => {
|
||||
const params = { count: " 42 " };
|
||||
const result = readNumberParam(params, "count");
|
||||
expect(result).toBe(42);
|
||||
});
|
||||
|
||||
it("should return undefined for object value", () => {
|
||||
const params = { count: { value: 42 } };
|
||||
const result = readNumberParam(params, "count");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined for array value", () => {
|
||||
const params = { count: [1, 2, 3] };
|
||||
const result = readNumberParam(params, "count");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle zero correctly", () => {
|
||||
const params = { count: 0 };
|
||||
const result = readNumberParam(params, "count");
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle negative numbers", () => {
|
||||
const params = { count: -10 };
|
||||
const result = readNumberParam(params, "count");
|
||||
expect(result).toBe(-10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("jsonResult", () => {
|
||||
it("should return formatted JSON result", () => {
|
||||
const payload = { name: "test", value: 42 };
|
||||
const result = jsonResult(payload);
|
||||
|
||||
expect(result.content).toHaveLength(1);
|
||||
expect(result.content[0].type).toBe("text");
|
||||
expect(result.content[0].text).toBe(JSON.stringify(payload, null, 2));
|
||||
expect(result.details).toBe(payload);
|
||||
});
|
||||
|
||||
it("should handle array payload", () => {
|
||||
const payload = [1, 2, 3];
|
||||
const result = jsonResult(payload);
|
||||
|
||||
expect(result.content[0].text).toBe(JSON.stringify(payload, null, 2));
|
||||
expect(result.details).toBe(payload);
|
||||
});
|
||||
|
||||
it("should handle string payload", () => {
|
||||
const payload = "simple string";
|
||||
const result = jsonResult(payload);
|
||||
|
||||
expect(result.content[0].text).toBe('"simple string"');
|
||||
expect(result.details).toBe(payload);
|
||||
});
|
||||
|
||||
it("should handle null payload", () => {
|
||||
const result = jsonResult(null);
|
||||
|
||||
expect(result.content[0].text).toBe("null");
|
||||
expect(result.details).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle nested objects", () => {
|
||||
const payload = {
|
||||
user: { name: "test", settings: { theme: "dark" } },
|
||||
items: [1, 2, 3],
|
||||
};
|
||||
const result = jsonResult(payload);
|
||||
|
||||
expect(result.content[0].text).toContain("user");
|
||||
expect(result.content[0].text).toContain("settings");
|
||||
expect(result.details).toBe(payload);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue