import assert from 'node:assert/strict'; import test from 'node:test'; import { CombinationEvaluationPool } from './combination-evaluation-pool.ts'; import { CombinationWorkerSession } from './combination-worker-session.ts'; import type { EvaluationCandidate } from './combination-evaluation-pool.ts'; import type { ExactSongChart } from './types.ts'; const chart: ExactSongChart = {songId:'pool',difficulty:'expert',durationSeconds:1,scoreRatio:1,specialStartSeconds:[0,0,0,0,0],notes:[]}; const candidates = Array.from({length:16},(_,index)=>({overallPower:1000+index,perfectNoteScore:100, cards:Array.from({length:5},(_,slot)=>({id:`${index}-${slot}`,name:'test',type:'pure',generation:'0',active:{scoreUpPercent:0,cooldownSeconds:1,durationSeconds:1,probabilityPercent:100},special:{scoreUpPercent:0,durationSeconds:1},allowedCooldownReductions:[0,4,8,12] as const})) })) satisfies EvaluationCandidate[]; class FakeWorker { onmessage: ((event:{data:any})=>void)|null=null;onerror:(()=>void)|null=null;terminations=0; requests:any[]=[];automatic=true; terminate(){this.terminations++;} respond(request:any,operationId=request.operationId){ const rankings=request.candidates.map((c:EvaluationCandidate)=>({overallPower:c.overallPower,cards:c.cards,result:{order:c.cards.map(c=>c.id),theoreticalMaxScore:c.overallPower,expectedScore:c.overallPower-1}})); this.onmessage?.({data:{type:'combination-result',operationId,rankings,indices:rankings.map((_:any,i:number)=>i)}}); } postMessage(request:any){this.requests.push(request);if(this.automatic)queueMicrotask(()=>this.respond(request));} } test('one pool reuses Workers across complete precision passes',async()=>{ const session=new CombinationWorkerSession();const id=session.start();const workers:FakeWorker[]=[]; const pool=new CombinationEvaluationPool(()=>{const w=new FakeWorker();workers.push(w);return w as unknown as Worker;},session,id,4); const phases:string[]=[];const options={resultLimit:10,onPhase:(p:string)=>phases.push(p),onProgress:()=>{}}; const first=await pool.run(candidates,chart,options);assert.equal(first.length,10);assert.equal(first[0].overallPower,1015); const full=await pool.run(candidates,chart,{...options});assert.deepEqual(first,full);assert.equal(workers.length,4); assert.ok(workers.every(w=>w.requests.length>=2));assert.deepEqual(phases,['exact','exact']); pool.close();pool.close();assert.ok(workers.every(w=>w.terminations===1)); }); test('stale operation replies are ignored and cancellation rejects the pending pass',async()=>{ const session=new CombinationWorkerSession();const id=session.start();const worker=new FakeWorker();worker.automatic=false; const pool=new CombinationEvaluationPool(()=>worker as unknown as Worker,session,id,1); let resolved=false;const pending=pool.run(candidates,chart,{resultLimit:10,onPhase:()=>{},onProgress:()=>{}}).then(()=>{resolved=true;}); worker.respond(worker.requests[0],0);await Promise.resolve();assert.equal(resolved,false); session.cancel();pool.close();await assert.rejects(pending,/취소/);assert.equal(worker.terminations,1); worker.respond(worker.requests[0]);assert.equal(resolved,false); }); test('Worker failures reject the pass and closing terminates every owned Worker',async()=>{ const session=new CombinationWorkerSession();const id=session.start();const worker=new FakeWorker();worker.automatic=false; const pool=new CombinationEvaluationPool(()=>worker as unknown as Worker,session,id,1); const pending=pool.run(candidates,chart,{resultLimit:10,onPhase:()=>{},onProgress:()=>{}}); worker.onerror?.();await assert.rejects(pending,/Worker/);pool.close();assert.equal(worker.terminations,1); }); test('every prepared candidate keeps all allowed cooldowns and receives precision evaluation',async()=>{ const session=new CombinationWorkerSession();const id=session.start();const workers:FakeWorker[]=[]; const pool=new CombinationEvaluationPool(()=>{const worker=new FakeWorker();workers.push(worker);return worker as unknown as Worker;},session,id,4); const result=await pool.run(candidates,chart,{resultLimit:10,onPhase:()=>{},onProgress:()=>{}}); assert.equal(pool.lastExactCount,candidates.length);assert.equal(result.length,10); assert.equal(workers.reduce((sum,worker)=>sum+worker.requests.reduce((n,request)=>n+request.candidates.length,0),0),candidates.length); assert.ok(workers.every(worker=>worker.requests.every(request=>!request.summaryOnly&&request.candidates.every((candidate:EvaluationCandidate)=>candidate.cards.every(card=>card.allowedCooldownReductions?.length===4))))); pool.close();assert.ok(workers.every(worker=>worker.terminations===1)); });