server/laf-cloud/functions/cloudRise/tests/analytics.test.mjs
2026-09-24 17:59:52 +08:00

90 lines
5.0 KiB
JavaScript

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}],
]);
}
});