badgr-cli 1.0.46 → 1.0.47
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/package.json +4 -2
- package/src/api.js +39 -0
- package/src/badgr.js +15 -0
- package/src/commands/batch.js +612 -0
- package/src/commands/rerun.js +75 -0
- package/src/commands/run.js +3 -19
- package/src/commands/train.js +49 -21
- package/src/store.js +27 -0
- package/src/workloadSpec.js +126 -0
- package/tests/api.test.js +29 -1
- package/tests/batch.test.js +329 -0
- package/tests/rerun.test.js +94 -0
- package/tests/train-lora-dataset.test.js +176 -0
- package/tests/workload-spec.test.js +180 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import {
|
|
6
|
+
parseWorkloadYaml,
|
|
7
|
+
parseInputEntry,
|
|
8
|
+
validateWorkloadSpec,
|
|
9
|
+
WorkloadSpecError,
|
|
10
|
+
} from '../src/workloadSpec.js';
|
|
11
|
+
|
|
12
|
+
let dir;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
dir = mkdtempSync(join(tmpdir(), 'badgr-batch-test-'));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
rmSync(dir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function writeYaml(name, content) {
|
|
23
|
+
const path = join(dir, name);
|
|
24
|
+
writeFileSync(path, content);
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('parseInputEntry', () => {
|
|
29
|
+
it('splits local:container on the last colon', () => {
|
|
30
|
+
const { localPath, containerPath } = parseInputEntry('./policy.py:/inputs/policy.py');
|
|
31
|
+
expect(localPath).toBe('./policy.py');
|
|
32
|
+
expect(containerPath).toBe('/inputs/policy.py');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('rejects a non-absolute container path', () => {
|
|
36
|
+
expect(() => parseInputEntry('./policy.py:inputs/policy.py')).toThrow(WorkloadSpecError);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('rejects an entry with no colon', () => {
|
|
40
|
+
expect(() => parseInputEntry('./policy.py')).toThrow(WorkloadSpecError);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('validateWorkloadSpec', () => {
|
|
45
|
+
it('requires name, image, command, max_cost, max_runtime_minutes', () => {
|
|
46
|
+
const errors = validateWorkloadSpec({});
|
|
47
|
+
expect(errors).toContain('name is required');
|
|
48
|
+
expect(errors).toContain('image is required');
|
|
49
|
+
expect(errors).toContain('command is required and must be a non-empty list');
|
|
50
|
+
expect(errors).toContain('max_cost is required and must be a positive number');
|
|
51
|
+
expect(errors).toContain('max_runtime_minutes is required and must be a positive number');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('passes on a minimal valid spec', () => {
|
|
55
|
+
const errors = validateWorkloadSpec({
|
|
56
|
+
name: 'toy', image: 'busybox', command: ['echo', 'hi'],
|
|
57
|
+
max_cost: 5, max_runtime_minutes: 10,
|
|
58
|
+
});
|
|
59
|
+
expect(errors).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('flags a malformed inputs entry', () => {
|
|
63
|
+
const errors = validateWorkloadSpec({
|
|
64
|
+
name: 'toy', image: 'busybox', command: ['echo'],
|
|
65
|
+
max_cost: 5, max_runtime_minutes: 10,
|
|
66
|
+
inputs: ['./policy.py'],
|
|
67
|
+
});
|
|
68
|
+
expect(errors.some(e => e.includes('inputs entry'))).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('flags outputs that are not absolute paths', () => {
|
|
72
|
+
const errors = validateWorkloadSpec({
|
|
73
|
+
name: 'toy', image: 'busybox', command: ['echo'],
|
|
74
|
+
max_cost: 5, max_runtime_minutes: 10,
|
|
75
|
+
outputs: ['outputs/metrics.json'],
|
|
76
|
+
});
|
|
77
|
+
expect(errors).toContain('outputs must be a list of absolute container paths');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('requires success_metric.file and key when success_metric is set', () => {
|
|
81
|
+
const errors = validateWorkloadSpec({
|
|
82
|
+
name: 'toy', image: 'busybox', command: ['echo'],
|
|
83
|
+
max_cost: 5, max_runtime_minutes: 10,
|
|
84
|
+
success_metric: {},
|
|
85
|
+
});
|
|
86
|
+
expect(errors.some(e => e.includes('success_metric.file'))).toBe(true);
|
|
87
|
+
expect(errors.some(e => e.includes('success_metric.key'))).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('parseWorkloadYaml', () => {
|
|
92
|
+
it('parses the minimal toy-robot-eval example from the spec', () => {
|
|
93
|
+
const path = writeYaml('workload.yml', `
|
|
94
|
+
name: toy-robot-eval
|
|
95
|
+
image: badgr/physical-ai-eval-lite:latest
|
|
96
|
+
|
|
97
|
+
command:
|
|
98
|
+
- python
|
|
99
|
+
- run_eval.py
|
|
100
|
+
- --policy
|
|
101
|
+
- /inputs/policy.py
|
|
102
|
+
- --scenarios
|
|
103
|
+
- /inputs/scenarios
|
|
104
|
+
|
|
105
|
+
inputs:
|
|
106
|
+
- ./policy.py:/inputs/policy.py
|
|
107
|
+
- ./scenarios:/inputs/scenarios
|
|
108
|
+
|
|
109
|
+
outputs:
|
|
110
|
+
- /outputs/metrics.json
|
|
111
|
+
- /outputs/videos
|
|
112
|
+
- /outputs/logs
|
|
113
|
+
|
|
114
|
+
max_cost: 20
|
|
115
|
+
max_runtime_minutes: 60
|
|
116
|
+
|
|
117
|
+
success_metric:
|
|
118
|
+
file: /outputs/metrics.json
|
|
119
|
+
key: pass_rate
|
|
120
|
+
higher_is_better: true
|
|
121
|
+
`);
|
|
122
|
+
const spec = parseWorkloadYaml(path);
|
|
123
|
+
expect(spec.name).toBe('toy-robot-eval');
|
|
124
|
+
expect(spec.image).toBe('badgr/physical-ai-eval-lite:latest');
|
|
125
|
+
expect(spec.command).toEqual(['python', 'run_eval.py', '--policy', '/inputs/policy.py', '--scenarios', '/inputs/scenarios']);
|
|
126
|
+
expect(spec.inputs).toHaveLength(2);
|
|
127
|
+
expect(spec.inputs[0].containerPath).toBe('/inputs/policy.py');
|
|
128
|
+
expect(spec.inputs[0].localPath).toBe(join(dir, 'policy.py'));
|
|
129
|
+
expect(spec.outputs).toEqual(['/outputs/metrics.json', '/outputs/videos', '/outputs/logs']);
|
|
130
|
+
expect(spec.maxCost).toBe(20);
|
|
131
|
+
expect(spec.maxRuntimeMinutes).toBe(60);
|
|
132
|
+
expect(spec.successMetric).toEqual({ file: '/outputs/metrics.json', key: 'pass_rate', higherIsBetter: true });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('defaults higherIsBetter to true when omitted', () => {
|
|
136
|
+
const path = writeYaml('workload.yml', `
|
|
137
|
+
name: toy
|
|
138
|
+
image: busybox
|
|
139
|
+
command: [echo, hi]
|
|
140
|
+
max_cost: 1
|
|
141
|
+
max_runtime_minutes: 1
|
|
142
|
+
success_metric:
|
|
143
|
+
file: /outputs/metrics.json
|
|
144
|
+
key: score
|
|
145
|
+
`);
|
|
146
|
+
const spec = parseWorkloadYaml(path);
|
|
147
|
+
expect(spec.successMetric.higherIsBetter).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('defaults inputs/outputs/env to empty', () => {
|
|
151
|
+
const path = writeYaml('workload.yml', `
|
|
152
|
+
name: toy
|
|
153
|
+
image: busybox
|
|
154
|
+
command: [echo, hi]
|
|
155
|
+
max_cost: 1
|
|
156
|
+
max_runtime_minutes: 1
|
|
157
|
+
`);
|
|
158
|
+
const spec = parseWorkloadYaml(path);
|
|
159
|
+
expect(spec.inputs).toEqual([]);
|
|
160
|
+
expect(spec.outputs).toEqual([]);
|
|
161
|
+
expect(spec.env).toEqual({});
|
|
162
|
+
expect(spec.successMetric).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('throws WorkloadSpecError with all problems on an invalid file', () => {
|
|
166
|
+
const path = writeYaml('workload.yml', `
|
|
167
|
+
image: busybox
|
|
168
|
+
`);
|
|
169
|
+
expect(() => parseWorkloadYaml(path)).toThrow(WorkloadSpecError);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('throws WorkloadSpecError on malformed YAML', () => {
|
|
173
|
+
const path = writeYaml('workload.yml', '{ not: valid: yaml: [');
|
|
174
|
+
expect(() => parseWorkloadYaml(path)).toThrow(WorkloadSpecError);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('throws WorkloadSpecError when the file does not exist', () => {
|
|
178
|
+
expect(() => parseWorkloadYaml(join(dir, 'nope.yml'))).toThrow(WorkloadSpecError);
|
|
179
|
+
});
|
|
180
|
+
});
|