feat: Add gt config command for managing agent settings
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>
This commit is contained in:
468
internal/cmd/config.go
Normal file
468
internal/cmd/config.go
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
// Package cmd provides CLI commands for the gt tool.
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configCmd = &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
GroupID: GroupConfig,
|
||||||
|
Short: "Manage Gas Town configuration",
|
||||||
|
RunE: requireSubcommand,
|
||||||
|
Long: `Manage Gas Town configuration settings.
|
||||||
|
|
||||||
|
This command allows you to view and modify configuration settings
|
||||||
|
for your Gas Town workspace, including agent aliases and defaults.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
gt config agent list List all agents (built-in and custom)
|
||||||
|
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 or set default agent`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agent subcommands
|
||||||
|
|
||||||
|
var configAgentListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List all agents",
|
||||||
|
Long: `List all available agents (built-in and custom).
|
||||||
|
|
||||||
|
Shows all built-in agent presets (claude, gemini, codex) and any
|
||||||
|
custom agents defined in your town settings.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt config agent list # Text output
|
||||||
|
gt config agent list --json # JSON output`,
|
||||||
|
RunE: runConfigAgentList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var configAgentGetCmd = &cobra.Command{
|
||||||
|
Use: "get <name>",
|
||||||
|
Short: "Show agent configuration",
|
||||||
|
Long: `Show the configuration for a specific agent.
|
||||||
|
|
||||||
|
Displays the full configuration for an agent, including command,
|
||||||
|
arguments, and other settings. Works for both built-in and custom agents.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt config agent get claude
|
||||||
|
gt config agent get my-custom-agent`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runConfigAgentGet,
|
||||||
|
}
|
||||||
|
|
||||||
|
var configAgentSetCmd = &cobra.Command{
|
||||||
|
Use: "set <name> <command>",
|
||||||
|
Short: "Set custom agent command",
|
||||||
|
Long: `Set a custom agent command in town settings.
|
||||||
|
|
||||||
|
This creates or updates a custom agent definition that overrides
|
||||||
|
or extends the built-in presets. The custom agent will be available
|
||||||
|
to all rigs in the town.
|
||||||
|
|
||||||
|
The command can include arguments. Use quotes if the command or
|
||||||
|
arguments contain spaces.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt config agent set claude-glm \"claude-glm --model glm-4\"
|
||||||
|
gt config agent set gemini-custom gemini --approval-mode yolo
|
||||||
|
gt config agent set claude \"claude-glm\" # Override built-in claude`,
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: runConfigAgentSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
var configAgentRemoveCmd = &cobra.Command{
|
||||||
|
Use: "remove <name>",
|
||||||
|
Short: "Remove custom agent",
|
||||||
|
Long: `Remove a custom agent definition from town settings.
|
||||||
|
|
||||||
|
This removes a custom agent from your town settings. Built-in agents
|
||||||
|
(claude, gemini, codex) cannot be removed.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt config agent remove claude-glm`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runConfigAgentRemove,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default-agent subcommand
|
||||||
|
|
||||||
|
var configDefaultAgentCmd = &cobra.Command{
|
||||||
|
Use: "default-agent [name]",
|
||||||
|
Short: "Get or set default agent",
|
||||||
|
Long: `Get or set the default agent for the town.
|
||||||
|
|
||||||
|
With no arguments, shows the current default agent.
|
||||||
|
With an argument, sets the default agent to the specified name.
|
||||||
|
|
||||||
|
The default agent is used when a rig doesn't specify its own agent
|
||||||
|
setting. Can be a built-in preset (claude, gemini, codex) or a
|
||||||
|
custom agent name.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt config default-agent # Show current default
|
||||||
|
gt config default-agent claude # Set to claude
|
||||||
|
gt config default-agent gemini # Set to gemini
|
||||||
|
gt config default-agent my-custom # Set to custom agent`,
|
||||||
|
RunE: runConfigDefaultAgent,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flags
|
||||||
|
var (
|
||||||
|
configAgentListJSON bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// AgentListItem represents an agent in list output.
|
||||||
|
type AgentListItem struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Command string `json:"command"`
|
||||||
|
Args string `json:"args,omitempty"`
|
||||||
|
Type string `json:"type"` // "built-in" or "custom"
|
||||||
|
IsCustom bool `json:"is_custom"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfigAgentList(cmd *cobra.Command, args []string) error {
|
||||||
|
townRoot, err := workspace.FindFromCwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("finding town root: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load town settings
|
||||||
|
settingsPath := config.TownSettingsPath(townRoot)
|
||||||
|
townSettings, err := config.LoadOrCreateTownSettings(settingsPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading town settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load agent registry
|
||||||
|
registryPath := config.DefaultAgentRegistryPath(townRoot)
|
||||||
|
if err := config.LoadAgentRegistry(registryPath); err != nil {
|
||||||
|
return fmt.Errorf("loading agent registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all agents
|
||||||
|
builtInAgents := []string{"claude", "gemini", "codex"}
|
||||||
|
customAgents := make(map[string]*config.RuntimeConfig)
|
||||||
|
if townSettings.Agents != nil {
|
||||||
|
for name, runtime := range townSettings.Agents {
|
||||||
|
customAgents[name] = runtime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build list items
|
||||||
|
var items []AgentListItem
|
||||||
|
for _, name := range builtInAgents {
|
||||||
|
preset := config.GetAgentPresetByName(name)
|
||||||
|
if preset != nil {
|
||||||
|
items = append(items, AgentListItem{
|
||||||
|
Name: name,
|
||||||
|
Command: preset.Command,
|
||||||
|
Args: strings.Join(preset.Args, " "),
|
||||||
|
Type: "built-in",
|
||||||
|
IsCustom: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, runtime := range customAgents {
|
||||||
|
argsStr := ""
|
||||||
|
if runtime.Args != nil {
|
||||||
|
argsStr = strings.Join(runtime.Args, " ")
|
||||||
|
}
|
||||||
|
items = append(items, AgentListItem{
|
||||||
|
Name: name,
|
||||||
|
Command: runtime.Command,
|
||||||
|
Args: argsStr,
|
||||||
|
Type: "custom",
|
||||||
|
IsCustom: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by name
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
return items[i].Name < items[j].Name
|
||||||
|
})
|
||||||
|
|
||||||
|
if configAgentListJSON {
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text output
|
||||||
|
fmt.Printf("%s\n\n", style.Bold.Render("Available Agents"))
|
||||||
|
for _, item := range items {
|
||||||
|
typeLabel := style.Dim.Render("[" + item.Type + "]")
|
||||||
|
fmt.Printf(" %s %s %s", style.Bold.Render(item.Name), typeLabel, item.Command)
|
||||||
|
if item.Args != "" {
|
||||||
|
fmt.Printf(" %s", item.Args)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show default
|
||||||
|
defaultAgent := townSettings.DefaultAgent
|
||||||
|
if defaultAgent == "" {
|
||||||
|
defaultAgent = "claude"
|
||||||
|
}
|
||||||
|
fmt.Printf("\nDefault: %s\n", style.Bold.Render(defaultAgent))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfigAgentGet(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
townRoot, err := workspace.FindFromCwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("finding town root: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load town settings for custom agents
|
||||||
|
settingsPath := config.TownSettingsPath(townRoot)
|
||||||
|
townSettings, err := config.LoadOrCreateTownSettings(settingsPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading town settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load agent registry
|
||||||
|
registryPath := config.DefaultAgentRegistryPath(townRoot)
|
||||||
|
if err := config.LoadAgentRegistry(registryPath); err != nil {
|
||||||
|
return fmt.Errorf("loading agent registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check custom agents first
|
||||||
|
if townSettings.Agents != nil {
|
||||||
|
if runtime, ok := townSettings.Agents[name]; ok {
|
||||||
|
displayAgentConfig(name, runtime, nil, true)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check built-in agents
|
||||||
|
preset := config.GetAgentPresetByName(name)
|
||||||
|
if preset != nil {
|
||||||
|
runtime := &config.RuntimeConfig{
|
||||||
|
Command: preset.Command,
|
||||||
|
Args: preset.Args,
|
||||||
|
}
|
||||||
|
displayAgentConfig(name, runtime, preset, false)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("agent '%s' not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayAgentConfig(name string, runtime *config.RuntimeConfig, preset *config.AgentPresetInfo, isCustom bool) {
|
||||||
|
fmt.Printf("%s\n\n", style.Bold.Render("Agent: "+name))
|
||||||
|
|
||||||
|
typeLabel := "custom"
|
||||||
|
if !isCustom {
|
||||||
|
typeLabel = "built-in"
|
||||||
|
}
|
||||||
|
fmt.Printf("Type: %s\n", typeLabel)
|
||||||
|
fmt.Printf("Command: %s\n", runtime.Command)
|
||||||
|
|
||||||
|
if runtime.Args != nil && len(runtime.Args) > 0 {
|
||||||
|
fmt.Printf("Args: %s\n", strings.Join(runtime.Args, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if preset != nil {
|
||||||
|
if preset.SessionIDEnv != "" {
|
||||||
|
fmt.Printf("Session ID Env: %s\n", preset.SessionIDEnv)
|
||||||
|
}
|
||||||
|
if preset.ResumeFlag != "" {
|
||||||
|
fmt.Printf("Resume Style: %s (%s)\n", preset.ResumeStyle, preset.ResumeFlag)
|
||||||
|
}
|
||||||
|
fmt.Printf("Supports Hooks: %v\n", preset.SupportsHooks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfigAgentSet(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
commandLine := args[1]
|
||||||
|
|
||||||
|
townRoot, err := workspace.FindFromCwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("finding town root: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load town settings
|
||||||
|
settingsPath := config.TownSettingsPath(townRoot)
|
||||||
|
townSettings, err := config.LoadOrCreateTownSettings(settingsPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading town settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse command line into command and args
|
||||||
|
parts := strings.Fields(commandLine)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return fmt.Errorf("command cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize agents map if needed
|
||||||
|
if townSettings.Agents == nil {
|
||||||
|
townSettings.Agents = make(map[string]*config.RuntimeConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update the agent
|
||||||
|
townSettings.Agents[name] = &config.RuntimeConfig{
|
||||||
|
Command: parts[0],
|
||||||
|
Args: parts[1:],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
if err := config.SaveTownSettings(settingsPath, townSettings); err != nil {
|
||||||
|
return fmt.Errorf("saving town settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Agent '%s' set to: %s\n", style.Bold.Render(name), commandLine)
|
||||||
|
|
||||||
|
// Check if this overrides a built-in
|
||||||
|
builtInAgents := []string{"claude", "gemini", "codex"}
|
||||||
|
for _, builtin := range builtInAgents {
|
||||||
|
if name == builtin {
|
||||||
|
fmt.Printf("\n%s\n", style.Dim.Render("(overriding built-in '"+builtin+"' preset)"))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfigAgentRemove(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
townRoot, err := workspace.FindFromCwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("finding town root: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if trying to remove built-in
|
||||||
|
builtInAgents := []string{"claude", "gemini", "codex"}
|
||||||
|
for _, builtin := range builtInAgents {
|
||||||
|
if name == builtin {
|
||||||
|
return fmt.Errorf("cannot remove built-in agent '%s' (use 'gt config agent set' to override it)", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load town settings
|
||||||
|
settingsPath := config.TownSettingsPath(townRoot)
|
||||||
|
townSettings, err := config.LoadOrCreateTownSettings(settingsPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading town settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if agent exists
|
||||||
|
if townSettings.Agents == nil || townSettings.Agents[name] == nil {
|
||||||
|
return fmt.Errorf("custom agent '%s' not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the agent
|
||||||
|
delete(townSettings.Agents, name)
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
if err := config.SaveTownSettings(settingsPath, townSettings); err != nil {
|
||||||
|
return fmt.Errorf("saving town settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Removed custom agent '%s'\n", style.Bold.Render(name))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfigDefaultAgent(cmd *cobra.Command, args []string) error {
|
||||||
|
townRoot, err := workspace.FindFromCwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("finding town root: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load town settings
|
||||||
|
settingsPath := config.TownSettingsPath(townRoot)
|
||||||
|
townSettings, err := config.LoadOrCreateTownSettings(settingsPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading town settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load agent registry
|
||||||
|
registryPath := config.DefaultAgentRegistryPath(townRoot)
|
||||||
|
if err := config.LoadAgentRegistry(registryPath); err != nil {
|
||||||
|
return fmt.Errorf("loading agent registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
// Show current default
|
||||||
|
defaultAgent := townSettings.DefaultAgent
|
||||||
|
if defaultAgent == "" {
|
||||||
|
defaultAgent = "claude"
|
||||||
|
}
|
||||||
|
fmt.Printf("Default agent: %s\n", style.Bold.Render(defaultAgent))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new default
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
// Verify agent exists
|
||||||
|
isValid := false
|
||||||
|
builtInAgents := []string{"claude", "gemini", "codex"}
|
||||||
|
for _, builtin := range builtInAgents {
|
||||||
|
if name == builtin {
|
||||||
|
isValid = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isValid && townSettings.Agents != nil {
|
||||||
|
if _, ok := townSettings.Agents[name]; ok {
|
||||||
|
isValid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValid {
|
||||||
|
return fmt.Errorf("agent '%s' not found (use 'gt config agent list' to see available agents)", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default
|
||||||
|
townSettings.DefaultAgent = name
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
if err := config.SaveTownSettings(settingsPath, townSettings); err != nil {
|
||||||
|
return fmt.Errorf("saving town settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Default agent set to '%s'\n", style.Bold.Render(name))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Add flags
|
||||||
|
configAgentListCmd.Flags().BoolVar(&configAgentListJSON, "json", false, "Output as JSON")
|
||||||
|
|
||||||
|
// Add agent subcommands
|
||||||
|
configAgentCmd := &cobra.Command{
|
||||||
|
Use: "agent",
|
||||||
|
Short: "Manage agent configuration",
|
||||||
|
RunE: requireSubcommand,
|
||||||
|
}
|
||||||
|
configAgentCmd.AddCommand(configAgentListCmd)
|
||||||
|
configAgentCmd.AddCommand(configAgentGetCmd)
|
||||||
|
configAgentCmd.AddCommand(configAgentSetCmd)
|
||||||
|
configAgentCmd.AddCommand(configAgentRemoveCmd)
|
||||||
|
|
||||||
|
// Add subcommands to config
|
||||||
|
configCmd.AddCommand(configAgentCmd)
|
||||||
|
configCmd.AddCommand(configDefaultAgentCmd)
|
||||||
|
|
||||||
|
// Register with root
|
||||||
|
rootCmd.AddCommand(configCmd)
|
||||||
|
}
|
||||||
583
internal/cmd/config_test.go
Normal file
583
internal/cmd/config_test.go
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -691,6 +691,31 @@ func LoadOrCreateTownSettings(path string) (*TownSettings, error) {
|
|||||||
return &settings, nil
|
return &settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveTownSettings saves town settings to a file.
|
||||||
|
func SaveTownSettings(path string, settings *TownSettings) error {
|
||||||
|
if settings.Type != "town-settings" && settings.Type != "" {
|
||||||
|
return fmt.Errorf("%w: expected type 'town-settings', got '%s'", ErrInvalidType, settings.Type)
|
||||||
|
}
|
||||||
|
if settings.Version > CurrentTownSettingsVersion {
|
||||||
|
return fmt.Errorf("%w: got %d, max supported %d", ErrInvalidVersion, settings.Version, CurrentTownSettingsVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||||
|
return fmt.Errorf("creating directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(settings, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encoding settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, data, 0644); err != nil { //nolint:gosec // G306: settings files don't contain secrets
|
||||||
|
return fmt.Errorf("writing settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ResolveAgentConfig resolves the agent configuration for a rig.
|
// ResolveAgentConfig resolves the agent configuration for a rig.
|
||||||
// It looks up the agent by name in town settings (custom agents) and built-in presets.
|
// It looks up the agent by name in town settings (custom agents) and built-in presets.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -970,3 +970,133 @@ func TestLoadRuntimeConfigFallsBackToDefaults(t *testing.T) {
|
|||||||
t.Errorf("Command = %q, want %q (default)", rc.Command, "claude")
|
t.Errorf("Command = %q, want %q (default)", rc.Command, "claude")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSaveTownSettings(t *testing.T) {
|
||||||
|
t.Run("saves valid town settings", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
settingsPath := filepath.Join(tmpDir, "settings", "config.json")
|
||||||
|
|
||||||
|
settings := &TownSettings{
|
||||||
|
Type: "town-settings",
|
||||||
|
Version: CurrentTownSettingsVersion,
|
||||||
|
DefaultAgent: "gemini",
|
||||||
|
Agents: map[string]*RuntimeConfig{
|
||||||
|
"my-agent": {
|
||||||
|
Command: "my-agent",
|
||||||
|
Args: []string{"--arg1", "--arg2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := SaveTownSettings(settingsPath, settings)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveTownSettings failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify file exists
|
||||||
|
data, err := os.ReadFile(settingsPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reading settings file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it contains expected content
|
||||||
|
content := string(data)
|
||||||
|
if !strings.Contains(content, `"type": "town-settings"`) {
|
||||||
|
t.Errorf("missing type field")
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, `"default_agent": "gemini"`) {
|
||||||
|
t.Errorf("missing default_agent field")
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, `"my-agent"`) {
|
||||||
|
t.Errorf("missing custom agent")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("creates parent directories", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
settingsPath := filepath.Join(tmpDir, "deeply", "nested", "settings", "config.json")
|
||||||
|
|
||||||
|
settings := NewTownSettings()
|
||||||
|
|
||||||
|
err := SaveTownSettings(settingsPath, settings)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveTownSettings failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify file exists
|
||||||
|
if _, err := os.Stat(settingsPath); err != nil {
|
||||||
|
t.Errorf("settings file not created: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects invalid type", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
settingsPath := filepath.Join(tmpDir, "config.json")
|
||||||
|
|
||||||
|
settings := &TownSettings{
|
||||||
|
Type: "invalid-type",
|
||||||
|
Version: CurrentTownSettingsVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := SaveTownSettings(settingsPath, settings)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for invalid type")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects unsupported version", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
settingsPath := filepath.Join(tmpDir, "config.json")
|
||||||
|
|
||||||
|
settings := &TownSettings{
|
||||||
|
Type: "town-settings",
|
||||||
|
Version: CurrentTownSettingsVersion + 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := SaveTownSettings(settingsPath, settings)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for unsupported version")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("roundtrip save and load", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
settingsPath := filepath.Join(tmpDir, "config.json")
|
||||||
|
|
||||||
|
original := &TownSettings{
|
||||||
|
Type: "town-settings",
|
||||||
|
Version: CurrentTownSettingsVersion,
|
||||||
|
DefaultAgent: "codex",
|
||||||
|
Agents: map[string]*RuntimeConfig{
|
||||||
|
"custom-1": {
|
||||||
|
Command: "custom-agent",
|
||||||
|
Args: []string{"--flag"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := SaveTownSettings(settingsPath, original)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveTownSettings failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded, err := LoadOrCreateTownSettings(settingsPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadOrCreateTownSettings failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loaded.Type != original.Type {
|
||||||
|
t.Errorf("Type = %q, want %q", loaded.Type, original.Type)
|
||||||
|
}
|
||||||
|
if loaded.Version != original.Version {
|
||||||
|
t.Errorf("Version = %d, want %d", loaded.Version, original.Version)
|
||||||
|
}
|
||||||
|
if loaded.DefaultAgent != original.DefaultAgent {
|
||||||
|
t.Errorf("DefaultAgent = %q, want %q", loaded.DefaultAgent, original.DefaultAgent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(loaded.Agents) != len(original.Agents) {
|
||||||
|
t.Errorf("Agents count = %d, want %d", len(loaded.Agents), len(original.Agents))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user