diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 0000000..152f665 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,82 @@ +name: Integration Test + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +permissions: + contents: read + +jobs: + vhs-test: + runs-on: ubuntu-latest + env: + CGO_ENABLED: 0 + AWS_ENDPOINT_URL: http://localhost:4566 + AWS_ACCESS_KEY_ID: test + AWS_SECRET_ACCESS_KEY: test + AWS_DEFAULT_REGION: us-east-1 + AWS_EC2_METADATA_DISABLED: "true" + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-go@v6 + with: + go-version-file: 'go.mod' + cache: true + + - name: Build + run: go build -o claws ./cmd/claws + + - name: Start LocalStack + run: | + docker run -d --name localstack -p 4566:4566 localstack/localstack:4.12.0 + echo "Waiting for LocalStack..." + for i in $(seq 1 30); do + if curl -s http://localhost:4566/_localstack/health | grep -qE '"s3": "(available|running)"'; then + echo "LocalStack is ready" + exit 0 + fi + sleep 1 + done + echo "LocalStack failed to start" + exit 1 + + - name: Setup demo data + timeout-minutes: 5 + run: ./scripts/localstack-demo-setup.sh + + - name: Create AWS config for demo + run: | + mkdir -p ~/.aws + cp scripts/demo-aws-config/config ~/.aws/config + cp scripts/demo-aws-config/credentials ~/.aws/credentials + + - name: Run VHS tapes + run: | + set -e + for tape in docs/tapes/*.tape; do + echo "==========================================" + echo "Running: $tape" + echo "==========================================" + docker run --rm --network host \ + -v "$(pwd)":/vhs \ + -v ~/.aws:/root/.aws:ro \ + -e AWS_ENDPOINT_URL=http://localhost:4566 \ + -e AWS_EC2_METADATA_DISABLED=true \ + ghcr.io/charmbracelet/vhs "$tape" + done + + - name: Upload screenshots on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: vhs-screenshots + path: docs/images/ + retention-days: 7 + + - name: Cleanup LocalStack + if: always() + run: docker stop localstack || true diff --git a/Taskfile.yml b/Taskfile.yml index f73547c..1e53fd7 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -107,7 +107,9 @@ tasks: cmds: - task: demo:record:gif - task: demo:record:themes + - task: demo:record:theme-light - task: demo:record:features + - task: demo:record:command-mode demo:record:gif: desc: Record demo.gif only @@ -154,6 +156,57 @@ tasks: -e AWS_EC2_METADATA_DISABLED=true \ ghcr.io/charmbracelet/vhs docs/tapes/features.tape + demo:record:theme-light: + desc: Record light theme screenshot (requires white terminal background) + deps: [build, localstack:start, localstack:demo-setup] + preconditions: + - sh: '[ "$(uname -s)" = "Linux" ]' + msg: "demo:record requires Linux (--network host not supported on macOS/Windows)" + cmds: + - | + docker run --rm --network host \ + -v "$(pwd)":/vhs \ + -v "$(pwd)/scripts/demo-aws-config:/root/.aws:ro" \ + -e AWS_ENDPOINT_URL=http://localhost:4566 \ + -e AWS_EC2_METADATA_DISABLED=true \ + ghcr.io/charmbracelet/vhs docs/tapes/theme-light.tape + + demo:record:command-mode: + desc: Record command mode suggestion/completion test + deps: [build, localstack:start, localstack:demo-setup] + preconditions: + - sh: '[ "$(uname -s)" = "Linux" ]' + msg: "demo:record requires Linux (--network host not supported on macOS/Windows)" + cmds: + - | + docker run --rm --network host \ + -v "$(pwd)":/vhs \ + -v "$(pwd)/scripts/demo-aws-config:/root/.aws:ro" \ + -e AWS_ENDPOINT_URL=http://localhost:4566 \ + -e AWS_EC2_METADATA_DISABLED=true \ + ghcr.io/charmbracelet/vhs docs/tapes/command-mode.tape + + test:vhs: + desc: Run all VHS tapes as integration tests + deps: [build, localstack:start, localstack:demo-setup] + preconditions: + - sh: '[ "$(uname -s)" = "Linux" ]' + msg: "test:vhs requires Linux (--network host not supported on macOS/Windows)" + cmds: + - | + set -e + for tape in docs/tapes/*.tape; do + echo "==========================================" + echo "Running: $tape" + echo "==========================================" + docker run --rm --network host \ + -v "$(pwd)":/vhs \ + -v "$(pwd)/scripts/demo-aws-config:/root/.aws:ro" \ + -e AWS_ENDPOINT_URL=http://localhost:4566 \ + -e AWS_EC2_METADATA_DISABLED=true \ + ghcr.io/charmbracelet/vhs "$tape" + done + test-localstack: desc: Run integration tests with LocalStack deps: [localstack:start] diff --git a/cmd/claws/main.go b/cmd/claws/main.go index 56a3a5c..187ee8b 100644 --- a/cmd/claws/main.go +++ b/cmd/claws/main.go @@ -26,6 +26,18 @@ func main() { propagateAllProxy() + // Set custom config path (CLI flag > env var > default) + configPath := opts.configFile + if configPath == "" { + configPath = strings.TrimSpace(os.Getenv("CLAWS_CONFIG")) + } + if configPath != "" { + if err := config.SetConfigPath(configPath); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + } + fileCfg := config.File() cfg := config.Global() @@ -110,6 +122,7 @@ type cliOptions struct { envCreds bool autosave *bool logFile string + configFile string service string resourceID string theme string @@ -161,6 +174,11 @@ func parseFlagsFromArgs(args []string) cliOptions { i++ opts.logFile = args[i] } + case "-c", "--config": + if i+1 < len(args) { + i++ + opts.configFile = args[i] + } case "-s", "--service": if i+1 < len(args) { i++ @@ -221,6 +239,8 @@ func printUsage() { fmt.Println(" Enable saving region/profile/theme to config file") fmt.Println(" --no-autosave") fmt.Println(" Disable saving region/profile/theme to config file") + fmt.Println(" -c, --config ") + fmt.Println(" Use custom config file instead of ~/.config/claws/config.yaml") fmt.Println(" -l, --log-file ") fmt.Println(" Enable debug logging to specified file") fmt.Println(" -t, --theme ") @@ -242,6 +262,7 @@ func printUsage() { fmt.Println(" claws -r us-east-1,ap-northeast-1 Query multiple regions") fmt.Println() fmt.Println("Environment Variables:") + fmt.Println(" CLAWS_CONFIG= Use custom config file") fmt.Println(" CLAWS_READ_ONLY=1|true Enable read-only mode") fmt.Println(" ALL_PROXY Propagated to HTTP_PROXY/HTTPS_PROXY if not set") } diff --git a/cmd/claws/main_test.go b/cmd/claws/main_test.go index bb130a8..cf3b860 100644 --- a/cmd/claws/main_test.go +++ b/cmd/claws/main_test.go @@ -124,3 +124,25 @@ func TestParseFlags_Combined(t *testing.T) { t.Error("readOnly should be true") } } + +func TestParseFlags_ConfigFile(t *testing.T) { + tests := []struct { + name string + args []string + expected string + }{ + {"short flag", []string{"-c", "/path/to/config.yaml"}, "/path/to/config.yaml"}, + {"long flag", []string{"--config", "/custom/config.yaml"}, "/custom/config.yaml"}, + {"with other flags", []string{"-p", "dev", "-c", "/config.yaml", "-r", "us-east-1"}, "/config.yaml"}, + {"no config", []string{"-p", "dev"}, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := parseFlagsFromArgs(tt.args) + if opts.configFile != tt.expected { + t.Errorf("configFile = %q, want %q", opts.configFile, tt.expected) + } + }) + } +} diff --git a/docs/configuration.md b/docs/configuration.md index 612f924..00dfa6c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -10,7 +10,29 @@ claws uses your standard AWS configuration: ## Configuration File -Optional settings can be stored in `~/.config/claws/config.yaml`: +Optional settings can be stored in `~/.config/claws/config.yaml`. + +### Custom Config File Path + +Use a custom config file instead of the default: + +```bash +# Via CLI flag +claws -c /path/to/config.yaml +claws --config ~/work/claws-work.yaml + +# Via environment variable +CLAWS_CONFIG=/path/to/config.yaml claws +``` + +**Precedence:** `-c` flag > `CLAWS_CONFIG` env var > default (`~/.config/claws/config.yaml`) + +Use cases: +- Environment-specific configs (work/personal) +- CI/CD with project-specific settings +- Testing with different configurations + +### Config File Format ```yaml timeouts: diff --git a/docs/images/actions-menu.png b/docs/images/actions-menu.png index 36ebf96..ff53437 100644 Binary files a/docs/images/actions-menu.png and b/docs/images/actions-menu.png differ diff --git a/docs/images/cmd-suggest-ec.png b/docs/images/cmd-suggest-ec.png new file mode 100644 index 0000000..4db6f8e Binary files /dev/null and b/docs/images/cmd-suggest-ec.png differ diff --git a/docs/images/cmd-suggest-ec2-c.png b/docs/images/cmd-suggest-ec2-c.png new file mode 100644 index 0000000..b74361d Binary files /dev/null and b/docs/images/cmd-suggest-ec2-c.png differ diff --git a/docs/images/cmd-suggest-ec2-slash.png b/docs/images/cmd-suggest-ec2-slash.png new file mode 100644 index 0000000..443ed61 Binary files /dev/null and b/docs/images/cmd-suggest-ec2-slash.png differ diff --git a/docs/images/cmd-suggest-ec2.png b/docs/images/cmd-suggest-ec2.png new file mode 100644 index 0000000..4db6f8e Binary files /dev/null and b/docs/images/cmd-suggest-ec2.png differ diff --git a/docs/images/cmd-tab-completion.png b/docs/images/cmd-tab-completion.png new file mode 100644 index 0000000..f3240fc Binary files /dev/null and b/docs/images/cmd-tab-completion.png differ diff --git a/docs/images/demo.gif b/docs/images/demo.gif index 15ae199..4f46f73 100644 Binary files a/docs/images/demo.gif and b/docs/images/demo.gif differ diff --git a/docs/images/detail-view.png b/docs/images/detail-view.png index dc12095..939cb42 100644 Binary files a/docs/images/detail-view.png and b/docs/images/detail-view.png differ diff --git a/docs/images/multi-account-region.png b/docs/images/multi-account-region.png index a7ef7b8..52b06cd 100644 Binary files a/docs/images/multi-account-region.png and b/docs/images/multi-account-region.png differ diff --git a/docs/images/multi-profile.png b/docs/images/multi-profile.png index 99b1cc3..a43fd2e 100644 Binary files a/docs/images/multi-profile.png and b/docs/images/multi-profile.png differ diff --git a/docs/images/multi-region-data.png b/docs/images/multi-region-data.png index 2adc098..7f2e098 100644 Binary files a/docs/images/multi-region-data.png and b/docs/images/multi-region-data.png differ diff --git a/docs/images/multi-region.png b/docs/images/multi-region.png index 2021fa6..84e27d3 100644 Binary files a/docs/images/multi-region.png and b/docs/images/multi-region.png differ diff --git a/docs/images/resource-browser.png b/docs/images/resource-browser.png index bf4a4a4..642b433 100644 Binary files a/docs/images/resource-browser.png and b/docs/images/resource-browser.png differ diff --git a/docs/images/test-diff-after-tab.png b/docs/images/test-diff-after-tab.png deleted file mode 100644 index 1a9c5d8..0000000 Binary files a/docs/images/test-diff-after-tab.png and /dev/null differ diff --git a/docs/images/test-diff-after-tab2.png b/docs/images/test-diff-after-tab2.png deleted file mode 100644 index f05ea91..0000000 Binary files a/docs/images/test-diff-after-tab2.png and /dev/null differ diff --git a/docs/images/test-diff-before-tab.png b/docs/images/test-diff-before-tab.png deleted file mode 100644 index e9fad98..0000000 Binary files a/docs/images/test-diff-before-tab.png and /dev/null differ diff --git a/docs/images/test-diff.gif b/docs/images/test-diff.gif deleted file mode 100644 index 0b83fb5..0000000 Binary files a/docs/images/test-diff.gif and /dev/null differ diff --git a/docs/images/theme-catppuccin.png b/docs/images/theme-catppuccin.png index 45aaa9d..3b5b3e2 100644 Binary files a/docs/images/theme-catppuccin.png and b/docs/images/theme-catppuccin.png differ diff --git a/docs/images/theme-dark.png b/docs/images/theme-dark.png index f5c431a..36d597d 100644 Binary files a/docs/images/theme-dark.png and b/docs/images/theme-dark.png differ diff --git a/docs/images/theme-dracula.png b/docs/images/theme-dracula.png index 06e9372..6cbc5c6 100644 Binary files a/docs/images/theme-dracula.png and b/docs/images/theme-dracula.png differ diff --git a/docs/images/theme-gruvbox.png b/docs/images/theme-gruvbox.png index 46deae7..3aa0fcf 100644 Binary files a/docs/images/theme-gruvbox.png and b/docs/images/theme-gruvbox.png differ diff --git a/docs/images/theme-light.png b/docs/images/theme-light.png index b4c6185..d07fd72 100644 Binary files a/docs/images/theme-light.png and b/docs/images/theme-light.png differ diff --git a/docs/images/theme-nord.png b/docs/images/theme-nord.png index 86343c3..95fdcd8 100644 Binary files a/docs/images/theme-nord.png and b/docs/images/theme-nord.png differ diff --git a/docs/tapes/README.md b/docs/tapes/README.md index d569e9f..5c20320 100644 --- a/docs/tapes/README.md +++ b/docs/tapes/README.md @@ -8,7 +8,9 @@ This directory contains [VHS](https://github.com/charmbracelet/vhs) tape files f |------|---------|--------| | `demo.tape` | Main demo GIF | `docs/images/demo.gif` | | `themes.tape` | Theme screenshots | `docs/images/theme-*.png` | +| `theme-light.tape` | Light theme (white bg) | `docs/images/theme-light.png` | | `features.tape` | Feature screenshots | `docs/images/*.png` | +| `command-mode.tape` | Command suggestions/completion | `docs/images/cmd-*.png` | ## Usage (Recommended) @@ -19,9 +21,14 @@ Use task commands from project root (requires Docker + Linux): task demo:record # Record individual items -task demo:record:gif # Main demo GIF only -task demo:record:themes # Theme screenshots only -task demo:record:features # Feature screenshots only +task demo:record:gif # Main demo GIF only +task demo:record:themes # Theme screenshots only +task demo:record:theme-light # Light theme (requires white terminal bg) +task demo:record:features # Feature screenshots only +task demo:record:command-mode # Command mode tests + +# Run all tapes as integration tests +task test:vhs ``` This automatically: diff --git a/docs/tapes/command-mode.tape b/docs/tapes/command-mode.tape new file mode 100644 index 0000000..e411922 --- /dev/null +++ b/docs/tapes/command-mode.tape @@ -0,0 +1,84 @@ +# Command Mode Suggestion & Completion Test +# Tests command mode navigation, suggestions, tab completion, and enter behavior + +Set FontSize 16 +Set Width 1200 +Set Height 600 +Set TypingSpeed 0.08 + +Type "./claws" +Enter +Sleep 2s +Require "Services" + +# --- Suggestion Tests --- + +# ec → ec2 | ecr | ecs +Type ":" +Sleep 300ms +Type "ec" +Sleep 500ms +Require "ec2" +Require "ecr" +Require "ecs" +Screenshot docs/images/cmd-suggest-ec.png + +# ec2 → ec2 (resolved) +Type "2" +Sleep 500ms +Screenshot docs/images/cmd-suggest-ec2.png + +# ec2/ → ec2 | ec2/capacity-reservations | ... +Type "/" +Sleep 500ms +Require "ec2/capacity" +Screenshot docs/images/cmd-suggest-ec2-slash.png + +# ec2/c → ec2/capacity-reservations +Type "c" +Sleep 500ms +Require "capacity-reservations" +Screenshot docs/images/cmd-suggest-ec2-c.png + +Escape +Sleep 300ms + +# --- Enter Navigation Tests --- + +# ec Enter → ec2 service +Type ":ec" +Sleep 300ms +Enter +Sleep 2s +Require "ec2" + +# ec2/ Enter → ec2 service +Type ":ec2/" +Sleep 300ms +Enter +Sleep 2s +Require "ec2" + +# ec2/c Enter → ec2/capacity-reservations +Type ":ec2/c" +Sleep 300ms +Enter +Sleep 2s +Require "Capacity" + +# --- Tab Completion Test --- + +# ec2/c Tab → completes to ec2/capacity-reservations +Type ":" +Sleep 300ms +Type "ec2/c" +Sleep 300ms +Tab +Sleep 500ms +Require "ec2/capacity-reservations" +Screenshot docs/images/cmd-tab-completion.png +Escape +Sleep 300ms + +Type "q" +Sleep 500ms diff --git a/docs/tapes/demo.tape b/docs/tapes/demo.tape index ca08f36..46efb17 100644 --- a/docs/tapes/demo.tape +++ b/docs/tapes/demo.tape @@ -14,6 +14,7 @@ Sleep 1.5s Enter Sleep 2.5s +Require "Services" Type ":" Sleep 1.5s @@ -21,11 +22,13 @@ Type "vpc/subnets" Sleep 1.5s Enter Sleep 2s +Require "Subnet ID" Type "/web" Sleep 1s Enter Sleep 2s +Require "web" Type "m" Sleep 1s @@ -33,6 +36,7 @@ Type "G" Sleep 1s Type "d" Sleep 3s +Require "Detail" Escape Sleep 1.5s @@ -42,14 +46,17 @@ Type "ec2/instances" Sleep 1.5s Enter Sleep 2s +Require "Instance ID" Type "/running" Sleep 1s Enter Sleep 2s +Require "running" Type "a" Sleep 2s +Require "Stop" Type "S" Sleep 1.5s Type "y" @@ -62,6 +69,7 @@ Sleep 2s Type "a" Sleep 2s +Require "Describe" Type "D" Sleep 2s Type "i-" diff --git a/docs/tapes/features.tape b/docs/tapes/features.tape index cecc590..83f591c 100644 --- a/docs/tapes/features.tape +++ b/docs/tapes/features.tape @@ -9,28 +9,33 @@ Set TypingSpeed 0.05 Type "./claws" Enter Sleep 2s +Require "Services" Type ":" Sleep 0.5s Type "ec2/instances" Enter Sleep 2s +Require "Instance ID" Screenshot docs/images/resource-browser.png Type "d" Sleep 2s +Require "Detail" Screenshot docs/images/detail-view.png Escape Sleep 1s Type "a" Sleep 1.5s +Require "Stop" Screenshot docs/images/actions-menu.png Escape Sleep 1s Type "R" Sleep 1s +Require "Region" Screenshot docs/images/multi-region.png Type "/ap-northeast-1" Sleep 1s diff --git a/docs/tapes/test-diff.tape b/docs/tapes/test-diff.tape deleted file mode 100644 index 9e9ffad..0000000 --- a/docs/tapes/test-diff.tape +++ /dev/null @@ -1,37 +0,0 @@ -Output docs/images/test-diff.gif - -Set Shell "bash" -Set FontSize 16 -Set Width 800 -Set Height 400 -Set Padding 10 - -Type "./claws" -Enter -Sleep 2s - -# Navigate to ec2 instances (has demo data) -Type ":ec2/instances" -Enter -Sleep 2s - -# Open command mode and type diff with space -Type ":diff " -Sleep 500ms - -Screenshot docs/images/test-diff-before-tab.png - -# Press Tab to complete -Tab -Sleep 500ms - -Screenshot docs/images/test-diff-after-tab.png - -# Press Tab again -Tab -Sleep 500ms - -Screenshot docs/images/test-diff-after-tab2.png - -Type "q" -Sleep 500ms diff --git a/docs/tapes/theme-light.tape b/docs/tapes/theme-light.tape index 1449aed..8bbfb0d 100644 --- a/docs/tapes/theme-light.tape +++ b/docs/tapes/theme-light.tape @@ -13,6 +13,7 @@ Sleep 0.5s Type "ec2/instances" Enter Sleep 2s +Require "Instance ID" Screenshot docs/images/theme-light.png diff --git a/docs/tapes/themes.tape b/docs/tapes/themes.tape index 0bb9c13..1682a01 100644 --- a/docs/tapes/themes.tape +++ b/docs/tapes/themes.tape @@ -15,26 +15,12 @@ Sleep 0.5s Type "ec2/instances" Enter Sleep 2s +Require "Instance ID" Screenshot docs/images/theme-dark.png Type "q" Sleep 0.5s -# Theme: light (white background) -Set Theme { "background": "#ffffff", "foreground": "#333333", "black": "#000000", "white": "#ffffff" } -Type "./claws -t light" -Enter -Sleep 2s -Type ":" -Sleep 0.5s -Type "ec2/instances" -Enter -Sleep 2s -Screenshot docs/images/theme-light.png -Type "q" -Sleep 0.5s - -# Reset to dark background for remaining themes -Set Theme "Builtin Dark" +# Theme: light - see theme-light.tape (requires white terminal background) # Theme: nord Type "./claws -t nord" @@ -45,6 +31,7 @@ Sleep 0.5s Type "ec2/instances" Enter Sleep 2s +Require "Instance ID" Screenshot docs/images/theme-nord.png Type "q" Sleep 0.5s @@ -58,6 +45,7 @@ Sleep 0.5s Type "ec2/instances" Enter Sleep 2s +Require "Instance ID" Screenshot docs/images/theme-dracula.png Type "q" Sleep 0.5s @@ -71,6 +59,7 @@ Sleep 0.5s Type "ec2/instances" Enter Sleep 2s +Require "Instance ID" Screenshot docs/images/theme-gruvbox.png Type "q" Sleep 0.5s @@ -84,6 +73,7 @@ Sleep 0.5s Type "ec2/instances" Enter Sleep 2s +Require "Instance ID" Screenshot docs/images/theme-catppuccin.png Type "q" Sleep 0.5s diff --git a/internal/config/file.go b/internal/config/file.go index bccb5a3..763c8c2 100644 --- a/internal/config/file.go +++ b/internal/config/file.go @@ -6,11 +6,13 @@ import ( "fmt" "os" "path/filepath" + "strings" "sync" "time" "gopkg.in/yaml.v3" + apperrors "github.com/clawscli/claws/internal/errors" "github.com/clawscli/claws/internal/log" ) @@ -27,7 +29,55 @@ const ( DefaultAIMaxToolCallsPerQuery = 50 ) +var ( + customConfigPath string + configPathMu sync.RWMutex +) + +// expandTilde expands ~ to user home directory. +func expandTilde(path string) (string, error) { + if strings.HasPrefix(path, "~/") { + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("expand ~: %w", err) + } + return filepath.Join(home, path[2:]), nil + } + return path, nil +} + +// SetConfigPath sets custom config file path. Must be called before File(). +// Returns error if file doesn't exist or isn't readable. +func SetConfigPath(path string) error { + expanded, err := expandTilde(path) + if err != nil { + return apperrors.Wrap(err, "config file", "path", path) + } + if _, err := os.Stat(expanded); err != nil { + return apperrors.Wrap(err, "config file", "path", expanded) + } + configPathMu.Lock() + customConfigPath = expanded + configPathMu.Unlock() + return nil +} + +// GetConfigPath returns the current custom config path (empty if using default). +func GetConfigPath() string { + configPathMu.RLock() + defer configPathMu.RUnlock() + return customConfigPath +} + func ConfigDir() (string, error) { + configPathMu.RLock() + custom := customConfigPath + configPathMu.RUnlock() + + if custom != "" { + return filepath.Dir(custom), nil + } + home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("get home dir: %w", err) @@ -36,6 +86,14 @@ func ConfigDir() (string, error) { } func ConfigPath() (string, error) { + configPathMu.RLock() + custom := customConfigPath + configPathMu.RUnlock() + + if custom != "" { + return custom, nil + } + dir, err := ConfigDir() if err != nil { return "", err diff --git a/internal/config/file_test.go b/internal/config/file_test.go index b77f068..639dc3a 100644 --- a/internal/config/file_test.go +++ b/internal/config/file_test.go @@ -688,6 +688,132 @@ func TestGetAIMaxToolCallsPerQuery(t *testing.T) { } } +func TestSetConfigPath(t *testing.T) { + // Create temp config file + tmpDir := t.TempDir() + customPath := filepath.Join(tmpDir, "custom-config.yaml") + if err := os.WriteFile(customPath, []byte("theme: dracula\n"), 0644); err != nil { + t.Fatalf("WriteFile failed: %v", err) + } + + // Reset custom path after test + defer func() { + configPathMu.Lock() + customConfigPath = "" + configPathMu.Unlock() + }() + + // Test setting valid path + if err := SetConfigPath(customPath); err != nil { + t.Fatalf("SetConfigPath failed: %v", err) + } + + // Verify GetConfigPath returns the custom path + if got := GetConfigPath(); got != customPath { + t.Errorf("GetConfigPath() = %q, want %q", got, customPath) + } + + // Verify ConfigPath returns the custom path + got, err := ConfigPath() + if err != nil { + t.Fatalf("ConfigPath failed: %v", err) + } + if got != customPath { + t.Errorf("ConfigPath() = %q, want %q", got, customPath) + } + + // Verify ConfigDir returns custom path's directory + gotDir, err := ConfigDir() + if err != nil { + t.Fatalf("ConfigDir failed: %v", err) + } + if gotDir != tmpDir { + t.Errorf("ConfigDir() = %q, want %q", gotDir, tmpDir) + } +} + +func TestSetConfigPath_NonExistent(t *testing.T) { + err := SetConfigPath("/nonexistent/path/config.yaml") + if err == nil { + t.Error("SetConfigPath should fail for non-existent file") + } +} + +func TestSetConfigPath_TildeExpansion(t *testing.T) { + home, err := os.UserHomeDir() + if err != nil { + t.Skip("cannot get user home dir") + } + + // Create temp file in home dir for test + tmpFile := filepath.Join(home, ".claws-test-config.yaml") + if err := os.WriteFile(tmpFile, []byte("theme: test\n"), 0600); err != nil { + t.Fatalf("WriteFile failed: %v", err) + } + defer os.Remove(tmpFile) + + // Reset custom path after test + defer func() { + configPathMu.Lock() + customConfigPath = "" + configPathMu.Unlock() + }() + + // Test tilde expansion + if err := SetConfigPath("~/.claws-test-config.yaml"); err != nil { + t.Fatalf("SetConfigPath with tilde failed: %v", err) + } + + // Verify path was expanded + got := GetConfigPath() + if got != tmpFile { + t.Errorf("GetConfigPath() = %q, want %q (expanded)", got, tmpFile) + } +} + +func TestCustomConfigPath_Load(t *testing.T) { + tmpDir := t.TempDir() + customPath := filepath.Join(tmpDir, "my-config.yaml") + configData := `theme: nord +startup: + regions: + - eu-west-1 + profiles: + - custom-profile +` + if err := os.WriteFile(customPath, []byte(configData), 0644); err != nil { + t.Fatalf("WriteFile failed: %v", err) + } + + // Reset custom path after test + defer func() { + configPathMu.Lock() + customConfigPath = "" + configPathMu.Unlock() + }() + + if err := SetConfigPath(customPath); err != nil { + t.Fatalf("SetConfigPath failed: %v", err) + } + + cfg, err := Load() + if err != nil { + t.Fatalf("Load failed: %v", err) + } + + if cfg.Theme.Preset != "nord" { + t.Errorf("Theme.Preset = %q, want %q", cfg.Theme.Preset, "nord") + } + + regions, profiles := cfg.GetStartup() + if len(regions) != 1 || regions[0] != "eu-west-1" { + t.Errorf("regions = %v, want [eu-west-1]", regions) + } + if len(profiles) != 1 || profiles[0] != "custom-profile" { + t.Errorf("profiles = %v, want [custom-profile]", profiles) + } +} + func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr)) }