diff --git a/pkg/cli/commands_auto_compile_test.go b/pkg/cli/commands_auto_compile_test.go new file mode 100644 index 0000000000..e8bcd8214a --- /dev/null +++ b/pkg/cli/commands_auto_compile_test.go @@ -0,0 +1,338 @@ +package cli + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestEnsureAutoCompileWorkflow(t *testing.T) { + tests := []struct { + name string + setupFunc func(testDir string) error + verbose bool + expectError bool + expectFileCreated bool + errorContains string + }{ + { + name: "create new auto-compile workflow file", + setupFunc: func(testDir string) error { + // Initialize git repo + return initTestGitRepo(testDir) + }, + verbose: true, + expectError: false, + expectFileCreated: true, + }, + { + name: "update outdated auto-compile workflow file", + setupFunc: func(testDir string) error { + // Initialize git repo and create outdated workflow file + if err := initTestGitRepo(testDir); err != nil { + return err + } + + workflowsDir := filepath.Join(testDir, ".github", "workflows") + if err := os.MkdirAll(workflowsDir, 0755); err != nil { + return err + } + + outdatedContent := `name: Outdated Auto Compile +on: + push: + paths: ['.github/workflows/*.md'] +jobs: + compile: + runs-on: ubuntu-latest + steps: + - name: Old Step + run: echo "old"` + + autoCompileFile := filepath.Join(workflowsDir, "auto-compile-workflows.yml") + return os.WriteFile(autoCompileFile, []byte(outdatedContent), 0644) + }, + verbose: true, + expectError: false, + expectFileCreated: true, + }, + { + name: "workflow file already up to date", + setupFunc: func(testDir string) error { + // Initialize git repo and create up-to-date workflow file + if err := initTestGitRepo(testDir); err != nil { + return err + } + + workflowsDir := filepath.Join(testDir, ".github", "workflows") + if err := os.MkdirAll(workflowsDir, 0755); err != nil { + return err + } + + autoCompileFile := filepath.Join(workflowsDir, "auto-compile-workflows.yml") + return os.WriteFile(autoCompileFile, []byte(autoCompileWorkflowTemplate), 0644) + }, + verbose: true, + expectError: false, + expectFileCreated: true, // File exists but should not be modified + }, + { + name: "not in git repository", + setupFunc: func(testDir string) error { + // Don't initialize git repo to simulate not being in a git repository + return nil + }, + verbose: false, + expectError: true, + errorContains: "auto-compile workflow management requires being in a git repository", + }, + { + name: "permission denied to create workflow directory", + setupFunc: func(testDir string) error { + // Initialize git repo + if err := initTestGitRepo(testDir); err != nil { + return err + } + + // Create .github directory with read-only permissions to simulate permission error + githubDir := filepath.Join(testDir, ".github") + if err := os.MkdirAll(githubDir, 0555); err != nil { + return err + } + + return nil + }, + verbose: false, + expectError: true, + errorContains: "failed to create workflows directory", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary directory for test + testDir := t.TempDir() + + // Change to test directory + originalDir, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + defer func() { + if err := os.Chdir(originalDir); err != nil { + t.Errorf("Failed to restore original directory: %v", err) + } + }() + + if err := os.Chdir(testDir); err != nil { + t.Fatalf("Failed to change to test directory: %v", err) + } + + // Setup test environment + if tt.setupFunc != nil { + if err := tt.setupFunc(testDir); err != nil { + t.Fatalf("Setup function failed: %v", err) + } + } + + // Execute function under test + err = ensureAutoCompileWorkflow(tt.verbose) + + // Check error expectations + if tt.expectError { + if err == nil { + t.Errorf("ensureAutoCompileWorkflow() expected error but got none") + } else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("ensureAutoCompileWorkflow() error = %v, want error containing %v", err, tt.errorContains) + } + } else { + if err != nil { + t.Errorf("ensureAutoCompileWorkflow() unexpected error: %v", err) + } + } + + // Check file creation expectations + if !tt.expectError && tt.expectFileCreated { + autoCompileFile := filepath.Join(testDir, ".github", "workflows", "auto-compile-workflows.yml") + if _, err := os.Stat(autoCompileFile); os.IsNotExist(err) { + t.Errorf("ensureAutoCompileWorkflow() should have created file %s", autoCompileFile) + } else { + // Verify file content + content, err := os.ReadFile(autoCompileFile) + if err != nil { + t.Errorf("Failed to read auto-compile workflow file: %v", err) + } else { + contentStr := strings.TrimSpace(string(content)) + expectedStr := strings.TrimSpace(autoCompileWorkflowTemplate) + if contentStr != expectedStr { + t.Errorf("ensureAutoCompileWorkflow() file content does not match template") + } + } + } + } + + // Clean up permissions for deletion + if strings.Contains(tt.name, "permission denied") { + githubDir := filepath.Join(testDir, ".github") + if err := os.Chmod(githubDir, 0755); err != nil { + t.Errorf("Failed to restore write permissions for cleanup: %v", err) + } + } + }) + } +} + +func TestEnsureAutoCompileWorkflowEdgeCases(t *testing.T) { + t.Run("handle read error on existing file", func(t *testing.T) { + // This test is challenging to create as we'd need to create a file that exists but can't be read + // For now, we'll test the function behavior with a non-git directory + testDir := t.TempDir() + originalDir, _ := os.Getwd() + defer func() { + if err := os.Chdir(originalDir); err != nil { + t.Errorf("Failed to restore original directory: %v", err) + } + }() + if err := os.Chdir(testDir); err != nil { + t.Fatalf("Failed to change to test directory: %v", err) + } + + // Create a non-git directory structure + err := ensureAutoCompileWorkflow(false) + if err == nil { + t.Error("ensureAutoCompileWorkflow() should fail when not in git repo") + } + if !strings.Contains(err.Error(), "requires being in a git repository") { + t.Errorf("ensureAutoCompileWorkflow() error should mention git repository requirement, got: %v", err) + } + }) + + t.Run("verbose mode produces output", func(t *testing.T) { + // Test verbose mode doesn't crash (output testing is complex) + testDir := t.TempDir() + originalDir, _ := os.Getwd() + defer func() { + if err := os.Chdir(originalDir); err != nil { + t.Errorf("Failed to restore original directory: %v", err) + } + }() + if err := os.Chdir(testDir); err != nil { + t.Fatalf("Failed to change to test directory: %v", err) + } + + if err := initTestGitRepo(testDir); err != nil { + t.Fatalf("Failed to initialize test git repo: %v", err) + } + + // This should work and not panic in verbose mode + err := ensureAutoCompileWorkflow(true) + if err != nil { + t.Errorf("ensureAutoCompileWorkflow() with verbose=true failed: %v", err) + } + + // Run again to test the "up-to-date" path + err = ensureAutoCompileWorkflow(true) + if err != nil { + t.Errorf("ensureAutoCompileWorkflow() second run with verbose=true failed: %v", err) + } + }) + + t.Run("non-verbose mode works", func(t *testing.T) { + testDir := t.TempDir() + originalDir, _ := os.Getwd() + defer func() { + if err := os.Chdir(originalDir); err != nil { + t.Errorf("Failed to restore original directory: %v", err) + } + }() + if err := os.Chdir(testDir); err != nil { + t.Fatalf("Failed to change to test directory: %v", err) + } + + if err := initTestGitRepo(testDir); err != nil { + t.Fatalf("Failed to initialize test git repo: %v", err) + } + + // This should work and not produce output + err := ensureAutoCompileWorkflow(false) + if err != nil { + t.Errorf("ensureAutoCompileWorkflow() with verbose=false failed: %v", err) + } + }) +} + +func TestAutoCompileWorkflowTemplate(t *testing.T) { + // Test that the template constant is properly defined + t.Run("template is not empty", func(t *testing.T) { + if autoCompileWorkflowTemplate == "" { + t.Error("autoCompileWorkflowTemplate should not be empty") + } + }) + + t.Run("template contains expected elements", func(t *testing.T) { + template := autoCompileWorkflowTemplate + + expectedElements := []string{ + "name:", + "on:", + "push:", + "paths:", + ".github/workflows/*.md", + "jobs:", + "runs-on:", + } + + for _, element := range expectedElements { + if !strings.Contains(template, element) { + t.Errorf("autoCompileWorkflowTemplate should contain '%s'", element) + } + } + }) +} + +// Helper function to initialize a git repository in test directory +func initTestGitRepo(dir string) error { + // Create .git directory structure to simulate being in a git repo + gitDir := filepath.Join(dir, ".git") + if err := os.MkdirAll(gitDir, 0755); err != nil { + return err + } + + // Create subdirectories + subdirs := []string{"objects", "refs", "refs/heads", "refs/tags"} + for _, subdir := range subdirs { + if err := os.MkdirAll(filepath.Join(gitDir, subdir), 0755); err != nil { + return err + } + } + + // Create HEAD file pointing to main branch + headFile := filepath.Join(gitDir, "HEAD") + if err := os.WriteFile(headFile, []byte("ref: refs/heads/main\n"), 0644); err != nil { + return err + } + + // Create a minimal git config + configFile := filepath.Join(gitDir, "config") + configContent := `[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true +[user] + name = Test User + email = test@example.com` + + if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil { + return err + } + + // Create description file + descFile := filepath.Join(gitDir, "description") + if err := os.WriteFile(descFile, []byte("Test repository"), 0644); err != nil { + return err + } + + return nil +} diff --git a/pkg/console/console_formatting_test.go b/pkg/console/console_formatting_test.go new file mode 100644 index 0000000000..c871588ef5 --- /dev/null +++ b/pkg/console/console_formatting_test.go @@ -0,0 +1,422 @@ +package console + +import ( + "strings" + "testing" +) + +func TestFormatCommandMessage(t *testing.T) { + tests := []struct { + name string + command string + expected string + }{ + { + name: "simple command", + command: "git status", + expected: "git status", + }, + { + name: "complex command with flags", + command: "gh aw compile --verbose", + expected: "gh aw compile --verbose", + }, + { + name: "empty command", + command: "", + expected: "", + }, + { + name: "command with special characters", + command: "make test && echo 'done'", + expected: "make test && echo 'done'", + }, + { + name: "multiline command", + command: "echo 'line1'\necho 'line2'", + expected: "echo 'line1'\necho 'line2'", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatCommandMessage(tt.command) + + // Should contain the thunderbolt emoji prefix + if !strings.Contains(result, "⚡") { + t.Errorf("FormatCommandMessage() should contain ⚡ prefix") + } + + // Should contain the command text + if !strings.Contains(result, tt.expected) { + t.Errorf("FormatCommandMessage() = %v, should contain %v", result, tt.expected) + } + + // Result should not be empty unless input was empty + if tt.command != "" && result == "" { + t.Errorf("FormatCommandMessage() returned empty string for non-empty input") + } + }) + } +} + +func TestFormatProgressMessage(t *testing.T) { + tests := []struct { + name string + message string + expected string + }{ + { + name: "simple progress message", + message: "Compiling workflow files...", + expected: "Compiling workflow files...", + }, + { + name: "build progress message", + message: "Building application", + expected: "Building application", + }, + { + name: "empty message", + message: "", + expected: "", + }, + { + name: "message with numbers", + message: "Processing 5 of 10 files", + expected: "Processing 5 of 10 files", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatProgressMessage(tt.message) + + // Should contain the hammer emoji prefix + if !strings.Contains(result, "🔨") { + t.Errorf("FormatProgressMessage() should contain 🔨 prefix") + } + + // Should contain the message text + if !strings.Contains(result, tt.expected) { + t.Errorf("FormatProgressMessage() = %v, should contain %v", result, tt.expected) + } + }) + } +} + +func TestFormatPromptMessage(t *testing.T) { + tests := []struct { + name string + message string + expected string + }{ + { + name: "confirmation prompt", + message: "Are you sure you want to continue? [y/N]: ", + expected: "Are you sure you want to continue? [y/N]: ", + }, + { + name: "input prompt", + message: "Enter workflow name: ", + expected: "Enter workflow name: ", + }, + { + name: "empty prompt", + message: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatPromptMessage(tt.message) + + // Should contain the question mark emoji prefix + if !strings.Contains(result, "❓") { + t.Errorf("FormatPromptMessage() should contain ❓ prefix") + } + + // Should contain the message text + if !strings.Contains(result, tt.expected) { + t.Errorf("FormatPromptMessage() = %v, should contain %v", result, tt.expected) + } + }) + } +} + +func TestFormatCountMessage(t *testing.T) { + tests := []struct { + name string + message string + expected string + }{ + { + name: "file count", + message: "Found 15 workflows to compile", + expected: "Found 15 workflows to compile", + }, + { + name: "zero count", + message: "Found 0 issues", + expected: "Found 0 issues", + }, + { + name: "percentage", + message: "Coverage: 85.5%", + expected: "Coverage: 85.5%", + }, + { + name: "empty count message", + message: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatCountMessage(tt.message) + + // Should contain the chart emoji prefix + if !strings.Contains(result, "📊") { + t.Errorf("FormatCountMessage() should contain 📊 prefix") + } + + // Should contain the message text + if !strings.Contains(result, tt.expected) { + t.Errorf("FormatCountMessage() = %v, should contain %v", result, tt.expected) + } + }) + } +} + +func TestFormatVerboseMessage(t *testing.T) { + tests := []struct { + name string + message string + expected string + }{ + { + name: "debug message", + message: "Debug: Parsing frontmatter section", + expected: "Debug: Parsing frontmatter section", + }, + { + name: "detailed trace", + message: "Trace: Function called with args: [arg1, arg2]", + expected: "Trace: Function called with args: [arg1, arg2]", + }, + { + name: "empty verbose message", + message: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatVerboseMessage(tt.message) + + // Should contain the magnifying glass emoji prefix + if !strings.Contains(result, "🔍") { + t.Errorf("FormatVerboseMessage() should contain 🔍 prefix") + } + + // Should contain the message text + if !strings.Contains(result, tt.expected) { + t.Errorf("FormatVerboseMessage() = %v, should contain %v", result, tt.expected) + } + }) + } +} + +func TestFormatListHeader(t *testing.T) { + tests := []struct { + name string + header string + expected string + }{ + { + name: "simple header", + header: "Available Workflows", + expected: "Available Workflows", + }, + { + name: "header with underscores", + header: "==================", + expected: "==================", + }, + { + name: "empty header", + header: "", + expected: "", + }, + { + name: "header with numbers", + header: "Section 1: Configuration", + expected: "Section 1: Configuration", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatListHeader(tt.header) + + // Should contain the header text + if !strings.Contains(result, tt.expected) { + t.Errorf("FormatListHeader() = %v, should contain %v", result, tt.expected) + } + + // Result should not be empty unless input was empty + if tt.header != "" && result == "" { + t.Errorf("FormatListHeader() returned empty string for non-empty input") + } + }) + } +} + +func TestFormatListItem(t *testing.T) { + tests := []struct { + name string + item string + expected string + }{ + { + name: "simple item", + item: "weekly-research.md", + expected: "weekly-research.md", + }, + { + name: "item with path", + item: "src/workflow/daily-plan.md", + expected: "src/workflow/daily-plan.md", + }, + { + name: "empty item", + item: "", + expected: "", + }, + { + name: "item with spaces", + item: "Complex Workflow Name", + expected: "Complex Workflow Name", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatListItem(tt.item) + + // Should contain the bullet point prefix + if !strings.Contains(result, "•") { + t.Errorf("FormatListItem() should contain • prefix") + } + + // Should contain the item text + if !strings.Contains(result, tt.expected) { + t.Errorf("FormatListItem() = %v, should contain %v", result, tt.expected) + } + + // Should contain proper indentation + if !strings.Contains(result, " •") { + t.Errorf("FormatListItem() should contain proper indentation ' •'") + } + }) + } +} + +func TestFormatErrorMessage(t *testing.T) { + tests := []struct { + name string + message string + expected string + }{ + { + name: "simple error", + message: "File not found", + expected: "File not found", + }, + { + name: "detailed error", + message: "failed to compile workflow: invalid syntax at line 15", + expected: "failed to compile workflow: invalid syntax at line 15", + }, + { + name: "empty error message", + message: "", + expected: "", + }, + { + name: "error with code", + message: "exit code 1: command failed", + expected: "exit code 1: command failed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatErrorMessage(tt.message) + + // Should contain the X emoji prefix + if !strings.Contains(result, "✗") { + t.Errorf("FormatErrorMessage() should contain ✗ prefix") + } + + // Should contain the error message text + if !strings.Contains(result, tt.expected) { + t.Errorf("FormatErrorMessage() = %v, should contain %v", result, tt.expected) + } + }) + } +} + +// Edge case tests for all formatting functions +func TestFormattingFunctionsWithSpecialCharacters(t *testing.T) { + specialChars := "!@#$%^&*()[]{}|\\:;\"'<>,.?/`~" + + // Test that all functions handle special characters without panicking + t.Run("special characters don't cause panic", func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Formatting function panicked with special characters: %v", r) + } + }() + + FormatCommandMessage(specialChars) + FormatProgressMessage(specialChars) + FormatPromptMessage(specialChars) + FormatCountMessage(specialChars) + FormatVerboseMessage(specialChars) + FormatListHeader(specialChars) + FormatListItem(specialChars) + FormatErrorMessage(specialChars) + }) +} + +func TestFormattingFunctionsWithUnicodeCharacters(t *testing.T) { + unicodeText := "Test with unicode: 你好 🌍 café naïve résumé" + + // Test that all functions handle unicode characters properly + t.Run("unicode characters handled properly", func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Formatting function panicked with unicode characters: %v", r) + } + }() + + result1 := FormatCommandMessage(unicodeText) + if !strings.Contains(result1, unicodeText) { + t.Errorf("FormatCommandMessage did not preserve unicode text") + } + + result2 := FormatProgressMessage(unicodeText) + if !strings.Contains(result2, unicodeText) { + t.Errorf("FormatProgressMessage did not preserve unicode text") + } + + result3 := FormatErrorMessage(unicodeText) + if !strings.Contains(result3, unicodeText) { + t.Errorf("FormatErrorMessage did not preserve unicode text") + } + }) +}