1package lsp
2
3import (
4 "bufio"
5 "bytes"
6 "context"
7 "encoding/json"
8 "errors"
9 "io"
10 "os"
11 "path/filepath"
12 "runtime"
13 "strconv"
14 "strings"
15 "testing"
16 "time"
17)
18
19func TestFakeLSPProcess(t *testing.T) {
20 if os.Getenv("ZUT_FAKE_LSP") != "1" {
21 return
22 }
23 reader := bufio.NewReader(os.Stdin)
24 for {
25 payload, err := ReadMessage(reader)
26 if err != nil {
27 return
28 }
29 var message rpcEnvelope
30 if json.Unmarshal(payload, &message) != nil {
31 continue
32 }
33 switch message.Method {
34 case "initialize":
35 if os.Getenv("ZUT_FAKE_LSP_HANG_INITIALIZE") == "1" {
36 continue
37 }
38 response := []byte(`{"jsonrpc":"2.0","id":` + string(message.ID) + `,"result":{"capabilities":{"definitionProvider":true}}}`)
39 _ = WriteMessage(os.Stdout, response)
40 case "test/echo":
41 response := []byte(`{"jsonrpc":"2.0","id":` + string(message.ID) + `,"result":{"echo":true}}`)
42 _ = WriteMessage(os.Stdout, response)
43 case "shutdown":
44 response := []byte(`{"jsonrpc":"2.0","id":` + string(message.ID) + `,"result":null}`)
45 _ = WriteMessage(os.Stdout, response)
46 case "textDocument/didOpen":
47 var params struct {
48 TextDocument struct {
49 URI string `json:"uri"`
50 } `json:"textDocument"`
51 }
52 _ = json.Unmarshal(message.Params, ¶ms)
53 response := map[string]any{"jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": map[string]any{"uri": params.TextDocument.URI, "diagnostics": []any{map[string]any{"range": map[string]any{"start": map[string]int{"line": 0, "character": 0}, "end": map[string]int{"line": 0, "character": 1}}, "severity": 1, "code": "FAKE", "message": "fake problem"}}}}
54 data, _ := json.Marshal(response)
55 _ = WriteMessage(os.Stdout, data)
56 }
57 }
58}
59
60func TestManagerCapabilitiesHonorsContextDuringInitialize(t *testing.T) {
61 root := t.TempDir()
62 config := `{"autoDetect":false,"servers":{"fake":{"kind":"lsp","command":` + strconv.Quote(os.Args[0]) + `,"args":["-test.run=TestFakeLSPProcess"],"env":{"ZUT_FAKE_LSP":"1","ZUT_FAKE_LSP_HANG_INITIALIZE":"1"}}}}`
63 if err := os.WriteFile(filepath.Join(root, "lsp.json"), []byte(config), 0o644); err != nil {
64 t.Fatal(err)
65 }
66 manager := NewManager()
67 t.Cleanup(func() { _ = manager.Close() })
68 ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
69 defer cancel()
70 started := time.Now()
71 _, err := manager.Capabilities(ctx, root, "", "fake")
72 if !errors.Is(err, context.DeadlineExceeded) {
73 t.Fatalf("Capabilities error = %v, want context deadline exceeded", err)
74 }
75 if elapsed := time.Since(started); elapsed > time.Second {
76 t.Fatalf("Capabilities returned after %s, want prompt context cancellation", elapsed)
77 }
78}
79
80func TestClientFakeStdioRPC(t *testing.T) {
81 root := t.TempDir()
82 script := os.Args[0]
83 spec := ServerConfig{ID: "fake", Kind: "lsp", Command: script, Args: []string{"-test.run=TestFakeLSPProcess"}, Env: map[string]string{"ZUT_FAKE_LSP": "1"}}
84 diagnostics := make(chan []Diagnostic, 1)
85 client, err := NewClientWithContext(context.Background(), spec, root, ClientOptions{OnDiagnostics: func(value []Diagnostic) { diagnostics <- value }})
86 if err != nil {
87 t.Fatal(err)
88 }
89 t.Cleanup(func() { _ = client.Close() })
90 result, err := client.Request(context.Background(), "test/echo", map[string]any{})
91 if err != nil || !bytes.Contains(result, []byte(`"echo":true`)) {
92 t.Fatalf("echo result = %s, err = %v", result, err)
93 }
94 path := filepath.Join(root, "main.go")
95 if err := os.WriteFile(path, []byte("package main\n"), 0o644); err != nil {
96 t.Fatal(err)
97 }
98 if err := client.DidOpen(path, "go", "package main\n"); err != nil {
99 t.Fatal(err)
100 }
101 select {
102 case values := <-diagnostics:
103 if len(values) != 1 || values[0].Code != "FAKE" || values[0].Path != path {
104 t.Fatalf("diagnostics = %#v", values)
105 }
106 case <-time.After(2 * time.Second):
107 t.Fatal("timed out waiting for fake diagnostics")
108 }
109}
110
111func TestFileURIPathRoundTrip(t *testing.T) {
112 path := filepath.Join(t.TempDir(), "space name", "main.go")
113 uri, err := pathToURI(path)
114 if err != nil {
115 t.Fatal(err)
116 }
117 if !strings.HasPrefix(uri, "file:///") {
118 t.Fatalf("URI = %q, want an absolute file URI", uri)
119 }
120 got, err := uriToPath(uri)
121 if err != nil {
122 t.Fatal(err)
123 }
124 abs, err := filepath.Abs(path)
125 if err != nil {
126 t.Fatal(err)
127 }
128 if filepath.Clean(got) != filepath.Clean(abs) {
129 t.Fatalf("round-trip path = %q, want %q", got, abs)
130 }
131 if runtime.GOOS != "windows" {
132 posixPath, err := uriToPath("file:///x:tmp/main.go")
133 if err != nil || posixPath != "/x:tmp/main.go" {
134 t.Fatalf("POSIX drive-like URI = %q, err = %v", posixPath, err)
135 }
136 }
137 if runtime.GOOS == "windows" {
138 unc := `\\server\share\main.go`
139 uncURI, err := pathToURI(unc)
140 if err != nil {
141 t.Fatal(err)
142 }
143 if !strings.HasPrefix(uncURI, "file://server/") {
144 t.Fatalf("UNC URI = %q", uncURI)
145 }
146 uncPath, err := uriToPath(uncURI)
147 if err != nil || filepath.Clean(uncPath) != filepath.Clean(unc) {
148 t.Fatalf("UNC round trip = %q, err = %v", uncPath, err)
149 }
150 }
151}
152
153func TestFramingRejectsMalformedAndOversizedMessages(t *testing.T) {
154 cases := []string{
155 "\r\n{}",
156 "Content-Length: -1\r\n\r\n",
157 "Content-Length: nope\r\n\r\n",
158 "Content-Length: 1\r\n" + strings.Repeat("X", maxHeaderBytes) + "\r\n",
159 "Content-Length: " + strconv.Itoa(maxMessageBytes+1) + "\r\n\r\n",
160 }
161 for _, wire := range cases {
162 if _, err := ReadMessage(bufio.NewReader(strings.NewReader(wire))); err == nil {
163 t.Fatalf("accepted malformed frame %q", wire[:min(len(wire), 80)])
164 }
165 }
166 var out bytes.Buffer
167 if err := WriteMessage(&out, make([]byte, maxMessageBytes+1)); err == nil {
168 t.Fatal("accepted oversized outbound frame")
169 }
170}
171
172func TestFramingRoundTripWithAdditionalHeader(t *testing.T) {
173 payload := []byte(`{"jsonrpc":"2.0","id":7,"result":{"ok":true}}`)
174 wire := append([]byte("Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n"), Frame(payload)...)
175 got, err := ReadMessage(bufio.NewReader(bytes.NewReader(wire)))
176 if err != nil {
177 t.Fatal(err)
178 }
179 if !bytes.Equal(got, payload) {
180 t.Fatalf("payload = %q, want %q", got, payload)
181 }
182}
183
184func TestZutHomeEnvironmentPrecedence(t *testing.T) {
185 configuredHome := filepath.Join(t.TempDir(), "zut")
186 xdgHome := filepath.Join(t.TempDir(), "state")
187 t.Setenv("ZUT_HOME", configuredHome)
188 t.Setenv("XDG_STATE_HOME", xdgHome)
189 if got := zutHome(); got != configuredHome {
190 t.Fatalf("ZUT_HOME path = %q, want %q", got, configuredHome)
191 }
192 t.Setenv("ZUT_HOME", "")
193 if got := zutHome(); got != filepath.Join(xdgHome, "zut") {
194 t.Fatalf("XDG_STATE_HOME path = %q, want %q", got, filepath.Join(xdgHome, "zut"))
195 }
196}
197
198func TestLoadConfigMergesGlobalProjectAndBuiltins(t *testing.T) {
199 root := t.TempDir()
200 zutHome := t.TempDir()
201 t.Setenv("ZUT_HOME", zutHome)
202 if err := os.WriteFile(filepath.Join(zutHome, "lsp.json"), []byte(`{"servers":{"gopls":{"settings":{"global":true},"args":["--global"]}}}`), 0o644); err != nil {
203 t.Fatal(err)
204 }
205 if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example\n"), 0o644); err != nil {
206 t.Fatal(err)
207 }
208 if err := os.WriteFile(filepath.Join(root, "lsp.json"), []byte(`{"gopls":{"command":"local-gopls","args":["--local"],"settings":{"local":true}},"sarif-lint":{"command":"reviewdog","kind":"cli","parser":"sarif","fileTypes":[".go"]}}`), 0o644); err != nil {
209 t.Fatal(err)
210 }
211 config, err := LoadConfig(root)
212 if err != nil {
213 t.Fatal(err)
214 }
215 gopls := config.Servers["gopls"]
216 if gopls.Command != "local-gopls" || len(gopls.Args) != 1 || gopls.Args[0] != "--local" {
217 t.Fatalf("gopls override = %#v", gopls)
218 }
219 if len(gopls.FileTypes) == 0 || len(gopls.RootMarkers) == 0 {
220 t.Fatalf("builtin selectors were not retained: %#v", gopls)
221 }
222 if gopls.Settings["global"] != true || gopls.Settings["local"] != true {
223 t.Fatalf("settings did not merge: %#v", gopls.Settings)
224 }
225 if config.Servers["sarif-lint"].Kind != "cli" || config.Servers["sarif-lint"].Parser != "sarif" {
226 t.Fatalf("custom linter = %#v", config.Servers["sarif-lint"])
227 }
228 selected := config.ApplicableServers(root, filepath.Join(root, "main.go"))
229 var foundGopls, foundSarif bool
230 for _, server := range selected {
231 foundGopls = foundGopls || server.ID == "gopls"
232 foundSarif = foundSarif || server.ID == "sarif-lint"
233 }
234 if !foundGopls || !foundSarif {
235 t.Fatalf("selected servers = %#v", selected)
236 }
237}
238
239func TestLoadConfigAcceptsProviderArrays(t *testing.T) {
240 root := t.TempDir()
241 zutHome := t.TempDir()
242 t.Setenv("ZUT_HOME", zutHome)
243 if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example\n"), 0o644); err != nil {
244 t.Fatal(err)
245 }
246 configFile := `{
247 "autoDetect": true,
248 "providers": [
249 "gopls",
250 {"id":"gopls","args":["serve"]},
251 {"id":"custom-lint","kind":"cli","command":"custom-lint","fileTypes":[".go"],"rootMarkers":["go.mod"],"cli":{"parser":"generic","mode":"files"}}
252 ]
253 }`
254 if err := os.WriteFile(filepath.Join(root, "lsp.json"), []byte(configFile), 0o644); err != nil {
255 t.Fatal(err)
256 }
257 config, err := LoadConfig(root)
258 if err != nil {
259 t.Fatal(err)
260 }
261 if got := config.Servers["gopls"].Args; len(got) != 1 || got[0] != "serve" {
262 t.Fatalf("gopls provider override = %#v", got)
263 }
264 lint := config.Servers["custom-lint"]
265 if lint.Kind != "cli" || lint.Parser != "generic" || lint.Mode != "files" {
266 t.Fatalf("custom provider = %#v", lint)
267 }
268}
269
270func TestLoadConfigAutoDetectFalseKeepsExplicitProviders(t *testing.T) {
271 root := t.TempDir()
272 t.Setenv("ZUT_HOME", t.TempDir())
273 if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example\n"), 0o644); err != nil {
274 t.Fatal(err)
275 }
276 if err := os.WriteFile(filepath.Join(root, "lsp.json"), []byte(`{"autoDetect":false,"providers":["gopls"]}`), 0o644); err != nil {
277 t.Fatal(err)
278 }
279 config, err := LoadConfig(root)
280 if err != nil {
281 t.Fatal(err)
282 }
283 selected := config.ApplicableServers(root, filepath.Join(root, "main.go"))
284 if len(selected) != 1 || selected[0].ID != "gopls" {
285 t.Fatalf("explicit providers with autoDetect=false = %#v", selected)
286 }
287}
288
289func TestResolveCommandPrefersWorkspaceTools(t *testing.T) {
290 if runtime.GOOS == "windows" {
291 t.Skip("executable bits are not represented on Windows")
292 }
293 root := t.TempDir()
294 bin := filepath.Join(root, "node_modules", ".bin")
295 if err := os.MkdirAll(bin, 0o755); err != nil {
296 t.Fatal(err)
297 }
298 command := filepath.Join(bin, "fake-lsp")
299 if err := os.WriteFile(command, []byte("#!/bin/sh\n"), 0o755); err != nil {
300 t.Fatal(err)
301 }
302 got, err := ResolveCommand(root, "fake-lsp")
303 if err != nil {
304 t.Fatal(err)
305 }
306 if got != command {
307 t.Fatalf("resolved %q, want %q", got, command)
308 }
309}
310
311func TestParsers(t *testing.T) {
312 root := t.TempDir()
313 eslint := `[ {"filePath":"src/a.js","messages":[{"ruleId":"no-x","severity":2,"message":"bad","line":2,"column":3}] } ]`
314 got := ParseDiagnostics("eslint-json", "eslint", root, eslint)
315 if len(got) != 1 || got[0].Severity != "error" || got[0].Range.Start.Line != 1 {
316 t.Fatalf("eslint = %#v", got)
317 }
318 golangci := `{"Issues":[{"FromLinter":"govet","Text":"bad","Severity":"warning","Pos":{"Filename":"a.go","Line":3,"Column":4}}]}`
319 if got = ParseDiagnostics("golangci-lint-json", "golangci", root, golangci); len(got) != 1 || got[0].Severity != "warning" {
320 t.Fatalf("golangci = %#v", got)
321 }
322 ruff := `[{"code":"F401","message":"unused","filename":"a.py","location":{"row":1,"column":2}}]`
323 if got = ParseDiagnostics("ruff-json", "ruff", root, ruff); len(got) != 1 || got[0].Code != "F401" {
324 t.Fatalf("ruff = %#v", got)
325 }
326 sarif := `{"runs":[{"results":[{"ruleId":"R1","level":"error","message":{"text":"bad"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"a.c"},"region":{"startLine":4,"startColumn":2}}}]}]}]}`
327 if got = ParseDiagnostics("sarif", "scan", root, sarif); len(got) != 1 || got[0].Range.Start.Line != 3 {
328 t.Fatalf("sarif = %#v", got)
329 }
330 if got = ParseDiagnostics("generic", "lint", root, "a.go:5:6: warning: bad"); len(got) != 1 || got[0].Severity != "warning" {
331 t.Fatalf("generic = %#v", got)
332 }
333 if got = ParseDiagnostics("generic", "lint", root, "a.go:5:6: cannot find name"); len(got) != 1 || got[0].Message != "cannot find name" {
334 t.Fatalf("generic message = %#v", got)
335 }
336}
337
338func TestDiagnosticDedupUsesCodeAndStartPosition(t *testing.T) {
339 path := filepath.Join(t.TempDir(), "main.go")
340 base := Diagnostic{Path: path, Severity: "error", Code: "E1", Message: "same", Range: Range{Start: Position{Line: 1, Character: 2}, End: Position{Line: 1, Character: 3}}}
341 endChanged := base
342 endChanged.Range.End.Character = 20
343 otherCode := base
344 otherCode.Code = "E2"
345 otherStart := base
346 otherStart.Range.Start.Line++
347 if got := DeduplicateDiagnostics([]Diagnostic{base, endChanged, otherCode, otherStart}); len(got) != 3 {
348 t.Fatalf("deduplicated diagnostics = %#v, want three reports", got)
349 }
350}
351
352func TestManagerClearsPublishedDiagnostics(t *testing.T) {
353 manager := NewManager()
354 root := t.TempDir()
355 path := filepath.Join(root, "main.go")
356 if err := os.WriteFile(path, []byte("package main\n"), 0o644); err != nil {
357 t.Fatal(err)
358 }
359 ws, err := manager.workspace(root)
360 if err != nil {
361 t.Fatal(err)
362 }
363 manager.storeDiagnostics(ws, "fake", []Diagnostic{{Path: path, Message: "problem"}})
364 if got := manager.snapshot(ws, path); len(got) != 1 {
365 t.Fatalf("stored diagnostics = %#v", got)
366 }
367 manager.storeDiagnostics(ws, "fake", []Diagnostic{{Path: path, Clear: true}})
368 if got := manager.snapshot(ws, path); len(got) != 0 {
369 t.Fatalf("cleared diagnostics = %#v", got)
370 }
371}
372
373func TestDiagnosticsDedupAndGrouping(t *testing.T) {
374 root := t.TempDir()
375 makeDiag := func(path, message string, line int, server string) Diagnostic {
376 return Diagnostic{Path: filepath.Join(root, path), Severity: "error", Code: "E", Message: message, Server: server, Range: Range{Start: Position{Line: line}}}
377 }
378 input := []Diagnostic{makeDiag("a.go", "same", 0, "gopls"), makeDiag("a.go", "same", 0, "eslint"), makeDiag("a.go", `Cannot find module "missing"`, 1, "gopls"), makeDiag("a.go", "cascade one", 2, "gopls"), makeDiag("a.go", "cascade two", 3, "gopls"), makeDiag("b.go", "repeat", 1, "a"), makeDiag("c.go", "repeat", 2, "b"), makeDiag("d.go", "repeat", 3, "c")}
379 if got := DeduplicateDiagnostics(input); len(got) != len(input)-1 {
380 t.Fatalf("dedup count = %d", len(got))
381 }
382 text := SummarizeDiagnostics(input, root, 50)
383 if !strings.Contains(text, "repeated") || !strings.Contains(text, "SECONDARY") {
384 t.Fatalf("summary lacks grouping:\n%s", text)
385 }
386}
387
388func TestApplyWorkspaceEditUsesUTF16AndRejectsOutside(t *testing.T) {
389 root := t.TempDir()
390 path := filepath.Join(root, "a.txt")
391 if err := os.WriteFile(path, []byte("a😀b\n"), 0o644); err != nil {
392 t.Fatal(err)
393 }
394 uri := URIForPath(path)
395 ed := WorkspaceEdit{Changes: map[string][]TextEdit{uri: {{Range: Range{Start: Position{Line: 0, Character: 1}, End: Position{Line: 0, Character: 3}}, NewText: "X"}}}}
396 if err := ApplyWorkspaceEdit(root, ed); err != nil {
397 t.Fatal(err)
398 }
399 data, _ := os.ReadFile(path)
400 if string(data) != "aXb\n" {
401 t.Fatalf("edited data = %q", data)
402 }
403 outside := WorkspaceEdit{Changes: map[string][]TextEdit{URIForPath(filepath.Join(filepath.Dir(root), "outside.txt")): {{Range: Range{}, NewText: "x"}}}}
404 if err := ApplyWorkspaceEdit(root, outside); err == nil {
405 t.Fatal("outside edit was accepted")
406 }
407}
408
409func TestWorkspaceEditPreflightsAllTargets(t *testing.T) {
410 root := t.TempDir()
411 inside := filepath.Join(root, "inside.txt")
412 outside := filepath.Join(t.TempDir(), "outside.txt")
413 if err := os.WriteFile(inside, []byte("old\n"), 0o644); err != nil {
414 t.Fatal(err)
415 }
416 if err := os.WriteFile(outside, []byte("old\n"), 0o644); err != nil {
417 t.Fatal(err)
418 }
419 rootReal, err := filepath.EvalSymlinks(root)
420 if err != nil {
421 t.Fatal(err)
422 }
423 _, err = prepareEditTargets(rootReal, map[string][]TextEdit{
424 URIForPath(inside): {{Range: Range{}, NewText: "inside"}},
425 URIForPath(outside): {{Range: Range{}, NewText: "outside"}},
426 }, nil)
427 if err == nil {
428 t.Fatal("workspace edit accepted an unsafe target after a safe target")
429 }
430}
431
432func TestApplyWorkspaceEditFollowsInWorkspaceSymlink(t *testing.T) {
433 if runtime.GOOS == "windows" {
434 t.Skip("symlink permissions vary on Windows")
435 }
436 root := t.TempDir()
437 target := filepath.Join(root, "target.txt")
438 link := filepath.Join(root, "link.txt")
439 if err := os.WriteFile(target, []byte("old\n"), 0o644); err != nil {
440 t.Fatal(err)
441 }
442 if err := os.Symlink(target, link); err != nil {
443 t.Fatal(err)
444 }
445 edit := WorkspaceEdit{Changes: map[string][]TextEdit{URIForPath(link): {{Range: Range{Start: Position{Line: 0, Character: 0}, End: Position{Line: 0, Character: 3}}, NewText: "new"}}}}
446 if err := ApplyWorkspaceEdit(root, edit); err != nil {
447 t.Fatal(err)
448 }
449 data, err := os.ReadFile(target)
450 if err != nil {
451 t.Fatal(err)
452 }
453 if string(data) != "new\n" {
454 t.Fatalf("target data = %q", data)
455 }
456 info, err := os.Lstat(link)
457 if err != nil {
458 t.Fatal(err)
459 }
460 if info.Mode()&os.ModeSymlink == 0 {
461 t.Fatal("workspace edit replaced the symlink")
462 }
463}
464
465func TestLimitedBufferCapsReaderCopy(t *testing.T) {
466 var buffer limitedBuffer
467 buffer.limit = 4
468 copied, err := io.Copy(&buffer, strings.NewReader("123456"))
469 if err != nil {
470 t.Fatal(err)
471 }
472 if copied != 6 || buffer.String() != "1234" {
473 t.Fatalf("copied %d bytes into %q", copied, buffer.String())
474 }
475}
476
477func TestDiagnosticJSONCodeNumber(t *testing.T) {
478 var diagnostic Diagnostic
479 if err := json.Unmarshal([]byte(`{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":1}},"severity":2,"code":123,"message":"warning"}`), &diagnostic); err != nil {
480 t.Fatal(err)
481 }
482 if diagnostic.Code != "123" || diagnostic.Severity != "warning" {
483 t.Fatalf("diagnostic = %#v", diagnostic)
484 }
485}
486
487func TestManagerClosePreventsNewWorkspaces(t *testing.T) {
488 manager := NewManager()
489 root := t.TempDir()
490 if err := manager.Close(); err != nil {
491 t.Fatal(err)
492 }
493 if err := manager.Close(); err != nil {
494 t.Fatal(err)
495 }
496 if _, err := manager.Config(root); err == nil {
497 t.Fatal("closed manager accepted a new workspace")
498 }
499 if err := manager.Reload(root); err == nil {
500 t.Fatal("closed manager accepted a reload")
501 }
502}
503
504func TestReduceDiagnosticsSuppressesLocationOnlyChanges(t *testing.T) {
505 manager := NewManager()
506 root := t.TempDir()
507 path := filepath.Join(root, "main.go")
508 first := Diagnostic{Path: path, Severity: "error", Message: "type mismatch", Range: Range{Start: Position{Line: 1, Character: 2}}}
509 if got := manager.ReduceDiagnostics(root, path, []Diagnostic{first}); len(got) != 1 {
510 t.Fatalf("first reduction returned %d diagnostics, want 1", len(got))
511 }
512 shifted := first
513 shifted.Range.Start.Line = 20
514 if got := manager.ReduceDiagnostics(root, path, []Diagnostic{shifted}); len(got) != 0 {
515 t.Fatalf("shifted diagnostic was replayed: %#v", got)
516 }
517 if got := manager.ReduceDiagnostics(root, path, nil); len(got) != 0 {
518 t.Fatalf("clear returned %#v", got)
519 }
520 if got := manager.ReduceDiagnostics(root, path, []Diagnostic{shifted}); len(got) != 1 {
521 t.Fatalf("diagnostic did not reappear after clear: %#v", got)
522 }
523}