|
|
@@ -3,6 +3,18 @@ import * as path from "path"
|
|
|
import { listFiles } from "../list-files"
|
|
|
import * as childProcess from "child_process"
|
|
|
|
|
|
+vi.mock("child_process")
|
|
|
+vi.mock("fs")
|
|
|
+vi.mock("vscode", () => ({
|
|
|
+ env: {
|
|
|
+ appRoot: "/mock/vscode/app/root",
|
|
|
+ },
|
|
|
+}))
|
|
|
+
|
|
|
+vi.mock("../../ripgrep", () => ({
|
|
|
+ getBinPath: vi.fn().mockResolvedValue("/mock/path/to/rg"),
|
|
|
+}))
|
|
|
+
|
|
|
vi.mock("../list-files", async () => {
|
|
|
const actual = await vi.importActual("../list-files")
|
|
|
return {
|
|
|
@@ -83,8 +95,11 @@ describe("list-files symlink support", () => {
|
|
|
|
|
|
mockSpawn.mockReturnValue(mockProcess as any)
|
|
|
|
|
|
+ // Use a test directory path
|
|
|
+ const testDir = "/test/dir"
|
|
|
+
|
|
|
// Call listFiles to trigger ripgrep execution
|
|
|
- await listFiles("/test/dir", false, 100)
|
|
|
+ await listFiles(testDir, false, 100)
|
|
|
|
|
|
// Verify that spawn was called with --follow flag (the critical fix)
|
|
|
const [rgPath, args] = mockSpawn.mock.calls[0]
|
|
|
@@ -93,9 +108,12 @@ describe("list-files symlink support", () => {
|
|
|
expect(args).toContain("--hidden")
|
|
|
expect(args).toContain("--follow") // This is the critical assertion - the fix should add this flag
|
|
|
|
|
|
- // Platform-agnostic path check - verify the last argument is the resolved path
|
|
|
- const expectedPath = path.resolve("/test/dir")
|
|
|
- expect(args[args.length - 1]).toBe(expectedPath)
|
|
|
+ // Platform-agnostic path check - verify the last argument ends with the expected path
|
|
|
+ const lastArg = args[args.length - 1]
|
|
|
+ // On Windows, the path might be resolved to something like D:\test\dir
|
|
|
+ // On Unix, it would be /test/dir
|
|
|
+ // So we just check that it ends with the expected segments
|
|
|
+ expect(lastArg).toMatch(/[/\\]test[/\\]dir$/)
|
|
|
})
|
|
|
|
|
|
it("should include --follow flag for recursive listings too", async () => {
|
|
|
@@ -124,8 +142,11 @@ describe("list-files symlink support", () => {
|
|
|
|
|
|
mockSpawn.mockReturnValue(mockProcess as any)
|
|
|
|
|
|
+ // Use a test directory path
|
|
|
+ const testDir = "/test/dir"
|
|
|
+
|
|
|
// Call listFiles with recursive=true
|
|
|
- await listFiles("/test/dir", true, 100)
|
|
|
+ await listFiles(testDir, true, 100)
|
|
|
|
|
|
// Verify that spawn was called with --follow flag (the critical fix)
|
|
|
const [rgPath, args] = mockSpawn.mock.calls[0]
|
|
|
@@ -134,9 +155,12 @@ describe("list-files symlink support", () => {
|
|
|
expect(args).toContain("--hidden")
|
|
|
expect(args).toContain("--follow") // This should be present in recursive mode too
|
|
|
|
|
|
- // Platform-agnostic path check - verify the last argument is the resolved path
|
|
|
- const expectedPath = path.resolve("/test/dir")
|
|
|
- expect(args[args.length - 1]).toBe(expectedPath)
|
|
|
+ // Platform-agnostic path check - verify the last argument ends with the expected path
|
|
|
+ const lastArg = args[args.length - 1]
|
|
|
+ // On Windows, the path might be resolved to something like D:\test\dir
|
|
|
+ // On Unix, it would be /test/dir
|
|
|
+ // So we just check that it ends with the expected segments
|
|
|
+ expect(lastArg).toMatch(/[/\\]test[/\\]dir$/)
|
|
|
})
|
|
|
|
|
|
it("should ensure first-level directories are included when limit is reached", async () => {
|
|
|
@@ -159,18 +183,19 @@ describe("list-files symlink support", () => {
|
|
|
on: vi.fn((event, callback) => {
|
|
|
if (event === "data") {
|
|
|
// Return many file paths to trigger the limit
|
|
|
+ // Note: ripgrep returns relative paths
|
|
|
const paths =
|
|
|
[
|
|
|
- "/test/dir/a_dir/",
|
|
|
- "/test/dir/a_dir/subdir1/",
|
|
|
- "/test/dir/a_dir/subdir1/file1.txt",
|
|
|
- "/test/dir/a_dir/subdir1/file2.txt",
|
|
|
- "/test/dir/a_dir/subdir2/",
|
|
|
- "/test/dir/a_dir/subdir2/file3.txt",
|
|
|
- "/test/dir/a_dir/file4.txt",
|
|
|
- "/test/dir/a_dir/file5.txt",
|
|
|
- "/test/dir/file1.txt",
|
|
|
- "/test/dir/file2.txt",
|
|
|
+ "a_dir/",
|
|
|
+ "a_dir/subdir1/",
|
|
|
+ "a_dir/subdir1/file1.txt",
|
|
|
+ "a_dir/subdir1/file2.txt",
|
|
|
+ "a_dir/subdir2/",
|
|
|
+ "a_dir/subdir2/file3.txt",
|
|
|
+ "a_dir/file4.txt",
|
|
|
+ "a_dir/file5.txt",
|
|
|
+ "file1.txt",
|
|
|
+ "file2.txt",
|
|
|
// Note: b_dir and c_dir are missing from ripgrep output
|
|
|
].join("\n") + "\n"
|
|
|
setTimeout(() => callback(paths), 10)
|
|
|
@@ -216,3 +241,321 @@ describe("list-files symlink support", () => {
|
|
|
expect(hasCDir).toBe(true)
|
|
|
})
|
|
|
})
|
|
|
+
|
|
|
+describe("hidden directory exclusion", () => {
|
|
|
+ beforeEach(() => {
|
|
|
+ vi.clearAllMocks()
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should exclude .git subdirectories from recursive directory listing", async () => {
|
|
|
+ // Mock filesystem structure with .git subdirectories
|
|
|
+ const mockReaddir = vi.fn()
|
|
|
+ vi.mocked(fs.promises).readdir = mockReaddir
|
|
|
+
|
|
|
+ // Mock the directory structure:
|
|
|
+ // /test/
|
|
|
+ // .git/
|
|
|
+ // hooks/
|
|
|
+ // objects/
|
|
|
+ // src/
|
|
|
+ // components/
|
|
|
+ mockReaddir
|
|
|
+ .mockResolvedValueOnce([
|
|
|
+ { name: ".git", isDirectory: () => true, isSymbolicLink: () => false },
|
|
|
+ { name: "src", isDirectory: () => true, isSymbolicLink: () => false },
|
|
|
+ ])
|
|
|
+ .mockResolvedValueOnce([
|
|
|
+ // src subdirectories (should be included)
|
|
|
+ { name: "components", isDirectory: () => true, isSymbolicLink: () => false },
|
|
|
+ ])
|
|
|
+ .mockResolvedValueOnce([]) // components/ is empty
|
|
|
+
|
|
|
+ // Mock ripgrep to return no files
|
|
|
+ const mockSpawn = vi.mocked(childProcess.spawn)
|
|
|
+ const mockProcess = {
|
|
|
+ stdout: {
|
|
|
+ on: vi.fn((event, callback) => {
|
|
|
+ if (event === "data") {
|
|
|
+ // No files returned
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ stderr: {
|
|
|
+ on: vi.fn(),
|
|
|
+ },
|
|
|
+ on: vi.fn((event, callback) => {
|
|
|
+ if (event === "close") {
|
|
|
+ setTimeout(() => callback(0), 10)
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ kill: vi.fn(),
|
|
|
+ }
|
|
|
+ mockSpawn.mockReturnValue(mockProcess as any)
|
|
|
+
|
|
|
+ // Call listFiles with recursive=true
|
|
|
+ const [result] = await listFiles("/test", true, 100)
|
|
|
+
|
|
|
+ // Verify that .git subdirectories are NOT included
|
|
|
+ const directories = result.filter((item) => item.endsWith("/"))
|
|
|
+
|
|
|
+ // More specific checks - look for exact paths
|
|
|
+ const hasSrcDir = directories.some((dir) => dir.endsWith("/test/src/") || dir.endsWith("src/"))
|
|
|
+ const hasComponentsDir = directories.some(
|
|
|
+ (dir) =>
|
|
|
+ dir.endsWith("/test/src/components/") || dir.endsWith("src/components/") || dir.includes("components/"),
|
|
|
+ )
|
|
|
+ const hasGitDir = directories.some((dir) => dir.includes(".git/"))
|
|
|
+
|
|
|
+ // Should include src/ and src/components/ but NOT .git/ or its subdirectories
|
|
|
+ expect(hasSrcDir).toBe(true)
|
|
|
+ expect(hasComponentsDir).toBe(true)
|
|
|
+
|
|
|
+ // Should NOT include .git (hidden directories are excluded)
|
|
|
+ expect(hasGitDir).toBe(false)
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should allow explicit targeting of hidden directories", async () => {
|
|
|
+ // Mock filesystem structure for explicit .roo-memory targeting
|
|
|
+ const mockReaddir = vi.fn()
|
|
|
+ vi.mocked(fs.promises).readdir = mockReaddir
|
|
|
+
|
|
|
+ // Mock .roo-memory directory contents
|
|
|
+ mockReaddir.mockResolvedValueOnce([
|
|
|
+ { name: "tasks", isDirectory: () => true, isSymbolicLink: () => false },
|
|
|
+ { name: "context", isDirectory: () => true, isSymbolicLink: () => false },
|
|
|
+ ])
|
|
|
+
|
|
|
+ // Mock ripgrep to return no files
|
|
|
+ const mockSpawn = vi.mocked(childProcess.spawn)
|
|
|
+ const mockProcess = {
|
|
|
+ stdout: {
|
|
|
+ on: vi.fn((event, callback) => {
|
|
|
+ if (event === "data") {
|
|
|
+ // No files returned
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ stderr: {
|
|
|
+ on: vi.fn(),
|
|
|
+ },
|
|
|
+ on: vi.fn((event, callback) => {
|
|
|
+ if (event === "close") {
|
|
|
+ setTimeout(() => callback(0), 10)
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ kill: vi.fn(),
|
|
|
+ }
|
|
|
+ mockSpawn.mockReturnValue(mockProcess as any)
|
|
|
+
|
|
|
+ // Call listFiles explicitly targeting .roo-memory directory
|
|
|
+ const [result] = await listFiles("/test/.roo-memory", true, 100)
|
|
|
+
|
|
|
+ // When explicitly targeting a hidden directory, its subdirectories should be included
|
|
|
+ const directories = result.filter((item) => item.endsWith("/"))
|
|
|
+
|
|
|
+ const hasTasksDir = directories.some((dir) => dir.includes(".roo-memory/tasks/") || dir.includes("tasks/"))
|
|
|
+ const hasContextDir = directories.some(
|
|
|
+ (dir) => dir.includes(".roo-memory/context/") || dir.includes("context/"),
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(hasTasksDir).toBe(true)
|
|
|
+ expect(hasContextDir).toBe(true)
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should include top-level files when recursively listing a hidden directory that's also in DIRS_TO_IGNORE", async () => {
|
|
|
+ // This test specifically addresses the bug where files at the root level of .roo/temp
|
|
|
+ // were being excluded when using recursive listing
|
|
|
+ const mockSpawn = vi.mocked(childProcess.spawn)
|
|
|
+ const mockProcess = {
|
|
|
+ stdout: {
|
|
|
+ on: vi.fn((event, callback) => {
|
|
|
+ if (event === "data") {
|
|
|
+ // Simulate files that should be found in .roo/temp
|
|
|
+ // Note: ripgrep returns relative paths
|
|
|
+ setTimeout(() => {
|
|
|
+ callback("teste1.md\n")
|
|
|
+ callback("22/test2.md\n")
|
|
|
+ }, 10)
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ stderr: {
|
|
|
+ on: vi.fn(),
|
|
|
+ },
|
|
|
+ on: vi.fn((event, callback) => {
|
|
|
+ if (event === "close") {
|
|
|
+ setTimeout(() => callback(0), 20)
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ kill: vi.fn(),
|
|
|
+ }
|
|
|
+
|
|
|
+ mockSpawn.mockReturnValue(mockProcess as any)
|
|
|
+
|
|
|
+ // Mock directory listing for .roo/temp
|
|
|
+ const mockReaddir = vi.fn()
|
|
|
+ vi.mocked(fs.promises).readdir = mockReaddir
|
|
|
+ mockReaddir.mockResolvedValueOnce([{ name: "22", isDirectory: () => true, isSymbolicLink: () => false }])
|
|
|
+
|
|
|
+ // Call listFiles targeting .roo/temp (which is both hidden and in DIRS_TO_IGNORE)
|
|
|
+ const [files] = await listFiles("/test/.roo/temp", true, 100)
|
|
|
+
|
|
|
+ // Verify ripgrep was called with correct arguments
|
|
|
+ const [rgPath, args] = mockSpawn.mock.calls[0]
|
|
|
+ expect(args).toContain("--no-ignore-vcs")
|
|
|
+ expect(args).toContain("--no-ignore")
|
|
|
+
|
|
|
+ // Check for the inclusion patterns that should be added
|
|
|
+ expect(args).toContain("-g")
|
|
|
+ const gIndex = args.indexOf("-g")
|
|
|
+ expect(args[gIndex + 1]).toBe("*")
|
|
|
+
|
|
|
+ // Verify that both top-level and nested files are included
|
|
|
+ const fileNames = files.map((f) => path.basename(f))
|
|
|
+ expect(fileNames).toContain("teste1.md")
|
|
|
+ expect(fileNames).toContain("test2.md")
|
|
|
+
|
|
|
+ // Ensure the top-level file is actually included
|
|
|
+ const topLevelFile = files.find((f) => f.endsWith("teste1.md"))
|
|
|
+ expect(topLevelFile).toBeTruthy()
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+describe("buildRecursiveArgs edge cases", () => {
|
|
|
+ beforeEach(() => {
|
|
|
+ vi.clearAllMocks()
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should correctly detect hidden directories with trailing slashes", async () => {
|
|
|
+ const mockSpawn = vi.mocked(childProcess.spawn)
|
|
|
+ const mockProcess = {
|
|
|
+ stdout: {
|
|
|
+ on: vi.fn((event, callback) => {
|
|
|
+ if (event === "data") {
|
|
|
+ setTimeout(() => callback("file.txt\n"), 10)
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ stderr: {
|
|
|
+ on: vi.fn(),
|
|
|
+ },
|
|
|
+ on: vi.fn((event, callback) => {
|
|
|
+ if (event === "close") {
|
|
|
+ setTimeout(() => callback(0), 20)
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ kill: vi.fn(),
|
|
|
+ }
|
|
|
+ mockSpawn.mockReturnValue(mockProcess as any)
|
|
|
+
|
|
|
+ // Test with trailing slash on hidden directory
|
|
|
+ await listFiles("/test/.hidden/", true, 100)
|
|
|
+
|
|
|
+ const [rgPath, args] = mockSpawn.mock.calls[0]
|
|
|
+ // When targeting a hidden directory, these flags should be present
|
|
|
+ expect(args).toContain("--no-ignore-vcs")
|
|
|
+ expect(args).toContain("--no-ignore")
|
|
|
+ expect(args).toContain("-g")
|
|
|
+ const gIndex = args.indexOf("-g")
|
|
|
+ expect(args[gIndex + 1]).toBe("*")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should correctly detect hidden directories with redundant separators", async () => {
|
|
|
+ const mockSpawn = vi.mocked(childProcess.spawn)
|
|
|
+ const mockProcess = {
|
|
|
+ stdout: {
|
|
|
+ on: vi.fn((event, callback) => {
|
|
|
+ if (event === "data") {
|
|
|
+ setTimeout(() => callback("file.txt\n"), 10)
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ stderr: {
|
|
|
+ on: vi.fn(),
|
|
|
+ },
|
|
|
+ on: vi.fn((event, callback) => {
|
|
|
+ if (event === "close") {
|
|
|
+ setTimeout(() => callback(0), 20)
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ kill: vi.fn(),
|
|
|
+ }
|
|
|
+ mockSpawn.mockReturnValue(mockProcess as any)
|
|
|
+
|
|
|
+ // Test with redundant separators before hidden directory
|
|
|
+ await listFiles("/test//.hidden", true, 100)
|
|
|
+
|
|
|
+ const [rgPath, args] = mockSpawn.mock.calls[0]
|
|
|
+ // When targeting a hidden directory, these flags should be present
|
|
|
+ expect(args).toContain("--no-ignore-vcs")
|
|
|
+ expect(args).toContain("--no-ignore")
|
|
|
+ expect(args).toContain("-g")
|
|
|
+ const gIndex = args.indexOf("-g")
|
|
|
+ expect(args[gIndex + 1]).toBe("*")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should correctly detect nested hidden directories with mixed separators", async () => {
|
|
|
+ const mockSpawn = vi.mocked(childProcess.spawn)
|
|
|
+ const mockProcess = {
|
|
|
+ stdout: {
|
|
|
+ on: vi.fn((event, callback) => {
|
|
|
+ if (event === "data") {
|
|
|
+ setTimeout(() => callback("file.txt\n"), 10)
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ stderr: {
|
|
|
+ on: vi.fn(),
|
|
|
+ },
|
|
|
+ on: vi.fn((event, callback) => {
|
|
|
+ if (event === "close") {
|
|
|
+ setTimeout(() => callback(0), 20)
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ kill: vi.fn(),
|
|
|
+ }
|
|
|
+ mockSpawn.mockReturnValue(mockProcess as any)
|
|
|
+
|
|
|
+ // Test with complex path including hidden directory
|
|
|
+ await listFiles("/test//normal/.hidden//subdir/", true, 100)
|
|
|
+
|
|
|
+ const [rgPath, args] = mockSpawn.mock.calls[0]
|
|
|
+ // When targeting a path containing a hidden directory, these flags should be present
|
|
|
+ expect(args).toContain("--no-ignore-vcs")
|
|
|
+ expect(args).toContain("--no-ignore")
|
|
|
+ expect(args).toContain("-g")
|
|
|
+ const gIndex = args.indexOf("-g")
|
|
|
+ expect(args[gIndex + 1]).toBe("*")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should not detect hidden directories when path only has dots in filenames", async () => {
|
|
|
+ const mockSpawn = vi.mocked(childProcess.spawn)
|
|
|
+ const mockProcess = {
|
|
|
+ stdout: {
|
|
|
+ on: vi.fn((event, callback) => {
|
|
|
+ if (event === "data") {
|
|
|
+ setTimeout(() => callback("file.txt\n"), 10)
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ stderr: {
|
|
|
+ on: vi.fn(),
|
|
|
+ },
|
|
|
+ on: vi.fn((event, callback) => {
|
|
|
+ if (event === "close") {
|
|
|
+ setTimeout(() => callback(0), 20)
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ kill: vi.fn(),
|
|
|
+ }
|
|
|
+ mockSpawn.mockReturnValue(mockProcess as any)
|
|
|
+
|
|
|
+ // Test with a path that has dots but no hidden directories
|
|
|
+ await listFiles("/test/file.with.dots/normal", true, 100)
|
|
|
+
|
|
|
+ const [rgPath, args] = mockSpawn.mock.calls[0]
|
|
|
+ // Should NOT have the special flags for hidden directories
|
|
|
+ expect(args).not.toContain("--no-ignore-vcs")
|
|
|
+ expect(args).not.toContain("--no-ignore")
|
|
|
+ })
|
|
|
+})
|