// fileread_test.go -- FileRead 工具的单元测试.
//
// 覆盖场景:
// - 正常读取文件(带行号 cat -n 格式)
// - offset 和 limit 参数
// - 空文件处理
// - 文件不存在错误
// - 二进制文件检测
// - 图片文件检测(含 base64 内联,尺寸检测)
// - PDF 文件检测(含 magic bytes,pages 参数验证)
// - Jupyter Notebook 解析
// - 目录检测
// - 设备文件阻止
// - 编码检测(UTF-8 BOM,UTF-16)
// - 符号链接处理
// - 路径验证
// - 文件状态缓存集成
// - formatFileSize 辅助函数
// - PDF 页码范围解析
package builtin
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
// ─────────────────────────────────────────────────────────────────────
// 测试 Mock
// ─────────────────────────────────────────────────────────────────────
// mockFileCache 是 FileCacheRecorder 的测试 mock.
type mockFileCacheForRead struct {
recorded map[string][]byte
}
func newMockFileCacheForRead() *mockFileCacheForRead {
return &mockFileCacheForRead{recorded: make(map[string][]byte)}
}
func (m *mockFileCacheForRead) Record(path string, content []byte) {
m.recorded[path] = content
}
// mockStateCache 是 FileStateCacheRecorder 的测试 mock.
type mockStateCache struct {
entries map[string]FileStateCacheEntry
}
func newMockStateCache() *mockStateCache {
return &mockStateCache{entries: make(map[string]FileStateCacheEntry)}
}
func (m *mockStateCache) RecordState(path string, entry FileStateCacheEntry) {
m.entries[path] = entry
}
// ─────────────────────────────────────────────────────────────────────
// 基础读取测试
// ─────────────────────────────────────────────────────────────────────
// TestFileReadTool_BasicRead 测试基本文件读取
func TestFileReadTool_BasicRead(t *testing.T) {
tool := NewFileReadTool()
dir := t.TempDir()
filePath := filepath.Join(dir, "test.txt")
os.WriteFile(filePath, []byte("line one\nline two\nline three\n"), 0644)
input, _ := json.Marshal(fileReadInput{FilePath: filePath})
result, err := tool.Execute(context.Background(), input, nil)
if err != nil {
t.Fatalf("执行失败: %v", err)
}
if result.IsError {
t.Fatalf("不应标记为错误: %s", result.Output)
}
// 验证 cat -n 格式的行号
if !strings.Contains(result.Output, "1\t") {
t.Errorf("输出应包含行号, 实际: %q", result.Output)
}
if !strings.Contains(result.Output, "line one") {
t.Errorf("输出应包含文件内容, 实际: %q", result.Output)
}
}
// TestFileReadTool_OffsetAndLimit 测试 offset 和 limit 参数
func TestFileReadTool_OffsetAndLimit(t *testing.T) {
tool := NewFileReadTool()
dir := t.TempDir()
filePath := filepath.Join(dir, "test.txt")
content := "line1\nline2\nline3\nline4\nline5\n"
os.WriteFile(filePath, []byte(content), 0644)
// offset=2 表示跳过前 2 行,limit=2 表示读 2 行
input, _ := json.Marshal(fileReadInput{FilePath: filePath, Offset: 2, Limit: 2})
result, err := tool.Execute(context.Background(), input, nil)
if err != nil {
t.Fatalf("执行失败: %v", err)
}
if result.IsError {
t.Fatalf("不应标记为错误: %s", result.Output)
}
// 应该包含 line3 和 line4(第 3,4 行)
if !strings.Contains(result.Output, "line3") {
t.Errorf("应包含 line3: %q", result.Output)
}
if !strings.Contains(result.Output, "line4") {
t.Errorf("应包含 line4: %q", result.Output)
}
// 不应包含 line1, line2, line5
if strings.Contains(result.Output, "line1") {
t.Errorf("不应包含 line1: %q", result.Output)
}
if strings.Contains(result.Output, "line5") {
t.Errorf("不应包含 line5: %q", result.Output)
}
}
// TestFileReadTool_EmptyFile 测试空文件
func TestFileReadTool_EmptyFile(t *testing.T) {
tool := NewFileReadTool()
dir := t.TempDir()
filePath := filepath.Join(dir, "empty.txt")
os.WriteFile(filePath, []byte{}, 0644)
input, _ := json.Marshal(fileReadInput{FilePath: filePath})
result, err := tool.Execute(context.Background(), input, nil)
if err != nil {
t.Fatalf("执行失败: %v", err)
}
if result.IsError {
t.Error("空文件不应标记为错误")
}
if !strings.Contains(result.Output, "empty") {
t.Errorf("应提示文件为空: %q", result.Output)
}
}
// TestFileReadTool_FileNotFound 测试文件不存在
func TestFileReadTool_FileNotFound(t *testing.T) {
tool := NewFileReadTool()
input, _ := json.Marshal(fileReadInput{FilePath: "/nonexistent/file.txt"})
result, err := tool.Execute(context.Background(), input, nil)
if err != nil {
t.Fatalf("不应返回 Go error: %v", err)
}
if !result.IsError {
t.Error("文件不存在应标记为错误")
}
if !strings.Contains(result.Output, "not found") {
t.Errorf("错误信息不匹配: %s", result.Output)
}
}
// TestFileReadTool_BinaryFile 测试二进制文件检测
func TestFileReadTool_BinaryFile(t *testing.T) {
tool := NewFileReadTool()
dir := t.TempDir()
filePath := filepath.Join(dir, "binary.dat")
// 写入含有 null 字节的二进制数据(不以图片 magic bytes 开头)
os.WriteFile(filePath, []byte{0x01, 0x02, 0x00, 0x03, 0x04, 0x05, 0x06}, 0644)
input, _ := json.Marshal(fileReadInput{FilePath: filePath})
result, err := tool.Execute(context.Background(), input, nil)
if err != nil {
t.Fatalf("执行失败: %v", err)
}
if result.IsError {
t.Error("二进制文件检测不应标记为错误")
}
if !strings.Contains(result.Output, "Binary file") {
t.Errorf("应提示为二进制文件: %q", result.Output)
}
}
// TestFileReadTool_Directory 测试读取目录报错
func TestFileReadTool_Directory(t *testing.T) {
tool := NewFileReadTool()
dir := t.TempDir()
input, _ := json.Marshal(fileReadInput{FilePath: dir})
result, err := tool.Execute(context.Background(), input, nil)
if err != nil {
t.Fatalf("不应返回 Go error: %v", err)
}
if !result.IsError {
t.Error("读取目录应报错")
}
if !strings.Contains(result.Output, "directory") {
t.Errorf("应提示为目录: %s", result.Output)
}
}
// TestFileReadTool_EmptyFilePath 测试空文件路径
func TestFileReadTool_EmptyFilePath(t *testing.T) {
tool := NewFileReadTool()
input, _ := json.Marshal(fileReadInput{FilePath: ""})
result, err := tool.Execute(context.Background(), input, nil)
if err != nil {
t.Fatalf("不应返回 Go error: %v", err)
}
if !result.IsError {
t.Error("空路径应报错")
}
}
// ─────────────────────────────────────────────────────────────────────
// 图片文件测试
// ─────────────────────────────────────────────────────────────────────
// TestFileReadTool_ImageFile 测试图片文件返回 base64 内联
func TestFileReadTool_ImageFile(t *testing.T) {
tool := NewFileReadTool()
dir := t.TempDir()
filePath := filepath.Join(dir, "photo.png")
// 写入一个最小的 1x1 PNG
// PNG magic + IHDR chunk (1x1, 8-bit RGBA)
pngData := []byte{
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG magic
0x00, 0x00, 0x00, 0x0D, // IHDR length
0x49, 0x48, 0x44, 0x52, // "IHDR"
0x00, 0x00, 0x00, 0x01, // width: 1
0x00, 0x00, 0x00, 0x01, // height: 1
0x08, 0x02, // 8-bit RGB
0x00, 0x00, 0x00,
0x90, 0x77, 0x53, 0xDE, // CRC
0x00, 0x00, 0x00, 0x0C, // IDAT length
0x49, 0x44, 0x41, 0x54, // "IDAT"
0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00,
0x00, 0x02, 0x00, 0x01,
0xE2, 0x21, 0xBC, 0x33, // CRC
0x00, 0x00, 0x00, 0x00, // IEND length
0x49, 0x45, 0x4E, 0x44, // "IEND"
0xAE, 0x42, 0x60, 0x82, // CRC
}
os.WriteFile(filePath, pngData, 0644)
input, _ := json.Marshal(fileReadInput{FilePath: filePath})
result, err := tool.Execute(context.Background(), input, nil)
if err != nil {
t.Fatalf("执行失败: %v", err)
}
if result.IsError {
t.Errorf("图片文件不应标记为错误: %s", result.Output)
}
if !strings.Contains(result.Output, "Image file") {
t.Errorf("应识别为图片文件: %q", result.Output)
}
if !strings.Contains(result.Output, "image/png") {
t.Errorf("应检测到 PNG 类型: %q", result.Output)
}
if !strings.Contains(result.Output, "base64") {
t.Errorf("应包含 base64 数据: %q", result.Output)
}
// 验证 Data 中的 ImageResult
if result.Data == nil {
t.Fatal("Data 不应为 nil")
}
imgResult, ok := result.Data.(*ImageResult)
if !ok {
t.Fatalf("Data 类型应为 *ImageResult, 实际: %T", result.Data)
}
if imgResult.MediaType != "image/png" {
t.Errorf("MediaType 应为 image/png, 实际: %s", imgResult.MediaType)
}
if imgResult.Base64 == "" {
t.Error("Base64 不应为空")
}
}
// TestFileReadTool_JPEGImage 测试 JPEG 图片文件
func TestFileReadTool_JPEGImage(t *testing.T) {
tool := NewFileReadTool()
dir := t.TempDir()
filePath := filepath.Join(dir, "photo.jpg")
// JPEG magic bytes
jpegData := []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46}
os.WriteFile(filePath, jpegData, 0644)
input, _ := json.Marshal(fileReadInput{FilePath: filePath})
result, err := tool.Execute(context.Background(), input, nil)
if err != nil {
t.Fatalf("执行失败: %v", err)
}
if !strings.Contains(result.Output, "image/jpeg") {
t.Errorf("应检测到 JPEG 类型: %q", result.Output)
}
}
// TestFileReadTool_SVGAsText 测试 SVG 文件作为文本读取
func TestFileReadTool_SVGAsText(t *testing.T) {
tool := NewFileReadTool()
dir := t.TempDir()
filePath := filepath.Join(dir, "icon.svg")
svgContent := ``
os.WriteFile(filePath, []byte(svgContent), 0644)
input, _ := json.Marshal(fileReadInput{FilePath: filePath})
result, err := tool.Execute(context.Background(), input, nil)
if err != nil {
t.Fatalf("执行失败: %v", err)
}
if result.IsError {
t.Errorf("SVG 读取不应标记为错误: %s", result.Output)
}
// SVG 应该作为文本返回,包含 SVG 源码
if !strings.Contains(result.Output, "