package logic import ( "sort" "testing" ) func TestPlanEvenSpreadMovesPrefersLowerLoadDay(t *testing.T) { tasks := []RefineTaskCandidate{ {TaskItemID: 101, Week: 16, DayOfWeek: 1, SectionFrom: 1, SectionTo: 2, OriginRank: 1}, {TaskItemID: 102, Week: 16, DayOfWeek: 1, SectionFrom: 3, SectionTo: 4, OriginRank: 2}, } slots := []RefineSlotCandidate{ {Week: 12, DayOfWeek: 1, SectionFrom: 1, SectionTo: 2}, {Week: 12, DayOfWeek: 2, SectionFrom: 1, SectionTo: 2}, {Week: 12, DayOfWeek: 3, SectionFrom: 1, SectionTo: 2}, } moves, err := PlanEvenSpreadMoves(tasks, slots, RefineCompositePlanOptions{ ExistingDayLoad: map[string]int{ composeDayKey(12, 1): 5, composeDayKey(12, 2): 1, composeDayKey(12, 3): 0, }, }) if err != nil { t.Fatalf("PlanEvenSpreadMoves 返回错误: %v", err) } if len(moves) != 2 { t.Fatalf("期望移动 2 条,实际=%d", len(moves)) } // 1. 低负载日(周三)应优先被填充; // 2. 第二条应落在次低负载日(周二),而不是高负载日(周一)。 weekDayByID := make(map[int][2]int, len(moves)) for _, move := range moves { weekDayByID[move.TaskItemID] = [2]int{move.ToWeek, move.ToDay} } if got := weekDayByID[101]; got != [2]int{12, 3} { t.Fatalf("任务101应优先落到 W12D3,实际=%v", got) } if got := weekDayByID[102]; got != [2]int{12, 2} { t.Fatalf("任务102应落到 W12D2,实际=%v", got) } } func TestPlanMinContextSwitchMovesGroupsSameContext(t *testing.T) { tasks := []RefineTaskCandidate{ {TaskItemID: 201, Week: 16, DayOfWeek: 1, SectionFrom: 1, SectionTo: 2, ContextTag: "数学", OriginRank: 1}, {TaskItemID: 202, Week: 16, DayOfWeek: 1, SectionFrom: 3, SectionTo: 4, ContextTag: "算法", OriginRank: 2}, {TaskItemID: 203, Week: 16, DayOfWeek: 1, SectionFrom: 5, SectionTo: 6, ContextTag: "数学", OriginRank: 3}, } slots := []RefineSlotCandidate{ {Week: 12, DayOfWeek: 1, SectionFrom: 1, SectionTo: 2}, {Week: 12, DayOfWeek: 1, SectionFrom: 3, SectionTo: 4}, {Week: 12, DayOfWeek: 1, SectionFrom: 5, SectionTo: 6}, } moves, err := PlanMinContextSwitchMoves(tasks, slots, RefineCompositePlanOptions{}) if err != nil { t.Fatalf("PlanMinContextSwitchMoves 返回错误: %v", err) } if len(moves) != 3 { t.Fatalf("期望移动 3 条,实际=%d", len(moves)) } // 1. “数学”有 2 条,分组后应先连续落在最早两个坑位; // 2. 因此 201 与 203 对应的目标节次应是 1-2 与 3-4(顺序由 origin_rank 决定)。 sort.SliceStable(moves, func(i, j int) bool { if moves[i].ToWeek != moves[j].ToWeek { return moves[i].ToWeek < moves[j].ToWeek } if moves[i].ToDay != moves[j].ToDay { return moves[i].ToDay < moves[j].ToDay } return moves[i].ToSectionFrom < moves[j].ToSectionFrom }) if moves[0].TaskItemID != 201 || moves[1].TaskItemID != 203 { t.Fatalf("期望前两个坑位由同上下文任务占据,实际=%+v", moves) } if moves[2].TaskItemID != 202 { t.Fatalf("期望最后一个坑位为算法任务,实际=%+v", moves[2]) } } func TestPlanMinContextSwitchMovesFallsBackToTaskNameWhenAllGeneral(t *testing.T) { tasks := []RefineTaskCandidate{ {TaskItemID: 301, Week: 16, DayOfWeek: 1, SectionFrom: 1, SectionTo: 2, Name: "随机事件与概率基础概念复习", ContextTag: "General", OriginRank: 1}, {TaskItemID: 302, Week: 16, DayOfWeek: 1, SectionFrom: 3, SectionTo: 4, Name: "数制、码制与逻辑代数基础", ContextTag: "General", OriginRank: 2}, {TaskItemID: 303, Week: 16, DayOfWeek: 1, SectionFrom: 5, SectionTo: 6, Name: "第二章 条件概率与全概率公式", ContextTag: "General", OriginRank: 3}, } slots := []RefineSlotCandidate{ {Week: 12, DayOfWeek: 1, SectionFrom: 1, SectionTo: 2}, {Week: 12, DayOfWeek: 1, SectionFrom: 3, SectionTo: 4}, {Week: 12, DayOfWeek: 1, SectionFrom: 5, SectionTo: 6}, } moves, err := PlanMinContextSwitchMoves(tasks, slots, RefineCompositePlanOptions{}) if err != nil { t.Fatalf("PlanMinContextSwitchMoves 返回错误: %v", err) } if len(moves) != 3 { t.Fatalf("期望移动 3 条,实际=%d", len(moves)) } sort.SliceStable(moves, func(i, j int) bool { if moves[i].ToWeek != moves[j].ToWeek { return moves[i].ToWeek < moves[j].ToWeek } if moves[i].ToDay != moves[j].ToDay { return moves[i].ToDay < moves[j].ToDay } return moves[i].ToSectionFrom < moves[j].ToSectionFrom }) if moves[0].TaskItemID != 301 || moves[1].TaskItemID != 303 { t.Fatalf("期望概率任务通过名称兜底连续聚类,实际=%+v", moves) } if moves[2].TaskItemID != 302 { t.Fatalf("期望数电任务落在最后一个坑位,实际=%+v", moves[2]) } } func TestPlanEvenSpreadMovesReturnsErrorWhenSpanNotMatched(t *testing.T) { tasks := []RefineTaskCandidate{ {TaskItemID: 301, Week: 16, DayOfWeek: 1, SectionFrom: 1, SectionTo: 3, OriginRank: 1}, // span=3 } slots := []RefineSlotCandidate{ {Week: 12, DayOfWeek: 1, SectionFrom: 1, SectionTo: 2}, // span=2 } _, err := PlanEvenSpreadMoves(tasks, slots, RefineCompositePlanOptions{}) if err == nil { t.Fatalf("期望 span 不匹配时报错,实际 err=nil") } }