jevstrudel.git / tools / agreement / agreement.test.mjs
1import { describe, expect, it } from 'vitest';
2import { criticArt, format, readSongs, report } from './agreement.mjs';
3
4const spec = (critic) => `---
5title: X
6theme: jev
7measured: {"total":{"art":0.1}}
8${critic}
9revisions:
10  - rev: 1
11    art: 0.11
12---
13
14# X
15
16  art: 0.99
17`;
18
19describe('criticArt: the song as it stands, from its SPEC.md', () => {
20  it('reads critic.art, not a revision art or the body', () => {
21    expect(criticArt(spec('critic:\n  date: 2026-09-25\n  art: 0.68\n  criteria:\n    hook: 3.1'))).toBe(0.68);
22    expect(criticArt(spec('critic:\n  art: 0.5  # a comment'))).toBe(0.5);
23    expect(criticArt(spec(''))).toBeNull();
24    expect(criticArt(spec('critic:\n  art: null'))).toBeNull();
25  });
26
27  it('reads every song in the repo, scored or not', () => {
28    const songs = readSongs(new URL('../../songs/', import.meta.url));
29    expect(songs.length).toBeGreaterThan(5);
30    for (const s of songs) {
31      expect(s.id).toMatch(/^[a-z0-9-]+\/[a-z0-9-]+$/);
32      if (s.art) expect(s.art.p).toBeGreaterThanOrEqual(0);
33    }
34    expect(songs.some((s) => s.art)).toBe(true);
35  });
36});
37
38describe('report: agreement with the critic’s current scores', () => {
39  const songs = [
40    { id: 'jev/high', art: { p: 0.8 } },
41    { id: 'jev/mid', art: { p: 0.6 } },
42    { id: 'jev/twin', art: { p: 0.6 } },
43    { id: 'jev/unscored', art: null },
44  ];
45
46  it('counts each (a, b, pick) n times, either way round, and leaves out ties and unknowns', () => {
47    const r = report(songs, [
48      { a: 'jev/high', b: 'jev/mid', pick: 'jev/high', n: 3 }, // agree
49      { a: 'jev/mid', b: 'jev/high', pick: 'jev/mid', n: 1 }, // disagree, shown the other way round
50      { a: 'jev/twin', b: 'jev/high', pick: 'jev/high', n: 2 }, // agree
51      { a: 'jev/mid', b: 'jev/twin', pick: 'jev/mid', n: 5 }, // a tie: no preference
52      { a: 'jev/gone', b: 'jev/high', pick: 'jev/high', n: 4 }, // no longer here
53      { a: 'jev/unscored', b: 'jev/high', pick: 'jev/high', n: 1 }, // unscored
54    ]);
55    expect(r.overall).toEqual({ agreed: 5, counted: 6, share: 5 / 6, ties: 5, unknown: 5, votes: 16 });
56    expect(r.songs).toEqual([
57      { id: 'jev/high', art: 0.8, agreed: 5, counted: 6, share: 5 / 6 },
58      { id: 'jev/mid', art: 0.6, agreed: 3, counted: 4, share: 3 / 4 },
59      { id: 'jev/twin', art: 0.6, agreed: 2, counted: 2, share: 1 },
60    ]);
61  });
62
63  it('has no share with nothing counted', () => {
64    const r = report(songs, []);
65    expect(r.overall.share).toBeNull();
66    expect(r.songs).toEqual([]);
67    expect(format(r)).toMatch(/— of the time \(0 of 0 votes\)/);
68  });
69
70  it('formats a line per song', () => {
71    const text = format(report(songs, [{ a: 'jev/high', b: 'jev/mid', pick: 'jev/mid', n: 2 }]));
72    expect(text).toMatch(/agree with the critic 0% of the time \(0 of 2 votes\)/);
73    expect(text).toMatch(/jev\/high\s+80%\s+0%\s+2/);
74  });
75});