237 lines
7.9 KiB
Python
237 lines
7.9 KiB
Python
#!/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()
|