package tools import ( "context" "io" "os" "path/filepath" "regexp" "strings" "testing" "github.com/stretchr/testify/assert" ) // TestFilesystemTool_ReadFile_Success verifies successful file reading func TestFilesystemTool_ReadFile_Success(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "test.txt") os.WriteFile(testFile, []byte("test content"), 0o644) tool := NewReadFileBytesTool("", false, MaxReadFileSize) ctx := context.Background() args := map[string]any{ "path": testFile, } result := tool.Execute(ctx, args) // Success should not be an error if result.IsError { t.Errorf("Expected success, got IsError=true: %s", result.ForLLM) } // ForLLM should contain file content if !strings.Contains(result.ForLLM, "test content") { t.Errorf("Expected ForLLM to contain 'test content', got: %s", result.ForLLM) } // ReadFile returns NewToolResult which only sets ForLLM, not ForUser // This is the expected behavior - file content goes to LLM, not directly to user if result.ForUser != "" { t.Errorf("Expected ForUser to be empty for NewToolResult, got: %s", result.ForUser) } } // TestFilesystemTool_ReadFile_NotFound verifies error handling for missing file func TestFilesystemTool_ReadFile_NotFound(t *testing.T) { tool := NewReadFileBytesTool("", false, MaxReadFileSize) ctx := context.Background() args := map[string]any{ "path": "/nonexistent_file_12345.txt", } result := tool.Execute(ctx, args) // Failure should be marked as error if !result.IsError { t.Errorf("Expected error for missing file, got IsError=false") } // Should contain error message if !strings.Contains(result.ForLLM, "failed to open file") && !strings.Contains(result.ForUser, "failed to open") { t.Errorf( "Expected error message, got ForLLM: %s, ForUser: %s", result.ForLLM, result.ForUser, ) } } // TestFilesystemTool_ReadFile_MissingPath verifies error handling for missing path func TestFilesystemTool_ReadFile_MissingPath(t *testing.T) { tool := &ReadFileTool{} ctx := context.Background() args := map[string]any{} result := tool.Execute(ctx, args) // Should return error result if !result.IsError { t.Errorf("Expected error when path is missing") } // Should mention required parameter if !strings.Contains(result.ForLLM, "path is required") && !strings.Contains(result.ForUser, "path is required") { t.Errorf("Expected 'path is required' message, got ForLLM: %s", result.ForLLM) } } // TestFilesystemTool_WriteFile_Success verifies successful file writing func TestFilesystemTool_WriteFile_Success(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "newfile.txt") tool := NewWriteFileTool("", false) ctx := context.Background() args := map[string]any{ "path": testFile, "content": "hello world", } result := tool.Execute(ctx, args) // Success should not be an error if result.IsError { t.Errorf("Expected success, got IsError=true: %s", result.ForLLM) } // WriteFile returns SilentResult if !result.Silent { t.Errorf("Expected Silent=true for WriteFile, got false") } // ForUser should be empty (silent result) if result.ForUser != "" { t.Errorf("Expected ForUser to be empty for SilentResult, got: %s", result.ForUser) } // Verify file was actually written content, err := os.ReadFile(testFile) if err != nil { t.Fatalf("Failed to read written file: %v", err) } if string(content) != "hello world" { t.Errorf("Expected file content 'hello world', got: %s", string(content)) } } // TestFilesystemTool_WriteFile_LiteralBackslashN verifies write_file keeps // literal backslash sequences unchanged when they are passed as plain text. func TestFilesystemTool_WriteFile_LiteralBackslashN(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "literal.txt") tool := NewWriteFileTool("", false) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "content": `aaa\naaa`, }) assert.False(t, result.IsError, "expected success, got: %s", result.ForLLM) data, err := os.ReadFile(testFile) assert.NoError(t, err) assert.Equal(t, `aaa\naaa`, string(data)) } // TestFilesystemTool_WriteFile_PreservesCRLF verifies write_file does not // normalize line endings and writes CRLF bytes as provided. func TestFilesystemTool_WriteFile_PreservesCRLF(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "crlf.txt") content := "line1\r\nline2\r\n" tool := NewWriteFileTool("", false) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "content": content, }) assert.False(t, result.IsError, "expected success, got: %s", result.ForLLM) data, err := os.ReadFile(testFile) assert.NoError(t, err) assert.Equal(t, []byte(content), data) } // TestFilesystemTool_WriteFile_CreateDir verifies directory creation func TestFilesystemTool_WriteFile_CreateDir(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "subdir", "newfile.txt") tool := NewWriteFileTool("", false) ctx := context.Background() args := map[string]any{ "path": testFile, "content": "test", } result := tool.Execute(ctx, args) // Success should not be an error if result.IsError { t.Errorf("Expected success with directory creation, got IsError=true: %s", result.ForLLM) } // Verify directory was created and file written content, err := os.ReadFile(testFile) if err != nil { t.Fatalf("Failed to read written file: %v", err) } if string(content) != "test" { t.Errorf("Expected file content 'test', got: %s", string(content)) } } // TestFilesystemTool_WriteFile_MissingPath verifies error handling for missing path func TestFilesystemTool_WriteFile_MissingPath(t *testing.T) { tool := NewWriteFileTool("", false) ctx := context.Background() args := map[string]any{ "content": "test", } result := tool.Execute(ctx, args) // Should return error result if !result.IsError { t.Errorf("Expected error when path is missing") } } // TestFilesystemTool_WriteFile_MissingContent verifies error handling for missing content func TestFilesystemTool_WriteFile_MissingContent(t *testing.T) { tool := NewWriteFileTool("", false) ctx := context.Background() args := map[string]any{ "path": "/tmp/test.txt", } result := tool.Execute(ctx, args) // Should return error result if !result.IsError { t.Errorf("Expected error when content is missing") } // Should mention required parameter if !strings.Contains(result.ForLLM, "content is required") && !strings.Contains(result.ForUser, "content is required") { t.Errorf("Expected 'content is required' message, got ForLLM: %s", result.ForLLM) } } // TestFilesystemTool_WriteFile_OverwriteDefaultBlocked verifies that writing to an // existing file without overwrite=true returns an error. func TestFilesystemTool_WriteFile_OverwriteDefaultBlocked(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "existing.txt") os.WriteFile(testFile, []byte("original"), 0o644) tool := NewWriteFileTool("", false) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "content": "new content", }) assert.True(t, result.IsError, "expected error when overwriting without overwrite=true") assert.Contains(t, result.ForLLM, "already exists") assert.Contains(t, result.ForLLM, "overwrite=true") // Original content must be untouched data, err := os.ReadFile(testFile) assert.NoError(t, err) assert.Equal(t, "original", string(data)) } // TestFilesystemTool_WriteFile_OverwriteExplicitAllowed verifies that setting // overwrite=true replaces the existing file. func TestFilesystemTool_WriteFile_OverwriteExplicitAllowed(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "existing.txt") os.WriteFile(testFile, []byte("original"), 0o644) tool := NewWriteFileTool("", false) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "content": "replaced", "overwrite": true, }) assert.False(t, result.IsError, "expected success with overwrite=true, got: %s", result.ForLLM) data, err := os.ReadFile(testFile) assert.NoError(t, err) assert.Equal(t, "replaced", string(data)) } // TestFilesystemTool_WriteFile_NewFileNoOverwriteFlag verifies that a new (non-existing) // file can be written without setting overwrite=true. func TestFilesystemTool_WriteFile_NewFileNoOverwriteFlag(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "newfile.txt") tool := NewWriteFileTool("", false) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "content": "brand new", }) assert.False(t, result.IsError, "expected success for new file, got: %s", result.ForLLM) data, err := os.ReadFile(testFile) assert.NoError(t, err) assert.Equal(t, "brand new", string(data)) } // TestFilesystemTool_WriteFile_OverwriteFalseExplicitBlocked verifies that // explicitly passing overwrite=false also blocks overwriting. func TestFilesystemTool_WriteFile_OverwriteFalseExplicitBlocked(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "existing.txt") os.WriteFile(testFile, []byte("original"), 0o644) tool := NewWriteFileTool("", false) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "content": "new content", "overwrite": false, }) assert.True(t, result.IsError, "expected error when overwrite=false") assert.Contains(t, result.ForLLM, "already exists") data, err := os.ReadFile(testFile) assert.NoError(t, err) assert.Equal(t, "original", string(data)) } // TestFilesystemTool_WriteFile_OverwriteSandboxed verifies the overwrite guard // works correctly in restricted (sandbox) mode. func TestFilesystemTool_WriteFile_OverwriteSandboxed(t *testing.T) { workspace := t.TempDir() testFile := "file.txt" os.WriteFile(filepath.Join(workspace, testFile), []byte("original"), 0o644) tool := NewWriteFileTool(workspace, true) // Without overwrite=true → blocked result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "content": "new content", }) assert.True(t, result.IsError, "expected error in sandbox mode without overwrite=true") assert.Contains(t, result.ForLLM, "already exists") // With overwrite=true → allowed result = tool.Execute(context.Background(), map[string]any{ "path": testFile, "content": "replaced in sandbox", "overwrite": true, }) assert.False( t, result.IsError, "expected success in sandbox mode with overwrite=true, got: %s", result.ForLLM, ) data, err := os.ReadFile(filepath.Join(workspace, testFile)) assert.NoError(t, err) assert.Equal(t, "replaced in sandbox", string(data)) } // TestFilesystemTool_ListDir_Success verifies successful directory listing func TestFilesystemTool_ListDir_Success(t *testing.T) { tmpDir := t.TempDir() os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("content"), 0o644) os.WriteFile(filepath.Join(tmpDir, "file2.txt"), []byte("content"), 0o644) os.Mkdir(filepath.Join(tmpDir, "subdir"), 0o755) tool := NewListDirTool("", false) ctx := context.Background() args := map[string]any{ "path": tmpDir, } result := tool.Execute(ctx, args) // Success should not be an error if result.IsError { t.Errorf("Expected success, got IsError=true: %s", result.ForLLM) } // Should list files and directories if !strings.Contains(result.ForLLM, "file1.txt") || !strings.Contains(result.ForLLM, "file2.txt") { t.Errorf("Expected files in listing, got: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "subdir") { t.Errorf("Expected subdir in listing, got: %s", result.ForLLM) } } // TestFilesystemTool_ListDir_NotFound verifies error handling for non-existent directory func TestFilesystemTool_ListDir_NotFound(t *testing.T) { tool := NewListDirTool("", false) ctx := context.Background() args := map[string]any{ "path": "/nonexistent_directory_12345", } result := tool.Execute(ctx, args) // Failure should be marked as error if !result.IsError { t.Errorf("Expected error for non-existent directory, got IsError=false") } // Should contain error message if !strings.Contains(result.ForLLM, "failed to read") && !strings.Contains(result.ForUser, "failed to read") { t.Errorf( "Expected error message, got ForLLM: %s, ForUser: %s", result.ForLLM, result.ForUser, ) } } // TestFilesystemTool_ListDir_DefaultPath verifies default to current directory func TestFilesystemTool_ListDir_DefaultPath(t *testing.T) { tool := NewListDirTool("", false) ctx := context.Background() args := map[string]any{} result := tool.Execute(ctx, args) // Should use "." as default path if result.IsError { t.Errorf("Expected success with default path '.', got IsError=true: %s", result.ForLLM) } } // Block paths that look inside workspace but point outside via symlink. func TestFilesystemTool_ReadFile_RejectsSymlinkEscape(t *testing.T) { root := t.TempDir() workspace := filepath.Join(root, "workspace") if err := os.MkdirAll(workspace, 0o755); err != nil { t.Fatalf("failed to create workspace: %v", err) } secret := filepath.Join(root, "secret.txt") if err := os.WriteFile(secret, []byte("top secret"), 0o644); err != nil { t.Fatalf("failed to write secret file: %v", err) } link := filepath.Join(workspace, "leak.txt") if err := os.Symlink(secret, link); err != nil { t.Skipf("symlink not supported in this environment: %v", err) } tool := NewReadFileTool(workspace, true, MaxReadFileSize) result := tool.Execute(context.Background(), map[string]any{ "path": link, }) if !result.IsError { t.Fatalf("expected symlink escape to be blocked") } // os.Root might return different errors depending on platform/implementation // but it definitely should error. // Our wrapper returns "access denied or file not found" if !strings.Contains(result.ForLLM, "access denied") && !strings.Contains(result.ForLLM, "file not found") && !strings.Contains(result.ForLLM, "no such file") { t.Fatalf("expected symlink escape error, got: %s", result.ForLLM) } } func TestFilesystemTool_EmptyWorkspace_AccessDenied(t *testing.T) { tool := NewReadFileTool("", true, MaxReadFileSize) // restrict=true but workspace="" // Try to read a sensitive file (simulated by a temp file outside workspace) tmpDir := t.TempDir() secretFile := filepath.Join(tmpDir, "shadow") os.WriteFile(secretFile, []byte("secret data"), 0o600) result := tool.Execute(context.Background(), map[string]any{ "path": secretFile, }) // We EXPECT IsError=true (access blocked due to empty workspace) assert.True( t, result.IsError, "Security Regression: Empty workspace allowed access! content: %s", result.ForLLM, ) // Verify it failed for the right reason assert.Contains( t, result.ForLLM, "workspace is not defined", "Expected 'workspace is not defined' error", ) } // TestRootMkdirAll verifies that root.MkdirAll (used by atomicWriteFileInRoot) handles all cases: // single dir, deeply nested dirs, already-existing dirs, and a file blocking a directory path. func TestRootMkdirAll(t *testing.T) { workspace := t.TempDir() root, err := os.OpenRoot(workspace) if err != nil { t.Fatalf("failed to open root: %v", err) } defer root.Close() // Case 1: Single directory err = root.MkdirAll("dir1", 0o755) assert.NoError(t, err) _, err = os.Stat(filepath.Join(workspace, "dir1")) assert.NoError(t, err) // Case 2: Deeply nested directory err = root.MkdirAll("a/b/c/d", 0o755) assert.NoError(t, err) _, err = os.Stat(filepath.Join(workspace, "a/b/c/d")) assert.NoError(t, err) // Case 3: Already exists — must be idempotent err = root.MkdirAll("a/b/c/d", 0o755) assert.NoError(t, err) // Case 4: A regular file blocks directory creation — must error err = os.WriteFile(filepath.Join(workspace, "file_exists"), []byte("data"), 0o644) assert.NoError(t, err) err = root.MkdirAll("file_exists", 0o755) assert.Error(t, err, "expected error when a file exists at the directory path") } func TestFilesystemTool_WriteFile_Restricted_CreateDir(t *testing.T) { workspace := t.TempDir() tool := NewWriteFileTool(workspace, true) ctx := context.Background() testFile := "deep/nested/path/to/file.txt" content := "deep content" args := map[string]any{ "path": testFile, "content": content, } result := tool.Execute(ctx, args) assert.False(t, result.IsError, "Expected success, got: %s", result.ForLLM) // Verify file content actualPath := filepath.Join(workspace, testFile) data, err := os.ReadFile(actualPath) assert.NoError(t, err) assert.Equal(t, content, string(data)) } // TestHostRW_Read_PermissionDenied verifies that hostRW.Read surfaces access denied errors. func TestHostRW_Read_PermissionDenied(t *testing.T) { if os.Getuid() == 0 { t.Skip("skipping permission test: running as root") } tmpDir := t.TempDir() protected := filepath.Join(tmpDir, "protected.txt") err := os.WriteFile(protected, []byte("secret"), 0o000) assert.NoError(t, err) defer os.Chmod(protected, 0o644) // ensure cleanup _, err = (&hostFs{}).ReadFile(protected) assert.Error(t, err) assert.Contains(t, err.Error(), "access denied") } // TestHostRW_Read_Directory verifies that hostRW.Read returns an error when given a directory path. func TestHostRW_Read_Directory(t *testing.T) { tmpDir := t.TempDir() _, err := (&hostFs{}).ReadFile(tmpDir) assert.Error(t, err, "expected error when reading a directory as a file") } // TestRootRW_Read_Directory verifies that rootRW.Read returns an error when given a directory. func TestRootRW_Read_Directory(t *testing.T) { workspace := t.TempDir() root, err := os.OpenRoot(workspace) assert.NoError(t, err) defer root.Close() // Create a subdirectory err = root.Mkdir("subdir", 0o755) assert.NoError(t, err) _, err = (&sandboxFs{workspace: workspace}).ReadFile("subdir") assert.Error(t, err, "expected error when reading a directory as a file") } // TestHostRW_Write_ParentDirMissing verifies that hostRW.Write creates parent dirs automatically. func TestHostRW_Write_ParentDirMissing(t *testing.T) { tmpDir := t.TempDir() target := filepath.Join(tmpDir, "a", "b", "c", "file.txt") err := (&hostFs{}).WriteFile(target, []byte("hello")) assert.NoError(t, err) data, err := os.ReadFile(target) assert.NoError(t, err) assert.Equal(t, "hello", string(data)) } // TestRootRW_Write_ParentDirMissing verifies that rootRW.Write creates // nested parent directories automatically within the sandbox. func TestRootRW_Write_ParentDirMissing(t *testing.T) { workspace := t.TempDir() relPath := "x/y/z/file.txt" err := (&sandboxFs{workspace: workspace}).WriteFile(relPath, []byte("nested")) assert.NoError(t, err) data, err := os.ReadFile(filepath.Join(workspace, relPath)) assert.NoError(t, err) assert.Equal(t, "nested", string(data)) } // TestHostRW_Write verifies the hostRW.Write helper function func TestHostRW_Write(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "atomic_test.txt") testData := []byte("atomic test content") err := (&hostFs{}).WriteFile(testFile, testData) assert.NoError(t, err) content, err := os.ReadFile(testFile) assert.NoError(t, err) assert.Equal(t, testData, content) // Verify it overwrites correctly newData := []byte("new atomic content") err = (&hostFs{}).WriteFile(testFile, newData) assert.NoError(t, err) content, err = os.ReadFile(testFile) assert.NoError(t, err) assert.Equal(t, newData, content) } // TestRootRW_Write verifies the rootRW.Write helper function func TestRootRW_Write(t *testing.T) { tmpDir := t.TempDir() relPath := "atomic_root_test.txt" testData := []byte("atomic root test content") erw := &sandboxFs{workspace: tmpDir} err := erw.WriteFile(relPath, testData) assert.NoError(t, err) root, err := os.OpenRoot(tmpDir) assert.NoError(t, err) defer root.Close() f, err := root.Open(relPath) assert.NoError(t, err) defer f.Close() content, err := io.ReadAll(f) assert.NoError(t, err) assert.Equal(t, testData, content) // Verify it overwrites correctly newData := []byte("new root atomic content") err = erw.WriteFile(relPath, newData) assert.NoError(t, err) f2, err := root.Open(relPath) assert.NoError(t, err) defer f2.Close() content, err = io.ReadAll(f2) assert.NoError(t, err) assert.Equal(t, newData, content) } // TestWhitelistFs_AllowsMatchingPaths verifies that whitelistFs allows access to // paths matching the whitelist patterns while blocking non-matching paths. func TestWhitelistFs_AllowsMatchingPaths(t *testing.T) { workspace := t.TempDir() outsideDir := t.TempDir() outsideFile := filepath.Join(outsideDir, "allowed.txt") os.WriteFile(outsideFile, []byte("outside content"), 0o644) // Pattern allows access to the outsideDir. patterns := []*regexp.Regexp{regexp.MustCompile(`^` + regexp.QuoteMeta(outsideDir))} tool := NewReadFileTool(workspace, true, MaxReadFileSize, patterns) // Read from whitelisted path should succeed. result := tool.Execute(context.Background(), map[string]any{"path": outsideFile}) if result.IsError { t.Errorf("expected whitelisted path to be readable, got: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "outside content") { t.Errorf("expected file content, got: %s", result.ForLLM) } // Read from non-whitelisted path outside workspace should fail. otherDir := t.TempDir() otherFile := filepath.Join(otherDir, "blocked.txt") os.WriteFile(otherFile, []byte("blocked"), 0o644) result = tool.Execute(context.Background(), map[string]any{"path": otherFile}) if !result.IsError { t.Errorf("expected non-whitelisted path to be blocked, got: %s", result.ForLLM) } } func TestWhitelistFs_BlocksSymlinkEscapeInAllowedDir(t *testing.T) { workspace := t.TempDir() allowedDir := t.TempDir() secretDir := t.TempDir() secretFile := filepath.Join(secretDir, "secret.txt") if err := os.WriteFile(secretFile, []byte("top secret"), 0o644); err != nil { t.Fatalf("WriteFile(secretFile) error = %v", err) } linkPath := filepath.Join(allowedDir, "link_out") if err := os.Symlink(secretDir, linkPath); err != nil { t.Skipf("symlink not supported in this environment: %v", err) } patterns := []*regexp.Regexp{regexp.MustCompile(`^` + regexp.QuoteMeta(allowedDir))} tool := NewReadFileTool(workspace, true, MaxReadFileSize, patterns) result := tool.Execute( context.Background(), map[string]any{"path": filepath.Join(linkPath, "secret.txt")}, ) if !result.IsError { t.Fatalf("expected symlink escape from allowed dir to be blocked, got: %s", result.ForLLM) } } func TestWhitelistFs_WriteAllowsNewFileUnderAllowedDir(t *testing.T) { workspace := t.TempDir() rootDir := t.TempDir() allowedDir := filepath.Join(rootDir, "allowed") targetFile := filepath.Join(allowedDir, "nested", "file.txt") patterns := []*regexp.Regexp{regexp.MustCompile(`^` + regexp.QuoteMeta(allowedDir))} tool := NewWriteFileTool(workspace, true, patterns) result := tool.Execute(context.Background(), map[string]any{ "path": targetFile, "content": "outside write", }) if result.IsError { t.Fatalf("expected whitelisted write to succeed, got: %s", result.ForLLM) } data, err := os.ReadFile(targetFile) if err != nil { t.Fatalf("ReadFile(targetFile) error = %v", err) } if string(data) != "outside write" { t.Fatalf("target file content = %q, want %q", string(data), "outside write") } } func TestWhitelistFs_AllowsResolvedAllowedRootAlias(t *testing.T) { workspace := t.TempDir() realDir := t.TempDir() linkParent := t.TempDir() allowedAlias := filepath.Join(linkParent, "allowed-link") if err := os.Symlink(realDir, allowedAlias); err != nil { t.Skipf("symlink not supported in this environment: %v", err) } targetFile := filepath.Join(allowedAlias, "nested", "alias.txt") if err := os.MkdirAll(filepath.Dir(targetFile), 0o755); err != nil { t.Fatalf("MkdirAll(targetFile dir) error = %v", err) } if err := os.WriteFile(targetFile, []byte("through alias"), 0o644); err != nil { t.Fatalf("WriteFile(targetFile) error = %v", err) } patterns := []*regexp.Regexp{ regexp.MustCompile( "^" + regexp.QuoteMeta(filepath.Clean(allowedAlias)) + "(?:" + regexp.QuoteMeta(string(os.PathSeparator)) + "|$)", ), } tool := NewReadFileTool(workspace, true, MaxReadFileSize, patterns) result := tool.Execute(context.Background(), map[string]any{"path": targetFile}) if result.IsError { t.Fatalf("expected symlink-backed allowed root to be readable, got: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "through alias") { t.Fatalf("expected file content, got: %s", result.ForLLM) } } // TestReadFileTool_ChunkedReading verifies the pagination logic of the tool // by reading a file in multiple chunks using 'offset' and 'length'. func TestReadFileTool_ChunkedReading(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "pagination_test.txt") fullContent := "abcdefghijklmnopqrstuvwxyz" err := os.WriteFile(testFile, []byte(fullContent), 0o644) if err != nil { t.Fatalf("Failed to write test file: %v", err) } tool := NewReadFileTool(tmpDir, false, MaxReadFileSize) ctx := context.Background() // --- Step 1: Read the first chunk (10 bytes) --- args1 := map[string]any{ "path": testFile, "offset": 0, "length": 10, } result1 := tool.Execute(ctx, args1) if result1.IsError { t.Fatalf("Chunk 1 failed: %s", result1.ForLLM) } if !strings.Contains(result1.ForLLM, "abcdefghij") { t.Errorf("Chunk 1 should contain 'abcdefghij', got: %s", result1.ForLLM) } if !strings.Contains(result1.ForLLM, "[TRUNCATED") { t.Errorf("Chunk 1 header should indicate truncation, got: %s", result1.ForLLM) } if !strings.Contains(result1.ForLLM, "offset=10") { t.Errorf("Chunk 1 header should suggest next offset=10, got: %s", result1.ForLLM) } // Step 2: Read the second chunk (10 bytes) --- args2 := map[string]any{ "path": testFile, "offset": 10, "length": 10, } result2 := tool.Execute(ctx, args2) if result2.IsError { t.Fatalf("Chunk 2 failed: %s", result2.ForLLM) } if !strings.Contains(result2.ForLLM, "klmnopqrst") { t.Errorf("Chunk 2 should contain 'klmnopqrst', got: %s", result2.ForLLM) } if !strings.Contains(result2.ForLLM, "offset=20") { t.Errorf("Chunk 2 header should suggest next offset=20, got: %s", result2.ForLLM) } // Step 3: Read the final chunk (remaining 6 bytes) --- args3 := map[string]any{ "path": testFile, "offset": 20, "length": 10, } result3 := tool.Execute(ctx, args3) if result3.IsError { t.Fatalf("Chunk 3 failed: %s", result3.ForLLM) } if !strings.Contains(result3.ForLLM, "uvwxyz") { t.Errorf("Chunk 3 should contain 'uvwxyz', got: %s", result3.ForLLM) } if !strings.Contains(result3.ForLLM, "[END OF FILE") { t.Errorf("Chunk 3 header should indicate end of file, got: %s", result3.ForLLM) } if strings.Contains(result3.ForLLM, "[TRUNCATED") { t.Errorf("Chunk 3 header should NOT indicate truncation, got: %s", result3.ForLLM) } } // TestReadFileTool_OffsetBeyondEOF checks the behavior when requesting // An offset that exceeds the total file size. func TestReadFileTool_OffsetBeyondEOF(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "short.txt") err := os.WriteFile(testFile, []byte("12345"), 0o644) if err != nil { t.Fatalf("Failed to write test file: %v", err) } tool := NewReadFileTool(tmpDir, false, MaxReadFileSize) ctx := context.Background() args := map[string]any{ "path": testFile, "offset": int64(100), } result := tool.Execute(ctx, args) if result.IsError { t.Errorf("A mistake was not expected, obtained IsError=true: %s", result.ForLLM) } expectedMsg := "[END OF FILE - no content at this offset]" if result.ForLLM != expectedMsg { t.Errorf("The message %q was expected, obtained: %q", expectedMsg, result.ForLLM) } } func TestReadFileLinesTool_ChunkedReading(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "pagination_lines.txt") fullContent := strings.Join([]string{ "line 1", "line 2", "line 3", "line 4", "line 5", "line 6", }, "\n") + "\n" err := os.WriteFile(testFile, []byte(fullContent), 0o644) if err != nil { t.Fatalf("Failed to write test file: %v", err) } tool := NewReadFileLinesTool(tmpDir, false, MaxReadFileSize) result1 := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": 1, "max_lines": 2, }) if result1.IsError { t.Fatalf("Chunk 1 failed: %s", result1.ForLLM) } if !strings.Contains(result1.ForLLM, "1|line 1\n2|line 2\n") { t.Fatalf("expected first two lines, got: %s", result1.ForLLM) } if !strings.Contains(result1.ForLLM, "lines 1-2") { t.Fatalf("expected line range 1-2, got: %s", result1.ForLLM) } if !strings.Contains(result1.ForLLM, "start_line=3") { t.Fatalf("expected continuation start_line=3, got: %s", result1.ForLLM) } if !strings.Contains(result1.ForLLM, "max_lines=2") { t.Fatalf("expected continuation max_lines=2, got: %s", result1.ForLLM) } result2 := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": 3, "max_lines": 2, }) if result2.IsError { t.Fatalf("Chunk 2 failed: %s", result2.ForLLM) } if !strings.Contains(result2.ForLLM, "3|line 3\n4|line 4\n") { t.Fatalf("expected middle chunk, got: %s", result2.ForLLM) } if !strings.Contains(result2.ForLLM, "start_line=5") { t.Fatalf("expected continuation start_line=5, got: %s", result2.ForLLM) } if !strings.Contains(result2.ForLLM, "max_lines=2") { t.Fatalf("expected continuation max_lines=2, got: %s", result2.ForLLM) } result3 := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": 5, "max_lines": 2, }) if result3.IsError { t.Fatalf("Chunk 3 failed: %s", result3.ForLLM) } if !strings.Contains(result3.ForLLM, "5|line 5\n6|line 6\n") { t.Fatalf("expected final chunk, got: %s", result3.ForLLM) } if !strings.Contains(result3.ForLLM, "[END OF FILE") { t.Fatalf("expected EOF marker, got: %s", result3.ForLLM) } } func TestReadFileLinesTool_DefaultOffsetAndRemainingLines(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "default_lines.txt") err := os.WriteFile(testFile, []byte("line 1\nline 2\nline 3\n"), 0o644) if err != nil { t.Fatalf("Failed to write test file: %v", err) } tool := NewReadFileLinesTool(tmpDir, false, MaxReadFileSize) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": 1, }) if result.IsError { t.Fatalf("Execute() error = %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "1|line 1\n2|line 2\n3|line 3\n") { t.Fatalf("expected remaining lines by default, got: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "lines 1-3") { t.Fatalf("expected line range 1-3, got: %s", result.ForLLM) } } func TestReadFileTool_LegacyLengthUsesByteModeForText(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "legacy_bytes.txt") err := os.WriteFile(testFile, []byte("abcdefghijklmnopqrstuvwxyz"), 0o644) if err != nil { t.Fatalf("Failed to write test file: %v", err) } tool := NewReadFileBytesTool(tmpDir, false, MaxReadFileSize) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "offset": 10, "length": 5, }) if result.IsError { t.Fatalf("Execute() error = %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "read: bytes 10-14") { t.Fatalf("expected byte-based header, got: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "klmno") { t.Fatalf("expected byte chunk content, got: %s", result.ForLLM) } if strings.Contains(result.ForLLM, "lines ") { t.Fatalf("expected legacy byte mode, got line-based header: %s", result.ForLLM) } } func TestReadFileLinesTool_OffsetBeyondEOF(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "short_lines.txt") err := os.WriteFile(testFile, []byte("line 1\nline 2\n"), 0o644) if err != nil { t.Fatalf("Failed to write test file: %v", err) } tool := NewReadFileLinesTool(tmpDir, false, MaxReadFileSize) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": int64(100), }) if result.IsError { t.Fatalf("unexpected error: %s", result.ForLLM) } if result.ForLLM != "[END OF FILE - no content at or after start_line=100]" { t.Fatalf("unexpected EOF message: %q", result.ForLLM) } } func TestReadFileLinesTool_RegistryValidationSupportsMaxLinesAndRejectsLimit(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "registry_lines.txt") err := os.WriteFile(testFile, []byte("line 1\nline 2\nline 3\n"), 0o644) if err != nil { t.Fatalf("Failed to write test file: %v", err) } reg := NewToolRegistry() reg.Register(NewReadFileLinesTool(tmpDir, false, MaxReadFileSize)) result := reg.Execute(context.Background(), "read_file", map[string]any{ "path": testFile, "start_line": 1, "max_lines": 1, }) if result.IsError { t.Fatalf("expected max_lines to pass registry validation, got: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "1|line 1\n") { t.Fatalf("expected first line via max_lines, got: %s", result.ForLLM) } result = reg.Execute(context.Background(), "read_file", map[string]any{ "path": testFile, "start_line": 2, "limit": 1, }) if !result.IsError { t.Fatalf("expected limit to be rejected, got success: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "unexpected property \"limit\"") { t.Fatalf("expected registry validation error for limit, got: %s", result.ForLLM) } } func TestReadFileLinesTool_RejectsOffset(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "legacy_offset.txt") err := os.WriteFile(testFile, []byte("line 1\nline 2\n"), 0o644) if err != nil { t.Fatalf("Failed to write test file: %v", err) } tool := NewReadFileLinesTool(tmpDir, false, MaxReadFileSize) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": 1, "offset": 1, }) if !result.IsError { t.Fatalf("expected offset to be rejected, got success: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "offset is not supported in line mode; use start_line") { t.Fatalf("unexpected error for offset in line mode: %s", result.ForLLM) } } func TestReadFileLinesTool_RejectsLength(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "legacy_length.txt") err := os.WriteFile(testFile, []byte("line 1\nline 2\n"), 0o644) if err != nil { t.Fatalf("Failed to write test file: %v", err) } tool := NewReadFileLinesTool(tmpDir, false, MaxReadFileSize) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": 1, "length": 1, }) if !result.IsError { t.Fatalf("expected length to be rejected, got success: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "length is not supported in line mode; use max_lines") { t.Fatalf("unexpected error for length in line mode: %s", result.ForLLM) } } func TestReadFileLinesTool_RejectsLimit(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "legacy_limit.txt") err := os.WriteFile(testFile, []byte("line 1\nline 2\n"), 0o644) if err != nil { t.Fatalf("Failed to write test file: %v", err) } tool := NewReadFileLinesTool(tmpDir, false, MaxReadFileSize) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": 1, "limit": 1, }) if !result.IsError { t.Fatalf("expected limit to be rejected, got success: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "limit is not supported in line mode; use max_lines") { t.Fatalf("unexpected error for limit in line mode: %s", result.ForLLM) } } func TestReadFileLinesTool_BinaryFileRejected(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "binary.dat") data := []byte{0x00, 0x01, 'A', 'B', 'C', 'D', 'E', 'F'} err := os.WriteFile(testFile, data, 0o644) if err != nil { t.Fatalf("Failed to write test file: %v", err) } tool := NewReadFileLinesTool(tmpDir, false, MaxReadFileSize) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": 1, }) if !result.IsError { t.Fatalf("expected binary file rejection in line mode, got: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "switch read_file mode to 'bytes'") { t.Fatalf("expected binary file rejection message, got: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "mode to 'bytes'") { t.Fatalf("expected suggestion to switch read_file mode, got: %s", result.ForLLM) } } func TestReadFileLinesTool_TruncatesSingleLongLineAtByteBudget(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "long_line.txt") content := "first line\n" + strings.Repeat("x", 70*1024) + "\n" err := os.WriteFile(testFile, []byte(content), 0o644) if err != nil { t.Fatalf("Failed to write test file: %v", err) } tool := NewReadFileLinesTool(tmpDir, false, MaxReadFileSize) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": 1, }) if result.IsError { t.Fatalf("Execute() error = %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "was cut mid-line") { t.Fatalf("expected explicit mid-line truncation warning, got: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "1|first line\n") { t.Fatalf("expected the first line with line prefix, got: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "2|") { t.Fatalf("expected line prefix for the truncated line, got: %s", result.ForLLM) } } func TestReadFileLinesTool_NoTrailingNewline(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "no_trailing_newline.txt") err := os.WriteFile(testFile, []byte("line 1\nline 2"), 0o644) if err != nil { t.Fatalf("Failed to write test file: %v", err) } tool := NewReadFileLinesTool(tmpDir, false, MaxReadFileSize) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": 1, }) if result.IsError { t.Fatalf("Execute() error = %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "1|line 1\n2|line 2") { t.Fatalf( "expected final line without trailing newline to be preserved, got: %s", result.ForLLM, ) } if !strings.Contains(result.ForLLM, "[END OF FILE - no further content.]") { t.Fatalf("expected EOF marker, got: %s", result.ForLLM) } } func TestReadFileLinesTool_ExactByteBudgetBoundaryIncludesPrefix(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "exact_boundary.txt") err := os.WriteFile(testFile, []byte("1234567\nsecond line\n"), 0o644) if err != nil { t.Fatalf("Failed to write test file: %v", err) } tool := NewReadFileLinesTool(tmpDir, false, 10) result := tool.Execute(context.Background(), map[string]any{ "path": testFile, "start_line": 1, }) if result.IsError { t.Fatalf("Execute() error = %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "1|1234567\n") { t.Fatalf( "expected first line to fit exactly in the byte budget with its prefix, got: %s", result.ForLLM, ) } if strings.Contains(result.ForLLM, "2|") { t.Fatalf( "expected second line to be excluded once the exact output byte budget was reached, got: %s", result.ForLLM, ) } if !strings.Contains(result.ForLLM, "file_bytes: 8 | output_bytes: 10") { t.Fatalf("expected separate file/output byte counters, got: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "start_line=2") { t.Fatalf("expected continuation at line 2, got: %s", result.ForLLM) } }