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.
Files changed (66) hide show
  1. package/LICENSE +207 -0
  2. package/README.md +135 -3
  3. package/package.json +44 -2
  4. package/src/api.js +16 -0
  5. package/src/badgr.js +2 -2
  6. package/src/commands/batch.js +11 -0
  7. package/src/commands/comfyui.js +31 -15
  8. package/src/commands/embed.js +13 -10
  9. package/src/commands/launch.js +8 -1
  10. package/src/commands/login.js +75 -20
  11. package/src/commands/run.js +45 -13
  12. package/src/commands/sbatch.js +6 -1
  13. package/src/commands/serve.js +44 -30
  14. package/src/commands/train.js +8 -12
  15. package/src/commands/transcribe.js +13 -10
  16. package/src/envFlag.js +10 -0
  17. package/src/onboarding.js +8 -1
  18. package/src/progress.js +48 -0
  19. package/tests/agent-images.test.js +0 -17
  20. package/tests/api.test.js +0 -168
  21. package/tests/artifactDownload.test.js +0 -113
  22. package/tests/artifacts.test.js +0 -168
  23. package/tests/batch.test.js +0 -641
  24. package/tests/browser.test.js +0 -51
  25. package/tests/capacity.test.js +0 -68
  26. package/tests/commands.test.js +0 -417
  27. package/tests/config.test.js +0 -96
  28. package/tests/connect.test.js +0 -83
  29. package/tests/detect.test.js +0 -191
  30. package/tests/down.test.js +0 -150
  31. package/tests/errors.test.js +0 -130
  32. package/tests/fallback-timeout.test.js +0 -41
  33. package/tests/fanout.test.js +0 -124
  34. package/tests/gpu-doctor-classifiers.test.js +0 -402
  35. package/tests/gpu-doctor-doctor.test.js +0 -304
  36. package/tests/gpu-doctor-probe-cache.test.js +0 -110
  37. package/tests/gpu-doctor-probes.test.js +0 -257
  38. package/tests/heartbeat.test.js +0 -70
  39. package/tests/job-progress-poll.test.js +0 -136
  40. package/tests/launch-command-argv.test.js +0 -93
  41. package/tests/launch-readiness.test.js +0 -403
  42. package/tests/launch.test.js +0 -440
  43. package/tests/onboarding.test.js +0 -134
  44. package/tests/productized-dry-run.test.js +0 -141
  45. package/tests/productized-runners.test.js +0 -237
  46. package/tests/pull.test.js +0 -266
  47. package/tests/rerun.test.js +0 -94
  48. package/tests/restart.test.js +0 -88
  49. package/tests/router.test.js +0 -98
  50. package/tests/run-lifecycle.test.js +0 -1054
  51. package/tests/sbatch.test.js +0 -190
  52. package/tests/secrets.test.js +0 -16
  53. package/tests/serve-apps.test.js +0 -189
  54. package/tests/serve-lifecycle.test.js +0 -931
  55. package/tests/slurm.test.js +0 -77
  56. package/tests/spec.test.js +0 -201
  57. package/tests/status.test.js +0 -73
  58. package/tests/store.test.js +0 -187
  59. package/tests/task.test.js +0 -109
  60. package/tests/template.test.js +0 -556
  61. package/tests/train-lora-dataset.test.js +0 -176
  62. package/tests/upload.test.js +0 -79
  63. package/tests/workload-rerun.test.js +0 -56
  64. package/tests/workload-spec.test.js +0 -180
  65. package/tests/workload-templates.test.js +0 -865
  66. package/tests/workload-workspace-paths.test.js +0 -46
