package handlers_test import ( "context" "encoding/json" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/yan1h/agent-coding-workflow/internal/acp/handlers" ) func TestGrep_HappyPath(t *testing.T) { t.Parallel() tmp := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(tmp, "a.go"), []byte("package a\n\nfunc Foo() {}\n"), 0o644)) require.NoError(t, os.WriteFile(filepath.Join(tmp, "b.go"), []byte("package b\n\nfunc Bar() {}\n"), 0o644)) h := handlers.NewGrepHandler() sess := handlers.SessionContext{CwdPath: tmp} res := h.Handle(context.Background(), sess, mustJSON(t, map[string]any{"sessionId": "x", "pattern": "func Foo"})) require.Nil(t, res.Err) var out struct { Matches []struct { File string `json:"file"` Line int `json:"line"` Text string `json:"text"` } `json:"matches"` Truncated bool `json:"truncated"` } require.NoError(t, json.Unmarshal(res.OK, &out)) require.Len(t, out.Matches, 1) assert.Equal(t, "a.go", out.Matches[0].File) assert.Equal(t, 3, out.Matches[0].Line) assert.Contains(t, out.Matches[0].Text, "func Foo") } func TestGrep_CaseInsensitiveByDefault(t *testing.T) { t.Parallel() tmp := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(tmp, "a.go"), []byte("HELLO world\n"), 0o644)) h := handlers.NewGrepHandler() sess := handlers.SessionContext{CwdPath: tmp} res := h.Handle(context.Background(), sess, mustJSON(t, map[string]any{"sessionId": "x", "pattern": "hello"})) require.Nil(t, res.Err) var out struct { Matches []any `json:"matches"` } require.NoError(t, json.Unmarshal(res.OK, &out)) require.Len(t, out.Matches, 1) } func TestGrep_PathOutsideWorktree(t *testing.T) { t.Parallel() tmp := t.TempDir() h := handlers.NewGrepHandler() sess := handlers.SessionContext{CwdPath: tmp} outside := filepath.Join(filepath.VolumeName(tmp)+string(filepath.Separator), "etc") res := h.Handle(context.Background(), sess, mustJSON(t, map[string]any{"sessionId": "x", "pattern": "x", "path": outside})) require.NotNil(t, res.Err) assert.Equal(t, -32001, res.Err.Code) } func TestGrep_InvalidPattern(t *testing.T) { t.Parallel() tmp := t.TempDir() h := handlers.NewGrepHandler() sess := handlers.SessionContext{CwdPath: tmp} res := h.Handle(context.Background(), sess, mustJSON(t, map[string]any{"sessionId": "x", "pattern": "[invalid("})) require.NotNil(t, res.Err) assert.Equal(t, -32602, res.Err.Code) } func TestGrep_EmptyPattern(t *testing.T) { t.Parallel() tmp := t.TempDir() h := handlers.NewGrepHandler() sess := handlers.SessionContext{CwdPath: tmp} res := h.Handle(context.Background(), sess, mustJSON(t, map[string]any{"sessionId": "x", "pattern": ""})) require.NotNil(t, res.Err) assert.Equal(t, -32602, res.Err.Code) } func TestGrep_MaxMatchesCap(t *testing.T) { t.Parallel() tmp := t.TempDir() // 10 matching lines; cap at 3. content := "m\nm\nm\nm\nm\nm\nm\nm\nm\nm\n" require.NoError(t, os.WriteFile(filepath.Join(tmp, "a.txt"), []byte(content), 0o644)) h := &handlers.GrepHandler{MaxMatches: 3, MaxFiles: 100, MaxFileSize: 1 << 20} sess := handlers.SessionContext{CwdPath: tmp} res := h.Handle(context.Background(), sess, mustJSON(t, map[string]any{"sessionId": "x", "pattern": "m"})) require.Nil(t, res.Err) var out struct { Matches []any `json:"matches"` Truncated bool `json:"truncated"` } require.NoError(t, json.Unmarshal(res.OK, &out)) require.Len(t, out.Matches, 3) require.True(t, out.Truncated) } func TestGrep_SkipsGitDir(t *testing.T) { t.Parallel() tmp := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(tmp, ".git"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(tmp, ".git", "config"), []byte("secret pattern\n"), 0o644)) require.NoError(t, os.WriteFile(filepath.Join(tmp, "a.txt"), []byte("normal pattern\n"), 0o644)) h := handlers.NewGrepHandler() sess := handlers.SessionContext{CwdPath: tmp} res := h.Handle(context.Background(), sess, mustJSON(t, map[string]any{"sessionId": "x", "pattern": "pattern"})) require.Nil(t, res.Err) var out struct { Matches []struct { File string `json:"file"` } `json:"matches"` } require.NoError(t, json.Unmarshal(res.OK, &out)) require.Len(t, out.Matches, 1) assert.Equal(t, "a.txt", out.Matches[0].File) }