Skip to content

Commit a2ab791

Browse files
test: add 21 tests for lib/git.ts (run, helpers, fallbacks)
1 parent e786aba commit a2ab791

1 file changed

Lines changed: 178 additions & 0 deletions

File tree

tests/lib/git.test.ts

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

0 commit comments

Comments
 (0)