Version: 0.7.2.dev.260322

feat(schedule-plan):  重构智能排程链路并修复粗排双节对齐问题

-  新增“对话级排程状态持久化”能力:引入 `agent_schedule_states` 模型/DAO,并接入启动迁移
-  智能排程图升级:补齐小幅微调(quick refine)分支,完善预算/并发/状态字段流转
-  预览链路增强:完善排程预览服务读写与桥接逻辑,新增本地预览页 `infra/schedule_preview_viewer.html`
- ♻️ 缓存治理统一:将相关缓存处理收口到 DAO + `cache_deleter` 联动清理,移除旧散落逻辑
- 🐛 修复粗排核心 bug:禁止单节降级,强制双节并按 `1-2/3-4/...` 对齐;修复结束日扫描边界问题
-  新增粗排回归测试:覆盖孤立单节、偶数起点双节、Filler 对齐等关键场景
This commit is contained in:
Losita
2026-03-22 13:50:10 +08:00
parent f3f9902e93
commit e5b27df80d
20 changed files with 1961 additions and 166 deletions

View File

@@ -0,0 +1,154 @@
package logic
import (
"testing"
"github.com/LoveLosita/smartflow/backend/model"
"github.com/LoveLosita/smartflow/backend/respond"
)
// newTestGrid 创建仅用于单测的最小 grid。
//
// 职责边界:
// 1. 只负责初始化时间窗口与 data 容器;
// 2. 不负责填充节次状态(由各测试用例自行设置)。
func newTestGrid(startWeek, startDay, endWeek, endDay int) *grid {
return &grid{
data: make(map[int]map[int][13]slotNode),
startWeek: startWeek,
startDay: startDay,
endWeek: endWeek,
endDay: endDay,
}
}
// setDayStatus 批量设置某一天 1~12 节的状态。
func setDayStatus(g *grid, week, day int, status slotStatus) {
for s := 1; s <= 12; s++ {
g.setNode(week, day, s, slotNode{Status: status})
}
}
// setSectionStatus 设置单个节次状态。
func setSectionStatus(g *grid, week, day, section int, status slotStatus) {
g.setNode(week, day, section, slotNode{Status: status})
}
// TestComputeAllocation_SkipIsolatedOneSlot 验证“孤立 1 节”不会被错误写成任务。
//
// 用例意图:
// 1. 第一天只放一个孤立可用节次10 节),后继 11 节被屏蔽;
// 2. 第二天提供一个合法的连续 2 节1-2 节);
// 3. 期望算法跳过第一天孤立节次,把任务落到第二天 1-2 节。
func TestComputeAllocation_SkipIsolatedOneSlot(t *testing.T) {
g := newTestGrid(1, 1, 1, 2)
// 1. 先全部置为 Blocked避免默认 Free 干扰本用例。
setDayStatus(g, 1, 1, Blocked)
setDayStatus(g, 1, 2, Blocked)
// 2. 构造“孤立 1 节 + 合法 2 节”场景。
setSectionStatus(g, 1, 1, 10, Free) // 第一天仅 10 节可用11/12 仍然 Blocked。
setSectionStatus(g, 1, 2, 1, Free)
setSectionStatus(g, 1, 2, 2, Free)
items := []model.TaskClassItem{{ID: 1}}
got, err := computeAllocation(g, items, "rapid")
if err != nil {
t.Fatalf("期望分配成功,实际报错: %v", err)
}
if len(got) != 1 || got[0].EmbeddedTime == nil {
t.Fatalf("期望回填 1 条 EmbeddedTime实际: %+v", got)
}
tt := got[0].EmbeddedTime
if tt.Week != 1 || tt.DayOfWeek != 2 || tt.SectionFrom != 1 || tt.SectionTo != 2 {
t.Fatalf("期望落位到 W1D2 1-2 节,实际: week=%d day=%d from=%d to=%d",
tt.Week, tt.DayOfWeek, tt.SectionFrom, tt.SectionTo)
}
}
// TestComputeAllocation_RejectAllIsolatedSlots 验证“全是孤立 1 节”时应返回时间不足。
//
// 用例意图:
// 1. 虽然总可用节次数量达到 2但它们分散成两个孤立 1 节;
// 2. 业务要求普通任务默认必须 2 连续节,因此应整体失败而不是偷偷降级为 1 节。
func TestComputeAllocation_RejectAllIsolatedSlots(t *testing.T) {
g := newTestGrid(1, 1, 1, 2)
// 1. 先全部置为 Blocked。
setDayStatus(g, 1, 1, Blocked)
setDayStatus(g, 1, 2, Blocked)
// 2. 仅放两个彼此分离的孤立可用节次。
setSectionStatus(g, 1, 1, 10, Free)
setSectionStatus(g, 1, 2, 10, Free)
items := []model.TaskClassItem{{ID: 1}}
_, err := computeAllocation(g, items, "rapid")
if err == nil {
t.Fatalf("期望返回时间不足错误,实际为 nil")
}
if err.Error() != respond.TimeNotEnoughForAutoScheduling.Error() {
t.Fatalf("期望错误=%s实际=%s", respond.TimeNotEnoughForAutoScheduling.Error(), err.Error())
}
}
// TestComputeAllocation_RejectEvenStartPair 验证偶数起点双节(如 8-9不允许作为粗排结果。
//
// 用例意图:
// 1. 构造一个看似连续的 8-9 空位;
// 2. 同时给出一个合法的 11-12 对齐空位;
// 3. 期望算法跳过 8-9选择 11-12。
func TestComputeAllocation_RejectEvenStartPair(t *testing.T) {
g := newTestGrid(1, 1, 1, 1)
// 1. 全部先置为 Blocked避免默认 Free 干扰判断。
setDayStatus(g, 1, 1, Blocked)
// 2. 构造“偶数起点双节 + 合法奇数起点双节”。
setSectionStatus(g, 1, 1, 8, Free)
setSectionStatus(g, 1, 1, 9, Free)
setSectionStatus(g, 1, 1, 11, Free)
setSectionStatus(g, 1, 1, 12, Free)
items := []model.TaskClassItem{{ID: 1}}
got, err := computeAllocation(g, items, "rapid")
if err != nil {
t.Fatalf("期望分配成功,实际报错: %v", err)
}
if got[0].EmbeddedTime == nil {
t.Fatalf("期望回填 EmbeddedTime实际为 nil")
}
tt := got[0].EmbeddedTime
if tt.SectionFrom != 11 || tt.SectionTo != 12 {
t.Fatalf("期望落位到 11-12实际落位到 %d-%d", tt.SectionFrom, tt.SectionTo)
}
}
// TestComputeAllocation_FillerNeedOddEvenPair 验证 Filler 课程块也必须满足奇数起点双节对齐。
//
// 用例意图:
// 1. 仅提供一个 Filler 课程块 8-9偶数起点
// 2. 即使总可用节数为 2也不能被当作合法落位
// 3. 期望返回时间不足错误。
func TestComputeAllocation_FillerNeedOddEvenPair(t *testing.T) {
g := newTestGrid(1, 1, 1, 1)
// 1. 全部先置为 Blocked。
setDayStatus(g, 1, 1, Blocked)
// 2. 课程块 8-9 标记为 Filler但其起点为偶数不满足对齐规则。
g.setNode(1, 1, 8, slotNode{Status: Filler, EventID: 1001})
g.setNode(1, 1, 9, slotNode{Status: Filler, EventID: 1001})
items := []model.TaskClassItem{{ID: 1}}
_, err := computeAllocation(g, items, "rapid")
if err == nil {
t.Fatalf("期望返回时间不足错误,实际为 nil")
}
if err.Error() != respond.TimeNotEnoughForAutoScheduling.Error() {
t.Fatalf("期望错误=%s实际=%s", respond.TimeNotEnoughForAutoScheduling.Error(), err.Error())
}
}