@crewhaus/tool-harness-synthesizer 0.1.4 → 0.1.6

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,142 @@
1
+ import { HarnessSynthesizerError } from "./index";
2
+ /** Sentinel that frames the harness's single JSON result line on stdout. */
3
+ export const VERIFIER_SENTINEL = "__CREWHAUS_VERIFIER__";
4
+ const VERIFIER_IMAGE = "node:22-alpine";
5
+ const DEFAULT_TIMEOUT_MS = 10_000;
6
+ /**
7
+ * Compile `payload.code` and run it over `payload.samples`, collecting a
8
+ * boolean verdict per sample. A compile failure returns `{ compileError }`;
9
+ * a per-sample runtime throw is caught (verdict `false`, `errors++`).
10
+ *
11
+ * Single source of truth for verifier scoring: it runs both (a) inside the
12
+ * container — `VERIFIER_HARNESS` embeds this function's own source via
13
+ * `.toString()` — and (b) in unit tests via a fake sandbox. It MUST stay
14
+ * self-contained: reference no imports or module scope, or the in-container
15
+ * copy throws a ReferenceError.
16
+ */
17
+ export function evalVerifierPayload(payload) {
18
+ let fn;
19
+ try {
20
+ fn = new Function("input", "output", String(payload.code));
21
+ }
22
+ catch (err) {
23
+ return { compileError: err instanceof Error ? err.message : String(err) };
24
+ }
25
+ const verdicts = [];
26
+ let errors = 0;
27
+ const samples = payload.samples || [];
28
+ for (let i = 0; i < samples.length; i++) {
29
+ try {
30
+ const s = samples[i];
31
+ verdicts.push(Boolean(fn(s.input, s.output)));
32
+ }
33
+ catch {
34
+ verdicts.push(false);
35
+ errors++;
36
+ }
37
+ }
38
+ return { verdicts, errors };
39
+ }
40
+ /**
41
+ * The in-container harness (`node -e <this>`). Reads `{ code, samples }`
42
+ * from stdin, runs `evalVerifierPayload` (embedded verbatim), and writes
43
+ * `SENTINEL + JSON` once on stdout. Untrusted `code` is stdin DATA, never
44
+ * part of this script.
45
+ */
46
+ const VERIFIER_HARNESS = [
47
+ '"use strict";',
48
+ `const SENTINEL = ${JSON.stringify(VERIFIER_SENTINEL)};`,
49
+ `const evalVerifierPayload = ${evalVerifierPayload.toString()};`,
50
+ 'let raw = "";',
51
+ 'try { raw = require("fs").readFileSync(0, "utf8"); } catch (e) { raw = ""; }',
52
+ "let payload;",
53
+ "try { payload = JSON.parse(raw); }",
54
+ 'catch (e) { process.stdout.write(SENTINEL + JSON.stringify({ compileError: "invalid harness payload" })); process.exit(0); }',
55
+ "process.stdout.write(SENTINEL + JSON.stringify(evalVerifierPayload(payload)));",
56
+ ].join("\n");
57
+ function parseHarnessResult(stdout) {
58
+ const idx = stdout.lastIndexOf(VERIFIER_SENTINEL);
59
+ if (idx === -1)
60
+ return undefined;
61
+ const tail = stdout.slice(idx + VERIFIER_SENTINEL.length);
62
+ const line = tail.split("\n", 1)[0] ?? "";
63
+ let obj;
64
+ try {
65
+ obj = JSON.parse(line);
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ if (typeof obj !== "object" || obj === null)
71
+ return undefined;
72
+ if ("compileError" in obj &&
73
+ typeof obj.compileError === "string") {
74
+ return { compileError: obj.compileError };
75
+ }
76
+ const rec = obj;
77
+ if (Array.isArray(rec.verdicts)) {
78
+ return {
79
+ verdicts: rec.verdicts.map((v) => Boolean(v)),
80
+ errors: typeof rec.errors === "number" ? rec.errors : 0,
81
+ };
82
+ }
83
+ return undefined;
84
+ }
85
+ /**
86
+ * Marshal `{ code, samples }` (labels stripped) into the jail, run the
87
+ * harness, parse the framed result, and score verdicts against the
88
+ * host-held `expected` labels.
89
+ *
90
+ * Throws `HarnessSynthesizerError` on: non-serializable samples, compile
91
+ * failure, timeout, non-zero harness exit, or unparseable output. Caught
92
+ * per-sample runtime errors are reflected in `errors` (not thrown).
93
+ */
94
+ export async function runVerifierInSandbox(sandbox, code, samples, opts = {}) {
95
+ // Strip `expected` — the jail produces verdicts; the host scores them.
96
+ const payload = {
97
+ code,
98
+ samples: samples.map((s) => ({ input: s.input, output: s.output })),
99
+ };
100
+ let stdin;
101
+ try {
102
+ // JSON.stringify throws on BigInt and circular references — surface
103
+ // that as a clear error rather than shipping a broken payload.
104
+ stdin = JSON.stringify(payload);
105
+ }
106
+ catch (err) {
107
+ throw new HarnessSynthesizerError(`verifier samples are not JSON-serializable: ${err instanceof Error ? err.message : String(err)}`);
108
+ }
109
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
110
+ const result = await sandbox.exec({
111
+ image: opts.image ?? VERIFIER_IMAGE,
112
+ argv: ["node", "-e", VERIFIER_HARNESS],
113
+ stdin,
114
+ timeoutMs,
115
+ });
116
+ if (result.timedOut) {
117
+ throw new HarnessSynthesizerError(`verifier evaluation timed out after ${timeoutMs}ms`);
118
+ }
119
+ const parsed = parseHarnessResult(result.stdout);
120
+ if (parsed === undefined) {
121
+ if (result.exitCode !== 0) {
122
+ throw new HarnessSynthesizerError(`verifier harness exited ${result.exitCode}: ${result.stderr.slice(-500)}`);
123
+ }
124
+ throw new HarnessSynthesizerError("verifier harness produced no result");
125
+ }
126
+ if ("compileError" in parsed) {
127
+ throw new HarnessSynthesizerError(`verifier code did not compile: ${parsed.compileError}`);
128
+ }
129
+ if (parsed.verdicts.length !== samples.length) {
130
+ throw new HarnessSynthesizerError(`verifier harness returned ${parsed.verdicts.length} verdicts for ${samples.length} samples`);
131
+ }
132
+ let correct = 0;
133
+ const verdicts = [];
134
+ for (let i = 0; i < samples.length; i++) {
135
+ const v = parsed.verdicts[i] === true;
136
+ verdicts.push(v);
137
+ if (v === samples[i].expected)
138
+ correct++;
139
+ }
140
+ const heuristic = samples.length === 0 ? 0 : correct / samples.length;
141
+ return { verdicts, heuristic, errors: parsed.errors };
142
+ }
package/package.json CHANGED
@@ -1,20 +1,23 @@
1
1
  {
2
2
  "name": "@crewhaus/tool-harness-synthesizer",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "Track D / §55 — Thompson-sampled tree search over candidate verifier functions for skills and tools. Smaller LLM + synthesized verifier beats larger LLM (AutoHarness, arxiv 2603.03329).",
6
- "main": "src/index.ts",
7
- "types": "src/index.ts",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
8
  "exports": {
9
- ".": "./src/index.ts"
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
10
13
  },
11
14
  "scripts": {
12
15
  "test": "bun test src"
13
16
  },
14
17
  "dependencies": {
15
- "@crewhaus/errors": "0.1.4",
16
- "@crewhaus/prompt-optimizer": "0.1.4",
17
- "@crewhaus/sandbox": "0.1.4"
18
+ "@crewhaus/errors": "0.1.6",
19
+ "@crewhaus/prompt-optimizer": "0.1.6",
20
+ "@crewhaus/sandbox": "0.1.6"
18
21
  },
19
22
  "license": "Apache-2.0",
20
23
  "author": {
@@ -34,5 +37,5 @@
34
37
  "publishConfig": {
35
38
  "access": "public"
36
39
  },
37
- "files": ["src", "README.md", "LICENSE", "NOTICE"]
40
+ "files": ["dist", "README.md", "LICENSE", "NOTICE"]
38
41
  }
package/src/index.test.ts DELETED
@@ -1,431 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { OptimizerState } from "@crewhaus/prompt-optimizer";
3
- import type { Sandbox, SandboxExecOptions, SandboxExecResult } from "@crewhaus/sandbox";
4
- import {
5
- HarnessSynthesizerError,
6
- VerifierMutationProvider,
7
- type VerifierSample,
8
- runVerifier,
9
- synthesizeVerifier,
10
- thompsonPick,
11
- } from "./index";
12
- import { VERIFIER_SENTINEL, evalVerifierPayload, runVerifierInSandbox } from "./sandboxed-eval";
13
-
14
- /**
15
- * Test double for `@crewhaus/sandbox`. Reports `backend: "docker"` so it
16
- * passes `runVerifier`'s fail-closed gate (the real `noop` backend is
17
- * rejected), and reproduces the in-container harness output by running the
18
- * SAME `evalVerifierPayload` routine in-process. Deterministic, no docker
19
- * daemon. The verifier strings under test are trusted, so evaluating them
20
- * in-process here (rather than in a real jail) is safe.
21
- */
22
- class FakeDockerSandbox implements Sandbox {
23
- readonly backend = "docker" as const;
24
- readonly calls: SandboxExecOptions[] = [];
25
- async exec(opts: SandboxExecOptions): Promise<SandboxExecResult> {
26
- this.calls.push(opts);
27
- const payload = JSON.parse(opts.stdin ?? "{}");
28
- const result = evalVerifierPayload(payload);
29
- return {
30
- stdout: VERIFIER_SENTINEL + JSON.stringify(result),
31
- stderr: "",
32
- exitCode: 0,
33
- timedOut: false,
34
- durationMs: 1,
35
- };
36
- }
37
- async close(): Promise<void> {}
38
- }
39
-
40
- const evenSamples: VerifierSample[] = [
41
- { input: null, output: 0, expected: true },
42
- { input: null, output: 1, expected: false },
43
- { input: null, output: 2, expected: true },
44
- { input: null, output: 3, expected: false },
45
- { input: null, output: 4, expected: true },
46
- ];
47
-
48
- describe("runVerifier", () => {
49
- const sandbox = new FakeDockerSandbox();
50
-
51
- test("scores a correct verifier at 1.0", async () => {
52
- const r = await runVerifier(
53
- "return typeof output === 'number' && output % 2 === 0",
54
- evenSamples,
55
- { sandbox },
56
- );
57
- expect(r.heuristic).toBe(1);
58
- expect(r.errors).toBe(0);
59
- expect(r.verdicts).toEqual([true, false, true, false, true]);
60
- });
61
-
62
- test("scores a constant-true verifier at the majority class", async () => {
63
- const r = await runVerifier("return true", evenSamples, { sandbox });
64
- // 3 of 5 expected: true → score 0.6
65
- expect(r.heuristic).toBe(0.6);
66
- });
67
-
68
- test("captures runtime errors without throwing", async () => {
69
- const r = await runVerifier("throw new Error('boom')", evenSamples, { sandbox });
70
- expect(r.errors).toBe(5);
71
- expect(r.heuristic).toBe(0.4); // false vs expected: 2 of 5 are expected false
72
- });
73
-
74
- test("throws on uncompilable code", async () => {
75
- await expect(runVerifier("not valid javascript {{{", evenSamples, { sandbox })).rejects.toThrow(
76
- HarnessSynthesizerError,
77
- );
78
- });
79
-
80
- test("fails closed on a noop (non-isolating) sandbox", async () => {
81
- const noop: Sandbox = {
82
- backend: "noop",
83
- exec: async () => {
84
- throw new Error("must not run untrusted code under noop");
85
- },
86
- close: async () => {},
87
- };
88
- await expect(runVerifier("return true", evenSamples, { sandbox: noop })).rejects.toThrow(
89
- HarnessSynthesizerError,
90
- );
91
- });
92
-
93
- test("ships code + label-free samples to node:22-alpine via stdin", async () => {
94
- const sb = new FakeDockerSandbox();
95
- await runVerifier("return true", evenSamples, { sandbox: sb });
96
- const call = sb.calls[0];
97
- expect(call?.image).toBe("node:22-alpine");
98
- expect(call?.argv?.[0]).toBe("node");
99
- const payload = JSON.parse(call?.stdin ?? "{}");
100
- expect(payload.code).toBe("return true");
101
- expect(payload.samples).toHaveLength(5);
102
- // The jail must never receive the labels it is scored against.
103
- expect(JSON.stringify(payload)).not.toContain("expected");
104
- });
105
- });
106
-
107
- describe("thompsonPick", () => {
108
- test("returns 0 for a single candidate", () => {
109
- const idx = thompsonPick(
110
- [{ id: "x", code: "return true", score: 1, heuristic: 1, alpha: 10, beta: 1 }],
111
- () => 0.5,
112
- );
113
- expect(idx).toBe(0);
114
- });
115
-
116
- test("favors high-heuristic candidates when sampling is biased", () => {
117
- const nodes = [
118
- { id: "a", code: "1", score: 0.1, heuristic: 0.1, alpha: 1, beta: 9 },
119
- { id: "b", code: "1", score: 0.9, heuristic: 0.9, alpha: 9, beta: 1 },
120
- ];
121
- const idx = thompsonPick(nodes, () => 0.5);
122
- expect([0, 1]).toContain(idx);
123
- });
124
- });
125
-
126
- describe("synthesizeVerifier", () => {
127
- const sandbox = new FakeDockerSandbox();
128
-
129
- test("returns immediately when a seed already meets target", async () => {
130
- const result = await synthesizeVerifier({
131
- seedCandidates: ["return typeof output === 'number' && output % 2 === 0"],
132
- samples: evenSamples,
133
- refiner: async () => "throw new Error('should not be called')",
134
- target: 1.0,
135
- sandbox,
136
- });
137
- expect(result.converged).toBe(true);
138
- expect(result.iterations).toBe(0);
139
- expect(result.best.heuristic).toBe(1);
140
- });
141
-
142
- test("converges via refiner when seed is poor", async () => {
143
- const result = await synthesizeVerifier({
144
- seedCandidates: ["return true"],
145
- samples: evenSamples,
146
- refiner: async () => "return typeof output === 'number' && output % 2 === 0",
147
- target: 1.0,
148
- maxIterations: 3,
149
- rng: () => 0.5,
150
- sandbox,
151
- });
152
- expect(result.converged).toBe(true);
153
- expect(result.best.heuristic).toBe(1);
154
- });
155
-
156
- test("returns best-so-far when iterations exhaust", async () => {
157
- const result = await synthesizeVerifier({
158
- seedCandidates: ["return false"], // score 0.4
159
- samples: evenSamples,
160
- refiner: async () => "return true", // score 0.6
161
- target: 1.0,
162
- maxIterations: 3,
163
- rng: () => 0.5,
164
- sandbox,
165
- });
166
- expect(result.converged).toBe(false);
167
- expect(result.best.heuristic).toBeGreaterThanOrEqual(0.6);
168
- });
169
-
170
- test("picks the higher-heuristic seed when multiple seeds are supplied", async () => {
171
- // Two seeds → the `pool.reduce` comparator runs (with one seed reduce
172
- // returns the lone element without invoking the callback). The perfect
173
- // seed must win and short-circuit at iteration 0.
174
- const result = await synthesizeVerifier({
175
- seedCandidates: [
176
- "return false", // heuristic 0.4
177
- "return typeof output === 'number' && output % 2 === 0", // heuristic 1.0
178
- ],
179
- samples: evenSamples,
180
- refiner: async () => "throw new Error('should not be called')",
181
- target: 1.0,
182
- rng: () => 0.5,
183
- sandbox,
184
- });
185
- expect(result.converged).toBe(true);
186
- expect(result.iterations).toBe(0);
187
- expect(result.best.id).toBe("seed_1");
188
- expect(result.best.heuristic).toBe(1);
189
- expect(result.trajectory).toHaveLength(2);
190
- });
191
-
192
- test("throws on empty seed candidates", async () => {
193
- await expect(
194
- synthesizeVerifier({
195
- seedCandidates: [],
196
- samples: evenSamples,
197
- refiner: async () => "return true",
198
- }),
199
- ).rejects.toThrow(HarnessSynthesizerError);
200
- });
201
-
202
- test("throws on empty sample set", async () => {
203
- await expect(
204
- synthesizeVerifier({
205
- seedCandidates: ["return true"],
206
- samples: [],
207
- refiner: async () => "return true",
208
- }),
209
- ).rejects.toThrow(HarnessSynthesizerError);
210
- });
211
- });
212
-
213
- /**
214
- * A sandbox whose container call returns a caller-supplied result verbatim, so
215
- * tests can drive `runVerifierInSandbox`'s error branches (timeout, nonzero
216
- * exit, empty/garbled output) deterministically without a real container.
217
- */
218
- class ScriptedSandbox implements Sandbox {
219
- readonly backend = "docker" as const;
220
- readonly calls: SandboxExecOptions[] = [];
221
- constructor(private readonly result: SandboxExecResult) {}
222
- async exec(opts: SandboxExecOptions): Promise<SandboxExecResult> {
223
- this.calls.push(opts);
224
- return this.result;
225
- }
226
- async close(): Promise<void> {}
227
- }
228
-
229
- const ioSamples: VerifierSample[] = [{ input: null, output: 0, expected: true }];
230
-
231
- describe("VerifierMutationProvider", () => {
232
- const baseState = (prompt: string): OptimizerState =>
233
- ({
234
- iteration: 0,
235
- best: { id: "c0", prompt, mutations: [], score: 0.5 },
236
- trajectory: [],
237
- trainSet: [],
238
- devSet: [],
239
- }) as OptimizerState;
240
-
241
- test("next() runs the inner search and appends a verifier annotation", async () => {
242
- const sandbox = new FakeDockerSandbox();
243
- const provider = new VerifierMutationProvider(
244
- evenSamples,
245
- // Seed is already perfect, so the inner search converges at iteration 0.
246
- async () => "return false",
247
- ["return typeof output === 'number' && output % 2 === 0"],
248
- 4,
249
- sandbox,
250
- );
251
- expect(provider.name).toBe("verifier-synthesis");
252
- const result = await provider.next(baseState("BASE PROMPT"));
253
- expect(result.prompt.startsWith("BASE PROMPT")).toBe(true);
254
- expect(result.prompt).toContain("[verifier seed_0, h=1.000]");
255
- expect(result.mutations).toEqual([{ kind: "rephrase-instruction" }]);
256
- expect(result.rationale).toContain("verifier-synthesis pass 1");
257
- expect(result.rationale).toContain("(converged)");
258
- });
259
-
260
- test("next() increments the synthesis pass counter across calls", async () => {
261
- const sandbox = new FakeDockerSandbox();
262
- const provider = new VerifierMutationProvider(
263
- evenSamples,
264
- async () => "return typeof output === 'number' && output % 2 === 0",
265
- ["return typeof output === 'number' && output % 2 === 0"],
266
- 4,
267
- sandbox,
268
- );
269
- const first = await provider.next(baseState("P"));
270
- const second = await provider.next(baseState("P"));
271
- expect(first.rationale).toContain("pass 1");
272
- expect(second.rationale).toContain("pass 2");
273
- });
274
-
275
- test("omits the sandbox option when none is injected (default backend path)", async () => {
276
- // No sandbox passed → synthesizeVerifier creates the default backend. Pin it
277
- // to `noop` so the fail-closed gate throws BEFORE any container is spawned —
278
- // this exercises the `this.sandbox === undefined` branch with zero real I/O.
279
- const prev = process.env["CREWHAUS_SANDBOX"];
280
- process.env["CREWHAUS_SANDBOX"] = "noop";
281
- try {
282
- const provider = new VerifierMutationProvider(
283
- evenSamples,
284
- async () => "return true",
285
- ["return true"],
286
- 1,
287
- );
288
- await expect(provider.next(baseState("P"))).rejects.toThrow(HarnessSynthesizerError);
289
- } finally {
290
- // Restore exactly: an unset var must be *removed*, not set to the string
291
- // "undefined" (which `readEnvBackend` would reject). `Reflect.deleteProperty`
292
- // does this without tripping biome's noDelete rule.
293
- if (prev === undefined) Reflect.deleteProperty(process.env, "CREWHAUS_SANDBOX");
294
- else process.env["CREWHAUS_SANDBOX"] = prev;
295
- }
296
- });
297
-
298
- test("non-converged inner search omits the (converged) marker", async () => {
299
- const sandbox = new FakeDockerSandbox();
300
- const provider = new VerifierMutationProvider(
301
- evenSamples,
302
- async () => "return false", // 0.4 — never reaches 1.0
303
- ["return false"],
304
- 2,
305
- sandbox,
306
- );
307
- const result = await provider.next(baseState("P"));
308
- expect(result.rationale).not.toContain("(converged)");
309
- });
310
- });
311
-
312
- describe("runVerifierInSandbox error branches", () => {
313
- test("throws on non-JSON-serializable samples (BigInt)", async () => {
314
- const sandbox = new FakeDockerSandbox();
315
- const bad: VerifierSample[] = [{ input: 1n, output: 0, expected: true }];
316
- await expect(runVerifierInSandbox(sandbox, "return true", bad)).rejects.toThrow(
317
- /not JSON-serializable/,
318
- );
319
- // Nothing was shipped to the jail.
320
- expect(sandbox.calls).toHaveLength(0);
321
- });
322
-
323
- test("throws when the harness times out", async () => {
324
- const sandbox = new ScriptedSandbox({
325
- stdout: "",
326
- stderr: "",
327
- exitCode: 0,
328
- timedOut: true,
329
- durationMs: 1,
330
- });
331
- await expect(
332
- runVerifierInSandbox(sandbox, "return true", ioSamples, { timeoutMs: 1234 }),
333
- ).rejects.toThrow(/timed out after 1234ms/);
334
- });
335
-
336
- test("throws with stderr tail when the harness exits nonzero and emits no result", async () => {
337
- const sandbox = new ScriptedSandbox({
338
- stdout: "no sentinel here",
339
- stderr: "fatal: allocation failure",
340
- exitCode: 137,
341
- timedOut: false,
342
- durationMs: 1,
343
- });
344
- await expect(runVerifierInSandbox(sandbox, "return true", ioSamples)).rejects.toThrow(
345
- /exited 137: fatal: allocation failure/,
346
- );
347
- });
348
-
349
- test("throws 'no result' when exit is clean but no sentinel is present", async () => {
350
- const sandbox = new ScriptedSandbox({
351
- stdout: "garbage without the sentinel",
352
- stderr: "",
353
- exitCode: 0,
354
- timedOut: false,
355
- durationMs: 1,
356
- });
357
- await expect(runVerifierInSandbox(sandbox, "return true", ioSamples)).rejects.toThrow(
358
- /produced no result/,
359
- );
360
- });
361
-
362
- test("throws on a verdict-count mismatch", async () => {
363
- // Sentinel present, valid JSON, but two verdicts for one sample.
364
- const sandbox = new ScriptedSandbox({
365
- stdout: `${VERIFIER_SENTINEL}${JSON.stringify({ verdicts: [true, false], errors: 0 })}`,
366
- stderr: "",
367
- exitCode: 0,
368
- timedOut: false,
369
- durationMs: 1,
370
- });
371
- await expect(runVerifierInSandbox(sandbox, "return true", ioSamples)).rejects.toThrow(
372
- /returned 2 verdicts for 1 samples/,
373
- );
374
- });
375
-
376
- test("surfaces a compileError reported by the harness", async () => {
377
- const sandbox = new ScriptedSandbox({
378
- stdout: `${VERIFIER_SENTINEL}${JSON.stringify({ compileError: "Unexpected token" })}`,
379
- stderr: "",
380
- exitCode: 0,
381
- timedOut: false,
382
- durationMs: 1,
383
- });
384
- await expect(runVerifierInSandbox(sandbox, "bad {{", ioSamples)).rejects.toThrow(
385
- /did not compile: Unexpected token/,
386
- );
387
- });
388
- });
389
-
390
- describe("parseHarnessResult edge cases (via runVerifierInSandbox)", () => {
391
- test("treats a non-JSON result line after the sentinel as no result", async () => {
392
- // Exercises the JSON.parse catch inside parseHarnessResult (returns undefined).
393
- const sandbox = new ScriptedSandbox({
394
- stdout: `${VERIFIER_SENTINEL}this-is-not-json`,
395
- stderr: "",
396
- exitCode: 0,
397
- timedOut: false,
398
- durationMs: 1,
399
- });
400
- await expect(runVerifierInSandbox(sandbox, "return true", ioSamples)).rejects.toThrow(
401
- /produced no result/,
402
- );
403
- });
404
-
405
- test("treats a sentinel-framed object without a verdicts array as no result", async () => {
406
- // Valid JSON object, but neither a compileError string nor a verdicts array
407
- // → parseHarnessResult falls through to its final `return undefined`.
408
- const sandbox = new ScriptedSandbox({
409
- stdout: `${VERIFIER_SENTINEL}${JSON.stringify({ verdicts: "not-an-array" })}`,
410
- stderr: "",
411
- exitCode: 0,
412
- timedOut: false,
413
- durationMs: 1,
414
- });
415
- await expect(runVerifierInSandbox(sandbox, "return true", ioSamples)).rejects.toThrow(
416
- /produced no result/,
417
- );
418
- });
419
- });
420
-
421
- describe("runVerifier (live docker — gated by CREWHAUS_VERIFIER_LIVE_DOCKER=1)", () => {
422
- test("scores a basic verifier in a real container", async () => {
423
- if (process.env["CREWHAUS_VERIFIER_LIVE_DOCKER"] !== "1") return; // skipped by default
424
- const r = await runVerifier(
425
- "return typeof output === 'number' && output % 2 === 0",
426
- evenSamples,
427
- );
428
- expect(r.heuristic).toBe(1);
429
- expect(r.errors).toBe(0);
430
- });
431
- });