@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.
- package/package.json +4 -4
- package/dist/auth/audit-log.js.map +0 -1
- package/dist/auth/audit-log.test.d.ts +0 -1
- package/dist/auth/audit-log.test.js +0 -261
- package/dist/auth/audit-log.test.js.map +0 -1
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/oidc-provider.js.map +0 -1
- package/dist/auth/oidc-provider.test.d.ts +0 -1
- package/dist/auth/oidc-provider.test.js +0 -520
- package/dist/auth/oidc-provider.test.js.map +0 -1
- package/dist/auth/rbac.js.map +0 -1
- package/dist/auth/rbac.test.d.ts +0 -1
- package/dist/auth/rbac.test.js +0 -224
- package/dist/auth/rbac.test.js.map +0 -1
- package/dist/auth/saml-provider.js.map +0 -1
- package/dist/auth/saml-provider.test.d.ts +0 -1
- package/dist/auth/saml-provider.test.js +0 -422
- package/dist/auth/saml-provider.test.js.map +0 -1
- package/dist/auth/types.js.map +0 -1
- package/dist/auth/types.test.d.ts +0 -1
- package/dist/auth/types.test.js +0 -147
- package/dist/auth/types.test.js.map +0 -1
- package/dist/backlog/ab-reference-scanner.js.map +0 -1
- package/dist/backlog/ab-reference-scanner.test.d.ts +0 -1
- package/dist/backlog/ab-reference-scanner.test.js +0 -83
- package/dist/backlog/ab-reference-scanner.test.js.map +0 -1
- package/dist/backlog/azure-devops-provider.js.map +0 -1
- package/dist/backlog/backlog-provider.js.map +0 -1
- package/dist/backlog/backlog-provider.test.d.ts +0 -1
- package/dist/backlog/backlog-provider.test.js +0 -426
- package/dist/backlog/backlog-provider.test.js.map +0 -1
- package/dist/backlog/clickup-provider.js.map +0 -1
- package/dist/backlog/clickup-provider.test.d.ts +0 -1
- package/dist/backlog/clickup-provider.test.js +0 -426
- package/dist/backlog/clickup-provider.test.js.map +0 -1
- package/dist/backlog/clickup-reference-scanner.js.map +0 -1
- package/dist/backlog/clickup-reference-scanner.test.d.ts +0 -1
- package/dist/backlog/clickup-reference-scanner.test.js +0 -92
- package/dist/backlog/clickup-reference-scanner.test.js.map +0 -1
- package/dist/backlog/code-linker.js.map +0 -1
- package/dist/backlog/code-linker.test.d.ts +0 -1
- package/dist/backlog/code-linker.test.js +0 -325
- package/dist/backlog/code-linker.test.js.map +0 -1
- package/dist/backlog/index.js.map +0 -1
- package/dist/backlog/jira-provider.js.map +0 -1
- package/dist/backlog/jira-provider.test.d.ts +0 -1
- package/dist/backlog/jira-provider.test.js +0 -449
- package/dist/backlog/jira-provider.test.js.map +0 -1
- package/dist/backlog/jira-reference-scanner.js.map +0 -1
- package/dist/backlog/jira-reference-scanner.test.d.ts +0 -1
- package/dist/backlog/jira-reference-scanner.test.js +0 -127
- package/dist/backlog/jira-reference-scanner.test.js.map +0 -1
- package/dist/backlog/types.js.map +0 -1
- package/dist/chunker/ast-chunker.js.map +0 -1
- package/dist/chunker/ast-chunker.test.d.ts +0 -1
- package/dist/chunker/ast-chunker.test.js +0 -391
- package/dist/chunker/ast-chunker.test.js.map +0 -1
- package/dist/chunker/chunker.js.map +0 -1
- package/dist/chunker/index.js.map +0 -1
- package/dist/config/config-parser.js.map +0 -1
- package/dist/config/config-parser.test.d.ts +0 -1
- package/dist/config/config-parser.test.js +0 -699
- package/dist/config/config-parser.test.js.map +0 -1
- package/dist/docs/confluence-provider.js.map +0 -1
- package/dist/docs/confluence-provider.test.d.ts +0 -1
- package/dist/docs/confluence-provider.test.js +0 -765
- package/dist/docs/confluence-provider.test.js.map +0 -1
- package/dist/docs/index.js.map +0 -1
- package/dist/docs/sharepoint-provider.js.map +0 -1
- package/dist/docs/sharepoint-provider.test.d.ts +0 -1
- package/dist/docs/sharepoint-provider.test.js +0 -873
- package/dist/docs/sharepoint-provider.test.js.map +0 -1
- package/dist/embedding/bm25-index.js.map +0 -1
- package/dist/embedding/bm25-index.test.d.ts +0 -1
- package/dist/embedding/bm25-index.test.js +0 -289
- package/dist/embedding/bm25-index.test.js.map +0 -1
- package/dist/embedding/hybrid-search.js.map +0 -1
- package/dist/embedding/hybrid-search.test.d.ts +0 -1
- package/dist/embedding/hybrid-search.test.js +0 -266
- package/dist/embedding/hybrid-search.test.js.map +0 -1
- package/dist/embedding/index.js.map +0 -1
- package/dist/embedding/lancedb-store.js.map +0 -1
- package/dist/embedding/lancedb-store.test.d.ts +0 -1
- package/dist/embedding/lancedb-store.test.js +0 -268
- package/dist/embedding/lancedb-store.test.js.map +0 -1
- package/dist/embedding/model-lifecycle-manager.js.map +0 -1
- package/dist/embedding/model-lifecycle-manager.test.d.ts +0 -1
- package/dist/embedding/model-lifecycle-manager.test.js +0 -642
- package/dist/embedding/model-lifecycle-manager.test.js.map +0 -1
- package/dist/embedding/ollama-embedding-provider.js.map +0 -1
- package/dist/embedding/ollama-embedding-provider.test.d.ts +0 -1
- package/dist/embedding/ollama-embedding-provider.test.js +0 -198
- package/dist/embedding/ollama-embedding-provider.test.js.map +0 -1
- package/dist/embedding/openai-compatible-embedding-provider.js.map +0 -1
- package/dist/embedding/openai-compatible-embedding-provider.test.d.ts +0 -1
- package/dist/embedding/openai-compatible-embedding-provider.test.js +0 -456
- package/dist/embedding/openai-compatible-embedding-provider.test.js.map +0 -1
- package/dist/embedding/qdrant-store.js.map +0 -1
- package/dist/embedding/qdrant-store.test.d.ts +0 -1
- package/dist/embedding/qdrant-store.test.js +0 -359
- package/dist/embedding/qdrant-store.test.js.map +0 -1
- package/dist/enrichment/index.js.map +0 -1
- package/dist/enrichment/nl-enricher.js.map +0 -1
- package/dist/enrichment/nl-enricher.test.d.ts +0 -1
- package/dist/enrichment/nl-enricher.test.js +0 -154
- package/dist/enrichment/nl-enricher.test.js.map +0 -1
- package/dist/enrichment/ollama-client.js.map +0 -1
- package/dist/enrichment/ollama-client.test.d.ts +0 -1
- package/dist/enrichment/ollama-client.test.js +0 -129
- package/dist/enrichment/ollama-client.test.js.map +0 -1
- package/dist/git/git-client.js.map +0 -1
- package/dist/git/git-client.test.d.ts +0 -1
- package/dist/git/git-client.test.js +0 -200
- package/dist/git/git-client.test.js.map +0 -1
- package/dist/git/ignore-filter.js.map +0 -1
- package/dist/git/ignore-filter.test.d.ts +0 -1
- package/dist/git/ignore-filter.test.js +0 -87
- package/dist/git/ignore-filter.test.js.map +0 -1
- package/dist/git/index.js.map +0 -1
- package/dist/git/simple-git-client.js.map +0 -1
- package/dist/graph/cross-repo-resolver.js.map +0 -1
- package/dist/graph/cross-repo-resolver.test.d.ts +0 -1
- package/dist/graph/cross-repo-resolver.test.js +0 -548
- package/dist/graph/cross-repo-resolver.test.js.map +0 -1
- package/dist/graph/dependency-graph.js.map +0 -1
- package/dist/graph/dependency-graph.test.d.ts +0 -1
- package/dist/graph/dependency-graph.test.js +0 -276
- package/dist/graph/dependency-graph.test.js.map +0 -1
- package/dist/graph/graph-builder.js.map +0 -1
- package/dist/graph/graph-builder.test.d.ts +0 -1
- package/dist/graph/graph-builder.test.js +0 -178
- package/dist/graph/graph-builder.test.js.map +0 -1
- package/dist/graph/import-resolver.js.map +0 -1
- package/dist/graph/import-resolver.test.d.ts +0 -1
- package/dist/graph/import-resolver.test.js +0 -282
- package/dist/graph/import-resolver.test.js.map +0 -1
- package/dist/graph/index.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/indexer/file-scanner.js.map +0 -1
- package/dist/indexer/file-scanner.test.d.ts +0 -1
- package/dist/indexer/file-scanner.test.js +0 -110
- package/dist/indexer/file-scanner.test.js.map +0 -1
- package/dist/indexer/incremental-indexer.js.map +0 -1
- package/dist/indexer/incremental-indexer.test.d.ts +0 -1
- package/dist/indexer/incremental-indexer.test.js +0 -266
- package/dist/indexer/incremental-indexer.test.js.map +0 -1
- package/dist/indexer/index-check.js.map +0 -1
- package/dist/indexer/index-check.test.d.ts +0 -1
- package/dist/indexer/index-check.test.js +0 -100
- package/dist/indexer/index-check.test.js.map +0 -1
- package/dist/indexer/index-state.js.map +0 -1
- package/dist/indexer/index-state.test.d.ts +0 -1
- package/dist/indexer/index-state.test.js +0 -140
- package/dist/indexer/index-state.test.js.map +0 -1
- package/dist/indexer/index.js.map +0 -1
- package/dist/indexer/multi-repo-indexer.js.map +0 -1
- package/dist/indexer/multi-repo-indexer.test.d.ts +0 -1
- package/dist/indexer/multi-repo-indexer.test.js +0 -238
- package/dist/indexer/multi-repo-indexer.test.js.map +0 -1
- package/dist/parser/index.js.map +0 -1
- package/dist/parser/language-registry.js.map +0 -1
- package/dist/parser/language-registry.test.d.ts +0 -1
- package/dist/parser/language-registry.test.js +0 -225
- package/dist/parser/language-registry.test.js.map +0 -1
- package/dist/parser/markdown-parser.js.map +0 -1
- package/dist/parser/markdown-parser.test.d.ts +0 -1
- package/dist/parser/markdown-parser.test.js +0 -600
- package/dist/parser/markdown-parser.test.js.map +0 -1
- package/dist/parser/tree-sitter-parser.js.map +0 -1
- package/dist/retrieval/context-expander.js.map +0 -1
- package/dist/retrieval/context-expander.test.d.ts +0 -1
- package/dist/retrieval/context-expander.test.js +0 -339
- package/dist/retrieval/context-expander.test.js.map +0 -1
- package/dist/retrieval/cross-encoder-reranker.js.map +0 -1
- package/dist/retrieval/cross-encoder-reranker.test.d.ts +0 -1
- package/dist/retrieval/cross-encoder-reranker.test.js +0 -305
- package/dist/retrieval/cross-encoder-reranker.test.js.map +0 -1
- package/dist/retrieval/index.js.map +0 -1
- package/dist/retrieval/query-analyzer.js.map +0 -1
- package/dist/retrieval/query-analyzer.test.d.ts +0 -1
- package/dist/retrieval/query-analyzer.test.js +0 -236
- package/dist/retrieval/query-analyzer.test.js.map +0 -1
- package/dist/retrieval/token-budget.js.map +0 -1
- package/dist/retrieval/token-budget.test.d.ts +0 -1
- package/dist/retrieval/token-budget.test.js +0 -404
- package/dist/retrieval/token-budget.test.js.map +0 -1
- package/dist/storage/azure-blob-provider.js.map +0 -1
- package/dist/storage/azure-blob-provider.test.d.ts +0 -1
- package/dist/storage/azure-blob-provider.test.js +0 -250
- package/dist/storage/azure-blob-provider.test.js.map +0 -1
- package/dist/storage/gcs-provider.js.map +0 -1
- package/dist/storage/gcs-provider.test.d.ts +0 -1
- package/dist/storage/gcs-provider.test.js +0 -299
- package/dist/storage/gcs-provider.test.js.map +0 -1
- package/dist/storage/index.js.map +0 -1
- package/dist/storage/s3-provider.js.map +0 -1
- package/dist/storage/s3-provider.test.d.ts +0 -1
- package/dist/storage/s3-provider.test.js +0 -329
- package/dist/storage/s3-provider.test.js.map +0 -1
- package/dist/storage/types.js.map +0 -1
- package/dist/types/chunk.js.map +0 -1
- package/dist/types/config.js.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/types/provider.js.map +0 -1
- 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
|