@hubblecommerce/overmind-core 0.1.3 → 0.1.5

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 (31) hide show
  1. package/dist/src/integrations/confluence/confluence.client.d.ts +2 -8
  2. package/dist/src/integrations/confluence/confluence.client.d.ts.map +1 -1
  3. package/dist/src/integrations/confluence/confluence.client.js +13 -40
  4. package/dist/src/integrations/confluence/confluence.client.test.d.ts +2 -0
  5. package/dist/src/integrations/confluence/confluence.client.test.d.ts.map +1 -0
  6. package/dist/src/integrations/confluence/confluence.client.test.js +320 -0
  7. package/dist/src/integrations/gitlab/gitlab.client.d.ts +8 -0
  8. package/dist/src/integrations/gitlab/gitlab.client.d.ts.map +1 -1
  9. package/dist/src/integrations/gitlab/gitlab.client.js +20 -0
  10. package/dist/src/integrations/gitlab/gitlab.client.test.d.ts +2 -0
  11. package/dist/src/integrations/gitlab/gitlab.client.test.d.ts.map +1 -0
  12. package/dist/src/integrations/gitlab/gitlab.client.test.js +62 -0
  13. package/dist/src/integrations/jira/jira.client.d.ts +3 -2
  14. package/dist/src/integrations/jira/jira.client.d.ts.map +1 -1
  15. package/dist/src/integrations/jira/jira.client.js +23 -86
  16. package/dist/src/integrations/jira/jira.client.test.d.ts +2 -0
  17. package/dist/src/integrations/jira/jira.client.test.d.ts.map +1 -0
  18. package/dist/src/integrations/jira/jira.client.test.js +171 -0
  19. package/dist/src/llm/anthropic-retry-wrapper.test.d.ts +2 -0
  20. package/dist/src/llm/anthropic-retry-wrapper.test.d.ts.map +1 -0
  21. package/dist/src/llm/anthropic-retry-wrapper.test.js +125 -0
  22. package/dist/src/processors/confluence-document.processor.test.d.ts +2 -0
  23. package/dist/src/processors/confluence-document.processor.test.d.ts.map +1 -0
  24. package/dist/src/processors/confluence-document.processor.test.js +202 -0
  25. package/dist/src/processors/confluence-html-parser.test.d.ts +2 -0
  26. package/dist/src/processors/confluence-html-parser.test.d.ts.map +1 -0
  27. package/dist/src/processors/confluence-html-parser.test.js +214 -0
  28. package/dist/src/tools/confluence-search.tool.d.ts +6 -6
  29. package/dist/src/tools/get-current-date.tool.d.ts.map +1 -1
  30. package/dist/src/tools/get-current-date.tool.js +1 -5
  31. package/package.json +7 -2
