package lua import ( "testing" "time" ) // 「启动即跑一轮」判据。 // // 为什么需要它(照参考的事后复盘):只用 interval 时,宿主重启后短寿命凭据 // (cline 1h / codearts ~2h / raccoon 3h)要等满一个周期才续期,期间一直显示 // 「已过期」、积分行 401。修复 = 声明 first_due_now 的任务在启动后短暂延迟内 // 先跑一轮。 // // 但它**必须 opt-in**:真正周期性的任务(如每日签到)不该因为重启就重跑。 func TestScheduleFirstDueNow(t *testing.T) { // ① 声明了 first_due_now 的任务:首轮应在 catchUpDelay 内,而不是等一个周期 now := time.Now() catchUp := ScheduledTask{Fn: "f", Every: 7200, FirstDueNow: true} rt := &PluginRuntime{} d := rt.firstDue(catchUp) if d.After(now.Add(catchUpDelay + time.Second)) { t.Errorf("first_due_now 的任务首轮应在 %v 内,实得 %v 后", catchUpDelay, d.Sub(now)) } if d.Before(now) { t.Errorf("首轮时间不该在过去:%v", d.Sub(now)) } // ② 没声明的:必须等满一个周期(否则每日/每小时的语义会被破坏) plain := ScheduledTask{Fn: "f", Every: 7200} d2 := rt.firstDue(plain) if d2.Sub(now) < 7000*time.Second { t.Errorf("未声明 first_due_now 的任务不该提前跑:实得 %v", d2.Sub(now)) } // ③ at 形式(每日固定时刻)不受标记影响 —— 它本来就该等到那个时刻 daily := ScheduledTask{Fn: "f", At: "08:00"} d3 := rt.firstDue(daily) if d3.Sub(now) > 25*time.Hour { t.Errorf("每日任务的首次时间超出一天:%v", d3.Sub(now)) } if d3.Sub(now) < catchUpDelay && !daily.FirstDueNow { // 只有当 08:00 恰好在 catchUpDelay 内才可能,属正常 t.Logf("每日任务首轮临近(可能恰好在 08:00 附近):%v", d3.Sub(now)) } // ④ 声明了 first_due_now 的每日任务:优先补跑(凭据不等时刻) dailyCatchUp := ScheduledTask{Fn: "f", At: "08:00", FirstDueNow: true} d4 := rt.firstDue(dailyCatchUp) if d4.Sub(now) > catchUpDelay+time.Second { t.Errorf("声明 first_due_now 时即便有 at 也应先补跑:%v", d4.Sub(now)) } } // 解析层判据:first_due_now 必须真的从插件源码读出来(写错键名会静默变成 // false —— 那就是"看起来配了、实际没补跑")。 func TestScheduleParsesFirstDueNow(t *testing.T) { const src = ` local plugin = { name = "schedprobe", version = "1.0" } plugin.exports = { "noop" } function plugin.noop() end function plugin.refresh_creds() end function plugin.auto_claim() end plugin.schedule = { { fn = "auto_claim", every = 86400, at = "08:00", jitter = 300 }, { fn = "refresh_creds", every = 1800, first_due_now = true, jitter = 60 }, } return plugin ` _, ps, _ := runtimePlugins(t) if err := ps.LoadSource("schedprobe", src); err != nil { t.Fatal(err) } ps.mu.RLock() var sched []ScheduledTask for _, p := range ps.plugins { if p.Info.Name == "schedprobe" { sched = p.Schedule } } ps.mu.RUnlock() if len(sched) != 2 { t.Fatalf("应解析出 2 个任务,实得 %d: %+v", len(sched), sched) } byFn := map[string]ScheduledTask{} for _, task := range sched { byFn[task.Fn] = task } if task, ok := byFn["refresh_creds"]; !ok { t.Fatal("refresh_creds 任务未解析") } else if !task.FirstDueNow { t.Error("first_due_now 未被解析为 true(键名写错会静默失效)") } else if task.Every != 1800 { t.Errorf("every = %d,应为 1800", task.Every) } if task, ok := byFn["auto_claim"]; !ok { t.Fatal("auto_claim 任务未解析") } else if task.FirstDueNow { t.Error("auto_claim 未声明 first_due_now,应为 false(每日任务不该因重启重跑)") } else if task.At != "08:00" { t.Errorf("at = %q,应为 08:00", task.At) } }