Files
gosentry/src/scheduler/scheduler_test.go
T
mixeme b1874845d5 T2.3: Split domain.Job (durable) from domain.JobRuntime (transient)
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>
2026-06-19 00:08:05 +03:00

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)
}
}
}