badgr-cli 1.1.1 → 1.1.3

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 (73) hide show
  1. package/LICENSE +207 -0
  2. package/README.md +13 -6
  3. package/package.json +44 -2
  4. package/src/api.js +16 -0
  5. package/src/badgr.js +26 -10
  6. package/src/commands/batch.js +11 -0
  7. package/src/commands/billing.js +3 -3
  8. package/src/commands/comfyui.js +31 -15
  9. package/src/commands/connect.js +4 -1
  10. package/src/commands/diagnose.js +493 -0
  11. package/src/commands/embed.js +13 -10
  12. package/src/commands/job.js +246 -0
  13. package/src/commands/launch.js +152 -16
  14. package/src/commands/login.js +75 -20
  15. package/src/commands/run.js +74 -16
  16. package/src/commands/sbatch.js +6 -1
  17. package/src/commands/serve.js +44 -30
  18. package/src/commands/train.js +8 -12
  19. package/src/commands/transcribe.js +13 -10
  20. package/src/credentials.js +33 -0
  21. package/src/envFlag.js +10 -0
  22. package/src/fallback.js +13 -2
  23. package/src/onboarding.js +8 -1
  24. package/src/progress.js +48 -0
  25. package/src/commands/task.js +0 -25
  26. package/tests/agent-images.test.js +0 -17
  27. package/tests/api.test.js +0 -168
  28. package/tests/artifactDownload.test.js +0 -113
  29. package/tests/artifacts.test.js +0 -168
  30. package/tests/batch.test.js +0 -641
  31. package/tests/browser.test.js +0 -51
  32. package/tests/capacity.test.js +0 -68
  33. package/tests/commands.test.js +0 -417
  34. package/tests/config.test.js +0 -96
  35. package/tests/connect.test.js +0 -83
  36. package/tests/detect.test.js +0 -191
  37. package/tests/down.test.js +0 -150
  38. package/tests/errors.test.js +0 -130
  39. package/tests/fallback-timeout.test.js +0 -41
  40. package/tests/fanout.test.js +0 -124
  41. package/tests/gpu-doctor-classifiers.test.js +0 -402
  42. package/tests/gpu-doctor-doctor.test.js +0 -304
  43. package/tests/gpu-doctor-probe-cache.test.js +0 -110
  44. package/tests/gpu-doctor-probes.test.js +0 -257
  45. package/tests/heartbeat.test.js +0 -70
  46. package/tests/job-progress-poll.test.js +0 -136
  47. package/tests/launch-command-argv.test.js +0 -93
  48. package/tests/launch-readiness.test.js +0 -403
  49. package/tests/launch.test.js +0 -440
  50. package/tests/onboarding.test.js +0 -134
  51. package/tests/productized-dry-run.test.js +0 -141
  52. package/tests/productized-runners.test.js +0 -237
  53. package/tests/pull.test.js +0 -266
  54. package/tests/rerun.test.js +0 -94
  55. package/tests/restart.test.js +0 -88
  56. package/tests/router.test.js +0 -98
  57. package/tests/run-lifecycle.test.js +0 -1054
  58. package/tests/sbatch.test.js +0 -190
  59. package/tests/secrets.test.js +0 -16
  60. package/tests/serve-apps.test.js +0 -189
  61. package/tests/serve-lifecycle.test.js +0 -931
  62. package/tests/slurm.test.js +0 -77
  63. package/tests/spec.test.js +0 -201
  64. package/tests/status.test.js +0 -73
  65. package/tests/store.test.js +0 -187
  66. package/tests/task.test.js +0 -109
  67. package/tests/template.test.js +0 -556
  68. package/tests/train-lora-dataset.test.js +0 -176
  69. package/tests/upload.test.js +0 -79
  70. package/tests/workload-rerun.test.js +0 -56
  71. package/tests/workload-spec.test.js +0 -180
  72. package/tests/workload-templates.test.js +0 -865
  73. 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
- });