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.9 kB · 335 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336import { beforeEach, describe, expect, it, vi } from 'vitest';import type { TangledApiClient } from '../../src/lib/api-client.js';import { buildRepoAtUri, parseAtUri, resolveHandleToDid } from '../../src/utils/at-uri.js';
// Mock API clientconst createMockClient = (): TangledApiClient => { return { getAgent: vi.fn(() => ({ com: { atproto: { identity: { resolveHandle: vi.fn(), }, }, }, })), } as unknown as TangledApiClient;};
describe('parseAtUri', () => { it('should parse AT-URI with rkey', () => { const uri = 'at://did:plc:abc123/sh.tangled.repo.issue/xyz789'; const result = parseAtUri(uri);
expect(result).toEqual({ did: 'did:plc:abc123', collection: 'sh.tangled.repo.issue', rkey: 'xyz789', }); });
it('should parse AT-URI without rkey', () => { const uri = 'at://did:plc:abc123/sh.tangled.repo'; const result = parseAtUri(uri);
expect(result).toEqual({ did: 'did:plc:abc123', collection: 'sh.tangled.repo', }); });
it('should parse AT-URI with nested collection', () => { const uri = 'at://did:plc:abc123/sh.tangled.repo.issue.state/xyz'; const result = parseAtUri(uri);
expect(result).toEqual({ did: 'did:plc:abc123', collection: 'sh.tangled.repo.issue.state', rkey: 'xyz', }); });
it('should return null for invalid URI', () => { expect(parseAtUri('not-a-uri')).toBeNull(); expect(parseAtUri('http://example.com')).toBeNull(); expect(parseAtUri('at://invalid-did/collection')).toBeNull(); expect(parseAtUri('')).toBeNull(); });
it('should handle DIDs with various characters', () => { const uri = 'at://did:web:example.com/collection/rkey'; const result = parseAtUri(uri);
expect(result).toEqual({ did: 'did:web:example.com', collection: 'collection', rkey: 'rkey', }); });});
describe('resolveHandleToDid', () => { let mockClient: TangledApiClient;
beforeEach(() => { mockClient = createMockClient(); });
it('should resolve handle to DID', async () => { const mockResolve = vi.fn().mockResolvedValue({ data: { did: 'did:plc:abc123' }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { identity: { resolveHandle: mockResolve, }, }, }, } as never);
const result = await resolveHandleToDid('mark.bsky.social', mockClient);
expect(result).toBe('did:plc:abc123'); expect(mockResolve).toHaveBeenCalledWith({ handle: 'mark.bsky.social' }); });
it('should strip leading @ from handle', async () => { const mockResolve = vi.fn().mockResolvedValue({ data: { did: 'did:plc:abc123' }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { identity: { resolveHandle: mockResolve, }, }, }, } as never);
await resolveHandleToDid('@mark.bsky.social', mockClient);
expect(mockResolve).toHaveBeenCalledWith({ handle: 'mark.bsky.social' }); });
it('should throw error when handle not found', async () => { const mockResolve = vi.fn().mockResolvedValue({ data: { did: null }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { identity: { resolveHandle: mockResolve, }, }, }, } as never);
await expect(resolveHandleToDid('nonexistent.bsky.social', mockClient)).rejects.toThrow( 'No DID found for handle: nonexistent.bsky.social' ); });
it('should throw error on network failure', async () => { const mockResolve = vi.fn().mockRejectedValue(new Error('Network error'));
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { identity: { resolveHandle: mockResolve, }, }, }, } as never);
await expect(resolveHandleToDid('mark.bsky.social', mockClient)).rejects.toThrow( "Failed to resolve handle 'mark.bsky.social': Network error" ); });});
describe('buildRepoAtUri', () => { let mockClient: TangledApiClient;
beforeEach(() => { mockClient = createMockClient(); });
it('should query PDS and use repo record rkey', async () => { const mockListRecords = vi.fn().mockResolvedValue({ data: { records: [ { uri: 'at://did:plc:abc123/sh.tangled.repo/3mef23waqwq22', value: { name: 'my-repo', description: 'Test repo' }, }, ], }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { listRecords: mockListRecords, }, }, }, } as never);
const result = await buildRepoAtUri('did:plc:abc123', 'my-repo', mockClient);
expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/3mef23waqwq22'); expect(mockListRecords).toHaveBeenCalledWith({ repo: 'did:plc:abc123', collection: 'sh.tangled.repo', limit: 100, }); });
it('should resolve handle then query for repo record', async () => { const mockResolve = vi.fn().mockResolvedValue({ data: { did: 'did:plc:abc123' }, });
const mockListRecords = vi.fn().mockResolvedValue({ data: { records: [ { uri: 'at://did:plc:abc123/sh.tangled.repo/xyz789', value: { name: 'my-repo' }, }, ], }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { identity: { resolveHandle: mockResolve, }, repo: { listRecords: mockListRecords, }, }, }, } as never);
const result = await buildRepoAtUri('mark.bsky.social', 'my-repo', mockClient);
expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/xyz789'); expect(mockResolve).toHaveBeenCalledWith({ handle: 'mark.bsky.social' }); expect(mockListRecords).toHaveBeenCalledWith({ repo: 'did:plc:abc123', collection: 'sh.tangled.repo', limit: 100, }); });
it('should find correct repo among multiple records', async () => { const mockListRecords = vi.fn().mockResolvedValue({ data: { records: [ { uri: 'at://did:plc:abc123/sh.tangled.repo/aaa111', value: { name: 'other-repo' }, }, { uri: 'at://did:plc:abc123/sh.tangled.repo/bbb222', value: { name: 'target-repo' }, }, { uri: 'at://did:plc:abc123/sh.tangled.repo/ccc333', value: { name: 'another-repo' }, }, ], }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { listRecords: mockListRecords, }, }, }, } as never);
const result = await buildRepoAtUri('did:plc:abc123', 'target-repo', mockClient);
expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/bbb222'); });
it('should throw error when repository not found', async () => { const mockListRecords = vi.fn().mockResolvedValue({ data: { records: [ { uri: 'at://did:plc:abc123/sh.tangled.repo/xyz789', value: { name: 'different-repo' }, }, ], }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { listRecords: mockListRecords, }, }, }, } as never);
await expect(buildRepoAtUri('did:plc:abc123', 'nonexistent-repo', mockClient)).rejects.toThrow( "Repository 'nonexistent-repo' not found for did:plc:abc123" ); });
it('should throw error when handle resolution fails', async () => { const mockResolve = vi.fn().mockRejectedValue(new Error('Resolution failed'));
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { identity: { resolveHandle: mockResolve, }, }, }, } as never);
await expect(buildRepoAtUri('mark.bsky.social', 'my-repo', mockClient)).rejects.toThrow( "Failed to resolve handle 'mark.bsky.social': Resolution failed" ); });
it('should throw error when listRecords fails', async () => { const mockListRecords = vi.fn().mockRejectedValue(new Error('API error'));
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { listRecords: mockListRecords, }, }, }, } as never);
await expect(buildRepoAtUri('did:plc:abc123', 'my-repo', mockClient)).rejects.toThrow( 'Failed to resolve repository AT-URI: API error' ); });});