8624896379
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Worker 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 / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 53s
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
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m52s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m29s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m4s
AI Code Review / AI Code Review (pull_request) Successful in 7m0s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 10m9s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 12m9s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 13m18s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 11m27s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 20m25s
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 - Security (pull_request) Has been cancelled
222 lines
7.7 KiB
Python
Executable File
222 lines
7.7 KiB
Python
Executable File
"""功能计费改造测试:爆款读配置、对口型/智能剪辑预扣逻辑。
|
||
|
||
策略:
|
||
- 爆款:通过修改缓存中的 FeatureConfig(multiplier/model_pricing)验证价格随配置变化
|
||
- lip_sync / smart_edit:直接测 LipsyncService 的预扣/结算/退款辅助方法,
|
||
PointsService 用 mock,避免依赖真实积分账户。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
from packages.domain import feature_pricing_service as fps
|
||
from packages.domain.feature_pricing_service import FeatureConfig, refresh_feature_configs
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def _reset_cache():
|
||
refresh_feature_configs()
|
||
yield
|
||
refresh_feature_configs()
|
||
|
||
|
||
def _seed_cache(configs: dict) -> None:
|
||
import time
|
||
|
||
fps._cache = (time.monotonic(), configs)
|
||
|
||
|
||
class TestViralVideoReadsConfig:
|
||
def test_multiplier_change_changes_price(self):
|
||
"""配置里 multiplier 改大后,爆款价格随之变大(证明不再读死常量)。"""
|
||
from packages.domain.points_rules import calculate_viral_video_credits
|
||
|
||
# 基线兜底
|
||
base = calculate_viral_video_credits(15, 1280, 720)
|
||
assert base == 29.68
|
||
|
||
fallback = fps._fallback_configs()
|
||
vv = fallback["viral_video"]
|
||
vv.profit_multiplier = 2.0
|
||
_seed_cache(fallback)
|
||
|
||
changed = calculate_viral_video_credits(15, 1280, 720)
|
||
assert changed > base
|
||
# 精确校验:video_cost 相同,仅系数从 1.3 → 2.0
|
||
_, bd = __import__(
|
||
"packages.domain.points_rules", fromlist=["calculate_viral_video_credits_with_breakdown"]
|
||
).calculate_viral_video_credits_with_breakdown(15, 1280, 720)
|
||
assert bd["profit_multiplier"] == 2.0
|
||
|
||
def test_model_price_from_config(self):
|
||
"""model_pricing 改单价后,token 成本按新单价计算。"""
|
||
from packages.domain.points_rules import calculate_viral_video_credits_with_breakdown
|
||
|
||
fallback = fps._fallback_configs()
|
||
vv = fallback["viral_video"]
|
||
# seedance-2.5/720p/false 从 70 改成 100
|
||
vv.model_pricing["seedance-2.5"]["720p"]["false"] = 100.0
|
||
_seed_cache(fallback)
|
||
|
||
_, bd = calculate_viral_video_credits_with_breakdown(15, 1280, 720)
|
||
assert bd["model_price"] == 100.0
|
||
|
||
def test_price_cap_from_config(self):
|
||
from packages.domain.points_rules import calculate_viral_video_credits_with_breakdown
|
||
|
||
fallback = fps._fallback_configs()
|
||
vv = fallback["viral_video"]
|
||
vv.price_cap = 5.0
|
||
_seed_cache(fallback)
|
||
|
||
credits, bd = calculate_viral_video_credits_with_breakdown(15, 1280, 720)
|
||
assert credits == 5.0
|
||
assert bd["price_cap"] == 5.0
|
||
|
||
def test_disabled_feature_returns_zero_credits(self):
|
||
"""功能 is_enabled=false 时计费函数返回 0(纯计费层语义)。"""
|
||
from packages.domain.points_rules import calculate_viral_video_credits_with_breakdown
|
||
|
||
fallback = fps._fallback_configs()
|
||
fallback["viral_video"].is_enabled = False
|
||
_seed_cache(fallback)
|
||
|
||
credits, bd = calculate_viral_video_credits_with_breakdown(15, 1280, 720)
|
||
assert credits == 0.0
|
||
assert bd["feature_enabled"] is False
|
||
assert bd["charged"] is False
|
||
|
||
|
||
class TestLipSyncPricing:
|
||
def _make_service(self):
|
||
from app.services.lipsync_service import LipsyncService
|
||
|
||
svc = LipsyncService.__new__(LipsyncService)
|
||
svc.db = MagicMock()
|
||
return svc
|
||
|
||
def _lip_cfg(self, **kw):
|
||
base = dict(
|
||
feature_key="lip_sync",
|
||
name="对口型",
|
||
is_enabled=True,
|
||
fixed_cost=0.1,
|
||
profit_multiplier=1.0,
|
||
dynamic_unit_cost=0.05,
|
||
billing_mode="per_second",
|
||
price_cap=0.0,
|
||
model_pricing={},
|
||
description="",
|
||
)
|
||
base.update(kw)
|
||
return FeatureConfig(**base)
|
||
|
||
def test_estimate_duration_from_script(self):
|
||
svc = self._make_service()
|
||
# 10 个字 / 5 = 2 秒,下限 1
|
||
assert svc._estimate_duration(script_text="一二三四五六七八九十") == 2.0
|
||
# 无任何信息 → 默认 10 秒
|
||
assert svc._estimate_duration() == 10.0
|
||
|
||
def test_calculate_lipsync_price_per_second(self):
|
||
_seed_cache({"lip_sync": self._lip_cfg()})
|
||
price, bd = fps.calculate_price("lip_sync", dynamic_cost=20.0 * 0.05)
|
||
# dynamic 1.0 + fixed 0.1 = 1.1
|
||
assert price == 1.1
|
||
assert bd["charged"] is True
|
||
|
||
def test_settle_refunds_overcharge(self):
|
||
"""实际时长短 → 只退不补,退还差额。"""
|
||
svc = self._make_service()
|
||
_seed_cache({"lip_sync": self._lip_cfg()})
|
||
|
||
job = MagicMock()
|
||
job.credits_prepaid = 2.0
|
||
job.credits_cost = 0.0 # 未结算
|
||
job.user_id = "u1"
|
||
job.credits_transaction_id = "txn-old"
|
||
|
||
with patch("packages.domain.points_service.PointsService") as MockPS:
|
||
inst = MockPS.return_value
|
||
inst.refund_points.return_value = {"success": True}
|
||
svc._settle_lip_sync(job, actual_duration=10.0)
|
||
|
||
# final: (10*0.05 + 0.1)*1.0 = 0.6;退 2.0-0.6=1.4
|
||
assert round(job.credits_cost, 2) == 0.6
|
||
inst.refund_points.assert_called_once()
|
||
kwargs = inst.refund_points.call_args.kwargs
|
||
assert kwargs["amount"] == 1.4
|
||
|
||
def test_settle_no_refund_when_longer(self):
|
||
"""首期只退不补:实际更贵不补扣。"""
|
||
svc = self._make_service()
|
||
_seed_cache({"lip_sync": self._lip_cfg()})
|
||
|
||
job = MagicMock()
|
||
job.credits_prepaid = 0.5
|
||
job.credits_cost = 0.0
|
||
|
||
with patch("packages.domain.points_service.PointsService") as MockPS:
|
||
inst = MockPS.return_value
|
||
svc._settle_lip_sync(job, actual_duration=60.0)
|
||
|
||
assert round(job.credits_cost, 2) > 0.5
|
||
inst.refund_points.assert_not_called()
|
||
|
||
def test_refund_on_failure_full(self):
|
||
svc = self._make_service()
|
||
job = MagicMock()
|
||
job.credits_prepaid = 3.0
|
||
job.credits_cost = 0.0
|
||
job.user_id = "u1"
|
||
job.credits_transaction_id = "t1"
|
||
|
||
with patch("packages.domain.points_service.PointsService") as MockPS:
|
||
inst = MockPS.return_value
|
||
inst.refund_points.return_value = {"success": True}
|
||
svc._refund_lip_sync(job)
|
||
|
||
kwargs = inst.refund_points.call_args.kwargs
|
||
assert kwargs["amount"] == 3.0
|
||
|
||
|
||
class TestSmartEditFixedPrice:
|
||
def test_fixed_price_formula(self):
|
||
"""首期固定价:dynamic=0,price=fixed*multiplier,cap 封顶。"""
|
||
cfg = FeatureConfig(
|
||
feature_key="smart_edit",
|
||
name="智能剪辑",
|
||
is_enabled=True,
|
||
fixed_cost=2.0,
|
||
profit_multiplier=1.5,
|
||
billing_mode="model_based",
|
||
price_cap=0.0,
|
||
)
|
||
_seed_cache({"smart_edit": cfg})
|
||
price, bd = fps.calculate_price("smart_edit", dynamic_cost=0.0)
|
||
# (0+2)*1.5 = 3.0
|
||
assert price == 3.0
|
||
assert bd["dynamic_cost"] == 0.0
|
||
|
||
def test_fixed_price_with_cap(self):
|
||
cfg = FeatureConfig(
|
||
feature_key="smart_edit",
|
||
is_enabled=True,
|
||
fixed_cost=10.0,
|
||
profit_multiplier=2.0,
|
||
price_cap=8.0,
|
||
)
|
||
_seed_cache({"smart_edit": cfg})
|
||
price, _ = fps.calculate_price("smart_edit", dynamic_cost=0.0)
|
||
assert price == 8.0
|
||
|
||
def test_disabled_smart_edit_free(self):
|
||
cfg = FeatureConfig(feature_key="smart_edit", is_enabled=False, fixed_cost=2.0)
|
||
_seed_cache({"smart_edit": cfg})
|
||
price, bd = fps.calculate_price("smart_edit", dynamic_cost=0.0)
|
||
assert price == 0.0
|
||
assert bd["charged"] is False
|