You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

166 lines
6.4 KiB

package memory
import (
"testing"
"time"
"yunyan/comm"
"yunyan/pb"
)
func mkItem(cat, date, clock, rule string, weekday int32, ahead int32) *pb.DBMemoryItem {
return &pb.DBMemoryItem{
Id: 1, Category: cat, HappenDate: date, HappenTime: clock,
RepeatRule: rule, Weekday: weekday, RemindAhead: ahead,
State: pb.MemoryState_MemoryState_Pending, Tz: "Asia/Shanghai",
}
}
// 一次性项:只在那一天产生一个时刻,且要扣掉提前量
func TestExpandItem_Once(t *testing.T) {
loc, _ := time.LoadLocation("Asia/Shanghai")
from := time.Date(2026, 9, 5, 8, 0, 0, 0, loc)
it := mkItem(comm.MemoryCatAlarm, "2026-09-06", "09:00", comm.MemoryRepeatOnce, 0, 5)
slots := expandItem(it, from, 7, loc)
if len(slots) != 1 {
t.Fatalf("一次性项应产生 1 个时刻,得到 %d", len(slots))
}
want := time.Date(2026, 9, 6, 8, 55, 0, 0, loc).Unix()
if slots[0].RemindAt != want {
t.Errorf("提醒时刻应为 08:55(09:00 提前5分钟),得到 %s",
time.Unix(slots[0].RemindAt, 0).In(loc).Format("2006-01-02 15:04"))
}
}
// 已经过去的一次性项不该再排。排了就是 App 一打开立刻弹一堆过期提醒。
func TestExpandItem_SkipsPast(t *testing.T) {
loc, _ := time.LoadLocation("Asia/Shanghai")
from := time.Date(2026, 9, 10, 8, 0, 0, 0, loc)
it := mkItem(comm.MemoryCatAlarm, "2026-09-06", "09:00", comm.MemoryRepeatOnce, 0, 5)
if slots := expandItem(it, from, 7, loc); len(slots) != 0 {
t.Errorf("过期项不该产生时刻,得到 %d 个", len(slots))
}
}
// 重复项是 v0.1 设计的坑:一个 remind_at 字段表达不了「每天」。
// 展开必须给出窗口内的每一天。
func TestExpandItem_Daily(t *testing.T) {
loc, _ := time.LoadLocation("Asia/Shanghai")
from := time.Date(2026, 9, 5, 8, 0, 0, 0, loc)
it := mkItem(comm.MemoryCatAlarm, "2026-09-01", "09:00", comm.MemoryRepeatDaily, 0, 5)
slots := expandItem(it, from, 7, loc)
if len(slots) != 7 {
t.Fatalf("每天重复在 7 天窗口内应有 7 个时刻,得到 %d", len(slots))
}
// 今天 09:00 还没到(from 是 08:00),所以第一个应该是今天
first := time.Unix(slots[0].RemindAt, 0).In(loc)
if first.Format("2006-01-02") != "2026-09-05" {
t.Errorf("第一个时刻应是今天 2026-09-05,得到 %s", first.Format("2006-01-02"))
}
}
// 每周:锚点日远在过去时也必须能算出未来的时刻,
// 否则去年设的闹钟今年就不响了。
func TestExpandItem_WeeklyWithOldAnchor(t *testing.T) {
loc, _ := time.LoadLocation("Asia/Shanghai")
from := time.Date(2026, 9, 5, 8, 0, 0, 0, loc) // 周六
// 锚点在一年前,规则是每周一
it := mkItem(comm.MemoryCatAlarm, "2025-09-01", "09:00", comm.MemoryRepeatWeekly, 1, 5)
slots := expandItem(it, from, 14, loc)
if len(slots) != 2 {
t.Fatalf("14 天窗口内每周一应有 2 个时刻,得到 %d", len(slots))
}
for _, s := range slots {
d := time.Unix(s.HappenAt, 0).In(loc)
if d.Weekday() != time.Monday {
t.Errorf("每周一的时刻落在了 %v (%s)", d.Weekday(), d.Format("2006-01-02"))
}
}
}
// 灵感与花销是「记下来」不是「提醒我」,不该排提醒
func TestExpandItem_IdeaAndExpenseNeverRemind(t *testing.T) {
loc, _ := time.LoadLocation("Asia/Shanghai")
from := time.Date(2026, 9, 5, 8, 0, 0, 0, loc)
for _, cat := range []string{comm.MemoryCatIdea, comm.MemoryCatExpense} {
it := mkItem(cat, "2026-09-06", "09:00", comm.MemoryRepeatOnce, 0, 5)
if slots := expandItem(it, from, 7, loc); len(slots) != 0 {
t.Errorf("%s 不该产生提醒,得到 %d 个", cat, len(slots))
}
}
}
// remind_ahead=0 表示不提醒
func TestExpandItem_NoRemindWhenAheadZero(t *testing.T) {
loc, _ := time.LoadLocation("Asia/Shanghai")
from := time.Date(2026, 9, 5, 8, 0, 0, 0, loc)
it := mkItem(comm.MemoryCatTodo, "2026-09-06", "09:00", comm.MemoryRepeatOnce, 0, 0)
if slots := expandItem(it, from, 7, loc); len(slots) != 0 {
t.Errorf("remind_ahead=0 不该产生提醒,得到 %d 个", len(slots))
}
}
// 已完成的项不再提醒
func TestExpandItem_DoneNeverRemind(t *testing.T) {
loc, _ := time.LoadLocation("Asia/Shanghai")
from := time.Date(2026, 9, 5, 8, 0, 0, 0, loc)
it := mkItem(comm.MemoryCatTodo, "2026-09-06", "09:00", comm.MemoryRepeatOnce, 0, 5)
it.State = pb.MemoryState_MemoryState_Done
if slots := expandItem(it, from, 7, loc); len(slots) != 0 {
t.Errorf("已完成项不该产生提醒,得到 %d 个", len(slots))
}
}
// 没有 happen_time 的全天项按 9:00 计,不能落在 0:00 半夜吵人
func TestExpandItem_AllDayUsesNineAM(t *testing.T) {
loc, _ := time.LoadLocation("Asia/Shanghai")
from := time.Date(2026, 9, 5, 0, 30, 0, 0, loc)
it := mkItem(comm.MemoryCatTodo, "2026-09-05", "", comm.MemoryRepeatOnce, 0, 5)
slots := expandItem(it, from, 7, loc)
if len(slots) != 1 {
t.Fatalf("应有 1 个时刻,得到 %d", len(slots))
}
h := time.Unix(slots[0].HappenAt, 0).In(loc)
if h.Hour() != 9 || h.Minute() != 0 {
t.Errorf("全天项应按 09:00 计,得到 %02d:%02d", h.Hour(), h.Minute())
}
}
// expandAll 必须按时间升序:客户端要按 iOS 的 64 条上限截断,
// 顺序错了会把明天的排掉、留下一周后的
func TestExpandAll_SortedAscending(t *testing.T) {
items := []*pb.DBMemoryItem{
mkItem(comm.MemoryCatAlarm, "2026-09-30", "09:00", comm.MemoryRepeatOnce, 0, 5),
mkItem(comm.MemoryCatAlarm, "2026-09-06", "09:00", comm.MemoryRepeatDaily, 0, 5),
}
items[0].Id, items[1].Id = 10, 20
slots := expandAll(items, 30, "Asia/Shanghai")
for i := 1; i < len(slots); i++ {
if slots[i-1].RemindAt > slots[i].RemindAt {
t.Fatalf("展开结果未按时间升序:第 %d 个 %d > 第 %d 个 %d",
i-1, slots[i-1].RemindAt, i, slots[i].RemindAt)
}
}
}
// 单条项按它自己的 tz 算,不跟着请求方的时区走:
// 「明早 8 点」不该因为用户落地伦敦就变成伦敦时间 8 点
func TestExpandItem_UsesItemOwnTimezone(t *testing.T) {
it := mkItem(comm.MemoryCatAlarm, "2026-09-06", "09:00", comm.MemoryRepeatOnce, 0, 0)
it.RemindAhead = 5
it.Tz = "Asia/Shanghai"
// 请求方声称在伦敦,但项自己带了上海时区
slots := expandAll([]*pb.DBMemoryItem{it}, 30, "Europe/London")
if len(slots) != 1 {
t.Fatalf("应有 1 个时刻,得到 %d", len(slots))
}
sh, _ := time.LoadLocation("Asia/Shanghai")
got := time.Unix(slots[0].HappenAt, 0).In(sh)
if got.Hour() != 9 {
t.Errorf("应按项自己的上海时区算出 09:00,得到 %02d:%02d", got.Hour(), got.Minute())
}
}