import test from 'node:test'; import assert from 'node:assert/strict'; import { inflateSync } from 'node:zlib'; import worker from '../src/worker.js'; import { QuotaGate, validateResult } from '../src/store.js'; import { TypingGame } from '../src/game.js'; import { MAX_WORD_LENGTH } from '../src/words.js'; import { dailyPassage, normalizeTopic } from '../src/passages.js'; import { scorePNG } from '../src/og.js'; class KV { constructor(){this.data=new Map();} async get(key,type){const entry=this.data.get(key);if(!entry)return null;return type==='json'?JSON.parse(entry.value):entry.value;} async put(key,value,options={}){this.data.set(key,{value,metadata:options.metadata});} async list({prefix,limit=1000,cursor='0'}){const keys=[...this.data].filter(([key])=>key.startsWith(prefix)).sort(([a],[b])=>a.localeCompare(b)).map(([name,v])=>({name,metadata:v.metadata}));const start=+cursor;return{keys:keys.slice(start,start+limit),list_complete:start+limit>=keys.length,cursor:String(start+limit)};} } class Storage { constructor(){this.data=new Map();this.pending=Promise.resolve();this.alarmAt=null;} async get(key){return structuredClone(this.data.get(key));} async put(key,value){this.data.set(key,structuredClone(value));} async delete(key){for(const k of Array.isArray(key)?key:[key])this.data.delete(k);} async setAlarm(at){this.alarmAt=at;} async getAlarm(){return this.alarmAt;} async list({prefix='',end,limit=1000}={}){return new Map([...this.data].filter(([key])=>key.startsWith(prefix)&&(!end||keya.localeCompare(b)).slice(0,limit));} transaction(fn){const next=this.pending.then(()=>fn(this));this.pending=next.catch(()=>{});return next;} } const env=()=>({KV:new KV(),QUOTA:{idFromName:name=>name,get:()=>({fetch:async()=>Response.json({ok:true})})},REV:'test'}); const req=(path,init={})=>new Request('https://typy.example'+path,init); const post=(body,headers={})=>req('/api/results',{method:'POST',headers:{'Content-Type':'application/json',Origin:'https://typy.example',...headers},body:JSON.stringify(body)}); function makeRun({words=['calm','clear'],mode='30',duration=30000}={}){ const game=new TypingGame({words,mode,advance:'space'});let at=0;for(const char of words[0]){game.press(char,at);at+=100;}game.press(' ',at);if(mode==='endless')game.finish(duration);else game.tick(duration);return game.result(duration); } const input=()=>({run:makeRun(),source:{kind:'common'},name:'A friend'}); test('result score is recomputed and private extra fields are discarded',()=>{ const body=input();body.run.stats.wpm=999;body.run.typedContent='do not persist me';body.run.events[0].key='secret'; const record=validateResult(body);assert.equal(record.run.stats.wpm,2);assert.equal(record.run.stats.accuracy,100);assert.equal(record.run.stats.correctChars,5);assert.equal(record.run.typedContent,undefined);assert.equal(record.run.events[0].key,undefined); }); test('rejects malformed timing, oversized passages and truncated history',()=>{ const bad=input();bad.run.events[1].t=-1;assert.throws(()=>validateResult(bad),/replay event/); const tooMany=input();tooMany.run.words=Array(301).fill('word');assert.throws(()=>validateResult(tooMany),/1–300/); const truncated=input();truncated.run.replayTruncated=true;assert.throws(()=>validateResult(truncated),/too long/); const drift=input();drift.run.wordTimings[1].start+=10;assert.throws(()=>validateResult(drift),/consecutive/); }); test('result link round trip stores only explicit sharing data',async()=>{ const e=env(),saved=await worker.fetch(post(input()),e);assert.equal(saved.status,201);const info=await saved.json();assert.match(info.url,/^\/r\/[a-f0-9]{24}$/); const read=await worker.fetch(req('/api/results/'+info.id),e);assert.equal(read.status,200);const value=await read.json();assert.equal(value.run.stats.wpm,2);assert.equal(value.name,'A friend');assert.equal(e.KV.data.size,1); const missing=await worker.fetch(req('/api/results/'+'a'.repeat(24)),e);assert.equal(missing.status,404); }); test('sharing rejects cross-site origins, wrong content types and invalid JSON',async()=>{ const e=env();assert.equal((await worker.fetch(post(input(),{Origin:'https://elsewhere.example'}),e)).status,403); assert.equal((await worker.fetch(req('/api/results',{method:'POST',headers:{'Content-Type':'text/plain'},body:'no'}),e)).status,415); assert.equal((await worker.fetch(req('/api/results',{method:'POST',headers:{'Content-Type':'application/json'},body:'{' }),e)).status,400); assert.equal(e.KV.data.size,0); }); test('daily passage is canonical, changes by date, and only full daily runs can post',()=>{ const day=new Date().toISOString().slice(0,10),passage=dailyPassage(day); assert.equal(passage.words.length,300);assert.deepEqual(passage,dailyPassage(day));assert.notDeepEqual(dailyPassage('2026-10-20').words,dailyPassage('2026-10-21').words); const body={run:makeRun({words:passage.words,mode:'60',duration:60000}),source:{kind:'daily',day}}; assert.equal(validateResult(body).source.day,day); const early=structuredClone(body);early.run.mode='30';early.run.durationMs=30000;assert.throws(()=>validateResult(early)); const altered=structuredClone(body);altered.run.words[2]='tampered';assert.throws(()=>validateResult(altered),/full 60-second/); }); test('independent daily entries survive concurrent saves without shared-row overwrites',async()=>{ const e=env(),day=new Date().toISOString().slice(0,10),words=dailyPassage(day).words; const run=makeRun({words,mode:'60',duration:60000}); const responses=await Promise.all(Array.from({length:8},(_,i)=>worker.fetch(post({run,source:{kind:'daily',day},name:'Player '+i}),e))); assert.ok(responses.every(r=>r.status===201)); const response=await worker.fetch(req('/api/board?day='+day),e);assert.equal(response.status,200);const board=await response.json();assert.equal(board.entries.length,8);assert.equal(new Set(board.entries.map(r=>r.id)).size,8); }); test('cached topics bypass model calls and quota; unavailable generation reports clearly',async()=>{ const e=env();await e.KV.put('topic:v1:react-hooks',JSON.stringify({kind:'topic',topic:'react-hooks',words:['one','two'],title:'react hooks'})); e.QUOTA.get=()=>({fetch:async()=>{throw new Error('should not use quota');}}); const result=await worker.fetch(req('/api/passage?topic=react-hooks'),e);assert.equal(result.status,200);assert.equal((await result.json()).cached,true); const missing=await worker.fetch(req('/api/passage?topic=fano-plane'),e);assert.equal(missing.status,503);assert.equal((await missing.json()).code,'generation_unavailable'); assert.equal(normalizeTopic(' React Hooks '),'react-hooks');assert.throws(()=>normalizeTopic('make-a-bomb'),/general-audience/); }); test('topic generation reserves quota, filters output and caches plain prose',async()=>{ const e=env();e.VENICE_API_KEY='test-only';let calls=0,reservations=0;e.QUOTA.get=()=>({fetch:async()=>{reservations++;return Response.json({ok:true});}}); const original=globalThis.fetch;globalThis.fetch=async(url,options)=>{calls++;assert.equal(url,'https://api.venice.ai/api/v1/chat/completions');const body=JSON.parse(options.body);assert.equal(body.model,'mistral-small-3-2-24b-instruct');return Response.json({choices:[{message:{content:'The ocean contains a remarkable variety of life. Small plants near the surface use sunlight to grow. Many animals depend on this energy and on the steady movement of water through the open sea.'}}]});}; try{const a=await worker.fetch(req('/api/passage?topic=ocean-life'),e);assert.equal(a.status,200);assert.ok((await a.json()).words.length>=30);const b=await worker.fetch(req('/api/passage?topic=ocean-life'),e);assert.equal((await b.json()).cached,true);assert.equal(calls,1);assert.equal(reservations,1);}finally{globalThis.fetch=original;} }); test('provider failure does not save fabricated content or reveal credentials',async()=>{ const e=env();e.VENICE_API_KEY='never-print-this';const original=globalThis.fetch;globalThis.fetch=async()=>new Response('private provider error',{status:500}); try{const res=await worker.fetch(req('/api/passage?topic=astronomy'),e);assert.equal(res.status,502);const text=await res.text();assert.ok(!text.includes('private provider'));assert.ok(!text.includes('never-print'));assert.equal(e.KV.data.size,0);}finally{globalThis.fetch=original;} }); test('quota object reserves a strict global daily ceiling concurrently and persists no raw IP',async()=>{ const storage=new Storage(),gate=new QuotaGate({storage},{TOPICS_PER_VISITOR:'8',TOPICS_PER_DAY:'2'}); const ips=['192.0.2.1','192.0.2.2','192.0.2.3']; const results=await Promise.all(ips.map(ip=>gate.fetch(new Request('https://quota/take',{method:'POST',body:JSON.stringify({kind:'topic',ip})})))); assert.deepEqual(results.map(r=>r.status).sort(),[200,200,429]);assert.equal((await storage.get('counters')).topic,2); const persisted=JSON.stringify([...storage.data]);assert.ok(ips.every(ip=>!persisted.includes(ip)));assert.equal((await results.find(r=>r.status===429).json()).code,'daily_global_limit'); }); test('missing quota binding fails closed before saving',async()=>{ const e=env();delete e.QUOTA;assert.equal((await worker.fetch(req('/api/health'),e)).status,503); const res=await worker.fetch(post(input()),e);assert.equal(res.status,503);assert.equal((await res.json()).code,'quota_unavailable');assert.equal(e.KV.data.size,0); }); test('PNG output is a real 1200x630 image and changes with the score',async()=>{ const a=await scorePNG(),b=await scorePNG({run:{stats:{wpm:92,accuracy:98.6,bestStreak:21}}}); assert.deepEqual([...a.slice(0,8)],[137,80,78,71,13,10,26,10]);assert.equal(new DataView(a.buffer,a.byteOffset).getUint32(16),1200);assert.equal(new DataView(a.buffer,a.byteOffset).getUint32(20),630);assert.notDeepEqual(a,b); let offset=8,idat=[];while(offset{ const e=env(),data=input();data.name='\" &