| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- package hooks
- import (
- "context"
- "os"
- "path/filepath"
- "testing"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- func TestExecutor(t *testing.T) {
- // Create temp directory for test hooks.
- tempDir := t.TempDir()
- t.Run("executes simple hook with env vars", func(t *testing.T) {
- hookPath := filepath.Join(tempDir, "test-hook.sh")
- hookScript := `#!/bin/bash
- export CRUSH_PERMISSION=approve
- export CRUSH_MESSAGE="test message"
- `
- err := os.WriteFile(hookPath, []byte(hookScript), 0o755)
- require.NoError(t, err)
- executor := NewExecutor(tempDir)
- ctx := context.Background()
- hookCtx := HookContext{
- HookType: HookPreToolUse,
- SessionID: "test-session",
- WorkingDir: tempDir,
- Data: map[string]any{
- "tool_input": map[string]any{
- "command": "ls",
- },
- },
- }
- result, err := executor.Execute(ctx, hookPath, hookCtx)
- require.NoError(t, err)
- assert.True(t, result.Continue)
- assert.Equal(t, "approve", result.Permission)
- assert.Equal(t, "test message", result.Message)
- })
- t.Run("helper functions are available", func(t *testing.T) {
- hookPath := filepath.Join(tempDir, "helper-test.sh")
- hookScript := `#!/bin/bash
- crush_approve "auto approved"
- `
- err := os.WriteFile(hookPath, []byte(hookScript), 0o755)
- require.NoError(t, err)
- executor := NewExecutor(tempDir)
- ctx := context.Background()
- hookCtx := HookContext{
- HookType: HookPreToolUse,
- SessionID: "test-session",
- WorkingDir: tempDir,
- Data: map[string]any{},
- }
- result, err := executor.Execute(ctx, hookPath, hookCtx)
- require.NoError(t, err)
- assert.Equal(t, "approve", result.Permission)
- assert.Equal(t, "auto approved", result.Message)
- })
- t.Run("crush_deny sets continue=false and exits", func(t *testing.T) {
- hookPath := filepath.Join(tempDir, "deny-test.sh")
- hookScript := `#!/bin/bash
- crush_deny "blocked"
- `
- err := os.WriteFile(hookPath, []byte(hookScript), 0o755)
- require.NoError(t, err)
- executor := NewExecutor(tempDir)
- ctx := context.Background()
- hookCtx := HookContext{
- HookType: HookPreToolUse,
- SessionID: "test-session",
- WorkingDir: tempDir,
- Data: map[string]any{},
- }
- result, err := executor.Execute(ctx, hookPath, hookCtx)
- require.NoError(t, err)
- assert.False(t, result.Continue)
- assert.Equal(t, "deny", result.Permission)
- assert.Equal(t, "blocked", result.Message)
- })
- t.Run("reads JSON from stdin", func(t *testing.T) {
- hookPath := filepath.Join(tempDir, "stdin-test.sh")
- hookScript := `#!/bin/bash
- COMMAND=$(crush_get_tool_input command)
- if [ "$COMMAND" = "dangerous" ]; then
- crush_deny "dangerous command"
- fi
- `
- err := os.WriteFile(hookPath, []byte(hookScript), 0o755)
- require.NoError(t, err)
- executor := NewExecutor(tempDir)
- ctx := context.Background()
- hookCtx := HookContext{
- HookType: HookPreToolUse,
- SessionID: "test-session",
- WorkingDir: tempDir,
- Data: map[string]any{
- "tool_input": map[string]any{
- "command": "dangerous",
- },
- },
- }
- result, err := executor.Execute(ctx, hookPath, hookCtx)
- require.NoError(t, err)
- assert.False(t, result.Continue)
- assert.Equal(t, "deny", result.Permission)
- })
- t.Run("env variables are set correctly", func(t *testing.T) {
- hookPath := filepath.Join(tempDir, "env-test.sh")
- hookScript := `#!/bin/bash
- if [ "$CRUSH_HOOK_TYPE" = "pre-tool-use" ] && \
- [ "$CRUSH_SESSION_ID" = "test-123" ] && \
- [ "$CRUSH_TOOL_NAME" = "bash" ]; then
- export CRUSH_MESSAGE="env vars correct"
- fi
- `
- err := os.WriteFile(hookPath, []byte(hookScript), 0o755)
- require.NoError(t, err)
- executor := NewExecutor(tempDir)
- ctx := context.Background()
- hookCtx := HookContext{
- HookType: HookPreToolUse,
- SessionID: "test-123",
- WorkingDir: tempDir,
- ToolName: "bash",
- ToolCallID: "call-123",
- Data: map[string]any{},
- }
- result, err := executor.Execute(ctx, hookPath, hookCtx)
- require.NoError(t, err)
- assert.Equal(t, "env vars correct", result.Message)
- })
- t.Run("supports JSON output for complex mutations", func(t *testing.T) {
- hookPath := filepath.Join(tempDir, "json-test.sh")
- hookScript := `#!/bin/bash
- cat <<EOF
- {
- "permission": "approve",
- "modified_input": {
- "command": "ls -la",
- "safe": true
- }
- }
- EOF
- `
- err := os.WriteFile(hookPath, []byte(hookScript), 0o755)
- require.NoError(t, err)
- executor := NewExecutor(tempDir)
- ctx := context.Background()
- hookCtx := HookContext{
- HookType: HookPreToolUse,
- SessionID: "test-session",
- WorkingDir: tempDir,
- Data: map[string]any{},
- }
- result, err := executor.Execute(ctx, hookPath, hookCtx)
- require.NoError(t, err)
- assert.Equal(t, "approve", result.Permission)
- assert.Equal(t, "ls -la", result.ModifiedInput["command"])
- assert.Equal(t, true, result.ModifiedInput["safe"])
- })
- t.Run("handles exit code 1 as error", func(t *testing.T) {
- hookPath := filepath.Join(tempDir, "error-test.sh")
- hookScript := `#!/bin/bash
- echo "error occurred" >&2
- exit 1
- `
- err := os.WriteFile(hookPath, []byte(hookScript), 0o755)
- require.NoError(t, err)
- executor := NewExecutor(tempDir)
- ctx := context.Background()
- hookCtx := HookContext{
- HookType: HookPreToolUse,
- SessionID: "test-session",
- WorkingDir: tempDir,
- Data: map[string]any{},
- }
- _, err = executor.Execute(ctx, hookPath, hookCtx)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "hook failed with exit code 1")
- })
- t.Run("context files helper", func(t *testing.T) {
- hookPath := filepath.Join(tempDir, "files-test.sh")
- hookScript := `#!/bin/bash
- crush_add_context_file "file1.md"
- crush_add_context_file "file2.txt"
- `
- err := os.WriteFile(hookPath, []byte(hookScript), 0o755)
- require.NoError(t, err)
- executor := NewExecutor(tempDir)
- ctx := context.Background()
- hookCtx := HookContext{
- HookType: HookUserPromptSubmit,
- SessionID: "test-session",
- WorkingDir: tempDir,
- Data: map[string]any{},
- }
- result, err := executor.Execute(ctx, hookPath, hookCtx)
- require.NoError(t, err)
- assert.Equal(t, []string{"file1.md", "file2.txt"}, result.ContextFiles)
- })
- t.Run("context content helper", func(t *testing.T) {
- hookPath := filepath.Join(tempDir, "content-test.sh")
- hookScript := `#!/bin/bash
- crush_add_context "This is additional context"
- `
- err := os.WriteFile(hookPath, []byte(hookScript), 0o755)
- require.NoError(t, err)
- executor := NewExecutor(tempDir)
- ctx := context.Background()
- hookCtx := HookContext{
- HookType: HookUserPromptSubmit,
- SessionID: "test-session",
- WorkingDir: tempDir,
- Data: map[string]any{},
- }
- result, err := executor.Execute(ctx, hookPath, hookCtx)
- require.NoError(t, err)
- assert.Equal(t, "This is additional context", result.ContextContent)
- })
- t.Run("returns error if hook file doesn't exist", func(t *testing.T) {
- executor := NewExecutor(tempDir)
- ctx := context.Background()
- hookCtx := HookContext{
- HookType: HookPreToolUse,
- SessionID: "test-session",
- WorkingDir: tempDir,
- Data: map[string]any{},
- }
- _, err := executor.Execute(ctx, "/nonexistent/hook.sh", hookCtx)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "failed to read hook")
- })
- t.Run("passes custom environment variables", func(t *testing.T) {
- hookPath := filepath.Join(tempDir, "custom-env-test.sh")
- hookScript := `#!/bin/bash
- if [ "$CUSTOM_API_KEY" = "secret123" ] && [ "$CUSTOM_REGION" = "us-west-2" ]; then
- export CRUSH_MESSAGE="custom env vars set correctly"
- fi
- `
- err := os.WriteFile(hookPath, []byte(hookScript), 0o755)
- require.NoError(t, err)
- executor := NewExecutor(tempDir)
- ctx := context.Background()
- hookCtx := HookContext{
- HookType: HookPreToolUse,
- SessionID: "test-session",
- WorkingDir: tempDir,
- Data: map[string]any{},
- Environment: map[string]string{
- "CUSTOM_API_KEY": "secret123",
- "CUSTOM_REGION": "us-west-2",
- },
- }
- result, err := executor.Execute(ctx, hookPath, hookCtx)
- require.NoError(t, err)
- assert.Equal(t, "custom env vars set correctly", result.Message)
- })
- t.Run("modify input helper function", func(t *testing.T) {
- hookPath := filepath.Join(tempDir, "modify-input-test.sh")
- hookScript := `#!/bin/bash
- crush_modify_input "command" "ls -la"
- crush_modify_input "working_dir" "/tmp"
- `
- err := os.WriteFile(hookPath, []byte(hookScript), 0o755)
- require.NoError(t, err)
- executor := NewExecutor(tempDir)
- ctx := context.Background()
- hookCtx := HookContext{
- HookType: HookPreToolUse,
- SessionID: "test-session",
- WorkingDir: tempDir,
- Data: map[string]any{},
- }
- result, err := executor.Execute(ctx, hookPath, hookCtx)
- require.NoError(t, err)
- require.NotNil(t, result.ModifiedInput)
- assert.Equal(t, "ls -la", result.ModifiedInput["command"])
- assert.Equal(t, "/tmp", result.ModifiedInput["working_dir"])
- })
- t.Run("modify output helper function", func(t *testing.T) {
- hookPath := filepath.Join(tempDir, "modify-output-test.sh")
- hookScript := `#!/bin/bash
- crush_modify_output "status" "redacted"
- crush_modify_output "data" "[REDACTED]"
- `
- err := os.WriteFile(hookPath, []byte(hookScript), 0o755)
- require.NoError(t, err)
- executor := NewExecutor(tempDir)
- ctx := context.Background()
- hookCtx := HookContext{
- HookType: HookPostToolUse,
- SessionID: "test-session",
- WorkingDir: tempDir,
- Data: map[string]any{},
- }
- result, err := executor.Execute(ctx, hookPath, hookCtx)
- require.NoError(t, err)
- require.NotNil(t, result.ModifiedOutput)
- assert.Equal(t, "redacted", result.ModifiedOutput["status"])
- assert.Equal(t, "[REDACTED]", result.ModifiedOutput["data"])
- })
- t.Run("modify input with JSON types", func(t *testing.T) {
- hookPath := filepath.Join(tempDir, "modify-input-json-test.sh")
- hookScript := `#!/bin/bash
- crush_modify_input "offset" "100"
- crush_modify_input "limit" "50"
- crush_modify_input "run_in_background" "true"
- crush_modify_input "ignore" '["*.log","*.tmp"]'
- `
- err := os.WriteFile(hookPath, []byte(hookScript), 0o755)
- require.NoError(t, err)
- executor := NewExecutor(tempDir)
- ctx := context.Background()
- hookCtx := HookContext{
- HookType: HookPreToolUse,
- SessionID: "test-session",
- WorkingDir: tempDir,
- Data: map[string]any{},
- }
- result, err := executor.Execute(ctx, hookPath, hookCtx)
- require.NoError(t, err)
- require.NotNil(t, result.ModifiedInput)
- assert.Equal(t, float64(100), result.ModifiedInput["offset"])
- assert.Equal(t, float64(50), result.ModifiedInput["limit"])
- assert.Equal(t, true, result.ModifiedInput["run_in_background"])
- assert.Equal(t, []any{"*.log", "*.tmp"}, result.ModifiedInput["ignore"])
- })
- }
- func TestGetHelpersScript(t *testing.T) {
- script := GetHelpersScript()
- assert.NotEmpty(t, script)
- assert.Contains(t, script, "crush_approve")
- assert.Contains(t, script, "crush_deny")
- assert.Contains(t, script, "crush_add_context")
- }
|