Skip to content
2 changes: 1 addition & 1 deletion internal/app/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestNewProgramNormalizesInvalidCurrentModelOnStartup(t *testing.T) {
}

configPath := filepath.Join(configDir, "config.yaml")
raw := []byte("selected_provider: openai\ncurrent_model: unsupported-current\nworkdir: .\nshell: powershell\n")
raw := []byte("selected_provider: openai\ncurrent_model: unsupported-current\nshell: powershell\n")
if err := os.WriteFile(configPath, raw, 0o644); err != nil {
t.Fatalf("write config: %v", err)
}
Expand Down
32 changes: 30 additions & 2 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ func TestParseConfigFormats(t *testing.T) {
tests := []struct {
name string
data string
err string
assert func(t *testing.T, cfg *Config)
}{
{
name: "current format ignores persisted provider metadata",
data: `
selected_provider: openai
current_model: gpt-5.4
workdir: .
shell: powershell

provider_overrides:
Expand Down Expand Up @@ -87,12 +87,31 @@ providers:
}
},
},
{
name: "legacy default_workdir key is rejected",
data: `
selected_provider: openai
current_model: gpt-4.1
default_workdir: ./from-default
shell: powershell
`,
err: "legacy config key \"default_workdir\" is no longer supported",
},
{
name: "legacy workdir key is rejected",
data: `
selected_provider: openai
current_model: gpt-4.1
workdir: ./from-legacy
shell: powershell
`,
err: "legacy config key \"workdir\" is no longer supported",
},
{
name: "legacy persisted providers list keeps selection only",
data: `
selected_provider: openai
current_model: gpt-5.4
workdir: .
shell: powershell
providers:
- name: openai
Expand Down Expand Up @@ -167,6 +186,12 @@ providers:
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
cfg, err := parseConfig([]byte(tt.data))
if tt.err != "" {
if err == nil || !strings.Contains(err.Error(), tt.err) {
t.Fatalf("expected parseConfig() error containing %q, got %v", tt.err, err)
}
return
}
if err != nil {
t.Fatalf("parseConfig() error = %v", err)
}
Expand Down Expand Up @@ -572,6 +597,9 @@ func TestLoaderLoadAndSaveRoundTrip(t *testing.T) {
t.Fatalf("read config file: %v", err)
}
text := string(data)
if strings.Contains(text, "default_workdir:") || strings.Contains(text, "\nworkdir:") || strings.HasPrefix(text, "workdir:") {
t.Fatalf("expected persisted config to avoid any workdir keys, got:\n%s", text)
}
if strings.Contains(text, "\nproviders:") || strings.HasPrefix(text, "providers:") {
t.Fatalf("expected persisted config to omit providers, got:\n%s", text)
}
Expand Down
23 changes: 14 additions & 9 deletions internal/config/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ type Loader struct {
}

type persistedConfig struct {
SelectedProvider string `yaml:"selected_provider"`
CurrentModel string `yaml:"current_model"`
Workdir string `yaml:"workdir"`
Shell string `yaml:"shell"`
MaxLoops int `yaml:"max_loops,omitempty"`
ToolTimeoutSec int `yaml:"tool_timeout_sec,omitempty"`
Tools ToolsConfig `yaml:"tools,omitempty"`
SelectedProvider string `yaml:"selected_provider"`
CurrentModel string `yaml:"current_model"`
LegacyDefaultWorkdir *string `yaml:"default_workdir,omitempty"`
LegacyWorkdir *string `yaml:"workdir,omitempty"`
Shell string `yaml:"shell"`
MaxLoops int `yaml:"max_loops,omitempty"`
ToolTimeoutSec int `yaml:"tool_timeout_sec,omitempty"`
Tools ToolsConfig `yaml:"tools,omitempty"`
}

func NewLoader(baseDir string, defaults *Config) *Loader {
Expand Down Expand Up @@ -152,11 +153,16 @@ func parseCurrentConfig(data []byte) (*Config, error) {
if err := yaml.Unmarshal(data, &file); err != nil {
return nil, err
}
if file.LegacyDefaultWorkdir != nil {
return nil, fmt.Errorf("legacy config key %q is no longer supported", "default_workdir")
}
if file.LegacyWorkdir != nil {
return nil, fmt.Errorf("legacy config key %q is no longer supported", "workdir")
}

cfg := &Config{
SelectedProvider: strings.TrimSpace(file.SelectedProvider),
CurrentModel: strings.TrimSpace(file.CurrentModel),
Workdir: strings.TrimSpace(file.Workdir),
Shell: strings.TrimSpace(file.Shell),
MaxLoops: file.MaxLoops,
ToolTimeoutSec: file.ToolTimeoutSec,
Expand All @@ -170,7 +176,6 @@ func marshalPersistedConfig(snapshot Config) ([]byte, error) {
file := persistedConfig{
SelectedProvider: snapshot.SelectedProvider,
CurrentModel: snapshot.CurrentModel,
Workdir: snapshot.Workdir,
Shell: snapshot.Shell,
MaxLoops: snapshot.MaxLoops,
ToolTimeoutSec: snapshot.ToolTimeoutSec,
Expand Down
52 changes: 49 additions & 3 deletions internal/config/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,52 @@ func TestLoaderLoadMalformedYAML(t *testing.T) {
}
}

func TestLoaderRejectsLegacyWorkdirKey(t *testing.T) {
t.Parallel()

loader := NewLoader(t.TempDir(), testDefaultConfig())
if err := os.MkdirAll(loader.BaseDir(), 0o755); err != nil {
t.Fatalf("mkdir base dir: %v", err)
}
raw := `
selected_provider: openai
current_model: gpt-4.1
workdir: .
shell: powershell
`
if err := os.WriteFile(loader.ConfigPath(), []byte(strings.TrimSpace(raw)+"\n"), 0o644); err != nil {
t.Fatalf("write legacy config: %v", err)
}

_, err := loader.Load(context.Background())
if err == nil || !strings.Contains(err.Error(), "legacy config key \"workdir\" is no longer supported") {
t.Fatalf("expected legacy workdir rejection, got %v", err)
}
}

func TestLoaderRejectsLegacyDefaultWorkdirKey(t *testing.T) {
t.Parallel()

loader := NewLoader(t.TempDir(), testDefaultConfig())
if err := os.MkdirAll(loader.BaseDir(), 0o755); err != nil {
t.Fatalf("mkdir base dir: %v", err)
}
raw := `
selected_provider: openai
current_model: gpt-4.1
default_workdir: .
shell: powershell
`
if err := os.WriteFile(loader.ConfigPath(), []byte(strings.TrimSpace(raw)+"\n"), 0o644); err != nil {
t.Fatalf("write legacy config: %v", err)
}

_, err := loader.Load(context.Background())
if err == nil || !strings.Contains(err.Error(), "legacy config key \"default_workdir\" is no longer supported") {
t.Fatalf("expected legacy default_workdir rejection, got %v", err)
}
}

func TestLoaderLoadInvalidBaseDir(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -72,7 +118,6 @@ func TestLoaderRewritesLegacyProvidersFormatOnLoad(t *testing.T) {
legacy := `
selected_provider: openai
current_model: gpt-5.4
workdir: .
shell: powershell
providers:
- name: openai
Expand Down Expand Up @@ -105,6 +150,9 @@ providers:
t.Fatalf("read rewritten config: %v", err)
}
text := string(rewritten)
if strings.Contains(text, "default_workdir:") || strings.Contains(text, "\nworkdir:") || strings.HasPrefix(text, "workdir:") {
t.Fatalf("expected rewritten config to avoid any workdir keys, got:\n%s", text)
}
if strings.Contains(text, "provider_overrides:") {
t.Fatalf("expected rewritten config to drop provider overrides, got:\n%s", text)
}
Expand All @@ -126,7 +174,6 @@ func TestLoaderRewritesNormalizedSelectionStateOnLoad(t *testing.T) {

raw := `
selected_provider: missing-provider
workdir: .
shell: powershell
`
if err := os.WriteFile(loader.ConfigPath(), []byte(strings.TrimSpace(raw)+"\n"), 0o644); err != nil {
Expand Down Expand Up @@ -167,7 +214,6 @@ func TestLoaderRewritesMissingCurrentModelOnLoad(t *testing.T) {

raw := `
selected_provider: openai
workdir: .
shell: powershell
`
if err := os.WriteFile(loader.ConfigPath(), []byte(strings.TrimSpace(raw)+"\n"), 0o644); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/config/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type Config struct {
Providers []ProviderConfig `yaml:"-"`
SelectedProvider string `yaml:"selected_provider"`
CurrentModel string `yaml:"current_model"`
Workdir string `yaml:"workdir"`
Workdir string `yaml:"-"`
Shell string `yaml:"shell"`
MaxLoops int `yaml:"max_loops,omitempty"`
ToolTimeoutSec int `yaml:"tool_timeout_sec,omitempty"`
Expand Down
Loading
Loading