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

fix: normalize JSON output fields across all issue commands

All issue commands now return the full common field set (number, title,
body, state, author, createdAt, uri, cid) in --json mode:

- create: adds state:'open' (always open on creation)
- view: adds number via getCompleteIssueData (replaces separate getIssue
+ getIssueState calls)
- edit: adds number + state via parallel resolveSequentialNumber +
getIssueState after updateIssue (avoids re-fetching the updated record)
- close/reopen: adds body, author, createdAt via getCompleteIssueData with
stateOverride; replaces separate getIssue + resolveSequentialNumber calls
- delete: adds body, author, createdAt, state via getCompleteIssueData;
replaces scattered local variables

Update command tests to mock getCompleteIssueData and assert on the full
field set.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

authored by markbennett.ca

Claude Sonnet 4.5 and committed by tangled.org 4489d742 8ce6e5cb

+178 -137
+69 -107
src/commands/issue.ts
··· 7 7 closeIssue, 8 8 createIssue, 9 9 deleteIssue, 10 - getIssue, 10 + getCompleteIssueData, 11 11 getIssueState, 12 12 listIssues, 13 13 reopenIssue, 14 + resolveSequentialNumber, 14 15 updateIssue, 15 16 } from '../lib/issues-api.js'; 17 + import type { IssueData } from '../lib/issues-api.js'; 16 18 import { buildRepoAtUri } from '../utils/at-uri.js'; 17 19 import { requireAuth } from '../utils/auth-helpers.js'; 18 20 import { readBodyInput } from '../utils/body-input.js'; ··· 87 89 } 88 90 89 91 /** 90 - * Resolve a sequential issue number from a displayId or by scanning the issue list. 91 - * Fast path: if displayId is "#N", return N directly. 92 - * Fallback: fetch all issues, sort oldest-first, return 1-based position. 92 + * A custom subclass of Command with support for adding the common issue JSON flag. 93 93 */ 94 - async function resolveSequentialNumber( 95 - displayId: string, 96 - issueUri: string, 97 - client: TangledApiClient, 98 - repoAtUri: string 99 - ): Promise<number | undefined> { 100 - const match = displayId.match(/^#(\d+)$/); 101 - if (match) return Number.parseInt(match[1], 10); 102 - 103 - const { issues } = await listIssues({ client, repoAtUri, limit: 100 }); 104 - const sorted = issues.sort( 105 - (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() 106 - ); 107 - const idx = sorted.findIndex((i) => i.uri === issueUri); 108 - return idx >= 0 ? idx + 1 : undefined; 94 + class IssueCommand extends Command { 95 + addIssueJsonOption() { 96 + return this.option( 97 + '--json [fields]', 98 + 'Output JSON; optionally specify comma-separated fields (number, title, body, state, author, createdAt, uri, cid)' 99 + ); 100 + } 109 101 } 110 102 111 103 /** 112 104 * Issue view subcommand 113 105 */ 114 106 function createViewCommand(): Command { 115 - return new Command('view') 107 + return new IssueCommand('view') 116 108 .description('View details of a specific issue') 117 109 .argument('<issue-id>', 'Issue number (e.g., 1, #2) or rkey') 118 - .option( 119 - '--json [fields]', 120 - 'Output JSON; optionally specify comma-separated fields (title, body, state, author, createdAt, uri, cid)' 121 - ) 110 + .addIssueJsonOption() 122 111 .action(async (issueId: string, options: { json?: string | true }) => { 123 112 try { 124 113 // 1. Validate auth ··· 143 132 // 4. Resolve issue ID to URI 144 133 const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoAtUri); 145 134 146 - // 5. Fetch issue details 147 - const issue = await getIssue({ client, issueUri }); 148 - 149 - // 6. Fetch issue state 150 - const state = await getIssueState({ client, issueUri: issue.uri }); 135 + // 5. Fetch complete issue data (record, sequential number, state) 136 + const issueData = await getCompleteIssueData(client, issueUri, displayId, repoAtUri); 151 137 152 - // 7. Output result 138 + // 6. Output result 153 139 if (options.json !== undefined) { 154 - const issueData = { 155 - title: issue.title, 156 - body: issue.body, 157 - state, 158 - author: issue.author, 159 - createdAt: issue.createdAt, 160 - uri: issue.uri, 161 - cid: issue.cid, 162 - }; 163 140 outputJson(issueData, typeof options.json === 'string' ? options.json : undefined); 164 141 return; 165 142 } 166 143 167 - console.log(`\nIssue ${displayId} ${formatIssueState(state)}`); 168 - console.log(`Title: ${issue.title}`); 169 - console.log(`Author: ${issue.author}`); 170 - console.log(`Created: ${formatDate(issue.createdAt)}`); 144 + console.log(`\nIssue ${displayId} ${formatIssueState(issueData.state)}`); 145 + console.log(`Title: ${issueData.title}`); 146 + console.log(`Author: ${issueData.author}`); 147 + console.log(`Created: ${formatDate(issueData.createdAt)}`); 171 148 console.log(`Repo: ${context.name}`); 172 - console.log(`URI: ${issue.uri}`); 149 + console.log(`URI: ${issueData.uri}`); 173 150 174 - if (issue.body) { 151 + if (issueData.body) { 175 152 console.log('\nBody:'); 176 - console.log(issue.body); 153 + console.log(issueData.body); 177 154 } 178 155 179 156 console.log(); // Empty line at end ··· 190 167 * Issue edit subcommand 191 168 */ 192 169 function createEditCommand(): Command { 193 - return new Command('edit') 170 + return new IssueCommand('edit') 194 171 .description('Edit an issue title and/or body') 195 172 .argument('<issue-id>', 'Issue number or rkey') 196 173 .option('-t, --title <string>', 'New issue title') 197 174 .option('-b, --body <string>', 'New issue body text') 198 175 .option('-F, --body-file <path>', 'Read body from file (- for stdin)') 199 - .option( 200 - '--json [fields]', 201 - 'Output JSON of the updated issue; optionally specify comma-separated fields (title, body, author, createdAt, uri, cid)' 202 - ) 176 + .addIssueJsonOption() 203 177 .action( 204 178 async ( 205 179 issueId: string, ··· 251 225 252 226 // 9. Output result 253 227 if (options.json !== undefined) { 254 - const issueData = { 228 + const [number, state] = await Promise.all([ 229 + resolveSequentialNumber(displayId, updatedIssue.uri, client, repoAtUri), 230 + getIssueState({ client, issueUri: updatedIssue.uri }), 231 + ]); 232 + const issueData: IssueData = { 233 + number, 255 234 title: updatedIssue.title, 256 235 body: updatedIssue.body, 236 + state, 257 237 author: updatedIssue.author, 258 238 createdAt: updatedIssue.createdAt, 259 239 uri: updatedIssue.uri, ··· 283 263 * Issue close subcommand 284 264 */ 285 265 function createCloseCommand(): Command { 286 - return new Command('close') 266 + return new IssueCommand('close') 287 267 .description('Close an issue') 288 268 .argument('<issue-id>', 'Issue number or rkey') 289 - .option( 290 - '--json [fields]', 291 - 'Output JSON; optionally specify comma-separated fields (number, title, uri, state, cid)' 292 - ) 269 + .addIssueJsonOption() 293 270 .action(async (issueId: string, options: { json?: string | true }) => { 294 271 try { 295 272 // 1. Validate auth ··· 314 291 // 4. Resolve issue ID to URI 315 292 const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoAtUri); 316 293 317 - // 5. Fetch issue details and sequential number 318 - const issue = await getIssue({ client, issueUri }); 319 - const number = await resolveSequentialNumber(displayId, issueUri, client, repoAtUri); 294 + // 5. Fetch complete issue data (state will be 'closed' after operation) 295 + const issueData = await getCompleteIssueData( 296 + client, 297 + issueUri, 298 + displayId, 299 + repoAtUri, 300 + 'closed' 301 + ); 320 302 321 303 // 6. Close issue 322 304 await closeIssue({ client, issueUri }); 323 305 324 306 // 7. Display success 325 307 if (options.json !== undefined) { 326 - outputJson( 327 - { number, title: issue.title, uri: issueUri, state: 'closed', cid: issue.cid }, 328 - typeof options.json === 'string' ? options.json : undefined 329 - ); 308 + outputJson(issueData, typeof options.json === 'string' ? options.json : undefined); 330 309 } else { 331 310 console.log(`✓ Issue ${displayId} closed`); 332 - console.log(` Title: ${issue.title}`); 311 + console.log(` Title: ${issueData.title}`); 333 312 } 334 313 } catch (error) { 335 314 console.error( ··· 344 323 * Issue reopen subcommand 345 324 */ 346 325 function createReopenCommand(): Command { 347 - return new Command('reopen') 326 + return new IssueCommand('reopen') 348 327 .description('Reopen a closed issue') 349 328 .argument('<issue-id>', 'Issue number or rkey') 350 - .option( 351 - '--json [fields]', 352 - 'Output JSON; optionally specify comma-separated fields (number, title, uri, state, cid)' 353 - ) 329 + .addIssueJsonOption() 354 330 .action(async (issueId: string, options: { json?: string | true }) => { 355 331 try { 356 332 // 1. Validate auth ··· 375 351 // 4. Resolve issue ID to URI 376 352 const { uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoAtUri); 377 353 378 - // 5. Fetch issue details and sequential number 379 - const issue = await getIssue({ client, issueUri }); 380 - const number = await resolveSequentialNumber(displayId, issueUri, client, repoAtUri); 354 + // 5. Fetch complete issue data (state will be 'open' after operation) 355 + const issueData = await getCompleteIssueData( 356 + client, 357 + issueUri, 358 + displayId, 359 + repoAtUri, 360 + 'open' 361 + ); 381 362 382 363 // 6. Reopen issue 383 364 await reopenIssue({ client, issueUri }); 384 365 385 366 // 7. Display success 386 367 if (options.json !== undefined) { 387 - outputJson( 388 - { number, title: issue.title, uri: issueUri, state: 'open', cid: issue.cid }, 389 - typeof options.json === 'string' ? options.json : undefined 390 - ); 368 + outputJson(issueData, typeof options.json === 'string' ? options.json : undefined); 391 369 } else { 392 370 console.log(`✓ Issue ${displayId} reopened`); 393 - console.log(` Title: ${issue.title}`); 371 + console.log(` Title: ${issueData.title}`); 394 372 } 395 373 } catch (error) { 396 374 console.error( ··· 405 383 * Issue delete subcommand 406 384 */ 407 385 function createDeleteCommand(): Command { 408 - return new Command('delete') 386 + return new IssueCommand('delete') 409 387 .description('Delete an issue permanently') 410 388 .argument('<issue-id>', 'Issue number or rkey') 411 389 .option('-f, --force', 'Skip confirmation prompt') 412 - .option( 413 - '--json [fields]', 414 - 'Output JSON; optionally specify comma-separated fields (number, title, uri, cid)' 415 - ) 390 + .addIssueJsonOption() 416 391 .action(async (issueId: string, options: { force?: boolean; json?: string | true }) => { 417 392 // 1. Validate auth 418 393 const client = createApiClient(); ··· 433 408 // 3. Build repo AT-URI, resolve issue ID, and fetch issue details 434 409 let issueUri: string; 435 410 let displayId: string; 436 - let issueTitle: string; 437 - let issueCid: string; 438 - let issueNumber: number | undefined; 411 + let issueData: IssueData; 439 412 try { 440 413 const repoAtUri = await buildRepoAtUri(context.owner, context.name, client); 441 414 ({ uri: issueUri, displayId } = await resolveIssueUri(issueId, client, repoAtUri)); 442 - const issue = await getIssue({ client, issueUri }); 443 - issueTitle = issue.title; 444 - issueCid = issue.cid; 445 - issueNumber = await resolveSequentialNumber(displayId, issueUri, client, repoAtUri); 415 + issueData = await getCompleteIssueData(client, issueUri, displayId, repoAtUri); 446 416 } catch (error) { 447 417 console.error( 448 418 `✗ Failed to delete issue: ${error instanceof Error ? error.message : 'Unknown error'}` ··· 453 423 // 4. Confirm deletion if not --force (outside try so process.exit(0) propagates cleanly) 454 424 if (!options.force) { 455 425 const confirmed = await confirm({ 456 - message: `Are you sure you want to delete issue ${displayId} "${issueTitle}"? This cannot be undone.`, 426 + message: `Are you sure you want to delete issue ${displayId} "${issueData.title}"? This cannot be undone.`, 457 427 default: false, 458 428 }); 459 429 ··· 467 437 try { 468 438 await deleteIssue({ client, issueUri }); 469 439 if (options.json !== undefined) { 470 - outputJson( 471 - { number: issueNumber, title: issueTitle, uri: issueUri, cid: issueCid }, 472 - typeof options.json === 'string' ? options.json : undefined 473 - ); 440 + outputJson(issueData, typeof options.json === 'string' ? options.json : undefined); 474 441 } else { 475 442 console.log(`✓ Issue ${displayId} deleted`); 476 - console.log(` Title: ${issueTitle}`); 443 + console.log(` Title: ${issueData.title}`); 477 444 } 478 445 } catch (error) { 479 446 console.error( ··· 506 473 * Issue create subcommand 507 474 */ 508 475 function createCreateCommand(): Command { 509 - return new Command('create') 476 + return new IssueCommand('create') 510 477 .description('Create a new issue') 511 478 .argument('<title>', 'Issue title') 512 479 .option('-b, --body <string>', 'Issue body text') 513 480 .option('-F, --body-file <path>', 'Read body from file (- for stdin)') 514 - .option( 515 - '--json [fields]', 516 - 'Output JSON; optionally specify comma-separated fields (number, title, body, author, createdAt, uri, cid)' 517 - ) 481 + .addIssueJsonOption() 518 482 .action( 519 483 async ( 520 484 title: string, ··· 570 534 571 535 // 8. Output result 572 536 if (options.json !== undefined) { 573 - const issueData = { 537 + const issueData: IssueData = { 574 538 number, 575 539 title: issue.title, 576 540 body: issue.body, 541 + state: 'open', 577 542 author: issue.author, 578 543 createdAt: issue.createdAt, 579 544 uri: issue.uri, ··· 601 566 * Issue list subcommand 602 567 */ 603 568 function createListCommand(): Command { 604 - return new Command('list') 569 + return new IssueCommand('list') 605 570 .description('List issues for the current repository') 606 571 .option('-l, --limit <number>', 'Maximum number of issues to fetch', '50') 607 - .option( 608 - '--json [fields]', 609 - 'Output JSON; optionally specify comma-separated fields (number, title, body, state, author, createdAt, uri, cid)' 610 - ) 572 + .addIssueJsonOption() 611 573 .action(async (options: { limit: string; json?: string | true }) => { 612 574 try { 613 575 // 1. Validate auth
+109 -30
tests/commands/issue.test.ts
··· 615 615 issues: [mockIssue], 616 616 cursor: undefined, 617 617 }); 618 - vi.mocked(issuesApi.getIssue).mockResolvedValue(mockIssue); 619 - vi.mocked(issuesApi.getIssueState).mockResolvedValue('open'); 618 + vi.mocked(issuesApi.getCompleteIssueData).mockResolvedValue({ 619 + number: 1, 620 + title: mockIssue.title, 621 + body: mockIssue.body, 622 + state: 'open', 623 + author: mockIssue.author, 624 + createdAt: mockIssue.createdAt, 625 + uri: mockIssue.uri, 626 + cid: mockIssue.cid, 627 + }); 620 628 621 629 const command = createIssueCommand(); 622 630 await command.parseAsync(['node', 'test', 'view', '1']); 623 631 624 - expect(issuesApi.getIssue).toHaveBeenCalledWith({ 625 - client: mockClient, 626 - issueUri: mockIssue.uri, 627 - }); 628 - expect(issuesApi.getIssueState).toHaveBeenCalledWith({ 629 - client: mockClient, 630 - issueUri: mockIssue.uri, 631 - }); 632 + expect(issuesApi.getCompleteIssueData).toHaveBeenCalledWith( 633 + mockClient, 634 + mockIssue.uri, 635 + '#1', 636 + 'at://did:plc:abc123/sh.tangled.repo/xyz789' 637 + ); 632 638 expect(consoleLogSpy).toHaveBeenCalledWith('\nIssue #1 [OPEN]'); 633 639 expect(consoleLogSpy).toHaveBeenCalledWith('Title: Test Issue'); 634 640 expect(consoleLogSpy).toHaveBeenCalledWith('\nBody:'); ··· 636 642 }); 637 643 638 644 it('should view issue by rkey', async () => { 639 - vi.mocked(issuesApi.getIssue).mockResolvedValue(mockIssue); 640 - vi.mocked(issuesApi.getIssueState).mockResolvedValue('closed'); 645 + vi.mocked(issuesApi.getCompleteIssueData).mockResolvedValue({ 646 + number: undefined, 647 + title: mockIssue.title, 648 + body: mockIssue.body, 649 + state: 'closed', 650 + author: mockIssue.author, 651 + createdAt: mockIssue.createdAt, 652 + uri: mockIssue.uri, 653 + cid: mockIssue.cid, 654 + }); 641 655 642 656 const command = createIssueCommand(); 643 657 await command.parseAsync(['node', 'test', 'view', 'issue1']); 644 658 645 - expect(issuesApi.getIssue).toHaveBeenCalledWith({ 646 - client: mockClient, 647 - issueUri: 'at://did:plc:abc123/sh.tangled.repo.issue/issue1', 648 - }); 659 + expect(issuesApi.getCompleteIssueData).toHaveBeenCalledWith( 660 + mockClient, 661 + 'at://did:plc:abc123/sh.tangled.repo.issue/issue1', 662 + 'issue1', 663 + 'at://did:plc:abc123/sh.tangled.repo/xyz789' 664 + ); 649 665 expect(consoleLogSpy).toHaveBeenCalledWith('\nIssue issue1 [CLOSED]'); 650 666 }); 651 667 652 668 it('should show issue without body', async () => { 653 - const issueWithoutBody = { ...mockIssue, body: undefined }; 654 669 vi.mocked(issuesApi.listIssues).mockResolvedValue({ 655 - issues: [issueWithoutBody], 670 + issues: [mockIssue], 656 671 cursor: undefined, 657 672 }); 658 - vi.mocked(issuesApi.getIssue).mockResolvedValue(issueWithoutBody); 659 - vi.mocked(issuesApi.getIssueState).mockResolvedValue('open'); 673 + vi.mocked(issuesApi.getCompleteIssueData).mockResolvedValue({ 674 + number: 1, 675 + title: mockIssue.title, 676 + body: undefined, 677 + state: 'open', 678 + author: mockIssue.author, 679 + createdAt: mockIssue.createdAt, 680 + uri: mockIssue.uri, 681 + cid: mockIssue.cid, 682 + }); 660 683 661 684 const command = createIssueCommand(); 662 685 await command.parseAsync(['node', 'test', 'view', '1']); ··· 709 732 issues: [mockIssue], 710 733 cursor: undefined, 711 734 }); 712 - vi.mocked(issuesApi.getIssue).mockResolvedValue(mockIssue); 713 - vi.mocked(issuesApi.getIssueState).mockResolvedValue('open'); 735 + vi.mocked(issuesApi.getCompleteIssueData).mockResolvedValue({ 736 + number: 1, 737 + title: mockIssue.title, 738 + body: mockIssue.body, 739 + state: 'open', 740 + author: mockIssue.author, 741 + createdAt: mockIssue.createdAt, 742 + uri: mockIssue.uri, 743 + cid: mockIssue.cid, 744 + }); 714 745 715 746 const command = createIssueCommand(); 716 747 await command.parseAsync(['node', 'test', 'view', '1', '--json']); 717 748 718 749 const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); 719 750 expect(jsonOutput).toMatchObject({ 751 + number: 1, 720 752 title: 'Test Issue', 721 753 body: 'Issue body', 722 754 state: 'open', ··· 731 763 issues: [mockIssue], 732 764 cursor: undefined, 733 765 }); 734 - vi.mocked(issuesApi.getIssue).mockResolvedValue(mockIssue); 735 - vi.mocked(issuesApi.getIssueState).mockResolvedValue('closed'); 766 + vi.mocked(issuesApi.getCompleteIssueData).mockResolvedValue({ 767 + number: 1, 768 + title: mockIssue.title, 769 + body: mockIssue.body, 770 + state: 'closed', 771 + author: mockIssue.author, 772 + createdAt: mockIssue.createdAt, 773 + uri: mockIssue.uri, 774 + cid: mockIssue.cid, 775 + }); 736 776 737 777 const command = createIssueCommand(); 738 778 await command.parseAsync(['node', 'test', 'view', '1', '--json', 'title,state']); ··· 866 906 cursor: undefined, 867 907 }); 868 908 vi.mocked(issuesApi.updateIssue).mockResolvedValue(updatedIssue); 909 + vi.mocked(issuesApi.resolveSequentialNumber).mockResolvedValue(1); 910 + vi.mocked(issuesApi.getIssueState).mockResolvedValue('open'); 869 911 870 912 const command = createIssueCommand(); 871 913 await command.parseAsync(['node', 'test', 'edit', '1', '--title', 'New Title', '--json']); 872 914 873 915 const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0] as string); 874 916 expect(jsonOutput).toMatchObject({ 917 + number: 1, 875 918 title: 'New Title', 919 + state: 'open', 876 920 author: 'did:plc:abc123', 877 921 uri: mockIssue.uri, 878 922 cid: mockIssue.cid, ··· 888 932 cursor: undefined, 889 933 }); 890 934 vi.mocked(issuesApi.updateIssue).mockResolvedValue(updatedIssue); 935 + vi.mocked(issuesApi.resolveSequentialNumber).mockResolvedValue(1); 936 + vi.mocked(issuesApi.getIssueState).mockResolvedValue('open'); 891 937 892 938 const command = createIssueCommand(); 893 939 await command.parseAsync([ ··· 947 993 }); 948 994 949 995 vi.mocked(atUri.buildRepoAtUri).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); 950 - vi.mocked(issuesApi.getIssue).mockResolvedValue(mockIssue); 996 + vi.mocked(issuesApi.getCompleteIssueData).mockResolvedValue({ 997 + number: 1, 998 + title: mockIssue.title, 999 + body: undefined, 1000 + state: 'closed', 1001 + author: mockIssue.author, 1002 + createdAt: mockIssue.createdAt, 1003 + uri: mockIssue.uri, 1004 + cid: mockIssue.cid, 1005 + }); 951 1006 }); 952 1007 953 1008 afterEach(() => { ··· 993 1048 expect(jsonOutput).toEqual({ 994 1049 number: 1, 995 1050 title: 'Test Issue', 996 - uri: mockIssue.uri, 997 1051 state: 'closed', 1052 + author: mockIssue.author, 1053 + createdAt: mockIssue.createdAt, 1054 + uri: mockIssue.uri, 998 1055 cid: mockIssue.cid, 999 1056 }); 1000 1057 }); ··· 1050 1107 }); 1051 1108 1052 1109 vi.mocked(atUri.buildRepoAtUri).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); 1053 - vi.mocked(issuesApi.getIssue).mockResolvedValue(mockIssue); 1110 + vi.mocked(issuesApi.getCompleteIssueData).mockResolvedValue({ 1111 + number: 1, 1112 + title: mockIssue.title, 1113 + body: undefined, 1114 + state: 'open', 1115 + author: mockIssue.author, 1116 + createdAt: mockIssue.createdAt, 1117 + uri: mockIssue.uri, 1118 + cid: mockIssue.cid, 1119 + }); 1054 1120 }); 1055 1121 1056 1122 afterEach(() => { ··· 1096 1162 expect(jsonOutput).toEqual({ 1097 1163 number: 1, 1098 1164 title: 'Test Issue', 1099 - uri: mockIssue.uri, 1100 1165 state: 'open', 1166 + author: mockIssue.author, 1167 + createdAt: mockIssue.createdAt, 1168 + uri: mockIssue.uri, 1101 1169 cid: mockIssue.cid, 1102 1170 }); 1103 1171 }); ··· 1154 1222 }); 1155 1223 1156 1224 vi.mocked(atUri.buildRepoAtUri).mockResolvedValue('at://did:plc:abc123/sh.tangled.repo/xyz789'); 1157 - vi.mocked(issuesApi.getIssue).mockResolvedValue(mockIssue); 1225 + vi.mocked(issuesApi.getCompleteIssueData).mockResolvedValue({ 1226 + number: 1, 1227 + title: mockIssue.title, 1228 + body: undefined, 1229 + state: 'open', 1230 + author: mockIssue.author, 1231 + createdAt: mockIssue.createdAt, 1232 + uri: mockIssue.uri, 1233 + cid: mockIssue.cid, 1234 + }); 1158 1235 }); 1159 1236 1160 1237 afterEach(() => { ··· 1241 1318 expect(jsonOutput).toEqual({ 1242 1319 number: 1, 1243 1320 title: 'Test Issue', 1321 + state: 'open', 1322 + author: mockIssue.author, 1323 + createdAt: mockIssue.createdAt, 1244 1324 uri: mockIssue.uri, 1245 1325 cid: mockIssue.cid, 1246 1326 }); 1247 - expect(jsonOutput).not.toHaveProperty('state'); 1248 1327 }); 1249 1328 1250 1329 it('should output filtered JSON when --json with fields is passed', async () => {