ls_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. package tools
  2. import (
  3. "context"
  4. "encoding/json"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "testing"
  9. "github.com/stretchr/testify/assert"
  10. "github.com/stretchr/testify/require"
  11. )
  12. func TestLsTool_Info(t *testing.T) {
  13. tool := NewLsTool()
  14. info := tool.Info()
  15. assert.Equal(t, LSToolName, info.Name)
  16. assert.NotEmpty(t, info.Description)
  17. assert.Contains(t, info.Parameters, "path")
  18. assert.Contains(t, info.Parameters, "ignore")
  19. assert.Contains(t, info.Required, "path")
  20. }
  21. func TestLsTool_Run(t *testing.T) {
  22. // Create a temporary directory for testing
  23. tempDir, err := os.MkdirTemp("", "ls_tool_test")
  24. require.NoError(t, err)
  25. defer os.RemoveAll(tempDir)
  26. // Create a test directory structure
  27. testDirs := []string{
  28. "dir1",
  29. "dir2",
  30. "dir2/subdir1",
  31. "dir2/subdir2",
  32. "dir3",
  33. "dir3/.hidden_dir",
  34. "__pycache__",
  35. }
  36. testFiles := []string{
  37. "file1.txt",
  38. "file2.txt",
  39. "dir1/file3.txt",
  40. "dir2/file4.txt",
  41. "dir2/subdir1/file5.txt",
  42. "dir2/subdir2/file6.txt",
  43. "dir3/file7.txt",
  44. "dir3/.hidden_file.txt",
  45. "__pycache__/cache.pyc",
  46. ".hidden_root_file.txt",
  47. }
  48. // Create directories
  49. for _, dir := range testDirs {
  50. dirPath := filepath.Join(tempDir, dir)
  51. err := os.MkdirAll(dirPath, 0755)
  52. require.NoError(t, err)
  53. }
  54. // Create files
  55. for _, file := range testFiles {
  56. filePath := filepath.Join(tempDir, file)
  57. err := os.WriteFile(filePath, []byte("test content"), 0644)
  58. require.NoError(t, err)
  59. }
  60. t.Run("lists directory successfully", func(t *testing.T) {
  61. tool := NewLsTool()
  62. params := LSParams{
  63. Path: tempDir,
  64. }
  65. paramsJSON, err := json.Marshal(params)
  66. require.NoError(t, err)
  67. call := ToolCall{
  68. Name: LSToolName,
  69. Input: string(paramsJSON),
  70. }
  71. response, err := tool.Run(context.Background(), call)
  72. require.NoError(t, err)
  73. // Check that visible directories and files are included
  74. assert.Contains(t, response.Content, "dir1")
  75. assert.Contains(t, response.Content, "dir2")
  76. assert.Contains(t, response.Content, "dir3")
  77. assert.Contains(t, response.Content, "file1.txt")
  78. assert.Contains(t, response.Content, "file2.txt")
  79. // Check that hidden files and directories are not included
  80. assert.NotContains(t, response.Content, ".hidden_dir")
  81. assert.NotContains(t, response.Content, ".hidden_file.txt")
  82. assert.NotContains(t, response.Content, ".hidden_root_file.txt")
  83. // Check that __pycache__ is not included
  84. assert.NotContains(t, response.Content, "__pycache__")
  85. })
  86. t.Run("handles non-existent path", func(t *testing.T) {
  87. tool := NewLsTool()
  88. params := LSParams{
  89. Path: filepath.Join(tempDir, "non_existent_dir"),
  90. }
  91. paramsJSON, err := json.Marshal(params)
  92. require.NoError(t, err)
  93. call := ToolCall{
  94. Name: LSToolName,
  95. Input: string(paramsJSON),
  96. }
  97. response, err := tool.Run(context.Background(), call)
  98. require.NoError(t, err)
  99. assert.Contains(t, response.Content, "path does not exist")
  100. })
  101. t.Run("handles empty path parameter", func(t *testing.T) {
  102. // For this test, we need to mock the config.WorkingDirectory function
  103. // Since we can't easily do that, we'll just check that the response doesn't contain an error message
  104. tool := NewLsTool()
  105. params := LSParams{
  106. Path: "",
  107. }
  108. paramsJSON, err := json.Marshal(params)
  109. require.NoError(t, err)
  110. call := ToolCall{
  111. Name: LSToolName,
  112. Input: string(paramsJSON),
  113. }
  114. response, err := tool.Run(context.Background(), call)
  115. require.NoError(t, err)
  116. // The response should either contain a valid directory listing or an error
  117. // We'll just check that it's not empty
  118. assert.NotEmpty(t, response.Content)
  119. })
  120. t.Run("handles invalid parameters", func(t *testing.T) {
  121. tool := NewLsTool()
  122. call := ToolCall{
  123. Name: LSToolName,
  124. Input: "invalid json",
  125. }
  126. response, err := tool.Run(context.Background(), call)
  127. require.NoError(t, err)
  128. assert.Contains(t, response.Content, "error parsing parameters")
  129. })
  130. t.Run("respects ignore patterns", func(t *testing.T) {
  131. tool := NewLsTool()
  132. params := LSParams{
  133. Path: tempDir,
  134. Ignore: []string{"file1.txt", "dir1"},
  135. }
  136. paramsJSON, err := json.Marshal(params)
  137. require.NoError(t, err)
  138. call := ToolCall{
  139. Name: LSToolName,
  140. Input: string(paramsJSON),
  141. }
  142. response, err := tool.Run(context.Background(), call)
  143. require.NoError(t, err)
  144. // The output format is a tree, so we need to check for specific patterns
  145. // Check that file1.txt is not directly mentioned
  146. assert.NotContains(t, response.Content, "- file1.txt")
  147. // Check that dir1/ is not directly mentioned
  148. assert.NotContains(t, response.Content, "- dir1/")
  149. })
  150. t.Run("handles relative path", func(t *testing.T) {
  151. // Save original working directory
  152. origWd, err := os.Getwd()
  153. require.NoError(t, err)
  154. defer func() {
  155. os.Chdir(origWd)
  156. }()
  157. // Change to a directory above the temp directory
  158. parentDir := filepath.Dir(tempDir)
  159. err = os.Chdir(parentDir)
  160. require.NoError(t, err)
  161. tool := NewLsTool()
  162. params := LSParams{
  163. Path: filepath.Base(tempDir),
  164. }
  165. paramsJSON, err := json.Marshal(params)
  166. require.NoError(t, err)
  167. call := ToolCall{
  168. Name: LSToolName,
  169. Input: string(paramsJSON),
  170. }
  171. response, err := tool.Run(context.Background(), call)
  172. require.NoError(t, err)
  173. // Should list the temp directory contents
  174. assert.Contains(t, response.Content, "dir1")
  175. assert.Contains(t, response.Content, "file1.txt")
  176. })
  177. }
  178. func TestShouldSkip(t *testing.T) {
  179. testCases := []struct {
  180. name string
  181. path string
  182. ignorePatterns []string
  183. expected bool
  184. }{
  185. {
  186. name: "hidden file",
  187. path: "/path/to/.hidden_file",
  188. ignorePatterns: []string{},
  189. expected: true,
  190. },
  191. {
  192. name: "hidden directory",
  193. path: "/path/to/.hidden_dir",
  194. ignorePatterns: []string{},
  195. expected: true,
  196. },
  197. {
  198. name: "pycache directory",
  199. path: "/path/to/__pycache__/file.pyc",
  200. ignorePatterns: []string{},
  201. expected: true,
  202. },
  203. {
  204. name: "node_modules directory",
  205. path: "/path/to/node_modules/package",
  206. ignorePatterns: []string{},
  207. expected: false, // The shouldSkip function doesn't directly check for node_modules in the path
  208. },
  209. {
  210. name: "normal file",
  211. path: "/path/to/normal_file.txt",
  212. ignorePatterns: []string{},
  213. expected: false,
  214. },
  215. {
  216. name: "normal directory",
  217. path: "/path/to/normal_dir",
  218. ignorePatterns: []string{},
  219. expected: false,
  220. },
  221. {
  222. name: "ignored by pattern",
  223. path: "/path/to/ignore_me.txt",
  224. ignorePatterns: []string{"ignore_*.txt"},
  225. expected: true,
  226. },
  227. {
  228. name: "not ignored by pattern",
  229. path: "/path/to/keep_me.txt",
  230. ignorePatterns: []string{"ignore_*.txt"},
  231. expected: false,
  232. },
  233. }
  234. for _, tc := range testCases {
  235. t.Run(tc.name, func(t *testing.T) {
  236. result := shouldSkip(tc.path, tc.ignorePatterns)
  237. assert.Equal(t, tc.expected, result)
  238. })
  239. }
  240. }
  241. func TestCreateFileTree(t *testing.T) {
  242. paths := []string{
  243. "/path/to/file1.txt",
  244. "/path/to/dir1/file2.txt",
  245. "/path/to/dir1/subdir/file3.txt",
  246. "/path/to/dir2/file4.txt",
  247. }
  248. tree := createFileTree(paths)
  249. // Check the structure of the tree
  250. assert.Len(t, tree, 1) // Should have one root node
  251. // Check the root node
  252. rootNode := tree[0]
  253. assert.Equal(t, "path", rootNode.Name)
  254. assert.Equal(t, "directory", rootNode.Type)
  255. assert.Len(t, rootNode.Children, 1)
  256. // Check the "to" node
  257. toNode := rootNode.Children[0]
  258. assert.Equal(t, "to", toNode.Name)
  259. assert.Equal(t, "directory", toNode.Type)
  260. assert.Len(t, toNode.Children, 3) // file1.txt, dir1, dir2
  261. // Find the dir1 node
  262. var dir1Node *TreeNode
  263. for _, child := range toNode.Children {
  264. if child.Name == "dir1" {
  265. dir1Node = child
  266. break
  267. }
  268. }
  269. require.NotNil(t, dir1Node)
  270. assert.Equal(t, "directory", dir1Node.Type)
  271. assert.Len(t, dir1Node.Children, 2) // file2.txt and subdir
  272. }
  273. func TestPrintTree(t *testing.T) {
  274. // Create a simple tree
  275. tree := []*TreeNode{
  276. {
  277. Name: "dir1",
  278. Path: "dir1",
  279. Type: "directory",
  280. Children: []*TreeNode{
  281. {
  282. Name: "file1.txt",
  283. Path: "dir1/file1.txt",
  284. Type: "file",
  285. },
  286. {
  287. Name: "subdir",
  288. Path: "dir1/subdir",
  289. Type: "directory",
  290. Children: []*TreeNode{
  291. {
  292. Name: "file2.txt",
  293. Path: "dir1/subdir/file2.txt",
  294. Type: "file",
  295. },
  296. },
  297. },
  298. },
  299. },
  300. {
  301. Name: "file3.txt",
  302. Path: "file3.txt",
  303. Type: "file",
  304. },
  305. }
  306. result := printTree(tree, "/root")
  307. // Check the output format
  308. assert.Contains(t, result, "- /root/")
  309. assert.Contains(t, result, " - dir1/")
  310. assert.Contains(t, result, " - file1.txt")
  311. assert.Contains(t, result, " - subdir/")
  312. assert.Contains(t, result, " - file2.txt")
  313. assert.Contains(t, result, " - file3.txt")
  314. }
  315. func TestListDirectory(t *testing.T) {
  316. // Create a temporary directory for testing
  317. tempDir, err := os.MkdirTemp("", "list_directory_test")
  318. require.NoError(t, err)
  319. defer os.RemoveAll(tempDir)
  320. // Create a test directory structure
  321. testDirs := []string{
  322. "dir1",
  323. "dir1/subdir1",
  324. ".hidden_dir",
  325. }
  326. testFiles := []string{
  327. "file1.txt",
  328. "file2.txt",
  329. "dir1/file3.txt",
  330. "dir1/subdir1/file4.txt",
  331. ".hidden_file.txt",
  332. }
  333. // Create directories
  334. for _, dir := range testDirs {
  335. dirPath := filepath.Join(tempDir, dir)
  336. err := os.MkdirAll(dirPath, 0755)
  337. require.NoError(t, err)
  338. }
  339. // Create files
  340. for _, file := range testFiles {
  341. filePath := filepath.Join(tempDir, file)
  342. err := os.WriteFile(filePath, []byte("test content"), 0644)
  343. require.NoError(t, err)
  344. }
  345. t.Run("lists files with no limit", func(t *testing.T) {
  346. files, truncated, err := listDirectory(tempDir, []string{}, 1000)
  347. require.NoError(t, err)
  348. assert.False(t, truncated)
  349. // Check that visible files and directories are included
  350. containsPath := func(paths []string, target string) bool {
  351. targetPath := filepath.Join(tempDir, target)
  352. for _, path := range paths {
  353. if strings.HasPrefix(path, targetPath) {
  354. return true
  355. }
  356. }
  357. return false
  358. }
  359. assert.True(t, containsPath(files, "dir1"))
  360. assert.True(t, containsPath(files, "file1.txt"))
  361. assert.True(t, containsPath(files, "file2.txt"))
  362. assert.True(t, containsPath(files, "dir1/file3.txt"))
  363. // Check that hidden files and directories are not included
  364. assert.False(t, containsPath(files, ".hidden_dir"))
  365. assert.False(t, containsPath(files, ".hidden_file.txt"))
  366. })
  367. t.Run("respects limit and returns truncated flag", func(t *testing.T) {
  368. files, truncated, err := listDirectory(tempDir, []string{}, 2)
  369. require.NoError(t, err)
  370. assert.True(t, truncated)
  371. assert.Len(t, files, 2)
  372. })
  373. t.Run("respects ignore patterns", func(t *testing.T) {
  374. files, truncated, err := listDirectory(tempDir, []string{"*.txt"}, 1000)
  375. require.NoError(t, err)
  376. assert.False(t, truncated)
  377. // Check that no .txt files are included
  378. for _, file := range files {
  379. assert.False(t, strings.HasSuffix(file, ".txt"), "Found .txt file: %s", file)
  380. }
  381. // But directories should still be included
  382. containsDir := false
  383. for _, file := range files {
  384. if strings.Contains(file, "dir1") {
  385. containsDir = true
  386. break
  387. }
  388. }
  389. assert.True(t, containsDir)
  390. })
  391. }