Files
MatchMaster/tools/test-daily-task-cache.cjs
T
2026-10-16 15:02:14 +08:00

388 lines
26 KiB
JavaScript

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