157 lines
6.9 KiB
Python
157 lines
6.9 KiB
Python
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())
|