git.test.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. /**
  2. * Tests for git utilities
  3. */
  4. import { describe, it, expect, vi, beforeEach } from "vitest"
  5. import { getGitInfo, getGitBranch, branchExists, generateBranchName, isGitWorktree } from "../git.js"
  6. import simpleGit from "simple-git"
  7. // Mock simple-git
  8. vi.mock("simple-git")
  9. describe("git utilities", () => {
  10. beforeEach(() => {
  11. vi.clearAllMocks()
  12. })
  13. describe("getGitInfo", () => {
  14. it("should return default info for empty cwd", async () => {
  15. const result = await getGitInfo("")
  16. expect(result).toEqual({
  17. branch: null,
  18. isClean: true,
  19. isRepo: false,
  20. })
  21. })
  22. it("should return default info for non-git directory", async () => {
  23. const mockGit = {
  24. checkIsRepo: vi.fn().mockResolvedValue(false),
  25. }
  26. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  27. const result = await getGitInfo("/some/path")
  28. expect(result).toEqual({
  29. branch: null,
  30. isClean: true,
  31. isRepo: false,
  32. })
  33. })
  34. it("should return git info for clean repository", async () => {
  35. const mockGit = {
  36. checkIsRepo: vi.fn().mockResolvedValue(true),
  37. revparse: vi.fn().mockResolvedValue("main\n"),
  38. status: vi.fn().mockResolvedValue({ files: [] }),
  39. }
  40. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  41. const result = await getGitInfo("/git/repo")
  42. expect(result).toEqual({
  43. branch: "main",
  44. isClean: true,
  45. isRepo: true,
  46. })
  47. })
  48. it("should return git info for dirty repository", async () => {
  49. const mockGit = {
  50. checkIsRepo: vi.fn().mockResolvedValue(true),
  51. revparse: vi.fn().mockResolvedValue("feature-branch\n"),
  52. status: vi.fn().mockResolvedValue({
  53. files: [{ path: "file.txt", working_dir: "M" }],
  54. }),
  55. }
  56. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  57. const result = await getGitInfo("/git/repo")
  58. expect(result).toEqual({
  59. branch: "feature-branch",
  60. isClean: false,
  61. isRepo: true,
  62. })
  63. })
  64. it("should handle errors gracefully", async () => {
  65. const mockGit = {
  66. checkIsRepo: vi.fn().mockRejectedValue(new Error("Git error")),
  67. }
  68. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  69. const result = await getGitInfo("/git/repo")
  70. expect(result).toEqual({
  71. branch: null,
  72. isClean: true,
  73. isRepo: false,
  74. })
  75. })
  76. })
  77. describe("getGitBranch", () => {
  78. it("should return null for empty cwd", async () => {
  79. const result = await getGitBranch("")
  80. expect(result).toBeNull()
  81. })
  82. it("should return null for non-git directory", async () => {
  83. const mockGit = {
  84. checkIsRepo: vi.fn().mockResolvedValue(false),
  85. }
  86. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  87. const result = await getGitBranch("/some/path")
  88. expect(result).toBeNull()
  89. })
  90. it("should return branch name", async () => {
  91. const mockGit = {
  92. checkIsRepo: vi.fn().mockResolvedValue(true),
  93. revparse: vi.fn().mockResolvedValue("develop\n"),
  94. }
  95. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  96. const result = await getGitBranch("/git/repo")
  97. expect(result).toBe("develop")
  98. })
  99. it("should handle errors gracefully", async () => {
  100. const mockGit = {
  101. checkIsRepo: vi.fn().mockRejectedValue(new Error("Git error")),
  102. }
  103. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  104. const result = await getGitBranch("/git/repo")
  105. expect(result).toBeNull()
  106. })
  107. })
  108. describe("branchExists", () => {
  109. it("should return false for empty cwd", async () => {
  110. const result = await branchExists("", "main")
  111. expect(result).toBe(false)
  112. })
  113. it("should return false for empty branchName", async () => {
  114. const result = await branchExists("/git/repo", "")
  115. expect(result).toBe(false)
  116. })
  117. it("should return false for non-git directory", async () => {
  118. const mockGit = {
  119. checkIsRepo: vi.fn().mockResolvedValue(false),
  120. }
  121. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  122. const result = await branchExists("/some/path", "main")
  123. expect(result).toBe(false)
  124. })
  125. it("should return true when local branch exists", async () => {
  126. const mockGit = {
  127. checkIsRepo: vi.fn().mockResolvedValue(true),
  128. branch: vi.fn().mockResolvedValue({
  129. all: ["main", "develop", "feature-branch"],
  130. }),
  131. }
  132. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  133. const result = await branchExists("/git/repo", "feature-branch")
  134. expect(result).toBe(true)
  135. })
  136. it("should return true when remote branch exists", async () => {
  137. const mockGit = {
  138. checkIsRepo: vi.fn().mockResolvedValue(true),
  139. branch: vi.fn().mockResolvedValue({
  140. all: ["main", "remotes/origin/feature-branch"],
  141. }),
  142. }
  143. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  144. const result = await branchExists("/git/repo", "feature-branch")
  145. expect(result).toBe(true)
  146. })
  147. it("should return false when branch does not exist", async () => {
  148. const mockGit = {
  149. checkIsRepo: vi.fn().mockResolvedValue(true),
  150. branch: vi.fn().mockResolvedValue({
  151. all: ["main", "develop"],
  152. }),
  153. }
  154. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  155. const result = await branchExists("/git/repo", "nonexistent")
  156. expect(result).toBe(false)
  157. })
  158. it("should handle errors gracefully", async () => {
  159. const mockGit = {
  160. checkIsRepo: vi.fn().mockRejectedValue(new Error("Git error")),
  161. }
  162. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  163. const result = await branchExists("/git/repo", "main")
  164. expect(result).toBe(false)
  165. })
  166. })
  167. describe("generateBranchName", () => {
  168. it("should generate branch name with lowercase and hyphens", () => {
  169. const result = generateBranchName("Fix Bug in Auth")
  170. expect(result).toMatch(/^fix-bug-in-auth-\d+$/)
  171. })
  172. it("should replace special characters with hyphens", () => {
  173. const result = generateBranchName("Feature: Add @user support!")
  174. expect(result).toMatch(/^feature-add-user-support-\d+$/)
  175. })
  176. it("should remove leading and trailing hyphens", () => {
  177. const result = generateBranchName("---test---")
  178. expect(result).toMatch(/^test-\d+$/)
  179. })
  180. it("should collapse multiple hyphens into one", () => {
  181. const result = generateBranchName("fix multiple spaces")
  182. expect(result).toMatch(/^fix-multiple-spaces-\d+$/)
  183. })
  184. it("should truncate to 50 characters", () => {
  185. const longPrompt = "a".repeat(100)
  186. const result = generateBranchName(longPrompt)
  187. const withoutTimestamp = result.split("-").slice(0, -1).join("-")
  188. expect(withoutTimestamp.length).toBeLessThanOrEqual(50)
  189. })
  190. it("should add timestamp for uniqueness", async () => {
  191. const prompt = "test feature"
  192. const result1 = generateBranchName(prompt)
  193. await new Promise((resolve) => setTimeout(resolve, 5))
  194. const result2 = generateBranchName(prompt)
  195. expect(result1).not.toBe(result2)
  196. expect(result1).toMatch(/^test-feature-\d+$/)
  197. expect(result2).toMatch(/^test-feature-\d+$/)
  198. })
  199. it("should handle empty string", () => {
  200. const result = generateBranchName("")
  201. expect(result).toMatch(/^kilo-\d+$/)
  202. })
  203. it("should handle only special characters", () => {
  204. const result = generateBranchName("!@#$%^&*()")
  205. expect(result).toMatch(/^kilo-\d+$/)
  206. })
  207. it("should handle unicode characters", () => {
  208. const result = generateBranchName("Add 日本語 support")
  209. expect(result).toMatch(/^add-support-\d+$/)
  210. })
  211. it("should handle mixed case properly", () => {
  212. const result = generateBranchName("FixBugInAuthSystem")
  213. expect(result).toMatch(/^fixbuginauthsystem-\d+$/)
  214. })
  215. })
  216. describe("isGitWorktree", () => {
  217. it("should return false for empty cwd", async () => {
  218. const result = await isGitWorktree("")
  219. expect(result).toBe(false)
  220. })
  221. it("should return false for non-git directory", async () => {
  222. const mockGit = {
  223. checkIsRepo: vi.fn().mockResolvedValue(false),
  224. }
  225. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  226. const result = await isGitWorktree("/some/path")
  227. expect(result).toBe(false)
  228. })
  229. it("should return false for regular git repository", async () => {
  230. const mockGit = {
  231. checkIsRepo: vi.fn().mockResolvedValue(true),
  232. revparse: vi.fn().mockResolvedValue(".git\n"),
  233. }
  234. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  235. const result = await isGitWorktree("/git/repo")
  236. expect(result).toBe(false)
  237. })
  238. it("should return true for git worktree", async () => {
  239. const mockGit = {
  240. checkIsRepo: vi.fn().mockResolvedValue(true),
  241. revparse: vi.fn().mockResolvedValue("/path/to/.git/worktrees/feature-branch\n"),
  242. }
  243. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  244. const result = await isGitWorktree("/git/worktree")
  245. expect(result).toBe(true)
  246. })
  247. it("should handle errors gracefully", async () => {
  248. const mockGit = {
  249. checkIsRepo: vi.fn().mockRejectedValue(new Error("Git error")),
  250. }
  251. vi.mocked(simpleGit).mockReturnValue(mockGit as any)
  252. const result = await isGitWorktree("/git/repo")
  253. expect(result).toBe(false)
  254. })
  255. })
  256. })