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! :)
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});