4a4b8f4a05
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
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 / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m12s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m20s
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m36s
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
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m27s
AI Code Review / AI Code Review (pull_request) Successful in 7m13s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 11m7s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 14m54s
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
- 编导prompt加粗强调严禁使用'很近',正确用词'最近' - _try_gen增加hard fix字符串替换:口播/分镜对白含'很近'自动替换 - 三级重试合格校验:口播含'很近'视为不合格触发重试 refactor(points): 移除ai_video/数字人/配音/改写/标题/封面/抖音解析扣点,仅保留声音克隆 - 删除5个路由的@points_gate装饰器(generation_preview/generation_cover/generation_tasks/scripts_ai/ai_avatar_render) - 删除tts.py/lipsync.py/narrative_service.py中ai_voice/ai_digital_human手动扣点+refund块 - points_rules.py精简POINTS_SCENES:仅保留voice_clone_train(0积分)和voice_clone_synth(1积分/分钟) - 删除DAILY_FREE_CLIP_LIMIT免费混剪额度逻辑,get_daily_usage返回unlimited占位 - points_gate.py删除ai_video特殊分支,未知/免费场景calculate_points_cost返回0直接放行 - points.py删除UNKNOWN_SCENE 400校验,任意scene可查 - 更新16个测试文件适配新规则:198个扣点相关单测全绿,viral_video 58个测试无回归
113 lines
4.0 KiB
Python
113 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
|