The jev reference: what api_reference and the reference tab say about jev(), choice, score, noul and walk comes from jevCore.mjs's jsdoc (package.json's jsdoc-json). Every function the REPL's scope gets from jev.mjs must have an entry, and every example in one, like the song the hosted MCP's instructions teach with (worker/src/primer.ts), is played here against a stand-in Jev, so the docs cannot drift from the code.
Runs in nix flake check (flake.nix, checks.jev-tests).
9import { spawnSync } from 'node:child_process'; 10import { readFileSync } from 'node:fs'; 11import { describe, expect, it } from 'vitest'; 12import { JEV_PRIMER, PRIMER_SONG } from '../../../worker/src/primer'; 13import { playThrough } from './playThrough.mjs'; 14import { referenceData } from './reference.mjs';
16const ROOT = new URL('../../../', import.meta.url).pathname;
jsdoc's doclets for jevCore.mjs, as doc.json has them (its -X is the same parse without a template).
20function jevDocs() { 21 const run = spawnSync( 22 `${ROOT}node_modules/.bin/jsdoc`, 23 ['-X', '-c', 'jsdoc/jsdoc.config.json', 'website/src/jev/jevCore.mjs'], 24 { cwd: ROOT, encoding: 'utf8', maxBuffer: 16 << 20 }, 25 ); 26 if (run.status !== 0) throw new Error(`jsdoc failed: ${run.stderr}`); 27 return JSON.parse(run.stdout).filter((d) => !d.undocumented && d.kind !== 'package'); 28}
The names jev.mjs puts in the REPL's scope (it evaluates every export).
31const scopeNames = () => { 32 const src = readFileSync(`${ROOT}website/src/jev/jev.mjs`, 'utf8'); 33 const listed = [...src.matchAll(/^export \{([^}]*)\}/gm)].flatMap((m) => m[1].split(',').map((s) => s.trim())); 34 const declared = [...src.matchAll(/^export (?:const|function) (\w+)/gm)].map((m) => m[1]); 35 return [...listed, ...declared].filter(Boolean).sort(); 36};
A played example asks Jev, is accepted by the relay, and no rule of it throws.
42async function expectPlays(code, cycles) { 43 const played = await playThrough(code, cycles); 44 expect(played.heard, 'it plays').toBeGreaterThan(0); 45 expect(played.requests.length, 'Jev is asked').toBeGreaterThan(0); 46 expect(played.requests.filter((r) => r.why).map((r) => r.why), 'the relay accepts every request').toEqual([]); 47 const threw = played.views.flatMap((v) => 48 v.decisions.filter((d) => d?.status === 'fallback' && d.reason && !d.ended), 49 ); 50 expect(threw.map((d) => `${d.cycles}: ${d.reason}`), 'no rule throws').toEqual([]); 51 return played; 52}
54describe('the jev reference', () => { 55 it('has an entry, tagged jev, for everything jev.mjs puts in the REPL scope', () => { 56 expect(reference.filter((f) => f.tags.includes('jev')).map((f) => f.name).sort()).toEqual(scopeNames()); 57 }); 58 59 it('gives each a description, its parameters and an example', () => { 60 for (const f of reference) { 61 expect(f.description.length, f.name).toBeGreaterThan(200); 62 expect(f.params.length, f.name).toBeGreaterThan(0); 63 expect(f.examples.length, f.name).toBeGreaterThan(0); 64 } 65 }); 66 67 it("keeps the description's lists as lines", () => { 68 const jev = reference.find((f) => f.name === 'jev'); 69 expect(jev.description).toMatch(/^- fallback: \{ name: value \}/m); 70 }); 71 72 for (const f of reference) { 73 f.examples.forEach((example, i) => { 74 it(`${f.name}'s example ${i + 1} plays`, async () => { 75 await expectPlays(example, 48); 76 }, 60_000); 77 }); 78 } 79}); 80 81describe("the hosted MCP's primer", () => { 82 it('teaches with its song, and points at the reference', () => { 83 expect(JEV_PRIMER).toContain(PRIMER_SONG); 84 for (const name of scopeNames()) expect(JEV_PRIMER, name).toMatch(new RegExp(`\\b${name}\\b`)); 85 }); 86 87 it('has a song that plays, asks Jev, and ends', async () => { 88 const { lastSound, cps } = await expectPlays(PRIMER_SONG, 64); 89 expect(cps, 'it sets its tempo').toBeGreaterThan(0); 90 expect(lastSound, 'it ends').toBeLessThan(40); 91 }, 60_000); 92});