Something went wrong. Try again.
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 fro
Something went wrong. Try again.
8.1 kB · 262 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263import { homedir } from 'node:os';import { join } from 'node:path';import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';import { clearLocalRemote, clearUserRemote, getConfiguredRemote, loadConfig, setLocalRemote, setUserRemote,} from '../../src/lib/config.js';
// Mock modulesvi.mock('node:fs/promises');vi.mock('simple-git');vi.mock('cosmiconfig');
// Import mocked modulesimport * as fs from 'node:fs/promises';import { cosmiconfig } from 'cosmiconfig';import { simpleGit } from 'simple-git';
describe('Config Management', () => { let originalEnv: string | undefined;
beforeEach(() => { vi.clearAllMocks(); originalEnv = process.env.TANGLED_REMOTE; delete process.env.TANGLED_REMOTE; });
afterEach(() => { if (originalEnv !== undefined) { process.env.TANGLED_REMOTE = originalEnv; } else { delete process.env.TANGLED_REMOTE; } });
describe('loadConfig', () => { it('should return config from TANGLED_REMOTE environment variable', async () => { process.env.TANGLED_REMOTE = 'upstream';
const config = await loadConfig();
expect(config).toEqual({ remote: 'upstream' }); });
it('should load config from file when env var not set', async () => { const mockExplorer = { search: vi.fn().mockResolvedValue({ config: { remote: 'origin' }, filepath: '/test/.tangledrc', isEmpty: false, }), };
vi.mocked(cosmiconfig).mockReturnValue(mockExplorer as never);
// Mock Git root const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), revparse: vi.fn().mockResolvedValue('/test/repo\n'), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const config = await loadConfig('/test/repo');
expect(config).toEqual({ remote: 'origin' }); expect(mockExplorer.search).toHaveBeenCalledWith('/test/repo'); });
it('should return empty config when no config file found', async () => { const mockExplorer = { search: vi.fn().mockResolvedValue(null), };
vi.mocked(cosmiconfig).mockReturnValue(mockExplorer as never);
// Mock Git root const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(false), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const config = await loadConfig();
expect(config).toEqual({}); });
it('should handle cosmiconfig errors gracefully', async () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const mockExplorer = { search: vi.fn().mockRejectedValue(new Error('Config read error')), };
vi.mocked(cosmiconfig).mockReturnValue(mockExplorer as never);
// Mock Git root const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(false), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const config = await loadConfig();
expect(config).toEqual({}); expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to load config'));
consoleWarnSpy.mockRestore(); }); });
describe('getConfiguredRemote', () => { it('should return remote name from config', async () => { process.env.TANGLED_REMOTE = 'upstream';
const remote = await getConfiguredRemote();
expect(remote).toBe('upstream'); });
it('should return null when no config found', async () => { const mockExplorer = { search: vi.fn().mockResolvedValue(null), };
vi.mocked(cosmiconfig).mockReturnValue(mockExplorer as never);
// Mock not in Git repo const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(false), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const remote = await getConfiguredRemote();
expect(remote).toBeNull(); }); });
describe('setLocalRemote', () => { it('should write config to Git root directory', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), revparse: vi.fn().mockResolvedValue('/test/repo\n'), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never); vi.mocked(fs.writeFile).mockResolvedValue(undefined);
await setLocalRemote('origin', '/test/repo');
expect(fs.writeFile).toHaveBeenCalledWith( '/test/repo/.tangledrc', `${JSON.stringify({ remote: 'origin' }, null, 2)}\n`, 'utf-8' ); });
it('should throw error when not in Git repository', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(false), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
await expect(setLocalRemote('origin')).rejects.toThrow('Not in a Git repository'); });
it('should throw error on write failure', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), revparse: vi.fn().mockResolvedValue('/test/repo\n'), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never); vi.mocked(fs.writeFile).mockRejectedValue(new Error('Write failed'));
await expect(setLocalRemote('origin')).rejects.toThrow('Failed to write local config'); }); });
describe('setUserRemote', () => { it('should write config to user home directory', async () => { vi.mocked(fs.mkdir).mockResolvedValue(undefined); vi.mocked(fs.writeFile).mockResolvedValue(undefined);
await setUserRemote('origin');
const expectedPath = join(homedir(), '.tangledrc'); expect(fs.mkdir).toHaveBeenCalledWith(expect.any(String), { recursive: true }); expect(fs.writeFile).toHaveBeenCalledWith( expectedPath, `${JSON.stringify({ remote: 'origin' }, null, 2)}\n`, 'utf-8' ); });
it('should throw error on write failure', async () => { vi.mocked(fs.mkdir).mockResolvedValue(undefined); vi.mocked(fs.writeFile).mockRejectedValue(new Error('Write failed'));
await expect(setUserRemote('origin')).rejects.toThrow('Failed to write user config'); }); });
describe('clearLocalRemote', () => { it('should delete local config file', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), revparse: vi.fn().mockResolvedValue('/test/repo\n'), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never); vi.mocked(fs.unlink).mockResolvedValue(undefined);
await clearLocalRemote('/test/repo');
expect(fs.unlink).toHaveBeenCalledWith('/test/repo/.tangledrc'); });
it('should not throw error if file does not exist', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), revparse: vi.fn().mockResolvedValue('/test/repo\n'), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const error = new Error('File not found') as NodeJS.ErrnoException; error.code = 'ENOENT'; vi.mocked(fs.unlink).mockRejectedValue(error);
await expect(clearLocalRemote()).resolves.not.toThrow(); });
it('should throw error when not in Git repository', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(false), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
await expect(clearLocalRemote()).rejects.toThrow('Not in a Git repository'); }); });
describe('clearUserRemote', () => { it('should delete user config file', async () => { vi.mocked(fs.unlink).mockResolvedValue(undefined);
await clearUserRemote();
const expectedPath = join(homedir(), '.tangledrc'); expect(fs.unlink).toHaveBeenCalledWith(expectedPath); });
it('should not throw error if file does not exist', async () => { const error = new Error('File not found') as NodeJS.ErrnoException; error.code = 'ENOENT'; vi.mocked(fs.unlink).mockRejectedValue(error);
await expect(clearUserRemote()).resolves.not.toThrow(); }); });});