const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); const ts = require('typescript'); function loadClass(file, names, globals = {}, deps = {}) { const source = fs.readFileSync(file, 'utf8'); const parsed = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true); const cls = parsed.statements.find(ts.isClassDeclaration); const code = names ? 'export default class ' + cls.name.text + ' {\n' + cls.members .filter(member => names.includes(member.name?.getText(parsed))) .map(member => member.getText(parsed)).join('\n') + '\n}' : source; const exports = {}; vm.runInNewContext(ts.transpileModule(code, { compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS, } }).outputText, { exports, require: name => deps[name], console: { log() {} }, ...globals }); return exports.default; } function fixture() { const info = { uid: 'u', coin: 10, freezeAmount: 1, hammerAmount: 2, magicAmount: 3 }; const storage = { prop: {} }, events = []; const cc = { find: () => null, isValid: () => false, fx: { GameConfig: { GM_INFO: info, PASS_CHECK: [] }, StorageMessage: { getStorage: key => storage[key], setStorage: (key, value) => storage[key] = value }, GameTool: { changeCoin: amount => info.coin += amount, setUserProp() {}, shushu_Track: (name, data) => events.push({ name, ...data }), // Existing shared helper owns the infinite-health event. setUserPowerTime: (num, change_reason) => events.push({ name: 'resource_get', id: '2005', num, change_reason, compensate: false }), }, } }; return { cc, info, storage, events }; } const rows = events => events.map(e => [String(e.id), e.num]).sort((a, b) => a[0].localeCompare(b[0])); test('seven-day mixed rewards report each resource once and skip a processed receipt', () => { const f = fixture(); const Reward = loadClass('assets/seven_day_gift/SevenDayGiftReward.ts', null, { cc: f.cc }, { './SevenDayGiftApi': { default: { getUid: () => 'u', saveProps: (_, cb) => cb({ code: 1 }) } }, }); const receipt = { claimId: 'day-2', rewards: [ { type: 'coin', count: 300 }, { type: 'freeze', count: 1 }, { type: 'hammer', count: 2 }, { type: 'magic_wand', count: 3 }, { type: 'infinite_health', count: 900 }, ] }; Reward.grant(receipt, ok => assert.equal(ok, true)); Reward.grant(receipt, ok => assert.equal(ok, true)); assert.deepEqual(rows(f.events), [['1001', 300], ['2001', 1], ['2002', 2], ['2003', 3], ['2005', 900]]); assert.ok(f.events.every(e => e.change_reason === 'seven_day_gift' && e.compensate === false)); assert.equal(f.info.coin, 310); }); test('seven-day zero rewards produce no resource events', () => { const f = fixture(); const Reward = loadClass('assets/seven_day_gift/SevenDayGiftReward.ts', null, { cc: f.cc }, { './SevenDayGiftApi': { default: { getUid: () => 'u' } }, }); Reward.grant({ claimId: 'empty', rewards: [{ type: 'coin', count: 0 }] }, ok => assert.equal(ok, true)); assert.equal(f.events.length, 0); }); test('weekly chest reports coins and all three props, with infinite health reported once', () => { const f = fixture(); const Daily = loadClass('assets/action_bundle/script/DailyQuests.ts', ['openReward', 'getShopProp'], { cc: f.cc, wx: {}, DailyTaskProgressTracker: { trackTask() {} }, }); const view = new Daily(); view.grantFutureProps = () => {}; view.openReward('weekly_140', [ { type: 'coin', count: 2000 }, { type: 'freeze', count: 1 }, { type: 'hammer', count: 1 }, { type: 'magic_wand', count: 1 }, { type: 'infinite_health', count: 3600 }, ], false); assert.deepEqual(rows(f.events), [['1001', 2000], ['2001', 1], ['2002', 1], ['2003', 1], ['2005', 3600]]); assert.ok(f.events.filter(e => e.id !== '2005').every(e => e.change_reason === 'quests')); f.events.length = 0; view.openReward('empty', [], false); assert.equal(f.events.length, 0); }); function passFixture() { const f = fixture(); const Utils = loadClass('assets/Script/module/Pay/Utils.ts', ['passGiftPending', 'applyPassGift', 'commitPassSettlement', 'syncPassResources'], { cc: f.cc, settlementRewards: () => [], settlementClaimFlags: () => ({}), }); Utils.withCoinSave = work => work(); return { ...f, Utils }; } test('battle-pass bonus reports the actual 3000 coins once per pending order', () => { const f = passFixture(); f.Utils.passGiftPending['u:paid'] = 'pending'; f.Utils.applyPassGift('paid'); f.Utils.applyPassGift('paid'); f.Utils.applyPassGift('unknown'); assert.deepEqual(f.events, [{ name: 'resource_get', change_reason: 'battlepass', id: '1001', num: 3000, compensate: false }]); assert.equal(f.info.coin, 3010); }); for (const action of ['claim', 'purchase_claim']) test(`battle-pass ${action} reports saved rewards and skips duplicate receipts`, async () => { const f = passFixture(); const result = { data: { alreadyGranted: false, resources: { coinAmount: 310, freezeAmount: 2, hammerAmount: 4, magicAmount: 6, userPowerTime: 900 }, rewards: [{ type: 'coin', count: 300 }, { type: 'freeze', count: 1 }, { type: 'hammer', count: 2 }, { type: 'magic', count: 3 }, { type: 'infinite_health', count: 900 }], } }; f.Utils.settlementRequest = async () => result; await f.Utils.commitPassSettlement(action, { end: 1 }, 'paid'); result.data.alreadyGranted = true; await f.Utils.commitPassSettlement(action, { end: 1 }, 'paid'); assert.deepEqual(rows(f.events), [['1001', 300], ['2001', 1], ['2002', 2], ['2003', 3], ['2005', 900]]); assert.ok(f.events.every(e => e.change_reason === 'battlepass' && e.compensate === true)); f.Utils.settlementRequest = async () => { throw new Error('offline'); }; await assert.rejects(f.Utils.commitPassSettlement(action, { end: 2 }), /offline/); assert.equal(f.events.length, 5); });