- 数据层:messages 表增加 part_type 字段,新建 file_versions 表支持版本追踪 - 后端:saveWorkspace 版本追踪、saveAgentOutput 源头分离、generateBriefMessage 成员简报 - 后端:applyDocumentEdit 增量编辑、buildWorkflowStep phase-aware 工作流引擎 - API:文件版本查询/回退接口 - 前端:part_type 驱动渲染,产物面板版本历史 - 新增写手团队(主编/搜索员/策划编辑/合规审查员)配置 - store 模块、scheduler 模块、web-search skill Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
150 lines
4.1 KiB
Go
150 lines
4.1 KiB
Go
package scheduler
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func TestParseCron(t *testing.T) {
|
||
tests := []struct {
|
||
expr string
|
||
wantErr bool
|
||
}{
|
||
{"0 8 * * *", false}, // 每天8点
|
||
{"*/10 * * * *", false}, // 每10分钟
|
||
{"0 9 * * 1", false}, // 每周一9点
|
||
{"30 14 1 * *", false}, // 每月1日14:30
|
||
{"0,30 * * * *", false}, // 每小时0分和30分
|
||
{"0 9-17 * * 1-5", false}, // 工作日9-17点每小时
|
||
{"bad", true}, // 无效
|
||
{"* * *", true}, // 字段不足
|
||
{"60 * * * *", true}, // 分钟超范围
|
||
{"* 25 * * *", true}, // 小时超范围
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
_, err := ParseCron(tt.expr)
|
||
if (err != nil) != tt.wantErr {
|
||
t.Errorf("ParseCron(%q) error=%v, wantErr=%v", tt.expr, err, tt.wantErr)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCronMatch(t *testing.T) {
|
||
// 2026-03-07 是周六(weekday=6)
|
||
base := time.Date(2026, 3, 7, 8, 0, 0, 0, time.Local)
|
||
|
||
tests := []struct {
|
||
expr string
|
||
time time.Time
|
||
want bool
|
||
}{
|
||
// 每天8:00
|
||
{"0 8 * * *", base, true},
|
||
{"0 8 * * *", base.Add(1 * time.Minute), false}, // 8:01 不匹配
|
||
{"0 8 * * *", base.Add(-1 * time.Hour), false}, // 7:00 不匹配
|
||
|
||
// 每10分钟
|
||
{"*/10 * * * *", time.Date(2026, 3, 7, 8, 0, 0, 0, time.Local), true},
|
||
{"*/10 * * * *", time.Date(2026, 3, 7, 8, 10, 0, 0, time.Local), true},
|
||
{"*/10 * * * *", time.Date(2026, 3, 7, 8, 20, 0, 0, time.Local), true},
|
||
{"*/10 * * * *", time.Date(2026, 3, 7, 8, 5, 0, 0, time.Local), false},
|
||
|
||
// 每周一9:00(周六不匹配)
|
||
{"0 9 * * 1", base, false},
|
||
{"0 9 * * 1", time.Date(2026, 3, 9, 9, 0, 0, 0, time.Local), true}, // 周一
|
||
|
||
// 每周六8:00
|
||
{"0 8 * * 6", base, true},
|
||
|
||
// 多值
|
||
{"0,30 * * * *", time.Date(2026, 3, 7, 8, 0, 0, 0, time.Local), true},
|
||
{"0,30 * * * *", time.Date(2026, 3, 7, 8, 30, 0, 0, time.Local), true},
|
||
{"0,30 * * * *", time.Date(2026, 3, 7, 8, 15, 0, 0, time.Local), false},
|
||
|
||
// 范围
|
||
{"0 9-17 * * *", time.Date(2026, 3, 7, 9, 0, 0, 0, time.Local), true},
|
||
{"0 9-17 * * *", time.Date(2026, 3, 7, 17, 0, 0, 0, time.Local), true},
|
||
{"0 9-17 * * *", time.Date(2026, 3, 7, 8, 0, 0, 0, time.Local), false},
|
||
{"0 9-17 * * *", time.Date(2026, 3, 7, 18, 0, 0, 0, time.Local), false},
|
||
|
||
// 步进范围
|
||
{"0 9-17/2 * * *", time.Date(2026, 3, 7, 9, 0, 0, 0, time.Local), true},
|
||
{"0 9-17/2 * * *", time.Date(2026, 3, 7, 11, 0, 0, 0, time.Local), true},
|
||
{"0 9-17/2 * * *", time.Date(2026, 3, 7, 10, 0, 0, 0, time.Local), false},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
expr, err := ParseCron(tt.expr)
|
||
if err != nil {
|
||
t.Fatalf("ParseCron(%q) 解析失败: %v", tt.expr, err)
|
||
}
|
||
got := expr.Match(tt.time)
|
||
if got != tt.want {
|
||
t.Errorf("CronExpr(%q).Match(%s) = %v, want %v",
|
||
tt.expr, tt.time.Format("2006-01-02 15:04 Mon"), got, tt.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestMatchOnce(t *testing.T) {
|
||
now := time.Date(2026, 3, 7, 14, 30, 0, 0, time.Local)
|
||
|
||
tests := []struct {
|
||
expr string
|
||
want bool
|
||
}{
|
||
{"2026-03-07 14:30", true},
|
||
{"2026-03-07 14:31", false},
|
||
{"2026-03-08 14:30", false},
|
||
{"bad-format", false},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
got := matchOnce(tt.expr, now)
|
||
if got != tt.want {
|
||
t.Errorf("matchOnce(%q, %s) = %v, want %v", tt.expr, now.Format("2006-01-02 15:04"), got, tt.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestParseField(t *testing.T) {
|
||
tests := []struct {
|
||
field string
|
||
min int
|
||
max int
|
||
want []int
|
||
wantNil bool
|
||
}{
|
||
{"*", 0, 59, nil, true},
|
||
{"5", 0, 59, []int{5}, false},
|
||
{"1,3,5", 0, 59, []int{1, 3, 5}, false},
|
||
{"1-5", 0, 59, []int{1, 2, 3, 4, 5}, false},
|
||
{"*/15", 0, 59, []int{0, 15, 30, 45}, false},
|
||
{"10-20/3", 0, 59, []int{10, 13, 16, 19}, false},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
got, err := parseField(tt.field, tt.min, tt.max)
|
||
if err != nil {
|
||
t.Errorf("parseField(%q) 失败: %v", tt.field, err)
|
||
continue
|
||
}
|
||
if tt.wantNil {
|
||
if got != nil {
|
||
t.Errorf("parseField(%q) = %v, want nil", tt.field, got)
|
||
}
|
||
continue
|
||
}
|
||
if len(got) != len(tt.want) {
|
||
t.Errorf("parseField(%q) = %v, want %v", tt.field, got, tt.want)
|
||
continue
|
||
}
|
||
for i := range got {
|
||
if got[i] != tt.want[i] {
|
||
t.Errorf("parseField(%q)[%d] = %d, want %d", tt.field, i, got[i], tt.want[i])
|
||
}
|
||
}
|
||
}
|
||
}
|