1import { afterEach, describe, expect, it } from 'vitest'; 2import { whyNotPerformance } from '../../../worker/src/listening'; 3import { choice, createJev, score, setReplay, walk } from './jevCore.mjs'; 4import { boothStore } from './boothStore.mjs'; 5import { answerOf, armReplay, codeHash, performanceOf, performanceParam, performancePath } from './performance.mjs'; 6import { loadTally, sectionOf, sendReaction, tallyFor } from './reactions.mjs'; 7 8const settle = () => new Promise((r) => setTimeout(r, 0)); 9async function play(pat, cycles) { 10 for (let c = 0; c < cycles; c++) { 11 pat.queryArc(c, c + 1); 12 await settle(); 13 } 14} 15 16const sectionQ = choice('Pick the next section', { intro: 'i', verse: 'v', outro: 'o' }); 17const sections = { 18 intro: { at: 0, next: ['verse'] }, 19 verse: { at: 2, next: ['verse', 'outro'] }, 20 outro: { at: 4, next: [] }, 21};
a song of two jev()s played to its end against a stub Jev
24async function played() { 25 boothStore.beginEval(); 26 const jev = createJev(async (state, q) => ({ 27 ...(q.section ? { section: { choice: 'outro', confidence: 0.8, probabilities: { verse: 0.2, outro: 0.8 } } } : {}), 28 ...(q.drums ? { drums: { score: 1.25, confidence: 0.4, probabilities: { 0: 0.1, 1: 0.6, 2: 0.3, 'not a key': 1 } } } : {}), 29 })); 30 const { section } = jev({ questions: { section: sectionQ } }).every(2); 31 const form = walk(section, { sections }); 32 const dj = jev({ questions: { drums: score('How much?', ['out', 'back', 'full']) } }).every(2, { 33 after: { section }, 34 fallback: { drums: 2 }, 35 }); 36 boothStore.commitEval(); 37 await play(form.arrange(dj.drums.score), 8); 38 return boothStore.getSnapshot().booth; 39}
41describe('performances', () => { 42 afterEach(() => setReplay(null)); 43 44 it('performanceParam reads a well-formed id only', () => { 45 expect(performanceParam('?performance=abcdefghijklmnopqrstuv')).toBe('abcdefghijklmnopqrstuv'); 46 expect(performanceParam('?session=probe&performance=abcdefghijklmnopqrstuv')).toBe('abcdefghijklmnopqrstuv'); 47 expect(performanceParam('?performance=short')).toBeNull(); 48 expect(performanceParam('?performance=../../../../etc/passwd..')).toBeNull(); 49 expect(performanceParam('')).toBeNull(); 50 expect(performancePath('jev/dial-up', 'x'.repeat(22))).toBe(`/songs/jev/dial-up/?performance=${'x'.repeat(22)}`); 51 }); 52 53 it('answerOf keeps what the site keeps', () => { 54 expect(answerOf({ type: 'choice', choice: 'a', confidence: 0, probabilities: {}, fallback: true })).toEqual({ 55 choice: 'a', 56 fallback: true, 57 }); 58 expect( 59 answerOf({ type: 'choice', choice: 'b', confidence: 0.5, probabilities: { a: 0.5, b: 0.5, 'x y': 1 }, sampled: 0.7, top: 'a' }), 60 ).toEqual({ choice: 'b', confidence: 0.5, probabilities: { a: 0.5, b: 0.5 }, sampled: 0.7, top: 'a' }); 61 expect(answerOf({ type: 'noul', noul: 0.3, absent: true, junk: 1 })).toEqual({ noul: 0.3, absent: true }); 62 }); 63 64 it('performanceOf records every jev per segment, in a shape the Worker accepts', async () => { 65 const booth = await played(); 66 const hash = await codeHash('song'); 67 expect(hash).toMatch(/^[0-9a-f]{64}$/); 68 const perf = performanceOf(booth, { song: 'jev/dial-up', codeHash: hash, ended: 'finished', upTo: booth.last }); 69 expect(perf.form).toEqual({ jev: 0, question: 'section' }); 70 expect(perf.jevs).toHaveLength(2); 71 expect(perf.jevs[0].segments.map((s) => s.values.section.choice)).toEqual(['intro', 'verse', 'outro']); 72 expect(perf.jevs[1].segments[1].values.drums).toMatchObject({ score: 1.25, confidence: 0.4 }); 73 expect(whyNotPerformance(perf, new Set(['jev/dial-up']))).toBeNull(); 74 // stopped early: only the segments that played 75 const stopped = performanceOf(booth, { song: 'jev/dial-up', codeHash: hash, ended: 'stopped', upTo: 1 }); 76 expect(stopped.jevs.map((j) => j.segments.length)).toEqual([2, 2]); 77 expect(performanceOf(null, { song: 'jev/dial-up', codeHash: hash, ended: 'stopped', upTo: 1 })).toBeNull(); 78 }); 79 80 it('armReplay replays a performance of this song, and says when its code has changed', async () => { 81 const song = { id: 'jev/dial-up', code: 'song' }; 82 const jevs = [{ segments: [{ status: 'answered', values: { move: { choice: 'b' } } }] }]; 83 const serve = (body) => async (url) => { 84 expect(url).toBe(`/jev/performances/${'p'.repeat(22)}`); 85 return Response.json(body); 86 }; 87 const same = await armReplay('p'.repeat(22), song, serve({ song: song.id, codeHash: await codeHash('song'), jevs })); 88 expect(same.changed).toBe(false); 89 const changed = await armReplay('p'.repeat(22), song, serve({ song: song.id, codeHash: 'f'.repeat(64), jevs })); 90 expect(changed.changed).toBe(true); 91 // a jev() declared now replays: it asks nothing 92 boothStore.beginEval(); 93 let asked = 0; 94 const { move } = createJev(async () => { 95 asked++; 96 return {}; 97 })({ questions: { move: choice('m', { a: 'a', b: 'b' }) } }).every(1, { segments: 1 }); 98 boothStore.commitEval(); 99 await boothStore.getSnapshot().booth.askOpening(); 100 expect(move.choice.queryArc(0, 1).map((h) => h.value)).toEqual(['b']); 101 expect(asked).toBe(0); 102 await expect(armReplay('p'.repeat(22), song, serve({ song: 'jev/other', codeHash: 'f'.repeat(64), jevs }))).rejects.toThrow( 103 /of jev\/other/, 104 ); 105 await expect(armReplay('p'.repeat(22), song, async () => new Response('', { status: 404 }))).rejects.toThrow(/no such/); 106 }); 107}); 108 109describe('reactions', () => { 110 it('sectionOf names a reaction by the form’s section, or the section’s number', async () => { 111 const booth = await played(); 112 expect(sectionOf(booth, 1)).toBe('verse'); 113 expect(sectionOf({ decisions: [] }, 2)).toBe('3'); 114 }); 115 116 it('sendReaction counts at once in the page and posts it; loadTally reads the site’s', async () => { 117 const posted = []; 118 sendReaction({ song: 'jev/x', section: 'drop', reaction: 'fire' }, async (url, init) => { 119 posted.push([url, JSON.parse(init.body)]); 120 return new Response(null, { status: 204 }); 121 }); 122 expect(posted).toEqual([['/jev/reactions', { song: 'jev/x', section: 'drop', reaction: 'fire' }]]); 123 expect(tallyFor('jev/x')).toEqual({ drop: { fire: 1, sleep: 0 } }); 124 await loadTally('jev/y', async (url) => { 125 expect(url).toBe('/jev/reactions?song=jev%2Fy'); 126 return Response.json({ song: 'jev/y', sections: [{ section: 'intro', fire: 2, sleep: 5 }] }); 127 }); 128 expect(tallyFor('jev/y')).toEqual({ intro: { fire: 2, sleep: 5 } }); 129 }); 130});