@hubblecommerce/overmind-core 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/src/integrations/confluence/confluence.client.test.d.ts +2 -0
  2. package/dist/src/integrations/confluence/confluence.client.test.d.ts.map +1 -0
  3. package/dist/src/integrations/confluence/confluence.client.test.js +320 -0
  4. package/dist/src/integrations/gitlab/gitlab.client.d.ts +8 -0
  5. package/dist/src/integrations/gitlab/gitlab.client.d.ts.map +1 -1
  6. package/dist/src/integrations/gitlab/gitlab.client.js +20 -0
  7. package/dist/src/integrations/gitlab/gitlab.client.test.d.ts +2 -0
  8. package/dist/src/integrations/gitlab/gitlab.client.test.d.ts.map +1 -0
  9. package/dist/src/integrations/gitlab/gitlab.client.test.js +62 -0
  10. package/dist/src/integrations/jira/jira.client.test.d.ts +2 -0
  11. package/dist/src/integrations/jira/jira.client.test.d.ts.map +1 -0
  12. package/dist/src/integrations/jira/jira.client.test.js +171 -0
  13. package/dist/src/llm/anthropic-retry-wrapper.test.d.ts +2 -0
  14. package/dist/src/llm/anthropic-retry-wrapper.test.d.ts.map +1 -0
  15. package/dist/src/llm/anthropic-retry-wrapper.test.js +125 -0
  16. package/dist/src/processors/confluence-document.processor.test.d.ts +2 -0
  17. package/dist/src/processors/confluence-document.processor.test.d.ts.map +1 -0
  18. package/dist/src/processors/confluence-document.processor.test.js +202 -0
  19. package/dist/src/processors/confluence-html-parser.test.d.ts +2 -0
  20. package/dist/src/processors/confluence-html-parser.test.d.ts.map +1 -0
  21. package/dist/src/processors/confluence-html-parser.test.js +214 -0
  22. package/dist/src/tools/confluence-search.tool.d.ts +6 -6
  23. package/dist/src/utils/repomix-search.d.ts +31 -0
  24. package/dist/src/utils/repomix-search.d.ts.map +1 -0
  25. package/dist/src/utils/repomix-search.js +80 -0
  26. package/dist/src/utils/repomix-search.test.d.ts +2 -0
  27. package/dist/src/utils/repomix-search.test.d.ts.map +1 -0
  28. package/dist/src/utils/repomix-search.test.js +75 -0
  29. package/package.json +11 -2
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=confluence.client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confluence.client.test.d.ts","sourceRoot":"","sources":["../../../../src/integrations/confluence/confluence.client.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,320 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { ConfluenceClient } from './confluence.client.js';
3
+ const BASE_URL = 'https://example.atlassian.net';
4
+ function makeClient() {
5
+ return new ConfluenceClient({
6
+ baseUrl: BASE_URL,
7
+ email: 'test@test.com',
8
+ apiToken: 'token',
9
+ rateLimit: 10000, // effectively no rate-limit delay in tests
10
+ });
11
+ }
12
+ function jsonResponse(data, status = 200) {
13
+ return new Response(JSON.stringify(data), {
14
+ status,
15
+ headers: { 'Content-Type': 'application/json' },
16
+ });
17
+ }
18
+ const spaceFixture = {
19
+ id: 'space-id-1',
20
+ key: 'TEST',
21
+ name: 'Test Space',
22
+ type: 'global',
23
+ status: 'current',
24
+ authorId: 'author1',
25
+ createdAt: '2024-01-01T00:00:00.000Z',
26
+ _links: { webui: '/spaces/TEST' },
27
+ };
28
+ const spaceResponse = { results: [spaceFixture], _links: {} };
29
+ function makePageFixture(id) {
30
+ return {
31
+ id,
32
+ status: 'current',
33
+ title: `Page ${id}`,
34
+ spaceId: spaceFixture.id,
35
+ authorId: 'author1',
36
+ createdAt: '2024-01-01T00:00:00.000Z',
37
+ version: { createdAt: '2024-01-01T00:00:00.000Z', message: '', number: 1, minorEdit: false, authorId: 'author1' },
38
+ _links: { webui: `/wiki/spaces/TEST/pages/${id}` },
39
+ };
40
+ }
41
+ describe('ConfluenceClient', () => {
42
+ let fetchMock;
43
+ beforeEach(() => {
44
+ fetchMock = vi.fn();
45
+ vi.stubGlobal('fetch', fetchMock);
46
+ vi.useFakeTimers();
47
+ });
48
+ afterEach(() => {
49
+ vi.unstubAllGlobals();
50
+ vi.useRealTimers();
51
+ });
52
+ describe('getSpacePages', () => {
53
+ it('returns pages for a single-page response (no pagination)', async () => {
54
+ const pageResponse = { results: [makePageFixture('p1')], _links: {} };
55
+ fetchMock
56
+ .mockResolvedValueOnce(jsonResponse(spaceResponse))
57
+ .mockResolvedValueOnce(jsonResponse(pageResponse));
58
+ const promise = makeClient().getSpacePages('TEST');
59
+ await vi.runAllTimersAsync();
60
+ const pages = await promise;
61
+ expect(pages).toHaveLength(1);
62
+ expect(pages[0].id).toBe('p1');
63
+ });
64
+ it('follows pagination and combines results from multiple pages', async () => {
65
+ const page1Response = {
66
+ results: [makePageFixture('p1')],
67
+ _links: { next: `${BASE_URL}/wiki/api/v2/spaces/space-id-1/pages?cursor=abc` },
68
+ };
69
+ const page2Response = {
70
+ results: [makePageFixture('p2')],
71
+ _links: {},
72
+ };
73
+ fetchMock
74
+ .mockResolvedValueOnce(jsonResponse(spaceResponse))
75
+ .mockResolvedValueOnce(jsonResponse(page1Response))
76
+ .mockResolvedValueOnce(jsonResponse(page2Response));
77
+ const promise = makeClient().getSpacePages('TEST');
78
+ await vi.runAllTimersAsync();
79
+ const pages = await promise;
80
+ expect(pages).toHaveLength(2);
81
+ expect(pages[0].id).toBe('p1');
82
+ expect(pages[1].id).toBe('p2');
83
+ });
84
+ it('throws when space key is not found', async () => {
85
+ fetchMock.mockResolvedValueOnce(jsonResponse({ results: [], _links: {} }));
86
+ const promise = makeClient().getSpacePages('UNKNOWN');
87
+ const assertion = expect(promise).rejects.toThrow('Space with key "UNKNOWN" not found');
88
+ await vi.runAllTimersAsync();
89
+ await assertion;
90
+ });
91
+ });
92
+ describe('getPageLabels', () => {
93
+ it('returns label names from the response', async () => {
94
+ const labelResponse = {
95
+ results: [
96
+ { id: '1', name: 'project-info', prefix: 'global' },
97
+ { id: '2', name: 'active', prefix: 'global' },
98
+ ],
99
+ start: 0, limit: 200, size: 2, _links: {},
100
+ };
101
+ fetchMock.mockResolvedValueOnce(jsonResponse(labelResponse));
102
+ const promise = makeClient().getPageLabels('page1');
103
+ await vi.runAllTimersAsync();
104
+ const labels = await promise;
105
+ expect(labels).toEqual(['project-info', 'active']);
106
+ });
107
+ it('returns empty array when page has no labels', async () => {
108
+ const labelResponse = { results: [], start: 0, limit: 200, size: 0, _links: {} };
109
+ fetchMock.mockResolvedValueOnce(jsonResponse(labelResponse));
110
+ const promise = makeClient().getPageLabels('page1');
111
+ await vi.runAllTimersAsync();
112
+ const labels = await promise;
113
+ expect(labels).toEqual([]);
114
+ });
115
+ });
116
+ describe('createPageWithADF', () => {
117
+ const createPageResponse = {
118
+ id: 'new-page-1',
119
+ status: 'current',
120
+ title: 'My New Page',
121
+ spaceId: spaceFixture.id,
122
+ authorId: 'author1',
123
+ createdAt: '2024-01-01T00:00:00.000Z',
124
+ version: { createdAt: '2024-01-01T00:00:00.000Z', message: '', number: 1, minorEdit: false, authorId: 'author1' },
125
+ _links: { webui: '/wiki/spaces/TEST/pages/new-page-1' },
126
+ };
127
+ it('sends correct body structure with ADF content', async () => {
128
+ fetchMock.mockResolvedValueOnce(jsonResponse(createPageResponse));
129
+ const adfBody = { type: 'doc', version: 1, content: [] };
130
+ const promise = makeClient().createPageWithADF({ spaceId: 'S1', title: 'My New Page', adfBody });
131
+ await vi.runAllTimersAsync();
132
+ await promise;
133
+ const [, options] = fetchMock.mock.calls[0];
134
+ const body = JSON.parse(options.body);
135
+ expect(body.title).toBe('My New Page');
136
+ expect(body.spaceId).toBe('S1');
137
+ expect(body.body.representation).toBe('atlas_doc_format');
138
+ expect(JSON.parse(body.body.value)).toEqual(adfBody);
139
+ });
140
+ it('includes parentId in payload when provided', async () => {
141
+ fetchMock.mockResolvedValueOnce(jsonResponse(createPageResponse));
142
+ const promise = makeClient().createPageWithADF({ spaceId: 'S1', title: 'Child', adfBody: {}, parentId: 'parent-1' });
143
+ await vi.runAllTimersAsync();
144
+ await promise;
145
+ const [, options] = fetchMock.mock.calls[0];
146
+ const body = JSON.parse(options.body);
147
+ expect(body.parentId).toBe('parent-1');
148
+ });
149
+ it('omits parentId from payload when not provided', async () => {
150
+ fetchMock.mockResolvedValueOnce(jsonResponse(createPageResponse));
151
+ const promise = makeClient().createPageWithADF({ spaceId: 'S1', title: 'Root Page', adfBody: {} });
152
+ await vi.runAllTimersAsync();
153
+ await promise;
154
+ const [, options] = fetchMock.mock.calls[0];
155
+ const body = JSON.parse(options.body);
156
+ expect(body.parentId).toBeUndefined();
157
+ });
158
+ it('returns the created page', async () => {
159
+ fetchMock.mockResolvedValueOnce(jsonResponse(createPageResponse));
160
+ const promise = makeClient().createPageWithADF({ spaceId: 'S1', title: 'My New Page', adfBody: {} });
161
+ await vi.runAllTimersAsync();
162
+ const page = await promise;
163
+ expect(page.id).toBe('new-page-1');
164
+ expect(page.title).toBe('My New Page');
165
+ });
166
+ });
167
+ describe('getSpaces', () => {
168
+ it('returns spaces from paginated response', async () => {
169
+ fetchMock.mockResolvedValueOnce(jsonResponse({ results: [spaceFixture], _links: {} }));
170
+ const promise = makeClient().getSpaces();
171
+ await vi.runAllTimersAsync();
172
+ const spaces = await promise;
173
+ expect(spaces).toHaveLength(1);
174
+ expect(spaces[0].key).toBe('TEST');
175
+ });
176
+ });
177
+ describe('getPageById', () => {
178
+ it('returns the page for a given id', async () => {
179
+ const page = makePageFixture('p42');
180
+ fetchMock.mockResolvedValueOnce(jsonResponse(page));
181
+ const promise = makeClient().getPageById('p42');
182
+ await vi.runAllTimersAsync();
183
+ const result = await promise;
184
+ expect(result.id).toBe('p42');
185
+ });
186
+ it('uses default body format storage', async () => {
187
+ fetchMock.mockResolvedValueOnce(jsonResponse(makePageFixture('p1')));
188
+ const promise = makeClient().getPageById('p1');
189
+ await vi.runAllTimersAsync();
190
+ await promise;
191
+ const [url] = fetchMock.mock.calls[0];
192
+ expect(url).toContain('body-format=storage');
193
+ });
194
+ it('accepts atlas_doc_format body format', async () => {
195
+ fetchMock.mockResolvedValueOnce(jsonResponse(makePageFixture('p1')));
196
+ const promise = makeClient().getPageById('p1', 'atlas_doc_format');
197
+ await vi.runAllTimersAsync();
198
+ await promise;
199
+ const [url] = fetchMock.mock.calls[0];
200
+ expect(url).toContain('body-format=atlas_doc_format');
201
+ });
202
+ });
203
+ describe('getPageContent', () => {
204
+ it('returns body storage value', async () => {
205
+ const page = { ...makePageFixture('p1'), body: { storage: { value: '<p>Hello</p>' } } };
206
+ fetchMock.mockResolvedValueOnce(jsonResponse(page));
207
+ const promise = makeClient().getPageContent('p1');
208
+ await vi.runAllTimersAsync();
209
+ const content = await promise;
210
+ expect(content).toBe('<p>Hello</p>');
211
+ });
212
+ it('throws when page has no body', async () => {
213
+ const page = makePageFixture('p1'); // no body field
214
+ fetchMock.mockResolvedValueOnce(jsonResponse(page));
215
+ const promise = makeClient().getPageContent('p1');
216
+ const assertion = expect(promise).rejects.toThrow('no content');
217
+ await vi.runAllTimersAsync();
218
+ await assertion;
219
+ });
220
+ });
221
+ describe('getChildPages', () => {
222
+ it('returns child pages in a single request when below limit', async () => {
223
+ const results = [{ id: 'c1' }, { id: 'c2' }];
224
+ fetchMock.mockResolvedValueOnce(jsonResponse({ results, size: 2 }));
225
+ const promise = makeClient().getChildPages('parent1');
226
+ await vi.runAllTimersAsync();
227
+ const pages = await promise;
228
+ expect(pages).toHaveLength(2);
229
+ expect(fetchMock).toHaveBeenCalledTimes(1);
230
+ });
231
+ it('paginates when first response returns exactly the limit', async () => {
232
+ const fullPage = Array.from({ length: 250 }, (_, i) => ({ id: `c${i}` }));
233
+ fetchMock
234
+ .mockResolvedValueOnce(jsonResponse({ results: fullPage, size: 250 }))
235
+ .mockResolvedValueOnce(jsonResponse({ results: [{ id: 'c250' }], size: 1 }));
236
+ const promise = makeClient().getChildPages('parent1');
237
+ await vi.runAllTimersAsync();
238
+ const pages = await promise;
239
+ expect(pages).toHaveLength(251);
240
+ expect(fetchMock).toHaveBeenCalledTimes(2);
241
+ const [secondUrl] = fetchMock.mock.calls[1];
242
+ expect(secondUrl).toContain('start=250');
243
+ });
244
+ });
245
+ describe('getRecentPages', () => {
246
+ it('returns pages without space filter when no spaceKeys provided', async () => {
247
+ const pageResponse = { results: [makePageFixture('p1')] };
248
+ fetchMock.mockResolvedValueOnce(jsonResponse(pageResponse));
249
+ const promise = makeClient().getRecentPages();
250
+ await vi.runAllTimersAsync();
251
+ const pages = await promise;
252
+ expect(pages).toHaveLength(1);
253
+ const [url] = fetchMock.mock.calls[0];
254
+ expect(url).not.toContain('space-key');
255
+ });
256
+ it('adds space-key filter when spaceKeys provided', async () => {
257
+ const pageResponse = { results: [makePageFixture('p1')] };
258
+ fetchMock.mockResolvedValueOnce(jsonResponse(pageResponse));
259
+ const promise = makeClient().getRecentPages(['TEST', 'PROJ']);
260
+ await vi.runAllTimersAsync();
261
+ await promise;
262
+ const [url] = fetchMock.mock.calls[0];
263
+ expect(url).toContain('space-key=TEST');
264
+ expect(url).toContain('space-key=PROJ');
265
+ });
266
+ it('respects custom limit', async () => {
267
+ fetchMock.mockResolvedValueOnce(jsonResponse({ results: [] }));
268
+ const promise = makeClient().getRecentPages(undefined, 50);
269
+ await vi.runAllTimersAsync();
270
+ await promise;
271
+ const [url] = fetchMock.mock.calls[0];
272
+ expect(url).toContain('limit=50');
273
+ });
274
+ });
275
+ describe('getPageLabels pagination', () => {
276
+ it('fetches next page when first response fills the limit', async () => {
277
+ const fullPage = Array.from({ length: 200 }, (_, i) => ({ id: `${i}`, name: `label-${i}`, prefix: 'global' }));
278
+ const secondPage = { results: [{ id: '200', name: 'extra', prefix: 'global' }], start: 200, limit: 200, size: 1, _links: {} };
279
+ fetchMock
280
+ .mockResolvedValueOnce(jsonResponse({ results: fullPage, start: 0, limit: 200, size: 200, _links: {} }))
281
+ .mockResolvedValueOnce(jsonResponse(secondPage));
282
+ const promise = makeClient().getPageLabels('page1');
283
+ await vi.runAllTimersAsync();
284
+ const labels = await promise;
285
+ expect(labels).toHaveLength(201);
286
+ expect(labels[200]).toBe('extra');
287
+ });
288
+ });
289
+ describe('error handling', () => {
290
+ it('throws on 4xx error without retrying', async () => {
291
+ fetchMock.mockResolvedValue(new Response('Not Found', { status: 404, statusText: 'Not Found' }));
292
+ const promise = makeClient().getPageById('page1');
293
+ const assertion = expect(promise).rejects.toThrow();
294
+ await vi.runAllTimersAsync();
295
+ await assertion;
296
+ // Only 1 fetch call (no retries for 4xx)
297
+ expect(fetchMock).toHaveBeenCalledTimes(1);
298
+ });
299
+ it('retries on 5xx error and eventually throws', async () => {
300
+ fetchMock.mockResolvedValue(new Response('Server Error', { status: 500, statusText: 'Internal Server Error' }));
301
+ const promise = makeClient().getPageById('page1');
302
+ const assertion = expect(promise).rejects.toThrow();
303
+ await vi.runAllTimersAsync();
304
+ await assertion;
305
+ // 3 attempts total (initial + 2 retries)
306
+ expect(fetchMock).toHaveBeenCalledTimes(3);
307
+ });
308
+ it('succeeds on retry after transient 5xx', async () => {
309
+ const page = makePageFixture('p1');
310
+ fetchMock
311
+ .mockResolvedValueOnce(new Response('Server Error', { status: 500, statusText: 'Internal Server Error' }))
312
+ .mockResolvedValueOnce(jsonResponse(page));
313
+ const promise = makeClient().getPageById('p1');
314
+ await vi.runAllTimersAsync();
315
+ const result = await promise;
316
+ expect(result.id).toBe('p1');
317
+ expect(fetchMock).toHaveBeenCalledTimes(2);
318
+ });
319
+ });
320
+ });
@@ -1,3 +1,11 @@
1
1
  import { Gitlab } from '@gitbeaker/rest';
