Release beta 2.0.0.1021
check / local-gate (push) Failing after 1m38s

This commit is contained in:
JWeBot Contributors
2026-10-21 17:05:09 +08:00
parent 5a3c4fb94c
commit e4365841ed
79 changed files with 1572 additions and 1840 deletions
+257
View File
@@ -931,6 +931,263 @@ class RecorderSignatureMigrationTests(unittest.TestCase):
engine.warmup.assert_called_once()
class RecorderConversationStateTests(unittest.TestCase):
def test_legacy_active_state_migrates_only_to_its_matching_conversation(self):
engine = mock.Mock()
with mock.patch.object(
MODULE,
"ensure_directories",
), mock.patch.object(
MODULE,
"OcrEngine",
return_value=engine,
), mock.patch.object(
MODULE,
"read_json",
return_value={
"initialized": True,
"last_group_fingerprint": "group-a",
"last_visual_hash": "visual-a",
"visible_signatures": ["message-a"],
"recent_trigger_signatures": ["trigger-a"],
"message_signature_scheme": MODULE.MESSAGE_SIGNATURE_SCHEME,
},
):
recorder = MODULE.Recorder(queue_enabled=True)
group_a = recorder.conversation_state("group-a")
group_b = recorder.conversation_state("group-b")
self.assertTrue(group_a["initialized"])
self.assertEqual(group_a["visible_signatures"], ["message-a"])
self.assertFalse(group_b["initialized"])
self.assertEqual(group_b["visible_signatures"], [])
def test_switching_conversations_does_not_cross_deduplicate_messages(self):
engine = mock.Mock()
first_a = MODULE.IncomingMessage(
"Alice",
"@JWeBot first",
0.98,
0.99,
0.98,
100,
"a-first",
)
first_b = MODULE.IncomingMessage(
"Bob",
"@JWeBot first",
0.98,
0.99,
0.98,
100,
"b-first",
)
second_a = MODULE.IncomingMessage(
"Alice",
"@JWeBot second",
0.98,
0.99,
0.98,
160,
"a-second",
)
engine.recognize_messages.side_effect = [
([first_a], 0.01),
([first_b], 0.01),
([first_a, second_a], 0.01),
]
engine.last_cache_hits = 0
engine.last_cache_misses = 1
engine.last_cache_refresh = False
queued = []
image = MODULE.Image.new(
"RGB",
(MODULE.EXPECTED_WIDTH, MODULE.EXPECTED_HEIGHT),
"white",
)
with mock.patch.object(
MODULE,
"ensure_directories",
), mock.patch.object(
MODULE,
"OcrEngine",
return_value=engine,
), mock.patch.object(
MODULE,
"read_json",
return_value={},
), mock.patch.object(
MODULE,
"incoming_mask_and_bubbles",
return_value=(None, []),
), mock.patch.object(
MODULE,
"write_history_event",
), mock.patch.object(
MODULE,
"queue_event",
side_effect=queued.append,
):
recorder = MODULE.Recorder(queue_enabled=True)
recorder.save_state = mock.Mock()
recorder.process_stable_image(
image,
"layout-a-1",
visual_hash="visual-a-1",
observed_at="2026-10-20T12:00:00+08:00",
group_fingerprint="group-a",
)
recorder.process_stable_image(
image,
"layout-b-1",
visual_hash="visual-b-1",
observed_at="2026-10-20T12:00:01+08:00",
group_fingerprint="group-b",
)
recorder.process_stable_image(
image,
"layout-a-2",
visual_hash="visual-a-2",
observed_at="2026-10-20T12:00:02+08:00",
group_fingerprint="group-a",
)
self.assertEqual(len(queued), 1)
self.assertEqual(queued[0]["group_fingerprint"], "group-a")
self.assertEqual(queued[0]["text"], "@JWeBot second")
states = recorder.state["conversation_states"]
self.assertEqual(states["group-a"]["visible_signatures"], ["a-first", "a-second"])
self.assertEqual(states["group-b"]["visible_signatures"], ["b-first"])
def test_shadow_direct_messages_are_observed_without_queueing(self):
class ShadowTarget:
def question(self, text, *, bot_mention):
del bot_mention
return text if text.startswith("/") else ""
class ShadowRegistry:
def target_for_fingerprint(self, fingerprint):
return ShadowTarget() if fingerprint == "direct-a" else None
def build_shadow_event(
target,
*,
observed_at,
text,
body_confidence,
visual_signature,
bot_mention,
observed_sender,
sender_confidence,
ordinal,
):
del (
target,
body_confidence,
visual_signature,
bot_mention,
observed_sender,
sender_confidence,
ordinal,
)
return {
"event_id": "shadow-event",
"observed_at": observed_at,
"sender": "Test User",
"text": text,
"mention_bot": True,
"group_fingerprint": "direct-a",
"shadow_only": True,
"dispatch_allowed": False,
}
engine = mock.Mock()
first = MODULE.IncomingMessage(
"unknown",
"/状态",
0.0,
0.0,
0.98,
100,
"direct-first",
)
second = MODULE.IncomingMessage(
"unknown",
"/版本",
0.0,
0.0,
0.98,
160,
"direct-second",
)
engine.recognize_messages.side_effect = [
([first], 0.01),
([first, second], 0.01),
]
engine.last_cache_hits = 0
engine.last_cache_misses = 1
engine.last_cache_refresh = False
history = []
image = MODULE.Image.new(
"RGB",
(MODULE.EXPECTED_WIDTH, MODULE.EXPECTED_HEIGHT),
"white",
)
with mock.patch.object(
MODULE,
"ensure_directories",
), mock.patch.object(
MODULE,
"OcrEngine",
return_value=engine,
), mock.patch.object(
MODULE,
"read_json",
return_value={},
), mock.patch.object(
MODULE,
"load_conversation_shadow_support",
return_value=(ShadowRegistry(), build_shadow_event),
), mock.patch.object(
MODULE,
"incoming_mask_and_bubbles",
return_value=(None, []),
), mock.patch.object(
MODULE,
"write_history_event",
side_effect=history.append,
), mock.patch.object(
MODULE,
"queue_event",
) as queue_event:
recorder = MODULE.Recorder(queue_enabled=True)
recorder.save_state = mock.Mock()
recorder.process_stable_image(
image,
"layout-direct-1",
visual_hash="visual-direct-1",
observed_at="2026-10-20T12:00:00+08:00",
group_fingerprint="direct-a",
)
recorder.process_stable_image(
image,
"layout-direct-2",
visual_hash="visual-direct-2",
observed_at="2026-10-20T12:00:01+08:00",
group_fingerprint="direct-a",
)
queue_event.assert_not_called()
self.assertEqual(len(history), 1)
self.assertEqual(history[0]["text"], "/版本")
self.assertTrue(history[0]["shadow_only"])
self.assertFalse(history[0]["dispatch_allowed"])
self.assertEqual(recorder.state["shadowed_total"], 1)
class StorageTests(unittest.TestCase):
def test_elapsed_since_iso_is_timezone_aware_and_bounded(self):
now = datetime(2026, 10, 14, 12, 0, 1, tzinfo=timezone.utc)