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)