@@ -19,6 +19,7 @@ export declare class ConfluenceClient {
19
19
  * Make HTTP request to Confluence API with retries
20
20
  */
21
21
  private request;
22
+ private paginate;
22
23
  /**
23
24
  * Get all spaces (with pagination)
24
25
  */
@@ -55,17 +56,10 @@ export declare class ConfluenceClient {
55
56
  * Returns only label names as strings
56
57
  */
57
58
  getPageLabels(pageId: string): Promise<string[]>;
58
- /**
59
- * Get numeric space ID from space key
60
- * Helper method to convert space key to numeric ID required by v2 API
61
- * @param spaceKey - The space key (e.g., "TEST")
62
- * @returns The numeric space ID
63
- */
64
- getSpaceIdByKey(spaceKey: string): Promise<string>;
65
59
  /**
66
60
  * Create a new page in Confluence using REST API v2 with ADF (Atlassian Document Format)
67
61
  * This is the modern format for the new Confluence editor
68
- * @param spaceId - The numeric space ID (not space key) - use getSpaceIdByKey() if you have a space key
62
+ * @param spaceId - The numeric space ID (not space key)
69
63
  * @param title - The page title
70
64
  * @param adfBody - The page content in ADF format (from @atlaskit/editor-markdown-transformer)
71
65
  * @param parentId - Optional parent page ID to create as child page
@@ -1 +1 @@
1
- {"version":3,"file":"confluence.client.d.ts","sourceRoot":"","sources":["../../../../src/integrations/confluence/confluence.client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,cAAc,EAEd,eAAe,EAKlB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,sBAAsB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAuBD,qBAAa,gBAAgB;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,eAAe,CAAa;gBAExB,MAAM,EAAE,sBAAsB;IAS1C;;OAEG;YACW,gBAAgB;IAa9B;;OAEG;YACW,OAAO;IAwDrB;;OAEG;IACG,SAAS,CAAC,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAkB/D;;;OAGG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAyBpF;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,GAAE,SAAS,GAAG,kBAA8B,GAAG,OAAO,CAAC,cAAc,CAAC;IAKlH;;OAEG;IACG,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUrD;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAW/D;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAoBhE;;OAEG;IACG,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAYzF;;;OAGG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IA0BtD;;;;;OAKG;IACG,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKxD;;;;;;;;OAQG;IACG,iBAAiB,CAAC,MAAM,EAAE;QAC5B,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,cAAc,CAAC;CAuC9B"}
1
+ {"version":3,"file":"confluence.client.d.ts","sourceRoot":"","sources":["../../../../src/integrations/confluence/confluence.client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,cAAc,EAEd,eAAe,EAKlB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,sBAAsB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAuBD,qBAAa,gBAAgB;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,eAAe,CAAa;gBAExB,MAAM,EAAE,sBAAsB;IAS1C;;OAEG;YACW,gBAAgB;IAa9B;;OAEG;YACW,OAAO;YAwDP,QAAQ;IActB;;OAEG;IACG,SAAS,CAAC,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAI/D;;;OAGG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAKpF;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,GAAE,SAAS,GAAG,kBAA8B,GAAG,OAAO,CAAC,cAAc,CAAC;IAKlH;;OAEG;IACG,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUrD;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAW/D;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAoBhE;;OAEG;IACG,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAYzF;;;OAGG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IA0BtD;;;;;;;;OAQG;IACG,iBAAiB,CAAC,MAAM,EAAE;QAC5B,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,cAAc,CAAC;CAuC9B"}
@@ -73,46 +73,29 @@ export class ConfluenceClient {
73
73
  }
74
74
  throw lastError || new Error('Request failed after max retries');
75
75
  }
76
+ async paginate(initialUrl) {
77
+ const items = [];
78
+ let nextUrl = initialUrl;
79
+ while (nextUrl) {
80
+ const response = await this.request(nextUrl);
81
+ items.push(...response.results);
82
+ nextUrl = response._links.next?.replace(this.baseUrl, '');
83
+ }
84
+ return items;
85
+ }
76
86
  /**
77
87
  * Get all spaces (with pagination)
78
88
  */
79
89
  async getSpaces(limit = 25) {
80
- const spaces = [];
81
- let nextUrl = `/wiki/api/v2/spaces?limit=${limit}`;
82
- while (nextUrl) {
83
- const response = await this.request(nextUrl);
84
- const results = response.results;
85
- spaces.push(...results);
86
- // Extract cursor from next link if exists
87
- const links = response._links;
88
- const nextLink = links.next;
89
- nextUrl = nextLink ? nextLink.replace(this.baseUrl, '') : undefined;
90
- }
91
- return spaces;
90
+ return this.paginate(`/wiki/api/v2/spaces?limit=${limit}`);
92
91
  }
93
92
  /**
94
93
  * Get all pages in a space (with pagination)
95
94
  * Note: Accepts space key and automatically resolves to space ID for API v2
96
95
  */
97
96
  async getSpacePages(spaceKey, limit = 25) {
98
- // First, resolve space key to space ID (required by API v2)
99
97
  const space = await this.getSpaceByKey(spaceKey);
100
- const spaceId = space.id;
101
- const pages = [];
102
- let nextUrl = `/wiki/api/v2/spaces/${spaceId}/pages?limit=${limit}&status=current`;
103
- while (nextUrl) {
104
- const response = await this.request(nextUrl);
105
- const results = response.results;
106
- pages.push(...results);
107
- // Extract cursor from next link if exists
108
- const links = response._links;
109
- nextUrl = links.next || undefined;
110
- if (nextUrl) {
111
- // Convert full URL to relative path
112
- nextUrl = nextUrl.replace(this.baseUrl, '');
113
- }
114
- }
115
- return pages;
98
+ return this.paginate(`/wiki/api/v2/spaces/${space.id}/pages?limit=${limit}&status=current`);
116
99
  }
117
100
  /**
118
101
  * Get a single page by ID with content
@@ -197,20 +180,10 @@ export class ConfluenceClient {
197
180
  // Return only the label names (without prefix)
198
181
  return labels.map(label => label.name);
199
182
  }
200
- /**
201
- * Get numeric space ID from space key
202
- * Helper method to convert space key to numeric ID required by v2 API
203
- * @param spaceKey - The space key (e.g., "TEST")
204
- * @returns The numeric space ID
205
- */
206
- async getSpaceIdByKey(spaceKey) {
207
- const space = await this.getSpaceByKey(spaceKey);
208
- return space.id;
209
- }
210
183
  /**
211
184
  * Create a new page in Confluence using REST API v2 with ADF (Atlassian Document Format)
212
185
  * This is the modern format for the new Confluence editor
213
- * @param spaceId - The numeric space ID (not space key) - use getSpaceIdByKey() if you have a space key
186
+ * @param spaceId - The numeric space ID (not space key)
214
187
  * @param title - The page title
215
188
  * @param adfBody - The page content in ADF format (from @atlaskit/editor-markdown-transformer)
216
189
  * @param parentId - Optional parent page ID to create as child page
@@ -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
+ });
@@ -48,9 +48,10 @@ export interface JiraClientConfig {
48
48
  apiToken: string;
49
49
  }
50
50
  export declare class JiraClient {
51
- private baseUrl;
52
- private authHeader;
51
+ private readonly baseUrl;
52
+ private readonly authHeader;
53
53
  constructor(config: JiraClientConfig);
54
+ private request;
54
55
  /**
55
56
  * Parse Atlassian Document Format (ADF) to plain text
56
57
  */
@@ -1 +1 @@
1
- {"version":3,"file":"jira.client.d.ts","sourceRoot":"","sources":["../../../../src/integrations/jira/jira.client.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAkBD,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QACJ,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,MAAM,EAAE;YACJ,IAAI,EAAE,MAAM,CAAC;YACb,EAAE,EAAE,MAAM,CAAC;SACd,CAAC;QACF,SAAS,EAAE;YACP,IAAI,EAAE,MAAM,CAAC;YACb,EAAE,EAAE,MAAM,CAAC;SACd,CAAC;QACF,OAAO,EAAE;YACL,GAAG,EAAE,MAAM,CAAC;YACZ,EAAE,EAAE,MAAM,CAAC;YACX,IAAI,EAAE,MAAM,CAAC;SAChB,CAAC;QACF,QAAQ,CAAC,EAAE;YACP,SAAS,EAAE,MAAM,CAAC;YAClB,WAAW,EAAE,MAAM,CAAC;SACvB,GAAG,IAAI,CAAC;QACT,QAAQ,CAAC,EAAE;YACP,SAAS,EAAE,MAAM,CAAC;YAClB,WAAW,EAAE,MAAM,CAAC;SACvB,GAAG,IAAI,CAAC;QACT,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,UAAU,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACnD,CAAC;IACF,QAAQ,EAAE,WAAW,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,UAAU;IACnB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;gBAEf,MAAM,EAAE,gBAAgB;IAKpC;;OAEG;IACH,OAAO,CAAC,cAAc;IAgCtB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAexB;;OAEG;IACG,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;CAuI3D"}
1
+ {"version":3,"file":"jira.client.d.ts","sourceRoot":"","sources":["../../../../src/integrations/jira/jira.client.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAkBD,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QACJ,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,MAAM,EAAE;YACJ,IAAI,EAAE,MAAM,CAAC;YACb,EAAE,EAAE,MAAM,CAAC;SACd,CAAC;QACF,SAAS,EAAE;YACP,IAAI,EAAE,MAAM,CAAC;YACb,EAAE,EAAE,MAAM,CAAC;SACd,CAAC;QACF,OAAO,EAAE;YACL,GAAG,EAAE,MAAM,CAAC;YACZ,EAAE,EAAE,MAAM,CAAC;YACX,IAAI,EAAE,MAAM,CAAC;SAChB,CAAC;QACF,QAAQ,CAAC,EAAE;YACP,SAAS,EAAE,MAAM,CAAC;YAClB,WAAW,EAAE,MAAM,CAAC;SACvB,GAAG,IAAI,CAAC;QACT,QAAQ,CAAC,EAAE;YACP,SAAS,EAAE,MAAM,CAAC;YAClB,WAAW,EAAE,MAAM,CAAC;SACvB,GAAG,IAAI,CAAC;QACT,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,UAAU,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACnD,CAAC;IACF,QAAQ,EAAE,WAAW,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,UAAU;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,MAAM,EAAE,gBAAgB;YAKtB,OAAO;IAgBrB;;OAEG;IACH,OAAO,CAAC,cAAc;IAgCtB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAexB;;OAEG;IACG,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;CA4B3D"}