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,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
- });