From 6d742d41539ce6fd6be416290a2b1a0b10437ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Vi=E1=BA=BFt=20C=C6=B0=E1=BB=9Dng?= <38743533+hoakhongmau98@users.noreply.github.com> Date: Tue, 23 Jun 2026 03:08:44 +0700 Subject: [PATCH] feat(config): add GOCLAW_CRON_JOB_TIMEOUT env var for cron timeout (#1099) Allow overriding the cron job execution timeout via env var, matching the existing GOCLAW_* env var pattern used throughout the codebase. Falls back to the existing default (10m) when unset or invalid, since CronConfig.JobTimeoutDuration() already handles parse errors with a slog.Warn and the DefaultJobTimeout constant. Co-authored-by: will.nguyen Co-authored-by: Claude Opus 4.7 (1M context) --- internal/config/config_load.go | 3 ++ internal/config/config_load_test.go | 53 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/internal/config/config_load.go b/internal/config/config_load.go index cb63775a..2e62ad4a 100644 --- a/internal/config/config_load.go +++ b/internal/config/config_load.go @@ -379,6 +379,9 @@ func (c *Config) applyEnvOverrides() { if c.Tools.Browser.RemoteURL != "" { c.Tools.Browser.Enabled = true } + + // Cron job execution + envStr("GOCLAW_CRON_JOB_TIMEOUT", &c.Cron.JobTimeout) } // Save writes the config to a JSON file. diff --git a/internal/config/config_load_test.go b/internal/config/config_load_test.go index 912c04bc..49e9db49 100644 --- a/internal/config/config_load_test.go +++ b/internal/config/config_load_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "testing" + "time" ) // --- Default --- @@ -429,3 +430,55 @@ func TestLoad_OwnerIDsEmpty(t *testing.T) { } } } + +// --- Cron job timeout env override --- + +func TestLoad_CronJobTimeout_EnvVar(t *testing.T) { + t.Setenv("GOCLAW_CRON_JOB_TIMEOUT", "1h") + + cfg, err := Load("/nonexistent/path") + if err != nil { + t.Fatalf("load error: %v", err) + } + if got := cfg.Cron.JobTimeoutDuration(); got != time.Hour { + t.Fatalf("cron timeout: got %v, want 1h", got) + } +} + +func TestLoad_CronJobTimeout_Invalid_FallsBackToDefault(t *testing.T) { + t.Setenv("GOCLAW_CRON_JOB_TIMEOUT", "not-a-duration") + + cfg, err := Load("/nonexistent/path") + if err != nil { + t.Fatalf("load error: %v", err) + } + if got := cfg.Cron.JobTimeoutDuration(); got != DefaultJobTimeout { + t.Fatalf("invalid duration should fall back to default %v, got %v", DefaultJobTimeout, got) + } +} + +func TestLoad_CronJobTimeout_Unset_UsesDefault(t *testing.T) { + cfg, err := Load("/nonexistent/path") + if err != nil { + t.Fatalf("load error: %v", err) + } + if got := cfg.Cron.JobTimeoutDuration(); got != DefaultJobTimeout { + t.Fatalf("unset env should use default %v, got %v", DefaultJobTimeout, got) + } +} + +func TestLoad_CronJobTimeout_EnvOverridesFile(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "config.json5") + os.WriteFile(cfgPath, []byte(`{"cron":{"job_timeout":"5m"}}`), 0644) + + t.Setenv("GOCLAW_CRON_JOB_TIMEOUT", "30m") + + cfg, err := Load(cfgPath) + if err != nil { + t.Fatalf("load error: %v", err) + } + if got := cfg.Cron.JobTimeoutDuration(); got != 30*time.Minute { + t.Fatalf("env should override file: got %v, want 30m", got) + } +}