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 main 335 lines 8.9 kB view raw
1import { beforeEach, describe, expect, it, vi } from 'vitest'; 2import type { TangledApiClient } from '../../src/lib/api-client.js'; 3import { buildRepoAtUri, parseAtUri, resolveHandleToDid } from '../../src/utils/at-uri.js'; 4 5// Mock API client 6const createMockClient = (): TangledApiClient => { 7 return { 8 getAgent: vi.fn(() => ({ 9 com: { 10 atproto: { 11 identity: { 12 resolveHandle: vi.fn(), 13 }, 14 }, 15 }, 16 })), 17 } as unknown as TangledApiClient; 18}; 19 20describe('parseAtUri', () => { 21 it('should parse AT-URI with rkey', () => { 22 const uri = 'at://did:plc:abc123/sh.tangled.repo.issue/xyz789'; 23 const result = parseAtUri(uri); 24 25 expect(result).toEqual({ 26 did: 'did:plc:abc123', 27 collection: 'sh.tangled.repo.issue', 28 rkey: 'xyz789', 29 }); 30 }); 31 32 it('should parse AT-URI without rkey', () => { 33 const uri = 'at://did:plc:abc123/sh.tangled.repo'; 34 const result = parseAtUri(uri); 35 36 expect(result).toEqual({ 37 did: 'did:plc:abc123', 38 collection: 'sh.tangled.repo', 39 }); 40 }); 41 42 it('should parse AT-URI with nested collection', () => { 43 const uri = 'at://did:plc:abc123/sh.tangled.repo.issue.state/xyz'; 44 const result = parseAtUri(uri); 45 46 expect(result).toEqual({ 47 did: 'did:plc:abc123', 48 collection: 'sh.tangled.repo.issue.state', 49 rkey: 'xyz', 50 }); 51 }); 52 53 it('should return null for invalid URI', () => { 54 expect(parseAtUri('not-a-uri')).toBeNull(); 55 expect(parseAtUri('http://example.com')).toBeNull(); 56 expect(parseAtUri('at://invalid-did/collection')).toBeNull(); 57 expect(parseAtUri('')).toBeNull(); 58 }); 59 60 it('should handle DIDs with various characters', () => { 61 const uri = 'at://did:web:example.com/collection/rkey'; 62 const result = parseAtUri(uri); 63 64 expect(result).toEqual({ 65 did: 'did:web:example.com', 66 collection: 'collection', 67 rkey: 'rkey', 68 }); 69 }); 70}); 71 72describe('resolveHandleToDid', () => { 73 let mockClient: TangledApiClient; 74 75 beforeEach(() => { 76 mockClient = createMockClient(); 77 }); 78 79 it('should resolve handle to DID', async () => { 80 const mockResolve = vi.fn().mockResolvedValue({ 81 data: { did: 'did:plc:abc123' }, 82 }); 83 84 vi.mocked(mockClient.getAgent).mockReturnValue({ 85 com: { 86 atproto: { 87 identity: { 88 resolveHandle: mockResolve, 89 }, 90 }, 91 }, 92 } as never); 93 94 const result = await resolveHandleToDid('mark.bsky.social', mockClient); 95 96 expect(result).toBe('did:plc:abc123'); 97 expect(mockResolve).toHaveBeenCalledWith({ handle: 'mark.bsky.social' }); 98 }); 99 100 it('should strip leading @ from handle', async () => { 101 const mockResolve = vi.fn().mockResolvedValue({ 102 data: { did: 'did:plc:abc123' }, 103 }); 104 105 vi.mocked(mockClient.getAgent).mockReturnValue({ 106 com: { 107 atproto: { 108 identity: { 109 resolveHandle: mockResolve, 110 }, 111 }, 112 }, 113 } as never); 114 115 await resolveHandleToDid('@mark.bsky.social', mockClient); 116 117 expect(mockResolve).toHaveBeenCalledWith({ handle: 'mark.bsky.social' }); 118 }); 119 120 it('should throw error when handle not found', async () => { 121 const mockResolve = vi.fn().mockResolvedValue({ 122 data: { did: null }, 123 }); 124 125 vi.mocked(mockClient.getAgent).mockReturnValue({ 126 com: { 127 atproto: { 128 identity: { 129 resolveHandle: mockResolve, 130 }, 131 }, 132 }, 133 } as never); 134 135 await expect(resolveHandleToDid('nonexistent.bsky.social', mockClient)).rejects.toThrow( 136 'No DID found for handle: nonexistent.bsky.social' 137 ); 138 }); 139 140 it('should throw error on network failure', async () => { 141 const mockResolve = vi.fn().mockRejectedValue(new Error('Network error')); 142 143 vi.mocked(mockClient.getAgent).mockReturnValue({ 144 com: { 145 atproto: { 146 identity: { 147 resolveHandle: mockResolve, 148 }, 149 }, 150 }, 151 } as never); 152 153 await expect(resolveHandleToDid('mark.bsky.social', mockClient)).rejects.toThrow( 154 "Failed to resolve handle 'mark.bsky.social': Network error" 155 ); 156 }); 157}); 158 159describe('buildRepoAtUri', () => { 160 let mockClient: TangledApiClient; 161 162 beforeEach(() => { 163 mockClient = createMockClient(); 164 }); 165 166 it('should query PDS and use repo record rkey', async () => { 167 const mockListRecords = vi.fn().mockResolvedValue({ 168 data: { 169 records: [ 170 { 171 uri: 'at://did:plc:abc123/sh.tangled.repo/3mef23waqwq22', 172 value: { name: 'my-repo', description: 'Test repo' }, 173 }, 174 ], 175 }, 176 }); 177 178 vi.mocked(mockClient.getAgent).mockReturnValue({ 179 com: { 180 atproto: { 181 repo: { 182 listRecords: mockListRecords, 183 }, 184 }, 185 }, 186 } as never); 187 188 const result = await buildRepoAtUri('did:plc:abc123', 'my-repo', mockClient); 189 190 expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/3mef23waqwq22'); 191 expect(mockListRecords).toHaveBeenCalledWith({ 192 repo: 'did:plc:abc123', 193 collection: 'sh.tangled.repo', 194 limit: 100, 195 }); 196 }); 197 198 it('should resolve handle then query for repo record', async () => { 199 const mockResolve = vi.fn().mockResolvedValue({ 200 data: { did: 'did:plc:abc123' }, 201 }); 202 203 const mockListRecords = vi.fn().mockResolvedValue({ 204 data: { 205 records: [ 206 { 207 uri: 'at://did:plc:abc123/sh.tangled.repo/xyz789', 208 value: { name: 'my-repo' }, 209 }, 210 ], 211 }, 212 }); 213 214 vi.mocked(mockClient.getAgent).mockReturnValue({ 215 com: { 216 atproto: { 217 identity: { 218 resolveHandle: mockResolve, 219 }, 220 repo: { 221 listRecords: mockListRecords, 222 }, 223 }, 224 }, 225 } as never); 226 227 const result = await buildRepoAtUri('mark.bsky.social', 'my-repo', mockClient); 228 229 expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/xyz789'); 230 expect(mockResolve).toHaveBeenCalledWith({ handle: 'mark.bsky.social' }); 231 expect(mockListRecords).toHaveBeenCalledWith({ 232 repo: 'did:plc:abc123', 233 collection: 'sh.tangled.repo', 234 limit: 100, 235 }); 236 }); 237 238 it('should find correct repo among multiple records', async () => { 239 const mockListRecords = vi.fn().mockResolvedValue({ 240 data: { 241 records: [ 242 { 243 uri: 'at://did:plc:abc123/sh.tangled.repo/aaa111', 244 value: { name: 'other-repo' }, 245 }, 246 { 247 uri: 'at://did:plc:abc123/sh.tangled.repo/bbb222', 248 value: { name: 'target-repo' }, 249 }, 250 { 251 uri: 'at://did:plc:abc123/sh.tangled.repo/ccc333', 252 value: { name: 'another-repo' }, 253 }, 254 ], 255 }, 256 }); 257 258 vi.mocked(mockClient.getAgent).mockReturnValue({ 259 com: { 260 atproto: { 261 repo: { 262 listRecords: mockListRecords, 263 }, 264 }, 265 }, 266 } as never); 267 268 const result = await buildRepoAtUri('did:plc:abc123', 'target-repo', mockClient); 269 270 expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/bbb222'); 271 }); 272 273 it('should throw error when repository not found', async () => { 274 const mockListRecords = vi.fn().mockResolvedValue({ 275 data: { 276 records: [ 277 { 278 uri: 'at://did:plc:abc123/sh.tangled.repo/xyz789', 279 value: { name: 'different-repo' }, 280 }, 281 ], 282 }, 283 }); 284 285 vi.mocked(mockClient.getAgent).mockReturnValue({ 286 com: { 287 atproto: { 288 repo: { 289 listRecords: mockListRecords, 290 }, 291 }, 292 }, 293 } as never); 294 295 await expect(buildRepoAtUri('did:plc:abc123', 'nonexistent-repo', mockClient)).rejects.toThrow( 296 "Repository 'nonexistent-repo' not found for did:plc:abc123" 297 ); 298 }); 299 300 it('should throw error when handle resolution fails', async () => { 301 const mockResolve = vi.fn().mockRejectedValue(new Error('Resolution failed')); 302 303 vi.mocked(mockClient.getAgent).mockReturnValue({ 304 com: { 305 atproto: { 306 identity: { 307 resolveHandle: mockResolve, 308 }, 309 }, 310 }, 311 } as never); 312 313 await expect(buildRepoAtUri('mark.bsky.social', 'my-repo', mockClient)).rejects.toThrow( 314 "Failed to resolve handle 'mark.bsky.social': Resolution failed" 315 ); 316 }); 317 318 it('should throw error when listRecords fails', async () => { 319 const mockListRecords = vi.fn().mockRejectedValue(new Error('API error')); 320 321 vi.mocked(mockClient.getAgent).mockReturnValue({ 322 com: { 323 atproto: { 324 repo: { 325 listRecords: mockListRecords, 326 }, 327 }, 328 }, 329 } as never); 330 331 await expect(buildRepoAtUri('did:plc:abc123', 'my-repo', mockClient)).rejects.toThrow( 332 'Failed to resolve repository AT-URI: API error' 333 ); 334 }); 335});