fchek 1.0.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 (66) hide show
  1. package/README.md +64 -0
  2. package/bin/fchek.js +107 -0
  3. package/lib/api.js +110 -0
  4. package/lib/audit.js +211 -0
  5. package/lib/bench.js +248 -0
  6. package/lib/config.js +191 -0
  7. package/lib/context.js +356 -0
  8. package/lib/convention.js +526 -0
  9. package/lib/coverage.js +604 -0
  10. package/lib/db.js +135 -0
  11. package/lib/deps-check.js +264 -0
  12. package/lib/deps.js +374 -0
  13. package/lib/docker.js +84 -0
  14. package/lib/doctor.js +149 -0
  15. package/lib/dom.js +226 -0
  16. package/lib/fuzz.js +470 -0
  17. package/lib/git.js +290 -0
  18. package/lib/goto.js +544 -0
  19. package/lib/launch.js +182 -0
  20. package/lib/lint.js +624 -0
  21. package/lib/new_features.test.js +181 -0
  22. package/lib/output.js +46 -0
  23. package/lib/port.js +173 -0
  24. package/lib/process.js +228 -0
  25. package/lib/profile.js +453 -0
  26. package/lib/python.js +41 -0
  27. package/lib/race.js +186 -0
  28. package/lib/registry.js +179 -0
  29. package/lib/repl.js +135 -0
  30. package/lib/run.js +403 -0
  31. package/lib/screenshot.js +152 -0
  32. package/lib/secrets.js +257 -0
  33. package/lib/state.js +219 -0
  34. package/lib/test.js +471 -0
  35. package/lib/vuln.js +253 -0
  36. package/lib/watch.js +240 -0
  37. package/lib/winlog.js +123 -0
  38. package/package.json +27 -0
  39. package/skills/ACTIVATE.md +274 -0
  40. package/skills/README.md +163 -0
  41. package/skills/agent.md +444 -0
  42. package/skills/api.md +47 -0
  43. package/skills/bench.md +117 -0
  44. package/skills/context.md +116 -0
  45. package/skills/convention.md +143 -0
  46. package/skills/coverage.md +99 -0
  47. package/skills/csharp.md +97 -0
  48. package/skills/db.md +66 -0
  49. package/skills/deps-check.md +135 -0
  50. package/skills/deps.md +143 -0
  51. package/skills/docker.md +61 -0
  52. package/skills/dom.md +56 -0
  53. package/skills/fuzz.md +167 -0
  54. package/skills/goto.md +111 -0
  55. package/skills/lint.md +123 -0
  56. package/skills/port.md +57 -0
  57. package/skills/profile.md +91 -0
  58. package/skills/race.md +117 -0
  59. package/skills/repl.md +81 -0
  60. package/skills/rules.md +318 -0
  61. package/skills/run.md +135 -0
  62. package/skills/secrets.md +170 -0
  63. package/skills/security.md +360 -0
  64. package/skills/state.md +261 -0
  65. package/skills/vuln.md +57 -0
  66. package/skills/windows.md +320 -0
