const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const ts = require('typescript'); const { fixture } = require('./check-api.cjs'); const at = (f, time) => { f.client.offset = time - Date.now(); }; const DAY = 86400000; test('daily reminder uses server Beijing date, survives relogin and isolates accounts/environments', () => { const storage = new Map(), f = fixture(storage); f.client.info = { ...f.info }; at(f, f.info.serverTime); const reminder = f.client.dailyReminder(); assert(reminder); assert.equal(reminder.day, Math.floor((f.info.serverTime + 8 * 3600000) / DAY)); f.client.markDailyReminderShown(reminder); assert.equal(f.client.dailyReminder(), null); const restarted = fixture(storage); restarted.client.info = { ...f.info }; at(restarted, f.info.serverTime); assert.equal(restarted.client.dailyReminder(), null); const midnight = (reminder.day + 1) * DAY - 8 * 3600000; at(f, midnight + 1000); assert.equal(f.client.dailyReminder().day, reminder.day + 1); f.client.markDailyReminderShown(reminder); assert(f.client.dailyReminder(), 'old date cannot consume new day'); f.game.uid = 'bob'; assert.equal(f.client.dailyReminder(), null, 'stale account client cannot remind'); const other = new f.client.constructor('bob', 'https://test/:bob'); other.info = { ...f.info }; other.offset = f.client.offset; assert.notEqual(other.dailyReminder().key, reminder.key); const production = fixture(storage, { baseUrl: 'https://production/' }); production.client.info = { ...f.info }; at(production, f.info.serverTime); assert.notEqual(production.client.dailyReminder().key, reminder.key); }); test('only qualified active periods remind; paid and purchase-disabled players are included', () => { const f = fixture(); at(f, f.info.serverTime); assert.equal(f.client.dailyReminder(), null); f.client.info = { ...f.info }; for (const status of ['unavailable', 'locked', 'intermission']) { f.client.info.status = status; assert.equal(f.client.dailyReminder(), null); } for (const status of ['purchasable', 'purchase_disabled', 'unlocked']) { f.client.info.status = status; assert(f.client.dailyReminder()); } at(f, f.info.startsAt - 1000); assert.equal(f.client.dailyReminder(), null); at(f, f.info.endsAt + 1000); assert.equal(f.client.dailyReminder(), null); at(f, f.info.serverTime); const old = f.client.dailyReminder(); f.client.info.periodId = 'goldMiner:test:new'; f.client.markDailyReminderShown(old); assert(f.client.dailyReminder(), 'stale period cannot consume current reminder'); f.game.token = ''; assert.equal(f.client.dailyReminder(), null); }); const hostCode = ts.transpileModule(fs.readFileSync('assets/Script/coin_madness/CoinMadnessHost.ts', 'utf8'), { compilerOptions: { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.CommonJS, experimentalDecorators: true } }).outputText; function hostFixture() { const f = fixture(); f.client.info = { ...f.info }; at(f, f.info.serverTime); f.game.canIos = true; let current = f.client, notice = 0, asset, initialized = 0, done = 0; Object.defineProperty(f.client, 'settlementNotice', { get: () => notice }); f.client.readForDisplay = () => Promise.resolve({ state: {} }); f.client.refresh = () => new Promise(() => {}); const queued = [], bundle = { load(_path, _type, cb) { asset = cb; }, releaseAll() {} }; const cc = { ...f.cc, Component: class {}, _decorator: { ccclass: c => c }, isValid: () => true, Prefab: class {}, macro: { MAX_ZINDEX: 100 }, Director: { EVENT_AFTER_DRAW: 1 }, director: { once(_e, cb) { cb(); } }, assetManager: { loadBundle(_name, cb) { cb(null, bundle); }, getBundle: () => bundle, removeBundle() {} }, instantiate: () => ({ destroy() {}, getComponent: () => ({ initialize() { initialized++; } }) }) }; const exports = {}; new Function('require', 'exports', 'cc', hostCode)(name => { if (name.includes('HomePopupQueue')) return { default: { get: () => ({ enqueue: (key, priority, run) => queued.push({ key, priority, run }) }) } }; if (name.includes('GoldMinerService')) return { default: { current: () => current } }; if (name.includes('Utils')) return { default: { serverUrl: () => 'https://test/' } }; return { MiniGameSdk: { API: { showToast() {} } } }; }, exports, cc); const host = new exports.default(); host.service = f.client; host.entry = { active: true }; host.node = { getComponent: () => null, addChild() {} }; return { ...f, host, queued, run: () => queued.shift().run(() => done++), assets: error => asset(error, {}), setCurrent: value => current = value, setNotice: value => notice = value, initialized: () => initialized, done: () => done }; } test('queued reminder marks only after successful mount, holds queue until close and never repeats today', async () => { const f = hostFixture(); f.host.queueDailyReminder(); f.host.queueDailyReminder(); assert.equal(f.queued.length, 1); assert.equal(f.queued[0].priority, 11); f.run(); assert(f.client.dailyReminder()); assert.equal(f.done(), 0); await f.assets(null); assert.equal(f.initialized(), 1); assert.equal(f.client.dailyReminder(), null); assert.equal(f.done(), 0); f.host.close(); assert.equal(f.done(), 1); f.host.nextDailyReminderAt = 0; f.host.queueDailyReminder(); assert.equal(f.queued.length, 0); }); test('failed load keeps daily eligibility and releases queue for retry', async () => { const f = hostFixture(); f.host.queueDailyReminder(); f.run(); await f.assets(new Error('asset failed')); assert.equal(f.initialized(), 0); assert.equal(f.done(), 1); assert(f.client.dailyReminder()); f.host.queueDailyReminder(); assert.equal(f.queued.length, 0, 'retry has backoff'); f.host.nextDailyReminderAt = 0; f.host.queueDailyReminder(); f.run(); await f.assets(null); assert.equal(f.initialized(), 1); f.host.close(); }); test('settlement, hidden entry and manual panel suppress automatic reminder', () => { const f = hostFixture(); f.setNotice(100); f.host.queueDailyReminder(); assert.equal(f.queued.length, 0); f.setNotice(0); f.host.entry.active = false; f.host.queueDailyReminder(); assert.equal(f.queued.length, 0); f.host.entry.active = true; f.host.panel = {}; f.host.queueDailyReminder(); assert.equal(f.queued.length, 0); f.host.panel = null; f.host.queueDailyReminder(); f.setNotice(100); f.run(); assert.equal(f.done(), 1); assert(f.client.dailyReminder()); }); test('account or expiry changes while queued/loading do not open or consume the reminder', async () => { const queued = hostFixture(); queued.host.queueDailyReminder(); queued.setCurrent(null); queued.run(); assert.equal(queued.done(), 1); assert.equal(queued.initialized(), 0); for (const expire of [false, true]) { const f = hostFixture(); f.host.queueDailyReminder(); f.run(); if (expire) at(f, f.info.endsAt + 1000); else f.setCurrent(null); await f.assets(null); assert.equal(f.initialized(), 0); assert.equal(f.done(), 1); assert.equal(f.storage.has('goldMinerDailyReminder:https://test/:alice'), false); } });