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! :)
0

Configure Feed

Select the types of activity you want to include in your feed.

Fix issue listing for cross-PDS repositories

* Rename buildRepoAtUri → resolveRepoId; return repoDid from the
sh.tangled.repo record instead of the record's AT-URI, since issue
records reference repoDid in their .repo field
* Fix repo name matching to check both rkey and value.name, handling
older tangled.org records that use an opaque TID as rkey

Co-authored-by: Claude (claude-sonnet-4-6) <noreply@anthropic.com>

authored by

Ken-ichi Ueda
Claude (claude-sonnet-4-6)
and committed by
Tangled
(Jun 1, 2026, 9:07 PM +0300) da67dcd5 b112c6a1

+157 -225
+7 -7
src/commands/issue.ts
··· 13 13 resolveSequentialNumber, 14 14 updateIssue, 15 15 } from '../lib/issues-api.js'; 16 - import { buildRepoAtUri } from '../utils/at-uri.js'; 16 + import { resolveRepoId } from '../utils/at-uri.js'; 17 17 import { ensureAuthenticated, requireAuth } from '../utils/auth-helpers.js'; 18 18 import { readBodyInput } from '../utils/body-input.js'; 19 19 import { formatDate, formatIssueState, outputJson } from '../utils/formatting.js'; ··· 122 122 } 123 123 124 124 // 3. Build repo AT-URI 125 - const repoAtUri = await buildRepoAtUri(context.owner, context.name, client); 125 + const repoAtUri = await resolveRepoId(context.owner, context.name); 126 126 127 127 // 4. Resolve issue ID to URI 128 128 const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoAtUri); ··· 195 195 } 196 196 197 197 // 4. Build repo AT-URI 198 - const repoAtUri = await buildRepoAtUri(context.owner, context.name, client); 198 + const repoAtUri = await resolveRepoId(context.owner, context.name); 199 199 200 200 // 5. Resolve issue ID to URI 201 201 const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoAtUri); ··· 275 275 } 276 276 277 277 // 3. Build repo AT-URI 278 - const repoAtUri = await buildRepoAtUri(context.owner, context.name, client); 278 + const repoAtUri = await resolveRepoId(context.owner, context.name); 279 279 280 280 // 4. Resolve issue ID to URI 281 281 const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoAtUri); ··· 332 332 } 333 333 334 334 // 3. Build repo AT-URI 335 - const repoAtUri = await buildRepoAtUri(context.owner, context.name, client); 335 + const repoAtUri = await resolveRepoId(context.owner, context.name); 336 336 337 337 // 4. Resolve issue ID to URI 338 338 const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoAtUri); ··· 421 421 } 422 422 423 423 // 5. Build repo AT-URI 424 - const repoAtUri = await buildRepoAtUri(context.owner, context.name, client); 424 + const repoAtUri = await resolveRepoId(context.owner, context.name); 425 425 426 426 // 6. Create issue (suppress progress message in JSON mode) 427 427 if (options.json === undefined) { ··· 496 496 } 497 497 498 498 // 3. Build repo AT-URI 499 - const repoAtUri = await buildRepoAtUri(context.owner, context.name, client); 499 + const repoAtUri = await resolveRepoId(context.owner, context.name); 500 500 501 501 // 4. Fetch issues 502 502 const limit = Number.parseInt(options.limit, 10);
+40 -27
src/utils/at-uri.ts
··· 1 - import type { TangledApiClient } from '../lib/api-client.js'; 1 + import { AtpAgent } from '@atproto/api'; 2 + import { IdResolver } from '@atproto/identity'; 3 + import { resolvePdsFromIdentifier } from './pds-resolver.js'; 2 4 3 5 /** 4 6 * Parse an AT-URI into its components ··· 28 30 } 29 31 30 32 /** 31 - * Resolve a handle to a DID using the AT Protocol identity resolution 33 + * Resolve a handle to a DID using AT Protocol identity resolution. 34 + * Uses @atproto/identity directly rather than routing through the authenticated 35 + * agent, so the result is independent of which PDS the caller is logged into. 36 + * 32 37 * @param handle - Handle string (e.g., "mark.bsky.social" or "@mark.bsky.social") 33 - * @param client - Authenticated API client 34 38 * @returns DID string (e.g., "did:plc:abc123") 35 39 * @throws Error if handle cannot be resolved 36 40 */ 37 - export async function resolveHandleToDid( 38 - handle: string, 39 - client: TangledApiClient 40 - ): Promise<string> { 41 + export async function resolveHandleToDid(handle: string): Promise<string> { 41 42 // Strip leading @ if present 42 43 const cleanHandle = handle.startsWith('@') ? handle.slice(1) : handle; 43 44 44 45 try { 45 - const response = await client.getAgent().com.atproto.identity.resolveHandle({ 46 - handle: cleanHandle, 47 - }); 46 + const resolver = new IdResolver(); 47 + const did = await resolver.handle.resolve(cleanHandle); 48 48 49 - if (!response.data.did) { 49 + if (!did) { 50 50 throw new Error(`No DID found for handle: ${cleanHandle}`); 51 51 } 52 52 53 - return response.data.did; 53 + return did; 54 54 } catch (error) { 55 + if (error instanceof Error && error.message.startsWith('No DID found')) { 56 + throw error; 57 + } 55 58 if (error instanceof Error) { 56 59 throw new Error(`Failed to resolve handle '${cleanHandle}': ${error.message}`); 57 60 } ··· 60 63 } 61 64 62 65 /** 63 - * Build a repository AT-URI from owner and repository name 66 + * Resolve a repository identifier to its repo DID (the identifier used in issue records). 67 + * Resolves the owner's PDS independently of the caller's authenticated session, 68 + * so cross-PDS lookups (e.g., a tngl.sh user looking up a bsky.social repo) work. 69 + * 70 + * Issue records store `repoDid` in their `.repo` field, not the AT-URI of the 71 + * `sh.tangled.repo` record. Falls back to the record's AT-URI if `repoDid` is absent. 72 + * 64 73 * @param ownerDidOrHandle - DID (e.g., "did:plc:abc") or handle (e.g., "mark.bsky.social") 65 74 * @param repoName - Repository name 66 - * @param client - Authenticated API client 67 - * @returns AT-URI string (e.g., "at://did:plc:abc/sh.tangled.repo/3mef23waqwq22") 75 + * @returns repoDid string, or record AT-URI as fallback 68 76 * @throws Error if repository not found 69 77 */ 70 - export async function buildRepoAtUri( 71 - ownerDidOrHandle: string, 72 - repoName: string, 73 - client: TangledApiClient 74 - ): Promise<string> { 78 + export async function resolveRepoId(ownerDidOrHandle: string, repoName: string): Promise<string> { 75 79 // Resolve owner to DID 76 80 const isDid = ownerDidOrHandle.startsWith('did:'); 77 - const did = isDid ? ownerDidOrHandle : await resolveHandleToDid(ownerDidOrHandle, client); 81 + const did = isDid ? ownerDidOrHandle : await resolveHandleToDid(ownerDidOrHandle); 78 82 79 83 try { 84 + // Resolve the owner's PDS and query their records directly, 85 + // bypassing the caller's authenticated agent entirely 86 + const ownerPds = await resolvePdsFromIdentifier(did); 87 + const ownerAgent = new AtpAgent({ service: ownerPds }); 88 + 80 89 // Query for sh.tangled.repo records 81 - const response = await client.getAgent().com.atproto.repo.listRecords({ 90 + const response = await ownerAgent.com.atproto.repo.listRecords({ 82 91 repo: did, 83 92 collection: 'sh.tangled.repo', 84 93 limit: 100, // Reasonable limit for most users 85 94 }); 86 95 87 - // Find the record matching the repo name 96 + // Find the record matching the repo name. 97 + // Newer records use the repo name as the rkey; older records store the name 98 + // in value.name with an opaque tid as the rkey. Check both. 88 99 const repoRecord = response.data.records.find((record) => { 89 - const recordData = record.value as { name?: string }; 90 - return recordData.name === repoName; 100 + const rkey = record.uri.split('/').pop(); 101 + const recordName = (record.value as { name?: string }).name; 102 + return rkey === repoName || recordName === repoName; 91 103 }); 92 104 93 105 if (!repoRecord) { 94 106 throw new Error(`Repository '${repoName}' not found for ${ownerDidOrHandle}`); 95 107 } 96 108 97 - // Return the record's URI (which includes the correct rkey) 98 - return repoRecord.uri; 109 + // Return repoDid if present; fall back to record URI 110 + const repoDid = (repoRecord.value as { repoDid?: string }).repoDid; 111 + return repoDid ?? repoRecord.uri; 99 112 } catch (error) { 100 113 if (error instanceof Error) { 101 114 throw new Error(`Failed to resolve repository AT-URI: ${error.message}`);
+6 -6
tests/commands/issue.test.ts
··· 49 49 }); 50 50 51 51 // Mock AT-URI builder 52 - vi.mocked(atUri.buildRepoAtUri).mockResolvedValue( 52 + vi.mocked(atUri.resolveRepoId).mockResolvedValue( 53 53 'at://did:plc:abc123/sh.tangled.repo/test-repo' 54 54 ); 55 55 ··· 341 341 }); 342 342 343 343 // Mock AT-URI builder 344 - vi.mocked(atUri.buildRepoAtUri).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); 344 + vi.mocked(atUri.resolveRepoId).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); 345 345 }); 346 346 347 347 afterEach(() => { ··· 604 604 protocol: 'ssh', 605 605 }); 606 606 607 - vi.mocked(atUri.buildRepoAtUri).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); 607 + vi.mocked(atUri.resolveRepoId).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); 608 608 609 609 vi.mocked(authHelpers.requireAuth).mockResolvedValue({ 610 610 did: 'did:plc:abc123', ··· 830 830 protocol: 'ssh', 831 831 }); 832 832 833 - vi.mocked(atUri.buildRepoAtUri).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); 833 + vi.mocked(atUri.resolveRepoId).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); 834 834 835 835 vi.mocked(bodyInput.readBodyInput).mockResolvedValue(undefined); 836 836 vi.mocked(authHelpers.requireAuth).mockResolvedValue({ ··· 1004 1004 protocol: 'ssh', 1005 1005 }); 1006 1006 1007 - vi.mocked(atUri.buildRepoAtUri).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); 1007 + vi.mocked(atUri.resolveRepoId).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); 1008 1008 vi.mocked(issuesApi.getCompleteIssueData).mockResolvedValue({ 1009 1009 number: 1, 1010 1010 title: mockIssue.title, ··· 1121 1121 protocol: 'ssh', 1122 1122 }); 1123 1123 1124 - vi.mocked(atUri.buildRepoAtUri).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); 1124 + vi.mocked(atUri.resolveRepoId).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); 1125 1125 vi.mocked(issuesApi.getCompleteIssueData).mockResolvedValue({ 1126 1126 number: 1, 1127 1127 title: mockIssue.title,
+104 -185
tests/utils/at-uri.test.ts
··· 1 1 import { beforeEach, describe, expect, it, vi } from 'vitest'; 2 - import type { TangledApiClient } from '../../src/lib/api-client.js'; 3 - import { buildRepoAtUri, parseAtUri, resolveHandleToDid } from '../../src/utils/at-uri.js'; 2 + import { parseAtUri, resolveHandleToDid, resolveRepoId } from '../../src/utils/at-uri.js'; 4 3 5 - // Mock API client 6 - const 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 - }; 4 + const { mockHandleResolve, mockListRecords, mockResolvePds } = vi.hoisted(() => ({ 5 + mockHandleResolve: vi.fn(), 6 + mockListRecords: vi.fn(), 7 + mockResolvePds: vi.fn().mockResolvedValue('https://pds.example.com'), 8 + })); 9 + 10 + vi.mock('@atproto/identity', () => ({ 11 + IdResolver: vi.fn().mockImplementation(() => ({ 12 + handle: { resolve: mockHandleResolve }, 13 + })), 14 + })); 15 + 16 + vi.mock('../../src/utils/pds-resolver.js', () => ({ 17 + resolvePdsFromIdentifier: mockResolvePds, 18 + })); 19 + 20 + vi.mock('@atproto/api', () => ({ 21 + AtpAgent: vi.fn().mockImplementation(() => ({ 22 + com: { atproto: { repo: { listRecords: mockListRecords } } }, 23 + })), 24 + })); 19 25 20 26 describe('parseAtUri', () => { 21 27 it('should parse AT-URI with rkey', () => { ··· 70 76 }); 71 77 72 78 describe('resolveHandleToDid', () => { 73 - let mockClient: TangledApiClient; 74 - 75 79 beforeEach(() => { 76 - mockClient = createMockClient(); 80 + vi.clearAllMocks(); 77 81 }); 78 82 79 83 it('should resolve handle to DID', async () => { 80 - const mockResolve = vi.fn().mockResolvedValue({ 81 - data: { did: 'did:plc:abc123' }, 82 - }); 84 + mockHandleResolve.mockResolvedValue('did:plc:abc123'); 83 85 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); 86 + const result = await resolveHandleToDid('mark.bsky.social'); 95 87 96 88 expect(result).toBe('did:plc:abc123'); 97 - expect(mockResolve).toHaveBeenCalledWith({ handle: 'mark.bsky.social' }); 89 + expect(mockHandleResolve).toHaveBeenCalledWith('mark.bsky.social'); 98 90 }); 99 91 100 92 it('should strip leading @ from handle', async () => { 101 - const mockResolve = vi.fn().mockResolvedValue({ 102 - data: { did: 'did:plc:abc123' }, 103 - }); 93 + mockHandleResolve.mockResolvedValue('did:plc:abc123'); 104 94 105 - vi.mocked(mockClient.getAgent).mockReturnValue({ 106 - com: { 107 - atproto: { 108 - identity: { 109 - resolveHandle: mockResolve, 110 - }, 111 - }, 112 - }, 113 - } as never); 95 + await resolveHandleToDid('@mark.bsky.social'); 114 96 115 - await resolveHandleToDid('@mark.bsky.social', mockClient); 116 - 117 - expect(mockResolve).toHaveBeenCalledWith({ handle: 'mark.bsky.social' }); 97 + expect(mockHandleResolve).toHaveBeenCalledWith('mark.bsky.social'); 118 98 }); 119 99 120 - it('should throw error when handle not found', async () => { 121 - const mockResolve = vi.fn().mockResolvedValue({ 122 - data: { did: null }, 123 - }); 100 + it('should throw error when handle resolves to nothing', async () => { 101 + mockHandleResolve.mockResolvedValue(undefined); 124 102 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( 103 + await expect(resolveHandleToDid('nonexistent.bsky.social')).rejects.toThrow( 136 104 'No DID found for handle: nonexistent.bsky.social' 137 105 ); 138 106 }); 139 107 140 - it('should throw error on network failure', async () => { 141 - const mockResolve = vi.fn().mockRejectedValue(new Error('Network error')); 108 + it('should throw error on resolution failure', async () => { 109 + mockHandleResolve.mockRejectedValue(new Error('Network error')); 142 110 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( 111 + await expect(resolveHandleToDid('mark.bsky.social')).rejects.toThrow( 154 112 "Failed to resolve handle 'mark.bsky.social': Network error" 155 113 ); 156 114 }); 157 115 }); 158 116 159 - describe('buildRepoAtUri', () => { 160 - let mockClient: TangledApiClient; 161 - 117 + describe('resolveRepoId', () => { 162 118 beforeEach(() => { 163 - mockClient = createMockClient(); 119 + vi.clearAllMocks(); 120 + mockResolvePds.mockResolvedValue('https://pds.example.com'); 164 121 }); 165 122 166 - it('should query PDS and use repo record rkey', async () => { 167 - const mockListRecords = vi.fn().mockResolvedValue({ 123 + it('should return repoDid from the repo record', async () => { 124 + mockListRecords.mockResolvedValue({ 168 125 data: { 169 126 records: [ 170 127 { 171 - uri: 'at://did:plc:abc123/sh.tangled.repo/3mef23waqwq22', 172 - value: { name: 'my-repo', description: 'Test repo' }, 128 + uri: 'at://did:plc:abc123/sh.tangled.repo/my-repo', 129 + value: { repoDid: 'did:plc:repodid111', knot: 'knot1.tangled.sh' }, 173 130 }, 174 131 ], 175 132 }, 176 133 }); 177 134 178 - vi.mocked(mockClient.getAgent).mockReturnValue({ 179 - com: { 180 - atproto: { 181 - repo: { 182 - listRecords: mockListRecords, 183 - }, 184 - }, 185 - }, 186 - } as never); 135 + const result = await resolveRepoId('did:plc:abc123', 'my-repo'); 187 136 188 - const result = await buildRepoAtUri('did:plc:abc123', 'my-repo', mockClient); 189 - 190 - expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/3mef23waqwq22'); 137 + expect(mockResolvePds).toHaveBeenCalledWith('did:plc:abc123'); 191 138 expect(mockListRecords).toHaveBeenCalledWith({ 192 139 repo: 'did:plc:abc123', 193 140 collection: 'sh.tangled.repo', 194 141 limit: 100, 195 142 }); 143 + expect(result).toBe('did:plc:repodid111'); 196 144 }); 197 145 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({ 146 + it('should resolve handle to DID then return repoDid', async () => { 147 + mockHandleResolve.mockResolvedValue('did:plc:abc123'); 148 + mockListRecords.mockResolvedValue({ 204 149 data: { 205 150 records: [ 206 151 { 207 - uri: 'at://did:plc:abc123/sh.tangled.repo/xyz789', 208 - value: { name: 'my-repo' }, 152 + uri: 'at://did:plc:abc123/sh.tangled.repo/my-repo', 153 + value: { repoDid: 'did:plc:repodid111', knot: 'knot1.tangled.sh' }, 209 154 }, 210 155 ], 211 156 }, 212 157 }); 213 158 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); 159 + const result = await resolveRepoId('mark.bsky.social', 'my-repo'); 226 160 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 - }); 161 + expect(mockHandleResolve).toHaveBeenCalledWith('mark.bsky.social'); 162 + expect(mockResolvePds).toHaveBeenCalledWith('did:plc:abc123'); 163 + expect(result).toBe('did:plc:repodid111'); 236 164 }); 237 165 238 - it('should find correct repo among multiple records', async () => { 239 - const mockListRecords = vi.fn().mockResolvedValue({ 166 + it('should find correct repo among multiple records by rkey', async () => { 167 + mockListRecords.mockResolvedValue({ 240 168 data: { 241 169 records: [ 242 170 { 243 - uri: 'at://did:plc:abc123/sh.tangled.repo/aaa111', 244 - value: { name: 'other-repo' }, 171 + uri: 'at://did:plc:abc123/sh.tangled.repo/other-repo', 172 + value: { repoDid: 'did:plc:other' }, 245 173 }, 246 174 { 247 - uri: 'at://did:plc:abc123/sh.tangled.repo/bbb222', 248 - value: { name: 'target-repo' }, 175 + uri: 'at://did:plc:abc123/sh.tangled.repo/target-repo', 176 + value: { repoDid: 'did:plc:target' }, 249 177 }, 250 178 { 251 - uri: 'at://did:plc:abc123/sh.tangled.repo/ccc333', 252 - value: { name: 'another-repo' }, 179 + uri: 'at://did:plc:abc123/sh.tangled.repo/another-repo', 180 + value: { repoDid: 'did:plc:another' }, 253 181 }, 254 182 ], 255 183 }, 256 184 }); 257 185 258 - vi.mocked(mockClient.getAgent).mockReturnValue({ 259 - com: { 260 - atproto: { 261 - repo: { 262 - listRecords: mockListRecords, 186 + const result = await resolveRepoId('did:plc:abc123', 'target-repo'); 187 + 188 + expect(result).toBe('did:plc:target'); 189 + }); 190 + 191 + it('should match by value.name when rkey is an opaque tid', async () => { 192 + // Older tangled.org records use an opaque tid as rkey and store the repo 193 + // name in value.name instead 194 + mockListRecords.mockResolvedValue({ 195 + data: { 196 + records: [ 197 + { 198 + uri: 'at://did:plc:abc123/sh.tangled.repo/3mksu5zaint22', 199 + value: { name: 'my-repo', repoDid: 'did:plc:repodid222', knot: 'knot1.tangled.sh' }, 263 200 }, 264 - }, 201 + ], 265 202 }, 266 - } as never); 203 + }); 267 204 268 - const result = await buildRepoAtUri('did:plc:abc123', 'target-repo', mockClient); 205 + const result = await resolveRepoId('did:plc:abc123', 'my-repo'); 269 206 270 - expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/bbb222'); 207 + expect(result).toBe('did:plc:repodid222'); 208 + }); 209 + 210 + it('should fall back to record URI when repoDid is absent', async () => { 211 + mockListRecords.mockResolvedValue({ 212 + data: { 213 + records: [ 214 + { 215 + uri: 'at://did:plc:abc123/sh.tangled.repo/my-repo', 216 + value: { knot: 'knot1.tangled.sh' }, 217 + }, 218 + ], 219 + }, 220 + }); 221 + 222 + const result = await resolveRepoId('did:plc:abc123', 'my-repo'); 223 + 224 + expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/my-repo'); 271 225 }); 272 226 273 227 it('should throw error when repository not found', async () => { 274 - const mockListRecords = vi.fn().mockResolvedValue({ 228 + mockListRecords.mockResolvedValue({ 275 229 data: { 276 - records: [ 277 - { 278 - uri: 'at://did:plc:abc123/sh.tangled.repo/xyz789', 279 - value: { name: 'different-repo' }, 280 - }, 281 - ], 230 + records: [{ uri: 'at://did:plc:abc123/sh.tangled.repo/different-repo', value: {} }], 282 231 }, 283 232 }); 284 233 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( 234 + await expect(resolveRepoId('did:plc:abc123', 'nonexistent-repo')).rejects.toThrow( 296 235 "Repository 'nonexistent-repo' not found for did:plc:abc123" 297 236 ); 298 237 }); 299 238 300 239 it('should throw error when handle resolution fails', async () => { 301 - const mockResolve = vi.fn().mockRejectedValue(new Error('Resolution failed')); 240 + mockHandleResolve.mockRejectedValue(new Error('Resolution failed')); 302 241 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( 242 + await expect(resolveRepoId('mark.bsky.social', 'my-repo')).rejects.toThrow( 314 243 "Failed to resolve handle 'mark.bsky.social': Resolution failed" 315 244 ); 316 245 }); 317 246 318 247 it('should throw error when listRecords fails', async () => { 319 - const mockListRecords = vi.fn().mockRejectedValue(new Error('API error')); 248 + mockListRecords.mockRejectedValue(new Error('API error')); 320 249 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( 250 + await expect(resolveRepoId('did:plc:abc123', 'my-repo')).rejects.toThrow( 332 251 'Failed to resolve repository AT-URI: API error' 333 252 ); 334 253 });