summaryrefslogtreecommitdiffhomepage
path: root/packages/lsp/src/diagnostics.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/lsp/src/diagnostics.test.ts')
-rw-r--r--packages/lsp/src/diagnostics.test.ts201
1 files changed, 154 insertions, 47 deletions
diff --git a/packages/lsp/src/diagnostics.test.ts b/packages/lsp/src/diagnostics.test.ts
index 9f2b6b4..70b1ffa 100644
--- a/packages/lsp/src/diagnostics.test.ts
+++ b/packages/lsp/src/diagnostics.test.ts
@@ -1,51 +1,158 @@
import { describe, expect, it } from "vitest";
-import { DiagnosticsStore } from "./diagnostics.js";
+import { type Diagnostic, DiagnosticsStore } from "./diagnostics.js";
describe("diagnostics", () => {
- it("formats diagnostics with severity, location, and message", () => {
- const store = new DiagnosticsStore();
- store.setPushDiagnostics({
- uri: "file:///test.ts",
- diagnostics: [
- {
- range: { start: { line: 0, character: 5 }, end: { line: 0, character: 10 } },
- severity: 1,
- source: "typescript",
- message: "Cannot find name 'hello'.",
- },
- ],
- });
-
- const formatted = store.format("file:///test.ts");
- expect(formatted).toContain("ERROR");
- expect(formatted).toContain("L1:6");
- expect(formatted).toContain("Cannot find name 'hello'.");
- expect(formatted).toContain("[typescript]");
- });
-
- it("merges push and pull diagnostics with deduplication", () => {
- const store = new DiagnosticsStore();
- const diag = {
- range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } },
- severity: 1,
- message: "Error",
- };
-
- store.setPushDiagnostics({
- uri: "file:///test.ts",
- diagnostics: [diag],
- });
- store.setPullDiagnostics("file:///test.ts", {
- kind: "full",
- items: [diag],
- });
-
- const merged = store.getMerged("file:///test.ts");
- expect(merged).toHaveLength(1);
- });
-
- it("returns empty string when no diagnostics exist", () => {
- const store = new DiagnosticsStore();
- expect(store.format("file:///nonexistent.ts")).toBe("");
- });
+ it("formats diagnostics with severity, location, and message", () => {
+ const store = new DiagnosticsStore();
+ store.setPushDiagnostics({
+ uri: "file:///test.ts",
+ diagnostics: [
+ {
+ range: { start: { line: 0, character: 5 }, end: { line: 0, character: 10 } },
+ severity: 1,
+ source: "typescript",
+ message: "Cannot find name 'hello'.",
+ },
+ ],
+ });
+
+ const formatted = store.format("file:///test.ts");
+ expect(formatted).toContain("ERROR");
+ expect(formatted).toContain("L1:6");
+ expect(formatted).toContain("Cannot find name 'hello'.");
+ expect(formatted).toContain("[typescript]");
+ });
+
+ it("merges push and pull diagnostics with deduplication", () => {
+ const store = new DiagnosticsStore();
+ const diag = {
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } },
+ severity: 1,
+ message: "Error",
+ };
+
+ store.setPushDiagnostics({
+ uri: "file:///test.ts",
+ diagnostics: [diag],
+ });
+ store.setPullDiagnostics("file:///test.ts", {
+ kind: "full",
+ items: [diag],
+ });
+
+ const merged = store.getMerged("file:///test.ts");
+ expect(merged).toHaveLength(1);
+ });
+
+ it("returns empty string when no diagnostics exist", () => {
+ const store = new DiagnosticsStore();
+ expect(store.format("file:///nonexistent.ts")).toBe("");
+ });
+
+ it("purge drops push diagnostics, pull diagnostics, and the received flag (Bug 3)", () => {
+ const store = new DiagnosticsStore();
+ store.setPushDiagnostics({
+ uri: "file:///project/a.ts",
+ diagnostics: [
+ {
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
+ severity: 1,
+ message: "boom",
+ },
+ ],
+ });
+ store.setPullDiagnostics("file:///project/a.ts", {
+ kind: "full",
+ items: [
+ {
+ range: { start: { line: 1, character: 0 }, end: { line: 1, character: 1 } },
+ severity: 2,
+ message: "warn",
+ },
+ ],
+ });
+ expect(store.hasReceivedPush("file:///project/a.ts")).toBe(true);
+ expect(store.format("file:///project/a.ts")).toContain("boom");
+
+ store.purge("file:///project/a.ts");
+
+ // Everything for that URI is gone — the store no longer retains it.
+ expect(store.hasReceivedPush("file:///project/a.ts")).toBe(false);
+ expect(store.format("file:///project/a.ts")).toBe("");
+ expect(store.getMerged("file:///project/a.ts")).toHaveLength(0);
+
+ // Other URIs are untouched.
+ expect(store.format("file:///project/b.ts")).toBe("");
+ });
+
+ it("bounds pushDiagnostics — oldest background file is evicted past the cap (Bug 3 remainder)", () => {
+ // Language servers scan the workspace and push diagnostics for files the
+ // agent never opened. These never enter the client's openDocuments LRU,
+ // so without an independent cap the pushDiagnostics map grows forever.
+ const store = new DiagnosticsStore();
+ const CAP = 100;
+
+ const diag = (uri: string): readonly Diagnostic[] => [
+ {
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
+ severity: 1,
+ message: `err in ${uri}`,
+ },
+ ];
+
+ // Push `CAP` distinct background files (at the cap — none evicted yet).
+ for (let i = 0; i < CAP; i++) {
+ store.setPushDiagnostics({ uri: `file:///bg/file${i}.ts`, diagnostics: diag(`file${i}`) });
+ }
+ expect(store.hasReceivedPush("file:///bg/file0.ts")).toBe(true);
+ expect(store.format("file:///bg/file0.ts")).toContain("err in file0");
+
+ // One past the cap → the least-recently-pushed (file0) is evicted.
+ store.setPushDiagnostics({
+ uri: "file:///bg/file100.ts",
+ diagnostics: diag("file100"),
+ });
+
+ // file0 (oldest) is gone — no didClose, no retained diagnostics, no flag.
+ expect(store.hasReceivedPush("file:///bg/file0.ts")).toBe(false);
+ expect(store.format("file:///bg/file0.ts")).toBe("");
+ expect(store.getMerged("file:///bg/file0.ts")).toHaveLength(0);
+
+ // The newest (file100) and a middle entry are retained.
+ expect(store.hasReceivedPush("file:///bg/file100.ts")).toBe(true);
+ expect(store.format("file:///bg/file100.ts")).toContain("err in file100");
+ expect(store.format("file:///bg/file50.ts")).toContain("err in file50");
+ });
+
+ it("re-pushing a URI refreshes its LRU position so it is not evicted", () => {
+ const store = new DiagnosticsStore();
+ const CAP = 100;
+
+ const diag = (uri: string): readonly Diagnostic[] => [
+ {
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
+ severity: 1,
+ message: uri,
+ },
+ ];
+
+ for (let i = 0; i < CAP; i++) {
+ store.setPushDiagnostics({ uri: `file:///bg/file${i}.ts`, diagnostics: diag(`file${i}`) });
+ }
+
+ // Re-push file0 (an actively-edited file gets re-pushed on each change):
+ // this should move it to the tail (most-recently-used), NOT leave it at
+ // the head where it would be the first eviction candidate.
+ store.setPushDiagnostics({ uri: "file:///bg/file0.ts", diagnostics: diag("file0-updated") });
+
+ // Now push one past the cap. file1 (now the oldest untouched) should be
+ // evicted; file0 (refreshed) must survive.
+ store.setPushDiagnostics({ uri: "file:///bg/file100.ts", diagnostics: diag("file100") });
+
+ expect(store.hasReceivedPush("file:///bg/file0.ts")).toBe(true);
+ expect(store.format("file:///bg/file0.ts")).toContain("file0-updated");
+
+ expect(store.hasReceivedPush("file:///bg/file1.ts")).toBe(false);
+ expect(store.format("file:///bg/file1.ts")).toBe("");
+ });
});