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.
@@ -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
+ });