+349
-3
@@ -8,6 +8,7 @@ import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
@@ -924,9 +925,9 @@ class LedgerAndApplicationTests(unittest.TestCase):
|
||||
self.assertEqual(version["route"], "local")
|
||||
self.assertIn("JWeBot 版本", version["text"])
|
||||
self.assertIn("kernel", version["text"])
|
||||
self.assertIn("最新版本:beta 1.1.1.1016", version["text"])
|
||||
self.assertIn("更新时间:2026-10-16 17:19:45", version["text"])
|
||||
self.assertIn("更新摘要:修复扫码加权重复购买", version["text"])
|
||||
self.assertIn("最新版本:beta 1.1.2.1019", version["text"])
|
||||
self.assertIn("更新时间:2026-10-19 16:38:10", version["text"])
|
||||
self.assertIn("更新摘要:新增低频主动插话、随机冷门知识问答", version["text"])
|
||||
self.assertEqual(debug["route"], "local")
|
||||
self.assertIn("调试埋点", debug["text"])
|
||||
self.assertEqual(balance["route"], "local")
|
||||
@@ -1132,6 +1133,223 @@ class LedgerAndApplicationTests(unittest.TestCase):
|
||||
self.assertIn(ranking[0]["display_name"], daily_delivery["result_text"])
|
||||
self.application.deepseek.complete.assert_not_called()
|
||||
|
||||
def test_low_frequency_ambient_chat_uses_recent_context(self):
|
||||
ledger = self.application.ledger
|
||||
observed_at = MODULE.datetime(
|
||||
2026,
|
||||
10,
|
||||
13,
|
||||
12,
|
||||
42,
|
||||
tzinfo=MODULE.BEIJING_TIMEZONE,
|
||||
).isoformat()
|
||||
for index, (sender, text) in enumerate(
|
||||
(
|
||||
("Alice", "今天这个加班安排是不是有点离谱"),
|
||||
("Bob", "我也觉得还要继续修 bug"),
|
||||
("Alice", "先把问题处理完再说"),
|
||||
("Bob", "感觉咖啡已经救不了我了"),
|
||||
),
|
||||
start=1,
|
||||
):
|
||||
self.assertTrue(
|
||||
ledger.record_person_observation(
|
||||
event_id=f"ambient-observation-{index}",
|
||||
group_fingerprint="group-a",
|
||||
sender=sender,
|
||||
text=text,
|
||||
observed_at=observed_at,
|
||||
source="test",
|
||||
)
|
||||
)
|
||||
ledger.record_repeat_history(
|
||||
event_id=f"ambient-context-{index}",
|
||||
group_fingerprint="group-a",
|
||||
sender=sender,
|
||||
text=text,
|
||||
observed_at=observed_at,
|
||||
)
|
||||
self.application.deliver = mock.Mock()
|
||||
now = MODULE.datetime(
|
||||
2026,
|
||||
10,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
tzinfo=MODULE.BEIJING_TIMEZONE,
|
||||
)
|
||||
|
||||
with mock.patch.object(MODULE, "TRIVIA_QUIZ_PROBABILITY_PERCENT", 0), \
|
||||
mock.patch.object(MODULE, "AMBIENT_CHAT_PROBABILITY_PERCENT", 100):
|
||||
self.assertTrue(
|
||||
self.application.process_clock_once(
|
||||
now=now,
|
||||
group_fingerprint="group-a",
|
||||
)
|
||||
)
|
||||
|
||||
delivery = self.application.deliver.call_args.args[0]
|
||||
self.assertIn("加班味", delivery["result_text"])
|
||||
row = ledger.connection.execute(
|
||||
"""
|
||||
SELECT kind, state FROM clock_broadcasts
|
||||
WHERE group_fingerprint = ? AND slot_key = ?
|
||||
""",
|
||||
("group-a", "hourly:2026-10-13T13"),
|
||||
).fetchone()
|
||||
self.assertEqual(row["kind"], "ambient_chat")
|
||||
self.assertEqual(row["state"], "delivered")
|
||||
self.assertFalse(
|
||||
self.application.process_clock_once(
|
||||
now=now,
|
||||
group_fingerprint="group-a",
|
||||
)
|
||||
)
|
||||
|
||||
def test_trivia_quiz_broadcast_and_answer_rewards_mora(self):
|
||||
self.assertGreater(len(MODULE.trivia_question_bank()), 200)
|
||||
ledger = self.application.ledger
|
||||
observed_at = MODULE.datetime(
|
||||
2026,
|
||||
10,
|
||||
13,
|
||||
12,
|
||||
42,
|
||||
tzinfo=MODULE.BEIJING_TIMEZONE,
|
||||
).isoformat()
|
||||
for sender in ("Alice", "Bob"):
|
||||
ledger.record_person_observation(
|
||||
event_id=f"trivia-member-{sender}",
|
||||
group_fingerprint="group-a",
|
||||
sender=sender,
|
||||
text="冷门题准备一下",
|
||||
observed_at=observed_at,
|
||||
source="test",
|
||||
)
|
||||
self.application.deliver = mock.Mock()
|
||||
now = MODULE.datetime(
|
||||
2026,
|
||||
10,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
tzinfo=MODULE.BEIJING_TIMEZONE,
|
||||
)
|
||||
|
||||
with mock.patch.object(MODULE, "TRIVIA_QUIZ_PROBABILITY_PERCENT", 100):
|
||||
self.assertTrue(
|
||||
self.application.process_clock_once(
|
||||
now=now,
|
||||
group_fingerprint="group-a",
|
||||
)
|
||||
)
|
||||
|
||||
delivery = self.application.deliver.call_args.args[0]
|
||||
self.assertIn("冷门知识问答", delivery["result_text"])
|
||||
self.assertIn("限时5分钟", delivery["result_text"])
|
||||
self.assertIn("/回答 A", delivery["result_text"])
|
||||
state = ledger.local_game("group-a", MODULE.TRIVIA_GAME_KEY)
|
||||
self.assertIsNotNone(state)
|
||||
assert state is not None
|
||||
state["expires_at"] = time.time() + 300
|
||||
ledger.save_local_game("group-a", MODULE.TRIVIA_GAME_KEY, state)
|
||||
answer = str(state["answer"])
|
||||
reward = int(state["reward"])
|
||||
result = self.application.accept(
|
||||
event(
|
||||
"trivia-answer",
|
||||
f"@JWeBot /回答 {answer}",
|
||||
sender="Alice",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(result["route"], "local")
|
||||
self.assertIn("答对了", result["text"])
|
||||
self.assertIn(f"奖励摩拉 +{reward}", result["text"])
|
||||
self.assertIsNone(ledger.local_game("group-a", MODULE.TRIVIA_GAME_KEY))
|
||||
person = ledger.resolve_person("group-a", "Alice")
|
||||
account = ledger._load_player_account("group-a", str(person["person_key"]))
|
||||
self.assertEqual(account["balance"], 1000 + reward)
|
||||
|
||||
def test_ambient_chat_cooldown_keeps_next_hour_scan_broadcast(self):
|
||||
ledger = self.application.ledger
|
||||
observed_at = MODULE.datetime(
|
||||
2026,
|
||||
10,
|
||||
13,
|
||||
12,
|
||||
45,
|
||||
tzinfo=MODULE.BEIJING_TIMEZONE,
|
||||
).isoformat()
|
||||
for index, (sender, text) in enumerate(
|
||||
(
|
||||
("Alice", "今天加班真的有点顶不住"),
|
||||
("Bob", "先把这个线上问题看完"),
|
||||
("Alice", "咖啡续上"),
|
||||
("Bob", "修完再开一把游戏"),
|
||||
),
|
||||
start=1,
|
||||
):
|
||||
ledger.record_person_observation(
|
||||
event_id=f"ambient-cooldown-observation-{index}",
|
||||
group_fingerprint="group-a",
|
||||
sender=sender,
|
||||
text=text,
|
||||
observed_at=observed_at,
|
||||
source="test",
|
||||
)
|
||||
ledger.record_repeat_history(
|
||||
event_id=f"ambient-cooldown-context-{index}",
|
||||
group_fingerprint="group-a",
|
||||
sender=sender,
|
||||
text=text,
|
||||
observed_at=observed_at,
|
||||
)
|
||||
self.application.deliver = mock.Mock()
|
||||
first = MODULE.datetime(
|
||||
2026,
|
||||
10,
|
||||
13,
|
||||
13,
|
||||
0,
|
||||
tzinfo=MODULE.BEIJING_TIMEZONE,
|
||||
)
|
||||
second = MODULE.datetime(
|
||||
2026,
|
||||
10,
|
||||
13,
|
||||
14,
|
||||
0,
|
||||
tzinfo=MODULE.BEIJING_TIMEZONE,
|
||||
)
|
||||
|
||||
with mock.patch.object(MODULE, "TRIVIA_QUIZ_PROBABILITY_PERCENT", 0), \
|
||||
mock.patch.object(MODULE, "AMBIENT_CHAT_PROBABILITY_PERCENT", 100):
|
||||
self.assertTrue(
|
||||
self.application.process_clock_once(
|
||||
now=first,
|
||||
group_fingerprint="group-a",
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
self.application.process_clock_once(
|
||||
now=second,
|
||||
group_fingerprint="group-a",
|
||||
)
|
||||
)
|
||||
|
||||
second_delivery = self.application.deliver.call_args.args[0]
|
||||
self.assertIn("扫码了", second_delivery["result_text"])
|
||||
row = ledger.connection.execute(
|
||||
"""
|
||||
SELECT kind, state FROM clock_broadcasts
|
||||
WHERE group_fingerprint = ? AND slot_key = ?
|
||||
""",
|
||||
("group-a", "hourly:2026-10-13T14"),
|
||||
).fetchone()
|
||||
self.assertEqual(row["kind"], "hourly")
|
||||
self.assertEqual(row["state"], "delivered")
|
||||
|
||||
def test_failed_hourly_broadcast_does_not_increment_scan_ranking(self):
|
||||
ledger = self.application.ledger
|
||||
for event_id, sender in (("clock-c", "Alice"), ("clock-d", "Bob")):
|
||||
@@ -1162,6 +1380,134 @@ class LedgerAndApplicationTests(unittest.TestCase):
|
||||
self.assertEqual(ledger.clock_scan_ranking("group-a", "2026-10-13"), [])
|
||||
self.assertEqual(ledger.status()["clock_broadcasts_pending"], 0)
|
||||
|
||||
def test_existing_pending_hourly_broadcast_is_delivered(self):
|
||||
ledger = self.application.ledger
|
||||
for event_id, sender in (("clock-pending-a", "Alice"), ("clock-pending-b", "Bob")):
|
||||
ledger.record_person_observation(
|
||||
event_id=event_id,
|
||||
group_fingerprint="group-a",
|
||||
sender=sender,
|
||||
text="群内聊天记录",
|
||||
observed_at=MODULE.utc_now(),
|
||||
source="test",
|
||||
)
|
||||
person = ledger.resolve_person("group-a", "Bob")
|
||||
self.assertTrue(
|
||||
ledger.reserve_clock_broadcast(
|
||||
group_fingerprint="group-a",
|
||||
slot_key="hourly:2026-10-13T05",
|
||||
local_date="2026-10-13",
|
||||
kind="hourly",
|
||||
message="现在是北京时间 5:00,bob 扫码了。",
|
||||
scan_person_key=str(person["person_key"]),
|
||||
scan_label="bob",
|
||||
)
|
||||
)
|
||||
self.application.deliver = mock.Mock()
|
||||
now = MODULE.datetime(
|
||||
2026,
|
||||
10,
|
||||
13,
|
||||
5,
|
||||
0,
|
||||
tzinfo=MODULE.BEIJING_TIMEZONE,
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
self.application.process_clock_once(
|
||||
now=now,
|
||||
group_fingerprint="group-a",
|
||||
)
|
||||
)
|
||||
|
||||
delivery = self.application.deliver.call_args.args[0]
|
||||
self.assertIn("bob 扫码了", delivery["result_text"])
|
||||
self.assertEqual(
|
||||
ledger.clock_scan_ranking("group-a", "2026-10-13")[0]["display_name"],
|
||||
"bob",
|
||||
)
|
||||
|
||||
def test_scan_boost_pity_forces_target_after_two_misses(self):
|
||||
ledger = self.application.ledger
|
||||
for event_id, sender in (("pity-member-a", "Alice"), ("pity-member-b", "Bob")):
|
||||
ledger.record_person_observation(
|
||||
event_id=event_id,
|
||||
group_fingerprint="group-a",
|
||||
sender=sender,
|
||||
text="群内聊天记录",
|
||||
observed_at=MODULE.utc_now(),
|
||||
source="test",
|
||||
)
|
||||
alice = ledger.resolve_person("group-a", "Alice")
|
||||
bob = ledger.resolve_person("group-a", "Bob")
|
||||
carol = ledger.resolve_person("group-a", "Carol")
|
||||
account = MODULE.normalize_player_account({})
|
||||
account["scan_boosts"] = [
|
||||
{
|
||||
"target_person_key": str(bob["person_key"]),
|
||||
"target_display_name": "bob",
|
||||
"weight": 5,
|
||||
"created_at": MODULE.datetime(
|
||||
2026,
|
||||
10,
|
||||
13,
|
||||
3,
|
||||
30,
|
||||
tzinfo=MODULE.BEIJING_TIMEZONE,
|
||||
).timestamp(),
|
||||
"expires_at": MODULE.datetime(
|
||||
2099,
|
||||
10,
|
||||
14,
|
||||
3,
|
||||
30,
|
||||
tzinfo=MODULE.BEIJING_TIMEZONE,
|
||||
).timestamp(),
|
||||
}
|
||||
]
|
||||
ledger._save_player_account(
|
||||
"group-a",
|
||||
str(alice["person_key"]),
|
||||
account,
|
||||
MODULE.utc_now(),
|
||||
)
|
||||
for hour, person, label in (
|
||||
(4, alice, "alice"),
|
||||
(5, carol, "carol"),
|
||||
):
|
||||
slot_key = f"hourly:2026-10-13T{hour:02d}"
|
||||
self.assertTrue(
|
||||
ledger.reserve_clock_broadcast(
|
||||
group_fingerprint="group-a",
|
||||
slot_key=slot_key,
|
||||
local_date="2026-10-13",
|
||||
kind="hourly",
|
||||
message="test",
|
||||
scan_person_key=str(person["person_key"]),
|
||||
scan_label=label,
|
||||
)
|
||||
)
|
||||
self.assertTrue(ledger.complete_clock_broadcast("group-a", slot_key))
|
||||
now = MODULE.datetime(
|
||||
2026,
|
||||
10,
|
||||
13,
|
||||
6,
|
||||
0,
|
||||
tzinfo=MODULE.BEIJING_TIMEZONE,
|
||||
)
|
||||
|
||||
status = ledger.scan_boost_pity_status("group-a", local_now=now)
|
||||
self.assertTrue(status[0]["guaranteed"])
|
||||
self.assertEqual(status[0]["display_name"], "bob")
|
||||
broadcast = self.application.clock.due_broadcast(
|
||||
now=now,
|
||||
group_fingerprint="group-a",
|
||||
)
|
||||
|
||||
self.assertIsNotNone(broadcast)
|
||||
self.assertEqual(broadcast.scan_label, "bob")
|
||||
|
||||
def test_scan_ranking_command_uses_current_beijing_date(self):
|
||||
ledger = self.application.ledger
|
||||
local_date = MODULE.datetime.now(MODULE.BEIJING_TIMEZONE).date().isoformat()
|
||||
|
||||
Reference in New Issue
Block a user