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.
- package/LICENSE +207 -0
- package/README.md +135 -3
- package/package.json +44 -2
- package/src/api.js +16 -0
- package/src/badgr.js +2 -2
- package/src/commands/batch.js +11 -0
- package/src/commands/comfyui.js +31 -15
- package/src/commands/embed.js +13 -10
- package/src/commands/launch.js +8 -1
- package/src/commands/login.js +75 -20
- package/src/commands/run.js +45 -13
- package/src/commands/sbatch.js +6 -1
- package/src/commands/serve.js +44 -30
- package/src/commands/train.js +8 -12
- package/src/commands/transcribe.js +13 -10
- package/src/envFlag.js +10 -0
- package/src/onboarding.js +8 -1
- package/src/progress.js +48 -0
- package/tests/agent-images.test.js +0 -17
- package/tests/api.test.js +0 -168
- package/tests/artifactDownload.test.js +0 -113
- package/tests/artifacts.test.js +0 -168
- package/tests/batch.test.js +0 -641
- package/tests/browser.test.js +0 -51
- package/tests/capacity.test.js +0 -68
- package/tests/commands.test.js +0 -417
- package/tests/config.test.js +0 -96
- package/tests/connect.test.js +0 -83
- package/tests/detect.test.js +0 -191
- package/tests/down.test.js +0 -150
- package/tests/errors.test.js +0 -130
- package/tests/fallback-timeout.test.js +0 -41
- package/tests/fanout.test.js +0 -124
- package/tests/gpu-doctor-classifiers.test.js +0 -402
- package/tests/gpu-doctor-doctor.test.js +0 -304
- package/tests/gpu-doctor-probe-cache.test.js +0 -110
- package/tests/gpu-doctor-probes.test.js +0 -257
- package/tests/heartbeat.test.js +0 -70
- package/tests/job-progress-poll.test.js +0 -136
- package/tests/launch-command-argv.test.js +0 -93
- package/tests/launch-readiness.test.js +0 -403
- package/tests/launch.test.js +0 -440
- package/tests/onboarding.test.js +0 -134
- package/tests/productized-dry-run.test.js +0 -141
- package/tests/productized-runners.test.js +0 -237
- package/tests/pull.test.js +0 -266
- package/tests/rerun.test.js +0 -94
- package/tests/restart.test.js +0 -88
- package/tests/router.test.js +0 -98
- package/tests/run-lifecycle.test.js +0 -1054
- package/tests/sbatch.test.js +0 -190
- package/tests/secrets.test.js +0 -16
- package/tests/serve-apps.test.js +0 -189
- package/tests/serve-lifecycle.test.js +0 -931
- package/tests/slurm.test.js +0 -77
- package/tests/spec.test.js +0 -201
- package/tests/status.test.js +0 -73
- package/tests/store.test.js +0 -187
- package/tests/task.test.js +0 -109
- package/tests/template.test.js +0 -556
- package/tests/train-lora-dataset.test.js +0 -176
- package/tests/upload.test.js +0 -79
- package/tests/workload-rerun.test.js +0 -56
- package/tests/workload-spec.test.js +0 -180
- package/tests/workload-templates.test.js +0 -865
- package/tests/workload-workspace-paths.test.js +0 -46
package/tests/template.test.js
DELETED
|
@@ -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
|
-
});
|