pi-lens 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +4 -10
- package/clients/__tests__/file-time.test.js +216 -0
- package/clients/__tests__/format-service.test.js +245 -0
- package/clients/__tests__/formatters.test.js +271 -0
- package/clients/agent-behavior-client.test.js +94 -0
- package/clients/biome-client.test.js +144 -0
- package/clients/cache-manager.test.js +197 -0
- package/clients/complexity-client.test.js +234 -0
- package/clients/dependency-checker.test.js +60 -0
- package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
- package/clients/dispatch/__tests__/runner-registration.test.js +234 -0
- package/clients/dispatch/__tests__/runner-registration.test.ts +2 -2
- package/clients/dispatch/dispatcher.edge.test.js +82 -0
- package/clients/dispatch/dispatcher.format.test.js +46 -0
- package/clients/dispatch/dispatcher.inline.test.js +74 -0
- package/clients/dispatch/dispatcher.test.js +116 -0
- package/clients/dispatch/runners/architect.test.js +138 -0
- package/clients/dispatch/runners/ast-grep-napi.test.js +106 -0
- package/clients/dispatch/runners/lsp.js +42 -5
- package/clients/dispatch/runners/oxlint.test.js +230 -0
- package/clients/dispatch/runners/pyright.test.js +98 -0
- package/clients/dispatch/runners/python-slop.test.js +203 -0
- package/clients/dispatch/runners/scan_codebase.test.js +89 -0
- package/clients/dispatch/runners/shellcheck.test.js +98 -0
- package/clients/dispatch/runners/spellcheck.test.js +158 -0
- package/clients/dispatch/utils/format-utils.js +1 -6
- package/clients/dispatch/utils/format-utils.ts +1 -6
- package/clients/dogfood.test.js +201 -0
- package/clients/file-kinds.test.js +169 -0
- package/clients/formatters.js +1 -1
- package/clients/go-client.test.js +127 -0
- package/clients/jscpd-client.test.js +127 -0
- package/clients/knip-client.test.js +112 -0
- package/clients/lsp/__tests__/client.test.js +310 -0
- package/clients/lsp/__tests__/client.test.ts +1 -46
- package/clients/lsp/__tests__/config.test.js +167 -0
- package/clients/lsp/__tests__/error-recovery.test.js +213 -0
- package/clients/lsp/__tests__/integration.test.js +127 -0
- package/clients/lsp/__tests__/launch.test.js +313 -0
- package/clients/lsp/__tests__/server.test.js +259 -0
- package/clients/lsp/__tests__/service.test.js +435 -0
- package/clients/lsp/client.js +32 -44
- package/clients/lsp/client.ts +36 -45
- package/clients/lsp/server.js +27 -2
- package/clients/metrics-client.test.js +141 -0
- package/clients/ruff-client.test.js +132 -0
- package/clients/rust-client.test.js +108 -0
- package/clients/sanitize.test.js +177 -0
- package/clients/secrets-scanner.test.js +100 -0
- package/clients/test-runner-client.test.js +192 -0
- package/clients/todo-scanner.test.js +301 -0
- package/clients/type-coverage-client.test.js +105 -0
- package/clients/typescript-client.codefix.test.js +157 -0
- package/clients/typescript-client.test.js +105 -0
- package/commands/rate.test.js +119 -0
- package/index.ts +66 -72
- package/package.json +1 -1
- package/clients/bus/bus.js +0 -191
- package/clients/bus/bus.ts +0 -251
- package/clients/bus/events.js +0 -214
- package/clients/bus/events.ts +0 -279
- package/clients/bus/index.js +0 -8
- package/clients/bus/index.ts +0 -9
- package/clients/bus/integration.js +0 -158
- package/clients/bus/integration.ts +0 -227
- package/clients/dispatch/bus-dispatcher.js +0 -178
- package/clients/dispatch/bus-dispatcher.ts +0 -258
- package/clients/services/__tests__/effect-integration.test.ts +0 -111
- package/clients/services/effect-integration.js +0 -198
- package/clients/services/effect-integration.ts +0 -276
- package/clients/services/index.js +0 -7
- package/clients/services/index.ts +0 -8
- package/clients/services/runner-service.js +0 -134
- package/clients/services/runner-service.ts +0 -225
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatter Detection Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests smart detection of formatters based on:
|
|
5
|
+
* - Config files (biome.json, .prettierrc, etc.)
|
|
6
|
+
* - Dependencies (package.json, requirements.txt)
|
|
7
|
+
* - Binary availability
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { biomeFormatter, prettierFormatter, ruffFormatter, blackFormatter, gofmtFormatter, rustfmtFormatter, getFormattersForFile, clearFormatterCache, formatFile, } from "../formatters.js";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
import { dirname } from "path";
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
const TEST_DIR = path.join(__dirname, "..", "..", "test-formatters");
|
|
18
|
+
describe("Formatter Detection", () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
clearFormatterCache();
|
|
21
|
+
if (fs.existsSync(TEST_DIR)) {
|
|
22
|
+
fs.rmSync(TEST_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
fs.mkdirSync(TEST_DIR, { recursive: true });
|
|
25
|
+
});
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
clearFormatterCache();
|
|
28
|
+
if (fs.existsSync(TEST_DIR)) {
|
|
29
|
+
fs.rmSync(TEST_DIR, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
describe("biomeFormatter.detect()", () => {
|
|
33
|
+
it("should detect biome.json config file", async () => {
|
|
34
|
+
fs.writeFileSync(path.join(TEST_DIR, "biome.json"), '{"formatter": {}}');
|
|
35
|
+
const detected = await biomeFormatter.detect(TEST_DIR);
|
|
36
|
+
expect(detected).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it("should detect biome.jsonc config file", async () => {
|
|
39
|
+
fs.writeFileSync(path.join(TEST_DIR, "biome.jsonc"), "{}");
|
|
40
|
+
const detected = await biomeFormatter.detect(TEST_DIR);
|
|
41
|
+
expect(detected).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it("should detect @biomejs/biome in devDependencies", async () => {
|
|
44
|
+
fs.writeFileSync(path.join(TEST_DIR, "package.json"), JSON.stringify({ devDependencies: { "@biomejs/biome": "^1.0.0" } }));
|
|
45
|
+
const detected = await biomeFormatter.detect(TEST_DIR);
|
|
46
|
+
expect(detected).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
it("should return false when no biome config", async () => {
|
|
49
|
+
const detected = await biomeFormatter.detect(TEST_DIR);
|
|
50
|
+
expect(detected).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it("should find biome.json in parent directory", async () => {
|
|
53
|
+
const subDir = path.join(TEST_DIR, "src", "components");
|
|
54
|
+
fs.mkdirSync(subDir, { recursive: true });
|
|
55
|
+
fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
|
|
56
|
+
const detected = await biomeFormatter.detect(subDir);
|
|
57
|
+
expect(detected).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe("prettierFormatter.detect()", () => {
|
|
61
|
+
it("should detect .prettierrc config file", async () => {
|
|
62
|
+
fs.writeFileSync(path.join(TEST_DIR, ".prettierrc"), "{}");
|
|
63
|
+
const detected = await prettierFormatter.detect(TEST_DIR);
|
|
64
|
+
expect(detected).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
it("should detect prettier in devDependencies", async () => {
|
|
67
|
+
fs.writeFileSync(path.join(TEST_DIR, "package.json"), JSON.stringify({ devDependencies: { prettier: "^3.0.0" } }));
|
|
68
|
+
const detected = await prettierFormatter.detect(TEST_DIR);
|
|
69
|
+
expect(detected).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it("should detect prettier field in package.json", async () => {
|
|
72
|
+
fs.writeFileSync(path.join(TEST_DIR, "package.json"), JSON.stringify({ prettier: { semi: false } }));
|
|
73
|
+
const detected = await prettierFormatter.detect(TEST_DIR);
|
|
74
|
+
expect(detected).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
it("should return false when no prettier config", async () => {
|
|
77
|
+
const detected = await prettierFormatter.detect(TEST_DIR);
|
|
78
|
+
expect(detected).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe("ruffFormatter.detect()", () => {
|
|
82
|
+
it("should detect [tool.ruff] in pyproject.toml", async () => {
|
|
83
|
+
fs.writeFileSync(path.join(TEST_DIR, "pyproject.toml"), "[tool.ruff]\nline-length = 100");
|
|
84
|
+
const detected = await ruffFormatter.detect(TEST_DIR);
|
|
85
|
+
expect(detected).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
it("should detect ruff.toml config file", async () => {
|
|
88
|
+
fs.writeFileSync(path.join(TEST_DIR, "ruff.toml"), "line-length = 100");
|
|
89
|
+
const detected = await ruffFormatter.detect(TEST_DIR);
|
|
90
|
+
expect(detected).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
it("should detect .ruff.toml config file", async () => {
|
|
93
|
+
fs.writeFileSync(path.join(TEST_DIR, ".ruff.toml"), "line-length = 100");
|
|
94
|
+
const detected = await ruffFormatter.detect(TEST_DIR);
|
|
95
|
+
expect(detected).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
it("should detect ruff in requirements.txt", async () => {
|
|
98
|
+
fs.writeFileSync(path.join(TEST_DIR, "requirements.txt"), "ruff==0.1.0\n");
|
|
99
|
+
const detected = await ruffFormatter.detect(TEST_DIR);
|
|
100
|
+
expect(detected).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
it("should detect ruff even if [tool.black] exists (no preference logic)", async () => {
|
|
103
|
+
// Create pyproject.toml with black config
|
|
104
|
+
fs.writeFileSync(path.join(TEST_DIR, "pyproject.toml"), "[tool.black]\nline-length = 100");
|
|
105
|
+
// Also write requirements with ruff
|
|
106
|
+
fs.writeFileSync(path.join(TEST_DIR, "requirements.txt"), "ruff\n");
|
|
107
|
+
// The current implementation doesn't have preference logic
|
|
108
|
+
// Both black and ruff would be detected if their configs exist
|
|
109
|
+
// This is intentional - users can disable one if needed
|
|
110
|
+
const detected = await blackFormatter.detect(TEST_DIR);
|
|
111
|
+
expect(detected).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe("blackFormatter.detect()", () => {
|
|
115
|
+
it("should detect [tool.black] in pyproject.toml", async () => {
|
|
116
|
+
fs.writeFileSync(path.join(TEST_DIR, "pyproject.toml"), "[tool.black]\nline-length = 100");
|
|
117
|
+
const detected = await blackFormatter.detect(TEST_DIR);
|
|
118
|
+
expect(detected).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
it("should detect black in requirements.txt", async () => {
|
|
121
|
+
fs.writeFileSync(path.join(TEST_DIR, "requirements.txt"), "black==23.0.0\n");
|
|
122
|
+
const detected = await blackFormatter.detect(TEST_DIR);
|
|
123
|
+
expect(detected).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
it("should return false when no black config", async () => {
|
|
126
|
+
const detected = await blackFormatter.detect(TEST_DIR);
|
|
127
|
+
expect(detected).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe("gofmtFormatter.detect()", () => {
|
|
131
|
+
it("should detect gofmt binary availability", async () => {
|
|
132
|
+
// This test depends on whether gofmt is installed
|
|
133
|
+
// We can't reliably test this in CI, but we can verify the logic
|
|
134
|
+
const detected = await gofmtFormatter.detect(TEST_DIR);
|
|
135
|
+
// Should return boolean based on binary availability
|
|
136
|
+
expect(typeof detected).toBe("boolean");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe("rustfmtFormatter.detect()", () => {
|
|
140
|
+
it("should detect rustfmt binary availability", async () => {
|
|
141
|
+
const detected = await rustfmtFormatter.detect(TEST_DIR);
|
|
142
|
+
expect(typeof detected).toBe("boolean");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe("getFormattersForFile()", () => {
|
|
146
|
+
it("should return formatters for TypeScript file with biome config", async () => {
|
|
147
|
+
fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
|
|
148
|
+
const tsFile = path.join(TEST_DIR, "test.ts");
|
|
149
|
+
const formatters = await getFormattersForFile(tsFile, TEST_DIR);
|
|
150
|
+
expect(formatters.map(f => f.name)).toContain("biome");
|
|
151
|
+
});
|
|
152
|
+
it("should return formatters for TypeScript file with prettier", async () => {
|
|
153
|
+
fs.writeFileSync(path.join(TEST_DIR, "package.json"), JSON.stringify({ devDependencies: { prettier: "^3.0.0" } }));
|
|
154
|
+
const tsFile = path.join(TEST_DIR, "test.ts");
|
|
155
|
+
const formatters = await getFormattersForFile(tsFile, TEST_DIR);
|
|
156
|
+
expect(formatters.map(f => f.name)).toContain("prettier");
|
|
157
|
+
});
|
|
158
|
+
it("should return multiple formatters for TypeScript file", async () => {
|
|
159
|
+
fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
|
|
160
|
+
fs.writeFileSync(path.join(TEST_DIR, "package.json"), JSON.stringify({ devDependencies: { prettier: "^3.0.0" } }));
|
|
161
|
+
const tsFile = path.join(TEST_DIR, "test.ts");
|
|
162
|
+
const formatters = await getFormattersForFile(tsFile, TEST_DIR);
|
|
163
|
+
// Both biome and prettier should be returned
|
|
164
|
+
expect(formatters.length).toBeGreaterThanOrEqual(1);
|
|
165
|
+
const names = formatters.map(f => f.name);
|
|
166
|
+
expect(names).toContain("biome");
|
|
167
|
+
});
|
|
168
|
+
it("should return ruff for Python file with pyproject.toml", async () => {
|
|
169
|
+
fs.writeFileSync(path.join(TEST_DIR, "pyproject.toml"), "[tool.ruff]\nline-length = 100");
|
|
170
|
+
const pyFile = path.join(TEST_DIR, "test.py");
|
|
171
|
+
const formatters = await getFormattersForFile(pyFile, TEST_DIR);
|
|
172
|
+
expect(formatters.map(f => f.name)).toContain("ruff");
|
|
173
|
+
});
|
|
174
|
+
it("should return black for Python file with black config", async () => {
|
|
175
|
+
fs.writeFileSync(path.join(TEST_DIR, "pyproject.toml"), "[tool.black]\nline-length = 100");
|
|
176
|
+
const pyFile = path.join(TEST_DIR, "test.py");
|
|
177
|
+
const formatters = await getFormattersForFile(pyFile, TEST_DIR);
|
|
178
|
+
// Should prefer black over ruff
|
|
179
|
+
expect(formatters.map(f => f.name)).toContain("black");
|
|
180
|
+
});
|
|
181
|
+
it("should return empty array for unsupported extensions", async () => {
|
|
182
|
+
const txtFile = path.join(TEST_DIR, "test.txt");
|
|
183
|
+
fs.writeFileSync(txtFile, "content");
|
|
184
|
+
const formatters = await getFormattersForFile(txtFile, TEST_DIR);
|
|
185
|
+
expect(formatters).toEqual([]);
|
|
186
|
+
});
|
|
187
|
+
it("should cache detection results", async () => {
|
|
188
|
+
fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
|
|
189
|
+
const tsFile = path.join(TEST_DIR, "test.ts");
|
|
190
|
+
// First call
|
|
191
|
+
await getFormattersForFile(tsFile, TEST_DIR);
|
|
192
|
+
// Second call should use cache
|
|
193
|
+
const formatters = await getFormattersForFile(tsFile, TEST_DIR);
|
|
194
|
+
expect(formatters.map(f => f.name)).toContain("biome");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
describe("clearFormatterCache()", () => {
|
|
198
|
+
it("should clear cached detection results", async () => {
|
|
199
|
+
fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
|
|
200
|
+
const tsFile = path.join(TEST_DIR, "test.ts");
|
|
201
|
+
// First detection
|
|
202
|
+
await getFormattersForFile(tsFile, TEST_DIR);
|
|
203
|
+
// Clear cache
|
|
204
|
+
clearFormatterCache();
|
|
205
|
+
// Delete config
|
|
206
|
+
fs.rmSync(path.join(TEST_DIR, "biome.json"));
|
|
207
|
+
// Should re-detect (now without biome)
|
|
208
|
+
const formatters = await getFormattersForFile(tsFile, TEST_DIR);
|
|
209
|
+
expect(formatters.map(f => f.name)).not.toContain("biome");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
describe("formatFile()", () => {
|
|
213
|
+
it("should format file and report changes", async () => {
|
|
214
|
+
// Create a simple test - we'll skip actual formatter execution
|
|
215
|
+
// because we can't guarantee formatters are installed
|
|
216
|
+
const testFile = path.join(TEST_DIR, "test.txt");
|
|
217
|
+
fs.writeFileSync(testFile, "unchanged");
|
|
218
|
+
const mockFormatter = {
|
|
219
|
+
name: "mock",
|
|
220
|
+
command: ["echo", "$FILE"],
|
|
221
|
+
extensions: [".txt"],
|
|
222
|
+
detect: async () => true,
|
|
223
|
+
};
|
|
224
|
+
const result = await formatFile(testFile, mockFormatter);
|
|
225
|
+
// echo command should succeed but not change file
|
|
226
|
+
expect(result.success).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
it("should handle formatter execution with valid command", async () => {
|
|
229
|
+
const testFile = path.join(TEST_DIR, "valid.txt");
|
|
230
|
+
fs.writeFileSync(testFile, "content");
|
|
231
|
+
// Use a valid command that succeeds but doesn't modify file
|
|
232
|
+
const mockFormatter = {
|
|
233
|
+
name: "valid",
|
|
234
|
+
command: process.platform === "win32" ? ["cmd", "/c", "echo", "$FILE"] : ["echo", "$FILE"],
|
|
235
|
+
extensions: [".txt"],
|
|
236
|
+
detect: async () => true,
|
|
237
|
+
};
|
|
238
|
+
const result = await formatFile(testFile, mockFormatter);
|
|
239
|
+
// Should not throw, completes with success
|
|
240
|
+
expect(result).toBeDefined();
|
|
241
|
+
expect(typeof result.success).toBe("boolean");
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe("Formatter extensions", () => {
|
|
245
|
+
it("biome should handle TS/JS/JSON/CSS/Vue/Svelte", () => {
|
|
246
|
+
expect(biomeFormatter.extensions).toContain(".ts");
|
|
247
|
+
expect(biomeFormatter.extensions).toContain(".tsx");
|
|
248
|
+
expect(biomeFormatter.extensions).toContain(".js");
|
|
249
|
+
expect(biomeFormatter.extensions).toContain(".json");
|
|
250
|
+
expect(biomeFormatter.extensions).toContain(".css");
|
|
251
|
+
expect(biomeFormatter.extensions).toContain(".vue");
|
|
252
|
+
expect(biomeFormatter.extensions).toContain(".svelte");
|
|
253
|
+
});
|
|
254
|
+
it("prettier should handle Markdown and YAML", () => {
|
|
255
|
+
expect(prettierFormatter.extensions).toContain(".md");
|
|
256
|
+
expect(prettierFormatter.extensions).toContain(".mdx");
|
|
257
|
+
expect(prettierFormatter.extensions).toContain(".yaml");
|
|
258
|
+
expect(prettierFormatter.extensions).toContain(".yml");
|
|
259
|
+
});
|
|
260
|
+
it("ruff should handle Python files", () => {
|
|
261
|
+
expect(ruffFormatter.extensions).toContain(".py");
|
|
262
|
+
expect(ruffFormatter.extensions).toContain(".pyi");
|
|
263
|
+
});
|
|
264
|
+
it("gofmt should handle Go files", () => {
|
|
265
|
+
expect(gofmtFormatter.extensions).toContain(".go");
|
|
266
|
+
});
|
|
267
|
+
it("rustfmt should handle Rust files", () => {
|
|
268
|
+
expect(rustfmtFormatter.extensions).toContain(".rs");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { AgentBehaviorClient } from "./agent-behavior-client.js";
|
|
3
|
+
describe("AgentBehaviorClient", () => {
|
|
4
|
+
let client;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
client = new AgentBehaviorClient();
|
|
7
|
+
client.reset();
|
|
8
|
+
});
|
|
9
|
+
describe("blind write detection", () => {
|
|
10
|
+
it("should NOT warn when read precedes write", () => {
|
|
11
|
+
client.recordToolCall("read", "src/file.ts");
|
|
12
|
+
client.recordToolCall("edit", "src/file.ts");
|
|
13
|
+
const warnings = client.recordToolCall("write", "src/file.ts");
|
|
14
|
+
expect(warnings).toHaveLength(0);
|
|
15
|
+
});
|
|
16
|
+
it("should warn when multiple writes happen without reads", () => {
|
|
17
|
+
// First write is OK (no history)
|
|
18
|
+
client.recordToolCall("write", "src/file1.ts");
|
|
19
|
+
// Second write - still in window, accumulates
|
|
20
|
+
client.recordToolCall("edit", "src/file2.ts");
|
|
21
|
+
// Third write without any read - now we have a pattern
|
|
22
|
+
const warnings = client.recordToolCall("edit", "src/file3.ts");
|
|
23
|
+
expect(warnings).toHaveLength(1);
|
|
24
|
+
expect(warnings[0].type).toBe("blind-write");
|
|
25
|
+
});
|
|
26
|
+
it("should not warn for single write with no history", () => {
|
|
27
|
+
const warnings = client.recordToolCall("write", "src/file.ts");
|
|
28
|
+
expect(warnings).toHaveLength(0);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe("thrashing detection", () => {
|
|
32
|
+
it("should warn after 3 consecutive identical tool calls", () => {
|
|
33
|
+
client.recordToolCall("bash", undefined);
|
|
34
|
+
// Second call - no warning yet
|
|
35
|
+
let warnings = client.recordToolCall("bash", undefined);
|
|
36
|
+
expect(warnings).toHaveLength(0);
|
|
37
|
+
// Third consecutive - should warn
|
|
38
|
+
warnings = client.recordToolCall("bash", undefined);
|
|
39
|
+
expect(warnings).toHaveLength(1);
|
|
40
|
+
expect(warnings[0].type).toBe("thrashing");
|
|
41
|
+
expect(warnings[0].details.callCount).toBe(3);
|
|
42
|
+
});
|
|
43
|
+
it("should NOT warn for different tool calls", () => {
|
|
44
|
+
client.recordToolCall("read", "src/file.ts");
|
|
45
|
+
client.recordToolCall("bash", "npm test");
|
|
46
|
+
const warnings = client.recordToolCall("edit", "src/file.ts");
|
|
47
|
+
expect(warnings).toHaveLength(0);
|
|
48
|
+
});
|
|
49
|
+
it("should reset count when tool changes", () => {
|
|
50
|
+
client.recordToolCall("bash", undefined);
|
|
51
|
+
client.recordToolCall("bash", undefined);
|
|
52
|
+
// Different tool resets the count
|
|
53
|
+
client.recordToolCall("read", "src/file.ts");
|
|
54
|
+
// Now start new consecutive sequence
|
|
55
|
+
client.recordToolCall("bash", undefined);
|
|
56
|
+
const warnings = client.recordToolCall("bash", undefined);
|
|
57
|
+
expect(warnings).toHaveLength(0); // Only 2 consecutive, not 3
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe("edit counting", () => {
|
|
61
|
+
it("should track edit count per file", () => {
|
|
62
|
+
client.recordToolCall("edit", "src/a.ts");
|
|
63
|
+
client.recordToolCall("edit", "src/a.ts");
|
|
64
|
+
client.recordToolCall("edit", "src/b.ts");
|
|
65
|
+
expect(client.getEditCount("src/a.ts")).toBe(2);
|
|
66
|
+
expect(client.getEditCount("src/b.ts")).toBe(1);
|
|
67
|
+
expect(client.getEditCount("src/c.ts")).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("formatWarnings", () => {
|
|
71
|
+
it("should format multiple warnings", () => {
|
|
72
|
+
const warnings = [
|
|
73
|
+
{
|
|
74
|
+
type: "blind-write",
|
|
75
|
+
message: "⚠ BLIND WRITE — editing file",
|
|
76
|
+
severity: "warning",
|
|
77
|
+
details: {},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "thrashing",
|
|
81
|
+
message: "🔴 THRASHING — 3 consecutive calls",
|
|
82
|
+
severity: "error",
|
|
83
|
+
details: {},
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
const formatted = client.formatWarnings(warnings);
|
|
87
|
+
expect(formatted).toContain("BLIND WRITE");
|
|
88
|
+
expect(formatted).toContain("THRASHING");
|
|
89
|
+
});
|
|
90
|
+
it("should return empty string for no warnings", () => {
|
|
91
|
+
expect(client.formatWarnings([])).toBe("");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { BiomeClient } from "./biome-client.js";
|
|
4
|
+
import { createTempFile, setupTestEnvironment } from "./test-utils.js";
|
|
5
|
+
describe("BiomeClient", () => {
|
|
6
|
+
let client;
|
|
7
|
+
let tmpDir;
|
|
8
|
+
let cleanup;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
client = new BiomeClient();
|
|
11
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-biome-test-"));
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
cleanup();
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
cleanup();
|
|
18
|
+
});
|
|
19
|
+
describe("isSupportedFile", () => {
|
|
20
|
+
it("should support JS/TS files", () => {
|
|
21
|
+
expect(client.isSupportedFile("test.js")).toBe(true);
|
|
22
|
+
expect(client.isSupportedFile("test.jsx")).toBe(true);
|
|
23
|
+
expect(client.isSupportedFile("test.ts")).toBe(true);
|
|
24
|
+
expect(client.isSupportedFile("test.tsx")).toBe(true);
|
|
25
|
+
expect(client.isSupportedFile("test.mjs")).toBe(true);
|
|
26
|
+
expect(client.isSupportedFile("test.cjs")).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
it("should support CSS and JSON", () => {
|
|
29
|
+
expect(client.isSupportedFile("style.css")).toBe(true);
|
|
30
|
+
expect(client.isSupportedFile("config.json")).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
it("should not support unsupported files", () => {
|
|
33
|
+
expect(client.isSupportedFile("test.py")).toBe(false);
|
|
34
|
+
expect(client.isSupportedFile("test.md")).toBe(false);
|
|
35
|
+
expect(client.isSupportedFile("test.txt")).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe("isAvailable", () => {
|
|
39
|
+
it("should check biome availability", () => {
|
|
40
|
+
const available = client.isAvailable();
|
|
41
|
+
// Just verify it doesn't throw - actual availability depends on environment
|
|
42
|
+
expect(typeof available).toBe("boolean");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("checkFile", () => {
|
|
46
|
+
it("should return empty array for non-existent files", () => {
|
|
47
|
+
if (!client.isAvailable())
|
|
48
|
+
return;
|
|
49
|
+
const result = client.checkFile("/nonexistent/file.ts");
|
|
50
|
+
expect(result).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
it("should return array of diagnostics for TS files", {
|
|
53
|
+
timeout: 15000,
|
|
54
|
+
}, () => {
|
|
55
|
+
if (!client.isAvailable())
|
|
56
|
+
return;
|
|
57
|
+
const content = `
|
|
58
|
+
const x: number = "string";
|
|
59
|
+
`;
|
|
60
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
61
|
+
const result = client.checkFile(filePath);
|
|
62
|
+
// Should return an array (may or may not have issues)
|
|
63
|
+
expect(Array.isArray(result)).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe("formatDiagnostics", () => {
|
|
67
|
+
it("should format diagnostics for display", () => {
|
|
68
|
+
const diags = [
|
|
69
|
+
{
|
|
70
|
+
line: 1,
|
|
71
|
+
column: 0,
|
|
72
|
+
endLine: 1,
|
|
73
|
+
endColumn: 10,
|
|
74
|
+
severity: "error",
|
|
75
|
+
message: "Unexpected var",
|
|
76
|
+
rule: "noVar",
|
|
77
|
+
category: "lint",
|
|
78
|
+
fixable: true,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
const formatted = client.formatDiagnostics(diags, "test.ts");
|
|
82
|
+
expect(formatted).toContain("Biome");
|
|
83
|
+
expect(formatted).toContain("1 issue");
|
|
84
|
+
expect(formatted).toContain("noVar");
|
|
85
|
+
});
|
|
86
|
+
it("should show fixable count", () => {
|
|
87
|
+
const diags = [
|
|
88
|
+
{
|
|
89
|
+
line: 1,
|
|
90
|
+
column: 0,
|
|
91
|
+
endLine: 1,
|
|
92
|
+
endColumn: 10,
|
|
93
|
+
severity: "error",
|
|
94
|
+
message: "Error 1",
|
|
95
|
+
rule: "rule1",
|
|
96
|
+
category: "lint",
|
|
97
|
+
fixable: true,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
line: 2,
|
|
101
|
+
column: 0,
|
|
102
|
+
endLine: 2,
|
|
103
|
+
endColumn: 10,
|
|
104
|
+
severity: "warning",
|
|
105
|
+
message: "Warning 1",
|
|
106
|
+
rule: "rule2",
|
|
107
|
+
category: "lint",
|
|
108
|
+
fixable: false,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
const formatted = client.formatDiagnostics(diags, "test.ts");
|
|
112
|
+
expect(formatted).toContain("1 fixable");
|
|
113
|
+
});
|
|
114
|
+
it("should truncate long diagnostic lists", () => {
|
|
115
|
+
const diags = Array.from({ length: 20 }, (_, i) => ({
|
|
116
|
+
line: i + 1,
|
|
117
|
+
column: 0,
|
|
118
|
+
endLine: i + 1,
|
|
119
|
+
endColumn: 10,
|
|
120
|
+
severity: "warning",
|
|
121
|
+
message: `Warning ${i}`,
|
|
122
|
+
rule: `rule${i}`,
|
|
123
|
+
category: "lint",
|
|
124
|
+
fixable: false,
|
|
125
|
+
}));
|
|
126
|
+
const formatted = client.formatDiagnostics(diags, "test.ts");
|
|
127
|
+
expect(formatted).toContain("...");
|
|
128
|
+
expect(formatted).toContain("5 more");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe("formatFile", () => {
|
|
132
|
+
it("should format a file", () => {
|
|
133
|
+
if (!client.isAvailable())
|
|
134
|
+
return;
|
|
135
|
+
const content = `const x={a:1,b:2}`;
|
|
136
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
137
|
+
const result = client.formatFile(filePath);
|
|
138
|
+
expect(result.success).toBe(true);
|
|
139
|
+
// Check if file was formatted (should have spaces)
|
|
140
|
+
const formatted = fs.readFileSync(filePath, "utf-8");
|
|
141
|
+
expect(formatted).toContain(": "); // Should have spaces after colons
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|