b1874845d5
Move all transient execution state off domain.Job into a new domain.JobRuntime, keyed by job ID: - domain: Job now holds only durable YAML fields; remove the yaml:"-" fields (LastRun/NextRun/LastState/Logs/Output) and NextDue. Add runtime.go with JobRuntime plus NewRuntime/NewRuntimes constructors, which now own the runtime-init logic moved out of normalizeJobs. - runner: RunJob no longer mutates the job; it is pure and returns the RunRecord for the caller to fold into the runtime. - scheduler: take a shared map[int]*JobRuntime and route status/next-run bookkeeping through runtimeFor(job); prepareNextRun writes a *JobRuntime. - storage: normalizeJobs touches only durable config. - gui: own the runtime map (NewRuntimes), share it with the scheduler, and read/write runtime state via runtimeFor; maintain the map by ID on add/edit/delete. - tests: update scheduler/storage tests to the split; tidy a pre-existing import-order nit in scheduler.go. This also satisfies T2.4 (storage load/save only Job, runtime init in domain.NewRuntime, round-trip tests), since removing the fields forced it. Runtime-map ownership remains GUI-side glue until T3.1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
70 lines
2.1 KiB
Go
70 lines
2.1 KiB
Go
package scheduler
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.mixdep.ru/mix/gosentry/src/domain"
|
|
)
|
|
|
|
func TestPrepareNextRunSetsDisplayString(t *testing.T) {
|
|
jobs := []domain.Job{{Schedule: "*/5 * * * *", Enabled: true}}
|
|
s := &Scheduler{jobs: &jobs, runtimes: domain.NewRuntimes(jobs), schedules: make(map[int]domain.Schedule)}
|
|
s.parseJobSchedule(&jobs[0])
|
|
runtime := s.runtimeFor(&jobs[0])
|
|
from := time.Date(2026, 6, 14, 12, 3, 0, 0, time.UTC)
|
|
|
|
s.prepareNextRun(&jobs[0], runtime, from)
|
|
|
|
want := "2026-06-14 12:05:00"
|
|
if runtime.NextRun != want {
|
|
t.Errorf("NextRun: got %q, want %q", runtime.NextRun, want)
|
|
}
|
|
wantDue := time.Date(2026, 6, 14, 12, 5, 0, 0, time.UTC)
|
|
if !runtime.NextDue.Equal(wantDue) {
|
|
t.Errorf("NextDue: got %v, want %v", runtime.NextDue, wantDue)
|
|
}
|
|
}
|
|
|
|
func TestPrepareNextRunSetsInvalidScheduleLabel(t *testing.T) {
|
|
jobs := []domain.Job{{Schedule: "not-a-cron", Enabled: true}}
|
|
s := &Scheduler{jobs: &jobs, runtimes: domain.NewRuntimes(jobs), schedules: make(map[int]domain.Schedule)}
|
|
// parseJobSchedule will drop the invalid spec, so schedules map stays empty.
|
|
s.parseJobSchedule(&jobs[0])
|
|
runtime := s.runtimeFor(&jobs[0])
|
|
|
|
s.prepareNextRun(&jobs[0], runtime, time.Now())
|
|
|
|
if runtime.NextRun != "Invalid schedule" {
|
|
t.Errorf("NextRun: got %q, want 'Invalid schedule'", runtime.NextRun)
|
|
}
|
|
if !runtime.NextDue.IsZero() {
|
|
t.Errorf("NextDue should be zero for invalid schedule, got %v", runtime.NextDue)
|
|
}
|
|
}
|
|
|
|
func TestRunningOutputIncludesInvocation(t *testing.T) {
|
|
started := time.Date(2026, 6, 17, 23, 40, 0, 0, time.Local)
|
|
job := domain.Job{
|
|
Name: "Backup",
|
|
Command: `C:\Program Files\FreeFileSync\FreeFileSync.exe`,
|
|
Arguments: `D:\Local\Jobs\Auto.ffs_batch`,
|
|
SuccessExitCodes: "0,1",
|
|
}
|
|
|
|
output := runningOutput(job, "Manual", started)
|
|
for _, want := range []string{
|
|
"Running since 2026-06-17 23:40:00",
|
|
"Manual",
|
|
job.Command,
|
|
job.Arguments,
|
|
"0,1",
|
|
"start_only",
|
|
} {
|
|
if !strings.Contains(output, want) {
|
|
t.Fatalf("expected running output to contain %q, got:\n%s", want, output)
|
|
}
|
|
}
|
|
}
|