@code-rag/core 0.1.0 → 0.1.1

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 (205) hide show
  1. package/package.json +4 -4
  2. package/dist/auth/audit-log.js.map +0 -1
  3. package/dist/auth/audit-log.test.d.ts +0 -1
  4. package/dist/auth/audit-log.test.js +0 -261
  5. package/dist/auth/audit-log.test.js.map +0 -1
  6. package/dist/auth/index.js.map +0 -1
  7. package/dist/auth/oidc-provider.js.map +0 -1
  8. package/dist/auth/oidc-provider.test.d.ts +0 -1
  9. package/dist/auth/oidc-provider.test.js +0 -520
  10. package/dist/auth/oidc-provider.test.js.map +0 -1
  11. package/dist/auth/rbac.js.map +0 -1
  12. package/dist/auth/rbac.test.d.ts +0 -1
  13. package/dist/auth/rbac.test.js +0 -224
  14. package/dist/auth/rbac.test.js.map +0 -1
  15. package/dist/auth/saml-provider.js.map +0 -1
  16. package/dist/auth/saml-provider.test.d.ts +0 -1
  17. package/dist/auth/saml-provider.test.js +0 -422
  18. package/dist/auth/saml-provider.test.js.map +0 -1
  19. package/dist/auth/types.js.map +0 -1
  20. package/dist/auth/types.test.d.ts +0 -1
  21. package/dist/auth/types.test.js +0 -147
  22. package/dist/auth/types.test.js.map +0 -1
  23. package/dist/backlog/ab-reference-scanner.js.map +0 -1
  24. package/dist/backlog/ab-reference-scanner.test.d.ts +0 -1
  25. package/dist/backlog/ab-reference-scanner.test.js +0 -83
  26. package/dist/backlog/ab-reference-scanner.test.js.map +0 -1
  27. package/dist/backlog/azure-devops-provider.js.map +0 -1
  28. package/dist/backlog/backlog-provider.js.map +0 -1
  29. package/dist/backlog/backlog-provider.test.d.ts +0 -1
  30. package/dist/backlog/backlog-provider.test.js +0 -426
  31. package/dist/backlog/backlog-provider.test.js.map +0 -1
  32. package/dist/backlog/clickup-provider.js.map +0 -1
  33. package/dist/backlog/clickup-provider.test.d.ts +0 -1
  34. package/dist/backlog/clickup-provider.test.js +0 -426
  35. package/dist/backlog/clickup-provider.test.js.map +0 -1
  36. package/dist/backlog/clickup-reference-scanner.js.map +0 -1
  37. package/dist/backlog/clickup-reference-scanner.test.d.ts +0 -1
  38. package/dist/backlog/clickup-reference-scanner.test.js +0 -92
  39. package/dist/backlog/clickup-reference-scanner.test.js.map +0 -1
  40. package/dist/backlog/code-linker.js.map +0 -1
  41. package/dist/backlog/code-linker.test.d.ts +0 -1
  42. package/dist/backlog/code-linker.test.js +0 -325
  43. package/dist/backlog/code-linker.test.js.map +0 -1
  44. package/dist/backlog/index.js.map +0 -1
  45. package/dist/backlog/jira-provider.js.map +0 -1
  46. package/dist/backlog/jira-provider.test.d.ts +0 -1
  47. package/dist/backlog/jira-provider.test.js +0 -449
  48. package/dist/backlog/jira-provider.test.js.map +0 -1
  49. package/dist/backlog/jira-reference-scanner.js.map +0 -1
  50. package/dist/backlog/jira-reference-scanner.test.d.ts +0 -1
  51. package/dist/backlog/jira-reference-scanner.test.js +0 -127
  52. package/dist/backlog/jira-reference-scanner.test.js.map +0 -1
  53. package/dist/backlog/types.js.map +0 -1
  54. package/dist/chunker/ast-chunker.js.map +0 -1
  55. package/dist/chunker/ast-chunker.test.d.ts +0 -1
  56. package/dist/chunker/ast-chunker.test.js +0 -391
  57. package/dist/chunker/ast-chunker.test.js.map +0 -1
  58. package/dist/chunker/chunker.js.map +0 -1
  59. package/dist/chunker/index.js.map +0 -1
  60. package/dist/config/config-parser.js.map +0 -1
  61. package/dist/config/config-parser.test.d.ts +0 -1
  62. package/dist/config/config-parser.test.js +0 -699
  63. package/dist/config/config-parser.test.js.map +0 -1
  64. package/dist/docs/confluence-provider.js.map +0 -1
  65. package/dist/docs/confluence-provider.test.d.ts +0 -1
  66. package/dist/docs/confluence-provider.test.js +0 -765
  67. package/dist/docs/confluence-provider.test.js.map +0 -1
  68. package/dist/docs/index.js.map +0 -1
  69. package/dist/docs/sharepoint-provider.js.map +0 -1
  70. package/dist/docs/sharepoint-provider.test.d.ts +0 -1
  71. package/dist/docs/sharepoint-provider.test.js +0 -873
  72. package/dist/docs/sharepoint-provider.test.js.map +0 -1
  73. package/dist/embedding/bm25-index.js.map +0 -1
  74. package/dist/embedding/bm25-index.test.d.ts +0 -1
  75. package/dist/embedding/bm25-index.test.js +0 -289
  76. package/dist/embedding/bm25-index.test.js.map +0 -1
  77. package/dist/embedding/hybrid-search.js.map +0 -1
  78. package/dist/embedding/hybrid-search.test.d.ts +0 -1
  79. package/dist/embedding/hybrid-search.test.js +0 -266
  80. package/dist/embedding/hybrid-search.test.js.map +0 -1
  81. package/dist/embedding/index.js.map +0 -1
  82. package/dist/embedding/lancedb-store.js.map +0 -1
  83. package/dist/embedding/lancedb-store.test.d.ts +0 -1
  84. package/dist/embedding/lancedb-store.test.js +0 -268
  85. package/dist/embedding/lancedb-store.test.js.map +0 -1
  86. package/dist/embedding/model-lifecycle-manager.js.map +0 -1
  87. package/dist/embedding/model-lifecycle-manager.test.d.ts +0 -1
  88. package/dist/embedding/model-lifecycle-manager.test.js +0 -642
  89. package/dist/embedding/model-lifecycle-manager.test.js.map +0 -1
  90. package/dist/embedding/ollama-embedding-provider.js.map +0 -1
  91. package/dist/embedding/ollama-embedding-provider.test.d.ts +0 -1
  92. package/dist/embedding/ollama-embedding-provider.test.js +0 -198
  93. package/dist/embedding/ollama-embedding-provider.test.js.map +0 -1
  94. package/dist/embedding/openai-compatible-embedding-provider.js.map +0 -1
  95. package/dist/embedding/openai-compatible-embedding-provider.test.d.ts +0 -1
  96. package/dist/embedding/openai-compatible-embedding-provider.test.js +0 -456
  97. package/dist/embedding/openai-compatible-embedding-provider.test.js.map +0 -1
  98. package/dist/embedding/qdrant-store.js.map +0 -1
  99. package/dist/embedding/qdrant-store.test.d.ts +0 -1
  100. package/dist/embedding/qdrant-store.test.js +0 -359
  101. package/dist/embedding/qdrant-store.test.js.map +0 -1
  102. package/dist/enrichment/index.js.map +0 -1
  103. package/dist/enrichment/nl-enricher.js.map +0 -1
  104. package/dist/enrichment/nl-enricher.test.d.ts +0 -1
  105. package/dist/enrichment/nl-enricher.test.js +0 -154
  106. package/dist/enrichment/nl-enricher.test.js.map +0 -1
  107. package/dist/enrichment/ollama-client.js.map +0 -1
  108. package/dist/enrichment/ollama-client.test.d.ts +0 -1
  109. package/dist/enrichment/ollama-client.test.js +0 -129
  110. package/dist/enrichment/ollama-client.test.js.map +0 -1
  111. package/dist/git/git-client.js.map +0 -1
  112. package/dist/git/git-client.test.d.ts +0 -1
  113. package/dist/git/git-client.test.js +0 -200
  114. package/dist/git/git-client.test.js.map +0 -1
  115. package/dist/git/ignore-filter.js.map +0 -1
  116. package/dist/git/ignore-filter.test.d.ts +0 -1
  117. package/dist/git/ignore-filter.test.js +0 -87
  118. package/dist/git/ignore-filter.test.js.map +0 -1
  119. package/dist/git/index.js.map +0 -1
  120. package/dist/git/simple-git-client.js.map +0 -1
  121. package/dist/graph/cross-repo-resolver.js.map +0 -1
  122. package/dist/graph/cross-repo-resolver.test.d.ts +0 -1
  123. package/dist/graph/cross-repo-resolver.test.js +0 -548
  124. package/dist/graph/cross-repo-resolver.test.js.map +0 -1
  125. package/dist/graph/dependency-graph.js.map +0 -1
  126. package/dist/graph/dependency-graph.test.d.ts +0 -1
  127. package/dist/graph/dependency-graph.test.js +0 -276
  128. package/dist/graph/dependency-graph.test.js.map +0 -1
  129. package/dist/graph/graph-builder.js.map +0 -1
  130. package/dist/graph/graph-builder.test.d.ts +0 -1
  131. package/dist/graph/graph-builder.test.js +0 -178
  132. package/dist/graph/graph-builder.test.js.map +0 -1
  133. package/dist/graph/import-resolver.js.map +0 -1
  134. package/dist/graph/import-resolver.test.d.ts +0 -1
  135. package/dist/graph/import-resolver.test.js +0 -282
  136. package/dist/graph/import-resolver.test.js.map +0 -1
  137. package/dist/graph/index.js.map +0 -1
  138. package/dist/index.js.map +0 -1
  139. package/dist/indexer/file-scanner.js.map +0 -1
  140. package/dist/indexer/file-scanner.test.d.ts +0 -1
  141. package/dist/indexer/file-scanner.test.js +0 -110
  142. package/dist/indexer/file-scanner.test.js.map +0 -1
  143. package/dist/indexer/incremental-indexer.js.map +0 -1
  144. package/dist/indexer/incremental-indexer.test.d.ts +0 -1
  145. package/dist/indexer/incremental-indexer.test.js +0 -266
  146. package/dist/indexer/incremental-indexer.test.js.map +0 -1
  147. package/dist/indexer/index-check.js.map +0 -1
  148. package/dist/indexer/index-check.test.d.ts +0 -1
  149. package/dist/indexer/index-check.test.js +0 -100
  150. package/dist/indexer/index-check.test.js.map +0 -1
  151. package/dist/indexer/index-state.js.map +0 -1
  152. package/dist/indexer/index-state.test.d.ts +0 -1
  153. package/dist/indexer/index-state.test.js +0 -140
  154. package/dist/indexer/index-state.test.js.map +0 -1
  155. package/dist/indexer/index.js.map +0 -1
  156. package/dist/indexer/multi-repo-indexer.js.map +0 -1
  157. package/dist/indexer/multi-repo-indexer.test.d.ts +0 -1
  158. package/dist/indexer/multi-repo-indexer.test.js +0 -238
  159. package/dist/indexer/multi-repo-indexer.test.js.map +0 -1
  160. package/dist/parser/index.js.map +0 -1
  161. package/dist/parser/language-registry.js.map +0 -1
  162. package/dist/parser/language-registry.test.d.ts +0 -1
  163. package/dist/parser/language-registry.test.js +0 -225
  164. package/dist/parser/language-registry.test.js.map +0 -1
  165. package/dist/parser/markdown-parser.js.map +0 -1
  166. package/dist/parser/markdown-parser.test.d.ts +0 -1
  167. package/dist/parser/markdown-parser.test.js +0 -600
  168. package/dist/parser/markdown-parser.test.js.map +0 -1
  169. package/dist/parser/tree-sitter-parser.js.map +0 -1
  170. package/dist/retrieval/context-expander.js.map +0 -1
  171. package/dist/retrieval/context-expander.test.d.ts +0 -1
  172. package/dist/retrieval/context-expander.test.js +0 -339
  173. package/dist/retrieval/context-expander.test.js.map +0 -1
  174. package/dist/retrieval/cross-encoder-reranker.js.map +0 -1
  175. package/dist/retrieval/cross-encoder-reranker.test.d.ts +0 -1
  176. package/dist/retrieval/cross-encoder-reranker.test.js +0 -305
  177. package/dist/retrieval/cross-encoder-reranker.test.js.map +0 -1
  178. package/dist/retrieval/index.js.map +0 -1
  179. package/dist/retrieval/query-analyzer.js.map +0 -1
  180. package/dist/retrieval/query-analyzer.test.d.ts +0 -1
  181. package/dist/retrieval/query-analyzer.test.js +0 -236
  182. package/dist/retrieval/query-analyzer.test.js.map +0 -1
  183. package/dist/retrieval/token-budget.js.map +0 -1
  184. package/dist/retrieval/token-budget.test.d.ts +0 -1
  185. package/dist/retrieval/token-budget.test.js +0 -404
  186. package/dist/retrieval/token-budget.test.js.map +0 -1
  187. package/dist/storage/azure-blob-provider.js.map +0 -1
  188. package/dist/storage/azure-blob-provider.test.d.ts +0 -1
  189. package/dist/storage/azure-blob-provider.test.js +0 -250
  190. package/dist/storage/azure-blob-provider.test.js.map +0 -1
  191. package/dist/storage/gcs-provider.js.map +0 -1
  192. package/dist/storage/gcs-provider.test.d.ts +0 -1
  193. package/dist/storage/gcs-provider.test.js +0 -299
  194. package/dist/storage/gcs-provider.test.js.map +0 -1
  195. package/dist/storage/index.js.map +0 -1
  196. package/dist/storage/s3-provider.js.map +0 -1
  197. package/dist/storage/s3-provider.test.d.ts +0 -1
  198. package/dist/storage/s3-provider.test.js +0 -329
  199. package/dist/storage/s3-provider.test.js.map +0 -1
  200. package/dist/storage/types.js.map +0 -1
  201. package/dist/types/chunk.js.map +0 -1
  202. package/dist/types/config.js.map +0 -1
  203. package/dist/types/index.js.map +0 -1
  204. package/dist/types/provider.js.map +0 -1
  205. package/dist/types/search.js.map +0 -1
