WIP: A simple cli for daily tangled use cases and AI integration. This is for my personal use right now, but happy if others get mileage from it! :)
at pull-request-format-testing 264 lines 8.3 kB view raw
1import { homedir } from 'node:os'; 2import { join } from 'node:path'; 3import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 4import { 5 clearLocalRemote, 6 clearUserRemote, 7 getConfiguredRemote, 8 loadConfig, 9 setLocalRemote, 10 setUserRemote, 11} from '../../src/lib/config.js'; 12 13// Mock modules 14vi.mock('node:fs/promises'); 15vi.mock('simple-git'); 16vi.mock('cosmiconfig'); 17 18// Import mocked modules 19import * as fs from 'node:fs/promises'; 20import { cosmiconfig } from 'cosmiconfig'; 21import { simpleGit } from 'simple-git'; 22 23describe('Config Management', () => { 24 let originalEnv: string | undefined; 25 26 beforeEach(() => { 27 vi.clearAllMocks(); 28 originalEnv = process.env.TANGLED_REMOTE; 29 // biome-ignore lint/performance/noDelete: Need to actually delete env var, not set to undefined 30 delete process.env.TANGLED_REMOTE; 31 }); 32 33 afterEach(() => { 34 if (originalEnv !== undefined) { 35 process.env.TANGLED_REMOTE = originalEnv; 36 } else { 37 // biome-ignore lint/performance/noDelete: Need to actually delete env var, not set to undefined 38 delete process.env.TANGLED_REMOTE; 39 } 40 }); 41 42 describe('loadConfig', () => { 43 it('should return config from TANGLED_REMOTE environment variable', async () => { 44 process.env.TANGLED_REMOTE = 'upstream'; 45 46 const config = await loadConfig(); 47 48 expect(config).toEqual({ remote: 'upstream' }); 49 }); 50 51 it('should load config from file when env var not set', async () => { 52 const mockExplorer = { 53 search: vi.fn().mockResolvedValue({ 54 config: { remote: 'origin' }, 55 filepath: '/test/.tangledrc', 56 isEmpty: false, 57 }), 58 }; 59 60 vi.mocked(cosmiconfig).mockReturnValue(mockExplorer as never); 61 62 // Mock Git root 63 const mockGit = { 64 checkIsRepo: vi.fn().mockResolvedValue(true), 65 revparse: vi.fn().mockResolvedValue('/test/repo\n'), 66 }; 67 vi.mocked(simpleGit).mockReturnValue(mockGit as never); 68 69 const config = await loadConfig('/test/repo'); 70 71 expect(config).toEqual({ remote: 'origin' }); 72 expect(mockExplorer.search).toHaveBeenCalledWith('/test/repo'); 73 }); 74 75 it('should return empty config when no config file found', async () => { 76 const mockExplorer = { 77 search: vi.fn().mockResolvedValue(null), 78 }; 79 80 vi.mocked(cosmiconfig).mockReturnValue(mockExplorer as never); 81 82 // Mock Git root 83 const mockGit = { 84 checkIsRepo: vi.fn().mockResolvedValue(false), 85 }; 86 vi.mocked(simpleGit).mockReturnValue(mockGit as never); 87 88 const config = await loadConfig(); 89 90 expect(config).toEqual({}); 91 }); 92 93 it('should handle cosmiconfig errors gracefully', async () => { 94 const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); 95 96 const mockExplorer = { 97 search: vi.fn().mockRejectedValue(new Error('Config read error')), 98 }; 99 100 vi.mocked(cosmiconfig).mockReturnValue(mockExplorer as never); 101 102 // Mock Git root 103 const mockGit = { 104 checkIsRepo: vi.fn().mockResolvedValue(false), 105 }; 106 vi.mocked(simpleGit).mockReturnValue(mockGit as never); 107 108 const config = await loadConfig(); 109 110 expect(config).toEqual({}); 111 expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to load config')); 112 113 consoleWarnSpy.mockRestore(); 114 }); 115 }); 116 117 describe('getConfiguredRemote', () => { 118 it('should return remote name from config', async () => { 119 process.env.TANGLED_REMOTE = 'upstream'; 120 121 const remote = await getConfiguredRemote(); 122 123 expect(remote).toBe('upstream'); 124 }); 125 126 it('should return null when no config found', async () => { 127 const mockExplorer = { 128 search: vi.fn().mockResolvedValue(null), 129 }; 130 131 vi.mocked(cosmiconfig).mockReturnValue(mockExplorer as never); 132 133 // Mock not in Git repo 134 const mockGit = { 135 checkIsRepo: vi.fn().mockResolvedValue(false), 136 }; 137 vi.mocked(simpleGit).mockReturnValue(mockGit as never); 138 139 const remote = await getConfiguredRemote(); 140 141 expect(remote).toBeNull(); 142 }); 143 }); 144 145 describe('setLocalRemote', () => { 146 it('should write config to Git root directory', async () => { 147 const mockGit = { 148 checkIsRepo: vi.fn().mockResolvedValue(true), 149 revparse: vi.fn().mockResolvedValue('/test/repo\n'), 150 }; 151 vi.mocked(simpleGit).mockReturnValue(mockGit as never); 152 vi.mocked(fs.writeFile).mockResolvedValue(undefined); 153 154 await setLocalRemote('origin', '/test/repo'); 155 156 expect(fs.writeFile).toHaveBeenCalledWith( 157 '/test/repo/.tangledrc', 158 `${JSON.stringify({ remote: 'origin' }, null, 2)}\n`, 159 'utf-8' 160 ); 161 }); 162 163 it('should throw error when not in Git repository', async () => { 164 const mockGit = { 165 checkIsRepo: vi.fn().mockResolvedValue(false), 166 }; 167 vi.mocked(simpleGit).mockReturnValue(mockGit as never); 168 169 await expect(setLocalRemote('origin')).rejects.toThrow('Not in a Git repository'); 170 }); 171 172 it('should throw error on write failure', async () => { 173 const mockGit = { 174 checkIsRepo: vi.fn().mockResolvedValue(true), 175 revparse: vi.fn().mockResolvedValue('/test/repo\n'), 176 }; 177 vi.mocked(simpleGit).mockReturnValue(mockGit as never); 178 vi.mocked(fs.writeFile).mockRejectedValue(new Error('Write failed')); 179 180 await expect(setLocalRemote('origin')).rejects.toThrow('Failed to write local config'); 181 }); 182 }); 183 184 describe('setUserRemote', () => { 185 it('should write config to user home directory', async () => { 186 vi.mocked(fs.mkdir).mockResolvedValue(undefined); 187 vi.mocked(fs.writeFile).mockResolvedValue(undefined); 188 189 await setUserRemote('origin'); 190 191 const expectedPath = join(homedir(), '.tangledrc'); 192 expect(fs.mkdir).toHaveBeenCalledWith(expect.any(String), { recursive: true }); 193 expect(fs.writeFile).toHaveBeenCalledWith( 194 expectedPath, 195 `${JSON.stringify({ remote: 'origin' }, null, 2)}\n`, 196 'utf-8' 197 ); 198 }); 199 200 it('should throw error on write failure', async () => { 201 vi.mocked(fs.mkdir).mockResolvedValue(undefined); 202 vi.mocked(fs.writeFile).mockRejectedValue(new Error('Write failed')); 203 204 await expect(setUserRemote('origin')).rejects.toThrow('Failed to write user config'); 205 }); 206 }); 207 208 describe('clearLocalRemote', () => { 209 it('should delete local config file', async () => { 210 const mockGit = { 211 checkIsRepo: vi.fn().mockResolvedValue(true), 212 revparse: vi.fn().mockResolvedValue('/test/repo\n'), 213 }; 214 vi.mocked(simpleGit).mockReturnValue(mockGit as never); 215 vi.mocked(fs.unlink).mockResolvedValue(undefined); 216 217 await clearLocalRemote('/test/repo'); 218 219 expect(fs.unlink).toHaveBeenCalledWith('/test/repo/.tangledrc'); 220 }); 221 222 it('should not throw error if file does not exist', async () => { 223 const mockGit = { 224 checkIsRepo: vi.fn().mockResolvedValue(true), 225 revparse: vi.fn().mockResolvedValue('/test/repo\n'), 226 }; 227 vi.mocked(simpleGit).mockReturnValue(mockGit as never); 228 229 const error = new Error('File not found') as NodeJS.ErrnoException; 230 error.code = 'ENOENT'; 231 vi.mocked(fs.unlink).mockRejectedValue(error); 232 233 await expect(clearLocalRemote()).resolves.not.toThrow(); 234 }); 235 236 it('should throw error when not in Git repository', async () => { 237 const mockGit = { 238 checkIsRepo: vi.fn().mockResolvedValue(false), 239 }; 240 vi.mocked(simpleGit).mockReturnValue(mockGit as never); 241 242 await expect(clearLocalRemote()).rejects.toThrow('Not in a Git repository'); 243 }); 244 }); 245 246 describe('clearUserRemote', () => { 247 it('should delete user config file', async () => { 248 vi.mocked(fs.unlink).mockResolvedValue(undefined); 249 250 await clearUserRemote(); 251 252 const expectedPath = join(homedir(), '.tangledrc'); 253 expect(fs.unlink).toHaveBeenCalledWith(expectedPath); 254 }); 255 256 it('should not throw error if file does not exist', async () => { 257 const error = new Error('File not found') as NodeJS.ErrnoException; 258 error.code = 'ENOENT'; 259 vi.mocked(fs.unlink).mockRejectedValue(error); 260 261 await expect(clearUserRemote()).resolves.not.toThrow(); 262 }); 263 }); 264});