diff --git a/internal/cli/config.go b/internal/cli/config.go index 078ac55..8592dfa 100644 --- a/internal/cli/config.go +++ b/internal/cli/config.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/exec" + "time" "github.com/CoreyRDean/intent/internal/config" "github.com/CoreyRDean/intent/internal/state" @@ -107,6 +108,12 @@ func lookupKnown(c *config.Config, key string) string { return c.UpdateChannel case "auto_update": return fmt.Sprintf("%t", c.AutoUpdate) + case "daemon.enabled", "daemon_enabled": + return fmt.Sprintf("%t", c.DaemonEnabled) + case "daemon.idle_unload_after", "daemon_idle_unload_after": + return c.DaemonIdleUnloadAfter.String() + case "cache.enabled", "cache_enabled": + return fmt.Sprintf("%t", c.CacheEnabled) } return "" } @@ -123,5 +130,13 @@ func setKnown(c *config.Config, key, value string) { c.UpdateChannel = value case "auto_update": c.AutoUpdate = value == "true" || value == "yes" + case "daemon.enabled", "daemon_enabled": + c.DaemonEnabled = value == "true" || value == "yes" + case "daemon.idle_unload_after", "daemon_idle_unload_after": + if d, err := time.ParseDuration(value); err == nil { + c.DaemonIdleUnloadAfter = d + } + case "cache.enabled", "cache_enabled": + c.CacheEnabled = value == "true" || value == "yes" } } diff --git a/internal/cli/smoke_test.go b/internal/cli/smoke_test.go index 51313f6..739017e 100644 --- a/internal/cli/smoke_test.go +++ b/internal/cli/smoke_test.go @@ -306,6 +306,41 @@ func TestConfigRoundTrip(t *testing.T) { } } +func TestConfigRoundTripSectionedKnownKey(t *testing.T) { + stateDir := t.TempDir() + cacheDir := t.TempDir() + baseEnv := []string{ + "HOME=" + os.Getenv("HOME"), + "PATH=" + os.Getenv("PATH"), + "INTENT_STATE_DIR=" + stateDir, + "INTENT_CACHE_DIR=" + cacheDir, + } + + cmd1 := exec.Command(testBinary, "config", "set", "daemon.enabled", "false") + cmd1.Env = baseEnv + if out, err := cmd1.CombinedOutput(); err != nil { + t.Fatalf("config set daemon.enabled: %v\n%s", err, out) + } + + cmd2 := exec.Command(testBinary, "config", "get", "daemon.enabled") + cmd2.Env = baseEnv + out, err := cmd2.Output() + if err != nil { + t.Fatalf("config get daemon.enabled: %v", err) + } + if got := strings.TrimSpace(string(out)); got != "false" { + t.Fatalf("config get daemon.enabled: got %q, want %q", got, "false") + } + + cfgBody, err := os.ReadFile(filepath.Join(stateDir, "intent", "config.toml")) + if err != nil { + t.Fatalf("read config file: %v", err) + } + if !strings.Contains(string(cfgBody), "daemon.enabled = false") { + t.Fatalf("config file missing spec-style daemon.enabled key:\n%s", string(cfgBody)) + } +} + func TestConfigSetRejectsRemoteDaemonHost(t *testing.T) { stateDir := t.TempDir() cacheDir := t.TempDir() diff --git a/internal/config/config.go b/internal/config/config.go index 63b8ed5..216f1c1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -124,13 +124,13 @@ func read(path string) (*Config, error) { c.UpdateChannel = v case "auto_update": c.AutoUpdate = parseBool(v) - case "daemon_enabled": + case "daemon_enabled", "daemon.enabled": c.DaemonEnabled = parseBool(v) - case "daemon_idle_unload_after": + case "daemon_idle_unload_after", "daemon.idle_unload_after": if d, err := time.ParseDuration(v); err == nil { c.DaemonIdleUnloadAfter = d } - case "cache_enabled": + case "cache_enabled", "cache.enabled": c.CacheEnabled = parseBool(v) } } @@ -165,15 +165,17 @@ func Write(path string, c *Config) error { fmt.Fprintf(w, "timeout = %q\n", c.Timeout.String()) fmt.Fprintf(w, "update_channel = %q\n", c.UpdateChannel) fmt.Fprintf(w, "auto_update = %t\n", c.AutoUpdate) - fmt.Fprintf(w, "daemon_enabled = %t\n", c.DaemonEnabled) - fmt.Fprintf(w, "daemon_idle_unload_after = %q\n", c.DaemonIdleUnloadAfter.String()) - fmt.Fprintf(w, "cache_enabled = %t\n", c.CacheEnabled) + fmt.Fprintf(w, "daemon.enabled = %t\n", c.DaemonEnabled) + fmt.Fprintf(w, "daemon.idle_unload_after = %q\n", c.DaemonIdleUnloadAfter.String()) + fmt.Fprintf(w, "cache.enabled = %t\n", c.CacheEnabled) // Persist unknown raw keys that are not covered by the known struct fields. knownFields := map[string]bool{ "backend": true, "model": true, "auto_run": true, "sandbox": true, "max_tool_steps": true, "timeout": true, "update_channel": true, - "auto_update": true, "daemon_enabled": true, - "daemon_idle_unload_after": true, "cache_enabled": true, + "auto_update": true, + "daemon_enabled": true, "daemon.enabled": true, + "daemon_idle_unload_after": true, "daemon.idle_unload_after": true, + "cache_enabled": true, "cache.enabled": true, } for k, v := range c.Raw { if !knownFields[k] { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0811757..9426d4b 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -3,6 +3,7 @@ package config import ( "os" "path/filepath" + "strings" "testing" "time" ) @@ -19,6 +20,8 @@ base_url = "https://example.test/v1" model = "gpt-4.1-mini" [daemon] +enabled = false +idle_unload_after = "45m" host = "127.0.0.1" port = "18080" @@ -53,4 +56,78 @@ enabled = true if got := cfg.Raw["daemon.port"]; got != "18080" { t.Fatalf("daemon.port=%q want %q", got, "18080") } + if cfg.DaemonEnabled { + t.Fatalf("daemon enabled=%t want false", cfg.DaemonEnabled) + } + if cfg.DaemonIdleUnloadAfter != 45*time.Minute { + t.Fatalf("daemon idle unload after=%s want %s", cfg.DaemonIdleUnloadAfter, 45*time.Minute) + } + if !cfg.CacheEnabled { + t.Fatal("cache enabled=false want true") + } +} + +func TestReadSupportsLegacyFlatAliases(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.toml") + if err := os.WriteFile(path, []byte(` +daemon_enabled = false +daemon_idle_unload_after = "15m" +cache_enabled = false +`), 0o600); err != nil { + t.Fatalf("write config: %v", err) + } + + cfg, err := read(path) + if err != nil { + t.Fatalf("read config: %v", err) + } + if cfg.DaemonEnabled { + t.Fatalf("daemon enabled=%t want false", cfg.DaemonEnabled) + } + if cfg.DaemonIdleUnloadAfter != 15*time.Minute { + t.Fatalf("daemon idle unload after=%s want %s", cfg.DaemonIdleUnloadAfter, 15*time.Minute) + } + if cfg.CacheEnabled { + t.Fatal("cache enabled=true want false") + } +} + +func TestWriteUsesSpecStyleDaemonAndCacheKeys(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.toml") + cfg := Defaults() + cfg.DaemonEnabled = false + cfg.DaemonIdleUnloadAfter = 45 * time.Minute + cfg.CacheEnabled = false + cfg.Raw["daemon.host"] = "127.0.0.1" + cfg.Raw["backends.openai.base_url"] = "https://example.test/v1" + + if err := Write(path, cfg); err != nil { + t.Fatalf("write config: %v", err) + } + + body, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read config: %v", err) + } + got := string(body) + for _, want := range []string{ + `daemon.enabled = false`, + `daemon.idle_unload_after = "45m0s"`, + `cache.enabled = false`, + `daemon.host = "127.0.0.1"`, + `backends.openai.base_url = "https://example.test/v1"`, + } { + if !strings.Contains(got, want) { + t.Fatalf("config missing %q:\n%s", want, got) + } + } + for _, unwanted := range []string{ + "daemon_enabled =", + "daemon_idle_unload_after =", + "cache_enabled =", + } { + if strings.Contains(got, unwanted) { + t.Fatalf("config unexpectedly contained legacy key %q:\n%s", unwanted, got) + } + } }