diff --git a/pkg/cli/add_interactive_engine.go b/pkg/cli/add_interactive_engine.go index 5d7c17bc35..67acb43546 100644 --- a/pkg/cli/add_interactive_engine.go +++ b/pkg/cli/add_interactive_engine.go @@ -74,7 +74,7 @@ func (c *AddInteractiveConfig) selectAIEngineAndKey() error { // If engine is already overridden, skip selection if c.EngineOverride != "" { - fmt.Fprintf(os.Stderr, "Using coding agent: %s\n", c.EngineOverride) + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Using coding agent: %s", c.EngineOverride))) return c.configureEngineAPISecret(c.EngineOverride) } diff --git a/pkg/cli/add_interactive_secrets_test.go b/pkg/cli/add_interactive_secrets_test.go index f68a9bfec6..5809542750 100644 --- a/pkg/cli/add_interactive_secrets_test.go +++ b/pkg/cli/add_interactive_secrets_test.go @@ -3,9 +3,13 @@ package cli import ( + "bytes" + "context" + "io" "os" "testing" + "github.com/github/gh-aw/pkg/console" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -169,6 +173,37 @@ func TestAddInteractiveConfig_configureEngineAPISecret_skipSecret(t *testing.T) } } +func TestAddInteractiveConfig_selectAIEngineAndKey_engineOverrideFormatsInfoMessage(t *testing.T) { + config := &AddInteractiveConfig{ + Ctx: context.Background(), + EngineOverride: "copilot", + SkipSecret: true, + RepoOverride: "owner/repo", + } + + oldStderr := os.Stderr + r, w, err := os.Pipe() + require.NoError(t, err, "Failed to create stderr pipe") + os.Stderr = w + t.Cleanup(func() { os.Stderr = oldStderr }) + + err = config.selectAIEngineAndKey() + + w.Close() + + var buf bytes.Buffer + _, copyErr := io.Copy(&buf, r) + require.NoError(t, copyErr, "Failed to read stderr output") + require.NoError(t, err, "selectAIEngineAndKey should succeed with an explicit engine override") + + assert.Contains( + t, + buf.String(), + console.FormatInfoMessage("Using coding agent: copilot"), + "Expected engine override path to use formatted info output", + ) +} + func TestParseSecretNames(t *testing.T) { tests := []struct { name string diff --git a/pkg/cli/commands_file_watching_test.go b/pkg/cli/commands_file_watching_test.go index bac66f6883..a765005bd5 100644 --- a/pkg/cli/commands_file_watching_test.go +++ b/pkg/cli/commands_file_watching_test.go @@ -3,14 +3,17 @@ package cli import ( + "bytes" "context" "fmt" + "io" "os" "path/filepath" "strings" "testing" "time" + "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/stringutil" "github.com/github/gh-aw/pkg/testutil" @@ -439,6 +442,40 @@ func TestCompileSingleFile(t *testing.T) { } }) + t.Run("compile single file formats errors for stderr", func(t *testing.T) { + tempDir := testutil.TempDir(t, "test-*") + workflowsDir := filepath.Join(tempDir, ".github/workflows") + os.MkdirAll(workflowsDir, 0755) + + filePath := filepath.Join(workflowsDir, "invalid.md") + content := "---\nmalformed: yaml: content:\n - missing\n proper: structure\n---\n# Invalid\n" + os.WriteFile(filePath, []byte(content), 0644) + + compiler := workflow.NewCompiler() + stats := &CompilationStats{} + + oldStderr := os.Stderr + r, w, err := os.Pipe() + require.NoError(t, err, "Failed to create stderr pipe") + os.Stderr = w + t.Cleanup(func() { os.Stderr = oldStderr }) + + result := compileSingleFile(compiler, filePath, stats, false, false) + + w.Close() + + var buf bytes.Buffer + _, err = io.Copy(&buf, r) + require.NoError(t, err, "Failed to read stderr output") + + strippedOutput := stringutil.StripANSI(buf.String()) + + assert.True(t, result, "Expected compilation to be attempted") + assert.Contains(t, strippedOutput, "✗", "Expected compile errors to include the formatted error marker") + assert.Contains(t, strippedOutput, "invalid.md", "Expected stderr output to identify the failing workflow") + assert.Contains(t, strippedOutput, "unexpected ':'", "Expected stderr output to include the compiler error details") + }) + t.Run("compile single file with checkExists true and file exists", func(t *testing.T) { tempDir := testutil.TempDir(t, "test-*") workflowsDir := filepath.Join(tempDir, ".github/workflows") @@ -516,3 +553,38 @@ func TestCompileSingleFile(t *testing.T) { } }) } + +func TestCompileModifiedFilesWithDependencies_FormatsWatchMessage(t *testing.T) { + tempDir := testutil.TempDir(t, "test-*") + workflowsDir := filepath.Join(tempDir, ".github/workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755), "Failed to create workflows directory") + + filePath := filepath.Join(workflowsDir, "test.md") + content := "---\non: push\nengine: claude\n---\n# Test\n\nTest workflow content" + require.NoError(t, os.WriteFile(filePath, []byte(content), 0644), "Failed to write workflow file") + + compiler := workflow.NewCompiler() + depGraph := NewDependencyGraph(workflowsDir) + require.NoError(t, depGraph.BuildGraph(compiler), "Failed to build dependency graph") + + oldStderr := os.Stderr + r, w, err := os.Pipe() + require.NoError(t, err, "Failed to create stderr pipe") + os.Stderr = w + t.Cleanup(func() { os.Stderr = oldStderr }) + + compileModifiedFilesWithDependencies(compiler, depGraph, []string{filePath}, false) + + w.Close() + + var buf bytes.Buffer + _, err = io.Copy(&buf, r) + require.NoError(t, err, "Failed to read stderr output") + + assert.Contains( + t, + buf.String(), + console.FormatProgressMessage("Watching for file changes"), + "Expected watch mode to use formatted progress output", + ) +} diff --git a/pkg/cli/compile_file_operations.go b/pkg/cli/compile_file_operations.go index d8eec5f010..b29817f634 100644 --- a/pkg/cli/compile_file_operations.go +++ b/pkg/cli/compile_file_operations.go @@ -69,9 +69,8 @@ func compileSingleFile(compiler *workflow.Compiler, file string, stats *Compilat } if err := CompileWorkflowWithValidation(compiler, file, verbose, false, false, false, false, false); err != nil { - // Always show compilation errors on new line - // Note: Don't wrap in FormatErrorMessage as the error is already formatted by console.FormatError - fmt.Fprintln(os.Stderr, err.Error()) + // Always show compilation errors on a new line using standard CLI error styling. + fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) stats.Errors++ stats.FailedWorkflows = append(stats.FailedWorkflows, filepath.Base(file)) } else { @@ -171,7 +170,7 @@ func compileModifiedFilesWithDependencies(compiler *workflow.Compiler, depGraph } } - fmt.Fprintln(os.Stderr, "Watching for file changes") + fmt.Fprintln(os.Stderr, console.FormatProgressMessage("Watching for file changes")) if verbose { fmt.Fprintln(os.Stderr, console.FormatProgressMessage(fmt.Sprintf("Recompiling %d workflow(s) affected by %d change(s)...", len(workflowsToCompile), len(files)))) }