server/laf-cloud/functions/goldMiner/tests/gold-miner-postman.test.mjs
2026-09-24 17:59:52 +08:00

427 lines
26 KiB
JavaScript

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://<APPID>.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}));
});