@@ -1,556 +0,0 @@
1
- /**
2
- * Template catalog + dispatch tests
3
- *
4
- * Covers:
5
- * - Catalog integrity: all 10 templates, required fields, uniqueness
6
- * - buildTemplateFlags: correct flags for each template type
7
- * - parseTemplateOverrides: flag parsing, user overrides win
8
- * - badgr serve template <name>: dispatches via serveCommand (mocked API)
9
- * - badgr run template <name>: dispatches via runCommand (mocked API)
10
- * - Type-mismatch routing guard (job via serve, endpoint via run)
11
- * - Unknown template guard
12
- */
13
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
14
- import {
15
- TEMPLATES,
16
- TEMPLATE_MAP,
17
- buildTemplateFlags,
18
- parseTemplateOverrides,
19
- } from '../src/catalog.js';
20
- import { serveCommand } from '../src/commands/serve.js';
21
- import { runCommand } from '../src/commands/run.js';
22
-
23
- // ── Module mocks ──────────────────────────────────────────────────────────────
24
-
25
- vi.mock('../src/api.js', () => ({
26
- callApi: vi.fn(),
27
- terminateDeployment: vi.fn().mockResolvedValue({}),
28
- listDeployments: vi.fn().mockResolvedValue({ deployments: [], count: 0 }),
29
- }));
30
-
31
- vi.mock('../src/store.js', () => ({
32
- selectedComputeFromDeployment: (dep) => ({
33
- gpu: dep.gpu_type ?? null,
34
- gpuCount: dep.gpu_count ?? null,
35
- vcpus: dep.selected_vcpus ?? null,
36
- ramGb: dep.selected_ram_gb ?? null,
37
- vramGb: dep.selected_vram_gb ?? null,
38
- }),
39
- addDeployment: vi.fn(),
40
- addReceipt: vi.fn(),
41
- updateReceipt: vi.fn(),
42
- generateReceiptId: vi.fn(() => 'rcpt-tmpl-001'),
43
- generateDeploymentId: vi.fn(() => 'dep-tmpl-001'),
44
- listDeployments: vi.fn(() => []),
45
- listReceipts: vi.fn(() => []),
46
- findDeployment: vi.fn(() => null),
47
- updateDeployment: vi.fn(),
48
- removeDeployment: vi.fn(),
49
- }));
50
-
51
- import * as api from '../src/api.js';
52
- import * as store from '../src/store.js';
53
-
54
- // ── Shared helpers ────────────────────────────────────────────────────────────
55
-
56
- const config = { apiKey: 'sk-test', baseUrl: 'https://api.test/v1' };
57
-
58
- const chalk = {
59
- bold: s => s, dim: s => s, red: s => s,
60
- yellow: s => s, green: s => s, cyan: s => s,
61
- };
62
-
63
- function makeServeDep(overrides = {}) {
64
- return {
65
- deployment_id: 'dep-tmpl-001',
66
- status: 'running',
67
- gpu_type: 'RTX_4090',
68
- gpu_count: 1,
69
- cost_per_hour: 0.50,
70
- provider: 'runpod',
71
- receipt_id: 'rcpt-tmpl-001',
72
- tier: '1',
73
- endpoint_url: 'https://dep-tmpl-001.aibadgr.com',
74
- ...overrides,
75
- };
76
- }
77
-
78
- function makeRunDep(overrides = {}) {
79
- return {
80
- deployment_id: 'dep-tmpl-001',
81
- status: 'running',
82
- gpu_type: 'A100',
83
- gpu_count: 1,
84
- cost_per_hour: 2.50,
85
- provider: 'runpod',
86
- receipt_id: 'rcpt-tmpl-001',
87
- tier: '1',
88
- ...overrides,
89
- };
90
- }
91
-
92
- beforeEach(() => {
93
- vi.useFakeTimers();
94
- process.exitCode = undefined;
95
- vi.spyOn(console, 'log').mockImplementation(() => {});
96
- vi.spyOn(console, 'error').mockImplementation(() => {});
97
- vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
98
- vi.clearAllMocks();
99
- store.generateReceiptId.mockReturnValue('rcpt-tmpl-001');
100
- api.terminateDeployment.mockResolvedValue({});
101
- });
102
-
103
- afterEach(() => {
104
- vi.useRealTimers();
105
- vi.restoreAllMocks();
106
- process.exitCode = undefined;
107
- });
108
-
109
- // ─────────────────────────────────────────────────────────────────────────────
110
- // 1. Catalog integrity
111
- // ─────────────────────────────────────────────────────────────────────────────
112
-
113
- describe('TEMPLATES catalog', () => {
114
- const EXPECTED_NAMES = [
115
- 'comfyui', 'axolotl', 'batch-inference', 'unsloth', 'vllm', 'openwebui', 'llama-cpp',
116
- 'invokeai', 'kohya-ss', 'text-gen-webui', 'sglang', 'tgi',
117
- 'auto1111', 'forge', 'nerfstudio', 'openfold', 'blender-render',
118
- 'openmm', 'gromacs', 'lammps', 'diffusers', 'torchtune',
119
- ];
120
-
121
- it('contains exactly 22 templates', () => {
122
- expect(TEMPLATES).toHaveLength(22);
123
- });
124
-
125
- it('contains all expected template names', () => {
126
- const names = TEMPLATES.map(t => t.name);
127
- for (const name of EXPECTED_NAMES) {
128
- expect(names, `missing: ${name}`).toContain(name);
129
- }
130
- });
131
-
132
- it('template names are unique', () => {
133
- const names = TEMPLATES.map(t => t.name);
134
- expect(new Set(names).size).toBe(names.length);
135
- });
136
-
137
- it('every template has required fields', () => {
138
- for (const t of TEMPLATES) {
139
- expect(t.name, `${t.name}: name`).toBeTruthy();
140
- expect(t.title, `${t.name}: title`).toBeTruthy();
141
- expect(t.description, `${t.name}: description`).toBeTruthy();
142
- expect(['endpoint', 'job'], `${t.name}: type`).toContain(t.type);
143
- expect(t.image, `${t.name}: image`).toBeTruthy();
144
- expect(t.gpu, `${t.name}: gpu`).toBeTruthy();
145
- expect(t.gpu_count, `${t.name}: gpu_count`).toBeGreaterThan(0);
146
- expect(t.min_vram_gb, `${t.name}: min_vram_gb`).toBeGreaterThan(0);
147
- }
148
- });
149
-
150
- it('endpoint templates have port and health_path', () => {
151
- for (const t of TEMPLATES.filter(t => t.type === 'endpoint')) {
152
- expect(t.port, `${t.name}: port`).toBeGreaterThan(0);
153
- expect(t.health_path, `${t.name}: health_path`).toBeTruthy();
154
- }
155
- });
156
-
157
- it('job templates do not have a port', () => {
158
- for (const t of TEMPLATES.filter(t => t.type === 'job')) {
159
- expect(t.port, `${t.name}: port should be absent`).toBeFalsy();
160
- }
161
- });
162
-
163
- it('no env value is undefined', () => {
164
- for (const t of TEMPLATES) {
165
- for (const [k, v] of Object.entries(t.env ?? {})) {
166
- expect(v, `${t.name}.env.${k}`).not.toBeUndefined();
167
- }
168
- }
169
- });
170
-
171
- it('vLLM requires ≥ 24 GB VRAM', () => {
172
- expect(TEMPLATE_MAP['vllm'].min_vram_gb).toBeGreaterThanOrEqual(24);
173
- });
174
-
175
- it('SGLang requires ≥ 40 GB VRAM', () => {
176
- expect(TEMPLATE_MAP['sglang'].min_vram_gb).toBeGreaterThanOrEqual(40);
177
- });
178
-
179
- it('job templates use high-VRAM GPUs', () => {
180
- const highVram = ['A100', 'H100', 'L40S', 'A6000', 'RTX_4090'];
181
- for (const t of TEMPLATES.filter(t => t.type === 'job')) {
182
- expect(highVram, `${t.name}: gpu`).toContain(t.gpu);
183
- }
184
- });
185
-
186
- it('TEMPLATE_MAP keys match template names', () => {
187
- for (const [k, t] of Object.entries(TEMPLATE_MAP)) {
188
- expect(k).toBe(t.name);
189
- }
190
- expect(Object.keys(TEMPLATE_MAP)).toHaveLength(22);
191
- });
192
- });
193
-
194
- // ─────────────────────────────────────────────────────────────────────────────
195
- // 2. buildTemplateFlags
196
- // ─────────────────────────────────────────────────────────────────────────────
197
-
198
- describe('buildTemplateFlags', () => {
199
- it('produces --image and --gpu for every template', () => {
200
- for (const t of TEMPLATES) {
201
- const flags = buildTemplateFlags(t, {});
202
- expect(flags).toContain('--image');
203
- expect(flags).toContain(t.image);
204
- expect(flags).toContain('--gpu');
205
- expect(flags).toContain(t.gpu);
206
- }
207
- });
208
-
209
- it('endpoint templates include --health-path', () => {
210
- for (const t of TEMPLATES.filter(t => t.type === 'endpoint')) {
211
- const flags = buildTemplateFlags(t, {});
212
- expect(flags, `${t.name}: --health-path`).toContain('--health-path');
213
- expect(flags, `${t.name}: health_path value`).toContain(t.health_path);
214
- }
215
- });
216
-
217
- it('job templates do not include --health-path', () => {
218
- for (const t of TEMPLATES.filter(t => t.type === 'job')) {
219
- const flags = buildTemplateFlags(t, {});
220
- expect(flags, `${t.name}: no --health-path`).not.toContain('--health-path');
221
- }
222
- });
223
-
224
- it('strips placeholder env values (those starting with <)', () => {
225
- for (const t of TEMPLATES) {
226
- const flags = buildTemplateFlags(t, {});
227
- const envValues = flags
228
- .filter((_, i) => i > 0 && flags[i - 1] === '--env')
229
- .map(kv => kv.split('=').slice(1).join('='));
230
- for (const v of envValues) {
231
- expect(v, `${t.name}: placeholder passed through`).not.toMatch(/^</);
232
- }
233
- }
234
- });
235
-
236
- it('vllm includes MODEL, MAX_MODEL_LEN, TENSOR_PARALLEL_SIZE env vars', () => {
237
- const flags = buildTemplateFlags(TEMPLATE_MAP['vllm'], {});
238
- expect(flags).toContain('MODEL=meta-llama/Llama-3.1-8B-Instruct');
239
- expect(flags.join(' ')).toContain('MAX_MODEL_LEN=8192');
240
- expect(flags.join(' ')).toContain('TENSOR_PARALLEL_SIZE=1');
241
- });
242
-
243
- it('llama-cpp includes LLAMA_ARG_HF_REPO and LLAMA_ARG_HF_FILE', () => {
244
- const flags = buildTemplateFlags(TEMPLATE_MAP['llama-cpp'], {});
245
- expect(flags.join(' ')).toContain('LLAMA_ARG_HF_REPO=');
246
- expect(flags.join(' ')).toContain('LLAMA_ARG_HF_FILE=');
247
- });
248
-
249
- it('user --gpu override replaces template default', () => {
250
- const flags = buildTemplateFlags(TEMPLATE_MAP['vllm'], { gpu: 'A100' });
251
- const gpuIdx = flags.indexOf('--gpu');
252
- expect(flags[gpuIdx + 1]).toBe('A100');
253
- });
254
-
255
- it('user --env override replaces template env default', () => {
256
- const flags = buildTemplateFlags(TEMPLATE_MAP['vllm'], {
257
- env: { MODEL: 'mistralai/Mistral-7B-v0.1' },
258
- });
259
- expect(flags).toContain('MODEL=mistralai/Mistral-7B-v0.1');
260
- expect(flags).not.toContain('MODEL=meta-llama/Llama-3.1-8B-Instruct');
261
- });
262
-
263
- it('--max-cost is included when supplied in overrides', () => {
264
- const flags = buildTemplateFlags(TEMPLATE_MAP['comfyui'], { maxCost: 5 });
265
- expect(flags).toContain('--max-cost');
266
- expect(flags).toContain('5');
267
- });
268
-
269
- it('--count is included only when > 1', () => {
270
- const single = buildTemplateFlags(TEMPLATE_MAP['vllm'], {});
271
- expect(single).not.toContain('--count');
272
-
273
- const multi = buildTemplateFlags(TEMPLATE_MAP['vllm'], { count: 2 });
274
- expect(multi).toContain('--count');
275
- expect(multi).toContain('2');
276
- });
277
-
278
- it('--persistent flag is forwarded', () => {
279
- const flags = buildTemplateFlags(TEMPLATE_MAP['comfyui'], { persistent: true });
280
- expect(flags).toContain('--persistent');
281
- });
282
- });
283
-
284
- // ─────────────────────────────────────────────────────────────────────────────
285
- // 3. parseTemplateOverrides
286
- // ─────────────────────────────────────────────────────────────────────────────
287
-
288
- describe('parseTemplateOverrides', () => {
289
- it('parses --max-cost', () => {
290
- const o = parseTemplateOverrides(['--max-cost', '10']);
291
- expect(o.maxCost).toBe(10);
292
- });
293
-
294
- it('parses --gpu', () => {
295
- const o = parseTemplateOverrides(['--gpu', 'A100']);
296
- expect(o.gpu).toBe('A100');
297
- });
298
-
299
- it('parses --max-runtime', () => {
300
- const o = parseTemplateOverrides(['--max-runtime', '120']);
301
- expect(o.maxRuntime).toBe(120);
302
- });
303
-
304
- it('parses multiple --env flags', () => {
305
- const o = parseTemplateOverrides(['--env', 'MODEL=mistral', '--env', 'HF_TOKEN=abc123']);
306
- expect(o.env.MODEL).toBe('mistral');
307
- expect(o.env.HF_TOKEN).toBe('abc123');
308
- });
309
-
310
- it('parses --persistent', () => {
311
- const o = parseTemplateOverrides(['--persistent']);
312
- expect(o.persistent).toBe(true);
313
- });
314
-
315
- it('parses --no-wait', () => {
316
- const o = parseTemplateOverrides(['--no-wait']);
317
- expect(o.noWait).toBe(true);
318
- });
319
-
320
- it('ignores unknown flags without throwing', () => {
321
- expect(() => parseTemplateOverrides(['--unknown-flag', 'value'])).not.toThrow();
322
- });
323
-
324
- it('returns empty env object when no --env flags given', () => {
325
- const o = parseTemplateOverrides(['--max-cost', '5']);
326
- expect(o.env).toEqual({});
327
- });
328
- });
329
-
330
- // ─────────────────────────────────────────────────────────────────────────────
331
- // 4. badgr serve template <name> — endpoint dispatch
332
- // ─────────────────────────────────────────────────────────────────────────────
333
-
334
- // Sequence for serveCommand:
335
- // callApi('/serve', ...) → dep (via callWithFallback → callApi)
336
- // callApi('/deployments/<id>', ...) → { status: 'running' } (pre-health check)
337
- // callApi('/deployments/<id>', ...) → { status: 'running', endpoint_ready: true } (waitForEndpoint poll)
338
- //
339
- // Readiness comes entirely from Badgr's own deployment status — the CLI never
340
- // fetches the pod/RunPod-proxy endpoint directly.
341
-
342
- function setupServe(depOverrides = {}, readyOverrides = {}) {
343
- api.callApi
344
- .mockResolvedValueOnce(makeServeDep(depOverrides)) // POST /serve
345
- .mockResolvedValueOnce({ status: 'running' }) // pre-health dep status
346
- .mockResolvedValueOnce({ status: 'running', endpoint_ready: true, ...readyOverrides }); // waitForEndpoint poll
347
- }
348
-
349
- describe('badgr serve template <name>', () => {
350
- it('vllm: dispatches with correct image, max_cost_usd, and readiness on /v1/models', async () => {
351
- setupServe({}, { health_path: '/v1/models' });
352
- const p = serveCommand(config, ['template', 'vllm', '--max-cost', '5'], chalk);
353
- await vi.advanceTimersByTimeAsync(5000);
354
- await p;
355
-
356
- const [route, opts] = api.callApi.mock.calls[0];
357
- expect(route).toBe('/serve');
358
- expect(opts.body.image).toBe('vllm/vllm-openai:latest');
359
- expect(opts.body.max_cost_usd).toBe(5);
360
- expect(process.exitCode).toBeFalsy();
361
- });
362
-
363
- it('comfyui: dispatches with ComfyUI image and readiness on /system_stats', async () => {
364
- setupServe({}, { health_path: '/system_stats' });
365
- const p = serveCommand(config, ['template', 'comfyui', '--max-cost', '3'], chalk);
366
- await vi.advanceTimersByTimeAsync(5000);
367
- await p;
368
-
369
- const [, opts] = api.callApi.mock.calls[0];
370
- expect(opts.body.image).toBe('yanwk/comfyui-boot:cu126-megapak');
371
- expect(process.exitCode).toBeFalsy();
372
- });
373
-
374
- it('sglang: dispatches with A100 GPU', async () => {
375
- setupServe({ gpu_type: 'A100' });
376
- const p = serveCommand(config, ['template', 'sglang', '--max-cost', '10'], chalk);
377
- await vi.advanceTimersByTimeAsync(5000);
378
- await p;
379
-
380
- const [, opts] = api.callApi.mock.calls[0];
381
- expect(opts.body.image).toBe('lmsysorg/sglang:latest');
382
- expect(opts.body.gpu).toBe('A100');
383
- expect(process.exitCode).toBeFalsy();
384
- });
385
-
386
- it('tgi: dispatches with TGI image', async () => {
387
- setupServe();
388
- const p = serveCommand(config, ['template', 'tgi', '--max-cost', '5'], chalk);
389
- await vi.advanceTimersByTimeAsync(5000);
390
- await p;
391
-
392
- const [, opts] = api.callApi.mock.calls[0];
393
- expect(opts.body.image).toBe('ghcr.io/huggingface/text-generation-inference:latest');
394
- expect(process.exitCode).toBeFalsy();
395
- });
396
-
397
- it('llama-cpp: dispatches with llama-cpp image', async () => {
398
- setupServe();
399
- const p = serveCommand(config, ['template', 'llama-cpp', '--max-cost', '3'], chalk);
400
- await vi.advanceTimersByTimeAsync(5000);
401
- await p;
402
-
403
- const [, opts] = api.callApi.mock.calls[0];
404
- expect(opts.body.image).toBe('michaelmanleyx/llama-cpp:server-cuda');
405
- expect(process.exitCode).toBeFalsy();
406
- });
407
-
408
- it('invokeai: dispatches with InvokeAI image', async () => {
409
- setupServe();
410
- const p = serveCommand(config, ['template', 'invokeai', '--max-cost', '5'], chalk);
411
- await vi.advanceTimersByTimeAsync(5000);
412
- await p;
413
-
414
- const [, opts] = api.callApi.mock.calls[0];
415
- expect(opts.body.image).toBe('ghcr.io/invoke-ai/invokeai:latest');
416
- expect(process.exitCode).toBeFalsy();
417
- });
418
-
419
- it('kohya-ss: dispatches with Kohya SS image', async () => {
420
- setupServe();
421
- const p = serveCommand(config, ['template', 'kohya-ss', '--max-cost', '5'], chalk);
422
- await vi.advanceTimersByTimeAsync(5000);
423
- await p;
424
-
425
- const [, opts] = api.callApi.mock.calls[0];
426
- expect(opts.body.image).toBe('bmaltais/kohya-ss-gui:latest');
427
- expect(process.exitCode).toBeFalsy();
428
- });
429
-
430
- it('text-gen-webui: dispatches with text-gen-webui image', async () => {
431
- setupServe();
432
- const p = serveCommand(config, ['template', 'text-gen-webui', '--max-cost', '5'], chalk);
433
- await vi.advanceTimersByTimeAsync(5000);
434
- await p;
435
-
436
- const [, opts] = api.callApi.mock.calls[0];
437
- expect(opts.body.image).toBe('atinoda/text-generation-webui:default-nightly');
438
- expect(process.exitCode).toBeFalsy();
439
- });
440
-
441
- it('user --gpu override is forwarded', async () => {
442
- setupServe({ gpu_type: 'H100' });
443
- const p = serveCommand(config, ['template', 'vllm', '--gpu', 'H100', '--max-cost', '20'], chalk);
444
- await vi.advanceTimersByTimeAsync(5000);
445
- await p;
446
-
447
- const [, opts] = api.callApi.mock.calls[0];
448
- expect(opts.body.gpu).toBe('H100');
449
- expect(process.exitCode).toBeFalsy();
450
- });
451
-
452
- it('user --env override replaces template default', async () => {
453
- setupServe();
454
- const p = serveCommand(
455
- config,
456
- ['template', 'vllm', '--env', 'MODEL=mistralai/Mistral-7B-v0.1', '--max-cost', '5'],
457
- chalk,
458
- );
459
- await vi.advanceTimersByTimeAsync(5000);
460
- await p;
461
-
462
- const [, opts] = api.callApi.mock.calls[0];
463
- expect(opts.body.env.MODEL).toBe('mistralai/Mistral-7B-v0.1');
464
- expect(process.exitCode).toBeFalsy();
465
- });
466
- });
467
-
468
- // ─────────────────────────────────────────────────────────────────────────────
469
- // 5. badgr run template <name> — job dispatch
470
- // ─────────────────────────────────────────────────────────────────────────────
471
-
472
- // Sequence for runCommand:
473
- // callApi('/run', ...) → dep (via callWithFallback → callApi)
474
- // callApi('/deployments/<id>', ...) → { status: 'completed', exit_code: 0 }
475
- // callApi('/deployments/<id>/logs', ...) → { logs: [] }
476
-
477
- function setupRun(depOverrides = {}) {
478
- api.callApi
479
- .mockResolvedValueOnce(makeRunDep(depOverrides)) // POST /run
480
- .mockResolvedValueOnce({ status: 'completed', exit_code: 0 }) // status poll
481
- .mockResolvedValueOnce({ logs: [] }); // logs
482
- }
483
-
484
- describe('badgr run template <name>', () => {
485
- it('axolotl: dispatches with A100 image, max_cost, max_runtime', async () => {
486
- setupRun();
487
- const p = runCommand(config, ['template', 'axolotl', '--max-cost', '20', '--max-runtime', '120'], chalk);
488
- await vi.advanceTimersByTimeAsync(10000);
489
- await p;
490
-
491
- const [route, opts] = api.callApi.mock.calls[0];
492
- expect(route).toBe('/run');
493
- expect(opts.body.image).toBe('axolotlai/axolotl-cloud-uv:main-latest');
494
- expect(opts.body.gpu).toBe('A100');
495
- expect(opts.body.max_cost_usd).toBe(20);
496
- expect(opts.body.max_runtime_seconds).toBe(7200); // 120 min * 60
497
- expect(process.exitCode).toBeFalsy();
498
- });
499
-
500
- it('unsloth: dispatches with unsloth image and RTX_4090', async () => {
501
- setupRun({ gpu_type: 'RTX_4090' });
502
- const p = runCommand(config, ['template', 'unsloth', '--max-cost', '10', '--max-runtime', '90'], chalk);
503
- await vi.advanceTimersByTimeAsync(10000);
504
- await p;
505
-
506
- const [, opts] = api.callApi.mock.calls[0];
507
- expect(opts.body.image).toBe('unslothai/unsloth:latest');
508
- expect(opts.body.gpu).toBe('RTX_4090');
509
- expect(process.exitCode).toBeFalsy();
510
- });
511
- });
512
-
513
- // ─────────────────────────────────────────────────────────────────────────────
514
- // 6. Routing guards
515
- // ─────────────────────────────────────────────────────────────────────────────
516
-
517
- describe('routing guards', () => {
518
- it('badgr run template <endpoint> → error with serve suggestion', async () => {
519
- await runCommand(config, ['template', 'vllm', '--max-cost', '5'], chalk);
520
- expect(process.exitCode).toBe(1);
521
- expect(console.error).toHaveBeenCalledWith(
522
- expect.stringContaining('badgr serve template vllm'),
523
- );
524
- });
525
-
526
- it('badgr serve template <job> → error with run suggestion', async () => {
527
- await serveCommand(config, ['template', 'axolotl', '--max-cost', '20'], chalk);
528
- expect(process.exitCode).toBe(1);
529
- expect(console.error).toHaveBeenCalledWith(
530
- expect.stringContaining('badgr run template axolotl'),
531
- );
532
- });
533
-
534
- it('badgr serve template <unknown> → error with list suggestion', async () => {
535
- await serveCommand(config, ['template', 'doesnotexist'], chalk);
536
- expect(process.exitCode).toBe(1);
537
- expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Unknown template'));
538
- expect(console.error).toHaveBeenCalledWith(expect.stringContaining('badgr template list'));
539
- });
540
-
541
- it('badgr run template <unknown> → error with list suggestion', async () => {
542
- await runCommand(config, ['template', 'alsonotreal'], chalk);
543
- expect(process.exitCode).toBe(1);
544
- expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Unknown template'));
545
- });
546
-
547
- it('badgr serve template with no name → error', async () => {
548
- await serveCommand(config, ['template'], chalk);
549
- expect(process.exitCode).toBe(1);
550
- });
551
-
552
- it('badgr run template with no name → error', async () => {
553
- await runCommand(config, ['template'], chalk);
554
- expect(process.exitCode).toBe(1);
555
- });
556
- });