@@ -0,0 +1,236 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user