package/lib/test.js ADDED
@@ -0,0 +1,471 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * test.js — auto-detect test framework and run tests
5
+ *
6
+ * Detects: pytest, jest, vitest, cargo test, go test, dotnet test,
7
+ * mocha, jasmine, rspec, phpunit
8
+ *
9
+ * Returns structured JSON: passed, failed, errors, duration.
10
+ * Agent uses this to verify nothing is broken after every change.
11
+ */
12
+
13
+ const { spawnSync, execSync } = require('child_process');
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+ const { output, ok, fail } = require('./output');
17
+ const { findPython } = require('./python');
18
+
19
+ const HELP = `
20
+ fchek test [dir] [--watch] [--coverage] [--filter=<name>]
21
+
22
+ Auto-detect test framework and run tests. Returns structured results.
23
+
24
+ Supported frameworks (auto-detected):
25
+ Python → pytest (preferred) or unittest
26
+ JS/TS → jest, vitest, mocha (reads package.json test script)
27
+ Rust → cargo test
28
+ Go → go test ./...
29
+ C#/.NET → dotnet test
30
+ C/C++ → CMake + CTest / GoogleTest / Catch2
31
+
32
+ Options:
33
+ --coverage Run with coverage enabled
34
+ --filter=<name> Run only tests matching name
35
+ --timeout=60000 Max time in ms (default: 120000)
36
+
37
+ Examples:
38
+ fchek test
39
+ fchek test src/
40
+ fchek test --filter=auth
41
+ fchek test --coverage
42
+ `.trim();
43
+
44
+ const DEFAULT_TIMEOUT = 120_000;
45
+
46
+ function commandExists(cmd) {
47
+ try {
48
+ execSync(
49
+ process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`,
50
+ { stdio: 'ignore', timeout: 3000 }
51
+ );
52
+ return true;
53
+ } catch { return false; }
54
+ }
55
+
56
+ // ─── Framework detection ─────────────────────────────────────────────────────
57
+
58
+ function detectFramework(dir) {
59
+ const abs = path.resolve(dir);
60
+
61
+ // .fchekrc override
62
+ try {
63
+ const rc = JSON.parse(fs.readFileSync(path.join(abs, '.fchekrc'), 'utf8'));
64
+ if (rc.test_command) return { type: 'custom', command: rc.test_command, dir: abs };
65
+ } catch {}
66
+
67
+ // C# / .NET
68
+ if (fs.readdirSync(abs).some(f => f.endsWith('.csproj') || f.endsWith('.sln'))) {
69
+ return { type: 'dotnet', dir: abs };
70
+ }
71
+
72
+ // Rust
73
+ if (fs.existsSync(path.join(abs, 'Cargo.toml'))) {
74
+ return { type: 'cargo', dir: abs };
75
+ }
76
+
77
+ // Go
78
+ if (fs.existsSync(path.join(abs, 'go.mod'))) {
79
+ return { type: 'go', dir: abs };
80
+ }
81
+
82
+ // C/C++ — CMakeLists.txt (CTest/GoogleTest/Catch2)
83
+ const hasCMake = fs.existsSync(path.join(abs, 'CMakeLists.txt'));
84
+ const hasMakefile = fs.existsSync(path.join(abs, 'Makefile')) || fs.existsSync(path.join(abs, 'makefile'));
85
+ const hasCFiles = (() => {
86
+ try { return fs.readdirSync(abs).some(f => f.endsWith('.cpp') || f.endsWith('.c')); } catch { return false; }
87
+ })();
88
+ if (hasCMake || (hasMakefile && hasCFiles)) {
89
+ return { type: 'ctest', dir: abs, hasCMake, hasMakefile };
90
+ }
91
+ const pkgPath = path.join(abs, 'package.json');
92
+ if (fs.existsSync(pkgPath)) {
93
+ try {
94
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
95
+ const testScript = pkg.scripts?.test || '';
96
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
97
+ if (deps.vitest || testScript.includes('vitest')) return { type: 'vitest', dir: abs };
98
+ if (deps.jest || testScript.includes('jest')) return { type: 'jest', dir: abs };
99
+ if (deps.mocha || testScript.includes('mocha')) return { type: 'mocha', dir: abs };
100
+ if (testScript) return { type: 'npm_test', dir: abs };
101
+ } catch {}
102
+ }
103
+
104
+ // Python
105
+ const pyMarkers = ['pytest.ini', 'pyproject.toml', 'setup.cfg', 'conftest.py'];
106
+ if (pyMarkers.some(m => fs.existsSync(path.join(abs, m)))) {
107
+ return { type: 'pytest', dir: abs };
108
+ }
109
+ // Fallback: check for test files
110
+ const hasTestPy = (() => {
111
+ try {
112
+ return fs.readdirSync(abs).some(f => f.startsWith('test_') && f.endsWith('.py'));
113
+ } catch { return false; }
114
+ })();
115
+ if (hasTestPy) return { type: 'pytest', dir: abs };
116
+
117
+ // Walk up
118
+ const parent = path.dirname(abs);
119
+ if (parent !== abs) return detectFramework(parent);
120
+
121
+ return null;
122
+ }
123
+
124
+ // ─── Result parsers ───────────────────────────────────────────────────────────
125
+
126
+ function parsePytest(stdout, stderr, status) {
127
+ // pytest writes summary to stderr on some versions, stdout on others
128
+ const combined = (stdout || '') + (stderr || '');
129
+
130
+ const passedM = combined.match(/(\d+) passed/);
131
+ const failedM = combined.match(/(\d+) failed/);
132
+ const errorsM = combined.match(/(\d+) error/);
133
+ const skippedM = combined.match(/(\d+) skipped/);
134
+
135
+ const passed = passedM ? parseInt(passedM[1]) : 0;
136
+ const failed = failedM ? parseInt(failedM[1]) : 0;
137
+ const errors = errorsM ? parseInt(errorsM[1]) : 0;
138
+ const skipped = skippedM ? parseInt(skippedM[1]) : 0;
139
+
140
+ // If exit_code 0 and no numbers found — pytest ran but output format unexpected
141
+ // Try alternate: count "PASSED" / "FAILED" lines
142
+ const passedAlt = (combined.match(/\bPASSED\b/g) || []).length;
143
+ const failedAlt = (combined.match(/\bFAILED\b/g) || []).length;
144
+
145
+ const finalPassed = passed > 0 ? passed : passedAlt;
146
+ const finalFailed = failed > 0 ? failed : failedAlt;
147
+
148
+ const failures = [];
149
+ for (const m of combined.matchAll(/FAILED (.+?) - (.+)/g)) {
150
+ failures.push({ test: m[1].trim(), reason: m[2].trim() });
151
+ }
152
+ // Also catch short failure format
153
+ for (const m of combined.matchAll(/FAILED (.+)\n/g)) {
154
+ if (!failures.find(f => f.test === m[1].trim())) {
155
+ failures.push({ test: m[1].trim(), reason: '' });
156
+ }
157
+ }
158
+
159
+ return {
160
+ passed: finalPassed,
161
+ failed: finalFailed,
162
+ errors,
163
+ skipped,
164
+ failures: failures.slice(0, 20),
165
+ };
166
+ }
167
+
168
+ function parseJest(stdout, stderr, status) {
169
+ const combined = stdout + stderr;
170
+ const suitesPassed = parseInt((combined.match(/(\d+) passed/) || [0, 0])[1]);
171
+ const suitesFailed = parseInt((combined.match(/(\d+) failed/) || [0, 0])[1]);
172
+ const tests = parseInt((combined.match(/Tests:\s+.*?(\d+) total/) || [0, 0])[1]);
173
+
174
+ const failures = [];
175
+ for (const m of combined.matchAll(/● (.+)\n\n([\s\S]+?)(?=●|\n\n\n)/g)) {
176
+ failures.push({ test: m[1].trim(), reason: m[2].trim().slice(0, 200) });
177
+ }
178
+
179
+ return { passed: suitesPassed, failed: suitesFailed, total_tests: tests, failures: failures.slice(0, 20) };
180
+ }
181
+
182
+ function parseCargo(stdout, stderr, status) {
183
+ const combined = stdout + stderr;
184
+ const passed = parseInt((combined.match(/(\d+) passed/) || [0, 0])[1]);
185
+ const failed = parseInt((combined.match(/(\d+) failed/) || [0, 0])[1]);
186
+
187
+ const failures = [];
188
+ for (const m of combined.matchAll(/---- (.+) stdout ----\n([\s\S]+?)(?=----|\z)/g)) {
189
+ failures.push({ test: m[1].trim(), output: m[2].trim().slice(0, 200) });
190
+ }
191
+
192
+ return { passed, failed, failures: failures.slice(0, 20) };
193
+ }
194
+
195
+ function parseGo(stdout, stderr, status) {
196
+ const combined = stdout + stderr;
197
+ const passed = (combined.match(/--- PASS/g) || []).length;
198
+ const failed = (combined.match(/--- FAIL/g) || []).length;
199
+
200
+ const failures = [];
201
+ for (const m of combined.matchAll(/--- FAIL: (\S+)/g)) {
202
+ failures.push({ test: m[1] });
203
+ }
204
+
205
+ return { passed, failed, failures };
206
+ }
207
+
208
+ function parseCtest(stdout, stderr, status) {
209
+ const combined = stdout + stderr;
210
+
211
+ // CTest format: "X/Y Test #N: name ... Passed/Failed Xs"
212
+ const passed = (combined.match(/\bPassed\b/g) || []).length;
213
+ const failed = (combined.match(/\bFailed\b/g) || []).length;
214
+
215
+ // GoogleTest format: "[ PASSED ] N tests." / "[ FAILED ] N tests"
216
+ const gtPassM = combined.match(/\[\s+PASSED\s+\]\s+(\d+) test/);
217
+ const gtFailM = combined.match(/\[\s+FAILED\s+\]\s+(\d+) test/);
218
+ const gtPassed = gtPassM ? parseInt(gtPassM[1]) : null;
219
+ const gtFailed = gtFailM ? parseInt(gtFailM[1]) : null;
220
+
221
+ // Catch2 format: "N test cases, N assertions, N failures"
222
+ const c2M = combined.match(/(\d+) test cases?,\s*(\d+) assertions?,\s*(\d+) failures?/);
223
+ const c2Passed = c2M ? parseInt(c2M[1]) - parseInt(c2M[3]) : null;
224
+ const c2Failed = c2M ? parseInt(c2M[3]) : null;
225
+
226
+ const finalPassed = gtPassed ?? c2Passed ?? passed;
227
+ const finalFailed = gtFailed ?? c2Failed ?? failed;
228
+
229
+ // Collect failed test names from GTest
230
+ const failures = [];
231
+ for (const m of combined.matchAll(/\[\s+FAILED\s+\]\s+(.+)/g)) {
232
+ failures.push({ test: m[1].trim() });
233
+ }
234
+ // CTest failures
235
+ for (const m of combined.matchAll(/Test\s+#\d+:\s+(\S+)\s+.*?Failed/g)) {
236
+ if (!failures.find(f => f.test === m[1])) {
237
+ failures.push({ test: m[1].trim() });
238
+ }
239
+ }
240
+
241
+ return {
242
+ passed: finalPassed,
243
+ failed: finalFailed,
244
+ failures: failures.slice(0, 20),
245
+ framework_hint: gtPassed !== null ? 'googletest' : c2Passed !== null ? 'catch2' : 'ctest',
246
+ };
247
+ }
248
+
249
+ function parseDotnet(stdout, stderr, status) {
250
+ const combined = stdout + stderr;
251
+ const passed = parseInt((combined.match(/(\d+) test(?:s)? passed/) || [0, 0])[1]);
252
+ const failed = parseInt((combined.match(/(\d+) test(?:s)? failed/) || [0, 0])[1]);
253
+ const skipped = parseInt((combined.match(/(\d+) test(?:s)? skipped/) || [0, 0])[1]);
254
+
255
+ const failures = [];
256
+ for (const m of combined.matchAll(/X (.+) \[/g)) {
257
+ failures.push({ test: m[1].trim() });
258
+ }
259
+
260
+ return { passed, failed, skipped, failures: failures.slice(0, 20) };
261
+ }
262
+
263
+ // ─── Runners ──────────────────────────────────────────────────────────────────
264
+
265
+ function runFramework(framework, opts, timeoutMs) {
266
+ const { type, dir, command } = framework;
267
+ const { coverage, filter } = opts;
268
+
269
+ let cmd, args, env = { ...process.env };
270
+
271
+ switch (type) {
272
+ case 'custom': {
273
+ const parts = command.split(/\s+/);
274
+ cmd = parts[0]; args = parts.slice(1);
275
+ break;
276
+ }
277
+ case 'pytest': {
278
+ const py = findPython();
279
+ if (!py) return { error: 'Python not found' };
280
+ // Check if pytest is available
281
+ const checkPytest = spawnSync(py, ['-m', 'pytest', '--version'], {
282
+ encoding: 'utf8', timeout: 5000, windowsHide: true,
283
+ });
284
+ if (checkPytest.status !== 0) {
285
+ return { error: 'pytest not found. Install: pip install pytest' };
286
+ }
287
+ cmd = py;
288
+ args = ['-m', 'pytest', '--tb=short', '-q'];
289
+ if (coverage) args.push('--cov=.', '--cov-report=term-missing');
290
+ if (filter) args.push('-k', filter);
291
+ break;
292
+ }
293
+ case 'jest': {
294
+ cmd = commandExists('jest') ? 'jest' : (commandExists('npx') ? 'npx' : null);
295
+ args = cmd === 'npx' ? ['jest'] : [];
296
+ args.push('--json', '--no-coverage');
297
+ if (coverage) args.push('--coverage');
298
+ if (filter) args.push('--testNamePattern', filter);
299
+ break;
300
+ }
301
+ case 'vitest': {
302
+ cmd = 'npx';
303
+ args = ['vitest', 'run', '--reporter=json'];
304
+ if (filter) args.push('--testNamePattern', filter);
305
+ break;
306
+ }
307
+ case 'mocha': {
308
+ cmd = commandExists('mocha') ? 'mocha' : 'npx';
309
+ args = cmd === 'npx' ? ['mocha'] : [];
310
+ if (filter) args.push('--grep', filter);
311
+ break;
312
+ }
313
+ case 'npm_test': {
314
+ cmd = 'npm'; args = ['test'];
315
+ break;
316
+ }
317
+ case 'cargo': {
318
+ cmd = 'cargo'; args = ['test'];
319
+ if (filter) args.push(filter);
320
+ break;
321
+ }
322
+ case 'go': {
323
+ cmd = 'go'; args = ['test', './...', '-v'];
324
+ if (filter) args.push('-run', filter);
325
+ if (coverage) args.push('-cover');
326
+ break;
327
+ }
328
+ case 'dotnet': {
329
+ cmd = 'dotnet'; args = ['test', '--logger', 'console;verbosity=normal'];
330
+ if (filter) args.push('--filter', filter);
331
+ if (coverage) args.push('--collect', 'XPlat Code Coverage');
332
+ break;
333
+ }
334
+ case 'ctest': {
335
+ // Step 1: Build with CMake or Make
336
+ if (framework.hasCMake) {
337
+ // cmake -B build && cmake --build build
338
+ const cmakeConf = spawnSync(
339
+ 'cmake', ['-B', 'build', '-DCMAKE_BUILD_TYPE=Debug'],
340
+ { encoding: 'utf8', cwd: framework.dir, timeout: 60000, windowsHide: true }
341
+ );
342
+ if (cmakeConf.status !== 0) {
343
+ return { error: `CMake configure failed:\n${(cmakeConf.stderr || cmakeConf.stdout || '').slice(0, 500)}` };
344
+ }
345
+ const cmakeBuild = spawnSync(
346
+ 'cmake', ['--build', 'build'],
347
+ { encoding: 'utf8', cwd: framework.dir, timeout: timeoutMs, windowsHide: true }
348
+ );
349
+ if (cmakeBuild.status !== 0) {
350
+ return { error: `CMake build failed:\n${(cmakeBuild.stderr || cmakeBuild.stdout || '').slice(0, 500)}` };
351
+ }
352
+ // Run with ctest
353
+ const ctestArgs = ['--output-on-failure', '-V'];
354
+ if (filter) ctestArgs.push('-R', filter);
355
+ cmd = 'ctest'; args = ctestArgs;
356
+ } else if (framework.hasMakefile) {
357
+ // make test or make check
358
+ const makeTest = spawnSync(
359
+ 'make', ['test'],
360
+ { encoding: 'utf8', cwd: framework.dir, timeout: timeoutMs, windowsHide: true }
361
+ );
362
+ if (makeTest.status !== 0) {
363
+ // Fallback: try "make check"
364
+ const makeCheck = spawnSync(
365
+ 'make', ['check'],
366
+ { encoding: 'utf8', cwd: framework.dir, timeout: timeoutMs, windowsHide: true }
367
+ );
368
+ return {
369
+ exit_code: makeCheck.status,
370
+ all_passed: makeCheck.status === 0,
371
+ duration_ms: 0,
372
+ stdout: (makeCheck.stdout || '').slice(0, 5000),
373
+ stderr: (makeCheck.stderr || '').slice(0, 2000),
374
+ ...parseCtest(makeCheck.stdout || '', makeCheck.stderr || '', makeCheck.status),
375
+ };
376
+ }
377
+ return {
378
+ exit_code: makeTest.status,
379
+ all_passed: makeTest.status === 0,
380
+ duration_ms: 0,
381
+ stdout: (makeTest.stdout || '').slice(0, 5000),
382
+ stderr: (makeTest.stderr || '').slice(0, 2000),
383
+ ...parseCtest(makeTest.stdout || '', makeTest.stderr || '', makeTest.status),
384
+ };
385
+ }
386
+ break;
387
+ }
388
+ default:
389
+ return { error: `Unknown framework: ${type}` };
390
+ }
391
+
392
+ if (!cmd) return { error: `Test runner not found for ${type}` };
393
+
394
+ const t0 = Date.now();
395
+ const res = spawnSync(cmd, args, {
396
+ encoding: 'utf8',
397
+ cwd: dir,
398
+ timeout: timeoutMs,
399
+ env,
400
+ windowsHide: true,
401
+ });
402
+ const duration = Date.now() - t0;
403
+
404
+ if (res.error?.code === 'ETIMEDOUT') {
405
+ return { error: `Tests timed out after ${timeoutMs}ms` };
406
+ }
407
+
408
+ const stdout = res.stdout || '';
409
+ const stderr = res.stderr || '';
410
+
411
+ let parsed;
412
+ switch (type) {
413
+ case 'pytest': parsed = parsePytest(stdout, stderr, res.status); break;
414
+ case 'jest': parsed = parseJest(stdout, stderr, res.status); break;
415
+ case 'vitest': parsed = parseJest(stdout, stderr, res.status); break;
416
+ case 'cargo': parsed = parseCargo(stdout, stderr, res.status); break;
417
+ case 'go': parsed = parseGo(stdout, stderr, res.status); break;
418
+ case 'dotnet': parsed = parseDotnet(stdout, stderr, res.status); break;
419
+ case 'ctest': parsed = parseCtest(stdout, stderr, res.status); break;
420
+ default: parsed = { raw: true };
421
+ }
422
+
423
+ return {
424
+ ...parsed,
425
+ exit_code: res.status,
426
+ all_passed: res.status === 0,
427
+ duration_ms: duration,
428
+ stdout: stdout.slice(0, 5000),
429
+ stderr: stderr.slice(0, 2000),
430
+ };
431
+ }
432
+
433
+ async function run(args) {
434
+ if (args[0] === '--help') { console.log(HELP); return; }
435
+
436
+ const dir = args.find(a => !a.startsWith('--')) || '.';
437
+ const coverage = args.includes('--coverage');
438
+ const filter = (args.find(a => a.startsWith('--filter=')) || '').replace('--filter=', '') || null;
439
+ const timeout = parseInt(
440
+ (args.find(a => a.startsWith('--timeout=')) || `--timeout=${DEFAULT_TIMEOUT}`)
441
+ .replace('--timeout=', ''), 10
442
+ );
443
+
444
+ if (!fs.existsSync(dir)) {
445
+ return output(fail(`Directory not found: ${dir}`));
446
+ }
447
+
448
+ const framework = detectFramework(path.resolve(dir));
449
+ if (!framework) {
450
+ return output(fail(
451
+ `No test framework detected in: ${path.resolve(dir)}\n` +
452
+ `Supported: pytest, jest, vitest, mocha, cargo test, go test, dotnet test\n` +
453
+ `Or set "test_command" in .fchekrc`
454
+ ));
455
+ }
456
+
457
+ const result = runFramework(framework, { coverage, filter }, timeout);
458
+
459
+ if (result.error) {
460
+ return output(fail(result.error));
461
+ }
462
+
463
+ output(ok({
464
+ dir: path.resolve(dir),
465
+ framework: framework.type,
466
+ ...result,
467
+ verdict: result.all_passed ? 'all_passed' : 'failures_found',
468
+ }));
469
+ }
470
+
471
+ module.exports = { run, detectFramework };