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 pull-request-format-testing 865 lines 23 kB view raw
1import { beforeEach, describe, expect, it, vi } from 'vitest'; 2import type { TangledApiClient } from '../../src/lib/api-client.js'; 3import { 4 closeIssue, 5 createIssue, 6 deleteIssue, 7 getIssue, 8 getIssueState, 9 listIssues, 10 reopenIssue, 11 updateIssue, 12} from '../../src/lib/issues-api.js'; 13 14// Mock API client factory 15const createMockClient = (authenticated = true): TangledApiClient => { 16 const mockAgent = { 17 com: { 18 atproto: { 19 repo: { 20 createRecord: vi.fn(), 21 listRecords: vi.fn(), 22 getRecord: vi.fn(), 23 putRecord: vi.fn(), 24 deleteRecord: vi.fn(), 25 }, 26 }, 27 }, 28 }; 29 30 return { 31 isAuthenticated: vi.fn(async () => authenticated), 32 getSession: vi.fn(() => 33 authenticated ? { did: 'did:plc:test123', handle: 'test.bsky.social' } : null 34 ), 35 getAgent: vi.fn(() => mockAgent), 36 } as unknown as TangledApiClient; 37}; 38 39describe('createIssue', () => { 40 let mockClient: TangledApiClient; 41 42 beforeEach(() => { 43 mockClient = createMockClient(true); 44 }); 45 46 it('should create an issue with all fields', async () => { 47 const mockCreateRecord = vi.fn().mockResolvedValue({ 48 data: { 49 uri: 'at://did:plc:test123/sh.tangled.repo.issue/abc123', 50 cid: 'cid123', 51 }, 52 }); 53 54 vi.mocked(mockClient.getAgent).mockReturnValue({ 55 com: { 56 atproto: { 57 repo: { 58 createRecord: mockCreateRecord, 59 }, 60 }, 61 }, 62 } as never); 63 64 const result = await createIssue({ 65 client: mockClient, 66 repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', 67 title: 'Bug: Login fails', 68 body: 'Detailed description of the bug', 69 }); 70 71 expect(result).toMatchObject({ 72 repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', 73 title: 'Bug: Login fails', 74 body: 'Detailed description of the bug', 75 uri: 'at://did:plc:test123/sh.tangled.repo.issue/abc123', 76 cid: 'cid123', 77 author: 'did:plc:test123', 78 }); 79 80 expect(mockCreateRecord).toHaveBeenCalledWith({ 81 repo: 'did:plc:test123', 82 collection: 'sh.tangled.repo.issue', 83 record: expect.objectContaining({ 84 $type: 'sh.tangled.repo.issue', 85 repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', 86 title: 'Bug: Login fails', 87 body: 'Detailed description of the bug', 88 createdAt: expect.any(String), 89 }), 90 }); 91 }); 92 93 it('should create an issue without body', async () => { 94 const mockCreateRecord = vi.fn().mockResolvedValue({ 95 data: { 96 uri: 'at://did:plc:test123/sh.tangled.repo.issue/abc123', 97 cid: 'cid123', 98 }, 99 }); 100 101 vi.mocked(mockClient.getAgent).mockReturnValue({ 102 com: { 103 atproto: { 104 repo: { 105 createRecord: mockCreateRecord, 106 }, 107 }, 108 }, 109 } as never); 110 111 const result = await createIssue({ 112 client: mockClient, 113 repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', 114 title: 'Simple issue', 115 }); 116 117 expect(result.body).toBeUndefined(); 118 expect(mockCreateRecord).toHaveBeenCalled(); 119 }); 120 121 it('should throw error when not authenticated', async () => { 122 mockClient = createMockClient(false); 123 124 await expect( 125 createIssue({ 126 client: mockClient, 127 repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', 128 title: 'Test', 129 }) 130 ).rejects.toThrow('Must be authenticated'); 131 }); 132 133 it('should throw error on API failure', async () => { 134 const mockCreateRecord = vi.fn().mockRejectedValue(new Error('API error')); 135 136 vi.mocked(mockClient.getAgent).mockReturnValue({ 137 com: { 138 atproto: { 139 repo: { 140 createRecord: mockCreateRecord, 141 }, 142 }, 143 }, 144 } as never); 145 146 await expect( 147 createIssue({ 148 client: mockClient, 149 repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', 150 title: 'Test', 151 }) 152 ).rejects.toThrow('Failed to create issue: API error'); 153 }); 154}); 155 156describe('listIssues', () => { 157 let mockClient: TangledApiClient; 158 159 beforeEach(() => { 160 mockClient = createMockClient(true); 161 }); 162 163 it('should list issues for a repository', async () => { 164 const mockListRecords = vi.fn().mockResolvedValue({ 165 data: { 166 records: [ 167 { 168 uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 169 cid: 'cid1', 170 value: { 171 $type: 'sh.tangled.repo.issue', 172 repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', 173 title: 'Issue 1', 174 body: 'Description 1', 175 createdAt: '2024-01-01T00:00:00.000Z', 176 }, 177 }, 178 { 179 uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue2', 180 cid: 'cid2', 181 value: { 182 $type: 'sh.tangled.repo.issue', 183 repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', 184 title: 'Issue 2', 185 createdAt: '2024-01-02T00:00:00.000Z', 186 }, 187 }, 188 ], 189 cursor: undefined, 190 }, 191 }); 192 193 vi.mocked(mockClient.getAgent).mockReturnValue({ 194 com: { 195 atproto: { 196 repo: { 197 listRecords: mockListRecords, 198 }, 199 }, 200 }, 201 } as never); 202 203 const result = await listIssues({ 204 client: mockClient, 205 repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', 206 }); 207 208 expect(result.issues).toHaveLength(2); 209 expect(result.issues[0]).toMatchObject({ 210 title: 'Issue 1', 211 body: 'Description 1', 212 uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 213 }); 214 }); 215 216 it('should filter issues by repository', async () => { 217 const mockListRecords = vi.fn().mockResolvedValue({ 218 data: { 219 records: [ 220 { 221 uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 222 cid: 'cid1', 223 value: { 224 repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', 225 title: 'Issue 1', 226 createdAt: '2024-01-01T00:00:00.000Z', 227 }, 228 }, 229 { 230 uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue2', 231 cid: 'cid2', 232 value: { 233 repo: 'at://did:plc:owner/sh.tangled.repo/other-repo', 234 title: 'Issue 2', 235 createdAt: '2024-01-02T00:00:00.000Z', 236 }, 237 }, 238 ], 239 cursor: undefined, 240 }, 241 }); 242 243 vi.mocked(mockClient.getAgent).mockReturnValue({ 244 com: { 245 atproto: { 246 repo: { 247 listRecords: mockListRecords, 248 }, 249 }, 250 }, 251 } as never); 252 253 const result = await listIssues({ 254 client: mockClient, 255 repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', 256 }); 257 258 // Should only include issue from my-repo, not other-repo 259 expect(result.issues).toHaveLength(1); 260 expect(result.issues[0].title).toBe('Issue 1'); 261 }); 262 263 it('should return empty array when no issues found', async () => { 264 const mockListRecords = vi.fn().mockResolvedValue({ 265 data: { 266 records: [], 267 cursor: undefined, 268 }, 269 }); 270 271 vi.mocked(mockClient.getAgent).mockReturnValue({ 272 com: { 273 atproto: { 274 repo: { 275 listRecords: mockListRecords, 276 }, 277 }, 278 }, 279 } as never); 280 281 const result = await listIssues({ 282 client: mockClient, 283 repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', 284 }); 285 286 expect(result.issues).toEqual([]); 287 }); 288 289 it('should throw error when not authenticated', async () => { 290 mockClient = createMockClient(false); 291 292 await expect( 293 listIssues({ 294 client: mockClient, 295 repoAtUri: 'at://did:plc:owner/sh.tangled.repo/my-repo', 296 }) 297 ).rejects.toThrow('Must be authenticated'); 298 }); 299 300 it('should throw error for invalid repo URI', async () => { 301 await expect( 302 listIssues({ 303 client: mockClient, 304 repoAtUri: 'invalid-uri', 305 }) 306 ).rejects.toThrow('Invalid repository AT-URI'); 307 }); 308}); 309 310describe('getIssue', () => { 311 let mockClient: TangledApiClient; 312 313 beforeEach(() => { 314 mockClient = createMockClient(true); 315 }); 316 317 it('should get a specific issue', async () => { 318 const mockGetRecord = vi.fn().mockResolvedValue({ 319 data: { 320 uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 321 cid: 'cid1', 322 value: { 323 $type: 'sh.tangled.repo.issue', 324 repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', 325 title: 'Test Issue', 326 body: 'Test Description', 327 createdAt: '2024-01-01T00:00:00.000Z', 328 }, 329 }, 330 }); 331 332 vi.mocked(mockClient.getAgent).mockReturnValue({ 333 com: { 334 atproto: { 335 repo: { 336 getRecord: mockGetRecord, 337 }, 338 }, 339 }, 340 } as never); 341 342 const result = await getIssue({ 343 client: mockClient, 344 issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 345 }); 346 347 expect(result).toMatchObject({ 348 title: 'Test Issue', 349 body: 'Test Description', 350 uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 351 cid: 'cid1', 352 }); 353 354 expect(mockGetRecord).toHaveBeenCalledWith({ 355 repo: 'did:plc:owner', 356 collection: 'sh.tangled.repo.issue', 357 rkey: 'issue1', 358 }); 359 }); 360 361 it('should throw error when issue not found', async () => { 362 const mockGetRecord = vi.fn().mockRejectedValue(new Error('Record not found')); 363 364 vi.mocked(mockClient.getAgent).mockReturnValue({ 365 com: { 366 atproto: { 367 repo: { 368 getRecord: mockGetRecord, 369 }, 370 }, 371 }, 372 } as never); 373 374 await expect( 375 getIssue({ 376 client: mockClient, 377 issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/nonexistent', 378 }) 379 ).rejects.toThrow('Issue not found'); 380 }); 381 382 it('should throw error for invalid issue URI', async () => { 383 await expect( 384 getIssue({ 385 client: mockClient, 386 issueUri: 'invalid-uri', 387 }) 388 ).rejects.toThrow('Invalid issue AT-URI'); 389 }); 390 391 it('should throw error when not authenticated', async () => { 392 mockClient = createMockClient(false); 393 394 await expect( 395 getIssue({ 396 client: mockClient, 397 issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 398 }) 399 ).rejects.toThrow('Must be authenticated'); 400 }); 401}); 402 403describe('updateIssue', () => { 404 let mockClient: TangledApiClient; 405 406 beforeEach(() => { 407 mockClient = createMockClient(true); 408 }); 409 410 it('should update issue title', async () => { 411 const mockGetRecord = vi.fn().mockResolvedValue({ 412 data: { 413 uri: 'at://did:plc:test123/sh.tangled.repo.issue/issue1', 414 cid: 'old-cid', 415 value: { 416 repo: 'at://did:plc:test123/sh.tangled.repo/my-repo', 417 title: 'Old Title', 418 body: 'Original body', 419 createdAt: '2024-01-01T00:00:00.000Z', 420 }, 421 }, 422 }); 423 424 const mockPutRecord = vi.fn().mockResolvedValue({ 425 data: { 426 uri: 'at://did:plc:test123/sh.tangled.repo.issue/issue1', 427 cid: 'new-cid', 428 }, 429 }); 430 431 vi.mocked(mockClient.getAgent).mockReturnValue({ 432 com: { 433 atproto: { 434 repo: { 435 getRecord: mockGetRecord, 436 putRecord: mockPutRecord, 437 }, 438 }, 439 }, 440 } as never); 441 442 const result = await updateIssue({ 443 client: mockClient, 444 issueUri: 'at://did:plc:test123/sh.tangled.repo.issue/issue1', 445 title: 'New Title', 446 }); 447 448 expect(result.title).toBe('New Title'); 449 expect(result.body).toBe('Original body'); // Body unchanged 450 451 expect(mockPutRecord).toHaveBeenCalledWith({ 452 repo: 'did:plc:test123', 453 collection: 'sh.tangled.repo.issue', 454 rkey: 'issue1', 455 record: expect.objectContaining({ 456 title: 'New Title', 457 body: 'Original body', 458 }), 459 swapRecord: 'old-cid', 460 }); 461 }); 462 463 it('should update issue body', async () => { 464 const mockGetRecord = vi.fn().mockResolvedValue({ 465 data: { 466 uri: 'at://did:plc:test123/sh.tangled.repo.issue/issue1', 467 cid: 'old-cid', 468 value: { 469 repo: 'at://did:plc:test123/sh.tangled.repo/my-repo', 470 title: 'Title', 471 body: 'Old body', 472 createdAt: '2024-01-01T00:00:00.000Z', 473 }, 474 }, 475 }); 476 477 const mockPutRecord = vi.fn().mockResolvedValue({ 478 data: { 479 cid: 'new-cid', 480 }, 481 }); 482 483 vi.mocked(mockClient.getAgent).mockReturnValue({ 484 com: { 485 atproto: { 486 repo: { 487 getRecord: mockGetRecord, 488 putRecord: mockPutRecord, 489 }, 490 }, 491 }, 492 } as never); 493 494 const result = await updateIssue({ 495 client: mockClient, 496 issueUri: 'at://did:plc:test123/sh.tangled.repo.issue/issue1', 497 body: 'New body', 498 }); 499 500 expect(result.title).toBe('Title'); // Title unchanged 501 expect(result.body).toBe('New body'); 502 }); 503 504 it('should throw error when updating issue not owned by user', async () => { 505 await expect( 506 updateIssue({ 507 client: mockClient, 508 issueUri: 'at://did:plc:someone-else/sh.tangled.repo.issue/issue1', 509 title: 'New Title', 510 }) 511 ).rejects.toThrow('Cannot update issue: you are not the author'); 512 }); 513 514 it('should throw error when not authenticated', async () => { 515 mockClient = createMockClient(false); 516 517 await expect( 518 updateIssue({ 519 client: mockClient, 520 issueUri: 'at://did:plc:test123/sh.tangled.repo.issue/issue1', 521 title: 'New Title', 522 }) 523 ).rejects.toThrow('Must be authenticated'); 524 }); 525}); 526 527describe('closeIssue', () => { 528 let mockClient: TangledApiClient; 529 530 beforeEach(() => { 531 mockClient = createMockClient(true); 532 }); 533 534 it('should close an issue', async () => { 535 const mockGetRecord = vi.fn().mockResolvedValue({ 536 data: { 537 uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 538 cid: 'cid1', 539 value: { 540 repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', 541 title: 'Test Issue', 542 createdAt: '2024-01-01T00:00:00.000Z', 543 }, 544 }, 545 }); 546 547 const mockCreateRecord = vi.fn().mockResolvedValue({ 548 data: { 549 uri: 'at://did:plc:test123/sh.tangled.repo.issue.state/state1', 550 cid: 'state-cid', 551 }, 552 }); 553 554 vi.mocked(mockClient.getAgent).mockReturnValue({ 555 com: { 556 atproto: { 557 repo: { 558 getRecord: mockGetRecord, 559 createRecord: mockCreateRecord, 560 }, 561 }, 562 }, 563 } as never); 564 565 await closeIssue({ 566 client: mockClient, 567 issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 568 }); 569 570 expect(mockCreateRecord).toHaveBeenCalledWith({ 571 repo: 'did:plc:test123', 572 collection: 'sh.tangled.repo.issue.state', 573 record: { 574 $type: 'sh.tangled.repo.issue.state', 575 issue: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 576 state: 'sh.tangled.repo.issue.state.closed', 577 }, 578 }); 579 }); 580 581 it('should throw error when not authenticated', async () => { 582 mockClient = createMockClient(false); 583 584 await expect( 585 closeIssue({ 586 client: mockClient, 587 issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 588 }) 589 ).rejects.toThrow('Must be authenticated'); 590 }); 591}); 592 593describe('deleteIssue', () => { 594 let mockClient: TangledApiClient; 595 596 beforeEach(() => { 597 mockClient = createMockClient(true); 598 }); 599 600 it('should delete an issue', async () => { 601 const mockDeleteRecord = vi.fn().mockResolvedValue({}); 602 603 vi.mocked(mockClient.getAgent).mockReturnValue({ 604 com: { 605 atproto: { 606 repo: { 607 deleteRecord: mockDeleteRecord, 608 }, 609 }, 610 }, 611 } as never); 612 613 await deleteIssue({ 614 client: mockClient, 615 issueUri: 'at://did:plc:test123/sh.tangled.repo.issue/issue1', 616 }); 617 618 expect(mockDeleteRecord).toHaveBeenCalledWith({ 619 repo: 'did:plc:test123', 620 collection: 'sh.tangled.repo.issue', 621 rkey: 'issue1', 622 }); 623 }); 624 625 it('should throw error when deleting issue not owned by user', async () => { 626 await expect( 627 deleteIssue({ 628 client: mockClient, 629 issueUri: 'at://did:plc:someone-else/sh.tangled.repo.issue/issue1', 630 }) 631 ).rejects.toThrow('Cannot delete issue: you are not the author'); 632 }); 633 634 it('should throw error when issue not found', async () => { 635 const mockDeleteRecord = vi.fn().mockRejectedValue(new Error('Record not found')); 636 637 vi.mocked(mockClient.getAgent).mockReturnValue({ 638 com: { 639 atproto: { 640 repo: { 641 deleteRecord: mockDeleteRecord, 642 }, 643 }, 644 }, 645 } as never); 646 647 await expect( 648 deleteIssue({ 649 client: mockClient, 650 issueUri: 'at://did:plc:test123/sh.tangled.repo.issue/nonexistent', 651 }) 652 ).rejects.toThrow('Issue not found'); 653 }); 654 655 it('should throw error when not authenticated', async () => { 656 mockClient = createMockClient(false); 657 658 await expect( 659 deleteIssue({ 660 client: mockClient, 661 issueUri: 'at://did:plc:test123/sh.tangled.repo.issue/issue1', 662 }) 663 ).rejects.toThrow('Must be authenticated'); 664 }); 665}); 666 667describe('getIssueState', () => { 668 let mockClient: TangledApiClient; 669 670 beforeEach(() => { 671 mockClient = createMockClient(true); 672 }); 673 674 it('should return open when no state records exist', async () => { 675 const mockListRecords = vi.fn().mockResolvedValue({ 676 data: { records: [] }, 677 }); 678 679 vi.mocked(mockClient.getAgent).mockReturnValue({ 680 com: { atproto: { repo: { listRecords: mockListRecords } } }, 681 } as never); 682 683 const result = await getIssueState({ 684 client: mockClient, 685 issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 686 }); 687 688 expect(result).toBe('open'); 689 expect(mockListRecords).toHaveBeenCalledWith({ 690 repo: 'did:plc:owner', 691 collection: 'sh.tangled.repo.issue.state', 692 limit: 100, 693 }); 694 }); 695 696 it('should return closed when latest state record is closed', async () => { 697 const mockListRecords = vi.fn().mockResolvedValue({ 698 data: { 699 records: [ 700 { 701 uri: 'at://did:plc:owner/sh.tangled.repo.issue.state/state1', 702 cid: 'cid1', 703 value: { 704 issue: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 705 state: 'sh.tangled.repo.issue.state.closed', 706 }, 707 }, 708 ], 709 }, 710 }); 711 712 vi.mocked(mockClient.getAgent).mockReturnValue({ 713 com: { atproto: { repo: { listRecords: mockListRecords } } }, 714 } as never); 715 716 const result = await getIssueState({ 717 client: mockClient, 718 issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 719 }); 720 721 expect(result).toBe('closed'); 722 }); 723 724 it('should return open when latest state record is open', async () => { 725 const mockListRecords = vi.fn().mockResolvedValue({ 726 data: { 727 records: [ 728 { 729 uri: 'at://did:plc:owner/sh.tangled.repo.issue.state/state1', 730 cid: 'cid1', 731 value: { 732 issue: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 733 state: 'sh.tangled.repo.issue.state.closed', 734 }, 735 }, 736 { 737 uri: 'at://did:plc:owner/sh.tangled.repo.issue.state/state2', 738 cid: 'cid2', 739 value: { 740 issue: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 741 state: 'sh.tangled.repo.issue.state.open', 742 }, 743 }, 744 ], 745 }, 746 }); 747 748 vi.mocked(mockClient.getAgent).mockReturnValue({ 749 com: { atproto: { repo: { listRecords: mockListRecords } } }, 750 } as never); 751 752 const result = await getIssueState({ 753 client: mockClient, 754 issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 755 }); 756 757 expect(result).toBe('open'); 758 }); 759 760 it('should filter state records to only the target issue', async () => { 761 const mockListRecords = vi.fn().mockResolvedValue({ 762 data: { 763 records: [ 764 { 765 uri: 'at://did:plc:owner/sh.tangled.repo.issue.state/state1', 766 cid: 'cid1', 767 value: { 768 issue: 'at://did:plc:owner/sh.tangled.repo.issue/other-issue', 769 state: 'sh.tangled.repo.issue.state.closed', 770 }, 771 }, 772 ], 773 }, 774 }); 775 776 vi.mocked(mockClient.getAgent).mockReturnValue({ 777 com: { atproto: { repo: { listRecords: mockListRecords } } }, 778 } as never); 779 780 // The closed state is for a different issue, so this one should be open 781 const result = await getIssueState({ 782 client: mockClient, 783 issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 784 }); 785 786 expect(result).toBe('open'); 787 }); 788 789 it('should throw error when not authenticated', async () => { 790 mockClient = createMockClient(false); 791 792 await expect( 793 getIssueState({ 794 client: mockClient, 795 issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 796 }) 797 ).rejects.toThrow('Must be authenticated'); 798 }); 799}); 800 801describe('reopenIssue', () => { 802 let mockClient: TangledApiClient; 803 804 beforeEach(() => { 805 mockClient = createMockClient(true); 806 }); 807 808 it('should reopen a closed issue', async () => { 809 const mockGetRecord = vi.fn().mockResolvedValue({ 810 data: { 811 uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 812 cid: 'cid1', 813 value: { 814 repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', 815 title: 'Test Issue', 816 createdAt: '2024-01-01T00:00:00.000Z', 817 }, 818 }, 819 }); 820 821 const mockCreateRecord = vi.fn().mockResolvedValue({ 822 data: { 823 uri: 'at://did:plc:test123/sh.tangled.repo.issue.state/state1', 824 cid: 'state-cid', 825 }, 826 }); 827 828 vi.mocked(mockClient.getAgent).mockReturnValue({ 829 com: { 830 atproto: { 831 repo: { 832 getRecord: mockGetRecord, 833 createRecord: mockCreateRecord, 834 }, 835 }, 836 }, 837 } as never); 838 839 await reopenIssue({ 840 client: mockClient, 841 issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 842 }); 843 844 expect(mockCreateRecord).toHaveBeenCalledWith({ 845 repo: 'did:plc:test123', 846 collection: 'sh.tangled.repo.issue.state', 847 record: { 848 $type: 'sh.tangled.repo.issue.state', 849 issue: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 850 state: 'sh.tangled.repo.issue.state.open', 851 }, 852 }); 853 }); 854 855 it('should throw error when not authenticated', async () => { 856 mockClient = createMockClient(false); 857 858 await expect( 859 reopenIssue({ 860 client: mockClient, 861 issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 862 }) 863 ).rejects.toThrow('Must be authenticated'); 864 }); 865});