Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1230,3 +1230,69 @@ This rule is from a remote directory.
t.Errorf("task not found in stdout. Output:\n%s", output)
}
}

// TestSingleExpansion verifies that content is expanded only once in the full flow
func TestSingleExpansion(t *testing.T) {
dirs := setupTestDirs(t)

// Create a task that uses a parameter with expansion syntax
taskFile := filepath.Join(dirs.tasksDir, "test-expand.md")
taskContent := `Task with parameter: ${param1}

And a value that looks like expansion syntax but should not be expanded: ${"nested"}`
if err := os.WriteFile(taskFile, []byte(taskContent), 0644); err != nil {
t.Fatalf("failed to create task file: %v", err)
}

// Run with param1 set to a value that contains expansion syntax
output := runTool(t, "-C", dirs.tmpDir, "-p", "param1=!`echo hello`", "test-expand")

// The param1 should be replaced with the literal string "!`echo hello`"
// It should NOT be expanded again (that would execute the command)
if !strings.Contains(output, "!`echo hello`") {
t.Errorf("Expected param1 to be replaced with literal value, got: %s", output)
}

// Verify "hello" is not in output (which would indicate the command was executed)
// Note: there may be other "hello" strings, so check for the specific context
if strings.Contains(output, "Task with parameter: hello") {
t.Errorf("Parameter value was re-expanded (command was executed), got: %s", output)
}
}

// TestCommandExpansionOnce verifies that command files are expanded only once
func TestCommandExpansionOnce(t *testing.T) {
dirs := setupTestDirs(t)
commandsDir := filepath.Join(dirs.tmpDir, ".agents", "commands")
if err := os.MkdirAll(commandsDir, 0o755); err != nil {
t.Fatalf("failed to create commands dir: %v", err)
}

// Create a command file with a parameter
commandFile := filepath.Join(commandsDir, "test-cmd.md")
commandContent := `Command param: ${cmd_param}`
if err := os.WriteFile(commandFile, []byte(commandContent), 0644); err != nil {
t.Fatalf("failed to create command file: %v", err)
}

// Create a task that calls the command with a param containing expansion syntax
taskFile := filepath.Join(dirs.tasksDir, "test-cmd-task.md")
taskContent := `/test-cmd cmd_param="!` + "`echo injected`" + `"`
if err := os.WriteFile(taskFile, []byte(taskContent), 0644); err != nil {
t.Fatalf("failed to create task file: %v", err)
}

// Run the task
output := runTool(t, "-C", dirs.tmpDir, "test-cmd-task")

// The command parameter should be replaced with the literal string "!`echo injected`"
// It should NOT be expanded again (that would execute the command)
if !strings.Contains(output, "!`echo injected`") {
t.Errorf("Expected command param to be replaced with literal value, got: %s", output)
}

// Verify "injected" is not in output (which would indicate the command was executed)
if strings.Contains(output, "Command param: injected") {
t.Errorf("Command parameter value was re-expanded (command was executed), got: %s", output)
}
}
20 changes: 12 additions & 8 deletions pkg/codingcontext/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,21 +193,25 @@ func (cc *Context) findTask(taskName string) error {
cc.agent = agent
}

// Expand parameters only if expand_params is not explicitly set to false
contentToProcess := md.Content
if shouldExpandParams(frontMatter.ExpandParams) {
contentToProcess = cc.expandParams(md.Content, nil)
}

task, err := ParseTask(contentToProcess)
// Parse the task content first to separate text blocks from slash commands
task, err := ParseTask(md.Content)
if err != nil {
return err
}

// Build the final content by processing each block
// Text blocks are expanded if expand_params is not false
// Slash command arguments are NOT expanded here - they are passed as literals
// to command files where they may be substituted via ${param} templates
finalContent := strings.Builder{}
for _, block := range task {
if block.Text != nil {
finalContent.WriteString(block.Text.Content())
textContent := block.Text.Content()
// Expand parameters in text blocks only if expand_params is not explicitly set to false
if shouldExpandParams(frontMatter.ExpandParams) {
textContent = cc.expandParams(textContent, nil)
}
finalContent.WriteString(textContent)
} else if block.SlashCommand != nil {
commandContent, err := cc.findCommand(block.SlashCommand.Name, block.SlashCommand.Params())
if err != nil {
Expand Down