import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { Script, createContext } from 'node:vm'; const read = name => JSON.parse(readFileSync(new URL(`../postman/${name}`, import.meta.url), 'utf8')); const collection = read('gold-miner.postman_collection.json'); const template = read('gold-miner.postman_environment.json'); const requests = collection.item.flatMap(folder => folder.item); const request = (folder, prefix) => { const value = collection.item[folder - 1].item.find(item => item.name.startsWith(prefix)); assert.ok(value, `${folder}: ${prefix}`); return value; }; const script = (item, listen) => item.event.find(event => event.listen === listen)?.script.exec.join('\n') || ''; const copy = value => value === undefined ? value : JSON.parse(JSON.stringify(value)); const SKIP = Symbol('skip request'); // Executes the exported scripts without HTTP, Postman packages or real credentials. function sandbox(values = {}) { const env = new Map(template.values.map(({ key, value }) => [key, value])); Object.entries({ baseUrl: 'https://test.invalid', uid: 'test-user', token: 'test-token', ...values }) .forEach(([key, value]) => env.set(key, String(value))); let local = new Map(), counter = 0, response = {}, responseStatus = 200, failures = []; const replace = value => value.replace(/\{\{([^}]+)\}\}/g, (_, key) => { if (key === '$guid') return `00000000-0000-4000-8000-${String(++counter).padStart(12, '0')}`; assert.ok(local.has(key) || env.has(key), `unresolved variable: ${key}`); return local.has(key) ? local.get(key) : env.get(key); }); const pm = { environment: { get: key => env.get(key), set: (key, value) => env.set(key, String(value)), unset: key => env.delete(key) }, variables: { get: key => local.get(key) ?? env.get(key), set: (key, value) => local.set(key, String(value)), replaceIn: replace }, execution: { skipRequest() { throw SKIP; } }, response: { json: () => copy(response), to: { have: { status: status => assert.equal(status, responseStatus) } } }, expect: actual => ({ to: { eql: expected => assert.deepEqual(copy(actual), copy(expected)) } }), test(name, callback) { try { callback(); } catch (error) { failures.push(new Error(`${name}: ${error.message}`)); } }, }; function run(code) { new Script(code).runInContext(createContext({ pm, console: { error() { } } })); } return { env, pre(item) { local = new Map(); try { run(script(collection, 'prerequest')); run(script(item, 'prerequest')); return { body: item.request.body ? JSON.parse(replace(item.request.body.raw)) : undefined }; } catch (error) { if (error === SKIP) return { skipped: true }; throw error; } }, post(item, payload, status = 200) { response = payload; responseStatus = status; failures = []; run(script(item, 'test')); if (failures.length) throw new AggregateError(failures, failures.map(error => error.message).join('\n')); }, }; } const periodId = 'goldMiner:2026-09-17'; const startsAt = Date.parse('2026-09-17T00:00:00+08:00'); function info(overrides = {}) { const d = { status: 'unlocked', periodId, startsAt, endsAt: startsAt + 4 * 86400000, serverTime: startsAt + 1000, productId: 'gold_miner', priceFen: 123, progressWins: 5, maxTarget: 5, ...overrides, }; d.tasks = [3, 5].map((targetWins, index) => ({ taskId: `task_${index + 1}`, targetWins, itemsSnapshot: [{ type: 'coin', count: 50 }], progress: Math.min(d.progressWins, targetWins), completed: d.progressWins >= targetWins, claimStatus: d.progressWins >= targetWins ? d.status === 'unlocked' ? 'claimable' : 'pending_unlock' : 'locked', })); return d; } const success = data => ({ code: 1, data, msg: '成功' }); const grant = () => ({ grantId: 'grant-1', taskId: 'task_1', items: [{ type: 'coin', count: 50 }], claimStatus: 'issuing', claimedThrough: 0, requiresClientDelivery: true }); test('collection, environment and every script are well formed', () => { assert.equal(collection.info.schema, 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'); assert.equal(requests.length, 48); assert.equal(new Set(template.values.map(value => value.key)).size, template.values.length); for (const key of ['token', 'adminToken', 'gmWinBody', 'legacySessionFrom']) { const value = template.values.find(value => value.key === key); assert.equal(value.type, 'secret'); assert.equal(value.value, ''); } for (const item of [collection, ...requests]) { for (const event of item.event) new Script(event.script.exec.join('\n'), { filename: `${item.name || 'collection'}:${event.listen}` }); } for (const item of requests) { assert.ok(['POST', 'GET'].includes(item.request.method)); if (item.request.method === 'POST') { assert.equal(item.request.body.mode, 'raw'); assert.equal(item.request.header[0].value, 'application/json'); } else { assert.equal(item.request.url.raw, '{{baseUrl}}/activityConfig/list'); assert.equal(item.request.body, undefined); } assert.equal(item.request.url.raw, `{{baseUrl}}/${item.request.url.path.join('/')}`); const functionName = item.request.url.path.join('/'); assert.ok(readFileSync(new URL(`../../${functionName}.ts`, import.meta.url), 'utf8').length > 0); if (functionName.startsWith('goldMiner')) { assert.ok(['goldMiner/index', 'goldMiner/admin', 'goldMiner/merchantNotify'].includes(functionName), 'activity requests must use the current public routes'); const yaml = readFileSync(new URL(`../../${functionName}.yaml`, import.meta.url), 'utf8'); assert.match(yaml, /^name: (.+)$/m); assert.equal(yaml.match(/^name: (.+)$/m)[1].trim(), functionName); assert.match(yaml, /^\s+- POST\s*$/m); } assert.ok(script(item, 'test').includes('pm.test(')); if (item.request.body && !item.request.body.raw.startsWith('{{')) JSON.parse(item.request.body.raw); } }); test('unconfigured application is skipped without producing a request', () => { const s = sandbox({ baseUrl: 'https://.laf.run' }); assert.deepEqual(s.pre(request(1, '01')), { skipped: true }); }); test('changing account clears grants, progress payload and selected targets', () => { const s = sandbox(); s.pre(request(1, '01')); for (const key of ['gmWinBody', 'gmGrant', 'grantId', 'gmSavedGrantId', 'claimPeriodId', 'claimTaskId', 'outTradeNo']) s.env.set(key, 'stale'); s.env.set('uid', 'another-user'); assert.deepEqual(s.pre(request(1, '01')), { skipped: true }); for (const key of ['gmWinBody', 'gmGrant', 'grantId', 'gmSavedGrantId', 'claimPeriodId', 'claimTaskId', 'outTradeNo']) assert.equal(s.env.get(key), undefined); assert.equal(s.pre(request(1, '01')).body.uid, 'another-user'); }); test('active info enforces shared progress and independent task claimability', () => { const s = sandbox(); const item = request(1, '01'); s.post(item, success(info())); assert.equal(s.env.get('periodId'), periodId); assert.equal(s.env.get('secondTaskId'), 'task_2'); const broken = info(); broken.tasks[1].claimStatus = 'locked'; assert.throws(() => s.post(item, success(broken)), /活动状态结构/); }); test('locked and unavailable responses accept missing task/product fields', () => { const s = sandbox(); const item = request(1, '01'); s.post(item, success(info())); s.post(item, success({ status: 'locked', serverTime: startsAt, periodId, tasks: [] })); assert.equal(s.env.get('productId'), undefined); assert.equal(s.env.get('periodId'), periodId); s.post(item, success({ status: 'unavailable', serverTime: startsAt, tasks: [] })); assert.equal(s.env.get('periodId'), undefined); }); test('carry opening assertions check unlocked status and reset progress without exposing source', () => { const s = sandbox({ expectedStatus: 'unlocked', expectedProgressWins: '0' }); const d = info({ progressWins: 0 }); s.post(request(1, '01'), success(d)); d.status = 'purchasable'; assert.throws(() => s.post(request(1, '01'), success(d)), /活动状态结构/); }); test('info scripts reject leaked fields and preserve issuing as the only pending task', () => { const s = sandbox(); const item = request(1, '01'); const d = info(); d.tasks[0].claimStatus = 'issuing'; s.post(item, success(d)); d.tasks[0].grantId = 'internal'; assert.throws(() => s.post(item, success(d)), /活动状态结构/); delete d.tasks[0].grantId; d.entitlement = { status: 'unlocked' }; assert.throws(() => s.post(item, success(d)), /活动状态结构/); }); test('purchase disabled info permits progress but blocks native and customer preorders', () => { const s = sandbox({ currentMainLevel: 41, mainLevelAfterWin: 42 }); s.post(request(1, '01'), success(info({ status: 'purchase_disabled', progressWins: 0 }))); assert.equal(s.pre(request(2, '01')).body.levelAmount, 42); assert.deepEqual(s.pre(request(3, '01')), { skipped: true }); assert.deepEqual(s.pre(request(7, '01')), { skipped: true }); }); test('new win creates one payload, retry reuses it even after current period changes', () => { const s = sandbox({ periodId, currentMainLevel: 41, mainLevelAfterWin: 42, gmInfo: JSON.stringify(info({ progressWins: 0 })) }); const first = s.pre(request(2, '01')).body; assert.equal(first.levelAmount, 42); assert.equal(first.goldMiner.clearedMainLevel, 42); s.post(request(2, '01'), success({ levelAmount: 42, goldMiner: { code: 1, activityCounted: true, duplicate: false, progressWins: 1 } })); s.env.set('periodId', 'goldMiner:2026-09-24'); s.env.set('token', 'renewed-token'); const retried = s.pre(request(2, '03')).body; assert.deepEqual(retried.goldMiner, first.goldMiner); assert.equal(retried.token, 'renewed-token'); s.post(request(2, '03'), success({ goldMiner: { code: 1, activityCounted: false, duplicate: true, progressWins: 1 } })); const conflict = s.pre(request(2, '04')).body; assert.equal(conflict.goldMiner.eventId, first.goldMiner.eventId); assert.equal(conflict.levelAmount, first.levelAmount); assert.equal(conflict.goldMiner.clearedMainLevel, 43); assert.deepEqual(JSON.parse(s.env.get('gmWinBody')), first); }); test('progress tests inspect nested failure, not only successful save envelope', () => { const s = sandbox({ gmWinBefore: 0 }); assert.throws(() => s.post(request(2, '01'), success({ goldMiner: { code: 0, errorCode: 'PERIOD_ENDED' } })), /活动子结果与计数/); s.post(request(2, '05'), success({ goldMiner: { code: 0, errorCode: 'PERIOD_ENDED' } })); assert.throws(() => s.post(request(2, '05'), success({ goldMiner: { code: 1, progressWins: 1 } })), /不补记过期进度/); }); test('qualification and capped events assert no progress, endless preserves main save', () => { const s = sandbox({ periodId, currentMainLevel: 40, mainLevelAfterWin: 41, expectedProgressResult: 'qualification', gmInfo: JSON.stringify({ status: 'locked', periodId }) }); assert.equal(s.pre(request(2, '01')).body.levelAmount, 41); s.post(request(2, '01'), success({ levelAmount: 41, goldMiner: { code: 1, activityCounted: false, reason: 'qualification_level', progressWins: 0 } })); s.env.set('expectedProgressResult', 'capped'); s.env.set('endlessSequence', '999'); s.env.set('gmInfo', JSON.stringify(info())); const endless = s.pre(request(2, '02')).body; assert.equal(endless.isWuXian, 'true'); assert.equal(endless.levelAmount, 41); assert.equal(endless.goldMiner.endlessSequence, 999); s.post(request(2, '02'), success({ levelAmount: 41, goldMiner: { code: 1, activityCounted: false, duplicate: false, progressWins: 5 } })); s.env.set('mainLevelAfterWin', '42'); assert.deepEqual(s.pre(request(2, '02')), { skipped: true }); }); test('order creation captures snapshot pricing and reuses original order identity', () => { const s = sandbox(); s.post(request(1, '01'), success(info({ status: 'purchasable' }))); const first = s.pre(request(3, '01')).body; assert.equal(first.itemPrice, 1); assert.equal(first.itemCount, 999); s.post(request(3, '01'), success({ outTradeNo: 'order-1', signData: JSON.stringify({ productId: 'gold_miner', buyQuantity: 1, goodsPrice: 123, outTradeNo: 'order-1' }), signature: 'signature', paySig: 'paySig' })); const retry = s.pre(request(3, '03')).body; assert.notEqual(first.createRequestId, retry.createRequestId); s.post(request(3, '03'), success({ outTradeNo: 'order-1' })); assert.throws(() => s.post(request(3, '03'), success({ outTradeNo: 'order-2' })), /复用原订单/); }); test('payment assertions distinguish pending, paid and late-payment routes', () => { const s = sandbox({ outTradeNo: 'order-1' }); const items = collection.item[2].item.filter(item => item.name.startsWith('查单')); for (const item of items) { assert.equal(s.pre(item).body.outTradeNo, 'order-1'); s.post(item, { code: 0, errorCode: 'PAYMENT_PENDING' }); assert.throws(() => s.post(item, success({ pay_state: 2 })), /查单匹配指定状态/); } s.env.set('expectedPaymentState', 'paid'); assert.deepEqual(s.pre(items[0]), { skipped: true }); s.env.set('expectedTargetPeriodId', 'goldMiner:2026-09-24'); s.env.set('expectedFulfillmentRoute', 'carry_next'); const paid = success({ pay_state: 2, rewardDelivery: 'goldMiner.claim', goldMiner: { fulfillmentStatus: 'fulfilled', fulfillmentRoute: 'carry_next', targetPeriodId: 'goldMiner:2026-09-24' } }); for (const item of items) s.post(item, paid); paid.data.goldMiner.targetPeriodId = periodId; assert.throws(() => s.post(items[0], paid), /查单匹配指定状态/); }); test('claim-save-confirm chain forbids premature confirmation and duplicate credit', () => { const s = sandbox({ claimPeriodId: periodId, claimTaskId: 'task_1' }); s.pre(request(4, '01')); s.post(request(4, '01'), success(grant())); assert.deepEqual(s.pre(request(4, '05')), { skipped: true }); s.post(request(4, '02'), success(grant())); assert.throws(() => s.post(request(4, '02'), success({ ...grant(), grantId: 'grant-2' })), /重复领取无新奖励凭证/); s.post(request(4, '03'), success({ coinAmount: 100 })); assert.deepEqual(s.pre(request(4, '04')), { skipped: true }); s.env.set('coinAfterDelivery', '149'); assert.deepEqual(s.pre(request(4, '04')), { skipped: true }); s.env.set('coinAfterDelivery', '150'); assert.equal(s.pre(request(4, '04')).body.coinAmount, '150'); s.post(request(4, '04'), success({ coinAmount: 150 })); assert.deepEqual(s.pre(request(4, '04')), { skipped: true }); assert.equal(s.pre(request(4, '05')).body.grantId, 'grant-1'); s.post(request(4, '05'), success({ grantId: 'grant-1', claimStatus: 'claimed', claimedThrough: 1 })); s.pre(request(4, '05')); s.post(request(4, '05'), success({ grantId: 'grant-1', claimStatus: 'claimed', claimedThrough: 1 })); assert.throws(() => s.post(request(4, '05'), success({ grantId: 'wrong-grant', claimStatus: 'claimed', claimedThrough: 2 })), /确认同一凭证/); assert.deepEqual(s.pre(request(4, '04')), { skipped: true }); }); test('failed coin save cannot enable delivery confirmation', () => { const s = sandbox({ claimPeriodId: periodId, claimTaskId: 'task_1' }); s.pre(request(4, '01')); s.post(request(4, '01'), success(grant())); assert.throws(() => s.post(request(4, '04'), { code: 0, data: null }), /业务成功/); assert.equal(s.env.get('gmSavedGrantId'), undefined); assert.deepEqual(s.pre(request(4, '05')), { skipped: true }); }); test('selected historical task cannot use another task grant', () => { const s = sandbox({ claimPeriodId: periodId, claimTaskId: 'task_1' }); s.pre(request(4, '01')); s.post(request(4, '01'), success(grant())); s.env.set('claimTaskId', 'task_2'); assert.deepEqual(s.pre(request(4, '04')), { skipped: true }); assert.deepEqual(s.pre(request(4, '05')), { skipped: true }); }); test('empty settlement page retains cursor; terminal page blocks further requests', () => { const s = sandbox(); s.post(request(5, '01'), success({ status: 'pending_delivery', items: [], nextCursor: 'history-cursor' })); assert.equal(s.pre(request(5, '02')).body.afterId, 'history-cursor'); s.post(request(5, '02'), success({ status: 'pending_delivery', items: [], nextCursor: null })); assert.deepEqual(s.pre(request(5, '02')), { skipped: true }); }); test('admin publication blocks blank commercial values; purchase flag is a JSON boolean', () => { const s = sandbox({ adminToken: 'test-admin', adminPeriodId: periodId }); assert.deepEqual(s.pre(request(6, '02')), { skipped: true }); s.env.set('configJson', JSON.stringify({ priceFen: null, tasks: [] })); assert.deepEqual(s.pre(request(6, '02')), { skipped: true }); s.env.set('configJson', JSON.stringify({ configVersion: 'test-v1', priceFen: 123, tasks: [{ taskId: 'test' }] })); assert.equal(s.pre(request(6, '02')).body.config.priceFen, 123); assert.deepEqual(s.pre(request(6, '03')), { skipped: true }); s.env.set('purchaseEnabled', 'false'); assert.equal(s.pre(request(6, '03')).body.purchaseEnabled, false); }); test('legacy collection preserves scoped session ticket and gates real customer messages', () => { const s = sandbox({ openid: 'openid' }); s.pre(request(1, '01')); s.post(request(1, '01'), success(info({ status: 'purchasable' }))); assert.equal(s.pre(request(7, '01')).body.channel, 'legacy_ios'); const data = { outTradeNo: 'order-1', periodId, priceFen: 123, productId: 'gold_miner', quantity: 1, channel: 'legacy_ios', sessionFrom: JSON.stringify({ outTradeNo: 'order-1', propName: 'gold_miner', goldMinerTicket: 'scoped-ticket' }) }; s.post(request(7, '01'), success(data)); s.pre(request(7, '02')); s.post(request(7, '02'), success(data)); assert.throws(() => s.post(request(7, '02'), success({ ...data, outTradeNo: 'different' })), /复用原订单/); assert.deepEqual(s.pre(request(7, '03')), { skipped: true }); s.env.set('allowCustomerMessage', 'true'); const sent = s.pre(request(7, '03')).body; assert.equal(sent.SessionFrom, data.sessionFrom); assert.equal(sent.FromUserName, 'openid'); assert.equal(sent.token, undefined); s.post(request(7, '04'), true); assert.throws(() => s.post(request(7, '04'), { code: 0 })); s.post(request(7, '05'), false); s.post(request(7, '06'), { code: 'FAIL' }, 500); s.post(request(7, '07'), { code: 0, errorCode: 'PAYMENT_CHANNEL_CONFLICT' }); s.env.set('uid', 'other'); assert.deepEqual(s.pre(request(1, '01')), { skipped: true }); assert.equal(s.env.get('legacySessionFrom'), undefined); assert.equal(s.env.get('allowCustomerMessage'), undefined); }); test('settling pages advance the cursor and settlement delivery uses original grant without claim', () => { const s = sandbox({ settlementCursor: 'history-cursor' }); s.post(request(5, '02'), success({ status: 'settling', items: [], nextCursor: 'next-history-cursor' })); assert.equal(s.pre(request(5, '02')).body.afterId, 'next-history-cursor'); s.post(request(5, '02'), success({ status: 'settling', items: [], nextCursor: null })); assert.deepEqual(s.pre(request(5, '02')), { skipped: true }); assert.equal(s.pre(request(5, '01')).body.afterId, undefined); s.post(request(5, '05'), success({ status: 'pending_delivery', items: [{ settlementId: 'settlement-1', periodId, rewards: [{ taskId: 'task_1', grantId: 'grant-1', items: [{ type: 'coin', count: 50 }] }] }], nextCursor: null })); assert.equal(s.env.get('grantId'), 'grant-1'); assert.deepEqual(s.pre(request(5, '06')), { skipped: true }); s.post(request(4, '03'), success({ coinAmount: 100 })); s.env.set('coinAfterDelivery', '150'); assert.equal(s.pre(request(4, '04')).body.coinAmount, '150'); s.post(request(4, '04'), success({ coinAmount: 150 })); const body = s.pre(request(5, '06')).body; assert.equal(body.action, 'confirm_settlement_delivery'); assert.deepEqual(body.grantIds, ['grant-1']); assert.equal(body.settlementId, 'settlement-1'); s.post(request(5, '06'), success({ settlementId: 'settlement-1', confirmedGrantIds: ['grant-1'], deliveryStatus: 'client_saved' })); assert.deepEqual(s.pre(request(4, '04')), { skipped: true }); }); test('settlement collection omits client limits and accepts only globally empty no_pending_rewards responses', () => { const s = sandbox(); for (const prefix of ['01', '05']) { assert.equal(s.pre(request(5, prefix)).body.limit, undefined); s.post(request(5, prefix), success({ status: 'no_pending_rewards', items: [], nextCursor: null })); assert.equal(s.env.get('settlementCursor'), ''); assert.throws(() => s.post(request(5, prefix), success({ status: 'no_pending_rewards', items: [], nextCursor: 'next' }))); assert.throws(() => s.post(request(5, prefix), success({ status: 'no_pending_rewards', items: [{}], nextCursor: null }))); } assert.deepEqual(s.pre(request(5, '02')), { skipped: true }); const negative = s.pre(request(1, '06')).body; assert.equal(negative.limit, undefined); assert.equal(negative.afterId, 42); }); test('temporary info accepts custom duration while weekly info still enforces the normal calendar', () => { const s = sandbox(), d = info({ periodId: 'goldMiner:test:short', endsAt: startsAt + 1800000 }); s.post(request(1, '01'), success(d)); assert.equal(s.env.get('periodId'), d.periodId); assert.throws(() => s.post(request(1, '01'), success({ ...d, periodId })), /活动状态结构/); assert.throws(() => s.post(request(1, '01'), success({ ...d, serverTime: d.endsAt })), /活动状态结构/); }); const schedule = (activities = []) => ({ schemaVersion: 1, serverNow: startsAt + 1000, windowEndsAt: startsAt + 1000 + 7 * 86400000, refreshAt: Math.min(startsAt + 31000, ...activities.flatMap(a => [a.startsAt, a.endsAt]).filter(t => t > startsAt + 1000)), activities }); const scheduleEntry = (extra = {}) => ({ activityId: 'goldMiner', periodId: 'goldMiner:test:short', configVersion: 'v1', startsAt, endsAt: startsAt + 10000, unlockLevel: 41, phase: 'active', purchaseEnabled: true, ...extra }); test('public schedule GET and POST require no login and do not overwrite player period or grant', () => { const s = sandbox({ uid: '', token: '', adminToken: '', periodId: 'player-period', grantId: 'original-grant' }); assert.deepEqual(s.pre(request(8, '01')), { body: undefined }); assert.deepEqual(s.pre(request(8, '02')), { body: {} }); for (const item of collection.item[7].item) { s.post(item, success(schedule([scheduleEntry()]))); assert.equal(s.env.get('periodId'), 'player-period'); assert.equal(s.env.get('grantId'), 'original-grant'); assert.equal(JSON.parse(s.env.get('gmActivitySchedule')).activities[0].periodId, 'goldMiner:test:short'); } const yaml = readFileSync(new URL('../../activityConfig/list.yaml', import.meta.url), 'utf8'); assert.match(yaml, /^\s+- GET\s*$/m); assert.match(yaml, /^\s+- POST\s*$/m); }); test('schedule accepts empty lists and cloudRise upcoming entries; errors preserve cached success', () => { const s = sandbox(), item = request(8, '01'); s.post(item, success(schedule())); assert.deepEqual(JSON.parse(s.env.get('gmActivitySchedule')).activities, []); const cloud = { activityId: 'cloudRise', periodId: 'race', configVersion: 'v1', startsAt: startsAt + 5000, endsAt: startsAt + 60000, unlockLevel: 100, phase: 'upcoming' }; const d = schedule([cloud, scheduleEntry()]); s.post(item, success(d)); const cached = s.env.get('gmActivitySchedule'); assert.throws(() => s.post(item, { code: 0, data: null, msg: '数据库暂不可用' }), /公共日程查询成功/); assert.equal(s.env.get('gmActivitySchedule'), cached); assert.throws(() => s.post(item, success({ ...d, refreshAt: d.serverNow + 30000 })), /公共日程字段/); assert.equal(s.env.get('gmActivitySchedule'), cached); assert.throws(() => s.post(item, success(schedule([scheduleEntry({ tasks: [] })]))), /公共日程字段/); assert.throws(() => s.post(item, success(schedule([scheduleEntry(), cloud]))), /公共日程字段/); s.env.set('expectedListedPeriodId', 'missing'); assert.throws(() => s.post(item, success(d)), /公共日程字段/); s.env.set('expectedListedPeriodId', 'goldMiner:test:short'); s.post(item, success(d)); }); test('integrated temporary publisher reuses the same draft and validates immutable response', () => { const s = sandbox({ adminToken: 'admin-test' }), item = request(6, '04'); const first = s.pre(item).body; assert.equal(first.action, 'publish_test_period'); assert.equal(first.endsAt - first.startsAt, 1800000); assert.match(first.periodId, /^goldMiner:test:/); assert.deepEqual(s.pre(item).body, first); assert.equal(s.env.get('tempPeriodId'), first.periodId); assert.equal(s.env.get('testPeriodRequestBody'), undefined); const result = { periodId: first.periodId, startsAt: first.startsAt, endsAt: first.endsAt, nextStartsAt: first.endsAt + 1000, configHash: 'hash', configSnapshot: { ...first.config, effectiveFromPeriodId: first.periodId } }; s.post(item, success(result)); assert.throws(() => s.post(item, success({ ...result, endsAt: first.endsAt + 1 })), /时间与请求一致/); const standalone = read('gold-miner-test-period.postman_collection.json').item[0]; assert.equal(script(item, 'prerequest'), script(standalone, 'prerequest')); assert.equal(script(item, 'test'), script(standalone, 'test')); }); test('public cached schedule is cleared when switching deployment context', () => { const s = sandbox(); s.pre(request(8, '01')); s.post(request(8, '01'), success(schedule())); s.env.set('baseUrl', 'https://another.invalid'); assert.deepEqual(s.pre(request(8, '01')), { skipped: true }); assert.equal(s.env.get('gmActivitySchedule'), undefined); assert.deepEqual(s.pre(request(8, '01')), { body: undefined }); }); test('VIP Postman publication and pending-unlock display match the six-tier protocol', () => { const config = JSON.parse(readFileSync(new URL('../vip-tiers.example.json', import.meta.url), 'utf8')).config; const s = sandbox({ adminToken: 'admin-test', configJson: JSON.stringify(config) }); assert.deepEqual(s.pre(request(6, '02')).body.config, config); s.post(request(6, '02'), success(config)); const unpaid = info({ status: 'purchasable', vipLevel: 0, priceFen: 300, productId: 'gold_miner_vip0' }); s.post(request(1, '01'), success(unpaid)); assert.ok(unpaid.tasks.every(t => t.claimStatus === 'pending_unlock')); assert.equal(s.env.get('productId'), 'gold_miner_vip0'); const broken = copy(unpaid); broken.tasks[0].claimStatus = 'claimable'; assert.throws(() => s.post(request(1, '01'), success(broken)), /活动状态结构/); // Independent tasks can all be ready, including with progress beyond the selected tier's cap. s.post(request(1, '01'), success(info({ vipLevel: 1, progressWins: 61 }))); s.env.set('claimPeriodId', periodId); s.env.set('claimTaskId', 'task_2'); s.pre(request(4, '08')); s.post(request(4, '08'), success({ ...grant(), taskId: 'task_2' })); assert.equal(s.env.get('claimTaskId'), 'task_2'); s.post(request(4, '05'), success({ grantId: 'grant-1', claimStatus: 'claimed', claimedThrough: 0 })); });