import test from 'node:test'; import assert from 'node:assert/strict'; import { registerHooks, stripTypeScriptTypes } from 'node:module'; import { readFileSync } from 'node:fs'; const NOW = Date.parse('2026-09-30T12:00:00+08:00'), DAY = 86400000; let now = NOW, beforeUpdate = null, failProfile = false; const realNow = Date.now; Date.now = () => now; test.after(() => { Date.now = realNow; }); const tables = { users: [], userRechargeStats: [] }; const copy = value => value == null ? value : structuredClone(value); const matches = (row, query) => Object.entries(query).every(([key, value]) => value === null ? row[key] == null : row[key] === value); globalThis.__starterCloud = { database: () => ({ collection: name => ({ where(query) { return { async getOne() { if (name === 'userRechargeStats' && failProfile) throw new Error('profile unavailable'); return { data: copy(tables[name].find(row => matches(row, query))) }; }, async update(patch) { if (beforeUpdate) { const fn = beforeUpdate; beforeUpdate = null; fn(); } const rows = tables[name].filter(row => matches(row, query)); rows.forEach(row => Object.assign(row, copy(patch))); return { updated: rows.length }; }, }; } }) }), }; globalThis.__starterUtils = { checkToken: (got, expected) => got === expected }; registerHooks({ resolve(specifier, context, next) { if (specifier === '@lafjs/cloud') return { url: 'data:text/javascript,export default globalThis.__starterCloud', shortCircuit: true }; if (specifier === '@/Utils') return { url: 'data:text/javascript,export default globalThis.__starterUtils', shortCircuit: true }; if (specifier === '@/paymentProfile') return { url: new URL('../functions/paymentProfile.ts', import.meta.url).href, shortCircuit: true }; if (specifier.startsWith('@/')) throw new Error('Unexpected dependency: ' + specifier); return next(specifier, context); }, load(url, context, next) { if (url.endsWith('.ts')) return { format: 'module', source: stripTypeScriptTypes(readFileSync(new URL(url), 'utf8')), shortCircuit: true }; return next(url, context); }, }); const { default: activity, starterPackPatch, starterPackOrderSnapshot } = await import('../functions/limitedTimeEvent.ts'); const { PAYMENT_PROFILE_RULE } = await import('../functions/paymentProfile.ts'); function reset(patch = {}) { now = NOW; beforeUpdate = null; failProfile = false; for (const name of Object.keys(tables)) tables[name] = []; tables.users.push({ _id: 'u1', openid: 'o1', token: 't1', levelAmount: 15, pay_user: false, register_time: NOW - 90 * DAY, starter_pack: 0, starter_packState: 0, ...patch }); return tables.users[0]; } const ctx = extra => ({ body: { uid: 'u1', token: 't1', event: 'starter_pack', ...extra } }); const read = () => activity(ctx({ action: 'read' })); const trigger = (reason = 'shop', extra = {}) => activity(ctx({ action: 'reactivate', reason, ...extra })); function profile(user, daily = 600, anchor = 600, patch = {}) { const row = { _id: user._id, openid: user.openid, data_valid: true, amount_basis: 'confirmed_order_amount', profile_rule_version: PAYMENT_PROFILE_RULE.version, profile_calculated_as_of: Date.parse('2026-09-30T00:00:00+08:00'), maintenance_identity: { pay_user: true, register_time: user.register_time }, spending: { daily_reference_fen: daily }, ticket_factors: { lucky: { anchor_fen: anchor } }, ...patch }; tables.userRechargeStats.push(row); return row; } test('read or save without a reason cannot start; first unpaid offer lasts 12 hours', async () => { const user = reset(); await read(); await activity(ctx({ action: 'save' })); assert.equal(user.starter_pack, 0); await Promise.all([trigger(), trigger('level_purchase')]); assert.equal(user.starter_pack, NOW + DAY / 2); assert.equal(user.starterPackRound, 1); assert.equal(user.starterPackOffer.productId, 'starter_pack'); assert.deepEqual(user.starterPackOffer.reward, { coin: 3000, hammer: 1, freeze: 1, magic_wand: 1, infinite_health: 1800 }); }); test('formula thresholds select four paid products and exact rewards for six hours', async () => { for (const [daily, id, price, coin, props, minutes] of [ [0, 'starter_pack_2', 600, 3000, 1, 30], [1199, 'starter_pack_2', 600, 3000, 1, 30], [1200, 'starter_pack_3', 1200, 4500, 3, 45], [1800, 'starter_pack_4', 1800, 5000, 5, 60], [3000, 'starter_pack_5', 3000, 8000, 8, 180], [100000, 'starter_pack_5', 3000, 8000, 8, 180], ]) { const user = reset({ pay_user: true }); profile(user, daily); await trigger(); assert.equal(user.starter_pack, NOW + DAY / 4); assert.equal(user.starterPackOffer.productId, id); assert.equal(user.starterPackOffer.priceFen, price); assert.deepEqual(user.starterPackOffer.reward, { coin, hammer: props, freeze: props, magic_wand: props, infinite_health: minutes * 60 }); } }); test('ticket reference is multiplied by 0.7 and history never overrides the formula', async () => { const user = reset({ pay_user: true, lastStarterPackPrice: 3000 }); profile(user, 1000, 3000); await trigger(); assert.equal(user.starterPackOffer.priceFen, 1800); const second = reset({ pay_user: true, lastStarterPackPrice: 3000 }); profile(second, 1200, 600); await trigger(); assert.equal(second.starterPackOffer.priceFen, 1200); }); test('missing, invalid, stale, future, mismatched and unavailable paid profiles fall back to 6 yuan', async () => { for (const patch of [null, { data_valid: false }, { profile_rule_version: 'old' }, { profile_calculated_as_of: NOW - 3 * DAY }, { profile_calculated_as_of: NOW + DAY }, { openid: 'someone-else' }, { maintenance_identity: { pay_user: false } }, { amount_basis: 'unknown' }, { spending: { daily_reference_fen: null } }, { ticket_factors: { lucky: { anchor_fen: NaN } } }]) { const user = reset({ pay_user: true }); if (patch) profile(user, 100000, 100000, patch); await trigger(); assert.equal(user.starterPackOffer.priceFen, 600); assert.equal(user.starterPackOffer.selectionBasis, 'fallback'); } const user = reset({ pay_user: true }); failProfile = true; await trigger(); assert.equal(user.starterPackOffer.priceFen, 600); }); test('one daily batch of lag is allowed and pay_user determines first-purchase identity', async () => { const paid = reset({ pay_user: true }); profile(paid, 3000, 600, { profile_calculated_as_of: Date.parse('2026-09-29T00:00:00+08:00') }); await trigger(); assert.equal(paid.starterPackOffer.priceFen, 3000); const free = reset({ pay_user: false, vip_level: 5 }); profile(free, 100000, 100000); await trigger(); assert.equal(free.starterPackOffer.priceFen, 300); for (const pay_user of [undefined, null, 'false', 0]) { const unknown = reset({ pay_user }); await trigger(); assert.equal(unknown.starter_pack, 0); } }); test('invalid registration timestamps use the same null identity as rechargeStats', async () => { for (const register_time of [null, undefined, 0, '0', -1, '2026-09-01']) { const user = reset({ pay_user: true, register_time }); profile(user, 3000, 600, { maintenance_identity: { pay_user: true, register_time: null } }); await trigger(); assert.equal(user.starterPackOffer.priceFen, 3000); } }); test('a displayed offer stays fixed when payment identity and daily factors change', async () => { const user = reset(); await trigger(); const offer = copy(user.starterPackOffer); user.pay_user = true; profile(user, 100000, 100000); now += 3600000; await read(); await trigger(); assert.deepEqual(user.starterPackOffer, offer); assert.equal(starterPackOrderSnapshot(user, 'starter_pack').starterPackOffer.priceFen, 300); }); test('legacy active offers retain their original deadline and old promised reward', async () => { for (const version of [undefined, 1, 2]) { const user = reset({ starter_pack: NOW + DAY, starterPackVersion: version }); await Promise.all([read(), read()]); assert.equal(user.starter_pack, NOW + DAY); assert.equal(user.starterPackRound, 1); assert.equal(user.starterPackOffer.legacy, true); assert.equal(user.starterPackOffer.reward.hammer, 3); assert.equal(user.starterPackOffer.reward.infinite_health, 1800); const locked = copy(user); await read(); assert.deepEqual(user, locked); } }); test('reopening uses Beijing end date plus two days even after early purchase', async () => { for (const ending of ['2026-09-23T02:00:00+08:00', '2026-09-23T23:59:00+08:00']) { const user = reset({ pay_user: true, starter_pack: Date.parse(ending), starter_packState: 1, starterPackRound: 4 }); now = Date.parse('2026-09-24T23:59:59.999+08:00'); await trigger(); assert.equal(user.starterPackRound, 4); now++; await Promise.all([trigger(), trigger('level_purchase')]); assert.equal(user.starterPackRound, 5); assert.equal(user.starter_packState, 0); assert.equal(user.starter_pack, now + DAY / 4); } }); test('old paid users with no historical deadline can enter a new paid round', async () => { const user = reset({ pay_user: true, starter_packState: 1 }); await trigger(); assert.equal(user.starterPackOffer.priceFen, 600); assert.equal(user.starterPackRound, 2); }); test('approved triggers are independent; unknown resources are not treated as zero', async () => { const valid = [ ['low_coin', { coinAmount: 499 }], ['low_health', { healthAmount: 0, userPowerTime: 0, healthtimestamp: NOW }], ['low_props', { freezeAmount: 3, hammerAmount: 2, magicAmount: 0 }], ['shop', {}], ['level_purchase', {}], ['purchase', {}], ['four_failures', {}], ]; for (const [reason, patch] of valid) { const user = reset(patch); await trigger(reason, { level: 15, failureCount: 4 }); assert.equal(user.starterPackRound, 1, reason); } for (const [reason, patch, extra] of [ ['low_coin', { coinAmount: 500 }], ['low_coin', { coinAmount: null }], ['low_health', { healthAmount: 0 }], ['low_health', { healthAmount: 0, userPowerTime: NOW / 1000 + 1 }], ['low_health', { healthAmount: 0, userPowerTime: 0, healthtimestamp: NOW - 1800000 }], ['low_props', { freezeAmount: 3, hammerAmount: 3 }], ['low_props', { freezeAmount: 3, hammerAmount: 3, magicAmount: 4 }], ['four_failures', {}, { level: 15, failureCount: 3 }], ['four_failures', {}, { level: 16, failureCount: 4 }], ['login', {}], ]) { const user = reset(patch); await trigger(reason, extra); assert.equal(user.starter_pack, 0, reason); } }); test('level 15 is mandatory; user identity and authentication remain enforced', async () => { for (const levelAmount of [undefined, NaN, 14]) { const user = reset({ levelAmount }); await trigger(); assert.equal(user.starter_pack, 0); } reset(); assert.equal((await activity(ctx({ action: 'reactivate', reason: 'shop', token: 'wrong' }))).code, 0); assert.equal((await activity(ctx({ action: 'read', uid: 'missing' }))).code, 0); }); test('concurrent identity or resource changes cannot publish stale offers', async () => { const user = reset(); beforeUpdate = () => { user.pay_user = true; }; await trigger(); assert.equal(user.starterPackOffer.priceFen, 600); const poor = reset({ coinAmount: 499 }); beforeUpdate = () => { poor.coinAmount = 500; }; await trigger('low_coin'); assert.equal(poor.starter_pack, 0); }); test('automatic popup tracking is per round and never changes reopening time', async () => { const user = reset(); await trigger(); const expiry = user.starter_pack; await activity(ctx({ action: 'shown', expiry: expiry - 1, automatic: true })); assert.equal(user.starterPackAutoShownRound, undefined); await activity(ctx({ action: 'shown', expiry })); assert.equal(user.starterPackAutoShownRound, undefined); await activity(ctx({ action: 'shown', expiry, automatic: true })); assert.equal(user.starterPackAutoShownRound, 1); assert.equal(user.starter_pack, expiry); }); test('form-encoded failure counts and automatic flags match the client transport', async () => { const user = reset(); await trigger('four_failures', { level: '15', failureCount: '4' }); assert.equal(user.starterPackRound, 1); const expiry = String(user.starter_pack); await activity(ctx({ action: 'shown', expiry, automatic: 'false' })); assert.equal(user.starterPackAutoShownRound, undefined); await activity(ctx({ action: 'shown', expiry, automatic: 'true' })); assert.equal(user.starterPackAutoShownRound, 1); for (const failureCount of ['', '3', '4.5', 'invalid']) { const rejected = reset(); await trigger('four_failures', { level: '15', failureCount }); assert.equal(rejected.starter_pack, 0); } }); test('order snapshots bind a valid current SKU, price, reward and cycle', async () => { const user = reset({ pay_user: true }); profile(user, 3000); await trigger(); const snapshot = starterPackOrderSnapshot(user, 'starter_pack_5'); assert.equal(snapshot.starterPackOffer.priceFen, 3000); assert.equal(snapshot.starterPackOffer.reward.coin, 8000); assert.equal(snapshot.activityType, 'lucky'); assert.equal(snapshot.rewardVersion, PAYMENT_PROFILE_RULE.starterPackRewardVersion); assert.throws(() => starterPackOrderSnapshot(user, 'starter_pack'), /档位/); user.starter_packState = 1; assert.throws(() => starterPackOrderSnapshot(user, 'starter_pack_5'), /已购买/); user.starter_packState = 0; now = user.starter_pack; assert.throws(() => starterPackOrderSnapshot(user, 'starter_pack_5'), /未开放/); assert.deepEqual(starterPackOrderSnapshot(user, 'other_product'), {}); }); test('formula ignores rejection state and historical successful price', () => { const user = reset({ pay_user: true, lastStarterPackPrice: 3000, rejectionCount: 2 }); const row = profile(user, 1200); const patch = starterPackPatch(user, now, true, { reason: 'shop' }, row); assert.equal(patch.starterPackOffer.priceFen, 1200); assert.equal(patch.starterPackOffer.rewardVersion, PAYMENT_PROFILE_RULE.starterPackRewardVersion); }); test('account migration preserves the locked offer and its per-round popup record', async () => { const { normalizeWucaiUser } = await import('../functions/wucaiMigration.ts'); const user = reset(); await trigger(); user.starterPackLastShownAt = NOW; user.starterPackAutoShownRound = 1; const migrated = normalizeWucaiUser(user); assert.deepEqual(migrated.starterPackOffer, user.starterPackOffer); assert.notEqual(migrated.starterPackOffer, user.starterPackOffer); assert.equal(migrated.starterPackAutoShownRound, 1); assert.equal(migrated.starterPackLastShownAt, NOW); });