badgr-cli 1.1.0 → 1.1.2
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/LICENSE +207 -0
- package/README.md +135 -3
- package/package.json +44 -2
- package/src/api.js +16 -0
- package/src/badgr.js +2 -2
- package/src/commands/batch.js +11 -0
- package/src/commands/comfyui.js +31 -15
- package/src/commands/embed.js +13 -10
- package/src/commands/launch.js +8 -1
- package/src/commands/login.js +75 -20
- package/src/commands/run.js +45 -13
- package/src/commands/sbatch.js +6 -1
- package/src/commands/serve.js +44 -30
- package/src/commands/train.js +8 -12
- package/src/commands/transcribe.js +13 -10
- package/src/envFlag.js +10 -0
- package/src/onboarding.js +8 -1
- package/src/progress.js +48 -0
- package/tests/agent-images.test.js +0 -17
- package/tests/api.test.js +0 -168
- package/tests/artifactDownload.test.js +0 -113
- package/tests/artifacts.test.js +0 -168
- package/tests/batch.test.js +0 -641
- package/tests/browser.test.js +0 -51
- package/tests/capacity.test.js +0 -68
- package/tests/commands.test.js +0 -417
- package/tests/config.test.js +0 -96
- package/tests/connect.test.js +0 -83
- package/tests/detect.test.js +0 -191
- package/tests/down.test.js +0 -150
- package/tests/errors.test.js +0 -130
- package/tests/fallback-timeout.test.js +0 -41
- package/tests/fanout.test.js +0 -124
- package/tests/gpu-doctor-classifiers.test.js +0 -402
- package/tests/gpu-doctor-doctor.test.js +0 -304
- package/tests/gpu-doctor-probe-cache.test.js +0 -110
- package/tests/gpu-doctor-probes.test.js +0 -257
- package/tests/heartbeat.test.js +0 -70
- package/tests/job-progress-poll.test.js +0 -136
- package/tests/launch-command-argv.test.js +0 -93
- package/tests/launch-readiness.test.js +0 -403
- package/tests/launch.test.js +0 -440
- package/tests/onboarding.test.js +0 -134
- package/tests/productized-dry-run.test.js +0 -141
- package/tests/productized-runners.test.js +0 -237
- package/tests/pull.test.js +0 -266
- package/tests/rerun.test.js +0 -94
- package/tests/restart.test.js +0 -88
- package/tests/router.test.js +0 -98
- package/tests/run-lifecycle.test.js +0 -1054
- package/tests/sbatch.test.js +0 -190
- package/tests/secrets.test.js +0 -16
- package/tests/serve-apps.test.js +0 -189
- package/tests/serve-lifecycle.test.js +0 -931
- package/tests/slurm.test.js +0 -77
- package/tests/spec.test.js +0 -201
- package/tests/status.test.js +0 -73
- package/tests/store.test.js +0 -187
- package/tests/task.test.js +0 -109
- package/tests/template.test.js +0 -556
- package/tests/train-lora-dataset.test.js +0 -176
- package/tests/upload.test.js +0 -79
- package/tests/workload-rerun.test.js +0 -56
- package/tests/workload-spec.test.js +0 -180
- package/tests/workload-templates.test.js +0 -865
- package/tests/workload-workspace-paths.test.js +0 -46
|
@@ -1,865 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workload template tests: comfyui, train, transcribe, embed
|
|
3
|
-
*
|
|
4
|
-
* Covers arg parsing, input validation, API dispatch, and receipt recording.
|
|
5
|
-
*/
|
|
6
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
7
|
-
import { parseComfyuiArgs, comfyuiCommand } from '../src/commands/comfyui.js';
|
|
8
|
-
import { parseTrainArgs, detectFramework, findLocalDatasetPaths, trainCommand } from '../src/commands/train.js';
|
|
9
|
-
import { parseTranscribeArgs, resolveAudioInput, transcribeCommand } from '../src/commands/transcribe.js';
|
|
10
|
-
import { parseEmbedArgs, resolveEmbedInput, embedCommand } from '../src/commands/embed.js';
|
|
11
|
-
|
|
12
|
-
// ── Module mocks ──────────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
vi.mock('../src/api.js', () => ({
|
|
15
|
-
callApi: vi.fn(),
|
|
16
|
-
terminateDeployment: vi.fn().mockResolvedValue({}),
|
|
17
|
-
listDeployments: vi.fn().mockResolvedValue({ deployments: [], count: 0 }),
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
vi.mock('../src/store.js', () => ({
|
|
21
|
-
addDeployment: vi.fn(),
|
|
22
|
-
addReceipt: vi.fn(),
|
|
23
|
-
updateReceipt: vi.fn(),
|
|
24
|
-
generateReceiptId: vi.fn(() => 'rcpt-test-001'),
|
|
25
|
-
generateDeploymentId: vi.fn(() => 'dep-test-001'),
|
|
26
|
-
listDeployments: vi.fn(() => []),
|
|
27
|
-
listReceipts: vi.fn(() => []),
|
|
28
|
-
findDeployment: vi.fn(() => null),
|
|
29
|
-
updateDeployment: vi.fn(),
|
|
30
|
-
removeDeployment: vi.fn(),
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
|
-
// Mock callWithFallback directly so tests don't depend on API call ordering.
|
|
34
|
-
vi.mock('../src/fallback.js', async (importOriginal) => {
|
|
35
|
-
const actual = await importOriginal();
|
|
36
|
-
return {
|
|
37
|
-
...actual,
|
|
38
|
-
callWithFallback: vi.fn(),
|
|
39
|
-
};
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
vi.mock('fs', async (importOriginal) => {
|
|
43
|
-
const actual = await importOriginal();
|
|
44
|
-
return {
|
|
45
|
-
...actual,
|
|
46
|
-
readFileSync: vi.fn(),
|
|
47
|
-
existsSync: vi.fn(),
|
|
48
|
-
statSync: vi.fn(),
|
|
49
|
-
};
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
import * as api from '../src/api.js';
|
|
53
|
-
import * as store from '../src/store.js';
|
|
54
|
-
import * as fallback from '../src/fallback.js';
|
|
55
|
-
import * as fs from 'fs';
|
|
56
|
-
|
|
57
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
const config = { apiKey: 'sk-test', baseUrl: 'https://api.test/v1' };
|
|
60
|
-
|
|
61
|
-
const chalk = {
|
|
62
|
-
bold: s => s, dim: s => s, red: s => s,
|
|
63
|
-
yellow: s => s, green: s => s, cyan: s => s,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const ENDPOINT_URL = 'https://dep-test-001.aibadgr.com/v1';
|
|
67
|
-
|
|
68
|
-
function makeServeDep(overrides = {}) {
|
|
69
|
-
return {
|
|
70
|
-
deployment_id: 'dep-test-001',
|
|
71
|
-
status: 'running',
|
|
72
|
-
gpu_type: 'RTX_4090',
|
|
73
|
-
gpu_count: 1,
|
|
74
|
-
cost_per_hour: 0.72,
|
|
75
|
-
provider: 'test-provider',
|
|
76
|
-
receipt_id: 'rcpt-test-001',
|
|
77
|
-
tier: '1',
|
|
78
|
-
endpoint_url: ENDPOINT_URL,
|
|
79
|
-
...overrides,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function makeRunDep(overrides = {}) {
|
|
84
|
-
return {
|
|
85
|
-
deployment_id: 'dep-test-001',
|
|
86
|
-
status: 'queued',
|
|
87
|
-
gpu_type: 'RTX_4090',
|
|
88
|
-
gpu_count: 1,
|
|
89
|
-
cost_per_hour: 0.72,
|
|
90
|
-
provider: 'test-provider',
|
|
91
|
-
receipt_id: 'rcpt-test-001',
|
|
92
|
-
tier: '1',
|
|
93
|
-
...overrides,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ── Setup / teardown ──────────────────────────────────────────────────────────
|
|
98
|
-
|
|
99
|
-
beforeEach(() => {
|
|
100
|
-
vi.useFakeTimers();
|
|
101
|
-
process.exitCode = undefined;
|
|
102
|
-
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
103
|
-
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
104
|
-
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
105
|
-
vi.clearAllMocks();
|
|
106
|
-
store.generateReceiptId.mockReturnValue('rcpt-test-001');
|
|
107
|
-
api.terminateDeployment.mockResolvedValue({});
|
|
108
|
-
api.listDeployments.mockResolvedValue({ deployments: [], count: 0 });
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
afterEach(() => {
|
|
112
|
-
vi.useRealTimers();
|
|
113
|
-
vi.restoreAllMocks();
|
|
114
|
-
process.exitCode = undefined;
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
118
|
-
// parseComfyuiArgs
|
|
119
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
120
|
-
|
|
121
|
-
describe('parseComfyuiArgs', () => {
|
|
122
|
-
it('parses workflow positional after "run" subcommand', () => {
|
|
123
|
-
const { workflow, flags } = parseComfyuiArgs(['run', 'workflow.json']);
|
|
124
|
-
expect(workflow).toBe('workflow.json');
|
|
125
|
-
expect(flags.gpu).toBeUndefined();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('parses workflow without "run" subcommand', () => {
|
|
129
|
-
const { workflow } = parseComfyuiArgs(['workflow.json']);
|
|
130
|
-
expect(workflow).toBe('workflow.json');
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('parses --gpu flag', () => {
|
|
134
|
-
const { flags } = parseComfyuiArgs(['run', 'wf.json', '--gpu', 'L40S']);
|
|
135
|
-
expect(flags.gpu).toBe('L40S');
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('parses --max-cost flag', () => {
|
|
139
|
-
const { flags } = parseComfyuiArgs(['wf.json', '--max-cost', '5']);
|
|
140
|
-
expect(flags.maxCost).toBe(5);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('parses --no-wait flag', () => {
|
|
144
|
-
const { flags } = parseComfyuiArgs(['wf.json', '--no-wait']);
|
|
145
|
-
expect(flags.noWait).toBe(true);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('parses --check-nodes flag', () => {
|
|
149
|
-
const { flags } = parseComfyuiArgs(['wf.json', '--check-nodes', 'CLIPTextEncode,KSampler']);
|
|
150
|
-
expect(flags.checkNodes).toBe('CLIPTextEncode,KSampler');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('parses --persistent flag', () => {
|
|
154
|
-
const { flags } = parseComfyuiArgs(['wf.json', '--persistent']);
|
|
155
|
-
expect(flags.persistent).toBe(true);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('returns null workflow when no positional arg', () => {
|
|
159
|
-
const { workflow } = parseComfyuiArgs(['--gpu', 'RTX_4090']);
|
|
160
|
-
expect(workflow).toBeNull();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('parses --env flag', () => {
|
|
164
|
-
const { flags } = parseComfyuiArgs(['wf.json', '--env', 'FOO=bar', '--max-cost', '5']);
|
|
165
|
-
expect(flags.env).toEqual(['FOO=bar']);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
170
|
-
// comfyuiCommand
|
|
171
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
172
|
-
|
|
173
|
-
describe('comfyuiCommand', () => {
|
|
174
|
-
it('errors when no workflow provided', async () => {
|
|
175
|
-
await comfyuiCommand(config, [], chalk);
|
|
176
|
-
expect(process.exitCode).toBe(1);
|
|
177
|
-
expect(fallback.callWithFallback).not.toHaveBeenCalled();
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('errors when --max-cost and --persistent are both absent', async () => {
|
|
181
|
-
await comfyuiCommand(config, ['run', 'wf.json'], chalk);
|
|
182
|
-
expect(process.exitCode).toBe(1);
|
|
183
|
-
expect(fallback.callWithFallback).not.toHaveBeenCalled();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('errors when workflow file does not exist', async () => {
|
|
187
|
-
fs.existsSync.mockReturnValue(false);
|
|
188
|
-
await comfyuiCommand(config, ['run', 'wf.json', '--max-cost', '5'], chalk);
|
|
189
|
-
expect(process.exitCode).toBe(1);
|
|
190
|
-
expect(fallback.callWithFallback).not.toHaveBeenCalled();
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('errors when workflow JSON is invalid', async () => {
|
|
194
|
-
fs.existsSync.mockReturnValue(true);
|
|
195
|
-
fs.readFileSync.mockReturnValue('not valid json {{{');
|
|
196
|
-
await comfyuiCommand(config, ['wf.json', '--max-cost', '5'], chalk);
|
|
197
|
-
expect(process.exitCode).toBe(1);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('launches ComfyUI and records deployment', async () => {
|
|
201
|
-
const wfContent = JSON.stringify({ '1': { class_type: 'KSampler' } });
|
|
202
|
-
fs.existsSync.mockReturnValue(true);
|
|
203
|
-
fs.readFileSync.mockReturnValue(wfContent);
|
|
204
|
-
fallback.callWithFallback.mockResolvedValue(makeServeDep());
|
|
205
|
-
|
|
206
|
-
global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });
|
|
207
|
-
|
|
208
|
-
const p = comfyuiCommand(config, ['wf.json', '--no-wait', '--max-cost', '5'], chalk);
|
|
209
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
210
|
-
await p;
|
|
211
|
-
|
|
212
|
-
expect(store.addDeployment).toHaveBeenCalledWith(expect.objectContaining({
|
|
213
|
-
id: 'dep-test-001',
|
|
214
|
-
}));
|
|
215
|
-
expect(store.updateReceipt).toHaveBeenCalledWith('rcpt-test-001', expect.objectContaining({
|
|
216
|
-
status: 'starting',
|
|
217
|
-
}));
|
|
218
|
-
expect(process.exitCode).toBeFalsy();
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('passes COMFYUI_WORKFLOW_B64 env var to callWithFallback', async () => {
|
|
222
|
-
const wfContent = JSON.stringify({ '1': { class_type: 'CLIPTextEncode' } });
|
|
223
|
-
fs.existsSync.mockReturnValue(true);
|
|
224
|
-
fs.readFileSync.mockReturnValue(wfContent);
|
|
225
|
-
fallback.callWithFallback.mockResolvedValue(makeServeDep());
|
|
226
|
-
|
|
227
|
-
const p = comfyuiCommand(config, ['wf.json', '--no-wait', '--max-cost', '5'], chalk);
|
|
228
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
229
|
-
await p;
|
|
230
|
-
|
|
231
|
-
expect(fallback.callWithFallback).toHaveBeenCalledWith(
|
|
232
|
-
'/serve',
|
|
233
|
-
expect.anything(),
|
|
234
|
-
expect.any(Function),
|
|
235
|
-
expect.anything(),
|
|
236
|
-
expect.anything(),
|
|
237
|
-
expect.anything(),
|
|
238
|
-
);
|
|
239
|
-
const bodyBuilder = fallback.callWithFallback.mock.calls[0][2];
|
|
240
|
-
const body = bodyBuilder();
|
|
241
|
-
expect(body.env.COMFYUI_WORKFLOW_B64).toBe(Buffer.from(wfContent).toString('base64'));
|
|
242
|
-
expect(body.image).toBe('yanwk/comfyui-boot:cu126-megapak');
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it('skips health check when --no-wait', async () => {
|
|
246
|
-
fs.existsSync.mockReturnValue(true);
|
|
247
|
-
fs.readFileSync.mockReturnValue(JSON.stringify({ '1': {} }));
|
|
248
|
-
fallback.callWithFallback.mockResolvedValue(makeServeDep());
|
|
249
|
-
global.fetch = vi.fn();
|
|
250
|
-
|
|
251
|
-
const p = comfyuiCommand(config, ['wf.json', '--no-wait', '--max-cost', '5'], chalk);
|
|
252
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
253
|
-
await p;
|
|
254
|
-
|
|
255
|
-
expect(global.fetch).not.toHaveBeenCalled();
|
|
256
|
-
expect(store.updateReceipt).toHaveBeenCalledWith('rcpt-test-001', expect.objectContaining({
|
|
257
|
-
status: 'starting',
|
|
258
|
-
}));
|
|
259
|
-
expect(process.exitCode).toBeFalsy();
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it('health-checks /system_stats and records ready state', async () => {
|
|
263
|
-
fs.existsSync.mockReturnValue(true);
|
|
264
|
-
fs.readFileSync.mockReturnValue(JSON.stringify({ '1': {} }));
|
|
265
|
-
fallback.callWithFallback.mockResolvedValue(makeServeDep());
|
|
266
|
-
|
|
267
|
-
api.callApi.mockResolvedValue({ status: 'running' }); // dep status polls
|
|
268
|
-
|
|
269
|
-
const fetchedUrls = [];
|
|
270
|
-
global.fetch = vi.fn().mockImplementation((url) => {
|
|
271
|
-
fetchedUrls.push(url);
|
|
272
|
-
return Promise.resolve({ ok: true, status: 200 });
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
const p = comfyuiCommand(config, ['wf.json', '--max-cost', '5'], chalk);
|
|
276
|
-
await vi.advanceTimersByTimeAsync(5000);
|
|
277
|
-
await p;
|
|
278
|
-
|
|
279
|
-
expect(fetchedUrls.some(u => u.includes('/system_stats'))).toBe(true);
|
|
280
|
-
expect(store.updateReceipt).toHaveBeenCalledWith('rcpt-test-001', expect.objectContaining({
|
|
281
|
-
status: 'ready',
|
|
282
|
-
}));
|
|
283
|
-
expect(process.exitCode).toBeFalsy();
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it('records failed receipt when deployment crashes mid-health-check', async () => {
|
|
287
|
-
fs.existsSync.mockReturnValue(true);
|
|
288
|
-
fs.readFileSync.mockReturnValue(JSON.stringify({ '1': {} }));
|
|
289
|
-
fallback.callWithFallback.mockResolvedValue(makeServeDep());
|
|
290
|
-
|
|
291
|
-
api.callApi.mockResolvedValue({ status: 'failed', error: 'OOM' });
|
|
292
|
-
global.fetch = vi.fn().mockRejectedValue(new Error('Connection refused'));
|
|
293
|
-
|
|
294
|
-
const p = comfyuiCommand(config, ['wf.json', '--max-cost', '5'], chalk);
|
|
295
|
-
await vi.advanceTimersByTimeAsync(10000);
|
|
296
|
-
await p;
|
|
297
|
-
|
|
298
|
-
expect(store.updateReceipt).toHaveBeenCalledWith('rcpt-test-001', expect.objectContaining({
|
|
299
|
-
status: 'failed',
|
|
300
|
-
}));
|
|
301
|
-
expect(process.exitCode).toBe(1);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('prints failure_class/next_action from the backend using the shared vocabulary', async () => {
|
|
305
|
-
fs.existsSync.mockReturnValue(true);
|
|
306
|
-
fs.readFileSync.mockReturnValue(JSON.stringify({ '1': {} }));
|
|
307
|
-
fallback.callWithFallback.mockResolvedValue(makeServeDep());
|
|
308
|
-
|
|
309
|
-
api.callApi.mockResolvedValue({
|
|
310
|
-
status: 'failed',
|
|
311
|
-
error: 'OOM',
|
|
312
|
-
failure_class: 'container_start_failed',
|
|
313
|
-
next_action: 'Check the image and command, then retry.',
|
|
314
|
-
});
|
|
315
|
-
global.fetch = vi.fn().mockRejectedValue(new Error('Connection refused'));
|
|
316
|
-
|
|
317
|
-
const p = comfyuiCommand(config, ['wf.json', '--max-cost', '5'], chalk);
|
|
318
|
-
await vi.advanceTimersByTimeAsync(10000);
|
|
319
|
-
await p;
|
|
320
|
-
|
|
321
|
-
const errLines = console.error.mock.calls.flat().join('\n');
|
|
322
|
-
expect(errLines).toContain('Class: container_start_failed');
|
|
323
|
-
expect(errLines).toContain('Next: Check the image and command, then retry.');
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
328
|
-
// parseTrainArgs + helpers
|
|
329
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
330
|
-
|
|
331
|
-
describe('parseTrainArgs', () => {
|
|
332
|
-
it('parses config file positional', () => {
|
|
333
|
-
const { configFile } = parseTrainArgs(['config.yaml']);
|
|
334
|
-
expect(configFile).toBe('config.yaml');
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('parses --gpu flag', () => {
|
|
338
|
-
const { flags } = parseTrainArgs(['config.yaml', '--gpu', 'A100']);
|
|
339
|
-
expect(flags.gpu).toBe('A100');
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it('parses --max-runtime flag', () => {
|
|
343
|
-
const { flags } = parseTrainArgs(['config.yaml', '--max-runtime', '90']);
|
|
344
|
-
expect(flags.maxRuntime).toBe(90);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it('parses --max-cost flag', () => {
|
|
348
|
-
const { flags } = parseTrainArgs(['config.yaml', '--max-cost', '20']);
|
|
349
|
-
expect(flags.maxCost).toBe(20);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it('parses --framework flag', () => {
|
|
353
|
-
const { flags } = parseTrainArgs(['config.yaml', '--framework', 'unsloth']);
|
|
354
|
-
expect(flags.framework).toBe('unsloth');
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it('parses --detach flag', () => {
|
|
358
|
-
const { flags } = parseTrainArgs(['config.yaml', '--detach']);
|
|
359
|
-
expect(flags.detach).toBe(true);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it('returns null configFile when no positional arg', () => {
|
|
363
|
-
const { configFile } = parseTrainArgs(['--gpu', 'A100']);
|
|
364
|
-
expect(configFile).toBeNull();
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
describe('detectFramework', () => {
|
|
369
|
-
it('detects axolotl from base_model field', () => {
|
|
370
|
-
expect(detectFramework('base_model: meta-llama/Llama-2-7b-hf\nsequence_len: 2048')).toBe('axolotl');
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('detects unsloth from unsloth keyword', () => {
|
|
374
|
-
expect(detectFramework('# uses unsloth for fast training\nmodel: llama')).toBe('unsloth');
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it('detects trl from SFTTrainer', () => {
|
|
378
|
-
expect(detectFramework('trainer: SFTTrainer\nmodel: mistral')).toBe('trl');
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it('detects trl from DPOTrainer', () => {
|
|
382
|
-
expect(detectFramework('trainer: DPOTrainer')).toBe('trl');
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it('falls back to generic for unknown config', () => {
|
|
386
|
-
expect(detectFramework('some_key: some_value\nother: 123')).toBe('generic');
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
describe('findLocalDatasetPaths', () => {
|
|
391
|
-
it('detects local relative paths', () => {
|
|
392
|
-
const cfg = 'datasets:\n - path: ./my_data\n type: completion';
|
|
393
|
-
expect(findLocalDatasetPaths(cfg)).toContain('./my_data');
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it('detects absolute paths', () => {
|
|
397
|
-
const cfg = 'datasets:\n - path: /mnt/data/train.jsonl';
|
|
398
|
-
expect(findLocalDatasetPaths(cfg)).toContain('/mnt/data/train.jsonl');
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it('ignores HuggingFace dataset IDs', () => {
|
|
402
|
-
const cfg = 'datasets:\n - path: tatsu-lab/alpaca\n type: alpaca';
|
|
403
|
-
expect(findLocalDatasetPaths(cfg)).toEqual([]);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('ignores s3:// URIs', () => {
|
|
407
|
-
const cfg = 'datasets:\n - path: s3://my-bucket/data.jsonl';
|
|
408
|
-
expect(findLocalDatasetPaths(cfg)).toEqual([]);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
it('ignores https:// URLs', () => {
|
|
412
|
-
const cfg = 'datasets:\n - path: https://example.com/data.jsonl';
|
|
413
|
-
expect(findLocalDatasetPaths(cfg)).toEqual([]);
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
418
|
-
// trainCommand
|
|
419
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
420
|
-
|
|
421
|
-
describe('trainCommand', () => {
|
|
422
|
-
it('errors when no config file provided', async () => {
|
|
423
|
-
await trainCommand(config, [], chalk);
|
|
424
|
-
expect(process.exitCode).toBe(1);
|
|
425
|
-
expect(fallback.callWithFallback).not.toHaveBeenCalled();
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it('errors when config file does not exist', async () => {
|
|
429
|
-
fs.existsSync.mockReturnValue(false);
|
|
430
|
-
await trainCommand(config, ['config.yaml'], chalk);
|
|
431
|
-
expect(process.exitCode).toBe(1);
|
|
432
|
-
expect(fallback.callWithFallback).not.toHaveBeenCalled();
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it('launches training job and records receipt', async () => {
|
|
436
|
-
const axolotlConfig = 'base_model: meta-llama/Llama-2-7b-hf\nsequence_len: 2048';
|
|
437
|
-
fs.existsSync.mockReturnValue(true);
|
|
438
|
-
fs.readFileSync.mockReturnValue(axolotlConfig);
|
|
439
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
440
|
-
|
|
441
|
-
const p = trainCommand(config, ['config.yaml', '--detach'], chalk);
|
|
442
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
443
|
-
await p;
|
|
444
|
-
|
|
445
|
-
expect(store.addReceipt).toHaveBeenCalledWith(expect.objectContaining({
|
|
446
|
-
action: 'badgr train',
|
|
447
|
-
deploymentId: 'dep-test-001',
|
|
448
|
-
}));
|
|
449
|
-
expect(process.exitCode).toBeFalsy();
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
it('uses axolotl image for axolotl config', async () => {
|
|
453
|
-
const axolotlConfig = 'base_model: meta-llama/Llama-2-7b-hf\nsequence_len: 2048';
|
|
454
|
-
fs.existsSync.mockReturnValue(true);
|
|
455
|
-
fs.readFileSync.mockReturnValue(axolotlConfig);
|
|
456
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
457
|
-
|
|
458
|
-
const p = trainCommand(config, ['config.yaml', '--detach'], chalk);
|
|
459
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
460
|
-
await p;
|
|
461
|
-
|
|
462
|
-
const bodyBuilder = fallback.callWithFallback.mock.calls[0][2];
|
|
463
|
-
const body = bodyBuilder();
|
|
464
|
-
expect(body.image).toBe('winglian/axolotl:main-latest');
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
it('encodes config as TRAIN_CONFIG_B64', async () => {
|
|
468
|
-
const configContent = 'base_model: test-model\n';
|
|
469
|
-
fs.existsSync.mockReturnValue(true);
|
|
470
|
-
fs.readFileSync.mockReturnValue(configContent);
|
|
471
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
472
|
-
|
|
473
|
-
const p = trainCommand(config, ['config.yaml', '--detach'], chalk);
|
|
474
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
475
|
-
await p;
|
|
476
|
-
|
|
477
|
-
const bodyBuilder = fallback.callWithFallback.mock.calls[0][2];
|
|
478
|
-
const body = bodyBuilder();
|
|
479
|
-
expect(body.env.TRAIN_CONFIG_B64).toBe(Buffer.from(configContent).toString('base64'));
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
it('applies default 120min max-runtime', async () => {
|
|
483
|
-
fs.existsSync.mockReturnValue(true);
|
|
484
|
-
fs.readFileSync.mockReturnValue('base_model: test\n');
|
|
485
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
486
|
-
|
|
487
|
-
const p = trainCommand(config, ['config.yaml', '--detach'], chalk);
|
|
488
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
489
|
-
await p;
|
|
490
|
-
|
|
491
|
-
const bodyBuilder = fallback.callWithFallback.mock.calls[0][2];
|
|
492
|
-
const body = bodyBuilder();
|
|
493
|
-
expect(body.max_runtime_seconds).toBe(120 * 60);
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
it('honours explicit --max-runtime override', async () => {
|
|
497
|
-
fs.existsSync.mockReturnValue(true);
|
|
498
|
-
fs.readFileSync.mockReturnValue('base_model: test\n');
|
|
499
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
500
|
-
|
|
501
|
-
const p = trainCommand(config, ['config.yaml', '--max-runtime', '60', '--detach'], chalk);
|
|
502
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
503
|
-
await p;
|
|
504
|
-
|
|
505
|
-
const bodyBuilder = fallback.callWithFallback.mock.calls[0][2];
|
|
506
|
-
const body = bodyBuilder();
|
|
507
|
-
expect(body.max_runtime_seconds).toBe(60 * 60);
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
it('blocks unsloth config instead of running a mismatched command', async () => {
|
|
511
|
-
fs.existsSync.mockReturnValue(true);
|
|
512
|
-
fs.readFileSync.mockReturnValue('# unsloth training config\nmodel: llama\n');
|
|
513
|
-
|
|
514
|
-
await trainCommand(config, ['config.yaml', '--detach'], chalk);
|
|
515
|
-
|
|
516
|
-
expect(process.exitCode).toBe(1);
|
|
517
|
-
expect(fallback.callWithFallback).not.toHaveBeenCalled();
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
it('blocks generic (unrecognized) config instead of guessing a command', async () => {
|
|
521
|
-
fs.existsSync.mockReturnValue(true);
|
|
522
|
-
fs.readFileSync.mockReturnValue('some_key: some_value\nother: 123\n');
|
|
523
|
-
|
|
524
|
-
await trainCommand(config, ['config.yaml', '--detach'], chalk);
|
|
525
|
-
|
|
526
|
-
expect(process.exitCode).toBe(1);
|
|
527
|
-
expect(fallback.callWithFallback).not.toHaveBeenCalled();
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
it('uses the trl CLI command for a trl-detected config', async () => {
|
|
531
|
-
fs.existsSync.mockReturnValue(true);
|
|
532
|
-
fs.readFileSync.mockReturnValue('trainer: SFTTrainer\nmodel: mistral\n');
|
|
533
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
534
|
-
|
|
535
|
-
const p = trainCommand(config, ['config.yaml', '--detach'], chalk);
|
|
536
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
537
|
-
await p;
|
|
538
|
-
|
|
539
|
-
const bodyBuilder = fallback.callWithFallback.mock.calls[0][2];
|
|
540
|
-
const body = bodyBuilder();
|
|
541
|
-
expect(body.image).toBe('huggingface/trl-source:latest');
|
|
542
|
-
expect(body.command[2]).toContain('trl sft --config /tmp/config.yaml');
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
it('uses the axolotl CLI command for an axolotl-detected config', async () => {
|
|
546
|
-
fs.existsSync.mockReturnValue(true);
|
|
547
|
-
fs.readFileSync.mockReturnValue('base_model: meta-llama/Llama-2-7b-hf\nsequence_len: 2048\n');
|
|
548
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
549
|
-
|
|
550
|
-
const p = trainCommand(config, ['config.yaml', '--detach'], chalk);
|
|
551
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
552
|
-
await p;
|
|
553
|
-
|
|
554
|
-
const bodyBuilder = fallback.callWithFallback.mock.calls[0][2];
|
|
555
|
-
const body = bodyBuilder();
|
|
556
|
-
expect(body.command[2]).toContain('axolotl train /tmp/config.yaml');
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
561
|
-
// parseTranscribeArgs + resolveAudioInput
|
|
562
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
563
|
-
|
|
564
|
-
describe('parseTranscribeArgs', () => {
|
|
565
|
-
it('parses input positional', () => {
|
|
566
|
-
const { input } = parseTranscribeArgs(['meeting.mp3']);
|
|
567
|
-
expect(input).toBe('meeting.mp3');
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
it('parses --model flag', () => {
|
|
571
|
-
const { flags } = parseTranscribeArgs(['audio.mp3', '--model', 'medium']);
|
|
572
|
-
expect(flags.model).toBe('medium');
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
it('parses --language flag', () => {
|
|
576
|
-
const { flags } = parseTranscribeArgs(['audio.mp3', '--language', 'fr']);
|
|
577
|
-
expect(flags.language).toBe('fr');
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
it('parses --max-cost flag', () => {
|
|
581
|
-
const { flags } = parseTranscribeArgs(['audio.mp3', '--max-cost', '2']);
|
|
582
|
-
expect(flags.maxCost).toBe(2);
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
it('returns null input for empty args', () => {
|
|
586
|
-
const { input } = parseTranscribeArgs([]);
|
|
587
|
-
expect(input).toBeNull();
|
|
588
|
-
});
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
describe('resolveAudioInput', () => {
|
|
592
|
-
it('accepts https:// URL', () => {
|
|
593
|
-
const r = resolveAudioInput('https://example.com/audio.mp3');
|
|
594
|
-
expect(r.audioUrl).toBe('https://example.com/audio.mp3');
|
|
595
|
-
expect(r.audioB64).toBeNull();
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
it('accepts s3:// URI', () => {
|
|
599
|
-
const r = resolveAudioInput('s3://my-bucket/audio.mp3');
|
|
600
|
-
expect(r.audioUrl).toBe('s3://my-bucket/audio.mp3');
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it('returns error for missing local file', () => {
|
|
604
|
-
fs.existsSync.mockReturnValue(false);
|
|
605
|
-
const r = resolveAudioInput('missing.mp3');
|
|
606
|
-
expect(r.error).toMatch(/not found/i);
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
it('returns error for oversized local file', () => {
|
|
610
|
-
fs.existsSync.mockReturnValue(true);
|
|
611
|
-
fs.statSync.mockReturnValue({ size: 60 * 1024 * 1024 });
|
|
612
|
-
const r = resolveAudioInput('big.mp3');
|
|
613
|
-
expect(r.error).toMatch(/too large/i);
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
it('base64-encodes small local file', () => {
|
|
617
|
-
fs.existsSync.mockReturnValue(true);
|
|
618
|
-
fs.statSync.mockReturnValue({ size: 1024 });
|
|
619
|
-
fs.readFileSync.mockReturnValue(Buffer.from('fake-audio-data'));
|
|
620
|
-
const r = resolveAudioInput('small.mp3');
|
|
621
|
-
expect(r.audioB64).toBeTruthy();
|
|
622
|
-
expect(r.audioUrl).toBeNull();
|
|
623
|
-
});
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
627
|
-
// transcribeCommand
|
|
628
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
629
|
-
|
|
630
|
-
describe('transcribeCommand', () => {
|
|
631
|
-
it('errors when no input provided', async () => {
|
|
632
|
-
await transcribeCommand(config, [], chalk);
|
|
633
|
-
expect(process.exitCode).toBe(1);
|
|
634
|
-
expect(fallback.callWithFallback).not.toHaveBeenCalled();
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
it('errors for missing local file', async () => {
|
|
638
|
-
fs.existsSync.mockReturnValue(false);
|
|
639
|
-
await transcribeCommand(config, ['missing.mp3'], chalk);
|
|
640
|
-
expect(process.exitCode).toBe(1);
|
|
641
|
-
expect(fallback.callWithFallback).not.toHaveBeenCalled();
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
it('launches transcription job for URL input', async () => {
|
|
645
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
646
|
-
|
|
647
|
-
const p = transcribeCommand(
|
|
648
|
-
config,
|
|
649
|
-
['https://example.com/audio.mp3', '--detach'],
|
|
650
|
-
chalk,
|
|
651
|
-
);
|
|
652
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
653
|
-
await p;
|
|
654
|
-
|
|
655
|
-
expect(store.addReceipt).toHaveBeenCalledWith(expect.objectContaining({
|
|
656
|
-
action: 'badgr transcribe',
|
|
657
|
-
deploymentId: 'dep-test-001',
|
|
658
|
-
}));
|
|
659
|
-
expect(process.exitCode).toBeFalsy();
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
it('passes AUDIO_URL env var and uses Whisper image', async () => {
|
|
663
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
664
|
-
|
|
665
|
-
const p = transcribeCommand(
|
|
666
|
-
config,
|
|
667
|
-
['s3://bucket/audio.mp3', '--detach'],
|
|
668
|
-
chalk,
|
|
669
|
-
);
|
|
670
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
671
|
-
await p;
|
|
672
|
-
|
|
673
|
-
const bodyBuilder = fallback.callWithFallback.mock.calls[0][2];
|
|
674
|
-
const body = bodyBuilder();
|
|
675
|
-
expect(body.env.AUDIO_URL).toBe('s3://bucket/audio.mp3');
|
|
676
|
-
expect(body.image).toBe('fedirz/faster-whisper-server:latest-cuda');
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
it('passes AUDIO_B64 env var for small local file', async () => {
|
|
680
|
-
fs.existsSync.mockReturnValue(true);
|
|
681
|
-
fs.statSync.mockReturnValue({ size: 1024 });
|
|
682
|
-
fs.readFileSync.mockReturnValue(Buffer.from('audio-data'));
|
|
683
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
684
|
-
|
|
685
|
-
const p = transcribeCommand(config, ['recording.mp3', '--detach'], chalk);
|
|
686
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
687
|
-
await p;
|
|
688
|
-
|
|
689
|
-
const bodyBuilder = fallback.callWithFallback.mock.calls[0][2];
|
|
690
|
-
const body = bodyBuilder();
|
|
691
|
-
expect(body.env.AUDIO_B64).toBeTruthy();
|
|
692
|
-
expect(body.env.AUDIO_URL).toBeUndefined();
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
it('defaults to large-v3 Whisper model', async () => {
|
|
696
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
697
|
-
|
|
698
|
-
const p = transcribeCommand(
|
|
699
|
-
config,
|
|
700
|
-
['https://example.com/audio.mp3', '--detach'],
|
|
701
|
-
chalk,
|
|
702
|
-
);
|
|
703
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
704
|
-
await p;
|
|
705
|
-
|
|
706
|
-
const bodyBuilder = fallback.callWithFallback.mock.calls[0][2];
|
|
707
|
-
const body = bodyBuilder();
|
|
708
|
-
expect(body.env.WHISPER_MODEL).toBe('large-v3');
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
it('honours --model override', async () => {
|
|
712
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
713
|
-
|
|
714
|
-
const p = transcribeCommand(
|
|
715
|
-
config,
|
|
716
|
-
['https://example.com/audio.mp3', '--model', 'medium', '--detach'],
|
|
717
|
-
chalk,
|
|
718
|
-
);
|
|
719
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
720
|
-
await p;
|
|
721
|
-
|
|
722
|
-
const bodyBuilder = fallback.callWithFallback.mock.calls[0][2];
|
|
723
|
-
const body = bodyBuilder();
|
|
724
|
-
expect(body.env.WHISPER_MODEL).toBe('medium');
|
|
725
|
-
});
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
729
|
-
// parseEmbedArgs + resolveEmbedInput
|
|
730
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
731
|
-
|
|
732
|
-
describe('parseEmbedArgs', () => {
|
|
733
|
-
it('parses model + input positional args', () => {
|
|
734
|
-
const { model, input } = parseEmbedArgs(['BAAI/bge-large-en-v1.5', 'docs.txt']);
|
|
735
|
-
expect(model).toBe('BAAI/bge-large-en-v1.5');
|
|
736
|
-
expect(input).toBe('docs.txt');
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
it('uses default model when only input is given', () => {
|
|
740
|
-
const { model, input } = parseEmbedArgs(['docs.txt']);
|
|
741
|
-
expect(model).toBe('BAAI/bge-large-en-v1.5');
|
|
742
|
-
expect(input).toBe('docs.txt');
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
it('--model flag takes precedence when only one positional', () => {
|
|
746
|
-
const { model, input } = parseEmbedArgs(['--model', 'my-model', 'docs.txt']);
|
|
747
|
-
expect(model).toBe('my-model');
|
|
748
|
-
expect(input).toBe('docs.txt');
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
it('parses --max-cost flag', () => {
|
|
752
|
-
const { flags } = parseEmbedArgs(['BAAI/bge-large-en-v1.5', 'docs.txt', '--max-cost', '3']);
|
|
753
|
-
expect(flags.maxCost).toBe(3);
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
it('returns null input for empty args', () => {
|
|
757
|
-
const { input } = parseEmbedArgs([]);
|
|
758
|
-
expect(input).toBeNull();
|
|
759
|
-
});
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
describe('resolveEmbedInput', () => {
|
|
763
|
-
it('accepts https:// URL', () => {
|
|
764
|
-
const r = resolveEmbedInput('https://example.com/docs.txt');
|
|
765
|
-
expect(r.inputUrl).toBe('https://example.com/docs.txt');
|
|
766
|
-
expect(r.inputB64).toBeNull();
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
it('accepts s3:// URI', () => {
|
|
770
|
-
const r = resolveEmbedInput('s3://bucket/corpus.jsonl');
|
|
771
|
-
expect(r.inputUrl).toBe('s3://bucket/corpus.jsonl');
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
it('returns error for missing local file', () => {
|
|
775
|
-
fs.existsSync.mockReturnValue(false);
|
|
776
|
-
const r = resolveEmbedInput('missing.txt');
|
|
777
|
-
expect(r.error).toMatch(/not found/i);
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
it('returns error for oversized file (> 10 MB)', () => {
|
|
781
|
-
fs.existsSync.mockReturnValue(true);
|
|
782
|
-
fs.statSync.mockReturnValue({ size: 15 * 1024 * 1024 });
|
|
783
|
-
const r = resolveEmbedInput('large.txt');
|
|
784
|
-
expect(r.error).toMatch(/too large/i);
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
it('base64-encodes small local file', () => {
|
|
788
|
-
fs.existsSync.mockReturnValue(true);
|
|
789
|
-
fs.statSync.mockReturnValue({ size: 512 });
|
|
790
|
-
fs.readFileSync.mockReturnValue(Buffer.from('line1\nline2\n'));
|
|
791
|
-
const r = resolveEmbedInput('docs.txt');
|
|
792
|
-
expect(r.inputB64).toBeTruthy();
|
|
793
|
-
expect(r.inputUrl).toBeNull();
|
|
794
|
-
});
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
798
|
-
// embedCommand
|
|
799
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
800
|
-
|
|
801
|
-
describe('embedCommand', () => {
|
|
802
|
-
it('errors when no input provided', async () => {
|
|
803
|
-
await embedCommand(config, [], chalk);
|
|
804
|
-
expect(process.exitCode).toBe(1);
|
|
805
|
-
expect(fallback.callWithFallback).not.toHaveBeenCalled();
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
it('errors for missing local file', async () => {
|
|
809
|
-
fs.existsSync.mockReturnValue(false);
|
|
810
|
-
await embedCommand(config, ['BAAI/bge-large-en-v1.5', 'missing.txt'], chalk);
|
|
811
|
-
expect(process.exitCode).toBe(1);
|
|
812
|
-
expect(fallback.callWithFallback).not.toHaveBeenCalled();
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
it('launches embeddings job for URL input', async () => {
|
|
816
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
817
|
-
|
|
818
|
-
const p = embedCommand(
|
|
819
|
-
config,
|
|
820
|
-
['BAAI/bge-large-en-v1.5', 'https://example.com/docs.txt', '--detach'],
|
|
821
|
-
chalk,
|
|
822
|
-
);
|
|
823
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
824
|
-
await p;
|
|
825
|
-
|
|
826
|
-
expect(store.addReceipt).toHaveBeenCalledWith(expect.objectContaining({
|
|
827
|
-
action: 'badgr embed',
|
|
828
|
-
deploymentId: 'dep-test-001',
|
|
829
|
-
}));
|
|
830
|
-
expect(process.exitCode).toBeFalsy();
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
it('uses vLLM image with VLLM_TASK=embed', async () => {
|
|
834
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
835
|
-
|
|
836
|
-
const p = embedCommand(
|
|
837
|
-
config,
|
|
838
|
-
['BAAI/bge-large-en-v1.5', 's3://bucket/docs.jsonl', '--detach'],
|
|
839
|
-
chalk,
|
|
840
|
-
);
|
|
841
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
842
|
-
await p;
|
|
843
|
-
|
|
844
|
-
const bodyBuilder = fallback.callWithFallback.mock.calls[0][2];
|
|
845
|
-
const body = bodyBuilder();
|
|
846
|
-
expect(body.image).toBe('vllm/vllm-openai:latest');
|
|
847
|
-
expect(body.env.VLLM_TASK).toBe('embed');
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
it('passes model name in env', async () => {
|
|
851
|
-
fallback.callWithFallback.mockResolvedValue(makeRunDep());
|
|
852
|
-
|
|
853
|
-
const p = embedCommand(
|
|
854
|
-
config,
|
|
855
|
-
['my-org/my-embed-model', 's3://bucket/docs.jsonl', '--detach'],
|
|
856
|
-
chalk,
|
|
857
|
-
);
|
|
858
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
859
|
-
await p;
|
|
860
|
-
|
|
861
|
-
const bodyBuilder = fallback.callWithFallback.mock.calls[0][2];
|
|
862
|
-
const body = bodyBuilder();
|
|
863
|
-
expect(body.env.EMBED_MODEL).toBe('my-org/my-embed-model');
|
|
864
|
-
});
|
|
865
|
-
});
|