feat: implement t-604 keyword prompt moderation

This commit is contained in:
QiuSW
2026-07-06 14:57:25 +08:00
parent c89cb4cf28
commit ca0ffb8dbe
24 changed files with 787 additions and 10 deletions
+106
View File
@@ -0,0 +1,106 @@
from __future__ import annotations
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
from .models import SensitiveWord
from .normalization import normalize_text
from .providers.keyword import reset_keyword_matcher_cache
from .services import moderate_prompt
from .versioning import get_sensitive_words_version
@override_settings(MODERATION_CACHE_VERSION_KEY="test:moderation:sensitive_words:version")
class SensitiveWordNormalizationTests(TestCase):
def setUp(self):
cache.clear()
reset_keyword_matcher_cache()
def tearDown(self):
reset_keyword_matcher_cache()
cache.clear()
def test_normalize_text_removes_separators_zero_width_and_case(self):
self.assertEqual(normalize_text("Bad-\u200b Word"), "badword")
self.assertEqual(normalize_text("敏 感-词"), normalize_text("敏感词"))
def test_sensitive_word_saves_normalized_word_and_rejects_empty_normalized_word(self):
word = SensitiveWord.objects.create(word=" 敏 感-词 ", category="")
self.assertEqual(word.category, "custom")
self.assertEqual(word.normalized_word, "敏感词")
with self.assertRaises(ValidationError):
SensitiveWord.objects.create(word=" - \u200b ")
def test_word_save_bumps_shared_cache_version(self):
before = get_sensitive_words_version()
SensitiveWord.objects.create(word="敏感词")
self.assertNotEqual(get_sensitive_words_version(), before)
@override_settings(
MODERATION_ENABLED=True,
MODERATION_PROVIDER="keyword",
MODERATION_CACHE_VERSION_KEY="test:moderation:sensitive_words:version",
)
class KeywordModerationTests(TestCase):
def setUp(self):
cache.clear()
reset_keyword_matcher_cache()
def tearDown(self):
reset_keyword_matcher_cache()
cache.clear()
def test_prompt_blocks_normalized_keyword_without_storing_prompt(self):
word = SensitiveWord.objects.create(word="敏感词", category="policy")
prompt = "请处理敏-感\u200b 词内容"
with self.assertLogs("apps.moderation.services", level="INFO") as logs:
outcome = moderate_prompt(prompt=prompt)
self.assertTrue(outcome.blocked)
self.assertEqual(outcome.labels, ("policy",))
self.assertEqual(outcome.keywords, ("敏感词",))
self.assertEqual(outcome.matched_word_ids, (word.id,))
self.assertNotIn(prompt, "\n".join(logs.output))
def test_non_matching_prompt_passes(self):
SensitiveWord.objects.create(word="敏感词")
outcome = moderate_prompt(prompt="正常业务标题")
self.assertFalse(outcome.blocked)
def test_matcher_rebuilds_after_shared_version_changes(self):
self.assertFalse(moderate_prompt(prompt="新增词").blocked)
SensitiveWord.objects.create(word="新增词")
self.assertTrue(moderate_prompt(prompt="这个提示包含新增词").blocked)
@override_settings(
MODERATION_ENABLED=False,
MODERATION_PROVIDER="keyword",
MODERATION_CACHE_VERSION_KEY="test:moderation:sensitive_words:version",
)
class DisabledModerationTests(TestCase):
def setUp(self):
cache.clear()
reset_keyword_matcher_cache()
def tearDown(self):
reset_keyword_matcher_cache()
cache.clear()
def test_disabled_moderation_is_noop(self):
SensitiveWord.objects.create(word="敏感词")
outcome = moderate_prompt(prompt="敏感词")
self.assertFalse(outcome.blocked)