sync.test.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. /**
  2. * Sync Module Tests
  3. *
  4. * Tests for git hooks installation and sync functionality.
  5. */
  6. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  7. import * as fs from 'fs';
  8. import * as path from 'path';
  9. import * as os from 'os';
  10. import CodeGraph from '../src/index';
  11. describe('Sync Module', () => {
  12. describe('Git Hooks', () => {
  13. let testDir: string;
  14. let cg: CodeGraph;
  15. beforeEach(() => {
  16. testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-sync-test-'));
  17. // Create a sample source file
  18. const srcDir = path.join(testDir, 'src');
  19. fs.mkdirSync(srcDir);
  20. fs.writeFileSync(
  21. path.join(srcDir, 'index.ts'),
  22. `export function hello() { return 'world'; }`
  23. );
  24. // Initialize CodeGraph
  25. cg = CodeGraph.initSync(testDir, {
  26. config: {
  27. include: ['**/*.ts'],
  28. exclude: [],
  29. },
  30. });
  31. });
  32. afterEach(() => {
  33. if (cg) {
  34. cg.destroy();
  35. }
  36. if (fs.existsSync(testDir)) {
  37. fs.rmSync(testDir, { recursive: true, force: true });
  38. }
  39. });
  40. describe('isGitRepository()', () => {
  41. it('should return false for non-git directory', () => {
  42. expect(cg.isGitRepository()).toBe(false);
  43. });
  44. it('should return true for git directory', () => {
  45. // Initialize git
  46. fs.mkdirSync(path.join(testDir, '.git'));
  47. expect(cg.isGitRepository()).toBe(true);
  48. });
  49. });
  50. describe('isGitHookInstalled()', () => {
  51. it('should return false when no hook is installed', () => {
  52. // Initialize git
  53. fs.mkdirSync(path.join(testDir, '.git'));
  54. expect(cg.isGitHookInstalled()).toBe(false);
  55. });
  56. it('should return false for non-codegraph hook', () => {
  57. // Initialize git with a custom hook
  58. const hooksDir = path.join(testDir, '.git', 'hooks');
  59. fs.mkdirSync(path.join(testDir, '.git'));
  60. fs.mkdirSync(hooksDir);
  61. fs.writeFileSync(
  62. path.join(hooksDir, 'post-commit'),
  63. '#!/bin/sh\necho "custom hook"'
  64. );
  65. expect(cg.isGitHookInstalled()).toBe(false);
  66. });
  67. it('should return true when codegraph hook is installed', () => {
  68. // Initialize git
  69. fs.mkdirSync(path.join(testDir, '.git'));
  70. // Install hook
  71. cg.installGitHooks();
  72. expect(cg.isGitHookInstalled()).toBe(true);
  73. });
  74. });
  75. describe('installGitHooks()', () => {
  76. it('should fail if not a git repository', () => {
  77. const result = cg.installGitHooks();
  78. expect(result.success).toBe(false);
  79. expect(result.message).toContain('Not a git repository');
  80. });
  81. it('should install hook in git repository', () => {
  82. // Initialize git
  83. fs.mkdirSync(path.join(testDir, '.git'));
  84. const result = cg.installGitHooks();
  85. expect(result.success).toBe(true);
  86. expect(result.message).toContain('installed');
  87. // Verify hook file exists
  88. const hookPath = path.join(testDir, '.git', 'hooks', 'post-commit');
  89. expect(fs.existsSync(hookPath)).toBe(true);
  90. // Verify hook content contains marker
  91. const content = fs.readFileSync(hookPath, 'utf-8');
  92. expect(content).toContain('CodeGraph auto-sync hook');
  93. expect(content).toContain('codegraph sync');
  94. });
  95. it('should create hooks directory if missing', () => {
  96. // Initialize git without hooks directory
  97. fs.mkdirSync(path.join(testDir, '.git'));
  98. const result = cg.installGitHooks();
  99. expect(result.success).toBe(true);
  100. expect(fs.existsSync(path.join(testDir, '.git', 'hooks'))).toBe(true);
  101. });
  102. it('should backup existing non-codegraph hook', () => {
  103. // Initialize git with a custom hook
  104. const hooksDir = path.join(testDir, '.git', 'hooks');
  105. fs.mkdirSync(path.join(testDir, '.git'));
  106. fs.mkdirSync(hooksDir);
  107. const customHookContent = '#!/bin/sh\necho "custom hook"';
  108. fs.writeFileSync(
  109. path.join(hooksDir, 'post-commit'),
  110. customHookContent
  111. );
  112. const result = cg.installGitHooks();
  113. expect(result.success).toBe(true);
  114. expect(result.previousHookBackedUp).toBe(true);
  115. // Verify backup exists
  116. const backupPath = path.join(hooksDir, 'post-commit.codegraph-backup');
  117. expect(fs.existsSync(backupPath)).toBe(true);
  118. expect(fs.readFileSync(backupPath, 'utf-8')).toBe(customHookContent);
  119. });
  120. it('should update existing codegraph hook without backup', () => {
  121. // Initialize git
  122. fs.mkdirSync(path.join(testDir, '.git'));
  123. // Install hook first time
  124. cg.installGitHooks();
  125. // Install again (update)
  126. const result = cg.installGitHooks();
  127. expect(result.success).toBe(true);
  128. expect(result.message).toContain('updated');
  129. expect(result.previousHookBackedUp).toBeUndefined();
  130. });
  131. it('should make hook executable', () => {
  132. // Initialize git
  133. fs.mkdirSync(path.join(testDir, '.git'));
  134. cg.installGitHooks();
  135. const hookPath = path.join(testDir, '.git', 'hooks', 'post-commit');
  136. const stats = fs.statSync(hookPath);
  137. // Check executable bit (at least for owner)
  138. expect(stats.mode & 0o100).toBeTruthy();
  139. });
  140. });
  141. describe('removeGitHooks()', () => {
  142. it('should succeed if no hook exists', () => {
  143. // Initialize git
  144. fs.mkdirSync(path.join(testDir, '.git'));
  145. const result = cg.removeGitHooks();
  146. expect(result.success).toBe(true);
  147. expect(result.message).toContain('No post-commit hook found');
  148. });
  149. it('should not remove non-codegraph hook', () => {
  150. // Initialize git with a custom hook
  151. const hooksDir = path.join(testDir, '.git', 'hooks');
  152. fs.mkdirSync(path.join(testDir, '.git'));
  153. fs.mkdirSync(hooksDir);
  154. fs.writeFileSync(
  155. path.join(hooksDir, 'post-commit'),
  156. '#!/bin/sh\necho "custom hook"'
  157. );
  158. const result = cg.removeGitHooks();
  159. expect(result.success).toBe(false);
  160. expect(result.message).toContain('not installed by CodeGraph');
  161. // Verify hook still exists
  162. expect(fs.existsSync(path.join(hooksDir, 'post-commit'))).toBe(true);
  163. });
  164. it('should remove codegraph hook', () => {
  165. // Initialize git
  166. fs.mkdirSync(path.join(testDir, '.git'));
  167. // Install then remove
  168. cg.installGitHooks();
  169. const result = cg.removeGitHooks();
  170. expect(result.success).toBe(true);
  171. expect(result.message).toContain('removed');
  172. // Verify hook is gone
  173. const hookPath = path.join(testDir, '.git', 'hooks', 'post-commit');
  174. expect(fs.existsSync(hookPath)).toBe(false);
  175. });
  176. it('should restore backup when removing', () => {
  177. // Initialize git with a custom hook
  178. const hooksDir = path.join(testDir, '.git', 'hooks');
  179. fs.mkdirSync(path.join(testDir, '.git'));
  180. fs.mkdirSync(hooksDir);
  181. const customHookContent = '#!/bin/sh\necho "custom hook"';
  182. fs.writeFileSync(
  183. path.join(hooksDir, 'post-commit'),
  184. customHookContent
  185. );
  186. // Install (backs up custom hook) then remove
  187. cg.installGitHooks();
  188. const result = cg.removeGitHooks();
  189. expect(result.success).toBe(true);
  190. expect(result.restoredFromBackup).toBe(true);
  191. // Verify original hook is restored
  192. const hookPath = path.join(hooksDir, 'post-commit');
  193. expect(fs.existsSync(hookPath)).toBe(true);
  194. expect(fs.readFileSync(hookPath, 'utf-8')).toBe(customHookContent);
  195. // Verify backup is gone
  196. const backupPath = path.join(hooksDir, 'post-commit.codegraph-backup');
  197. expect(fs.existsSync(backupPath)).toBe(false);
  198. });
  199. });
  200. });
  201. describe('Sync Functionality', () => {
  202. let testDir: string;
  203. let cg: CodeGraph;
  204. beforeEach(async () => {
  205. testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-sync-func-'));
  206. // Create initial source files
  207. const srcDir = path.join(testDir, 'src');
  208. fs.mkdirSync(srcDir);
  209. fs.writeFileSync(
  210. path.join(srcDir, 'index.ts'),
  211. `export function hello() { return 'world'; }`
  212. );
  213. // Initialize and index
  214. cg = CodeGraph.initSync(testDir, {
  215. config: {
  216. include: ['**/*.ts'],
  217. exclude: [],
  218. },
  219. });
  220. await cg.indexAll();
  221. });
  222. afterEach(() => {
  223. if (cg) {
  224. cg.destroy();
  225. }
  226. if (fs.existsSync(testDir)) {
  227. fs.rmSync(testDir, { recursive: true, force: true });
  228. }
  229. });
  230. describe('getChangedFiles()', () => {
  231. it('should detect added files', () => {
  232. // Add a new file
  233. fs.writeFileSync(
  234. path.join(testDir, 'src', 'new.ts'),
  235. `export function newFunc() { return 42; }`
  236. );
  237. const changes = cg.getChangedFiles();
  238. expect(changes.added).toContain('src/new.ts');
  239. expect(changes.modified).toHaveLength(0);
  240. expect(changes.removed).toHaveLength(0);
  241. });
  242. it('should detect modified files', () => {
  243. // Modify existing file
  244. fs.writeFileSync(
  245. path.join(testDir, 'src', 'index.ts'),
  246. `export function hello() { return 'modified'; }`
  247. );
  248. const changes = cg.getChangedFiles();
  249. expect(changes.added).toHaveLength(0);
  250. expect(changes.modified).toContain('src/index.ts');
  251. expect(changes.removed).toHaveLength(0);
  252. });
  253. it('should detect removed files', () => {
  254. // Remove file
  255. fs.unlinkSync(path.join(testDir, 'src', 'index.ts'));
  256. const changes = cg.getChangedFiles();
  257. expect(changes.added).toHaveLength(0);
  258. expect(changes.modified).toHaveLength(0);
  259. expect(changes.removed).toContain('src/index.ts');
  260. });
  261. });
  262. describe('sync()', () => {
  263. it('should reindex added files', async () => {
  264. // Add a new file
  265. fs.writeFileSync(
  266. path.join(testDir, 'src', 'new.ts'),
  267. `export function newFunc() { return 42; }`
  268. );
  269. const result = await cg.sync();
  270. expect(result.filesAdded).toBe(1);
  271. expect(result.filesModified).toBe(0);
  272. expect(result.filesRemoved).toBe(0);
  273. // Verify new function is in the graph
  274. const nodes = cg.searchNodes('newFunc');
  275. expect(nodes.length).toBeGreaterThan(0);
  276. });
  277. it('should reindex modified files', async () => {
  278. // Modify existing file
  279. fs.writeFileSync(
  280. path.join(testDir, 'src', 'index.ts'),
  281. `export function goodbye() { return 'farewell'; }`
  282. );
  283. const result = await cg.sync();
  284. expect(result.filesModified).toBe(1);
  285. // Verify new function is in the graph
  286. const nodes = cg.searchNodes('goodbye');
  287. expect(nodes.length).toBeGreaterThan(0);
  288. // Verify old function is gone
  289. const oldNodes = cg.searchNodes('hello');
  290. expect(oldNodes.length).toBe(0);
  291. });
  292. it('should remove nodes from deleted files', async () => {
  293. // Remove file
  294. fs.unlinkSync(path.join(testDir, 'src', 'index.ts'));
  295. const result = await cg.sync();
  296. expect(result.filesRemoved).toBe(1);
  297. // Verify function is gone
  298. const nodes = cg.searchNodes('hello');
  299. expect(nodes.length).toBe(0);
  300. });
  301. it('should report no changes when nothing changed', async () => {
  302. const result = await cg.sync();
  303. expect(result.filesAdded).toBe(0);
  304. expect(result.filesModified).toBe(0);
  305. expect(result.filesRemoved).toBe(0);
  306. expect(result.filesChecked).toBeGreaterThan(0);
  307. });
  308. });
  309. });
  310. });