pi-lens 3.1.2 → 3.2.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 +55 -0
- package/README.md +16 -12
- package/clients/ast-grep-client.js +8 -1
- package/clients/ast-grep-client.ts +9 -1
- package/clients/biome-client.js +51 -38
- package/clients/biome-client.ts +60 -58
- package/clients/dependency-checker.js +30 -1
- package/clients/dependency-checker.ts +35 -1
- package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
- package/clients/dispatch/bus-dispatcher.js +15 -14
- package/clients/dispatch/bus-dispatcher.ts +32 -25
- package/clients/dispatch/dispatcher.js +18 -25
- package/clients/dispatch/dispatcher.test.ts +2 -1
- package/clients/dispatch/dispatcher.ts +17 -28
- package/clients/dispatch/plan.js +77 -32
- package/clients/dispatch/plan.ts +78 -32
- package/clients/dispatch/runners/ast-grep-napi.js +36 -376
- package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
- package/clients/dispatch/runners/index.js +8 -4
- package/clients/dispatch/runners/index.ts +8 -4
- package/clients/dispatch/runners/lsp.js +65 -0
- package/clients/dispatch/runners/lsp.ts +125 -0
- package/clients/dispatch/runners/oxlint.js +2 -2
- package/clients/dispatch/runners/oxlint.ts +2 -2
- package/clients/dispatch/runners/pyright.js +24 -8
- package/clients/dispatch/runners/pyright.ts +28 -14
- package/clients/dispatch/runners/rust-clippy.js +2 -2
- package/clients/dispatch/runners/rust-clippy.ts +2 -4
- package/clients/dispatch/runners/tree-sitter.js +14 -2
- package/clients/dispatch/runners/tree-sitter.ts +15 -2
- package/clients/dispatch/runners/ts-lsp.js +3 -3
- package/clients/dispatch/runners/ts-lsp.ts +8 -5
- package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
- package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
- package/clients/dispatch/types.js +3 -0
- package/clients/dispatch/types.ts +3 -0
- package/clients/formatters.js +67 -14
- package/clients/formatters.ts +68 -15
- package/clients/installer/index.js +78 -10
- package/clients/installer/index.ts +519 -426
- package/clients/jscpd-client.js +28 -0
- package/clients/jscpd-client.ts +41 -3
- package/clients/knip-client.js +30 -1
- package/clients/knip-client.ts +34 -2
- package/clients/lsp/__tests__/client.test.ts +64 -41
- package/clients/lsp/__tests__/config.test.ts +25 -17
- package/clients/lsp/__tests__/launch.test.ts +108 -43
- package/clients/lsp/__tests__/service.test.ts +76 -48
- package/clients/lsp/client.js +87 -2
- package/clients/lsp/client.ts +150 -6
- package/clients/lsp/config.js +8 -11
- package/clients/lsp/config.ts +24 -21
- package/clients/lsp/index.js +69 -0
- package/clients/lsp/index.ts +82 -0
- package/clients/lsp/interactive-install.js +19 -8
- package/clients/lsp/interactive-install.ts +52 -27
- package/clients/lsp/launch.js +182 -32
- package/clients/lsp/launch.ts +241 -38
- package/clients/lsp/path-utils.js +3 -46
- package/clients/lsp/path-utils.ts +11 -51
- package/clients/lsp/server.js +93 -71
- package/clients/lsp/server.ts +173 -131
- package/clients/path-utils.js +142 -0
- package/clients/path-utils.ts +153 -0
- package/clients/ruff-client.js +33 -4
- package/clients/ruff-client.ts +44 -13
- package/clients/safe-spawn.js +3 -1
- package/clients/safe-spawn.ts +3 -1
- package/clients/services/effect-integration.js +11 -7
- package/clients/services/effect-integration.ts +34 -26
- package/clients/sg-runner.js +51 -9
- package/clients/sg-runner.ts +58 -15
- package/clients/tree-sitter-client.js +12 -0
- package/clients/tree-sitter-client.ts +12 -0
- package/clients/typescript-client.js +6 -2
- package/clients/typescript-client.ts +9 -2
- package/commands/booboo.js +2 -4
- package/commands/booboo.ts +2 -4
- package/index.ts +377 -93
- package/package.json +2 -1
- package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
- package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
- package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
- package/tsconfig.json +1 -1
- package/clients/__tests__/file-time.test.js +0 -216
- package/clients/__tests__/format-service.test.js +0 -245
- package/clients/__tests__/formatters.test.js +0 -271
- package/clients/agent-behavior-client.test.js +0 -94
- package/clients/ast-grep-client.test.js +0 -129
- package/clients/ast-grep-client.test.ts +0 -155
- package/clients/biome-client.test.js +0 -144
- package/clients/cache-manager.test.js +0 -197
- package/clients/complexity-client.test.js +0 -234
- package/clients/dependency-checker.test.js +0 -60
- package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
- package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
- package/clients/dispatch/dispatcher.edge.test.js +0 -82
- package/clients/dispatch/dispatcher.format.test.js +0 -46
- package/clients/dispatch/dispatcher.inline.test.js +0 -74
- package/clients/dispatch/dispatcher.test.js +0 -115
- package/clients/dispatch/runners/architect.test.js +0 -138
- package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
- package/clients/dispatch/runners/oxlint.test.js +0 -230
- package/clients/dispatch/runners/pyright.test.js +0 -98
- package/clients/dispatch/runners/python-slop.test.js +0 -203
- package/clients/dispatch/runners/scan_codebase.test.js +0 -89
- package/clients/dispatch/runners/shellcheck.test.js +0 -98
- package/clients/dispatch/runners/spellcheck.test.js +0 -158
- package/clients/dispatch/runners/ts-slop.test.js +0 -180
- package/clients/dispatch/runners/ts-slop.test.ts +0 -230
- package/clients/dogfood.test.js +0 -201
- package/clients/file-kinds.test.js +0 -169
- package/clients/go-client.test.js +0 -127
- package/clients/jscpd-client.test.js +0 -127
- package/clients/knip-client.test.js +0 -112
- package/clients/lsp/__tests__/client.test.js +0 -325
- package/clients/lsp/__tests__/config.test.js +0 -166
- package/clients/lsp/__tests__/error-recovery.test.js +0 -213
- package/clients/lsp/__tests__/integration.test.js +0 -127
- package/clients/lsp/__tests__/launch.test.js +0 -260
- package/clients/lsp/__tests__/server.test.js +0 -259
- package/clients/lsp/__tests__/service.test.js +0 -417
- package/clients/metrics-client.test.js +0 -141
- package/clients/ruff-client.test.js +0 -132
- package/clients/rust-client.test.js +0 -108
- package/clients/sanitize.test.js +0 -177
- package/clients/secrets-scanner.test.js +0 -100
- package/clients/services/__tests__/effect-integration.test.js +0 -86
- package/clients/test-runner-client.test.js +0 -192
- package/clients/todo-scanner.test.js +0 -301
- package/clients/type-coverage-client.test.js +0 -105
- package/clients/typescript-client.codefix.test.js +0 -157
- package/clients/typescript-client.test.js +0 -105
- package/commands/clients/ast-grep-client.js +0 -250
- package/commands/clients/ast-grep-parser.js +0 -86
- package/commands/clients/ast-grep-rule-manager.js +0 -91
- package/commands/clients/ast-grep-types.js +0 -9
- package/commands/clients/biome-client.js +0 -380
- package/commands/clients/complexity-client.js +0 -667
- package/commands/clients/file-kinds.js +0 -177
- package/commands/clients/file-utils.js +0 -40
- package/commands/clients/jscpd-client.js +0 -169
- package/commands/clients/knip-client.js +0 -211
- package/commands/clients/ruff-client.js +0 -297
- package/commands/clients/safe-spawn.js +0 -88
- package/commands/clients/scan-utils.js +0 -83
- package/commands/clients/sg-runner.js +0 -190
- package/commands/clients/types.js +0 -11
- package/commands/clients/typescript-client.js +0 -505
- package/commands/rate.test.js +0 -119
- package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
- package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
- package/rules/ast-grep-rules/rules/no-eval.yml +0 -13
|
@@ -1,141 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,132 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
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
|
-
});
|
package/clients/sanitize.test.js
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for sanitize.ts
|
|
3
|
-
* Tool output sanitization
|
|
4
|
-
*/
|
|
5
|
-
import { describe, expect, it } from "vitest";
|
|
6
|
-
import { extractErrorMessage, normalizeWhitespace, sanitizeBiomeOutput, sanitizeLine, sanitizeOutput, sanitizeRuffOutput, sanitizeToolOutput, stripAnsi, truncateMessage, } from "./sanitize.js";
|
|
7
|
-
describe("stripAnsi", () => {
|
|
8
|
-
it("should remove ANSI color codes", () => {
|
|
9
|
-
expect(stripAnsi("\x1b[31mError\x1b[0m")).toBe("Error");
|
|
10
|
-
expect(stripAnsi("\x1b[1;32mSuccess\x1b[0m")).toBe("Success");
|
|
11
|
-
});
|
|
12
|
-
it("should remove extended ANSI codes", () => {
|
|
13
|
-
expect(stripAnsi("\x1b[1;2;3mText\x1b[0m")).toBe("Text");
|
|
14
|
-
});
|
|
15
|
-
it("should return original string if no ANSI codes", () => {
|
|
16
|
-
expect(stripAnsi("Plain text")).toBe("Plain text");
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
describe("normalizeWhitespace", () => {
|
|
20
|
-
it("should collapse multiple spaces", () => {
|
|
21
|
-
expect(normalizeWhitespace("Hello World")).toBe("Hello World");
|
|
22
|
-
});
|
|
23
|
-
it("should trim lines", () => {
|
|
24
|
-
expect(normalizeWhitespace(" Hello \n World ")).toBe("Hello\nWorld");
|
|
25
|
-
});
|
|
26
|
-
it("should remove empty lines", () => {
|
|
27
|
-
expect(normalizeWhitespace("Hello\n\n\nWorld")).toBe("Hello\nWorld");
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
describe("sanitizeLine", () => {
|
|
31
|
-
it("should remove common error prefixes", () => {
|
|
32
|
-
expect(sanitizeLine("[error] Something went wrong")).toBe("Something went wrong");
|
|
33
|
-
expect(sanitizeLine("error: Something went wrong")).toBe("Something went wrong");
|
|
34
|
-
});
|
|
35
|
-
it("should remove check/cross marks", () => {
|
|
36
|
-
expect(sanitizeLine("× Error message")).toBe("Error message");
|
|
37
|
-
expect(sanitizeLine("✓ Success message")).toBe("Success message");
|
|
38
|
-
});
|
|
39
|
-
it("should normalize whitespace", () => {
|
|
40
|
-
expect(sanitizeLine(" Error message ")).toBe("Error message");
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
describe("sanitizeOutput", () => {
|
|
44
|
-
it("should return empty string for empty input", () => {
|
|
45
|
-
expect(sanitizeOutput("")).toBe("");
|
|
46
|
-
expect(sanitizeOutput(" ")).toBe("");
|
|
47
|
-
});
|
|
48
|
-
it("should filter out detail lines", () => {
|
|
49
|
-
const output = "Error message\n at line 10\n at function foo";
|
|
50
|
-
const result = sanitizeOutput(output);
|
|
51
|
-
expect(result).toBe("Error message");
|
|
52
|
-
});
|
|
53
|
-
it("should remove ANSI codes and normalize", () => {
|
|
54
|
-
const output = "\x1b[31mError\x1b[0m\n\x1b[32mSuccess\x1b[0m";
|
|
55
|
-
const result = sanitizeOutput(output);
|
|
56
|
-
expect(result).toBe("Error\nSuccess");
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
describe("extractErrorMessage", () => {
|
|
60
|
-
it("should return undefined for empty input", () => {
|
|
61
|
-
expect(extractErrorMessage("")).toBeUndefined();
|
|
62
|
-
expect(extractErrorMessage(" ")).toBeUndefined();
|
|
63
|
-
});
|
|
64
|
-
it("should return first error line containing error indicator", () => {
|
|
65
|
-
// Note: extractErrorMessage sanitizes lines first, then checks isErrorLine
|
|
66
|
-
// Since "error:" prefix is removed by sanitizeLine, we need to use a keyword
|
|
67
|
-
// that remains in the sanitized line, like "failed" or "syntax"
|
|
68
|
-
const output = "Some info\nOperation failed: disk full\nMore info";
|
|
69
|
-
const result = extractErrorMessage(output);
|
|
70
|
-
expect(result).toBe("Operation failed: disk full");
|
|
71
|
-
});
|
|
72
|
-
it("should fall back to first non-empty line", () => {
|
|
73
|
-
const output = "info line\nanother info\nand more";
|
|
74
|
-
expect(extractErrorMessage(output)).toBe("info line");
|
|
75
|
-
});
|
|
76
|
-
it("should find error by keyword", () => {
|
|
77
|
-
const result = extractErrorMessage("warning: low severity\nfailed: operation");
|
|
78
|
-
// extractErrorMessage returns the first line with error indicators
|
|
79
|
-
// "failed:" contains "failed" which matches ERROR_INDICATORS
|
|
80
|
-
expect(result).toBe("failed: operation");
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
describe("truncateMessage", () => {
|
|
84
|
-
it("should not truncate short messages", () => {
|
|
85
|
-
const msg = "Short message";
|
|
86
|
-
expect(truncateMessage(msg, 100)).toBe(msg);
|
|
87
|
-
});
|
|
88
|
-
it("should truncate long messages", () => {
|
|
89
|
-
const msg = "A".repeat(200);
|
|
90
|
-
const result = truncateMessage(msg, 50);
|
|
91
|
-
expect(result).toBe(`${"A".repeat(49)}…`);
|
|
92
|
-
});
|
|
93
|
-
it("should use default max length of 140", () => {
|
|
94
|
-
const msg = "A".repeat(200);
|
|
95
|
-
const result = truncateMessage(msg);
|
|
96
|
-
expect(result.length).toBe(140);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
describe("sanitizeToolOutput", () => {
|
|
100
|
-
it("should return empty result for empty input", () => {
|
|
101
|
-
const result = sanitizeToolOutput("");
|
|
102
|
-
expect(result.summary).toBeUndefined();
|
|
103
|
-
expect(result.details).toBeUndefined();
|
|
104
|
-
expect(result.truncated).toBe(false);
|
|
105
|
-
});
|
|
106
|
-
it("should extract summary and details", () => {
|
|
107
|
-
const output = "error: something failed\nline 2\nline 3\nline 4";
|
|
108
|
-
const result = sanitizeToolOutput(output);
|
|
109
|
-
// sanitizeLine removes "error:" prefix
|
|
110
|
-
expect(result.summary).toContain("something failed");
|
|
111
|
-
expect(result.details).toBeDefined();
|
|
112
|
-
});
|
|
113
|
-
it("should mark as truncated when too many lines", () => {
|
|
114
|
-
const lines = [];
|
|
115
|
-
for (let i = 0; i < 30; i++) {
|
|
116
|
-
lines.push(`error line ${i}`);
|
|
117
|
-
}
|
|
118
|
-
const result = sanitizeToolOutput(lines.join("\n"));
|
|
119
|
-
expect(result.truncated).toBe(true);
|
|
120
|
-
expect(result.details).toContain("more lines");
|
|
121
|
-
});
|
|
122
|
-
it("should respect max summary length", () => {
|
|
123
|
-
const output = `error: ${"x".repeat(200)}`;
|
|
124
|
-
const result = sanitizeToolOutput(output, 50);
|
|
125
|
-
expect(result.summary?.length).toBeLessThanOrEqual(50);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
describe("sanitizeBiomeOutput", () => {
|
|
129
|
-
it("should parse JSON diagnostics", () => {
|
|
130
|
-
const json = JSON.stringify({
|
|
131
|
-
diagnostics: [
|
|
132
|
-
{
|
|
133
|
-
location: {
|
|
134
|
-
path: "file.ts",
|
|
135
|
-
span: { start: { line: 10, column: 0 } },
|
|
136
|
-
},
|
|
137
|
-
message: "Unexpected token",
|
|
138
|
-
},
|
|
139
|
-
],
|
|
140
|
-
});
|
|
141
|
-
const result = sanitizeBiomeOutput(json);
|
|
142
|
-
expect(result).toContain("file.ts");
|
|
143
|
-
expect(result).toContain("11"); // line + 1
|
|
144
|
-
expect(result).toContain("Unexpected token");
|
|
145
|
-
});
|
|
146
|
-
it("should handle text output with errors", () => {
|
|
147
|
-
const output = "error: something went wrong\ninfo: some info";
|
|
148
|
-
const result = sanitizeBiomeOutput(output);
|
|
149
|
-
expect(result).toContain("error");
|
|
150
|
-
});
|
|
151
|
-
it("should return empty for empty input", () => {
|
|
152
|
-
expect(sanitizeBiomeOutput("")).toBe("");
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
describe("sanitizeRuffOutput", () => {
|
|
156
|
-
it("should parse JSON diagnostics", () => {
|
|
157
|
-
const json = JSON.stringify([
|
|
158
|
-
{
|
|
159
|
-
location: { row: 10, column: 5 },
|
|
160
|
-
code: "E501",
|
|
161
|
-
message: "Line too long",
|
|
162
|
-
},
|
|
163
|
-
]);
|
|
164
|
-
const result = sanitizeRuffOutput(json);
|
|
165
|
-
expect(result).toContain("10:5");
|
|
166
|
-
expect(result).toContain("[E501]");
|
|
167
|
-
expect(result).toContain("Line too long");
|
|
168
|
-
});
|
|
169
|
-
it("should handle text output", () => {
|
|
170
|
-
const output = "file.py:10:5 [E501] Line too long";
|
|
171
|
-
const result = sanitizeRuffOutput(output);
|
|
172
|
-
expect(result).toContain("E501");
|
|
173
|
-
});
|
|
174
|
-
it("should return empty for empty input", () => {
|
|
175
|
-
expect(sanitizeRuffOutput("")).toBe("");
|
|
176
|
-
});
|
|
177
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { formatSecrets, scanForSecrets } from "./secrets-scanner.js";
|
|
3
|
-
describe("scanForSecrets", () => {
|
|
4
|
-
it("should detect Stripe/OpenAI keys (sk-*)", () => {
|
|
5
|
-
const content = `const apiKey = "sk-live-1234567890abcdefghij";`;
|
|
6
|
-
const findings = scanForSecrets(content);
|
|
7
|
-
expect(findings.length).toBe(1);
|
|
8
|
-
expect(findings[0].message).toContain("Stripe or OpenAI");
|
|
9
|
-
});
|
|
10
|
-
it("should detect GitHub personal tokens (ghp_*)", () => {
|
|
11
|
-
const content = `token = "ghp_1234567890abcdefghijklmnopqrstuvwxyz";`;
|
|
12
|
-
const findings = scanForSecrets(content);
|
|
13
|
-
expect(findings.length).toBe(1);
|
|
14
|
-
expect(findings[0].message).toContain("GitHub personal");
|
|
15
|
-
});
|
|
16
|
-
it("should detect AWS access keys (AKIA*)", () => {
|
|
17
|
-
const content = `const AWS_KEY = "AKIAIOSFODNN7EXAMPLE";`;
|
|
18
|
-
const findings = scanForSecrets(content);
|
|
19
|
-
expect(findings.length).toBe(1);
|
|
20
|
-
expect(findings[0].message).toContain("AWS access key");
|
|
21
|
-
});
|
|
22
|
-
it("should detect private key material", () => {
|
|
23
|
-
const content = `-----BEGIN RSA PRIVATE KEY-----
|
|
24
|
-
MIIEpAIBAAKCAQEA...`;
|
|
25
|
-
const findings = scanForSecrets(content);
|
|
26
|
-
expect(findings.length).toBe(1);
|
|
27
|
-
expect(findings[0].message).toContain("Private key");
|
|
28
|
-
});
|
|
29
|
-
it("should detect hardcoded passwords", () => {
|
|
30
|
-
const content = `const config = { password: "hunter2" };`;
|
|
31
|
-
const findings = scanForSecrets(content);
|
|
32
|
-
expect(findings.length).toBe(1);
|
|
33
|
-
expect(findings[0].message).toContain("password");
|
|
34
|
-
});
|
|
35
|
-
it("should detect secrets in .env format", () => {
|
|
36
|
-
const content = `API_KEY=sk-live-1234567890abcdefghij
|
|
37
|
-
DATABASE_URL=postgres://localhost`;
|
|
38
|
-
const findings = scanForSecrets(content);
|
|
39
|
-
expect(findings.length).toBe(1);
|
|
40
|
-
// sk-* pattern catches this first (more specific)
|
|
41
|
-
expect(findings[0].message).toContain("Stripe or OpenAI");
|
|
42
|
-
});
|
|
43
|
-
it("should NOT flag safe content", () => {
|
|
44
|
-
const content = `
|
|
45
|
-
const name = "test";
|
|
46
|
-
const url = "https://example.com";
|
|
47
|
-
const port = 3000;
|
|
48
|
-
const message = "Hello world";
|
|
49
|
-
`;
|
|
50
|
-
const findings = scanForSecrets(content);
|
|
51
|
-
expect(findings.length).toBe(0);
|
|
52
|
-
});
|
|
53
|
-
it("should NOT flag env var references", () => {
|
|
54
|
-
const content = `const key = process.env.API_KEY;`;
|
|
55
|
-
const findings = scanForSecrets(content);
|
|
56
|
-
expect(findings.length).toBe(0);
|
|
57
|
-
});
|
|
58
|
-
it("should detect multiple secrets", () => {
|
|
59
|
-
const content = `
|
|
60
|
-
const sk = "sk-live-1234567890abcdefghij";
|
|
61
|
-
const gh = "ghp_1234567890abcdefghijklmnopqrstuvwxyz";
|
|
62
|
-
`;
|
|
63
|
-
const findings = scanForSecrets(content);
|
|
64
|
-
expect(findings.length).toBe(2);
|
|
65
|
-
});
|
|
66
|
-
it("should report correct line numbers", () => {
|
|
67
|
-
const content = `line 1
|
|
68
|
-
line 2
|
|
69
|
-
const secret = "sk-live-1234567890abcdefghij";
|
|
70
|
-
line 4`;
|
|
71
|
-
const findings = scanForSecrets(content);
|
|
72
|
-
expect(findings.length).toBe(1);
|
|
73
|
-
expect(findings[0].line).toBe(3);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
describe("formatSecrets", () => {
|
|
77
|
-
it("should format findings for terminal output", () => {
|
|
78
|
-
const findings = [
|
|
79
|
-
{ line: 5, message: "Possible Stripe or OpenAI API key (sk-*)" },
|
|
80
|
-
];
|
|
81
|
-
const output = formatSecrets(findings, "src/config.ts");
|
|
82
|
-
expect(output).toContain("STOP");
|
|
83
|
-
expect(output).toContain("1 potential secret(s)");
|
|
84
|
-
expect(output).toContain("L5");
|
|
85
|
-
expect(output).toContain("src/config.ts");
|
|
86
|
-
});
|
|
87
|
-
it("should return empty string for no findings", () => {
|
|
88
|
-
const output = formatSecrets([], "src/config.ts");
|
|
89
|
-
expect(output).toBe("");
|
|
90
|
-
});
|
|
91
|
-
it("should truncate at 5 findings", () => {
|
|
92
|
-
const findings = Array.from({ length: 10 }, (_, i) => ({
|
|
93
|
-
line: i + 1,
|
|
94
|
-
message: "Test secret",
|
|
95
|
-
}));
|
|
96
|
-
const output = formatSecrets(findings, "src/config.ts");
|
|
97
|
-
expect(output).toContain("10 potential secret(s)");
|
|
98
|
-
expect(output).toContain("... and 5 more");
|
|
99
|
-
});
|
|
100
|
-
});
|