branch:
assistant-tools.test.ts
10891 bytesRaw
import { env } from "cloudflare:workers";
import { describe, expect, it } from "vitest";
import { getAgentByName } from "agents";
async function freshAgent(name: string) {
return getAgentByName(env.TestAssistantToolsAgent, name);
}
// ── Read tool ─────────────────────────────────────────────────────────
describe("assistant tools — read", () => {
it("reads a file with line numbers", async () => {
const agent = await freshAgent("read-basic");
await agent.seed([{ path: "/hello.txt", content: "line1\nline2\nline3" }]);
const result = (await agent.toolRead("/hello.txt")) as {
path: string;
content: string;
totalLines: number;
};
expect(result.path).toBe("/hello.txt");
expect(result.totalLines).toBe(3);
expect(result.content).toContain("1\tline1");
expect(result.content).toContain("2\tline2");
expect(result.content).toContain("3\tline3");
});
it("returns error for missing file", async () => {
const agent = await freshAgent("read-missing");
const result = (await agent.toolRead("/nope.txt")) as { error: string };
expect(result.error).toContain("File not found");
});
it("returns error for directory", async () => {
const agent = await freshAgent("read-dir");
await agent.seedDir("/mydir");
const result = (await agent.toolRead("/mydir")) as { error: string };
expect(result.error).toContain("directory");
});
it("supports offset and limit", async () => {
const agent = await freshAgent("read-offset");
const lines = Array.from({ length: 10 }, (_, i) => `line${i + 1}`).join(
"\n"
);
await agent.seed([{ path: "/big.txt", content: lines }]);
const result = (await agent.toolRead("/big.txt", 3, 2)) as {
content: string;
fromLine: number;
toLine: number;
};
expect(result.fromLine).toBe(3);
expect(result.toLine).toBe(4);
expect(result.content).toContain("3\tline3");
expect(result.content).toContain("4\tline4");
expect(result.content).not.toContain("2\tline2");
expect(result.content).not.toContain("5\tline5");
});
});
// ── Write tool ────────────────────────────────────────────────────────
describe("assistant tools — write", () => {
it("writes a file and reports stats", async () => {
const agent = await freshAgent("write-basic");
const result = (await agent.toolWrite("/out.txt", "hello world")) as {
path: string;
bytesWritten: number;
lines: number;
};
expect(result.path).toBe("/out.txt");
expect(result.bytesWritten).toBe(11);
expect(result.lines).toBe(1);
// Verify via read
const readResult = (await agent.toolRead("/out.txt")) as {
content: string;
};
expect(readResult.content).toContain("hello world");
});
it("creates parent directories", async () => {
const agent = await freshAgent("write-mkdir");
await agent.toolWrite("/a/b/c/deep.txt", "deep");
const result = (await agent.toolRead("/a/b/c/deep.txt")) as {
content: string;
};
expect(result.content).toContain("deep");
});
});
// ── Edit tool ─────────────────────────────────────────────────────────
describe("assistant tools — edit", () => {
it("replaces exact match", async () => {
const agent = await freshAgent("edit-exact");
await agent.seed([{ path: "/f.txt", content: "hello world" }]);
const result = (await agent.toolEdit("/f.txt", "hello", "goodbye")) as {
replaced: boolean;
};
expect(result.replaced).toBe(true);
const read = (await agent.toolRead("/f.txt")) as { content: string };
expect(read.content).toContain("goodbye world");
});
it("returns error for missing file", async () => {
const agent = await freshAgent("edit-missing");
const result = (await agent.toolEdit("/nope.txt", "a", "b")) as {
error: string;
};
expect(result.error).toContain("File not found");
});
it("returns error when old_string not found", async () => {
const agent = await freshAgent("edit-not-found");
await agent.seed([{ path: "/f.txt", content: "hello" }]);
const result = (await agent.toolEdit("/f.txt", "xyz", "abc")) as {
error: string;
};
expect(result.error).toContain("not found");
});
it("returns error when old_string has multiple matches", async () => {
const agent = await freshAgent("edit-multiple");
await agent.seed([{ path: "/f.txt", content: "aa bb aa" }]);
const result = (await agent.toolEdit("/f.txt", "aa", "cc")) as {
error: string;
};
expect(result.error).toContain("2 times");
});
it("creates new file with empty old_string", async () => {
const agent = await freshAgent("edit-create");
const result = (await agent.toolEdit("/new.txt", "", "new content")) as {
created: boolean;
};
expect(result.created).toBe(true);
const read = (await agent.toolRead("/new.txt")) as { content: string };
expect(read.content).toContain("new content");
});
it("fuzzy matches on whitespace differences", async () => {
const agent = await freshAgent("edit-fuzzy");
await agent.seed([{ path: "/f.txt", content: "hello world" }]);
const result = (await agent.toolEdit(
"/f.txt",
"hello world",
"goodbye world"
)) as { replaced: boolean; fuzzyMatch: boolean };
expect(result.replaced).toBe(true);
expect(result.fuzzyMatch).toBe(true);
const read = (await agent.toolRead("/f.txt")) as { content: string };
expect(read.content).toContain("goodbye world");
});
it("returns error for ambiguous fuzzy match", async () => {
const agent = await freshAgent("edit-fuzzy-ambiguous");
// Two regions that differ only in whitespace, both matching "hello world"
await agent.seed([
{
path: "/f.txt",
content: "hello world\nsome other text\nhello\tworld"
}
]);
const result = (await agent.toolEdit(
"/f.txt",
"hello world",
"goodbye world"
)) as { error: string };
expect(result.error).toContain("multiple locations");
});
});
// ── List tool ─────────────────────────────────────────────────────────
describe("assistant tools — list", () => {
it("lists files and directories", async () => {
const agent = await freshAgent("list-basic");
await agent.seed([
{ path: "/readme.md", content: "# Hello" },
{ path: "/src/index.ts", content: "export {}" }
]);
const result = (await agent.toolList("/")) as {
count: number;
entries: string[];
};
expect(result.count).toBeGreaterThanOrEqual(2);
expect(result.entries.some((e: string) => e.includes("readme.md"))).toBe(
true
);
expect(result.entries.some((e: string) => e.includes("src/"))).toBe(true);
});
});
// ── Find tool ─────────────────────────────────────────────────────────
describe("assistant tools — find", () => {
it("finds files by glob pattern", async () => {
const agent = await freshAgent("find-basic");
await agent.seed([
{ path: "/src/a.ts", content: "a" },
{ path: "/src/b.ts", content: "b" },
{ path: "/src/c.js", content: "c" },
{ path: "/readme.md", content: "# Hi" }
]);
const result = (await agent.toolFind("/src/**/*.ts")) as {
count: number;
files: string[];
};
expect(result.count).toBe(2);
expect(result.files).toContain("/src/a.ts");
expect(result.files).toContain("/src/b.ts");
});
});
// ── Grep tool ─────────────────────────────────────────────────────────
describe("assistant tools — grep", () => {
it("searches file contents with regex", async () => {
const agent = await freshAgent("grep-regex");
await agent.seed([
{ path: "/a.ts", content: "const foo = 1;\nconst bar = 2;" },
{ path: "/b.ts", content: "let baz = 3;" }
]);
const result = (await agent.toolGrep("const", "/**.ts")) as {
totalMatches: number;
filesWithMatches: number;
};
expect(result.totalMatches).toBe(2);
expect(result.filesWithMatches).toBe(1);
});
it("searches with fixed string", async () => {
const agent = await freshAgent("grep-fixed");
await agent.seed([
{ path: "/a.txt", content: "hello (world)" },
{ path: "/b.txt", content: "no match" }
]);
const result = (await agent.toolGrep("(world)", "/**.txt", true)) as {
totalMatches: number;
};
expect(result.totalMatches).toBe(1);
});
it("supports case-sensitive search", async () => {
const agent = await freshAgent("grep-case");
await agent.seed([{ path: "/a.txt", content: "Hello\nhello\nHELLO" }]);
const insensitive = (await agent.toolGrep(
"hello",
"/**.txt",
true,
false
)) as { totalMatches: number };
expect(insensitive.totalMatches).toBe(3);
const sensitive = (await agent.toolGrep(
"hello",
"/**.txt",
true,
true
)) as { totalMatches: number };
expect(sensitive.totalMatches).toBe(1);
});
it("returns context lines when requested", async () => {
const agent = await freshAgent("grep-context");
await agent.seed([
{ path: "/a.txt", content: "line1\nline2\nMATCH\nline4\nline5" }
]);
const result = (await agent.toolGrep(
"MATCH",
"/**.txt",
true,
true,
1
)) as { matches: Array<{ context: string }> };
expect(result.matches.length).toBe(1);
const ctx = result.matches[0].context;
expect(ctx).toContain("line2");
expect(ctx).toContain("MATCH");
expect(ctx).toContain("line4");
});
it("skips files larger than 1 MB", async () => {
const agent = await freshAgent("grep-large-skip");
// Seed a small file with a match and a large file (>1MB) with the same match
await agent.seed([{ path: "/small.txt", content: "FINDME here" }]);
await agent.seedLargeFile("/large.txt", 1_100_000); // ~1.1 MB
const result = (await agent.toolGrep("FINDME", "/**.*")) as {
totalMatches: number;
filesSkipped: number;
note: string;
};
expect(result.totalMatches).toBe(1);
expect(result.filesSkipped).toBe(1);
expect(result.note).toContain("skipped");
});
});