tlc-claude-code 1.6.4 → 1.8.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/dashboard/dist/components/ContainerSecurityPane.d.ts +45 -0
- package/dashboard/dist/components/ContainerSecurityPane.js +44 -0
- package/dashboard/dist/components/ContainerSecurityPane.test.d.ts +1 -0
- package/dashboard/dist/components/ContainerSecurityPane.test.js +153 -0
- package/package.json +1 -1
- package/server/lib/access-control.test.js +1 -1
- package/server/lib/agents-cancel-command.test.js +1 -1
- package/server/lib/agents-get-command.test.js +1 -1
- package/server/lib/agents-list-command.test.js +1 -1
- package/server/lib/agents-logs-command.test.js +1 -1
- package/server/lib/agents-retry-command.test.js +1 -1
- package/server/lib/budget-limits.test.js +2 -2
- package/server/lib/code-gate/bypass-logger.js +129 -0
- package/server/lib/code-gate/bypass-logger.test.js +142 -0
- package/server/lib/code-gate/first-commit-audit.js +138 -0
- package/server/lib/code-gate/first-commit-audit.test.js +203 -0
- package/server/lib/code-gate/gate-command.js +114 -0
- package/server/lib/code-gate/gate-command.test.js +111 -0
- package/server/lib/code-gate/gate-config.js +163 -0
- package/server/lib/code-gate/gate-config.test.js +181 -0
- package/server/lib/code-gate/gate-engine.js +193 -0
- package/server/lib/code-gate/gate-engine.test.js +258 -0
- package/server/lib/code-gate/gate-reporter.js +123 -0
- package/server/lib/code-gate/gate-reporter.test.js +159 -0
- package/server/lib/code-gate/hooks-generator.js +149 -0
- package/server/lib/code-gate/hooks-generator.test.js +142 -0
- package/server/lib/code-gate/llm-reviewer.js +176 -0
- package/server/lib/code-gate/llm-reviewer.test.js +161 -0
- package/server/lib/code-gate/multi-model-reviewer.js +172 -0
- package/server/lib/code-gate/multi-model-reviewer.test.js +217 -0
- package/server/lib/code-gate/push-gate.js +133 -0
- package/server/lib/code-gate/push-gate.test.js +190 -0
- package/server/lib/code-gate/rules/architecture-rules.js +228 -0
- package/server/lib/code-gate/rules/architecture-rules.test.js +155 -0
- package/server/lib/code-gate/rules/client-rules.js +120 -0
- package/server/lib/code-gate/rules/client-rules.test.js +121 -0
- package/server/lib/code-gate/rules/config-rules.js +140 -0
- package/server/lib/code-gate/rules/config-rules.test.js +103 -0
- package/server/lib/code-gate/rules/database-rules.js +158 -0
- package/server/lib/code-gate/rules/database-rules.test.js +119 -0
- package/server/lib/code-gate/rules/docker-rules.js +201 -0
- package/server/lib/code-gate/rules/docker-rules.test.js +104 -0
- package/server/lib/code-gate/rules/quality-rules.js +304 -0
- package/server/lib/code-gate/rules/quality-rules.test.js +199 -0
- package/server/lib/code-gate/rules/security-rules.js +228 -0
- package/server/lib/code-gate/rules/security-rules.test.js +131 -0
- package/server/lib/code-gate/rules/structure-rules.js +155 -0
- package/server/lib/code-gate/rules/structure-rules.test.js +107 -0
- package/server/lib/code-gate/rules/test-rules.js +93 -0
- package/server/lib/code-gate/rules/test-rules.test.js +97 -0
- package/server/lib/code-gate/typescript-gate.js +128 -0
- package/server/lib/code-gate/typescript-gate.test.js +131 -0
- package/server/lib/code-generator.test.js +1 -1
- package/server/lib/cost-command.test.js +1 -1
- package/server/lib/cost-optimizer.test.js +1 -1
- package/server/lib/cost-projections.test.js +1 -1
- package/server/lib/cost-reports.test.js +1 -1
- package/server/lib/cost-tracker.test.js +1 -1
- package/server/lib/crypto-patterns.test.js +1 -1
- package/server/lib/design-command.test.js +1 -1
- package/server/lib/design-parser.test.js +1 -1
- package/server/lib/gemini-vision.test.js +1 -1
- package/server/lib/infra/infra-generator.js +331 -0
- package/server/lib/infra/infra-generator.test.js +146 -0
- package/server/lib/input-validator.test.js +1 -1
- package/server/lib/litellm-client.test.js +1 -1
- package/server/lib/litellm-command.test.js +1 -1
- package/server/lib/litellm-config.test.js +1 -1
- package/server/lib/llm/adapters/api-adapter.js +95 -0
- package/server/lib/llm/adapters/api-adapter.test.js +81 -0
- package/server/lib/llm/adapters/codex-adapter.js +85 -0
- package/server/lib/llm/adapters/codex-adapter.test.js +54 -0
- package/server/lib/llm/adapters/gemini-adapter.js +100 -0
- package/server/lib/llm/adapters/gemini-adapter.test.js +54 -0
- package/server/lib/llm/index.js +109 -0
- package/server/lib/llm/index.test.js +147 -0
- package/server/lib/llm/provider-executor.js +168 -0
- package/server/lib/llm/provider-executor.test.js +244 -0
- package/server/lib/llm/provider-registry.js +104 -0
- package/server/lib/llm/provider-registry.test.js +157 -0
- package/server/lib/llm/review-service.js +222 -0
- package/server/lib/llm/review-service.test.js +220 -0
- package/server/lib/model-pricing.test.js +1 -1
- package/server/lib/models-command.test.js +1 -1
- package/server/lib/optimize-command.test.js +1 -1
- package/server/lib/orchestration-integration.test.js +1 -1
- package/server/lib/output-encoder.test.js +1 -1
- package/server/lib/quality-evaluator.test.js +1 -1
- package/server/lib/quality-gate-command.test.js +1 -1
- package/server/lib/quality-gate-scorer.test.js +1 -1
- package/server/lib/quality-history.test.js +1 -1
- package/server/lib/quality-presets.test.js +1 -1
- package/server/lib/quality-retry.test.js +1 -1
- package/server/lib/quality-thresholds.test.js +1 -1
- package/server/lib/secure-auth.test.js +1 -1
- package/server/lib/secure-code-command.test.js +1 -1
- package/server/lib/secure-errors.test.js +1 -1
- package/server/lib/security/auth-security.test.js +4 -3
- package/server/lib/shame/shame-registry.js +224 -0
- package/server/lib/shame/shame-registry.test.js +202 -0
- package/server/lib/standards/cleanup-dry-run.js +254 -0
- package/server/lib/standards/cleanup-dry-run.test.js +220 -0
- package/server/lib/vision-command.test.js +1 -1
- package/server/lib/visual-command.test.js +1 -1
- package/server/lib/visual-testing.test.js +1 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structure Rules
|
|
3
|
+
*
|
|
4
|
+
* Detects flat folder anti-patterns, files in wrong locations,
|
|
5
|
+
* and missing test colocation.
|
|
6
|
+
*
|
|
7
|
+
* @module code-gate/rules/structure-rules
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
/** Folder names that indicate flat anti-patterns when at root level */
|
|
13
|
+
const FLAT_FOLDER_PATTERNS = ['services', 'controllers', 'interfaces', 'helpers', 'utils'];
|
|
14
|
+
|
|
15
|
+
/** Files allowed at project root */
|
|
16
|
+
const ALLOWED_ROOT_FILES = [
|
|
17
|
+
'index.js', 'index.ts', 'index.mjs',
|
|
18
|
+
'package.json', 'package-lock.json',
|
|
19
|
+
'tsconfig.json', 'vitest.config.js', 'vitest.config.ts',
|
|
20
|
+
'jest.config.js', 'jest.config.ts', '.eslintrc.js',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/** Patterns for allowed root file naming */
|
|
24
|
+
const ALLOWED_ROOT_PATTERNS = [
|
|
25
|
+
/^\./, // dotfiles
|
|
26
|
+
/\.config\.[jt]s$/, // config files
|
|
27
|
+
/\.json$/, // JSON files
|
|
28
|
+
/\.ya?ml$/, // YAML files
|
|
29
|
+
/\.md$/, // Markdown
|
|
30
|
+
/\.lock$/, // Lock files
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Detect flat folder anti-patterns (services/, interfaces/, controllers/ at root).
|
|
35
|
+
*
|
|
36
|
+
* @param {string} filePath - File path relative to project root
|
|
37
|
+
* @param {string} content - File content (unused for structure checks)
|
|
38
|
+
* @returns {Array<{severity: string, rule: string, line: number, message: string, fix: string}>}
|
|
39
|
+
*/
|
|
40
|
+
function checkFlatFolders(filePath, content) {
|
|
41
|
+
const parts = filePath.split(/[/\\]/);
|
|
42
|
+
|
|
43
|
+
// Check if the first or second directory segment is a flat anti-pattern
|
|
44
|
+
for (let i = 0; i < Math.min(parts.length - 1, 2); i++) {
|
|
45
|
+
const dir = parts[i];
|
|
46
|
+
if (FLAT_FOLDER_PATTERNS.includes(dir)) {
|
|
47
|
+
// Only flag if not inside a module (e.g. src/user/services is fine)
|
|
48
|
+
const isNested = i >= 2 || (i === 1 && parts[0] === 'src' && parts.length > 3);
|
|
49
|
+
if (!isNested) {
|
|
50
|
+
return [{
|
|
51
|
+
severity: 'warn',
|
|
52
|
+
rule: 'no-flat-folders',
|
|
53
|
+
line: 1,
|
|
54
|
+
message: `File in flat '${dir}/' folder — use module-scoped organization`,
|
|
55
|
+
fix: `Move to a feature module: src/{feature}/${dir}/${path.basename(filePath)}`,
|
|
56
|
+
}];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Detect loose source files at project root that should be in subdirectories.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} filePath
|
|
68
|
+
* @param {string} content
|
|
69
|
+
* @returns {Array}
|
|
70
|
+
*/
|
|
71
|
+
function checkLooseFiles(filePath, content) {
|
|
72
|
+
// Only check files at root (no directory separator)
|
|
73
|
+
if (filePath.includes('/') || filePath.includes('\\')) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check against allowed files and patterns
|
|
78
|
+
if (ALLOWED_ROOT_FILES.includes(filePath)) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
for (const pattern of ALLOWED_ROOT_PATTERNS) {
|
|
82
|
+
if (pattern.test(filePath)) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Only flag JS/TS source files
|
|
88
|
+
if (!/\.[jt]sx?$/.test(filePath)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [{
|
|
93
|
+
severity: 'warn',
|
|
94
|
+
rule: 'no-loose-files',
|
|
95
|
+
line: 1,
|
|
96
|
+
message: `Source file '${filePath}' at project root`,
|
|
97
|
+
fix: 'Move into a feature module directory: src/{feature}/',
|
|
98
|
+
}];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check that source files have corresponding test files.
|
|
103
|
+
* Skips test files, config files, and non-JS files.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} filePath
|
|
106
|
+
* @param {string} content
|
|
107
|
+
* @param {Object} [options]
|
|
108
|
+
* @param {string[]} [options.allFiles] - All file paths in the changeset
|
|
109
|
+
* @returns {Array}
|
|
110
|
+
*/
|
|
111
|
+
function checkTestColocation(filePath, content, options = {}) {
|
|
112
|
+
// Skip non-JS files
|
|
113
|
+
if (!/\.[jt]sx?$/.test(filePath)) return [];
|
|
114
|
+
|
|
115
|
+
// Skip test files themselves
|
|
116
|
+
if (/\.(test|spec)\.[jt]sx?$/.test(filePath)) return [];
|
|
117
|
+
|
|
118
|
+
// Skip config files
|
|
119
|
+
if (/\.config\.[jt]sx?$/.test(filePath)) return [];
|
|
120
|
+
|
|
121
|
+
// Skip index files and type definitions
|
|
122
|
+
if (/^index\.[jt]sx?$/.test(path.basename(filePath))) return [];
|
|
123
|
+
if (/\.d\.ts$/.test(filePath)) return [];
|
|
124
|
+
|
|
125
|
+
const allFiles = options.allFiles || [];
|
|
126
|
+
const base = filePath.replace(/\.[jt]sx?$/, '');
|
|
127
|
+
const testPatterns = [
|
|
128
|
+
`${base}.test.js`,
|
|
129
|
+
`${base}.test.ts`,
|
|
130
|
+
`${base}.test.jsx`,
|
|
131
|
+
`${base}.test.tsx`,
|
|
132
|
+
`${base}.spec.js`,
|
|
133
|
+
`${base}.spec.ts`,
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const hasTest = testPatterns.some(tp => allFiles.includes(tp));
|
|
137
|
+
|
|
138
|
+
if (!hasTest) {
|
|
139
|
+
return [{
|
|
140
|
+
severity: 'block',
|
|
141
|
+
rule: 'require-test-file',
|
|
142
|
+
line: 1,
|
|
143
|
+
message: `No test file found for '${filePath}'`,
|
|
144
|
+
fix: `Create test file: ${base}.test.js`,
|
|
145
|
+
}];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
checkFlatFolders,
|
|
153
|
+
checkLooseFiles,
|
|
154
|
+
checkTestColocation,
|
|
155
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structure Rules Tests
|
|
3
|
+
*
|
|
4
|
+
* Detects flat folder anti-patterns, files in wrong locations,
|
|
5
|
+
* and missing test colocation.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
checkFlatFolders,
|
|
11
|
+
checkLooseFiles,
|
|
12
|
+
checkTestColocation,
|
|
13
|
+
} = require('./structure-rules.js');
|
|
14
|
+
|
|
15
|
+
describe('Structure Rules', () => {
|
|
16
|
+
describe('checkFlatFolders', () => {
|
|
17
|
+
it('detects services/ folder at root', () => {
|
|
18
|
+
const findings = checkFlatFolders('services/userService.js', 'code');
|
|
19
|
+
expect(findings).toHaveLength(1);
|
|
20
|
+
expect(findings[0].rule).toBe('no-flat-folders');
|
|
21
|
+
expect(findings[0].severity).toBe('warn');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('detects controllers/ folder', () => {
|
|
25
|
+
const findings = checkFlatFolders('controllers/userCtrl.js', 'code');
|
|
26
|
+
expect(findings).toHaveLength(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('detects interfaces/ at src root', () => {
|
|
30
|
+
const findings = checkFlatFolders('src/interfaces/IUser.ts', 'code');
|
|
31
|
+
expect(findings).toHaveLength(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('passes module-scoped paths', () => {
|
|
35
|
+
const findings = checkFlatFolders('src/user/services/userService.js', 'code');
|
|
36
|
+
expect(findings).toHaveLength(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('passes nested interfaces in module', () => {
|
|
40
|
+
const findings = checkFlatFolders('src/user/interfaces/IUser.ts', 'code');
|
|
41
|
+
expect(findings).toHaveLength(0);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('checkLooseFiles', () => {
|
|
46
|
+
it('detects .js files in project root', () => {
|
|
47
|
+
const findings = checkLooseFiles('random-script.js', 'code');
|
|
48
|
+
expect(findings).toHaveLength(1);
|
|
49
|
+
expect(findings[0].rule).toBe('no-loose-files');
|
|
50
|
+
expect(findings[0].severity).toBe('warn');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('allows index.js at root', () => {
|
|
54
|
+
const findings = checkLooseFiles('index.js', 'code');
|
|
55
|
+
expect(findings).toHaveLength(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('allows config files at root', () => {
|
|
59
|
+
const findings = checkLooseFiles('vitest.config.js', 'code');
|
|
60
|
+
expect(findings).toHaveLength(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('allows package.json at root', () => {
|
|
64
|
+
const findings = checkLooseFiles('package.json', '{}');
|
|
65
|
+
expect(findings).toHaveLength(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('passes files in subdirectories', () => {
|
|
69
|
+
const findings = checkLooseFiles('src/app.js', 'code');
|
|
70
|
+
expect(findings).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('checkTestColocation', () => {
|
|
75
|
+
it('flags source file without test file', () => {
|
|
76
|
+
const allFiles = ['src/user.js'];
|
|
77
|
+
const findings = checkTestColocation('src/user.js', 'code', { allFiles });
|
|
78
|
+
expect(findings).toHaveLength(1);
|
|
79
|
+
expect(findings[0].rule).toBe('require-test-file');
|
|
80
|
+
expect(findings[0].severity).toBe('block');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('passes when test file exists', () => {
|
|
84
|
+
const allFiles = ['src/user.js', 'src/user.test.js'];
|
|
85
|
+
const findings = checkTestColocation('src/user.js', 'code', { allFiles });
|
|
86
|
+
expect(findings).toHaveLength(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('skips check for test files themselves', () => {
|
|
90
|
+
const allFiles = ['src/user.test.js'];
|
|
91
|
+
const findings = checkTestColocation('src/user.test.js', 'test code', { allFiles });
|
|
92
|
+
expect(findings).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('skips check for config files', () => {
|
|
96
|
+
const allFiles = ['vitest.config.js'];
|
|
97
|
+
const findings = checkTestColocation('vitest.config.js', 'config', { allFiles });
|
|
98
|
+
expect(findings).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('skips check for non-JS files', () => {
|
|
102
|
+
const allFiles = ['README.md'];
|
|
103
|
+
const findings = checkTestColocation('README.md', '# readme', { allFiles });
|
|
104
|
+
expect(findings).toHaveLength(0);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Rules
|
|
3
|
+
*
|
|
4
|
+
* Detects test files with no assertions, skipped tests,
|
|
5
|
+
* and other test quality issues.
|
|
6
|
+
*
|
|
7
|
+
* @module code-gate/rules/test-rules
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if a file is a test file.
|
|
12
|
+
* @param {string} filePath
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
function isTestFile(filePath) {
|
|
16
|
+
return /\.(test|spec)\.[jt]sx?$/.test(filePath);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check for test files that contain no assertions.
|
|
21
|
+
* Tests without expect(), assert, or should are likely empty stubs.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} filePath
|
|
24
|
+
* @param {string} content
|
|
25
|
+
* @returns {Array<{severity: string, rule: string, line: number, message: string, fix: string}>}
|
|
26
|
+
*/
|
|
27
|
+
function checkEmptyTests(filePath, content) {
|
|
28
|
+
if (!isTestFile(filePath)) return [];
|
|
29
|
+
|
|
30
|
+
const hasAssertion =
|
|
31
|
+
/\bexpect\s*\(/.test(content) ||
|
|
32
|
+
/\bassert\b/.test(content) ||
|
|
33
|
+
/\.should\b/.test(content);
|
|
34
|
+
|
|
35
|
+
if (!hasAssertion) {
|
|
36
|
+
return [{
|
|
37
|
+
severity: 'block',
|
|
38
|
+
rule: 'no-empty-tests',
|
|
39
|
+
line: 1,
|
|
40
|
+
message: 'Test file contains no assertions',
|
|
41
|
+
fix: 'Add expect() or assert calls to verify behavior',
|
|
42
|
+
}];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check for skipped tests (.skip, xit, xdescribe).
|
|
50
|
+
* Skipped tests hide regressions.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} filePath
|
|
53
|
+
* @param {string} content
|
|
54
|
+
* @returns {Array}
|
|
55
|
+
*/
|
|
56
|
+
function checkSkippedTests(filePath, content) {
|
|
57
|
+
if (!isTestFile(filePath)) return [];
|
|
58
|
+
const findings = [];
|
|
59
|
+
const lines = content.split('\n');
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < lines.length; i++) {
|
|
62
|
+
const line = lines[i];
|
|
63
|
+
|
|
64
|
+
const skipPatterns = [
|
|
65
|
+
{ pattern: /\bdescribe\.skip\s*\(/, label: 'describe.skip' },
|
|
66
|
+
{ pattern: /\bit\.skip\s*\(/, label: 'it.skip' },
|
|
67
|
+
{ pattern: /\btest\.skip\s*\(/, label: 'test.skip' },
|
|
68
|
+
{ pattern: /\bxdescribe\s*\(/, label: 'xdescribe' },
|
|
69
|
+
{ pattern: /\bxit\s*\(/, label: 'xit' },
|
|
70
|
+
{ pattern: /\bxtest\s*\(/, label: 'xtest' },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
for (const { pattern, label } of skipPatterns) {
|
|
74
|
+
if (pattern.test(line)) {
|
|
75
|
+
findings.push({
|
|
76
|
+
severity: 'warn',
|
|
77
|
+
rule: 'no-skipped-tests',
|
|
78
|
+
line: i + 1,
|
|
79
|
+
message: `Skipped test found: ${label}`,
|
|
80
|
+
fix: 'Remove .skip or fix the test — skipped tests hide regressions',
|
|
81
|
+
});
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return findings;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
checkEmptyTests,
|
|
92
|
+
checkSkippedTests,
|
|
93
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Rules Tests
|
|
3
|
+
*
|
|
4
|
+
* Detects test files with no assertions, skipped tests, and
|
|
5
|
+
* source files without corresponding test files.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
checkEmptyTests,
|
|
11
|
+
checkSkippedTests,
|
|
12
|
+
} = require('./test-rules.js');
|
|
13
|
+
|
|
14
|
+
describe('Test Rules', () => {
|
|
15
|
+
describe('checkEmptyTests', () => {
|
|
16
|
+
it('detects test file with no assertions', () => {
|
|
17
|
+
const code = `
|
|
18
|
+
describe('User', () => {
|
|
19
|
+
it('should work', () => {
|
|
20
|
+
const user = getUser();
|
|
21
|
+
});
|
|
22
|
+
});`;
|
|
23
|
+
const findings = checkEmptyTests('user.test.js', code);
|
|
24
|
+
expect(findings).toHaveLength(1);
|
|
25
|
+
expect(findings[0].rule).toBe('no-empty-tests');
|
|
26
|
+
expect(findings[0].severity).toBe('block');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('passes test with expect assertion', () => {
|
|
30
|
+
const code = `
|
|
31
|
+
describe('User', () => {
|
|
32
|
+
it('should work', () => {
|
|
33
|
+
expect(getUser()).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
});`;
|
|
36
|
+
const findings = checkEmptyTests('user.test.js', code);
|
|
37
|
+
expect(findings).toHaveLength(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('passes test with assert assertion', () => {
|
|
41
|
+
const code = `
|
|
42
|
+
describe('User', () => {
|
|
43
|
+
it('should work', () => {
|
|
44
|
+
assert.ok(getUser());
|
|
45
|
+
});
|
|
46
|
+
});`;
|
|
47
|
+
const findings = checkEmptyTests('user.test.js', code);
|
|
48
|
+
expect(findings).toHaveLength(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('skips non-test files', () => {
|
|
52
|
+
const code = 'const x = 1;';
|
|
53
|
+
const findings = checkEmptyTests('app.js', code);
|
|
54
|
+
expect(findings).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('checkSkippedTests', () => {
|
|
59
|
+
it('detects describe.skip', () => {
|
|
60
|
+
const code = 'describe.skip("User", () => {});';
|
|
61
|
+
const findings = checkSkippedTests('user.test.js', code);
|
|
62
|
+
expect(findings).toHaveLength(1);
|
|
63
|
+
expect(findings[0].rule).toBe('no-skipped-tests');
|
|
64
|
+
expect(findings[0].severity).toBe('warn');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('detects it.skip', () => {
|
|
68
|
+
const code = 'it.skip("should work", () => {});';
|
|
69
|
+
const findings = checkSkippedTests('user.test.js', code);
|
|
70
|
+
expect(findings).toHaveLength(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('detects xdescribe', () => {
|
|
74
|
+
const code = 'xdescribe("User", () => {});';
|
|
75
|
+
const findings = checkSkippedTests('user.test.js', code);
|
|
76
|
+
expect(findings).toHaveLength(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('detects xit', () => {
|
|
80
|
+
const code = 'xit("should work", () => {});';
|
|
81
|
+
const findings = checkSkippedTests('user.test.js', code);
|
|
82
|
+
expect(findings).toHaveLength(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('passes normal test', () => {
|
|
86
|
+
const code = 'it("should work", () => { expect(1).toBe(1); });';
|
|
87
|
+
const findings = checkSkippedTests('user.test.js', code);
|
|
88
|
+
expect(findings).toHaveLength(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('skips non-test files', () => {
|
|
92
|
+
const code = 'describe.skip("something", () => {});';
|
|
93
|
+
const findings = checkSkippedTests('app.js', code);
|
|
94
|
+
expect(findings).toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript Compilation Gate
|
|
3
|
+
*
|
|
4
|
+
* Integrates `tsc --noEmit` as an optional check in the push gate.
|
|
5
|
+
* Catches 7 categories of bugs that compile silently with esbuild/Vite:
|
|
6
|
+
* - Invented enum values (Bug #4, #33)
|
|
7
|
+
* - Wrong field names (Bug #7)
|
|
8
|
+
* - Date type mismatches (Bug #8)
|
|
9
|
+
* - Undefined variables (Bug #31)
|
|
10
|
+
* - Accumulated type errors (Bug #32)
|
|
11
|
+
* - Missing required fields in spreads (Bug #34)
|
|
12
|
+
*
|
|
13
|
+
* @module code-gate/typescript-gate
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if a project has TypeScript configured.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} projectPath - Project root
|
|
22
|
+
* @param {Object} [options]
|
|
23
|
+
* @param {Object} [options.fs] - File system (injectable)
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
function detectTypeScript(projectPath, options = {}) {
|
|
27
|
+
const fs = options.fs || require('fs');
|
|
28
|
+
const tsconfigPath = path.join(projectPath, 'tsconfig.json');
|
|
29
|
+
return fs.existsSync(tsconfigPath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse tsc --noEmit output into gate findings.
|
|
34
|
+
* TypeScript error format: file(line,col): error TSxxxx: message
|
|
35
|
+
*
|
|
36
|
+
* @param {string} output - Raw tsc output
|
|
37
|
+
* @returns {Array<{severity: string, rule: string, file: string, line: number, message: string, fix: string}>}
|
|
38
|
+
*/
|
|
39
|
+
function parseTscOutput(output) {
|
|
40
|
+
if (!output || !output.trim()) return [];
|
|
41
|
+
const findings = [];
|
|
42
|
+
const lines = output.split('\n');
|
|
43
|
+
|
|
44
|
+
// TS error format: path/file.ts(line,col): error TSxxxx: message
|
|
45
|
+
const errorPattern = /^(.+?)\((\d+),\d+\):\s*error\s+(TS\d+):\s*(.+)$/;
|
|
46
|
+
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
const match = line.match(errorPattern);
|
|
49
|
+
if (match) {
|
|
50
|
+
const [, file, lineNum, code, message] = match;
|
|
51
|
+
findings.push({
|
|
52
|
+
severity: 'block',
|
|
53
|
+
rule: 'typescript-error',
|
|
54
|
+
file,
|
|
55
|
+
line: parseInt(lineNum, 10),
|
|
56
|
+
message: `${code}: ${message}`,
|
|
57
|
+
fix: 'Fix the TypeScript type error — tsc --noEmit must pass with zero errors',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return findings;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Run tsc --noEmit and return gate-compatible result.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} projectPath - Project root
|
|
69
|
+
* @param {Object} [options]
|
|
70
|
+
* @param {Function} [options.exec] - Command executor (injectable)
|
|
71
|
+
* @returns {Promise<{passed: boolean, findings: Array, summary: Object, skipped?: boolean}>}
|
|
72
|
+
*/
|
|
73
|
+
async function runTypeScriptGate(projectPath, options = {}) {
|
|
74
|
+
const exec = options.exec;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const result = await exec('npx tsc --noEmit', { cwd: projectPath });
|
|
78
|
+
const output = result.stdout || '';
|
|
79
|
+
const findings = parseTscOutput(output);
|
|
80
|
+
const summary = { total: findings.length, block: findings.length, warn: 0, info: 0 };
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
passed: findings.length === 0,
|
|
84
|
+
findings,
|
|
85
|
+
summary,
|
|
86
|
+
};
|
|
87
|
+
} catch (err) {
|
|
88
|
+
// If tsc fails with errors, the output is usually in stdout or stderr
|
|
89
|
+
if (err.stdout || err.stderr) {
|
|
90
|
+
const output = err.stdout || err.stderr || '';
|
|
91
|
+
const findings = parseTscOutput(output);
|
|
92
|
+
const summary = { total: findings.length, block: findings.length, warn: 0, info: 0 };
|
|
93
|
+
return {
|
|
94
|
+
passed: findings.length === 0,
|
|
95
|
+
findings,
|
|
96
|
+
summary,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// tsc not installed or other system error — skip gracefully
|
|
101
|
+
return {
|
|
102
|
+
passed: true,
|
|
103
|
+
findings: [],
|
|
104
|
+
summary: { total: 0, block: 0, warn: 0, info: 0 },
|
|
105
|
+
skipped: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a TypeScript gate with configurable options.
|
|
112
|
+
*
|
|
113
|
+
* @param {Object} [options]
|
|
114
|
+
* @param {boolean} [options.enabled=true]
|
|
115
|
+
* @returns {{ enabled: boolean }}
|
|
116
|
+
*/
|
|
117
|
+
function createTypeScriptGate(options = {}) {
|
|
118
|
+
return {
|
|
119
|
+
enabled: options.enabled !== false,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
detectTypeScript,
|
|
125
|
+
parseTscOutput,
|
|
126
|
+
runTypeScriptGate,
|
|
127
|
+
createTypeScriptGate,
|
|
128
|
+
};
|