claude-crap 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +44 -23
  3. package/dist/index.js +142 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/scanner/auto-scan.d.ts +57 -0
  6. package/dist/scanner/auto-scan.d.ts.map +1 -0
  7. package/dist/scanner/auto-scan.js +138 -0
  8. package/dist/scanner/auto-scan.js.map +1 -0
  9. package/dist/scanner/bootstrap.d.ts +89 -0
  10. package/dist/scanner/bootstrap.d.ts.map +1 -0
  11. package/dist/scanner/bootstrap.js +278 -0
  12. package/dist/scanner/bootstrap.js.map +1 -0
  13. package/dist/scanner/detector.d.ts +53 -0
  14. package/dist/scanner/detector.d.ts.map +1 -0
  15. package/dist/scanner/detector.js +173 -0
  16. package/dist/scanner/detector.js.map +1 -0
  17. package/dist/scanner/index.d.ts +23 -0
  18. package/dist/scanner/index.d.ts.map +1 -0
  19. package/dist/scanner/index.js +23 -0
  20. package/dist/scanner/index.js.map +1 -0
  21. package/dist/scanner/runner.d.ts +59 -0
  22. package/dist/scanner/runner.d.ts.map +1 -0
  23. package/dist/scanner/runner.js +159 -0
  24. package/dist/scanner/runner.js.map +1 -0
  25. package/dist/schemas/tool-schemas.d.ts +23 -0
  26. package/dist/schemas/tool-schemas.d.ts.map +1 -1
  27. package/dist/schemas/tool-schemas.js +23 -0
  28. package/dist/schemas/tool-schemas.js.map +1 -1
  29. package/package.json +5 -1
  30. package/plugin/.claude-plugin/plugin.json +1 -1
  31. package/plugin/bundle/mcp-server.mjs +732 -0
  32. package/plugin/bundle/mcp-server.mjs.map +4 -4
  33. package/plugin/package.json +1 -1
  34. package/src/index.ts +176 -0
  35. package/src/scanner/auto-scan.ts +212 -0
  36. package/src/scanner/bootstrap.ts +383 -0
  37. package/src/scanner/detector.ts +224 -0
  38. package/src/scanner/index.ts +30 -0
  39. package/src/scanner/runner.ts +212 -0
  40. package/src/schemas/tool-schemas.ts +27 -0
  41. package/src/tests/auto-scan.test.ts +137 -0
  42. package/src/tests/integration/mcp-server.integration.test.ts +3 -1
  43. package/src/tests/scanner-bootstrap.test.ts +186 -0
  44. package/src/tests/scanner-detector.test.ts +181 -0
  45. package/src/tests/scanner-runner.test.ts +63 -0
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Execute a single scanner CLI and capture its raw output.
3
+ *
4
+ * Each scanner has a fixed invocation that produces the format its
5
+ * adapter expects:
6
+ *
7
+ * - ESLint → `npx eslint -f json .` (JSON array)
8
+ * - Semgrep → `semgrep --sarif --quiet .` (SARIF 2.1.0)
9
+ * - Bandit → `bandit -f json -r . -q` (JSON object)
10
+ * - Stryker → `npx stryker run` then read `reports/mutation/mutation.json`
11
+ *
12
+ * ESLint and Bandit exit non-zero when findings exist — that is
13
+ * expected, not an error. The runner captures stdout regardless of
14
+ * exit code for those scanners.
15
+ *
16
+ * Stryker is special: it writes to a file instead of stdout, so we
17
+ * read the output file after the process exits.
18
+ *
19
+ * @module scanner/runner
20
+ */
21
+
22
+ import { execFile } from "node:child_process";
23
+ import { readFileSync, existsSync } from "node:fs";
24
+ import { join } from "node:path";
25
+ import type { KnownScanner } from "../adapters/common.js";
26
+
27
+ // ── Types ──────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Result of executing a single scanner.
31
+ */
32
+ export interface ScannerRunResult {
33
+ /** Which scanner was executed. */
34
+ scanner: KnownScanner;
35
+ /** Whether execution completed and produced parseable output. */
36
+ success: boolean;
37
+ /** The scanner's raw output (stdout or file contents). */
38
+ rawOutput: string;
39
+ /** Error message when `success` is false. */
40
+ error?: string;
41
+ /** Wall-clock execution time in milliseconds. */
42
+ durationMs: number;
43
+ }
44
+
45
+ // ── Scanner command definitions ────────────────────────────────────
46
+
47
+ interface ScannerCommand {
48
+ /** Binary or npx command. */
49
+ command: string;
50
+ /** CLI arguments. */
51
+ args: string[];
52
+ /** Maximum execution time in ms. */
53
+ timeoutMs: number;
54
+ /** If true, non-zero exit is expected when findings exist. */
55
+ nonZeroIsNormal: boolean;
56
+ /** If set, read output from this file instead of stdout. */
57
+ outputFile?: string;
58
+ }
59
+
60
+ function getScannerCommand(
61
+ scanner: KnownScanner,
62
+ workspaceRoot: string,
63
+ ): ScannerCommand {
64
+ switch (scanner) {
65
+ case "eslint":
66
+ return {
67
+ command: "npx",
68
+ args: ["eslint", "-f", "json", "."],
69
+ timeoutMs: 120_000,
70
+ nonZeroIsNormal: true,
71
+ };
72
+ case "semgrep":
73
+ return {
74
+ command: "semgrep",
75
+ args: ["--sarif", "--quiet", "."],
76
+ timeoutMs: 120_000,
77
+ nonZeroIsNormal: false,
78
+ };
79
+ case "bandit":
80
+ return {
81
+ command: "bandit",
82
+ args: ["-f", "json", "-r", ".", "-q"],
83
+ timeoutMs: 120_000,
84
+ nonZeroIsNormal: true,
85
+ };
86
+ case "stryker":
87
+ return {
88
+ command: "npx",
89
+ args: ["stryker", "run"],
90
+ timeoutMs: 300_000,
91
+ nonZeroIsNormal: false,
92
+ outputFile: join(workspaceRoot, "reports", "mutation", "mutation.json"),
93
+ };
94
+ }
95
+ }
96
+
97
+ // ── Public API ──────────────────────────────────────────────────────
98
+
99
+ /**
100
+ * Execute a scanner CLI and return its raw output.
101
+ *
102
+ * @param scanner Which scanner to run.
103
+ * @param workspaceRoot Absolute path to the project root (used as cwd).
104
+ * @returns A {@link ScannerRunResult} with stdout or file output.
105
+ */
106
+ export function runScanner(
107
+ scanner: KnownScanner,
108
+ workspaceRoot: string,
109
+ ): Promise<ScannerRunResult> {
110
+ const start = Date.now();
111
+ const cmd = getScannerCommand(scanner, workspaceRoot);
112
+
113
+ return new Promise((resolve) => {
114
+ execFile(
115
+ cmd.command,
116
+ cmd.args,
117
+ {
118
+ cwd: workspaceRoot,
119
+ timeout: cmd.timeoutMs,
120
+ maxBuffer: 50 * 1024 * 1024, // 50 MB — large codebases produce verbose output
121
+ env: { ...process.env, FORCE_COLOR: "0" }, // suppress ANSI in output
122
+ },
123
+ (err, stdout, stderr) => {
124
+ const durationMs = Date.now() - start;
125
+
126
+ // For scanners where non-zero exit means "findings exist",
127
+ // we still have valid output in stdout.
128
+ if (err && !cmd.nonZeroIsNormal) {
129
+ // Stryker: check if the output file was written despite the error
130
+ if (cmd.outputFile && existsSync(cmd.outputFile)) {
131
+ try {
132
+ const fileOutput = readFileSync(cmd.outputFile, "utf-8");
133
+ resolve({
134
+ scanner,
135
+ success: true,
136
+ rawOutput: fileOutput,
137
+ durationMs,
138
+ });
139
+ return;
140
+ } catch {
141
+ // Fall through to error path
142
+ }
143
+ }
144
+
145
+ resolve({
146
+ scanner,
147
+ success: false,
148
+ rawOutput: "",
149
+ error: stderr || (err as Error).message,
150
+ durationMs,
151
+ });
152
+ return;
153
+ }
154
+
155
+ // For file-based output (Stryker), read from file
156
+ if (cmd.outputFile) {
157
+ if (existsSync(cmd.outputFile)) {
158
+ try {
159
+ const fileOutput = readFileSync(cmd.outputFile, "utf-8");
160
+ resolve({
161
+ scanner,
162
+ success: true,
163
+ rawOutput: fileOutput,
164
+ durationMs,
165
+ });
166
+ return;
167
+ } catch (readErr) {
168
+ resolve({
169
+ scanner,
170
+ success: false,
171
+ rawOutput: "",
172
+ error: `Failed to read output file: ${(readErr as Error).message}`,
173
+ durationMs,
174
+ });
175
+ return;
176
+ }
177
+ }
178
+ resolve({
179
+ scanner,
180
+ success: false,
181
+ rawOutput: "",
182
+ error: `Scanner completed but output file not found: ${cmd.outputFile}`,
183
+ durationMs,
184
+ });
185
+ return;
186
+ }
187
+
188
+ // Stdout-based output
189
+ const output = stdout.trim();
190
+ if (!output) {
191
+ resolve({
192
+ scanner,
193
+ success: true,
194
+ rawOutput: "[]", // ESLint returns empty when no files match
195
+ durationMs,
196
+ });
197
+ return;
198
+ }
199
+
200
+ resolve({
201
+ scanner,
202
+ success: true,
203
+ rawOutput: output,
204
+ durationMs,
205
+ });
206
+ },
207
+ );
208
+ });
209
+ }
210
+
211
+ // Exported for testing
212
+ export { getScannerCommand, type ScannerCommand };
@@ -199,6 +199,33 @@ export const ingestScannerOutputSchema = {
199
199
  * from an external scanner, deduplicates against the internal store, and
200
200
  * normalizes the output into claude-crap's canonical format.
201
201
  */
202
+ /**
203
+ * Schema for the `auto_scan` tool. Auto-detects available scanners
204
+ * in the workspace, runs them, and ingests findings into the SARIF store.
205
+ */
206
+ /**
207
+ * Schema for the `bootstrap_scanner` tool. Detects project type,
208
+ * installs the appropriate scanner, creates config files, and runs
209
+ * auto_scan to verify.
210
+ */
211
+ export const bootstrapScannerSchema = {
212
+ type: "object",
213
+ description:
214
+ "Detect the project type (JavaScript, TypeScript, Python, Java, C#), install the appropriate scanner (ESLint for JS/TS, Bandit for Python, Semgrep for Java/C#), create a minimal config file, and run auto_scan to verify. Skips installation if a scanner is already configured. Use this when auto_scan finds no scanners and quality grades are vacuously A.",
215
+ properties: {},
216
+ required: [],
217
+ additionalProperties: false,
218
+ } as const;
219
+
220
+ export const autoScanSchema = {
221
+ type: "object",
222
+ description:
223
+ "Auto-detect available scanners (ESLint, Semgrep, Bandit, Stryker) in the workspace, execute them, and ingest findings into the SARIF store. Returns detection results, per-scanner execution stats, and total findings ingested. Call this to populate findings without manual scanner invocation.",
224
+ properties: {},
225
+ required: [],
226
+ additionalProperties: false,
227
+ } as const;
228
+
202
229
  export const ingestSarifSchema = {
203
230
  type: "object",
204
231
  description:
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Unit tests for the auto-scan orchestrator.
3
+ *
4
+ * These tests verify the orchestration logic: detection → run → ingest.
5
+ * They use a real (temporary) workspace with config files to trigger
6
+ * detection, but scanner execution will fail (binaries aren't installed
7
+ * in the test environment). This tests the graceful-failure path.
8
+ *
9
+ * @module tests/auto-scan.test
10
+ */
11
+
12
+ import { describe, it } from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { tmpdir } from "node:os";
17
+ import pino from "pino";
18
+
19
+ import { autoScan, type AutoScanResult } from "../scanner/auto-scan.js";
20
+ import { SarifStore } from "../sarif/sarif-store.js";
21
+
22
+ const logger = pino({ level: "silent" });
23
+
24
+ function makeTmpDir(): string {
25
+ return mkdtempSync(join(tmpdir(), "crap-autoscan-"));
26
+ }
27
+
28
+ describe("autoScan", () => {
29
+ it("returns empty results when no scanners are detected", async () => {
30
+ const dir = makeTmpDir();
31
+ try {
32
+ const store = new SarifStore({
33
+ workspaceRoot: dir,
34
+ outputDir: join(dir, ".claude-crap/reports"),
35
+ });
36
+ const result = await autoScan(dir, store, logger);
37
+ assert.equal(result.detected.length, 4);
38
+ assert.ok(result.totalDurationMs >= 0);
39
+ // No scanners available means no results
40
+ // (unless the host has scanner binaries installed)
41
+ assert.ok(Array.isArray(result.results));
42
+ assert.equal(typeof result.totalFindings, "number");
43
+ } finally {
44
+ rmSync(dir, { recursive: true, force: true });
45
+ }
46
+ });
47
+
48
+ it("detects scanners from config files in workspace", async () => {
49
+ const dir = makeTmpDir();
50
+ try {
51
+ writeFileSync(join(dir, "eslint.config.mjs"), "export default [];");
52
+ writeFileSync(join(dir, ".semgrep.yml"), "rules: []");
53
+
54
+ const store = new SarifStore({
55
+ workspaceRoot: dir,
56
+ outputDir: join(dir, ".claude-crap/reports"),
57
+ });
58
+ const result = await autoScan(dir, store, logger);
59
+
60
+ // ESLint and Semgrep should be detected
61
+ const eslintDetection = result.detected.find((d) => d.scanner === "eslint");
62
+ const semgrepDetection = result.detected.find((d) => d.scanner === "semgrep");
63
+ assert.ok(eslintDetection);
64
+ assert.equal(eslintDetection.available, true);
65
+ assert.ok(semgrepDetection);
66
+ assert.equal(semgrepDetection.available, true);
67
+ } finally {
68
+ rmSync(dir, { recursive: true, force: true });
69
+ }
70
+ });
71
+
72
+ it("returns correct structure even when all scanners fail", async () => {
73
+ const dir = makeTmpDir();
74
+ try {
75
+ // Create config files so scanners are detected but will fail to run
76
+ // (the temp dir isn't a real project)
77
+ writeFileSync(join(dir, "eslint.config.mjs"), "export default [];");
78
+
79
+ const store = new SarifStore({
80
+ workspaceRoot: dir,
81
+ outputDir: join(dir, ".claude-crap/reports"),
82
+ });
83
+ const result = await autoScan(dir, store, logger);
84
+
85
+ // Structure must be valid regardless of scanner success
86
+ assert.ok(Array.isArray(result.detected));
87
+ assert.ok(Array.isArray(result.results));
88
+ assert.equal(typeof result.totalFindings, "number");
89
+ assert.equal(typeof result.totalDurationMs, "number");
90
+
91
+ // eslint was detected, so it should appear in results
92
+ const eslintResult = result.results.find((r) => r.scanner === "eslint");
93
+ if (eslintResult) {
94
+ assert.equal(typeof eslintResult.success, "boolean");
95
+ assert.equal(typeof eslintResult.durationMs, "number");
96
+ assert.equal(typeof eslintResult.findingsIngested, "number");
97
+ }
98
+ } finally {
99
+ rmSync(dir, { recursive: true, force: true });
100
+ }
101
+ });
102
+
103
+ it("totalFindings sums findings across all successful scanners", async () => {
104
+ const dir = makeTmpDir();
105
+ try {
106
+ const store = new SarifStore({
107
+ workspaceRoot: dir,
108
+ outputDir: join(dir, ".claude-crap/reports"),
109
+ });
110
+ const result = await autoScan(dir, store, logger);
111
+
112
+ // Sum of individual scanner findings should equal total
113
+ const sumFromResults = result.results
114
+ .filter((r) => r.success)
115
+ .reduce((sum, r) => sum + r.findingsIngested, 0);
116
+ assert.equal(result.totalFindings, sumFromResults);
117
+ } finally {
118
+ rmSync(dir, { recursive: true, force: true });
119
+ }
120
+ });
121
+
122
+ it("result has one ScannerDetection per known scanner", async () => {
123
+ const dir = makeTmpDir();
124
+ try {
125
+ const store = new SarifStore({
126
+ workspaceRoot: dir,
127
+ outputDir: join(dir, ".claude-crap/reports"),
128
+ });
129
+ const result = await autoScan(dir, store, logger);
130
+
131
+ const scannerNames = result.detected.map((d) => d.scanner).sort();
132
+ assert.deepEqual(scannerNames, ["bandit", "eslint", "semgrep", "stryker"]);
133
+ } finally {
134
+ rmSync(dir, { recursive: true, force: true });
135
+ }
136
+ });
137
+ });
@@ -232,13 +232,15 @@ 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 seven tools via tools/list", async () => {
235
+ it("exposes all nine tools via tools/list", async () => {
236
236
  const response = await client!.request<{ result?: { tools?: Array<{ name: string }> } }>(
237
237
  "tools/list",
238
238
  );
239
239
  const names = (response.result?.tools ?? []).map((t) => t.name).sort();
240
240
  assert.deepEqual(names, [
241
241
  "analyze_file_ast",
242
+ "auto_scan",
243
+ "bootstrap_scanner",
242
244
  "compute_crap",
243
245
  "compute_tdr",
244
246
  "ingest_sarif",
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Unit tests for the scanner bootstrap module.
3
+ *
4
+ * Tests project type detection, ESLint config generation, and the
5
+ * bootstrap orchestrator's short-circuit and error paths.
6
+ *
7
+ * @module tests/scanner-bootstrap.test
8
+ */
9
+
10
+ import { describe, it } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+
16
+ import { detectProjectType, generateEslintConfig } from "../scanner/bootstrap.js";
17
+
18
+ function makeTmpDir(): string {
19
+ return mkdtempSync(join(tmpdir(), "crap-bootstrap-"));
20
+ }
21
+
22
+ describe("detectProjectType", () => {
23
+ it("returns typescript when package.json and tsconfig.json exist", () => {
24
+ const dir = makeTmpDir();
25
+ try {
26
+ writeFileSync(join(dir, "package.json"), "{}");
27
+ writeFileSync(join(dir, "tsconfig.json"), "{}");
28
+ assert.equal(detectProjectType(dir), "typescript");
29
+ } finally {
30
+ rmSync(dir, { recursive: true, force: true });
31
+ }
32
+ });
33
+
34
+ it("returns javascript when only package.json exists", () => {
35
+ const dir = makeTmpDir();
36
+ try {
37
+ writeFileSync(join(dir, "package.json"), "{}");
38
+ assert.equal(detectProjectType(dir), "javascript");
39
+ } finally {
40
+ rmSync(dir, { recursive: true, force: true });
41
+ }
42
+ });
43
+
44
+ it("returns python when pyproject.toml exists", () => {
45
+ const dir = makeTmpDir();
46
+ try {
47
+ writeFileSync(join(dir, "pyproject.toml"), "[project]");
48
+ assert.equal(detectProjectType(dir), "python");
49
+ } finally {
50
+ rmSync(dir, { recursive: true, force: true });
51
+ }
52
+ });
53
+
54
+ it("returns python when setup.py exists", () => {
55
+ const dir = makeTmpDir();
56
+ try {
57
+ writeFileSync(join(dir, "setup.py"), "from setuptools import setup");
58
+ assert.equal(detectProjectType(dir), "python");
59
+ } finally {
60
+ rmSync(dir, { recursive: true, force: true });
61
+ }
62
+ });
63
+
64
+ it("returns python when requirements.txt exists", () => {
65
+ const dir = makeTmpDir();
66
+ try {
67
+ writeFileSync(join(dir, "requirements.txt"), "flask==2.0");
68
+ assert.equal(detectProjectType(dir), "python");
69
+ } finally {
70
+ rmSync(dir, { recursive: true, force: true });
71
+ }
72
+ });
73
+
74
+ it("returns java when pom.xml exists", () => {
75
+ const dir = makeTmpDir();
76
+ try {
77
+ writeFileSync(join(dir, "pom.xml"), "<project></project>");
78
+ assert.equal(detectProjectType(dir), "java");
79
+ } finally {
80
+ rmSync(dir, { recursive: true, force: true });
81
+ }
82
+ });
83
+
84
+ it("returns java when build.gradle exists", () => {
85
+ const dir = makeTmpDir();
86
+ try {
87
+ writeFileSync(join(dir, "build.gradle"), "plugins {}");
88
+ assert.equal(detectProjectType(dir), "java");
89
+ } finally {
90
+ rmSync(dir, { recursive: true, force: true });
91
+ }
92
+ });
93
+
94
+ it("returns java when build.gradle.kts exists", () => {
95
+ const dir = makeTmpDir();
96
+ try {
97
+ writeFileSync(join(dir, "build.gradle.kts"), "plugins {}");
98
+ assert.equal(detectProjectType(dir), "java");
99
+ } finally {
100
+ rmSync(dir, { recursive: true, force: true });
101
+ }
102
+ });
103
+
104
+ it("returns csharp when .csproj file exists", () => {
105
+ const dir = makeTmpDir();
106
+ try {
107
+ writeFileSync(join(dir, "MyApp.csproj"), "<Project />");
108
+ assert.equal(detectProjectType(dir), "csharp");
109
+ } finally {
110
+ rmSync(dir, { recursive: true, force: true });
111
+ }
112
+ });
113
+
114
+ it("returns csharp when .sln file exists", () => {
115
+ const dir = makeTmpDir();
116
+ try {
117
+ writeFileSync(join(dir, "MyApp.sln"), "");
118
+ assert.equal(detectProjectType(dir), "csharp");
119
+ } finally {
120
+ rmSync(dir, { recursive: true, force: true });
121
+ }
122
+ });
123
+
124
+ it("returns csharp when Directory.Build.props exists", () => {
125
+ const dir = makeTmpDir();
126
+ try {
127
+ writeFileSync(join(dir, "Directory.Build.props"), "<Project />");
128
+ assert.equal(detectProjectType(dir), "csharp");
129
+ } finally {
130
+ rmSync(dir, { recursive: true, force: true });
131
+ }
132
+ });
133
+
134
+ it("returns unknown for an empty directory", () => {
135
+ const dir = makeTmpDir();
136
+ try {
137
+ assert.equal(detectProjectType(dir), "unknown");
138
+ } finally {
139
+ rmSync(dir, { recursive: true, force: true });
140
+ }
141
+ });
142
+
143
+ it("typescript wins over python when both signals present", () => {
144
+ const dir = makeTmpDir();
145
+ try {
146
+ writeFileSync(join(dir, "package.json"), "{}");
147
+ writeFileSync(join(dir, "tsconfig.json"), "{}");
148
+ writeFileSync(join(dir, "requirements.txt"), "flask");
149
+ assert.equal(detectProjectType(dir), "typescript");
150
+ } finally {
151
+ rmSync(dir, { recursive: true, force: true });
152
+ }
153
+ });
154
+ });
155
+
156
+ describe("generateEslintConfig", () => {
157
+ it("TypeScript config includes typescript-eslint import", () => {
158
+ const config = generateEslintConfig(true);
159
+ assert.ok(config.includes('import tseslint from "typescript-eslint"'));
160
+ assert.ok(config.includes("tseslint.config"));
161
+ assert.ok(config.includes("tseslint.configs.recommended"));
162
+ });
163
+
164
+ it("JavaScript config uses plain array export", () => {
165
+ const config = generateEslintConfig(false);
166
+ assert.ok(config.includes("export default ["));
167
+ assert.ok(!config.includes("typescript-eslint"));
168
+ });
169
+
170
+ it("both configs include standard ignores", () => {
171
+ for (const isTS of [true, false]) {
172
+ const config = generateEslintConfig(isTS);
173
+ assert.ok(config.includes('"dist/"'), `${isTS ? "TS" : "JS"} should ignore dist/`);
174
+ assert.ok(config.includes('"node_modules/"'), `${isTS ? "TS" : "JS"} should ignore node_modules/`);
175
+ assert.ok(config.includes('"coverage/"'), `${isTS ? "TS" : "JS"} should ignore coverage/`);
176
+ }
177
+ });
178
+
179
+ it("both configs import @eslint/js", () => {
180
+ for (const isTS of [true, false]) {
181
+ const config = generateEslintConfig(isTS);
182
+ assert.ok(config.includes('import js from "@eslint/js"'));
183
+ assert.ok(config.includes("js.configs.recommended"));
184
+ }
185
+ });
186
+ });