1import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3import { tmpdir } from "node:os";
4import { join } from "node:path";
5import simpleGit from "simple-git";
6import { createGitShowTool } from "../src/agent/tools/git/show.js";
7import { createGitDiffTool } from "../src/agent/tools/git/diff.js";
8import { createGitBlameTool } from "../src/agent/tools/git/blame.js";
9import { createGitLogTool } from "../src/agent/tools/git/log.js";
10import { DEFAULT_MAX_LINES, DEFAULT_MAX_BYTES } from "../src/util/truncate.js";
11
12function textOf(result: any): string {
13 return result.content[0].text;
14}
15
16let workDir: string;
17let git: ReturnType<typeof simpleGit>;
18
19beforeAll(async () => {
20 workDir = mkdtempSync(join(tmpdir(), "rumilo-git-test-"));
21 git = simpleGit(workDir);
22 await git.init();
23 await git.addConfig("user.name", "Test");
24 await git.addConfig("user.email", "test@test.com");
25
26 // Create a large file for truncation tests
27 const largeLine = "x".repeat(100);
28 const largeContent = Array.from({ length: 3000 }, (_, i) => `${i}: ${largeLine}`).join("\n");
29 writeFileSync(join(workDir, "large.txt"), largeContent);
30 await git.add("large.txt");
31 await git.commit("add large file");
32
33 // Create many commits for log default-limit test
34 for (let i = 0; i < 30; i++) {
35 writeFileSync(join(workDir, "counter.txt"), String(i));
36 await git.add("counter.txt");
37 await git.commit(`commit number ${i}`);
38 }
39});
40
41afterAll(() => {
42 try {
43 rmSync(workDir, { recursive: true, force: true });
44 } catch {}
45});
46
47describe("git_show truncation (issue #8)", () => {
48 test("truncates large output and appends notice", async () => {
49 const tool = createGitShowTool(workDir);
50 // The first commit has the large file diff, which should exceed truncation limits
51 const logs = await git.log();
52 const firstCommitHash = logs.all[logs.all.length - 1]!.hash;
53 const result = await tool.execute("call-1", { ref: firstCommitHash });
54 const text = textOf(result);
55
56 // Output should be bounded - not return all 3000+ lines raw
57 const lines = text.split("\n");
58 expect(lines.length).toBeLessThanOrEqual(DEFAULT_MAX_LINES + 5); // small margin for notice
59 expect(Buffer.byteLength(text, "utf-8")).toBeLessThanOrEqual(DEFAULT_MAX_BYTES + 500); // margin for notice
60 expect(text).toContain("[truncated");
61 });
62
63 test("small output is not truncated", async () => {
64 const tool = createGitShowTool(workDir);
65 const result = await tool.execute("call-2", { ref: "HEAD" });
66 const text = textOf(result);
67 // HEAD commit is small (counter.txt change), should NOT be truncated
68 expect(text).not.toContain("[truncated");
69 });
70});
71
72describe("git_diff truncation (issue #8)", () => {
73 test("truncates large diff output", async () => {
74 const tool = createGitDiffTool(workDir);
75 const logs = await git.log();
76 const firstCommitHash = logs.all[logs.all.length - 1]!.hash;
77 const secondCommitHash = logs.all[logs.all.length - 2]!.hash;
78 // Diff between first commit (large file add) and second commit
79 const result = await tool.execute("call-3", { ref: firstCommitHash, ref2: secondCommitHash });
80 const text = textOf(result);
81 // The diff won't be huge (only counter.txt changes), so let's create a proper large diff scenario
82 // Instead, diff from the first commit to HEAD which has many changes but also large.txt unchanged
83 // Better: modify large.txt to create a big diff
84 // Actually, let's just verify the mechanism works by checking the first commit via show already.
85 // For diff specifically, create a modified version of large.txt
86 const largeLine2 = "y".repeat(100);
87 const largeContent2 = Array.from({ length: 3000 }, (_, i) => `${i}: ${largeLine2}`).join("\n");
88 writeFileSync(join(workDir, "large.txt"), largeContent2);
89 const result2 = await tool.execute("call-3b", { ref: "HEAD" });
90 const text2 = textOf(result2);
91 const lines2 = text2.split("\n");
92 expect(lines2.length).toBeLessThanOrEqual(DEFAULT_MAX_LINES + 5);
93 expect(text2).toContain("[truncated");
94 // Restore the file
95 await git.checkout(["--", "large.txt"]);
96 });
97});
98
99describe("git_blame truncation (issue #8)", () => {
100 test("truncates large blame output", async () => {
101 const tool = createGitBlameTool(workDir);
102 const result = await tool.execute("call-4", { path: "large.txt" });
103 const text = textOf(result);
104 const lines = text.split("\n");
105 expect(lines.length).toBeLessThanOrEqual(DEFAULT_MAX_LINES + 5);
106 expect(Buffer.byteLength(text, "utf-8")).toBeLessThanOrEqual(DEFAULT_MAX_BYTES + 500);
107 expect(text).toContain("[truncated");
108 });
109});
110
111describe("git_log default limit (issue #9)", () => {
112 test("returns at most 20 commits when n is not specified", async () => {
113 const tool = createGitLogTool(workDir);
114 const result: any = await tool.execute("call-5", {});
115 // We have 31 commits total (1 large file + 30 counter), default should limit to 20
116 expect(result.details.count).toBeLessThanOrEqual(20);
117 expect(result.details.count).toBe(20);
118 });
119
120 test("explicit n overrides default limit", async () => {
121 const tool = createGitLogTool(workDir);
122 const result: any = await tool.execute("call-6", { n: 5 });
123 expect(result.details.count).toBe(5);
124 });
125
126 test("explicit n larger than 20 works", async () => {
127 const tool = createGitLogTool(workDir);
128 const result: any = await tool.execute("call-7", { n: 25 });
129 expect(result.details.count).toBe(25);
130 });
131});