import { describe, expect, test } from "vitest"; import { applyDynamicStrategyDefaults, configToForm, formToConfig, validateLuckyGiftForm } from "./configForm.js"; describe("ops center lucky gift dynamic_v3 form", () => { test("loads safe dynamic defaults without inventing app-specific monetary caps", () => { const form = configToForm({ app_code: "lalu", pool_id: "default", strategy_version: "dynamic_v3" }); expect(form).toMatchObject({ anchor_rate_percent: "1", enabled: false, initial_pool_coins: "0", jackpot_multipliers: "200, 500, 1000", max_single_payout: "0", pool_rate_percent: "98", profit_rate_percent: "1", strategy_version: "dynamic_v3", target_rtp_percent: "98", }); expect(form.stages.map((stage) => [stage.min_recharge_7d_coins, stage.min_recharge_30d_coins])).toEqual([ ["0", "0"], ["0", "1"], ["1", "1"], ]); expect(form.stages.map((stage) => stage.tiers.map((tier) => Number(tier.probabilityPercent)))).toEqual([ [5, 4, 86, 5], [5, 4, 86, 5], [5, 4, 86, 5], ]); }); test("round-trips every dynamic strategy field and canonical stage keys", () => { const config = completeDynamicConfig(); // 使用空原对象可证明每个字段都真正经过 formToConfig 写回;若沿用 config 作为 spread 基底, // 漏掉的动态字段也会被旧值掩盖,测试无法发现发布 payload 的字段丢失。 const output = formToConfig({}, configToForm(config)); expect(output).toEqual(config); expect(output.stages[1]).toMatchObject({ min_recharge_30d_coins: 20_000, min_recharge_7d_coins: 10_000, stage: "normal", }); expect(output.stages[1].tiers[0]).toEqual({ enabled: true, high_water_only: false, multiplier: 0, probability_percent: 5, tier_id: "normal_none", }); }); test("drops legacy dynamic initial seed when loading and publishing a new version", () => { const legacy = completeDynamicConfig(); legacy.initial_pool_coins = 1_000_000; const form = configToForm(legacy); expect(form.initial_pool_coins).toBe("0"); expect(formToConfig(legacy, form).initial_pool_coins).toBe(0); }); test("allows disabled drafts but blocks enabled dynamic rules until app monetary limits are filled", () => { const draft = configToForm({ app_code: "lalu", pool_id: "default", strategy_version: "dynamic_v3" }); expect(validateLuckyGiftForm(draft)).toEqual([]); const enabled = { ...draft, enabled: true }; const errors = validateLuckyGiftForm(enabled); expect(errors).toContain("启用前必须填写 50 美元等值的大奖消费门槛"); expect(errors).toContain("启用前必须填写单次返奖上限"); expect(errors).toContain("启用前必须填写主播每日上限"); }); test("keeps fixed_v2 readable and does not apply dynamic-only validation", () => { const form = configToForm({ app_code: "yumi", control_band_percent: 1, enabled: true, gift_price_reference: 100, normal_max_equivalent_draws: 20_000, novice_max_equivalent_draws: 2_000, pool_id: "legacy", pool_rate_percent: 96, settlement_window_wager: 1_000_000, stages: fixedStages(), strategy_version: "fixed_v2", target_rtp_percent: 95, }); expect(form.strategy_version).toBe("fixed_v2"); expect(validateLuckyGiftForm(form)).toEqual([]); }); test("switching a legacy form to dynamic creates a disabled safe draft", () => { const legacy = configToForm({ app_code: "yumi", control_band_percent: 1, enabled: true, gift_price_reference: 100, normal_max_equivalent_draws: 20_000, novice_max_equivalent_draws: 2_000, pool_id: "legacy", pool_rate_percent: 96, settlement_window_wager: 1_000_000, stages: fixedStages(), strategy_version: "fixed_v2", target_rtp_percent: 95, }); const dynamic = applyDynamicStrategyDefaults(legacy); expect(dynamic).toMatchObject({ enabled: false, initial_pool_coins: "0", pool_rate_percent: "98", strategy_version: "dynamic_v3", }); expect(dynamic.stages.map((stage) => stage.tiers.map((tier) => Number(tier.multiplier)))).toEqual([ [0, 0.5, 1, 2], [0, 0.5, 1, 2], [0, 0.5, 1, 2], ]); expect(dynamic.stages.map((stage) => stage.tiers.map((tier) => Number(tier.probabilityPercent)))).toEqual([ [5, 4, 86, 5], [5, 4, 86, 5], [5, 4, 86, 5], ]); expect(validateLuckyGiftForm(dynamic)).toEqual([]); }); test("rejects invalid disabled draft values that the owner never accepts", () => { const draft = configToForm({ app_code: "lalu", pool_id: "default", strategy_version: "dynamic_v3" }); const errors = validateLuckyGiftForm({ ...draft, jackpot_spend_threshold_coins: "-1", max_single_payout: "-1", profit_rate_percent: "-1", anchor_rate_percent: "2", stages: draft.stages.map((stage) => stage.stage === "normal" ? { ...stage, min_recharge_7d_coins: "-1" } : stage, ), }); expect(errors).toContain("平台盈利和主播收益比例不能小于 0"); expect(errors).toContain("大奖消费门槛不能小于 0"); expect(errors).toContain("单次返奖上限不能小于 0"); expect(errors).toContain("充值阶段门槛不能小于 0"); }); }); function completeDynamicConfig() { return { anchor_daily_payout_cap: 6_000_000, anchor_rate_percent: 1, app_code: "lalu", control_band_percent: 3, device_daily_payout_cap: 4_000_000, effective_from_ms: 0, enabled: true, gift_price_reference: 100, high_water_nonzero_factor_percent: 130, high_watermark_coins: 20_000_000, initial_pool_coins: 0, jackpot_global_rtp_max_percent: 98, jackpot_multipliers: [200, 500, 1000], jackpot_spend_threshold_coins: 50_000, jackpot_user_72h_rtp_max_percent: 96, jackpot_user_day_rtp_max_percent: 96, loss_streak_guarantee: 5, low_water_nonzero_factor_percent: 70, low_watermark_coins: 10_000_000, max_jackpot_hits_per_user_day: 5, max_single_payout: 1_000_000, normal_max_equivalent_draws: 20_000, novice_max_equivalent_draws: 2_000, pool_id: "default", pool_rate_percent: 98, profit_rate_percent: 1, recharge_boost_factor_percent: 110, recharge_boost_window_ms: 300_000, room_hourly_payout_cap: 5_000_000, settlement_window_wager: 1_000_000, stages: dynamicStages(), strategy_version: "dynamic_v3", target_rtp_percent: 98, user_daily_payout_cap: 3_000_000, user_hourly_payout_cap: 2_000_000, }; } function dynamicStages() { return [ dynamicStage("novice", 0, 0), dynamicStage("normal", 10_000, 20_000), dynamicStage("advanced", 20_000, 30_000), ]; } function dynamicStage(stage, min7d, min30d) { return { min_recharge_30d_coins: min30d, min_recharge_7d_coins: min7d, stage, tiers: [ tier(`${stage}_none`, 0, 5), tier(`${stage}_0_5x`, 0.5, 4), tier(`${stage}_1x`, 1, 86), tier(`${stage}_2x`, 2, 5), ], }; } function fixedStages() { return ["novice", "normal", "advanced"].map((stage) => ({ stage, tiers: [ tier(`${stage}_none`, 0, 10), tier(`${stage}_0_5x`, 0.5, 20), tier(`${stage}_1x`, 1, 55), tier(`${stage}_2x`, 2, 15), ], })); } function tier(tierId, multiplier, probabilityPercent) { return { enabled: true, high_water_only: false, multiplier, probability_percent: probabilityPercent, tier_id: tierId, }; }