Something went wrong. Try again.
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 fro
Something went wrong. Try again.
28 kB · 998 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999import { beforeEach, describe, expect, it, vi } from 'vitest';import type { TangledApiClient } from '../../src/lib/api-client.js';import { getBacklinks } from '../../src/lib/constellation.js';import { closeIssue, createIssue, getCompleteIssueData, getIssue, getIssueState, listIssues, reopenIssue, resolveSequentialNumber, updateIssue,} from '../../src/lib/issues-api.js';
vi.mock('../../src/lib/constellation.js');
// Mock API client factoryconst createMockClient = (authenticated = true): TangledApiClient => { const mockAgent = { com: { atproto: { repo: { createRecord: vi.fn(), listRecords: vi.fn(), getRecord: vi.fn(), putRecord: vi.fn(), deleteRecord: vi.fn(), }, }, }, };
return { isAuthenticated: vi.fn(() => authenticated), getSession: vi.fn(() => authenticated ? { did: 'did:plc:test123', handle: 'test.bsky.social' } : null ), getAgent: vi.fn(() => mockAgent), } as unknown as TangledApiClient;};
describe('createIssue', () => { let mockClient: TangledApiClient;
beforeEach(() => { mockClient = createMockClient(true); });
it('should create an issue with all fields', async () => { const mockCreateRecord = vi.fn().mockResolvedValue({ data: { uri: 'at://did:plc:test123/sh.tangled.repo.issue/abc123', cid: 'cid123', }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { createRecord: mockCreateRecord, }, }, }, } as never);
const result = await createIssue({ client: mockClient, repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Bug: Login fails', body: 'Detailed description of the bug', });
expect(result).toMatchObject({ repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Bug: Login fails', body: 'Detailed description of the bug', uri: 'at://did:plc:test123/sh.tangled.repo.issue/abc123', cid: 'cid123', author: 'did:plc:test123', });
expect(mockCreateRecord).toHaveBeenCalledWith({ repo: 'did:plc:test123', collection: 'sh.tangled.repo.issue', record: expect.objectContaining({ $type: 'sh.tangled.repo.issue', repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Bug: Login fails', body: 'Detailed description of the bug', createdAt: expect.any(String), }), }); });
it('should create an issue without body', async () => { const mockCreateRecord = vi.fn().mockResolvedValue({ data: { uri: 'at://did:plc:test123/sh.tangled.repo.issue/abc123', cid: 'cid123', }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { createRecord: mockCreateRecord, }, }, }, } as never);
const result = await createIssue({ client: mockClient, repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Simple issue', });
expect(result.body).toBeUndefined(); expect(mockCreateRecord).toHaveBeenCalled(); });
it('should throw error when not authenticated', async () => { mockClient = createMockClient(false);
await expect( createIssue({ client: mockClient, repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Test', }) ).rejects.toThrow('Must be authenticated'); });
it('should throw error on API failure', async () => { const mockCreateRecord = vi.fn().mockRejectedValue(new Error('API error'));
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { createRecord: mockCreateRecord, }, }, }, } as never);
await expect( createIssue({ client: mockClient, repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Test', }) ).rejects.toThrow('Failed to create issue: API error'); });});
describe('listIssues', () => { let mockClient: TangledApiClient;
beforeEach(() => { mockClient = createMockClient(true); });
it('should list issues from multiple PDSs via constellation', async () => { vi.mocked(getBacklinks).mockResolvedValue({ total: 2, records: [ { did: 'did:plc:owner', collection: 'sh.tangled.repo.issue', rkey: 'issue1' }, { did: 'did:plc:collab', collection: 'sh.tangled.repo.issue', rkey: 'issue2' }, ], cursor: null, });
const mockGetRecord = vi .fn() .mockResolvedValueOnce({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', cid: 'cid1', value: { $type: 'sh.tangled.repo.issue', repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Issue 1', body: 'Description 1', createdAt: '2024-01-01T00:00:00.000Z', }, }, }) .mockResolvedValueOnce({ data: { uri: 'at://did:plc:collab/sh.tangled.repo.issue/issue2', cid: 'cid2', value: { $type: 'sh.tangled.repo.issue', repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Issue 2', createdAt: '2024-01-02T00:00:00.000Z', }, }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: mockGetRecord } } }, } as never);
const result = await listIssues({ client: mockClient, repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', });
expect(result.issues).toHaveLength(2); expect(result.issues[0]).toMatchObject({ title: 'Issue 1', body: 'Description 1', uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', author: 'did:plc:owner', }); expect(result.issues[1]).toMatchObject({ title: 'Issue 2', uri: 'at://did:plc:collab/sh.tangled.repo.issue/issue2', author: 'did:plc:collab', });
expect(getBacklinks).toHaveBeenCalledWith( 'at://did:plc:owner/sh.tangled.repo/my-repo', 'sh.tangled.repo.issue', '.repo', 50, undefined ); });
it('should return empty array when no issues found', async () => { vi.mocked(getBacklinks).mockResolvedValue({ total: 0, records: [], cursor: null });
const result = await listIssues({ client: mockClient, repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', });
expect(result.issues).toEqual([]); });
it('should forward cursor from constellation', async () => { vi.mocked(getBacklinks).mockResolvedValue({ total: 100, records: [], cursor: 'nextpage' });
const result = await listIssues({ client: mockClient, repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', });
expect(result.cursor).toBe('nextpage'); });
it('should throw error when not authenticated', async () => { mockClient = createMockClient(false);
await expect( listIssues({ client: mockClient, repoId: 'at://did:plc:owner/sh.tangled.repo/my-repo', }) ).rejects.toThrow('Must be authenticated'); });});
describe('getIssue', () => { let mockClient: TangledApiClient;
beforeEach(() => { mockClient = createMockClient(true); });
it('should get a specific issue', async () => { const mockGetRecord = vi.fn().mockResolvedValue({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', cid: 'cid1', value: { $type: 'sh.tangled.repo.issue', repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Test Issue', body: 'Test Description', createdAt: '2024-01-01T00:00:00.000Z', }, }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: mockGetRecord, }, }, }, } as never);
const result = await getIssue({ client: mockClient, issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', });
expect(result).toMatchObject({ title: 'Test Issue', body: 'Test Description', uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', cid: 'cid1', });
expect(mockGetRecord).toHaveBeenCalledWith({ repo: 'did:plc:owner', collection: 'sh.tangled.repo.issue', rkey: 'issue1', }); });
it('should throw error when issue not found', async () => { const mockGetRecord = vi.fn().mockRejectedValue(new Error('Record not found'));
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: mockGetRecord, }, }, }, } as never);
await expect( getIssue({ client: mockClient, issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/nonexistent', }) ).rejects.toThrow('Issue not found'); });
it('should throw error for invalid issue URI', async () => { await expect( getIssue({ client: mockClient, issueUri: 'invalid-uri', }) ).rejects.toThrow('Invalid issue AT-URI'); });
it('should throw error when not authenticated', async () => { mockClient = createMockClient(false);
await expect( getIssue({ client: mockClient, issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', }) ).rejects.toThrow('Must be authenticated'); });});
describe('updateIssue', () => { let mockClient: TangledApiClient;
beforeEach(() => { mockClient = createMockClient(true); });
it('should update issue title', async () => { const mockGetRecord = vi.fn().mockResolvedValue({ data: { uri: 'at://did:plc:test123/sh.tangled.repo.issue/issue1', cid: 'old-cid', value: { repo: 'at://did:plc:test123/sh.tangled.repo/my-repo', title: 'Old Title', body: 'Original body', createdAt: '2024-01-01T00:00:00.000Z', }, }, });
const mockPutRecord = vi.fn().mockResolvedValue({ data: { uri: 'at://did:plc:test123/sh.tangled.repo.issue/issue1', cid: 'new-cid', }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: mockGetRecord, putRecord: mockPutRecord, }, }, }, } as never);
const result = await updateIssue({ client: mockClient, issueUri: 'at://did:plc:test123/sh.tangled.repo.issue/issue1', title: 'New Title', });
expect(result.title).toBe('New Title'); expect(result.body).toBe('Original body'); // Body unchanged
expect(mockPutRecord).toHaveBeenCalledWith({ repo: 'did:plc:test123', collection: 'sh.tangled.repo.issue', rkey: 'issue1', record: expect.objectContaining({ title: 'New Title', body: 'Original body', }), swapRecord: 'old-cid', }); });
it('should update issue body', async () => { const mockGetRecord = vi.fn().mockResolvedValue({ data: { uri: 'at://did:plc:test123/sh.tangled.repo.issue/issue1', cid: 'old-cid', value: { repo: 'at://did:plc:test123/sh.tangled.repo/my-repo', title: 'Title', body: 'Old body', createdAt: '2024-01-01T00:00:00.000Z', }, }, });
const mockPutRecord = vi.fn().mockResolvedValue({ data: { cid: 'new-cid', }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: mockGetRecord, putRecord: mockPutRecord, }, }, }, } as never);
const result = await updateIssue({ client: mockClient, issueUri: 'at://did:plc:test123/sh.tangled.repo.issue/issue1', body: 'New body', });
expect(result.title).toBe('Title'); // Title unchanged expect(result.body).toBe('New body'); });
it('should throw error when updating issue not owned by user', async () => { await expect( updateIssue({ client: mockClient, issueUri: 'at://did:plc:someone-else/sh.tangled.repo.issue/issue1', title: 'New Title', }) ).rejects.toThrow('Cannot update issue: you are not the author'); });
it('should throw error when not authenticated', async () => { mockClient = createMockClient(false);
await expect( updateIssue({ client: mockClient, issueUri: 'at://did:plc:test123/sh.tangled.repo.issue/issue1', title: 'New Title', }) ).rejects.toThrow('Must be authenticated'); });});
describe('closeIssue', () => { let mockClient: TangledApiClient;
beforeEach(() => { mockClient = createMockClient(true); });
it('should close an issue', async () => { const mockGetRecord = vi.fn().mockResolvedValue({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', cid: 'cid1', value: { repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Test Issue', createdAt: '2024-01-01T00:00:00.000Z', }, }, });
const mockCreateRecord = vi.fn().mockResolvedValue({ data: { uri: 'at://did:plc:test123/sh.tangled.repo.issue.state/state1', cid: 'state-cid', }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: mockGetRecord, createRecord: mockCreateRecord, }, }, }, } as never);
await closeIssue({ client: mockClient, issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', });
expect(mockCreateRecord).toHaveBeenCalledWith({ repo: 'did:plc:test123', collection: 'sh.tangled.repo.issue.state', record: { $type: 'sh.tangled.repo.issue.state', issue: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', state: 'sh.tangled.repo.issue.state.closed', }, }); });
it('should throw error when not authenticated', async () => { mockClient = createMockClient(false);
await expect( closeIssue({ client: mockClient, issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', }) ).rejects.toThrow('Must be authenticated'); });});
describe('getIssueState', () => { let mockClient: TangledApiClient;
beforeEach(() => { mockClient = createMockClient(true); });
it('should return open when no state records exist', async () => { vi.mocked(getBacklinks).mockResolvedValue({ total: 0, records: [], cursor: null });
const result = await getIssueState({ client: mockClient, issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', });
expect(result).toBe('open'); expect(getBacklinks).toHaveBeenCalledWith( 'at://did:plc:owner/sh.tangled.repo.issue/issue1', 'sh.tangled.repo.issue.state', '.issue', 100 ); });
it('should return closed when latest state record is closed', async () => { vi.mocked(getBacklinks).mockResolvedValue({ total: 1, records: [ { did: 'did:plc:owner', collection: 'sh.tangled.repo.issue.state', rkey: 'state1' }, ], cursor: null, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: vi.fn().mockResolvedValue({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue.state/state1', cid: 'cid1', value: { state: 'sh.tangled.repo.issue.state.closed' }, }, }), }, }, }, } as never);
const result = await getIssueState({ client: mockClient, issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', });
expect(result).toBe('closed'); });
it('should return open when latest state record (by rkey) is open', async () => { vi.mocked(getBacklinks).mockResolvedValue({ total: 2, records: [ { did: 'did:plc:owner', collection: 'sh.tangled.repo.issue.state', rkey: 'aaa111' }, { did: 'did:plc:owner', collection: 'sh.tangled.repo.issue.state', rkey: 'bbb222' }, ], cursor: null, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: vi .fn() .mockResolvedValueOnce({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue.state/aaa111', cid: 'cid1', value: { state: 'sh.tangled.repo.issue.state.closed' }, }, }) .mockResolvedValueOnce({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue.state/bbb222', cid: 'cid2', value: { state: 'sh.tangled.repo.issue.state.open' }, }, }), }, }, }, } as never);
const result = await getIssueState({ client: mockClient, issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', });
expect(result).toBe('open'); });
it('should use rkey sort order to determine most recent state across PDSs', async () => { // Collaborator's close (rkey 'ccc333') is more recent than owner's open (rkey 'aaa111') vi.mocked(getBacklinks).mockResolvedValue({ total: 2, records: [ { did: 'did:plc:owner', collection: 'sh.tangled.repo.issue.state', rkey: 'aaa111' }, { did: 'did:plc:collab', collection: 'sh.tangled.repo.issue.state', rkey: 'ccc333' }, ], cursor: null, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: vi .fn() .mockResolvedValueOnce({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue.state/aaa111', cid: 'cid1', value: { state: 'sh.tangled.repo.issue.state.open' }, }, }) .mockResolvedValueOnce({ data: { uri: 'at://did:plc:collab/sh.tangled.repo.issue.state/ccc333', cid: 'cid2', value: { state: 'sh.tangled.repo.issue.state.closed' }, }, }), }, }, }, } as never);
const result = await getIssueState({ client: mockClient, issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', });
expect(result).toBe('closed'); });
it('should throw error when not authenticated', async () => { mockClient = createMockClient(false);
await expect( getIssueState({ client: mockClient, issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', }) ).rejects.toThrow('Must be authenticated'); });});
describe('reopenIssue', () => { let mockClient: TangledApiClient;
beforeEach(() => { mockClient = createMockClient(true); });
it('should reopen a closed issue', async () => { const mockGetRecord = vi.fn().mockResolvedValue({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', cid: 'cid1', value: { repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Test Issue', createdAt: '2024-01-01T00:00:00.000Z', }, }, });
const mockCreateRecord = vi.fn().mockResolvedValue({ data: { uri: 'at://did:plc:test123/sh.tangled.repo.issue.state/state1', cid: 'state-cid', }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: mockGetRecord, createRecord: mockCreateRecord, }, }, }, } as never);
await reopenIssue({ client: mockClient, issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', });
expect(mockCreateRecord).toHaveBeenCalledWith({ repo: 'did:plc:test123', collection: 'sh.tangled.repo.issue.state', record: { $type: 'sh.tangled.repo.issue.state', issue: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', state: 'sh.tangled.repo.issue.state.open', }, }); });
it('should throw error when not authenticated', async () => { mockClient = createMockClient(false);
await expect( reopenIssue({ client: mockClient, issueUri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', }) ).rejects.toThrow('Must be authenticated'); });});
describe('resolveSequentialNumber', () => { let mockClient: TangledApiClient;
beforeEach(() => { mockClient = createMockClient(true); });
it('should return number directly for #N displayId without an API call (fast path)', async () => { const result = await resolveSequentialNumber( '#3', 'at://did:plc:owner/sh.tangled.repo.issue/issue3', mockClient, 'at://did:plc:owner/sh.tangled.repo/my-repo' ); expect(result).toBe(3); });
it('should scan issue list and return 1-based position for rkey displayId', async () => { vi.mocked(getBacklinks).mockResolvedValue({ total: 2, records: [ { did: 'did:plc:owner', collection: 'sh.tangled.repo.issue', rkey: 'issue-a' }, { did: 'did:plc:owner', collection: 'sh.tangled.repo.issue', rkey: 'issue-b' }, ], cursor: null, });
const mockGetRecord = vi .fn() .mockResolvedValueOnce({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue-a', cid: 'cid1', value: { $type: 'sh.tangled.repo.issue', repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'First', createdAt: '2024-01-01T00:00:00.000Z', }, }, }) .mockResolvedValueOnce({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue-b', cid: 'cid2', value: { $type: 'sh.tangled.repo.issue', repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Second', createdAt: '2024-01-02T00:00:00.000Z', }, }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: mockGetRecord } } }, } as never);
const result = await resolveSequentialNumber( 'issue-b', 'at://did:plc:owner/sh.tangled.repo.issue/issue-b', mockClient, 'at://did:plc:owner/sh.tangled.repo/my-repo' ); expect(result).toBe(2); });
it('should return undefined when issue URI not found in list', async () => { vi.mocked(getBacklinks).mockResolvedValue({ total: 1, records: [{ did: 'did:plc:owner', collection: 'sh.tangled.repo.issue', rkey: 'issue-a' }], cursor: null, });
const mockGetRecord = vi.fn().mockResolvedValue({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue-a', cid: 'cid1', value: { $type: 'sh.tangled.repo.issue', repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'First', createdAt: '2024-01-01T00:00:00.000Z', }, }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: mockGetRecord } } }, } as never);
const result = await resolveSequentialNumber( 'nonexistent', 'at://did:plc:owner/sh.tangled.repo.issue/nonexistent', mockClient, 'at://did:plc:owner/sh.tangled.repo/my-repo' ); expect(result).toBeUndefined(); });});
describe('getCompleteIssueData', () => { let mockClient: TangledApiClient;
beforeEach(() => { vi.clearAllMocks(); mockClient = createMockClient(true); });
it('should return all fields including fetched state', async () => { vi.mocked(getBacklinks).mockResolvedValue({ total: 1, records: [{ did: 'did:plc:owner', collection: 'sh.tangled.repo.issue.state', rkey: 's1' }], cursor: null, });
const mockGetRecord = vi .fn() .mockResolvedValueOnce({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', cid: 'cid1', value: { $type: 'sh.tangled.repo.issue', repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Test Issue', body: 'Test body', createdAt: '2024-01-01T00:00:00.000Z', }, }, }) .mockResolvedValueOnce({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue.state/s1', cid: 'scid1', value: { state: 'sh.tangled.repo.issue.state.closed' }, }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: mockGetRecord } } }, } as never);
const result = await getCompleteIssueData( mockClient, 'at://did:plc:owner/sh.tangled.repo.issue/issue1', '#1', // fast-path for number 'at://did:plc:owner/sh.tangled.repo/my-repo' );
expect(result).toEqual({ number: 1, title: 'Test Issue', body: 'Test body', state: 'closed', author: 'did:plc:owner', createdAt: '2024-01-01T00:00:00.000Z', uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', cid: 'cid1', }); });
it('should use stateOverride and skip the getIssueState network call', async () => { const mockGetRecord = vi.fn().mockResolvedValue({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', cid: 'cid1', value: { $type: 'sh.tangled.repo.issue', repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'Test Issue', createdAt: '2024-01-01T00:00:00.000Z', }, }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: mockGetRecord } } }, } as never);
const result = await getCompleteIssueData( mockClient, 'at://did:plc:owner/sh.tangled.repo.issue/issue1', '#2', 'at://did:plc:owner/sh.tangled.repo/my-repo', 'closed' );
expect(result.number).toBe(2); expect(result.state).toBe('closed'); expect(getBacklinks).not.toHaveBeenCalled(); });
it('should return undefined body and default open state when issue has no body or state records', async () => { vi.mocked(getBacklinks).mockResolvedValue({ total: 0, records: [], cursor: null });
const mockGetRecord = vi.fn().mockResolvedValue({ data: { uri: 'at://did:plc:owner/sh.tangled.repo.issue/issue1', cid: 'cid1', value: { $type: 'sh.tangled.repo.issue', repo: 'at://did:plc:owner/sh.tangled.repo/my-repo', title: 'No body issue', createdAt: '2024-01-01T00:00:00.000Z', }, }, });
vi.mocked(mockClient.getAgent).mockReturnValue({ com: { atproto: { repo: { getRecord: mockGetRecord } } }, } as never);
const result = await getCompleteIssueData( mockClient, 'at://did:plc:owner/sh.tangled.repo.issue/issue1', '#1', 'at://did:plc:owner/sh.tangled.repo/my-repo' );
expect(result.body).toBeUndefined(); expect(result.state).toBe('open'); });});