Implements GitHub issue #127 - allow custom agent configuration through a CLI interface instead of command-line aliases. The gt config command provides: - gt config agent list [--json] List all agents - gt config agent get <name> Show agent configuration - gt config agent set <name> <cmd> Set custom agent command - gt config agent remove <name> Remove custom agent - gt config default-agent [name] Get/set default agent Users can now define custom agents (e.g., claude-glm) and override built-in presets (claude, gemini, codex) through town settings instead of shell aliases. Changes: - Add SaveTownSettings() to internal/config/loader.go - Add internal/cmd/config.go with full config command implementation - Add comprehensive unit tests for both SaveTownSettings and all config subcommands (17 test cases covering success and error scenarios) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
584 lines
15 KiB
Go
584 lines
15 KiB
Go
package cmd
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/gastown/internal/config"
|
|
)
|
|
|
|
// setupTestTown creates a minimal Gas Town workspace for testing.
|
|
func setupTestTownForConfig(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
townRoot := t.TempDir()
|
|
|
|
// Create mayor directory with required files
|
|
mayorDir := filepath.Join(townRoot, "mayor")
|
|
if err := os.MkdirAll(mayorDir, 0755); err != nil {
|
|
t.Fatalf("mkdir mayor: %v", err)
|
|
}
|
|
|
|
// Create town.json
|
|
townConfig := &config.TownConfig{
|
|
Type: "town",
|
|
Version: config.CurrentTownVersion,
|
|
Name: "test-town",
|
|
PublicName: "Test Town",
|
|
CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
}
|
|
townConfigPath := filepath.Join(mayorDir, "town.json")
|
|
if err := config.SaveTownConfig(townConfigPath, townConfig); err != nil {
|
|
t.Fatalf("save town.json: %v", err)
|
|
}
|
|
|
|
// Create empty rigs.json
|
|
rigsConfig := &config.RigsConfig{
|
|
Version: 1,
|
|
Rigs: make(map[string]config.RigEntry),
|
|
}
|
|
rigsPath := filepath.Join(mayorDir, "rigs.json")
|
|
if err := config.SaveRigsConfig(rigsPath, rigsConfig); err != nil {
|
|
t.Fatalf("save rigs.json: %v", err)
|
|
}
|
|
|
|
return townRoot
|
|
}
|
|
|
|
func TestConfigAgentList(t *testing.T) {
|
|
t.Run("lists built-in agents", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
settingsPath := config.TownSettingsPath(townRoot)
|
|
|
|
// Change to town root so workspace.FindFromCwd works
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Run the command
|
|
cmd := &cobra.Command{}
|
|
args := []string{}
|
|
err := runConfigAgentList(cmd, args)
|
|
if err != nil {
|
|
t.Fatalf("runConfigAgentList failed: %v", err)
|
|
}
|
|
|
|
// Verify settings file was created (LoadOrCreate creates it)
|
|
if _, err := os.Stat(settingsPath); err != nil {
|
|
// This is OK - list command works without settings file
|
|
}
|
|
})
|
|
|
|
t.Run("lists built-in and custom agents", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
settingsPath := config.TownSettingsPath(townRoot)
|
|
|
|
// Create settings with custom agent
|
|
settings := &config.TownSettings{
|
|
Type: "town-settings",
|
|
Version: config.CurrentTownSettingsVersion,
|
|
DefaultAgent: "claude",
|
|
Agents: map[string]*config.RuntimeConfig{
|
|
"my-custom": {
|
|
Command: "my-agent",
|
|
Args: []string{"--flag"},
|
|
},
|
|
},
|
|
}
|
|
if err := config.SaveTownSettings(settingsPath, settings); err != nil {
|
|
t.Fatalf("save settings: %v", err)
|
|
}
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Load agent registry
|
|
registryPath := config.DefaultAgentRegistryPath(townRoot)
|
|
if err := config.LoadAgentRegistry(registryPath); err != nil {
|
|
t.Fatalf("load agent registry: %v", err)
|
|
}
|
|
|
|
// Run the command
|
|
cmd := &cobra.Command{}
|
|
args := []string{}
|
|
err := runConfigAgentList(cmd, args)
|
|
if err != nil {
|
|
t.Fatalf("runConfigAgentList failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("JSON output", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Set JSON flag
|
|
configAgentListJSON = true
|
|
defer func() { configAgentListJSON = false }()
|
|
|
|
// Load agent registry
|
|
registryPath := config.DefaultAgentRegistryPath(townRoot)
|
|
if err := config.LoadAgentRegistry(registryPath); err != nil {
|
|
t.Fatalf("load agent registry: %v", err)
|
|
}
|
|
|
|
// Capture output
|
|
// Note: This test verifies the command runs without error
|
|
// Full JSON validation would require capturing stdout
|
|
cmd := &cobra.Command{}
|
|
args := []string{}
|
|
err := runConfigAgentList(cmd, args)
|
|
if err != nil {
|
|
t.Fatalf("runConfigAgentList failed: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestConfigAgentGet(t *testing.T) {
|
|
t.Run("gets built-in agent", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Load agent registry
|
|
registryPath := config.DefaultAgentRegistryPath(townRoot)
|
|
if err := config.LoadAgentRegistry(registryPath); err != nil {
|
|
t.Fatalf("load agent registry: %v", err)
|
|
}
|
|
|
|
// Run the command
|
|
cmd := &cobra.Command{}
|
|
args := []string{"claude"}
|
|
err := runConfigAgentGet(cmd, args)
|
|
if err != nil {
|
|
t.Fatalf("runConfigAgentGet failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("gets custom agent", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
settingsPath := config.TownSettingsPath(townRoot)
|
|
|
|
// Create settings with custom agent
|
|
settings := &config.TownSettings{
|
|
Type: "town-settings",
|
|
Version: config.CurrentTownSettingsVersion,
|
|
DefaultAgent: "claude",
|
|
Agents: map[string]*config.RuntimeConfig{
|
|
"my-custom": {
|
|
Command: "my-agent",
|
|
Args: []string{"--flag1", "--flag2"},
|
|
},
|
|
},
|
|
}
|
|
if err := config.SaveTownSettings(settingsPath, settings); err != nil {
|
|
t.Fatalf("save settings: %v", err)
|
|
}
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Load agent registry
|
|
registryPath := config.DefaultAgentRegistryPath(townRoot)
|
|
if err := config.LoadAgentRegistry(registryPath); err != nil {
|
|
t.Fatalf("load agent registry: %v", err)
|
|
}
|
|
|
|
// Run the command
|
|
cmd := &cobra.Command{}
|
|
args := []string{"my-custom"}
|
|
err := runConfigAgentGet(cmd, args)
|
|
if err != nil {
|
|
t.Fatalf("runConfigAgentGet failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("returns error for unknown agent", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Load agent registry
|
|
registryPath := config.DefaultAgentRegistryPath(townRoot)
|
|
if err := config.LoadAgentRegistry(registryPath); err != nil {
|
|
t.Fatalf("load agent registry: %v", err)
|
|
}
|
|
|
|
// Run the command with unknown agent
|
|
cmd := &cobra.Command{}
|
|
args := []string{"unknown-agent"}
|
|
err := runConfigAgentGet(cmd, args)
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown agent")
|
|
}
|
|
if !strings.Contains(err.Error(), "not found") {
|
|
t.Errorf("error = %v, want 'not found'", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestConfigAgentSet(t *testing.T) {
|
|
t.Run("sets custom agent", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
settingsPath := config.TownSettingsPath(townRoot)
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Run the command
|
|
cmd := &cobra.Command{}
|
|
args := []string{"my-agent", "my-agent --arg1 --arg2"}
|
|
err := runConfigAgentSet(cmd, args)
|
|
if err != nil {
|
|
t.Fatalf("runConfigAgentSet failed: %v", err)
|
|
}
|
|
|
|
// Verify settings were saved
|
|
loaded, err := config.LoadOrCreateTownSettings(settingsPath)
|
|
if err != nil {
|
|
t.Fatalf("load settings: %v", err)
|
|
}
|
|
|
|
if loaded.Agents == nil {
|
|
t.Fatal("Agents map is nil")
|
|
}
|
|
agent, ok := loaded.Agents["my-agent"]
|
|
if !ok {
|
|
t.Fatal("custom agent not found in settings")
|
|
}
|
|
if agent.Command != "my-agent" {
|
|
t.Errorf("Command = %q, want 'my-agent'", agent.Command)
|
|
}
|
|
if len(agent.Args) != 2 {
|
|
t.Errorf("Args count = %d, want 2", len(agent.Args))
|
|
}
|
|
})
|
|
|
|
t.Run("sets agent with single command (no args)", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
settingsPath := config.TownSettingsPath(townRoot)
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Run the command
|
|
cmd := &cobra.Command{}
|
|
args := []string{"simple-agent", "simple-agent"}
|
|
err := runConfigAgentSet(cmd, args)
|
|
if err != nil {
|
|
t.Fatalf("runConfigAgentSet failed: %v", err)
|
|
}
|
|
|
|
// Verify settings were saved
|
|
loaded, err := config.LoadOrCreateTownSettings(settingsPath)
|
|
if err != nil {
|
|
t.Fatalf("load settings: %v", err)
|
|
}
|
|
|
|
agent := loaded.Agents["simple-agent"]
|
|
if agent.Command != "simple-agent" {
|
|
t.Errorf("Command = %q, want 'simple-agent'", agent.Command)
|
|
}
|
|
if len(agent.Args) != 0 {
|
|
t.Errorf("Args count = %d, want 0", len(agent.Args))
|
|
}
|
|
})
|
|
|
|
t.Run("overrides existing agent", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
settingsPath := config.TownSettingsPath(townRoot)
|
|
|
|
// Create initial settings
|
|
settings := &config.TownSettings{
|
|
Type: "town-settings",
|
|
Version: config.CurrentTownSettingsVersion,
|
|
DefaultAgent: "claude",
|
|
Agents: map[string]*config.RuntimeConfig{
|
|
"my-agent": {
|
|
Command: "old-command",
|
|
Args: []string{"--old"},
|
|
},
|
|
},
|
|
}
|
|
if err := config.SaveTownSettings(settingsPath, settings); err != nil {
|
|
t.Fatalf("save initial settings: %v", err)
|
|
}
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Run the command to override
|
|
cmd := &cobra.Command{}
|
|
args := []string{"my-agent", "new-command --new"}
|
|
err := runConfigAgentSet(cmd, args)
|
|
if err != nil {
|
|
t.Fatalf("runConfigAgentSet failed: %v", err)
|
|
}
|
|
|
|
// Verify settings were updated
|
|
loaded, err := config.LoadOrCreateTownSettings(settingsPath)
|
|
if err != nil {
|
|
t.Fatalf("load settings: %v", err)
|
|
}
|
|
|
|
agent := loaded.Agents["my-agent"]
|
|
if agent.Command != "new-command" {
|
|
t.Errorf("Command = %q, want 'new-command'", agent.Command)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestConfigAgentRemove(t *testing.T) {
|
|
t.Run("removes custom agent", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
settingsPath := config.TownSettingsPath(townRoot)
|
|
|
|
// Create settings with custom agent
|
|
settings := &config.TownSettings{
|
|
Type: "town-settings",
|
|
Version: config.CurrentTownSettingsVersion,
|
|
DefaultAgent: "claude",
|
|
Agents: map[string]*config.RuntimeConfig{
|
|
"my-agent": {
|
|
Command: "my-agent",
|
|
Args: []string{"--flag"},
|
|
},
|
|
},
|
|
}
|
|
if err := config.SaveTownSettings(settingsPath, settings); err != nil {
|
|
t.Fatalf("save settings: %v", err)
|
|
}
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Run the command
|
|
cmd := &cobra.Command{}
|
|
args := []string{"my-agent"}
|
|
err := runConfigAgentRemove(cmd, args)
|
|
if err != nil {
|
|
t.Fatalf("runConfigAgentRemove failed: %v", err)
|
|
}
|
|
|
|
// Verify agent was removed
|
|
loaded, err := config.LoadOrCreateTownSettings(settingsPath)
|
|
if err != nil {
|
|
t.Fatalf("load settings: %v", err)
|
|
}
|
|
|
|
if loaded.Agents != nil {
|
|
if _, ok := loaded.Agents["my-agent"]; ok {
|
|
t.Error("agent still exists after removal")
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("rejects removing built-in agent", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Try to remove a built-in agent
|
|
cmd := &cobra.Command{}
|
|
args := []string{"claude"}
|
|
err := runConfigAgentRemove(cmd, args)
|
|
if err == nil {
|
|
t.Fatal("expected error when removing built-in agent")
|
|
}
|
|
if !strings.Contains(err.Error(), "cannot remove built-in") {
|
|
t.Errorf("error = %v, want 'cannot remove built-in'", err)
|
|
}
|
|
})
|
|
|
|
t.Run("returns error for non-existent custom agent", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Try to remove a non-existent agent
|
|
cmd := &cobra.Command{}
|
|
args := []string{"non-existent"}
|
|
err := runConfigAgentRemove(cmd, args)
|
|
if err == nil {
|
|
t.Fatal("expected error for non-existent agent")
|
|
}
|
|
if !strings.Contains(err.Error(), "not found") {
|
|
t.Errorf("error = %v, want 'not found'", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestConfigDefaultAgent(t *testing.T) {
|
|
t.Run("gets default agent (shows current)", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Run the command with no args (should show current default)
|
|
cmd := &cobra.Command{}
|
|
args := []string{}
|
|
err := runConfigDefaultAgent(cmd, args)
|
|
if err != nil {
|
|
t.Fatalf("runConfigDefaultAgent failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("sets default agent to built-in", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
settingsPath := config.TownSettingsPath(townRoot)
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Set default to gemini
|
|
cmd := &cobra.Command{}
|
|
args := []string{"gemini"}
|
|
err := runConfigDefaultAgent(cmd, args)
|
|
if err != nil {
|
|
t.Fatalf("runConfigDefaultAgent failed: %v", err)
|
|
}
|
|
|
|
// Verify settings were saved
|
|
loaded, err := config.LoadOrCreateTownSettings(settingsPath)
|
|
if err != nil {
|
|
t.Fatalf("load settings: %v", err)
|
|
}
|
|
|
|
if loaded.DefaultAgent != "gemini" {
|
|
t.Errorf("DefaultAgent = %q, want 'gemini'", loaded.DefaultAgent)
|
|
}
|
|
})
|
|
|
|
t.Run("sets default agent to custom", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
settingsPath := config.TownSettingsPath(townRoot)
|
|
|
|
// Create settings with custom agent
|
|
settings := &config.TownSettings{
|
|
Type: "town-settings",
|
|
Version: config.CurrentTownSettingsVersion,
|
|
DefaultAgent: "claude",
|
|
Agents: map[string]*config.RuntimeConfig{
|
|
"my-custom": {
|
|
Command: "my-agent",
|
|
Args: []string{},
|
|
},
|
|
},
|
|
}
|
|
if err := config.SaveTownSettings(settingsPath, settings); err != nil {
|
|
t.Fatalf("save settings: %v", err)
|
|
}
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Set default to custom agent
|
|
cmd := &cobra.Command{}
|
|
args := []string{"my-custom"}
|
|
err := runConfigDefaultAgent(cmd, args)
|
|
if err != nil {
|
|
t.Fatalf("runConfigDefaultAgent failed: %v", err)
|
|
}
|
|
|
|
// Verify settings were saved
|
|
loaded, err := config.LoadOrCreateTownSettings(settingsPath)
|
|
if err != nil {
|
|
t.Fatalf("load settings: %v", err)
|
|
}
|
|
|
|
if loaded.DefaultAgent != "my-custom" {
|
|
t.Errorf("DefaultAgent = %q, want 'my-custom'", loaded.DefaultAgent)
|
|
}
|
|
})
|
|
|
|
t.Run("returns error for unknown agent", func(t *testing.T) {
|
|
townRoot := setupTestTownForConfig(t)
|
|
|
|
// Change to town root
|
|
originalWd, _ := os.Getwd()
|
|
defer os.Chdir(originalWd)
|
|
if err := os.Chdir(townRoot); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
|
|
// Try to set default to unknown agent
|
|
cmd := &cobra.Command{}
|
|
args := []string{"unknown-agent"}
|
|
err := runConfigDefaultAgent(cmd, args)
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown agent")
|
|
}
|
|
if !strings.Contains(err.Error(), "not found") {
|
|
t.Errorf("error = %v, want 'not found'", err)
|
|
}
|
|
})
|
|
}
|