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
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* --dry-run for the productized runner commands (badgr train lora, badgr comfyui batch).
|
|
3
|
-
*
|
|
4
|
-
* Both must preview the request (preset/workflow, GPU, cost) without calling the API,
|
|
5
|
-
* uploading files, or requiring an API key — mirroring the existing `badgr run --dry-run`
|
|
6
|
-
* and `badgr up --dry-run` behavior.
|
|
7
|
-
*/
|
|
8
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
9
|
-
import { trainLoraCommand, parseTrainLoraArgs } from '../src/commands/train.js';
|
|
10
|
-
import { comfyBatchCommand, parseComfyBatchArgs } from '../src/commands/comfyui.js';
|
|
11
|
-
|
|
12
|
-
vi.mock('../src/api.js', () => ({
|
|
13
|
-
callApi: vi.fn(),
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
vi.mock('../src/store.js', () => ({
|
|
17
|
-
addReceipt: vi.fn(),
|
|
18
|
-
generateReceiptId: vi.fn(() => 'rcpt-dry-001'),
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
vi.mock('fs', async (importOriginal) => {
|
|
22
|
-
const actual = await importOriginal();
|
|
23
|
-
return {
|
|
24
|
-
...actual,
|
|
25
|
-
readFileSync: vi.fn(),
|
|
26
|
-
existsSync: vi.fn(),
|
|
27
|
-
};
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
import * as api from '../src/api.js';
|
|
31
|
-
import * as store from '../src/store.js';
|
|
32
|
-
import * as fs from 'fs';
|
|
33
|
-
|
|
34
|
-
const chalk = {
|
|
35
|
-
bold: s => s, dim: s => s, red: s => s, yellow: s => s, green: s => s, cyan: s => s,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const config = { apiKey: 'sk-test', baseUrl: 'https://api.test/v1' };
|
|
39
|
-
|
|
40
|
-
beforeEach(() => {
|
|
41
|
-
process.exitCode = undefined;
|
|
42
|
-
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
43
|
-
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
44
|
-
vi.clearAllMocks();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
afterEach(() => {
|
|
48
|
-
vi.restoreAllMocks();
|
|
49
|
-
process.exitCode = undefined;
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe('parseTrainLoraArgs', () => {
|
|
53
|
-
it('parses --dry-run', () => {
|
|
54
|
-
const flags = parseTrainLoraArgs(['--base-model', 'x', '--dry-run']);
|
|
55
|
-
expect(flags.dryRun).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('trainLoraCommand --dry-run', () => {
|
|
60
|
-
it('previews the job without calling the API or requiring an API key', async () => {
|
|
61
|
-
await trainLoraCommand({ apiKey: null, baseUrl: 'https://api.test/v1' }, [
|
|
62
|
-
'--base-model', 'mistralai/Mistral-7B-v0.1',
|
|
63
|
-
'--dataset', 'https://example.com/data.jsonl',
|
|
64
|
-
'--dry-run',
|
|
65
|
-
], chalk);
|
|
66
|
-
|
|
67
|
-
expect(api.callApi).not.toHaveBeenCalled();
|
|
68
|
-
expect(store.addReceipt).not.toHaveBeenCalled();
|
|
69
|
-
expect(process.exitCode).toBeFalsy();
|
|
70
|
-
const logged = console.log.mock.calls.flat().join('\n');
|
|
71
|
-
expect(logged).toContain('Dry run');
|
|
72
|
-
expect(logged).toContain('mistralai/Mistral-7B-v0.1');
|
|
73
|
-
expect(logged).toContain('RTX_4090'); // default 'small' preset GPU
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('does not upload a local dataset file during --dry-run', async () => {
|
|
77
|
-
fs.existsSync.mockReturnValue(true);
|
|
78
|
-
fs.readFileSync.mockReturnValue('dummy');
|
|
79
|
-
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ upload_id: 'up_x' }) });
|
|
80
|
-
|
|
81
|
-
await trainLoraCommand(config, [
|
|
82
|
-
'--base-model', 'mistralai/Mistral-7B-v0.1',
|
|
83
|
-
'--dataset', './local.jsonl',
|
|
84
|
-
'--dry-run',
|
|
85
|
-
], chalk);
|
|
86
|
-
|
|
87
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
88
|
-
fetchSpy.mockRestore();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('still requires --base-model even with --dry-run', async () => {
|
|
92
|
-
await trainLoraCommand(config, ['--dry-run'], chalk);
|
|
93
|
-
expect(process.exitCode).toBe(1);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('flags an unknown preset instead of silently accepting it', async () => {
|
|
97
|
-
await trainLoraCommand(config, [
|
|
98
|
-
'--base-model', 'mistralai/Mistral-7B-v0.1',
|
|
99
|
-
'--preset', 'huge',
|
|
100
|
-
'--dry-run',
|
|
101
|
-
], chalk);
|
|
102
|
-
const logged = console.log.mock.calls.flat().join('\n');
|
|
103
|
-
expect(logged).toContain('unknown');
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('parseComfyBatchArgs', () => {
|
|
108
|
-
it('parses --dry-run', () => {
|
|
109
|
-
const flags = parseComfyBatchArgs(['--workflow', 'sdxl-basic', '--dry-run']);
|
|
110
|
-
expect(flags.dryRun).toBe(true);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe('comfyBatchCommand --dry-run', () => {
|
|
115
|
-
it('previews the batch without calling the API or requiring an API key', async () => {
|
|
116
|
-
await comfyBatchCommand({ apiKey: null, baseUrl: 'https://api.test/v1' }, [
|
|
117
|
-
'--workflow', 'sdxl-basic',
|
|
118
|
-
'--prompt', 'a cat on a beach',
|
|
119
|
-
'--dry-run',
|
|
120
|
-
], chalk);
|
|
121
|
-
|
|
122
|
-
expect(api.callApi).not.toHaveBeenCalled();
|
|
123
|
-
expect(store.addReceipt).not.toHaveBeenCalled();
|
|
124
|
-
expect(process.exitCode).toBeFalsy();
|
|
125
|
-
const logged = console.log.mock.calls.flat().join('\n');
|
|
126
|
-
expect(logged).toContain('Dry run');
|
|
127
|
-
expect(logged).toContain('sdxl-basic');
|
|
128
|
-
expect(logged).toContain('RTX_4090');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('warns when the prompt count exceeds the 20-prompt cap', async () => {
|
|
132
|
-
const flags = { workflow: 'sdxl-basic', inlinePrompts: Array(21).fill('x') };
|
|
133
|
-
await comfyBatchCommand({ apiKey: null, baseUrl: 'https://api.test/v1' }, [
|
|
134
|
-
'--workflow', 'sdxl-basic',
|
|
135
|
-
...Array(21).fill(['--prompt', 'x']).flat(),
|
|
136
|
-
'--dry-run',
|
|
137
|
-
], chalk);
|
|
138
|
-
const logged = console.log.mock.calls.flat().join('\n');
|
|
139
|
-
expect(logged).toContain('exceeds the 20-prompt limit');
|
|
140
|
-
});
|
|
141
|
-
});
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Productized runner tests
|
|
3
|
-
*
|
|
4
|
-
* Covers:
|
|
5
|
-
* - Blessed vLLM aliases expand to full model ID + correct GPU
|
|
6
|
-
* - serveCommand sends full model ID to API when alias is given
|
|
7
|
-
* - BLESSED_VLLM_MODELS has required fields
|
|
8
|
-
* - BLESSED_COMFY_WORKFLOWS has required fields
|
|
9
|
-
*/
|
|
10
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
|
-
import { parseServeArgs, serveCommand } from '../src/commands/serve.js';
|
|
12
|
-
import { BLESSED_VLLM_MODELS, BLESSED_COMFY_WORKFLOWS } from '../src/catalog.js';
|
|
13
|
-
|
|
14
|
-
// ── Module mocks ──────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
vi.mock('../src/api.js', () => ({
|
|
17
|
-
callApi: vi.fn(),
|
|
18
|
-
terminateDeployment: vi.fn().mockResolvedValue({}),
|
|
19
|
-
listDeployments: vi.fn().mockResolvedValue({ deployments: [], count: 0 }),
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
vi.mock('../src/store.js', () => ({
|
|
23
|
-
addDeployment: vi.fn(),
|
|
24
|
-
addReceipt: vi.fn(),
|
|
25
|
-
updateReceipt: vi.fn(),
|
|
26
|
-
generateReceiptId: vi.fn(() => 'rcpt-prod-001'),
|
|
27
|
-
generateDeploymentId: vi.fn(() => 'dep-prod-001'),
|
|
28
|
-
listDeployments: vi.fn(() => []),
|
|
29
|
-
listReceipts: vi.fn(() => []),
|
|
30
|
-
findDeployment: vi.fn(() => null),
|
|
31
|
-
updateDeployment: vi.fn(),
|
|
32
|
-
removeDeployment: vi.fn(),
|
|
33
|
-
}));
|
|
34
|
-
|
|
35
|
-
vi.mock('../src/fallback.js', () => ({
|
|
36
|
-
normalizeTier: vi.fn(t => t || null),
|
|
37
|
-
callWithFallback: vi.fn(),
|
|
38
|
-
HIGH_RATE_THRESHOLD: 2.0,
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
vi.mock('../src/config.js', () => ({
|
|
42
|
-
requireApiKey: vi.fn(),
|
|
43
|
-
}));
|
|
44
|
-
|
|
45
|
-
vi.mock('../src/errors.js', () => ({
|
|
46
|
-
formatCliError: vi.fn((code, ctx) => `Error: ${code}`),
|
|
47
|
-
}));
|
|
48
|
-
|
|
49
|
-
import * as fallback from '../src/fallback.js';
|
|
50
|
-
|
|
51
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
const config = { apiKey: 'sk-test', baseUrl: 'https://api.test/v1' };
|
|
54
|
-
|
|
55
|
-
const chalk = {
|
|
56
|
-
bold: s => s,
|
|
57
|
-
dim: s => s,
|
|
58
|
-
red: s => s,
|
|
59
|
-
yellow: s => s,
|
|
60
|
-
green: s => s,
|
|
61
|
-
cyan: s => s,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
function makeServeDep(overrides = {}) {
|
|
65
|
-
return {
|
|
66
|
-
deployment_id: 'dep-prod-001',
|
|
67
|
-
status: 'running',
|
|
68
|
-
gpu_type: 'RTX_4090',
|
|
69
|
-
gpu_count: 1,
|
|
70
|
-
cost_per_hour: 1.20,
|
|
71
|
-
provider: 'runpod',
|
|
72
|
-
receipt_id: 'rcpt-prod-001',
|
|
73
|
-
tier: '1',
|
|
74
|
-
endpoint_url: 'https://dep-prod-001.aibadgr.com/v1',
|
|
75
|
-
...overrides,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
beforeEach(() => {
|
|
80
|
-
vi.clearAllMocks();
|
|
81
|
-
process.exitCode = undefined;
|
|
82
|
-
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
83
|
-
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
84
|
-
vi.spyOn(process.stdout, 'write').mockImplementation(() => {});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// ── Catalog shape tests ───────────────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
describe('BLESSED_VLLM_MODELS catalog', () => {
|
|
90
|
-
it('has qwen-7b, llama-8b, qwen-coder-7b', () => {
|
|
91
|
-
expect(Object.keys(BLESSED_VLLM_MODELS)).toEqual(
|
|
92
|
-
expect.arrayContaining(['qwen-7b', 'llama-8b', 'qwen-coder-7b'])
|
|
93
|
-
);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('each entry has model_id, gpu_type, image, health_path', () => {
|
|
97
|
-
for (const [alias, spec] of Object.entries(BLESSED_VLLM_MODELS)) {
|
|
98
|
-
expect(spec.model_id, `${alias}.model_id`).toBeTruthy();
|
|
99
|
-
expect(spec.gpu_type, `${alias}.gpu_type`).toBeTruthy();
|
|
100
|
-
expect(spec.image, `${alias}.image`).toBeTruthy();
|
|
101
|
-
expect(spec.health_path, `${alias}.health_path`).toBeTruthy();
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('qwen-7b resolves to Qwen2.5-7B-Instruct on RTX_4090', () => {
|
|
106
|
-
const spec = BLESSED_VLLM_MODELS['qwen-7b'];
|
|
107
|
-
expect(spec.model_id).toBe('Qwen/Qwen2.5-7B-Instruct');
|
|
108
|
-
expect(spec.gpu_type).toBe('RTX_4090');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('llama-8b resolves to Llama-3.1-8B-Instruct on RTX_4090', () => {
|
|
112
|
-
const spec = BLESSED_VLLM_MODELS['llama-8b'];
|
|
113
|
-
expect(spec.model_id).toBe('meta-llama/Llama-3.1-8B-Instruct');
|
|
114
|
-
expect(spec.gpu_type).toBe('RTX_4090');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('qwen-coder-7b resolves to Qwen2.5-Coder-7B-Instruct on RTX_4090', () => {
|
|
118
|
-
const spec = BLESSED_VLLM_MODELS['qwen-coder-7b'];
|
|
119
|
-
expect(spec.model_id).toBe('Qwen/Qwen2.5-Coder-7B-Instruct');
|
|
120
|
-
expect(spec.gpu_type).toBe('RTX_4090');
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe('BLESSED_COMFY_WORKFLOWS catalog', () => {
|
|
125
|
-
it('has sdxl-basic', () => {
|
|
126
|
-
expect(BLESSED_COMFY_WORKFLOWS['sdxl-basic']).toBeDefined();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('sdxl-basic has gpu_type and output_type', () => {
|
|
130
|
-
const wf = BLESSED_COMFY_WORKFLOWS['sdxl-basic'];
|
|
131
|
-
expect(wf.gpu_type).toBe('RTX_4090');
|
|
132
|
-
expect(wf.output_type).toBe('images');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('has flux-basic with gpu_type and output_type', () => {
|
|
136
|
-
const wf = BLESSED_COMFY_WORKFLOWS['flux-basic'];
|
|
137
|
-
expect(wf).toBeDefined();
|
|
138
|
-
expect(wf.gpu_type).toBe('RTX_4090');
|
|
139
|
-
expect(wf.output_type).toBe('images');
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// ── parseServeArgs alias recognition ─────────────────────────────────────────
|
|
144
|
-
|
|
145
|
-
describe('parseServeArgs — alias handling', () => {
|
|
146
|
-
it('passes alias through as positional model', () => {
|
|
147
|
-
const { model } = parseServeArgs(['qwen-7b', '--max-cost', '10']);
|
|
148
|
-
expect(model).toBe('qwen-7b');
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('alias is distinct from full model ID in parseServeArgs', () => {
|
|
152
|
-
const { model: alias } = parseServeArgs(['qwen-7b']);
|
|
153
|
-
const { model: full } = parseServeArgs(['Qwen/Qwen2.5-7B-Instruct']);
|
|
154
|
-
expect(alias).toBe('qwen-7b');
|
|
155
|
-
expect(full).toBe('Qwen/Qwen2.5-7B-Instruct');
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// ── serveCommand alias expansion ──────────────────────────────────────────────
|
|
160
|
-
|
|
161
|
-
describe('serveCommand — alias expansion', () => {
|
|
162
|
-
it('sends full model_id to API when alias qwen-7b is used', async () => {
|
|
163
|
-
fallback.callWithFallback.mockResolvedValue(makeServeDep({
|
|
164
|
-
model: 'Qwen/Qwen2.5-7B-Instruct',
|
|
165
|
-
}));
|
|
166
|
-
// Mock fetch for health check (immediate pass)
|
|
167
|
-
global.fetch = vi.fn().mockResolvedValue({ ok: true });
|
|
168
|
-
|
|
169
|
-
await serveCommand(config, ['qwen-7b', '--max-cost', '10', '--no-wait'], chalk);
|
|
170
|
-
|
|
171
|
-
expect(fallback.callWithFallback).toHaveBeenCalled();
|
|
172
|
-
const bodyFn = fallback.callWithFallback.mock.calls[0][2];
|
|
173
|
-
const body = bodyFn(null);
|
|
174
|
-
expect(body.model).toBe('Qwen/Qwen2.5-7B-Instruct');
|
|
175
|
-
expect(body.gpu).toBe('RTX_4090');
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('sends full model_id to API when alias llama-8b is used', async () => {
|
|
179
|
-
fallback.callWithFallback.mockResolvedValue(makeServeDep({
|
|
180
|
-
model: 'meta-llama/Llama-3.1-8B-Instruct',
|
|
181
|
-
}));
|
|
182
|
-
global.fetch = vi.fn().mockResolvedValue({ ok: true });
|
|
183
|
-
|
|
184
|
-
await serveCommand(config, ['llama-8b', '--max-cost', '10', '--no-wait'], chalk);
|
|
185
|
-
|
|
186
|
-
const bodyFn = fallback.callWithFallback.mock.calls[0][2];
|
|
187
|
-
const body = bodyFn(null);
|
|
188
|
-
expect(body.model).toBe('meta-llama/Llama-3.1-8B-Instruct');
|
|
189
|
-
expect(body.gpu).toBe('RTX_4090');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('sends full model_id to API when alias qwen-coder-7b is used', async () => {
|
|
193
|
-
fallback.callWithFallback.mockResolvedValue(makeServeDep({
|
|
194
|
-
model: 'Qwen/Qwen2.5-Coder-7B-Instruct',
|
|
195
|
-
}));
|
|
196
|
-
global.fetch = vi.fn().mockResolvedValue({ ok: true });
|
|
197
|
-
|
|
198
|
-
await serveCommand(config, ['qwen-coder-7b', '--max-cost', '5', '--no-wait'], chalk);
|
|
199
|
-
|
|
200
|
-
const bodyFn = fallback.callWithFallback.mock.calls[0][2];
|
|
201
|
-
const body = bodyFn(null);
|
|
202
|
-
expect(body.model).toBe('Qwen/Qwen2.5-Coder-7B-Instruct');
|
|
203
|
-
expect(body.gpu).toBe('RTX_4090');
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('passes full model ID unchanged (no alias lookup)', async () => {
|
|
207
|
-
fallback.callWithFallback.mockResolvedValue(makeServeDep({
|
|
208
|
-
model: 'meta-llama/Llama-3.1-70B-Instruct',
|
|
209
|
-
gpu_type: 'H100',
|
|
210
|
-
}));
|
|
211
|
-
global.fetch = vi.fn().mockResolvedValue({ ok: true });
|
|
212
|
-
|
|
213
|
-
await serveCommand(config, ['meta-llama/Llama-3.1-70B-Instruct', '--max-cost', '20', '--gpu', 'H100', '--no-wait'], chalk);
|
|
214
|
-
|
|
215
|
-
const bodyFn = fallback.callWithFallback.mock.calls[0][2];
|
|
216
|
-
const body = bodyFn(null);
|
|
217
|
-
expect(body.model).toBe('meta-llama/Llama-3.1-70B-Instruct');
|
|
218
|
-
expect(body.gpu).toBe('H100');
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('rejects --max-cost missing for alias (same as non-alias)', async () => {
|
|
222
|
-
await serveCommand(config, ['qwen-7b'], chalk);
|
|
223
|
-
expect(process.exitCode).toBe(1);
|
|
224
|
-
expect(fallback.callWithFallback).not.toHaveBeenCalled();
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('alias auto-sets RTX_4090 but --gpu flag overrides it', async () => {
|
|
228
|
-
fallback.callWithFallback.mockResolvedValue(makeServeDep({ gpu_type: 'L40S' }));
|
|
229
|
-
global.fetch = vi.fn().mockResolvedValue({ ok: true });
|
|
230
|
-
|
|
231
|
-
await serveCommand(config, ['qwen-7b', '--max-cost', '10', '--gpu', 'L40S', '--no-wait'], chalk);
|
|
232
|
-
|
|
233
|
-
const bodyFn = fallback.callWithFallback.mock.calls[0][2];
|
|
234
|
-
const body = bodyFn(null);
|
|
235
|
-
expect(body.gpu).toBe('L40S');
|
|
236
|
-
});
|
|
237
|
-
});
|
package/tests/pull.test.js
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import { changedFilesFromPatch, intersectFiles, parsePullArgs, localChangedFiles } from '../src/commands/pull.js';
|
|
4
|
-
|
|
5
|
-
describe('localChangedFiles', () => {
|
|
6
|
-
function fakeGit(stdout) {
|
|
7
|
-
return () => ({ status: 0, stdout, stderr: '' });
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
it('parses an unstaged modification (leading-space status code)', () => {
|
|
11
|
-
// Regression: a naive .trim() on the line eats this leading status
|
|
12
|
-
// space and shifts the fixed-width slice, corrupting the filename.
|
|
13
|
-
expect(localChangedFiles(fakeGit(' M src/checkout.ts\n'))).toEqual(['src/checkout.ts']);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('parses a staged modification (trailing-space status code)', () => {
|
|
17
|
-
expect(localChangedFiles(fakeGit('M src/checkout.ts\n'))).toEqual(['src/checkout.ts']);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('parses an untracked file', () => {
|
|
21
|
-
expect(localChangedFiles(fakeGit('?? new-file.txt\n'))).toEqual(['new-file.txt']);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('parses a rename, keeping only the destination path', () => {
|
|
25
|
-
expect(localChangedFiles(fakeGit('R old-name.txt -> new-name.txt\n'))).toEqual(['new-name.txt']);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('parses multiple changed files, sorted', () => {
|
|
29
|
-
expect(localChangedFiles(fakeGit(' M b.txt\n?? a.txt\n'))).toEqual(['a.txt', 'b.txt']);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('returns an empty array for a clean worktree', () => {
|
|
33
|
-
expect(localChangedFiles(fakeGit(''))).toEqual([]);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('throws when git status fails', () => {
|
|
37
|
-
const failing = () => ({ status: 1, stdout: '', stderr: 'not a git repository' });
|
|
38
|
-
expect(() => localChangedFiles(failing)).toThrow('not a git repository');
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('badgr pull helpers', () => {
|
|
43
|
-
it('parses deployment id and safety flags', () => {
|
|
44
|
-
expect(parsePullArgs(['dep-123', '--branch'])).toEqual({ deploymentId: 'dep-123', flags: { branch: true, diffOnly: false, yes: false } });
|
|
45
|
-
expect(parsePullArgs(['dep-123', '--diff-only'])).toEqual({ deploymentId: 'dep-123', flags: { branch: false, diffOnly: true, yes: false } });
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('parses --yes and -y as the same flag', () => {
|
|
49
|
-
expect(parsePullArgs(['dep-123', '--yes']).flags.yes).toBe(true);
|
|
50
|
-
expect(parsePullArgs(['dep-123', '-y']).flags.yes).toBe(true);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('defaults all safety flags to false when only the id is given', () => {
|
|
54
|
-
expect(parsePullArgs(['dep-123'])).toEqual({ deploymentId: 'dep-123', flags: { branch: false, diffOnly: false, yes: false } });
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('returns undefined deploymentId when no positional arg is given', () => {
|
|
58
|
-
expect(parsePullArgs(['--branch']).deploymentId).toBeUndefined();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('extracts changed files from a git patch and detects conflicts', () => {
|
|
62
|
-
const patch = ['diff --git a/src/a.js b/src/a.js', '--- a/src/a.js', '+++ b/src/a.js', 'diff --git a/new.txt b/new.txt', '--- /dev/null', '+++ b/new.txt'].join('\n');
|
|
63
|
-
expect(changedFilesFromPatch(patch)).toEqual(['new.txt', 'src/a.js']);
|
|
64
|
-
expect(intersectFiles(['new.txt', 'src/a.js'], ['src/a.js', 'README.md'])).toEqual(['src/a.js']);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('extracts changed files from a patch that only adds new files', () => {
|
|
68
|
-
const patch = ['diff --git a/new.txt b/new.txt', '--- /dev/null', '+++ b/new.txt'].join('\n');
|
|
69
|
-
expect(changedFilesFromPatch(patch)).toEqual(['new.txt']);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('returns an empty conflict list when nothing overlaps', () => {
|
|
73
|
-
expect(intersectFiles(['a.js', 'b.js'], ['c.js'])).toEqual([]);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Configurable per-test behavior — set inside each test before calling
|
|
78
|
-
// pullCommand, since vi.mock factories are hoisted and evaluated once.
|
|
79
|
-
let gitBehavior = {};
|
|
80
|
-
let tarWriteBehavior = () => {};
|
|
81
|
-
|
|
82
|
-
function resetGitBehavior() {
|
|
83
|
-
gitBehavior = {
|
|
84
|
-
revParse: () => ({ status: 0, stdout: 'true\n', stderr: '' }),
|
|
85
|
-
status: () => ({ status: 0, stdout: '', stderr: '' }),
|
|
86
|
-
checkout: () => ({ status: 0, stdout: '', stderr: '' }),
|
|
87
|
-
applyCheck: () => ({ status: 0, stdout: '', stderr: '' }),
|
|
88
|
-
apply: () => ({ status: 0, stdout: '', stderr: '' }),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
vi.mock('child_process', () => ({
|
|
93
|
-
spawnSync: vi.fn((cmd, args) => {
|
|
94
|
-
if (args[0] === 'rev-parse') return gitBehavior.revParse();
|
|
95
|
-
if (args[0] === 'status') return gitBehavior.status();
|
|
96
|
-
if (args[0] === 'checkout') return gitBehavior.checkout(args);
|
|
97
|
-
if (args[0] === 'apply' && args[1] === '--check') return gitBehavior.applyCheck(args);
|
|
98
|
-
if (args[0] === 'apply') return gitBehavior.apply(args);
|
|
99
|
-
return { status: 0, stdout: '', stderr: '' };
|
|
100
|
-
}),
|
|
101
|
-
}));
|
|
102
|
-
|
|
103
|
-
vi.mock('tar', () => {
|
|
104
|
-
const x = vi.fn(async opts => tarWriteBehavior(opts));
|
|
105
|
-
return { default: { x }, x };
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
function writeNonPatchArtifact({ cwd }) {
|
|
109
|
-
fs.mkdirSync(`${cwd}/test-results`, { recursive: true });
|
|
110
|
-
fs.writeFileSync(`${cwd}/test-results/output.txt`, 'ok');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function writePatchArtifact(patchText) {
|
|
114
|
-
return ({ cwd }) => {
|
|
115
|
-
fs.writeFileSync(`${cwd}/badgr-agent.patch`, patchText);
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const CLEAN_PATCH = [
|
|
120
|
-
'diff --git a/src/checkout.ts b/src/checkout.ts',
|
|
121
|
-
'--- a/src/checkout.ts',
|
|
122
|
-
'+++ b/src/checkout.ts',
|
|
123
|
-
'@@ -1 +1 @@',
|
|
124
|
-
'-old',
|
|
125
|
-
'+new',
|
|
126
|
-
].join('\n');
|
|
127
|
-
|
|
128
|
-
describe('pullCommand', () => {
|
|
129
|
-
const config = { apiKey: 'test-key', baseUrl: 'https://example.test/v1' };
|
|
130
|
-
const chalk = { red: s => s, dim: s => s, green: s => s, yellow: s => s };
|
|
131
|
-
|
|
132
|
-
beforeEach(() => {
|
|
133
|
-
process.exitCode = undefined;
|
|
134
|
-
resetGitBehavior();
|
|
135
|
-
tarWriteBehavior = writeNonPatchArtifact;
|
|
136
|
-
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
137
|
-
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
138
|
-
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
139
|
-
global.fetch = vi.fn().mockResolvedValue({
|
|
140
|
-
ok: true,
|
|
141
|
-
arrayBuffer: async () => new ArrayBuffer(8),
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
afterEach(() => {
|
|
146
|
-
vi.restoreAllMocks();
|
|
147
|
-
process.exitCode = undefined;
|
|
148
|
-
delete global.fetch;
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('prints a usage error and exits 1 when no deployment id is given', async () => {
|
|
152
|
-
const { pullCommand } = await import('../src/commands/pull.js');
|
|
153
|
-
await pullCommand(config, [], chalk);
|
|
154
|
-
expect(process.exitCode).toBe(1);
|
|
155
|
-
expect(console.error).toHaveBeenCalled();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('errors when not run inside a git worktree', async () => {
|
|
159
|
-
gitBehavior.revParse = () => ({ status: 0, stdout: 'false\n', stderr: '' });
|
|
160
|
-
const { pullCommand } = await import('../src/commands/pull.js');
|
|
161
|
-
await pullCommand(config, ['dep-abc'], chalk);
|
|
162
|
-
expect(process.exitCode).toBe(1);
|
|
163
|
-
const logged = console.error.mock.calls.map(c => c.join(' ')).join('\n');
|
|
164
|
-
expect(logged).toContain('git worktree');
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('reports a download failure clearly', async () => {
|
|
168
|
-
global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 404, statusText: 'Not Found', text: async () => '' });
|
|
169
|
-
const { pullCommand } = await import('../src/commands/pull.js');
|
|
170
|
-
await pullCommand(config, ['dep-missing'], chalk);
|
|
171
|
-
expect(process.exitCode).toBe(1);
|
|
172
|
-
const logged = console.error.mock.calls.map(c => c.join(' ')).join('\n');
|
|
173
|
-
expect(logged).toContain('Could not pull dep-missing');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('points to `badgr artifacts <id>` instead of erroring when there is no code patch', async () => {
|
|
177
|
-
const { pullCommand } = await import('../src/commands/pull.js');
|
|
178
|
-
await pullCommand(config, ['dep-test-001'], chalk);
|
|
179
|
-
|
|
180
|
-
expect(process.exitCode).toBeUndefined();
|
|
181
|
-
const logged = console.log.mock.calls.map(c => c.join(' ')).join('\n');
|
|
182
|
-
expect(logged).toContain('badgr artifacts dep-test-001');
|
|
183
|
-
expect(logged).toContain('nothing to apply');
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('applies cleanly when there are no local conflicts', async () => {
|
|
187
|
-
tarWriteBehavior = writePatchArtifact(CLEAN_PATCH);
|
|
188
|
-
gitBehavior.status = () => ({ status: 0, stdout: '', stderr: '' }); // no local changes at all
|
|
189
|
-
|
|
190
|
-
const { pullCommand } = await import('../src/commands/pull.js');
|
|
191
|
-
await pullCommand(config, ['dep-clean-001'], chalk);
|
|
192
|
-
|
|
193
|
-
expect(process.exitCode).toBeUndefined();
|
|
194
|
-
const logged = console.log.mock.calls.map(c => c.join(' ')).join('\n');
|
|
195
|
-
expect(logged).toContain('Applied cloud changes from dep-clean-001');
|
|
196
|
-
expect(logged).toContain('src/checkout.ts');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('refuses to overwrite when local edits conflict, without --branch', async () => {
|
|
200
|
-
tarWriteBehavior = writePatchArtifact(CLEAN_PATCH);
|
|
201
|
-
gitBehavior.status = () => ({ status: 0, stdout: ' M src/checkout.ts\n', stderr: '' });
|
|
202
|
-
|
|
203
|
-
const { pullCommand } = await import('../src/commands/pull.js');
|
|
204
|
-
await pullCommand(config, ['dep-conflict-001'], chalk);
|
|
205
|
-
|
|
206
|
-
expect(process.exitCode).toBe(1);
|
|
207
|
-
const logged = console.error.mock.calls.map(c => c.join(' ')).join('\n');
|
|
208
|
-
expect(logged).toContain('conflict with local edits');
|
|
209
|
-
expect(logged).toContain('src/checkout.ts');
|
|
210
|
-
expect(logged).toContain('--branch');
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('applies on a new branch when --branch is passed despite conflicts', async () => {
|
|
214
|
-
tarWriteBehavior = writePatchArtifact(CLEAN_PATCH);
|
|
215
|
-
gitBehavior.status = () => ({ status: 0, stdout: ' M src/checkout.ts\n', stderr: '' });
|
|
216
|
-
const checkoutSpy = vi.fn(() => ({ status: 0, stdout: '', stderr: '' }));
|
|
217
|
-
gitBehavior.checkout = checkoutSpy;
|
|
218
|
-
|
|
219
|
-
const { pullCommand } = await import('../src/commands/pull.js');
|
|
220
|
-
await pullCommand(config, ['dep-conflict-002', '--branch'], chalk);
|
|
221
|
-
|
|
222
|
-
expect(process.exitCode).toBeUndefined();
|
|
223
|
-
expect(checkoutSpy).toHaveBeenCalledWith(['checkout', '-b', 'badgr/dep-conflict-002']);
|
|
224
|
-
const logged = console.log.mock.calls.map(c => c.join(' ')).join('\n');
|
|
225
|
-
expect(logged).toContain('Created branch badgr/dep-conflict-002');
|
|
226
|
-
expect(logged).toContain('Applied cloud changes from dep-conflict-002');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('--diff-only prints the raw patch and does not apply anything', async () => {
|
|
230
|
-
tarWriteBehavior = writePatchArtifact(CLEAN_PATCH);
|
|
231
|
-
gitBehavior.status = () => ({ status: 0, stdout: ' M src/checkout.ts\n', stderr: '' });
|
|
232
|
-
|
|
233
|
-
const { pullCommand } = await import('../src/commands/pull.js');
|
|
234
|
-
await pullCommand(config, ['dep-diff-001', '--diff-only'], chalk);
|
|
235
|
-
|
|
236
|
-
expect(process.exitCode).toBeUndefined();
|
|
237
|
-
expect(process.stdout.write).toHaveBeenCalledWith(CLEAN_PATCH);
|
|
238
|
-
const logged = console.log.mock.calls.map(c => c.join(' ')).join('\n');
|
|
239
|
-
expect(logged).not.toContain('Applied cloud changes');
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('surfaces a clear error when the patch does not apply cleanly', async () => {
|
|
243
|
-
tarWriteBehavior = writePatchArtifact(CLEAN_PATCH);
|
|
244
|
-
gitBehavior.status = () => ({ status: 0, stdout: '', stderr: '' });
|
|
245
|
-
gitBehavior.applyCheck = () => ({ status: 1, stdout: '', stderr: 'patch does not apply' });
|
|
246
|
-
|
|
247
|
-
const { pullCommand } = await import('../src/commands/pull.js');
|
|
248
|
-
await pullCommand(config, ['dep-bad-patch'], chalk);
|
|
249
|
-
|
|
250
|
-
expect(process.exitCode).toBe(1);
|
|
251
|
-
const logged = console.error.mock.calls.map(c => c.join(' ')).join('\n');
|
|
252
|
-
expect(logged).toContain('patch does not apply');
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('accepts --yes alongside --branch without changing the outcome', async () => {
|
|
256
|
-
tarWriteBehavior = writePatchArtifact(CLEAN_PATCH);
|
|
257
|
-
gitBehavior.status = () => ({ status: 0, stdout: ' M src/checkout.ts\n', stderr: '' });
|
|
258
|
-
|
|
259
|
-
const { pullCommand } = await import('../src/commands/pull.js');
|
|
260
|
-
await pullCommand(config, ['dep-conflict-003', '--branch', '--yes'], chalk);
|
|
261
|
-
|
|
262
|
-
expect(process.exitCode).toBeUndefined();
|
|
263
|
-
const logged = console.log.mock.calls.map(c => c.join(' ')).join('\n');
|
|
264
|
-
expect(logged).toContain('Applied cloud changes from dep-conflict-003');
|
|
265
|
-
});
|
|
266
|
-
});
|