Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions internal/cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"os/exec"
"time"

"github.com/CoreyRDean/intent/internal/config"
"github.com/CoreyRDean/intent/internal/state"
Expand Down Expand Up @@ -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 ""
}
Expand All @@ -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
Comment on lines +136 to +137

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject invalid daemon idle_unload_after values

i config set daemon.idle_unload_after <value> now treats this key as known, but an invalid duration is silently ignored because parse errors are dropped. In that case the command still exits successfully, and config.Write persists the previous value for this known field, so users think the setting was applied when it was not. This is a behavior regression for the newly supported key and should return a validation error (or otherwise surface failure) when time.ParseDuration fails.

Useful? React with 👍 / 👎.

}
case "cache.enabled", "cache_enabled":
c.CacheEnabled = value == "true" || value == "yes"
}
}
35 changes: 35 additions & 0 deletions internal/cli/smoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
18 changes: 10 additions & 8 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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] {
Expand Down
77 changes: 77 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
Expand All @@ -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"

Expand Down Expand Up @@ -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)
}
}
}
Loading