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,403 +0,0 @@
1
- /**
2
- * Launch readiness — gap-filling tests for features on the outreach checklist
3
- *
4
- * Covers items missing from run-lifecycle and serve-lifecycle:
5
- * - badgr down → terminates, writes receipt, prints cost summary
6
- * - badgr status → shows running deployments, rate, endpoint; empty state
7
- * - compat_failure error → correct user-facing message for CUDA mismatch
8
- * - failure_category surfaced → compat failure receipt shows failure reason
9
- */
10
-
11
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
-
13
- // ── Module mocks ──────────────────────────────────────────────────────────────
14
-
15
- vi.mock('../src/api.js', () => ({
16
- callApi: vi.fn(),
17
- terminateDeployment: vi.fn(),
18
- listDeployments: vi.fn(),
19
- }));
20
-
21
- vi.mock('../src/store.js', () => ({
22
- addDeployment: vi.fn(),
23
- addReceipt: vi.fn(),
24
- updateReceipt: vi.fn(),
25
- generateReceiptId: vi.fn(() => 'rcpt-down-001'),
26
- generateDeploymentId: vi.fn(() => 'dep-down-001'),
27
- listDeployments: vi.fn(() => []),
28
- listReceipts: vi.fn(() => []),
29
- findDeployment: vi.fn(() => null),
30
- updateDeployment: vi.fn(),
31
- removeDeployment: vi.fn(),
32
- }));
33
-
34
- vi.mock('../src/config.js', () => ({
35
- requireApiKey: vi.fn(),
36
- loadConfig: vi.fn(() => ({ apiKey: 'sk-test', baseUrl: 'https://api.test/v1' })),
37
- saveConfig: vi.fn(),
38
- }));
39
-
40
- import { downCommand } from '../src/commands/down.js';
41
- import { statusCommand } from '../src/commands/status.js';
42
- import { callWithFallback, CapacityError } from '../src/fallback.js';
43
- import * as api from '../src/api.js';
44
- import * as store from '../src/store.js';
45
-
46
- // ── Helpers ───────────────────────────────────────────────────────────────────
47
-
48
- const config = { apiKey: 'sk-test', baseUrl: 'https://api.test/v1' };
49
-
50
- const chalk = {
51
- bold: s => s,
52
- dim: s => s,
53
- red: s => s,
54
- yellow: s => s,
55
- green: s => s,
56
- cyan: s => s,
57
- };
58
-
59
- const NOW = 1_700_000_000; // fixed unix timestamp
60
- const STARTED = NOW - 600; // 10 minutes ago
61
-
62
- function makeTerminatedDep(overrides = {}) {
63
- return {
64
- deployment_id: 'dep-abc123',
65
- gpu_type: 'RTX_4090',
66
- cost_per_hour: 0.85,
67
- started_at: STARTED,
68
- stopped_at: NOW,
69
- status: 'stopped',
70
- teardown_ok: 'ok',
71
- ...overrides,
72
- };
73
- }
74
-
75
- function makeRunningDep(overrides = {}) {
76
- return {
77
- deployment_id: 'dep-run-001',
78
- workload_type: 'job',
79
- gpu_type: 'A100',
80
- gpu_count: 1,
81
- cost_per_hour: 2.50,
82
- status: 'running',
83
- endpoint_url: null,
84
- model: null,
85
- ...overrides,
86
- };
87
- }
88
-
89
- beforeEach(() => {
90
- vi.spyOn(console, 'log').mockImplementation(() => {});
91
- vi.spyOn(console, 'error').mockImplementation(() => {});
92
- vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
93
- vi.clearAllMocks();
94
- store.generateReceiptId.mockReturnValue('rcpt-down-001');
95
- store.findDeployment.mockReturnValue(null);
96
- });
97
-
98
- afterEach(() => {
99
- vi.restoreAllMocks();
100
- });
101
-
102
- // ─────────────────────────────────────────────────────────────────────────────
103
- // 1. badgr down
104
- // ─────────────────────────────────────────────────────────────────────────────
105
-
106
- describe('badgr down', () => {
107
- it('calls terminateDeployment with the provided deployment ID', async () => {
108
- api.terminateDeployment.mockResolvedValue(makeTerminatedDep());
109
-
110
- await downCommand(config, ['dep-abc123'], chalk);
111
-
112
- expect(api.terminateDeployment).toHaveBeenCalledWith(config, 'dep-abc123');
113
- });
114
-
115
- it('writes a receipt with runtime, finalCost, and action=badgr down', async () => {
116
- api.terminateDeployment.mockResolvedValue(makeTerminatedDep());
117
-
118
- await downCommand(config, ['dep-abc123'], chalk);
119
-
120
- expect(store.addReceipt).toHaveBeenCalledWith(expect.objectContaining({
121
- action: 'badgr down',
122
- deploymentId: 'dep-abc123',
123
- gpu: 'RTX_4090',
124
- status: 'terminated',
125
- }));
126
- // Runtime should be ~600s (10 min window)
127
- const call = store.addReceipt.mock.calls[0][0];
128
- expect(call.runtimeSeconds).toBeGreaterThan(0);
129
- expect(call.finalCost).toBeGreaterThan(0);
130
- });
131
-
132
- it('removes the local deployment record', async () => {
133
- api.terminateDeployment.mockResolvedValue(makeTerminatedDep());
134
-
135
- await downCommand(config, ['dep-abc123'], chalk);
136
-
137
- expect(store.removeDeployment).toHaveBeenCalledWith('dep-abc123');
138
- });
139
-
140
- it('resolves local name to deployment ID via findDeployment', async () => {
141
- store.findDeployment.mockReturnValue({ id: 'dep-abc123', name: 'my-job' });
142
- api.terminateDeployment.mockResolvedValue(makeTerminatedDep());
143
-
144
- await downCommand(config, ['my-job'], chalk);
145
-
146
- expect(api.terminateDeployment).toHaveBeenCalledWith(config, 'dep-abc123');
147
- });
148
-
149
- it('prints error and returns without crashing on API failure', async () => {
150
- api.terminateDeployment.mockRejectedValue(new Error('network error'));
151
-
152
- await downCommand(config, ['dep-abc123'], chalk);
153
-
154
- expect(console.error).toHaveBeenCalledWith(
155
- expect.stringContaining('Could not stop deployment'),
156
- );
157
- expect(store.addReceipt).not.toHaveBeenCalled();
158
- });
159
-
160
- it('prints usage hint when no deployment ID given', async () => {
161
- await downCommand(config, [], chalk);
162
-
163
- expect(console.error).toHaveBeenCalledWith(
164
- expect.stringContaining('Usage'),
165
- );
166
- expect(api.terminateDeployment).not.toHaveBeenCalled();
167
- });
168
- });
169
-
170
- // ─────────────────────────────────────────────────────────────────────────────
171
- // 2. badgr status
172
- // ─────────────────────────────────────────────────────────────────────────────
173
-
174
- describe('badgr status', () => {
175
- it('shows "Nothing running" when no active deployments', async () => {
176
- api.listDeployments.mockResolvedValue({ deployments: [] });
177
-
178
- await statusCommand(config, [], chalk);
179
-
180
- const allOutput = console.log.mock.calls.flat().join(' ');
181
- expect(allOutput).toContain('Nothing running');
182
- });
183
-
184
- it('displays running deployment with GPU type and billing rate', async () => {
185
- api.listDeployments.mockResolvedValue({ deployments: [makeRunningDep()] });
186
-
187
- await statusCommand(config, [], chalk);
188
-
189
- const allOutput = console.log.mock.calls.flat().join(' ');
190
- expect(allOutput).toContain('dep-run-001');
191
- expect(allOutput).toContain('A100');
192
- expect(allOutput).toContain('2.50');
193
- });
194
-
195
- it('shows endpoint URL for endpoint workloads', async () => {
196
- api.listDeployments.mockResolvedValue({
197
- deployments: [makeRunningDep({
198
- workload_type: 'endpoint',
199
- endpoint_url: 'https://dep-run-001.aibadgr.com/v1',
200
- })],
201
- });
202
-
203
- await statusCommand(config, [], chalk);
204
-
205
- const allOutput = console.log.mock.calls.flat().join(' ');
206
- expect(allOutput).toContain('dep-run-001.aibadgr.com');
207
- });
208
-
209
- it('shows total billing rate when multiple deployments are running', async () => {
210
- api.listDeployments.mockResolvedValue({
211
- deployments: [
212
- makeRunningDep({ deployment_id: 'dep-1', cost_per_hour: 1.20 }),
213
- makeRunningDep({ deployment_id: 'dep-2', cost_per_hour: 2.50 }),
214
- ],
215
- });
216
-
217
- await statusCommand(config, [], chalk);
218
-
219
- const allOutput = console.log.mock.calls.flat().join(' ');
220
- // Total: 1.20 + 2.50 = 3.70
221
- expect(allOutput).toContain('3.70');
222
- });
223
-
224
- it('shows provisioning-state deployment as starting', async () => {
225
- api.listDeployments.mockResolvedValue({
226
- deployments: [makeRunningDep({ status: 'provisioning', cost_per_hour: 0 })],
227
- });
228
-
229
- await statusCommand(config, [], chalk);
230
-
231
- const allOutput = console.log.mock.calls.flat().join(' ');
232
- expect(allOutput).toContain('starting');
233
- });
234
-
235
- it('falls back to local store when API is unavailable', async () => {
236
- api.listDeployments.mockRejectedValue(new Error('connection refused'));
237
- store.listDeployments.mockReturnValue([
238
- { id: 'dep-local-001', name: 'local-job', gpu: 'L40S', status: 'running', costPerHour: 1.10 },
239
- ]);
240
-
241
- await statusCommand(config, [], chalk);
242
-
243
- const allOutput = console.log.mock.calls.flat().join(' ');
244
- expect(allOutput).toContain('dep-local-001');
245
- });
246
- });
247
-
248
- // ─────────────────────────────────────────────────────────────────────────────
249
- // 3. CUDA / compat failure error messages
250
- // ─────────────────────────────────────────────────────────────────────────────
251
-
252
- describe('compat_failure error message', () => {
253
- function makeCompatError() {
254
- const err = new Error('PROVISIONING_FAILED');
255
- err.errorData = {
256
- code: 'PROVISIONING_FAILED',
257
- failure_category: 'compat_failure',
258
- };
259
- return err;
260
- }
261
-
262
- it('shows GPU/CUDA incompatibility message when failure_category is compat_failure', async () => {
263
- api.callApi.mockRejectedValue(makeCompatError());
264
-
265
- let thrown;
266
- try {
267
- await callWithFallback(
268
- '/run',
269
- { apiKey: 'sk-test', baseUrl: 'https://api.test/v1' },
270
- () => ({ gpu: 'RTX_4090' }),
271
- '1',
272
- chalk,
273
- { thing: 'job', cmd: 'badgr run' },
274
- );
275
- } catch (err) {
276
- thrown = err;
277
- }
278
-
279
- expect(thrown).toBeInstanceOf(CapacityError);
280
- expect(thrown.message).toContain('incompatib');
281
- });
282
-
283
- it('shows generic provisioning failure when failure_category is infrastructure', async () => {
284
- const err = new Error('PROVISIONING_FAILED');
285
- err.errorData = { code: 'PROVISIONING_FAILED', failure_category: 'infrastructure' };
286
- api.callApi.mockRejectedValue(err);
287
-
288
- let thrown;
289
- try {
290
- await callWithFallback(
291
- '/run',
292
- { apiKey: 'sk-test', baseUrl: 'https://api.test/v1' },
293
- () => ({}),
294
- '1',
295
- chalk,
296
- { thing: 'job', cmd: 'badgr run' },
297
- );
298
- } catch (err2) {
299
- thrown = err2;
300
- }
301
-
302
- expect(thrown).toBeInstanceOf(CapacityError);
303
- // Should NOT mention CUDA/compat
304
- expect(thrown.message).not.toContain('incompatib');
305
- expect(thrown.message).toContain('failed to start');
306
- });
307
-
308
- it('compat message mentions trying a different GPU type or image', async () => {
309
- api.callApi.mockRejectedValue(makeCompatError());
310
-
311
- let thrown;
312
- try {
313
- await callWithFallback(
314
- '/run',
315
- { apiKey: 'sk-test', baseUrl: 'https://api.test/v1' },
316
- () => ({}),
317
- '2',
318
- chalk,
319
- { thing: 'job', cmd: 'badgr run' },
320
- );
321
- } catch (err) {
322
- thrown = err;
323
- }
324
-
325
- expect(thrown.message).toMatch(/GPU type|image/i);
326
- });
327
- });
328
-
329
- // ─────────────────────────────────────────────────────────────────────────────
330
- // 4. Stale-capacity retry — PROVISIONING_FAILED triggers one immediate retry
331
- // ─────────────────────────────────────────────────────────────────────────────
332
-
333
- describe('stale-capacity retry on PROVISIONING_FAILED', () => {
334
- function makeProvisioningFailedErr() {
335
- const err = new Error('PROVISIONING_FAILED');
336
- err.errorData = { code: 'PROVISIONING_FAILED', failure_category: 'infrastructure' };
337
- return err;
338
- }
339
-
340
- it('retries the API call once before escalating on PROVISIONING_FAILED (tier=2 → no tier-2 expansion)', async () => {
341
- // tier=2 means no tier-2 fallback expansion, so only the stale-capacity retry fires.
342
- api.callApi.mockRejectedValue(makeProvisioningFailedErr());
343
-
344
- try {
345
- await callWithFallback(
346
- '/run',
347
- { apiKey: 'sk-test', baseUrl: 'https://api.test/v1' },
348
- () => ({}),
349
- '2',
350
- chalk,
351
- { thing: 'job', cmd: 'badgr run' },
352
- );
353
- } catch (_) {}
354
-
355
- // First attempt + one stale-capacity retry = 2 calls minimum
356
- expect(api.callApi.mock.calls.length).toBeGreaterThanOrEqual(2);
357
- });
358
-
359
- it('sends prefer_different_provider=true on the PROVISIONING_FAILED retry', async () => {
360
- api.callApi.mockRejectedValue(makeProvisioningFailedErr());
361
-
362
- const bodies = [];
363
- api.callApi.mockImplementation((_ep, opts) => {
364
- bodies.push(opts.body);
365
- return Promise.reject(makeProvisioningFailedErr());
366
- });
367
-
368
- try {
369
- await callWithFallback(
370
- '/run',
371
- { apiKey: 'sk-test', baseUrl: 'https://api.test/v1' },
372
- () => ({ gpu: 'RTX_4090' }),
373
- '2',
374
- chalk,
375
- { thing: 'job', cmd: 'badgr run' },
376
- );
377
- } catch (_) {}
378
-
379
- // First call: no prefer_different_provider; retry call: must include it
380
- expect(bodies[0]).not.toHaveProperty('prefer_different_provider');
381
- expect(bodies[1]).toMatchObject({ prefer_different_provider: true });
382
- });
383
-
384
- it('does NOT double-retry on non-PROVISIONING_FAILED errors', async () => {
385
- const err = new Error('NO_CAPACITY_MATCH');
386
- err.errorData = { code: 'NO_CAPACITY_MATCH' };
387
- api.callApi.mockRejectedValue(err);
388
-
389
- try {
390
- await callWithFallback(
391
- '/run',
392
- { apiKey: 'sk-test', baseUrl: 'https://api.test/v1' },
393
- () => ({}),
394
- '2',
395
- chalk,
396
- { thing: 'job', cmd: 'badgr run' },
397
- );
398
- } catch (_) {}
399
-
400
- // tier=2 → no tier-2 expansion; no stale-capacity retry for NO_CAPACITY_MATCH
401
- expect(api.callApi.mock.calls.length).toBe(1);
402
- });
403
- });