import asyncio import json import tempfile from datetime import datetime from pathlib import Path from zoneinfo import ZoneInfo from class_monitor.app import daily_summary_period, immediate_notification, sender_is_excluded from class_monitor.llm import Analyzer, _json, is_pure_acknowledgement from class_monitor.storage import Storage class FakeAnalyzer: def __init__(self, analyzer): self.analyzer = analyzer async def call(self, _system, _user): return {"importance": 0, "category": "其他", "need_action": False, "deadline": None, "summary": "普通消息", "reason": "测试"} def test_json(): assert _json('```json\n{"importance":2}\n```')["importance"] == 2 def test_pure_acknowledgement_filter_is_conservative(): assert is_pure_acknowledgement("收到") assert is_pure_acknowledgement("收到,谢谢老师!") assert is_pure_acknowledgement("[表情][表情]") assert is_pure_acknowledgement("👍") assert not is_pure_acknowledgement("收到,请问几点?") assert not is_pure_acknowledgement("收到,孩子明天请假") assert not is_pure_acknowledgement("我没有收到通知") assert not is_pure_acknowledgement("收到", '[{"type":"image"}]') def test_sender_exclusion_is_scoped_to_group_and_sender(): cfg = {"excluded_sender_ids_by_group": {"kindergarten": ["robot"]}} assert sender_is_excluded(cfg, {"group_id": "kindergarten", "user_id": "robot"}) assert not sender_is_excluded(cfg, {"group_id": "primary", "user_id": "robot"}) assert not sender_is_excluded(cfg, {"group_id": "kindergarten", "user_id": "parent"}) def test_immediate_notification_uses_original_text_and_optional_items(): row = { "sender_name": "王老师", "text": "明天请带水彩笔和剪刀。", "summary": "老师通知准备绘画工具", "media_json": "[]", "result_json": json.dumps({"items_to_prepare": ["水彩笔", "剪刀"]}), } title, text = immediate_notification("小学群", row) assert title == "" assert text == "【小学群】 发送人:王老师\n消息:明天请带水彩笔和剪刀。\n准备物品:水彩笔、剪刀" assert "班级消息" not in title def test_immediate_notification_uses_summary_for_non_text_message(): row = { "sender_name": "老师", "text": "[图片]", "summary": "老师发送了一张课程安排图片", "media_json": '[{"type":"image"}]', "result_json": "{}", } title, text = immediate_notification("幼儿园群", row) assert title == "" assert text == "【幼儿园群】 发送人:老师\n摘要:老师发送了一张课程安排图片" assert "消息:" not in text assert "准备物品" not in text def test_daily_summary_period_regular_day(): now = datetime(2026, 9, 5, 19, 0, tzinfo=ZoneInfo("Asia/Singapore")) start, period, title_kind, empty_overview = daily_summary_period(now) assert start == datetime(2026, 9, 5, 0, 0, tzinfo=now.tzinfo) assert period == "2026-09-05" assert title_kind == "日报" assert empty_overview == "今日群内无消息" def test_daily_summary_period_sunday_includes_friday_to_sunday(): now = datetime(2026, 9, 6, 19, 0, tzinfo=ZoneInfo("Asia/Singapore")) start, period, title_kind, empty_overview = daily_summary_period(now) assert start == datetime(2026, 9, 4, 0, 0, tzinfo=now.tzinfo) assert period == "2026-09-04 至 2026-09-06" assert title_kind == "周末总结" assert empty_overview == "周五至周日群内无消息" def test_storage(): async def run(): with tempfile.TemporaryDirectory() as d: s = Storage(str(Path(d) / "x.db")) await s.init() item = {"group_id": "1", "message_id": "2", "user_id": "3", "sender": {"nickname": "老师"}, "time": 1, "raw_message": "明天带材料"} pk, inserted = await s.save_message(item, "明天带材料", []) assert inserted and pk _, inserted2 = await s.save_message(item, "明天带材料", []) assert not inserted2 claimed = await s.claim_pending() assert claimed["text"] == "明天带材料" asyncio.run(run()) def test_receipts_are_excluded_from_context_and_daily_summary(): async def run(): with tempfile.TemporaryDirectory() as d: s = Storage(str(Path(d) / "x.db")) await s.init() receipt = {"group_id": "1", "message_id": "1", "user_id": "p1", "sender": {"nickname": "家长甲"}, "time": 1} useful = {"group_id": "1", "message_id": "2", "user_id": "t1", "sender": {"nickname": "老师"}, "time": 2} current = {"group_id": "1", "message_id": "3", "user_id": "p2", "sender": {"nickname": "家长乙"}, "time": 3} receipt_pk, _ = await s.save_message(receipt, "收到", []) useful_pk, _ = await s.save_message(useful, "明天带材料", []) current_pk, _ = await s.save_message(current, "请问带什么?", []) await s.save_analysis(receipt_pk, {"importance": 0, "category": "回执", "need_action": False, "deadline": None, "summary": "纯确认回复", "reason": "测试"}) await s.save_analysis(useful_pk, {"importance": 2, "category": "材料", "need_action": True, "deadline": None, "summary": "带材料", "reason": "测试"}) context = await s.context({"id": current_pk, "group_id": "1"}, 10) assert [row["text"] for row in context] == ["明天带材料"] rows = await s.messages_for_day("1970-01-01T00:00:00+00:00", "1970-01-02T00:00:00+00:00", "1") assert [row["text"] for row in rows] == ["明天带材料", "请问带什么?"] asyncio.run(run()) def test_parent_question_is_exactly_importance_one(): async def run(): analyzer = Analyzer.__new__(Analyzer) analyzer.cfg = {"teacher_user_ids": [], "teacher_names": []} async def high_importance(_system, _user): return {"importance": 3, "category": "其他", "need_action": False, "deadline": None, "summary": "问题", "reason": "测试"} analyzer.call = high_importance row = {"sender_id": "parent", "sender_name": "家长", "text": "请问明天几点到校?", "raw_json": json.dumps({"sender": {"role": "member"}})} result = await analyzer.analyze(row, []) assert result["importance"] == 1 asyncio.run(run()) def test_group_admin_is_weighted(): async def run(): analyzer = Analyzer.__new__(Analyzer) analyzer.cfg = {"teacher_user_ids": [], "teacher_names": []} analyzer.call = FakeAnalyzer(analyzer).call row = { "sender_id": "3", "sender_name": "群管理员", "text": "大家看一下", "raw_json": json.dumps({"sender": {"role": "admin"}}), } result = await analyzer.analyze(row, []) assert result["importance"] == 1 asyncio.run(run())