claude-crap 0.3.7 → 0.4.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 +33 -0
- package/README.md +74 -7
- package/dist/adapters/common.d.ts +1 -1
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +1 -1
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/dart-analyzer.d.ts +41 -0
- package/dist/adapters/dart-analyzer.d.ts.map +1 -0
- package/dist/adapters/dart-analyzer.js +120 -0
- package/dist/adapters/dart-analyzer.js.map +1 -0
- package/dist/adapters/dotnet-format.d.ts +35 -0
- package/dist/adapters/dotnet-format.d.ts.map +1 -0
- package/dist/adapters/dotnet-format.js +96 -0
- package/dist/adapters/dotnet-format.js.map +1 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/crap-config.d.ts +4 -0
- package/dist/crap-config.d.ts.map +1 -1
- package/dist/crap-config.js +51 -28
- package/dist/crap-config.js.map +1 -1
- package/dist/dashboard/file-detail.d.ts.map +1 -1
- package/dist/dashboard/file-detail.js.map +1 -1
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +7 -12
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +89 -5
- package/dist/index.js.map +1 -1
- package/dist/metrics/workspace-walker.d.ts +4 -1
- package/dist/metrics/workspace-walker.d.ts.map +1 -1
- package/dist/metrics/workspace-walker.js +12 -28
- package/dist/metrics/workspace-walker.js.map +1 -1
- package/dist/monorepo/project-map.d.ts +112 -0
- package/dist/monorepo/project-map.d.ts.map +1 -0
- package/dist/monorepo/project-map.js +384 -0
- package/dist/monorepo/project-map.js.map +1 -0
- package/dist/scanner/auto-scan.d.ts +1 -0
- package/dist/scanner/auto-scan.d.ts.map +1 -1
- package/dist/scanner/auto-scan.js +14 -5
- package/dist/scanner/auto-scan.js.map +1 -1
- package/dist/scanner/bootstrap.d.ts +1 -1
- package/dist/scanner/bootstrap.d.ts.map +1 -1
- package/dist/scanner/bootstrap.js +15 -1
- package/dist/scanner/bootstrap.js.map +1 -1
- package/dist/scanner/complexity-scanner.d.ts +2 -0
- package/dist/scanner/complexity-scanner.d.ts.map +1 -1
- package/dist/scanner/complexity-scanner.js +11 -26
- package/dist/scanner/complexity-scanner.js.map +1 -1
- package/dist/scanner/detector.d.ts +24 -4
- package/dist/scanner/detector.d.ts.map +1 -1
- package/dist/scanner/detector.js +110 -10
- package/dist/scanner/detector.js.map +1 -1
- package/dist/scanner/runner.d.ts +4 -1
- package/dist/scanner/runner.d.ts.map +1 -1
- package/dist/scanner/runner.js +25 -3
- package/dist/scanner/runner.js.map +1 -1
- package/dist/schemas/tool-schemas.d.ts +16 -1
- package/dist/schemas/tool-schemas.d.ts.map +1 -1
- package/dist/schemas/tool-schemas.js +16 -1
- package/dist/schemas/tool-schemas.js.map +1 -1
- package/dist/shared/exclusions.d.ts +53 -0
- package/dist/shared/exclusions.d.ts.map +1 -0
- package/dist/shared/exclusions.js +126 -0
- package/dist/shared/exclusions.js.map +1 -0
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +37 -0
- package/plugin/bundle/mcp-server.mjs +762 -144
- package/plugin/bundle/mcp-server.mjs.map +4 -4
- package/plugin/package-lock.json +15 -2
- package/plugin/package.json +2 -1
- package/scripts/bundle-plugin.mjs +2 -1
- package/src/adapters/common.ts +1 -1
- package/src/adapters/dart-analyzer.ts +161 -0
- package/src/adapters/dotnet-format.ts +125 -0
- package/src/adapters/index.ts +8 -0
- package/src/crap-config.ts +78 -18
- package/src/dashboard/file-detail.ts +0 -2
- package/src/dashboard/server.ts +9 -10
- package/src/index.ts +103 -5
- package/src/metrics/workspace-walker.ts +15 -27
- package/src/monorepo/project-map.ts +476 -0
- package/src/scanner/auto-scan.ts +17 -6
- package/src/scanner/bootstrap.ts +18 -1
- package/src/scanner/complexity-scanner.ts +15 -26
- package/src/scanner/detector.ts +119 -10
- package/src/scanner/runner.ts +25 -2
- package/src/schemas/tool-schemas.ts +17 -1
- package/src/shared/exclusions.ts +156 -0
- package/src/tests/adapters/dispatch.test.ts +2 -2
- package/src/tests/auto-scan.test.ts +2 -2
- package/src/tests/boot-monorepo.test.ts +804 -0
- package/src/tests/boot-scanner-detection.test.ts +692 -0
- package/src/tests/boot-single-project.test.ts +780 -0
- package/src/tests/exclusions.test.ts +117 -0
- package/src/tests/integration/mcp-server.integration.test.ts +2 -1
- package/src/tests/project-map.test.ts +302 -0
- package/src/tests/scanner-detector.test.ts +31 -11
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_SKIP_DIRS,
|
|
6
|
+
DEFAULT_SKIP_PATTERNS,
|
|
7
|
+
createExclusionFilter,
|
|
8
|
+
} from "../shared/exclusions.js";
|
|
9
|
+
|
|
10
|
+
describe("DEFAULT_SKIP_DIRS", () => {
|
|
11
|
+
it("includes core directories", () => {
|
|
12
|
+
for (const dir of ["node_modules", ".git", "dist", "build", "bundle", "out", "target", "coverage", "vendor"]) {
|
|
13
|
+
assert.ok(DEFAULT_SKIP_DIRS.has(dir), `missing: ${dir}`);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("includes framework build outputs", () => {
|
|
18
|
+
for (const dir of [".next", ".nuxt", ".output", ".vercel", ".svelte-kit", ".astro", ".angular", ".turbo", ".parcel-cache", ".expo"]) {
|
|
19
|
+
assert.ok(DEFAULT_SKIP_DIRS.has(dir), `missing: ${dir}`);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("includes language-specific caches", () => {
|
|
24
|
+
for (const dir of [".venv", "venv", "__pycache__", ".cache", ".dart_tool", ".gradle"]) {
|
|
25
|
+
assert.ok(DEFAULT_SKIP_DIRS.has(dir), `missing: ${dir}`);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("includes plugin state dirs", () => {
|
|
30
|
+
for (const dir of [".claude-crap", ".codesight"]) {
|
|
31
|
+
assert.ok(DEFAULT_SKIP_DIRS.has(dir), `missing: ${dir}`);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("DEFAULT_SKIP_PATTERNS", () => {
|
|
37
|
+
it("includes minified and bundled file patterns", () => {
|
|
38
|
+
const patterns = new Set(DEFAULT_SKIP_PATTERNS);
|
|
39
|
+
assert.ok(patterns.has("*.min.js"));
|
|
40
|
+
assert.ok(patterns.has("*.min.css"));
|
|
41
|
+
assert.ok(patterns.has("*.bundle.js"));
|
|
42
|
+
assert.ok(patterns.has("*.chunk.js"));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("createExclusionFilter", () => {
|
|
47
|
+
describe("shouldSkipDir", () => {
|
|
48
|
+
it("skips default directories", () => {
|
|
49
|
+
const filter = createExclusionFilter();
|
|
50
|
+
assert.equal(filter.shouldSkipDir("node_modules"), true);
|
|
51
|
+
assert.equal(filter.shouldSkipDir("dist"), true);
|
|
52
|
+
assert.equal(filter.shouldSkipDir("bundle"), true);
|
|
53
|
+
assert.equal(filter.shouldSkipDir(".next"), true);
|
|
54
|
+
assert.equal(filter.shouldSkipDir(".dart_tool"), true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("allows normal directories", () => {
|
|
58
|
+
const filter = createExclusionFilter();
|
|
59
|
+
assert.equal(filter.shouldSkipDir("src"), false);
|
|
60
|
+
assert.equal(filter.shouldSkipDir("lib"), false);
|
|
61
|
+
assert.equal(filter.shouldSkipDir("apps"), false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("skips hidden directories except .claude-plugin", () => {
|
|
65
|
+
const filter = createExclusionFilter();
|
|
66
|
+
assert.equal(filter.shouldSkipDir(".hidden"), true);
|
|
67
|
+
assert.equal(filter.shouldSkipDir(".secret"), true);
|
|
68
|
+
assert.equal(filter.shouldSkipDir(".claude-plugin"), false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("respects user directory exclusions with trailing slash", () => {
|
|
72
|
+
const filter = createExclusionFilter(["legacy/", "generated/"]);
|
|
73
|
+
assert.equal(filter.shouldSkipDir("legacy"), true);
|
|
74
|
+
assert.equal(filter.shouldSkipDir("generated"), true);
|
|
75
|
+
assert.equal(filter.shouldSkipDir("src"), false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("shouldSkipFile", () => {
|
|
80
|
+
it("skips default minified patterns", () => {
|
|
81
|
+
const filter = createExclusionFilter();
|
|
82
|
+
assert.equal(filter.shouldSkipFile("lib/app.min.js", "app.min.js"), true);
|
|
83
|
+
assert.equal(filter.shouldSkipFile("styles/main.min.css", "main.min.css"), true);
|
|
84
|
+
assert.equal(filter.shouldSkipFile("lib/vendor.bundle.js", "vendor.bundle.js"), true);
|
|
85
|
+
assert.equal(filter.shouldSkipFile("lib/0.chunk.js", "0.chunk.js"), true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("allows normal source files", () => {
|
|
89
|
+
const filter = createExclusionFilter();
|
|
90
|
+
assert.equal(filter.shouldSkipFile("src/index.ts", "index.ts"), false);
|
|
91
|
+
assert.equal(filter.shouldSkipFile("lib/utils.js", "utils.js"), false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("applies user glob patterns to filenames", () => {
|
|
95
|
+
const filter = createExclusionFilter(["*.proto.ts"]);
|
|
96
|
+
assert.equal(filter.shouldSkipFile("src/api/service.proto.ts", "service.proto.ts"), true);
|
|
97
|
+
assert.equal(filter.shouldSkipFile("src/api/service.ts", "service.ts"), false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("applies user glob patterns to relative paths", () => {
|
|
101
|
+
const filter = createExclusionFilter(["src/generated/**"]);
|
|
102
|
+
assert.equal(filter.shouldSkipFile("src/generated/types.ts", "types.ts"), true);
|
|
103
|
+
assert.equal(filter.shouldSkipFile("src/real/types.ts", "types.ts"), false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("works with empty user exclusions", () => {
|
|
107
|
+
const filter = createExclusionFilter([]);
|
|
108
|
+
assert.equal(filter.shouldSkipFile("src/index.ts", "index.ts"), false);
|
|
109
|
+
assert.equal(filter.shouldSkipFile("lib/app.min.js", "app.min.js"), true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("works with undefined user exclusions", () => {
|
|
113
|
+
const filter = createExclusionFilter();
|
|
114
|
+
assert.equal(filter.shouldSkipFile("src/index.ts", "index.ts"), false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -232,7 +232,7 @@ describe("MCP server integration", { skip: !serverBuilt }, () => {
|
|
|
232
232
|
assert.ok(child && !child.killed, "server child should be running");
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
-
it("exposes all
|
|
235
|
+
it("exposes all eleven tools via tools/list", async () => {
|
|
236
236
|
const response = await client!.request<{ result?: { tools?: Array<{ name: string }> } }>(
|
|
237
237
|
"tools/list",
|
|
238
238
|
);
|
|
@@ -245,6 +245,7 @@ describe("MCP server integration", { skip: !serverBuilt }, () => {
|
|
|
245
245
|
"compute_tdr",
|
|
246
246
|
"ingest_sarif",
|
|
247
247
|
"ingest_scanner_output",
|
|
248
|
+
"list_projects",
|
|
248
249
|
"require_test_harness",
|
|
249
250
|
"score_project",
|
|
250
251
|
]);
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the project-map discovery module.
|
|
3
|
+
*
|
|
4
|
+
* Covers workspace classification (single-project vs monorepo),
|
|
5
|
+
* per-project language detection across all supported project types,
|
|
6
|
+
* deduplication when a workspace appears in both npm workspaces and
|
|
7
|
+
* directory scan results, and the persist/load round-trip.
|
|
8
|
+
*
|
|
9
|
+
* Each test creates a fresh temporary directory tree and removes it in
|
|
10
|
+
* a `finally` block so failures leave no artefacts on disk.
|
|
11
|
+
*
|
|
12
|
+
* @module tests/project-map.test
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it } from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
discoverProjectMap,
|
|
23
|
+
persistProjectMap,
|
|
24
|
+
loadProjectMap,
|
|
25
|
+
type ProjectMap,
|
|
26
|
+
type ProjectEntry,
|
|
27
|
+
} from "../monorepo/project-map.js";
|
|
28
|
+
|
|
29
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function makeTmpDir(): string {
|
|
32
|
+
return mkdtempSync(join(tmpdir(), "crap-projmap-"));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Write a file at `absPath`, creating all parent directories first.
|
|
37
|
+
*/
|
|
38
|
+
function touch(absPath: string, content = ""): void {
|
|
39
|
+
mkdirSync(join(absPath, ".."), { recursive: true });
|
|
40
|
+
writeFileSync(absPath, content, "utf8");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Return the ProjectEntry for the given relative path, or throw if absent.
|
|
45
|
+
*/
|
|
46
|
+
function findProject(map: ProjectMap, relPath: string): ProjectEntry {
|
|
47
|
+
const entry = map.projects.find((p) => p.path === relPath);
|
|
48
|
+
assert.ok(entry, `expected project at path "${relPath}" — found: ${map.projects.map((p) => p.path).join(", ")}`);
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── discoverProjectMap ────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
describe("discoverProjectMap", () => {
|
|
55
|
+
it("single-project workspace is not a monorepo and has no sub-projects", async () => {
|
|
56
|
+
const dir = makeTmpDir();
|
|
57
|
+
try {
|
|
58
|
+
writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "my-app" }));
|
|
59
|
+
writeFileSync(join(dir, "tsconfig.json"), "{}");
|
|
60
|
+
|
|
61
|
+
const map = await discoverProjectMap(dir);
|
|
62
|
+
|
|
63
|
+
assert.equal(map.isMonorepo, false);
|
|
64
|
+
assert.deepEqual(map.projects, []);
|
|
65
|
+
assert.equal(map.workspaceRoot, dir);
|
|
66
|
+
assert.equal(typeof map.generatedAt, "string");
|
|
67
|
+
} finally {
|
|
68
|
+
rmSync(dir, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("npm workspaces monorepo detects both sub-projects with correct types", async () => {
|
|
73
|
+
const dir = makeTmpDir();
|
|
74
|
+
try {
|
|
75
|
+
// Root manifest declares workspaces
|
|
76
|
+
writeFileSync(
|
|
77
|
+
join(dir, "package.json"),
|
|
78
|
+
JSON.stringify({ name: "root", workspaces: ["apps/web", "apps/api"] }),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// apps/web — TypeScript project
|
|
82
|
+
mkdirSync(join(dir, "apps", "web"), { recursive: true });
|
|
83
|
+
writeFileSync(join(dir, "apps", "web", "package.json"), JSON.stringify({ name: "web" }));
|
|
84
|
+
writeFileSync(join(dir, "apps", "web", "tsconfig.json"), "{}");
|
|
85
|
+
|
|
86
|
+
// apps/api — plain JavaScript project (no tsconfig)
|
|
87
|
+
mkdirSync(join(dir, "apps", "api"), { recursive: true });
|
|
88
|
+
writeFileSync(join(dir, "apps", "api", "package.json"), JSON.stringify({ name: "api" }));
|
|
89
|
+
|
|
90
|
+
const map = await discoverProjectMap(dir);
|
|
91
|
+
|
|
92
|
+
assert.equal(map.isMonorepo, true);
|
|
93
|
+
assert.equal(map.projects.length, 2);
|
|
94
|
+
|
|
95
|
+
const web = findProject(map, "apps/web");
|
|
96
|
+
assert.equal(web.type, "typescript");
|
|
97
|
+
assert.equal(web.name, "web");
|
|
98
|
+
|
|
99
|
+
const api = findProject(map, "apps/api");
|
|
100
|
+
assert.equal(api.type, "javascript");
|
|
101
|
+
assert.equal(api.name, "api");
|
|
102
|
+
} finally {
|
|
103
|
+
rmSync(dir, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("mixed monorepo discovers both an npm workspace and a Dart project", async () => {
|
|
108
|
+
const dir = makeTmpDir();
|
|
109
|
+
try {
|
|
110
|
+
// Root declares only apps/web in workspaces; apps/mobile is found by scan
|
|
111
|
+
writeFileSync(
|
|
112
|
+
join(dir, "package.json"),
|
|
113
|
+
JSON.stringify({ name: "root", workspaces: ["apps/web"] }),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// apps/web — TypeScript
|
|
117
|
+
mkdirSync(join(dir, "apps", "web"), { recursive: true });
|
|
118
|
+
writeFileSync(join(dir, "apps", "web", "package.json"), JSON.stringify({ name: "web" }));
|
|
119
|
+
writeFileSync(join(dir, "apps", "web", "tsconfig.json"), "{}");
|
|
120
|
+
|
|
121
|
+
// apps/mobile — Dart / Flutter (discovered via directory scan, not workspaces)
|
|
122
|
+
mkdirSync(join(dir, "apps", "mobile"), { recursive: true });
|
|
123
|
+
writeFileSync(
|
|
124
|
+
join(dir, "apps", "mobile", "pubspec.yaml"),
|
|
125
|
+
"name: mobile\nenvironment:\n sdk: '>=3.0.0 <4.0.0'\n",
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const map = await discoverProjectMap(dir);
|
|
129
|
+
|
|
130
|
+
assert.equal(map.isMonorepo, true);
|
|
131
|
+
|
|
132
|
+
const web = findProject(map, "apps/web");
|
|
133
|
+
assert.equal(web.type, "typescript");
|
|
134
|
+
|
|
135
|
+
const mobile = findProject(map, "apps/mobile");
|
|
136
|
+
assert.equal(mobile.type, "dart");
|
|
137
|
+
} finally {
|
|
138
|
+
rmSync(dir, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("Python project is detected with type 'python' and scanner 'bandit'", async () => {
|
|
143
|
+
const dir = makeTmpDir();
|
|
144
|
+
try {
|
|
145
|
+
// Root with no workspaces — but has a sub-directory with Python signals
|
|
146
|
+
writeFileSync(
|
|
147
|
+
join(dir, "package.json"),
|
|
148
|
+
JSON.stringify({ name: "root", workspaces: ["apps/ml"] }),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
mkdirSync(join(dir, "apps", "ml"), { recursive: true });
|
|
152
|
+
writeFileSync(
|
|
153
|
+
join(dir, "apps", "ml", "pyproject.toml"),
|
|
154
|
+
"[project]\nname = \"ml\"\n",
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const map = await discoverProjectMap(dir);
|
|
158
|
+
|
|
159
|
+
const ml = findProject(map, "apps/ml");
|
|
160
|
+
assert.equal(ml.type, "python");
|
|
161
|
+
assert.equal(ml.scanner, "bandit");
|
|
162
|
+
} finally {
|
|
163
|
+
rmSync(dir, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("Java project is detected with type 'java' and scanner 'dotnet_format'", async () => {
|
|
168
|
+
const dir = makeTmpDir();
|
|
169
|
+
try {
|
|
170
|
+
writeFileSync(
|
|
171
|
+
join(dir, "package.json"),
|
|
172
|
+
JSON.stringify({ name: "root", workspaces: ["apps/backend"] }),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
mkdirSync(join(dir, "apps", "backend"), { recursive: true });
|
|
176
|
+
writeFileSync(
|
|
177
|
+
join(dir, "apps", "backend", "pom.xml"),
|
|
178
|
+
"<project><modelVersion>4.0.0</modelVersion></project>",
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const map = await discoverProjectMap(dir);
|
|
182
|
+
|
|
183
|
+
const backend = findProject(map, "apps/backend");
|
|
184
|
+
assert.equal(backend.type, "java");
|
|
185
|
+
assert.equal(backend.scanner, "semgrep");
|
|
186
|
+
} finally {
|
|
187
|
+
rmSync(dir, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("C# project is detected with type 'csharp' and scanner 'dotnet_format'", async () => {
|
|
192
|
+
const dir = makeTmpDir();
|
|
193
|
+
try {
|
|
194
|
+
writeFileSync(
|
|
195
|
+
join(dir, "package.json"),
|
|
196
|
+
JSON.stringify({ name: "root", workspaces: ["apps/api"] }),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
mkdirSync(join(dir, "apps", "api"), { recursive: true });
|
|
200
|
+
writeFileSync(
|
|
201
|
+
join(dir, "apps", "api", "MyApp.csproj"),
|
|
202
|
+
"<Project Sdk=\"Microsoft.NET.Sdk\"></Project>",
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const map = await discoverProjectMap(dir);
|
|
206
|
+
|
|
207
|
+
const api = findProject(map, "apps/api");
|
|
208
|
+
assert.equal(api.type, "csharp");
|
|
209
|
+
assert.equal(api.scanner, "dotnet_format");
|
|
210
|
+
} finally {
|
|
211
|
+
rmSync(dir, { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("empty workspace returns isMonorepo false and an empty projects array", async () => {
|
|
216
|
+
const dir = makeTmpDir();
|
|
217
|
+
try {
|
|
218
|
+
const map = await discoverProjectMap(dir);
|
|
219
|
+
|
|
220
|
+
assert.equal(map.isMonorepo, false);
|
|
221
|
+
assert.deepEqual(map.projects, []);
|
|
222
|
+
} finally {
|
|
223
|
+
rmSync(dir, { recursive: true, force: true });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("project listed in npm workspaces AND found by directory scan appears only once", async () => {
|
|
228
|
+
const dir = makeTmpDir();
|
|
229
|
+
try {
|
|
230
|
+
// apps/shared is declared in workspaces; it also lives inside apps/
|
|
231
|
+
// which the directory scanner would naturally traverse
|
|
232
|
+
writeFileSync(
|
|
233
|
+
join(dir, "package.json"),
|
|
234
|
+
JSON.stringify({ name: "root", workspaces: ["apps/shared"] }),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
mkdirSync(join(dir, "apps", "shared"), { recursive: true });
|
|
238
|
+
writeFileSync(join(dir, "apps", "shared", "package.json"), JSON.stringify({ name: "shared" }));
|
|
239
|
+
writeFileSync(join(dir, "apps", "shared", "tsconfig.json"), "{}");
|
|
240
|
+
|
|
241
|
+
const map = await discoverProjectMap(dir);
|
|
242
|
+
|
|
243
|
+
const matches = map.projects.filter((p) => p.path === "apps/shared");
|
|
244
|
+
assert.equal(
|
|
245
|
+
matches.length,
|
|
246
|
+
1,
|
|
247
|
+
`expected exactly 1 entry for "apps/shared", got ${matches.length}`,
|
|
248
|
+
);
|
|
249
|
+
} finally {
|
|
250
|
+
rmSync(dir, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ── persistProjectMap / loadProjectMap ───────────────────────────────
|
|
256
|
+
|
|
257
|
+
describe("persistProjectMap / loadProjectMap", () => {
|
|
258
|
+
it("persisted map round-trips through loadProjectMap with deep equality", async () => {
|
|
259
|
+
const dir = makeTmpDir();
|
|
260
|
+
try {
|
|
261
|
+
const original: ProjectMap = {
|
|
262
|
+
generatedAt: new Date().toISOString(),
|
|
263
|
+
workspaceRoot: dir,
|
|
264
|
+
isMonorepo: true,
|
|
265
|
+
projects: [
|
|
266
|
+
{
|
|
267
|
+
name: "web",
|
|
268
|
+
path: "apps/web",
|
|
269
|
+
type: "typescript",
|
|
270
|
+
scanner: "eslint",
|
|
271
|
+
scannerAvailable: true,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: "ml",
|
|
275
|
+
path: "apps/ml",
|
|
276
|
+
type: "python",
|
|
277
|
+
scanner: "bandit",
|
|
278
|
+
scannerAvailable: false,
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
await persistProjectMap(original, dir);
|
|
284
|
+
const loaded = loadProjectMap(dir);
|
|
285
|
+
|
|
286
|
+
assert.ok(loaded !== null, "loadProjectMap returned null after persist");
|
|
287
|
+
assert.deepEqual(loaded, original);
|
|
288
|
+
} finally {
|
|
289
|
+
rmSync(dir, { recursive: true, force: true });
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("loadProjectMap returns null when no persisted file exists", () => {
|
|
294
|
+
const dir = makeTmpDir();
|
|
295
|
+
try {
|
|
296
|
+
const result = loadProjectMap(dir);
|
|
297
|
+
assert.equal(result, null);
|
|
298
|
+
} finally {
|
|
299
|
+
rmSync(dir, { recursive: true, force: true });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -90,24 +90,34 @@ describe("detectScanners", () => {
|
|
|
90
90
|
}
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
it("detects eslint from package.json
|
|
93
|
+
it("detects eslint from package.json — not installed vs installed", async () => {
|
|
94
94
|
const dir = makeTmpDir();
|
|
95
95
|
try {
|
|
96
96
|
writeFileSync(
|
|
97
97
|
join(dir, "package.json"),
|
|
98
98
|
JSON.stringify({ devDependencies: { eslint: "^9.0.0" } }),
|
|
99
99
|
);
|
|
100
|
+
// Without node_modules/.bin/eslint — declared but not installed
|
|
100
101
|
const results = await detectScanners(dir);
|
|
101
102
|
const eslint = results.find((r) => r.scanner === "eslint");
|
|
102
103
|
assert.ok(eslint);
|
|
103
|
-
assert.equal(eslint.available,
|
|
104
|
-
assert.ok(eslint.reason.includes("
|
|
104
|
+
assert.equal(eslint.available, false);
|
|
105
|
+
assert.ok(eslint.reason.includes("not installed"));
|
|
106
|
+
|
|
107
|
+
// With binary present — installed
|
|
108
|
+
mkdirSync(join(dir, "node_modules", ".bin"), { recursive: true });
|
|
109
|
+
writeFileSync(join(dir, "node_modules", ".bin", "eslint"), "");
|
|
110
|
+
const results2 = await detectScanners(dir);
|
|
111
|
+
const eslint2 = results2.find((r) => r.scanner === "eslint");
|
|
112
|
+
assert.ok(eslint2);
|
|
113
|
+
assert.equal(eslint2.available, true);
|
|
114
|
+
assert.ok(eslint2.reason.includes("installed"));
|
|
105
115
|
} finally {
|
|
106
116
|
rmSync(dir, { recursive: true, force: true });
|
|
107
117
|
}
|
|
108
118
|
});
|
|
109
119
|
|
|
110
|
-
it("detects stryker from package.json
|
|
120
|
+
it("detects stryker from package.json — not installed vs installed", async () => {
|
|
111
121
|
const dir = makeTmpDir();
|
|
112
122
|
try {
|
|
113
123
|
writeFileSync(
|
|
@@ -116,11 +126,21 @@ describe("detectScanners", () => {
|
|
|
116
126
|
devDependencies: { "@stryker-mutator/core": "^7.0.0" },
|
|
117
127
|
}),
|
|
118
128
|
);
|
|
129
|
+
// Without binary — declared but not installed
|
|
119
130
|
const results = await detectScanners(dir);
|
|
120
131
|
const stryker = results.find((r) => r.scanner === "stryker");
|
|
121
132
|
assert.ok(stryker);
|
|
122
|
-
assert.equal(stryker.available,
|
|
123
|
-
assert.ok(stryker.reason.includes("
|
|
133
|
+
assert.equal(stryker.available, false);
|
|
134
|
+
assert.ok(stryker.reason.includes("not installed"));
|
|
135
|
+
|
|
136
|
+
// With binary present — installed
|
|
137
|
+
mkdirSync(join(dir, "node_modules", ".bin"), { recursive: true });
|
|
138
|
+
writeFileSync(join(dir, "node_modules", ".bin", "stryker"), "");
|
|
139
|
+
const results2 = await detectScanners(dir);
|
|
140
|
+
const stryker2 = results2.find((r) => r.scanner === "stryker");
|
|
141
|
+
assert.ok(stryker2);
|
|
142
|
+
assert.equal(stryker2.available, true);
|
|
143
|
+
assert.ok(stryker2.reason.includes("installed"));
|
|
124
144
|
} finally {
|
|
125
145
|
rmSync(dir, { recursive: true, force: true });
|
|
126
146
|
}
|
|
@@ -133,9 +153,9 @@ describe("detectScanners", () => {
|
|
|
133
153
|
// Config and package.json probes will all fail.
|
|
134
154
|
// Binary probe results depend on the host — don't assert on those,
|
|
135
155
|
// but do assert the structure is correct.
|
|
136
|
-
assert.equal(results.length,
|
|
156
|
+
assert.equal(results.length, 6);
|
|
137
157
|
for (const r of results) {
|
|
138
|
-
assert.ok(["eslint", "semgrep", "bandit", "stryker"].includes(r.scanner));
|
|
158
|
+
assert.ok(["eslint", "semgrep", "bandit", "stryker", "dart_analyze", "dotnet_format"].includes(r.scanner));
|
|
139
159
|
assert.equal(typeof r.available, "boolean");
|
|
140
160
|
assert.equal(typeof r.reason, "string");
|
|
141
161
|
}
|
|
@@ -150,7 +170,7 @@ describe("detectScanners", () => {
|
|
|
150
170
|
writeFileSync(join(dir, "package.json"), "not json at all");
|
|
151
171
|
// Should not throw — just skip the package.json probe
|
|
152
172
|
const results = await detectScanners(dir);
|
|
153
|
-
assert.equal(results.length,
|
|
173
|
+
assert.equal(results.length, 6);
|
|
154
174
|
} finally {
|
|
155
175
|
rmSync(dir, { recursive: true, force: true });
|
|
156
176
|
}
|
|
@@ -172,10 +192,10 @@ describe("detectScanners", () => {
|
|
|
172
192
|
}
|
|
173
193
|
});
|
|
174
194
|
|
|
175
|
-
it("SCANNER_SIGNALS covers all
|
|
195
|
+
it("SCANNER_SIGNALS covers all supported scanners", () => {
|
|
176
196
|
assert.deepEqual(
|
|
177
197
|
Object.keys(SCANNER_SIGNALS).sort(),
|
|
178
|
-
["bandit", "eslint", "semgrep", "stryker"],
|
|
198
|
+
["bandit", "dart_analyze", "dotnet_format", "eslint", "semgrep", "stryker"],
|
|
179
199
|
);
|
|
180
200
|
});
|
|
181
201
|
});
|