#!/usr/bin/env python3 import hashlib import json import sys import tempfile import unittest from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[1] sys.path[:0] = [ str(PROJECT_ROOT / "packages" / "jwebot-config" / "src"), str(PROJECT_ROOT / "packages" / "jwebot-sdk" / "src"), str(PROJECT_ROOT / "packages" / "jwebot-official-extensions" / "src"), str(PROJECT_ROOT / "packages" / "jwebot-kernel" / "src"), ] from jwebot_kernel.conversation_scope import ( # noqa: E402 ConversationConfigError, ConversationKind, ConversationTarget, ShadowConversationRegistry, build_shadow_event, ) def fingerprint(label: str) -> str: return hashlib.sha256(label.encode()).hexdigest() class ConversationTargetTests(unittest.TestCase): def test_group_requires_an_explicit_mention(self): target = ConversationTarget( key="main-group", kind=ConversationKind.GROUP, title_fingerprint=fingerprint("main-group"), ) self.assertEqual( target.question("@JWeBot hello", bot_mention="JWeBot"), "hello", ) self.assertEqual(target.question("hello", bot_mention="JWeBot"), "") def test_direct_chat_accepts_only_commands_or_wake_words(self): target = ConversationTarget( key="test-direct", kind=ConversationKind.DIRECT, title_fingerprint=fingerprint("test-direct"), sender="Test User", ) self.assertEqual( target.question("/状态", bot_mention="JWeBot"), "/状态", ) self.assertEqual( target.question("JWeBot: hello", bot_mention="JWeBot"), "hello", ) self.assertEqual(target.question("普通聊天", bot_mention="JWeBot"), "") def test_direct_chat_requires_a_configured_sender(self): with self.assertRaisesRegex(ConversationConfigError, "direct sender"): ConversationTarget( key="test-direct", kind=ConversationKind.DIRECT, title_fingerprint=fingerprint("test-direct"), ) def test_title_identity_must_be_a_sha256_digest(self): with self.assertRaisesRegex(ConversationConfigError, "SHA-256"): ConversationTarget( key="test-direct", kind=ConversationKind.DIRECT, title_fingerprint="visible-contact-name", sender="Test User", ) class ShadowRegistryTests(unittest.TestCase): def test_registry_is_disabled_without_an_explicit_path(self): registry = ShadowConversationRegistry.from_environment({}) self.assertFalse(registry.enabled) self.assertEqual(registry.targets, ()) def test_disabled_registry_never_matches_targets(self): digest = fingerprint("test-direct") registry = ShadowConversationRegistry.from_mapping( { "format_version": 1, "enabled": False, "targets": [ { "key": "test-direct", "kind": "direct", "title_fingerprint": digest, "sender": "Test User", } ], } ) self.assertIsNone(registry.target_for_fingerprint(digest)) def test_explicit_config_loads_only_shadow_targets(self): digest = fingerprint("test-direct") config = { "format_version": 1, "enabled": True, "targets": [ { "key": "test-direct", "kind": "direct", "title_fingerprint": digest, "sender": "Test User", } ], } with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "shadow.json" path.write_text(json.dumps(config), encoding="utf-8") registry = ShadowConversationRegistry.from_environment( {"JWEBOT_CONVERSATION_SHADOW_CONFIG": str(path)} ) self.assertTrue(registry.enabled) self.assertEqual(registry.target_for_fingerprint(digest), registry.targets[0]) def test_duplicate_title_fingerprints_are_rejected(self): digest = fingerprint("duplicate") with self.assertRaisesRegex(ConversationConfigError, "fingerprints"): ShadowConversationRegistry.from_mapping( { "format_version": 1, "enabled": True, "targets": [ { "key": "direct-a", "kind": "direct", "title_fingerprint": digest, "sender": "Test A", }, { "key": "direct-b", "kind": "direct", "title_fingerprint": digest, "sender": "Test B", }, ], } ) def test_group_targets_are_rejected_in_private_shadow_phase(self): with self.assertRaisesRegex(ConversationConfigError, "direct targets only"): ShadowConversationRegistry.from_mapping( { "format_version": 1, "enabled": True, "targets": [ { "key": "main-group", "kind": "group", "title_fingerprint": fingerprint("main-group"), } ], } ) def test_unknown_delivery_field_is_rejected(self): with self.assertRaisesRegex(ConversationConfigError, "unknown fields"): ShadowConversationRegistry.from_mapping( { "format_version": 1, "enabled": True, "delivery_enabled": True, "targets": [], } ) class ShadowEventTests(unittest.TestCase): def test_direct_event_uses_verified_conversation_identity(self): digest = fingerprint("test-direct") target = ConversationTarget( key="test-direct", kind=ConversationKind.DIRECT, title_fingerprint=digest, sender="Test User", ) event = build_shadow_event( target, observed_at="2026-10-20T12:00:00+08:00", text="/状态", body_confidence=0.98, visual_signature="visual-1", bot_mention="JWeBot", ) self.assertEqual(event["sender"], "Test User") self.assertEqual(event["sender_confidence"], 1.0) self.assertEqual(event["question"], "/状态") self.assertTrue(event["mention_bot"]) self.assertEqual(event["group_fingerprint"], digest) self.assertEqual(event["conversation_kind"], "direct") self.assertTrue(event["shadow_only"]) self.assertFalse(event["dispatch_allowed"]) def test_unaddressed_direct_message_is_observed_but_not_triggered(self): target = ConversationTarget( key="test-direct", kind=ConversationKind.DIRECT, title_fingerprint=fingerprint("test-direct"), sender="Test User", ) event = build_shadow_event( target, observed_at="2026-10-20T12:00:00+08:00", text="普通聊天", body_confidence=0.98, visual_signature="visual-2", bot_mention="JWeBot", ) self.assertEqual(event["question"], "") self.assertFalse(event["mention_bot"]) self.assertTrue(event["shadow_only"]) self.assertFalse(event["dispatch_allowed"]) if __name__ == "__main__": unittest.main()