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/sbatch.test.js
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* badgr sbatch — /run body translation from #SBATCH directives, dry-run
|
|
3
|
-
* behavior, and job-array fan-out (one deployment per task index).
|
|
4
|
-
*/
|
|
5
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
6
|
-
import { mkdtempSync, writeFileSync, rmSync } from 'fs';
|
|
7
|
-
import { tmpdir } from 'os';
|
|
8
|
-
import { join } from 'path';
|
|
9
|
-
|
|
10
|
-
vi.mock('../src/store.js', () => ({
|
|
11
|
-
selectedComputeFromDeployment: (dep) => ({
|
|
12
|
-
gpu: dep.gpu_type ?? null,
|
|
13
|
-
gpuCount: dep.gpu_count ?? null,
|
|
14
|
-
vcpus: dep.selected_vcpus ?? null,
|
|
15
|
-
ramGb: dep.selected_ram_gb ?? null,
|
|
16
|
-
vramGb: dep.selected_vram_gb ?? null,
|
|
17
|
-
}),
|
|
18
|
-
addReceipt: vi.fn(),
|
|
19
|
-
updateReceipt: vi.fn(),
|
|
20
|
-
generateReceiptId: vi.fn(() => 'rcpt-sbatch-001'),
|
|
21
|
-
loadStore: vi.fn(() => ({ receipts: [], deployments: [] })),
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
vi.mock('../src/api.js', () => ({
|
|
25
|
-
callApi: vi.fn(),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
vi.mock('../src/fallback.js', async (importOriginal) => {
|
|
29
|
-
const actual = await importOriginal();
|
|
30
|
-
return { ...actual, callWithFallback: vi.fn() };
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
vi.mock('../src/batch.js', () => ({
|
|
34
|
-
monitorBatchJob: vi.fn(() => Promise.resolve({ status: 'succeeded', exitCode: 0, runtimeMs: 5000, reason: 'complete' })),
|
|
35
|
-
fmtRuntime: (ms) => `${Math.round(ms / 1000)}s`,
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
import { sbatchCommand, buildRunBody } from '../src/commands/sbatch.js';
|
|
39
|
-
import * as store from '../src/store.js';
|
|
40
|
-
import * as api from '../src/api.js';
|
|
41
|
-
import * as fallback from '../src/fallback.js';
|
|
42
|
-
|
|
43
|
-
const chalk = {
|
|
44
|
-
bold: s => s, dim: s => s, red: s => s, yellow: s => s, green: s => s, cyan: s => s,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const config = { apiKey: 'sk-test', baseUrl: 'https://api.test/v1' };
|
|
48
|
-
|
|
49
|
-
let dir;
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
dir = mkdtempSync(join(tmpdir(), 'badgr-sbatch-cmd-test-'));
|
|
52
|
-
process.exitCode = undefined;
|
|
53
|
-
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
54
|
-
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
55
|
-
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
56
|
-
vi.clearAllMocks();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
afterEach(() => {
|
|
60
|
-
rmSync(dir, { recursive: true, force: true });
|
|
61
|
-
vi.restoreAllMocks();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe('buildRunBody', () => {
|
|
65
|
-
it('translates a GPU job into the /run body shape', () => {
|
|
66
|
-
const job = {
|
|
67
|
-
name: 'train', command: 'python train.py', cpus: 32, memGb: 128,
|
|
68
|
-
gpuCount: 2, gpuType: 'a100', env: { FOO: 'bar' },
|
|
69
|
-
};
|
|
70
|
-
const body = buildRunBody(job, {
|
|
71
|
-
image: 'python:3.11-slim', tier: '1', region: null,
|
|
72
|
-
maxCostUsd: 5, maxRuntimeMinutes: 60,
|
|
73
|
-
});
|
|
74
|
-
expect(body.command).toEqual(['bash', '-lc', 'python train.py']);
|
|
75
|
-
expect(body.gpu).toBe('A100');
|
|
76
|
-
expect(body.gpu_count).toBe(2);
|
|
77
|
-
expect(body.cpu).toBe(32);
|
|
78
|
-
expect(body.memory_gb).toBe(128);
|
|
79
|
-
expect(body.env).toEqual({ FOO: 'bar' });
|
|
80
|
-
expect(body.max_cost_usd).toBe(5);
|
|
81
|
-
expect(body.max_runtime_seconds).toBe(3600);
|
|
82
|
-
expect(body.name).toBe('train');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('requests no GPU when the script has no gres/gpus directive', () => {
|
|
86
|
-
const job = { name: 'cpu-job', command: 'echo hi', cpus: null, memGb: null, gpuCount: 0, gpuType: null, env: {} };
|
|
87
|
-
const body = buildRunBody(job, { image: 'python:3.11-slim', tier: '1', region: null, maxCostUsd: 1, maxRuntimeMinutes: 10 });
|
|
88
|
-
expect(body.gpu).toBe('NONE');
|
|
89
|
-
// Backend's RunBody requires gpu_count >= 1; CPU-only routing goes through
|
|
90
|
-
// the separate no_gpu flag instead of gpu_count: 0 (which the real
|
|
91
|
-
// Pydantic-validated backend rejects with a 422 before provisioning).
|
|
92
|
-
expect(body.gpu_count).toBe(1);
|
|
93
|
-
expect(body.no_gpu).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('suffixes the name and injects SLURM_ARRAY_TASK_ID for array tasks', () => {
|
|
97
|
-
const job = { name: 'screen', command: 'python screen.py', cpus: null, memGb: null, gpuCount: 1, gpuType: null, env: {} };
|
|
98
|
-
const body = buildRunBody(job, { image: 'python:3.11-slim', tier: '1', region: null, maxCostUsd: 1, maxRuntimeMinutes: 10, taskId: 7, arrayJobId: 'rcpt-x' });
|
|
99
|
-
expect(body.name).toBe('screen-7');
|
|
100
|
-
expect(body.env.SLURM_ARRAY_TASK_ID).toBe('7');
|
|
101
|
-
expect(body.env.SLURM_ARRAY_JOB_ID).toBe('rcpt-x');
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe('sbatchCommand', () => {
|
|
106
|
-
function writeScript(name, contents) {
|
|
107
|
-
const p = join(dir, name);
|
|
108
|
-
writeFileSync(p, contents);
|
|
109
|
-
return p;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
it('dry-run parses and prints the plan without calling the network', async () => {
|
|
113
|
-
const scriptPath = writeScript('job.slurm', `#!/bin/bash\n#SBATCH --job-name=t\n#SBATCH --gres=gpu:1\npython run.py\n`);
|
|
114
|
-
await sbatchCommand(config, [scriptPath, '--dry-run'], chalk);
|
|
115
|
-
expect(fallback.callWithFallback).not.toHaveBeenCalled();
|
|
116
|
-
expect(process.exitCode).toBeUndefined();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('submits a single job and reports success', async () => {
|
|
120
|
-
const scriptPath = writeScript('job.slurm', `#!/bin/bash\n#SBATCH --job-name=t\n#SBATCH --gres=gpu:1\npython run.py\n`);
|
|
121
|
-
|
|
122
|
-
fallback.callWithFallback.mockResolvedValue({
|
|
123
|
-
deployment_id: 'dep-1', receipt_id: 'rcpt-1', gpu_type: 'RTX_4090',
|
|
124
|
-
gpu_count: 1, cost_per_hour: 0.5, provider: 'runpod', tier: '1', status: 'running',
|
|
125
|
-
});
|
|
126
|
-
api.callApi.mockResolvedValue({
|
|
127
|
-
status: 'succeeded', failure_reason: null, teardown_ok: 'ok',
|
|
128
|
-
runtime_seconds: 5, accrued_cost_usd: 0.01, provider: 'runpod',
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
await sbatchCommand(config, [scriptPath], chalk);
|
|
132
|
-
|
|
133
|
-
expect(fallback.callWithFallback).toHaveBeenCalledTimes(1);
|
|
134
|
-
expect(store.addReceipt).toHaveBeenCalledTimes(1);
|
|
135
|
-
expect(store.addReceipt).toHaveBeenCalledWith(expect.objectContaining({
|
|
136
|
-
workloadShape: 'slurm',
|
|
137
|
-
computeSelected: expect.objectContaining({ gpu: 'RTX_4090', gpuCount: 1 }),
|
|
138
|
-
}));
|
|
139
|
-
expect(process.exitCode).toBeUndefined();
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('fans out array jobs into one deployment per task', async () => {
|
|
143
|
-
const scriptPath = writeScript('array.slurm', `#!/bin/bash\n#SBATCH --job-name=screen\n#SBATCH --array=1-3\n#SBATCH --gpus=1\npython screen.py\n`);
|
|
144
|
-
|
|
145
|
-
let callCount = 0;
|
|
146
|
-
fallback.callWithFallback.mockImplementation(() => {
|
|
147
|
-
callCount += 1;
|
|
148
|
-
return Promise.resolve({
|
|
149
|
-
deployment_id: `dep-${callCount}`, receipt_id: `rcpt-${callCount}`, gpu_type: 'RTX_4090',
|
|
150
|
-
gpu_count: 1, cost_per_hour: 0.5, provider: 'runpod', tier: '1', status: 'running',
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
api.callApi.mockResolvedValue({
|
|
154
|
-
status: 'succeeded', failure_reason: null, teardown_ok: 'ok',
|
|
155
|
-
runtime_seconds: 5, accrued_cost_usd: 0.01, provider: 'runpod',
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
await sbatchCommand(config, [scriptPath], chalk);
|
|
159
|
-
|
|
160
|
-
expect(fallback.callWithFallback).toHaveBeenCalledTimes(3);
|
|
161
|
-
expect(store.addReceipt).toHaveBeenCalledTimes(3);
|
|
162
|
-
expect(store.addReceipt).toHaveBeenCalledWith(expect.objectContaining({ workloadShape: 'slurm-array' }));
|
|
163
|
-
expect(process.exitCode).toBeUndefined();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('reports failure exit code when a task does not succeed', async () => {
|
|
167
|
-
const scriptPath = writeScript('job.slurm', `#!/bin/bash\n#SBATCH --gres=gpu:1\npython run.py\n`);
|
|
168
|
-
const { monitorBatchJob } = await import('../src/batch.js');
|
|
169
|
-
monitorBatchJob.mockResolvedValueOnce({ status: 'failed', exitCode: 1, runtimeMs: 1000, reason: 'infrastructure' });
|
|
170
|
-
|
|
171
|
-
fallback.callWithFallback.mockResolvedValue({
|
|
172
|
-
deployment_id: 'dep-1', receipt_id: 'rcpt-1', gpu_type: 'RTX_4090',
|
|
173
|
-
gpu_count: 1, cost_per_hour: 0.5, provider: 'runpod', tier: '1', status: 'running',
|
|
174
|
-
});
|
|
175
|
-
api.callApi.mockResolvedValue({
|
|
176
|
-
status: 'failed', failure_reason: 'infrastructure', teardown_ok: 'ok',
|
|
177
|
-
runtime_seconds: 1, accrued_cost_usd: 0.01, provider: 'runpod',
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
await sbatchCommand(config, [scriptPath], chalk);
|
|
181
|
-
expect(process.exitCode).toBe(1);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('errors before touching the network when the script has no command', async () => {
|
|
185
|
-
const scriptPath = writeScript('empty.slurm', `#!/bin/bash\n#SBATCH --job-name=empty\n`);
|
|
186
|
-
await sbatchCommand(config, [scriptPath], chalk);
|
|
187
|
-
expect(process.exitCode).toBe(1);
|
|
188
|
-
expect(fallback.callWithFallback).not.toHaveBeenCalled();
|
|
189
|
-
});
|
|
190
|
-
});
|
package/tests/secrets.test.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
describe('badgr secrets (removed)', () => {
|
|
6
|
-
it('no longer ships commands/secrets.js', () => {
|
|
7
|
-
const file = path.join(__dirname, '../src/commands/secrets.js');
|
|
8
|
-
expect(fs.existsSync(file)).toBe(false);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('badgr.js has no case for the secrets command', async () => {
|
|
12
|
-
const badgrSrc = fs.readFileSync(path.join(__dirname, '../src/badgr.js'), 'utf8');
|
|
13
|
-
expect(badgrSrc).not.toMatch(/case 'secrets'/);
|
|
14
|
-
expect(badgrSrc).not.toContain('commands/secrets.js');
|
|
15
|
-
});
|
|
16
|
-
});
|
package/tests/serve-apps.test.js
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `badgr serve openwebui` — a chat UI that connects to a model endpoint.
|
|
3
|
-
*
|
|
4
|
-
* Reuses a running vLLM endpoint for the requested model if one exists, or
|
|
5
|
-
* launches one first, then wires OPENAI_API_BASE_URL/OPENAI_API_KEY to it —
|
|
6
|
-
* via the same serveCommand/template machinery `badgr serve template <name>`
|
|
7
|
-
* already uses. No new provisioning path.
|
|
8
|
-
*/
|
|
9
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
10
|
-
import { serveCommand } from '../src/commands/serve.js';
|
|
11
|
-
|
|
12
|
-
vi.mock('../src/api.js', () => ({
|
|
13
|
-
callApi: vi.fn(),
|
|
14
|
-
terminateDeployment: vi.fn().mockResolvedValue({}),
|
|
15
|
-
listDeployments: vi.fn().mockResolvedValue({ deployments: [], count: 0 }),
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
vi.mock('../src/store.js', () => ({
|
|
19
|
-
addDeployment: vi.fn(),
|
|
20
|
-
addReceipt: vi.fn(),
|
|
21
|
-
updateReceipt: vi.fn(),
|
|
22
|
-
generateReceiptId: vi.fn(() => 'rcpt-app-001'),
|
|
23
|
-
generateDeploymentId: vi.fn(() => 'dep-app-001'),
|
|
24
|
-
listDeployments: vi.fn(() => []),
|
|
25
|
-
listReceipts: vi.fn(() => []),
|
|
26
|
-
findDeployment: vi.fn(() => null),
|
|
27
|
-
updateDeployment: vi.fn(),
|
|
28
|
-
removeDeployment: vi.fn(),
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
import * as api from '../src/api.js';
|
|
32
|
-
import * as store from '../src/store.js';
|
|
33
|
-
|
|
34
|
-
const config = { apiKey: 'sk-test', baseUrl: 'https://api.test/v1' };
|
|
35
|
-
const chalk = {
|
|
36
|
-
bold: s => s, dim: s => s, red: s => s, yellow: s => s, green: s => s, cyan: s => s,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
function makeServeDep(overrides = {}) {
|
|
40
|
-
return {
|
|
41
|
-
deployment_id: 'dep-app-001',
|
|
42
|
-
status: 'running',
|
|
43
|
-
gpu_type: 'RTX_4090',
|
|
44
|
-
gpu_count: 1,
|
|
45
|
-
cost_per_hour: 0.50,
|
|
46
|
-
provider: 'runpod',
|
|
47
|
-
receipt_id: 'rcpt-app-001',
|
|
48
|
-
tier: '1',
|
|
49
|
-
endpoint_url: 'https://dep-app-001.aibadgr.com/v1',
|
|
50
|
-
...overrides,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Queues one POST /serve → status → ready cycle onto api.callApi.
|
|
55
|
-
function queueServeCycle(depOverrides = {}) {
|
|
56
|
-
api.callApi
|
|
57
|
-
.mockResolvedValueOnce(makeServeDep(depOverrides))
|
|
58
|
-
.mockResolvedValueOnce({ status: 'running' })
|
|
59
|
-
.mockResolvedValueOnce({ status: 'running', endpoint_ready: true });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
beforeEach(() => {
|
|
63
|
-
vi.useFakeTimers();
|
|
64
|
-
process.exitCode = undefined;
|
|
65
|
-
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
66
|
-
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
67
|
-
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
68
|
-
vi.clearAllMocks();
|
|
69
|
-
store.generateReceiptId.mockReturnValue('rcpt-app-001');
|
|
70
|
-
api.terminateDeployment.mockResolvedValue({});
|
|
71
|
-
api.listDeployments.mockResolvedValue({ deployments: [], count: 0 });
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe('badgr serve openwebui — reuses a running vLLM endpoint', () => {
|
|
75
|
-
it('connects to an existing endpoint instead of launching a second one', async () => {
|
|
76
|
-
api.listDeployments.mockResolvedValueOnce({
|
|
77
|
-
deployments: [{
|
|
78
|
-
status: 'running',
|
|
79
|
-
workload_type: 'endpoint',
|
|
80
|
-
model: 'Qwen/Qwen2.5-7B-Instruct',
|
|
81
|
-
endpoint_url: 'https://dep-existing.aibadgr.com/v1',
|
|
82
|
-
}],
|
|
83
|
-
count: 1,
|
|
84
|
-
});
|
|
85
|
-
queueServeCycle(); // only one cycle — openwebui itself
|
|
86
|
-
|
|
87
|
-
const p = serveCommand(config, ['openwebui', '--model', 'qwen-7b', '--max-cost', '5'], chalk);
|
|
88
|
-
await vi.advanceTimersByTimeAsync(5000);
|
|
89
|
-
await p;
|
|
90
|
-
|
|
91
|
-
expect(api.callApi.mock.calls.length).toBe(3); // no second /serve for vLLM
|
|
92
|
-
const [, opts] = api.callApi.mock.calls[0];
|
|
93
|
-
expect(opts.body.image).toBe('ghcr.io/open-webui/open-webui:main');
|
|
94
|
-
expect(opts.body.env.OPENAI_API_BASE_URL).toBe('https://dep-existing.aibadgr.com/v1');
|
|
95
|
-
expect(process.exitCode).toBeFalsy();
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe('badgr serve openwebui — launches vLLM first when none is running', () => {
|
|
100
|
-
it('serves vLLM (warning about the separate budget), then Open WebUI wired to the new endpoint', async () => {
|
|
101
|
-
// listDeployments is called 4 times: pre-launch discovery (none) →
|
|
102
|
-
// vLLM's own duplicate check (none) → post-launch discovery (found) →
|
|
103
|
-
// Open WebUI's own duplicate check (none — no image field to match).
|
|
104
|
-
api.listDeployments
|
|
105
|
-
.mockResolvedValueOnce({ deployments: [], count: 0 })
|
|
106
|
-
.mockResolvedValueOnce({ deployments: [], count: 0 })
|
|
107
|
-
.mockResolvedValueOnce({
|
|
108
|
-
deployments: [{
|
|
109
|
-
deployment_id: 'dep-vllm-new',
|
|
110
|
-
status: 'running',
|
|
111
|
-
workload_type: 'endpoint',
|
|
112
|
-
model: 'Qwen/Qwen2.5-7B-Instruct',
|
|
113
|
-
endpoint_url: 'https://dep-new.aibadgr.com/v1',
|
|
114
|
-
}],
|
|
115
|
-
count: 1,
|
|
116
|
-
});
|
|
117
|
-
queueServeCycle({ model: 'Qwen/Qwen2.5-7B-Instruct' }); // vLLM launch
|
|
118
|
-
queueServeCycle(); // Open WebUI launch
|
|
119
|
-
|
|
120
|
-
const p = serveCommand(config, ['openwebui', '--model', 'qwen-7b', '--max-cost', '5'], chalk);
|
|
121
|
-
await vi.advanceTimersByTimeAsync(10_000);
|
|
122
|
-
await p;
|
|
123
|
-
|
|
124
|
-
expect(api.callApi.mock.calls.length).toBe(6);
|
|
125
|
-
const [, vllmOpts] = api.callApi.mock.calls[0];
|
|
126
|
-
expect(vllmOpts.body.model).toBe('Qwen/Qwen2.5-7B-Instruct');
|
|
127
|
-
const [, webuiOpts] = api.callApi.mock.calls[3];
|
|
128
|
-
expect(webuiOpts.body.image).toBe('ghcr.io/open-webui/open-webui:main');
|
|
129
|
-
expect(webuiOpts.body.env.OPENAI_API_BASE_URL).toBe('https://dep-new.aibadgr.com/v1');
|
|
130
|
-
expect(process.exitCode).toBeFalsy();
|
|
131
|
-
|
|
132
|
-
// --max-cost applies to both the auto-launched vLLM and Open WebUI
|
|
133
|
-
// separately — the CLI must say so rather than double-spend silently.
|
|
134
|
-
const logged = console.log.mock.calls.flat().join('\n');
|
|
135
|
-
expect(logged).toContain('own --max-cost $5 cap');
|
|
136
|
-
expect(logged).toContain('Total possible spend across both deployments: ~$10.00');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('prints a badgr down hint for the auto-launched vLLM deployment if Open WebUI then fails', async () => {
|
|
140
|
-
api.listDeployments
|
|
141
|
-
.mockResolvedValueOnce({ deployments: [], count: 0 }) // pre-launch discovery
|
|
142
|
-
.mockResolvedValueOnce({ deployments: [], count: 0 }) // vLLM's own dup-check
|
|
143
|
-
.mockResolvedValueOnce({ // post-launch discovery — vLLM is up
|
|
144
|
-
deployments: [{
|
|
145
|
-
deployment_id: 'dep-vllm-999',
|
|
146
|
-
status: 'running',
|
|
147
|
-
workload_type: 'endpoint',
|
|
148
|
-
model: 'Qwen/Qwen2.5-7B-Instruct',
|
|
149
|
-
endpoint_url: 'https://dep-vllm-999.aibadgr.com/v1',
|
|
150
|
-
}],
|
|
151
|
-
count: 1,
|
|
152
|
-
})
|
|
153
|
-
.mockResolvedValueOnce({ deployments: [], count: 0 }); // Open WebUI's own dup-check
|
|
154
|
-
|
|
155
|
-
queueServeCycle({ model: 'Qwen/Qwen2.5-7B-Instruct' }); // vLLM launch succeeds
|
|
156
|
-
// Open WebUI's own POST /serve "succeeds" but hands back no endpoint URL —
|
|
157
|
-
// serveCommand's own no-endpoint-url guard fires and sets exitCode = 1.
|
|
158
|
-
api.callApi.mockResolvedValueOnce(makeServeDep({ endpoint_url: undefined }));
|
|
159
|
-
|
|
160
|
-
const p = serveCommand(config, ['openwebui', '--model', 'qwen-7b', '--max-cost', '5'], chalk);
|
|
161
|
-
await vi.advanceTimersByTimeAsync(10_000);
|
|
162
|
-
await p;
|
|
163
|
-
|
|
164
|
-
expect(process.exitCode).toBe(1);
|
|
165
|
-
const logged = console.error.mock.calls.flat().join('\n');
|
|
166
|
-
expect(logged).toContain('badgr down dep-vllm-999');
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe('badgr serve openwebui --connect', () => {
|
|
171
|
-
it('skips vLLM discovery/launch entirely and wires the given URL', async () => {
|
|
172
|
-
queueServeCycle();
|
|
173
|
-
const p = serveCommand(
|
|
174
|
-
config,
|
|
175
|
-
['openwebui', '--connect', 'https://my-server.example.com/v1', '--max-cost', '5'],
|
|
176
|
-
chalk,
|
|
177
|
-
);
|
|
178
|
-
await vi.advanceTimersByTimeAsync(5000);
|
|
179
|
-
await p;
|
|
180
|
-
|
|
181
|
-
// Only the one serve cycle ran (Open WebUI itself) — no vLLM discovery/launch.
|
|
182
|
-
expect(api.callApi.mock.calls.length).toBe(3);
|
|
183
|
-
const [, opts] = api.callApi.mock.calls[0];
|
|
184
|
-
expect(opts.body.env.OPENAI_API_BASE_URL).toBe('https://my-server.example.com/v1');
|
|
185
|
-
|
|
186
|
-
const logged = console.log.mock.calls.flat().join('\n');
|
|
187
|
-
expect(logged).not.toContain('needs a model endpoint behind it');
|
|
188
|
-
});
|
|
189
|
-
});
|