Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/just-cameron/tangled-cli. Automation-first CLI for Tangled.org
Something went wrong. Try again.
15 kB · 436 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437import { beforeEach, describe, expect, it, vi } from 'vitest';import type { RepositoryContext } from '../../src/lib/context.js';import { getCurrentRepoContext, getTangledRemotes, promptForRemote,} from '../../src/lib/context.js';
// Mock modulesvi.mock('simple-git');vi.mock('../../src/lib/config.js');vi.mock('../../src/utils/prompts.js');
// Import mocked modulesimport { simpleGit } from 'simple-git';import * as configModule from '../../src/lib/config.js';import * as promptsModule from '../../src/utils/prompts.js';
describe('Context Resolution', () => { beforeEach(() => { vi.clearAllMocks(); });
describe('getTangledRemotes', () => { it('should return empty array when not in a Git repository', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(false), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const remotes = await getTangledRemotes();
expect(remotes).toEqual([]); });
it('should return empty array when no tangled remotes exist', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), getRemotes: vi .fn() .mockResolvedValue([{ name: 'origin', refs: { fetch: 'git@github.com:user/repo.git' } }]), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const remotes = await getTangledRemotes();
expect(remotes).toEqual([]); });
it('should parse SSH tangled remote', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), getRemotes: vi.fn().mockResolvedValue([ { name: 'origin', refs: { fetch: 'git@tangled.org:did:plc:b2mcbcamkwyznc5fkplwlxbf/tangled-cli.git' }, }, ]), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const remotes = await getTangledRemotes();
expect(remotes).toEqual([ { owner: 'did:plc:b2mcbcamkwyznc5fkplwlxbf', ownerType: 'did', name: 'tangled-cli', remoteName: 'origin', remoteUrl: 'git@tangled.org:did:plc:b2mcbcamkwyznc5fkplwlxbf/tangled-cli.git', protocol: 'ssh', publicUrl: 'https://tangled.org/did:plc:b2mcbcamkwyznc5fkplwlxbf/tangled-cli', selectionSource: 'git', }, ]); });
it('should parse HTTPS tangled remote', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), getRemotes: vi.fn().mockResolvedValue([ { name: 'origin', refs: { fetch: 'https://tangled.org/alice.bsky.social/tangled-cli' }, }, ]), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const remotes = await getTangledRemotes();
expect(remotes).toEqual([ { owner: 'alice.bsky.social', ownerType: 'handle', name: 'tangled-cli', remoteName: 'origin', remoteUrl: 'https://tangled.org/alice.bsky.social/tangled-cli', protocol: 'https', publicUrl: 'https://tangled.org/alice.bsky.social/tangled-cli', selectionSource: 'git', }, ]); });
it('should parse multiple tangled remotes', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), getRemotes: vi.fn().mockResolvedValue([ { name: 'origin', refs: { fetch: 'git@tangled.org:did:plc:abc123/repo.git' }, }, { name: 'upstream', refs: { fetch: 'git@tangled.org:did:plc:xyz789/repo.git' }, }, ]), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const remotes = await getTangledRemotes();
expect(remotes).toHaveLength(2); expect(remotes[0].remoteName).toBe('origin'); expect(remotes[1].remoteName).toBe('upstream'); });
it('should skip invalid tangled remotes with warning', async () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), getRemotes: vi.fn().mockResolvedValue([ { name: 'invalid', refs: { fetch: 'git@tangled.org:invalid' }, // Missing repo name }, { name: 'valid', refs: { fetch: 'git@tangled.org:did:plc:abc123/repo.git' }, }, ]), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const remotes = await getTangledRemotes();
expect(remotes).toHaveLength(1); expect(remotes[0].remoteName).toBe('valid'); expect(consoleWarnSpy).toHaveBeenCalledWith( expect.stringContaining('Invalid tangled.org remote URL') );
consoleWarnSpy.mockRestore(); });
it('should handle Git errors gracefully', async () => { const mockGit = { checkIsRepo: vi.fn().mockRejectedValue(new Error('Git error')), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const remotes = await getTangledRemotes();
expect(remotes).toEqual([]); }); });
describe('promptForRemote', () => { it('should return single remote without prompting', async () => { const remote: RepositoryContext = { owner: 'did:plc:abc123', ownerType: 'did', name: 'repo', remoteName: 'origin', remoteUrl: 'git@tangled.org:did:plc:abc123/repo.git', protocol: 'ssh', publicUrl: 'https://tangled.org/did:plc:abc123/repo', selectionSource: 'git', };
const result = await promptForRemote([remote]);
expect(result).toBe(remote); expect(promptsModule.promptForRemoteSelection).not.toHaveBeenCalled(); });
it('should prompt when multiple remotes available', async () => { const remotes: RepositoryContext[] = [ { owner: 'did:plc:abc123', ownerType: 'did', name: 'repo', remoteName: 'origin', remoteUrl: 'git@tangled.org:did:plc:abc123/repo.git', protocol: 'ssh', }, { owner: 'did:plc:xyz789', ownerType: 'did', name: 'repo', remoteName: 'upstream', remoteUrl: 'git@tangled.org:did:plc:xyz789/repo.git', protocol: 'ssh', }, ];
vi.mocked(promptsModule.promptForRemoteSelection).mockResolvedValue('upstream');
const result = await promptForRemote(remotes);
expect(result.remoteName).toBe('upstream'); expect(promptsModule.promptForRemoteSelection).toHaveBeenCalledWith([ { name: 'origin', url: 'git@tangled.org:did:plc:abc123/repo.git' }, { name: 'upstream', url: 'git@tangled.org:did:plc:xyz789/repo.git' }, ]); });
it('should throw error when no remotes provided', async () => { await expect(promptForRemote([])).rejects.toThrow('No remotes available to select from'); }); });
describe('getCurrentRepoContext', () => { it('should return null when not in a Git repository', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(false), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const context = await getCurrentRepoContext();
expect(context).toBeNull(); });
it('should return null when no tangled remotes exist', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), getRemotes: vi .fn() .mockResolvedValue([{ name: 'origin', refs: { fetch: 'git@github.com:user/repo.git' } }]), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const context = await getCurrentRepoContext();
expect(context).toBeNull(); });
it('should return single tangled remote', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), getRemotes: vi.fn().mockResolvedValue([ { name: 'origin', refs: { fetch: 'git@tangled.org:did:plc:abc123/repo.git' }, }, ]), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never);
const context = await getCurrentRepoContext();
expect(context).toEqual({ owner: 'did:plc:abc123', ownerType: 'did', name: 'repo', remoteName: 'origin', remoteUrl: 'git@tangled.org:did:plc:abc123/repo.git', protocol: 'ssh', publicUrl: 'https://tangled.org/did:plc:abc123/repo', selectionSource: 'git', }); });
it('should use configured remote when multiple remotes exist', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), getRemotes: vi.fn().mockResolvedValue([ { name: 'origin', refs: { fetch: 'git@tangled.org:did:plc:abc123/repo.git' }, }, { name: 'upstream', refs: { fetch: 'git@tangled.org:did:plc:xyz789/repo.git' }, }, ]), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never); vi.mocked(configModule.getConfiguredRemote).mockResolvedValue('upstream');
const context = await getCurrentRepoContext();
expect(context?.remoteName).toBe('upstream'); });
it('should fall back to origin when config points to non-existent remote', async () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), getRemotes: vi.fn().mockResolvedValue([ { name: 'origin', refs: { fetch: 'git@tangled.org:did:plc:abc123/repo.git' }, }, { name: 'upstream', refs: { fetch: 'git@tangled.org:did:plc:xyz789/repo.git' }, }, ]), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never); vi.mocked(configModule.getConfiguredRemote).mockResolvedValue('nonexistent');
const context = await getCurrentRepoContext();
expect(context?.remoteName).toBe('origin'); expect(consoleWarnSpy).toHaveBeenCalledWith( expect.stringContaining('Configured remote "nonexistent" not found') );
consoleWarnSpy.mockRestore(); });
it('should prefer origin remote when no config set', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), getRemotes: vi.fn().mockResolvedValue([ { name: 'upstream', refs: { fetch: 'git@tangled.org:did:plc:abc123/repo.git' }, }, { name: 'origin', refs: { fetch: 'git@tangled.org:did:plc:xyz789/repo.git' }, }, ]), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never); vi.mocked(configModule.getConfiguredRemote).mockResolvedValue(null);
const context = await getCurrentRepoContext();
expect(context?.remoteName).toBe('origin'); });
it('should prompt when no origin and no config', async () => { const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), getRemotes: vi.fn().mockResolvedValue([ { name: 'upstream', refs: { fetch: 'git@tangled.org:did:plc:abc123/repo.git' }, }, { name: 'fork', refs: { fetch: 'git@tangled.org:did:plc:xyz789/repo.git' }, }, ]), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never); vi.mocked(configModule.getConfiguredRemote).mockResolvedValue(null); vi.mocked(promptsModule.promptForRemoteSelection).mockResolvedValue('fork'); vi.mocked(promptsModule.promptToSaveRemote).mockResolvedValue(false);
const context = await getCurrentRepoContext();
expect(context?.remoteName).toBe('fork'); expect(promptsModule.promptForRemoteSelection).toHaveBeenCalled(); });
it('should save config when user confirms', async () => { const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), getRemotes: vi.fn().mockResolvedValue([ { name: 'upstream', refs: { fetch: 'git@tangled.org:did:plc:abc123/repo.git' }, }, { name: 'fork', refs: { fetch: 'git@tangled.org:did:plc:xyz789/repo.git' }, }, ]), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never); vi.mocked(configModule.getConfiguredRemote).mockResolvedValue(null); vi.mocked(promptsModule.promptForRemoteSelection).mockResolvedValue('fork'); vi.mocked(promptsModule.promptToSaveRemote).mockResolvedValue(true); vi.mocked(configModule.setLocalRemote).mockResolvedValue(undefined);
const context = await getCurrentRepoContext();
expect(context?.remoteName).toBe('fork'); expect(configModule.setLocalRemote).toHaveBeenCalledWith('fork', process.cwd()); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Saved remote "fork"'));
consoleLogSpy.mockRestore(); });
it('should continue even if saving config fails', async () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const mockGit = { checkIsRepo: vi.fn().mockResolvedValue(true), getRemotes: vi.fn().mockResolvedValue([ { name: 'upstream', refs: { fetch: 'git@tangled.org:did:plc:abc123/repo.git' }, }, { name: 'fork', refs: { fetch: 'git@tangled.org:did:plc:xyz789/repo.git' }, }, ]), }; vi.mocked(simpleGit).mockReturnValue(mockGit as never); vi.mocked(configModule.getConfiguredRemote).mockResolvedValue(null); vi.mocked(promptsModule.promptForRemoteSelection).mockResolvedValue('upstream'); vi.mocked(promptsModule.promptToSaveRemote).mockResolvedValue(true); vi.mocked(configModule.setLocalRemote).mockRejectedValue(new Error('Write failed'));
const context = await getCurrentRepoContext();
expect(context?.remoteName).toBe('upstream'); expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to save config'));
consoleWarnSpy.mockRestore(); }); });});