2
2
  export declare function getGitlabClient(): InstanceType<typeof Gitlab>;
3
+ /**
4
+ * Get the SHA of the latest commit on the default branch.
5
+ */
6
+ export declare function getLatestCommitSha(client: InstanceType<typeof Gitlab>, projectId: number | string): Promise<string>;
7
+ /**
8
+ * Download the repository archive as a Buffer.
9
+ */
10
+ export declare function downloadArchive(client: InstanceType<typeof Gitlab>, projectId: number | string): Promise<Buffer>;
3
11
  //# sourceMappingURL=gitlab.client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gitlab.client.d.ts","sourceRoot":"","sources":["../../../../src/integrations/gitlab/gitlab.client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAKzC,wBAAgB,eAAe,IAAI,YAAY,CAAC,OAAO,MAAM,CAAC,CAa7D"}
1
+ {"version":3,"file":"gitlab.client.d.ts","sourceRoot":"","sources":["../../../../src/integrations/gitlab/gitlab.client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAMzC,wBAAgB,eAAe,IAAI,YAAY,CAAC,OAAO,MAAM,CAAC,CAa7D;AAGD;;GAEG;AACH,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,YAAY,CAAC,OAAO,MAAM,CAAC,EACnC,SAAS,EAAE,MAAM,GAAG,MAAM,GAC3B,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACjC,MAAM,EAAE,YAAY,CAAC,OAAO,MAAM,CAAC,EACnC,SAAS,EAAE,MAAM,GAAG,MAAM,GAC3B,OAAO,CAAC,MAAM,CAAC,CAIjB"}
@@ -1,5 +1,6 @@
1
1
  import { Gitlab } from '@gitbeaker/rest';
2
2
  import { config } from '../../config/app.config.js';
3
+ /* v8 ignore start -- gitbeaker singleton wiring, no custom logic */
3
4
  let gitlabClient = null;
4
5
  export function getGitlabClient() {
5
6
  if (!gitlabClient) {
@@ -13,3 +14,22 @@ export function getGitlabClient() {
13
14
  }
14
15
  return gitlabClient;
15
16
  }
17
+ /* v8 ignore stop */
18
+ /**
19
+ * Get the SHA of the latest commit on the default branch.
20
+ */
21
+ export async function getLatestCommitSha(client, projectId) {
22
+ const commits = await client.Commits.all(projectId, { perPage: 1, maxPages: 1 });
23
+ if (commits.length === 0) {
24
+ throw new Error(`No commits found for project ${projectId}`);
25
+ }
26
+ return commits[0].id;
27
+ }
28
+ /**
29
+ * Download the repository archive as a Buffer.
30
+ */
31
+ export async function downloadArchive(client, projectId) {
32
+ const blob = await client.Repositories.showArchive(projectId, { fileType: 'tar.gz' });
33
+ const arrayBuffer = await blob.arrayBuffer();
34
+ return Buffer.from(arrayBuffer);
35
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=gitlab.client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitlab.client.test.d.ts","sourceRoot":"","sources":["../../../../src/integrations/gitlab/gitlab.client.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { getLatestCommitSha, downloadArchive } from './gitlab.client.js';
3
+ function makeMockClient(overrides = {}) {
4
+ const commitsAll = overrides.commitsAll ?? vi.fn();
5
+ const repositoriesShowArchive = overrides.repositoriesShowArchive ?? vi.fn();
6
+ const client = {
7
+ Commits: { all: commitsAll },
8
+ Repositories: { showArchive: repositoriesShowArchive },
9
+ };
10
+ return { client, mocks: { commitsAll, repositoriesShowArchive } };
11
+ }
12
+ describe('getLatestCommitSha', () => {
13
+ it('returns the SHA of the latest commit', async () => {
14
+ const { client, mocks } = makeMockClient({
15
+ commitsAll: vi.fn().mockResolvedValue([
16
+ { id: 'abc123def456', short_id: 'abc123d', title: 'Latest commit' },
17
+ ]),
18
+ });
19
+ const sha = await getLatestCommitSha(client, 42);
20
+ expect(sha).toBe('abc123def456');
21
+ expect(mocks.commitsAll).toHaveBeenCalledWith(42, { perPage: 1, maxPages: 1 });
22
+ });
23
+ it('accepts string project IDs', async () => {
24
+ const { client, mocks } = makeMockClient({
25
+ commitsAll: vi.fn().mockResolvedValue([
26
+ { id: 'sha999', short_id: 'sha999' },
27
+ ]),
28
+ });
29
+ const sha = await getLatestCommitSha(client, 'group%2Fproject');
30
+ expect(sha).toBe('sha999');
31
+ expect(mocks.commitsAll).toHaveBeenCalledWith('group%2Fproject', { perPage: 1, maxPages: 1 });
32
+ });
33
+ it('throws when the commits array is empty', async () => {
34
+ const { client } = makeMockClient({
35
+ commitsAll: vi.fn().mockResolvedValue([]),
36
+ });
37
+ await expect(getLatestCommitSha(client, 42))
38
+ .rejects.toThrow('No commits found');
39
+ });
40
+ });
41
+ describe('downloadArchive', () => {
42
+ it('returns a Buffer with the archive contents', async () => {
43
+ const archiveData = new Uint8Array([0x1f, 0x8b, 0x08, 0x00]);
44
+ const blob = new Blob([archiveData]);
45
+ const { client, mocks } = makeMockClient({
46
+ repositoriesShowArchive: vi.fn().mockResolvedValue(blob),
47
+ });
48
+ const result = await downloadArchive(client, 42);
49
+ expect(result).toBeInstanceOf(Buffer);
50
+ expect(result.length).toBe(4);
51
+ expect(result[0]).toBe(0x1f);
52
+ expect(mocks.repositoriesShowArchive).toHaveBeenCalledWith(42, { fileType: 'tar.gz' });
53
+ });
54
+ it('accepts string project IDs', async () => {
55
+ const blob = new Blob([new Uint8Array([0x00])]);
56
+ const { client, mocks } = makeMockClient({
57
+ repositoriesShowArchive: vi.fn().mockResolvedValue(blob),
58
+ });
59
+ await downloadArchive(client, 'group%2Fproject');
60
+ expect(mocks.repositoriesShowArchive).toHaveBeenCalledWith('group%2Fproject', { fileType: 'tar.gz' });
61
+ });
62
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=jira.client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jira.client.test.d.ts","sourceRoot":"","sources":["../../../../src/integrations/jira/jira.client.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,171 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { JiraClient } from './jira.client.js';
3
+ const BASE_URL = 'https://example.atlassian.net';
4
+ function makeClient() {
5
+ return new JiraClient({ baseUrl: BASE_URL, email: 'test@test.com', apiToken: 'token' });
6
+ }
7
+ const issueFixture = {
8
+ id: '10001',
9
+ key: 'PROJ-1',
10
+ self: `${BASE_URL}/rest/api/3/issue/PROJ-1`,
11
+ fields: {
12
+ summary: 'Test Issue',
13
+ status: { name: 'In Progress', id: '1' },
14
+ issuetype: { name: 'Story', id: '1' },
15
+ project: { key: 'PROJ', id: '1', name: 'My Project' },
16
+ assignee: null,
17
+ reporter: null,
18
+ created: '2024-01-01T00:00:00.000Z',
19
+ updated: '2024-01-02T00:00:00.000Z',
20
+ resolutiondate: null,
21
+ labels: [],
22
+ components: [],
23
+ },
24
+ };
25
+ function makeCommentsResponse(comments) {
26
+ return { comments };
27
+ }
28
+ function makeAdfComment(content, displayName = 'John Doe') {
29
+ return {
30
+ id: '1',
31
+ author: { accountId: 'u1', displayName },
32
+ body: { type: 'doc', version: 1, content },
33
+ created: '2024-01-01T00:00:00.000Z',
34
+ updated: '2024-01-01T00:00:00.000Z',
35
+ };
36
+ }
37
+ function jsonResponse(data, status = 200) {
38
+ return new Response(JSON.stringify(data), {
39
+ status,
40
+ headers: { 'Content-Type': 'application/json' },
41
+ });
42
+ }
43
+ describe('JiraClient.getIssue', () => {
44
+ let fetchMock;
45
+ beforeEach(() => {
46
+ fetchMock = vi.fn();
47
+ vi.stubGlobal('fetch', fetchMock);
48
+ });
49
+ afterEach(() => {
50
+ vi.unstubAllGlobals();
51
+ });
52
+ it('returns issue with parsed ADF comments', async () => {
53
+ const comment = makeAdfComment([
54
+ { type: 'paragraph', content: [{ type: 'text', text: 'Hello world' }] },
55
+ ]);
56
+ fetchMock
57
+ .mockResolvedValueOnce(jsonResponse(issueFixture))
58
+ .mockResolvedValueOnce(jsonResponse(makeCommentsResponse([comment])));
59
+ const result = await makeClient().getIssue('PROJ-1');
60
+ expect(result.key).toBe('PROJ-1');
61
+ expect(result.comments).toHaveLength(1);
62
+ expect(result.comments[0].author).toBe('John Doe');
63
+ expect(result.comments[0].body).toContain('Hello world');
64
+ });
65
+ it('parses paragraph ADF node to plain text', async () => {
66
+ const comment = makeAdfComment([
67
+ { type: 'paragraph', content: [{ type: 'text', text: 'A paragraph.' }] },
68
+ ]);
69
+ fetchMock
70
+ .mockResolvedValueOnce(jsonResponse(issueFixture))
71
+ .mockResolvedValueOnce(jsonResponse(makeCommentsResponse([comment])));
72
+ const { comments } = await makeClient().getIssue('PROJ-1');
73
+ expect(comments[0].body).toBe('A paragraph.');
74
+ });
75
+ it('parses blockquote ADF node with > prefix', async () => {
76
+ const comment = makeAdfComment([
77
+ { type: 'blockquote', content: [
78
+ { type: 'paragraph', content: [{ type: 'text', text: 'quoted text' }] },
79
+ ] },
80
+ ]);
81
+ fetchMock
82
+ .mockResolvedValueOnce(jsonResponse(issueFixture))
83
+ .mockResolvedValueOnce(jsonResponse(makeCommentsResponse([comment])));
84
+ const { comments } = await makeClient().getIssue('PROJ-1');
85
+ expect(comments[0].body).toContain('> ');
86
+ expect(comments[0].body).toContain('quoted text');
87
+ });
88
+ it('parses heading ADF node with ## prefix', async () => {
89
+ const comment = makeAdfComment([
90
+ { type: 'heading', content: [{ type: 'text', text: 'Section Header' }] },
91
+ ]);
92
+ fetchMock
93
+ .mockResolvedValueOnce(jsonResponse(issueFixture))
94
+ .mockResolvedValueOnce(jsonResponse(makeCommentsResponse([comment])));
95
+ const { comments } = await makeClient().getIssue('PROJ-1');
96
+ expect(comments[0].body).toContain('## Section Header');
97
+ });
98
+ it('parses listItem ADF node with - prefix', async () => {
99
+ const comment = makeAdfComment([
100
+ { type: 'listItem', content: [{ type: 'text', text: 'list item' }] },
101
+ ]);
102
+ fetchMock
103
+ .mockResolvedValueOnce(jsonResponse(issueFixture))
104
+ .mockResolvedValueOnce(jsonResponse(makeCommentsResponse([comment])));
105
+ const { comments } = await makeClient().getIssue('PROJ-1');
106
+ expect(comments[0].body).toContain('- list item');
107
+ });
108
+ it('parses codeBlock ADF node as fenced code', async () => {
109
+ const comment = makeAdfComment([
110
+ { type: 'codeBlock', content: [{ type: 'text', text: 'console.log("hi")' }] },
111
+ ]);
112
+ fetchMock
113
+ .mockResolvedValueOnce(jsonResponse(issueFixture))
114
+ .mockResolvedValueOnce(jsonResponse(makeCommentsResponse([comment])));
115
+ const { comments } = await makeClient().getIssue('PROJ-1');
116
+ expect(comments[0].body).toContain('```');
117
+ expect(comments[0].body).toContain('console.log("hi")');
118
+ });
119
+ it('parses mention ADF node using attrs.text', async () => {
120
+ const comment = makeAdfComment([
121
+ { type: 'paragraph', content: [
122
+ { type: 'mention', attrs: { text: '@Jane', id: 'u2' } },
123
+ ] },
124
+ ]);
125
+ fetchMock
126
+ .mockResolvedValueOnce(jsonResponse(issueFixture))
127
+ .mockResolvedValueOnce(jsonResponse(makeCommentsResponse([comment])));
128
+ const { comments } = await makeClient().getIssue('PROJ-1');
129
+ expect(comments[0].body).toContain('@Jane');
130
+ });
131
+ it('passes through string comment body as-is', async () => {
132
+ const comment = {
133
+ id: '2',
134
+ author: { accountId: 'u1', displayName: 'Jane' },
135
+ body: 'plain string comment',
136
+ created: '2024-01-01T00:00:00.000Z',
137
+ updated: '2024-01-01T00:00:00.000Z',
138
+ };
139
+ fetchMock
140
+ .mockResolvedValueOnce(jsonResponse(issueFixture))
141
+ .mockResolvedValueOnce(jsonResponse(makeCommentsResponse([comment])));
142
+ const { comments } = await makeClient().getIssue('PROJ-1');
143
+ // String comments don't have type 'doc' so they are filtered out
144
+ expect(comments).toHaveLength(0);
145
+ });
146
+ it('filters out non-ADF comments (type !== "doc")', async () => {
147
+ const badComment = {
148
+ id: '3',
149
+ author: { accountId: 'u1', displayName: 'Bob' },
150
+ body: { type: 'unknown', content: [] },
151
+ created: '2024-01-01T00:00:00.000Z',
152
+ updated: '2024-01-01T00:00:00.000Z',
153
+ };
154
+ fetchMock
155
+ .mockResolvedValueOnce(jsonResponse(issueFixture))
156
+ .mockResolvedValueOnce(jsonResponse(makeCommentsResponse([badComment])));
157
+ const { comments } = await makeClient().getIssue('PROJ-1');
158
+ expect(comments).toHaveLength(0);
159
+ });
160
+ it('returns empty comments array when no comments exist', async () => {
161
+ fetchMock
162
+ .mockResolvedValueOnce(jsonResponse(issueFixture))
163
+ .mockResolvedValueOnce(jsonResponse(makeCommentsResponse([])));
164
+ const { comments } = await makeClient().getIssue('PROJ-1');
165
+ expect(comments).toEqual([]);
166
+ });
167
+ it('throws on non-ok HTTP response', async () => {
168
+ fetchMock.mockResolvedValueOnce(new Response('Not Found', { status: 404, statusText: 'Not Found' }));
169
+ await expect(makeClient().getIssue('PROJ-999')).rejects.toThrow('Jira API error: 404');
170
+ });
171
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=anthropic-retry-wrapper.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic-retry-wrapper.test.d.ts","sourceRoot":"","sources":["../../../src/llm/anthropic-retry-wrapper.test.ts"],"names":[],"mappings":""}