package handlers_test import ( "context" "encoding/json" "math" "os" "path/filepath" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/yan1h/agent-coding-workflow/internal/acp/handlers" ) func TestFsRead_HappyPath(t *testing.T) { t.Parallel() tmp := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(tmp, "a.txt"), []byte("hello\n"), 0o644)) h := handlers.NewFsReadHandler() sess := handlers.SessionContext{SessionID: uuid.New(), CwdPath: tmp} params := mustJSON(t, map[string]any{"sessionId": "x", "path": filepath.Join(tmp, "a.txt")}) res := h.Handle(context.Background(), sess, params) require.Nil(t, res.Err) require.NotNil(t, res.OK) var out struct { Content string `json:"content"` } require.NoError(t, json.Unmarshal(res.OK, &out)) assert.Equal(t, "hello\n", out.Content) } func TestFsRead_PathOutsideWorktree(t *testing.T) { t.Parallel() tmp := t.TempDir() h := handlers.NewFsReadHandler() sess := handlers.SessionContext{CwdPath: tmp} outside := filepath.Join(filepath.VolumeName(tmp)+string(filepath.Separator), "etc", "passwd") params := mustJSON(t, map[string]any{"sessionId": "x", "path": outside}) res := h.Handle(context.Background(), sess, params) require.NotNil(t, res.Err) assert.Equal(t, -32001, res.Err.Code) } func TestFsRead_FileNotFound(t *testing.T) { t.Parallel() tmp := t.TempDir() h := handlers.NewFsReadHandler() sess := handlers.SessionContext{CwdPath: tmp} params := mustJSON(t, map[string]any{"sessionId": "x", "path": filepath.Join(tmp, "nope.txt")}) res := h.Handle(context.Background(), sess, params) require.NotNil(t, res.Err) assert.Equal(t, -32603, res.Err.Code) } func TestFsRead_LineLimit(t *testing.T) { t.Parallel() tmp := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(tmp, "f.txt"), []byte("L0\nL1\nL2\nL3\n"), 0o644)) h := handlers.NewFsReadHandler() sess := handlers.SessionContext{CwdPath: tmp} line := 1 limit := 2 params := mustJSON(t, map[string]any{ "sessionId": "x", "path": filepath.Join(tmp, "f.txt"), "line": line, "limit": limit, }) res := h.Handle(context.Background(), sess, params) require.Nil(t, res.Err) var out struct { Content string `json:"content"` } _ = json.Unmarshal(res.OK, &out) assert.Equal(t, "L1\nL2", out.Content) } // TestFsRead_HugeLimit_NoOverflow 锁定 bug #2:limit 为 math.MaxInt 时 // start+limit 会整数溢出为负,旧逻辑下 lines[start:end] 会 panic。修复后应安全 // 钳制到文件末尾,返回从 line 起的剩余内容。 func TestFsRead_HugeLimit_NoOverflow(t *testing.T) { t.Parallel() tmp := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(tmp, "f.txt"), []byte("L0\nL1\nL2\nL3\n"), 0o644)) h := handlers.NewFsReadHandler() sess := handlers.SessionContext{CwdPath: tmp} line := 1 limit := math.MaxInt params := mustJSON(t, map[string]any{ "sessionId": "x", "path": filepath.Join(tmp, "f.txt"), "line": line, "limit": limit, }) var res handlers.FsResult require.NotPanics(t, func() { res = h.Handle(context.Background(), sess, params) }) require.Nil(t, res.Err) var out struct { Content string `json:"content"` } require.NoError(t, json.Unmarshal(res.OK, &out)) // 从 line=1 到末尾(split 出 5 个元素:L0 L1 L2 L3 "")。 assert.Equal(t, "L1\nL2\nL3\n", out.Content) } func mustJSON(t *testing.T, v any) json.RawMessage { t.Helper() b, err := json.Marshal(v) require.NoError(t, err) return b }