import unittest import numpy as np from wechat_bot import WeChatBot class SessionFingerprintTest(unittest.TestCase): @staticmethod def _bot(): bot = WeChatBot.__new__(WeChatBot) bot.scale = 1.0 bot.session_item_h = 64 bot._known_fps = set() return bot @staticmethod def _row(name_variant: int) -> np.ndarray: img = np.full((64, 230, 3), 238, dtype=np.uint8) # 两个客户使用完全相同的高对比头像。 for y in range(20, 44): for x in range(12, 42): tone = 45 if ((x // 5) + (y // 4)) % 2 else 205 img[y, x, :] = tone # 在昵称首行画两组分布明显不同的合成字形。 if name_variant == 1: for x in (62, 68, 76, 84, 96, 108, 122): img[12:28, x:x + 2, :] = 35 img[13:15, 62:126, :] = 35 img[21:23, 68:110, :] = 35 else: for y in (12, 16, 21, 26): img[y:y + 2, 62:132, :] = 35 for offset in range(16): img[11 + offset, 136 + offset:138 + offset, :] = 35 return img def test_same_avatar_different_name_patterns_are_isolated(self): bot = self._bot() first = bot._session_fingerprint(self._row(1), 32) second = bot._session_fingerprint(self._row(2), 32) self.assertEqual(16, len(first)) self.assertEqual(16, len(second)) self.assertEqual(first[:8], second[:8], "测试前提:头像哈希必须相同") self.assertNotEqual(first, second) def test_small_render_noise_and_label_inversion_are_normalized(self): bot = self._bot() original = self._row(1) canonical = bot._session_fingerprint(original, 32) rng = np.random.default_rng(20260723) noise = rng.integers(-2, 3, size=original.shape, dtype=np.int16) variant = np.clip(original.astype(np.int16) + noise, 0, 255).astype(np.uint8) # 模拟选中态:文字区前景/背景同时反相,绝对边缘强度仍应稳定。 variant[9:30, 58:190, :] = 255 - variant[9:30, 58:190, :] self.assertEqual(canonical, bot._session_fingerprint(variant, 32)) def test_new_composite_fingerprint_never_merges_into_legacy_avatar_key(self): bot = self._bot() composite = bot._session_fingerprint(self._row(1), 32) legacy = composite[:8] bot._known_fps = {legacy} self.assertEqual(composite, bot._canonical_fp(composite)) self.assertIn(composite, bot._known_fps) def test_identical_visible_rows_use_isolated_fallbacks(self): bot = self._bot() first_row = self._row(1) second_row = first_row.copy() # 消息预览属于可见行兜底摘要,但不参与稳定昵称签名。 first_row[38:45, 70:100, :] = 60 second_row[38:45, 120:160, :] = 60 image = np.vstack((first_row, second_row)) first = bot._session_fingerprint(image, 32) second = bot._session_fingerprint(image, 96) self.assertEqual(16, len(first)) self.assertEqual(16, len(second)) self.assertNotEqual(first, second) def test_out_of_bounds_fallback_is_16_bytes_and_image_specific(self): bot = self._bot() first_img = np.zeros((8, 10, 3), dtype=np.uint8) second_img = first_img.copy() second_img[0, 0, 0] = 1 first = bot._session_fingerprint(first_img, 0) second = bot._session_fingerprint(second_img, 0) self.assertEqual(16, len(first)) self.assertEqual(16, len(second)) self.assertNotEqual(first, second) if __name__ == "__main__": unittest.main()