7a63905a1c
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m54s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m59s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 18s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m10s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 7m0s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m40s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 16m34s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 17m23s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 18m47s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 21m15s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 29s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 2m21s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 35m5s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 10s
120 lines
4.0 KiB
Python
120 lines
4.0 KiB
Python
"""积分消耗规则单元测试 (#1895) — v1.6.2: 仅保留 voice_clone 相关"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
import pytest
|
|
|
|
from packages.domain.points_rules import (
|
|
FREE_USER_MULTIPLIER,
|
|
MEMBER_DISCOUNT,
|
|
MEMBERSHIP_PRICES,
|
|
POINTS_PACKAGES,
|
|
POINTS_SCENES,
|
|
calculate_points_cost,
|
|
)
|
|
|
|
|
|
class TestPointsScenesConfig:
|
|
"""场景配置完整性"""
|
|
|
|
def test_voice_clone_scenes_defined(self):
|
|
# 仅保留声音克隆两个场景
|
|
assert set(POINTS_SCENES.keys()) == {"voice_clone_train", "voice_clone_synth"}
|
|
|
|
def test_required_keys_present(self):
|
|
for key, scene in POINTS_SCENES.items():
|
|
assert "base_points" in scene, f"{key} missing base_points"
|
|
assert "unit" in scene, f"{key} missing unit"
|
|
assert "name" in scene, f"{key} missing name"
|
|
|
|
def test_voice_clone_train_is_free(self):
|
|
assert POINTS_SCENES["voice_clone_train"]["base_points"] == 0
|
|
|
|
def test_voice_clone_synth_is_per_minute(self):
|
|
assert POINTS_SCENES["voice_clone_synth"]["base_points"] == 1
|
|
assert POINTS_SCENES["voice_clone_synth"]["unit"] == "分钟"
|
|
|
|
|
|
class TestPointsPackages:
|
|
def test_three_packages(self):
|
|
assert len(POINTS_PACKAGES) == 3
|
|
assert POINTS_PACKAGES["starter_pack"]["points"] == 100
|
|
assert POINTS_PACKAGES["basic_pack"]["points"] == 500
|
|
assert POINTS_PACKAGES["pro_pack"]["points"] == 2000
|
|
|
|
|
|
class TestMembershipPrices:
|
|
def test_three_plans(self):
|
|
assert len(MEMBERSHIP_PRICES) == 3
|
|
assert MEMBERSHIP_PRICES["monthly"]["price_cents"] == 1990
|
|
assert MEMBERSHIP_PRICES["quarterly"]["duration_days"] == 90
|
|
assert MEMBERSHIP_PRICES["yearly"]["price_cents"] == 15900
|
|
|
|
|
|
class TestCalculatePointsCost:
|
|
"""核心计费逻辑"""
|
|
|
|
# ── 声音克隆合成(按时长计费) ──
|
|
|
|
def test_voice_clone_synth_base(self):
|
|
cost = calculate_points_cost("voice_clone_synth", is_member=False, duration_minutes=3)
|
|
assert cost == math.ceil(3 * FREE_USER_MULTIPLIER)
|
|
|
|
def test_voice_clone_synth_rounds_up(self):
|
|
cost = calculate_points_cost("voice_clone_synth", is_member=False, duration_minutes=2.3)
|
|
assert cost == math.ceil(3 * FREE_USER_MULTIPLIER)
|
|
|
|
def test_voice_clone_synth_minimum_1_minute(self):
|
|
cost = calculate_points_cost("voice_clone_synth", is_member=False, duration_minutes=0.1)
|
|
assert cost == math.ceil(1 * FREE_USER_MULTIPLIER)
|
|
|
|
# ── 免费场景 ──
|
|
|
|
def test_voice_clone_train_free(self):
|
|
cost = calculate_points_cost("voice_clone_train", is_member=False)
|
|
assert cost == 0
|
|
|
|
def test_voice_clone_train_free_for_member(self):
|
|
cost = calculate_points_cost("voice_clone_train", is_member=True)
|
|
assert cost == 0
|
|
|
|
# ── 会员折扣 ──
|
|
|
|
def test_monthly_member_discount(self):
|
|
cost = calculate_points_cost("voice_clone_synth", is_member=True, duration_minutes=1, member_type="monthly")
|
|
assert cost == max(1, math.floor(1 * MEMBER_DISCOUNT["monthly"]))
|
|
|
|
def test_yearly_member_deep_discount(self):
|
|
cost = calculate_points_cost(
|
|
"voice_clone_synth",
|
|
is_member=True,
|
|
duration_minutes=2,
|
|
member_type="yearly",
|
|
)
|
|
assert cost == max(1, math.floor(2 * MEMBER_DISCOUNT["yearly"]))
|
|
|
|
def test_member_without_type_no_discount(self):
|
|
cost = calculate_points_cost("voice_clone_synth", is_member=True, duration_minutes=1)
|
|
assert cost == 1
|
|
|
|
# ── 已下线/未知场景(向后兼容:返回 0) ──
|
|
|
|
@pytest.mark.parametrize(
|
|
"scene",
|
|
[
|
|
"ai_voice",
|
|
"ai_video",
|
|
"ai_digital_human",
|
|
"ai_rewrite",
|
|
"ai_cover",
|
|
"ai_title",
|
|
"douyin_extract",
|
|
"nonexistent",
|
|
],
|
|
)
|
|
def test_retired_scenes_return_zero(self, scene):
|
|
assert calculate_points_cost(scene, is_member=False) == 0
|
|
assert calculate_points_cost(scene, is_member=True, duration_minutes=10) == 0
|