import test from 'node:test'; import assert from 'node:assert/strict'; import {collect, send, TEST_APP_ID, PRODUCTION_APP_ID} from '../analytics.ts'; function fixture(options = {}) { const instances = []; const sdkModule = { initWithBatchMode(appId, url, config) { const instance = { appId, url, config, events: [], closed: false, trackFirst(event) { this.events.push(event); if(options.validationError) event.callback(Error('invalid')); }, flush(callback) { if(!options.silent) callback(options.failure ? Error('network') : undefined); }, close() { this.closed=true; }, }; instances.push(instance); return instance; }}; return {instances, sdkModule}; } const user = {openid:'player-openid', distinctId:'player-distinct'}; const event = { id:'stable-id', event:'cloud_rise_start', properties:{stage:1,end_count:17}, appId:TEST_APP_ID, time:1789880000000 }; test('analytics SDK uses first-event IDs, correct identities and explicit flush before acknowledging', async () => { const f=fixture(); assert.deepEqual(await send([event],user,f.sdkModule),['stable-id']); const sdk=f.instances[0], actual=sdk.events[0]; assert.equal(sdk.appId,TEST_APP_ID); assert.equal(sdk.url,'https://data.nika4fun.com/sync_data'); assert.equal(sdk.config.batchSize,2); assert.equal(sdk.closed,true); assert.equal(actual.accountId,user.openid); assert.equal(actual.distinctId,user.distinctId); assert.equal(actual.event,event.event); assert.equal(actual.firstCheckId,event.id); assert.deepEqual(actual.properties,event.properties); assert.equal(actual.time.getTime(),event.time+8*3600000); }); test('analytics SDK separates test and production batches even when retried together', async () => { const f=fixture(), prod={...event,id:'prod-id',appId:PRODUCTION_APP_ID}; assert.deepEqual((await send([event,prod],user,f.sdkModule)).sort(),['prod-id','stable-id']); assert.deepEqual(f.instances.map(i=>[i.appId,i.events.map(e=>e.firstCheckId)]), [[TEST_APP_ID,['stable-id']],[PRODUCTION_APP_ID,['prod-id']]]); }); test('analytics delivery errors and validation errors remain retryable with stable IDs', async () => { for(const options of [{failure:true},{validationError:true}]) { const f=fixture(options); assert.deepEqual(await send([event],user,f.sdkModule),[]); assert.equal(f.instances[0].closed,true); const retry=fixture(); assert.deepEqual(await send([event],user,retry.sdkModule),[event.id]); assert.equal(retry.instances[0].events[0].firstCheckId,f.instances[0].events[0].firstCheckId); } }); test('analytics timeout is bounded and does not acknowledge an uncertain delivery', async () => { const f=fixture({silent:true}), start=Date.now(); assert.deepEqual(await send([event],user,f.sdkModule),[]); assert.ok(Date.now()-start < 3000); assert.equal(f.instances[0].closed,true); }); test('analytics waits for a real user identity and skips empty queues', async () => { const f=fixture(); assert.deepEqual(await send([event],{},f.sdkModule),[]); assert.deepEqual(await send([],user,f.sdkModule),[]); assert.equal(f.instances.length,0); assert.deepEqual(await send([event],{distinctId:'visitor'},f.sdkModule),[event.id]); assert.equal(f.instances[0].events[0].accountId,undefined); }); test('analytics maps legacy isDebug semantics consistently and does not count the player as a bot', () => { for(const stageNumber of [1,2,3]) { const target=[5,7,9][stageNumber-1]; for(const value of [true,'true',false,'false',undefined]) { const stage={stage:stageNumber,startedAt:123,target,status:'playing',success_num:0,results:[], opponents:Array.from({length:99},(_,i)=>({success_num:i<21?target:target-1}))}; const run={id:'r',stages:[stage]}; const events=collect(null,run,value,123); assert.equal(events.length,1); assert.deepEqual(events[0].properties,{stage:stageNumber,end_count:21}); assert.equal(events[0].appId,value===true||value==='true'?PRODUCTION_APP_ID:TEST_APP_ID); assert.deepEqual(collect(structuredClone(run),run,value,124),[]); } } }); test('analytics reports final failures at stage targets and exact saved awards without recalculation', () => { for(const stage of [1,2,3]) { const target=[5,7,9][stage-1]; const before={id:'r',stages:[{stage,startedAt:123,target,status:'playing',success_num:target-1, results:Array.from({length:target-1},(_,i)=>({id:String(i),outcome:'win'})),opponents:[]}]}; const after=structuredClone(before), last=after.stages[0]; last.results.push({id:'last',outcome:'lose'});last.status='lost'; assert.deepEqual(collect(before,after,false,124).map(e=>[e.event,e.properties]),[ ['cloud_rise_step',{stage,step:target,result:'failure'}],['cloud_rise_fail',{stage,step:target}], ]); last.results.at(-1).outcome='win';last.status='won';last.success_num=target;last.rewardSaved=true;last.reward=210; assert.deepEqual(collect(before,after,false,124).map(e=>[e.event,e.properties]),[ ['cloud_rise_step',{stage,step:target,result:'success'}],['cloud_rise_succeed',{stage,coin_amount:210}], ]); } });