import test from 'node:test'; import assert from 'node:assert/strict'; import { registerHooks } from 'node:module'; const NOW = 1800000000000, END = NOW - 1000, PERIOD = 30 * 86400000; let now = NOW, drop = false, failFreeze = '', beforeFind = null, failArchive = false, dropArchive = false, failClear = false; let beforeArchive = null; const reads = [], scans = [], writes = []; const tables = { users: [], usersAd: [], order: [], idcount: [], passCheckJobCursors: [], passCheckSettlements: [] }; const clone = v => v == null ? v : structuredClone(v); const get = (r, k) => k.split('.').reduce((v, key) => v?.[key], r); const match = (r, q) => Object.entries(q).every(([k, v]) => { if (k === '$expr') return now < new Date(v.$lt[1]).getTime(); if (v === null) return get(r, k) == null; if (v && typeof v === 'object' && '$gt' in v) return get(r, k) > v.$gt; if (v && typeof v === 'object' && '$lt' in v) return get(r, k) < v.$lt; if (v && typeof v === 'object' && '$lte' in v) return get(r, k) <= v.$lte; return JSON.stringify(get(r, k)) === JSON.stringify(v); }); const collection = name => ({ async createIndex(keys, options) { assert.equal(name, 'passCheckSettlements'); assert.deepEqual(keys, { accountTable: 1, uid: 1, end: 1 }); assert.equal(options.unique, true); return options.name; }, async findOne(q) { reads.push({ name, q: clone(q) }); return clone(tables[name].find(r => match(r, q))); }, find(q) { let limit = Infinity; const query = { sort() { return query; }, limit(value) { limit = value; return query; }, async toArray() { scans.push({ name, q: clone(q) }); if (beforeFind) await beforeFind(name); return clone(tables[name].filter(r => match(r, q)).sort((a, b) => a._id < b._id ? -1 : a._id > b._id ? 1 : 0).slice(0, limit)); } }; return query; }, async updateOne(q, update, options = {}) { writes.push({ name, q: clone(q), update: clone(update) }); if (q._id === failFreeze && update.$set?.passSettlementPending) throw Error('freeze failed'); if (name === 'passCheckSettlements') { if (beforeArchive) await beforeArchive(); if (failArchive) throw Error('archive unavailable'); } let row = tables[name].find(r => match(r, q)); if (!row && options.upsert) { if (tables[name].some(r => r._id === q._id)) throw Object.assign(Error('duplicate key'), { code: 11000 }); row = { ...clone(q), ...clone(update.$setOnInsert || {}) }; tables[name].push(row); } if (!row) return { matchedCount: 0, modifiedCount: 0 }; if (failClear && update.$unset?.passSettlementPending !== undefined) throw Error('clear unavailable'); for (const [k, v] of Object.entries(update.$set || {})) { const keys = k.split('.'); let target = row; for (const part of keys.slice(0, -1)) target = target[part] ||= {}; target[keys.at(-1)] = clone(v); } for (const [k, v] of Object.entries(update.$inc || {})) row[k] = (row[k] || 0) + v; for (const k of Object.keys(update.$unset || {})) { const keys = k.split('.'); let target = row; for (const part of keys.slice(0, -1)) target = target?.[part]; if (target) delete target[keys.at(-1)]; } if (drop && update.$set?.timestamp) { drop = false; throw Error('lost response'); } if (dropArchive && name === 'passCheckSettlements') { dropArchive = false; throw Error('lost archive response'); } return { matchedCount: 1, modifiedCount: 1 }; } }); globalThis.__passTestCloud = { mongo: { db: { collection } }, database: () => ({ collection: name => ({ where: q => ({ getOne: async () => ({ data: await collection(name).findOne(q) }), get: async () => ({ data: clone(tables[name].filter(r => match(r, q))) }), update: async values => ({ updated: (await collection(name).updateOne(q, { $set: values })).modifiedCount }) }) }) }) }; registerHooks({ resolve(specifier, context, next) { if (specifier === '@lafjs/cloud') return { url: 'data:text/javascript,export default globalThis.__passTestCloud', shortCircuit: true }; if (specifier === '@/Utils') return { url: 'data:text/javascript,export default {checkToken:(a,b)=>a===b}', shortCircuit: true }; if (specifier.startsWith('@/')) return { url: new URL('../functions/' + specifier.slice(2) + '.ts', import.meta.url).href, shortCircuit: true }; return next(specifier, context); } }); const { default: settlement, currentPassStart, freezeUser } = await import('../functions/passCheckSettlement.ts'); const { default: upgrade, passOrderFields } = await import('../functions/passCheckUpgrade.ts'); const { default: runJob } = await import('../functions/passCheckJobs.ts'); const realNow = Date.now; Date.now = () => now; test.after(() => { Date.now = realNow; }); function reset() { now = NOW; drop = false; failFreeze = ''; beforeFind = null; failArchive = false; dropArchive = false; failClear = false; beforeArchive = null; reads.length = 0; scans.length = 0; writes.length = 0; for (const key of Object.keys(tables)) tables[key] = []; process.env.PASSCHECKTIME_ID = 'clock'; tables.idcount.push({ _id: 'clock', passcheckTime: END - PERIOD }); const stage = { time: String(END), experience: 4, free: [0, 1, 1], passCheck: [1, 1, 1], activate: false }; tables.users.push({ _id: 'u', openid: 'o', token: 't', coinAmount: 10, passCheck: JSON.stringify({ 2: stage }) }); return tables.users[0]; } const body = data => ({ body: { uid: 'u', token: 't', ...data } }); const call = data => settlement(body(data)); const archived = (end = END, uid = 'u') => tables.passCheckSettlements.find(r => r.uid === uid && r.end === end)?.row; test('offline freeze is immutable, never credits resources, and uses millisecond periods', async () => { const u = reset(); await call({ action: 'read' }); assert.equal(u.coinAmount, 10); u.passCheck = JSON.stringify({ 2: { time: END, experience: 999 } }); assert.equal((await call({ action: 'read' })).data.rows[0].stage.experience, 4); assert.equal(await currentPassStart(), END); now = END + PERIOD; assert.equal(await currentPassStart(), now); }); test('concurrent requests and a lost response cannot award twice', async () => { const u = reset(), request = { action: 'claim', end: END, rewards: [{ type: 'coin', count: 15 }] }; drop = true; const replies = await Promise.all([call(request), call(request)]); assert.equal(u.coinAmount, 25); assert.ok(replies.some(r => r.code === 1)); assert.equal((await call(request)).data.alreadyGranted, true); assert.equal(u.coinAmount, 25); }); test('offer is scoped to the rendered view and permanent abandonment revokes ordering', async () => { const u = reset(), offerToken = 'original_render_token'; assert.equal((await call({ action: 'offer_shown', end: END, offerToken })).code, 1); assert.equal((await call({ action: 'offer_shown', end: END, offerToken: 'another_render_token' })).code, 410); const order = { itemid: 'battlepass', passVersion: 2, passEnd: END, passOfferToken: offerToken }; assert.equal((await passOrderFields(u, order)).passPostSeason, true); await call({ action: 'decline', end: END }); await assert.rejects(passOrderFields(u, order)); }); test('paid duplicate gets a manual-review marker, not a second grant', async () => { const u = reset(); await call({ action: 'read' }); const order = { openid: 'o', itemid: 'battlepass', passEnd: String(END), passPostSeason: true, state: 1 }; tables.order.push({ ...order, outTradeNo: 'first' }, { ...order, outTradeNo: 'second' }); const request = { action: 'purchase_claim', end: END, rewards: [{ type: 'coin', count: 300 }] }; assert.equal((await call({ ...request, outTradeNo: 'first' })).code, 1); assert.equal((await call({ ...request, outTradeNo: 'second' })).code, 410); assert.equal(u.coinAmount, 310); assert.equal(tables.order[1].passReview, 'duplicate_post_season_payment'); }); test('late paid entitlement preserves prior claim masks and original verified XP', async () => { const u = reset(); await call({ action: 'claim', end: END, rewards: [], claimFlags: { free: [0, 0, 0], passCheck: [1, 1, 1] } }); tables.order.push({ openid: 'o', itemid: 'battlepass_30', passVersion: 2, passEnd: String(END), state: 1, outTradeNo: 'late' }); const reply = await upgrade(body({ action: 'save', outTradeNo: 'late', revision: 0, coinAmount: 3010, passCheck: JSON.stringify({ 2: { time: String(END), experience: 9999 } }), legacy: u.passCheck })); assert.equal(reply.code, 1); assert.equal(archived().stage.experience, 24); assert.deepEqual(archived().stage.free, [0, 0, 0]); assert.equal(archived().stage.tier, 30); assert.equal(archived().claimed, false); assert.equal((await upgrade(body({ action: 'order', outTradeNo: 'late' }))).data.alreadyGranted, true); }); test('unauthenticated requests and expired ordinary writes cannot alter the snapshot', async () => { const u = reset(); assert.equal((await call({ action: 'claim', end: END, token: 'wrong', rewards: [] })).code, 0); const result = await upgrade(body({ action: 'save', revision: 0, passCheck: u.passCheck, legacy: u.passCheck })); assert.equal(result.code, 410); assert.equal(archived().stage.experience, 4); }); const jobCursor = table => tables.passCheckJobCursors.find(r => r._id === 'archive:' + table); function populate(count) { const original = reset(); tables.users = Array.from({ length: count }, (_, i) => ({ ...clone(original), _id: 'u' + String(i).padStart(4, '0') })); } test('job scans only users once and idle ticks never scan users or write cursors', async () => { const u = reset(); tables.usersAd.push({ ...clone(u), _id: 'ad' }); const adBefore = clone(tables.usersAd); assert.equal((await runJob()).count, 1); assert.equal(jobCursor('users').completed, true); assert.equal(jobCursor('users').periodEnd, END); assert.ok(archived()); assert.equal(u.passSettlements, undefined); assert.equal(u.passSettlementPending, undefined); assert.equal(u.coinAmount, 10); assert.equal(jobCursor('usersAd'), undefined); assert.deepEqual(tables.usersAd, adBefore); assert.ok([...reads, ...scans, ...writes].every(r => r.name !== 'usersAd' && !['season:usersAd', 'archive:usersAd'].includes(r.q._id))); scans.length = 0; writes.length = 0; for (let i = 0; i < 5; i++) { now += 60000; assert.equal((await runJob()).count, 0); } assert.deepEqual(scans, []); assert.deepEqual(writes, []); }); test('job resumes batches without rewinding and only opens a new sweep at the next boundary', async () => { populate(201); assert.equal((await runJob()).count, 100); assert.equal(jobCursor('users').after, 'u0099'); assert.equal((await runJob()).count, 100); assert.equal(jobCursor('users').after, 'u0199'); assert.equal((await runJob()).count, 1); assert.equal(jobCursor('users').completed, true); now = END + PERIOD - 1; assert.equal((await runJob()).count, 0); now++; // Online reads can advance the global clock first; this must not hide a new season. await currentPassStart(); const u = tables.users[0]; u.passCheck = JSON.stringify({ 2: { time: String(now), experience: 8, free: [1] } }); assert.equal((await runJob()).count, 100); assert.equal(jobCursor('users').periodEnd, now); assert.equal(jobCursor('users').after, 'u0099'); assert.equal(archived(now, u._id).stage.experience, 8); assert.equal(u.coinAmount, 10); }); test('exact full batches terminate instead of restarting', async () => { populate(100); assert.equal((await runJob()).count, 100); assert.equal(jobCursor('users').completed, false); assert.equal(jobCursor('usersAd'), undefined); assert.equal((await runJob()).count, 0); assert.equal(jobCursor('users').completed, true); scans.length = 0; await runJob(); assert.deepEqual(scans, []); }); test('failed batch preserves its cursor, releases the lease and retries idempotently', async () => { populate(102); await runJob(); failFreeze = 'u0101'; await assert.rejects(runJob(), /freeze failed/); assert.equal(jobCursor('users').after, 'u0099'); assert.equal(jobCursor('users').completed, false); assert.equal(jobCursor('users').leaseUntil, 0); assert.ok(archived(END, 'u0100')); const revision = tables.users[100].passSettlementRevision; failFreeze = ''; assert.equal((await runJob()).count, 2); assert.equal(jobCursor('users').completed, true); assert.equal(tables.users[100].passSettlementRevision, revision); }); test('legacy circular cursors are preserved but never used to skip the initial backfill', async () => { reset(); tables.passCheckJobCursors.push({ _id: 'users', after: 'z' }); await runJob(); assert.ok(archived()); assert.deepEqual(tables.passCheckJobCursors.find(r => r._id === 'users'), { _id: 'users', after: 'z' }); }); test('obsolete usersAd job state is never read or updated, including across periods', async () => { const u = reset(); tables.usersAd.push({ ...clone(u), _id: 'ad' }); const oldCursor = { _id: 'season:usersAd', periodEnd: END - PERIOD, after: null, completed: false, revision: 2, leaseUntil: 0 }; tables.passCheckJobCursors.push(clone(oldCursor)); const adBefore = clone(tables.usersAd); await runJob(); await runJob(); now = END + PERIOD; await runJob(); assert.deepEqual(tables.passCheckJobCursors.find(r => r._id === oldCursor._id), oldCursor); assert.deepEqual(tables.usersAd, adBefore); assert.ok([...reads, ...scans, ...writes].every(r => r.name !== 'usersAd' && !['season:usersAd', 'archive:usersAd'].includes(r.q._id))); }); test('cross-period recovery finishes the old sweep before starting the latest period', async () => { populate(101); await runJob(); now = END + 3 * PERIOD; assert.equal((await runJob()).count, 1); assert.equal(jobCursor('users').periodEnd, END); assert.equal(jobCursor('users').completed, true); assert.equal((await runJob()).count, 100); assert.equal(jobCursor('users').periodEnd, now); }); test('overlapping workers skip leased batches and cannot overwrite newer progress after expiry', async () => { populate(101); let resume, started; const paused = new Promise(resolve => { started = resolve; }); beforeFind = async name => { if (name !== 'users') return; beforeFind = null; started(); await new Promise(resolve => { resume = resolve; }); }; const stale = runJob(); await paused; assert.equal((await runJob()).count, 0); now += 5 * 60000 + 1; assert.equal((await runJob()).count, 100); assert.equal((await runJob()).count, 1); assert.equal(jobCursor('users').completed, true); const finished = clone(jobCursor('users')); resume(); await stale; assert.deepEqual(jobCursor('users'), finished); scans.length = 0; await runJob(); assert.deepEqual(scans, []); }); test('online reads still freeze late-arriving records after the period scan is complete', async () => { reset(); tables.users = []; await runJob(); tables.users.push({ _id: 'u', token: 't', coinAmount: 10, passCheck: JSON.stringify({ 2: { time: String(END), experience: 4 } }) }); scans.length = 0; await runJob(); assert.deepEqual(scans, []); assert.equal((await call({ action: 'read' })).data.rows[0].end, END); assert.equal(tables.users[0].coinAmount, 10); }); test('a future configured start never begins a settlement scan', async () => { reset(); tables.idcount[0].passcheckTime = NOW + PERIOD; assert.equal((await runJob()).count, 0); assert.deepEqual(scans, []); assert.deepEqual(tables.passCheckJobCursors, []); }); test('many legacy seasons migrate into separate documents without losing receipts or growing users', async () => { const u = reset(); const legacy = {}; for (let i = 0; i < 40; i++) { const end = END - i * PERIOD; legacy[end] = { end, stage: { time: String(end), experience: i, free: [0], passCheck: [0], activate: true }, frozenAt: end + 1, claimed: true, emptySeen: true, offer: 'purchased', purchase: 'old-order-' + i, rewards: [{ type: 'coin', count: 10 }], purchaseRewards: [{ type: 'coin', count: 20 }] }; } u.passSettlements = clone(legacy); const result = await call({ action: 'read' }); assert.equal(result.code, 1); assert.equal(result.data.rows.length, 40); assert.equal(tables.passCheckSettlements.length, 40); for (const row of Object.values(legacy)) assert.deepEqual(archived(row.end), row); assert.equal(u.passSettlements, undefined); assert.equal(u.passSettlementPending, undefined); assert.equal(u.coinAmount, 10); const revision = u.passSettlementRevision; await call({ action: 'read' }); assert.equal(u.passSettlementRevision, revision); assert.equal(tables.passCheckSettlements.length, 40); }); test('legacy migration interruption retains a recoverable single slot and never reopens claimed rewards', async () => { const u = reset(); const row = { end: END, stage: { time: String(END), free: [0], passCheck: [0] }, claimed: true, emptySeen: true, offer: 'declined', rewards: [{ type: 'coin', count: 7 }] }; u.passSettlements = { [END]: clone(row) }; failArchive = true; assert.equal((await call({ action: 'read' })).code, 500); assert.equal(u.passSettlements, undefined); assert.deepEqual(u.passSettlementPending.row, row); assert.equal(tables.passCheckSettlements.length, 0); failArchive = false; const result = await call({ action: 'claim', end: END, rewards: [{ type: 'coin', count: 99 }] }); assert.equal(result.code, 1); assert.equal(result.data.alreadyGranted, true); assert.deepEqual(result.data.rewards, row.rewards); assert.equal(u.coinAmount, 10); assert.equal(u.passSettlementPending, undefined); assert.equal(archived().claimed, true); }); test('reward credit survives archive failure and retries cannot grant again or replace the receipt', async () => { const u = reset(); await call({ action: 'read' }); failArchive = true; const request = { action: 'claim', end: END, rewards: [{ type: 'coin', count: 15 }] }; assert.equal((await call(request)).code, 500); assert.equal(u.coinAmount, 25); assert.equal(u.passSettlementPending.row.claimed, true); assert.equal(archived().claimed, false); assert.equal((await call({ ...request, rewards: [{ type: 'coin', count: 999 }] })).code, 500); assert.equal(u.coinAmount, 25); failArchive = false; const result = await call(request); assert.equal(result.code, 1); assert.equal(result.data.alreadyGranted, true); assert.deepEqual(result.data.rewards, request.rewards); assert.equal(u.coinAmount, 25); assert.equal(u.passSettlementPending, undefined); assert.equal(u.passSettlements, undefined); }); test('lost archive responses and failed slot cleanup both recover without double credit', async () => { for (const failure of ['response', 'cleanup']) { const u = reset(); await call({ action: 'read' }); dropArchive = failure === 'response'; failClear = failure === 'cleanup'; const request = { action: 'claim', end: END, rewards: [{ type: 'coin', count: 15 }] }; assert.equal((await call(request)).code, 500); assert.equal(u.coinAmount, 25); assert.equal(archived().claimed, true); assert.ok(u.passSettlementPending); failClear = false; const result = await call(request); assert.equal(result.code, 1); assert.equal(result.data.alreadyGranted, true); assert.equal(u.coinAmount, 25); assert.equal(u.passSettlementPending, undefined); } }); test('a stale archive flusher cannot overwrite a newer row or clear its successor slot', async () => { const u = reset(); await call({ action: 'read' }); failArchive = true; await call({ action: 'claim', end: END, rewards: [{ type: 'coin', count: 15 }] }); failArchive = false; let resume, started; const paused = new Promise(resolve => { started = resolve; }); beforeArchive = async () => { beforeArchive = null; started(); await new Promise(resolve => { resume = resolve; }); }; const stale = call({ action: 'read' }); await paused; assert.equal((await call({ action: 'read' })).code, 1); failClear = true; assert.equal((await call({ action: 'decline', end: END })).code, 500); const newest = clone(u.passSettlementPending); resume(); assert.equal((await stale).code, 500); assert.deepEqual(u.passSettlementPending, newest); assert.equal(archived().offer, 'declined'); failClear = false; assert.equal((await call({ action: 'read' })).code, 1); assert.equal(archived().offer, 'declined'); assert.equal(u.passSettlementPending, undefined); assert.equal(u.coinAmount, 25); }); test('late paid upgrades recover their archive after failure without adding XP or coins twice', async () => { const u = reset(); await call({ action: 'read' }); tables.order.push({ openid: 'o', itemid: 'battlepass_30', passVersion: 2, passEnd: String(END), state: 1, outTradeNo: 'late' }); const request = body({ action: 'save', outTradeNo: 'late', revision: 0, coinAmount: 3010, passCheck: u.passCheck, legacy: u.passCheck }); failArchive = true; await assert.rejects(upgrade(request), /archive unavailable/); assert.equal(u.coinAmount, 3010); assert.equal(u.passSettlementPending.row.stage.experience, 24); failArchive = false; assert.equal((await upgrade(request)).data.alreadyGranted, true); assert.equal(archived().stage.experience, 24); assert.equal(archived().stage.tier, 30); assert.equal(u.coinAmount, 3010); assert.equal(u.passSettlements, undefined); assert.equal(u.passSettlementPending, undefined); }); test('post-season paid rewards recover after archive failure without a second grant', async () => { const u = reset(); await call({ action: 'read' }); tables.order.push({ openid: 'o', itemid: 'battlepass', passEnd: String(END), passPostSeason: true, state: 1, outTradeNo: 'paid' }); const request = { action: 'purchase_claim', end: END, outTradeNo: 'paid', rewards: [{ type: 'coin', count: 300 }] }; failArchive = true; assert.equal((await call(request)).code, 500); assert.equal(u.coinAmount, 310); failArchive = false; const result = await call(request); assert.equal(result.data.alreadyGranted, true); assert.equal(tables.order[0].state, 2); assert.equal(archived().purchase, 'paid'); assert.equal(u.coinAmount, 310); }); test('independent snapshots isolate users and concurrent freezes create only one record per season', async () => { const u = reset(); const other = { ...clone(u), _id: 'other' }; tables.users.push(other); await Promise.all([freezeUser(u), freezeUser(u), freezeUser(other)]); assert.equal(tables.passCheckSettlements.length, 2); await call({ action: 'claim', end: END, rewards: [{ type: 'coin', count: 15 }] }); assert.equal(archived().claimed, true); assert.equal(archived(END, 'other').claimed, false); assert.equal(other.coinAmount, 10); }); test('the new job cursor migrates users even when the old period scan already completed', async () => { const u = reset(); u.passSettlements = { [END]: { end: END, stage: { time: String(END), experience: 4 }, claimed: true, offer: 'declined' } }; const old = { _id: 'season:users', periodEnd: END, completed: true, after: 'z', revision: 2, leaseUntil: 0 }; tables.passCheckJobCursors.push(clone(old)); assert.equal((await runJob()).count, 1); assert.deepEqual(tables.passCheckJobCursors.find(r => r._id === 'season:users'), old); assert.equal(archived().claimed, true); assert.equal(u.passSettlements, undefined); scans.length = 0; assert.equal((await runJob()).count, 0); assert.deepEqual(scans, []); }); test('conflicting legacy and archive records stop migration without deleting either copy', async () => { const u = reset(); await call({ action: 'read' }); const original = clone(archived()); u.passSettlements = { [END]: { ...clone(original), claimed: true } }; assert.equal((await call({ action: 'read' })).code, 409); assert.equal(u.passSettlements[END].claimed, true); assert.deepEqual(archived(), original); }); test('in-season and unrelated order metadata remain compatible with async archive lookup', async () => { const u = reset(); u.passCheck = JSON.stringify({ 2: { time: String(NOW + PERIOD), activate: false } }); assert.deepEqual(await passOrderFields(u, { itemid: 'new_gold_1' }), {}); assert.deepEqual(await passOrderFields(u, { itemid: 'battlepass' }), {}); assert.deepEqual(await passOrderFields(u, { itemid: 'battlepass', passVersion: 2, passEnd: NOW + PERIOD }), { passVersion: 2, passEnd: String(NOW + PERIOD) }); await assert.rejects(passOrderFields(u, { itemid: 'battlepass_12', passVersion: 2, passEnd: NOW + PERIOD })); assert.deepEqual(tables.passCheckSettlements, []); });