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/heartbeat.test.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* badgr heartbeat — resets an endpoint's idle-timeout clock
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
5
|
-
import { heartbeatCommand } from '../src/commands/heartbeat.js';
|
|
6
|
-
|
|
7
|
-
vi.mock('../src/api.js', () => ({
|
|
8
|
-
heartbeatDeployment: vi.fn(),
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
vi.mock('../src/store.js', () => ({
|
|
12
|
-
findDeployment: vi.fn(() => null),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
vi.mock('../src/config.js', () => ({
|
|
16
|
-
requireApiKey: vi.fn(),
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
import * as api from '../src/api.js';
|
|
20
|
-
import * as store from '../src/store.js';
|
|
21
|
-
|
|
22
|
-
const config = { apiKey: 'sk-test', baseUrl: 'https://api.test/v1' };
|
|
23
|
-
|
|
24
|
-
const chalk = {
|
|
25
|
-
bold: s => s,
|
|
26
|
-
dim: s => s,
|
|
27
|
-
red: s => s,
|
|
28
|
-
yellow: s => s,
|
|
29
|
-
green: s => s,
|
|
30
|
-
cyan: s => s,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
process.exitCode = undefined;
|
|
35
|
-
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
36
|
-
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
37
|
-
vi.resetAllMocks();
|
|
38
|
-
store.findDeployment.mockReturnValue(null);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
afterEach(() => {
|
|
42
|
-
vi.restoreAllMocks();
|
|
43
|
-
process.exitCode = undefined;
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('requires a deployment id', async () => {
|
|
47
|
-
await heartbeatCommand(config, [], chalk);
|
|
48
|
-
expect(api.heartbeatDeployment).not.toHaveBeenCalled();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('calls heartbeatDeployment and prints confirmation', async () => {
|
|
52
|
-
api.heartbeatDeployment.mockResolvedValue({ deployment_id: 'dep-abc123', last_activity_at: 1700000000 });
|
|
53
|
-
|
|
54
|
-
const lines = [];
|
|
55
|
-
console.log.mockImplementation((...args) => lines.push(args.join(' ')));
|
|
56
|
-
|
|
57
|
-
await heartbeatCommand(config, ['dep-abc123'], chalk);
|
|
58
|
-
|
|
59
|
-
expect(api.heartbeatDeployment).toHaveBeenCalledWith(config, 'dep-abc123');
|
|
60
|
-
expect(lines.some(l => l.includes('Heartbeat recorded for dep-abc123'))).toBe(true);
|
|
61
|
-
expect(process.exitCode).toBeFalsy();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('reports an error and sets exitCode on failure', async () => {
|
|
65
|
-
api.heartbeatDeployment.mockRejectedValue(new Error('deployment not found'));
|
|
66
|
-
|
|
67
|
-
await heartbeatCommand(config, ['dep-missing'], chalk);
|
|
68
|
-
|
|
69
|
-
expect(process.exitCode).toBe(1);
|
|
70
|
-
});
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { pollJobUntilTerminal, renderJobClosingBlock } from '../src/progress.js';
|
|
3
|
-
|
|
4
|
-
const chalk = new Proxy({}, { get: () => (s) => s });
|
|
5
|
-
const config = { apiKey: 'k', baseUrl: 'https://api.test' };
|
|
6
|
-
|
|
7
|
-
function silenceStdout() {
|
|
8
|
-
return vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe('pollJobUntilTerminal', () => {
|
|
12
|
-
it('returns immediately once the job reaches a terminal status', async () => {
|
|
13
|
-
const writeSpy = silenceStdout();
|
|
14
|
-
const detail = { job_id: 'job_1', status: 'completed', stage_label: 'Complete', health: 'completed' };
|
|
15
|
-
const callApi = vi.fn().mockResolvedValue(detail);
|
|
16
|
-
|
|
17
|
-
const result = await pollJobUntilTerminal(callApi, config, 'job_1', { chalk, maxMs: 60_000, pollMs: 1 });
|
|
18
|
-
|
|
19
|
-
expect(result.outcome).toBe('completed');
|
|
20
|
-
expect(result.detail).toBe(detail);
|
|
21
|
-
writeSpy.mockRestore();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('surfaces progress fields (stage/current/total) while polling', async () => {
|
|
25
|
-
const writeSpy = silenceStdout();
|
|
26
|
-
const callApi = vi.fn()
|
|
27
|
-
.mockResolvedValueOnce({ job_id: 'job_2', status: 'running', stage_label: 'Generating images', progress_current: 3, progress_total: 10, progress_unit: 'images', health: 'progressing' })
|
|
28
|
-
.mockResolvedValueOnce({ job_id: 'job_2', status: 'completed', stage_label: 'Complete', health: 'completed' });
|
|
29
|
-
|
|
30
|
-
const result = await pollJobUntilTerminal(callApi, config, 'job_2', { chalk, maxMs: 60_000, pollMs: 1 });
|
|
31
|
-
|
|
32
|
-
expect(result.outcome).toBe('completed');
|
|
33
|
-
const output = writeSpy.mock.calls.map(c => c[0]).join('');
|
|
34
|
-
expect(output).toContain('3/10 images');
|
|
35
|
-
writeSpy.mockRestore();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('does not swallow repeated poll failures silently — gives up and reports polling_failed', async () => {
|
|
39
|
-
const writeSpy = silenceStdout();
|
|
40
|
-
const callApi = vi.fn().mockRejectedValue(new Error('network down'));
|
|
41
|
-
|
|
42
|
-
const result = await pollJobUntilTerminal(callApi, config, 'job_3', { chalk, maxMs: 60_000, pollMs: 1 });
|
|
43
|
-
|
|
44
|
-
expect(result.outcome).toBe('polling_failed');
|
|
45
|
-
// Must have warned the user at least once instead of looping silently.
|
|
46
|
-
const output = writeSpy.mock.calls.map(c => c[0]).join('');
|
|
47
|
-
expect(output).toMatch(/Lost contact/i);
|
|
48
|
-
writeSpy.mockRestore();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('announces a retry-on-a-different-route once, with teardown/billing status', async () => {
|
|
52
|
-
const writeSpy = silenceStdout();
|
|
53
|
-
const callApi = vi.fn()
|
|
54
|
-
.mockResolvedValueOnce({
|
|
55
|
-
job_id: 'job_retry', status: 'running', stage: 'retrying_different_route',
|
|
56
|
-
stage_label: 'Retrying on a different route', health: 'progressing',
|
|
57
|
-
progress_message: 'Retrying on a different route... attempt 2/2',
|
|
58
|
-
teardown_status: 'ok',
|
|
59
|
-
})
|
|
60
|
-
.mockResolvedValueOnce({
|
|
61
|
-
job_id: 'job_retry', status: 'completed', stage_label: 'Complete', health: 'completed',
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const result = await pollJobUntilTerminal(callApi, config, 'job_retry', { chalk, maxMs: 60_000, pollMs: 1 });
|
|
65
|
-
|
|
66
|
-
expect(result.outcome).toBe('completed');
|
|
67
|
-
const output = writeSpy.mock.calls.map(c => c[0]).join('');
|
|
68
|
-
expect(output).toContain('Retrying on a different route... attempt 2/2');
|
|
69
|
-
expect(output).toContain('Previous attempt teardown: succeeded');
|
|
70
|
-
expect(output).toContain('Billing: stopped');
|
|
71
|
-
writeSpy.mockRestore();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('recovers from transient poll failures once the API responds again', async () => {
|
|
75
|
-
const writeSpy = silenceStdout();
|
|
76
|
-
const callApi = vi.fn()
|
|
77
|
-
.mockRejectedValueOnce(new Error('timeout'))
|
|
78
|
-
.mockRejectedValueOnce(new Error('timeout'))
|
|
79
|
-
.mockResolvedValueOnce({ job_id: 'job_4', status: 'completed', stage_label: 'Complete', health: 'completed' });
|
|
80
|
-
|
|
81
|
-
const result = await pollJobUntilTerminal(callApi, config, 'job_4', { chalk, maxMs: 60_000, pollMs: 1 });
|
|
82
|
-
|
|
83
|
-
expect(result.outcome).toBe('completed');
|
|
84
|
-
writeSpy.mockRestore();
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe('renderJobClosingBlock', () => {
|
|
89
|
-
it('shows teardown/billing/logs/receipt for a completed job', () => {
|
|
90
|
-
const block = renderJobClosingBlock(chalk, {
|
|
91
|
-
job_id: 'job_5', status: 'completed', elapsed_seconds: 252,
|
|
92
|
-
charged_usd: 0.08, teardown_status: 'ok', billing_status: 'stopped',
|
|
93
|
-
}, 'rcpt-abc');
|
|
94
|
-
|
|
95
|
-
expect(block).toContain('Complete');
|
|
96
|
-
expect(block).toContain('Teardown: succeeded');
|
|
97
|
-
expect(block).toContain('Billing: stopped');
|
|
98
|
-
expect(block).toContain('badgr logs job_5');
|
|
99
|
-
expect(block).toContain('badgr receipts rcpt-abc');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('shows failure class, stage, and next action for a failed job', () => {
|
|
103
|
-
const block = renderJobClosingBlock(chalk, {
|
|
104
|
-
job_id: 'job_6', status: 'failed', elapsed_seconds: 1112,
|
|
105
|
-
error: { code: 'comfy_health_failed', message: 'ComfyUI did not start within 10 minutes.' },
|
|
106
|
-
failure_class: 'health_check_failed', stage: 'comfy_waiting_for_health',
|
|
107
|
-
teardown_status: 'ok', billing_status: 'stopped',
|
|
108
|
-
next_action: 'Run `badgr logs <job_id>` or retry with a longer max-runtime.',
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
expect(block).toContain('Failed');
|
|
112
|
-
expect(block).toContain('Class: health_check_failed');
|
|
113
|
-
expect(block).toContain('Stage: comfy_waiting_for_health');
|
|
114
|
-
expect(block).toContain('Next:');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('labels a runtime/spend cap failure as "Stopped — limit reached", not a crash', () => {
|
|
118
|
-
const block = renderJobClosingBlock(chalk, {
|
|
119
|
-
job_id: 'job_7', status: 'failed', elapsed_seconds: 545,
|
|
120
|
-
error: { code: 'max_runtime_reached', message: 'Job reached max runtime and was stopped. Billing ended.' },
|
|
121
|
-
failure_class: 'runtime_cap_reached',
|
|
122
|
-
teardown_status: 'ok', billing_status: 'stopped',
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
expect(block).toContain('Stopped — limit reached');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('shows cancelled block distinctly from failed', () => {
|
|
129
|
-
const block = renderJobClosingBlock(chalk, {
|
|
130
|
-
job_id: 'job_8', status: 'canceled', elapsed_seconds: 164,
|
|
131
|
-
teardown_status: 'ok', billing_status: 'stopped',
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
expect(block).toContain('Cancelled by user');
|
|
135
|
-
});
|
|
136
|
-
});
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* badgr launch — the task must reach the agent as a real argument array
|
|
3
|
-
* (opts.cmdArgv), never a shell string. Covers the exact tricky-task list
|
|
4
|
-
* from the "fix multi-word tasks permanently" spec: spaces, apostrophes,
|
|
5
|
-
* embedded quotes, `--`, Unicode, and shell metacharacters must all survive
|
|
6
|
-
* as exactly one array element, since there's no shell in this path at all.
|
|
7
|
-
*/
|
|
8
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
9
|
-
|
|
10
|
-
vi.mock('../src/credentials.js', () => ({
|
|
11
|
-
getCredential: vi.fn(() => 'stored-credential'),
|
|
12
|
-
setCredential: vi.fn(),
|
|
13
|
-
PROVIDER_ENV_KEYS: { anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY' },
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
const runCommandMock = vi.fn();
|
|
17
|
-
vi.mock('../src/commands/run.js', async (importOriginal) => {
|
|
18
|
-
const actual = await importOriginal();
|
|
19
|
-
return { ...actual, runCommand: (...args) => runCommandMock(...args) };
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const { launchCommand } = await import('../src/commands/launch.js');
|
|
23
|
-
|
|
24
|
-
const chalk = { red: s => s, dim: s => s, green: s => s, bold: s => s, yellow: s => s, cyan: s => s };
|
|
25
|
-
const config = { apiKey: 'test-key', baseUrl: 'https://example.test/v1' };
|
|
26
|
-
|
|
27
|
-
const TRICKY_TASKS = [
|
|
28
|
-
'Fix the checkout bug',
|
|
29
|
-
"Fix John's checkout bug",
|
|
30
|
-
'Fix "checkout" validation',
|
|
31
|
-
'Fix checkout -- do not modify billing',
|
|
32
|
-
'Fix café checkout',
|
|
33
|
-
'Fix $(echo dangerous)',
|
|
34
|
-
'Fix checkout; rm -rf something',
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
beforeEach(() => {
|
|
38
|
-
process.exitCode = undefined;
|
|
39
|
-
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
40
|
-
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
41
|
-
runCommandMock.mockClear();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
afterEach(() => {
|
|
45
|
-
vi.restoreAllMocks();
|
|
46
|
-
process.exitCode = undefined;
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe('badgr launch <agent> "<task>" — task transport', () => {
|
|
50
|
-
for (const task of TRICKY_TASKS) {
|
|
51
|
-
it(`claude: passes ${JSON.stringify(task)} through as one argv element, not a shell string`, async () => {
|
|
52
|
-
await launchCommand(config, ['claude', task], chalk);
|
|
53
|
-
|
|
54
|
-
expect(runCommandMock).toHaveBeenCalledTimes(1);
|
|
55
|
-
const [, , , opts] = runCommandMock.mock.calls[0];
|
|
56
|
-
expect(opts.cmdArgv).toEqual(['claude', '-p', task]);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it(`codex: passes ${JSON.stringify(task)} through as one argv element, not a shell string`, async () => {
|
|
60
|
-
await launchCommand(config, ['codex', task], chalk);
|
|
61
|
-
|
|
62
|
-
const [, , , opts] = runCommandMock.mock.calls[0];
|
|
63
|
-
expect(opts.cmdArgv).toEqual(['badgr-codex-run', task]);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it(`cline: passes ${JSON.stringify(task)} through as one argv element, not a shell string`, async () => {
|
|
67
|
-
await launchCommand(config, ['cline', task], chalk);
|
|
68
|
-
|
|
69
|
-
const [, , , opts] = runCommandMock.mock.calls[0];
|
|
70
|
-
expect(opts.cmdArgv).toEqual(['badgr-cline-run', task]);
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
it('single-word tasks continue working', async () => {
|
|
75
|
-
await launchCommand(config, ['claude', 'hello'], chalk);
|
|
76
|
-
const [, , , opts] = runCommandMock.mock.calls[0];
|
|
77
|
-
expect(opts.cmdArgv).toEqual(['claude', '-p', 'hello']);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('explicit `-- <command>` form passes the real array through, not a joined string', async () => {
|
|
81
|
-
await launchCommand(config, ['.', '--', 'codex', 'exec', 'Fix the checkout bug'], chalk);
|
|
82
|
-
|
|
83
|
-
expect(runCommandMock).toHaveBeenCalledTimes(1);
|
|
84
|
-
const [, , , opts] = runCommandMock.mock.calls[0];
|
|
85
|
-
expect(opts.cmdArgv).toEqual(['codex', 'exec', 'Fix the checkout bug']);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('playwright (no natural-language task) still launches via an argv array', async () => {
|
|
89
|
-
await launchCommand(config, ['playwright'], chalk);
|
|
90
|
-
const [, , , opts] = runCommandMock.mock.calls[0];
|
|
91
|
-
expect(opts.cmdArgv).toEqual(['npx', 'playwright', 'test', '--reporter=line,html', '--trace=retain-on-failure']);
|
|
92
|
-
});
|
|
93
|
-
});
|