@tangle-network/agent-bench 0.8.0 → 0.8.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/CHANGELOG.md +8 -0
- package/HARNESS.md +1 -1
- package/dist/adapters.js +4 -0
- package/dist/adapters.js.map +1 -1
- package/dist/benchmarks/mcad-bench.d.ts +106 -0
- package/dist/benchmarks/mcad-bench.js +569 -0
- package/dist/benchmarks/mcad-bench.js.map +1 -0
- package/dist/benchmarks/mcad-cq-bench.d.ts +82 -0
- package/dist/benchmarks/mcad-cq-bench.js +339 -0
- package/dist/benchmarks/mcad-cq-bench.js.map +1 -0
- package/dist/benchmarks/mcad-cq-golds.d.ts +36 -0
- package/dist/benchmarks/mcad-cq-golds.js +342 -0
- package/dist/benchmarks/mcad-cq-golds.js.map +1 -0
- package/dist/benchmarks/mcad-golds.d.ts +20 -0
- package/dist/benchmarks/mcad-golds.js +318 -0
- package/dist/benchmarks/mcad-golds.js.map +1 -0
- package/dist/benchmarks/mcad-tasks.d.ts +66 -0
- package/dist/benchmarks/mcad-tasks.js +508 -0
- package/dist/benchmarks/mcad-tasks.js.map +1 -0
- package/package.json +4 -4
- package/src/adapters.ts +11 -0
- package/src/benchmarks/mcad-bench.test.mts +455 -0
- package/src/benchmarks/mcad-bench.ts +561 -0
- package/src/benchmarks/mcad-cq-bench.ts +423 -0
- package/src/benchmarks/mcad-cq-golds.ts +374 -0
- package/src/benchmarks/mcad-cq.test.mts +386 -0
- package/src/benchmarks/mcad-golds.ts +359 -0
- package/src/benchmarks/mcad-tasks.ts +490 -0
- package/src/swe-arena/gepa-seat.mts +1 -1
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCAD-CQ adapter tests, in two halves.
|
|
3
|
+
*
|
|
4
|
+
* PURE (no Python anywhere): the prompt mapping — that the CadQuery deliverable
|
|
5
|
+
* replaces v1's OpenSCAD one while the upstream dimensional text stays
|
|
6
|
+
* byte-identical, and that an unrecognised preamble throws instead of shipping a
|
|
7
|
+
* prompt with two contradictory deliverables; interpreter resolution and the
|
|
8
|
+
* fail-loud install message; the ASCII-STL gate; the extra `stepEmitted` check;
|
|
9
|
+
* and `stripCodeFence`, reused from v1.
|
|
10
|
+
*
|
|
11
|
+
* LIVE (gated on the CadQuery interpreter existing): the real judge in BOTH
|
|
12
|
+
* directions. Accept — every calibrated task scores 1.0 on its own gold, STEP
|
|
13
|
+
* included. Reject — four separate failure modes, each isolated so it fails for
|
|
14
|
+
* exactly one reason: one bore deleted (the named probes fail and NOTHING else),
|
|
15
|
+
* no STL written at all, an STL written in binary, and the STEP export removed
|
|
16
|
+
* (which must cost exactly one check). A judge that has only been shown passing
|
|
17
|
+
* inputs is not calibrated, and a new scored check nobody made fail is not tested.
|
|
18
|
+
*
|
|
19
|
+
* npx vitest run src/benchmarks/mcad-cq.test.mts
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync } from 'node:fs'
|
|
23
|
+
import { describe, expect, it } from 'vitest'
|
|
24
|
+
import { stripCodeFence } from './mcad-bench'
|
|
25
|
+
import {
|
|
26
|
+
CADQUERY_INSTALL_FIX,
|
|
27
|
+
MCAD_CQ_DELIVERABLE,
|
|
28
|
+
MCAD_CQ_DELIVERABLE_MULTIBODY,
|
|
29
|
+
STEP_MAGIC,
|
|
30
|
+
asciiStlProblem,
|
|
31
|
+
cadqueryMissingError,
|
|
32
|
+
createMcadCqAdapter,
|
|
33
|
+
resolveCadqueryPython,
|
|
34
|
+
runAndMeasureCadQuery,
|
|
35
|
+
toCadQueryPrompt,
|
|
36
|
+
withStepCheck,
|
|
37
|
+
} from './mcad-cq-bench'
|
|
38
|
+
import { MCAD_CQ_GOLDS, MCAD_CQ_UNCALIBRATED } from './mcad-cq-golds'
|
|
39
|
+
import { MCAD_DELIVERABLE, MCAD_DELIVERABLE_MULTIBODY, MCAD_TASKS } from './mcad-tasks'
|
|
40
|
+
|
|
41
|
+
/** The upstream dimensional text of a v1 task, with its deliverable preamble removed. */
|
|
42
|
+
function upstreamBody(prompt: string): string {
|
|
43
|
+
for (const preamble of [MCAD_DELIVERABLE_MULTIBODY, MCAD_DELIVERABLE]) {
|
|
44
|
+
if (prompt.startsWith(preamble)) return prompt.slice(preamble.length)
|
|
45
|
+
}
|
|
46
|
+
throw new Error('fixture: prompt has no known v1 preamble')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Prompt mapping
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
describe('mcad-cq prompt mapping', () => {
|
|
54
|
+
it('swaps the deliverable and keeps the upstream dimensional text byte-identical', () => {
|
|
55
|
+
for (const task of MCAD_TASKS) {
|
|
56
|
+
const mapped = toCadQueryPrompt(task)
|
|
57
|
+
expect(mapped, task.id).toContain('CadQuery')
|
|
58
|
+
expect(mapped, task.id).toContain('part.step')
|
|
59
|
+
expect(mapped, task.id).toContain('part.stl')
|
|
60
|
+
expect(mapped, task.id).toContain('ASCII STL, not binary')
|
|
61
|
+
expect(mapped, task.id).not.toContain('OpenSCAD')
|
|
62
|
+
expect(mapped, task.id).not.toContain('$fn')
|
|
63
|
+
// the tail is v1's, unchanged
|
|
64
|
+
expect(mapped.endsWith(upstreamBody(task.prompt)), task.id).toBe(true)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('leaves v1 prompts untouched — the mapping is read-only', () => {
|
|
69
|
+
const before = MCAD_TASKS.map((t) => t.prompt)
|
|
70
|
+
MCAD_TASKS.forEach((t) => toCadQueryPrompt(t))
|
|
71
|
+
expect(MCAD_TASKS.map((t) => t.prompt)).toEqual(before)
|
|
72
|
+
for (const p of before) expect(p).toContain('OpenSCAD')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('task 10 keeps the separate-bodies deliverable; every other task keeps one fused solid', () => {
|
|
76
|
+
for (const task of MCAD_TASKS) {
|
|
77
|
+
const mapped = toCadQueryPrompt(task)
|
|
78
|
+
if (task.id === 'planetary-gear-stage') {
|
|
79
|
+
expect(mapped.startsWith(MCAD_CQ_DELIVERABLE_MULTIBODY)).toBe(true)
|
|
80
|
+
expect(mapped).not.toContain('one fused solid')
|
|
81
|
+
expect(mapped).toContain('separate')
|
|
82
|
+
} else {
|
|
83
|
+
expect(mapped.startsWith(MCAD_CQ_DELIVERABLE), task.id).toBe(true)
|
|
84
|
+
expect(mapped, task.id).toContain('one fused solid')
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('throws rather than guessing when a task carries an unknown preamble', () => {
|
|
90
|
+
expect(() =>
|
|
91
|
+
toCadQueryPrompt({
|
|
92
|
+
id: 'invented',
|
|
93
|
+
prompt: 'Author something in a format nobody registered.\n\nA 10 mm cube.',
|
|
94
|
+
spec: {},
|
|
95
|
+
source: 'none',
|
|
96
|
+
calibrated: false,
|
|
97
|
+
}),
|
|
98
|
+
).toThrow(/does not open with a known mcad deliverable preamble/)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Interpreter resolution + fail-loud message
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
describe('mcad-cq interpreter resolution', () => {
|
|
107
|
+
it('defaults to the package-owned .venv-cadquery interpreter', () => {
|
|
108
|
+
expect(resolveCadqueryPython({})).toMatch(/\.venv-cadquery\/bin\/python$/)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('honours an absolute MCAD_CQ_PYTHON and rejects a relative one', () => {
|
|
112
|
+
expect(resolveCadqueryPython({ MCAD_CQ_PYTHON: '/opt/cq/bin/python' })).toBe('/opt/cq/bin/python')
|
|
113
|
+
expect(() => resolveCadqueryPython({ MCAD_CQ_PYTHON: './cq/bin/python' })).toThrow(/must be an absolute path/)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('fails loud with the exact install command and both pins', () => {
|
|
117
|
+
const message = cadqueryMissingError('/nowhere/bin/python').message
|
|
118
|
+
expect(message).toContain('/nowhere/bin/python')
|
|
119
|
+
expect(message).toContain('uv venv --python 3.12 .venv-cadquery')
|
|
120
|
+
expect(message).toContain("uv pip install --python .venv-cadquery/bin/python 'cadquery==2.4.0' 'numpy<2'")
|
|
121
|
+
expect(message).toContain('MCAD_CQ_PYTHON')
|
|
122
|
+
expect(CADQUERY_INSTALL_FIX).toContain('cp313')
|
|
123
|
+
expect(CADQUERY_INSTALL_FIX).toContain('np.bool8')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Delivery gates (pure)
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
describe('mcad-cq delivery gates', () => {
|
|
132
|
+
const ascii = 'solid part\n facet normal 0 0 1\n outer loop\n vertex 0 0 0\n endloop\n endfacet\nendsolid part\n'
|
|
133
|
+
|
|
134
|
+
it('accepts a real ASCII STL', () => {
|
|
135
|
+
expect(asciiStlProblem(Buffer.from(ascii))).toBeUndefined()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('rejects empty, binary, mis-headed and facet-free files by name', () => {
|
|
139
|
+
expect(asciiStlProblem(Buffer.alloc(0))).toMatch(/is empty/)
|
|
140
|
+
// A binary STL's 80-byte header often begins "solid" too, so the NUL scan decides.
|
|
141
|
+
const binary = Buffer.concat([Buffer.from('solid binary export'.padEnd(80, ' ')), Buffer.alloc(4)])
|
|
142
|
+
expect(asciiStlProblem(binary)).toMatch(/not ASCII/)
|
|
143
|
+
expect(asciiStlProblem(Buffer.from('ISO-10303-21;\nHEADER;\n'))).toMatch(/does not start with "solid"/)
|
|
144
|
+
expect(asciiStlProblem(Buffer.from('solid empty\nendsolid empty\n'))).toMatch(/no "facet normal" records/)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('scores stepEmitted as one more check, and it can flip resolved on its own', () => {
|
|
148
|
+
const base = {
|
|
149
|
+
checks: [
|
|
150
|
+
{ name: 'bboxX', ok: true, measured: '100', expected: '[99, 101]' },
|
|
151
|
+
{ name: 'volume', ok: true, measured: '1', expected: '[0, 2]' },
|
|
152
|
+
],
|
|
153
|
+
failed: [],
|
|
154
|
+
score: 1,
|
|
155
|
+
resolved: true,
|
|
156
|
+
}
|
|
157
|
+
const withStep = withStepCheck(base, true)
|
|
158
|
+
expect(withStep.checks).toHaveLength(3)
|
|
159
|
+
expect(withStep.score).toBe(1)
|
|
160
|
+
expect(withStep.resolved).toBe(true)
|
|
161
|
+
|
|
162
|
+
const without = withStepCheck(base, false)
|
|
163
|
+
expect(without.score).toBeCloseTo(2 / 3, 9)
|
|
164
|
+
expect(without.resolved).toBe(false)
|
|
165
|
+
expect(without.failed.map((c) => c.name)).toEqual(['stepEmitted'])
|
|
166
|
+
expect(without.failed[0]?.expected).toContain(STEP_MAGIC)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('strips a markdown fence the model may have wrapped its answer in', () => {
|
|
170
|
+
expect(stripCodeFence('```python\nimport cadquery as cq\n```')).toBe('import cadquery as cq')
|
|
171
|
+
expect(stripCodeFence('```\nimport cadquery as cq\n```')).toBe('import cadquery as cq')
|
|
172
|
+
expect(stripCodeFence(' import cadquery as cq ')).toBe('import cadquery as cq')
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Loading (pure)
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
describe('mcad-cq task loading', () => {
|
|
181
|
+
it('carries spec/source/calibrated metadata and a CadQuery gold for every task', async () => {
|
|
182
|
+
const a = createMcadCqAdapter()
|
|
183
|
+
const tasks = await a.loadTasks()
|
|
184
|
+
expect(tasks).toHaveLength(MCAD_TASKS.length)
|
|
185
|
+
for (const t of tasks) {
|
|
186
|
+
const md = t.metadata as { spec: unknown; source: string; calibrated: boolean }
|
|
187
|
+
expect(md.spec, t.id).toBeTruthy()
|
|
188
|
+
expect(typeof md.source).toBe('string')
|
|
189
|
+
expect(md.calibrated, t.id).toBe(!MCAD_CQ_UNCALIBRATED.has(t.id))
|
|
190
|
+
const gold = await a.goldArtifact(t)
|
|
191
|
+
expect(typeof gold, t.id).toBe('string')
|
|
192
|
+
expect(gold, t.id).toContain('import cadquery as cq')
|
|
193
|
+
expect(gold, t.id).toContain('cq.exporters.export(result, "part.step")')
|
|
194
|
+
expect(gold, t.id).toContain('"ascii": True')
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('honours ids and limit', async () => {
|
|
199
|
+
const a = createMcadCqAdapter()
|
|
200
|
+
expect((await a.loadTasks({ ids: ['l-bracket'] })).map((t) => t.id)).toEqual(['l-bracket'])
|
|
201
|
+
expect(await a.loadTasks({ limit: 3 })).toHaveLength(3)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('scores an empty artifact 0 without starting an interpreter', async () => {
|
|
205
|
+
const a = createMcadCqAdapter()
|
|
206
|
+
const [t] = await a.loadTasks({ ids: ['calibration-block'] })
|
|
207
|
+
const s = await a.judge(t!, ' ')
|
|
208
|
+
expect(s.resolved).toBe(false)
|
|
209
|
+
expect(s.score).toBe(0)
|
|
210
|
+
expect(s.detail).toBe('empty artifact')
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Live judge (needs the CadQuery interpreter)
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
function cadqueryPresent(): boolean {
|
|
219
|
+
try {
|
|
220
|
+
return existsSync(resolveCadqueryPython())
|
|
221
|
+
} catch {
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const CADQUERY = cadqueryPresent()
|
|
227
|
+
const JUDGE_TIMEOUT = 300_000
|
|
228
|
+
|
|
229
|
+
/** The task-01 gold with ONE of its four bores deleted; everything else identical. */
|
|
230
|
+
const BLOCK_MISSING_ONE_BORE = MCAD_CQ_GOLDS['calibration-block']!.replace(
|
|
231
|
+
'for x in (-35, 35) for y in (-20, 20)',
|
|
232
|
+
'for (x, y) in ((-35, 20), (35, -20), (-35, -20))',
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
/** Correct geometry, but the script never writes the mesh the judge grades. */
|
|
236
|
+
const STEP_ONLY = `
|
|
237
|
+
import cadquery as cq
|
|
238
|
+
result = cq.Workplane("XY").box(100, 60, 20, centered=(True, True, False))
|
|
239
|
+
cq.exporters.export(result, "part.step")
|
|
240
|
+
`
|
|
241
|
+
|
|
242
|
+
/** Correct geometry, mesh written in BINARY STL — the prompt pins ASCII. */
|
|
243
|
+
const BINARY_STL = `
|
|
244
|
+
import cadquery as cq
|
|
245
|
+
result = cq.Workplane("XY").box(100, 60, 20, centered=(True, True, False))
|
|
246
|
+
cq.exporters.export(result, "part.step")
|
|
247
|
+
cq.exporters.export(result, "part.stl", exportType="STL", tolerance=0.01, angularTolerance=0.05)
|
|
248
|
+
`
|
|
249
|
+
|
|
250
|
+
/** The task-01 gold with only the STEP export removed. */
|
|
251
|
+
const NO_STEP_EXPORT = MCAD_CQ_GOLDS['calibration-block']!.replace('cq.exporters.export(result, "part.step")\n', '')
|
|
252
|
+
|
|
253
|
+
describe.skipIf(!CADQUERY)('mcad-cq judge, live cadquery', () => {
|
|
254
|
+
it('preflight passes when the interpreter is present', async () => {
|
|
255
|
+
await expect(createMcadCqAdapter().preflight()).resolves.toBeUndefined()
|
|
256
|
+
}, JUDGE_TIMEOUT)
|
|
257
|
+
|
|
258
|
+
for (const task of MCAD_TASKS.filter((t) => !MCAD_CQ_UNCALIBRATED.has(t.id))) {
|
|
259
|
+
it(
|
|
260
|
+
`gold for ${task.id} resolves at score 1.0`,
|
|
261
|
+
async () => {
|
|
262
|
+
const a = createMcadCqAdapter()
|
|
263
|
+
const [t] = await a.loadTasks({ ids: [task.id] })
|
|
264
|
+
const gold = await a.goldArtifact(t!)
|
|
265
|
+
expect(typeof gold).toBe('string')
|
|
266
|
+
const s = await a.judge(t!, gold as string)
|
|
267
|
+
const detail = JSON.parse(s.detail ?? '{}')
|
|
268
|
+
expect(detail.failed ?? [], `failed checks: ${s.detail}`).toEqual([])
|
|
269
|
+
expect(detail.checks?.stepEmitted).toBe(true)
|
|
270
|
+
expect(s.score).toBe(1)
|
|
271
|
+
expect(s.resolved).toBe(true)
|
|
272
|
+
},
|
|
273
|
+
JUDGE_TIMEOUT,
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
it(
|
|
278
|
+
'grades a fenced answer identically — the fence is a formatting slip, not geometry',
|
|
279
|
+
async () => {
|
|
280
|
+
const a = createMcadCqAdapter()
|
|
281
|
+
const [t] = await a.loadTasks({ ids: ['calibration-block'] })
|
|
282
|
+
const s = await a.judge(t!, `\`\`\`python\n${MCAD_CQ_GOLDS['calibration-block']!.trim()}\n\`\`\``)
|
|
283
|
+
expect(s.score).toBe(1)
|
|
284
|
+
expect(s.resolved).toBe(true)
|
|
285
|
+
},
|
|
286
|
+
JUDGE_TIMEOUT,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
it(
|
|
290
|
+
'MUST REJECT: one bore of four deleted fails its named probes and nothing else',
|
|
291
|
+
async () => {
|
|
292
|
+
const a = createMcadCqAdapter()
|
|
293
|
+
const [t] = await a.loadTasks({ ids: ['calibration-block'] })
|
|
294
|
+
const s = await a.judge(t!, BLOCK_MISSING_ONE_BORE)
|
|
295
|
+
expect(s.resolved).toBe(false)
|
|
296
|
+
expect(s.score).toBeGreaterThan(0)
|
|
297
|
+
expect(s.score).toBeLessThan(1)
|
|
298
|
+
const failed = (JSON.parse(s.detail as string).failed as Array<{ name: string }>).map((f) => f.name)
|
|
299
|
+
// the deleted bore is the (35, 20) one: its centre probe and the 3 mm-off
|
|
300
|
+
// probe both now sit in solid material
|
|
301
|
+
expect(failed).toContain('probeOutside[0] (35, 20, 10)')
|
|
302
|
+
expect(failed).toContain('probeOutside[4] (35, 23, 10)')
|
|
303
|
+
// and the part is otherwise correct, so nothing aggregate may be blamed:
|
|
304
|
+
// one filled 8 mm bore is +1005 mm^3 on a 4000 mm^3-wide band
|
|
305
|
+
expect(failed).not.toContain('volume')
|
|
306
|
+
expect(failed).not.toContain('bboxX')
|
|
307
|
+
expect(failed).not.toContain('solids')
|
|
308
|
+
expect(failed).not.toContain('stepEmitted')
|
|
309
|
+
},
|
|
310
|
+
JUDGE_TIMEOUT,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
it(
|
|
314
|
+
'MUST REJECT: a script that writes no part.stl scores 0 and the detail says so',
|
|
315
|
+
async () => {
|
|
316
|
+
const a = createMcadCqAdapter()
|
|
317
|
+
const [t] = await a.loadTasks({ ids: ['calibration-block'] })
|
|
318
|
+
const s = await a.judge(t!, STEP_ONLY)
|
|
319
|
+
expect(s.resolved).toBe(false)
|
|
320
|
+
expect(s.score).toBe(0)
|
|
321
|
+
expect(s.detail).toMatch(/wrote no part\.stl/)
|
|
322
|
+
// it DID write the STEP file, and the detail says that too
|
|
323
|
+
expect(s.detail).toMatch(/stepEmitted true/)
|
|
324
|
+
},
|
|
325
|
+
JUDGE_TIMEOUT,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
it(
|
|
329
|
+
'MUST REJECT: a binary STL is a format miss, not a parse problem',
|
|
330
|
+
async () => {
|
|
331
|
+
const a = createMcadCqAdapter()
|
|
332
|
+
const [t] = await a.loadTasks({ ids: ['calibration-block'] })
|
|
333
|
+
const s = await a.judge(t!, BINARY_STL)
|
|
334
|
+
expect(s.resolved).toBe(false)
|
|
335
|
+
expect(s.score).toBe(0)
|
|
336
|
+
expect(s.detail).toMatch(/not ASCII/)
|
|
337
|
+
},
|
|
338
|
+
JUDGE_TIMEOUT,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
it(
|
|
342
|
+
'MUST REJECT: dropping the STEP export costs exactly the stepEmitted check',
|
|
343
|
+
async () => {
|
|
344
|
+
const a = createMcadCqAdapter()
|
|
345
|
+
const [t] = await a.loadTasks({ ids: ['calibration-block'] })
|
|
346
|
+
const s = await a.judge(t!, NO_STEP_EXPORT)
|
|
347
|
+
expect(s.resolved).toBe(false)
|
|
348
|
+
const detail = JSON.parse(s.detail as string) as {
|
|
349
|
+
failed: Array<{ name: string }>
|
|
350
|
+
checks: Record<string, boolean>
|
|
351
|
+
stepEmitted: boolean
|
|
352
|
+
}
|
|
353
|
+
expect(detail.stepEmitted).toBe(false)
|
|
354
|
+
expect(detail.failed.map((f) => f.name)).toEqual(['stepEmitted'])
|
|
355
|
+
const total = Object.keys(detail.checks).length
|
|
356
|
+
expect(s.score).toBeCloseTo((total - 1) / total, 9)
|
|
357
|
+
},
|
|
358
|
+
JUDGE_TIMEOUT,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
it(
|
|
362
|
+
'MUST REJECT: a hanging script is killed at its deadline and scores nothing',
|
|
363
|
+
async () => {
|
|
364
|
+
const built = await runAndMeasureCadQuery('while True:\n pass\n', 3_000)
|
|
365
|
+
expect(built.ok).toBe(false)
|
|
366
|
+
if (built.ok) return
|
|
367
|
+
expect(built.detail).toMatch(/timed out after 3000 ms and was killed/)
|
|
368
|
+
expect(built.stepEmitted).toBe(false)
|
|
369
|
+
},
|
|
370
|
+
JUDGE_TIMEOUT,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
it(
|
|
374
|
+
'MUST REJECT: a script that raises is scored 0 with its traceback in the detail',
|
|
375
|
+
async () => {
|
|
376
|
+
const a = createMcadCqAdapter()
|
|
377
|
+
const [t] = await a.loadTasks({ ids: ['calibration-block'] })
|
|
378
|
+
const s = await a.judge(t!, 'import cadquery as cq\nraise SystemExit("no model here")\n')
|
|
379
|
+
expect(s.resolved).toBe(false)
|
|
380
|
+
expect(s.score).toBe(0)
|
|
381
|
+
expect(s.detail).toMatch(/script failed:/)
|
|
382
|
+
expect(s.detail).toMatch(/no model here/)
|
|
383
|
+
},
|
|
384
|
+
JUDGE_TIMEOUT,
|
|
385
|
+
)
|
|
386
|
+
})
|