import test from 'node:test'; import assert from 'node:assert/strict'; import { registerHooks } from 'node:module'; import { createHmac, createSign, createCipheriv, generateKeyPairSync } from 'node:crypto'; import { readFileSync } from 'node:fs'; const START = Date.parse('2026-09-17T00:00:00+08:00'), END = START + 4 * 86400000, NEXT = START + 7 * 86400000; const originalNow = Date.now, originalFetch = globalThis.fetch; let now = START + 1000, tables = {}, counter = 0, fault = null, beforeWrite = null; Date.now = () => now; globalThis.fetch = async () => { throw new Error('unexpected external request'); }; test.after(() => { Date.now = originalNow; globalThis.fetch = originalFetch; }); const copy = value => value == null ? value : structuredClone(value); const table = name => tables[name] ||= []; const field = (row, path) => path.split('.').reduce((v,k) => v?.[k], row); function put(row, path, value) { const parts = path.split('.'), last = parts.pop(); for (const p of parts) row = row[p] ||= {}; row[last] = copy(value); } function expression(row, value, variables = {}) { if (typeof value === 'string' && value.startsWith('$$')) return field(variables, value.slice(2)); if (typeof value === 'string' && value.startsWith('$')) return field(row, value.slice(1)); if (value === null || typeof value !== 'object' || value instanceof Date) return value; if (Array.isArray(value)) return value.map(v => expression(row, v, variables)); const [[op, args]] = Object.entries(value); if (op === '$filter') return expression(row, args.input, variables).filter(task => expression(row, args.cond, {...variables, [args.as]: task})); if (op === '$size') return expression(row, args, variables).length; const values = expression(row, args, variables); if (op === '$ifNull') return values[0] ?? values[1]; if (op === '$and') return values.every(Boolean); if (op === '$gt') return values[0] > values[1]; if (op === '$lte') return values[0] <= values[1]; if (op === '$ne') return values[0] !== values[1]; throw Error('unsupported expression ' + op); } function matches(row, query) { return Object.entries(query).every(([k,v]) => { if (k === '$expr') return v.$lt ? now < v.$lt[1].getTime() : expression(row, v); if (k === '$or') return v.some(q => matches(row,q)); const actual = field(row,k); if (v && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date)) return Object.entries(v).every(([op,x]) => { if (op === '$lt') return actual < x; if (op === '$lte') return actual <= x; if (op === '$gt') return actual > x; if (op === '$gte') return actual >= x; if (op === '$ne') return actual !== x; if (op === '$in') return x.includes(actual); if (op === '$nin') return !x.includes(actual); if (op === '$exists') return (actual !== undefined) === x; if (op === '$not' && x instanceof RegExp) return !x.test(actual); throw Error('unsupported query ' + op); }); return v === null ? actual == null : actual === v; }); } const mongo = { collection(name) { return { async findOne(query) { return copy(table(name).find(r => matches(r,query)) || null); }, find(query) { let values = table(name).filter(r=>matches(r,query)); return { sort(spec) { values.sort((a,b)=> { for (const [k,d] of Object.entries(spec)) { if (field(a,k)!==field(b,k)) return (field(a,k)r._id===data._id)) throw Object.assign(Error('duplicate'),{code:11000}); table(name).push(copy(data)); return { insertedId:data._id }; }, async updateOne(query, update, options={}) { if (beforeWrite) beforeWrite(name,query,update); if (fault && fault(name,query,update)) throw Error('injected storage failure'); let row=table(name).find(r=>matches(r,query)); if (!row && options.upsert) { if (table(name).some(r=>r._id===query._id)) throw Object.assign(Error('duplicate'),{code:11000}); row={_id:query._id || 'mock-'+(++counter),...copy(update.$setOnInsert || {})}; table(name).push(row); for (const [k,v] of Object.entries(update.$set || {})) put(row,k,v); return { modifiedCount:0, matchedCount:0, upsertedCount:1 }; } if (!row) return { modifiedCount:0,matchedCount:0 }; for (const [k,v] of Object.entries(update.$set || {})) put(row,k,v); for (const [k,v] of Object.entries(update.$inc || {})) put(row,k,(field(row,k)||0)+v); return {modifiedCount:1,matchedCount:1}; }, async deleteOne(query) { const i=table(name).findIndex(r=>matches(r,query)); if(i>=0)table(name).splice(i,1); return {deletedCount:i>=0?1:0}; }, async createIndex() { return 'index'; }, }; } }; globalThis.__goldCloud = { mongo:{db:mongo}, shared:{get(){return 'test-access-token';},set(){}}, storage:{bucket(){return {async readFile(){return {Body:{async transformToString(){return merchantKeys.privateKey;}}};}};}}, database:()=>({collection(name) { return { async add(data) { return mongo.collection(name).insertOne({_id:'mock-'+(++counter),...data}); }, where(query) { return { async getOne() { return {data:await mongo.collection(name).findOne(query)}; }, async get() { return {data:await mongo.collection(name).find(query).toArray()}; }, async update(data) { const r=await mongo.collection(name).updateOne(query,{$set:data}); return {updated:r.modifiedCount}; }, }; }, }; }}), }; globalThis.require = () => ({initWithBatchMode:()=>({track(){},trackFirst(){},close(){}})}); registerHooks({resolve(specifier,context,next) { if (specifier==='@lafjs/cloud') return {url:'data:text/javascript,export default globalThis.__goldCloud',shortCircuit:true}; if (specifier==='ip2region') return {url:'data:text/javascript,export default class { search(){return null;} }',shortCircuit:true}; if (specifier.startsWith('@/')) return {url:new URL('../../'+specifier.slice(2)+'.ts',import.meta.url).href,shortCircuit:true}; return next(specifier,context); }}); const R = await import('../config.ts'); const S = await import('../service.ts'); const P = await import('../payment.ts'); const C = await import('../../activityConfig/store.ts'); const {default:api} = await import('../index.ts'); const {default:jobs} = await import('../jobs.ts'); const {default:admin} = await import('../admin.ts'); const {default:android} = await import('../../wx/orderPaySig.ts'); const {default:ios} = await import('../../wx/iosorderPaySig.ts'); const {default:callback} = await import('../../wx/payCallBack.ts'); const {default:queryPayment} = await import('../../wx/getPayInfo.ts'); const {default:getOrderReward} = await import('../../wx/getOrderReward.ts'); const {default:legacyQuery} = await import('../../wx/iosgetPayInfo.ts'); const {default:customerService} = await import('../../wx/KeFuInfo.ts'); const L = await import('../legacyPayment.ts'); const M = await import('../merchant.ts'); const {default:merchantNotify} = await import('../merchantNotify.ts'); const {default:checkIos} = await import('../../wx/checkIos.ts'); const {default:saveLevel} = await import('../../userLevel.ts'); const config = {configVersion:'v1',status:'published',effectiveFromPeriodId:'goldMiner:2026-09-17',publishedAt:START-1000, unlockPassedLevel:41,timezone:'Asia/Shanghai',productId:'gold_miner',priceFen:600,currency:'CNY', tasks:[1,2,3].map((targetWins,i)=>({taskId:'t'+(i+1),sequence:i+1,targetWins,items:[{type:'coin',count:100*(i+1)}]}))}; const version = c => { const {configVersion,publishedAt,status,...data}=copy(c); return {_id:C.configId('goldMiner',configVersion),activityId:'goldMiner',recordType:'version',configVersion,publishedAt,status,effectiveFrom:R.periodDates(c.effectiveFromPeriodId).startsAt,config:data}; }; const entitlements=()=>table(R.TABLES.players).filter(p=>p.entitlement).map(p=>p.entitlement); const embeddedGrants=()=>table(R.TABLES.players).flatMap(p=>(p.tasks||[]).filter(t=>t.grantId)); const control=()=>{let c=table(R.TABLES.configs).find(c=>c.recordType==='control');if(!c){c={_id:S.controlId('goldMiner:2026-09-17'),activityId:'goldMiner',recordType:'control',purchaseEnabled:true};table(R.TABLES.configs).push(c);}return c;}; function reset(overrides={}) { now=START+1000;tables={};counter=0;fault=null;beforeWrite=null; table('users').push({_id:'u',token:'token',openid:'openid',session_key:'session',levelAmount:41,endLevelNum:1,coinAmount:500,isWhite:true,...overrides}); table(R.TABLES.configs).push(version(config)); process.env.PAYMENT_APP_ENV='production'; process.env.WX_MIDAS_PAY_SIGN_KEY='synthetic-key'; process.env.GOLD_MINER_ADMIN_TOKEN='admin-test'; process.env.ADMIN_TOKEN='admin-test'; delete process.env.GOLD_MINER_TEST_PERIODS_ENABLED; delete process.env.GOLD_MINER_LEGACY_ENABLED; globalThis.fetch = async () => { throw new Error('unexpected external request'); }; } const user=()=>copy(table('users')[0]); const ctx=body=>({body,headers:{},socket:{remoteAddress:'127.0.0.1'}}); const call=(action,more={})=>api(ctx({action,uid:'u',token:'token',...more})); const event=(i,mode='main',more={})=>({periodId:R.calendar(now).periodId,eventId:'event'+i,mode,outcome:'win', ...(mode==='main'?{clearedMainLevel:41+i}:{endlessSequence:i}),...more}); async function win(i,mode='main',more={}) { if (mode==='main') table('users')[0].levelAmount=41+i; return S.recordProgress('u',user(),event(i,mode,more)); } const player=periodId=>table(R.TABLES.players).find(p=>p.periodId===(periodId||'goldMiner:2026-09-17')); async function buy(create=android,more={}) { const result=await create(ctx({uid:'u',token:'token',itemid:'gold_miner',createRequestId:'buy1',itemCount:99,itemPrice:1,...more})); assert.equal(result.code,1,JSON.stringify(result)); return table('order').find(o=>o.outTradeNo===result.data.outTradeNo); } function notification(order,changes={}) { const Event='minigame_game_pay_goods_deliver_notify'; const Payload=JSON.stringify({OutTradeNo:order.outTradeNo,OpenId:order.openid,Env:0, GoodsInfo:{ProductId:order.itemid,Quantity:order.itemCount,OrigPrice:order.goodsPrice},...changes}); return ctx({Event,MiniGame:{Payload,PayEventSig:createHmac('sha256',process.env.WX_MIDAS_PAY_SIGN_KEY).update(Event+'&'+Payload).digest('hex'),IsMock:false}}); } async function pay(order) { const r=await callback(notification(order)); assert.equal(r.ErrCode,0,JSON.stringify(r)); } const claim=taskId=>call('claim',{periodId:'goldMiner:2026-09-17',taskId,requestId:'claim-'+taskId}); async function deliver(taskId,periodId='goldMiner:2026-09-17') { const g=await call('claim',{periodId,taskId,requestId:'claim-'+taskId}); assert.equal(g.code,1,JSON.stringify(g)); const r=await call('confirm_delivery',{periodId,taskId,grantId:g.data.grantId}); assert.equal(r.code,1,JSON.stringify(r)); return g.data.grantId; } test('calendar has exact Beijing Thursday/Monday boundaries and validates period IDs',()=>{ assert.equal(R.calendar(START).periodId,'goldMiner:2026-09-17'); assert.equal(R.open(R.calendar(START),START),true);assert.equal(R.open(R.calendar(END-1),END-1),true); assert.equal(R.open(R.calendar(END),END),false);assert.equal(R.calendar(NEXT).periodId,'goldMiner:2026-09-24'); assert.throws(()=>R.periodDates('goldMiner:2026-09-18'));assert.throws(()=>R.periodDates('goldMiner:2026-02-30')); assert.equal(R.periodDates('goldMiner:2026-12-31').nextPeriodId,'goldMiner:2027-01-07'); }); test('configuration rejects unsupported rewards, gaps, duplicate goals, invalid price and unbounded dedup size',()=>{ assert.equal(R.validateConfig(config).priceFen,600); for (const change of [c=>c.priceFen=0,c=>c.tasks[1].targetWins=1,c=>c.tasks[1].sequence=3, c=>c.tasks[1].taskId='t1',c=>c.tasks[0].items[0].type='hammer',c=>c.tasks[2].targetWins=1001,c=>c.timezone='UTC']) { const c=copy(config);change(c);assert.throws(()=>R.validateConfig(c)); } }); test('authentication fails closed, account scope is fixed, and level 40 is locked',async()=>{ reset({levelAmount:40});assert.equal((await call('info')).data.status,'locked'); for(const extra of [{token:''},{token:'bad'},{uid:'other'},{gameName:'iaa'}]) assert.equal((await call('info',extra)).errorCode,'UNAUTHORIZED'); delete table('users')[0].token;assert.equal((await call('info')).errorCode,'UNAUTHORIZED'); }); test('level 41 only unlocks; first main win creates a record without info or payment',async()=>{ reset();const zero=await S.recordProgress('u',user(),event(0));assert.equal(zero.activityCounted,false); assert.equal((await win(1)).progressWins,1);assert.equal(player().entitlementStatus,'none'); assert.equal(player().configSnapshot.tasks.length,3);assert.equal(table('users')[0].coinAmount,500); }); test('main and endless share progress; repeated template is allowed; request and business keys dedup',async()=>{ reset();await win(1);assert.equal((await win(1)).duplicate,true); assert.equal((await win(1,'main',{eventId:'new-request'})).duplicate,true); assert.equal((await win(2,'endless',{templateId:100})).progressWins,2); assert.equal((await win(3,'endless',{templateId:100})).progressWins,3); assert.equal((await win(4,'endless')).activityCounted,false);assert.equal(player().progressDedup.entries.length,3); await assert.rejects(()=>win(8,'endless',{eventId:'event1'}),e=>e.errorCode==='EVENT_PAYLOAD_CONFLICT'); }); test('parallel distinct and duplicate events update dedup and progress atomically',async()=>{ reset();table(R.TABLES.configs)[0].config.tasks=[{taskId:'all',sequence:1,targetWins:50,items:[{type:'coin',count:100}]}]; await Promise.all(Array.from({length:8},(_,i)=>win(i+1,'endless'))); await Promise.all(Array.from({length:8},()=>win(9,'endless'))); assert.equal(player().progressWins,9);assert.equal(player().progressDedup.entries.length,9); }); test('cutoff rejects new events, preserves previous success on retry, and never moves old events into next period',async()=>{ reset();const original=event(1);await win(1);now=END; assert.equal((await S.recordProgress('u',user(),original)).duplicate,true); await assert.rejects(()=>win(2),e=>e.errorCode==='PERIOD_ENDED');now=NEXT; await assert.rejects(()=>S.recordProgress('u',user(),{...original,eventId:'late',clearedMainLevel:43}),e=>e.errorCode==='PERIOD_ENDED'); assert.equal(player().progressWins,1); }); test('database time prevents an in-flight write after cutoff; no recovery record is created',async()=>{ reset();await call('info');now=END-1; beforeWrite=(name,q)=>{if(name===R.TABLES.players&&q.$expr) now=END;}; await assert.rejects(()=>win(1),e=>e.errorCode==='PERIOD_ENDED'); assert.equal(player().progressWins,0);assert.deepEqual(player().progressDedup.entries,[]); }); test('failed progress write is not counted and has no late replay queue',async()=>{ reset();await call('info');fault=(name,q)=>name===R.TABLES.players&&q.revision!==undefined; await assert.rejects(()=>win(1));fault=null;now=END; await assert.rejects(()=>win(1),e=>e.errorCode==='PERIOD_ENDED');assert.equal(player().progressWins,0); assert.equal(Object.keys(tables).some(n=>n.includes('clear_events')),false); }); test('period snapshot stays unchanged after config edits and next period uses the new version',async()=>{ reset();await call('info');table(R.TABLES.configs).push(version({...copy(config),configVersion:'vLater',effectiveFromPeriodId:'goldMiner:2026-09-24',publishedAt:START+5000,tasks:[{taskId:'x',sequence:1,targetWins:1,items:[{type:'coin',count:999}]}]})); assert.equal((await call('info')).data.tasks[0].itemsSnapshot[0].count,100); table(R.TABLES.configs).push(version({...copy(config),configVersion:'v2',effectiveFromPeriodId:'goldMiner:2026-09-24',publishedAt:START+10000,priceFen:900})); now=NEXT;assert.equal((await call('info')).data.priceFen,900);assert.equal(player().configSnapshot.priceFen,600); }); test('both native order routes use server price and one order per period, with a single business JSON field',async()=>{ for(const creator of [android,ios]) { reset();const result=await Promise.all([buy(creator),buy(creator,{createRequestId:'other'})]); assert.equal(table('order').length,1);assert.equal(result[0].goodsPrice,600);assert.equal(result[0].itemCount,1); assert.equal(result[0].goldMiner.periodId,'goldMiner:2026-09-17');assert.equal(result[0].goldMiner.fulfillmentRoute,null); assert.equal(result[0].goldMinerPeriodId,undefined);assert.equal(result[0].uid,undefined); } }); test('invalid auth, wrong reserved product, missing signing key, stopped and closed periods cannot create orders',async()=>{ reset();for(const extra of [{token:'bad'},{itemid:'gold_miner_other'},{createRequestId:''}]) { assert.equal((await android(ctx({uid:'u',token:'token',itemid:'gold_miner',createRequestId:'buy',...extra}))).code,0); } delete process.env.WX_MIDAS_PAY_SIGN_KEY;assert.equal((await android(ctx({uid:'u',token:'token',itemid:'gold_miner',createRequestId:'buy'}))).code,0); reset();await call('info');control().purchaseEnabled=false; assert.equal((await P.createNativeOrder({uid:'u',token:'token',itemid:'gold_miner',createRequestId:'b'},'and')).errorCode,'PURCHASE_DISABLED'); now=END;assert.equal((await P.createNativeOrder({uid:'u',token:'token',itemid:'gold_miner',createRequestId:'b'},'and')).errorCode,'PERIOD_ENDED'); }); test('production callback verifies signature, environment, user, product, amount and quantity',async()=>{ reset();const o=await buy(); for(const change of [c=>c.body.MiniGame.PayEventSig='0'.repeat(64),c=>c.body.MiniGame.IsMock=true, c=>Object.assign(c,notification(o,{OpenId:'wrong'})),c=>Object.assign(c,notification(o,{Env:1})), c=>Object.assign(c,notification(o,{GoodsInfo:{ProductId:o.itemid,Quantity:2,OrigPrice:600}}))]) { const c=notification(o);change(c);assert.notEqual((await callback(c)).ErrCode,0);assert.equal(o.state,0); } await pay(o);assert.equal(o.state,2);assert.equal(player().entitlementStatus,'unlocked');assert.equal(table('users')[0].coinAmount,500); }); test('payment and fulfillment retries do not grant twice or turn delivered original rewards into carry',async()=>{ reset();await win(1);const o=await buy();now=END;await pay(o); assert.equal(o.goldMiner.fulfillmentRoute,'settle_original'); const receipt=(await call('settlements')).data.items[0]; assert.equal((await call('confirm_settlement_delivery',{settlementId:receipt.settlementId,grantIds:receipt.rewards.map(r=>r.grantId)})).code,1); await pay(o);await jobs();assert.equal(entitlements().length,1); assert.equal(o.goldMiner.targetPeriodId,'goldMiner:2026-09-17');assert.equal(table('users')[0].coinAmount,500); }); test('earned tasks can be claimed and confirmed in any order without duplicating grants',async()=>{ reset();await win(1);await win(2);await win(3); assert.equal((await claim('t1')).errorCode,'NOT_PAID');const o=await buy();await pay(o); assert.equal((await claim('t2')).code,1); const grants=await Promise.all([claim('t1'),claim('t1')]);assert.equal(grants[0].data.grantId,grants[1].data.grantId); assert.equal(player().claimedThrough,0);assert.equal((await claim('t2')).code,1); assert.equal(table('users')[0].coinAmount,500); await deliver('t2');await deliver('t2');assert.equal(player().claimedThrough,0);await deliver('t1'); assert.equal(embeddedGrants().length,2);assert.equal(player().claimedThrough,2); }); test('only owning player may confirm and a fake grant cannot mark rewards delivered',async()=>{ reset();await win(1);const o=await buy();await pay(o);const g=await claim('t1'); assert.equal((await call('confirm_delivery',{periodId:o.goldMiner.periodId,taskId:'t1',grantId:'fake'})).errorCode,'INVALID_GRANT'); table('users').push({_id:'other',token:'other-token',levelAmount:50}); assert.equal((await call('confirm_delivery',{uid:'other',token:'other-token',periodId:o.goldMiner.periodId,taskId:'t1',grantId:g.data.grantId})).code,0); }); test('expiry closes claim, server prepares only unpaid deliveries and preserves in-flight grant',async()=>{ reset();await win(1);await win(2);await pay(await buy());const g=await claim('t1');now=END; assert.equal((await claim('t1')).errorCode,'PERIOD_SETTLEMENT_REQUIRED'); assert.equal((await call('settlements')).data.status,'settling'); assert.equal(player().progressClosed,false); // Query must not settle on behalf of the job. await jobs();assert.equal(player().progressClosed,true);assert.equal(table('users')[0].coinAmount,500); const list=(await call('settlements')).data;assert.equal(list.status,'pending_delivery'); const s=list.items[0];assert.deepEqual(s.rewards.map(r=>r.taskId),['t1','t2']); assert.equal(s.rewards[0].grantId,g.data.grantId); assert.equal((await call('ack_settlement',{settlementId:s.settlementId})).errorCode,'DELIVERY_PENDING'); assert.equal((await call('confirm_settlement_delivery',{settlementId:s.settlementId,grantIds:[s.rewards[1].grantId]})).data.deliveryStatus,'partial'); assert.equal(player().claimedThrough,0); // The original confirmation remains valid after cutoff and uses the same state as batch confirmation. assert.equal((await call('confirm_delivery',{periodId:s.periodId,taskId:'t1',grantId:g.data.grantId})).code,1); assert.deepEqual((await call('settlements')).data.items,[]); const body={settlementId:s.settlementId,grantIds:s.rewards.map(r=>r.grantId)}; assert.equal((await call('confirm_settlement_delivery',body)).data.deliveryStatus,'client_saved'); assert.equal((await call('confirm_settlement_delivery',body)).code,1); assert.deepEqual((await call('settlements')).data,{status:'no_pending_rewards',items:[],nextCursor:null}); assert.equal((await claim('t1')).errorCode,'PERIOD_SETTLEMENT_REQUIRED'); assert.equal((await call('ack_settlement',{settlementId:s.settlementId})).code,1); assert.equal(player().claimedThrough,2);assert.equal(table('users')[0].coinAmount,500); }); test('zero-reward delayed payment reserves next period; opens automatically and cannot be bought again',async()=>{ reset();const o=await buy();now=END;await pay(o); assert.equal(o.goldMiner.fulfillmentRoute,'carry_next');assert.equal(entitlements()[0].state,'reserved'); now=NEXT;await jobs();const info=await call('info');assert.equal(info.data.status,'unlocked');assert.equal(player('goldMiner:2026-09-24').entitlementSource,'delayed_payment_carry'); assert.equal(info.data.progressWins,0); assert.equal((await android(ctx({uid:'u',token:'token',itemid:'gold_miner',createRequestId:'next'}))).errorCode,'ALREADY_UNLOCKED'); now=NEXT+4*86400000;await jobs();assert.equal(entitlements().length,1); assert.equal(player('goldMiner:2026-09-24').settlementState,'no_reward'); }); test('carry activation preserves already-earned next-period unpaid progress',async()=>{ reset();const o=await buy();now=NEXT;await win(1);assert.equal(player('goldMiner:2026-09-24').progressWins,1); await pay(o);assert.equal(player('goldMiner:2026-09-24').progressWins,1);assert.equal((await call('info')).data.status,'unlocked'); }); test('normal in-period payment with zero rewards does not carry',async()=>{ reset();const o=await buy();await pay(o);now=END;await jobs();assert.equal(o.goldMiner.fulfillmentRoute,'current_period'); now=NEXT;assert.equal((await call('info')).data.status,'purchasable');assert.equal(entitlements().length,1); }); test('route and embedded entitlement survive storage outages without a new reward',async()=>{ reset();await win(1);const o=await buy(); fault=(name,q,u)=>name===R.TABLES.players&&!!u.$set?.entitlement; assert.notEqual((await callback(notification(o))).ErrCode,0);assert.equal(o.goldMiner.fulfillmentRoute,'current_period'); now=END;fault=null;await pay(o); const receipt=(await call('settlements')).data.items[0], id=receipt.rewards[0].grantId; await pay(o);assert.equal((await call('settlements')).data.items[0].rewards[0].grantId,id); assert.equal(embeddedGrants().length,1);assert.equal(entitlements().length,1); }); test('generic reward endpoint cannot fake payment; query retries confirmed fulfillment and requires auth',async()=>{ reset();const o=await buy(); assert.equal((await getOrderReward(ctx({uid:'u',token:'token',outTradeNo:o.outTradeNo}))).errorCode,'PAYMENT_PENDING');assert.equal(o.state,0); await pay(o); assert.equal((await queryPayment(ctx({uid:'u',token:'bad',outTradeNo:o.outTradeNo}))).errorCode,'UNAUTHORIZED'); const r=await legacyQuery(ctx({uid:'u',token:'token',outTradeNo:o.outTradeNo}));assert.equal(r.code,1);assert.equal(r.data.rewardDelivery,'goldMiner.claim'); }); test('legacy customer-service payment rejects calls without an authenticated pre-order',async()=>{ reset();const r=await customerService(ctx({FromUserName:'openid',SessionFrom:JSON.stringify({tpye:'ios',propName:'gold_miner'})})); assert.equal(r.errorCode,'INVALID_INPUT');assert.equal(table('order').length,0); }); test('userLevel integration accepts form-encoded events and keeps ordinary save success on activity failure',async()=>{ reset();const e=event(1);const r=await saveLevel(ctx({action:'save',uid:'u',token:'token',levelAmount:'42',goldMiner:JSON.stringify(e)})); assert.equal(r.code,1);assert.equal(r.data.goldMiner.progressWins,1);assert.equal(table('users')[0].levelAmount,42); now=END;const late=await saveLevel(ctx({action:'save',uid:'u',token:'token',levelAmount:43,goldMiner:JSON.stringify(event(2))})); assert.equal(late.code,1);assert.equal(late.data.goldMiner.errorCode,'PERIOD_ENDED');assert.equal(table('users')[0].levelAmount,43); }); test('admin is protected and can only publish immutable future configurations',async()=>{ reset();assert.equal((await admin(ctx({action:'setup_indexes'}))).errorCode,'UNAUTHORIZED'); assert.equal((await admin(ctx({action:'publish',adminToken:'admin-test',config}))).errorCode,'CONFIG_UNAVAILABLE'); const future={...config,configVersion:'v2',effectiveFromPeriodId:'goldMiner:2026-09-24'}; assert.equal((await admin(ctx({action:'publish',adminToken:'admin-test',config:future}))).code,1); assert.equal((await admin(ctx({action:'publish',adminToken:'admin-test',config:future}))).code,1); assert.equal((await admin(ctx({action:'setup_indexes',adminToken:'admin-test'}))).code,1); }); test('an issuing task is displayed as delivery pending instead of a fresh claim button',async()=>{ reset();await win(1);await pay(await buy());await claim('t1'); const t=(await call('info')).data.tasks[0];assert.equal(t.claimStatus,'issuing');assert.equal(t.claimable,undefined);assert.equal(t.issuing,undefined);assert.equal(t.blockReason,undefined); }); test('ordinary abandoned pending order does not permanently block next-week purchases; late conflict is flagged',async()=>{ reset();const old=await buy();now=NEXT;const next=await buy();await pay(next); assert.notEqual((await callback(notification(old))).ErrCode,0); assert.equal(old.goldMiner.fulfillmentStatus,'manual_review');assert.equal(next.goldMiner.fulfillmentStatus,'fulfilled'); assert.equal(entitlements().length,1);assert.equal(player('goldMiner:2026-09-24').sourceOrderNo,next.outTradeNo); }); test('very late zero-progress payment never silently carries to a third period',async()=>{ reset();const o=await buy();now=NEXT+4*86400000; assert.notEqual((await callback(notification(o))).ErrCode,0);assert.equal(o.goldMiner.fulfillmentStatus,'manual_review'); assert.equal(o.goldMiner.targetPeriodId,'goldMiner:2026-09-24');assert.equal(entitlements().length,0); }); test('test deployment verifies and fulfills test orders with the same business rules',async()=>{ reset();process.env.PAYMENT_APP_ENV='test';const o=await buy(ios);assert.ok(o.outTradeNo.startsWith('wct_')); await pay(o);assert.equal(o.state,2);assert.equal(player().entitlementStatus,'unlocked'); o.paymentAppEnv='production';assert.notEqual((await callback(notification(o))).ErrCode,0); }); test('draft or absent commercial configuration disables sales without inventing price or rewards',async()=>{ reset();table(R.TABLES.configs)[0].status='draft';assert.equal((await call('info')).data.status,'unavailable'); assert.equal((await P.createNativeOrder({uid:'u',token:'token',itemid:'gold_miner',createRequestId:'b'},'and')).code,0); assert.equal(table('order').length,0); }); test('existing entitlement is activated through a win report without first opening the activity',async()=>{ reset();const o=await buy();now=END;await pay(o);now=NEXT; await win(1);assert.equal(player('goldMiner:2026-09-24').entitlementStatus,'unlocked'); assert.equal(player('goldMiner:2026-09-24').progressWins,1); }); test('same event key is isolated per user and unrelated confirmations cannot change another player',async()=>{ reset();await win(1);const u={...user(),_id:'second',token:'second-token',openid:'second-openid'};table('users').push(u); await S.recordProgress('second',u,event(1));assert.equal(table(R.TABLES.players).length,2); assert.ok(table(R.TABLES.players).every(p=>p.progressWins===1)); }); test('settlement atomic write retries keep original grant and acknowledgement in player record',async()=>{ reset();await win(1);await pay(await buy());const g=await claim('t1');now=END; fault=(name,q,u)=>name===R.TABLES.players&&!!u.$set?.settlementPreparedAt; assert.ok((await jobs()).failures);assert.equal((await call('settlements')).data.status,'settling'); fault=null;now+=60000;await jobs();const receipt=(await call('settlements')).data.items[0]; assert.equal(receipt.rewards[0].grantId,g.data.grantId); await call('confirm_settlement_delivery',{settlementId:receipt.settlementId,grantIds:[g.data.grantId]}); await call('ack_settlement',{settlementId:receipt.settlementId});await jobs(); assert.ok(player().settlementAcknowledgedAt);assert.equal(table('users')[0].coinAmount,500); }); test('confirmation storage failure leaves the authoritative grant pending for retry',async()=>{ reset();await win(1);await pay(await buy());const g=(await claim('t1')).data; fault=(name,q,u)=>name===R.TABLES.players&&u.$set?.claimedThrough===1; const body={periodId:'goldMiner:2026-09-17',taskId:'t1',grantId:g.grantId}; assert.equal((await call('confirm_delivery',body)).code,0);assert.equal(player().claimedThrough,0);fault=null; assert.equal((await call('confirm_delivery',body)).code,1);assert.equal(embeddedGrants()[0].claimStatus,'claimed'); }); test('timer drains pending states in bounded batches and malformed current config does not stop old closures',async()=>{ reset();await call('info');const seed=copy(player());table(R.TABLES.players).length=0; for(let i=0;i<101;i++) table(R.TABLES.players).push({...copy(seed),_id:String(i).padStart(4,'0'),uid:'user'+i}); now=NEXT;table(R.TABLES.configs)[0].config.priceFen=0; const first=await jobs();assert.equal(first.players,100);assert.equal(first.failures,1); const second=await jobs();assert.equal(second.players,1);assert.ok(table(R.TABLES.players).every(p=>p.progressClosed)); }); test('main event cannot claim historical completed levels or exceed the current save',async()=>{ reset({levelAmount:100});await assert.rejects(()=>S.recordProgress('u',user(),event(1)),e=>e.errorCode==='INVALID_INPUT'); await assert.rejects(()=>S.recordProgress('u',user(),event(100)),e=>e.errorCode==='INVALID_INPUT'); }); // Merchant-channel tests use generated RSA keys and AES-GCM; no live provider is contacted. const merchantKeys = generateKeyPairSync('rsa', {modulusLength:2048,publicKeyEncoding:{type:'spki',format:'pem'},privateKeyEncoding:{type:'pkcs8',format:'pem'}}); let provider, requests, sentLinks; function signedResponse(data, status=200, changes={}) { const raw = typeof data==='string' ? data : JSON.stringify(data); const headers = {'wechatpay-timestamp':String(Math.floor(now/1000)),'wechatpay-nonce':'nonce','wechatpay-serial':'test-public-key'}; headers['wechatpay-signature']=createSign('RSA-SHA256').update(`${headers['wechatpay-timestamp']}\nnonce\n${raw}\n`).sign(merchantKeys.privateKey,'base64'); Object.assign(headers,changes); return {ok:status>=200&&status<300,status,headers:{get:k=>headers[k]},async text(){return raw;}}; } function legacyReset() { reset();requests=[];sentLinks=[];provider={tradeState:'NOT_EXIST',badSignature:false,prepayCalls:0,closeCalls:0}; Object.assign(process.env,{ GOLD_MINER_LEGACY_ENABLED:'true',WX_MCH_ID:'merchant',WX_MINIGAME_APP_ID:'app',WX_MCH_CERT_SERIAL_NO:'cert',WX_PAY_PRIVATE_KEY_BUCKET:'bucket', GOLD_MINER_ORDER_TICKET_SECRET:'0123456789abcdef0123456789abcdef',GOLD_MINER_MERCHANT_NOTIFY_URL:'https://test.invalid/goldMiner/merchantNotify', GOLD_MINER_PAY_PAGE_URL:'https://pay.invalid/order3.html',GOLD_MINER_CHECK_IOS_URL:'https://test.invalid/wx/checkIos', GOLD_MINER_WECHATPAY_PUBLIC_KEYS:JSON.stringify({'test-public-key':merchantKeys.publicKey}),GOLD_MINER_WECHATPAY_API_V3_KEY:'0123456789abcdef0123456789abcdef', }); globalThis.fetch=async (url,options)=>{ requests.push({url,options}); if(url.startsWith('https://api.weixin.qq.com/')) {sentLinks.push(JSON.parse(options.body));return {ok:true,async json(){return {errcode:0};}};} if(provider.throwNetwork) throw Error('network interrupted'); const order=table('order').find(o=>o.goldMiner?.paymentChannel==='legacy_ios'); const bad=provider.badSignature?{'wechatpay-signature':'invalid'}:{}; if(url.endsWith('/v3/pay/transactions/jsapi')) { provider.prepayCalls++;provider.tradeState='NOTPAY'; if(provider.afterPrepay) await provider.afterPrepay(order); return signedResponse({prepay_id:'wx-prepay-'+provider.prepayCalls},200,bad); } if(url.endsWith('/close')) { provider.closeCalls++; if(provider.closePaid) {provider.tradeState='SUCCESS';return signedResponse({code:'ORDERPAID'},400);} provider.tradeState='CLOSED'; return signedResponse('',204,bad); } if(provider.tradeState==='NOT_EXIST')return signedResponse({code:'ORDER_NOT_EXIST'},404,bad); return signedResponse({...merchantTransaction(order),trade_state:provider.tradeState,...provider.queryChanges},200,bad); }; } async function legacyBuy(more={}) { const r=await call('create_order',{channel:'legacy_ios',itemid:'gold_miner',createRequestId:'legacy-buy',...more}); assert.equal(r.code,1,JSON.stringify(r));return r.data; } const legacyOrder=()=>table('order').find(o=>o.goldMiner?.paymentChannel==='legacy_ios'); async function sendLink(data) {return customerService(ctx({FromUserName:'openid',SessionFrom:data.sessionFrom}));} function linkParam() {return JSON.parse(decodeURIComponent(sentLinks.at(-1).link.url.split('#data=')[1]));} function merchantTransaction(order,changes={}) { return {out_trade_no:order.outTradeNo,appid:'app',mchid:'merchant',payer:{openid:order.openid},trade_type:'JSAPI',trade_state:'SUCCESS', amount:{total:order.goodsPrice,currency:'CNY'},transaction_id:'transaction-'+order.outTradeNo,success_time:new Date(now).toISOString(),attach:order.goldMiner.periodId,...changes}; } function merchantNotice(order,changes={}) { const cipher=createCipheriv('aes-256-gcm',Buffer.from(process.env.GOLD_MINER_WECHATPAY_API_V3_KEY),Buffer.from('123456789012')); cipher.setAAD(Buffer.from('transaction')); const ciphertext=Buffer.concat([cipher.update(JSON.stringify(merchantTransaction(order,changes))),cipher.final(),cipher.getAuthTag()]).toString('base64'); const raw=JSON.stringify({event_type:'TRANSACTION.SUCCESS',resource:{algorithm:'AEAD_AES_256_GCM',original_type:'transaction',nonce:'123456789012',associated_data:'transaction',ciphertext}}); const response=signedResponse(raw);let status; return {body:JSON.parse(raw),rawBody:raw,headers:Object.fromEntries(['timestamp','nonce','serial','signature'].map(k=>['wechatpay-'+k,response.headers.get('wechatpay-'+k)])), response:{status(value){status=value;}},get status(){return status;}}; } test('legacy pre-order is authenticated, server-priced, canonical and channel-exclusive',async()=>{ legacyReset();const first=await legacyBuy({itemPrice:1,itemCount:99});const o=legacyOrder(); assert.equal(first.priceFen,600);assert.equal(first.quantity,1);assert.equal(o.goldMiner.periodId,R.calendar(now).periodId); assert.equal(o.goldMiner.merchantSnapshot.mchid,'merchant');assert.equal(table('iosOrder').length,0);assert.equal(requests.length,0); assert.equal((await legacyBuy({createRequestId:'another'})).outTradeNo,first.outTradeNo);assert.equal(table('order').length,1); assert.equal((await android(ctx({uid:'u',token:'token',itemid:'gold_miner',createRequestId:'native'}))).errorCode,'PAYMENT_CHANNEL_CONFLICT'); legacyReset();await buy();assert.equal((await call('create_order',{channel:'legacy_ios',itemid:'gold_miner',createRequestId:'legacy'})).errorCode,'PAYMENT_CHANNEL_CONFLICT'); }); test('legacy feature gate, auth and purchase cutoff prevent pre-orders and payment links',async()=>{ legacyReset();process.env.GOLD_MINER_LEGACY_ENABLED='false';assert.equal((await call('create_order',{channel:'legacy_ios'})).errorCode,'PAYMENT_CHANNEL_DISABLED'); legacyReset();assert.equal((await call('create_order',{token:'bad',channel:'legacy_ios'})).errorCode,'UNAUTHORIZED'); const d=await legacyBuy();control().purchaseEnabled=false;assert.equal((await sendLink(d)).errorCode,'PURCHASE_DISABLED'); control().purchaseEnabled=true;now=END;assert.equal((await sendLink(d)).code,0);assert.equal(provider.prepayCalls,0); }); test('customer link ignores client price and uses scoped tickets and environment-specific check URL',async()=>{ legacyReset();const d=await legacyBuy();let session=JSON.parse(d.sessionFrom);session.price=1;session.count=99; assert.equal((await sendLink({...d,sessionFrom:JSON.stringify(session)})).code,1); const o=legacyOrder(), param=linkParam(); assert.equal(param.checkUrl,'https://test.invalid/wx/checkIos');assert.equal(param.quantity,1);assert.equal(param.price,600); assert.equal(table('iosOrder')[0].goldMiner.configHash,o.goldMiner.configHash); assert.equal(table('iosOrder')[0].goldMiner.paymentChannel,'legacy_ios'); const posted=JSON.parse(requests.find(r=>r.url.endsWith('/transactions/jsapi')).options.body); assert.equal(posted.time_expire,new Date(END).toISOString());assert.equal(posted.amount.total,600); assert.equal((await sendLink(d)).code,1);assert.equal(provider.prepayCalls,1); assert.equal((await customerService(ctx({FromUserName:'another',SessionFrom:d.sessionFrom}))).errorCode,'UNAUTHORIZED'); session.goldMinerTicket=session.goldMinerTicket.slice(0,-1)+(session.goldMinerTicket.endsWith('0')?'1':'0'); assert.equal((await sendLink({...d,sessionFrom:JSON.stringify(session)})).errorCode,'UNAUTHORIZED'); }); test('checkIos preserves full snapshots, is idempotent, and never turns association into payment',async()=>{ legacyReset();const d=await legacyBuy();await sendLink(d);const param=linkParam();const o=legacyOrder(); assert.equal(await checkIos(ctx({openid:param.openid,time:param.time,outTradeNo:param.outTradeNo})),true); assert.equal(o.state,0);assert.equal(o.goldMiner.confirmedAt,0);assert.equal(table('iosOrder').length,0); assert.equal(await checkIos(ctx({openid:param.openid,time:param.time,outTradeNo:param.outTradeNo})),true); assert.equal(await checkIos(ctx({openid:'another',time:param.time,outTradeNo:param.outTradeNo})),false); assert.equal(await checkIos(ctx({openid:param.openid,time:'12345',outTradeNo:param.outTradeNo})),false); }); test('payment before transfer and stale compatibility copy cannot roll back paid order',async()=>{ legacyReset();const d=await legacyBuy();await sendLink(d);const param=linkParam(),o=legacyOrder(); assert.equal((await merchantNotify(merchantNotice(o))).code,'SUCCESS');assert.equal(o.state,2); assert.equal(table('iosOrder')[0].state,0); assert.equal(await checkIos(ctx({openid:param.openid,time:param.time,outTradeNo:param.outTradeNo})),true); assert.equal(o.state,2);assert.equal(o.goldMiner.fulfillmentStatus,'fulfilled');assert.equal(entitlements().length,1); }); test('compatibility repair copies all activity fields and refuses a conflicting snapshot',async()=>{ legacyReset();const d=await legacyBuy();await sendLink(d);const param=linkParam(),saved=copy(legacyOrder()); table('order').length=0; assert.equal(await checkIos(ctx({openid:param.openid,time:param.time,outTradeNo:param.outTradeNo})),true); assert.deepEqual(legacyOrder().goldMiner,saved.goldMiner); await sendLink(d);table('iosOrder')[0].goldMiner.periodId='goldMiner:2026-09-24'; assert.equal(await checkIos(ctx({uid:'u',token:'token',outTradeNo:param.outTradeNo})),false); assert.equal(table('iosOrder').length,1); }); test('merchant notification verifies signature, timestamp, decryption and exact payment fields',async()=>{ legacyReset();await legacyBuy();const o=legacyOrder(); for(const patch of [{mchid:'wrong'},{appid:'wrong'},{payer:{openid:'other'}},{amount:{total:1,currency:'CNY'}},{trade_state:'NOTPAY'},{trade_type:'NATIVE'}]) { const c=merchantNotice(o,patch);assert.equal((await merchantNotify(c)).code,'FAIL');assert.equal(c.status,500);assert.equal(o.state,0); } for(const patch of [{'wechatpay-signature':'fake'},{'wechatpay-timestamp':'1'},{'wechatpay-serial':'unknown'}]) { const c=merchantNotice(o);Object.assign(c.headers,patch);assert.equal((await merchantNotify(c)).code,'FAIL'); } const c=merchantNotice(o);c.rawBody=c.rawBody.replace('TRANSACTION.SUCCESS','TRANSACTION.FAIL');assert.equal((await merchantNotify(c)).code,'FAIL'); const valid=merchantNotice(o);assert.equal((await merchantNotify(valid)).code,'SUCCESS');assert.equal(valid.status,200); assert.equal(o.goldMiner.confirmationSource,'merchant_notify');assert.ok(o.goldMiner.transactionId);assert.equal(o.state,2); assert.equal((await merchantNotify(merchantNotice(o))).code,'SUCCESS');assert.equal(entitlements().length,1); }); test('merchant confirmation is durable before acknowledgement and fulfillment failures are retried',async()=>{ legacyReset();await legacyBuy();const o=legacyOrder();fault=(name,q,u)=>name===R.TABLES.players&&!!u.$set?.entitlement; const c=merchantNotice(o);assert.equal((await merchantNotify(c)).code,'SUCCESS');assert.equal(o.state,1);assert.ok(o.goldMiner.confirmedAt); fault=null;now+=60000;await jobs();assert.equal(o.state,2);assert.equal(entitlements().length,1); legacyReset();await legacyBuy();fault=(name,q,u)=>name==='order'&&u.$set?.['goldMiner.confirmedAt']; assert.equal((await merchantNotify(merchantNotice(legacyOrder()))).code,'FAIL');assert.equal(legacyOrder().state,0); }); test('merchant notifications cannot confirm native orders and native notifications cannot confirm merchant orders',async()=>{ legacyReset();const native=await buy();assert.equal((await merchantNotify(merchantNotice(native))).code,'FAIL');assert.equal(native.state,0); legacyReset();await legacyBuy();const o=legacyOrder();assert.notEqual((await callback(notification(o))).ErrCode,0);assert.equal(o.state,0); }); test('all authenticated query endpoints recover missing merchant notifications without client coin delivery',async()=>{ for(const fn of [queryPayment,legacyQuery,getOrderReward]) { legacyReset();const d=await legacyBuy();provider.tradeState='SUCCESS'; assert.equal((await fn(ctx({uid:'u',token:'bad',outTradeNo:d.outTradeNo}))).code,0);assert.equal(requests.length,0); const result=await fn(ctx({uid:'u',token:'token',outTradeNo:d.outTradeNo})); assert.equal(result.code,1,JSON.stringify(result));assert.equal(result.data.rewardDelivery,'goldMiner.claim'); assert.equal(legacyOrder().goldMiner.confirmationSource,'merchant_query');assert.equal(table('users')[0].coinAmount,500); } }); test('unverified query responses and network failures retain unpaid orders with bounded retry',async()=>{ legacyReset();const d=await legacyBuy();provider.tradeState='SUCCESS';provider.badSignature=true; let result=await legacyQuery(ctx({uid:'u',token:'token',outTradeNo:d.outTradeNo}));assert.equal(result.errorCode,'INVALID_PAYMENT_SIGNATURE'); const o=legacyOrder();assert.equal(o.state,0);assert.ok(o.goldMiner.nextQueryAt>now);const count=requests.length; await legacyQuery(ctx({uid:'u',token:'token',outTradeNo:d.outTradeNo}));assert.equal(requests.length,count); now=o.goldMiner.nextQueryAt;provider.badSignature=false;provider.throwNetwork=true; result=await legacyQuery(ctx({uid:'u',token:'token',outTradeNo:d.outTradeNo}));assert.equal(result.code,0);assert.equal(table('order').length,1);assert.equal(o.state,0); }); test('concurrent polling takes one query lease and repeats return the same entitlement',async()=>{ legacyReset();const d=await legacyBuy();provider.tradeState='SUCCESS'; await Promise.all(Array.from({length:6},()=>legacyQuery(ctx({uid:'u',token:'token',outTradeNo:d.outTradeNo})))); assert.equal(requests.filter(r=>r.options.method==='GET').length,1);assert.equal(entitlements().length,1); }); test('prepay timeout reuses the same order number and regenerates only the prepay session',async()=>{ legacyReset();const d=await legacyBuy();provider.afterPrepay=()=>{throw Error('lost prepay response');}; assert.equal((await sendLink(d)).code,0);const o=legacyOrder();assert.equal(o.goldMiner.prepayState,'creating'); provider.afterPrepay=null;assert.equal((await sendLink(d)).code,1);assert.equal(legacyOrder().outTradeNo,d.outTradeNo);assert.equal(table('order').length,1); assert.equal(provider.prepayCalls,2);assert.equal(requests.filter(r=>r.options.method==='GET').length,1); }); test('period expiry closes unpaid orders and close-versus-payment race still grants correctly',async()=>{ legacyReset();const d=await legacyBuy();await sendLink(d);const o=legacyOrder();now=END;await jobs(); assert.equal(o.goldMiner.prepayState,'closed');assert.equal(provider.closeCalls,1);assert.equal(o.state,0);assert.equal(table('order').length,1); assert.equal((await legacyQuery(ctx({uid:'u',token:'token',outTradeNo:d.outTradeNo}))).errorCode,'ORDER_CLOSED'); legacyReset();await legacyBuy();provider.tradeState='NOTPAY';provider.closePaid=true;now=END;await jobs(); assert.equal(legacyOrder().state,2);assert.equal(legacyOrder().goldMiner.fulfillmentRoute,'carry_next'); }); test('delayed merchant payment settles original rewards or carries exactly once',async()=>{ for(const earned of [false,true]) { legacyReset();const d=await legacyBuy();if(earned)await win(1);now=END; assert.equal((await merchantNotify(merchantNotice(legacyOrder()))).code,'SUCCESS'); assert.equal(legacyOrder().goldMiner.fulfillmentRoute,earned?'settle_original':'carry_next'); if(earned)assert.equal((await call('settlements')).data.items[0].rewards[0].taskId,'t1'); else {now=NEXT;assert.equal((await call('info')).data.status,'unlocked');assert.equal(player('goldMiner:2026-09-24').entitlementSource,'delayed_payment_carry');} assert.equal((await legacyQuery(ctx({uid:'u',token:'token',outTradeNo:d.outTradeNo}))).code,1); assert.equal(entitlements().length,1);assert.equal(table('users')[0].coinAmount,500); } }); test('login recovery and timer find payments without checkIos ever being called',async()=>{ legacyReset();await legacyBuy();provider.tradeState='SUCCESS';await L.recoverLegacyOrders('other');assert.equal(requests.length,0); await L.recoverLegacyOrders('u');assert.equal(legacyOrder().state,2); legacyReset();await legacyBuy();provider.tradeState='SUCCESS';await jobs();assert.equal(legacyOrder().state,2);assert.equal(table('iosOrder').length,0); }); test('test/production order and ticket boundaries cannot be bypassed',async()=>{ legacyReset();process.env.PAYMENT_APP_ENV='test';const d=await legacyBuy();assert.ok(d.outTradeNo.startsWith('wct_')); process.env.PAYMENT_APP_ENV='production';assert.equal((await sendLink(d)).errorCode,'ORDER_ENVIRONMENT_MISMATCH'); assert.equal((await merchantNotify(merchantNotice(legacyOrder()))).code,'FAIL');assert.equal(legacyOrder().state,0); }); test('legacy preorder refuses incomplete merchant verification and page configuration',async()=>{ for (const [name,value] of [['GOLD_MINER_WECHATPAY_API_V3_KEY',''],['GOLD_MINER_WECHATPAY_PUBLIC_KEYS','{}'],['GOLD_MINER_CHECK_IOS_URL','https://test.invalid/wrong'],['GOLD_MINER_PAY_PAGE_URL','https://pay.invalid/order3.html#wrong']]) { legacyReset();process.env[name]=value; const r=await call('create_order',{channel:'legacy_ios',itemid:'gold_miner',createRequestId:'config-check'}); assert.equal(r.errorCode,'PAYMENT_CONFIG_UNAVAILABLE');assert.equal(table('order').length,0); } }); test('B group career counter excludes the first 15 main levels without changing main saves',async()=>{ const b={layer_1:{name:'new1_50',enabled:true,group:'B'}}; for(const [from,to,abTests,expected] of [ [14,15,b,0], [14,17,b,2], [15,16,b,1], [14,17,JSON.stringify(b),2], [14,15,{layer_1:{name:'new1_50',group:'2'}},0], [14,15,{layer_1:{name:'new1_50',group:'A'}},1], [14,15,{layer_1:{name:'new1_50',group:'B',enabled:false}},1], [14,15,{layer_1:{name:'other',group:'B'}},1], [14,15,undefined,1], [14,15,'invalid-json',1], ]) { reset({levelAmount:from,addLevel:7,abTests}); const r=await saveLevel(ctx({action:'save',uid:'u',token:'token',levelAmount:to})); assert.equal(r.code,1);assert.equal(user().levelAmount,to);assert.equal(user().addLevel,7+expected); } }); test('B group career rule preserves independent main and endless gold miner progress',async()=>{ reset({addLevel:12,abTests:{layer_1:{name:'new1_50',enabled:true,group:'B'}}}); const main={action:'save',uid:'u',token:'token',levelAmount:42,goldMiner:event(1)}; let r=await saveLevel(ctx(main)); assert.equal(r.data.goldMiner.progressWins,1);assert.equal(user().addLevel,13); r=await saveLevel(ctx(main)); assert.equal(r.data.goldMiner.duplicate,true);assert.equal(player().progressWins,1);assert.equal(user().addLevel,13); r=await saveLevel(ctx({...main,isWuXian:'true',goldMiner:event(2,'endless')})); assert.equal(r.data.goldMiner.progressWins,2);assert.equal(user().addLevel,14);assert.equal(user().levelAmount,42); }); test('info combines activity states and removes legacy flags from every response shape',async()=>{ const removed=['availability','qualified','entitlement','purchasable','currency','nextStartsAt','nextPeriodId','claimedThrough']; async function expectState(status) { const r=await call('info');assert.equal(r.code,1);assert.equal(r.data.status,status); for(const field of removed) assert.equal(Object.hasOwn(r.data,field),false,field); return r.data; } reset();table(R.TABLES.configs)[0].status='draft';assert.deepEqual((await expectState('unavailable')).tasks,[]); reset();now=END;assert.deepEqual((await expectState('unavailable')).tasks,[]); reset({levelAmount:40});assert.deepEqual((await expectState('locked')).tasks,[]); reset();await win(1);await expectState('purchasable'); control().purchaseEnabled=false; let info=await expectState('purchase_disabled');assert.equal(info.progressWins,1);assert.equal(info.tasks[0].claimStatus,'pending_unlock'); await win(2);assert.equal((await expectState('purchase_disabled')).progressWins,2); assert.equal((await android(ctx({uid:'u',token:'token',itemid:'gold_miner',createRequestId:'disabled-test'}))).errorCode,'PURCHASE_DISABLED'); control().purchaseEnabled=true;const order=await buy();await pay(order); control().purchaseEnabled=false; info=await expectState('unlocked');assert.equal(info.tasks[0].claimStatus,'claimable'); assert.equal((await claim('t1')).code,1); }); test('info uses an explicit task projection through locked, claimable, issuing and claimed states',async()=>{ reset();await win(1);await win(2);await win(3); const read=async()=> (await call('info')).data; let info=await read();assert.deepEqual(info.tasks.map(t=>t.claimStatus),['pending_unlock','pending_unlock','pending_unlock']); assert.ok(info.tasks.every(t=>t.completed)); const order=await buy();await pay(order); info=await read();assert.deepEqual(info.tasks.map(t=>t.claimStatus),['claimable','claimable','claimable']); const grant=await claim('t1');assert.equal(grant.code,1); info=await read();assert.deepEqual(info.tasks.map(t=>t.claimStatus),['issuing','claimable','claimable']); assert.equal((await claim('t1')).data.grantId,grant.data.grantId); assert.equal((await claim('t2')).code,1); await deliver('t1'); info=await read();assert.deepEqual(info.tasks.map(t=>t.claimStatus),['claimed','issuing','claimable']); assert.deepEqual(info.tasks.map(t=>t.taskId),['t1','t2','t3']); const keys=['taskId','targetWins','itemsSnapshot','claimStatus','completed','progress'].sort(); for(const t of info.tasks) assert.deepEqual(Object.keys(t).sort(),keys); assert.ok(player().tasks[0].grantId);assert.ok(player().tasks[0].clientSavedAt); assert.equal(player().claimedThrough,1);assert.equal(player().tasks[0].sequence,1); }); test('manual claim crossing the database cutoff is rejected without authorizing a grant',async()=>{ reset();await win(1);await pay(await buy());now=END-1; beforeWrite=(name,q,u)=>{if(name===R.TABLES.players&&u.$set?.tasks){now=END;beforeWrite=null;}}; assert.equal((await claim('t1')).errorCode,'PERIOD_SETTLEMENT_REQUIRED'); assert.equal(player().tasks[0].claimStatus,'unclaimed');assert.equal(embeddedGrants().length,0); }); test('settlements return ready periods even when another period still needs settlement',async()=>{ reset();await win(1);await win(2);await pay(await buy());await deliver('t1');now=END;await jobs(); now=NEXT+1000;await win(1);await pay(await buy());now=NEXT+4*86400000;await jobs(); const all=(await call('settlements')).data;assert.equal(all.items.length,2); assert.deepEqual(all.items.flatMap(s=>s.rewards.map(r=>r.taskId)).sort(),['t1','t2']); const row=table(R.TABLES.players).sort((a,b)=>a._id.localeCompare(b._id))[0];delete row.settlementPreparedAt; const mixed=(await call('settlements')).data; assert.equal(mixed.status,'pending_delivery');assert.equal(mixed.items.length,1); assert.notEqual(mixed.items[0].periodId,row.periodId); await call('confirm_settlement_delivery',{settlementId:mixed.items[0].settlementId,grantIds:mixed.items[0].rewards.map(r=>r.grantId)}); assert.equal((await call('settlements')).data.status,'settling'); await jobs();assert.equal((await call('settlements')).data.status,'pending_delivery'); reset();await win(1);now=END;await jobs();assert.deepEqual((await call('settlements')).data.items,[]); reset();assert.deepEqual((await call('settlements')).data,{status:'no_pending_rewards',items:[],nextCursor:null}); }); test('userLevel never opens CloudRise when progress reaches the unlock level',async()=>{ reset({onlyId:1001,levelAmount:99}); table('activityConfigs').push({_id:'cloud-rise-unlock',activityId:'cloudRise',recordType:'version',configVersion:'v1',status:'published',enabled:true,effectiveFrom:START,publishedAt:START,config:{unlockLevel:100,durationHours:1,pools:[10000,15000,20000]}}); const result=await saveLevel(ctx({action:'save',uid:'u',token:'token',levelAmount:100})); assert.equal(result.code,1);assert.equal(result.data.cloudRisePeriod,undefined);assert.equal(user().cloudRisePeriod,undefined); }); test('server-sized settlement pages continue past settling periods and check the whole account before reporting no rewards',async()=>{ reset();await win(1);await pay(await buy());now=END;await jobs(); const earned=copy(player());tables[R.TABLES.players]=[]; for(let i=0;i<21;i++) table(R.TABLES.players).push({...copy(earned),_id:'history-'+String(i).padStart(2,'0')}); const rows=table(R.TABLES.players); for(const row of rows.slice(0,20)) { row.tasks.forEach(t=>t.claimStatus='claimed');row.settlementState='client_saved'; } const first=(await call('settlements')).data; assert.deepEqual(first,{status:'pending_delivery',items:[],nextCursor:'history-19'}); // Old client values cannot alter the server's page size. for(const limit of [1,100,0,'invalid']) assert.deepEqual((await call('settlements',{limit})).data,first); const second=(await call('settlements',{afterId:first.nextCursor})).data; assert.equal(second.items.length,1);assert.equal(second.nextCursor,null); rows[0].settlementPreparedAt=null; assert.deepEqual((await call('settlements')).data,{status:'settling',items:[],nextCursor:'history-19'}); assert.equal((await call('settlements',{afterId:first.nextCursor})).data.items.length,1); rows[0].settlementPreparedAt=now; // A ready reward before the cursor must also prevent a false global empty result. rows[0].tasks=copy(earned.tasks);rows[20].tasks.forEach(t=>t.claimStatus='claimed'); assert.equal((await call('settlements',{afterId:first.nextCursor})).data.status,'pending_delivery'); rows[0].settlementPreparedAt=null; const settling=(await call('settlements')).data; assert.deepEqual(settling,{status:'settling',items:[],nextCursor:'history-19'}); assert.equal((await call('settlements',{afterId:first.nextCursor})).data.status,'settling'); rows[0].settlementPreparedAt=now;rows[0].tasks.forEach(t=>t.claimStatus='claimed'); assert.deepEqual((await call('settlements')).data,{status:'no_pending_rewards',items:[],nextCursor:null}); assert.deepEqual((await call('settlements',{afterId:first.nextCursor})).data,{status:'no_pending_rewards',items:[],nextCursor:null}); assert.equal((await call('settlements',{afterId:42})).errorCode,'INVALID_INPUT'); }); test('global empty settlement check scopes account, expiry and test environment and detects reserved or unprepared rewards',async()=>{ reset();await win(1);await pay(await buy());now=END;await jobs(); const original=copy(player());tables[R.TABLES.players]=[]; table(R.TABLES.players).push({...copy(original),_id:'other-user',uid:'other'}, {...copy(original),_id:'future',endsAt:now+1000}, {...copy(original),_id:'test-only',periodId:'goldMiner:test:disabled'}); const empty={status:'no_pending_rewards',items:[],nextCursor:null}; assert.deepEqual((await call('settlements')).data,empty); const overdue={...copy(original),_id:'history'};table(R.TABLES.players).push(overdue); // Probe outside the requested page, including data with incorrect cached settlementState. overdue.settlementState='client_saved'; assert.equal((await call('settlements',{afterId:'zzzz'})).data.status,'pending_delivery'); delete overdue.tasks[0].grantId; assert.equal((await call('settlements',{afterId:'zzzz'})).data.status,'settling'); overdue.entitlementStatus='none';overdue.progressClosed=false; assert.equal((await call('settlements',{afterId:'zzzz'})).data.status,'settling'); overdue.progressClosed=true; assert.deepEqual((await call('settlements',{afterId:'zzzz'})).data,empty); delete overdue.tasks;overdue.entitlement={state:'reserved'}; assert.equal((await call('settlements',{afterId:'zzzz'})).data.status,'settling'); }); test('settlement confirmation is atomic, owned, idempotent and shares state with original confirmations',async()=>{ reset();await win(1);await win(2);await pay(await buy());now=END;await jobs(); const s=(await call('settlements')).data.items[0], ids=s.rewards.map(r=>r.grantId); const body={settlementId:s.settlementId,grantIds:ids}; assert.equal((await call('confirm_settlement_delivery',{...body,grantIds:[ids[0],'fake']})).errorCode,'INVALID_GRANT'); assert.equal(player().claimedThrough,0); assert.equal((await call('confirm_settlement_delivery',{...body,grantIds:[ids[0],ids[0]]})).errorCode,'INVALID_INPUT'); table('users').push({_id:'other',token:'other-token'}); assert.equal((await call('confirm_settlement_delivery',{...body,uid:'other',token:'other-token'})).errorCode,'NOT_FOUND'); fault=(name,q,u)=>name===R.TABLES.players&&u.$set?.claimedThrough===2; assert.equal((await call('confirm_settlement_delivery',body)).code,0);assert.equal(player().claimedThrough,0); fault=null;const results=await Promise.all([call('confirm_settlement_delivery',body),call('confirm_settlement_delivery',body)]); assert.ok(results.every(r=>r.code===1));assert.equal(player().claimedThrough,2); assert.equal((await call('confirm_delivery',{periodId:s.periodId,taskId:'t1',grantId:ids[0]})).code,1); assert.ok(embeddedGrants().every(g=>g.claimStatus==='claimed')); assert.deepEqual((await call('settlements')).data.items,[]); }); test('late payment adds settlement rewards after an unpaid period was already closed',async()=>{ reset();await win(1);const o=await buy();now=END;await jobs(); assert.deepEqual((await call('settlements')).data.items,[]); await pay(o);const s=(await call('settlements')).data.items[0]; assert.equal(s.periodId,o.goldMiner.periodId);assert.equal(s.rewards.length,1); assert.equal((await claim('t1')).errorCode,'PERIOD_SETTLEMENT_REQUIRED'); }); test('old and new confirmation racing advance each task only once',async()=>{ reset();await win(1);await win(2);await pay(await buy());const original=(await claim('t1')).data; now=END;await jobs();const receipt=(await call('settlements')).data.items[0]; const results=await Promise.all([ call('confirm_delivery',{periodId:receipt.periodId,taskId:'t1',grantId:original.grantId}), call('confirm_settlement_delivery',{settlementId:receipt.settlementId,grantIds:receipt.rewards.map(r=>r.grantId)}) ]); assert.ok(results.every(r=>r.code===1));assert.equal(player().claimedThrough,2); assert.equal(embeddedGrants().length,2);assert.deepEqual((await call('settlements')).data.items,[]); }); test('batch confirmation accepts gaps and later fills the legacy contiguous prefix',async()=>{ reset();await win(1);await win(2);await win(3);await pay(await buy());now=END;await jobs(); const s=(await call('settlements')).data.items[0]; assert.equal((await call('confirm_settlement_delivery',{settlementId:s.settlementId,grantIds:[s.rewards[2].grantId,s.rewards[0].grantId]})).data.deliveryStatus,'partial'); assert.equal(player().claimedThrough,1);assert.deepEqual(player().tasks.map(t=>t.claimStatus),['claimed','issuing','claimed']); assert.equal((await call('confirm_settlement_delivery',{settlementId:s.settlementId,grantIds:[s.rewards[1].grantId]})).data.deliveryStatus,'client_saved'); assert.equal(player().claimedThrough,3); }); test('background scan upgrades an old settlement without replacing original grants',async()=>{ reset();await win(1);await win(2);await pay(await buy());const original=(await claim('t1')).data; now=END; Object.assign(player(),{progressClosed:true,progressAtClose:2,progressFrozenAt:END,settlementTaskIds:['t1','t2'],settlementIds:[R.key('settlement',player()._id)],settlementState:'pending_delivery'}); assert.equal((await call('settlements')).data.status,'settling'); await jobs();const s=(await call('settlements')).data.items[0]; assert.equal(s.rewards[0].grantId,original.grantId);assert.ok(s.rewards[1].grantId); assert.equal(embeddedGrants().length,2); }); test('shared configs isolate activity versions, reject edits and choose only versions published before opening',async()=>{ reset();const future={...copy(config),configVersion:'sharedV1',effectiveFromPeriodId:'goldMiner:2026-09-24'}; assert.equal((await admin(ctx({action:'publish',adminToken:'admin-test',config:future}))).code,1); await C.publish('cloudRise','sharedV1',NEXT,{periodId:'race',startsAt:NEXT,endsAt:NEXT+1000}); assert.equal(table(R.TABLES.configs).filter(c=>c.configVersion==='sharedV1').length,2); assert.equal((await admin(ctx({action:'publish',adminToken:'admin-test',config:{...future,priceFen:999}}))).code,0); assert.equal((await call('info')).data.configVersion,'v1'); now=NEXT+1;assert.equal((await call('info')).data.configVersion,'sharedV1'); table(R.TABLES.configs).push(version({...future,configVersion:'tooLate',priceFen:999,publishedAt:NEXT+1})); assert.equal((await S.periodById('goldMiner:2026-09-24')).configVersion,'sharedV1'); }); test('reserved future player shell has no reward snapshot until next period opens',async()=>{ reset();const order=await buy();now=END;await pay(order); const shell=player('goldMiner:2026-09-24');assert.equal(shell.tasks,undefined);assert.equal(shell.configSnapshot,undefined); assert.equal(shell.entitlement.state,'reserved'); const future={...copy(config),configVersion:'future',effectiveFromPeriodId:'goldMiner:2026-09-24',publishedAt:now,priceFen:999}; table(R.TABLES.configs).push(version(future));now=NEXT;await jobs(); assert.equal(player('goldMiner:2026-09-24').configVersion,'future');assert.equal((await call('info')).data.priceFen,999); assert.equal((await call('info')).data.status,'unlocked'); }); test('pending-state batches back off failures and never rescan completed settlements',async()=>{ reset();await call('info');const seed=copy(player());table(R.TABLES.players).length=0; for(let i=0;i<101;i++)table(R.TABLES.players).push({...copy(seed),_id:String(i).padStart(4,'0'),uid:'user'+i}); now=END;fault=(name,q,u)=>name===R.TABLES.players&&q._id==='0000'&&!!u.$set?.settlementPreparedAt; assert.equal((await jobs()).failures,1); assert.equal((await jobs()).players,1);assert.equal((await jobs()).players,0); fault=null;now+=60000;assert.equal((await jobs()).players,1);assert.equal((await jobs()).players,0); assert.ok(Object.keys(tables).filter(k=>tables[k].length).every(k=>['users','activityConfigs','goldMinerPlayerPeriods'].includes(k))); }); test('offline migration previews, preserves IDs and receipts, and safely repeats without deleting old data',async()=>{ const {migrate}=await import('../../activityConfig/migrate.cjs'); reset();await win(1);await pay(await buy());const grant=(await claim('t1')).data;now=END;await jobs(); const before=copy(player()), receipt=(await call('settlements')).data.items[0]; await call('confirm_settlement_delivery',{settlementId:receipt.settlementId,grantIds:[grant.grantId]}); await call('ack_settlement',{settlementId:receipt.settlementId});const paid=copy(player()); table('goldMinerConfigs').push(copy(config));table(R.TABLES.configs).length=0; table('goldMinerPeriods').push({...await S.periodById('goldMiner:2026-09-17'),periodId:paid.periodId,configSnapshot:paid.configSnapshot,purchaseEnabled:false}); table('goldMinerEntitlements').push(copy(paid.entitlement)); table('goldMinerRewardGrants').push({_id:grant.grantId,uid:'u',periodId:paid.periodId,taskId:'t1',itemsSnapshot:grant.items,status:'client_saved'}); table('goldMinerSettlements').push({_id:receipt.settlementId,uid:'u',periodId:paid.periodId,taskIds:['t1'],acknowledgedAt:now}); delete player().entitlement;delete player().settlementId;delete player().settlementAcknowledgedAt; const snapshot=JSON.stringify(table(R.TABLES.players)); const preview=await migrate(mongo);assert.equal(preview.dryRun,true);assert.equal(JSON.stringify(table(R.TABLES.players)),snapshot); await migrate(mongo,{apply:true});assert.equal(player().tasks[0].grantId,grant.grantId); assert.equal(player().tasks[0].claimStatus,'claimed');assert.equal(player().settlementAcknowledgedAt,now); assert.equal((await S.periodById(paid.periodId)).purchaseEnabled,false); assert.equal((await migrate(mongo,{apply:true})).writes,0);assert.equal(table('goldMinerRewardGrants').length,1); assert.deepEqual((await call('settlements')).data.items,[]);assert.equal(before.tasks[0].grantId,grant.grantId); }); test('migration fails before writing anything if an old grant disagrees with player state',async()=>{ const {migrate}=await import('../../activityConfig/migrate.cjs');reset(); table('goldMinerConfigs').push(copy(config));table(R.TABLES.configs).length=0; table('goldMinerRewardGrants').push({_id:'bad',uid:'u',periodId:config.effectiveFromPeriodId,taskId:'missing'}); await assert.rejects(()=>migrate(mongo,{apply:true}),/Reward\/player mismatch/); assert.equal(table(R.TABLES.configs).length,0); }); test('mongosh adapter migrates cloudRise without altering users and permits same version across activities',async()=>{ const {migrate}=await import('../../activityConfig/migrate.cjs');reset();const before=copy(user()); table('cloudRiseConfig').push({_id:'old',periodId:'race1',startsAt:START,endsAt:END,unlockLevel:10,enabled:false}); const shell={getCollection:name=>mongo.collection(name)}; await migrate(shell,{apply:true});const row=table(R.TABLES.configs).find(c=>c.activityId==='cloudRise'); assert.equal(row.config.durationHours,24);assert.deepEqual(row.config.pools,[10000,15000,20000]); assert.equal(row.config.enabled,undefined);assert.equal(row.publishedAt,START-1);assert.deepEqual(user(),before); assert.equal((await migrate(shell,{apply:true})).writes,0); }); test('gold miner published example passes real admin validation and immutable shared storage',async()=>{ const {readFile}=await import('node:fs/promises');reset(); const body=JSON.parse(await readFile(new URL('../config.publish.example.json',import.meta.url),'utf8')); body.adminToken='admin-test';const result=await admin(ctx(body));assert.equal(result.code,1,JSON.stringify(result)); const row=table(R.TABLES.configs).find(c=>c.configVersion===body.config.configVersion); assert.equal(row.activityId,'goldMiner');assert.equal(row.config.tasks.length,5);assert.equal(row.config.priceFen,100); now=NEXT;assert.equal((await call('info')).data.configVersion,body.config.configVersion); }); // Temporary periods exercise the same live handlers, with a Monday clock and synthetic payment signatures. const TEMP = 'goldMiner:test:monday'; const TEMP_START = END + 3600000, TEMP_END = TEMP_START + 3600000; function enableTemporary() { process.env.PAYMENT_APP_ENV='test';process.env.GOLD_MINER_TEST_PERIODS_ENABLED='true';now=TEMP_START; } const temporaryBody = (extra={}) => ({action:'publish_test_period',adminToken:'admin-test',periodId:TEMP, startsAt:TEMP_START,endsAt:TEMP_END,config:{...copy(config),configVersion:'tempV1'},...extra}); async function publishTemporary(extra={}) { const result=await admin(ctx(temporaryBody(extra)));assert.equal(result.code,1,JSON.stringify(result));return result.data; } test('temporary admin requires authentication and both server environment gates',async()=>{ for(const [env,flag] of [['production','true'],['test','false'],['','true'],['test','']]) { reset();process.env.PAYMENT_APP_ENV=env;process.env.GOLD_MINER_TEST_PERIODS_ENABLED=flag; const r=await admin(ctx(temporaryBody({PAYMENT_APP_ENV:'test',GOLD_MINER_TEST_PERIODS_ENABLED:'true'}))); assert.equal(r.errorCode,'TEST_PERIOD_DISABLED');assert.equal(table(R.TABLES.configs).length,1); } reset();enableTemporary();assert.equal((await admin(ctx(temporaryBody({adminToken:'wrong'})))).errorCode,'UNAUTHORIZED'); await publishTemporary();process.env.PAYMENT_APP_ENV='production'; assert.equal((await call('info')).data.status,'unavailable'); await assert.rejects(()=>S.periodById(TEMP),e=>e.errorCode==='TEST_PERIOD_DISABLED'); }); test('Monday temporary period uses its exact boundaries for info, main/endless progress and native payment',async()=>{ reset();enableTemporary();now=TEMP_START-1;await publishTemporary(); assert.equal((await call('info')).data.status,'unavailable'); now=TEMP_START;const info=(await call('info')).data; assert.equal(info.periodId,TEMP);assert.equal(info.startsAt,TEMP_START);assert.equal(info.endsAt,TEMP_END); assert.equal(info.status,'purchasable'); const main=await saveLevel(ctx({action:'save',uid:'u',token:'token',levelAmount:42,goldMiner:event(1,'main',{periodId:TEMP})})); assert.equal(main.data.goldMiner.activityCounted,true); const endless=await saveLevel(ctx({action:'save',uid:'u',token:'token',levelAmount:42,isWuXian:'true',goldMiner:event(2,'endless',{periodId:TEMP})})); assert.equal(endless.data.goldMiner.activityCounted,true);assert.equal(player(TEMP).progressWins,2); const order=await buy();assert.equal(order.goldMiner.periodId,TEMP);assert.equal(order.goldMiner.endsAt,TEMP_END); await pay(order);assert.equal((await call('info')).data.status,'unlocked'); now=TEMP_END-1;const grant=await deliver('t1',TEMP);assert.ok(grant); now=TEMP_END; assert.equal((await call('info')).data.status,'unavailable'); assert.equal((await call('claim',{periodId:TEMP,taskId:'t2',requestId:'expired'})).errorCode,'PERIOD_SETTLEMENT_REQUIRED'); await assert.rejects(()=>win(3,'endless',{periodId:TEMP}),e=>e.errorCode==='PERIOD_ENDED'); assert.equal((await android(ctx({uid:'u',token:'token',itemid:'gold_miner',createRequestId:'late'}))).errorCode,'PERIOD_ENDED'); assert.equal((await call('settlements')).data.status,'settling'); await jobs();const receipt=(await call('settlements')).data.items[0]; assert.equal(receipt.periodId,TEMP);assert.deepEqual(receipt.rewards.map(t=>t.taskId),['t2']); const confirmed=await call('confirm_settlement_delivery',{settlementId:receipt.settlementId,grantIds:receipt.rewards.map(t=>t.grantId)}); assert.equal(confirmed.data.deliveryStatus,'client_saved');assert.equal(player(TEMP).claimedThrough,2); assert.deepEqual((await call('settlements')).data.items,[]);assert.equal(table('users')[0].coinAmount,500); }); test('temporary periods can start immediately, are immutable and cannot overlap even under concurrent publishing',async()=>{ reset();enableTemporary();now+=1000;const first=await publishTemporary(); assert.equal(first.configSnapshot.publishedAt,now);assert.equal((await call('info')).data.periodId,TEMP); now+=1000;assert.deepEqual(await publishTemporary(),first); assert.equal((await admin(ctx(temporaryBody({endsAt:TEMP_END+1})))).errorCode,'CONFIG_UNAVAILABLE'); assert.equal((await admin(ctx(temporaryBody({config:{...config,priceFen:1}})))).errorCode,'CONFIG_UNAVAILABLE'); const common={startsAt:TEMP_END,endsAt:TEMP_END+60000}; const results=await Promise.all(['a','b'].map(id=>admin(ctx(temporaryBody({...common,periodId:'goldMiner:test:'+id}))))); assert.equal(results.filter(r=>r.code===1).length,1);assert.equal(results.filter(r=>r.errorCode==='CONFIG_UNAVAILABLE').length,1); for(const extra of [{periodId:'goldMiner:2026-09-17'},{startsAt:TEMP_END},{startsAt:String(TEMP_START)}, {periodId:'goldMiner:test:expired',startsAt:TEMP_START-60000,endsAt:TEMP_START}, {nextPeriodId:'goldMiner:2026-09-17'},{nextPeriodId:'goldMiner:test:missing'}]) { assert.equal((await admin(ctx(temporaryBody(extra)))).code,0,JSON.stringify(extra)); } }); test('active temporary period takes precedence over weekly activity and retains its own purchase control',async()=>{ reset();enableTemporary();now=START+1000; await publishTemporary({startsAt:START,endsAt:START+60000});assert.equal((await call('info')).data.periodId,TEMP); assert.equal((await admin(ctx({action:'set_purchasable',adminToken:'admin-test',periodId:TEMP,purchaseEnabled:false}))).code,1); assert.equal((await call('info')).data.status,'purchase_disabled'); now=START+60000;assert.equal((await call('info')).data.periodId,'goldMiner:2026-09-17'); assert.equal((await call('info')).data.status,'purchasable');assert.equal(player(TEMP).progressWins,0); }); test('late temporary payment with earned rewards settles the original period and keeps grant identity',async()=>{ reset();enableTemporary();await publishTemporary();await win(1,'endless',{periodId:TEMP});const order=await buy(); now=TEMP_END;await pay(order);assert.equal(table('order')[0].goldMiner.fulfillmentRoute,'settle_original'); const receipt=(await call('settlements')).data.items[0];assert.equal(receipt.periodId,TEMP); assert.equal(receipt.rewards[0].grantId,R.key('grant',player(TEMP)._id,'t1'));await pay(order); assert.deepEqual((await call('settlements')).data.items[0],receipt); }); test('late temporary payment carries to a configured next test period and activates at its start',async()=>{ reset();enableTemporary();const nextId='goldMiner:test:next',startsAt=TEMP_END+60000; await publishTemporary({periodId:nextId,startsAt,endsAt:startsAt+60000}); await publishTemporary({nextPeriodId:nextId});const order=await buy();now=TEMP_END;await pay(order); assert.equal(table('order')[0].goldMiner.targetPeriodId,nextId);assert.equal(player(nextId).tasks,undefined); assert.equal(player(nextId).entitlement.state,'reserved');now=startsAt;await jobs(); const info=(await call('info')).data;assert.equal(info.periodId,nextId);assert.equal(info.status,'unlocked'); assert.equal(info.progressWins,0);assert.equal(entitlements().length,1); }); test('temporary carry defaults to the first weekly start at or after its end',async()=>{ reset();enableTemporary();const temp=await publishTemporary();assert.equal(temp.nextPeriodId,'goldMiner:2026-09-24'); const order=await buy();now=TEMP_END;await pay(order);now=NEXT; assert.equal((await call('info')).data.status,'unlocked');assert.equal(player('goldMiner:2026-09-24').progressWins,0); }); test('legacy temporary orders preserve custom cutoff through customer service and iOS transfer',async()=>{ legacyReset();enableTemporary();await publishTemporary();const data=await legacyBuy(); assert.equal(data.periodId,TEMP);const order=legacyOrder();assert.equal(order.goldMiner.endsAt,TEMP_END); assert.equal((await sendLink(data)).code,1);const param=linkParam(); assert.equal(table('iosOrder')[0].goldMiner.endsAt,TEMP_END); assert.equal(await checkIos(ctx({openid:param.openid,time:param.time,outTradeNo:param.outTradeNo})),true); assert.equal(legacyOrder().goldMiner.endsAt,TEMP_END); await win(1,'endless',{periodId:TEMP});now=TEMP_END; assert.equal((await merchantNotify(merchantNotice(order))).code,'SUCCESS'); assert.equal(legacyOrder().goldMiner.fulfillmentRoute,'settle_original');assert.equal((await call('settlements')).data.items[0].periodId,TEMP); assert.equal(table('iosOrder').length,0); }); test('disabled test periods cannot deliver rewards or recover test orders in production',async()=>{ reset();enableTemporary();await publishTemporary();await win(1,'endless',{periodId:TEMP});const order=await buy();await pay(order); const grant=(await call('claim',{periodId:TEMP,taskId:'t1',requestId:'g'})).data; now=TEMP_END;await jobs();const receipt=(await call('settlements')).data.items[0];process.env.PAYMENT_APP_ENV='production'; assert.equal((await call('claim',{periodId:TEMP,taskId:'t1',requestId:'g'})).errorCode,'TEST_PERIOD_DISABLED'); assert.equal((await call('confirm_delivery',{periodId:TEMP,taskId:'t1',grantId:grant.grantId})).errorCode,'TEST_PERIOD_DISABLED'); assert.equal((await call('confirm_settlement_delivery',{settlementId:receipt.settlementId,grantIds:[grant.grantId]})).errorCode,'TEST_PERIOD_DISABLED'); await assert.rejects(()=>P.fulfillOrder(order.outTradeNo),e=>e.errorCode==='TEST_PERIOD_DISABLED'); assert.deepEqual((await call('settlements')).data.items,[]);assert.equal(player(TEMP).claimedThrough,0); }); test('legacy customer payment checks its original period purchase switch during a temporary override',async()=>{ legacyReset();process.env.PAYMENT_APP_ENV='test';const data=await legacyBuy();control().purchaseEnabled=false; process.env.GOLD_MINER_TEST_PERIODS_ENABLED='true';await publishTemporary({startsAt:START,endsAt:START+60000}); assert.equal((await call('info')).data.periodId,TEMP); assert.equal((await sendLink(data)).errorCode,'PURCHASE_DISABLED');assert.equal(provider.prepayCalls,0); }); test('turning off the temporary gate rejects both payment callbacks before confirming an unpaid order',async()=>{ reset();enableTemporary();await publishTemporary();let order=await buy();process.env.GOLD_MINER_TEST_PERIODS_ENABLED='false'; assert.notEqual((await callback(notification(order))).ErrCode,0);assert.equal(table('order')[0].state,0); legacyReset();enableTemporary();await publishTemporary();await legacyBuy();order=legacyOrder(); process.env.GOLD_MINER_TEST_PERIODS_ENABLED='false'; assert.equal((await merchantNotify(merchantNotice(order))).code,'FAIL');assert.equal(order.state,0); }); test('temporary JSON example is accepted by admin and needs no weekly config',async()=>{ reset();enableTemporary();table(R.TABLES.configs).length=0; const {readFile}=await import('node:fs/promises'); const body=JSON.parse(await readFile(new URL('../test-period.publish.example.json',import.meta.url),'utf8')); body.adminToken='admin-test';now=body.startsAt; const result=await admin(ctx(body));assert.equal(result.code,1,JSON.stringify(result)); const info=(await call('info')).data;assert.equal(info.periodId,body.periodId);assert.equal(info.startsAt,body.startsAt); assert.equal(info.endsAt,body.endsAt);assert.equal(info.tasks.length,body.config.tasks.length); }); test('shared enabled defaults and latest selection apply to arbitrary future activities without fallback',async()=>{ reset(); const first=await C.publish('futureActivity','v1',now+1000,{value:1}); assert.equal(first.enabled,true); const stopped=await C.publish('futureActivity','v2',now+2000,{value:2},false); assert.equal(stopped.enabled,false); assert.equal(await C.latestEnabled('futureActivity',now+3000),null); assert.equal((await C.latest('futureActivity',now+3000)).configVersion,'v2'); await assert.rejects(C.publish('futureActivity','v2',now+2000,{value:2})); await assert.rejects(C.publish('futureActivity','v3',now+3000,{value:3},'false')); assert.equal(C.isEnabled({}),true);assert.equal(C.isEnabled(null),false); const legacy=table(R.TABLES.configs).find(r=>r.activityId==='futureActivity'&&r.configVersion==='v1');delete legacy.enabled; assert.equal((await C.publish('futureActivity','v1',now+1000,{value:1})).configVersion,'v1'); assert.equal((await C.latestEnabled('futureActivity',now+1500)).configVersion,'v1'); }); test('gold miner admin publishes outer enabled and a disabled latest version never falls back',async()=>{ reset();const body={action:'publish',adminToken:'admin-test',enabled:false, config:{...copy(config),configVersion:'disabled-v2',effectiveFromPeriodId:'goldMiner:2026-09-24'}}; const result=await admin(ctx(body));assert.equal(result.code,1);assert.equal(result.data.enabled,false); const row=table(R.TABLES.configs).find(r=>r.configVersion==='disabled-v2'); assert.equal(row.enabled,false);assert.equal(row.config.enabled,undefined); assert.equal((await admin(ctx(body))).code,1); assert.equal((await admin(ctx({...body,enabled:true}))).code,0); assert.equal((await admin(ctx({...body,enabled:'false'}))).code,0); now=NEXT;assert.equal(await S.currentPeriod(),null); assert.equal((await call('info')).data.status,'unavailable');assert.equal(table(R.TABLES.players).length,0); const raw=await S.periodById(R.calendar(now).periodId);assert.equal(raw.configVersion,'disabled-v2'); await assert.rejects(S.ensurePlayer('u',user(),raw),e=>e.errorCode==='PERIOD_DISABLED'); await assert.rejects(win(1),e=>e.errorCode==='PERIOD_ENDED'); assert.equal((await android(ctx({uid:'u',token:'token',itemid:'gold_miner',createRequestId:'stopped'}))).code,0); assert.equal(table('order').length,0);assert.equal(table(R.TABLES.players).length,0); row.enabled=true;assert.equal((await call('info')).data.status,'purchasable'); }); test('gold miner enabled closure preserves joined players progress payment and settlement',async()=>{ reset();await call('info');const snapshot=copy(player().configSnapshot); table(R.TABLES.configs)[0].enabled=false; assert.equal(await S.currentPeriod(),null); assert.equal((await call('info')).data.status,'purchasable'); assert.deepEqual(player().configSnapshot,snapshot); await win(1);const order=await buy();await pay(order); assert.equal((await call('info')).data.status,'unlocked'); await deliver('t1');await win(2); now=END;await jobs();const pending=await call('settlements'); assert.equal(pending.code,1);assert.equal(pending.data.items.length,1); assert.equal(pending.data.items[0].rewards[0].taskId,'t2'); }); test('gold miner disabled next period still honors a previously assigned paid entitlement',async()=>{ reset();const order=await buy();now=END;await pay(order); assert.equal(player('goldMiner:2026-09-24').entitlement.state,'reserved'); table(R.TABLES.configs)[0].enabled=false;now=NEXT; await jobs();assert.equal((await call('info')).data.status,'unlocked'); assert.equal(player('goldMiner:2026-09-24').entitlementStatus,'unlocked'); }); test('gold miner temporary enabled closure blocks new players but preserves participants',async()=>{ reset();enableTemporary();await publishTemporary({enabled:false}); assert.equal((await call('info')).data.status,'unavailable');assert.equal(table(R.TABLES.players).length,0); const period=table(R.TABLES.configs).find(r=>r.recordType==='testSchedule').periods[0];period.enabled=true; assert.equal((await call('info')).data.status,'purchasable');period.enabled=false; assert.equal((await call('info')).data.status,'purchasable'); assert.equal((await win(1,'endless',{periodId:TEMP})).activityCounted,true); assert.equal((await admin(ctx(temporaryBody({enabled:'false'})))).code,0); }); test('configuration validation reports exact field paths without changing error codes',async()=>{ const cases=[ ['config',()=>null], ['config.configVersion',c=>({...c,configVersion:''})], ['config.productId',c=>({...c,productId:'wrong'})], ['config.timezone',c=>({...c,timezone:'UTC'})], ['config.currency',c=>({...c,currency:'USD'})], ['config.unlockPassedLevel',c=>({...c,unlockPassedLevel:'41'})], ['config.priceFen',c=>({...c,priceFen:'100'})], ['config.publishedAt',c=>({...c,publishedAt:0})], ['config.tasks',c=>({...c,tasks:[]})], ['config.tasks[0]',c=>({...c,tasks:[null]})], ['config.tasks[0].taskId',c=>({...c,tasks:[{...c.tasks[0],taskId:''}]})], ['config.tasks[1].taskId',c=>({...c,tasks:[c.tasks[0],{...c.tasks[1],taskId:c.tasks[0].taskId}]})], ['config.tasks[0].sequence',c=>({...c,tasks:[{...c.tasks[0],sequence:2}]})], ['config.tasks[0].targetWins',c=>({...c,tasks:[{...c.tasks[0],targetWins:1001}]})], ['config.tasks[1].targetWins',c=>({...c,tasks:[c.tasks[0],{...c.tasks[1],targetWins:1}]})], ['config.tasks[0].items',c=>({...c,tasks:[{...c.tasks[0],items:[]}]})], ['config.tasks[0].items[0]',c=>({...c,tasks:[{...c.tasks[0],items:[null]}]})], ['config.tasks[0].items[0].type',c=>({...c,tasks:[{...c.tasks[0],items:[{type:'other',count:1}]}]})], ['config.tasks[0].items[0].count',c=>({...c,tasks:[{...c.tasks[0],items:[{type:'coin',count:-1}]}]})], ['config.tasks',c=>({...c,tasks:c.tasks.map(t=>({...t,items:[{type:'coin',count:Number.MAX_SAFE_INTEGER}]}))})], ]; for(const [path,change] of cases){ reset();assert.throws(()=>R.validateConfig(change(copy(config))),error=>{ const r=R.failure(error);assert.equal(r.code,0);assert.equal(r.data,null);assert.equal(r.errorCode,'CONFIG_UNAVAILABLE'); assert(r.msg.startsWith(path+' '),r.msg);assert.notEqual(r.msg,r.errorCode);return true; }); } assert.throws(()=>R.validateConfig({...config,effectiveFromPeriodId:'goldMiner:2026-09-18'}),error=>error.errorCode==='INVALID_PERIOD'&&error.message.includes('config.effectiveFromPeriodId')&&error.message.includes('周四')); }); test('both publish endpoints return actionable configuration details and reject malformed task objects',async()=>{ reset();const future={...copy(config),effectiveFromPeriodId:'goldMiner:2026-09-24',priceFen:0}; let result=await admin(ctx({action:'publish',adminToken:'admin-test',config:future})); assert.equal(result.errorCode,'CONFIG_UNAVAILABLE');assert.match(result.msg,/config\.priceFen.*大于 0/); for(const cfg of [undefined,null,[],JSON.stringify(future)]){ result=await admin(ctx({action:'publish',adminToken:'admin-test',config:cfg}));assert.match(result.msg,/config 必须为 JSON 对象/); } enableTemporary(); result=await admin(ctx(temporaryBody({config:{...config,tasks:[null]}}))); assert.equal(result.errorCode,'CONFIG_UNAVAILABLE');assert.equal(result.msg,'config.tasks[0] 必须为任务对象'); assert.equal(table(R.TABLES.configs).filter(r=>r.recordType==='testSchedule').length,0); result=await admin(ctx(temporaryBody({startsAt:'2026-09-21'})));assert.match(result.msg,/startsAt.*毫秒/); result=await admin(ctx(temporaryBody({endsAt:TEMP_START})));assert.equal(result.msg,'endsAt 必须严格晚于 startsAt'); await publishTemporary();result=await admin(ctx(temporaryBody({periodId:'goldMiner:test:overlap'}))); assert.equal(result.errorCode,'CONFIG_UNAVAILABLE');assert(result.msg.includes(TEMP));assert(result.msg.includes(String(TEMP_END))); }); test('business errors have readable defaults and explicit messages remain intact',async()=>{ reset();await win(1);const denied=await claim('t1'); assert.equal(denied.errorCode,'NOT_PAID');assert.match(denied.msg,/尚未解锁/); assert.match((await call('unsupported')).msg,/action/); assert.equal(R.failure(new R.ActivityError('INVALID_INPUT','field-specific explanation')).msg,'field-specific explanation'); assert.equal(R.failure(new Error('secret connection string')).msg,'黄金矿工处理失败,请使用原请求重试'); assert.equal(R.failure(new Error('secret connection string')).errorCode,'RETRYABLE'); }); test('payment configuration reports the failing variable without exposing supplied secrets',async()=>{ for(const [name,value] of [['WX_MCH_ID',''],['GOLD_MINER_WECHATPAY_API_V3_KEY','private-secret-short'], ['GOLD_MINER_WECHATPAY_PUBLIC_KEYS','{private-json'],['GOLD_MINER_CHECK_IOS_URL','https://test.invalid/wrong'], ['GOLD_MINER_PAY_PAGE_URL','https://secret:password@test.invalid/order3.html']]){ legacyReset();process.env[name]=value; const result=await call('create_order',{channel:'legacy_ios',itemid:'gold_miner',createRequestId:'diagnostic'}); assert.equal(result.errorCode,'PAYMENT_CONFIG_UNAVAILABLE');assert(result.msg.includes(name),result.msg); if(value)assert(!result.msg.includes(value));assert.equal(table('order').length,0); } reset();delete process.env.WX_MIDAS_PAY_SIGN_KEY; let result=await android(ctx({uid:'u',token:'token',itemid:'gold_miner',createRequestId:'diagnostic'}));assert.match(result.msg,/WX_MIDAS_PAY_SIGN_KEY/); reset();delete table('users')[0].session_key; result=await android(ctx({uid:'u',token:'token',itemid:'gold_miner',createRequestId:'diagnostic'}));assert.match(result.msg,/session_key/); }); const vipExample=JSON.parse(readFileSync(new URL('../vip-tiers.example.json',import.meta.url),'utf8')); const vipConfig=()=>({...copy(vipExample.config),status:'published',effectiveFromPeriodId:config.effectiveFromPeriodId,publishedAt:START-1000}); function useVipConfig(level) { table('users')[0].vip_level=level; tables[R.TABLES.configs]=[version(vipConfig())]; } test('VIP example validates all six exact totals and publishes via normal and test period admin routes',async()=>{ reset();const c=R.validateConfig(vipConfig()); assert.deepEqual(c.vipTiers.map(t=>t.priceFen),[300,600,1200,3000,6800,12800]); assert.deepEqual(c.vipTiers.map(t=>t.tasks.reduce((sum,t)=>sum+t.items[0].count,0)),[6000,6000,8000,15000,28000,45000]); assert.deepEqual(c.vipTiers.map(t=>t.tasks.at(-1).targetWins),[50,60,80,100,150,200]); const published=await admin(ctx({...copy(vipExample),adminToken:'admin-test'}));assert.equal(published.code,1,JSON.stringify(published)); enableTemporary();const temp=await admin(ctx(temporaryBody({config:copy(vipExample.config)}))); assert.equal(temp.code,1,JSON.stringify(temp));assert.equal(temp.data.configSnapshot.vipTiers.length,6); for(const edit of [c=>c.vipTiers.pop(),c=>c.vipTiers[1].vipLevel=0,c=>c.vipTiers[1].productId=c.vipTiers[0].productId, c=>c.vipTiers[1].tasks[0].targetWins=1,c=>c.vipTiers[2].tasks[9].targetWins=60,c=>c.vipTiers[5].tasks[0].items[0].count=0]) { const bad=vipConfig();edit(bad);assert.throws(()=>R.validateConfig(bad),/vipTiers/); } }); test('VIP info and both native payment routes use server-selected tier price and reward snapshots',async()=>{ for(let level=0;level<6;level++) { reset();useVipConfig(level);const tier=vipConfig().vipTiers[level]; const info=(await call('info',{vip_level:5})).data; assert.equal(info.vipLevel,level);assert.equal(info.priceFen,tier.priceFen);assert.equal(info.productId,tier.productId); assert.deepEqual(info.tasks.map(t=>t.targetWins),tier.tasks.map(t=>t.targetWins)); assert.deepEqual(info.tasks.map(t=>t.itemsSnapshot),tier.tasks.map(t=>t.items)); assert.equal(info.endsAt,END);assert.equal(info.startsAt,START); const order=await buy(level%2?ios:android,{itemid:tier.productId});assert.equal(order.goodsPrice,tier.priceFen); assert.equal(order.goldMiner.vipLevel,level);assert.equal(order.goldMiner.configHash,player().configHash); table('users')[0].vip_level=(level+1)%6; await pay(order);const paid=(await call('info')).data; assert.equal(paid.vipLevel,level);assert.equal(paid.priceFen,tier.priceFen);assert.equal(paid.endsAt,END); assert.deepEqual(paid.tasks.map(t=>t.itemsSnapshot),info.tasks.map(t=>t.itemsSnapshot)); } }); test('missing or invalid VIP and previously paid VIP0 still receive the VIP0 package',async()=>{ for(const level of [undefined,null,-1,6,'invalid',0]) { reset({pay_user:true,vip_profile:{has_ever_paid:true,data_valid:false}});useVipConfig(level); const info=(await call('info')).data;assert.equal(info.vipLevel,0);assert.equal(info.priceFen,300); const order=await buy(android,{itemid:info.productId});await pay(order);assert.equal((await call('info')).data.status,'unlocked'); } }); test('unpaid VIP changes refresh the offer, preserve progress, reject stale products and pin at order creation',async()=>{ reset();useVipConfig(0);await win(1); let info=(await call('info')).data;assert.equal(info.tasks[0].claimStatus,'pending_unlock');assert.equal(info.progressWins,1); assert.equal((await claim('task_1')).errorCode,'NOT_PAID'); table('users')[0].vip_level=5; const stale=await android(ctx({uid:'u',token:'token',itemid:info.productId,createRequestId:'stale'})); assert.equal(stale.errorCode,'INVALID_PRODUCT');assert.equal(table('order').length,0);assert.equal(player().offerLockedAt,undefined); info=(await call('info')).data;assert.equal(info.vipLevel,5);assert.equal(info.progressWins,1);assert.equal(info.tasks[0].claimStatus,'locked'); await win(2);await win(3);assert.equal((await call('info')).data.tasks[0].claimStatus,'pending_unlock'); const order=await buy(android,{itemid:info.productId});table('users')[0].vip_level=0; assert.equal((await call('info')).data.vipLevel,5);await pay(order); assert.equal((await call('info')).data.tasks[0].claimStatus,'claimable'); assert.equal(order.goodsPrice,12800);assert.equal(player().progressWins,3); }); test('unpaid wins beyond a lower VIP final goal remain available if VIP rises before ordering',async()=>{ reset();useVipConfig(0); for(let i=1;i<=61;i++) await win(i); assert.equal((await call('info')).data.progressWins,61); table('users')[0].vip_level=1;const info=(await call('info')).data; assert.equal(info.maxTarget,60);assert.ok(info.tasks.every(t=>t.claimStatus==='pending_unlock')); await pay(await buy(android,{itemid:info.productId})); const last=await call('claim',{periodId:info.periodId,taskId:'task_10',requestId:'last-first'});assert.equal(last.code,1); assert.equal((await call('confirm_delivery',{periodId:info.periodId,taskId:'task_10',grantId:last.data.grantId})).code,1); assert.equal(player().claimedThrough,0);assert.equal((await call('info')).data.tasks[0].claimStatus,'claimable'); now=END;await jobs();const s=(await call('settlements')).data.items[0];assert.equal(s.rewards.length,9); assert.ok(s.rewards.every(r=>r.taskId!=='task_10')); }); test('legacy VIP order and delayed carry preserve the originally purchased tier through VIP changes',async()=>{ legacyReset();useVipConfig(4);const data=await legacyBuy({itemid:'gold_miner_vip4'}); assert.equal(data.priceFen,6800);assert.equal(legacyOrder().goodsPrice,6800); reset();useVipConfig(5);const order=await buy(android,{itemid:'gold_miner_vip5'}); table('users')[0].vip_level=0;now=END;await pay(order);assert.equal(order.goldMiner.fulfillmentRoute,'carry_next'); now=NEXT;await jobs();const info=(await call('info')).data; assert.equal(info.vipLevel,5);assert.equal(info.priceFen,12800);assert.equal(info.maxTarget,200); assert.equal(info.tasks.reduce((sum,t)=>sum+t.itemsSnapshot[0].count,0),45000); });