installer.test.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /**
  2. * Installer Tests
  3. *
  4. * Tests for installer config-writer fixes:
  5. * - readJsonFile error handling
  6. * - writeClaudeMd section replacement
  7. */
  8. import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  9. import * as fs from 'fs';
  10. import * as path from 'path';
  11. import * as os from 'os';
  12. // We test the exported functions from config-writer
  13. import {
  14. writeMcpConfig,
  15. writePermissions,
  16. writeClaudeMd,
  17. hasMcpConfig,
  18. hasPermissions,
  19. hasClaudeMdSection,
  20. } from '../src/installer/config-writer';
  21. function createTempDir(): string {
  22. return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-installer-test-'));
  23. }
  24. function cleanupTempDir(dir: string): void {
  25. if (fs.existsSync(dir)) {
  26. fs.rmSync(dir, { recursive: true, force: true });
  27. }
  28. }
  29. describe('Installer Config Writer', () => {
  30. let origCwd: string;
  31. let tempDir: string;
  32. beforeEach(() => {
  33. tempDir = createTempDir();
  34. origCwd = process.cwd();
  35. process.chdir(tempDir);
  36. });
  37. afterEach(() => {
  38. process.chdir(origCwd);
  39. cleanupTempDir(tempDir);
  40. });
  41. describe('readJsonFile error handling', () => {
  42. it('should return empty object for non-existent file', () => {
  43. // writeMcpConfig reads .mcp.json - if it doesn't exist, it should create it
  44. writeMcpConfig('local');
  45. const mcpJson = path.join(tempDir, '.mcp.json');
  46. expect(fs.existsSync(mcpJson)).toBe(true);
  47. const content = JSON.parse(fs.readFileSync(mcpJson, 'utf-8'));
  48. expect(content.mcpServers).toBeDefined();
  49. expect(content.mcpServers.codegraph).toBeDefined();
  50. });
  51. it('should handle corrupted JSON by creating backup', () => {
  52. // Create a corrupted .mcp.json
  53. const mcpJson = path.join(tempDir, '.mcp.json');
  54. fs.writeFileSync(mcpJson, '{ this is not valid json !!!');
  55. // Suppress console.warn during test
  56. const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
  57. // Should not throw - gracefully handles corruption
  58. writeMcpConfig('local');
  59. // Should have warned
  60. expect(warnSpy).toHaveBeenCalled();
  61. const warnMsg = warnSpy.mock.calls[0][0];
  62. expect(warnMsg).toContain('Warning');
  63. // Backup should exist
  64. expect(fs.existsSync(mcpJson + '.backup')).toBe(true);
  65. // Original backup content should be the corrupted content
  66. const backup = fs.readFileSync(mcpJson + '.backup', 'utf-8');
  67. expect(backup).toContain('this is not valid json');
  68. // New file should be valid JSON with codegraph config
  69. const content = JSON.parse(fs.readFileSync(mcpJson, 'utf-8'));
  70. expect(content.mcpServers.codegraph).toBeDefined();
  71. warnSpy.mockRestore();
  72. });
  73. it('should preserve existing valid config when adding codegraph', () => {
  74. const mcpJson = path.join(tempDir, '.mcp.json');
  75. fs.writeFileSync(mcpJson, JSON.stringify({
  76. mcpServers: { other: { command: 'other-tool' } },
  77. customField: 'preserved',
  78. }, null, 2));
  79. writeMcpConfig('local');
  80. const content = JSON.parse(fs.readFileSync(mcpJson, 'utf-8'));
  81. expect(content.mcpServers.codegraph).toBeDefined();
  82. expect(content.mcpServers.other).toBeDefined();
  83. expect(content.customField).toBe('preserved');
  84. });
  85. });
  86. describe('writeClaudeMd section replacement', () => {
  87. it('should create new CLAUDE.md with markers', () => {
  88. const result = writeClaudeMd('local');
  89. expect(result.created).toBe(true);
  90. const content = fs.readFileSync(path.join(tempDir, '.claude', 'CLAUDE.md'), 'utf-8');
  91. expect(content).toContain('<!-- CODEGRAPH_START -->');
  92. expect(content).toContain('<!-- CODEGRAPH_END -->');
  93. expect(content).toContain('## CodeGraph');
  94. });
  95. it('should replace marked section on update', () => {
  96. // First write
  97. writeClaudeMd('local');
  98. // Modify file to add custom content before and after
  99. const claudeMdPath = path.join(tempDir, '.claude', 'CLAUDE.md');
  100. const original = fs.readFileSync(claudeMdPath, 'utf-8');
  101. const modified = '## My Custom Section\n\nCustom content\n\n' + original + '\n\n## Another Section\n\nMore content\n';
  102. fs.writeFileSync(claudeMdPath, modified);
  103. // Second write should leave the marked block as-is (byte-identical
  104. // body, so result is `created:false, updated:false` — both flags
  105. // are off but the surrounding custom content must survive).
  106. writeClaudeMd('local');
  107. const final = fs.readFileSync(claudeMdPath, 'utf-8');
  108. expect(final).toContain('## My Custom Section');
  109. expect(final).toContain('Custom content');
  110. expect(final).toContain('## Another Section');
  111. expect(final).toContain('More content');
  112. expect(final).toContain('## CodeGraph');
  113. });
  114. it('should use atomic writes (no temp files left behind)', () => {
  115. writeClaudeMd('local');
  116. const claudeDir = path.join(tempDir, '.claude');
  117. const files = fs.readdirSync(claudeDir);
  118. const tmpFiles = files.filter(f => f.includes('.tmp.'));
  119. expect(tmpFiles).toHaveLength(0);
  120. });
  121. it('should not overwrite content after unmarked section with ### subsections', () => {
  122. // Create a CLAUDE.md with an unmarked CodeGraph section that has ### subsections
  123. // followed by another ## section
  124. const claudeDir = path.join(tempDir, '.claude');
  125. fs.mkdirSync(claudeDir, { recursive: true });
  126. const claudeMdPath = path.join(claudeDir, 'CLAUDE.md');
  127. fs.writeFileSync(claudeMdPath, [
  128. '## Pre-existing Section',
  129. '',
  130. 'Some content',
  131. '',
  132. '## CodeGraph',
  133. '',
  134. '### Subsection A',
  135. '',
  136. 'Old codegraph content',
  137. '',
  138. '### Subsection B',
  139. '',
  140. 'More old content',
  141. '',
  142. '## Important Section After',
  143. '',
  144. 'This content must not be overwritten!',
  145. '',
  146. ].join('\n'));
  147. const result = writeClaudeMd('local');
  148. expect(result.updated).toBe(true);
  149. const final = fs.readFileSync(claudeMdPath, 'utf-8');
  150. // The section after CodeGraph must be preserved
  151. expect(final).toContain('## Important Section After');
  152. expect(final).toContain('This content must not be overwritten!');
  153. // Pre-existing section should also be preserved
  154. expect(final).toContain('## Pre-existing Section');
  155. // New CodeGraph content should be present with markers
  156. expect(final).toContain('<!-- CODEGRAPH_START -->');
  157. expect(final).toContain('<!-- CODEGRAPH_END -->');
  158. });
  159. it('should replace unmarked section without subsections', () => {
  160. const claudeDir = path.join(tempDir, '.claude');
  161. fs.mkdirSync(claudeDir, { recursive: true });
  162. const claudeMdPath = path.join(claudeDir, 'CLAUDE.md');
  163. // Note: regex needs \n before ## CodeGraph, so prefix with another section
  164. fs.writeFileSync(claudeMdPath, [
  165. '## Intro',
  166. '',
  167. 'Preamble',
  168. '',
  169. '## CodeGraph',
  170. '',
  171. 'Old simple content',
  172. '',
  173. '## Next Section',
  174. '',
  175. 'Must be preserved',
  176. '',
  177. ].join('\n'));
  178. writeClaudeMd('local');
  179. const final = fs.readFileSync(claudeMdPath, 'utf-8');
  180. expect(final).toContain('<!-- CODEGRAPH_START -->');
  181. expect(final).toContain('## Next Section');
  182. expect(final).toContain('Must be preserved');
  183. expect(final).not.toContain('Old simple content');
  184. });
  185. });
  186. });