docguard-cli 0.9.0 → 0.9.1
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/cli/commands/score.mjs +97 -11
- package/cli/validators/test-spec.mjs +51 -4
- package/package.json +1 -1
package/cli/commands/score.mjs
CHANGED
|
@@ -405,38 +405,77 @@ function calcDocQualityScore(dir, config) {
|
|
|
405
405
|
function calcTestingScore(dir, config) {
|
|
406
406
|
let score = 0;
|
|
407
407
|
|
|
408
|
-
// Check
|
|
408
|
+
// ── Check 1: Test files exist (40 pts) ──
|
|
409
|
+
// Check top-level test directories
|
|
409
410
|
const testDirs = ['tests', 'test', '__tests__', 'spec', 'e2e'];
|
|
410
|
-
const
|
|
411
|
-
if (hasTestDir) score += 40;
|
|
411
|
+
const hasTopLevelTestDir = testDirs.some(d => existsSync(resolve(dir, d)));
|
|
412
412
|
|
|
413
|
-
// Check test spec
|
|
413
|
+
// Check co-located tests: src/**/__tests__/ and src/**/*.test.* / src/**/*.spec.*
|
|
414
|
+
let hasColocatedTests = false;
|
|
415
|
+
if (!hasTopLevelTestDir) {
|
|
416
|
+
hasColocatedTests = findColocatedTests(dir);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Check vitest/jest config for custom test patterns
|
|
420
|
+
let hasConfigTests = false;
|
|
421
|
+
if (!hasTopLevelTestDir && !hasColocatedTests) {
|
|
422
|
+
const testConfigs = ['vitest.config.ts', 'vitest.config.js', 'vitest.config.mts', 'jest.config.ts', 'jest.config.js'];
|
|
423
|
+
for (const cfgFile of testConfigs) {
|
|
424
|
+
const cfgPath = resolve(dir, cfgFile);
|
|
425
|
+
if (existsSync(cfgPath)) {
|
|
426
|
+
try {
|
|
427
|
+
const cfgContent = readFileSync(cfgPath, 'utf-8');
|
|
428
|
+
const includeMatch = cfgContent.match(/include\s*:\s*\[([^\]]+)\]/);
|
|
429
|
+
if (includeMatch) {
|
|
430
|
+
const patterns = includeMatch[1].match(/['"]([^'"]+)['"]/g);
|
|
431
|
+
if (patterns) {
|
|
432
|
+
for (const p of patterns) {
|
|
433
|
+
const pattern = p.replace(/['"]|\s/g, '');
|
|
434
|
+
const rootDir = pattern.split('/')[0];
|
|
435
|
+
if (rootDir && rootDir !== '**' && rootDir !== '*') {
|
|
436
|
+
const fullDir = resolve(dir, rootDir);
|
|
437
|
+
if (existsSync(fullDir)) {
|
|
438
|
+
hasConfigTests = true;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
} catch { /* config parse may fail */ }
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (hasTopLevelTestDir || hasColocatedTests || hasConfigTests) score += 40;
|
|
452
|
+
|
|
453
|
+
// ── Check 2: TEST-SPEC.md exists (30 pts) ──
|
|
414
454
|
if (existsSync(resolve(dir, 'docs-canonical/TEST-SPEC.md'))) score += 30;
|
|
415
455
|
|
|
416
|
-
// Check
|
|
417
|
-
const
|
|
418
|
-
const hasTestConfig =
|
|
456
|
+
// ── Check 3: Test config or built-in runner (15 pts) ──
|
|
457
|
+
const testConfigs2 = ['jest.config.js', 'jest.config.ts', 'vitest.config.ts', 'vitest.config.js', 'pytest.ini', 'setup.cfg', '.mocharc.yml'];
|
|
458
|
+
const hasTestConfig = testConfigs2.some(f => existsSync(resolve(dir, f)));
|
|
419
459
|
|
|
420
460
|
if (hasTestConfig) {
|
|
421
461
|
score += 15;
|
|
422
462
|
} else {
|
|
423
|
-
// Check if using node:test (no config needed) — look in package.json scripts
|
|
424
463
|
const ptc = config.projectTypeConfig || {};
|
|
425
464
|
const pkgPath = resolve(dir, 'package.json');
|
|
426
465
|
if (ptc.testFramework === 'node:test') {
|
|
427
|
-
score += 15;
|
|
466
|
+
score += 15;
|
|
428
467
|
} else if (existsSync(pkgPath)) {
|
|
429
468
|
try {
|
|
430
469
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
431
470
|
const testScript = pkg.scripts?.test || '';
|
|
432
471
|
if (testScript.includes('node --test') || testScript.includes('node:test')) {
|
|
433
|
-
score += 15;
|
|
472
|
+
score += 15;
|
|
434
473
|
}
|
|
435
474
|
} catch { /* skip */ }
|
|
436
475
|
}
|
|
437
476
|
}
|
|
438
477
|
|
|
439
|
-
// Check
|
|
478
|
+
// ── Check 4: CI test step (15 pts) ──
|
|
440
479
|
const ciFiles = ['.github/workflows/ci.yml', '.github/workflows/test.yml'];
|
|
441
480
|
const hasCITest = ciFiles.some(f => existsSync(resolve(dir, f)));
|
|
442
481
|
if (hasCITest) score += 15;
|
|
@@ -444,6 +483,53 @@ function calcTestingScore(dir, config) {
|
|
|
444
483
|
return Math.min(100, score);
|
|
445
484
|
}
|
|
446
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Scan common source directories for co-located test files.
|
|
488
|
+
* Checks: __tests__/ dirs, *.test.*, *.spec.* anywhere in src/, app/, lib/, packages/
|
|
489
|
+
* Also checks root-level for *.test.* files.
|
|
490
|
+
*/
|
|
491
|
+
function findColocatedTests(dir) {
|
|
492
|
+
// Scan these common source roots for co-located tests
|
|
493
|
+
const sourceRoots = ['src', 'app', 'lib', 'packages', 'modules'];
|
|
494
|
+
const ignoreSet = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache']);
|
|
495
|
+
|
|
496
|
+
for (const root of sourceRoots) {
|
|
497
|
+
const rootDir = resolve(dir, root);
|
|
498
|
+
if (!existsSync(rootDir)) continue;
|
|
499
|
+
if (walkForTests(rootDir, ignoreSet)) return true;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Also check root-level for *.test.* files (some projects put tests at root)
|
|
503
|
+
try {
|
|
504
|
+
const rootEntries = readdirSync(dir);
|
|
505
|
+
for (const entry of rootEntries) {
|
|
506
|
+
if (/\.(test|spec)\.[^.]+$/.test(entry)) return true;
|
|
507
|
+
}
|
|
508
|
+
} catch { /* ignore */ }
|
|
509
|
+
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Recursively walk a dir looking for test files. Returns true as soon as one is found. */
|
|
514
|
+
function walkForTests(d, ignoreSet) {
|
|
515
|
+
let entries;
|
|
516
|
+
try { entries = readdirSync(d); } catch { return false; }
|
|
517
|
+
for (const entry of entries) {
|
|
518
|
+
if (ignoreSet.has(entry) || entry.startsWith('.')) continue;
|
|
519
|
+
const full = join(d, entry);
|
|
520
|
+
try {
|
|
521
|
+
const s = statSync(full);
|
|
522
|
+
if (s.isDirectory()) {
|
|
523
|
+
if (entry === '__tests__' || entry === '__test__') return true;
|
|
524
|
+
if (walkForTests(full, ignoreSet)) return true;
|
|
525
|
+
} else {
|
|
526
|
+
if (/\.(test|spec)\.[^.]+$/.test(entry)) return true;
|
|
527
|
+
}
|
|
528
|
+
} catch { continue; }
|
|
529
|
+
}
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
|
|
447
533
|
function calcSecurityScore(dir, config) {
|
|
448
534
|
let score = 0;
|
|
449
535
|
const ptc = config.projectTypeConfig || {};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Now respects projectTypeConfig (e.g., skip E2E for CLI tools)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
8
|
|
|
9
9
|
export function validateTestSpec(projectDir, config) {
|
|
@@ -130,19 +130,66 @@ export function validateTestSpec(projectDir, config) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
// If no test spec entries parsed, check if
|
|
133
|
+
// If no test spec entries parsed, check if tests exist anywhere
|
|
134
134
|
if (results.total === 0) {
|
|
135
135
|
results.total = 1;
|
|
136
|
+
|
|
137
|
+
// 1. Check top-level test dirs
|
|
136
138
|
const commonTestDirs = ['tests', 'test', '__tests__', 'spec'];
|
|
137
139
|
const hasTestDir = commonTestDirs.some(d =>
|
|
138
140
|
existsSync(resolve(projectDir, d))
|
|
139
141
|
);
|
|
140
|
-
|
|
142
|
+
|
|
143
|
+
// 2. Check co-located tests (src/**/__tests__/, src/**/*.test.*)
|
|
144
|
+
let hasColocated = false;
|
|
145
|
+
if (!hasTestDir) {
|
|
146
|
+
const sourceRoots = ['src', 'app', 'lib', 'packages'];
|
|
147
|
+
for (const root of sourceRoots) {
|
|
148
|
+
const rootPath = resolve(projectDir, root);
|
|
149
|
+
if (existsSync(rootPath) && hasTestFilesRecursive(rootPath)) {
|
|
150
|
+
hasColocated = true;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3. Check vitest/jest config for custom patterns
|
|
157
|
+
let hasConfigTests = false;
|
|
158
|
+
if (!hasTestDir && !hasColocated) {
|
|
159
|
+
const configs = ['vitest.config.ts', 'vitest.config.js', 'jest.config.ts', 'jest.config.js'];
|
|
160
|
+
hasConfigTests = configs.some(f => existsSync(resolve(projectDir, f)));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (hasTestDir || hasColocated || hasConfigTests) {
|
|
141
164
|
results.passed = 1;
|
|
142
165
|
} else {
|
|
143
|
-
results.warnings.push(
|
|
166
|
+
results.warnings.push(
|
|
167
|
+
'No test directory or co-located test files found. ' +
|
|
168
|
+
'Expected: tests/, src/**/__tests__/, or src/**/*.test.* files'
|
|
169
|
+
);
|
|
144
170
|
}
|
|
145
171
|
}
|
|
146
172
|
|
|
147
173
|
return results;
|
|
148
174
|
}
|
|
175
|
+
|
|
176
|
+
/** Recursively check if a directory contains test files */
|
|
177
|
+
function hasTestFilesRecursive(dir) {
|
|
178
|
+
const ignore = new Set(['node_modules', '.git', 'dist', 'build', 'coverage']);
|
|
179
|
+
let entries;
|
|
180
|
+
try { entries = readdirSync(dir); } catch { return false; }
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
if (ignore.has(entry) || entry.startsWith('.')) continue;
|
|
183
|
+
const full = resolve(dir, entry);
|
|
184
|
+
try {
|
|
185
|
+
const s = statSync(full);
|
|
186
|
+
if (s.isDirectory()) {
|
|
187
|
+
if (entry === '__tests__' || entry === '__test__') return true;
|
|
188
|
+
if (hasTestFilesRecursive(full)) return true;
|
|
189
|
+
} else if (/\.(test|spec)\.[^.]+$/.test(entry)) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
} catch { continue; }
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|