video-compliance-ai/backend/tests/test_script_review_api.py
Your Name e4959d584f feat: 完善代理商端业务逻辑与前后端框架
主要更新:
- 更新代理商端文档,明确项目由品牌方分配流程
- 新增Brief配置详情页(已配置)设计稿
- 完善工作台紧急待办中品牌新任务功能
- 整理Pencil设计文件中代理商端页面顺序
- 新增后端FastAPI框架及核心API
- 新增前端Next.js页面和组件库
- 添加.gitignore排除构建和缓存文件

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 19:27:31 +08:00

332 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
脚本预审 API 测试 (TDD - 红色阶段)
测试覆盖: 脚本提交、违规检测、语境理解
"""
import pytest
from httpx import AsyncClient
from app.schemas.review import ScriptReviewResponse, ViolationType, SoftRiskAction
class TestSubmitScript:
"""提交脚本预审"""
@pytest.mark.asyncio
async def test_submit_script_returns_200(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""提交脚本返回 200"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "这是一段测试脚本内容",
"platform": "douyin",
"brand_id": brand_id,
}
)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_submit_script_returns_review_result(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""提交脚本返回审核结果"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "这是一段测试脚本内容",
"platform": "douyin",
"brand_id": brand_id,
}
)
data = response.json()
parsed = ScriptReviewResponse.model_validate(data)
assert isinstance(parsed.summary, str) and parsed.summary
assert 0 <= parsed.score <= 100
@pytest.mark.asyncio
async def test_submit_empty_script_returns_422(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""提交空脚本返回 422"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "",
"platform": "douyin",
"brand_id": brand_id,
}
)
assert response.status_code == 422
class TestForbiddenWordDetection:
"""违禁词检测"""
@pytest.mark.asyncio
async def test_detect_absolute_word(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""检测广告极限词:最好、第一"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "我们的产品是全网最好的,销量第一",
"platform": "douyin",
"brand_id": brand_id,
}
)
data = response.json()
parsed = ScriptReviewResponse.model_validate(data)
assert len(parsed.violations) > 0
violation_types = [v.type for v in parsed.violations]
assert ViolationType.FORBIDDEN_WORD in violation_types
@pytest.mark.asyncio
async def test_detect_efficacy_word(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""检测功效词:根治、治愈"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "使用我们的产品可以根治失眠问题",
"platform": "douyin",
"brand_id": brand_id,
}
)
data = response.json()
parsed = ScriptReviewResponse.model_validate(data)
violation_types = [v.type for v in parsed.violations]
assert ViolationType.EFFICACY_CLAIM in violation_types
@pytest.mark.asyncio
async def test_return_violation_position(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""返回违规词位置"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "这是最好的产品", # "最好"是违禁词
"platform": "douyin",
"brand_id": brand_id,
}
)
data = response.json()
parsed = ScriptReviewResponse.model_validate(data)
assert len(parsed.violations) > 0, "应检测到'最好'违规"
violation = parsed.violations[0]
assert violation.position is not None
assert violation.position.start >= 0
assert violation.position.end > violation.position.start
@pytest.mark.asyncio
async def test_return_violation_suggestion(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""每个违规项包含修改建议"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "这是最好的产品", # "最好"是违禁词
"platform": "douyin",
"brand_id": brand_id,
}
)
data = response.json()
parsed = ScriptReviewResponse.model_validate(data)
assert len(parsed.violations) > 0, "应检测到'最好'违规"
assert isinstance(parsed.violations[0].suggestion, str)
assert parsed.violations[0].suggestion
class TestContextUnderstanding:
"""语境理解(降低误报)"""
@pytest.mark.asyncio
async def test_non_ad_context_not_flagged(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""非广告语境不应标记为违规:最开心的一天"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "今天是我最开心的一天,因为见到了老朋友",
"platform": "douyin",
"brand_id": brand_id,
}
)
data = response.json()
parsed = ScriptReviewResponse.model_validate(data)
forbidden_violations = [
v for v in parsed.violations
if v.type == ViolationType.FORBIDDEN_WORD and "" in v.content
]
assert len(forbidden_violations) == 0
@pytest.mark.asyncio
async def test_story_context_not_flagged(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""故事情节语境不应标记:他是第一个到达的人"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "他是第一个到达终点的人,大家都为他鼓掌",
"platform": "douyin",
"brand_id": brand_id,
}
)
data = response.json()
parsed = ScriptReviewResponse.model_validate(data)
forbidden_violations = [
v for v in parsed.violations
if v.type == ViolationType.FORBIDDEN_WORD and "第一" in v.content
]
assert len(forbidden_violations) == 0
@pytest.mark.asyncio
async def test_ad_context_flagged(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""广告语境应标记:我们的产品第一"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "我们的产品销量第一,品质最好",
"platform": "douyin",
"brand_id": brand_id,
}
)
data = response.json()
parsed = ScriptReviewResponse.model_validate(data)
assert len(parsed.violations) > 0
class TestSellingPointCheck:
"""卖点遗漏检查"""
@pytest.mark.asyncio
async def test_check_missing_selling_points(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""检查是否遗漏必要卖点"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "这个产品很好用",
"platform": "douyin",
"brand_id": brand_id,
"required_points": ["功效说明", "使用方法", "品牌名称"],
}
)
data = response.json()
parsed = ScriptReviewResponse.model_validate(data)
assert parsed.missing_points is not None
assert isinstance(parsed.missing_points, list)
@pytest.mark.asyncio
async def test_all_points_covered(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""所有卖点都覆盖时返回空"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "品牌A的护肤精华每天早晚各用一次可以让肌肤更水润",
"platform": "douyin",
"brand_id": brand_id,
"required_points": ["品牌名称", "使用方法", "功效说明"],
}
)
data = response.json()
parsed = ScriptReviewResponse.model_validate(data)
assert parsed.missing_points == []
class TestScoreCalculation:
"""合规分数计算"""
@pytest.mark.asyncio
async def test_clean_content_returns_high_score(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""合规内容返回高分(>=90"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "今天给大家分享一个护肤小技巧,记得每天早晚洁面哦",
"platform": "douyin",
"brand_id": brand_id,
}
)
data = response.json()
parsed = ScriptReviewResponse.model_validate(data)
assert parsed.score >= 90
high_risk = [v for v in parsed.violations if v.severity.value == "high"]
assert len(high_risk) == 0
@pytest.mark.asyncio
async def test_violation_content_returns_low_score(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""违规内容返回低分(<80"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "这是最好的产品,可以根治所有问题,效果第一",
"platform": "douyin",
"brand_id": brand_id,
}
)
data = response.json()
parsed = ScriptReviewResponse.model_validate(data)
assert parsed.score < 80
assert len(parsed.violations) > 0
@pytest.mark.asyncio
async def test_score_range_valid(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""分数在有效范围内 0-100"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "任意内容",
"platform": "douyin",
"brand_id": brand_id,
}
)
data = response.json()
parsed = ScriptReviewResponse.model_validate(data)
assert 0 <= parsed.score <= 100
class TestSoftRiskWarnings:
"""软性风控提示"""
@pytest.mark.asyncio
async def test_near_threshold_returns_warning(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""临界值接近阈值时返回软性提示(不阻断)"""
response = await client.post(
"/api/v1/scripts/review",
headers={"X-Tenant-ID": tenant_id},
json={
"content": "内容正常但指标接近阈值",
"platform": "douyin",
"brand_id": brand_id,
"soft_risk_context": {
"violation_rate": 0.045,
"violation_threshold": 0.05,
}
}
)
data = response.json()
parsed = ScriptReviewResponse.model_validate(data)
matched = [
w for w in parsed.soft_warnings
if w.code == "NEAR_THRESHOLD" and w.action_required == SoftRiskAction.CONFIRM
]
assert matched, "应返回临界值软性提示"
assert all(w.blocking is False for w in matched)