@@ -1,765 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { ConfluenceProvider, ConfluenceError, confluenceStorageToPlainText, } from './confluence-provider.js';
3
- // ---------------------------------------------------------------------------
4
- // Helpers
5
- // ---------------------------------------------------------------------------
6
- function createMockResponse(body, status = 200, statusText = 'OK') {
7
- return {
8
- ok: status >= 200 && status < 300,
9
- status,
10
- statusText,
11
- json: () => Promise.resolve(body),
12
- headers: new Headers(),
13
- redirected: false,
14
- type: 'basic',
15
- url: '',
16
- clone: () => createMockResponse(body, status, statusText),
17
- body: null,
18
- bodyUsed: false,
19
- arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
20
- blob: () => Promise.resolve(new Blob()),
21
- formData: () => Promise.resolve(new FormData()),
22
- text: () => Promise.resolve(JSON.stringify(body)),
23
- bytes: () => Promise.resolve(new Uint8Array()),
24
- };
25
- }
26
- function createConfluencePage(overrides) {
27
- return {
28
- id: '12345',
29
- title: 'Getting Started Guide',
30
- status: 'current',
31
- spaceId: 'space-id-1',
32
- parentId: 'parent-1',
33
- version: {
34
- number: 3,
35
- createdAt: '2026-02-01T10:00:00.000Z',
36
- },
37
- body: {
38
- storage: {
39
- value: '<p>Welcome to the <strong>Getting Started</strong> guide.</p><p>Follow these steps to begin.</p>',
40
- },
41
- },
42
- _links: {
43
- webui: '/spaces/DEV/pages/12345/Getting+Started+Guide',
44
- },
45
- labels: {
46
- results: [{ name: 'onboarding' }, { name: 'docs' }],
47
- },
48
- ...(overrides ?? {}),
49
- };
50
- }
51
- function createBlogPost(overrides) {
52
- return {
53
- id: '67890',
54
- title: 'Sprint 5 Retrospective',
55
- status: 'current',
56
- spaceId: 'space-id-1',
57
- version: {
58
- number: 1,
59
- createdAt: '2026-02-15T14:30:00.000Z',
60
- },
61
- body: {
62
- storage: {
63
- value: '<p>Summary of Sprint 5 outcomes.</p>',
64
- },
65
- },
66
- _links: {
67
- webui: '/spaces/DEV/blog/67890',
68
- },
69
- labels: {
70
- results: [{ name: 'retro' }],
71
- },
72
- ...(overrides ?? {}),
73
- };
74
- }
75
- function createComment(overrides) {
76
- return {
77
- id: '99999',
78
- title: 'Re: Getting Started Guide',
79
- status: 'current',
80
- spaceId: 'space-id-1',
81
- version: {
82
- number: 1,
83
- createdAt: '2026-02-02T08:00:00.000Z',
84
- },
85
- body: {
86
- storage: {
87
- value: '<p>Great guide, thanks!</p>',
88
- },
89
- },
90
- _links: {},
91
- labels: { results: [] },
92
- ...(overrides ?? {}),
93
- };
94
- }
95
- const VALID_CONFIG = {
96
- baseUrl: 'https://mycompany.atlassian.net',
97
- email: 'user@example.com',
98
- apiToken: 'test-api-token-123',
99
- spaceKeys: ['DEV', 'TEAM'],
100
- maxPages: 25,
101
- };
102
- const OAUTH_CONFIG = {
103
- baseUrl: 'https://mycompany.atlassian.net',
104
- oauthToken: 'oauth-bearer-token-456',
105
- spaceKeys: ['DEV'],
106
- };
107
- // ---------------------------------------------------------------------------
108
- // Tests
109
- // ---------------------------------------------------------------------------
110
- describe('ConfluenceProvider', () => {
111
- let provider;
112
- let fetchSpy;
113
- beforeEach(() => {
114
- provider = new ConfluenceProvider();
115
- fetchSpy = vi.fn();
116
- vi.stubGlobal('fetch', fetchSpy);
117
- });
118
- afterEach(() => {
119
- vi.restoreAllMocks();
120
- });
121
- // --- name ---
122
- it('should have name set to confluence', () => {
123
- expect(provider.name).toBe('confluence');
124
- });
125
- // --- initialize ---
126
- describe('initialize', () => {
127
- it('should return ok on successful connection with API token', async () => {
128
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [{ id: '1', key: 'DEV', name: 'Dev' }] }));
129
- const result = await provider.initialize(VALID_CONFIG);
130
- expect(result.isOk()).toBe(true);
131
- expect(fetchSpy).toHaveBeenCalledOnce();
132
- expect(fetchSpy.mock.calls[0][0]).toContain('/wiki/api/v2/spaces');
133
- });
134
- it('should return ok on successful connection with OAuth token', async () => {
135
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [] }));
136
- const result = await provider.initialize(OAUTH_CONFIG);
137
- expect(result.isOk()).toBe(true);
138
- });
139
- it('should return err when baseUrl is missing', async () => {
140
- const result = await provider.initialize({
141
- email: 'user@example.com',
142
- apiToken: 'token',
143
- });
144
- expect(result.isErr()).toBe(true);
145
- if (result.isErr()) {
146
- expect(result.error).toBeInstanceOf(ConfluenceError);
147
- expect(result.error.message).toContain('baseUrl');
148
- }
149
- });
150
- it('should return err when neither API token nor OAuth token is provided', async () => {
151
- const result = await provider.initialize({
152
- baseUrl: 'https://mycompany.atlassian.net',
153
- });
154
- expect(result.isErr()).toBe(true);
155
- if (result.isErr()) {
156
- expect(result.error).toBeInstanceOf(ConfluenceError);
157
- expect(result.error.message).toContain('authentication');
158
- }
159
- });
160
- it('should return err when email is provided but apiToken is missing', async () => {
161
- const result = await provider.initialize({
162
- baseUrl: 'https://mycompany.atlassian.net',
163
- email: 'user@example.com',
164
- });
165
- expect(result.isErr()).toBe(true);
166
- if (result.isErr()) {
167
- expect(result.error.message).toContain('authentication');
168
- }
169
- });
170
- it('should return err when connection fails with HTTP error', async () => {
171
- fetchSpy.mockResolvedValueOnce(createMockResponse({ message: 'Unauthorized' }, 401, 'Unauthorized'));
172
- const result = await provider.initialize(VALID_CONFIG);
173
- expect(result.isErr()).toBe(true);
174
- if (result.isErr()) {
175
- expect(result.error.message).toContain('401');
176
- }
177
- });
178
- it('should return err when connection throws a network error', async () => {
179
- fetchSpy.mockRejectedValueOnce(new Error('ECONNREFUSED'));
180
- const result = await provider.initialize(VALID_CONFIG);
181
- expect(result.isErr()).toBe(true);
182
- if (result.isErr()) {
183
- expect(result.error.message).toContain('ECONNREFUSED');
184
- }
185
- });
186
- it('should send correct Basic auth header for API token auth', async () => {
187
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [] }));
188
- await provider.initialize(VALID_CONFIG);
189
- const callHeaders = fetchSpy.mock.calls[0][1]?.headers;
190
- const expectedAuth = `Basic ${btoa(`${VALID_CONFIG.email}:${VALID_CONFIG.apiToken}`)}`;
191
- expect(callHeaders['Authorization']).toBe(expectedAuth);
192
- });
193
- it('should send correct Bearer auth header for OAuth', async () => {
194
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [] }));
195
- await provider.initialize(OAUTH_CONFIG);
196
- const callHeaders = fetchSpy.mock.calls[0][1]?.headers;
197
- expect(callHeaders['Authorization']).toBe(`Bearer ${OAUTH_CONFIG.oauthToken}`);
198
- });
199
- it('should strip trailing slashes from baseUrl', async () => {
200
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [] }));
201
- await provider.initialize({
202
- ...VALID_CONFIG,
203
- baseUrl: 'https://mycompany.atlassian.net///',
204
- });
205
- expect(fetchSpy.mock.calls[0][0]).toContain('https://mycompany.atlassian.net/wiki/api/v2/spaces');
206
- expect(fetchSpy.mock.calls[0][0]).not.toContain('///');
207
- });
208
- });
209
- // --- fetchPages ---
210
- describe('fetchPages', () => {
211
- async function initializeProvider() {
212
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [] }));
213
- await provider.initialize(VALID_CONFIG);
214
- }
215
- it('should fetch all pages without space filter', async () => {
216
- await initializeProvider();
217
- fetchSpy.mockResolvedValueOnce(createMockResponse({
218
- results: [createConfluencePage()],
219
- _links: {},
220
- }));
221
- const result = await provider.fetchPages([]);
222
- expect(result.isOk()).toBe(true);
223
- if (result.isOk()) {
224
- expect(result.value).toHaveLength(1);
225
- const page = result.value[0];
226
- expect(page.id).toBe('12345');
227
- expect(page.title).toBe('Getting Started Guide');
228
- expect(page.type).toBe('page');
229
- expect(page.labels).toEqual(['onboarding', 'docs']);
230
- }
231
- });
232
- it('should fetch pages filtered by space key', async () => {
233
- await initializeProvider();
234
- // resolveSpaceId call
235
- fetchSpy.mockResolvedValueOnce(createMockResponse({
236
- results: [{ id: 'space-id-dev', key: 'DEV', name: 'Development' }],
237
- }));
238
- // Fetch pages in space
239
- fetchSpy.mockResolvedValueOnce(createMockResponse({
240
- results: [createConfluencePage()],
241
- _links: {},
242
- }));
243
- const result = await provider.fetchPages(['DEV']);
244
- expect(result.isOk()).toBe(true);
245
- if (result.isOk()) {
246
- expect(result.value).toHaveLength(1);
247
- expect(result.value[0].spaceKey).toBe('DEV');
248
- }
249
- });
250
- it('should use configured space keys when no explicit filter is given', async () => {
251
- await initializeProvider();
252
- // resolveSpaceId for DEV
253
- fetchSpy.mockResolvedValueOnce(createMockResponse({
254
- results: [{ id: 'space-id-dev', key: 'DEV', name: 'Development' }],
255
- }));
256
- // Pages in DEV
257
- fetchSpy.mockResolvedValueOnce(createMockResponse({
258
- results: [createConfluencePage()],
259
- _links: {},
260
- }));
261
- // resolveSpaceId for TEAM
262
- fetchSpy.mockResolvedValueOnce(createMockResponse({
263
- results: [{ id: 'space-id-team', key: 'TEAM', name: 'Team' }],
264
- }));
265
- // Pages in TEAM
266
- fetchSpy.mockResolvedValueOnce(createMockResponse({
267
- results: [createConfluencePage({ id: '11111', title: 'Team Page' })],
268
- _links: {},
269
- }));
270
- const result = await provider.fetchPages();
271
- expect(result.isOk()).toBe(true);
272
- if (result.isOk()) {
273
- expect(result.value).toHaveLength(2);
274
- }
275
- });
276
- it('should handle pagination', async () => {
277
- await initializeProvider();
278
- // First page of results
279
- fetchSpy.mockResolvedValueOnce(createMockResponse({
280
- results: [createConfluencePage({ id: '1', title: 'Page 1' })],
281
- _links: { next: '/wiki/api/v2/pages?cursor=abc&body-format=storage' },
282
- }));
283
- // Second page of results
284
- fetchSpy.mockResolvedValueOnce(createMockResponse({
285
- results: [createConfluencePage({ id: '2', title: 'Page 2' })],
286
- _links: {},
287
- }));
288
- const result = await provider.fetchPages([]);
289
- expect(result.isOk()).toBe(true);
290
- if (result.isOk()) {
291
- expect(result.value).toHaveLength(2);
292
- expect(result.value[0].id).toBe('1');
293
- expect(result.value[1].id).toBe('2');
294
- }
295
- });
296
- it('should convert storage format to plain text', async () => {
297
- await initializeProvider();
298
- fetchSpy.mockResolvedValueOnce(createMockResponse({
299
- results: [createConfluencePage()],
300
- _links: {},
301
- }));
302
- const result = await provider.fetchPages([]);
303
- expect(result.isOk()).toBe(true);
304
- if (result.isOk()) {
305
- const page = result.value[0];
306
- expect(page.plainText).toContain('Welcome to the');
307
- expect(page.plainText).toContain('Getting Started');
308
- expect(page.plainText).not.toContain('<p>');
309
- expect(page.plainText).not.toContain('<strong>');
310
- }
311
- });
312
- it('should return err when space is not found', async () => {
313
- await initializeProvider();
314
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [] }));
315
- const result = await provider.fetchPages(['NONEXISTENT']);
316
- expect(result.isErr()).toBe(true);
317
- if (result.isErr()) {
318
- expect(result.error.message).toContain('Space not found');
319
- }
320
- });
321
- it('should return err on API failure', async () => {
322
- await initializeProvider();
323
- fetchSpy.mockResolvedValueOnce(createMockResponse({ message: 'Server Error' }, 500, 'Internal Server Error'));
324
- const result = await provider.fetchPages([]);
325
- expect(result.isErr()).toBe(true);
326
- if (result.isErr()) {
327
- expect(result.error.message).toContain('500');
328
- }
329
- });
330
- it('should return err on network failure', async () => {
331
- await initializeProvider();
332
- fetchSpy.mockRejectedValueOnce(new Error('Network error'));
333
- const result = await provider.fetchPages([]);
334
- expect(result.isErr()).toBe(true);
335
- if (result.isErr()) {
336
- expect(result.error.message).toContain('Network error');
337
- }
338
- });
339
- it('should return empty array when no pages exist', async () => {
340
- await initializeProvider();
341
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [], _links: {} }));
342
- const result = await provider.fetchPages([]);
343
- expect(result.isOk()).toBe(true);
344
- if (result.isOk()) {
345
- expect(result.value).toEqual([]);
346
- }
347
- });
348
- it('should construct correct page URL from webui link', async () => {
349
- await initializeProvider();
350
- fetchSpy.mockResolvedValueOnce(createMockResponse({
351
- results: [createConfluencePage()],
352
- _links: {},
353
- }));
354
- const result = await provider.fetchPages([]);
355
- expect(result.isOk()).toBe(true);
356
- if (result.isOk()) {
357
- expect(result.value[0].url).toBe('https://mycompany.atlassian.net/wiki/spaces/DEV/pages/12345/Getting+Started+Guide');
358
- }
359
- });
360
- it('should handle page without labels', async () => {
361
- await initializeProvider();
362
- fetchSpy.mockResolvedValueOnce(createMockResponse({
363
- results: [createConfluencePage({ labels: undefined })],
364
- _links: {},
365
- }));
366
- const result = await provider.fetchPages([]);
367
- expect(result.isOk()).toBe(true);
368
- if (result.isOk()) {
369
- expect(result.value[0].labels).toEqual([]);
370
- }
371
- });
372
- it('should handle page without body', async () => {
373
- await initializeProvider();
374
- fetchSpy.mockResolvedValueOnce(createMockResponse({
375
- results: [createConfluencePage({ body: undefined })],
376
- _links: {},
377
- }));
378
- const result = await provider.fetchPages([]);
379
- expect(result.isOk()).toBe(true);
380
- if (result.isOk()) {
381
- expect(result.value[0].plainText).toBe('');
382
- expect(result.value[0].storageFormat).toBe('');
383
- }
384
- });
385
- });
386
- // --- fetchPage ---
387
- describe('fetchPage', () => {
388
- async function initializeProvider() {
389
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [] }));
390
- await provider.initialize(VALID_CONFIG);
391
- }
392
- it('should fetch and map a single page', async () => {
393
- await initializeProvider();
394
- // Space lookup for spaceId
395
- fetchSpy.mockResolvedValueOnce(createMockResponse(createConfluencePage()));
396
- // resolveSpaceKeyFromId
397
- fetchSpy.mockResolvedValueOnce(createMockResponse({ id: 'space-id-1', key: 'DEV', name: 'Dev' }));
398
- const result = await provider.fetchPage('12345');
399
- expect(result.isOk()).toBe(true);
400
- if (result.isOk()) {
401
- expect(result.value.id).toBe('12345');
402
- expect(result.value.title).toBe('Getting Started Guide');
403
- expect(result.value.type).toBe('page');
404
- expect(result.value.version).toBe(3);
405
- }
406
- });
407
- it('should return err when page is not found (404)', async () => {
408
- await initializeProvider();
409
- fetchSpy.mockResolvedValueOnce(createMockResponse({ message: 'Not Found' }, 404, 'Not Found'));
410
- const result = await provider.fetchPage('nonexistent');
411
- expect(result.isErr()).toBe(true);
412
- if (result.isErr()) {
413
- expect(result.error).toBeInstanceOf(ConfluenceError);
414
- expect(result.error.message).toContain('not found');
415
- expect(result.error.message).toContain('nonexistent');
416
- }
417
- });
418
- it('should return err on non-404 HTTP error', async () => {
419
- await initializeProvider();
420
- fetchSpy.mockResolvedValueOnce(createMockResponse({ message: 'Forbidden' }, 403, 'Forbidden'));
421
- const result = await provider.fetchPage('12345');
422
- expect(result.isErr()).toBe(true);
423
- if (result.isErr()) {
424
- expect(result.error.message).toContain('403');
425
- }
426
- });
427
- it('should return err on network error', async () => {
428
- await initializeProvider();
429
- fetchSpy.mockRejectedValueOnce(new Error('Connection reset'));
430
- const result = await provider.fetchPage('12345');
431
- expect(result.isErr()).toBe(true);
432
- if (result.isErr()) {
433
- expect(result.error.message).toContain('Connection reset');
434
- }
435
- });
436
- });
437
- // --- fetchBlogPosts ---
438
- describe('fetchBlogPosts', () => {
439
- async function initializeProvider() {
440
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [] }));
441
- await provider.initialize(VALID_CONFIG);
442
- }
443
- it('should fetch all blog posts without space filter', async () => {
444
- await initializeProvider();
445
- fetchSpy.mockResolvedValueOnce(createMockResponse({
446
- results: [createBlogPost()],
447
- _links: {},
448
- }));
449
- const result = await provider.fetchBlogPosts([]);
450
- expect(result.isOk()).toBe(true);
451
- if (result.isOk()) {
452
- expect(result.value).toHaveLength(1);
453
- const post = result.value[0];
454
- expect(post.id).toBe('67890');
455
- expect(post.title).toBe('Sprint 5 Retrospective');
456
- expect(post.type).toBe('blogpost');
457
- expect(post.labels).toEqual(['retro']);
458
- }
459
- });
460
- it('should fetch blog posts filtered by space key', async () => {
461
- await initializeProvider();
462
- // resolveSpaceId
463
- fetchSpy.mockResolvedValueOnce(createMockResponse({
464
- results: [{ id: 'space-id-dev', key: 'DEV', name: 'Development' }],
465
- }));
466
- // Blog posts in space
467
- fetchSpy.mockResolvedValueOnce(createMockResponse({
468
- results: [createBlogPost()],
469
- _links: {},
470
- }));
471
- const result = await provider.fetchBlogPosts(['DEV']);
472
- expect(result.isOk()).toBe(true);
473
- if (result.isOk()) {
474
- expect(result.value).toHaveLength(1);
475
- expect(result.value[0].spaceKey).toBe('DEV');
476
- }
477
- });
478
- it('should return err on API failure', async () => {
479
- await initializeProvider();
480
- fetchSpy.mockResolvedValueOnce(createMockResponse({}, 500, 'Internal Server Error'));
481
- const result = await provider.fetchBlogPosts([]);
482
- expect(result.isErr()).toBe(true);
483
- if (result.isErr()) {
484
- expect(result.error.message).toContain('500');
485
- }
486
- });
487
- it('should return empty array when no blog posts exist', async () => {
488
- await initializeProvider();
489
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [], _links: {} }));
490
- const result = await provider.fetchBlogPosts([]);
491
- expect(result.isOk()).toBe(true);
492
- if (result.isOk()) {
493
- expect(result.value).toEqual([]);
494
- }
495
- });
496
- });
497
- // --- fetchComments ---
498
- describe('fetchComments', () => {
499
- async function initializeProvider() {
500
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [] }));
501
- await provider.initialize(VALID_CONFIG);
502
- }
503
- it('should fetch comments for a page', async () => {
504
- await initializeProvider();
505
- fetchSpy.mockResolvedValueOnce(createMockResponse({
506
- results: [createComment()],
507
- _links: {},
508
- }));
509
- const result = await provider.fetchComments('12345');
510
- expect(result.isOk()).toBe(true);
511
- if (result.isOk()) {
512
- expect(result.value).toHaveLength(1);
513
- const comment = result.value[0];
514
- expect(comment.id).toBe('99999');
515
- expect(comment.type).toBe('comment');
516
- expect(comment.plainText).toContain('Great guide');
517
- expect(comment.parentId).toBe('12345');
518
- }
519
- });
520
- it('should use correct API endpoint for comments', async () => {
521
- await initializeProvider();
522
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [], _links: {} }));
523
- await provider.fetchComments('12345');
524
- const url = fetchSpy.mock.calls[1][0];
525
- expect(url).toContain('/pages/12345/footer-comments');
526
- expect(url).toContain('body-format=storage');
527
- });
528
- it('should return err on API failure', async () => {
529
- await initializeProvider();
530
- fetchSpy.mockResolvedValueOnce(createMockResponse({}, 500, 'Internal Server Error'));
531
- const result = await provider.fetchComments('12345');
532
- expect(result.isErr()).toBe(true);
533
- if (result.isErr()) {
534
- expect(result.error.message).toContain('500');
535
- }
536
- });
537
- it('should return empty array when no comments exist', async () => {
538
- await initializeProvider();
539
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [], _links: {} }));
540
- const result = await provider.fetchComments('12345');
541
- expect(result.isOk()).toBe(true);
542
- if (result.isOk()) {
543
- expect(result.value).toEqual([]);
544
- }
545
- });
546
- });
547
- // --- getChangedPages ---
548
- describe('getChangedPages', () => {
549
- async function initializeProvider() {
550
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [] }));
551
- await provider.initialize(VALID_CONFIG);
552
- }
553
- it('should fetch changed pages since a date', async () => {
554
- await initializeProvider();
555
- fetchSpy.mockResolvedValueOnce(createMockResponse({
556
- results: [
557
- {
558
- content: {
559
- id: '12345',
560
- type: 'page',
561
- title: 'Updated Guide',
562
- status: 'current',
563
- },
564
- lastModified: '2026-02-20T10:00:00.000Z',
565
- },
566
- {
567
- content: {
568
- id: '67890',
569
- type: 'blogpost',
570
- title: 'New Blog Post',
571
- status: 'current',
572
- },
573
- lastModified: '2026-02-21T14:00:00.000Z',
574
- },
575
- ],
576
- _links: {},
577
- }));
578
- const since = new Date('2026-02-15T00:00:00.000Z');
579
- const result = await provider.getChangedPages(since);
580
- expect(result.isOk()).toBe(true);
581
- if (result.isOk()) {
582
- expect(result.value).toHaveLength(2);
583
- expect(result.value[0].id).toBe('12345');
584
- expect(result.value[0].type).toBe('page');
585
- expect(result.value[1].type).toBe('blogpost');
586
- }
587
- });
588
- it('should build CQL with correct date format', async () => {
589
- await initializeProvider();
590
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [], _links: {} }));
591
- const since = new Date('2026-01-15T08:30:00.000Z');
592
- await provider.getChangedPages(since);
593
- const url = fetchSpy.mock.calls[1][0];
594
- expect(url).toContain('2026-01-15');
595
- expect(url).toContain('lastModified');
596
- });
597
- it('should include space filter in CQL when spaceKeys configured', async () => {
598
- await initializeProvider();
599
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [], _links: {} }));
600
- const since = new Date('2026-02-01T00:00:00.000Z');
601
- await provider.getChangedPages(since);
602
- const url = fetchSpy.mock.calls[1][0];
603
- expect(url).toContain('space');
604
- expect(url).toContain('DEV');
605
- expect(url).toContain('TEAM');
606
- });
607
- it('should return err on API failure', async () => {
608
- await initializeProvider();
609
- fetchSpy.mockResolvedValueOnce(createMockResponse({}, 500, 'Internal Server Error'));
610
- const result = await provider.getChangedPages(new Date());
611
- expect(result.isErr()).toBe(true);
612
- if (result.isErr()) {
613
- expect(result.error.message).toContain('500');
614
- }
615
- });
616
- it('should return err on network error', async () => {
617
- await initializeProvider();
618
- fetchSpy.mockRejectedValueOnce(new Error('Timeout'));
619
- const result = await provider.getChangedPages(new Date());
620
- expect(result.isErr()).toBe(true);
621
- if (result.isErr()) {
622
- expect(result.error.message).toContain('Timeout');
623
- }
624
- });
625
- it('should return empty array when no pages changed', async () => {
626
- await initializeProvider();
627
- fetchSpy.mockResolvedValueOnce(createMockResponse({ results: [], _links: {} }));
628
- const result = await provider.getChangedPages(new Date());
629
- expect(result.isOk()).toBe(true);
630
- if (result.isOk()) {
631
- expect(result.value).toEqual([]);
632
- }
633
- });
634
- });
635
- // --- Error when not initialized ---
636
- describe('when not initialized', () => {
637
- it('should throw ConfluenceError from fetchPages', async () => {
638
- await expect(provider.fetchPages()).rejects.toThrow(ConfluenceError);
639
- });
640
- it('should throw ConfluenceError from fetchPage', async () => {
641
- await expect(provider.fetchPage('123')).rejects.toThrow(ConfluenceError);
642
- });
643
- it('should throw ConfluenceError from fetchBlogPosts', async () => {
644
- await expect(provider.fetchBlogPosts()).rejects.toThrow(ConfluenceError);
645
- });
646
- it('should throw ConfluenceError from fetchComments', async () => {
647
- await expect(provider.fetchComments('123')).rejects.toThrow(ConfluenceError);
648
- });
649
- it('should throw ConfluenceError from getChangedPages', async () => {
650
- await expect(provider.getChangedPages(new Date())).rejects.toThrow(ConfluenceError);
651
- });
652
- });
653
- });
654
- // ---------------------------------------------------------------------------
655
- // XHTML to plain text converter
656
- // ---------------------------------------------------------------------------
657
- describe('confluenceStorageToPlainText', () => {
658
- it('should convert simple paragraph HTML to plain text', () => {
659
- const html = '<p>Hello world</p>';
660
- expect(confluenceStorageToPlainText(html)).toBe('Hello world');
661
- });
662
- it('should strip inline formatting tags', () => {
663
- const html = '<p>This is <strong>bold</strong> and <em>italic</em> text.</p>';
664
- expect(confluenceStorageToPlainText(html)).toBe('This is bold and italic text.');
665
- });
666
- it('should convert block elements to newlines', () => {
667
- const html = '<h1>Title</h1><p>Paragraph 1</p><p>Paragraph 2</p>';
668
- const result = confluenceStorageToPlainText(html);
669
- expect(result).toContain('Title');
670
- expect(result).toContain('Paragraph 1');
671
- expect(result).toContain('Paragraph 2');
672
- // Should have newlines between blocks
673
- expect(result.split('\n').length).toBeGreaterThanOrEqual(3);
674
- });
675
- it('should handle Confluence code block macros', () => {
676
- const html = `
677
- <ac:structured-macro ac:name="code">
678
- <ac:parameter ac:name="language">typescript</ac:parameter>
679
- <ac:plain-text-body><![CDATA[const x = 42;]]></ac:plain-text-body>
680
- </ac:structured-macro>
681
- `;
682
- const result = confluenceStorageToPlainText(html);
683
- expect(result).toContain('const x = 42;');
684
- });
685
- it('should handle rich-text-body macros', () => {
686
- const html = `
687
- <ac:structured-macro ac:name="info">
688
- <ac:rich-text-body><p>Important information here.</p></ac:rich-text-body>
689
- </ac:structured-macro>
690
- `;
691
- const result = confluenceStorageToPlainText(html);
692
- expect(result).toContain('Important information here.');
693
- });
694
- it('should remove toc macros', () => {
695
- const html = '<ac:structured-macro ac:name="toc"><ac:parameter ac:name="maxLevel">3</ac:parameter></ac:structured-macro><p>Content</p>';
696
- const result = confluenceStorageToPlainText(html);
697
- expect(result).toBe('Content');
698
- });
699
- it('should remove anchor macros', () => {
700
- const html = '<ac:structured-macro ac:name="anchor"><ac:parameter ac:name="">section1</ac:parameter></ac:structured-macro><p>Content</p>';
701
- const result = confluenceStorageToPlainText(html);
702
- expect(result).toBe('Content');
703
- });
704
- it('should decode HTML entities', () => {
705
- const html = '<p>A &amp; B &lt; C &gt; D &quot;E&quot; F&#39;s</p>';
706
- const result = confluenceStorageToPlainText(html);
707
- expect(result).toBe('A & B < C > D "E" F\'s');
708
- });
709
- it('should decode numeric character references', () => {
710
- const html = '<p>&#169; 2026 &#x2014; All rights reserved</p>';
711
- const result = confluenceStorageToPlainText(html);
712
- expect(result).toContain('\u00A9');
713
- expect(result).toContain('\u2014');
714
- });
715
- it('should handle list items', () => {
716
- const html = '<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>';
717
- const result = confluenceStorageToPlainText(html);
718
- expect(result).toContain('Item 1');
719
- expect(result).toContain('Item 2');
720
- expect(result).toContain('Item 3');
721
- });
722
- it('should handle table content', () => {
723
- const html = '<table><tr><td>Cell 1</td><td>Cell 2</td></tr><tr><td>Cell 3</td><td>Cell 4</td></tr></table>';
724
- const result = confluenceStorageToPlainText(html);
725
- expect(result).toContain('Cell 1');
726
- expect(result).toContain('Cell 2');
727
- expect(result).toContain('Cell 3');
728
- expect(result).toContain('Cell 4');
729
- });
730
- it('should collapse multiple blank lines', () => {
731
- const html = '<p>Line 1</p><br/><br/><br/><p>Line 2</p>';
732
- const result = confluenceStorageToPlainText(html);
733
- // Should not have more than one consecutive blank line
734
- expect(result).not.toMatch(/\n\n\n/);
735
- });
736
- it('should return empty string for empty input', () => {
737
- expect(confluenceStorageToPlainText('')).toBe('');
738
- });
739
- it('should return empty string for null-ish input', () => {
740
- expect(confluenceStorageToPlainText(undefined)).toBe('');
741
- });
742
- it('should handle Confluence ac: tags without body', () => {
743
- const html = '<p>Before <ac:emoticon ac:name="smile" /> After</p>';
744
- const result = confluenceStorageToPlainText(html);
745
- expect(result).toContain('Before');
746
- expect(result).toContain('After');
747
- expect(result).not.toContain('ac:');
748
- });
749
- it('should preserve text from nested elements', () => {
750
- const html = '<div><p>Outer <span>Inner <strong>Bold</strong> text</span> end</p></div>';
751
- const result = confluenceStorageToPlainText(html);
752
- expect(result).toContain('Outer');
753
- expect(result).toContain('Inner');
754
- expect(result).toContain('Bold');
755
- expect(result).toContain('text');
756
- expect(result).toContain('end');
757
- });
758
- it('should handle &nbsp; entities', () => {
759
- const html = '<p>Word1&nbsp;&nbsp;&nbsp;Word2</p>';
760
- const result = confluenceStorageToPlainText(html);
761
- expect(result).toContain('Word1');
762
- expect(result).toContain('Word2');
763
- });
764
- });
765
- //# sourceMappingURL=confluence-provider.test.js.map