Skip to content

Commit 23954b8

Browse files
test: add comprehensive unit tests for lib/git.ts
- 20 tests covering run(), convenience functions, fallback behavior - Tests error handling: timeouts, ENOENT, stderr, exit codes - Tests getDiffFiles/getDiffStat fallback chains - Uses vitest mocks for child_process
1 parent c022a29 commit 23954b8

1 file changed

Lines changed: 160 additions & 0 deletions

File tree

tests/lib/git.test.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import * as cp from "child_process";
3+
4+
// Mock child_process before importing git module
5+
vi.mock("child_process", () => ({
6+
execFileSync: vi.fn(),
7+
}));
8+
9+
// Mock files to provide PROJECT_DIR
10+
vi.mock("../../src/lib/files.js", () => ({
11+
PROJECT_DIR: "/tmp/test-project",
12+
}));
13+
14+
import { run, getBranch, getStatus, getRecentCommits, getLastCommit, getLastCommitTime, getDiffFiles, getStagedFiles, getDiffStat } from "../../src/lib/git.js";
15+
16+
const mockExec = vi.mocked(cp.execFileSync);
17+
18+
beforeEach(() => {
19+
mockExec.mockReset();
20+
});
21+
22+
describe("run()", () => {
23+
it("accepts array args and returns trimmed stdout", () => {
24+
mockExec.mockReturnValue(" hello world ");
25+
const result = run(["status", "--short"]);
26+
expect(result).toBe("hello world");
27+
expect(mockExec).toHaveBeenCalledWith("git", ["status", "--short"], expect.objectContaining({
28+
cwd: "/tmp/test-project",
29+
encoding: "utf-8",
30+
}));
31+
});
32+
33+
it("accepts string arg and splits on whitespace", () => {
34+
mockExec.mockReturnValue("ok");
35+
run("log --oneline -5");
36+
expect(mockExec).toHaveBeenCalledWith("git", ["log", "--oneline", "-5"], expect.anything());
37+
});
38+
39+
it("returns timeout message when process is killed", () => {
40+
const err: any = new Error("killed");
41+
err.killed = true;
42+
mockExec.mockImplementation(() => { throw err; });
43+
expect(run(["status"])).toBe("[timed out after 10000ms]");
44+
});
45+
46+
it("returns custom timeout value in message", () => {
47+
const err: any = new Error("killed");
48+
err.signal = "SIGTERM";
49+
mockExec.mockImplementation(() => { throw err; });
50+
expect(run(["status"], { timeout: 5000 })).toBe("[timed out after 5000ms]");
51+
});
52+
53+
it("returns stderr on failure", () => {
54+
const err: any = new Error("fail");
55+
err.stdout = "";
56+
err.stderr = "fatal: not a git repository";
57+
mockExec.mockImplementation(() => { throw err; });
58+
expect(run(["status"])).toBe("fatal: not a git repository");
59+
});
60+
61+
it("returns ENOENT message when git not found", () => {
62+
const err: any = new Error("fail");
63+
err.code = "ENOENT";
64+
mockExec.mockImplementation(() => { throw err; });
65+
expect(run(["status"])).toBe("[git not found]");
66+
});
67+
68+
it("returns generic failure message when no output", () => {
69+
const err: any = new Error("fail");
70+
err.status = 128;
71+
err.stdout = "";
72+
err.stderr = "";
73+
mockExec.mockImplementation(() => { throw err; });
74+
expect(run(["push"])).toBe("[command failed: git push (exit 128)]");
75+
});
76+
});
77+
78+
describe("convenience functions", () => {
79+
it("getBranch calls branch --show-current", () => {
80+
mockExec.mockReturnValue("main");
81+
expect(getBranch()).toBe("main");
82+
expect(mockExec).toHaveBeenCalledWith("git", ["branch", "--show-current"], expect.anything());
83+
});
84+
85+
it("getStatus calls status --short", () => {
86+
mockExec.mockReturnValue("M file.ts");
87+
expect(getStatus()).toBe("M file.ts");
88+
});
89+
90+
it("getRecentCommits defaults to 5", () => {
91+
mockExec.mockReturnValue("abc123 commit");
92+
getRecentCommits();
93+
expect(mockExec).toHaveBeenCalledWith("git", ["log", "--oneline", "-5"], expect.anything());
94+
});
95+
96+
it("getRecentCommits accepts custom count", () => {
97+
mockExec.mockReturnValue("abc123 commit");
98+
getRecentCommits(10);
99+
expect(mockExec).toHaveBeenCalledWith("git", ["log", "--oneline", "-10"], expect.anything());
100+
});
101+
102+
it("getLastCommit returns single oneline", () => {
103+
mockExec.mockReturnValue("abc123 fix bug");
104+
expect(getLastCommit()).toBe("abc123 fix bug");
105+
});
106+
107+
it("getLastCommitTime returns timestamp", () => {
108+
mockExec.mockReturnValue("2026-03-09 10:00:00 -0700");
109+
expect(getLastCommitTime()).toBe("2026-03-09 10:00:00 -0700");
110+
});
111+
});
112+
113+
describe("getDiffFiles()", () => {
114+
it("returns diff output on success", () => {
115+
mockExec.mockReturnValue("src/index.ts\nsrc/lib/git.ts");
116+
expect(getDiffFiles()).toBe("src/index.ts\nsrc/lib/git.ts");
117+
});
118+
119+
it("falls back to HEAD~1 when primary ref fails", () => {
120+
mockExec
121+
.mockReturnValueOnce("[command failed: git diff (exit 128)]")
122+
.mockReturnValueOnce("src/index.ts");
123+
expect(getDiffFiles("origin/main")).toBe("src/index.ts");
124+
});
125+
126+
it("returns 'no commits' when both refs fail", () => {
127+
mockExec
128+
.mockReturnValueOnce("[command failed]")
129+
.mockReturnValueOnce("[command failed]");
130+
expect(getDiffFiles()).toBe("no commits");
131+
});
132+
});
133+
134+
describe("getDiffStat()", () => {
135+
it("returns stat on success", () => {
136+
mockExec.mockReturnValue("2 files changed, 10 insertions(+)");
137+
expect(getDiffStat()).toBe("2 files changed, 10 insertions(+)");
138+
});
139+
140+
it("falls back to HEAD~3 when primary fails", () => {
141+
mockExec
142+
.mockReturnValueOnce("[command failed]")
143+
.mockReturnValueOnce("1 file changed");
144+
expect(getDiffStat("origin/main")).toBe("1 file changed");
145+
});
146+
147+
it("returns fallback message when both fail", () => {
148+
mockExec
149+
.mockReturnValueOnce("[command failed]")
150+
.mockReturnValueOnce("[command failed]");
151+
expect(getDiffStat()).toBe("no diff stats available");
152+
});
153+
});
154+
155+
describe("getStagedFiles()", () => {
156+
it("returns staged file list", () => {
157+
mockExec.mockReturnValue("src/new.ts");
158+
expect(getStagedFiles()).toBe("src/new.ts");
159+
});
160+
});

0 commit comments

Comments
 (0)