const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); const { stripTypeScriptTypes } = require('node:module'); const read = p => fs.readFileSync(require('node:path').join(__dirname, '..', p), 'utf8'); const clean = s => s.replace(/^import .*;\r?$/gm, '').replace(/^export type /gm, 'type ').replace('export default class', 'class'); const tool = read('assets/Script/module/Tool/GameTool.ts'); const utils = read('assets/Script/module/Pay/Utils.ts'); const code = stripTypeScriptTypes([ clean(read('assets/Script/module/Task/DailyTaskProgressTracker.ts')), clean(read('assets/Script/module/Task/DailyTaskSync.ts')), 'class Utils {' + utils.slice(utils.indexOf(' //每日任务获取'), utils.indexOf(' //设置连胜次数')) + '}', 'const GameTool = {' + tool.slice(tool.indexOf(' pendingActivityCoins:'), tool.indexOf(' recordDailyTaskShare(')) + '};', 'globalThis.classes = {Tracker: DailyTaskProgressTracker, Sync: DailyTaskSync, Utils, GameTool};', ].join('\n')); function setup() { let now = Date.parse('2026-09-08T04:00:00Z'), timerId = 0; const timers = new Map(), requests = [], notifications = []; const info = {uid: 'test-user', otherLevel: 0, tasks: null}; const addTimer = (fn, delay, interval = 0) => { const id = ++timerId; timers.set(id, {fn, at: now + delay, interval}); return id; }; const context = {wx: {}, console: {log() {}}, Date: {now: () => now}, setTimeout: (fn, delay) => addTimer(fn, delay), clearTimeout: id => timers.delete(id), setInterval: (fn, delay) => addTimer(fn, delay, delay), clearInterval: id => timers.delete(id), cc: {director: {emit: e => notifications.push(e)}, fx: {GameConfig: {GM_INFO: info}, StorageMessage: {getStorage: () => info.uid}}}, }; vm.runInNewContext(code, context); const {Tracker, Sync, Utils, GameTool} = context.classes; context.cc.fx.GameTool = GameTool; GameTool.shushu_Track = () => {}; GameTool.getWechatGameVersion = () => '开发版'; Utils.apiBaseUrl = () => 'test/'; Utils.POST = (endpoint, fields, callback) => requests.push({endpoint, fields, callback, kind: fields.action || 'claim'}); const data = (extra = {}) => ({version: 2, dayKey: 'day1', weekKey: 'week1', serverNow: now, dayEndsAt: now + 86400000, weekEndsAt: now + 7 * 86400000, weeklyStars: 0, dailyTasks: [{taskId: 'pass', metric: 'pass_levels', target: 10, value: 0, state: 0}], weeklyRewards: [{rewardId: 'weekly_35', target: 35, state: 0, rewards: [{type: 'coin', count: 10}]}], ...extra}); const respond = (task, index = 0, extras = {}) => { const request = requests.splice(index, 1)[0]; assert.ok(request, 'a request is pending'); request.callback({code: 1, data: {task: structuredClone(task), ...extras}}); return request; }; const advance = ms => { const end = now + ms; while (true) { const next = [...timers].filter(([, t]) => t.at <= end).sort((a,b) => a[1].at - b[1].at)[0]; if (!next) break; const [id,t] = next; now = t.at; if (t.interval) t.at += t.interval; else timers.delete(id); t.fn(); } now = end; }; const load = task => { GameTool.getDailyQuestsInfo(() => {}); respond(task || data()); }; const ackSave = () => { const snapshot = JSON.parse(requests[0].fields.task); snapshot.serverNow = now; respond(snapshot); }; return {Tracker, Sync, Utils, GameTool, info, context, requests, notifications, data, respond, advance, load, ackSave, get now() {return now;}, value: () => info.tasks.dailyTasks[0].value}; } test('first reads coalesce and subsequent opens reuse the same valid in-memory task data', () => { const h = setup(), replies = []; h.GameTool.getDailyQuestsInfo(r => replies.push(r)); h.GameTool.getDailyQuestsInfo(r => replies.push(r)); assert.equal(h.requests.length, 1); h.respond(h.data()); assert.equal(replies.length, 2); for (let i=0; i<5; i++) h.GameTool.getDailyQuestsInfo(r => assert.equal(r.data.task, h.info.tasks)); assert.equal(h.requests.length, 0); h.advance(20000); assert.equal(h.Sync.now(), h.now, 'reading cached data never recalibrates the clock'); }); test('activity coins use the normal progress tracker and notify the task panel without changing the wallet',()=>{ const h=setup();h.info.coin=100; h.load(h.data({dailyTasks:[{taskId:'coins',metric:'collect_coin',target:100,value:20,state:0}]})); const before=h.notifications.length;h.GameTool.recordActivityCoin(30); assert.equal(h.value(),50);assert.equal(h.info.coin,100);assert(h.notifications.length>before); h.GameTool.recordActivityCoin(80);assert.equal(h.value(),100);assert.equal(h.info.tasks.dailyTasks[0].state,1); h.advance(1000);assert.equal(h.requests[0].kind,'save');h.ackSave(); h.info.tasks.dailyTasks[0].state=2;h.GameTool.recordActivityCoin(10);assert.equal(h.value(),100); }); test('activity coins wait for initial task loading and survive a failed read within the session',()=>{ const h=setup();h.GameTool.recordActivityCoin(30);h.GameTool.recordActivityCoin(20); assert.equal(h.requests.length,1);h.requests.shift().callback({code:0,msg:'offline'}); h.load(h.data({dailyTasks:[{taskId:'coins',metric:'collect_coin',target:100,value:0,state:0}]})); assert.equal(h.value(),50);h.GameTool.getDailyQuestsInfo(()=>{});assert.equal(h.value(),50); h.advance(1000);h.ackSave(); }); test('activity coin buffers are isolated by account and server',()=>{ const h=setup(), coins=()=>h.data({dailyTasks:[{taskId:'coins',metric:'collect_coin',target:100,value:0,state:0}]}); h.GameTool.recordActivityCoin(30);h.requests.shift().callback({code:0}); h.info.uid='other';h.load(coins());assert.equal(h.value(),0); h.Utils.apiBaseUrl=()=> 'production/';h.load(coins());h.GameTool.recordActivityCoin(10);assert.equal(h.value(),10); h.info.uid='test-user';h.Utils.apiBaseUrl=()=> 'test/';h.load(coins());assert.equal(h.value(),30); }); test('activity coins remain dirty after a failed task save and use the existing retry queue',()=>{ const h=setup();h.load(h.data({dailyTasks:[{taskId:'coins',metric:'collect_coin',target:100,value:0,state:0}]})); h.GameTool.recordActivityCoin(30);h.advance(1000);h.requests.shift().callback({code:0,msg:'offline'}); assert.equal(h.value(),30);h.GameTool.setDailyQuestInfo(()=>{});assert.equal(h.requests[0].kind,'save'); h.ackSave();assert.equal(h.value(),30); }); test('expiry uses server midnight for either daily or weekly boundary and coalesces refreshes', () => { for (const field of ['dayEndsAt', 'weekEndsAt']) { const h=setup(), serverNow=h.now+3600000; h.load(h.data({serverNow, dayEndsAt: serverNow+86400000, weekEndsAt: serverNow+7*86400000, [field]: serverNow+10000})); h.advance(9999); assert.ok(h.Sync.cached()); h.advance(1); assert.equal(h.Sync.cached(), null); h.GameTool.getDailyQuestsInfo(()=>{}); h.GameTool.getDailyQuestsInfo(()=>{}); assert.equal(h.requests.length,1); assert.equal(h.requests[0].kind,'read'); h.respond(h.data({dayKey: field==='dayEndsAt'?'day2':'day1', weekKey:'week2'})); assert.ok(h.Sync.cached()); } }); test('save responses preserve later progress and continue uploading before completing synchronization', () => { const h=setup(); h.load(); h.Tracker.record({metric:'pass_levels',amount:3}); h.advance(1000); assert.equal(h.requests[0].kind,'save'); assert.equal(JSON.parse(h.requests[0].fields.task).dailyTasks[0].value,3); h.Tracker.record({metric:'pass_levels'}); h.ackSave(); assert.equal(h.value(),4); assert.equal(h.requests.length,1); assert.equal(JSON.parse(h.requests[0].fields.task).dailyTasks[0].value,4); h.ackSave(); h.advance(1000); assert.equal(h.requests.length,0); }); test('a win streak reset during a save is preserved even if later wins exceed the submitted value', () => { const h=setup(); h.load(h.data({dailyTasks:[{taskId:'streak',metric:'win_streak',value:0,target:10,state:0}]})); h.Tracker.record({metric:'win_streak',amount:3}); h.advance(1000); h.Tracker.resetWinStreak(); h.Tracker.record({metric:'win_streak',amount:4}); const server=JSON.parse(h.requests[0].fields.task); server.dailyTasks[0].value=6; h.respond(server); assert.equal(h.value(),4); h.ackSave(); assert.equal(h.value(),4); }); test('daily and weekly claims wait for pending saves and synchronize returned state exactly once', () => { for (const scope of ['daily','weekly']) { const h=setup(); const data=h.data(); data.dailyTasks[0].target=1; data.weeklyRewards[0].state=1; h.load(data); h.Tracker.record({metric:'pass_levels'}); let result; h.GameTool.claimDailyQuestReward(scope==='daily'?'pass':'weekly_35',r=>{result=r;},scope); assert.deepEqual(h.requests.map(r=>r.kind),['save']); h.ackSave(); assert.deepEqual(h.requests.map(r=>r.kind),['claim']); const updated=structuredClone(h.info.tasks); (scope==='daily'?updated.dailyTasks:updated.weeklyRewards)[0].state=2; updated.weeklyStars=35; h.respond(updated,0,{rewards:[{type:'coin',count:10}]}); assert.equal(result.code,1); assert.equal(h.info.tasks.weeklyStars,35); h.GameTool.claimDailyQuestReward(scope==='daily'?'pass':'weekly_35',()=>{},scope); assert.equal(h.requests.length,0, 'already claimed state cannot issue another grant request'); } }); test('consecutive daily and weekly requests stay serialized without waiting for presentation callbacks', () => { const h = setup(), data = h.data(), animations = []; data.dailyTasks[0].state = 1; data.dailyTasks[0].value = 10; data.dailyTasks.push({ ...data.dailyTasks[0], taskId: 'pass2' }); data.weeklyRewards[0].state = 1; h.load(data); const receive = id => response => { assert.equal(response.code, 1); animations.push({ id, finished: false }); }; h.GameTool.claimDailyQuestReward('pass', receive('pass')); h.GameTool.claimDailyQuestReward('pass2', receive('pass2')); h.GameTool.claimDailyQuestReward('weekly_35', receive('weekly_35'), 'weekly'); assert.equal(h.requests.length, 1); assert.equal(h.requests[0].fields.taskId, 'pass'); const first = structuredClone(h.info.tasks); first.dailyTasks[0].state = 2; h.respond(first, 0, { rewards: [{ type: 'coin', count: 10 }] }); assert.equal(animations.length, 1); assert.equal(animations[0].finished, false); assert.equal(h.requests.length, 1); assert.equal(h.requests[0].fields.taskId, 'pass2'); const second = structuredClone(h.info.tasks); second.dailyTasks[1].state = 2; h.respond(second, 0, { rewards: [{ type: 'coin', count: 10 }] }); assert.equal(h.requests.length, 1); assert.equal(h.requests[0].fields.rewardId, 'weekly_35'); const third = structuredClone(h.info.tasks); third.weeklyRewards[0].state = 2; h.respond(third, 0, { rewards: [{ type: 'coin', count: 10 }] }); assert.equal(h.requests.length, 0); assert.equal(animations.length, 3); assert.ok(animations.every(animation => !animation.finished)); assert.ok(h.info.tasks.dailyTasks.every(task => task.state === 2)); assert.equal(h.Sync.busy, false); }); test('failed saves block claims, preserve unsaved progress and release the queue for retry', () => { const h=setup(); const data=h.data(); data.dailyTasks[0].target=1; h.load(data); h.Tracker.record({metric:'pass_levels'}); const replies=[]; h.GameTool.claimDailyQuestReward('pass',r=>replies.push(r)); h.requests.shift().callback({code:0,msg:'offline'}); assert.ok(h.requests.every(r=>r.kind!=='claim')); assert.equal(h.value(),1); assert.equal(replies[0].code,0); assert.equal(h.requests.length,0); h.GameTool.claimDailyQuestReward('pass',()=>{}); assert.equal(h.requests[0].kind,'save'); h.ackSave(); assert.equal(h.requests[0].kind,'claim'); }); test('new-period events are buffered while reading and old unsaved progress is never uploaded into the new day', () => { const h=setup(); h.load(h.data({dayEndsAt:h.now+500})); h.Tracker.record({metric:'pass_levels',amount:3}); h.advance(600); h.Tracker.record({metric:'pass_levels',amount:2}); assert.deepEqual(h.requests.map(r=>r.kind),['read']); h.respond(h.data({dayKey:'day2'})); assert.equal(h.value(),2); h.advance(1000); assert.equal(h.requests[0].kind,'save'); assert.equal(JSON.parse(h.requests[0].fields.task).dayKey,'day2'); assert.equal(JSON.parse(h.requests[0].fields.task).dailyTasks[0].value,2); }); test('online recovery before initial loading is retained and uploaded through the synchronization queue', () => { const h=setup(); h.Tracker.startOnline(); h.advance(65000); h.load(h.data({dailyTasks:[{taskId:'online',metric:'online_seconds',value:10,target:300,state:0}]})); assert.equal(h.value(),70); h.advance(1000); h.ackSave(); h.advance(24000); assert.equal(h.value(),100); }); test('online settlement spanning midnight counts only new-day seconds in the replacement tasks', () => { const h=setup(); h.load(h.data({dayEndsAt:h.now+10000,dailyTasks:[{taskId:'online',metric:'online_seconds',value:0,target:300,state:0}]})); h.Tracker.startOnline(); h.advance(30000); assert.equal(h.requests[0].kind,'read'); h.respond(h.data({dayKey:'day2',dailyTasks:[{taskId:'online',metric:'online_seconds',value:0,target:300,state:0}]})); assert.equal(h.value(),20); }); test('debug pause drains uploads, ignores new events, refreshes external resets and resumes without paused time', () => { const h=setup(); h.load(); h.Tracker.record({metric:'pass_levels',amount:3}); h.advance(1000); let paused; h.GameTool.debugDailyTasks('pause',r=>paused=r); assert.equal(paused,undefined); h.Tracker.record({metric:'pass_levels',amount:5}); assert.equal(h.value(),3); h.ackSave(); assert.equal(paused.code,1); h.GameTool.debugDailyTasks('refresh',()=>{}); assert.equal(h.requests[0].kind,'read'); h.respond(h.data()); assert.equal(h.value(),0); h.advance(30000); assert.equal(h.requests.length,0); h.GameTool.debugDailyTasks('resume',()=>{}); h.respond(h.data()); h.Tracker.record({metric:'pass_levels'}); assert.equal(h.value(),1); }); test('debug controls are unavailable outside development and long foreground absence refreshes once', () => { const h=setup(); h.load(); h.GameTool.getWechatGameVersion=()=> '正式版'; let result; h.GameTool.debugDailyTasks('pause',r=>result=r); assert.equal(result.code,0); h.GameTool.onDailyTasksBackground(); h.advance(1000); h.GameTool.onDailyTasksForeground(); assert.equal(h.requests.length,0); h.GameTool.onDailyTasksBackground(); h.advance(300000); h.GameTool.onDailyTasksForeground(); h.GameTool.onDailyTasksForeground(); assert.equal(h.requests.length,1); }); test('network restoration retries failed reads and stale-account callbacks cannot populate the new account cache', () => { const h=setup(); h.load(); h.GameTool.onDailyTaskNetworkChange(false); h.GameTool.onDailyTaskNetworkChange(true); assert.equal(h.requests.length,1); h.requests.shift().callback({code:0,msg:'offline'}); h.GameTool.onDailyTaskNetworkChange(true); assert.equal(h.requests.length,1); h.info.uid='another-user'; h.respond(h.data()); assert.equal(h.Sync.cached(),null); h.GameTool.getDailyQuestsInfo(()=>{}); h.respond(h.data()); assert.ok(h.Sync.cached()); }); test('invalid or expired server task snapshots are not exposed as valid cache entries', () => { const h=setup(); let result; h.GameTool.getDailyQuestsInfo(r=>result=r); h.respond(h.data({dayEndsAt:h.now-1})); assert.equal(result.code,0); assert.equal(h.Sync.cached(),null); h.GameTool.getDailyQuestsInfo(r=>result=r); h.requests.shift().callback({code:1,data:{task:{version:2}}}); assert.equal(result.code,0); assert.equal(h.Sync.cached(),null); for (const invalid of [null, h.data({version: 1}), h.data({version: 3}), h.data({version: '2'}), h.data({dailyTasks: {}})]) { const valid = h.data(); h.Tracker.setTaskData(valid); h.Tracker.setTaskData(invalid); assert.equal(h.Tracker.getTaskData(), valid, 'rejected snapshots must not replace local progress'); h.info.tasks = invalid; assert.equal(h.Tracker.getTaskData(), null, 'direct cache reads use the same compatibility rules'); h.GameTool.getDailyQuestsInfo(r=>result=r); h.respond(invalid); assert.equal(result.code,0); assert.equal(h.Sync.cached(),null); } h.GameTool.getDailyQuestsInfo(r=>result=r); h.respond(h.data({weeklyRewards: null})); assert.equal(result.code,0); assert.equal(h.Sync.cached(),null, 'responses still require weekly rewards'); }); test('malformed claim responses cannot be treated as grants or leave the request queue occupied', () => { const h=setup(),data=h.data();data.dailyTasks[0].state=1;data.dailyTasks[0].value=10;h.load(data); let result;h.GameTool.claimDailyQuestReward('pass',r=>result=r);h.requests.shift().callback({code:1,data:{}}); assert.equal(result.code,0);assert.equal(h.info.tasks.dailyTasks[0].state,1);assert.equal(h.Sync.busy,false); }); function method(source, name) { const start=source.search(new RegExp(`^ (?:private |public |static )*${name}\\(`,'m')); assert.notEqual(start,-1); const tail=source.slice(start), end=tail.search(/^ }\r?$/m); return tail.slice(0,end+5); } function installUI(h) { const homeSource=read('assets/Script/JiaZai.ts'), panelSource=read('assets/action_bundle/script/DailyQuests.ts'); const uiCode=stripTypeScriptTypes(`class JiaZai {${['openDailyQuests','checkTasks','refreshTaskRedDot','refreshTaskViews'].map(n=>method(homeSource,n)).join('\n')}} class Panel {${['closeDailyQuests','refreshFromCache','hasOpeningStateAnimation','claimKey','initCountdowns','updateCountdowns','hasExpiredDisplayedTasks','getRemainingSeconds','formatClock'].map(n=>method(panelSource,n)).join('\n')}} globalThis.UIClasses={JiaZai,Panel};`); vm.runInNewContext(uiCode,h.context); const {JiaZai,Panel}=h.context.UIClasses, home=new JiaZai(), panel=new Panel(), red={active:false}, day={text:''}, week={text:''}; const top={getChildByName:()=>({getChildByName:()=>red})}; const root={getChildByName:()=>({getChildByName:()=>top}),getComponent:()=>home,addChild:()=>{}}; const bg={getChildByName:n=>n==='dayCountDown'?day:week}; panel.node={active:true,get activeInHierarchy(){return this.active;},getChildByName:()=>bg,getComponent:()=>panel}; panel.dailyTaskStateAnimations=[]; panel.animatingClaims=new Set(); panel.pendingClaims=new Set(); panel.content={getChildByName:()=>null}; panel.renderWeeklyRewards=data=>{panel.rendered=data;}; h.context.cc.fx.GameConfig.isNewPlayerB=()=>false; panel.setNodeText=(node,value)=>{node.text=value;}; let timer; panel.unschedule=()=>h.context.clearInterval(timer); panel.schedule=(fn,seconds)=>{timer=h.context.setInterval(()=>{if(panel.node.active) fn.call(panel);},seconds*1000);}; panel.init=data=>{panel.cacheRefreshPending=false;panel.rendered=data;panel.touchStart=true;panel.initCountdowns(data);}; home.node=root; home.vibrateButtonClick=home.openLoad2=home.closeLoad=()=>{}; JiaZai.cachedDailyQuestsPrefab={}; let instances=0; Object.assign(h.context.cc,{isValid:node=>!!node,find:()=>root,instantiate:()=>{instances++;return panel.node;}}); h.context.cc.director.emit=()=>home.refreshTaskViews(); h.context.MiniGameSdk={API:{showToast(){}}}; return {home,panel,red,day,week,get instances(){return instances;}}; } test('real homepage open/close handlers reuse one popup and only the first load reads the server', () => { const h=setup(), ui=installUI(h); ui.home.checkTasks(); ui.home.openDailyQuests(null,'dayBtn'); assert.equal(h.requests.length,1); h.respond(h.data()); assert.equal(ui.instances,1); for(let i=0;i<4;i++){ui.panel.closeDailyQuests();ui.home.openDailyQuests(null,'dayBtn');} assert.equal(h.requests.length,0); assert.equal(ui.instances,1); h.Tracker.record({metric:'pass_levels',amount:10}); assert.equal(ui.red.active,true); ui.panel.closeDailyQuests(); assert.equal(h.requests.length,0); assert.equal(ui.red.active,true); }); test('open popup keeps old tasks at midnight and fetches on reopening without resetting a valid countdown', () => { const h=setup(), ui=installUI(h); h.load(h.data({dayEndsAt:h.now+2000})); ui.home.openDailyQuests(null,'dayBtn'); h.advance(1000); assert.ok(ui.day.text.endsWith('00:00:01')); ui.panel.closeDailyQuests();ui.home.openDailyQuests(null,'dayBtn');assert.ok(ui.day.text.endsWith('00:00:01')); h.advance(1000); assert.equal(h.requests.length,0); assert.ok(ui.day.text.endsWith('00:00:00')); assert.equal(ui.panel.rendered.dayKey,'day1'); h.advance(60000); assert.equal(h.requests.length,0); ui.panel.closeDailyQuests(); assert.equal(h.requests.length,0); ui.home.openDailyQuests(); assert.equal(h.requests.length,1); h.respond(h.data({dayKey:'day2'})); assert.equal(ui.panel.rendered.dayKey,'day2'); assert.equal(ui.panel.touchStart,true); }); test('failed refresh on reopening does not poll or reopen the expired popup', () => { const h=setup(), ui=installUI(h);h.load(h.data({dayEndsAt:h.now+1000}));ui.home.openDailyQuests(); h.advance(1000);ui.panel.closeDailyQuests();ui.home.openDailyQuests(); h.requests.shift().callback({code:0,msg:'offline'}); assert.equal(ui.panel.node.active,false);h.advance(60000);assert.equal(h.requests.length,0); ui.home.openDailyQuests();assert.equal(h.requests.length,1);h.respond(h.data({dayKey:'day2'})); assert.equal(ui.panel.node.active,true);assert.equal(ui.panel.rendered.dayKey,'day2'); }); test('background refresh after day or week rollover cannot replace an open popup until reopening', () => { for (const end of ['dayEndsAt','weekEndsAt']) { const h=setup(),ui=installUI(h);h.load(h.data({[end]:h.now+1000}));ui.home.openDailyQuests(); ui.panel.cacheRefreshPending=true; h.advance(1000);assert.equal(h.requests.length,0); h.GameTool.getDailyQuestsInfo(()=>{},true);h.respond(h.data({dayKey:'day2',weekKey:'week2'})); assert.equal(ui.panel.rendered.dayKey,'day1'); h.advance(2000);assert.equal(ui.panel.rendered.dayKey,'day1'); ui.panel.closeDailyQuests();ui.home.openDailyQuests(); assert.equal(ui.panel.rendered.dayKey,'day2');assert.equal(h.requests.length,0); } }); test('refresh received during a popup animation is displayed once the animation unlocks', () => { const h=setup(),ui=installUI(h);h.load();ui.home.openDailyQuests();ui.panel.touchStart=false; h.GameTool.debugDailyTasks('refresh',()=>{});const updated=h.data();updated.weeklyStars=70;h.respond(updated); assert.equal(ui.panel.rendered.weeklyStars,0);ui.panel.touchStart=true;h.advance(1000); assert.equal(ui.panel.rendered.weeklyStars,70);assert.equal(h.requests.length,0); }); test('failed debug pause cannot be followed by destructive refresh/resume until the upload succeeds', () => { const h=setup(); h.load(); h.Tracker.record({metric:'pass_levels'}); let result; h.GameTool.debugDailyTasks('pause',r=>result=r);h.requests.shift().callback({code:0,msg:'offline'}); assert.equal(result.code,0); for(const action of ['refresh','resume']){h.GameTool.debugDailyTasks(action,r=>result=r);assert.equal(result.code,0);assert.equal(h.requests.length,0);} h.GameTool.debugDailyTasks('pause',r=>result=r);h.ackSave();assert.equal(result.code,1); }); test('forced debug refresh can reset claimed daily and weekly states after a successful pause', () => { const h=setup(), initial=h.data();initial.dailyTasks[0].value=10;initial.dailyTasks[0].state=2;initial.weeklyRewards[0].state=2; h.load(initial);h.GameTool.debugDailyTasks('pause',()=>{});h.GameTool.debugDailyTasks('refresh',()=>{}); const reset=h.data();reset.dailyTasks[0].state=1;reset.dailyTasks[0].value=10;reset.weeklyRewards[0].state=1;h.respond(reset); assert.equal(h.info.tasks.dailyTasks[0].state,1);assert.equal(h.info.tasks.weeklyRewards[0].state,1); }); test('debug pause confirms recovered online progress so a subsequent test reset remains authoritative', () => { const h=setup();h.Tracker.startOnline();h.advance(60000); const initial=h.data({dailyTasks:[{taskId:'online',metric:'online_seconds',value:0,target:300,state:0}]});h.load(initial); assert.equal(h.value(),60);let paused; h.GameTool.debugDailyTasks('pause',r=>paused=r);h.ackSave();assert.equal(paused.code,1); h.GameTool.debugDailyTasks('refresh',()=>{});h.respond(initial);assert.equal(h.value(),0); }); test('the first request can obtain its owner UID from storage before Utils copies it into GM_INFO', () => { const h=setup();h.info.uid='';h.context.cc.fx.StorageMessage.getStorage=()=> 'stored-user';h.load(); assert.equal(h.info.uid,'stored-user');assert.ok(h.Sync.cached()); }); test('task transport completes failed requests once so timeouts cannot hold the synchronization queue', () => { for(const event of ['ontimeout','onerror','onabort']){ const h=setup(), xhr={open(){},setRequestHeader(){},send(){}}; h.context.cc.loader={getXMLHttpRequest:()=>xhr}; const post=method(utils,'POST').replace('public static POST','POST'); const compiled=stripTypeScriptTypes(`globalThis.post={${post}}.POST;`);vm.runInNewContext(compiled,h.context); h.Utils.POST=h.context.post; h.Utils.testHttpip='https://example.invalid/';h.Utils.httpip=h.Utils.testHttpip; h.Utils.trackWucaiRequest=(_path,_body,callback)=>callback; h.Utils.apiBaseUrl=()=>h.Utils.testHttpip; const results=[];h.GameTool.getDailyQuestsInfo(r=>results.push(r));xhr[event](); xhr.readyState=4;xhr.status=200;xhr.responseText=JSON.stringify({code:1,data:{task:h.data()}});xhr.onreadystatechange(); assert.equal(results.length,1);assert.equal(results[0].code,0);assert.equal(h.Sync.busy,false); } });