1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "runtime"
9 "strings"
10 "testing"
11 "time"
12 "unicode/utf8"
13
14 "github.com/bnema/zut/packages/agent/lsp"
15 "github.com/bnema/zut/packages/provider"
16)
17
18func TestLSPToolSchemaAndFormatting(t *testing.T) {
19 tool := NewLSPTool(t.TempDir(), lsp.NewManager())
20 var schema map[string]any
21 if err := json.Unmarshal(tool.Schema(), &schema); err != nil {
22 t.Fatal(err)
23 }
24 if tool.Name() != "lsp" || !strings.Contains(tool.Description(), "diagnostics") {
25 t.Fatalf("tool metadata missing")
26 }
27 result, err := tool.Execute(context.Background(), json.RawMessage(`{"action":"status"}`), nil)
28 if err != nil {
29 t.Fatal(err)
30 }
31 text, ok := result.Content[0].(provider.TextBlock)
32 if !ok || text.Text == "" {
33 t.Fatalf("status result = %#v", result.Content)
34 }
35 if !strings.Contains(text.Text, "[") && !strings.Contains(text.Text, "{") {
36 t.Fatalf("status was not formatted JSON: %q", text.Text)
37 }
38}
39
40func TestLSPToolGlobRespectsJailForSymlinkMatches(t *testing.T) {
41 if runtime.GOOS == "windows" {
42 t.Skip("symlink permissions vary on Windows")
43 }
44 root := t.TempDir()
45 outside := t.TempDir()
46 outsidePath := filepath.Join(outside, "outside.go")
47 if err := os.WriteFile(outsidePath, []byte("package outside\\n"), 0o644); err != nil {
48 t.Fatal(err)
49 }
50 if err := os.Symlink(outsidePath, filepath.Join(root, "link.go")); err != nil {
51 t.Fatal(err)
52 }
53 sandbox := NewSandbox(root)
54 sandbox.Lock()
55 tool := NewLSPTool(root, lsp.NewManager())
56 tool.Sandbox = sandbox
57 result, err := tool.Execute(context.Background(), json.RawMessage(`{"action":"diagnostics","path":"*.go","run_cli":false}`), nil)
58 if err != nil {
59 t.Fatal(err)
60 }
61 if !result.IsError {
62 t.Fatalf("jailed symlink glob was accepted: %#v", result)
63 }
64}
65
66func TestLSPToolTimeoutIsAlwaysBounded(t *testing.T) {
67 cases := []struct {
68 milliseconds int
69 want time.Duration
70 }{
71 {milliseconds: 0, want: defaultLSPTimeout},
72 {milliseconds: -1, want: defaultLSPTimeout},
73 {milliseconds: 25, want: 25 * time.Millisecond},
74 {milliseconds: maxLSPTimeoutMillis, want: maxLSPTimeout},
75 {milliseconds: maxLSPTimeoutMillis + 1, want: maxLSPTimeout},
76 {milliseconds: 1 << 62, want: maxLSPTimeout},
77 }
78 for _, tc := range cases {
79 if got := lspToolTimeout(tc.milliseconds); got != tc.want {
80 t.Errorf("lspToolTimeout(%d) = %s, want %s", tc.milliseconds, got, tc.want)
81 }
82 }
83}
84
85func TestBoundLSPText(t *testing.T) {
86 value := boundLSPText(strings.Repeat("x", maxLSPToolOutput+10))
87 if len(value) > maxLSPToolOutput || !strings.Contains(value, "truncated") {
88 t.Fatalf("bound output = %d bytes", len(value))
89 }
90 unicodeValue := boundLSPText(strings.Repeat("界", maxLSPToolOutput/3+10))
91 if !utf8.ValidString(unicodeValue) {
92 t.Fatal("bound output split a UTF-8 sequence")
93 }
94}