badgr-cli 1.0.42 → 1.0.44

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.
@@ -13,6 +13,11 @@
13
13
  * - Custom image skips health check automatically
14
14
  * - Tier-1 unavailable → auto-expand to tier-2
15
15
  * - Receipt records providerRoute, tier, gpu, endpointUrl, status
16
+ *
17
+ * Readiness is driven entirely by Badgr's own deployment status
18
+ * (GET /deployments/{id} — endpoint_ready/health_path/readiness_reason/fix_hint).
19
+ * The CLI never fetches the RunPod proxy/pod endpoint directly, so these tests
20
+ * mock api.callApi responses rather than global.fetch.
16
21
  */
17
22
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
18
23
  import { serveCommand } from '../src/commands/serve.js';
@@ -72,6 +77,17 @@ function makeServeDep(overrides = {}) {
72
77
  };
73
78
  }
74
79
 
80
+ // A GET /deployments/{id} response where the backend reports the app-level
81
+ // endpoint is ready (endpoint_ready:true) — this is what the CLI now waits on
82
+ // instead of fetching the pod directly.
83
+ function readyStatus(overrides = {}) {
84
+ return { status: 'running', endpoint_ready: true, health_path: '/v1/models', ...overrides };
85
+ }
86
+
87
+ function notReadyStatus(overrides = {}) {
88
+ return { status: 'running', endpoint_ready: false, health_path: '/v1/models', readiness_reason: 'starting', ...overrides };
89
+ }
90
+
75
91
  // ── Setup / teardown ──────────────────────────────────────────────────────────
76
92
 
