badgr-cli 1.0.47 → 1.1.0
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/README.md +38 -0
- package/package.json +1 -1
- package/src/api.js +16 -2
- package/src/artifactDownload.js +55 -0
- package/src/badgr.js +104 -0
- package/src/batch.js +22 -4
- package/src/browser.js +23 -0
- package/src/commands/artifacts.js +75 -0
- package/src/commands/batch.js +221 -20
- package/src/commands/billing.js +1 -12
- package/src/commands/capacity.js +9 -4
- package/src/commands/comfyui.js +3 -3
- package/src/commands/connect.js +83 -0
- package/src/commands/doctor.js +127 -0
- package/src/commands/down.js +29 -6
- package/src/commands/launch.js +431 -0
- package/src/commands/pull.js +137 -0
- package/src/commands/run.js +253 -37
- package/src/commands/sbatch.js +232 -0
- package/src/commands/serve.js +3 -3
- package/src/commands/status.js +12 -4
- package/src/commands/task.js +25 -0
- package/src/commands/test-run.js +4 -2
- package/src/credentials.js +65 -0
- package/src/fallback.js +7 -2
- package/src/fanout.js +70 -0
- package/src/gpuDoctor/diskInfo.js +42 -0
- package/src/gpuDoctor/doctor.js +451 -0
- package/src/gpuDoctor/gpuInfo.js +70 -0
- package/src/gpuDoctor/healthCheck.js +63 -0
- package/src/gpuDoctor/logClassifier.js +138 -0
- package/src/gpuDoctor/modelFit.js +107 -0
- package/src/gpuDoctor/probeCache.js +38 -0
- package/src/gpuDoctor/redact.js +29 -0
- package/src/gpuDoctor/torchInfo.js +61 -0
- package/src/gpuDoctor/workflowDoctor.js +96 -0
- package/src/onboarding.js +124 -0
- package/src/slurm.js +193 -0
- package/src/spec.js +59 -2
- package/src/store.js +16 -0
- package/tests/agent-images.test.js +17 -0
- package/tests/artifactDownload.test.js +113 -0
- package/tests/artifacts.test.js +168 -0
- package/tests/batch.test.js +312 -0
- package/tests/browser.test.js +51 -0
- package/tests/capacity.test.js +68 -0
- package/tests/commands.test.js +44 -0
- package/tests/connect.test.js +83 -0
- package/tests/down.test.js +23 -1
- package/tests/fallback-timeout.test.js +41 -0
- package/tests/fanout.test.js +124 -0
- package/tests/gpu-doctor-classifiers.test.js +402 -0
- package/tests/gpu-doctor-doctor.test.js +304 -0
- package/tests/gpu-doctor-probe-cache.test.js +110 -0
- package/tests/gpu-doctor-probes.test.js +257 -0
- package/tests/launch-command-argv.test.js +93 -0
- package/tests/launch-readiness.test.js +1 -0
- package/tests/launch.test.js +440 -0
- package/tests/onboarding.test.js +134 -0
- package/tests/pull.test.js +266 -0
- package/tests/run-lifecycle.test.js +405 -6
- package/tests/sbatch.test.js +190 -0
- package/tests/secrets.test.js +16 -0
- package/tests/slurm.test.js +77 -0
- package/tests/spec.test.js +59 -1
- package/tests/status.test.js +73 -0
- package/tests/task.test.js +109 -0
- package/tests/template.test.js +7 -0
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseSlurmScript, envForArrayTask, SlurmParseError } from '../src/slurm.js';
|
|
3
|
+
|
|
4
|
+
const BASIC = `#!/bin/bash
|
|
5
|
+
#SBATCH --job-name=train
|
|
6
|
+
#SBATCH --cpus-per-task=32
|
|
7
|
+
#SBATCH --mem=128G
|
|
8
|
+
#SBATCH --gres=gpu:a100:2
|
|
9
|
+
#SBATCH --time=02:30:00
|
|
10
|
+
#SBATCH --export=FOO=bar,BAZ=qux
|
|
11
|
+
|
|
12
|
+
python train.py --epochs 10
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
const ARRAY = `#!/bin/bash
|
|
16
|
+
#SBATCH --job-name=screen
|
|
17
|
+
#SBATCH --array=1-100:5
|
|
18
|
+
#SBATCH --gpus=1
|
|
19
|
+
#SBATCH --partition=gpu-fast
|
|
20
|
+
#SBATCH --mail-user=someone@example.com
|
|
21
|
+
|
|
22
|
+
python screen.py --scenario $SLURM_ARRAY_TASK_ID
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
describe('parseSlurmScript', () => {
|
|
26
|
+
it('parses cpus, mem, gres, time, export', () => {
|
|
27
|
+
const job = parseSlurmScript(BASIC, { filename: 'train.slurm' });
|
|
28
|
+
expect(job.name).toBe('train');
|
|
29
|
+
expect(job.cpus).toBe(32);
|
|
30
|
+
expect(job.memGb).toBeCloseTo(128);
|
|
31
|
+
expect(job.gpuCount).toBe(2);
|
|
32
|
+
expect(job.gpuType).toBe('a100');
|
|
33
|
+
expect(job.timeMinutes).toBe(150);
|
|
34
|
+
expect(job.env).toEqual({ FOO: 'bar', BAZ: 'qux' });
|
|
35
|
+
expect(job.command).toContain('python train.py --epochs 10');
|
|
36
|
+
expect(job.arrayIndices).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('parses --array ranges with a step and ignores unknown directives', () => {
|
|
40
|
+
const job = parseSlurmScript(ARRAY, { filename: 'screen.slurm' });
|
|
41
|
+
expect(job.arrayIndices).toEqual([1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56, 61, 66, 71, 76, 81, 86, 91, 96]);
|
|
42
|
+
expect(job.gpuCount).toBe(1);
|
|
43
|
+
expect(job.ignoredDirectives).toContain('--partition=gpu-fast');
|
|
44
|
+
expect(job.ignoredDirectives).toContain('--mail-user=someone@example.com');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('parses mem-per-cpu combined with cpus-per-task', () => {
|
|
48
|
+
const job = parseSlurmScript(`#SBATCH --cpus-per-task=4\n#SBATCH --mem-per-cpu=2G\necho hi\n`);
|
|
49
|
+
expect(job.memGb).toBeCloseTo(8);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('parses simple comma/range --array lists', () => {
|
|
53
|
+
const job = parseSlurmScript(`#SBATCH --array=1,3,5-7\necho hi\n`);
|
|
54
|
+
expect(job.arrayIndices).toEqual([1, 3, 5, 6, 7]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('throws when there is no runnable command', () => {
|
|
58
|
+
expect(() => parseSlurmScript(`#!/bin/bash\n#SBATCH --job-name=empty\n`)).toThrow(SlurmParseError);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('supports export lines in the script body in addition to --export', () => {
|
|
62
|
+
const job = parseSlurmScript(`#SBATCH --job-name=x\nexport MODE="fast"\npython run.py\n`);
|
|
63
|
+
expect(job.env.MODE).toBe('fast');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('envForArrayTask', () => {
|
|
68
|
+
it('injects SLURM_ARRAY_TASK_ID/JOB_ID on top of the base env', () => {
|
|
69
|
+
const env = envForArrayTask({ FOO: 'bar' }, 7, 'rcpt-123');
|
|
70
|
+
expect(env).toEqual({ FOO: 'bar', SLURM_ARRAY_TASK_ID: '7', SLURM_ARRAY_JOB_ID: 'rcpt-123' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('leaves env untouched for non-array jobs', () => {
|
|
74
|
+
const env = envForArrayTask({ FOO: 'bar' }, null, null);
|
|
75
|
+
expect(env).toEqual({ FOO: 'bar' });
|
|
76
|
+
});
|
|
77
|
+
});
|
package/tests/spec.test.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { parseSpec, validateSpec, specLines, parseFlags, normalizeGpuType } from '../src/spec.js';
|
|
2
|
+
import { parseSpec, validateSpec, specLines, parseFlags, normalizeGpuType, VM_CLASSES, LAUNCH_VM_SIZES, vmClassForWorkload, parseGbSize } from '../src/spec.js';
|
|
3
|
+
|
|
4
|
+
describe('vmClassForWorkload — deterministic workload → VM class table', () => {
|
|
5
|
+
it('maps playwright to browser', () => expect(vmClassForWorkload('playwright')).toBe('browser'));
|
|
6
|
+
it('maps cline/claude/codex to small', () => {
|
|
7
|
+
expect(vmClassForWorkload('cline')).toBe('small');
|
|
8
|
+
expect(vmClassForWorkload('claude')).toBe('small');
|
|
9
|
+
expect(vmClassForWorkload('codex')).toBe('small');
|
|
10
|
+
});
|
|
11
|
+
it('defaults an unknown/explicit-form workload (null) to small', () => {
|
|
12
|
+
expect(vmClassForWorkload(null)).toBe('small');
|
|
13
|
+
});
|
|
14
|
+
it('every LAUNCH_VM_SIZES entry has a VM_CLASSES definition', () => {
|
|
15
|
+
for (const size of LAUNCH_VM_SIZES) expect(VM_CLASSES[size]).toBeTruthy();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
3
18
|
|
|
4
19
|
describe('normalizeGpuType', () => {
|
|
5
20
|
it('maps RTX-4090 to RTX_4090', () => expect(normalizeGpuType('RTX-4090')).toBe('RTX_4090'));
|
|
@@ -141,3 +156,46 @@ describe('specLines', () => {
|
|
|
141
156
|
expect(lines.every(l => l !== null)).toBe(true);
|
|
142
157
|
});
|
|
143
158
|
});
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
describe('CPU compute sizing', () => {
|
|
162
|
+
it('selects CPU compute and heuristic size for agent tasks', async () => {
|
|
163
|
+
const { parseSpec } = await import('../src/spec.js');
|
|
164
|
+
const spec = parseSpec(['--type', 'job', '--compute', 'cpu', '--agent', 'claude', '--task', 'run e2e tests']);
|
|
165
|
+
expect(spec.compute).toBe('cpu');
|
|
166
|
+
expect(spec.gpu).toBe('CPU');
|
|
167
|
+
expect(spec.cpuSize).toBe('large');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('parseGbSize', () => {
|
|
172
|
+
it('parses a plain GB suffix', () => {
|
|
173
|
+
expect(parseGbSize('64GB')).toBe(64);
|
|
174
|
+
expect(parseGbSize('64G')).toBe(64);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('parses MB/TB/KB and converts to GB', () => {
|
|
178
|
+
expect(parseGbSize('65536MB')).toBeCloseTo(64);
|
|
179
|
+
expect(parseGbSize('1TB')).toBe(1024);
|
|
180
|
+
expect(parseGbSize('1048576KB')).toBeCloseTo(1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('is case-insensitive', () => {
|
|
184
|
+
expect(parseGbSize('24gb')).toBe(24);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('defaults a bare number to GB unless bareUnit overrides it', () => {
|
|
188
|
+
expect(parseGbSize('24')).toBe(24);
|
|
189
|
+
expect(parseGbSize('2048', { bareUnit: 'M' })).toBe(2);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('accepts a number input directly', () => {
|
|
193
|
+
expect(parseGbSize(32)).toBe(32);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('returns null for unparseable input', () => {
|
|
197
|
+
expect(parseGbSize('not-a-size')).toBeNull();
|
|
198
|
+
expect(parseGbSize(null)).toBeNull();
|
|
199
|
+
expect(parseGbSize(undefined)).toBeNull();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* badgr status — must show every non-terminal deployment, not just
|
|
3
|
+
* "running"/"provisioning". A live run found deployments stuck in
|
|
4
|
+
* "starting" that used to be silently dropped, reporting "Nothing
|
|
5
|
+
* running" while real billable resources kept running.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
8
|
+
|
|
9
|
+
vi.mock('../src/api.js', () => ({
|
|
10
|
+
listDeployments: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock('../src/store.js', () => ({
|
|
14
|
+
listDeployments: vi.fn(() => []),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { statusCommand } from '../src/commands/status.js';
|
|
18
|
+
import { listDeployments as apiDeployments } from '../src/api.js';
|
|
19
|
+
|
|
20
|
+
const chalk = new Proxy({}, { get: () => (s) => s });
|
|
21
|
+
|
|
22
|
+
function makeDep(overrides) {
|
|
23
|
+
return {
|
|
24
|
+
deployment_id: 'dep-001',
|
|
25
|
+
name: 'dep-001',
|
|
26
|
+
workload_type: 'job',
|
|
27
|
+
gpu_type: 'A100',
|
|
28
|
+
gpu_count: 1,
|
|
29
|
+
cost_per_hour: 0.5,
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function runStatus(deployments) {
|
|
35
|
+
apiDeployments.mockResolvedValueOnce({ deployments });
|
|
36
|
+
const logs = [];
|
|
37
|
+
const spy = vi.spyOn(console, 'log').mockImplementation((...args) => logs.push(args.join(' ')));
|
|
38
|
+
await statusCommand({ apiKey: 'test-key' }, [], chalk);
|
|
39
|
+
spy.mockRestore();
|
|
40
|
+
return logs.join('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('statusCommand', () => {
|
|
44
|
+
for (const status of ['running', 'provisioning', 'starting', 'queued']) {
|
|
45
|
+
it(`shows a deployment in "${status}" status as active, not hidden`, async () => {
|
|
46
|
+
const out = await runStatus([makeDep({ status })]);
|
|
47
|
+
expect(out).not.toContain('Nothing running');
|
|
48
|
+
expect(out).toContain('dep-001');
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const status of ['completed', 'succeeded', 'success', 'failed', 'stopped']) {
|
|
53
|
+
it(`excludes a deployment in terminal status "${status}"`, async () => {
|
|
54
|
+
const out = await runStatus([makeDep({ status })]);
|
|
55
|
+
expect(out).toContain('Nothing running');
|
|
56
|
+
expect(out).not.toContain('dep-001');
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
it('reports "Nothing running" when there are no deployments at all', async () => {
|
|
61
|
+
const out = await runStatus([]);
|
|
62
|
+
expect(out).toContain('Nothing running');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('shows the running badge for status "running" and a starting-style badge otherwise', async () => {
|
|
66
|
+
const out = await runStatus([
|
|
67
|
+
makeDep({ deployment_id: 'dep-run', status: 'running' }),
|
|
68
|
+
makeDep({ deployment_id: 'dep-start', status: 'starting' }),
|
|
69
|
+
]);
|
|
70
|
+
expect(out).toContain('running');
|
|
71
|
+
expect(out).toContain('starting');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { taskCommand } from '../src/commands/task.js';
|
|
3
|
+
|
|
4
|
+
const chalk = { red: s => s, dim: s => s, green: s => s, bold: s => s, yellow: s => s, cyan: s => s };
|
|
5
|
+
const config = { apiKey: 'test-key', baseUrl: 'https://example.test/v1' };
|
|
6
|
+
|
|
7
|
+
describe('taskCommand', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
process.exitCode = undefined;
|
|
10
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
11
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.restoreAllMocks();
|
|
16
|
+
process.exitCode = undefined;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('requires a description', async () => {
|
|
20
|
+
await taskCommand(config, [], chalk);
|
|
21
|
+
expect(process.exitCode).toBe(1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('is a thin wrapper: prints the description, then delegates to `badgr launch . -- <command>`', async () => {
|
|
25
|
+
await taskCommand(config, [
|
|
26
|
+
'Run the Chromium tests and tell me what failed',
|
|
27
|
+
'--dry-run', '--max-cost', '1', '--', 'npm', 'run', 'test:chromium',
|
|
28
|
+
], chalk);
|
|
29
|
+
|
|
30
|
+
expect(process.exitCode).toBeUndefined();
|
|
31
|
+
const logged = console.log.mock.calls.map(c => c.join(' ')).join('\n');
|
|
32
|
+
expect(logged).toContain('Task: Run the Chromium tests and tell me what failed');
|
|
33
|
+
expect(logged).toContain('Compute:');
|
|
34
|
+
expect(logged).toContain('CPU VM');
|
|
35
|
+
expect(logged).toContain('npm run test:chromium');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('supports an agent prompt as the command', async () => {
|
|
39
|
+
await taskCommand(config, [
|
|
40
|
+
'Fix the failing checkout test',
|
|
41
|
+
'--dry-run', '--max-cost', '1',
|
|
42
|
+
'--', 'claude', '-p', 'Fix the failing checkout test',
|
|
43
|
+
], chalk);
|
|
44
|
+
|
|
45
|
+
expect(process.exitCode).toBeUndefined();
|
|
46
|
+
const logged = console.log.mock.calls.map(c => c.join(' ')).join('\n');
|
|
47
|
+
expect(logged).toContain('claude -p Fix the failing checkout test');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('bubbles up errors from the underlying launch (e.g. --gpu rejection)', async () => {
|
|
51
|
+
await taskCommand(config, [
|
|
52
|
+
'Fix the checkout bug',
|
|
53
|
+
'--gpu', 'A100', '--dry-run', '--', 'claude', '-p', 'fix',
|
|
54
|
+
], chalk);
|
|
55
|
+
expect(process.exitCode).toBe(1);
|
|
56
|
+
const logged = console.error.mock.calls.map(c => c.join(' ')).join('\n');
|
|
57
|
+
expect(logged).toContain('CPU VM');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('bubbles up a missing-command error the same way badgr launch would', async () => {
|
|
61
|
+
await taskCommand(config, ['Fix the checkout bug'], chalk);
|
|
62
|
+
expect(process.exitCode).toBe(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('accepts the quoted --cmd form as the command', async () => {
|
|
66
|
+
await taskCommand(config, [
|
|
67
|
+
'Run tests',
|
|
68
|
+
'--dry-run', '--max-cost', '1', '--cmd', 'npm run test:chromium',
|
|
69
|
+
], chalk);
|
|
70
|
+
expect(process.exitCode).toBeUndefined();
|
|
71
|
+
const logged = console.log.mock.calls.map(c => c.join(' ')).join('\n');
|
|
72
|
+
expect(logged).toContain('npm run test:chromium');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('always launches against the current directory, never the description text', async () => {
|
|
76
|
+
await taskCommand(config, [
|
|
77
|
+
'Run the Chromium tests',
|
|
78
|
+
'--dry-run', '--max-cost', '1', '--', 'npm', 'test',
|
|
79
|
+
], chalk);
|
|
80
|
+
expect(process.exitCode).toBeUndefined();
|
|
81
|
+
const logged = console.log.mock.calls.map(c => c.join(' ')).join('\n');
|
|
82
|
+
expect(logged).not.toContain('Source: Run the Chromium tests');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('forwards --artifacts through to the underlying launch', async () => {
|
|
86
|
+
await taskCommand(config, [
|
|
87
|
+
'Run the Chromium tests and tell me what failed',
|
|
88
|
+
'--dry-run', '--max-cost', '1',
|
|
89
|
+
'--artifacts', 'playwright-report',
|
|
90
|
+
'--', 'npx', 'playwright', 'test',
|
|
91
|
+
], chalk);
|
|
92
|
+
expect(process.exitCode).toBeUndefined();
|
|
93
|
+
const logged = console.log.mock.calls.map(c => c.join(' ')).join('\n');
|
|
94
|
+
expect(logged).toContain('Artifacts:');
|
|
95
|
+
expect(logged).toContain('playwright-report');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('rejects a flag placed before the description instead of silently treating it as the task text', async () => {
|
|
99
|
+
// Regression: `badgr task --max-cost 1 "fix" -- npm test` used to treat
|
|
100
|
+
// "--max-cost" itself as the description ("Task: --max-cost"), then fail
|
|
101
|
+
// downstream with a confusing "Unexpected extra arguments" error that
|
|
102
|
+
// never explained the real cause.
|
|
103
|
+
await taskCommand(config, ['--max-cost', '1', 'Fix login bug', '--', 'npm', 'test'], chalk);
|
|
104
|
+
expect(process.exitCode).toBe(1);
|
|
105
|
+
const logged = console.error.mock.calls.map(c => c.join(' ')).join('\n');
|
|
106
|
+
expect(logged).toContain('description must come first');
|
|
107
|
+
expect(console.log).not.toHaveBeenCalledWith(expect.stringContaining('Task: --max-cost'));
|
|
108
|
+
});
|
|
109
|
+
});
|
package/tests/template.test.js
CHANGED
|
@@ -29,6 +29,13 @@ vi.mock('../src/api.js', () => ({
|
|
|
29
29
|
}));
|
|
30
30
|
|
|
31
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
|
+
}),
|
|
32
39
|
addDeployment: vi.fn(),
|
|
33
40
|
addReceipt: vi.fn(),
|
|
34
41
|
updateReceipt: vi.fn(),
|