e06680b797
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
77 lines
2.1 KiB
Go
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
|
|
}
|