Files
felhom-controller/controller/internal/scheduler/runnow_test.go
T
admin e06680b797 Operator actions in the report reply (D1, R-314/R-279/R-177, decision 185)
The hub may ask the running controller for a CLOSED list of actions,
carried in the report ACK (operator_actions) and answered on the next
report (operator_action_results): offsite_backup_now, abandon_stop,
abandon_extend (1-30 days), run_job (fill-watch, offsite-integrity,
offsite-proof, disk-health-check). Unknown action/job/argument -> refused,
nothing called. Once per id (in memory; every action is safe to repeat).

- internal/report/opactions.go: the executor; results re-sent until the
  hub stops listing the id.
- scheduler.RunNow: refuses unknown / already-running jobs; OnDemand(ctx)
  makes an operator's offsite-integrity run even when not due.
- ExtendAbandon never shortens the countdown and refuses in the hub phase;
  StopAbandon reports failure when the hub cancel failed (it said success).
- Report ACK read cap 4 KiB -> 64 KiB (an ACK over the cap dropped every
  field in it).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-08 14:49:27 +02:00

77 lines
2.1 KiB
Go

package scheduler
import (
"context"
"errors"
"testing"
"time"
)
// R-314/R-177 (`09` §3 decision 185): RunNow runs a registered job once, now, and refuses an unknown
// name and a job already running — the operator's run_job action rests on these three answers.
func TestRunNow_RunsOnceAndReportsCompletion(t *testing.T) {
s := discardScheduler()
calls := 0
var sawOnDemand bool
s.Daily("fill-watch", farFuture(5), func(ctx context.Context) error {
calls++
sawOnDemand = OnDemand(ctx)
return nil
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
s.Start(ctx)
defer s.Stop()
done, err := s.RunNow("fill-watch")
if err != nil {
t.Fatalf("RunNow(fill-watch): %v", err)
}
select {
case jerr := <-done:
if jerr != nil {
t.Fatalf("job error = %v", jerr)
}
case <-time.After(5 * time.Second):
t.Fatal("RunNow never completed")
}
if calls != 1 || !sawOnDemand {
t.Fatalf("calls=%d onDemand=%v, want 1 and true", calls, sawOnDemand)
}
if j := s.GetJobs()[0]; j.Running || j.LastRun.IsZero() {
t.Fatalf("after RunNow: Running=%v LastRun=%v — want false and set", j.Running, j.LastRun)
}
}
func TestRunNow_RefusesUnknownRunningAndUnstarted(t *testing.T) {
s := discardScheduler()
release := make(chan struct{})
s.Daily("offsite-integrity", farFuture(5), func(ctx context.Context) error { <-release; return nil })
if _, err := s.RunNow("offsite-integrity"); !errors.Is(err, ErrNotStarted) {
t.Fatalf("before Start: err=%v, want ErrNotStarted", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
s.Start(ctx)
defer s.Stop()
if _, err := s.RunNow("no-such-job"); !errors.Is(err, ErrUnknownJob) {
t.Fatalf("unknown: err=%v, want ErrUnknownJob", err)
}
done, err := s.RunNow("offsite-integrity")
if err != nil {
t.Fatalf("first RunNow: %v", err)
}
if _, err := s.RunNow("offsite-integrity"); !errors.Is(err, ErrJobRunning) {
t.Fatalf("second RunNow while running: err=%v, want ErrJobRunning", err)
}
close(release)
<-done
// Released: it may run again.
done2, err := s.RunNow("offsite-integrity")
if err != nil {
t.Fatalf("RunNow after completion: %v", err)
}
<-done2
}