pi-lens 3.2.0 → 3.3.1
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 +20 -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/launch.js +11 -6
- package/clients/lsp/launch.ts +11 -6
- 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
package/clients/lsp/client.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - Request/response handling
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { EventEmitter } from "node:events";
|
|
11
12
|
import { pathToFileURL } from "node:url";
|
|
12
13
|
import type { MessageConnection } from "vscode-jsonrpc";
|
|
13
14
|
import {
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
StreamMessageReader,
|
|
16
17
|
StreamMessageWriter,
|
|
17
18
|
} from "vscode-jsonrpc/node.js";
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
import type { LSPProcess } from "./launch.js";
|
|
20
21
|
import { normalizeMapKey, uriToPath } from "./path-utils.js";
|
|
21
22
|
|
|
@@ -104,7 +105,7 @@ export interface LSPClientInfo {
|
|
|
104
105
|
|
|
105
106
|
// --- Constants ---
|
|
106
107
|
|
|
107
|
-
const DIAGNOSTICS_DEBOUNCE_MS = 150;
|
|
108
|
+
const DIAGNOSTICS_DEBOUNCE_MS = 150; // ms — waits for follow-up semantic diagnostics
|
|
108
109
|
const INITIALIZE_TIMEOUT_MS = 120_000; // 2 minutes (was 45s) - allows time for npx to download packages
|
|
109
110
|
|
|
110
111
|
// --- Client Factory ---
|
|
@@ -127,6 +128,12 @@ export async function createLSPClient(options: {
|
|
|
127
128
|
const diagnostics = new Map<string, LSPDiagnostic[]>();
|
|
128
129
|
const pendingDiagnostics = new Map<string, ReturnType<typeof setTimeout>>();
|
|
129
130
|
|
|
131
|
+
// Local event emitter — signals waitForDiagnostics when new diagnostics arrive.
|
|
132
|
+
// Scoped to this client instance; replaces global bus pub/sub.
|
|
133
|
+
// setMaxListeners guards against Node.js warning for concurrent waitForDiagnostics calls.
|
|
134
|
+
const diagnosticEmitter = new EventEmitter();
|
|
135
|
+
diagnosticEmitter.setMaxListeners(50);
|
|
136
|
+
|
|
130
137
|
// Handle incoming diagnostics with debouncing
|
|
131
138
|
connection.onNotification(
|
|
132
139
|
"textDocument/publishDiagnostics",
|
|
@@ -142,31 +149,8 @@ export async function createLSPClient(options: {
|
|
|
142
149
|
diagnostics.set(filePath, newDiags);
|
|
143
150
|
pendingDiagnostics.delete(filePath);
|
|
144
151
|
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
const validDiags = newDiags.filter(
|
|
148
|
-
(d) => d.range?.start?.line !== undefined,
|
|
149
|
-
);
|
|
150
|
-
DiagnosticFound.publish({
|
|
151
|
-
runnerId: serverId,
|
|
152
|
-
filePath,
|
|
153
|
-
diagnostics: validDiags.map((d) => ({
|
|
154
|
-
id: `${serverId}:${d.code ?? "unknown"}:${d.range.start.line}`,
|
|
155
|
-
message: d.message,
|
|
156
|
-
filePath,
|
|
157
|
-
line: d.range.start.line + 1,
|
|
158
|
-
column: d.range.start.character + 1,
|
|
159
|
-
severity: severityFromNumber(d.severity),
|
|
160
|
-
semantic:
|
|
161
|
-
d.severity === 1
|
|
162
|
-
? "blocking"
|
|
163
|
-
: d.severity === 2
|
|
164
|
-
? "warning"
|
|
165
|
-
: "silent",
|
|
166
|
-
tool: serverId,
|
|
167
|
-
})),
|
|
168
|
-
durationMs: 0,
|
|
169
|
-
});
|
|
152
|
+
// Signal any active waitForDiagnostics calls for this file.
|
|
153
|
+
diagnosticEmitter.emit("diagnostics", filePath);
|
|
170
154
|
}, DIAGNOSTICS_DEBOUNCE_MS);
|
|
171
155
|
|
|
172
156
|
pendingDiagnostics.set(filePath, timer);
|
|
@@ -301,31 +285,35 @@ export async function createLSPClient(options: {
|
|
|
301
285
|
|
|
302
286
|
async waitForDiagnostics(filePath, timeoutMs = 10000) {
|
|
303
287
|
const normalizedPath = normalizeMapKey(filePath);
|
|
288
|
+
|
|
289
|
+
// Fast path: diagnostics already available
|
|
304
290
|
if (diagnostics.has(normalizedPath)) return;
|
|
305
291
|
|
|
306
|
-
|
|
307
|
-
return new Promise((resolve) => {
|
|
292
|
+
return new Promise<void>((resolve) => {
|
|
308
293
|
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
309
294
|
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
});
|
|
295
|
+
// Listen on the local emitter for this client's diagnostic notifications.
|
|
296
|
+
// No runnerId filter needed — this emitter is scoped to this client instance.
|
|
297
|
+
const onDiagnostics = (fp: string) => {
|
|
298
|
+
if (normalizeMapKey(fp) !== normalizedPath) return;
|
|
299
|
+
|
|
300
|
+
// Debounce: reset on each event to catch follow-up semantic diagnostics
|
|
301
|
+
// (LSP often sends syntax diagnostics first, semantic ones shortly after).
|
|
302
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
303
|
+
debounceTimer = setTimeout(() => {
|
|
304
|
+
diagnosticEmitter.off("diagnostics", onDiagnostics);
|
|
305
|
+
clearTimeout(timeout);
|
|
306
|
+
resolve();
|
|
307
|
+
}, DIAGNOSTICS_DEBOUNCE_MS);
|
|
308
|
+
};
|
|
325
309
|
|
|
310
|
+
diagnosticEmitter.on("diagnostics", onDiagnostics);
|
|
311
|
+
|
|
312
|
+
// Timeout fallback: resolve even if no diagnostics arrive
|
|
313
|
+
// (some files have no errors, or the server may be slow)
|
|
326
314
|
const timeout = setTimeout(() => {
|
|
327
315
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
328
|
-
|
|
316
|
+
diagnosticEmitter.off("diagnostics", onDiagnostics);
|
|
329
317
|
resolve();
|
|
330
318
|
}, timeoutMs);
|
|
331
319
|
});
|
|
@@ -421,6 +409,9 @@ export async function createLSPClient(options: {
|
|
|
421
409
|
}
|
|
422
410
|
pendingDiagnostics.clear();
|
|
423
411
|
|
|
412
|
+
// Remove all diagnostic listeners (cancels any in-flight waitForDiagnostics)
|
|
413
|
+
diagnosticEmitter.removeAllListeners();
|
|
414
|
+
|
|
424
415
|
// Graceful shutdown
|
|
425
416
|
try {
|
|
426
417
|
await connection.sendRequest("shutdown");
|
package/clients/lsp/launch.js
CHANGED
|
@@ -149,8 +149,11 @@ export async function launchLSP(command, args = [], options = {}) {
|
|
|
149
149
|
if (npmGlobalPath) {
|
|
150
150
|
spawnCommand = npmGlobalPath;
|
|
151
151
|
// Recompute needsShell for npm global path
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
needsShell =
|
|
153
|
+
isWindows &&
|
|
154
|
+
(spawnCommand.includes(" ") ||
|
|
155
|
+
/\.(cmd|bat)$/i.test(spawnCommand) ||
|
|
156
|
+
!/\.(exe|cmd|bat)$/i.test(spawnCommand));
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
159
|
let proc;
|
|
@@ -166,8 +169,10 @@ export async function launchLSP(command, args = [], options = {}) {
|
|
|
166
169
|
if (npmGlobalPath && npmGlobalPath !== spawnCommand) {
|
|
167
170
|
console.error(`[lsp] Trying npm global: ${npmGlobalPath}`);
|
|
168
171
|
// Recompute needsShell for npm global path
|
|
169
|
-
const
|
|
170
|
-
|
|
172
|
+
const needsShellGlobal = isWindows &&
|
|
173
|
+
(npmGlobalPath.includes(" ") ||
|
|
174
|
+
/\.(cmd|bat)$/i.test(npmGlobalPath) ||
|
|
175
|
+
!/\.(exe|cmd|bat)$/i.test(npmGlobalPath));
|
|
171
176
|
proc = trySpawn(npmGlobalPath, args, cwd, env, needsShellGlobal);
|
|
172
177
|
}
|
|
173
178
|
else {
|
|
@@ -192,7 +197,7 @@ export async function launchLSP(command, args = [], options = {}) {
|
|
|
192
197
|
let settled = false;
|
|
193
198
|
// Attach error handler that can reject for immediate errors
|
|
194
199
|
proc.on("error", (err) => {
|
|
195
|
-
if (!settled && err.code === "ENOENT") {
|
|
200
|
+
if (!settled && (err.code === "ENOENT" || err.code === "EINVAL")) {
|
|
196
201
|
settled = true;
|
|
197
202
|
reject(new Error(`LSP server binary not found: ${command}. ` +
|
|
198
203
|
`Install it or check your PATH.`));
|
|
@@ -254,7 +259,7 @@ export async function launchViaPackageManager(packageName, args = [], options =
|
|
|
254
259
|
await new Promise((resolve, reject) => {
|
|
255
260
|
let settled = false;
|
|
256
261
|
proc.on("error", (err) => {
|
|
257
|
-
if (!settled && err.code === "ENOENT") {
|
|
262
|
+
if (!settled && (err.code === "ENOENT" || err.code === "EINVAL")) {
|
|
258
263
|
settled = true;
|
|
259
264
|
reject(new Error(`Package manager not found for: ${packageName}. ` +
|
|
260
265
|
`Install Node.js or check your PATH.`));
|
package/clients/lsp/launch.ts
CHANGED
|
@@ -205,8 +205,11 @@ export async function launchLSP(
|
|
|
205
205
|
if (npmGlobalPath) {
|
|
206
206
|
spawnCommand = npmGlobalPath;
|
|
207
207
|
// Recompute needsShell for npm global path
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
needsShell =
|
|
209
|
+
isWindows &&
|
|
210
|
+
(spawnCommand.includes(" ") ||
|
|
211
|
+
/\.(cmd|bat)$/i.test(spawnCommand) ||
|
|
212
|
+
!/\.(exe|cmd|bat)$/i.test(spawnCommand));
|
|
210
213
|
}
|
|
211
214
|
}
|
|
212
215
|
|
|
@@ -225,9 +228,11 @@ export async function launchLSP(
|
|
|
225
228
|
if (npmGlobalPath && npmGlobalPath !== spawnCommand) {
|
|
226
229
|
console.error(`[lsp] Trying npm global: ${npmGlobalPath}`);
|
|
227
230
|
// Recompute needsShell for npm global path
|
|
228
|
-
const globalHasExt = /\.(exe|cmd|bat)$/i.test(npmGlobalPath);
|
|
229
231
|
const needsShellGlobal =
|
|
230
|
-
isWindows &&
|
|
232
|
+
isWindows &&
|
|
233
|
+
(npmGlobalPath.includes(" ") ||
|
|
234
|
+
/\.(cmd|bat)$/i.test(npmGlobalPath) ||
|
|
235
|
+
!/\.(exe|cmd|bat)$/i.test(npmGlobalPath));
|
|
231
236
|
proc = trySpawn(npmGlobalPath, args, cwd, env, needsShellGlobal);
|
|
232
237
|
} else {
|
|
233
238
|
throw err;
|
|
@@ -256,7 +261,7 @@ export async function launchLSP(
|
|
|
256
261
|
|
|
257
262
|
// Attach error handler that can reject for immediate errors
|
|
258
263
|
proc.on("error", (err: Error & { code?: string }) => {
|
|
259
|
-
if (!settled && err.code === "ENOENT") {
|
|
264
|
+
if (!settled && (err.code === "ENOENT" || err.code === "EINVAL")) {
|
|
260
265
|
settled = true;
|
|
261
266
|
reject(
|
|
262
267
|
new Error(
|
|
@@ -346,7 +351,7 @@ export async function launchViaPackageManager(
|
|
|
346
351
|
let settled = false;
|
|
347
352
|
|
|
348
353
|
proc.on("error", (err: Error & { code?: string }) => {
|
|
349
|
-
if (!settled && err.code === "ENOENT") {
|
|
354
|
+
if (!settled && (err.code === "ENOENT" || err.code === "EINVAL")) {
|
|
350
355
|
settled = true;
|
|
351
356
|
reject(
|
|
352
357
|
new Error(
|
package/clients/lsp/server.js
CHANGED
|
@@ -233,7 +233,19 @@ export const GoServer = {
|
|
|
233
233
|
root: createRootDetector(["go.mod", "go.sum"]),
|
|
234
234
|
async spawn(root) {
|
|
235
235
|
const proc = await spawnWithInteractiveInstall("go", "gopls", [], { cwd: root }, async () => await launchLSP("gopls", [], { cwd: root }));
|
|
236
|
-
|
|
236
|
+
// gopls works best with minimal initialization options
|
|
237
|
+
// The client capabilities fix (workspaceFolders: true) is the key fix
|
|
238
|
+
return proc
|
|
239
|
+
? {
|
|
240
|
+
process: proc,
|
|
241
|
+
initialization: {
|
|
242
|
+
// Disable experimental features that may cause issues
|
|
243
|
+
ui: {
|
|
244
|
+
semanticTokens: true,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
: undefined;
|
|
237
249
|
},
|
|
238
250
|
};
|
|
239
251
|
export const RustServer = {
|
|
@@ -243,7 +255,20 @@ export const RustServer = {
|
|
|
243
255
|
root: createRootDetector(["Cargo.toml", "Cargo.lock"]),
|
|
244
256
|
async spawn(root) {
|
|
245
257
|
const proc = await spawnWithInteractiveInstall("rust", "rust-analyzer", [], { cwd: root }, async () => await launchLSP("rust-analyzer", [], { cwd: root }));
|
|
246
|
-
|
|
258
|
+
// rust-analyzer needs minimal initialization to avoid capability mismatches
|
|
259
|
+
return proc
|
|
260
|
+
? {
|
|
261
|
+
process: proc,
|
|
262
|
+
initialization: {
|
|
263
|
+
// Disable features that may conflict with our client capabilities
|
|
264
|
+
cargo: {
|
|
265
|
+
buildScripts: { enable: true },
|
|
266
|
+
},
|
|
267
|
+
procMacro: { enable: true },
|
|
268
|
+
diagnostics: { enable: true },
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
: undefined;
|
|
247
272
|
},
|
|
248
273
|
};
|
|
249
274
|
export const RubyServer = {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { MetricsClient } from "./metrics-client.js";
|
|
4
|
+
import { createTempFile, setupTestEnvironment } from "./test-utils.js";
|
|
5
|
+
describe("MetricsClient", () => {
|
|
6
|
+
let client;
|
|
7
|
+
let tmpDir;
|
|
8
|
+
let cleanup;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
client = new MetricsClient();
|
|
11
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-metrics-test-"));
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
cleanup();
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
cleanup();
|
|
18
|
+
});
|
|
19
|
+
describe("calculateEntropy", () => {
|
|
20
|
+
it("should return 0 for empty string", () => {
|
|
21
|
+
expect(client.calculateEntropy("")).toBe(0);
|
|
22
|
+
});
|
|
23
|
+
it("should return 0 for single repeated character", () => {
|
|
24
|
+
expect(client.calculateEntropy("aaaaaa")).toBe(0);
|
|
25
|
+
});
|
|
26
|
+
it("should return 1 for two equally likely characters", () => {
|
|
27
|
+
expect(client.calculateEntropy("ababab")).toBe(1);
|
|
28
|
+
});
|
|
29
|
+
it("should return higher entropy for more diverse content", () => {
|
|
30
|
+
const lowEntropy = "aaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
31
|
+
const highEntropy = "abcdefghijklmnopqrstuvwxyz";
|
|
32
|
+
expect(client.calculateEntropy(highEntropy)).toBeGreaterThan(client.calculateEntropy(lowEntropy));
|
|
33
|
+
});
|
|
34
|
+
it("should match expected Shannon entropy for known input", () => {
|
|
35
|
+
// "aabb" has p(a)=0.5, p(b)=0.5, entropy = -2*(0.5*log2(0.5)) = 1
|
|
36
|
+
expect(client.calculateEntropy("aabb")).toBeCloseTo(1, 5);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe("recordBaseline", () => {
|
|
40
|
+
it("should record baseline for existing file", () => {
|
|
41
|
+
const content = "const x = 1;\nconst y = 2;";
|
|
42
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
43
|
+
client.recordBaseline(filePath);
|
|
44
|
+
const metrics = client.getFileMetrics(filePath);
|
|
45
|
+
expect(metrics).not.toBeNull();
|
|
46
|
+
expect(metrics?.totalLines).toBeGreaterThanOrEqual(2);
|
|
47
|
+
});
|
|
48
|
+
it("should not record baseline for non-existent file", () => {
|
|
49
|
+
client.recordBaseline("/nonexistent/file.ts");
|
|
50
|
+
// Should not throw, just silently skip
|
|
51
|
+
});
|
|
52
|
+
it("should not overwrite existing baseline", () => {
|
|
53
|
+
const content1 = "const x = 1;\n";
|
|
54
|
+
const content2 = "const x = 1;\nconst y = 2;\nconst z = 3;\n";
|
|
55
|
+
const filePath = createTempFile(tmpDir, "test.ts", content1);
|
|
56
|
+
client.recordBaseline(filePath);
|
|
57
|
+
// Modify file
|
|
58
|
+
fs.writeFileSync(filePath, content2);
|
|
59
|
+
// Record again - should not update baseline
|
|
60
|
+
client.recordBaseline(filePath);
|
|
61
|
+
const metrics = client.getFileMetrics(filePath);
|
|
62
|
+
expect(metrics?.entropyStart).toBe(client.calculateEntropy(content1));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe("recordWrite", () => {
|
|
66
|
+
it("should track agent-written lines", () => {
|
|
67
|
+
const original = "const x = 1;\n";
|
|
68
|
+
const filePath = createTempFile(tmpDir, "test.ts", original);
|
|
69
|
+
client.recordBaseline(filePath);
|
|
70
|
+
const modified = "const x = 1;\nconst y = 2;\nconst z = 3;\n";
|
|
71
|
+
fs.writeFileSync(filePath, modified);
|
|
72
|
+
client.recordWrite(filePath, modified);
|
|
73
|
+
const aiRatio = client.getAICodeRatio();
|
|
74
|
+
expect(aiRatio.agentLines).toBeGreaterThan(0);
|
|
75
|
+
});
|
|
76
|
+
it("should calculate AI code ratio", () => {
|
|
77
|
+
const file1 = createTempFile(tmpDir, "file1.ts", "original content line 1\noriginal content line 2\n");
|
|
78
|
+
const file2 = createTempFile(tmpDir, "file2.ts", "original\n");
|
|
79
|
+
client.recordBaseline(file1);
|
|
80
|
+
client.recordBaseline(file2);
|
|
81
|
+
// Simulate agent writing new content
|
|
82
|
+
const newContent1 = "original content line 1\noriginal content line 2\nagent line 3\nagent line 4\n";
|
|
83
|
+
fs.writeFileSync(file1, newContent1);
|
|
84
|
+
client.recordWrite(file1, newContent1);
|
|
85
|
+
const aiRatio = client.getAICodeRatio();
|
|
86
|
+
expect(aiRatio.fileCount).toBe(2);
|
|
87
|
+
expect(aiRatio.ratio).toBeGreaterThanOrEqual(0);
|
|
88
|
+
expect(aiRatio.ratio).toBeLessThanOrEqual(1);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe("getEntropyDeltas", () => {
|
|
92
|
+
it("should track entropy changes", () => {
|
|
93
|
+
const simple = "const x = 1;\n";
|
|
94
|
+
const filePath = createTempFile(tmpDir, "test.ts", simple);
|
|
95
|
+
client.recordBaseline(filePath);
|
|
96
|
+
// Make file more complex
|
|
97
|
+
const complex = `
|
|
98
|
+
function complex(a: number, b: number, c: number): number {
|
|
99
|
+
if (a > 0) {
|
|
100
|
+
if (b > 0) {
|
|
101
|
+
if (c > 0) {
|
|
102
|
+
return a + b + c;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
fs.writeFileSync(filePath, complex);
|
|
110
|
+
client.recordWrite(filePath, complex);
|
|
111
|
+
const deltas = client.getEntropyDeltas();
|
|
112
|
+
expect(deltas.length).toBe(1);
|
|
113
|
+
expect(deltas[0].delta).not.toBe(0);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe("formatSessionSummary", () => {
|
|
117
|
+
it("should return empty string when no files touched", () => {
|
|
118
|
+
expect(client.formatSessionSummary()).toBe("");
|
|
119
|
+
});
|
|
120
|
+
it("should format AI code ratio when files are modified", () => {
|
|
121
|
+
const filePath = createTempFile(tmpDir, "test.ts", "original\n");
|
|
122
|
+
client.recordBaseline(filePath);
|
|
123
|
+
const modified = "original\nnew line 1\nnew line 2\n";
|
|
124
|
+
fs.writeFileSync(filePath, modified);
|
|
125
|
+
client.recordWrite(filePath, modified);
|
|
126
|
+
const summary = client.formatSessionSummary();
|
|
127
|
+
expect(summary).toContain("AI Code");
|
|
128
|
+
expect(summary).toContain("file(s)");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe("reset", () => {
|
|
132
|
+
it("should clear all tracked data", () => {
|
|
133
|
+
const filePath = createTempFile(tmpDir, "test.ts", "content\n");
|
|
134
|
+
client.recordBaseline(filePath);
|
|
135
|
+
client.reset();
|
|
136
|
+
const aiRatio = client.getAICodeRatio();
|
|
137
|
+
expect(aiRatio.fileCount).toBe(0);
|
|
138
|
+
expect(client.formatSessionSummary()).toBe("");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { RuffClient } from "./ruff-client.js";
|
|
3
|
+
import { createTempFile, setupTestEnvironment } from "./test-utils.js";
|
|
4
|
+
describe("RuffClient", () => {
|
|
5
|
+
let client;
|
|
6
|
+
let tmpDir;
|
|
7
|
+
let cleanup;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
client = new RuffClient();
|
|
10
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-ruff-test-"));
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cleanup();
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
cleanup();
|
|
17
|
+
});
|
|
18
|
+
describe("isPythonFile", () => {
|
|
19
|
+
it("should recognize Python files", () => {
|
|
20
|
+
expect(client.isPythonFile("test.py")).toBe(true);
|
|
21
|
+
expect(client.isPythonFile("module.py")).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
it("should not recognize non-Python files", () => {
|
|
24
|
+
expect(client.isPythonFile("test.ts")).toBe(false);
|
|
25
|
+
expect(client.isPythonFile("test.js")).toBe(false);
|
|
26
|
+
expect(client.isPythonFile("test.txt")).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe("isAvailable", () => {
|
|
30
|
+
it("should check ruff availability", () => {
|
|
31
|
+
const available = client.isAvailable();
|
|
32
|
+
expect(typeof available).toBe("boolean");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe("checkFile", () => {
|
|
36
|
+
it("should return empty array for non-existent files", () => {
|
|
37
|
+
if (!client.isAvailable())
|
|
38
|
+
return;
|
|
39
|
+
const result = client.checkFile("/nonexistent/file.py");
|
|
40
|
+
expect(result).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
it("should detect lint issues in Python code", () => {
|
|
43
|
+
if (!client.isAvailable())
|
|
44
|
+
return;
|
|
45
|
+
const content = `
|
|
46
|
+
import os
|
|
47
|
+
import sys
|
|
48
|
+
|
|
49
|
+
x = 1
|
|
50
|
+
`;
|
|
51
|
+
const filePath = createTempFile(tmpDir, "test.py", content);
|
|
52
|
+
const result = client.checkFile(filePath);
|
|
53
|
+
// Should detect unused imports
|
|
54
|
+
expect(result.some((d) => d.rule === "F401" || d.message.includes("unused"))).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it("should return array of diagnostics", () => {
|
|
57
|
+
if (!client.isAvailable())
|
|
58
|
+
return;
|
|
59
|
+
const content = `
|
|
60
|
+
def foo():
|
|
61
|
+
x = undefined_variable
|
|
62
|
+
return x
|
|
63
|
+
`;
|
|
64
|
+
const filePath = createTempFile(tmpDir, "test.py", content);
|
|
65
|
+
const result = client.checkFile(filePath);
|
|
66
|
+
// Should return an array
|
|
67
|
+
expect(Array.isArray(result)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("formatDiagnostics", () => {
|
|
71
|
+
it("should format diagnostics for display", () => {
|
|
72
|
+
const diags = [
|
|
73
|
+
{
|
|
74
|
+
line: 1,
|
|
75
|
+
column: 0,
|
|
76
|
+
endLine: 1,
|
|
77
|
+
endColumn: 10,
|
|
78
|
+
severity: "error",
|
|
79
|
+
message: "Undefined name 'x'",
|
|
80
|
+
rule: "F821",
|
|
81
|
+
file: "test.py",
|
|
82
|
+
fixable: false,
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
const formatted = client.formatDiagnostics(diags);
|
|
86
|
+
expect(formatted).toContain("Ruff");
|
|
87
|
+
expect(formatted).toContain("F821");
|
|
88
|
+
expect(formatted).toContain("Undefined name");
|
|
89
|
+
});
|
|
90
|
+
it("should show fixable count", () => {
|
|
91
|
+
const diags = [
|
|
92
|
+
{
|
|
93
|
+
line: 1,
|
|
94
|
+
column: 0,
|
|
95
|
+
endLine: 1,
|
|
96
|
+
endColumn: 10,
|
|
97
|
+
severity: "warning",
|
|
98
|
+
message: "Unused import",
|
|
99
|
+
rule: "F401",
|
|
100
|
+
file: "test.py",
|
|
101
|
+
fixable: true,
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
const formatted = client.formatDiagnostics(diags);
|
|
105
|
+
expect(formatted).toContain("fixable");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe("checkFormatting", () => {
|
|
109
|
+
it("should detect formatting issues", () => {
|
|
110
|
+
if (!client.isAvailable())
|
|
111
|
+
return;
|
|
112
|
+
const content = `x=1
|
|
113
|
+
y=2
|
|
114
|
+
`;
|
|
115
|
+
const filePath = createTempFile(tmpDir, "test.py", content);
|
|
116
|
+
const result = client.checkFormatting(filePath);
|
|
117
|
+
// Should suggest formatting (missing spaces around =)
|
|
118
|
+
expect(typeof result).toBe("string");
|
|
119
|
+
});
|
|
120
|
+
it("should return empty string for well-formatted code", () => {
|
|
121
|
+
if (!client.isAvailable())
|
|
122
|
+
return;
|
|
123
|
+
const content = `x = 1
|
|
124
|
+
y = 2
|
|
125
|
+
`;
|
|
126
|
+
const filePath = createTempFile(tmpDir, "test.py", content);
|
|
127
|
+
const result = client.checkFormatting(filePath);
|
|
128
|
+
// Well-formatted code should return empty or minimal output
|
|
129
|
+
expect(result).toBe("");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { RustClient } from "./rust-client.js";
|
|
5
|
+
import { setupTestEnvironment } from "./test-utils.js";
|
|
6
|
+
describe("RustClient", () => {
|
|
7
|
+
let client;
|
|
8
|
+
let tmpDir;
|
|
9
|
+
let cleanup;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
client = new RustClient();
|
|
12
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-rust-test-"));
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
cleanup();
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
cleanup();
|
|
19
|
+
});
|
|
20
|
+
describe("isRustFile", () => {
|
|
21
|
+
it("should recognize Rust files", () => {
|
|
22
|
+
expect(client.isRustFile("main.rs")).toBe(true);
|
|
23
|
+
expect(client.isRustFile("lib.rs")).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
it("should not recognize non-Rust files", () => {
|
|
26
|
+
expect(client.isRustFile("main.ts")).toBe(false);
|
|
27
|
+
expect(client.isRustFile("main.py")).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe("isAvailable", () => {
|
|
31
|
+
it("should check cargo availability", () => {
|
|
32
|
+
const available = client.isAvailable();
|
|
33
|
+
expect(typeof available).toBe("boolean");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("checkFile", () => {
|
|
37
|
+
it("should return empty array for non-existent files", () => {
|
|
38
|
+
if (!client.isAvailable())
|
|
39
|
+
return;
|
|
40
|
+
const result = client.checkFile("/nonexistent/file.rs", tmpDir);
|
|
41
|
+
expect(result).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
it("should return array for valid Rust files", () => {
|
|
44
|
+
if (!client.isAvailable())
|
|
45
|
+
return;
|
|
46
|
+
// Need a Cargo.toml for cargo to work
|
|
47
|
+
fs.writeFileSync(path.join(tmpDir, "Cargo.toml"), `
|
|
48
|
+
[package]
|
|
49
|
+
name = "test"
|
|
50
|
+
version = "0.1.0"
|
|
51
|
+
edition = "2021"
|
|
52
|
+
`);
|
|
53
|
+
fs.mkdirSync(path.join(tmpDir, "src"), { recursive: true });
|
|
54
|
+
fs.writeFileSync(path.join(tmpDir, "src", "main.rs"), `
|
|
55
|
+
fn main() {
|
|
56
|
+
println!("Hello, world!");
|
|
57
|
+
}
|
|
58
|
+
`);
|
|
59
|
+
const result = client.checkFile(path.join(tmpDir, "src", "main.rs"), tmpDir);
|
|
60
|
+
expect(Array.isArray(result)).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe("formatDiagnostics", () => {
|
|
64
|
+
it("should format diagnostics for display", () => {
|
|
65
|
+
const diags = [
|
|
66
|
+
{
|
|
67
|
+
line: 3,
|
|
68
|
+
column: 0,
|
|
69
|
+
endLine: 3,
|
|
70
|
+
endColumn: 10,
|
|
71
|
+
severity: "error",
|
|
72
|
+
message: "cannot find value `x` in this scope",
|
|
73
|
+
code: "E0425",
|
|
74
|
+
file: "main.rs",
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
const formatted = client.formatDiagnostics(diags);
|
|
78
|
+
expect(formatted).toContain("Rust");
|
|
79
|
+
expect(formatted).toContain("1 issue");
|
|
80
|
+
expect(formatted).toContain("E0425");
|
|
81
|
+
});
|
|
82
|
+
it("should show error and warning counts", () => {
|
|
83
|
+
const diags = [
|
|
84
|
+
{
|
|
85
|
+
line: 1,
|
|
86
|
+
column: 0,
|
|
87
|
+
endLine: 1,
|
|
88
|
+
endColumn: 10,
|
|
89
|
+
severity: "error",
|
|
90
|
+
message: "Error",
|
|
91
|
+
file: "test.rs",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
line: 2,
|
|
95
|
+
column: 0,
|
|
96
|
+
endLine: 2,
|
|
97
|
+
endColumn: 10,
|
|
98
|
+
severity: "warning",
|
|
99
|
+
message: "Warning",
|
|
100
|
+
file: "test.rs",
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
const formatted = client.formatDiagnostics(diags);
|
|
104
|
+
expect(formatted).toContain("1 error(s)");
|
|
105
|
+
expect(formatted).toContain("1 warning(s)");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|