1import { describe, expect, it } from 'vitest'; 2import { CRITERIA, CRITIC_MODEL, RUBRIC, meanVerdict, setCritic, setRevisionArt } from './critic.mjs'; 3 4const verdict = (art, score) => ({ art, criteria: Object.fromEntries(Object.keys(CRITERIA).map((k) => [k, score])) }); 5 6describe('meanVerdict', () => { 7 it('means art and every criterion, and keeps each run', () => { 8 const v = meanVerdict([verdict(0.58, 2.9), verdict(0.64, 3.2), verdict(0.61, 3.1)]); 9 expect(v.art).toBe(0.61); 10 expect(v.artRuns).toEqual([0.58, 0.64, 0.61]); 11 for (const k of Object.keys(CRITERIA)) expect(v.criteria[k]).toBe(3.1); 12 }); 13 14 it('rounds like one verdict: art to the hundredth, criteria to the tenth', () => { 15 const v = meanVerdict([verdict(0.6, 3), verdict(0.61, 3.1), verdict(0.61, 3.1)]); 16 expect(v.art).toBe(0.61); // 0.6067 17 expect(v.criteria.hook).toBe(3.1); // 3.0667 18 }); 19 20 it('is one verdict unchanged when there is one run', () => { 21 expect(meanVerdict([verdict(0.57, 2.8)])).toEqual({ ...verdict(0.57, 2.8), artRuns: [0.57] }); 22 }); 23 24 it('refuses no runs', () => { 25 expect(() => meanVerdict([])).toThrow(); 26 }); 27}); 28 29const spec = `--- 30title: X 31song: song.js 32revisions: 33 - rev: 1 34 date: 2026-09-24 35 output: null # overwritten by rev 2 36 art: 0.57 37 - rev: 2 38 date: 2026-09-25 39 output: song.js 40--- 41 42# X 43`; 44 45describe('recording verdicts', () => { 46 it('puts art and artRuns after output, replacing a single-run art', () => { 47 const text = setRevisionArt(spec, 1, { art: 0.6, artRuns: [0.58, 0.6, 0.62] }); 48 expect(text).toContain( 49 ' output: null # overwritten by rev 2\n art: 0.6\n artRuns: [0.58, 0.6, 0.62]\n - rev: 2', 50 ); 51 // rewriting replaces both lines, never duplicates them 52 const again = setRevisionArt(text, 1, { art: 0.5, artRuns: [0.5] }); 53 expect(again.match(/art:/g)).toHaveLength(1); 54 expect(again.match(/artRuns:/g)).toHaveLength(1); 55 expect(again).toContain(' art: 0.5\n artRuns: [0.5]\n'); 56 }); 57 58 it('records an unscorable revision as null, dropping its runs', () => { 59 const scored = setRevisionArt(spec, 2, { art: 0.6, artRuns: [0.6] }); 60 const text = setRevisionArt(scored, 2, { reason: 'song.js is missing' }); 61 expect(text).toContain(' output: song.js\n art: null # not scored: song.js is missing\n---'); 62 expect(text).not.toContain('artRuns'); 63 }); 64 65 it('writes the critic block after song:, then replaces it', () => { 66 const v = meanVerdict([verdict(0.58, 2.9), verdict(0.64, 3.2)]); 67 const text = setCritic(spec, v, '2026-09-25'); 68 expect(text).toContain( 69 `song: song.js\ncritic:\n date: 2026-09-25\n model: ${CRITIC_MODEL}\n method: ${RUBRIC}\n art: 0.61\n` + 70 ' artRuns: [0.58, 0.64]\n criteria:\n hook: 3.1\n', 71 ); 72 const again = setCritic(text, meanVerdict([verdict(0.5, 2.5)]), '2026-09-26'); 73 expect(again.match(/^critic:/gm)).toHaveLength(1); 74 expect(again.match(/artRuns:/g)).toHaveLength(1); 75 expect(again).toContain(' art: 0.5\n artRuns: [0.5]\n'); 76 expect(again).toContain('\nrevisions:\n - rev: 1'); 77 }); 78});