@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,642 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { ModelLifecycleManager, ModelLifecycleError, } from './model-lifecycle-manager.js';
3
- // ---------------------------------------------------------------------------
4
- // Test helpers
5
- // ---------------------------------------------------------------------------
6
- function createMockExecutor(overrides) {
7
- return {
8
- execFile: vi.fn().mockRejectedValue(new Error('not mocked')),
9
- spawn: vi.fn().mockReturnValue({
10
- pid: 12345,
11
- unref: vi.fn(),
12
- on: vi.fn(),
13
- kill: vi.fn(),
14
- }),
15
- ...overrides,
16
- };
17
- }
18
- function createMockFetch(overrides) {
19
- const opts = {
20
- tagsOk: false,
21
- showOk: false,
22
- pullOk: true,
23
- pullBody: null,
24
- throwOnTags: false,
25
- ...overrides,
26
- };
27
- return vi.fn().mockImplementation(async (input) => {
28
- const url = typeof input === 'string' ? input : input.toString();
29
- if (url.includes('/api/tags')) {
30
- if (opts.throwOnTags) {
31
- throw new Error('ECONNREFUSED');
32
- }
33
- return {
34
- ok: opts.tagsOk,
35
- status: opts.tagsOk ? 200 : 503,
36
- text: async () => '',
37
- };
38
- }
39
- if (url.includes('/api/show')) {
40
- return {
41
- ok: opts.showOk,
42
- status: opts.showOk ? 200 : 404,
43
- text: async () => '',
44
- };
45
- }
46
- if (url.includes('/api/pull')) {
47
- return {
48
- ok: opts.pullOk,
49
- status: opts.pullOk ? 200 : 500,
50
- body: opts.pullBody,
51
- text: async () => 'pull error',
52
- };
53
- }
54
- return { ok: false, status: 404, text: async () => 'not found' };
55
- });
56
- }
57
- const SHORT_TIMEOUT_CONFIG = {
58
- healthCheckTimeoutMs: 200,
59
- healthCheckIntervalMs: 50,
60
- };
61
- // ---------------------------------------------------------------------------
62
- // Tests
63
- // ---------------------------------------------------------------------------
64
- describe('ModelLifecycleManager', () => {
65
- beforeEach(() => {
66
- vi.restoreAllMocks();
67
- });
68
- describe('constructor', () => {
69
- it('should use default config when none provided', () => {
70
- const manager = new ModelLifecycleManager();
71
- expect(manager.activeBackend).toBeNull();
72
- });
73
- it('should merge partial config with defaults', () => {
74
- const manager = new ModelLifecycleManager({ model: 'custom-model' });
75
- expect(manager.activeBackend).toBeNull();
76
- });
77
- it('should merge docker config deeply', () => {
78
- const manager = new ModelLifecycleManager({
79
- docker: { image: 'custom/ollama', gpu: 'nvidia' },
80
- });
81
- expect(manager.activeBackend).toBeNull();
82
- });
83
- });
84
- describe('detectBackend', () => {
85
- it('should return running Ollama when health check succeeds', async () => {
86
- const fetchFn = createMockFetch({ tagsOk: true });
87
- const executor = createMockExecutor();
88
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
89
- const backend = await manager.detectBackend();
90
- expect(backend).not.toBeNull();
91
- expect(backend.type).toBe('ollama');
92
- expect(backend.managedByUs).toBe(false);
93
- expect(backend.baseUrl).toBe('http://localhost:11434');
94
- });
95
- it('should return installed Ollama when binary exists but not running', async () => {
96
- const fetchFn = createMockFetch({ tagsOk: false });
97
- const whichCmd = process.platform === 'win32' ? 'where' : 'which';
98
- const executor = createMockExecutor({
99
- execFile: vi.fn().mockImplementation(async (cmd) => {
100
- if (cmd === whichCmd)
101
- return { stdout: '/usr/local/bin/ollama', stderr: '' };
102
- throw new Error('not found');
103
- }),
104
- });
105
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
106
- const backend = await manager.detectBackend();
107
- expect(backend).not.toBeNull();
108
- expect(backend.type).toBe('ollama');
109
- expect(backend.managedByUs).toBe(true);
110
- });
111
- it('should return Docker when Ollama is not available but Docker is', async () => {
112
- const fetchFn = createMockFetch({ tagsOk: false });
113
- const executor = createMockExecutor({
114
- execFile: vi.fn().mockImplementation(async (cmd) => {
115
- if (cmd === 'docker')
116
- return { stdout: '', stderr: '' };
117
- throw new Error('not found');
118
- }),
119
- });
120
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
121
- const backend = await manager.detectBackend();
122
- expect(backend).not.toBeNull();
123
- expect(backend.type).toBe('docker');
124
- expect(backend.managedByUs).toBe(true);
125
- });
126
- it('should return null when nothing is available', async () => {
127
- const fetchFn = createMockFetch({ tagsOk: false });
128
- const executor = createMockExecutor({
129
- execFile: vi.fn().mockRejectedValue(new Error('not found')),
130
- });
131
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
132
- const backend = await manager.detectBackend();
133
- expect(backend).toBeNull();
134
- });
135
- it('should handle fetch throwing (not just non-ok)', async () => {
136
- const fetchFn = createMockFetch({ throwOnTags: true });
137
- const executor = createMockExecutor({
138
- execFile: vi.fn().mockRejectedValue(new Error('not found')),
139
- });
140
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
141
- const backend = await manager.detectBackend();
142
- expect(backend).toBeNull();
143
- });
144
- });
145
- describe('ensureRunning', () => {
146
- it('should return ok immediately if Ollama is already running', async () => {
147
- const fetchFn = createMockFetch({ tagsOk: true });
148
- const executor = createMockExecutor();
149
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
150
- const result = await manager.ensureRunning();
151
- expect(result.isOk()).toBe(true);
152
- if (result.isOk()) {
153
- expect(result.value.type).toBe('ollama');
154
- expect(result.value.managedByUs).toBe(false);
155
- }
156
- expect(manager.activeBackend).not.toBeNull();
157
- });
158
- it('should start Ollama when installed but not running', async () => {
159
- let callCount = 0;
160
- const fetchFn = vi.fn().mockImplementation(async (input) => {
161
- const url = typeof input === 'string' ? input : input.toString();
162
- if (url.includes('/api/tags')) {
163
- callCount++;
164
- // First call: not running. Subsequent calls: running (simulates startup).
165
- return {
166
- ok: callCount > 1,
167
- status: callCount > 1 ? 200 : 503,
168
- text: async () => '',
169
- };
170
- }
171
- return { ok: false, status: 404, text: async () => '' };
172
- });
173
- const whichCmd = process.platform === 'win32' ? 'where' : 'which';
174
- const executor = createMockExecutor({
175
- execFile: vi.fn().mockImplementation(async (cmd) => {
176
- if (cmd === whichCmd)
177
- return { stdout: '/usr/local/bin/ollama', stderr: '' };
178
- throw new Error('not found');
179
- }),
180
- });
181
- const manager = new ModelLifecycleManager(SHORT_TIMEOUT_CONFIG, executor, fetchFn);
182
- const result = await manager.ensureRunning();
183
- expect(result.isOk()).toBe(true);
184
- if (result.isOk()) {
185
- expect(result.value.type).toBe('ollama');
186
- expect(result.value.managedByUs).toBe(true);
187
- expect(result.value.pid).toBe(12345);
188
- }
189
- expect(executor.spawn).toHaveBeenCalledWith('ollama', ['serve'], {
190
- detached: true,
191
- stdio: 'ignore',
192
- });
193
- });
194
- it('should start Docker container when Ollama is not available', async () => {
195
- let callCount = 0;
196
- const fetchFn = vi.fn().mockImplementation(async (input) => {
197
- const url = typeof input === 'string' ? input : input.toString();
198
- if (url.includes('/api/tags')) {
199
- callCount++;
200
- // First two calls (detect + first health check): not running
201
- // Third call: running (Docker container started)
202
- return {
203
- ok: callCount > 2,
204
- status: callCount > 2 ? 200 : 503,
205
- text: async () => '',
206
- };
207
- }
208
- return { ok: false, status: 404, text: async () => '' };
209
- });
210
- const executor = createMockExecutor({
211
- execFile: vi.fn().mockImplementation(async (cmd, args) => {
212
- if (cmd === 'docker' && args[0] === 'info')
213
- return { stdout: '', stderr: '' };
214
- if (cmd === 'docker' && args[0] === 'run')
215
- return { stdout: 'abc123def456789\n', stderr: '' };
216
- throw new Error('not found');
217
- }),
218
- });
219
- const manager = new ModelLifecycleManager(SHORT_TIMEOUT_CONFIG, executor, fetchFn);
220
- const result = await manager.ensureRunning();
221
- expect(result.isOk()).toBe(true);
222
- if (result.isOk()) {
223
- expect(result.value.type).toBe('docker');
224
- expect(result.value.managedByUs).toBe(true);
225
- expect(result.value.containerId).toBe('abc123def456');
226
- }
227
- });
228
- it('should return err with install instructions when nothing is available', async () => {
229
- const fetchFn = createMockFetch({ tagsOk: false });
230
- const executor = createMockExecutor({
231
- execFile: vi.fn().mockRejectedValue(new Error('not found')),
232
- });
233
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
234
- const result = await manager.ensureRunning();
235
- expect(result.isErr()).toBe(true);
236
- if (result.isErr()) {
237
- expect(result.error).toBeInstanceOf(ModelLifecycleError);
238
- expect(result.error.message).toContain('Install Ollama');
239
- }
240
- });
241
- it('should return err when autoStart is disabled and Ollama is not running', async () => {
242
- const fetchFn = createMockFetch({ tagsOk: false });
243
- const executor = createMockExecutor();
244
- const manager = new ModelLifecycleManager({ autoStart: false }, executor, fetchFn);
245
- const result = await manager.ensureRunning();
246
- expect(result.isErr()).toBe(true);
247
- if (result.isErr()) {
248
- expect(result.error.message).toContain('auto_start is disabled');
249
- }
250
- });
251
- it('should return ok when autoStart is disabled but Ollama is running', async () => {
252
- const fetchFn = createMockFetch({ tagsOk: true });
253
- const executor = createMockExecutor();
254
- const manager = new ModelLifecycleManager({ autoStart: false }, executor, fetchFn);
255
- const result = await manager.ensureRunning();
256
- expect(result.isOk()).toBe(true);
257
- if (result.isOk()) {
258
- expect(result.value.type).toBe('ollama');
259
- expect(result.value.managedByUs).toBe(false);
260
- }
261
- });
262
- it('should return err on health check timeout', async () => {
263
- // Ollama is installed but never starts (health check always fails)
264
- const fetchFn = createMockFetch({ tagsOk: false });
265
- const whichCmd = process.platform === 'win32' ? 'where' : 'which';
266
- const executor = createMockExecutor({
267
- execFile: vi.fn().mockImplementation(async (cmd) => {
268
- if (cmd === whichCmd)
269
- return { stdout: '/usr/local/bin/ollama', stderr: '' };
270
- throw new Error('not found');
271
- }),
272
- });
273
- const manager = new ModelLifecycleManager({ healthCheckTimeoutMs: 150, healthCheckIntervalMs: 50 }, executor, fetchFn);
274
- const result = await manager.ensureRunning();
275
- expect(result.isErr()).toBe(true);
276
- if (result.isErr()) {
277
- expect(result.error.message).toContain('health check timed out');
278
- }
279
- });
280
- it('should include GPU flags for Docker with nvidia GPU detection', async () => {
281
- let callCount = 0;
282
- const fetchFn = vi.fn().mockImplementation(async (input) => {
283
- const url = typeof input === 'string' ? input : input.toString();
284
- if (url.includes('/api/tags')) {
285
- callCount++;
286
- return {
287
- ok: callCount > 2,
288
- status: callCount > 2 ? 200 : 503,
289
- text: async () => '',
290
- };
291
- }
292
- return { ok: false, status: 404, text: async () => '' };
293
- });
294
- const execFileMock = vi.fn().mockImplementation(async (cmd, args) => {
295
- if (cmd === 'docker' && args[0] === 'info')
296
- return { stdout: '', stderr: '' };
297
- if (cmd === 'docker' && args[0] === 'run')
298
- return { stdout: 'container123\n', stderr: '' };
299
- if (cmd === 'nvidia-smi')
300
- return { stdout: 'GPU info', stderr: '' };
301
- throw new Error('not found');
302
- });
303
- const executor = createMockExecutor({ execFile: execFileMock });
304
- const manager = new ModelLifecycleManager({ ...SHORT_TIMEOUT_CONFIG, docker: { image: 'ollama/ollama', gpu: 'auto' } }, executor, fetchFn);
305
- const result = await manager.ensureRunning();
306
- expect(result.isOk()).toBe(true);
307
- // Verify docker run was called with --gpus all
308
- const dockerRunCall = execFileMock.mock.calls.find((c) => c[0] === 'docker' && c[1][0] === 'run');
309
- expect(dockerRunCall).toBeDefined();
310
- expect(dockerRunCall[1]).toContain('--gpus');
311
- expect(dockerRunCall[1]).toContain('all');
312
- });
313
- it('should not include GPU flags when gpu is none', async () => {
314
- let callCount = 0;
315
- const fetchFn = vi.fn().mockImplementation(async (input) => {
316
- const url = typeof input === 'string' ? input : input.toString();
317
- if (url.includes('/api/tags')) {
318
- callCount++;
319
- return {
320
- ok: callCount > 2,
321
- status: callCount > 2 ? 200 : 503,
322
- text: async () => '',
323
- };
324
- }
325
- return { ok: false, status: 404, text: async () => '' };
326
- });
327
- const execFileMock = vi.fn().mockImplementation(async (cmd, args) => {
328
- if (cmd === 'docker' && args[0] === 'info')
329
- return { stdout: '', stderr: '' };
330
- if (cmd === 'docker' && args[0] === 'run')
331
- return { stdout: 'container123\n', stderr: '' };
332
- throw new Error('not found');
333
- });
334
- const executor = createMockExecutor({ execFile: execFileMock });
335
- const manager = new ModelLifecycleManager({ ...SHORT_TIMEOUT_CONFIG, docker: { image: 'ollama/ollama', gpu: 'none' } }, executor, fetchFn);
336
- const result = await manager.ensureRunning();
337
- expect(result.isOk()).toBe(true);
338
- const dockerRunCall = execFileMock.mock.calls.find((c) => c[0] === 'docker' && c[1][0] === 'run');
339
- expect(dockerRunCall).toBeDefined();
340
- expect(dockerRunCall[1]).not.toContain('--gpus');
341
- });
342
- });
343
- describe('ensureModel', () => {
344
- it('should return ok if model is already available', async () => {
345
- const fetchFn = createMockFetch({ showOk: true });
346
- const executor = createMockExecutor();
347
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
348
- const result = await manager.ensureModel('nomic-embed-text');
349
- expect(result.isOk()).toBe(true);
350
- expect(fetchFn).toHaveBeenCalledWith(expect.stringContaining('/api/show'), expect.objectContaining({ method: 'POST' }));
351
- });
352
- it('should pull model when not available (no streaming body)', async () => {
353
- const fetchFn = createMockFetch({ showOk: false, pullOk: true, pullBody: null });
354
- const executor = createMockExecutor();
355
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
356
- const result = await manager.ensureModel('nomic-embed-text');
357
- expect(result.isOk()).toBe(true);
358
- expect(fetchFn).toHaveBeenCalledWith(expect.stringContaining('/api/pull'), expect.objectContaining({ method: 'POST' }));
359
- });
360
- it('should pull model with streaming progress', async () => {
361
- const encoder = new TextEncoder();
362
- const stream = new ReadableStream({
363
- start(controller) {
364
- controller.enqueue(encoder.encode('{"status":"downloading","completed":50,"total":100}\n'));
365
- controller.enqueue(encoder.encode('{"status":"verifying","completed":100,"total":100}\n'));
366
- controller.close();
367
- },
368
- });
369
- const fetchFn = createMockFetch({ showOk: false, pullOk: true, pullBody: stream });
370
- const executor = createMockExecutor();
371
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
372
- const progressUpdates = [];
373
- const onProgress = (status, completed, total) => {
374
- progressUpdates.push({ status, completed, total });
375
- };
376
- const result = await manager.ensureModel('nomic-embed-text', onProgress);
377
- expect(result.isOk()).toBe(true);
378
- expect(progressUpdates).toHaveLength(2);
379
- expect(progressUpdates[0]).toEqual({ status: 'downloading', completed: 50, total: 100 });
380
- expect(progressUpdates[1]).toEqual({ status: 'verifying', completed: 100, total: 100 });
381
- });
382
- it('should return err on pull failure (non-200)', async () => {
383
- const fetchFn = createMockFetch({ showOk: false, pullOk: false });
384
- const executor = createMockExecutor();
385
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
386
- const result = await manager.ensureModel('nomic-embed-text');
387
- expect(result.isErr()).toBe(true);
388
- if (result.isErr()) {
389
- expect(result.error.message).toContain('Failed to pull model');
390
- }
391
- });
392
- it('should return err on streaming error in progress', async () => {
393
- const encoder = new TextEncoder();
394
- const stream = new ReadableStream({
395
- start(controller) {
396
- controller.enqueue(encoder.encode('{"error":"model not found"}\n'));
397
- controller.close();
398
- },
399
- });
400
- const fetchFn = createMockFetch({ showOk: false, pullOk: true, pullBody: stream });
401
- const executor = createMockExecutor();
402
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
403
- const result = await manager.ensureModel('nonexistent-model');
404
- expect(result.isErr()).toBe(true);
405
- if (result.isErr()) {
406
- expect(result.error.message).toContain('Model pull error');
407
- }
408
- });
409
- it('should use default model from config when none specified', async () => {
410
- const fetchFn = createMockFetch({ showOk: true });
411
- const executor = createMockExecutor();
412
- const manager = new ModelLifecycleManager({ model: 'my-default-model' }, executor, fetchFn);
413
- const result = await manager.ensureModel();
414
- expect(result.isOk()).toBe(true);
415
- expect(fetchFn).toHaveBeenCalledWith(expect.stringContaining('/api/show'), expect.objectContaining({
416
- body: JSON.stringify({ name: 'my-default-model' }),
417
- }));
418
- });
419
- it('should skip unparseable lines in streaming response', async () => {
420
- const encoder = new TextEncoder();
421
- const stream = new ReadableStream({
422
- start(controller) {
423
- controller.enqueue(encoder.encode('not-json\n'));
424
- controller.enqueue(encoder.encode('{"status":"done","completed":100,"total":100}\n'));
425
- controller.close();
426
- },
427
- });
428
- const fetchFn = createMockFetch({ showOk: false, pullOk: true, pullBody: stream });
429
- const executor = createMockExecutor();
430
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
431
- const progressUpdates = [];
432
- const onProgress = (status, completed, total) => {
433
- progressUpdates.push({ status, completed, total });
434
- };
435
- const result = await manager.ensureModel('test-model', onProgress);
436
- expect(result.isOk()).toBe(true);
437
- // Only the valid JSON line should have produced a progress callback
438
- expect(progressUpdates).toHaveLength(1);
439
- expect(progressUpdates[0].status).toBe('done');
440
- });
441
- });
442
- describe('stop', () => {
443
- it('should return ok if no backend was started', async () => {
444
- const fetchFn = createMockFetch();
445
- const executor = createMockExecutor();
446
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
447
- const result = await manager.stop();
448
- expect(result.isOk()).toBe(true);
449
- });
450
- it('should return ok if backend was not managed by us', async () => {
451
- const fetchFn = createMockFetch({ tagsOk: true });
452
- const executor = createMockExecutor();
453
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
454
- const ensureResult = await manager.ensureRunning(); // sets managedByUs: false
455
- expect(ensureResult.isOk()).toBe(true);
456
- const stopResult = await manager.stop();
457
- expect(stopResult.isOk()).toBe(true);
458
- expect(executor.execFile).not.toHaveBeenCalledWith('docker', expect.arrayContaining(['stop']));
459
- });
460
- it('should kill Ollama process when managed by us', async () => {
461
- let callCount = 0;
462
- const fetchFn = vi.fn().mockImplementation(async (input) => {
463
- const url = typeof input === 'string' ? input : input.toString();
464
- if (url.includes('/api/tags')) {
465
- callCount++;
466
- return {
467
- ok: callCount > 1,
468
- status: callCount > 1 ? 200 : 503,
469
- text: async () => '',
470
- };
471
- }
472
- return { ok: false, status: 404, text: async () => '' };
473
- });
474
- const whichCmd = process.platform === 'win32' ? 'where' : 'which';
475
- const executor = createMockExecutor({
476
- execFile: vi.fn().mockImplementation(async (cmd) => {
477
- if (cmd === whichCmd)
478
- return { stdout: '/usr/local/bin/ollama', stderr: '' };
479
- throw new Error('not found');
480
- }),
481
- });
482
- const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true);
483
- const manager = new ModelLifecycleManager(SHORT_TIMEOUT_CONFIG, executor, fetchFn);
484
- const ensureResult = await manager.ensureRunning();
485
- expect(ensureResult.isOk()).toBe(true);
486
- const stopResult = await manager.stop();
487
- expect(stopResult.isOk()).toBe(true);
488
- expect(killSpy).toHaveBeenCalledWith(12345, 'SIGTERM');
489
- expect(manager.activeBackend).toBeNull();
490
- killSpy.mockRestore();
491
- });
492
- it('should stop Docker container when managed by us', async () => {
493
- let callCount = 0;
494
- const fetchFn = vi.fn().mockImplementation(async (input) => {
495
- const url = typeof input === 'string' ? input : input.toString();
496
- if (url.includes('/api/tags')) {
497
- callCount++;
498
- return {
499
- ok: callCount > 2,
500
- status: callCount > 2 ? 200 : 503,
501
- text: async () => '',
502
- };
503
- }
504
- return { ok: false, status: 404, text: async () => '' };
505
- });
506
- const execFileMock = vi.fn().mockImplementation(async (cmd, args) => {
507
- if (cmd === 'docker' && args[0] === 'info')
508
- return { stdout: '', stderr: '' };
509
- if (cmd === 'docker' && args[0] === 'run')
510
- return { stdout: 'container123abc\n', stderr: '' };
511
- if (cmd === 'docker' && args[0] === 'stop')
512
- return { stdout: '', stderr: '' };
513
- throw new Error('not found');
514
- });
515
- const executor = createMockExecutor({ execFile: execFileMock });
516
- const manager = new ModelLifecycleManager(SHORT_TIMEOUT_CONFIG, executor, fetchFn);
517
- const ensureResult = await manager.ensureRunning();
518
- expect(ensureResult.isOk()).toBe(true);
519
- const stopResult = await manager.stop();
520
- expect(stopResult.isOk()).toBe(true);
521
- expect(execFileMock).toHaveBeenCalledWith('docker', ['stop', 'container123']);
522
- expect(manager.activeBackend).toBeNull();
523
- });
524
- it('should handle stop errors gracefully (process already gone)', async () => {
525
- let callCount = 0;
526
- const fetchFn = vi.fn().mockImplementation(async (input) => {
527
- const url = typeof input === 'string' ? input : input.toString();
528
- if (url.includes('/api/tags')) {
529
- callCount++;
530
- return {
531
- ok: callCount > 1,
532
- status: callCount > 1 ? 200 : 503,
533
- text: async () => '',
534
- };
535
- }
536
- return { ok: false, status: 404, text: async () => '' };
537
- });
538
- const whichCmd = process.platform === 'win32' ? 'where' : 'which';
539
- const executor = createMockExecutor({
540
- execFile: vi.fn().mockImplementation(async (cmd) => {
541
- if (cmd === whichCmd)
542
- return { stdout: '/usr/local/bin/ollama', stderr: '' };
543
- throw new Error('not found');
544
- }),
545
- });
546
- const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => {
547
- throw new Error('ESRCH');
548
- });
549
- const manager = new ModelLifecycleManager(SHORT_TIMEOUT_CONFIG, executor, fetchFn);
550
- const ensureResult = await manager.ensureRunning();
551
- expect(ensureResult.isOk()).toBe(true);
552
- // Should return ok even though process.kill throws
553
- const stopResult = await manager.stop();
554
- expect(stopResult.isOk()).toBe(true);
555
- killSpy.mockRestore();
556
- });
557
- });
558
- describe('detection priority order', () => {
559
- it('should prefer running Ollama over installed Ollama', async () => {
560
- const fetchFn = createMockFetch({ tagsOk: true });
561
- const execFileMock = vi.fn().mockResolvedValue({
562
- stdout: '/usr/local/bin/ollama',
563
- stderr: '',
564
- });
565
- const executor = createMockExecutor({ execFile: execFileMock });
566
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
567
- const backend = await manager.detectBackend();
568
- expect(backend.type).toBe('ollama');
569
- expect(backend.managedByUs).toBe(false);
570
- // Should not even check which/docker
571
- expect(execFileMock).not.toHaveBeenCalled();
572
- });
573
- it('should prefer installed Ollama over Docker', async () => {
574
- const fetchFn = createMockFetch({ tagsOk: false });
575
- const whichCmd = process.platform === 'win32' ? 'where' : 'which';
576
- const execFileMock = vi.fn().mockImplementation(async (cmd) => {
577
- if (cmd === whichCmd)
578
- return { stdout: '/usr/local/bin/ollama', stderr: '' };
579
- if (cmd === 'docker')
580
- return { stdout: '', stderr: '' };
581
- throw new Error('not found');
582
- });
583
- const executor = createMockExecutor({ execFile: execFileMock });
584
- const manager = new ModelLifecycleManager({}, executor, fetchFn);
585
- const backend = await manager.detectBackend();
586
- expect(backend.type).toBe('ollama');
587
- expect(backend.managedByUs).toBe(true);
588
- // Should not have checked docker
589
- expect(execFileMock).not.toHaveBeenCalledWith('docker', expect.anything());
590
- });
591
- });
592
- describe('ModelLifecycleError', () => {
593
- it('should have correct name', () => {
594
- const error = new ModelLifecycleError('test');
595
- expect(error.name).toBe('ModelLifecycleError');
596
- expect(error.message).toBe('test');
597
- expect(error).toBeInstanceOf(Error);
598
- });
599
- });
600
- describe('custom baseUrl', () => {
601
- it('should use custom baseUrl for health checks', async () => {
602
- const fetchFn = createMockFetch({ tagsOk: true });
603
- const executor = createMockExecutor();
604
- const manager = new ModelLifecycleManager({ baseUrl: 'http://remote:9999' }, executor, fetchFn);
605
- const backend = await manager.detectBackend();
606
- expect(backend.baseUrl).toBe('http://remote:9999');
607
- expect(fetchFn).toHaveBeenCalledWith('http://remote:9999/api/tags', expect.anything());
608
- });
609
- });
610
- describe('docker image configuration', () => {
611
- it('should use custom Docker image', async () => {
612
- let callCount = 0;
613
- const fetchFn = vi.fn().mockImplementation(async (input) => {
614
- const url = typeof input === 'string' ? input : input.toString();
615
- if (url.includes('/api/tags')) {
616
- callCount++;
617
- return {
618
- ok: callCount > 2,
619
- status: callCount > 2 ? 200 : 503,
620
- text: async () => '',
621
- };
622
- }
623
- return { ok: false, status: 404, text: async () => '' };
624
- });
625
- const execFileMock = vi.fn().mockImplementation(async (cmd, args) => {
626
- if (cmd === 'docker' && args[0] === 'info')
627
- return { stdout: '', stderr: '' };
628
- if (cmd === 'docker' && args[0] === 'run')
629
- return { stdout: 'container123\n', stderr: '' };
630
- throw new Error('not found');
631
- });
632
- const executor = createMockExecutor({ execFile: execFileMock });
633
- const manager = new ModelLifecycleManager({ ...SHORT_TIMEOUT_CONFIG, docker: { image: 'my-custom/ollama:latest', gpu: 'none' } }, executor, fetchFn);
634
- const result = await manager.ensureRunning();
635
- expect(result.isOk()).toBe(true);
636
- const dockerRunCall = execFileMock.mock.calls.find((c) => c[0] === 'docker' && c[1][0] === 'run');
637
- expect(dockerRunCall).toBeDefined();
638
- expect(dockerRunCall[1]).toContain('my-custom/ollama:latest');
639
- });
640
- });
641
- });
642
- //# sourceMappingURL=model-lifecycle-manager.test.js.map