/** * SkillOpt held-out test set scaffold tests (F11). * * Covers: load/parse, capture infra opt-in, gate math (candidate vs baseline). */ import { describe, expect, test, beforeEach, afterEach } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { withEnv } from '../helpers/with-env.ts'; import { appendCapture, capturePath, capturesDir, loadHeldOut, runHeldOutGate, MIN_HELD_OUT_SIZE, } from '../../src/core/skillopt/held-out.ts'; import { assertBundledMutationHeldOut } from '../../src/core/skillopt/bundled-skill-gate.ts'; let tmp: string; beforeEach(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'skillopt-heldout-')); }); afterEach(() => { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ } }); describe('F11 held-out path helpers', () => { test('capturesDir honors GBRAIN_HOME', async () => { await withEnv({ GBRAIN_HOME: tmp }, async () => { const dir = capturesDir(); expect(dir).toBe(path.join(tmp, '.gbrain', 'skillopt-captures')); }); }); test('capturePath returns per-skill-per-run JSONL path', async () => { await withEnv({ GBRAIN_HOME: tmp }, async () => { const p = capturePath('my-skill', 'run-123'); expect(p).toBe(path.join(tmp, '.gbrain', 'skillopt-captures', 'my-skill', 'run-123.jsonl')); }); }); }); describe('F11 appendCapture', () => { test('writes a JSONL row + mkdir as needed', async () => { await withEnv({ GBRAIN_HOME: tmp }, async () => { appendCapture('test-skill', 'run-1', { ts: new Date().toISOString(), skill_name: 'test-skill', task: 'do X', final_text: 'Y', tool_calls: [{ name: 'search' }], }); const p = capturePath('test-skill', 'run-1'); expect(fs.existsSync(p)).toBe(true); const lines = fs.readFileSync(p, 'utf8').trim().split('\n'); expect(lines).toHaveLength(1); const row = JSON.parse(lines[0]!); expect(row.skill_name).toBe('test-skill'); }); }); test('two appends produce two lines', async () => { await withEnv({ GBRAIN_HOME: tmp }, async () => { appendCapture('test-skill', 'run-1', { ts: '2026-05-27T12:00:00Z', skill_name: 'test-skill', task: 'a', final_text: 'A', tool_calls: [], }); appendCapture('test-skill', 'run-1', { ts: '2026-05-27T12:01:00Z', skill_name: 'test-skill', task: 'b', final_text: 'B', tool_calls: [], }); const p = capturePath('test-skill', 'run-1'); expect(fs.readFileSync(p, 'utf8').trim().split('\n')).toHaveLength(2); }); }); }); describe('F11 loadHeldOut', () => { test('parses JSONL using benchmark loader contract', () => { const p = path.join(tmp, 'held.jsonl'); fs.writeFileSync(p, JSON.stringify({ task_id: 'h1', task: 'do x', judge: { kind: 'rule', checks: [{ op: 'contains', arg: 'x' }] } }) + '\n' + JSON.stringify({ task_id: 'h2', task: 'do y', judge: { kind: 'rule', checks: [{ op: 'contains', arg: 'y' }] } }) + '\n' ); const tasks = loadHeldOut(p); expect(tasks).toHaveLength(2); expect(tasks[0]!.task_id).toBe('h1'); }); test('throws on missing file', () => { expect(() => loadHeldOut(path.join(tmp, 'nope.jsonl'))).toThrow(); }); }); describe('F11 runHeldOutGate vacuous case', () => { test('empty held-out tasks passes vacuously with warn', async () => { // We don't need a real engine for the empty-case branch. const result = await runHeldOutGate({ engine: {} as never, candidateSkillText: 'x', baselineSkillText: 'x', heldOutTasks: [], targetModel: 'm', judgeModel: 'm', }); expect(result.passed).toBe(true); expect(result.baselineScore).toBe(0); expect(result.candidateScore).toBe(0); }); }); describe('D16 assertBundledMutationHeldOut (core ENFORCE)', () => { test('bundled + mutate + too-small held-out → throws (hard refuse)', () => { expect(() => assertBundledMutationHeldOut({ isBundled: true, willMutate: true, heldOutCount: MIN_HELD_OUT_SIZE - 1, skillName: 'brain-ops', })).toThrow(/held-out/i); }); test('bundled + mutate + empty held-out → throws (vacuous-pass hole closed)', () => { expect(() => assertBundledMutationHeldOut({ isBundled: true, willMutate: true, heldOutCount: 0, skillName: 'brain-ops', })).toThrow(/non-empty held-out/i); }); test('bundled + mutate + sufficient held-out → no throw', () => { expect(() => assertBundledMutationHeldOut({ isBundled: true, willMutate: true, heldOutCount: MIN_HELD_OUT_SIZE, skillName: 'brain-ops', })).not.toThrow(); }); test('not bundled → no throw even with zero held-out (user skills are free)', () => { expect(() => assertBundledMutationHeldOut({ isBundled: false, willMutate: true, heldOutCount: 0, skillName: 'my-skill', })).not.toThrow(); }); test('bundled but NOT mutating (no-mutate / proposed.md path) → no throw', () => { expect(() => assertBundledMutationHeldOut({ isBundled: true, willMutate: false, heldOutCount: 0, skillName: 'brain-ops', })).not.toThrow(); }); });