77
93
  beforeEach(() => {
@@ -83,6 +99,9 @@ beforeEach(() => {
83
99
  vi.clearAllMocks();
84
100
  store.generateReceiptId.mockReturnValue('rcpt-serve-001');
85
101
  api.terminateDeployment.mockResolvedValue({});
102
+ // No test should ever hit the network directly — waitForEndpoint polls
103
+ // Badgr's own API only. Fail loudly if anything calls fetch.
104
+ global.fetch = vi.fn().mockRejectedValue(new Error('serve.js must not call fetch() directly'));
86
105
  });
87
106
 
88
107
  afterEach(() => {
@@ -99,14 +118,8 @@ describe('successful serve', () => {
99
118
  it('Llama-3.1-8B on L40S via RunPod — endpoint ready, receipt records state', async () => {
100
119
  api.callApi
101
120
  .mockResolvedValueOnce(makeServeDep()) // POST /serve
102
- .mockResolvedValueOnce({ status: 'running' }); // pre-health status check
103
-
104
- // Mock the direct fetch call in waitForEndpoint
105
- global.fetch = vi.fn().mockResolvedValue({
106
- ok: true, status: 200,
107
- json: async () => ({ data: [{ id: 'meta-llama/Llama-3.1-8B-Instruct' }] }),
108
- text: async () => '',
109
- });
121
+ .mockResolvedValueOnce({ status: 'running' }) // pre-health status check
122
+ .mockResolvedValueOnce(readyStatus()); // waitForEndpoint poll
110
123
 
111
124
  const p = serveCommand(config, ['meta-llama/Llama-3.1-8B-Instruct', '--gpu', 'L40S', '--max-cost', '5'], chalk);
112
125
  await vi.advanceTimersByTimeAsync(5000);
@@ -121,6 +134,7 @@ describe('successful serve', () => {
121
134
  status: 'ready',
122
135
  endpointUrl: ENDPOINT_URL,
123
136
  }));
137
+ expect(global.fetch).not.toHaveBeenCalled();
124
138
  expect(process.exitCode).toBeFalsy();
125
139
  });
126
140
 
@@ -128,9 +142,8 @@ describe('successful serve', () => {
128
142
  const dep = makeServeDep({ gpu_type: 'A100', provider: 'vastai', model: 'mistralai/Mistral-7B-v0.1' });
129
143
  api.callApi
130
144
  .mockResolvedValueOnce(dep)
131
- .mockResolvedValueOnce({ status: 'running' });
132
-
133
- global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({}) });
145
+ .mockResolvedValueOnce({ status: 'running' })
146
+ .mockResolvedValueOnce(readyStatus());
134
147
 
135
148
  const p = serveCommand(config, ['mistralai/Mistral-7B-v0.1', '--gpu', 'A100', '--max-cost', '5'], chalk);
136
149
  await vi.advanceTimersByTimeAsync(5000);
@@ -150,12 +163,10 @@ describe('successful serve', () => {
150
163
 
151
164
  describe('billing ends when deployment stops', () => {
152
165
  it('serveCommand calls terminateDeployment on SIGINT (graceful stop)', async () => {
153
- // Simulate a deployment that is stopped externally (failed status returned during health check)
154
166
  api.callApi
155
167
  .mockResolvedValueOnce(makeServeDep())
156
- .mockResolvedValueOnce({ status: 'running' });
157
-
158
- global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({}) });
168
+ .mockResolvedValueOnce({ status: 'running' })
169
+ .mockResolvedValueOnce(readyStatus());
159
170
 
160
171
  const p = serveCommand(config, ['meta-llama/Llama-3.1-8B-Instruct', '--max-cost', '5'], chalk);
161
172
  await vi.advanceTimersByTimeAsync(5000);
@@ -199,9 +210,6 @@ describe('deployment fails during startup', () => {
199
210
  .mockResolvedValueOnce({ status: 'running' }) // pre-health check ok
200
211
  .mockResolvedValueOnce({ status: 'failed', error: 'CUDA error' }); // poll inside waitForEndpoint
201
212
 
202
- // Health check endpoint doesn't respond (dep crashed)
203
- global.fetch = vi.fn().mockRejectedValue(new Error('Connection refused'));
204
-
205
213
  const p = serveCommand(config, ['meta-llama/Llama-3.1-8B-Instruct', '--max-cost', '5'], chalk);
206
214
  // need to advance past the 8000ms sleep in waitForEndpoint loop
207
215
  await vi.advanceTimersByTimeAsync(10000);
@@ -212,6 +220,25 @@ describe('deployment fails during startup', () => {
212
220
  }));
213
221
  expect(process.exitCode).toBe(1);
214
222
  });
223
+
224
+ it('surfaces backend fix_hint as the failure reason (pod exited / readiness timeout)', async () => {
225
+ api.callApi
226
+ .mockResolvedValueOnce(makeServeDep())
227
+ .mockResolvedValueOnce({ status: 'running' })
228
+ .mockResolvedValueOnce({
229
+ status: 'failed',
230
+ error: 'pod_exited',
231
+ fix_hint: 'The pod stopped running before the endpoint ever responded — likely an OOM.',
232
+ });
233
+
234
+ const p = serveCommand(config, ['meta-llama/Llama-3.1-8B-Instruct', '--max-cost', '5'], chalk);
235
+ await vi.advanceTimersByTimeAsync(10000);
236
+ await p;
237
+
238
+ expect(process.exitCode).toBe(1);
239
+ const errLines = console.error.mock.calls.flat().join('\n');
240
+ expect(errLines).toContain('likely an OOM');
241
+ });
215
242
  });
216
243
 
217
244
  // ─────────────────────────────────────────────────────────────────────────────
@@ -219,18 +246,15 @@ describe('deployment fails during startup', () => {
219
246
  // ─────────────────────────────────────────────────────────────────────────────
220
247
 
221
248
  describe('health check timeout', () => {
222
- it('records health_check_timeout if /models never responds within 5 minutes', async () => {
249
+ it('records health_check_timeout if endpoint_ready never becomes true within 15 minutes', async () => {
223
250
  api.callApi
224
251
  .mockResolvedValueOnce(makeServeDep()) // POST /serve
225
252
  .mockResolvedValueOnce({ status: 'running' }) // pre-health check
226
- .mockResolvedValue({ status: 'running' }); // all subsequent dep polls: still running
227
-
228
- // Health endpoint never responds
229
- global.fetch = vi.fn().mockRejectedValue(new Error('ETIMEDOUT'));
253
+ .mockResolvedValue(notReadyStatus()); // all subsequent dep polls: still not ready
230
254
 
231
255
  const p = serveCommand(config, ['meta-llama/Llama-3.1-8B-Instruct', '--max-cost', '5'], chalk);
232
- // waitForEndpoint timeout is 5 * 60 * 1000 = 300000ms
233
- await vi.advanceTimersByTimeAsync(310000);
256
+ // waitForEndpoint timeout is VLLM_SERVE_WAIT_MS = 15 * 60 * 1000
257
+ await vi.advanceTimersByTimeAsync(910000);
234
258
  await p;
235
259
 
236
260
  expect(store.updateReceipt).toHaveBeenCalledWith('rcpt-serve-001', expect.objectContaining({
@@ -306,15 +330,15 @@ describe('invalid model ID or no capacity', () => {
306
330
  // ─────────────────────────────────────────────────────────────────────────────
307
331
 
308
332
  describe('--no-wait flag', () => {
309
- it('returns immediately without polling /models', async () => {
333
+ it('returns immediately without polling deployment status for readiness', async () => {
310
334
  api.callApi.mockResolvedValueOnce(makeServeDep());
311
- global.fetch = vi.fn(); // should never be called
312
335
 
313
336
  const p = serveCommand(config, ['meta-llama/Llama-3.1-8B-Instruct', '--no-wait', '--max-cost', '5'], chalk);
314
337
  await vi.advanceTimersByTimeAsync(100);
315
338
  await p;
316
339
 
317
- expect(global.fetch).not.toHaveBeenCalled();
340
+ // Only the POST /serve call — no readiness polling at all.
341
+ expect(api.callApi).toHaveBeenCalledTimes(1);
318
342
  // Receipt records 'starting' (not 'ready') when --no-wait is used
319
343
  expect(store.updateReceipt).toHaveBeenCalledWith('rcpt-serve-001', expect.objectContaining({
320
344
  status: 'starting',
@@ -330,7 +354,6 @@ describe('--no-wait flag', () => {
330
354
  describe('custom image health check', () => {
331
355
  it('skips health check for non-comfyui --image (no known readiness path)', async () => {
332
356
  api.callApi.mockResolvedValueOnce(makeServeDep({ model: null }));
333
- global.fetch = vi.fn(); // should not be called
334
357
 
335
358
  const p = serveCommand(
336
359
  config,
@@ -340,20 +363,15 @@ describe('custom image health check', () => {
340
363
  await vi.advanceTimersByTimeAsync(100);
341
364
  await p;
342
365
 
343
- expect(global.fetch).not.toHaveBeenCalled();
366
+ expect(api.callApi).toHaveBeenCalledTimes(1);
344
367
  expect(process.exitCode).toBeFalsy();
345
368
  });
346
369
 
347
- it('auto-detects comfyui image and polls /system_stats instead of skipping', async () => {
370
+ it('auto-detects comfyui image and waits on backend-reported endpoint_ready', async () => {
348
371
  api.callApi
349
372
  .mockResolvedValueOnce(makeServeDep({ model: null })) // POST /serve
350
- .mockResolvedValueOnce({ status: 'running' }); // pre-health dep status check
351
-
352
- const fetchedUrls = [];
353
- global.fetch = vi.fn().mockImplementation((url) => {
354
- fetchedUrls.push(url);
355
- return Promise.resolve({ ok: true, status: 200 });
356
- });
373
+ .mockResolvedValueOnce({ status: 'running' }) // pre-health dep status check
374
+ .mockResolvedValueOnce({ status: 'running', endpoint_ready: true, health_path: '/system_stats' });
357
375
 
358
376
  const p = serveCommand(
359
377
  config,
@@ -363,21 +381,15 @@ describe('custom image health check', () => {
363
381
  await vi.advanceTimersByTimeAsync(5000);
364
382
  await p;
365
383
 
366
- expect(fetchedUrls.some(u => u.includes('/system_stats'))).toBe(true);
367
- expect(fetchedUrls.some(u => u.includes('/models'))).toBe(false);
384
+ expect(store.updateReceipt).toHaveBeenCalledWith('rcpt-serve-001', expect.objectContaining({ status: 'ready' }));
368
385
  expect(process.exitCode).toBeFalsy();
369
386
  });
370
387
 
371
- it('uses --health-path when explicitly provided, overriding defaults', async () => {
388
+ it('sends --health-path override to the backend when explicitly provided', async () => {
372
389
  api.callApi
373
390
  .mockResolvedValueOnce(makeServeDep({ model: null })) // POST /serve
374
- .mockResolvedValueOnce({ status: 'running' }); // pre-health dep status check
375
-
376
- const fetchedUrls = [];
377
- global.fetch = vi.fn().mockImplementation((url) => {
378
- fetchedUrls.push(url);
379
- return Promise.resolve({ ok: true, status: 200 });
380
- });
391
+ .mockResolvedValueOnce({ status: 'running' }) // pre-health dep status check
392
+ .mockResolvedValueOnce({ status: 'running', endpoint_ready: true, health_path: '/health' });
381
393
 
382
394
  const p = serveCommand(
383
395
  config,
@@ -387,21 +399,16 @@ describe('custom image health check', () => {
387
399
  await vi.advanceTimersByTimeAsync(5000);
388
400
  await p;
389
401
 
390
- expect(fetchedUrls.some(u => u.includes('/health'))).toBe(true);
391
- expect(fetchedUrls.some(u => u.includes('/models'))).toBe(false);
402
+ const body = api.callApi.mock.calls[0][1].body;
403
+ expect(body.health_path).toBe('/health');
392
404
  expect(process.exitCode).toBeFalsy();
393
405
  });
394
406
 
395
407
  it('--health-path also works for model-based serve (overrides /models default)', async () => {
396
408
  api.callApi
397
409
  .mockResolvedValueOnce(makeServeDep()) // POST /serve
398
- .mockResolvedValueOnce({ status: 'running' }); // pre-health dep status check
399
-
400
- const fetchedUrls = [];
401
- global.fetch = vi.fn().mockImplementation((url) => {
402
- fetchedUrls.push(url);
403
- return Promise.resolve({ ok: true, status: 200 });
404
- });
410
+ .mockResolvedValueOnce({ status: 'running' }) // pre-health dep status check
411
+ .mockResolvedValueOnce({ status: 'running', endpoint_ready: true, health_path: '/v1/health' });
405
412
 
406
413
  const p = serveCommand(
407
414
  config,
@@ -411,8 +418,8 @@ describe('custom image health check', () => {
411
418
  await vi.advanceTimersByTimeAsync(5000);
412
419
  await p;
413
420
 
414
- expect(fetchedUrls.some(u => u.includes('/v1/health'))).toBe(true);
415
- expect(fetchedUrls.some(u => u.endsWith('/models'))).toBe(false);
421
+ const body = api.callApi.mock.calls[0][1].body;
422
+ expect(body.health_path).toBe('/v1/health');
416
423
  expect(process.exitCode).toBeFalsy();
417
424
  });
418
425
  });
@@ -430,9 +437,8 @@ describe('tier-2 fallback for serve', () => {
430
437
  api.callApi
431
438
  .mockRejectedValueOnce(capacityErr) // tier-1
432
439
  .mockResolvedValueOnce(makeServeDep({ provider: 'vast', tier: '2' })) // tier-2
433
- .mockResolvedValueOnce({ status: 'running' }); // pre-health
434
-
435
- global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({}) });
440
+ .mockResolvedValueOnce({ status: 'running' }) // pre-health
441
+ .mockResolvedValueOnce(readyStatus());
436
442
 
437
443
  const p = serveCommand(config, ['meta-llama/Llama-3.1-8B-Instruct', '--max-cost', '5'], chalk);
438
444
  await vi.advanceTimersByTimeAsync(5000);
@@ -499,6 +505,114 @@ describe('input validation for serve', () => {
499
505
  });
500
506
  });
501
507
 
508
+ // ─────────────────────────────────────────────────────────────────────────────
509
+ // --list-aliases — discoverability for blessed vLLM aliases
510
+ // ─────────────────────────────────────────────────────────────────────────────
511
+
512
+ describe('--list-aliases', () => {
513
+ it('prints tested model routes without requiring a model or API call', async () => {
514
+ await serveCommand({ apiKey: null, baseUrl: 'https://api.test/v1' }, ['--list-aliases'], chalk);
515
+ expect(api.callApi).not.toHaveBeenCalled();
516
+ expect(process.exitCode).toBeFalsy();
517
+ const logged = console.log.mock.calls.flat().join('\n');
518
+ expect(logged).toContain('Tested model routes:');
519
+ expect(logged).toContain('qwen-7b');
520
+ expect(logged).toContain('You can also try a Hugging Face model ID:');
521
+ });
522
+ });
523
+
524
+ // ─────────────────────────────────────────────────────────────────────────────
525
+ // Model support-level messaging — blessed route vs best-effort HF model vs
526
+ // custom container, plus gated-model HF_TOKEN guidance shown only on failure.
527
+ // ─────────────────────────────────────────────────────────────────────────────
528
+
529
+ describe('model support-level messaging', () => {
530
+ it('labels a full Hugging Face model ID as best-effort, not tested', async () => {
531
+ api.callApi
532
+ .mockResolvedValueOnce(makeServeDep({ model: 'Qwen/Qwen2.5-7B-Instruct' }))
533
+ .mockResolvedValueOnce({ status: 'running' })
534
+ .mockResolvedValueOnce(readyStatus());
535
+
536
+ const p = serveCommand(config, ['Qwen/Qwen2.5-7B-Instruct', '--max-cost', '10'], chalk);
537
+ await vi.advanceTimersByTimeAsync(5000);
538
+ await p;
539
+
540
+ const logged = console.log.mock.calls.flat().join('\n');
541
+ expect(logged).toContain('Route: best-effort Hugging Face model');
542
+ expect(logged).not.toContain('Route: tested');
543
+ });
544
+
545
+ it('labels a blessed alias as a tested route without extra caveats', async () => {
546
+ api.callApi
547
+ .mockResolvedValueOnce(makeServeDep({ model: 'Qwen/Qwen2.5-7B-Instruct' }))
548
+ .mockResolvedValueOnce({ status: 'running' })
549
+ .mockResolvedValueOnce(readyStatus());
550
+
551
+ const p = serveCommand(config, ['qwen-7b', '--max-cost', '10'], chalk);
552
+ await vi.advanceTimersByTimeAsync(5000);
553
+ await p;
554
+
555
+ const logged = console.log.mock.calls.flat().join('\n');
556
+ expect(logged).toContain('Route: tested');
557
+ expect(logged).not.toContain('best-effort');
558
+ });
559
+
560
+ it('does not show HF_TOKEN guidance up front for a gated model that launches fine', async () => {
561
+ api.callApi
562
+ .mockResolvedValueOnce(makeServeDep({ model: 'meta-llama/Llama-3.1-8B-Instruct' }))
563
+ .mockResolvedValueOnce({ status: 'running' })
564
+ .mockResolvedValueOnce(readyStatus());
565
+
566
+ const p = serveCommand(config, ['meta-llama/Llama-3.1-8B-Instruct', '--max-cost', '10'], chalk);
567
+ await vi.advanceTimersByTimeAsync(5000);
568
+ await p;
569
+
570
+ const logged = console.log.mock.calls.flat().join('\n');
571
+ expect(logged).not.toContain('may require Hugging Face access');
572
+ });
573
+
574
+ it('shows HF_TOKEN guidance only once a gated model actually fails to start', async () => {
575
+ api.callApi
576
+ .mockResolvedValueOnce(makeServeDep({ model: 'meta-llama/Llama-3.1-8B-Instruct' }))
577
+ .mockResolvedValueOnce({ status: 'failed', error: 'gated repo — 401' });
578
+
579
+ const p = serveCommand(config, ['meta-llama/Llama-3.1-8B-Instruct', '--max-cost', '10'], chalk);
580
+ await p;
581
+
582
+ const logged = console.error.mock.calls.flat().join('\n');
583
+ expect(logged).toContain('may require Hugging Face access');
584
+ expect(logged).toContain('--env HF_TOKEN=$HF_TOKEN');
585
+ expect(process.exitCode).toBe(1);
586
+ });
587
+
588
+ it('omits HF_TOKEN guidance on failure when HF_TOKEN is already provided', async () => {
589
+ api.callApi
590
+ .mockResolvedValueOnce(makeServeDep({ model: 'meta-llama/Llama-3.1-8B-Instruct' }))
591
+ .mockResolvedValueOnce({ status: 'failed', error: 'crashed' });
592
+
593
+ const p = serveCommand(config, [
594
+ 'meta-llama/Llama-3.1-8B-Instruct', '--max-cost', '10', '--env', 'HF_TOKEN=hf_abc123',
595
+ ], chalk);
596
+ await p;
597
+
598
+ const logged = console.error.mock.calls.flat().join('\n');
599
+ expect(logged).not.toContain('may require Hugging Face access');
600
+ });
601
+
602
+ it('labels a custom container as "custom server", not a Hugging Face model', async () => {
603
+ api.callApi.mockResolvedValueOnce(makeServeDep({ model: undefined, image: 'my/custom-server:latest' }));
604
+
605
+ const p = serveCommand(config, [
606
+ '--image', 'my/custom-server:latest', '--max-cost', '10', '--no-wait',
607
+ ], chalk);
608
+ await p;
609
+
610
+ const logged = console.log.mock.calls.flat().join('\n');
611
+ expect(logged).toContain('custom server');
612
+ expect(logged).toContain('Your container owns the app behavior.');
613
+ });
614
+ });
615
+
502
616
  // ─────────────────────────────────────────────────────────────────────────────
503
617
  // 11. llama.cpp runtime (--runtime llama.cpp --gguf)
504
618
  // ─────────────────────────────────────────────────────────────────────────────
@@ -522,7 +636,6 @@ describe('--runtime llama.cpp', () => {
522
636
  it('uses llama.cpp image with LLAMA_ARG_HF_REPO and LLAMA_ARG_HF_FILE env vars', async () => {
523
637
  // --no-wait: only POST /serve called (1 mock needed)
524
638
  api.callApi.mockResolvedValueOnce(makeServeDep());
525
- global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });
526
639
 
527
640
  const p = serveCommand(
528
641
  config,
@@ -540,36 +653,11 @@ describe('--runtime llama.cpp', () => {
540
653
  expect(process.exitCode).toBeFalsy();
541
654
  });
542
655
 
543
- it('health-checks /health (not /models)', async () => {
544
- api.callApi
545
- .mockResolvedValueOnce(makeServeDep())
546
- .mockResolvedValueOnce({ status: 'running' });
547
-
548
- const fetchedUrls = [];
549
- global.fetch = vi.fn().mockImplementation((url) => {
550
- fetchedUrls.push(url);
551
- return Promise.resolve({ ok: true, status: 200 });
552
- });
553
-
554
- const p = serveCommand(
555
- config,
556
- ['--runtime', 'llama.cpp', '--hf-repo', HF_REPO, '--hf-file', HF_FILE, '--max-cost', '10'],
557
- chalk,
558
- );
559
- await vi.advanceTimersByTimeAsync(5000);
560
- await p;
561
-
562
- expect(fetchedUrls.some(u => u.includes('/health'))).toBe(true);
563
- expect(fetchedUrls.some(u => u.endsWith('/models'))).toBe(false);
564
- expect(process.exitCode).toBeFalsy();
565
- });
566
-
567
656
  it('records deployment and receipt as ready on success', async () => {
568
657
  api.callApi
569
658
  .mockResolvedValueOnce(makeServeDep())
570
- .mockResolvedValueOnce({ status: 'running' });
571
-
572
- global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });
659
+ .mockResolvedValueOnce({ status: 'running' })
660
+ .mockResolvedValueOnce({ status: 'running', endpoint_ready: true, health_path: '/health' });
573
661
 
574
662
  const p = serveCommand(
575
663
  config,
@@ -591,7 +679,6 @@ describe('--runtime llama.cpp', () => {
591
679
  it('merges user --env with HF env vars', async () => {
592
680
  // --no-wait: only POST /serve called (1 mock needed)
593
681
  api.callApi.mockResolvedValueOnce(makeServeDep());
594
- global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });
595
682
 
596
683
  const p = serveCommand(
597
684
  config,
@@ -613,24 +700,20 @@ describe('--runtime llama.cpp', () => {
613
700
  // 12. E2E smoke test — tiny HF GGUF (ggml-org/tiny-llamas / stories260K.gguf)
614
701
  //
615
702
  // Uses a real, publicly-available tiny GGUF (~1 MB) to verify the full
616
- // request lifecycle: arg parsing → correct serve body → /health polling
617
- // receipt recorded as ready → OpenAI-compatible base URL returned.
703
+ // request lifecycle: arg parsing → correct serve body → readiness polling via
704
+ // Badgr's own deployment status → receipt recorded as ready → OpenAI-compatible
705
+ // base URL returned.
618
706
  // ─────────────────────────────────────────────────────────────────────────────
619
707
 
620
708
  describe('llama.cpp E2E smoke — tiny GGUF (ggml-org/tiny-llamas)', () => {
621
709
  const TINY_REPO = 'ggml-org/tiny-llamas';
622
710
  const TINY_FILE = 'stories260K.gguf';
623
711
 
624
- it('full lifecycle: arg parse → serve body → /health → ready receipt', async () => {
712
+ it('full lifecycle: arg parse → serve body → readiness poll → ready receipt', async () => {
625
713
  api.callApi
626
714
  .mockResolvedValueOnce(makeServeDep({ model: undefined })) // POST /serve
627
- .mockResolvedValueOnce({ status: 'running' }); // pre-health dep check
628
-
629
- const fetchedUrls = [];
630
- global.fetch = vi.fn().mockImplementation((url) => {
631
- fetchedUrls.push(url);
632
- return Promise.resolve({ ok: true, status: 200 });
633
- });
715
+ .mockResolvedValueOnce({ status: 'running' }) // pre-health dep check
716
+ .mockResolvedValueOnce({ status: 'running', endpoint_ready: true, health_path: '/health' });
634
717
 
635
718
  const p = serveCommand(
636
719
  config,
@@ -650,10 +733,6 @@ describe('llama.cpp E2E smoke — tiny GGUF (ggml-org/tiny-llamas)', () => {
650
733
  expect(body.env.LLAMA_ARG_HF_FILE).toBe(TINY_FILE);
651
734
  expect(body.model).toBeUndefined();
652
735
 
653
- // Readiness check hits /health, not /models
654
- expect(fetchedUrls.some(u => u.includes('/health'))).toBe(true);
655
- expect(fetchedUrls.some(u => u.endsWith('/models'))).toBe(false);
656
-
657
736
  // Receipt finalised as ready with endpoint URL
658
737
  expect(store.updateReceipt).toHaveBeenCalledWith('rcpt-serve-001', expect.objectContaining({
659
738
  status: 'ready',
@@ -669,16 +748,11 @@ describe('llama.cpp E2E smoke — tiny GGUF (ggml-org/tiny-llamas)', () => {
669
748
  // ─────────────────────────────────────────────────────────────────────────────
670
749
 
671
750
  describe('--task routing for managed runtimes', () => {
672
- it('--task transcribe polls /health instead of /models', async () => {
751
+ it('--task transcribe becomes ready via backend readiness (health_path=/health)', async () => {
673
752
  api.callApi
674
753
  .mockResolvedValueOnce(makeServeDep({ model: 'large-v3' })) // POST /serve
675
- .mockResolvedValueOnce({ status: 'running' }); // pre-health dep check
676
-
677
- const fetchedUrls = [];
678
- global.fetch = vi.fn().mockImplementation((url) => {
679
- fetchedUrls.push(url);
680
- return Promise.resolve({ ok: true, status: 200 });
681
- });
754
+ .mockResolvedValueOnce({ status: 'running' }) // pre-health dep check
755
+ .mockResolvedValueOnce({ status: 'running', endpoint_ready: true, health_path: '/health' });
682
756
 
683
757
  const p = serveCommand(
684
758
  config,
@@ -688,14 +762,12 @@ describe('--task routing for managed runtimes', () => {
688
762
  await vi.advanceTimersByTimeAsync(5000);
689
763
  await p;
690
764
 
691
- expect(fetchedUrls.some(u => u.includes('/health'))).toBe(true);
692
- expect(fetchedUrls.some(u => u.endsWith('/models'))).toBe(false);
765
+ expect(store.updateReceipt).toHaveBeenCalledWith('rcpt-serve-001', expect.objectContaining({ status: 'ready' }));
693
766
  expect(process.exitCode).toBeFalsy();
694
767
  });
695
768
 
696
769
  it('--task transcribe sends task field to backend', async () => {
697
770
  api.callApi.mockResolvedValueOnce(makeServeDep({ model: 'large-v3' }));
698
- global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });
699
771
 
700
772
  const p = serveCommand(
701
773
  config,
@@ -711,16 +783,11 @@ describe('--task routing for managed runtimes', () => {
711
783
  expect(process.exitCode).toBeFalsy();
712
784
  });
713
785
 
714
- it('--task image polls /health instead of /models', async () => {
786
+ it('--task image becomes ready via backend readiness (health_path=/health)', async () => {
715
787
  api.callApi
716
788
  .mockResolvedValueOnce(makeServeDep({ model: 'black-forest-labs/FLUX.1-schnell' }))
717
- .mockResolvedValueOnce({ status: 'running' });
718
-
719
- const fetchedUrls = [];
720
- global.fetch = vi.fn().mockImplementation((url) => {
721
- fetchedUrls.push(url);
722
- return Promise.resolve({ ok: true, status: 200 });
723
- });
789
+ .mockResolvedValueOnce({ status: 'running' })
790
+ .mockResolvedValueOnce({ status: 'running', endpoint_ready: true, health_path: '/health' });
724
791
 
725
792
  const p = serveCommand(
726
793
  config,
@@ -730,14 +797,12 @@ describe('--task routing for managed runtimes', () => {
730
797
  await vi.advanceTimersByTimeAsync(5000);
731
798
  await p;
732
799
 
733
- expect(fetchedUrls.some(u => u.includes('/health'))).toBe(true);
734
- expect(fetchedUrls.some(u => u.endsWith('/models'))).toBe(false);
800
+ expect(store.updateReceipt).toHaveBeenCalledWith('rcpt-serve-001', expect.objectContaining({ status: 'ready' }));
735
801
  expect(process.exitCode).toBeFalsy();
736
802
  });
737
803
 
738
804
  it('--task image sends task field to backend', async () => {
739
805
  api.callApi.mockResolvedValueOnce(makeServeDep({ model: 'black-forest-labs/FLUX.1-schnell' }));
740
- global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });
741
806
 
742
807
  const p = serveCommand(
743
808
  config,
@@ -753,16 +818,11 @@ describe('--task routing for managed runtimes', () => {
753
818
  expect(process.exitCode).toBeFalsy();
754
819
  });
755
820
 
756
- it('--task embed still polls /models (vLLM path)', async () => {
821
+ it('--task embed becomes ready via backend readiness (health_path=/v1/models)', async () => {
757
822
  api.callApi
758
823
  .mockResolvedValueOnce(makeServeDep({ model: 'BAAI/bge-large-en-v1.5' }))
759
- .mockResolvedValueOnce({ status: 'running' });
760
-
761
- const fetchedUrls = [];
762
- global.fetch = vi.fn().mockImplementation((url) => {
763
- fetchedUrls.push(url);
764
- return Promise.resolve({ ok: true, status: 200 });
765
- });
824
+ .mockResolvedValueOnce({ status: 'running' })
825
+ .mockResolvedValueOnce(readyStatus());
766
826
 
767
827
  const p = serveCommand(
768
828
  config,
@@ -772,8 +832,7 @@ describe('--task routing for managed runtimes', () => {
772
832
  await vi.advanceTimersByTimeAsync(5000);
773
833
  await p;
774
834
 
775
- expect(fetchedUrls.some(u => u.endsWith('/models'))).toBe(true);
776
- expect(fetchedUrls.some(u => u.includes('/health'))).toBe(false);
835
+ expect(store.updateReceipt).toHaveBeenCalledWith('rcpt-serve-001', expect.objectContaining({ status: 'ready' }));
777
836
  expect(process.exitCode).toBeFalsy();
778
837
  });
779
838
  });
@@ -327,18 +327,21 @@ describe('parseTemplateOverrides', () => {
327
327
  // Sequence for serveCommand:
328
328
  // callApi('/serve', ...) → dep (via callWithFallback → callApi)
329
329
  // callApi('/deployments/<id>', ...) → { status: 'running' } (pre-health check)
330
- // global.fetch(endpoint + health_path) → { ok: true }
330
+ // callApi('/deployments/<id>', ...) → { status: 'running', endpoint_ready: true } (waitForEndpoint poll)
331
+ //
332
+ // Readiness comes entirely from Badgr's own deployment status — the CLI never
333
+ // fetches the pod/RunPod-proxy endpoint directly.
331
334
 
332
- function setupServe(depOverrides = {}) {
335
+ function setupServe(depOverrides = {}, readyOverrides = {}) {
333
336
  api.callApi
334
337
  .mockResolvedValueOnce(makeServeDep(depOverrides)) // POST /serve
335
- .mockResolvedValueOnce({ status: 'running' }); // pre-health dep status
336
- global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({}) });
338
+ .mockResolvedValueOnce({ status: 'running' }) // pre-health dep status
339
+ .mockResolvedValueOnce({ status: 'running', endpoint_ready: true, ...readyOverrides }); // waitForEndpoint poll
337
340
  }
338
341
 
339
342
  describe('badgr serve template <name>', () => {
340
- it('vllm: dispatches with correct image, max_cost_usd, and health check on /v1/models', async () => {
341
- setupServe();
343
+ it('vllm: dispatches with correct image, max_cost_usd, and readiness on /v1/models', async () => {
344
+ setupServe({}, { health_path: '/v1/models' });
342
345
  const p = serveCommand(config, ['template', 'vllm', '--max-cost', '5'], chalk);
343
346
  await vi.advanceTimersByTimeAsync(5000);
344
347
  await p;
@@ -347,22 +350,17 @@ describe('badgr serve template <name>', () => {
347
350
  expect(route).toBe('/serve');
348
351
  expect(opts.body.image).toBe('vllm/vllm-openai:latest');
349
352
  expect(opts.body.max_cost_usd).toBe(5);
350
- // health check fetch should use the /v1/models path from the template
351
- const fetchUrl = global.fetch.mock.calls[0]?.[0] ?? '';
352
- expect(fetchUrl).toContain('/v1/models');
353
353
  expect(process.exitCode).toBeFalsy();
354
354
  });
355
355
 
356
- it('comfyui: dispatches with ComfyUI image and health check on /system_stats', async () => {
357
- setupServe();
356
+ it('comfyui: dispatches with ComfyUI image and readiness on /system_stats', async () => {
357
+ setupServe({}, { health_path: '/system_stats' });
358
358
  const p = serveCommand(config, ['template', 'comfyui', '--max-cost', '3'], chalk);
359
359
  await vi.advanceTimersByTimeAsync(5000);
360
360
  await p;
361
361
 
362
362
  const [, opts] = api.callApi.mock.calls[0];
363
363
  expect(opts.body.image).toBe('yanwk/comfyui-boot:cu126-megapak');
364
- const fetchUrl = global.fetch.mock.calls[0]?.[0] ?? '';
365
- expect(fetchUrl).toContain('/system_stats');
366
364
  expect(process.exitCode).toBeFalsy();
367
365
  });
368
366