claude-crap 0.3.5 → 0.3.7
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 +13 -0
- package/dist/dashboard/file-detail.d.ts +77 -0
- package/dist/dashboard/file-detail.d.ts.map +1 -0
- package/dist/dashboard/file-detail.js +120 -0
- package/dist/dashboard/file-detail.js.map +1 -0
- package/dist/dashboard/server.d.ts +3 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +108 -1
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +19 -2
- package/dist/index.js.map +1 -1
- package/dist/scanner/auto-scan.d.ts +8 -1
- package/dist/scanner/auto-scan.d.ts.map +1 -1
- package/dist/scanner/auto-scan.js +14 -1
- package/dist/scanner/auto-scan.js.map +1 -1
- package/dist/scanner/complexity-scanner.d.ts +54 -0
- package/dist/scanner/complexity-scanner.d.ts.map +1 -0
- package/dist/scanner/complexity-scanner.js +176 -0
- package/dist/scanner/complexity-scanner.js.map +1 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/bundle/dashboard/public/index.html +432 -12
- package/plugin/bundle/mcp-server.mjs +429 -71
- package/plugin/bundle/mcp-server.mjs.map +4 -4
- package/plugin/package-lock.json +2 -2
- package/plugin/package.json +1 -1
- package/scripts/bundle-plugin.mjs +53 -2
- package/src/dashboard/file-detail.ts +197 -0
- package/src/dashboard/public/index.html +432 -12
- package/src/dashboard/server.ts +141 -1
- package/src/index.ts +20 -2
- package/src/scanner/auto-scan.ts +26 -0
- package/src/scanner/complexity-scanner.ts +233 -0
- package/src/tests/complexity-scanner.test.ts +263 -0
- package/src/tests/file-detail-api.test.ts +258 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the /api/file-detail endpoint logic.
|
|
3
|
+
*
|
|
4
|
+
* These tests exercise the file detail builder function directly,
|
|
5
|
+
* without spawning the full MCP server. They verify that source lines,
|
|
6
|
+
* function metrics, and filtered findings are returned correctly.
|
|
7
|
+
*
|
|
8
|
+
* @module tests/file-detail-api.test
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
|
|
17
|
+
import { buildFileDetail } from "../dashboard/file-detail.js";
|
|
18
|
+
import { TreeSitterEngine } from "../ast/tree-sitter-engine.js";
|
|
19
|
+
import { SarifStore } from "../sarif/sarif-store.js";
|
|
20
|
+
import { wrapResultsInSarif } from "../adapters/common.js";
|
|
21
|
+
|
|
22
|
+
function makeTmpDir(): string {
|
|
23
|
+
return mkdtempSync(join(tmpdir(), "crap-file-detail-"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SAMPLE_TS = `export function greet(name: string): string {
|
|
27
|
+
return "hello " + name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function classify(x: number): string {
|
|
31
|
+
if (x < 0) return "negative";
|
|
32
|
+
if (x === 0) return "zero";
|
|
33
|
+
if (x < 10) return "small";
|
|
34
|
+
if (x < 100) return "medium";
|
|
35
|
+
return "large";
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
describe("buildFileDetail", () => {
|
|
40
|
+
const engine = new TreeSitterEngine();
|
|
41
|
+
|
|
42
|
+
it("returns source lines for a valid file", async () => {
|
|
43
|
+
const dir = makeTmpDir();
|
|
44
|
+
try {
|
|
45
|
+
writeFileSync(join(dir, "hello.ts"), SAMPLE_TS);
|
|
46
|
+
const store = new SarifStore({
|
|
47
|
+
workspaceRoot: dir,
|
|
48
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
49
|
+
});
|
|
50
|
+
const result = await buildFileDetail({
|
|
51
|
+
relativePath: "hello.ts",
|
|
52
|
+
workspaceRoot: dir,
|
|
53
|
+
astEngine: engine,
|
|
54
|
+
sarifStore: store,
|
|
55
|
+
cyclomaticMax: 15,
|
|
56
|
+
});
|
|
57
|
+
assert.equal(result.filePath, "hello.ts");
|
|
58
|
+
assert.ok(result.sourceLines.length > 0);
|
|
59
|
+
assert.ok(result.sourceLines[0]!.includes("export function greet"));
|
|
60
|
+
} finally {
|
|
61
|
+
rmSync(dir, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns function metrics for supported languages", async () => {
|
|
66
|
+
const dir = makeTmpDir();
|
|
67
|
+
try {
|
|
68
|
+
writeFileSync(join(dir, "hello.ts"), SAMPLE_TS);
|
|
69
|
+
const store = new SarifStore({
|
|
70
|
+
workspaceRoot: dir,
|
|
71
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
72
|
+
});
|
|
73
|
+
const result = await buildFileDetail({
|
|
74
|
+
relativePath: "hello.ts",
|
|
75
|
+
workspaceRoot: dir,
|
|
76
|
+
astEngine: engine,
|
|
77
|
+
sarifStore: store,
|
|
78
|
+
cyclomaticMax: 15,
|
|
79
|
+
});
|
|
80
|
+
assert.equal(result.language, "typescript");
|
|
81
|
+
assert.ok(result.functions.length >= 2);
|
|
82
|
+
const greet = result.functions.find((f) => f.name === "greet");
|
|
83
|
+
assert.ok(greet);
|
|
84
|
+
assert.equal(greet.cyclomaticComplexity, 1);
|
|
85
|
+
const classify = result.functions.find((f) => f.name === "classify");
|
|
86
|
+
assert.ok(classify);
|
|
87
|
+
assert.ok(classify.cyclomaticComplexity >= 4);
|
|
88
|
+
} finally {
|
|
89
|
+
rmSync(dir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("filters findings to the requested file only", async () => {
|
|
94
|
+
const dir = makeTmpDir();
|
|
95
|
+
try {
|
|
96
|
+
writeFileSync(join(dir, "a.ts"), 'export const a = 1;\n');
|
|
97
|
+
writeFileSync(join(dir, "b.ts"), 'export const b = 2;\n');
|
|
98
|
+
|
|
99
|
+
const store = new SarifStore({
|
|
100
|
+
workspaceRoot: dir,
|
|
101
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Ingest findings for both files
|
|
105
|
+
const doc = wrapResultsInSarif("eslint" as never, "0.1.0", [
|
|
106
|
+
{
|
|
107
|
+
ruleId: "no-unused-vars",
|
|
108
|
+
level: "warning",
|
|
109
|
+
message: { text: "unused in a" },
|
|
110
|
+
locations: [{
|
|
111
|
+
physicalLocation: {
|
|
112
|
+
artifactLocation: { uri: "a.ts" },
|
|
113
|
+
region: { startLine: 1, startColumn: 1 },
|
|
114
|
+
},
|
|
115
|
+
}],
|
|
116
|
+
properties: { sourceTool: "eslint", effortMinutes: 30 },
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
ruleId: "no-unused-vars",
|
|
120
|
+
level: "warning",
|
|
121
|
+
message: { text: "unused in b" },
|
|
122
|
+
locations: [{
|
|
123
|
+
physicalLocation: {
|
|
124
|
+
artifactLocation: { uri: "b.ts" },
|
|
125
|
+
region: { startLine: 1, startColumn: 1 },
|
|
126
|
+
},
|
|
127
|
+
}],
|
|
128
|
+
properties: { sourceTool: "eslint", effortMinutes: 30 },
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
store.ingestRun(doc, "eslint");
|
|
132
|
+
|
|
133
|
+
const result = await buildFileDetail({
|
|
134
|
+
relativePath: "a.ts",
|
|
135
|
+
workspaceRoot: dir,
|
|
136
|
+
astEngine: engine,
|
|
137
|
+
sarifStore: store,
|
|
138
|
+
cyclomaticMax: 15,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
assert.equal(result.findings.length, 1);
|
|
142
|
+
assert.ok(result.findings[0]!.message.includes("unused in a"));
|
|
143
|
+
assert.equal(result.summary.totalFindings, 1);
|
|
144
|
+
} finally {
|
|
145
|
+
rmSync(dir, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("throws for non-existent file", async () => {
|
|
150
|
+
const dir = makeTmpDir();
|
|
151
|
+
try {
|
|
152
|
+
const store = new SarifStore({
|
|
153
|
+
workspaceRoot: dir,
|
|
154
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
155
|
+
});
|
|
156
|
+
await assert.rejects(
|
|
157
|
+
buildFileDetail({
|
|
158
|
+
relativePath: "nonexistent.ts",
|
|
159
|
+
workspaceRoot: dir,
|
|
160
|
+
astEngine: engine,
|
|
161
|
+
sarifStore: store,
|
|
162
|
+
cyclomaticMax: 15,
|
|
163
|
+
}),
|
|
164
|
+
/not found|ENOENT/i,
|
|
165
|
+
);
|
|
166
|
+
} finally {
|
|
167
|
+
rmSync(dir, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("rejects path traversal attempts", async () => {
|
|
172
|
+
const dir = makeTmpDir();
|
|
173
|
+
try {
|
|
174
|
+
const store = new SarifStore({
|
|
175
|
+
workspaceRoot: dir,
|
|
176
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
177
|
+
});
|
|
178
|
+
await assert.rejects(
|
|
179
|
+
buildFileDetail({
|
|
180
|
+
relativePath: "../../etc/passwd",
|
|
181
|
+
workspaceRoot: dir,
|
|
182
|
+
astEngine: engine,
|
|
183
|
+
sarifStore: store,
|
|
184
|
+
cyclomaticMax: 15,
|
|
185
|
+
}),
|
|
186
|
+
/escapes the workspace/i,
|
|
187
|
+
);
|
|
188
|
+
} finally {
|
|
189
|
+
rmSync(dir, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("returns empty functions for unsupported languages", async () => {
|
|
194
|
+
const dir = makeTmpDir();
|
|
195
|
+
try {
|
|
196
|
+
writeFileSync(join(dir, "data.json"), '{"key": "value"}\n');
|
|
197
|
+
const store = new SarifStore({
|
|
198
|
+
workspaceRoot: dir,
|
|
199
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
200
|
+
});
|
|
201
|
+
const result = await buildFileDetail({
|
|
202
|
+
relativePath: "data.json",
|
|
203
|
+
workspaceRoot: dir,
|
|
204
|
+
astEngine: engine,
|
|
205
|
+
sarifStore: store,
|
|
206
|
+
cyclomaticMax: 15,
|
|
207
|
+
});
|
|
208
|
+
assert.equal(result.language, null);
|
|
209
|
+
assert.equal(result.functions.length, 0);
|
|
210
|
+
} finally {
|
|
211
|
+
rmSync(dir, { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("computes summary with correct effort and complexity stats", async () => {
|
|
216
|
+
const dir = makeTmpDir();
|
|
217
|
+
try {
|
|
218
|
+
writeFileSync(join(dir, "hello.ts"), SAMPLE_TS);
|
|
219
|
+
const store = new SarifStore({
|
|
220
|
+
workspaceRoot: dir,
|
|
221
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const doc = wrapResultsInSarif("eslint" as never, "0.1.0", [
|
|
225
|
+
{
|
|
226
|
+
ruleId: "no-magic-numbers",
|
|
227
|
+
level: "warning",
|
|
228
|
+
message: { text: "magic number" },
|
|
229
|
+
locations: [{
|
|
230
|
+
physicalLocation: {
|
|
231
|
+
artifactLocation: { uri: "hello.ts" },
|
|
232
|
+
region: { startLine: 6, startColumn: 7 },
|
|
233
|
+
},
|
|
234
|
+
}],
|
|
235
|
+
properties: { sourceTool: "eslint", effortMinutes: 30 },
|
|
236
|
+
},
|
|
237
|
+
]);
|
|
238
|
+
store.ingestRun(doc, "eslint");
|
|
239
|
+
|
|
240
|
+
const result = await buildFileDetail({
|
|
241
|
+
relativePath: "hello.ts",
|
|
242
|
+
workspaceRoot: dir,
|
|
243
|
+
astEngine: engine,
|
|
244
|
+
sarifStore: store,
|
|
245
|
+
cyclomaticMax: 15,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
assert.equal(result.summary.totalFindings, 1);
|
|
249
|
+
assert.equal(result.summary.warningCount, 1);
|
|
250
|
+
assert.equal(result.summary.errorCount, 0);
|
|
251
|
+
assert.ok(result.summary.totalEffortMinutes >= 30);
|
|
252
|
+
assert.ok(result.summary.maxComplexity >= 4); // classify has CC>=4
|
|
253
|
+
assert.ok(result.summary.avgComplexity > 0);
|
|
254
|
+
} finally {
|
|
255
|
+
rmSync(dir, { recursive: true, force: true });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
});
|