diff --git a/docs/src/content/docs/reference/engines.md b/docs/src/content/docs/reference/engines.md index a0452ec155a..26b2413abc4 100644 --- a/docs/src/content/docs/reference/engines.md +++ b/docs/src/content/docs/reference/engines.md @@ -21,11 +21,13 @@ engine: id: copilot version: latest model: gpt-5 # Optional: uses claude-sonnet-4 by default + args: ["--add-dir", "/workspace"] # Optional: custom CLI arguments ``` **Copilot-specific fields:** - **`model`** (optional): AI model to use (`gpt-5` or defaults to `claude-sonnet-4`) - **`version`** (optional): Version of the GitHub Copilot CLI to install (defaults to `latest`) +- **`args`** (optional): Array of custom command-line arguments to pass to the Copilot CLI (supported by all engines) **Environment Variables:** - **`COPILOT_MODEL`**: Alternative way to set the model (e.g., `gpt-5`) @@ -69,6 +71,7 @@ engine: version: beta model: claude-3-5-sonnet-20241022 max-turns: 5 + args: ["--custom-flag", "value"] # Optional: custom CLI arguments env: AWS_REGION: us-west-2 DEBUG_MODE: "true" @@ -107,6 +110,7 @@ engine: codex engine: id: codex model: gpt-4 + args: ["--custom-flag", "value"] # Optional: custom CLI arguments user-agent: custom-workflow-name env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY_CI }} @@ -123,6 +127,7 @@ engine: **Codex-specific fields:** - **`user-agent`** (optional): Custom user agent string for GitHub MCP server configuration - **`config`** (optional): Additional TOML configuration text appended to generated config.toml +- **`args`** (optional): Array of custom command-line arguments to pass to the Codex CLI (supported by all engines) **Secrets:** @@ -171,6 +176,39 @@ engine: CUSTOM_API_ENDPOINT: https://api.example.com ``` +## Engine Command-Line Arguments + +All engines support custom command-line arguments through the `args` field. These arguments are injected into the AI engine CLI command after all other arguments but before the prompt: + +```yaml +engine: + id: copilot + args: ["--add-dir", "/workspace"] +``` + +**Common use cases:** +- Adding additional directories to the context with `--add-dir` +- Enabling verbose logging with `--verbose` or `--debug` +- Passing engine-specific flags for advanced configuration + +**Example with multiple arguments:** +```yaml +engine: + id: copilot + args: ["--add-dir", "/workspace", "--verbose"] +``` + +This generates the following CLI command structure: +```bash +copilot [default-args] [tool-args] --add-dir /workspace --verbose --prompt "$INSTRUCTION" +``` + +**Important notes:** +- Arguments are added in the order specified in the array +- Arguments are always placed before the `--prompt` flag +- Different engines may support different command-line arguments +- Consult the specific engine's CLI documentation for available flags + ## Engine Error Patterns All engines support custom error pattern recognition for enhanced log validation: diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index b2015f630f3..23a72c5aac3 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -2969,6 +2969,13 @@ "config": { "type": "string", "description": "Additional TOML configuration text that will be appended to the generated config.toml in the action (codex engine only)" + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional array of command-line arguments to pass to the AI engine CLI. These arguments are injected after all other args but before the prompt." } }, "required": [ diff --git a/pkg/workflow/claude_engine.go b/pkg/workflow/claude_engine.go index 134cb13aa83..316342c89c4 100644 --- a/pkg/workflow/claude_engine.go +++ b/pkg/workflow/claude_engine.go @@ -144,6 +144,11 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str claudeArgs = append(claudeArgs, "--settings", "/tmp/gh-aw/.claude/settings.json") } + // Add custom args from engine configuration before the prompt + if workflowData.EngineConfig != nil && len(workflowData.EngineConfig.Args) > 0 { + claudeArgs = append(claudeArgs, workflowData.EngineConfig.Args...) + } + var stepLines []string stepName := "Execute Claude Code CLI" diff --git a/pkg/workflow/codex_engine.go b/pkg/workflow/codex_engine.go index 9480fd01475..19e3ba70178 100644 --- a/pkg/workflow/codex_engine.go +++ b/pkg/workflow/codex_engine.go @@ -124,10 +124,18 @@ func (e *CodexEngine) GetExecutionSteps(workflowData *WorkflowData, logFile stri // See https://github.com/githubnext/gh-aw/issues/892 fullAutoParam := " --full-auto --skip-git-repo-check " //"--dangerously-bypass-approvals-and-sandbox " + // Build custom args parameter if specified in engineConfig + var customArgsParam string + if workflowData.EngineConfig != nil && len(workflowData.EngineConfig.Args) > 0 { + for _, arg := range workflowData.EngineConfig.Args { + customArgsParam += arg + " " + } + } + command := fmt.Sprintf(`set -o pipefail INSTRUCTION=$(cat $GITHUB_AW_PROMPT) mkdir -p $CODEX_HOME/logs -codex %sexec%s%s"$INSTRUCTION" 2>&1 | tee %s`, modelParam, webSearchParam, fullAutoParam, logFile) +codex %sexec%s%s%s"$INSTRUCTION" 2>&1 | tee %s`, modelParam, webSearchParam, fullAutoParam, customArgsParam, logFile) env := map[string]string{ "CODEX_API_KEY": "${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }}", diff --git a/pkg/workflow/copilot_engine.go b/pkg/workflow/copilot_engine.go index e59c3b737bc..8a5ef263b60 100644 --- a/pkg/workflow/copilot_engine.go +++ b/pkg/workflow/copilot_engine.go @@ -96,6 +96,11 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st copilotArgs = append(copilotArgs, "--add-dir", "/tmp/gh-aw/cache-memory/") } + // Add custom args from engine configuration before the prompt + if workflowData.EngineConfig != nil && len(workflowData.EngineConfig.Args) > 0 { + copilotArgs = append(copilotArgs, workflowData.EngineConfig.Args...) + } + copilotArgs = append(copilotArgs, "--prompt", "\"$COPILOT_CLI_INSTRUCTION\"") command := fmt.Sprintf(`set -o pipefail COPILOT_CLI_INSTRUCTION=$(cat /tmp/gh-aw/aw-prompts/prompt.txt) diff --git a/pkg/workflow/custom_engine.go b/pkg/workflow/custom_engine.go index 5dba31739d2..d714065c9fd 100644 --- a/pkg/workflow/custom_engine.go +++ b/pkg/workflow/custom_engine.go @@ -63,6 +63,12 @@ func (e *CustomEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str envVars["GITHUB_AW_MAX_TURNS"] = workflowData.EngineConfig.MaxTurns } + // Add GITHUB_AW_ARGS if args are configured + if workflowData.EngineConfig != nil && len(workflowData.EngineConfig.Args) > 0 { + // Join args with space separator for environment variable + envVars["GITHUB_AW_ARGS"] = strings.Join(workflowData.EngineConfig.Args, " ") + } + // Add custom environment variables from engine config if workflowData.EngineConfig != nil && len(workflowData.EngineConfig.Env) > 0 { for key, value := range workflowData.EngineConfig.Env { diff --git a/pkg/workflow/engine.go b/pkg/workflow/engine.go index b311fa811da..9df36e74ed5 100644 --- a/pkg/workflow/engine.go +++ b/pkg/workflow/engine.go @@ -17,6 +17,7 @@ type EngineConfig struct { Steps []map[string]any ErrorPatterns []ErrorPattern Config string + Args []string } // NetworkPermissions represents network access permissions @@ -183,6 +184,20 @@ func (c *Compiler) ExtractEngineConfig(frontmatter map[string]any) (string, *Eng } } + // Extract optional 'args' field (array of strings) + if args, hasArgs := engineObj["args"]; hasArgs { + if argsArray, ok := args.([]any); ok { + config.Args = make([]string, 0, len(argsArray)) + for _, arg := range argsArray { + if argStr, ok := arg.(string); ok { + config.Args = append(config.Args, argStr) + } + } + } else if argsStrArray, ok := args.([]string); ok { + config.Args = argsStrArray + } + } + // Return the ID as the engineSetting for backwards compatibility return config.ID, config } diff --git a/pkg/workflow/engine_args_integration_test.go b/pkg/workflow/engine_args_integration_test.go new file mode 100644 index 00000000000..8e8b29825c0 --- /dev/null +++ b/pkg/workflow/engine_args_integration_test.go @@ -0,0 +1,273 @@ +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestEngineArgsIntegration(t *testing.T) { + // Create a temporary directory for the test + tmpDir := t.TempDir() + + // Create a test workflow with engine args + workflowContent := `--- +on: workflow_dispatch +engine: + id: copilot + args: ["--add-dir", "/"] +--- + +# Test Workflow + +This is a test workflow to verify engine args injection. +` + + workflowPath := filepath.Join(tmpDir, "test-workflow.md") + err := os.WriteFile(workflowPath, []byte(workflowContent), 0644) + if err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + err = compiler.CompileWorkflow(workflowPath) + if err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := filepath.Join(tmpDir, "test-workflow.lock.yml") + content, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + result := string(content) + + // Check that the compiled YAML contains the custom args + if !strings.Contains(result, "--add-dir /") { + t.Errorf("Expected compiled YAML to contain '--add-dir /', got:\n%s", result) + } + + // Verify args come before --prompt + addDirIdx := strings.Index(result, "--add-dir /") + promptIdx := strings.Index(result, "--prompt") + if addDirIdx == -1 || promptIdx == -1 { + t.Fatal("Could not find both --add-dir and --prompt in compiled YAML") + } + if addDirIdx > promptIdx { + t.Error("Expected --add-dir to come before --prompt in compiled YAML") + } +} + +func TestEngineArgsIntegrationMultipleArgs(t *testing.T) { + // Create a temporary directory for the test + tmpDir := t.TempDir() + + // Create a test workflow with multiple engine args + workflowContent := `--- +on: workflow_dispatch +engine: + id: copilot + args: ["--add-dir", "/workspace", "--verbose"] +--- + +# Test Workflow with Multiple Args + +This is a test workflow to verify multiple engine args injection. +` + + workflowPath := filepath.Join(tmpDir, "test-workflow.md") + err := os.WriteFile(workflowPath, []byte(workflowContent), 0644) + if err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + err = compiler.CompileWorkflow(workflowPath) + if err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := filepath.Join(tmpDir, "test-workflow.lock.yml") + content, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + result := string(content) + + // Check that the compiled YAML contains all custom args + if !strings.Contains(result, "--add-dir /workspace") { + t.Errorf("Expected compiled YAML to contain '--add-dir /workspace'") + } + if !strings.Contains(result, "--verbose") { + t.Errorf("Expected compiled YAML to contain '--verbose'") + } + + // Verify args come before --prompt + verboseIdx := strings.Index(result, "--verbose") + promptIdx := strings.Index(result, "--prompt") + if verboseIdx == -1 || promptIdx == -1 { + t.Fatal("Could not find both --verbose and --prompt in compiled YAML") + } + if verboseIdx > promptIdx { + t.Error("Expected --verbose to come before --prompt in compiled YAML") + } +} + +func TestEngineArgsIntegrationNoArgs(t *testing.T) { + // Create a temporary directory for the test + tmpDir := t.TempDir() + + // Create a test workflow without engine args + workflowContent := `--- +on: workflow_dispatch +engine: + id: copilot +--- + +# Test Workflow without Args + +This is a test workflow without engine args. +` + + workflowPath := filepath.Join(tmpDir, "test-workflow.md") + err := os.WriteFile(workflowPath, []byte(workflowContent), 0644) + if err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + err = compiler.CompileWorkflow(workflowPath) + if err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := filepath.Join(tmpDir, "test-workflow.lock.yml") + content, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + result := string(content) + + // Should still have the --prompt flag + if !strings.Contains(result, "--prompt") { + t.Errorf("Expected compiled YAML to contain '--prompt'") + } + + // Verify the workflow compiles successfully + if result == "" { + t.Error("Expected non-empty compiled YAML") + } +} + +func TestEngineArgsIntegrationClaude(t *testing.T) { + // Create a temporary directory for the test + tmpDir := t.TempDir() + + // Create a test workflow with claude engine args + workflowContent := `--- +on: workflow_dispatch +engine: + id: claude + args: ["--custom-flag", "value"] +--- + +# Test Workflow with Claude Args + +This is a test workflow to verify claude engine args injection. +` + + workflowPath := filepath.Join(tmpDir, "test-workflow.md") + err := os.WriteFile(workflowPath, []byte(workflowContent), 0644) + if err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + err = compiler.CompileWorkflow(workflowPath) + if err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := filepath.Join(tmpDir, "test-workflow.lock.yml") + content, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + result := string(content) + + // Check that the compiled YAML contains the custom args + if !strings.Contains(result, "--custom-flag") { + t.Errorf("Expected compiled YAML to contain '--custom-flag'") + } + if !strings.Contains(result, "value") { + t.Errorf("Expected compiled YAML to contain 'value'") + } +} + +func TestEngineArgsIntegrationCodex(t *testing.T) { + // Create a temporary directory for the test + tmpDir := t.TempDir() + + // Create a test workflow with codex engine args + workflowContent := `--- +on: workflow_dispatch +engine: + id: codex + args: ["--custom-flag", "value"] +--- + +# Test Workflow with Codex Args + +This is a test workflow to verify codex engine args injection. +` + + workflowPath := filepath.Join(tmpDir, "test-workflow.md") + err := os.WriteFile(workflowPath, []byte(workflowContent), 0644) + if err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + err = compiler.CompileWorkflow(workflowPath) + if err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := filepath.Join(tmpDir, "test-workflow.lock.yml") + content, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + result := string(content) + + // Check that the compiled YAML contains the custom args before INSTRUCTION + if !strings.Contains(result, "--custom-flag value") { + t.Errorf("Expected compiled YAML to contain '--custom-flag value'") + } + + // Verify args come before "$INSTRUCTION" + customFlagIdx := strings.Index(result, "--custom-flag value") + instructionIdx := strings.Index(result, "\"$INSTRUCTION\"") + if customFlagIdx == -1 || instructionIdx == -1 { + t.Fatal("Could not find both --custom-flag and $INSTRUCTION in compiled YAML") + } + if customFlagIdx > instructionIdx { + t.Error("Expected --custom-flag to come before $INSTRUCTION in compiled YAML") + } +} diff --git a/pkg/workflow/engine_args_test.go b/pkg/workflow/engine_args_test.go new file mode 100644 index 00000000000..13e44b61439 --- /dev/null +++ b/pkg/workflow/engine_args_test.go @@ -0,0 +1,401 @@ +package workflow + +import ( + "strings" + "testing" +) + +func TestEngineArgsFieldExtraction(t *testing.T) { + compiler := NewCompiler(false, "", "test") + + t.Run("Engine args field extraction with []any", func(t *testing.T) { + frontmatter := map[string]any{ + "engine": map[string]any{ + "id": "copilot", + "args": []any{"--add-dir", "/"}, + }, + } + + _, config := compiler.ExtractEngineConfig(frontmatter) + if config == nil { + t.Fatal("Expected config to be non-nil") + } + if config.ID != "copilot" { + t.Errorf("Expected ID 'copilot', got %s", config.ID) + } + if len(config.Args) != 2 { + t.Errorf("Expected 2 args, got %d", len(config.Args)) + } + if config.Args[0] != "--add-dir" || config.Args[1] != "/" { + t.Errorf("Expected [--add-dir /], got %v", config.Args) + } + }) + + t.Run("Engine args field extraction with []string", func(t *testing.T) { + frontmatter := map[string]any{ + "engine": map[string]any{ + "id": "copilot", + "args": []string{"--verbose", "--debug"}, + }, + } + + _, config := compiler.ExtractEngineConfig(frontmatter) + if config == nil { + t.Fatal("Expected config to be non-nil") + } + if len(config.Args) != 2 { + t.Errorf("Expected 2 args, got %d", len(config.Args)) + } + if config.Args[0] != "--verbose" || config.Args[1] != "--debug" { + t.Errorf("Expected [--verbose --debug], got %v", config.Args) + } + }) + + t.Run("Engine without args field", func(t *testing.T) { + frontmatter := map[string]any{ + "engine": map[string]any{ + "id": "copilot", + }, + } + + _, config := compiler.ExtractEngineConfig(frontmatter) + if config == nil { + t.Fatal("Expected config to be non-nil") + } + if config.Args != nil { + t.Errorf("Expected Args to be nil, got %v", config.Args) + } + }) + + t.Run("Engine args field with single argument", func(t *testing.T) { + frontmatter := map[string]any{ + "engine": map[string]any{ + "id": "copilot", + "args": []any{"--custom-flag"}, + }, + } + + _, config := compiler.ExtractEngineConfig(frontmatter) + if config == nil { + t.Fatal("Expected config to be non-nil") + } + if len(config.Args) != 1 { + t.Errorf("Expected 1 arg, got %d", len(config.Args)) + } + if config.Args[0] != "--custom-flag" { + t.Errorf("Expected [--custom-flag], got %v", config.Args) + } + }) + + t.Run("Engine args with complex arguments", func(t *testing.T) { + frontmatter := map[string]any{ + "engine": map[string]any{ + "id": "copilot", + "args": []any{"--add-dir", "/workspace", "--log-level", "debug"}, + }, + } + + _, config := compiler.ExtractEngineConfig(frontmatter) + if config == nil { + t.Fatal("Expected config to be non-nil") + } + if len(config.Args) != 4 { + t.Errorf("Expected 4 args, got %d", len(config.Args)) + } + expected := []string{"--add-dir", "/workspace", "--log-level", "debug"} + for i, arg := range expected { + if config.Args[i] != arg { + t.Errorf("Expected Args[%d] to be %s, got %s", i, arg, config.Args[i]) + } + } + }) +} + +func TestCopilotEngineArgsInjection(t *testing.T) { + engine := NewCopilotEngine() + + t.Run("Copilot engine injects args before prompt", func(t *testing.T) { + workflowData := &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + Args: []string{"--add-dir", "/"}, + }, + Tools: make(map[string]any), + SafeOutputs: nil, + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + if len(steps) == 0 { + t.Fatal("Expected at least one step") + } + + // Find the execution step + var executionStep GitHubActionStep + for _, step := range steps { + stepStr := strings.Join(step, "\n") + if strings.Contains(stepStr, "Execute GitHub Copilot CLI") { + executionStep = step + break + } + } + + if executionStep == nil { + t.Fatal("Expected to find execution step") + } + + // Convert step to string for easier inspection + stepStr := strings.Join(executionStep, "\n") + + // Check that args appear in the command + if !strings.Contains(stepStr, "--add-dir /") { + t.Errorf("Expected to find '--add-dir /' in step, got:\n%s", stepStr) + } + + // Check that args come before --prompt + addDirIdx := strings.Index(stepStr, "--add-dir /") + promptIdx := strings.Index(stepStr, "--prompt") + if addDirIdx == -1 || promptIdx == -1 { + t.Fatal("Could not find both --add-dir and --prompt in step") + } + if addDirIdx > promptIdx { + t.Error("Expected --add-dir to come before --prompt") + } + }) + + t.Run("Copilot engine without args", func(t *testing.T) { + workflowData := &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + }, + Tools: make(map[string]any), + SafeOutputs: nil, + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + if len(steps) == 0 { + t.Fatal("Expected at least one step") + } + + // Find the execution step + var executionStep GitHubActionStep + for _, step := range steps { + stepStr := strings.Join(step, "\n") + if strings.Contains(stepStr, "Execute GitHub Copilot CLI") { + executionStep = step + break + } + } + + if executionStep == nil { + t.Fatal("Expected to find execution step") + } + + // Should still have the --prompt flag + stepStr := strings.Join(executionStep, "\n") + if !strings.Contains(stepStr, "--prompt") { + t.Errorf("Expected to find '--prompt' in step") + } + }) + + t.Run("Copilot engine with multiple args", func(t *testing.T) { + workflowData := &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + Args: []string{"--add-dir", "/workspace", "--verbose"}, + }, + Tools: make(map[string]any), + SafeOutputs: nil, + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + if len(steps) == 0 { + t.Fatal("Expected at least one step") + } + + // Find the execution step + var executionStep GitHubActionStep + for _, step := range steps { + stepStr := strings.Join(step, "\n") + if strings.Contains(stepStr, "Execute GitHub Copilot CLI") { + executionStep = step + break + } + } + + if executionStep == nil { + t.Fatal("Expected to find execution step") + } + + stepStr := strings.Join(executionStep, "\n") + + // Check that all args appear in the command + if !strings.Contains(stepStr, "--add-dir /workspace") { + t.Errorf("Expected to find '--add-dir /workspace' in step") + } + if !strings.Contains(stepStr, "--verbose") { + t.Errorf("Expected to find '--verbose' in step") + } + }) +} + +func TestClaudeEngineArgsInjection(t *testing.T) { + engine := NewClaudeEngine() + + t.Run("Claude engine injects args before prompt", func(t *testing.T) { + workflowData := &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "claude", + Args: []string{"--custom-flag", "value"}, + }, + Tools: make(map[string]any), + SafeOutputs: nil, + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + if len(steps) == 0 { + t.Fatal("Expected at least one step") + } + + // Find the execution step + var executionStep GitHubActionStep + for _, step := range steps { + stepStr := strings.Join(step, "\n") + if strings.Contains(stepStr, "Execute Claude Code CLI") { + executionStep = step + break + } + } + + if executionStep == nil { + t.Fatal("Expected to find execution step") + } + + stepStr := strings.Join(executionStep, "\n") + + // Check that args appear in the command + if !strings.Contains(stepStr, "--custom-flag value") { + t.Errorf("Expected to find '--custom-flag value' in step, got:\n%s", stepStr) + } + }) + + t.Run("Claude engine without args", func(t *testing.T) { + workflowData := &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "claude", + }, + Tools: make(map[string]any), + SafeOutputs: nil, + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + if len(steps) == 0 { + t.Fatal("Expected at least one step") + } + + // Find the execution step + var executionStep GitHubActionStep + for _, step := range steps { + stepStr := strings.Join(step, "\n") + if strings.Contains(stepStr, "Execute Claude Code CLI") { + executionStep = step + break + } + } + + if executionStep == nil { + t.Fatal("Expected to find execution step") + } + + // Verify the workflow compiles successfully + stepStr := strings.Join(executionStep, "\n") + if stepStr == "" { + t.Error("Expected non-empty step") + } + }) +} + +func TestCodexEngineArgsInjection(t *testing.T) { + engine := NewCodexEngine() + + t.Run("Codex engine injects args before instruction", func(t *testing.T) { + workflowData := &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "codex", + Args: []string{"--custom-flag", "value"}, + }, + Tools: make(map[string]any), + SafeOutputs: nil, + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + if len(steps) == 0 { + t.Fatal("Expected at least one step") + } + + // Find the execution step + var executionStep GitHubActionStep + for _, step := range steps { + stepStr := strings.Join(step, "\n") + if strings.Contains(stepStr, "Run Codex") { + executionStep = step + break + } + } + + if executionStep == nil { + t.Fatal("Expected to find execution step") + } + + stepStr := strings.Join(executionStep, "\n") + + // Check that args appear in the command before INSTRUCTION + if !strings.Contains(stepStr, "--custom-flag value") { + t.Errorf("Expected to find '--custom-flag value' in step, got:\n%s", stepStr) + } + + // Check that args come before "$INSTRUCTION" + customFlagIdx := strings.Index(stepStr, "--custom-flag value") + instructionIdx := strings.Index(stepStr, "\"$INSTRUCTION\"") + if customFlagIdx == -1 || instructionIdx == -1 { + t.Fatal("Could not find both --custom-flag and $INSTRUCTION in step") + } + if customFlagIdx > instructionIdx { + t.Error("Expected --custom-flag to come before $INSTRUCTION") + } + }) + + t.Run("Codex engine without args", func(t *testing.T) { + workflowData := &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "codex", + }, + Tools: make(map[string]any), + SafeOutputs: nil, + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + if len(steps) == 0 { + t.Fatal("Expected at least one step") + } + + // Find the execution step + var executionStep GitHubActionStep + for _, step := range steps { + stepStr := strings.Join(step, "\n") + if strings.Contains(stepStr, "Run Codex") { + executionStep = step + break + } + } + + if executionStep == nil { + t.Fatal("Expected to find execution step") + } + + // Verify the workflow compiles successfully + stepStr := strings.Join(executionStep, "\n") + if stepStr == "" { + t.Error("Expected non-empty step") + } + }) +}