Files
filepuff-mcp/internal/server/session_integration_test.go
T
lukaszraczylo 664b7dac45 perf(file_read): compact line-number prefix by default
The default line prefix was the padded '  42| ' form; the compact '42|' form
existed only as opt-in (compact_line_numbers). The padding and trailing space
carry no information an LLM needs, costing ~3 chars/line — ~300 tokens on a
400-line read, the highest-volume path in the server. Flip the default to
compact; compact_line_numbers=false, mode/line_numbers=full, and the session
'full' pref all still restore the padded alignment (explicit override wins).

Updates the two tests that pinned the old default and refreshes stale comments.
2026-06-21 02:15:49 +01:00

314 lines
11 KiB
Go

package server
import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/lukaszraczylo/mcp-filepuff/internal/config"
"github.com/mark3labs/mcp-go/mcp"
)
// ---- Session prefs integration tests ----
// setSessionPrefs injects prefs directly on the server (bypasses MCP hook machinery).
func setSessionPrefs(srv *Server, prefs SessionPrefs) {
srv.sessionPrefs.Store(&prefs)
}
// TestSessionPrefsFileReadLineNumbersNone verifies that session pref line_numbers=none
// disables line-number prefixes and is overridden by explicit compact_line_numbers=true.
func TestSessionPrefsFileReadLineNumbersNone(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
content := "package main\n\nfunc Foo() {}\n"
if err := os.WriteFile(testFile, []byte(content), 0600); err != nil {
t.Fatalf("write file: %v", err)
}
cfg := &config.Config{WorkspaceRoot: tmpDir, MaxFileSize: 1 << 20}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
srv, err := New(cfg, logger)
if err != nil {
t.Fatalf("New: %v", err)
}
setSessionPrefs(srv, ParseSessionPrefs(map[string]any{"line_numbers": "none"}))
ctx := context.Background()
// Without explicit override: session pref should suppress line numbers.
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]interface{}{"path": testFile}
result, err := srv.handleFileRead(ctx, req)
if err != nil {
t.Fatalf("handleFileRead: %v", err)
}
text := result.Content[0].(mcp.TextContent).Text
// line_numbers=none suppresses the prefix entirely (no "│" gutter at all).
if strings.Contains(text, " 1│") {
t.Errorf("session line_numbers=none: expected no line-number prefix, got:\n%s", text)
}
// Explicit per-call compact_line_numbers=true should override session none.
req2 := mcp.CallToolRequest{}
req2.Params.Arguments = map[string]interface{}{
"path": testFile,
"compact_line_numbers": true,
}
result2, err := srv.handleFileRead(ctx, req2)
if err != nil {
t.Fatalf("handleFileRead (explicit compact): %v", err)
}
text2 := result2.Content[0].(mcp.TextContent).Text
// Compact format emits "1│" prefix.
if !strings.Contains(text2, "\u2502") {
t.Errorf("explicit compact_line_numbers should override session none, got:\n%s", text2)
}
}
// TestSessionPrefsFileReadLineNumbersCompact verifies line_numbers=compact session pref.
func TestSessionPrefsFileReadLineNumbersCompact(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "x.go")
if err := os.WriteFile(testFile, []byte("package main\nfunc Bar() {}\n"), 0600); err != nil {
t.Fatalf("write: %v", err)
}
cfg := &config.Config{WorkspaceRoot: tmpDir, MaxFileSize: 1 << 20}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
srv, _ := New(cfg, logger)
setSessionPrefs(srv, ParseSessionPrefs(map[string]any{"line_numbers": "compact"}))
ctx := context.Background()
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]interface{}{"path": testFile}
result, _ := srv.handleFileRead(ctx, req)
text := result.Content[0].(mcp.TextContent).Text
// Padded prefix is " 1│ "; compact is "1│".
if strings.Contains(text, " 1\u2502") {
t.Errorf("session line_numbers=compact should use compact prefix, got:\n%s", text)
}
// Should still have the │ separator somewhere.
if !strings.Contains(text, "\u2502") {
t.Errorf("session line_numbers=compact should still have \u2502 separator, got:\n%s", text)
}
}
// TestSessionPrefsResourceLinkThreshold verifies per-session threshold override.
func TestSessionPrefsResourceLinkThreshold(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "big.go")
var sb strings.Builder
sb.WriteString("package main\n\nfunc Foo() {\n")
for i := 0; i < 15; i++ {
sb.WriteString("// comment line\n")
}
sb.WriteString("}\n")
if err := os.WriteFile(testFile, []byte(sb.String()), 0600); err != nil {
t.Fatalf("write: %v", err)
}
// Config threshold = 0 (disabled) so content is always inlined by default.
cfg := &config.Config{WorkspaceRoot: tmpDir, MaxFileSize: 1 << 20, ResourceLinkThresholdBytes: 0}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
srv, _ := New(cfg, logger)
// Set session threshold = 10 bytes (tiny), so any real file triggers resource-link.
setSessionPrefs(srv, ParseSessionPrefs(map[string]any{"resource_link_threshold": float64(10)}))
ctx := context.Background()
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]interface{}{"path": testFile}
result, _ := srv.handleFileRead(ctx, req)
if len(result.Content) == 0 {
t.Fatal("expected content")
}
_, isLink := result.Content[0].(mcp.ResourceLink)
_, isText := result.Content[0].(mcp.TextContent)
if !isLink && isText {
t.Error("expected ResourceLink when session threshold is very small, got TextContent")
}
// force_inline should still bypass even a session threshold.
req2 := mcp.CallToolRequest{}
req2.Params.Arguments = map[string]interface{}{"path": testFile, "force_inline": true}
result2, _ := srv.handleFileRead(ctx, req2)
if _, ok := result2.Content[0].(mcp.TextContent); !ok {
t.Error("force_inline=true should bypass session threshold and return TextContent")
}
}
// TestSessionPrefsASTQueryFormat verifies default_format session pref for ast_query.
func TestSessionPrefsASTQueryFormat(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
if err := os.WriteFile(testFile, []byte("package main\n\nfunc Greet() {}\n"), 0600); err != nil {
t.Fatalf("write: %v", err)
}
cfg := &config.Config{WorkspaceRoot: tmpDir, MaxFileSize: 1 << 20}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
srv, _ := New(cfg, logger)
setSessionPrefs(srv, ParseSessionPrefs(map[string]any{"default_format": "compact"}))
ctx := context.Background()
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]interface{}{
"pattern": "func $NAME()",
"language": "go",
"paths": []interface{}{tmpDir},
// no format key → session default should apply
}
result, err := srv.handleASTQuery(ctx, req)
if err != nil {
t.Fatalf("handleASTQuery: %v", err)
}
if result == nil || len(result.Content) == 0 {
t.Fatal("empty result")
}
text := result.Content[0].(mcp.TextContent).Text
// Compact format emits one-line results without "**file:line**" markers.
if strings.Contains(text, "**") {
t.Errorf("session default_format=compact: expected compact output (no **), got:\n%s", text)
}
// Explicit format=verbose should override session compact.
req2 := mcp.CallToolRequest{}
req2.Params.Arguments = map[string]interface{}{
"pattern": "func $NAME()",
"language": "go",
"paths": []interface{}{tmpDir},
"format": "verbose",
}
result2, _ := srv.handleASTQuery(ctx, req2)
if result2 != nil && len(result2.Content) > 0 {
text2 := result2.Content[0].(mcp.TextContent).Text
if !strings.Contains(text2, "**") {
t.Errorf("explicit format=verbose should override session compact, got:\n%s", text2)
}
}
}
// TestSessionPrefsASTQueryMaxResults verifies default_max_results for ast_query.
func TestSessionPrefsASTQueryMaxResults(t *testing.T) {
tmpDir := t.TempDir()
// Build a file with 5 functions.
var sb strings.Builder
sb.WriteString("package main\n\n")
for i := 0; i < 5; i++ {
fmt.Fprintf(&sb, "func Fn%c() {}\n\n", rune('A'+i))
}
testFile := filepath.Join(tmpDir, "many.go")
if err := os.WriteFile(testFile, []byte(sb.String()), 0600); err != nil {
t.Fatalf("write: %v", err)
}
cfg := &config.Config{WorkspaceRoot: tmpDir, MaxFileSize: 1 << 20}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
srv, _ := New(cfg, logger)
// Session pref: max 2 results.
setSessionPrefs(srv, ParseSessionPrefs(map[string]any{"default_max_results": float64(2)}))
ctx := context.Background()
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]interface{}{
"pattern": "func $NAME()",
"language": "go",
"paths": []interface{}{tmpDir},
// no max_results → session pref of 2 should apply
}
result, err := srv.handleASTQuery(ctx, req)
if err != nil {
t.Fatalf("handleASTQuery: %v", err)
}
if result == nil || len(result.Content) == 0 {
t.Fatal("empty result")
}
text := result.Content[0].(mcp.TextContent).Text
// With 5 funcs and max=2, output should mention remaining.
if !strings.Contains(text, "remaining") && !strings.Contains(text, "cursor") {
t.Errorf("session max_results=2 with 5 matches should produce cursor line, got:\n%s", text)
}
}
// TestSessionPrefsFileSearchDefaultCluster verifies default_cluster session pref.
func TestSessionPrefsFileSearchDefaultCluster(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "x.go")
content := "package main\n\nfunc Foo() {}\nfunc Foo2() {}\n"
if err := os.WriteFile(testFile, []byte(content), 0600); err != nil {
t.Fatalf("write: %v", err)
}
cfg := &config.Config{
WorkspaceRoot: tmpDir,
MaxFileSize: 1 << 20,
SearchTimeout: 10 * time.Second,
}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
srv, err := New(cfg, logger)
if err != nil {
t.Fatalf("New: %v", err)
}
if srv.searcher == nil {
t.Skip("ripgrep not available")
}
setSessionPrefs(srv, ParseSessionPrefs(map[string]any{"default_cluster": true}))
ctx := context.Background()
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]interface{}{
"pattern": "func",
"paths": []interface{}{tmpDir},
// no cluster flag → session default_cluster=true should apply
}
result, err := srv.handleFileSearch(ctx, req)
if err != nil {
t.Fatalf("handleFileSearch: %v", err)
}
if result == nil || len(result.Content) == 0 {
t.Skip("search returned no results")
}
// Verify call succeeded (cluster behaviour is ripgrep-version dependent).
_ = result.Content[0].(mcp.TextContent).Text
}
// TestSessionPrefsMaxResultsExplicitOverride verifies explicit call-time max_results
// overrides session pref for ast_query.
func TestSessionPrefsMaxResultsExplicitOverride(t *testing.T) {
tmpDir := t.TempDir()
var sb strings.Builder
sb.WriteString("package main\n\n")
for i := 0; i < 5; i++ {
fmt.Fprintf(&sb, "func Fn%c() {}\n\n", rune('A'+i))
}
testFile := filepath.Join(tmpDir, "many.go")
if err := os.WriteFile(testFile, []byte(sb.String()), 0600); err != nil {
t.Fatalf("write: %v", err)
}
cfg := &config.Config{WorkspaceRoot: tmpDir, MaxFileSize: 1 << 20}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
srv, _ := New(cfg, logger)
// Session wants 2, caller supplies 10 — all 5 should fit without cursor.
setSessionPrefs(srv, ParseSessionPrefs(map[string]any{"default_max_results": float64(2)}))
ctx := context.Background()
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]interface{}{
"pattern": "func $NAME()",
"language": "go",
"paths": []interface{}{tmpDir},
"max_results": 10, // explicit override
}
result, _ := srv.handleASTQuery(ctx, req)
if result == nil || len(result.Content) == 0 {
t.Fatal("empty result")
}
text := result.Content[0].(mcp.TextContent).Text
// With max_results=10 and only 5 funcs, no cursor line expected.
if strings.Contains(text, "remaining") {
t.Errorf("explicit max_results=10 should override session 2; 5 funcs fit; no cursor expected, got:\n%s", text)
}
}