package db import ( "errors" "testing" "time" ) func TestAutoCycleDefaultsFromMigration(t *testing.T) { d, ctx := newTestDB(t) a, err := d.GetAutoCycle(ctx) if err != nil { t.Fatalf("get auto cycle: %v", err) } if a.Enabled { t.Fatalf("expected disabled by default") } if a.IntervalSeconds != 3600 { t.Fatalf("expected default interval 3600, got %d", a.IntervalSeconds) } if a.MaxRunSeconds != 0 { t.Fatalf("expected default max_run_seconds 0, got %d", a.MaxRunSeconds) } if a.Phase != AutoCyclePhaseIdle { t.Fatalf("expected phase idle, got %q", a.Phase) } if a.RunStartedAt != nil || a.NextRunAt != nil || a.LastRunStartedAt != nil || a.LastRunFinishedAt != nil { t.Fatalf("expected all timestamps unset, got %+v", a) } if a.LastOutcome != "" || a.LastError != "" || a.LastScannedFree != 0 || a.RunsTotal != 0 { t.Fatalf("expected empty last-run info, got %+v", a) } } func TestAutoCycleParamsRoundTripAndPartialUpdate(t *testing.T) { d, ctx := newTestDB(t) interval, maxRun := 120, 900 if err := d.SetAutoCycleParams(ctx, &interval, &maxRun); err != nil { t.Fatalf("set params: %v", err) } a, err := d.GetAutoCycle(ctx) if err != nil { t.Fatalf("get: %v", err) } if a.IntervalSeconds != 120 || a.MaxRunSeconds != 900 { t.Fatalf("expected 120/900, got %d/%d", a.IntervalSeconds, a.MaxRunSeconds) } // Partial: only max_run_seconds changes. newMax := 0 if err := d.SetAutoCycleParams(ctx, nil, &newMax); err != nil { t.Fatalf("partial set: %v", err) } a, _ = d.GetAutoCycle(ctx) if a.IntervalSeconds != 120 || a.MaxRunSeconds != 0 { t.Fatalf("expected 120/0 after partial update, got %d/%d", a.IntervalSeconds, a.MaxRunSeconds) } // Partial: only interval changes. newInterval := 60 if err := d.SetAutoCycleParams(ctx, &newInterval, nil); err != nil { t.Fatalf("partial set interval: %v", err) } a, _ = d.GetAutoCycle(ctx) if a.IntervalSeconds != 60 || a.MaxRunSeconds != 0 { t.Fatalf("expected 60/0, got %d/%d", a.IntervalSeconds, a.MaxRunSeconds) } } func TestAutoCycleParamsValidation(t *testing.T) { d, ctx := newTestDB(t) low := 59 if err := d.SetAutoCycleParams(ctx, &low, nil); !errors.Is(err, ErrValidation) { t.Fatalf("expected ErrValidation for interval 59, got %v", err) } neg := -1 if err := d.SetAutoCycleParams(ctx, nil, &neg); !errors.Is(err, ErrValidation) { t.Fatalf("expected ErrValidation for negative max_run_seconds, got %v", err) } // A rejected request must not partially apply the valid half. ok := 300 if err := d.SetAutoCycleParams(ctx, &ok, &neg); !errors.Is(err, ErrValidation) { t.Fatalf("expected ErrValidation, got %v", err) } a, _ := d.GetAutoCycle(ctx) if a.IntervalSeconds != 3600 { t.Fatalf("interval must stay 3600 after rejected update, got %d", a.IntervalSeconds) } } func TestAutoCycleEnabledAndStateRoundTrip(t *testing.T) { d, ctx := newTestDB(t) next := Now() if err := d.SetAutoCycleEnabled(ctx, true, &next, ""); err != nil { t.Fatalf("enable: %v", err) } a, _ := d.GetAutoCycle(ctx) if !a.Enabled || a.Phase != AutoCyclePhaseIdle { t.Fatalf("expected enabled+idle, got %+v", a) } if a.NextRunAt == nil || !a.NextRunAt.Equal(next) { t.Fatalf("expected next_run_at=%v, got %v", next, a.NextRunAt) } started := next.Add(time.Second) finished := next.Add(time.Minute) nextRun := finished.Add(time.Hour) st := AutoCycleState{ Phase: AutoCyclePhaseWaiting, NextRunAt: &nextRun, LastRunStartedAt: &started, LastRunFinishedAt: &finished, LastOutcome: AutoCycleOutcomeCompleted, LastError: "boom", LastScannedFree: 3, RunsTotal: 7, } if err := d.UpdateAutoCycleState(ctx, st); err != nil { t.Fatalf("update state: %v", err) } a, _ = d.GetAutoCycle(ctx) if !a.Enabled { t.Fatalf("UpdateAutoCycleState must not touch the enabled flag") } if a.Phase != AutoCyclePhaseWaiting || a.LastOutcome != AutoCycleOutcomeCompleted || a.LastError != "boom" || a.LastScannedFree != 3 || a.RunsTotal != 7 { t.Fatalf("state mismatch: %+v", a) } if a.RunStartedAt != nil { t.Fatalf("expected run_started_at nil, got %v", a.RunStartedAt) } if a.NextRunAt == nil || !a.NextRunAt.Equal(nextRun) || a.LastRunStartedAt == nil || !a.LastRunStartedAt.Equal(started) || a.LastRunFinishedAt == nil || !a.LastRunFinishedAt.Equal(finished) { t.Fatalf("timestamp mismatch: %+v", a) } // Disable with an outcome: phase back to idle, outcome recorded, // last_error cleared, counters preserved. if err := d.SetAutoCycleEnabled(ctx, false, nil, AutoCycleOutcomeStopped); err != nil { t.Fatalf("disable: %v", err) } a, _ = d.GetAutoCycle(ctx) if a.Enabled || a.Phase != AutoCyclePhaseIdle || a.LastOutcome != AutoCycleOutcomeStopped || a.LastError != "" || a.NextRunAt != nil || a.RunsTotal != 7 { t.Fatalf("unexpected state after disable: %+v", a) } if err := d.UpdateAutoCycleState(ctx, AutoCycleState{Phase: "bogus"}); !errors.Is(err, ErrValidation) { t.Fatalf("expected ErrValidation for bogus phase, got %v", err) } }